mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-04-22 15:40:07 -04:00
* locations onboarding flow * optional * Backend for default location on library creation * Rust fmt * Enhancing error handling and introducing more resilience * Removing .spacedrive metadata on library deletion * Rust fmt again * default to videos --------- Co-authored-by: Ericson Fogo Soares <ericson.ds999@gmail.com>
297 lines
7.2 KiB
Rust
297 lines
7.2 KiB
Rust
use crate::{
|
|
library::{Library, LibraryConfig, LibraryName},
|
|
location::{scan_location, LocationCreateArgs},
|
|
util::MaybeUndefined,
|
|
volume::get_volumes,
|
|
Node,
|
|
};
|
|
|
|
use sd_p2p::spacetunnel::RemoteIdentity;
|
|
use sd_prisma::prisma::{indexer_rule, statistics};
|
|
|
|
use std::{convert::identity, sync::Arc};
|
|
|
|
use chrono::Utc;
|
|
use directories::UserDirs;
|
|
use futures_concurrency::future::Join;
|
|
use rspc::{alpha::AlphaRouter, ErrorCode};
|
|
use serde::{Deserialize, Serialize};
|
|
use specta::Type;
|
|
use tokio::spawn;
|
|
use tracing::{debug, error};
|
|
use uuid::Uuid;
|
|
|
|
use super::{
|
|
utils::{get_size, library},
|
|
Ctx, R,
|
|
};
|
|
|
|
// TODO(@Oscar): Replace with `specta::json`
|
|
#[derive(Serialize, Type)]
|
|
pub struct LibraryConfigWrapped {
|
|
pub uuid: Uuid,
|
|
pub instance_id: Uuid,
|
|
pub instance_public_key: RemoteIdentity,
|
|
pub config: LibraryConfig,
|
|
}
|
|
|
|
pub(crate) fn mount() -> AlphaRouter<Ctx> {
|
|
R.router()
|
|
.procedure("list", {
|
|
R.query(|node, _: ()| async move {
|
|
node.libraries
|
|
.get_all()
|
|
.await
|
|
.into_iter()
|
|
.map(|lib| LibraryConfigWrapped {
|
|
uuid: lib.id,
|
|
instance_id: lib.instance_uuid,
|
|
instance_public_key: lib.identity.to_remote_identity(),
|
|
config: lib.config(),
|
|
})
|
|
.collect::<Vec<_>>()
|
|
})
|
|
})
|
|
.procedure("statistics", {
|
|
R.with2(library())
|
|
.query(|(node, library), _: ()| async move {
|
|
// TODO: get from database if library is offline
|
|
// let _statistics = library
|
|
// .db
|
|
// .statistics()
|
|
// .find_unique(statistics::id::equals(library.node_local_id))
|
|
// .exec()
|
|
// .await?;
|
|
|
|
let volumes = get_volumes().await;
|
|
// save_volume(&library).await?;
|
|
|
|
let mut total_capacity: u64 = 0;
|
|
let mut available_capacity: u64 = 0;
|
|
for volume in volumes {
|
|
total_capacity += volume.total_capacity;
|
|
available_capacity += volume.available_capacity;
|
|
}
|
|
|
|
let library_db_size = get_size(
|
|
node.config
|
|
.data_directory()
|
|
.join("libraries")
|
|
.join(&format!("{}.db", library.id)),
|
|
)
|
|
.await
|
|
.unwrap_or(0);
|
|
|
|
let thumbnail_folder_size =
|
|
get_size(node.config.data_directory().join("thumbnails"))
|
|
.await
|
|
.unwrap_or(0);
|
|
|
|
use statistics::*;
|
|
let params = vec![
|
|
id::set(1), // Each library is a database so only one of these ever exists
|
|
date_captured::set(Utc::now().into()),
|
|
total_object_count::set(0),
|
|
library_db_size::set(library_db_size.to_string()),
|
|
total_bytes_used::set(0.to_string()),
|
|
total_bytes_capacity::set(total_capacity.to_string()),
|
|
total_unique_bytes::set(0.to_string()),
|
|
total_bytes_free::set(available_capacity.to_string()),
|
|
preview_media_bytes::set(thumbnail_folder_size.to_string()),
|
|
];
|
|
|
|
Ok(library
|
|
.db
|
|
.statistics()
|
|
.upsert(
|
|
statistics::id::equals(1), // Each library is a database so only one of these ever exists
|
|
statistics::create(params.clone()),
|
|
params,
|
|
)
|
|
.exec()
|
|
.await?)
|
|
})
|
|
})
|
|
.procedure("create", {
|
|
#[derive(Deserialize, Type, Default)]
|
|
pub struct DefaultLocations {
|
|
desktop: bool,
|
|
documents: bool,
|
|
downloads: bool,
|
|
pictures: bool,
|
|
music: bool,
|
|
videos: bool,
|
|
}
|
|
|
|
#[derive(Deserialize, Type)]
|
|
pub struct CreateLibraryArgs {
|
|
name: LibraryName,
|
|
#[specta(default)]
|
|
default_locations: DefaultLocations,
|
|
}
|
|
|
|
async fn create_default_locations_on_library_creation(
|
|
DefaultLocations {
|
|
desktop,
|
|
documents,
|
|
downloads,
|
|
pictures,
|
|
music,
|
|
videos,
|
|
}: DefaultLocations,
|
|
node: Arc<Node>,
|
|
library: Arc<Library>,
|
|
) -> Result<(), rspc::Error> {
|
|
// If all of them are false, we skip
|
|
if [!desktop, !documents, !downloads, !pictures, !music, !videos]
|
|
.into_iter()
|
|
.all(identity)
|
|
{
|
|
return Ok(());
|
|
}
|
|
|
|
let Some(default_locations_paths) = UserDirs::new() else {
|
|
return Err(rspc::Error::new(
|
|
ErrorCode::NotFound,
|
|
"Didn't find any system locations".to_string(),
|
|
));
|
|
};
|
|
|
|
let default_rules_ids = library
|
|
.db
|
|
.indexer_rule()
|
|
.find_many(vec![indexer_rule::default::equals(Some(true))])
|
|
.select(indexer_rule::select!({ id }))
|
|
.exec()
|
|
.await
|
|
.map_err(|e| {
|
|
rspc::Error::with_cause(
|
|
ErrorCode::InternalServerError,
|
|
"Failed to get default indexer rules for default locations".to_string(),
|
|
e,
|
|
)
|
|
})?
|
|
.into_iter()
|
|
.map(|rule| rule.id)
|
|
.collect::<Vec<_>>();
|
|
|
|
let mut maybe_error = None;
|
|
|
|
[
|
|
(desktop, default_locations_paths.desktop_dir()),
|
|
(documents, default_locations_paths.document_dir()),
|
|
(downloads, default_locations_paths.download_dir()),
|
|
(pictures, default_locations_paths.picture_dir()),
|
|
(music, default_locations_paths.audio_dir()),
|
|
(videos, default_locations_paths.video_dir()),
|
|
]
|
|
.into_iter()
|
|
.filter_map(|entry| {
|
|
if let (true, Some(path)) = entry {
|
|
let node = Arc::clone(&node);
|
|
let library = Arc::clone(&library);
|
|
let indexer_rules_ids = default_rules_ids.clone();
|
|
let path = path.to_path_buf();
|
|
Some(spawn(async move {
|
|
let Some(location) = LocationCreateArgs {
|
|
path,
|
|
dry_run: false,
|
|
indexer_rules_ids,
|
|
}
|
|
.create(&node, &library)
|
|
.await
|
|
.map_err(rspc::Error::from)?
|
|
else {
|
|
return Ok(());
|
|
};
|
|
|
|
scan_location(&node, &library, location)
|
|
.await
|
|
.map_err(rspc::Error::from)
|
|
}))
|
|
} else {
|
|
None
|
|
}
|
|
})
|
|
.collect::<Vec<_>>()
|
|
.join()
|
|
.await
|
|
.into_iter()
|
|
.map(|spawn_res| {
|
|
spawn_res
|
|
.map_err(|_| {
|
|
rspc::Error::new(
|
|
ErrorCode::InternalServerError,
|
|
"A task to create a default location failed".to_string(),
|
|
)
|
|
})
|
|
.and_then(identity)
|
|
})
|
|
.fold(&mut maybe_error, |maybe_error, res| {
|
|
if let Err(e) = res {
|
|
error!("Failed to create default location: {e:#?}");
|
|
*maybe_error = Some(e);
|
|
}
|
|
maybe_error
|
|
});
|
|
|
|
if let Some(e) = maybe_error {
|
|
return Err(e);
|
|
}
|
|
|
|
debug!("Created default locations");
|
|
|
|
Ok(())
|
|
}
|
|
|
|
R.mutation(
|
|
|node,
|
|
CreateLibraryArgs {
|
|
name,
|
|
default_locations,
|
|
}: CreateLibraryArgs| async move {
|
|
debug!("Creating library");
|
|
|
|
let library = node.libraries.create(name, None, &node).await?;
|
|
|
|
debug!("Created library {}", library.id);
|
|
|
|
create_default_locations_on_library_creation(
|
|
default_locations,
|
|
node,
|
|
Arc::clone(&library),
|
|
)
|
|
.await?;
|
|
|
|
Ok(LibraryConfigWrapped {
|
|
uuid: library.id,
|
|
instance_id: library.instance_uuid,
|
|
instance_public_key: library.identity.to_remote_identity(),
|
|
config: library.config(),
|
|
})
|
|
},
|
|
)
|
|
})
|
|
.procedure("edit", {
|
|
#[derive(Type, Deserialize)]
|
|
pub struct EditLibraryArgs {
|
|
pub id: Uuid,
|
|
pub name: Option<LibraryName>,
|
|
pub description: MaybeUndefined<String>,
|
|
}
|
|
|
|
R.mutation(|node, args: EditLibraryArgs| async move {
|
|
Ok(node
|
|
.libraries
|
|
.edit(args.id, args.name, args.description)
|
|
.await?)
|
|
})
|
|
})
|
|
.procedure(
|
|
"delete",
|
|
R.mutation(|node, id: Uuid| async move {
|
|
node.libraries.delete(&id).await.map_err(Into::into)
|
|
}),
|
|
)
|
|
}
|