diff --git a/Cargo.lock b/Cargo.lock index 99f0967..ae47cc6 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -473,6 +473,12 @@ version = "0.6.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "349f9b6a179ed607305526ca489b34ad0a41aed5f7980fa90eb03160b69598fb" +[[package]] +name = "bit_field" +version = "0.10.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc827186963e592360843fb5ba4b973e145841266c1357f7180c43526f2e5b61" + [[package]] name = "bitflags" version = "1.3.2" @@ -497,6 +503,21 @@ dependencies = [ "wyz", ] +[[package]] +name = "bl" +version = "0.1.0" +dependencies = [ + "artifact", + "bytes", + "clients", + "color-eyre", + "image", + "serde", + "surrealdb", + "thiserror", + "ulid", +] + [[package]] name = "blake2" version = "0.10.6" @@ -827,6 +848,12 @@ dependencies = [ "tracing-error", ] +[[package]] +name = "color_quant" +version = "1.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "3d7b894f5411737b7867f4827955924d7c254fc9f4d91a6aad6b097804b1018b" + [[package]] name = "colored" version = "2.1.0" @@ -966,6 +993,25 @@ version = "1.1.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "7059fff8937831a9ae6f0fe4d658ffabf58f2ca96aa9dec1c889f936f705f216" +[[package]] +name = "crossbeam-deque" +version = "0.8.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613f8cc01fe9cf1a3eb3d7f488fd2fa8388403e97039e2f73692932e291a770d" +dependencies = [ + "crossbeam-epoch", + "crossbeam-utils", +] + +[[package]] +name = "crossbeam-epoch" +version = "0.9.18" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5b82ac4a3c2ca9c3460964f020e1402edd5753411d7737aa39c3714ad1b5420e" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-utils" version = "0.8.19" @@ -1248,6 +1294,22 @@ dependencies = [ "pin-project-lite", ] +[[package]] +name = "exr" +version = "1.72.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "887d93f60543e9a9362ef8a21beedd0a833c5d9610e18c67abe15a5963dcb1a4" +dependencies = [ + "bit_field", + "flume", + "half", + "lebe", + "miniz_oxide", + "rayon-core", + "smallvec", + "zune-inflate", +] + [[package]] name = "eyre" version = "0.6.12" @@ -1264,6 +1326,15 @@ version = "2.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "25cbce373ec4653f1a01a31e8a5e5ec0c622dc27ff9c4e6606eefef5cbbed4a5" +[[package]] +name = "fdeflate" +version = "0.3.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4f9bfee30e4dedf0ab8b422f03af778d9612b63f502710fc500a334ebe2de645" +dependencies = [ + "simd-adler32", +] + [[package]] name = "fixedbitset" version = "0.4.2" @@ -1528,6 +1599,16 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "gif" +version = "0.12.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "80792593675e051cf94a4b111980da2ba60d4a83e43e0048c5693baab3977045" +dependencies = [ + "color_quant", + "weezl", +] + [[package]] name = "gimli" version = "0.28.1" @@ -1950,6 +2031,24 @@ version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "cb56e1aa765b4b4f3aadfab769793b7087bb03a4ea4920644a6d238e2df5b9ed" +[[package]] +name = "image" +version = "0.24.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "034bbe799d1909622a74d1193aa50147769440040ff36cb2baa947609b0a4e23" +dependencies = [ + "bytemuck", + "byteorder", + "color_quant", + "exr", + "gif", + "jpeg-decoder", + "num-traits", + "png", + "qoi", + "tiff", +] + [[package]] name = "indenter" version = "0.3.3" @@ -2077,6 +2176,15 @@ dependencies = [ "libc", ] +[[package]] +name = "jpeg-decoder" +version = "0.3.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f5d4a7da358eff58addd2877a45865158f0d78c911d43a5784ceb7bbf52833b0" +dependencies = [ + "rayon", +] + [[package]] name = "js-sys" version = "0.3.66" @@ -2127,6 +2235,12 @@ dependencies = [ "spin 0.5.2", ] +[[package]] +name = "lebe" +version = "0.5.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "03087c2bad5e1034e8cace5926dec053fb3790248370865f5117a7d0213354c8" + [[package]] name = "leptos" version = "0.6.5" @@ -2535,6 +2649,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9d811f3e15f28568be3407c8e7fdb6514c1cda3cb30683f15b6a1a1dc4ea14a7" dependencies = [ "adler", + "simd-adler32", ] [[package]] @@ -2964,6 +3079,19 @@ version = "0.3.29" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "2900ede94e305130c13ddd391e0ab7cbaeb783945ae07a279c268cb05109c6cb" +[[package]] +name = "png" +version = "0.17.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1f6c3c3e617595665b8ea2ff95a86066be38fb121ff920a9c0eb282abcd1da5a" +dependencies = [ + "bitflags 1.3.2", + "crc32fast", + "fdeflate", + "flate2", + "miniz_oxide", +] + [[package]] name = "powerfmt" version = "0.2.0" @@ -3093,6 +3221,15 @@ dependencies = [ "syn 1.0.109", ] +[[package]] +name = "qoi" +version = "0.4.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7f6d64c71eb498fe9eae14ce4ec935c555749aef511cca85b5568910d6e48001" +dependencies = [ + "bytemuck", +] + [[package]] name = "quick-xml" version = "0.31.0" @@ -3182,6 +3319,26 @@ dependencies = [ "getrandom", ] +[[package]] +name = "rayon" +version = "1.8.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fa7237101a77a10773db45d62004a272517633fbcc3df19d96455ede1122e051" +dependencies = [ + "either", + "rayon-core", +] + +[[package]] +name = "rayon-core" +version = "1.12.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1465873a3dfdaa8ae7cb14b4383657caab0b3e8a0aa9ae8e04b044854c8dfce2" +dependencies = [ + "crossbeam-deque", + "crossbeam-utils", +] + [[package]] name = "redact" version = "0.1.8" @@ -3942,6 +4099,12 @@ dependencies = [ "rand_core", ] +[[package]] +name = "simd-adler32" +version = "0.3.7" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d66dc143e6b11c1eddc06d5c423cfc97062865baf299914ab64caa38182078fe" + [[package]] name = "simdutf8" version = "0.1.4" @@ -4403,6 +4566,17 @@ dependencies = [ "once_cell", ] +[[package]] +name = "tiff" +version = "0.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ba1310fcea54c6a9a4fd1aad794ecc02c31682f6bfbecdf460bf19533eed1e3e" +dependencies = [ + "flate2", + "jpeg-decoder", + "weezl", +] + [[package]] name = "time" version = "0.3.34" @@ -5155,6 +5329,12 @@ version = "0.25.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f20c57d8d7db6d3b86154206ae5d8fba62dd39573114de97c2cb0578251f8e1" +[[package]] +name = "weezl" +version = "0.1.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53a85b86a771b1c87058196170769dd264f66c0782acf1ae6cc51bfd64b39082" + [[package]] name = "winapi" version = "0.3.9" @@ -5439,3 +5619,12 @@ dependencies = [ "cc", "pkg-config", ] + +[[package]] +name = "zune-inflate" +version = "0.2.54" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "73ab332fe2f6680068f3582b16a24f90ad7096d5d39b974d1c0aff0125116f02" +dependencies = [ + "simd-adler32", +] diff --git a/Cargo.toml b/Cargo.toml index 1a4c373..f2d7561 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,6 +12,7 @@ codegen-units = 1 [workspace.dependencies] axum = { version = "0.7.4", features = ["macros"] } +bytes = "1.5.0" cfg-if = "1" color-eyre = "0.6" console_error_panic_hook = "0.1.7" @@ -31,6 +32,7 @@ tower = { version = "0.4", features = ["full"] } tower-http = { version = "0.5", features = ["full"] } tracing = "0.1" tracing-subscriber = "0.3" +ulid = "1.1.2" wasm-bindgen = "=0.2.89" [[workspace.metadata.leptos]] diff --git a/crates/artifact/Cargo.toml b/crates/artifact/Cargo.toml index 6897235..fc5bda0 100644 --- a/crates/artifact/Cargo.toml +++ b/crates/artifact/Cargo.toml @@ -6,13 +6,13 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -ulid = "1.1.2" object_store = { version = "0.9.0", features = ["aws"] } -bytes = "1.5.0" -thiserror = { workspace = true } -serde = { workspace = true } -color-eyre = { workspace = true } -surrealdb = { workspace = true } +bytes.workspace = true +thiserror.workspace = true +serde.workspace = true +color-eyre.workspace = true +surrealdb.workspace = true +ulid.workspace = true clients = { path = "../clients" } diff --git a/crates/artifact/src/lib.rs b/crates/artifact/src/lib.rs index 4ebf819..43a5257 100644 --- a/crates/artifact/src/lib.rs +++ b/crates/artifact/src/lib.rs @@ -6,31 +6,25 @@ use serde::{Deserialize, Serialize}; const ARTIFACT_PRIVATE_LTS_BUCKET: &str = "artifact-private-lts"; const ARTIFACT_PUBLIC_LTS_BUCKET: &str = "artifact-public-lts"; -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SurrealPrivateArtifact { - pub id: String, -} - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct SurrealPublicArtifact { - pub id: String, -} - -#[derive(Clone, Debug)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct PrivateArtifact { id: ulid::Ulid, + #[serde(skip)] contents: Option, } -#[derive(Debug, Clone)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct PublicArtifact { id: ulid::Ulid, + #[serde(skip)] contents: Option, } pub trait Artifact { fn new(contents: Option) -> Self; + fn upload_and_push(&self) -> impl Future> + Send; + fn id(&self) -> ulid::Ulid; fn has_contents(&self) -> bool; fn contents(&self) -> Option<&bytes::Bytes>; @@ -40,6 +34,7 @@ pub trait Artifact { fn object_store(&self) -> Result>; /// Downloads the artifact contents from the object store + #[allow(async_fn_in_trait)] async fn download(&mut self) -> Result<()> { let object_store = self.object_store()?; let path = object_store::path::Path::from(self.id().to_string()); @@ -60,6 +55,7 @@ pub trait Artifact { } /// Uploads the artifact contents to the object store + #[allow(async_fn_in_trait)] async fn upload(&self) -> Result<()> { let object_store = self.object_store()?; let path = object_store::path::Path::from(self.id().to_string()); @@ -91,6 +87,16 @@ impl Artifact for PublicArtifact { } } + async fn upload_and_push(&self) -> Result<()> { + self.upload().await.wrap_err("Failed to upload artifact")?; + self + .push_to_surreal() + .await + .wrap_err("Failed to push to surreal")?; + + Ok(()) + } + fn id(&self) -> ulid::Ulid { self.id } fn has_contents(&self) -> bool { self.contents.is_some() } fn contents(&self) -> Option<&bytes::Bytes> { self.contents.as_ref() } @@ -116,13 +122,10 @@ impl Artifact for PublicArtifact { .wrap_err("Failed to create surreal client")?; client.use_ns("main").use_db("main").await?; - let surreal_artifact = SurrealPublicArtifact { - id: self.id.to_string(), - }; let thing: Option = client - .create(("artifacts", &surreal_artifact.id)) - .content(surreal_artifact) + .create(("artifacts", self.id.to_string())) + .content(self.clone()) .await .wrap_err("Failed to create artifact in surreal")?; @@ -137,15 +140,14 @@ impl Artifact for PublicArtifact { .wrap_err("Failed to create surreal client")?; client.use_ns("main").use_db("main").await?; - let surreal_artifact: Option = client + let artifact: Option = client .select(("artifacts", &id.to_string())) .await .wrap_err("Failed to get artifact from surreal")?; - let surreal_artifact = - surreal_artifact.ok_or_eyre("Artifact does not exist in surreal")?; + let artifact = artifact.ok_or_eyre("Artifact does not exist in surreal")?; let artifact = PublicArtifact { - id: surreal_artifact.id.parse().wrap_err("Failed to parse id")?, + id: artifact.id, contents: None, }; @@ -161,6 +163,16 @@ impl Artifact for PrivateArtifact { } } + async fn upload_and_push(&self) -> Result<()> { + self.upload().await.wrap_err("Failed to upload artifact")?; + self + .push_to_surreal() + .await + .wrap_err("Failed to push to surreal")?; + + Ok(()) + } + fn id(&self) -> ulid::Ulid { self.id } fn has_contents(&self) -> bool { self.contents.is_some() } fn contents(&self) -> Option<&bytes::Bytes> { self.contents.as_ref() } @@ -186,13 +198,10 @@ impl Artifact for PrivateArtifact { .wrap_err("Failed to create surreal client")?; client.use_ns("main").use_db("main").await?; - let surreal_artifact = SurrealPrivateArtifact { - id: self.id.to_string(), - }; let thing: Option = client - .create(("artifacts", &surreal_artifact.id)) - .content(surreal_artifact) + .create(("artifacts", self.id.to_string())) + .content(self.clone()) .await .wrap_err("Failed to create artifact in surreal")?; @@ -207,15 +216,14 @@ impl Artifact for PrivateArtifact { .wrap_err("Failed to create surreal client")?; client.use_ns("main").use_db("main").await?; - let surreal_artifact: Option = client + let artifact: Option = client .select(("artifacts", &id.to_string())) .await .wrap_err("Failed to get artifact from surreal")?; - let surreal_artifact = - surreal_artifact.ok_or_eyre("Artifact does not exist in surreal")?; + let artifact = artifact.ok_or_eyre("Artifact does not exist in surreal")?; let artifact = PrivateArtifact { - id: surreal_artifact.id.parse().wrap_err("Failed to parse id")?, + id: artifact.id, contents: None, }; diff --git a/crates/bl/Cargo.toml b/crates/bl/Cargo.toml index 9c02bfe..17c34e4 100644 --- a/crates/bl/Cargo.toml +++ b/crates/bl/Cargo.toml @@ -6,3 +6,14 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] +clients = { path = "../clients" } +artifact = { path = "../artifact" } + +bytes.workspace = true +color-eyre.workspace = true +thiserror.workspace = true +surrealdb.workspace = true +serde.workspace = true +ulid.workspace = true + +image = "0.24.8" diff --git a/crates/bl/src/lib.rs b/crates/bl/src/lib.rs index 522d1f2..2cfc044 100644 --- a/crates/bl/src/lib.rs +++ b/crates/bl/src/lib.rs @@ -1,12 +1,140 @@ -pub fn add(left: usize, right: usize) -> usize { left + right } +use artifact::{Artifact, PrivateArtifact, PublicArtifact}; +use bytes::Bytes; +use clients::surreal::SurrealRootClient; +use color_eyre::eyre::{Result, WrapErr}; +use serde::{Deserialize, Serialize}; +use surrealdb::sql::Thing; -#[cfg(test)] -mod tests { - use super::*; +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct Photo { + pub id: ulid::Ulid, + pub photographer: Thing, + pub owner: Thing, + pub artifacts: PhotoArtifacts, +} + +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PhotoArtifacts { + pub original: PrivateArtifact, + pub thumbnail: PublicArtifact, +} - #[test] - fn it_works() { - let result = add(2, 2); - assert_eq!(result, 4); +#[derive(Clone, Debug, Deserialize, Serialize)] +pub struct PhotoGroup { + pub id: ulid::Ulid, + pub photos: Vec, + pub public: bool, +} + +#[derive(Clone, Debug, Default, Deserialize, Serialize)] +pub struct PhotoGroupUploadMeta { + pub public: bool, +} + +#[derive(Clone, Debug, Deserialize, Serialize, thiserror::Error)] +pub enum PhotoUploadError { + #[error("Failed to load original image: {0}")] + InvalidImage(String), + #[error("Failed to create artifact: {0}")] + ArtifactCreationError(String), + #[error("Surreal error: {0}")] + DBError(String), +} + +fn thumbnail_size(aspect_ratio: f32) -> (u32, u32) { + if aspect_ratio > 1.0 { + (200, (200.0 / aspect_ratio) as u32) + } else { + ((200.0 * aspect_ratio) as u32, 200) } } + +pub async fn upload_single_photo( + user_id: Thing, + original_bytes: Bytes, + group_meta: PhotoGroupUploadMeta, +) -> Result { + let original_image = + image::load_from_memory(&original_bytes).map_err(|e| { + PhotoUploadError::InvalidImage(format!( + "Failed to parse original image: {e}" + )) + })?; + + let original_artifact = PrivateArtifact::new(Some(original_bytes)); + original_artifact.upload_and_push().await.map_err(|e| { + PhotoUploadError::ArtifactCreationError(format!( + "Failed to create original artifact: {e}" + )) + })?; + + let aspect_ratio = + original_image.width() as f32 / original_image.height() as f32; + let thumbnail_size = thumbnail_size(aspect_ratio); + + let thumbnail_image = original_image.resize_exact( + thumbnail_size.0, + thumbnail_size.1, + image::imageops::FilterType::Lanczos3, + ); + let thumbnail_bytes: Bytes = thumbnail_image.as_bytes().to_vec().into(); + let thumbnail_artifact = PublicArtifact::new(Some(thumbnail_bytes)); + thumbnail_artifact.upload_and_push().await.map_err(|e| { + PhotoUploadError::ArtifactCreationError(format!( + "Failed to create thumbnail artifact: {e}" + )) + })?; + + let photo = Photo { + id: ulid::Ulid::new(), + photographer: user_id.clone(), + owner: user_id, + artifacts: PhotoArtifacts { + original: original_artifact, + thumbnail: thumbnail_artifact, + }, + }; + + let client = SurrealRootClient::new().await.map_err(|_| { + PhotoUploadError::DBError("Failed to create surreal client".to_string()) + })?; + client.use_ns("main").use_db("main").await.map_err(|_| { + PhotoUploadError::DBError("Failed to use surreal namespace".to_string()) + })?; + + let photo_thing: Option = client + .create(("photo", photo.id.to_string())) + .content(photo) + .await + .map_err(|_| { + PhotoUploadError::DBError("Failed to create photo in surreal".to_string()) + })?; + + let photo_thing = photo_thing.ok_or_else(|| { + PhotoUploadError::DBError("Failed to create photo in surreal".to_string()) + })?; + + let group = PhotoGroup { + id: ulid::Ulid::new(), + photos: vec![photo_thing], + public: group_meta.public, + }; + + let group_thing: Option = client + .create(("photo_group", group.id.to_string())) + .content(group.clone()) + .await + .map_err(|_| { + PhotoUploadError::DBError( + "Failed to create photo group in surreal".to_string(), + ) + })?; + + let _group_thing = group_thing.ok_or_else(|| { + PhotoUploadError::DBError( + "Failed to create photo group in surreal".to_string(), + ) + })?; + + Ok(group) +}