diff --git a/Cargo.lock b/Cargo.lock index 077fa8184..c5e36a052 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2,6 +2,16 @@ # It is not intended for manual editing. version = 3 +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "adler" version = "1.0.2" @@ -117,6 +127,21 @@ dependencies = [ "memchr", ] +[[package]] +name = "alloc-no-stdlib" +version = "2.0.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3" + +[[package]] +name = "alloc-stdlib" +version = "0.2.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece" +dependencies = [ + "alloc-no-stdlib", +] + [[package]] name = "android_system_properties" version = "0.1.4" @@ -164,6 +189,20 @@ dependencies = [ "futures-core", ] +[[package]] +name = "async-compression" +version = "0.3.14" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "345fd392ab01f746c717b1357165b76f0b67a60192007b234058c9045fdcf695" +dependencies = [ + "brotli", + "flate2", + "futures-core", + "memchr", + "pin-project-lite 0.2.9", + "tokio", +] + [[package]] name = "async-dup" version = "1.2.2" @@ -897,6 +936,27 @@ dependencies = [ "serde_with", ] +[[package]] +name = "brotli" +version = "3.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a1a0b1dbcc8ae29329621f8d4f0d835787c1c38bb1401979b49d13b0b305ff68" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", + "brotli-decompressor", +] + +[[package]] +name = "brotli-decompressor" +version = "2.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "59ad2d4653bf5ca36ae797b1f4bb4dbddb60ce49ca4aed8a2ce4829f60425b80" +dependencies = [ + "alloc-no-stdlib", + "alloc-stdlib", +] + [[package]] name = "bson" version = "2.4.0" @@ -1687,8 +1747,18 @@ version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "a01d95850c592940db9b8194bc39f4bc0e89dee5c4265e4b1807c34a9aba453c" dependencies = [ - "darling_core", - "darling_macro", + "darling_core 0.13.4", + "darling_macro 0.13.4", +] + +[[package]] +name = "darling" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4529658bdda7fd6769b8614be250cdcfc3aeb0ee72fe66f9e41e5e5eb73eac02" +dependencies = [ + "darling_core 0.14.1", + "darling_macro 0.14.1", ] [[package]] @@ -1705,13 +1775,38 @@ dependencies = [ "syn", ] +[[package]] +name = "darling_core" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "649c91bc01e8b1eac09fb91e8dbc7d517684ca6be8ebc75bb9cafc894f9fdb6f" +dependencies = [ + "fnv", + "ident_case", + "proc-macro2", + "quote", + "strsim", + "syn", +] + [[package]] name = "darling_macro" version = "0.13.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9c972679f83bdf9c42bd905396b6c3588a843a17f0f16dfcfa3e2c5d57441835" dependencies = [ - "darling_core", + "darling_core 0.13.4", + "quote", + "syn", +] + +[[package]] +name = "darling_macro" +version = "0.14.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ddfc69c5bfcbd2fc09a0f38451d2daf0e372e367986a83906d1b0dbc88134fb5" +dependencies = [ + "darling_core 0.14.1", "quote", "syn", ] @@ -1898,6 +1993,26 @@ dependencies = [ "syn", ] +[[package]] +name = "enumflags2" +version = "0.7.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e75d4cd21b95383444831539909fbb14b9dc3fdceb2a6f5d36577329a1f55ccb" +dependencies = [ + "enumflags2_derive", +] + +[[package]] +name = "enumflags2_derive" +version = "0.7.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f58dc3c5e468259f19f2d46304a6b28f1c3d034442e14b322d2b850e36f6d5ae" +dependencies = [ + "proc-macro2", + "quote", + "syn", +] + [[package]] name = "env_logger" version = "0.9.0" @@ -3136,6 +3251,9 @@ name = "multimap" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5ce46fe64a9d73be07dcbe690a38ce1b293be448fd8ce1e6c1b8062c9f72c6a" +dependencies = [ + "serde", +] [[package]] name = "native-tls" @@ -3195,7 +3313,7 @@ version = "0.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0df7ac00c4672f9d5aece54ee3347520b7e20f158656c7db2e6de01902eb7a6c" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro-crate", "proc-macro2", "quote", @@ -4412,6 +4530,71 @@ version = "1.0.10" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "f3f6f92acf49d1b98f7a81226834412ada05458b7364277387724a237f062695" +[[package]] +name = "salvo" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e0d5860968c3504a1d13618078d2c833b23b2c7b194ce23e999891953d04b20c" +dependencies = [ + "salvo_core", +] + +[[package]] +name = "salvo_core" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e104409bf6168999cae0d11d4340fdcb333592ddce2a5bd2c45e300e6a8e6f68" +dependencies = [ + "Inflector", + "async-compression", + "async-trait", + "base64 0.13.0", + "bytes 1.1.0", + "cookie 0.16.0", + "encoding_rs", + "enumflags2", + "fastrand", + "form_urlencoded", + "futures-util", + "headers", + "http", + "hyper", + "mime", + "mime_guess", + "multer", + "multimap", + "once_cell", + "parking_lot 0.12.1", + "percent-encoding", + "regex", + "salvo_macros", + "serde", + "serde_json", + "serde_urlencoded", + "tempfile", + "textnonce", + "thiserror", + "tokio", + "tokio-stream", + "tracing", + "url", +] + +[[package]] +name = "salvo_macros" +version = "0.34.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1fe2ec840671e1427625d4dfb1c00177c64052fe0bfacf26964ab6d75446f45" +dependencies = [ + "Inflector", + "darling 0.14.1", + "proc-macro-crate", + "proc-macro2", + "quote", + "regex", + "syn", +] + [[package]] name = "same-file" version = "1.0.6" @@ -4618,7 +4801,7 @@ version = "1.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e182d6ec6f05393cc0e5ed1bf81ad6db3a8feedf8ee515ecdd369809bcce8082" dependencies = [ - "darling", + "darling 0.13.4", "proc-macro2", "quote", "syn", @@ -4825,6 +5008,7 @@ dependencies = [ "async-std", "async-trait", "axum", + "bincode", "cargo", "chrono", "futures", @@ -4838,6 +5022,8 @@ dependencies = [ "portpicker", "regex", "rocket", + "salvo", + "serde", "serenity", "shuttle-codegen", "shuttle-common", @@ -5300,6 +5486,16 @@ dependencies = [ "syn", ] +[[package]] +name = "textnonce" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7743f8d70cd784ed1dc33106a18998d77758d281dc40dc3e6d050cf0f5286683" +dependencies = [ + "base64 0.12.3", + "rand 0.7.3", +] + [[package]] name = "textwrap" version = "0.15.0" diff --git a/api/src/factory.rs b/api/src/factory.rs index 1216e99e6..585fe5665 100644 --- a/api/src/factory.rs +++ b/api/src/factory.rs @@ -31,6 +31,10 @@ impl ShuttleFactory { #[async_trait] impl Factory for ShuttleFactory { + fn get_project_name(&self) -> ProjectName { + self.project_name.clone() + } + async fn get_db_connection_string( &mut self, db_type: Type, diff --git a/cargo-shuttle/src/factory.rs b/cargo-shuttle/src/factory.rs index aca08f383..6706aea69 100644 --- a/cargo-shuttle/src/factory.rs +++ b/cargo-shuttle/src/factory.rs @@ -160,6 +160,10 @@ impl Factory for LocalFactory { Ok(conn_str) } + + fn get_project_name(&self) -> ProjectName { + self.project.clone() + } } impl LocalFactory { diff --git a/examples/rocket/persist/Cargo.toml b/examples/rocket/persist/Cargo.toml new file mode 100644 index 000000000..a873fb07e --- /dev/null +++ b/examples/rocket/persist/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "persist" +version = "0.1.0" +edition = "2021" + +[lib] + +[dependencies] +rocket = { version = "0.5.0-rc.1", features = ["json"] } +serde = { version = "1.0", features = ["derive"] } +sqlx = { version = "0.5", features = ["runtime-tokio-native-tls", "postgres"] } +shuttle-service = { version = "0.5.0", features = ["web-rocket", "persist"] } diff --git a/examples/rocket/persist/README.md b/examples/rocket/persist/README.md new file mode 100644 index 000000000..d4761bcfa --- /dev/null +++ b/examples/rocket/persist/README.md @@ -0,0 +1,35 @@ +# Persist Example + +An example app to show what you can do with shuttle. + +## How to deploy the example + +To deploy the examples, check out the repository locally + +```bash +$ git clone https://github.com/shuttle-hq/shuttle.git +``` + +navigate to the Persist root folder + +```bash +$ cd examples/rocket/persist +``` + +Pick a project name that is something unique - in shuttle, +projects are globally unique. Then run + +```bash +$ cargo shuttle deploy --name=$PROJECT_NAME +``` + +Once deployed you can post to the endpoint the following values: +```bash +curl -X POST -H "Content-Type: application/json" -d '{"date":"2020-12-22", "temp_high":5, "temp_low":5, "precipitation": 5}' {$PROJECT_NAME}.shuttleapp.rs +``` + +The json data will then persist within Shuttle it can be queried with the following curl request + +```bash +curl {$PROJECT_NAME}.shuttleapp.rs/2020-12-22 +``` \ No newline at end of file diff --git a/examples/rocket/persist/Shuttle.toml b/examples/rocket/persist/Shuttle.toml new file mode 100644 index 000000000..8ab3c88f5 --- /dev/null +++ b/examples/rocket/persist/Shuttle.toml @@ -0,0 +1 @@ +name = "persist-rocket-app" diff --git a/examples/rocket/persist/src/lib.rs b/examples/rocket/persist/src/lib.rs new file mode 100644 index 000000000..b60f6af39 --- /dev/null +++ b/examples/rocket/persist/src/lib.rs @@ -0,0 +1,61 @@ +#[macro_use] +extern crate rocket; + +use rocket::response::status::BadRequest; +use rocket::serde::json::Json; +use rocket::State; +use serde::{Deserialize, Serialize}; + +use shuttle_service::PersistInstance; + +#[derive(Serialize, Deserialize, Clone)] +struct Weather { + date: String, + temp_high: f32, + temp_low: f32, + precipitation: f32, +} + +struct MyState { + persist: PersistInstance, +} + +#[post("/", data = "")] +async fn add( + data: Json, + state: &State, +) -> Result, BadRequest> { + // Change data Json to Weather + let weather: Weather = data.into_inner(); + + let _state = state + .persist + .save::( + format!("weather_{}", &weather.date.as_str()).as_str(), + weather.clone(), + ) + .map_err(|e| BadRequest(Some(e.to_string())))?; + Ok(Json(weather)) +} + +#[get("/")] +async fn retrieve( + date: String, + state: &State, +) -> Result, BadRequest> { + let weather = state + .persist + .load::(format!("weather_{}", &date).as_str()) + .map_err(|e| BadRequest(Some(e.to_string())))?; + Ok(Json(weather)) +} + +#[shuttle_service::main] +async fn rocket(#[persist::Persist] persist: PersistInstance) -> shuttle_service::ShuttleRocket { + let state = MyState { persist }; + let rocket = rocket::build() + .mount("/", routes![retrieve, add]) + .manage(state); + + Ok(rocket) +} diff --git a/service/Cargo.toml b/service/Cargo.toml index 353ac9273..b36571139 100644 --- a/service/Cargo.toml +++ b/service/Cargo.toml @@ -12,6 +12,7 @@ doctest = false anyhow = "1.0.62" async-trait = "0.1.57" axum = { version = "0.5.15", optional = true } +bincode = { version = "1.2.1", optional = true } cargo = { version = "0.64.0", optional = true } chrono = "0.4.22" futures = { version = "0.3.23", features = ["std"] } @@ -25,6 +26,7 @@ poem = { version = "1.3.40", optional = true } regex = "1.6.0" rocket = { version = "0.5.0-rc.2", optional = true } salvo = { version = "0.34.3", optional = true } +serde = { version = "1.0", optional = true } serenity = { version = "0.11.5", default-features = false, features = ["client", "gateway", "rustls_backend", "model"], optional = true } sqlx = { version = "0.6.1", optional = true } sync_wrapper = { version = "0.1.1", optional = true } @@ -64,6 +66,7 @@ sqlx-aws-mysql = ["sqlx-integration", "sqlx/mysql"] sqlx-aws-mariadb = ["sqlx-integration", "sqlx/mysql"] mongodb-integration = ["mongodb"] +persist = ["bincode", "serde/derive"] secrets = ["sqlx-postgres"] web-axum = ["axum", "sync_wrapper"] diff --git a/service/src/lib.rs b/service/src/lib.rs index 49781d8dc..b484b1cf3 100644 --- a/service/src/lib.rs +++ b/service/src/lib.rs @@ -235,6 +235,11 @@ pub use secrets::SecretStore; ))] pub mod aws; +#[cfg(feature = "persist")] +pub mod persist; +#[cfg(feature = "persist")] +pub use persist::PersistInstance; + #[cfg(feature = "codegen")] extern crate shuttle_codegen; #[cfg(feature = "codegen")] @@ -307,6 +312,8 @@ pub mod loader; /// An instance of factory is passed by the deployer as an argument to [Service::build][Service::build] in the initial phase of deployment. /// /// Also see the [main][main] macro. +use shuttle_common::project::ProjectName; + #[async_trait] pub trait Factory: Send + Sync { /// Declare that the [Service][Service] requires a database. @@ -316,6 +323,8 @@ pub trait Factory: Send + Sync { &mut self, db_type: database::Type, ) -> Result; + + fn get_project_name(&self) -> ProjectName; } /// Used to get resources of type `T` from factories. diff --git a/service/src/persist.rs b/service/src/persist.rs new file mode 100644 index 000000000..3a62d3477 --- /dev/null +++ b/service/src/persist.rs @@ -0,0 +1,115 @@ +use crate::{Factory, ResourceBuilder}; +use async_trait::async_trait; +use bincode::{deserialize_from, serialize_into, Error as BincodeError}; +use serde::de::DeserializeOwned; +use serde::Serialize; +use shuttle_common::project::ProjectName; +use std::fs; +use std::fs::File; +use std::io::BufReader; +use std::io::BufWriter; +use std::path::PathBuf; +use thiserror::Error; +use tokio::runtime::Runtime; + +#[derive(Error, Debug)] +pub enum PersistError { + #[error("failed to open file: {0}")] + Open(std::io::Error), + #[error("failed to create folder: {0}")] + CreateFolder(std::io::Error), + #[error("failed to serialize data: {0}")] + Serialize(BincodeError), + #[error("failed to deserialize data: {0}")] + Deserialize(BincodeError), +} + +pub struct Persist; + +pub struct PersistInstance { + project_name: ProjectName, +} + +impl PersistInstance { + pub fn save(&self, key: &str, struc: T) -> Result<(), PersistError> { + let storage_folder = self.get_storage_folder(); + fs::create_dir_all(storage_folder).map_err(PersistError::CreateFolder)?; + + let file_path = self.get_storage_file(key); + let file = File::create(file_path).map_err(PersistError::Open)?; + let mut writer = BufWriter::new(file); + Ok(serialize_into(&mut writer, &struc).map_err(PersistError::Serialize))? + } + + pub fn load(&self, key: &str) -> Result + where + T: DeserializeOwned, + { + let file_path = self.get_storage_file(key); + let file = File::open(file_path).map_err(PersistError::Open)?; + let reader = BufReader::new(file); + Ok(deserialize_from(reader).map_err(PersistError::Deserialize))? + } + + fn get_storage_folder(&self) -> PathBuf { + ["shuttle_persist", &self.project_name.to_string()] + .iter() + .collect() + } + + fn get_storage_file(&self, key: &str) -> PathBuf { + let mut path = self.get_storage_folder(); + path.push(format!("{key}.bin")); + + path + } +} + +#[async_trait] +impl ResourceBuilder for Persist { + fn new() -> Self { + Self {} + } + + async fn build( + self, + factory: &mut dyn Factory, + _runtime: &Runtime, + ) -> Result { + Ok(PersistInstance { + project_name: factory.get_project_name(), + }) + } +} + +#[cfg(test)] +mod tests { + use super::*; + use shuttle_common::project::ProjectName; + use std::str::FromStr; + + #[test] + fn test_save_and_load() { + let persist = PersistInstance { + project_name: ProjectName::from_str("test").unwrap(), + }; + + persist.save("test", "test").unwrap(); + let result: String = persist.load("test").unwrap(); + assert_eq!(result, "test"); + } + + #[test] + fn test_load_error() { + let persist = PersistInstance { + project_name: ProjectName::from_str("test").unwrap(), + }; + + // unwrapp error + let result = persist.load::("error").unwrap_err(); + assert_eq!( + result.to_string(), + "failed to open file: No such file or directory (os error 2)" + ); + } +} diff --git a/service/tests/integration/loader.rs b/service/tests/integration/loader.rs index 34f68f443..1a7abaaa8 100644 --- a/service/tests/integration/loader.rs +++ b/service/tests/integration/loader.rs @@ -1,7 +1,9 @@ use crate::helpers::{loader::build_so_create_loader, sqlx::PostgresInstance}; +use shuttle_common::project::ProjectName; use shuttle_service::loader::LoaderError; use shuttle_service::{database, Error, Factory}; +use std::str::FromStr; use std::net::{Ipv4Addr, SocketAddr}; use std::process::exit; @@ -15,18 +17,24 @@ const RESOURCES_PATH: &str = "tests/resources"; struct DummyFactory { postgres_instance: Option, + project_name: ProjectName, } impl DummyFactory { fn new() -> Self { Self { postgres_instance: None, + project_name: ProjectName::from_str("test").unwrap(), } } } #[async_trait] impl Factory for DummyFactory { + fn get_project_name(&self) -> ProjectName { + self.project_name.clone() + } + async fn get_db_connection_string(&mut self, _: database::Type) -> Result { let uri = if let Some(postgres_instance) = &self.postgres_instance { postgres_instance.get_uri()