diff --git a/Cargo.lock b/Cargo.lock index 12506b256..d1d12d3d7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4379,7 +4379,7 @@ dependencies = [ "iana-time-zone-haiku", "js-sys", "wasm-bindgen", - "windows-core 0.51.1", + "windows-core 0.52.0", ] [[package]] @@ -9144,7 +9144,7 @@ dependencies = [ [[package]] name = "sd-cloud-schema" version = "0.1.0" -source = "git+https://github.com/spacedriveapp/cloud-services-schema?branch=main#ff92c41e2b908732c58e77c26e0e3eec05f95549" +source = "git+https://github.com/spacedriveapp/cloud-services-schema?branch=main#dfbab0a7f19c1c3fafacb2053b1de628760d8dc8" dependencies = [ "argon2", "async-stream", diff --git a/core/src/api/cloud/libraries.rs b/core/src/api/cloud/libraries.rs new file mode 100644 index 000000000..609d24e53 --- /dev/null +++ b/core/src/api/cloud/libraries.rs @@ -0,0 +1,164 @@ +use crate::{api::utils::library, invalidate_query}; +use rspc::alpha::AlphaRouter; +use sd_cloud_schema::libraries; +use tracing::debug; +use uuid::Uuid; + +use crate::{ + api::{Ctx, R}, + try_get_cloud_services_client, +}; + +pub fn mount() -> AlphaRouter { + R.router() + .procedure("get", { + R.query(|node, req: libraries::get::Request| async move { + let libraries::get::Response(library) = super::handle_comm_error( + try_get_cloud_services_client!(node)? + .libraries() + .get(req) + .await, + "Failed to get library;", + )??; + + debug!(?library, "Got library"); + + Ok(library) + }) + }) + .procedure("list", { + R.query(|node, req: libraries::list::Request| async move { + let libraries::list::Response(libraries) = super::handle_comm_error( + try_get_cloud_services_client!(node)? + .libraries() + .list(req) + .await, + "Failed to list libraries;", + )??; + + debug!(?libraries, "Listed libraries"); + + Ok(libraries) + }) + }) + .procedure("create", { + R.with2(library()) + .mutation(|(node, library), _: ()| async move { + // let node_config = node.config.get().await; + // let cloud_library = sd_cloud_api::library::create( + // node.cloud_api_config().await, + // library.id, + // &library.config().await.name, + // library.instance_uuid, + // library.identity.to_remote_identity(), + // node_config.id, + // node_config.identity.to_remote_identity(), + // &node.p2p.peer_metadata(), + // ) + // .await?; + // node.libraries + // .edit( + // library.id, + // None, + // MaybeUndefined::Undefined, + // MaybeUndefined::Value(cloud_library.id), + // None, + // ) + // .await?; + + invalidate_query!(library, "cloud.library.get"); + // invalidate_query!(library, "cloud.library.get"); + debug!("TODO: Functionality not implemented"); + + Ok(()) + }) + }) + .procedure("join", { + R.mutation(|node, library_id: Uuid| async move { + // let Some(cloud_library) = + // sd_cloud_api::library::get(node.cloud_api_config().await, library_id).await? + // else { + // return Err(rspc::Error::new( + // rspc::ErrorCode::NotFound, + // "Library not found".to_string(), + // )); + // }; + + // let library = node + // .libraries + // .create_with_uuid( + // library_id, + // LibraryName::new(cloud_library.name).map_err(|e| { + // rspc::Error::new(rspc::ErrorCode::InternalServerError, e.to_string()) + // })?, + // None, + // false, + // None, + // &node, + // true, + // ) + // .await?; + // node.libraries + // .edit( + // library.id, + // None, + // MaybeUndefined::Undefined, + // MaybeUndefined::Value(cloud_library.id), + // None, + // ) + // .await?; + + // let node_config = node.config.get().await; + // let instances = sd_cloud_api::library::join( + // node.cloud_api_config().await, + // library_id, + // library.instance_uuid, + // library.identity.to_remote_identity(), + // node_config.id, + // node_config.identity.to_remote_identity(), + // node.p2p.peer_metadata(), + // ) + // .await?; + + // for instance in instances { + // crate::cloud::sync::receive::upsert_instance( + // library.id, + // &library.db, + // &library.sync, + // &node.libraries, + // &instance.uuid, + // instance.identity, + // &instance.node_id, + // RemoteIdentity::from_str(&instance.node_remote_identity) + // .expect("malformed remote identity in the DB"), + // instance.metadata, + // ) + // .await?; + // } + + // invalidate_query!(library, "cloud.library.get"); + // invalidate_query!(library, "cloud.library.list"); + + // Ok(LibraryConfigWrapped::from_library(&library).await) + + debug!("TODO: Functionality not implemented. Joining will be removed in the future, but for now, it's a no-op"); + + Ok(()) + }) + }) + .procedure("update", { + R.mutation(|node, req: libraries::update::Request| async move { + super::handle_comm_error( + try_get_cloud_services_client!(node)? + .libraries() + .update(req) + .await, + "Failed to update library;", + )??; + + debug!("Updated library"); + + Ok(()) + }) + }) +} diff --git a/core/src/api/cloud/library.rs b/core/src/api/cloud/library.rs index 4dc83ac94..ca110c397 100644 --- a/core/src/api/cloud/library.rs +++ b/core/src/api/cloud/library.rs @@ -1,3 +1,6 @@ +// This file is being deprecated in favor of libraries.rs +// This is due to the migration to the new API system, but the frontend is still using this file + use crate::{api::utils::library, invalidate_query}; use super::*; diff --git a/core/src/api/cloud/locations.rs b/core/src/api/cloud/locations.rs index f5ad209d6..a6c9c80cc 100644 --- a/core/src/api/cloud/locations.rs +++ b/core/src/api/cloud/locations.rs @@ -1,3 +1,6 @@ +// This file is being deprecated in favor of libraries.rs +// This is due to the migration to the new API system, but the frontend is still using this file + use crate::api::{Ctx, R}; use rspc::alpha::AlphaRouter; diff --git a/core/src/api/cloud/mod.rs b/core/src/api/cloud/mod.rs index 745a415c7..0b9ef5065 100644 --- a/core/src/api/cloud/mod.rs +++ b/core/src/api/cloud/mod.rs @@ -1,4 +1,4 @@ -use crate::{api::libraries::LibraryConfigWrapped, invalidate_query, library::LibraryName}; +// use crate::{api::libraries::LibraryConfigWrapped, invalidate_query, library::LibraryName}; use sd_cloud_schema::{auth, users}; @@ -11,6 +11,8 @@ use super::{Ctx, R}; mod devices; mod library; mod locations; +mod new_locations; +mod libraries; #[macro_export] macro_rules! try_get_cloud_services_client { @@ -29,7 +31,9 @@ macro_rules! try_get_cloud_services_client { pub(crate) fn mount() -> AlphaRouter { R.router() .merge("library.", library::mount()) + .merge("libraries.", libraries::mount()) .merge("locations.", locations::mount()) + .merge("new_locations.", new_locations::mount()) .merge("devices.", devices::mount()) .procedure("bootstrap", { R.mutation(|node, access_token: auth::AccessToken| async move { diff --git a/core/src/api/cloud/new_locations.rs b/core/src/api/cloud/new_locations.rs new file mode 100644 index 000000000..6ac9f5f37 --- /dev/null +++ b/core/src/api/cloud/new_locations.rs @@ -0,0 +1,89 @@ +use crate::{ + api::{Ctx, R}, + try_get_cloud_services_client, +}; + +use rspc::alpha::AlphaRouter; +use sd_cloud_schema::locations; +use tracing::debug; + +pub fn mount() -> AlphaRouter { + R.router() + .procedure("get", { + R.query(|node, req: locations::get::Request| async move { + let locations::get::Response(location) = super::handle_comm_error( + try_get_cloud_services_client!(node)? + .locations() + .get(req) + .await, + "Failed to get location;", + )??; + + debug!(?location, "Got location"); + + Ok(location) + }) + }) + .procedure("list", { + R.query(|node, req: locations::list::Request| async move { + let locations::list::Response(locations) = super::handle_comm_error( + try_get_cloud_services_client!(node)? + .locations() + .list(req) + .await, + "Failed to list locations;", + )??; + + debug!(?locations, "Listed locations"); + + Ok(locations) + }) + }) + .procedure("create", { + R.mutation(|node, req: locations::create::Request| async move { + super::handle_comm_error( + try_get_cloud_services_client!(node)? + .locations() + .create(req) + .await, + "Failed to create location;", + )??; + + debug!("Created location"); + + // Should we invalidate the location list cache here? + + Ok(()) + }) + }) + .procedure("delete", { + R.mutation(|node, req: locations::delete::Request| async move { + super::handle_comm_error( + try_get_cloud_services_client!(node)? + .locations() + .delete(req) + .await, + "Failed to delete location;", + )??; + + debug!("Deleted location"); + + Ok(()) + }) + }) + .procedure("update", { + R.mutation(|node, req: locations::update::Request| async move { + super::handle_comm_error( + try_get_cloud_services_client!(node)? + .locations() + .update(req) + .await, + "Failed to update location;", + )??; + + debug!("Updated location"); + + Ok(()) + }) + }) +} diff --git a/crates/cloud-api/src/lib.rs b/crates/cloud-api/src/lib.rs index a6d557dca..6880f4ab6 100644 --- a/crates/cloud-api/src/lib.rs +++ b/crates/cloud-api/src/lib.rs @@ -517,6 +517,7 @@ pub mod library { } #[derive(Type, Serialize, Deserialize)] +#[specta(rename = "Core_CloudLocation")] pub struct CloudLocation { id: String, name: String, diff --git a/interface/app/$libraryId/debug/cloud.tsx b/interface/app/$libraryId/debug/cloud.tsx index de06c1185..4f6a55527 100644 --- a/interface/app/$libraryId/debug/cloud.tsx +++ b/interface/app/$libraryId/debug/cloud.tsx @@ -2,8 +2,6 @@ import { CheckCircle, XCircle } from '@phosphor-icons/react'; import { Suspense, useMemo } from 'react'; import { auth, - CloudInstance, - CloudLibrary, HardwareModel, useBridgeQuery, useLibraryContext, @@ -51,16 +49,22 @@ export const Component = () => { // million-ignore function Authenticated() { const { library } = useLibraryContext(); - const cloudLibrary = useLibraryQuery(['cloud.library.get'], { suspense: true, retry: false }); - const cloudLibraryList = useBridgeQuery(['cloud.library.list'], { suspense: true, retry: false }); - console.log("[DEBUG] cloudLibraryList", cloudLibraryList); + const cloudLibrary: any = useLibraryQuery(['cloud.library.get'], { + suspense: true, + retry: false + }); + const cloudLibraryList = useBridgeQuery(['cloud.library.list'], { + suspense: true, + retry: false + }); + console.log('[DEBUG] cloudLibraryList', cloudLibraryList); const createLibrary = useLibraryMutation(['cloud.library.create']); const { t } = useLocale(); const thisInstance = useMemo(() => { if (!cloudLibrary.data) return undefined; return cloudLibrary.data.instances.find( - (instance) => instance.uuid === library.instance_id + (instance: any) => instance.uuid === library.instance_id ); }, [cloudLibrary.data, library.instance_id]); @@ -113,7 +117,7 @@ function Authenticated() { } // million-ignore -const Instances = ({ instances }: { instances: CloudInstance[] }) => { +const Instances = ({ instances }: { instances: any[] }) => { const { library } = useLibraryContext(); const filteredInstances = instances.filter((instance) => instance.uuid !== library.instance_id); return ( @@ -173,8 +177,8 @@ const Instances = ({ instances }: { instances: CloudInstance[] }) => { }; interface LibraryProps { - cloudLibrary: CloudLibrary; - thisInstance: CloudInstance | undefined; + cloudLibrary: any; + thisInstance: any | undefined; } // million-ignore @@ -206,7 +210,7 @@ const Library = ({ thisInstance, cloudLibrary }: LibraryProps) => { }; interface ThisInstanceProps { - instance: CloudInstance; + instance: any; } // million-ignore diff --git a/interface/app/$libraryId/settings/client/account/index.tsx b/interface/app/$libraryId/settings/client/account/index.tsx index 0985029b2..a7a6ea53c 100644 --- a/interface/app/$libraryId/settings/client/account/index.tsx +++ b/interface/app/$libraryId/settings/client/account/index.tsx @@ -86,14 +86,6 @@ function HostedLocationsPlayground() { locations.refetch(); } }); - const doTheThing = useBridgeMutation('cloud.locations.testing', { - onSuccess() { - toast.success('Uploaded file!'); - }, - onError(err) { - toast.error(err.message); - } - }); useEffect(() => { if (path === '' && locations.data?.[0]) { @@ -101,7 +93,7 @@ function HostedLocationsPlayground() { } }, [path, locations.data]); - const isLoading = createLocation.isLoading || removeLocation.isLoading || doTheThing.isLoading; + const isLoading = createLocation.isLoading || removeLocation.isLoading; return ( <> @@ -153,19 +145,6 @@ function HostedLocationsPlayground() { > Delete - ))} diff --git a/packages/client/src/core.ts b/packages/client/src/core.ts index 4c62391d6..6145a7dca 100644 --- a/packages/client/src/core.ts +++ b/packages/client/src/core.ts @@ -8,9 +8,13 @@ export type Procedures = { { key: "buildInfo", input: never, result: BuildInfo } | { key: "cloud.devices.get", input: DeviceGetRequest, result: Device } | { key: "cloud.devices.list", input: DeviceListRequest, result: Device[] } | + { key: "cloud.libraries.get", input: LibraryGetRequest, result: Library } | + { key: "cloud.libraries.list", input: LibraryListRequest, result: Library[] } | { key: "cloud.library.get", input: LibraryArgs, result: null } | { key: "cloud.library.list", input: never, result: null } | - { key: "cloud.locations.list", input: never, result: CloudLocation[] } | + { key: "cloud.locations.list", input: never, result: Core_CloudLocation[] } | + { key: "cloud.new_locations.get", input: LocationGetRequest, result: CloudLocation } | + { key: "cloud.new_locations.list", input: LocationListRequest, result: CloudLocation[] } | { key: "ephemeralFiles.getMediaData", input: string, result: MediaData | null } | { key: "files.get", input: LibraryArgs, result: ObjectWithFilePaths2 | null } | { key: "files.getConvertibleImageExtensions", input: never, result: string[] } | @@ -67,11 +71,17 @@ export type Procedures = { { key: "cloud.bootstrap", input: AccessToken, result: null } | { key: "cloud.devices.delete", input: DeviceDeleteRequest, result: null } | { key: "cloud.devices.update", input: DeviceUpdateRequest, result: null } | + { key: "cloud.libraries.create", input: LibraryArgs, result: null } | + { key: "cloud.libraries.join", input: string, result: null } | + { key: "cloud.libraries.update", input: LibraryUpdateRequest, result: null } | { key: "cloud.library.create", input: LibraryArgs, result: null } | { key: "cloud.library.join", input: string, result: null } | { key: "cloud.library.sync", input: LibraryArgs, result: null } | - { key: "cloud.locations.create", input: string, result: CloudLocation } | - { key: "cloud.locations.remove", input: string, result: CloudLocation } | + { key: "cloud.locations.create", input: string, result: Core_CloudLocation } | + { key: "cloud.locations.remove", input: string, result: Core_CloudLocation } | + { key: "cloud.new_locations.create", input: LocationCreateRequest, result: null } | + { key: "cloud.new_locations.delete", input: LocationDeleteRequest, result: null } | + { key: "cloud.new_locations.update", input: LocationUpdateRequest, result: null } | { key: "ephemeralFiles.copyFiles", input: LibraryArgs, result: null } | { key: "ephemeralFiles.createFile", input: LibraryArgs, result: string } | { key: "ephemeralFiles.createFolder", input: LibraryArgs, result: string } | @@ -183,7 +193,7 @@ export type ChangeNodeNameArgs = { name: string | null; p2p_port: Port | null; p export type Chapter = { id: number; start: [number, number]; end: [number, number]; time_base_den: number; time_base_num: number; metadata: Metadata } -export type CloudLocation = { id: string; name: string } +export type CloudLocation = { pub_id: LocationPubId; name: string; device: Device | null; library: Library | null; created_at: string; updated_at: string } export type Codec = { kind: string | null; sub_kind: string | null; tag: string | null; name: string | null; profile: string | null; bit_rate: number; props: Props | null } @@ -217,6 +227,8 @@ export type ConvertImageArgs = { location_id: number; file_path_id: number; dele export type ConvertibleExtension = "bmp" | "dib" | "ff" | "gif" | "ico" | "jpg" | "jpeg" | "png" | "pnm" | "qoi" | "tga" | "icb" | "vda" | "vst" | "tiff" | "tif" | "hif" | "heif" | "heifs" | "heic" | "heics" | "avif" | "avci" | "avcs" | "svg" | "svgz" | "pdf" | "webp" +export type Core_CloudLocation = { id: string; name: string } + export type CreateEphemeralFileArgs = { path: string; context: EphemeralFileCreateContextTypes; name: string | null } export type CreateEphemeralFolderArgs = { path: string; name: string | null } @@ -411,6 +423,8 @@ export type Label = { id: number; name: string; date_created: string | null; dat export type LabelWithObjects = { id: number; name: string; date_created: string | null; date_modified: string | null; label_objects: { object: { id: number; file_paths: FilePath[] } }[] } +export type Library = { pub_id: LibraryPubId; name: string; original_device: Device | null; created_at: string; updated_at: string } + /** * Can wrap a query argument to require it to contain a `library_id` and provide helpers for working with libraries. */ @@ -442,10 +456,18 @@ export type LibraryConfigVersion = "V0" | "V1" | "V2" | "V3" | "V4" | "V5" | "V6 export type LibraryConfigWrapped = { uuid: string; instance_id: string; instance_public_key: RemoteIdentity; config: LibraryConfig } +export type LibraryGetRequest = { access_token: AccessToken; pub_id: LibraryPubId; with_device: boolean } + +export type LibraryListRequest = { access_token: AccessToken; with_device: boolean } + export type LibraryName = string export type LibraryPreferences = { location?: { [key in string]: LocationSettings }; tag?: { [key in string]: TagSettings } } +export type LibraryPubId = string + +export type LibraryUpdateRequest = { access_token: AccessToken; pub_id: LibraryPubId; name: string } + export type LightScanArgs = { location_id: number; sub_path: string } export type ListenerState = { type: "Listening" } | { type: "Error"; error: string } | { type: "NotListening" } @@ -461,6 +483,16 @@ export type Location = { id: number; pub_id: number[]; name: string | null; path */ export type LocationCreateArgs = { path: string; dry_run: boolean; indexer_rules_ids: number[] } +export type LocationCreateRequest = { access_token: AccessToken; pub_id: LocationPubId; name: string; library_pub_id: LibraryPubId; device_pub_id: DevicePubId } + +export type LocationDeleteRequest = { access_token: AccessToken; pub_id: LocationPubId } + +export type LocationGetRequest = { access_token: AccessToken; pub_id: LocationPubId; with_library: boolean; with_device: boolean } + +export type LocationListRequest = { access_token: AccessToken; with_library: boolean; with_device: boolean } + +export type LocationPubId = string + export type LocationSettings = { explorer: ExplorerSettings } /** @@ -473,6 +505,8 @@ export type LocationSettings = { explorer: ExplorerSettings } */ export type LocationUpdateArgs = { id: number; name: string | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; indexer_rules_ids: number[]; path: string | null } +export type LocationUpdateRequest = { access_token: AccessToken; pub_id: LocationPubId; name: string } + export type LocationWithIndexerRule = { id: number; pub_id: number[]; name: string | null; path: string | null; total_capacity: number | null; available_capacity: number | null; size_in_bytes: number[] | null; is_archived: boolean | null; generate_preview_media: boolean | null; sync_preview_media: boolean | null; hidden: boolean | null; date_created: string | null; instance_id: number | null; indexer_rules: IndexerRule[] } export type MaybeUndefined = null | T