diff --git a/Cargo.toml b/Cargo.toml index a8e6df69a..41744a420 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -23,16 +23,16 @@ prisma-client-rust = { git = "https://github.com/Brendonovich/prisma-client-rust "sqlite-create-many", "migrations", "sqlite", -] } +], default-features = false } prisma-client-rust-cli = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "1c1a7fb7b436a01ee7763e7f75cfa8f25a5c10e2", features = [ "rspc", "sqlite-create-many", "migrations", "sqlite", -] } +], default-features = false } prisma-client-rust-sdk = { git = "https://github.com/Brendonovich/prisma-client-rust", rev = "1c1a7fb7b436a01ee7763e7f75cfa8f25a5c10e2", features = [ "sqlite", -] } +], default-features = false } rspc = { version = "0.1.4" } specta = { version = "1.0.4" } diff --git a/apps/mobile/src/components/explorer/Explorer.tsx b/apps/mobile/src/components/explorer/Explorer.tsx index 5f3102097..bdf78a725 100644 --- a/apps/mobile/src/components/explorer/Explorer.tsx +++ b/apps/mobile/src/components/explorer/Explorer.tsx @@ -31,7 +31,7 @@ const Explorer = ({ items }: ExplorerProps) => { const { modalRef, setData } = useActionsModalStore(); function handlePress(data: ExplorerItem) { - if (isPath(data) && data.item.is_dir) { + if (isPath(data) && data.item.is_dir && data.item.location_id !== null) { navigation.push('Location', { id: data.item.location_id, path: `${data.item.materialized_path}${data.item.name}/` diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma index 7f3dd069e..98e84ee37 100644 --- a/core/prisma/schema.prisma +++ b/core/prisma/schema.prisma @@ -123,28 +123,28 @@ model FilePath { id Int @id @default(autoincrement()) pub_id Bytes @unique - is_dir Boolean @default(false) + is_dir Boolean? // content addressable storage id - blake3 sampled checksum cas_id String? // full byte contents digested into blake3 checksum - integrity_checksum String? @unique + integrity_checksum String? // location that owns this path - location_id Int - location Location @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: Cascade) + location_id Int? + location Location? @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: Cascade) // the path of the file relative to its location - materialized_path String + materialized_path String? // the name and extension - name String - extension String // Extension MUST have 'COLLATE NOCASE' in migration + name String? + extension String? // Extension MUST have 'COLLATE NOCASE' in migration - size_in_bytes String @default("0") + size_in_bytes String? - inode Bytes // This is actually an unsigned 64 bit integer, but we don't have this type in SQLite - device Bytes // This is actually an unsigned 64 bit integer, but we don't have this type in SQLite + inode Bytes? // This is actually an unsigned 64 bit integer, but we don't have this type in SQLite + device Bytes? // This is actually an unsigned 64 bit integer, but we don't have this type in SQLite // the unique Object for this file path object_id Int? @@ -153,9 +153,9 @@ model FilePath { key_id Int? // replacement for encryption // permissions String? - date_created DateTime @default(now()) - date_modified DateTime @default(now()) - date_indexed DateTime @default(now()) + date_created DateTime? + date_modified DateTime? + date_indexed DateTime? // key Key? @relation(fields: [key_id], references: [id]) @@ -171,28 +171,29 @@ model Object { id Int @id @default(autoincrement()) pub_id Bytes @unique // Enum: sd_file_ext::kind::ObjectKind - kind Int @default(0) + kind Int? - key_id Int? + key_id Int? // handy ways to mark an object - hidden Boolean @default(false) - favorite Boolean @default(false) - important Boolean @default(false) + hidden Boolean? + favorite Boolean? + important Boolean? // if we have generated preview media for this object on at least one Node - has_thumbnail Boolean @default(false) - has_thumbstrip Boolean @default(false) - has_video_preview Boolean @default(false) + // commented out for now by @brendonovich since they they're irrelevant to the sync system + // has_thumbnail Boolean? + // has_thumbstrip Boolean? + // has_video_preview Boolean? // TODO: change above to: // has_generated_thumbnail Boolean @default(false) // has_generated_thumbstrip Boolean @default(false) // has_generated_video_preview Boolean @default(false) // integration with ipfs - ipfs_id String? + // ipfs_id String? // plain text note - note String? + note String? // the original known creation date of this object - date_created DateTime @default(now()) - date_accessed DateTime? + date_created DateTime? + date_accessed DateTime? tags TagOnObject[] labels LabelOnObject[] diff --git a/core/src/api/files.rs b/core/src/api/files.rs index 49d4f84e8..1264b37f9 100644 --- a/core/src/api/files.rs +++ b/core/src/api/files.rs @@ -85,7 +85,7 @@ pub(crate) fn mount() -> AlphaRouter { .object() .update( object::id::equals(args.id), - vec![object::favorite::set(args.favorite)], + vec![object::favorite::set(Some(args.favorite))], ) .exec() .await?; @@ -214,7 +214,7 @@ pub(crate) fn mount() -> AlphaRouter { library: &Library, ) -> Result<(), rspc::Error> { let location_path = location_path.as_ref(); - let iso_file_path = IsolatedFilePathData::from( + let iso_file_path = IsolatedFilePathData::try_from( library .db .file_path() @@ -225,7 +225,8 @@ pub(crate) fn mount() -> AlphaRouter { .ok_or(LocationError::FilePath(FilePathError::IdNotFound( from_file_path_id, )))?, - ); + ) + .map_err(LocationError::MissingField)?; if iso_file_path.full_name() == to { return Ok(()); @@ -275,8 +276,8 @@ pub(crate) fn mount() -> AlphaRouter { .update( file_path::id::equals(from_file_path_id), vec![ - file_path::name::set(new_file_name.to_string()), - file_path::extension::set(new_extension.to_string()), + file_path::name::set(Some(new_file_name.to_string())), + file_path::extension::set(Some(new_extension.to_string())), ], ) .exec() @@ -312,7 +313,11 @@ pub(crate) fn mount() -> AlphaRouter { .exec() .await? .into_iter() - .map(|file_path| (file_path.id, IsolatedFilePathData::from(file_path))) + .flat_map(|file_path| { + let id = file_path.id; + + IsolatedFilePathData::try_from(file_path).map(|d| (id, d)) + }) .map(|(file_path_id, iso_file_path)| { let from = location_path.join(&iso_file_path); let mut to = location_path.join(iso_file_path.parent()); @@ -377,8 +382,8 @@ pub(crate) fn mount() -> AlphaRouter { library.db.file_path().update( file_path::id::equals(file_path_id), vec![ - file_path::name::set(new_name), - file_path::extension::set(new_extension), + file_path::name::set(Some(new_name)), + file_path::extension::set(Some(new_extension)), ], ) }) diff --git a/core/src/api/search.rs b/core/src/api/search.rs index cd7ad42be..68b563a3e 100644 --- a/core/src/api/search.rs +++ b/core/src/api/search.rs @@ -166,10 +166,10 @@ enum ObjectHiddenFilter { Include, } -impl From for Option { - fn from(value: ObjectHiddenFilter) -> Self { - match value { - ObjectHiddenFilter::Exclude => Some(object::hidden::not(true)), +impl ObjectHiddenFilter { + fn to_param(self) -> Option { + match self { + ObjectHiddenFilter::Exclude => Some(object::hidden::not(Some(true))), ObjectHiddenFilter::Include => None, } } @@ -192,20 +192,21 @@ struct ObjectFilterArgs { impl ObjectFilterArgs { fn into_params(self) -> Vec { + use object::*; + chain_optional_iter( [], [ - self.hidden.into(), - self.favorite.map(object::favorite::equals), + self.hidden.to_param(), + self.favorite.map(Some).map(favorite::equals), self.date_accessed - .map(|date| date.into_prisma(object::date_accessed::equals)), - (!self.kind.is_empty()) - .then(|| object::kind::in_vec(self.kind.into_iter().collect())), + .map(|date| date.into_prisma(date_accessed::equals)), + (!self.kind.is_empty()).then(|| kind::in_vec(self.kind.into_iter().collect())), (!self.tags.is_empty()).then(|| { let tags = self.tags.into_iter().map(tag::id::equals).collect(); let tags_on_object = tag_on_object::tag::is(vec![or(tags)]); - object::tags::some(vec![tags_on_object]) + tags::some(vec![tags_on_object]) }), ], ) @@ -268,29 +269,26 @@ pub fn mount() -> AlphaRouter { _ => None, }; + use file_path::*; + let params = chain_optional_iter( filter .search .split(' ') .map(str::to_string) - .map(file_path::name::contains), + .map(name::contains), [ - filter.location_id.map(file_path::location_id::equals), - filter.extension.map(file_path::extension::equals), - filter - .created_at - .from - .map(|v| file_path::date_created::gte(v.into())), - filter - .created_at - .to - .map(|v| file_path::date_created::lte(v.into())), + filter.location_id.map(Some).map(location_id::equals), + filter.extension.map(Some).map(extension::equals), + filter.created_at.from.map(|v| date_created::gte(v.into())), + filter.created_at.to.map(|v| date_created::lte(v.into())), directory_materialized_path_str - .map(file_path::materialized_path::equals), + .map(Some) + .map(materialized_path::equals), filter.object.and_then(|obj| { let params = obj.into_params(); - (!params.is_empty()).then(|| file_path::object::is(params)) + (!params.is_empty()).then(|| object::is(params)) }), ], ); diff --git a/core/src/custom_uri.rs b/core/src/custom_uri.rs index 764bc4fa6..b8745e526 100644 --- a/core/src/custom_uri.rs +++ b/core/src/custom_uri.rs @@ -1,7 +1,7 @@ use crate::{ location::file_path_helper::{file_path_to_handle_custom_uri, IsolatedFilePathData}, prisma::{file_path, location}, - util::error::FileIOError, + util::{db::*, error::FileIOError}, Node, }; @@ -193,14 +193,14 @@ async fn handle_file( .await? .ok_or_else(|| HandleCustomUriError::NotFound("object"))?; - let Some(path) = &file_path.location.path else { - return Err(HandleCustomUriError::NoPath(file_path.location.id)) - }; + let location = maybe_missing(&file_path.location, "file_path.location")?; + let path = maybe_missing(&location.path, "file_path.location.path")?; let lru_entry = ( - Path::new(path).join(IsolatedFilePathData::from((location_id, &file_path))), - file_path.extension, + Path::new(path).join(IsolatedFilePathData::try_from((location_id, &file_path))?), + maybe_missing(file_path.extension, "extension")?, ); + FILE_METADATA_CACHE.insert(lru_cache_key, lru_entry.clone()); lru_entry @@ -400,8 +400,8 @@ pub enum HandleCustomUriError { RangeNotSatisfiable(&'static str), #[error("HandleCustomUriError::NotFound - resource '{0}'")] NotFound(&'static str), - #[error("no-path")] - NoPath(i32), + #[error("HandleCustomUriError::MissingField - '{0}'")] + MissingField(#[from] MissingFieldError), } impl From for Response> { @@ -444,7 +444,7 @@ impl From for Response> { .as_bytes() .to_vec(), ), - HandleCustomUriError::NoPath(id) => { + HandleCustomUriError::MissingField(id) => { error!("Location has no path"); builder .status(StatusCode::INTERNAL_SERVER_ERROR) diff --git a/core/src/job/mod.rs b/core/src/job/mod.rs index f57af9fb7..87e0c6bde 100644 --- a/core/src/job/mod.rs +++ b/core/src/job/mod.rs @@ -5,7 +5,7 @@ use crate::{ file_identifier::FileIdentifierJobError, fs::error::FileSystemJobsError, preview::ThumbnailerError, }, - util::error::FileIOError, + util::{db::MissingFieldError, error::FileIOError}, }; use sd_crypto::Error as CryptoError; @@ -15,6 +15,7 @@ use std::{ fmt::Debug, hash::{Hash, Hasher}, mem, + path::PathBuf, sync::Arc, }; @@ -54,6 +55,7 @@ pub enum JobError { MissingReport { id: Uuid, name: String }, #[error("missing some job data: '{value}'")] MissingData { value: String }, + #[error("error converting/handling paths")] Path, #[error("invalid job status integer: {0}")] @@ -74,12 +76,10 @@ pub enum JobError { FileSystemJobsError(#[from] FileSystemJobsError), #[error(transparent)] CryptoError(#[from] CryptoError), + #[error("missing-field: {0}")] + MissingField(#[from] MissingFieldError), #[error("item of type '{0}' with id '{1}' is missing from the db")] MissingFromDb(&'static str, String), - #[error("the cas id is not set on the path data")] - MissingCasId, - #[error("missing-location-path")] - MissingPath, // Not errors #[error("step completed with errors: {0:?}")] diff --git a/core/src/library/cat.rs b/core/src/library/cat.rs index b37f219f8..b89b29666 100644 --- a/core/src/library/cat.rs +++ b/core/src/library/cat.rs @@ -58,12 +58,12 @@ impl Category { pub async fn get_category_count(db: &Arc, category: Category) -> i32 { let param = match category { Category::Recents => not![object::date_accessed::equals(None)], - Category::Favorites => object::favorite::equals(true), + Category::Favorites => object::favorite::equals(Some(true)), Category::Photos | Category::Videos | Category::Music | Category::Encrypted - | Category::Books => object::kind::equals(category.to_object_kind() as i32), + | Category::Books => object::kind::equals(Some(category.to_object_kind() as i32)), _ => return 0, }; diff --git a/core/src/library/library.rs b/core/src/library/library.rs index 1cd805276..1f99e9158 100644 --- a/core/src/library/library.rs +++ b/core/src/library/library.rs @@ -9,7 +9,7 @@ use crate::{ object::{orphan_remover::OrphanRemoverActor, preview::get_thumbnail_path}, prisma::{file_path, location, PrismaClient}, sync::SyncManager, - util::error::FileIOError, + util::{db::maybe_missing, error::FileIOError}, NodeContext, }; @@ -124,16 +124,20 @@ impl Library { .exec() .await? .into_iter() - .map(|file_path| { - ( + .flat_map(|file_path| { + let location = maybe_missing(&file_path.location, "file_path.location")?; + + Ok::<_, LibraryManagerError>(( file_path.id, - file_path.location.path.as_ref().map(|location_path| { - Path::new(&location_path).join(IsolatedFilePathData::from(( - file_path.location.id, - &file_path, - ))) - }), - ) + location + .path + .as_ref() + .map(|location_path| { + IsolatedFilePathData::try_from((location.id, &file_path)) + .map(|data| Path::new(&location_path).join(data)) + }) + .transpose()?, + )) }), ); diff --git a/core/src/library/manager.rs b/core/src/library/manager.rs index 879ae2a5e..ea8d53beb 100644 --- a/core/src/library/manager.rs +++ b/core/src/library/manager.rs @@ -6,7 +6,7 @@ use crate::{ prisma::{location, node}, sync::{SyncManager, SyncMessage}, util::{ - db, + db::{self, MissingFieldError}, error::{FileIOError, NonUtf8PathError}, migrator::{Migrate, MigratorError}, }, @@ -65,8 +65,8 @@ pub enum LibraryManagerError { NonUtf8Path(#[from] NonUtf8PathError), #[error("failed to watch locations: {0}")] LocationWatcher(#[from] LocationManagerError), - #[error("no-path")] - NoPath(i32), + #[error("missing-field: {0}")] + MissingField(#[from] MissingFieldError), } impl From for rspc::Error { diff --git a/core/src/location/error.rs b/core/src/location/error.rs index b27bacb3f..44fd00c78 100644 --- a/core/src/location/error.rs +++ b/core/src/location/error.rs @@ -1,4 +1,7 @@ -use crate::{prisma::location, util::error::FileIOError}; +use crate::{ + prisma::location, + util::{db::MissingFieldError, error::FileIOError}, +}; use std::path::PathBuf; @@ -68,6 +71,8 @@ pub enum LocationError { FileIO(#[from] FileIOError), #[error("location missing path ")] MissingPath(location::id::Type), + #[error("missing-field: {0}")] + MissingField(#[from] MissingFieldError), } impl From for rspc::Error { diff --git a/core/src/location/file_path_helper/isolated_file_path_data.rs b/core/src/location/file_path_helper/isolated_file_path_data.rs index e24f834e0..0bdd1f223 100644 --- a/core/src/location/file_path_helper/isolated_file_path_data.rs +++ b/core/src/location/file_path_helper/isolated_file_path_data.rs @@ -29,7 +29,7 @@ pub struct IsolatedFilePathData<'a> { pub(in crate::location) is_dir: bool, pub(in crate::location) name: Cow<'a, str>, pub(in crate::location) extension: Cow<'a, str>, - pub(in crate::location) relative_path: Cow<'a, str>, + relative_path: Cow<'a, str>, } impl IsolatedFilePathData<'static> { @@ -90,6 +90,10 @@ impl<'a> IsolatedFilePathData<'a> { &self.extension } + pub fn materialized_path(&'a self) -> &'a str { + &self.materialized_path + } + pub fn is_root(&self) -> bool { self.is_dir && self.materialized_path == "/" @@ -261,25 +265,25 @@ impl<'a> IsolatedFilePathData<'a> { .unwrap_or_default() } - fn from_db_data( + pub fn from_db_data( location_id: location::id::Type, - db_materialized_path: &'a str, - db_is_dir: bool, - db_name: &'a str, - db_extension: &'a str, + is_dir: bool, + materialized_path: Cow<'a, str>, + name: Cow<'a, str>, + extension: Cow<'a, str>, ) -> Self { Self { - location_id, - materialized_path: Cow::Borrowed(db_materialized_path), - is_dir: db_is_dir, - name: Cow::Borrowed(db_name), - extension: Cow::Borrowed(db_extension), relative_path: Cow::Owned(assemble_relative_path( - db_materialized_path, - db_name, - db_extension, - db_is_dir, + &materialized_path, + &name, + &extension, + is_dir, )), + location_id, + materialized_path, + is_dir, + name, + extension, } } } @@ -304,10 +308,10 @@ impl From> for file_path::UniqueWhereParam { impl From> for file_path::WhereParam { fn from(path: IsolatedFilePathData<'static>) -> Self { Self::And(vec![ - file_path::location_id::equals(path.location_id), - file_path::materialized_path::equals(path.materialized_path.into_owned()), - file_path::name::equals(path.name.into_owned()), - file_path::extension::equals(path.extension.into_owned()), + file_path::location_id::equals(Some(path.location_id)), + file_path::materialized_path::equals(Some(path.materialized_path.into_owned())), + file_path::name::equals(Some(path.name.into_owned())), + file_path::extension::equals(Some(path.extension.into_owned())), ]) } } @@ -326,10 +330,10 @@ impl From<&IsolatedFilePathData<'_>> for file_path::UniqueWhereParam { impl From<&IsolatedFilePathData<'_>> for file_path::WhereParam { fn from(path: &IsolatedFilePathData<'_>) -> Self { Self::And(vec![ - file_path::location_id::equals(path.location_id), - file_path::materialized_path::equals(path.materialized_path.to_string()), - file_path::name::equals(path.name.to_string()), - file_path::extension::equals(path.extension.to_string()), + file_path::location_id::equals(Some(path.location_id)), + file_path::materialized_path::equals(Some(path.materialized_path.to_string())), + file_path::name::equals(Some(path.name.to_string())), + file_path::extension::equals(Some(path.extension.to_string())), ]) } } @@ -345,49 +349,47 @@ mod macros { macro_rules! impl_from_db { ($($file_path_kind:ident),+ $(,)?) => { $( - impl ::std::convert::From<$file_path_kind::Data> for $crate:: + impl ::std::convert::TryFrom<$file_path_kind::Data> for $crate:: location:: file_path_helper:: isolated_file_path_data:: IsolatedFilePathData<'static> { - fn from(path: $file_path_kind::Data) -> Self { - Self { - location_id: path.location_id, - relative_path: ::std::borrow::Cow::Owned( - $crate:: - location:: - file_path_helper:: - isolated_file_path_data:: - assemble_relative_path( - &path.materialized_path, - &path.name, - &path.extension, - path.is_dir, - ) - ), - materialized_path: ::std::borrow::Cow::Owned(path.materialized_path), - is_dir: path.is_dir, - name: ::std::borrow::Cow::Owned(path.name), - extension: ::std::borrow::Cow::Owned(path.extension), - } + type Error = $crate::util::db::MissingFieldError; + + fn try_from(path: $file_path_kind::Data) -> Result { + use $crate::util::db::maybe_missing; + use ::std::borrow::Cow; + + Ok(Self::from_db_data( + maybe_missing(path.location_id, "file_path.location_id")?, + maybe_missing(path.is_dir, "file_path.is_dir")?, + Cow::Owned(maybe_missing(path.materialized_path, "file_path.materialized_path")?), + Cow::Owned(maybe_missing(path.name, "file_path.name")?), + Cow::Owned(maybe_missing(path.extension, "file_path.extension")?) + )) } } - impl<'a> ::std::convert::From<&'a $file_path_kind::Data> for $crate:: + impl<'a> ::std::convert::TryFrom<&'a $file_path_kind::Data> for $crate:: location:: file_path_helper:: isolated_file_path_data:: IsolatedFilePathData<'a> { - fn from(path: &'a $file_path_kind::Data) -> Self { - Self::from_db_data( - path.location_id, - &path.materialized_path, - path.is_dir, - &path.name, - &path.extension - ) + type Error = $crate::util::db::MissingFieldError; + + fn try_from(path: &'a $file_path_kind::Data) -> Result { + use $crate::util::db::maybe_missing; + use ::std::borrow::Cow; + + Ok(Self::from_db_data( + maybe_missing(path.location_id, "file_path.location_id")?, + maybe_missing(path.is_dir, "file_path.is_dir")?, + Cow::Borrowed(maybe_missing(&path.materialized_path, "file_path.materialized_path")?), + Cow::Borrowed(maybe_missing(&path.name, "file_path.name")?), + Cow::Borrowed(maybe_missing(&path.extension, "file_path.extension")?) + )) } } )+ @@ -397,49 +399,47 @@ mod macros { macro_rules! impl_from_db_without_location_id { ($($file_path_kind:ident),+ $(,)?) => { $( - impl ::std::convert::From<($crate::prisma::location::id::Type, $file_path_kind::Data)> for $crate:: + impl ::std::convert::TryFrom<($crate::prisma::location::id::Type, $file_path_kind::Data)> for $crate:: location:: file_path_helper:: isolated_file_path_data:: IsolatedFilePathData<'static> { - fn from((location_id, path): ($crate::prisma::location::id::Type, $file_path_kind::Data)) -> Self { - Self { - location_id, - relative_path: Cow::Owned( - $crate:: - location:: - file_path_helper:: - isolated_file_path_data:: - assemble_relative_path( - &path.materialized_path, - &path.name, - &path.extension, - path.is_dir, - ) - ), - materialized_path: Cow::Owned(path.materialized_path), - is_dir: path.is_dir, - name: Cow::Owned(path.name), - extension: Cow::Owned(path.extension), - } + type Error = $crate::util::db::MissingFieldError; + + fn try_from((location_id, path): ($crate::prisma::location::id::Type, $file_path_kind::Data)) -> Result { + use $crate::util::db::maybe_missing; + use ::std::borrow::Cow; + + Ok(Self::from_db_data( + location_id, + maybe_missing(path.is_dir, "file_path.is_dir")?, + Cow::Owned(maybe_missing(path.materialized_path, "file_path.materialized_path")?), + Cow::Owned(maybe_missing(path.name, "file_path.name")?), + Cow::Owned(maybe_missing(path.extension, "file_path.extension")?) + )) } } - impl<'a> ::std::convert::From<($crate::prisma::location::id::Type, &'a $file_path_kind::Data)> for $crate:: + impl<'a> ::std::convert::TryFrom<($crate::prisma::location::id::Type, &'a $file_path_kind::Data)> for $crate:: location:: file_path_helper:: isolated_file_path_data:: IsolatedFilePathData<'a> { - fn from((location_id, path): ($crate::prisma::location::id::Type, &'a $file_path_kind::Data)) -> Self { - Self::from_db_data( + type Error = $crate::util::db::MissingFieldError; + + fn try_from((location_id, path): ($crate::prisma::location::id::Type, &'a $file_path_kind::Data)) -> Result { + use $crate::util::db::maybe_missing; + use ::std::borrow::Cow; + + Ok(Self::from_db_data( location_id, - &path.materialized_path, - path.is_dir, - &path.name, - &path.extension - ) + maybe_missing(path.is_dir, "file_path.is_dir")?, + Cow::Borrowed(maybe_missing(&path.materialized_path, "file_path.materialized_path")?), + Cow::Borrowed(maybe_missing(&path.name, "file_path.name")?), + Cow::Borrowed(maybe_missing(&path.extension, "file_path.extension")?) + )) } } )+ diff --git a/core/src/location/file_path_helper/mod.rs b/core/src/location/file_path_helper/mod.rs index ef2b50b95..54a3d9f23 100644 --- a/core/src/location/file_path_helper/mod.rs +++ b/core/src/location/file_path_helper/mod.rs @@ -159,6 +159,7 @@ pub async fn create_file_path( ) -> Result { use crate::{sync, util::db::uuid_to_bytes}; + use sd_prisma::prisma; use serde_json::json; use uuid::Uuid; @@ -207,25 +208,22 @@ pub async fn create_file_path( }, params, ), - db.file_path().create( - pub_id, - location::id::equals(location.id), - materialized_path.into_owned(), - name.into_owned(), - extension.into_owned(), - metadata.inode.to_le_bytes().into(), - metadata.device.to_le_bytes().into(), - { - use file_path::*; - vec![ - cas_id::set(cas_id), - is_dir::set(is_dir), - size_in_bytes::set(metadata.size_in_bytes.to_string()), - date_created::set(metadata.created_at.into()), - date_modified::set(metadata.modified_at.into()), - ] - }, - ), + db.file_path().create(pub_id, { + use file_path::*; + vec![ + location::connect(prisma::location::id::equals(location.id)), + materialized_path::set(Some(materialized_path.into_owned())), + name::set(Some(name.into_owned())), + extension::set(Some(extension.into_owned())), + inode::set(Some(metadata.inode.to_le_bytes().into())), + device::set(Some(metadata.device.to_le_bytes().into())), + cas_id::set(cas_id), + is_dir::set(Some(is_dir)), + size_in_bytes::set(Some(metadata.size_in_bytes.to_string())), + date_created::set(Some(metadata.created_at.into())), + date_modified::set(Some(metadata.modified_at.into())), + ] + }), ) .await?; @@ -255,11 +253,11 @@ pub fn filter_existing_file_path_params( }: &IsolatedFilePathData, ) -> Vec { vec![ - file_path::location_id::equals(*location_id), - file_path::materialized_path::equals(materialized_path.to_string()), - file_path::is_dir::equals(*is_dir), - file_path::name::equals(name.to_string()), - file_path::extension::equals(extension.to_string()), + file_path::location_id::equals(Some(*location_id)), + file_path::materialized_path::equals(Some(materialized_path.to_string())), + file_path::is_dir::equals(Some(*is_dir)), + file_path::name::equals(Some(name.to_string())), + file_path::extension::equals(Some(extension.to_string())), ] } @@ -277,10 +275,10 @@ pub fn loose_find_existing_file_path_params( }: &IsolatedFilePathData, ) -> Vec { vec![ - file_path::location_id::equals(*location_id), - file_path::materialized_path::equals(materialized_path.to_string()), - file_path::name::equals(name.to_string()), - file_path::extension::equals(extension.to_string()), + file_path::location_id::equals(Some(*location_id)), + file_path::materialized_path::equals(Some(materialized_path.to_string())), + file_path::name::equals(Some(name.to_string())), + file_path::extension::equals(Some(extension.to_string())), ] } diff --git a/core/src/location/indexer/indexer_job.rs b/core/src/location/indexer/indexer_job.rs index 017ac87c2..71d1d9f37 100644 --- a/core/src/location/indexer/indexer_job.rs +++ b/core/src/location/indexer/indexer_job.rs @@ -6,6 +6,7 @@ use crate::{ IsolatedFilePathData, }, to_remove_db_fetcher_fn, + util::db::maybe_missing, }; use std::{path::Path, sync::Arc}; @@ -62,10 +63,8 @@ impl StatefulJob for IndexerJob { state: &mut JobState, ) -> Result<(), JobError> { let location_id = state.init.location.id; - let location_path = state.init.location.path.as_ref(); - let Some(location_path) = location_path.map(Path::new) else { - return Err(JobError::MissingPath) - }; + let location_path = + maybe_missing(&state.init.location.path, "location.path").map(Path::new)?; let db = Arc::clone(&ctx.library.db); @@ -206,10 +205,8 @@ impl StatefulJob for IndexerJob { } IndexerJobStepInput::Walk(to_walk_entry) => { let location_id = state.init.location.id; - let location_path = state.init.location.path.as_ref(); - let Some(location_path) = location_path.map(Path::new) else { - return Err(JobError::MissingPath) - }; + let location_path = + maybe_missing(&state.init.location.path, "location.path").map(Path::new)?; let db = Arc::clone(&ctx.library.db); @@ -280,10 +277,8 @@ impl StatefulJob for IndexerJob { } async fn finalize(&mut self, ctx: WorkerContext, state: &mut JobState) -> JobResult { - let location_path = state.init.location.path.as_ref(); - let Some(location_path) = location_path.map(Path::new) else { - return Err(JobError::MissingPath) - }; + let location_path = + maybe_missing(&state.init.location.path, "location.path").map(Path::new)?; finalize_indexer(location_path, state, ctx) } diff --git a/core/src/location/indexer/mod.rs b/core/src/location/indexer/mod.rs index 9965155f0..fee1de8d1 100644 --- a/core/src/location/indexer/mod.rs +++ b/core/src/location/indexer/mod.rs @@ -14,6 +14,7 @@ use std::{ }; use rspc::ErrorCode; +use sd_prisma::prisma_sync; use serde::{de::DeserializeOwned, Deserialize, Serialize}; use serde_json::json; use thiserror::Error; @@ -151,41 +152,63 @@ async fn execute_indexer_save_step( use file_path::*; + let pub_id = uuid_to_bytes(entry.pub_id); + + let (sync_params, db_params): (Vec<_>, Vec<_>) = [ + ( + ( + location::NAME, + json!(prisma_sync::location::SyncId { + pub_id: pub_id.clone() + }), + ), + location_id::set(Some(location.id)), + ), + ( + (materialized_path::NAME, json!(materialized_path)), + materialized_path::set(Some(materialized_path.to_string())), + ), + ((name::NAME, json!(name)), name::set(Some(name.to_string()))), + ((is_dir::NAME, json!(*is_dir)), is_dir::set(Some(*is_dir))), + ( + (extension::NAME, json!(extension)), + extension::set(Some(extension.to_string())), + ), + ( + ( + size_in_bytes::NAME, + json!(entry.metadata.size_in_bytes.to_string()), + ), + size_in_bytes::set(Some(entry.metadata.size_in_bytes.to_string())), + ), + ( + (inode::NAME, json!(entry.metadata.inode.to_le_bytes())), + inode::set(Some(entry.metadata.inode.to_le_bytes().into())), + ), + ( + (device::NAME, json!(entry.metadata.device.to_le_bytes())), + device::set(Some(entry.metadata.device.to_le_bytes().into())), + ), + ( + (date_created::NAME, json!(entry.metadata.created_at)), + date_created::set(Some(entry.metadata.created_at.into())), + ), + ( + (date_modified::NAME, json!(entry.metadata.modified_at)), + date_modified::set(Some(entry.metadata.modified_at.into())), + ), + ] + .into_iter() + .unzip(); + ( sync.unique_shared_create( sync::file_path::SyncId { pub_id: uuid_to_bytes(entry.pub_id), }, - [ - (materialized_path::NAME, json!(materialized_path)), - (name::NAME, json!(name)), - (is_dir::NAME, json!(*is_dir)), - (extension::NAME, json!(extension)), - ( - size_in_bytes::NAME, - json!(entry.metadata.size_in_bytes.to_string()), - ), - (inode::NAME, json!(entry.metadata.inode.to_le_bytes())), - (device::NAME, json!(entry.metadata.device.to_le_bytes())), - (date_created::NAME, json!(entry.metadata.created_at)), - (date_modified::NAME, json!(entry.metadata.modified_at)), - ], - ), - file_path::create_unchecked( - uuid_to_bytes(entry.pub_id), - location.id, - materialized_path.to_string(), - name.to_string(), - extension.to_string(), - entry.metadata.inode.to_le_bytes().into(), - entry.metadata.device.to_le_bytes().into(), - vec![ - is_dir::set(*is_dir), - size_in_bytes::set(entry.metadata.size_in_bytes.to_string()), - date_created::set(entry.metadata.created_at.into()), - date_modified::set(entry.metadata.modified_at.into()), - ], + sync_params, ), + file_path::create_unchecked(pub_id, db_params), ) }) .unzip(); @@ -298,12 +321,12 @@ macro_rules! to_remove_db_fetcher_fn { iso_file_path; $db.file_path() .find_many(vec![ - $crate::prisma::file_path::location_id::equals($location_id), - $crate::prisma::file_path::materialized_path::equals( + $crate::prisma::file_path::location_id::equals(Some($location_id)), + $crate::prisma::file_path::materialized_path::equals(Some( iso_file_path .materialized_path_for_children() .expect("the received isolated file path must be from a directory"), - ), + )), ::prisma_client_rust::operator::not( unique_location_id_materialized_path_name_extension_params, ), diff --git a/core/src/location/indexer/rules.rs b/core/src/location/indexer/rules.rs index 19f336716..c3c71e14a 100644 --- a/core/src/location/indexer/rules.rs +++ b/core/src/location/indexer/rules.rs @@ -454,6 +454,18 @@ pub struct IndexerRule { } impl IndexerRule { + #[cfg(test)] + pub fn new(name: String, default: bool, rules: Vec) -> Self { + Self { + id: None, + name, + default, + rules, + date_created: Utc::now(), + date_modified: Utc::now(), + } + } + pub async fn apply( &self, source: impl AsRef, diff --git a/core/src/location/indexer/walk.rs b/core/src/location/indexer/walk.rs index e27a66a85..b500bc44f 100644 --- a/core/src/location/indexer/walk.rs +++ b/core/src/location/indexer/walk.rs @@ -305,7 +305,7 @@ where .map(move |file_paths| { let isolated_paths_already_in_db = file_paths .into_iter() - .map(IsolatedFilePathData::from) + .flat_map(IsolatedFilePathData::try_from) .collect::>(); indexed_paths.into_iter().filter_map(move |entry| { diff --git a/core/src/location/manager/helpers.rs b/core/src/location/manager/helpers.rs index 392330823..cb6262e89 100644 --- a/core/src/location/manager/helpers.rs +++ b/core/src/location/manager/helpers.rs @@ -1,4 +1,4 @@ -use crate::{library::Library, prisma::location}; +use crate::{library::Library, prisma::location, util::db::maybe_missing}; use std::{ collections::{HashMap, HashSet}, @@ -23,10 +23,7 @@ pub(super) async fn check_online( ) -> Result { let pub_id = Uuid::from_slice(&location.pub_id)?; - let location_path = location.path.as_ref(); - let Some(location_path) = location_path.map(Path::new) else { - return Err(LocationManagerError::MissingPath(location.id)) - }; + let location_path = maybe_missing(&location.path, "location.path").map(Path::new)?; if location.node_id == Some(library.node_local_id) { match fs::metadata(&location_path).await { diff --git a/core/src/location/manager/mod.rs b/core/src/location/manager/mod.rs index 2edb3b98d..e282104cd 100644 --- a/core/src/location/manager/mod.rs +++ b/core/src/location/manager/mod.rs @@ -1,4 +1,9 @@ -use crate::{job::JobManagerError, library::Library, prisma::location, util::error::FileIOError}; +use crate::{ + job::JobManagerError, + library::Library, + prisma::location, + util::{db::MissingFieldError, error::FileIOError}, +}; use std::{ collections::BTreeSet, @@ -102,8 +107,8 @@ pub enum LocationManagerError { CorruptedLocationPubId(#[from] uuid::Error), #[error("Job Manager error: (error: {0})")] JobManager(#[from] JobManagerError), - #[error("location missing location path: ")] - MissingPath(location::id::Type), + #[error("missing-field")] + MissingField(#[from] MissingFieldError), #[error("invalid inode")] InvalidInode, diff --git a/core/src/location/manager/watcher/mod.rs b/core/src/location/manager/watcher/mod.rs index ae4ba89a5..35e26cfd5 100644 --- a/core/src/location/manager/watcher/mod.rs +++ b/core/src/location/manager/watcher/mod.rs @@ -1,4 +1,4 @@ -use crate::{library::Library, prisma::location}; +use crate::{library::Library, prisma::location, util::db::maybe_missing}; use std::{ collections::HashSet, @@ -106,13 +106,9 @@ impl LocationWatcher { stop_rx, )); - let Some(path) = location.path else { - return Err(LocationManagerError::MissingPath(location.id)) - }; - Ok(Self { id: location.id, - path, + path: maybe_missing(location.path, "location.path")?, watcher, ignore_path_tx, handle: Some(handle), diff --git a/core/src/location/manager/watcher/utils.rs b/core/src/location/manager/watcher/utils.rs index 2efa352bd..389c336d6 100644 --- a/core/src/location/manager/watcher/utils.rs +++ b/core/src/location/manager/watcher/utils.rs @@ -16,13 +16,12 @@ use crate::{ }, object::{ file_identifier::FileMetadata, - object_just_id_has_thumbnail, preview::{can_generate_thumbnail_for_image, generate_image_thumbnail, get_thumbnail_path}, validation::hash::file_checksum, }, prisma::{file_path, location, object}, sync, - util::error::FileIOError, + util::{db::maybe_missing, error::FileIOError}, }; #[cfg(target_family = "unix")] @@ -75,9 +74,7 @@ pub(super) async fn create_dir( let path = path.as_ref(); - let Some(location_path) = &location.path else { - return Err(LocationManagerError::MissingPath(location_id)) - }; + let location_path = maybe_missing(&location.path, "location.path")?; trace!( "Location: creating directory: {}", @@ -109,7 +106,9 @@ pub(super) async fn create_dir( return Ok(()); }; - let created_path = create_file_path( + let materialized_path = iso_file_path.materialized_path().to_string(); + + create_file_path( library, iso_file_path, None, @@ -123,10 +122,10 @@ pub(super) async fn create_dir( ) .await?; - info!("Created path: {}", created_path.materialized_path); + info!("Created path: {}", &materialized_path); // scan the new directory - scan_location_sub_path(library, location, &created_path.materialized_path).await?; + scan_location_sub_path(library, location, &materialized_path).await?; invalidate_query!(library, "search.paths"); @@ -151,6 +150,8 @@ pub(super) async fn create_file( let db = &library.db; let iso_file_path = IsolatedFilePathData::new(location_id, &location_path, path, false)?; + let materialized_path = iso_file_path.materialized_path().to_string(); + let extension = iso_file_path.extension().to_string(); let (inode, device) = { #[cfg(target_family = "unix")] @@ -179,9 +180,7 @@ pub(super) async fn create_file( cas_id, kind, fs_metadata, - } = FileMetadata::new(&location_path, &iso_file_path) - .await - .map_err(|e| FileIOError::from((location_path.join(&iso_file_path), e)))?; + } = FileMetadata::new(&location_path, &iso_file_path).await?; let created_file = create_file_path( library, @@ -197,7 +196,9 @@ pub(super) async fn create_file( ) .await?; - info!("Created path: {}", created_file.materialized_path); + info!("Created path: {}", &materialized_path); + + object::select!(object_just_id { id }); let existing_object = db .object() @@ -205,7 +206,7 @@ pub(super) async fn create_file( file_path::cas_id::equals(Some(cas_id.clone())), file_path::pub_id::not(created_file.pub_id.clone()), ])]) - .select(object_just_id_has_thumbnail::select()) + .select(object_just_id::select()) .exec() .await?; @@ -216,13 +217,13 @@ pub(super) async fn create_file( .create( Uuid::new_v4().as_bytes().to_vec(), vec![ - object::date_created::set( + object::date_created::set(Some( DateTime::::from(fs_metadata.created_or_now()).into(), - ), - object::kind::set(kind as i32), + )), + object::kind::set(Some(kind as i32)), ], ) - .select(object_just_id_has_thumbnail::select()) + .select(object_just_id::select()) .exec() .await? }; @@ -235,12 +236,13 @@ pub(super) async fn create_file( .exec() .await?; - if !object.has_thumbnail && !created_file.extension.is_empty() { + if !extension.is_empty() { // Running in a detached task as thumbnail generation can take a while and we don't want to block the watcher let path = path.to_path_buf(); let library = library.clone(); + tokio::spawn(async move { - generate_thumbnail(&created_file.extension, &cas_id, path, &library).await; + generate_thumbnail(&extension, &cas_id, path, &library).await; }); } @@ -344,9 +346,7 @@ async fn inner_update_file( .await? .ok_or_else(|| LocationManagerError::MissingLocation(location_id))?; - let Some(location_path) = location.path.map(PathBuf::from) else { - return Err(LocationManagerError::MissingPath(location_id)) - }; + let location_path = maybe_missing(location.path.map(PathBuf::from), "location.path")?; trace!( "Location: updating file: {}", @@ -354,15 +354,13 @@ async fn inner_update_file( full_path.display() ); - let iso_file_path = IsolatedFilePathData::from(file_path); + let iso_file_path = IsolatedFilePathData::try_from(file_path)?; let FileMetadata { cas_id, fs_metadata, kind, - } = FileMetadata::new(&location_path, &iso_file_path) - .await - .map_err(|e| FileIOError::from((location_path.join(&iso_file_path), e)))?; + } = FileMetadata::new(&location_path, &iso_file_path).await?; if let Some(old_cas_id) = &file_path.cas_id { if old_cas_id != &cas_id { @@ -376,12 +374,15 @@ async fn inner_update_file( ), ( (size_in_bytes::NAME, json!(fs_metadata.len().to_string())), - size_in_bytes::set(fs_metadata.len().to_string()), + size_in_bytes::set(Some(fs_metadata.len().to_string())), ), { let date = DateTime::::from(fs_metadata.modified_or_now()).into(); - ((date_modified::NAME, json!(date)), date_modified::set(date)) + ( + (date_modified::NAME, json!(date)), + date_modified::set(Some(date)), + ) }, { // TODO: Should this be a skip rather than a null-set? @@ -432,19 +433,21 @@ async fn inner_update_file( if let Some(ref object) = file_path.object { // if this file had a thumbnail previously, we update it to match the new content - if library.thumbnail_exists(old_cas_id).await? && !file_path.extension.is_empty() { - generate_thumbnail(&file_path.extension, &cas_id, full_path, library).await; + if library.thumbnail_exists(old_cas_id).await? { + if let Some(ext) = &file_path.extension { + generate_thumbnail(ext, &cas_id, full_path, library).await; - // remove the old thumbnail as we're generating a new one - let thumb_path = get_thumbnail_path(library, old_cas_id); - fs::remove_file(&thumb_path) - .await - .map_err(|e| FileIOError::from((thumb_path, e)))?; + // remove the old thumbnail as we're generating a new one + let thumb_path = get_thumbnail_path(library, old_cas_id); + fs::remove_file(&thumb_path) + .await + .map_err(|e| FileIOError::from((thumb_path, e)))?; + } } let int_kind = kind as i32; - if object.kind != int_kind { + if object.kind.map(|k| k != int_kind).unwrap_or_default() { sync.write_op( db, sync.shared_update( @@ -456,7 +459,7 @@ async fn inner_update_file( ), db.object().update( object::id::equals(object.id), - vec![object::kind::set(int_kind)], + vec![object::kind::set(Some(int_kind))], ), ) .await?; @@ -509,13 +512,13 @@ pub(super) async fn rename( .exec() .await? { - let new = - IsolatedFilePathData::new(location_id, &location_path, new_path, file_path.is_dir)?; + let is_dir = maybe_missing(file_path.is_dir, "file_path.is_dir")?; + + let new = IsolatedFilePathData::new(location_id, &location_path, new_path, is_dir)?; // If the renamed path is a directory, we have to update every successor - if file_path.is_dir { - let old = - IsolatedFilePathData::new(location_id, &location_path, old_path, file_path.is_dir)?; + if is_dir { + let old = IsolatedFilePathData::new(location_id, &location_path, old_path, is_dir)?; // TODO: Fetch all file_paths that will be updated and dispatch sync events let updated = library @@ -524,8 +527,8 @@ pub(super) async fn rename( "UPDATE file_path \ SET materialized_path = REPLACE(materialized_path, {}, {}) \ WHERE location_id = {}", - PrismaValue::String(format!("{}/{}/", old.materialized_path, old.name)), - PrismaValue::String(format!("{}/{}/", new.materialized_path, new.name)), + PrismaValue::String(format!("{}/{}/", old.materialized_path(), old.name())), + PrismaValue::String(format!("{}/{}/", new.materialized_path(), new.name())), PrismaValue::Int(location_id as i64) )) .exec() @@ -539,9 +542,9 @@ pub(super) async fn rename( .update( file_path::pub_id::equals(file_path.pub_id), vec![ - file_path::materialized_path::set(new_path_materialized_str), - file_path::name::set(new.name.to_string()), - file_path::extension::set(new.extension.to_string()), + file_path::materialized_path::set(Some(new_path_materialized_str)), + file_path::name::set(Some(new.name().to_string())), + file_path::extension::set(Some(new.extension().to_string())), ], ) .exec() @@ -589,14 +592,16 @@ pub(super) async fn remove_by_file_path( Err(e) if e.kind() == ErrorKind::NotFound => { let db = &library.db; + let is_dir = maybe_missing(file_path.is_dir, "file_path.is_dir")?; + // if is doesn't, we can remove it safely from our db - if file_path.is_dir { - delete_directory( - library, - location_id, - Some(file_path.materialized_path.clone()), - ) - .await?; + if is_dir { + let materialized_path = maybe_missing( + file_path.materialized_path.clone(), + "file_path.materialized_path", + )?; + + delete_directory(library, location_id, Some(materialized_path)).await?; } else { db.file_path() .delete(file_path::pub_id::equals(file_path.pub_id.clone())) @@ -684,9 +689,7 @@ pub(super) async fn extract_inode_and_device_from_path( .await? .ok_or(LocationManagerError::MissingLocation(location_id))?; - let Some(location_path) = &location.path else { - return Err(LocationManagerError::MissingPath(location_id)) - }; + let location_path = maybe_missing(&location.path, "location.path")?; library .db @@ -702,12 +705,12 @@ pub(super) async fn extract_inode_and_device_from_path( |file_path| { Ok(( u64::from_le_bytes( - file_path.inode[0..8] + maybe_missing(file_path.inode, "file_path.inode")?[0..8] .try_into() .map_err(|_| LocationManagerError::InvalidInode)?, ), u64::from_le_bytes( - file_path.device[0..8] + maybe_missing(file_path.device, "file_path.device")?[0..8] .try_into() .map_err(|_| LocationManagerError::InvalidDevice)?, ), @@ -727,11 +730,6 @@ pub(super) async fn extract_location_path( .map_or( Err(LocationManagerError::MissingLocation(location_id)), // NOTE: The following usage of `PathBuf` doesn't incur a new allocation so it's fine - |location| { - location - .path - .map(PathBuf::from) - .ok_or(LocationManagerError::MissingPath(location_id)) - }, + |location| Ok(maybe_missing(location.path, "location.path")?.into()), ) } diff --git a/core/src/location/mod.rs b/core/src/location/mod.rs index 1cae4a124..9c4edbd27 100644 --- a/core/src/location/mod.rs +++ b/core/src/location/mod.rs @@ -8,7 +8,10 @@ use crate::{ }, prisma::{file_path, indexer_rules_in_location, location, node, object, PrismaClient}, sync, - util::{db::uuid_to_bytes, error::FileIOError}, + util::{ + db::{chain_optional_iter, uuid_to_bytes}, + error::FileIOError, + }, }; use std::{ @@ -672,14 +675,10 @@ pub async fn delete_directory( ) -> Result<(), QueryError> { let Library { db, .. } = library; - let children_params = if let Some(parent_materialized_path) = parent_materialized_path { - vec![ - file_path::location_id::equals(location_id), - file_path::materialized_path::starts_with(parent_materialized_path), - ] - } else { - vec![file_path::location_id::equals(location_id)] - }; + let children_params = chain_optional_iter( + [file_path::location_id::equals(Some(location_id))], + [parent_materialized_path.map(file_path::materialized_path::starts_with)], + ); // Fetching all object_ids from all children file_paths let object_ids = db diff --git a/core/src/object/file_identifier/file_identifier_job.rs b/core/src/object/file_identifier/file_identifier_job.rs index 16a9a946a..590d14738 100644 --- a/core/src/object/file_identifier/file_identifier_job.rs +++ b/core/src/object/file_identifier/file_identifier_job.rs @@ -9,7 +9,7 @@ use crate::{ file_path_for_file_identifier, IsolatedFilePathData, }, prisma::{file_path, location, PrismaClient, SortOrder}, - util::db::chain_optional_iter, + util::db::{chain_optional_iter, maybe_missing}, }; use std::{ @@ -76,10 +76,8 @@ impl StatefulJob for FileIdentifierJob { let location_id = state.init.location.id; - let location_path = state.init.location.path.as_ref(); - let Some(location_path) = location_path.map(Path::new) else { - return Err(JobError::MissingPath) - }; + let location_path = + maybe_missing(&state.init.location.path, "location.path").map(Path::new)?; let maybe_sub_iso_file_path = if let Some(ref sub_path) = state.init.sub_path { let full_path = ensure_sub_path_is_in_location(location_path, sub_path) @@ -234,8 +232,8 @@ fn orphan_path_filters( chain_optional_iter( [ file_path::object_id::equals(None), - file_path::is_dir::equals(false), - file_path::location_id::equals(location_id), + file_path::is_dir::equals(Some(false)), + file_path::location_id::equals(Some(location_id)), ], [ // this is a workaround for the cursor not working properly diff --git a/core/src/object/file_identifier/mod.rs b/core/src/object/file_identifier/mod.rs index 111c0b3a8..e01e9e42b 100644 --- a/core/src/object/file_identifier/mod.rs +++ b/core/src/object/file_identifier/mod.rs @@ -8,7 +8,10 @@ use crate::{ prisma::{file_path, location, object, PrismaClient}, sync, sync::SyncManager, - util::db::uuid_to_bytes, + util::{ + db::{maybe_missing, uuid_to_bytes}, + error::FileIOError, + }, }; use sd_file_ext::{extensions::Extension, kind::ObjectKind}; @@ -23,7 +26,7 @@ use futures::future::join_all; use serde::{Deserialize, Serialize}; use serde_json::json; use thiserror::Error; -use tokio::{fs, io}; +use tokio::fs; use tracing::{error, info}; use uuid::Uuid; @@ -59,10 +62,12 @@ impl FileMetadata { pub async fn new( location_path: impl AsRef, iso_file_path: &IsolatedFilePathData<'_>, // TODO: use dedicated CreateUnchecked type - ) -> Result { + ) -> Result { let path = location_path.as_ref().join(iso_file_path); - let fs_metadata = fs::metadata(&path).await?; + let fs_metadata = fs::metadata(&path) + .await + .map_err(|e| FileIOError::from((&path, e)))?; assert!( !fs_metadata.is_dir(), @@ -75,7 +80,9 @@ impl FileMetadata { .map(Into::into) .unwrap_or(ObjectKind::Unknown); - let cas_id = generate_cas_id(&path, fs_metadata.len()).await?; + let cas_id = generate_cas_id(&path, fs_metadata.len()) + .await + .map_err(|e| FileIOError::from((&path, e)))?; info!("Analyzed file: {path:?} {cas_id:?} {kind:?}"); @@ -101,25 +108,21 @@ async fn identifier_job_step( location: &location::Data, file_paths: &[file_path_for_file_identifier::Data], ) -> Result<(usize, usize), JobError> { - let location_path = location.path.as_ref(); - let Some(location_path) = location_path.map(Path::new) else { - return Err(JobError::MissingPath) - }; + let location_path = maybe_missing(&location.path, "location.path").map(Path::new)?; let file_path_metas = join_all(file_paths.iter().map(|file_path| async move { // NOTE: `file_path`'s `materialized_path` begins with a `/` character so we remove it to join it with `location.path` - FileMetadata::new( + let meta = FileMetadata::new( &location_path, - &IsolatedFilePathData::from((location.id, file_path)), + &IsolatedFilePathData::try_from((location.id, file_path))?, ) - .await - .map(|params| { - ( - // SAFETY: This should never happen - Uuid::from_slice(&file_path.pub_id).expect("file_path.pub_id is invalid!"), - (params, file_path), - ) - }) + .await?; + + Ok(( + // SAFETY: This should never happen + Uuid::from_slice(&file_path.pub_id).expect("file_path.pub_id is invalid!"), + (meta, file_path), + )) as Result<_, JobError> })) .await .into_iter() @@ -130,7 +133,7 @@ async fn identifier_job_step( data }) - .collect::>(); + .collect::>(); let unique_cas_ids = file_path_metas .values() @@ -261,7 +264,7 @@ async fn identifier_job_step( uuid_to_bytes(object_pub_id), vec![ object::date_created::set(fp.date_created), - object::kind::set(kind), + object::kind::set(Some(kind)), ], ), ); diff --git a/core/src/object/file_identifier/shallow.rs b/core/src/object/file_identifier/shallow.rs index 3432c3e88..3d4547f75 100644 --- a/core/src/object/file_identifier/shallow.rs +++ b/core/src/object/file_identifier/shallow.rs @@ -7,7 +7,7 @@ use crate::{ file_path_for_file_identifier, IsolatedFilePathData, }, prisma::{file_path, location, PrismaClient, SortOrder}, - util::db::chain_optional_iter, + util::db::{chain_optional_iter, maybe_missing}, }; use std::path::{Path, PathBuf}; @@ -33,10 +33,7 @@ pub async fn shallow( info!("Identifying orphan File Paths..."); let location_id = location.id; - let location_path = location.path.as_ref(); - let Some(location_path) = location_path.map(Path::new) else { - return Err(JobError::MissingPath) - }; + let location_path = maybe_missing(&location.path, "location.path").map(Path::new)?; let sub_iso_file_path = if sub_path != Path::new("") { let full_path = ensure_sub_path_is_in_location(location_path, &sub_path) @@ -125,13 +122,13 @@ fn orphan_path_filters( chain_optional_iter( [ file_path::object_id::equals(None), - file_path::is_dir::equals(false), - file_path::location_id::equals(location_id), - file_path::materialized_path::equals( + file_path::is_dir::equals(Some(false)), + file_path::location_id::equals(Some(location_id)), + file_path::materialized_path::equals(Some( sub_iso_file_path .materialized_path_for_children() .expect("sub path for shallow identifier must be a directory"), - ), + )), ], [file_path_id.map(file_path::id::gte)], ) diff --git a/core/src/object/fs/copy.rs b/core/src/object/fs/copy.rs index e387b3de1..99eb06426 100644 --- a/core/src/object/fs/copy.rs +++ b/core/src/object/fs/copy.rs @@ -6,7 +6,10 @@ use crate::{ library::Library, location::file_path_helper::IsolatedFilePathData, prisma::{file_path, location}, - util::error::FileIOError, + util::{ + db::{maybe_missing, MissingFieldError}, + error::FileIOError, + }, }; use std::{hash::Hash, path::PathBuf}; @@ -77,7 +80,7 @@ impl StatefulJob for FileCopierJob { ) .await? .into_iter() - .map(|file_data| { + .flat_map(|file_data| { // add the currently viewed subdirectory to the location root let mut full_target_path = targets_location_path.join(&state.init.target_location_relative_directory_path); @@ -85,11 +88,12 @@ impl StatefulJob for FileCopierJob { full_target_path.push(construct_target_filename( &file_data, &state.init.target_file_name_suffix, - )); - FileCopierJobStep { + )?); + + Ok::<_, MissingFieldError>(FileCopierJobStep { source_file_data: file_data, target_full_path: full_target_path, - } + }) }) .collect(); @@ -114,7 +118,7 @@ impl StatefulJob for FileCopierJob { let data = extract_job_data!(state); - if source_file_data.file_path.is_dir { + if maybe_missing(source_file_data.file_path.is_dir, "file_path.is_dir")? { fs::create_dir_all(target_full_path) .await .map_err(|e| FileIOError::from((target_full_path, e)))?; diff --git a/core/src/object/fs/cut.rs b/core/src/object/fs/cut.rs index 2fbf71380..f9b389923 100644 --- a/core/src/object/fs/cut.rs +++ b/core/src/object/fs/cut.rs @@ -90,7 +90,7 @@ impl StatefulJob for FileCutterJob { let full_output = data .full_target_directory_path - .join(construct_target_filename(step, &None)); + .join(construct_target_filename(step, &None)?); if step.full_path.parent().ok_or(JobError::Path)? == full_output.parent().ok_or(JobError::Path)? diff --git a/core/src/object/fs/delete.rs b/core/src/object/fs/delete.rs index b00f87c3b..747844c41 100644 --- a/core/src/object/fs/delete.rs +++ b/core/src/object/fs/delete.rs @@ -5,7 +5,7 @@ use crate::{ }, library::Library, prisma::{file_path, location}, - util::error::FileIOError, + util::{db::maybe_missing, error::FileIOError}, }; use std::hash::Hash; @@ -64,7 +64,10 @@ impl StatefulJob for FileDeleterJob { ) -> Result<(), JobError> { let step = &state.steps[0]; - if step.file_path.is_dir { + // need to handle stuff such as querying prisma for all paths of a file, and deleting all of those if requested (with a checkbox in the ui) + // maybe a files.countOccurances/and or files.getPath(location_id, path_id) to show how many of these files would be deleted (and where?) + + if maybe_missing(step.file_path.is_dir, "file_path.is_dir")? { fs::remove_dir_all(&step.full_path).await } else { fs::remove_file(&step.full_path).await diff --git a/core/src/object/fs/erase.rs b/core/src/object/fs/erase.rs index ef0d69314..6257cb564 100644 --- a/core/src/object/fs/erase.rs +++ b/core/src/object/fs/erase.rs @@ -6,7 +6,7 @@ use crate::{ library::Library, location::file_path_helper::IsolatedFilePathData, prisma::{file_path, location}, - util::error::FileIOError, + util::{db::maybe_missing, error::FileIOError}, }; use std::{hash::Hash, path::PathBuf}; @@ -90,7 +90,7 @@ impl StatefulJob for FileEraserJob { let step = &state.steps[0]; // Had to use `state.steps[0]` all over the place to appease the borrow checker - if step.file_path.is_dir { + if maybe_missing(step.file_path.is_dir, "file_path.is_dir")? { let data = extract_job_data_mut!(state); let mut dir = tokio::fs::read_dir(&step.full_path) diff --git a/core/src/object/fs/error.rs b/core/src/object/fs/error.rs index f0e01cd40..572182953 100644 --- a/core/src/object/fs/error.rs +++ b/core/src/object/fs/error.rs @@ -1,7 +1,7 @@ use crate::{ location::{file_path_helper::FilePathError, LocationError}, prisma::file_path, - util::error::FileIOError, + util::{db::MissingFieldError, error::FileIOError}, }; use std::path::Path; @@ -28,4 +28,6 @@ pub enum FileSystemJobsError { MatchingSrcDest(Box), #[error("action would overwrite another file: {}", .0.display())] WouldOverwrite(Box), + #[error("missing-field: {0}")] + MissingField(#[from] MissingFieldError), } diff --git a/core/src/object/fs/mod.rs b/core/src/object/fs/mod.rs index b192f35f1..cd8daee06 100644 --- a/core/src/object/fs/mod.rs +++ b/core/src/object/fs/mod.rs @@ -4,6 +4,7 @@ use crate::{ LocationError, }, prisma::{file_path, location, PrismaClient}, + util::db::{maybe_missing, MissingFieldError}, }; use std::path::{Path, PathBuf}; @@ -42,14 +43,16 @@ pub async fn get_location_path_from_location_id( db: &PrismaClient, location_id: file_path::id::Type, ) -> Result { - db.location() + let location = db + .location() .find_unique(location::id::equals(location_id)) .exec() .await? - .and_then(|location| location.path.map(PathBuf::from)) .ok_or(FileSystemJobsError::Location(LocationError::IdNotFound( location_id, - ))) + )))?; + + Ok(maybe_missing(location.path, "location.path")?.into()) } pub async fn get_many_files_datas( @@ -77,9 +80,11 @@ pub async fn get_many_files_datas( .map(|(maybe_file_path, file_path_id)| { maybe_file_path .ok_or(FileSystemJobsError::FilePathIdNotFound(*file_path_id)) - .map(|path_data| FileData { - full_path: location_path.join(IsolatedFilePathData::from(&path_data)), - file_path: path_data, + .and_then(|path_data| { + Ok(FileData { + full_path: location_path.join(IsolatedFilePathData::try_from(&path_data)?), + file_path: path_data, + }) }) }) .collect::, _>>() @@ -102,11 +107,13 @@ pub async fn get_file_data_from_isolated_file_path( .into_boxed_path(), ) }) - .map(|path_data| FileData { - full_path: location_path - .as_ref() - .join(IsolatedFilePathData::from(&path_data)), - file_path: path_data, + .and_then(|path_data| { + Ok(FileData { + full_path: location_path + .as_ref() + .join(IsolatedFilePathData::try_from(&path_data)?), + file_path: path_data, + }) }) } @@ -125,52 +132,43 @@ pub async fn fetch_source_and_target_location_paths( .await? { (Some(source_location), Some(target_location)) => Ok(( - source_location - .path - .map(PathBuf::from) - .ok_or(FileSystemJobsError::Location(LocationError::MissingPath( - source_location_id, - )))?, - target_location - .path - .map(PathBuf::from) - .ok_or(FileSystemJobsError::Location(LocationError::MissingPath( - target_location_id, - )))?, + maybe_missing(source_location.path.map(PathBuf::from), "location.path")?, + maybe_missing(target_location.path.map(PathBuf::from), "location.path")?, )), - (None, _) => Err(FileSystemJobsError::Location(LocationError::IdNotFound( - source_location_id, - ))), - (_, None) => Err(FileSystemJobsError::Location(LocationError::IdNotFound( - target_location_id, - ))), + (None, _) => Err(LocationError::IdNotFound(source_location_id))?, + (_, None) => Err(LocationError::IdNotFound(target_location_id))?, } } fn construct_target_filename( source_file_data: &FileData, target_file_name_suffix: &Option, -) -> String { +) -> Result { // extension wizardry for cloning and such // if no suffix has been selected, just use the file name // if a suffix is provided and it's a directory, use the directory name + suffix // if a suffix is provided and it's a file, use the (file name + suffix).extension - if let Some(ref suffix) = target_file_name_suffix { - if source_file_data.file_path.is_dir { - format!("{}{suffix}", source_file_data.file_path.name) + Ok(if let Some(ref suffix) = target_file_name_suffix { + if maybe_missing(source_file_data.file_path.is_dir, "file_path.is_dir")? { + format!( + "{}{suffix}", + maybe_missing(&source_file_data.file_path.name, "file_path.name")? + ) } else { format!( "{}{suffix}.{}", - source_file_data.file_path.name, source_file_data.file_path.extension, + maybe_missing(&source_file_data.file_path.name, "file_path.name")?, + maybe_missing(&source_file_data.file_path.extension, "file_path.extension")?, ) } - } else if source_file_data.file_path.is_dir { - source_file_data.file_path.name.clone() + } else if *maybe_missing(&source_file_data.file_path.is_dir, "file_path.is_dir")? { + maybe_missing(&source_file_data.file_path.name, "file_path.name")?.clone() } else { format!( "{}.{}", - source_file_data.file_path.name, source_file_data.file_path.extension + maybe_missing(&source_file_data.file_path.name, "file_path.name")?, + maybe_missing(&source_file_data.file_path.extension, "file_path.extension")? ) - } + }) } diff --git a/core/src/object/mod.rs b/core/src/object/mod.rs index 8791abc88..524f39773 100644 --- a/core/src/object/mod.rs +++ b/core/src/object/mod.rs @@ -16,7 +16,6 @@ pub mod validation; // Objects are what can be added to Spaces // Object selectables! -object::select!(object_just_id_has_thumbnail { id has_thumbnail }); object::select!(object_for_file_identifier { pub_id file_paths: select { pub_id cas_id } diff --git a/core/src/object/preview/thumbnail/mod.rs b/core/src/object/preview/thumbnail/mod.rs index 7ea7a45db..2b16c5c2a 100644 --- a/core/src/object/preview/thumbnail/mod.rs +++ b/core/src/object/preview/thumbnail/mod.rs @@ -5,7 +5,7 @@ use crate::{ library::Library, location::file_path_helper::{file_path_for_thumbnailer, FilePathError, IsolatedFilePathData}, prisma::location, - util::{error::FileIOError, version_manager::VersionManagerError}, + util::{db::maybe_missing, error::FileIOError, version_manager::VersionManagerError}, }; use std::{ @@ -230,7 +230,10 @@ async fn process_step( ctx.progress(vec![JobReportUpdate::Message(format!( "Processing {}", - step.file_path.materialized_path + maybe_missing( + &step.file_path.materialized_path, + "file_path.materialized_path" + )? ))]); let data = state @@ -268,14 +271,14 @@ pub async fn inner_process_step( let thumbnail_dir = thumbnail_dir.as_ref(); // assemble the file path - let path = location_path.join(IsolatedFilePathData::from((location.id, file_path))); + let path = location_path.join(IsolatedFilePathData::try_from((location.id, file_path))?); trace!("image_file {:?}", file_path); // get cas_id, if none found skip let Some(cas_id) = &file_path.cas_id else { warn!( "skipping thumbnail generation for {}", - file_path.materialized_path + maybe_missing(&file_path.materialized_path, "file_path.materialized_path")? ); return Ok(()); diff --git a/core/src/object/preview/thumbnail/shallow.rs b/core/src/object/preview/thumbnail/shallow.rs index dfb5f79f1..bcec742a8 100644 --- a/core/src/object/preview/thumbnail/shallow.rs +++ b/core/src/object/preview/thumbnail/shallow.rs @@ -133,13 +133,13 @@ async fn get_files_by_extensions( Ok(db .file_path() .find_many(vec![ - file_path::location_id::equals(location_id), + file_path::location_id::equals(Some(location_id)), file_path::extension::in_vec(extensions.iter().map(ToString::to_string).collect()), - file_path::materialized_path::equals( + file_path::materialized_path::equals(Some( parent_isolated_file_path_data .materialized_path_for_children() .expect("sub path iso_file_path must be a directory"), - ), + )), ]) .select(file_path_for_thumbnailer::select()) .exec() diff --git a/core/src/object/preview/thumbnail/thumbnailer_job.rs b/core/src/object/preview/thumbnail/thumbnailer_job.rs index 24b0f240a..7edc921dd 100644 --- a/core/src/object/preview/thumbnail/thumbnailer_job.rs +++ b/core/src/object/preview/thumbnail/thumbnailer_job.rs @@ -175,7 +175,7 @@ async fn get_files_by_extensions( Ok(db .file_path() .find_many(vec![ - file_path::location_id::equals(iso_file_path.location_id()), + file_path::location_id::equals(Some(iso_file_path.location_id())), file_path::extension::in_vec(extensions.iter().map(ToString::to_string).collect()), file_path::materialized_path::starts_with( iso_file_path diff --git a/core/src/object/validation/validator_job.rs b/core/src/object/validation/validator_job.rs index b4789e77e..2aefb8bc2 100644 --- a/core/src/object/validation/validator_job.rs +++ b/core/src/object/validation/validator_job.rs @@ -7,7 +7,7 @@ use crate::{ location::file_path_helper::{file_path_for_object_validator, IsolatedFilePathData}, prisma::{file_path, location}, sync, - util::error::FileIOError, + util::{db::maybe_missing, error::FileIOError}, }; use std::path::PathBuf; @@ -60,8 +60,8 @@ impl StatefulJob for ObjectValidatorJob { state.steps.extend( db.file_path() .find_many(vec![ - file_path::location_id::equals(state.init.location_id), - file_path::is_dir::equals(false), + file_path::location_id::equals(Some(state.init.location_id)), + file_path::is_dir::equals(Some(false)), file_path::integrity_checksum::equals(None), ]) .select(file_path_for_object_validator::select()) @@ -94,10 +94,10 @@ impl StatefulJob for ObjectValidatorJob { // we can also compare old and new checksums here // This if is just to make sure, we already queried objects where integrity_checksum is null if file_path.integrity_checksum.is_none() { - let path = data.root_path.join(IsolatedFilePathData::from(( - file_path.location.id, + let path = data.root_path.join(IsolatedFilePathData::try_from(( + maybe_missing(&file_path.location, "file_path.location")?.id, file_path, - ))); + ))?); let checksum = file_checksum(&path) .await .map_err(|e| FileIOError::from((path, e)))?; diff --git a/core/src/sync/manager.rs b/core/src/sync/manager.rs index b71711294..78012d8cc 100644 --- a/core/src/sync/manager.rs +++ b/core/src/sync/manager.rs @@ -6,7 +6,7 @@ use std::{collections::HashMap, sync::Arc}; use sd_sync::*; -use serde_json::{from_value, json, to_vec, Value}; +use serde_json::{json, to_vec, Value}; use tokio::sync::broadcast::{self, Receiver, Sender}; use uhlc::{HLCBuilder, HLC, NTP64}; use uuid::Uuid; @@ -200,33 +200,10 @@ impl SyncManager { match ModelSyncData::from_op(op.typ.clone()).unwrap() { ModelSyncData::FilePath(id, shared_op) => match shared_op { - SharedOperationData::Create(SharedOperationCreateData::Unique(mut data)) => { + SharedOperationData::Create(SharedOperationCreateData::Unique(data)) => { db.file_path() .create( id.pub_id, - { - let val: std::collections::HashMap = - from_value(data.remove(file_path::location::NAME).unwrap()) - .unwrap(); - let val = val.into_iter().next().unwrap(); - - location::UniqueWhereParam::deserialize(&val.0, val.1).unwrap() - }, - serde_json::from_value( - data.remove(file_path::materialized_path::NAME).unwrap(), - ) - .unwrap(), - serde_json::from_value(data.remove(file_path::name::NAME).unwrap()) - .unwrap(), - serde_json::from_value( - data.remove(file_path::extension::NAME) - .unwrap_or_else(|| serde_json::Value::String("".to_string())), - ) - .unwrap(), - serde_json::from_value(data.remove(file_path::inode::NAME).unwrap()) - .unwrap(), - serde_json::from_value(data.remove(file_path::device::NAME).unwrap()) - .unwrap(), data.into_iter() .flat_map(|(k, v)| file_path::SetParam::deserialize(&k, v)) .collect(), diff --git a/core/src/util/db.rs b/core/src/util/db.rs index 4946c0ea7..b03d177df 100644 --- a/core/src/util/db.rs +++ b/core/src/util/db.rs @@ -75,3 +75,35 @@ pub fn chain_optional_iter( pub fn uuid_to_bytes(uuid: Uuid) -> Vec { uuid.as_bytes().to_vec() } + +#[derive(Error, Debug)] +#[error("Missing field {0}")] +pub struct MissingFieldError(&'static str); + +pub trait OptionalField: Sized { + type Out; + + fn transform(self) -> Option; +} + +impl OptionalField for Option { + type Out = T; + + fn transform(self) -> Option { + self + } +} +impl<'a, T> OptionalField for &'a Option { + type Out = &'a T; + + fn transform(self) -> Option { + self.as_ref() + } +} + +pub fn maybe_missing<'a, T: OptionalField>( + data: T, + field: &'static str, +) -> Result { + data.transform().ok_or(MissingFieldError(field)) +} diff --git a/interface/app/$libraryId/Explorer/File/RenameTextBox.tsx b/interface/app/$libraryId/Explorer/File/RenameTextBox.tsx index 95bcaa79a..821c0821a 100644 --- a/interface/app/$libraryId/Explorer/File/RenameTextBox.tsx +++ b/interface/app/$libraryId/Explorer/File/RenameTextBox.tsx @@ -55,7 +55,7 @@ export default ({ ? filePathData.name : filePathData.name + '.' + filePathData.extension; - if (newName !== oldName) { + if (oldName !== null && filePathData.location_id !== null && newName !== oldName) { renameFile.mutate({ location_id: filePathData.location_id, kind: { @@ -79,7 +79,7 @@ export default ({ if (!node) return; range.setStart(node, 0); - range.setEnd(node, filePathData?.name.length || 0); + range.setEnd(node, filePathData?.name?.length || 0); const sel = window.getSelection(); sel?.removeAllRanges(); diff --git a/interface/app/$libraryId/Explorer/File/Thumb.tsx b/interface/app/$libraryId/Explorer/File/Thumb.tsx index 40f39c06e..becf8496e 100644 --- a/interface/app/$libraryId/Explorer/File/Thumb.tsx +++ b/interface/app/$libraryId/Explorer/File/Thumb.tsx @@ -172,7 +172,7 @@ function FileThumb({ size, cover, ...props }: ThumbProps) { } break; default: - setSrc(getIcon(kind, isDark, extension, isDir)); + if (isDir !== null) setSrc(getIcon(kind, isDark, extension, isDir)); break; } }, [ diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index f63db6beb..60f9108a9 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -5,7 +5,7 @@ export type Procedures = { queries: { key: "buildInfo", input: never, result: BuildInfo } | { key: "categories.list", input: LibraryArgs, result: { [key in Category]: number } } | - { key: "files.get", input: LibraryArgs, result: { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; date_accessed: string | null; file_paths: FilePath[]; media_data: MediaData | null } | null } | + { key: "files.get", input: LibraryArgs, result: { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null; file_paths: FilePath[]; media_data: MediaData | null } | null } | { key: "invalidation.test-invalidate", input: never, result: number } | { key: "jobs.getHistory", input: LibraryArgs, result: JobReport[] } | { key: "jobs.getRunning", input: LibraryArgs, result: JobReport[] } | @@ -98,7 +98,7 @@ export type FileDeleterJobInit = { location_id: number; file_path_ids: number[] export type FileEraserJobInit = { location_id: number; file_path_ids: number[]; passes: string } -export type FilePath = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string } +export type FilePath = { id: number; pub_id: number[]; is_dir: boolean | null; cas_id: string | null; integrity_checksum: string | null; location_id: number | null; materialized_path: string | null; name: string | null; extension: string | null; size_in_bytes: string | null; inode: number[] | null; device: number[] | null; object_id: number | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null } export type FilePathFilterArgs = { locationId?: number | null; search?: string; extension?: string | null; createdAt?: OptionalRange; path?: string | null; object?: ObjectFilterArgs | null } @@ -106,7 +106,7 @@ export type FilePathSearchArgs = { take?: number | null; order?: FilePathSearchO export type FilePathSearchOrdering = { name: SortOrder } | { sizeInBytes: SortOrder } | { dateCreated: SortOrder } | { dateModified: SortOrder } | { dateIndexed: SortOrder } | { object: ObjectSearchOrdering } -export type FilePathWithObject = { id: number; pub_id: number[]; is_dir: boolean; cas_id: string | null; integrity_checksum: string | null; location_id: number; materialized_path: string; name: string; extension: string; size_in_bytes: string; inode: number[]; device: number[]; object_id: number | null; key_id: number | null; date_created: string; date_modified: string; date_indexed: string; object: Object | null } +export type FilePathWithObject = { id: number; pub_id: number[]; is_dir: boolean | null; cas_id: string | null; integrity_checksum: string | null; location_id: number | null; materialized_path: string | null; name: string | null; extension: string | null; size_in_bytes: string | null; inode: number[] | null; device: number[] | null; object_id: number | null; key_id: number | null; date_created: string | null; date_modified: string | null; date_indexed: string | null; object: Object | null } export type FromPattern = { pattern: string; replace_all: boolean } @@ -184,7 +184,7 @@ export type NodeConfig = { id: string; name: string; p2p_port: number | null; p2 export type NodeState = ({ id: string; name: string; p2p_port: number | null; p2p_email: string | null; p2p_img_url: string | null }) & { data_path: string } -export type Object = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; date_accessed: string | null } +export type Object = { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null } export type ObjectFilterArgs = { favorite?: boolean | null; hidden?: ObjectHiddenFilter; dateAccessed?: MaybeNot | null; kind?: number[]; tags?: number[] } @@ -196,7 +196,7 @@ export type ObjectSearchOrdering = { dateAccessed: SortOrder } export type ObjectValidatorArgs = { id: number; path: string } -export type ObjectWithFilePaths = { id: number; pub_id: number[]; kind: number; key_id: number | null; hidden: boolean; favorite: boolean; important: boolean; has_thumbnail: boolean; has_thumbstrip: boolean; has_video_preview: boolean; ipfs_id: string | null; note: string | null; date_created: string; date_accessed: string | null; file_paths: FilePath[] } +export type ObjectWithFilePaths = { id: number; pub_id: number[]; kind: number | null; key_id: number | null; hidden: boolean | null; favorite: boolean | null; important: boolean | null; note: string | null; date_created: string | null; date_accessed: string | null; file_paths: FilePath[] } /** * Represents the operating system which the remote peer is running.