From 79522d9a502f2ee62e5fad140da7b412ef5e46f3 Mon Sep 17 00:00:00 2001 From: Jamie Pine Date: Sun, 24 Apr 2022 09:50:22 -0700 Subject: [PATCH] wip --- apps/server/src/main.rs | 82 ++++++++---------- .../migrations/20220424140258_/migration.sql | 37 ++++++++ core/prisma/schema.prisma | 16 ++-- core/src/library/loader.rs | 9 +- core/src/library/mod.rs | 13 +++ core/src/library/statistics.rs | 67 ++++++++++++++ core/src/state/client.rs | 1 + docs/developer/todo.md | 1 + packages/interface/src/App.tsx | 6 +- .../interface/src/components/file/Sidebar.tsx | 11 ++- packages/interface/src/screens/Content.tsx | 13 +++ .../src/screens/{Spaces.tsx => Debug.tsx} | 27 +++++- .../src/screens/settings/GeneralSettings.tsx | 52 +---------- pnpm-lock.yaml | Bin 187412 -> 196798 bytes 14 files changed, 218 insertions(+), 117 deletions(-) create mode 100644 core/prisma/migrations/20220424140258_/migration.sql create mode 100644 core/src/library/statistics.rs create mode 100644 packages/interface/src/screens/Content.tsx rename packages/interface/src/screens/{Spaces.tsx => Debug.tsx} (51%) diff --git a/apps/server/src/main.rs b/apps/server/src/main.rs index 305cac90c..f6bbd8b67 100644 --- a/apps/server/src/main.rs +++ b/apps/server/src/main.rs @@ -52,7 +52,43 @@ impl StreamHandler> for Socket { Ok(ws::Message::Text(text)) => { let msg: SocketMessage = serde_json::from_str(&text).unwrap(); - ctx.notify(msg); + let core = self.core.clone(); + + let recipient = ctx.address().recipient(); + + let fut = async move { + match msg.payload { + SocketMessagePayload::Query(query) => { + match core.query(query).await { + Ok(response) => recipient.do_send(SocketResponse { + id: msg.id.clone(), + payload: SocketResponsePayload::Query(response), + }), + Err(err) => { + // println!("query error: {:?}", err); + // Err(err.to_string()) + }, + }; + }, + SocketMessagePayload::Command(command) => { + match core.command(command).await { + Ok(response) => recipient.do_send(SocketResponse { + id: msg.id.clone(), + payload: SocketResponsePayload::Query(response), + }), + Err(err) => { + // println!("command error: {:?}", err); + // Err(err.to_string()) + }, + }; + }, + _ => {}, + } + }; + + fut.into_actor(self).spawn(ctx); + + () }, _ => (), } @@ -82,50 +118,6 @@ impl Handler for Socket { } } -impl Handler for Socket { - type Result = (); - - fn handle(&mut self, msg: SocketMessage, ctx: &mut Self::Context) -> Self::Result { - let core = self.core.clone(); - - let recipient = ctx.address().recipient(); - - let fut = async move { - match msg.payload { - SocketMessagePayload::Query(query) => { - match core.query(query).await { - Ok(response) => recipient.do_send(SocketResponse { - id: msg.id.clone(), - payload: SocketResponsePayload::Query(response), - }), - Err(err) => { - // println!("query error: {:?}", err); - // Err(err.to_string()) - }, - }; - }, - SocketMessagePayload::Command(command) => { - match core.command(command).await { - Ok(response) => recipient.do_send(SocketResponse { - id: msg.id.clone(), - payload: SocketResponsePayload::Query(response), - }), - Err(err) => { - // println!("command error: {:?}", err); - // Err(err.to_string()) - }, - }; - }, - _ => {}, - } - }; - - fut.into_actor(self).spawn(ctx); - - () - } -} - #[get("/")] async fn index() -> impl Responder { format!("Spacedrive Server!") diff --git a/core/prisma/migrations/20220424140258_/migration.sql b/core/prisma/migrations/20220424140258_/migration.sql new file mode 100644 index 000000000..2bce0eccc --- /dev/null +++ b/core/prisma/migrations/20220424140258_/migration.sql @@ -0,0 +1,37 @@ +/* + Warnings: + + - You are about to drop the column `total_byte_capacity` on the `library_statistics` table. All the data in the column will be lost. + +*/ +-- CreateTable +CREATE TABLE "FileConflict" ( + "original_file_id" INTEGER NOT NULL, + "detactched_file_id" INTEGER NOT NULL +); + +-- RedefineTables +PRAGMA foreign_keys=OFF; +CREATE TABLE "new_library_statistics" ( + "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, + "date_captured" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, + "library_id" INTEGER NOT NULL, + "total_file_count" INTEGER NOT NULL DEFAULT 0, + "total_bytes_used" TEXT NOT NULL DEFAULT '0', + "total_bytes_capacity" TEXT NOT NULL DEFAULT '0', + "total_unique_bytes" TEXT NOT NULL DEFAULT '0', + "total_bytes_free" TEXT NOT NULL DEFAULT '0', + "preview_media_bytes" TEXT NOT NULL DEFAULT '0' +); +INSERT INTO "new_library_statistics" ("date_captured", "id", "library_id", "total_bytes_used", "total_file_count", "total_unique_bytes") SELECT "date_captured", "id", "library_id", "total_bytes_used", "total_file_count", "total_unique_bytes" FROM "library_statistics"; +DROP TABLE "library_statistics"; +ALTER TABLE "new_library_statistics" RENAME TO "library_statistics"; +CREATE UNIQUE INDEX "library_statistics_library_id_key" ON "library_statistics"("library_id"); +PRAGMA foreign_key_check; +PRAGMA foreign_keys=ON; + +-- CreateIndex +CREATE UNIQUE INDEX "FileConflict_original_file_id_key" ON "FileConflict"("original_file_id"); + +-- CreateIndex +CREATE UNIQUE INDEX "FileConflict_detactched_file_id_key" ON "FileConflict"("detactched_file_id"); diff --git a/core/prisma/schema.prisma b/core/prisma/schema.prisma index d8a39b50f..74473ace6 100644 --- a/core/prisma/schema.prisma +++ b/core/prisma/schema.prisma @@ -43,13 +43,15 @@ model Library { } model LibraryStatistics { - id Int @id @default(autoincrement()) - date_captured DateTime @default(now()) - library_id Int @unique - total_file_count Int @default(0) - total_bytes_used String @default("0") - total_byte_capacity String @default("0") - total_unique_bytes String @default("0") + id Int @id @default(autoincrement()) + date_captured DateTime @default(now()) + library_id Int @unique + total_file_count Int @default(0) + total_bytes_used String @default("0") + total_bytes_capacity String @default("0") + total_unique_bytes String @default("0") + total_bytes_free String @default("0") + preview_media_bytes String @default("0") @@map("library_statistics") } diff --git a/core/src/library/loader.rs b/core/src/library/loader.rs index 1e3f57cd0..ad5b0a111 100644 --- a/core/src/library/loader.rs +++ b/core/src/library/loader.rs @@ -1,20 +1,15 @@ use anyhow::Result; -use thiserror::Error; use uuid::Uuid; use crate::state::client::LibraryState; use crate::{db::migrate, prisma::library, state}; use crate::{prisma, Core}; +use super::LibraryError; + pub static LIBRARY_DB_NAME: &str = "library.db"; pub static DEFAULT_NAME: &str = "My Library"; -#[derive(Error, Debug)] -pub enum LibraryError { - #[error("Database error")] - DatabaseError(#[from] prisma::QueryError), -} - pub async fn get(core: &Core) -> Result { let config = state::client::get(); let db = &core.database; diff --git a/core/src/library/mod.rs b/core/src/library/mod.rs index 186dcf607..bbb98fba0 100644 --- a/core/src/library/mod.rs +++ b/core/src/library/mod.rs @@ -1 +1,14 @@ pub mod loader; +pub mod statistics; + +use thiserror::Error; + +use crate::{prisma, sys::SysError}; + +#[derive(Error, Debug)] +pub enum LibraryError { + #[error("Database error")] + DatabaseError(#[from] prisma::QueryError), + #[error("System error")] + SysError(#[from] SysError), +} diff --git a/core/src/library/statistics.rs b/core/src/library/statistics.rs new file mode 100644 index 000000000..bfc0e1063 --- /dev/null +++ b/core/src/library/statistics.rs @@ -0,0 +1,67 @@ +use crate::{prisma::library_statistics, state::client, CoreContext}; +use serde::{Deserialize, Serialize}; +use ts_rs::TS; + +use super::LibraryError; + +#[derive(Debug, Serialize, Deserialize, TS, Clone)] +#[ts(export)] +pub struct Statistics { + total_file_count: i32, + total_bytes_used: String, + total_bytes_capacity: String, + total_bytes_free: String, + total_unique_bytes: String, + preview_media_bytes: String, + library_db_size: String, +} + +impl Into for library_statistics::Data { + fn into(self) -> Statistics { + Statistics { + total_file_count: self.total_file_count, + total_bytes_used: self.total_bytes_used, + total_bytes_capacity: self.total_bytes_capacity, + total_bytes_free: self.total_bytes_free, + total_unique_bytes: self.total_unique_bytes, + preview_media_bytes: self.preview_media_bytes, + library_db_size: String::new(), + } + } +} + +impl Default for Statistics { + fn default() -> Self { + Self { + total_file_count: 0, + total_bytes_used: String::new(), + total_bytes_capacity: String::new(), + total_bytes_free: String::new(), + total_unique_bytes: String::new(), + preview_media_bytes: String::new(), + library_db_size: String::new(), + } + } +} + +impl Statistics { + pub async fn recalculate(ctx: &CoreContext) -> Result<(), LibraryError> { + let config = client::get(); + let db = &ctx.database; + + let library_data = config.get_current_library(); + + let library_statistics_db = match db + .library_statistics() + .find_unique(library_statistics::id::equals(library_data.library_id)) + .exec() + .await? + { + Some(library_statistics_db) => library_statistics_db.into(), + // create the default values if database has no entry + None => Statistics::default(), + }; + + Ok(()) + } +} diff --git a/core/src/state/client.rs b/core/src/state/client.rs index bcc23010b..344f4e2f9 100644 --- a/core/src/state/client.rs +++ b/core/src/state/client.rs @@ -31,6 +31,7 @@ pub static CLIENT_STATE_CONFIG_NAME: &str = "client_state.json"; #[ts(export)] pub struct LibraryState { pub library_uuid: String, + pub library_id: i32, pub library_path: String, pub offline: bool, } diff --git a/docs/developer/todo.md b/docs/developer/todo.md index 64a5fb6cd..1062b020f 100644 --- a/docs/developer/todo.md +++ b/docs/developer/todo.md @@ -3,6 +3,7 @@ # Todo - Landing sections +- Client pool - Custom scrollbars - Tag files - Right click menu diff --git a/packages/interface/src/App.tsx b/packages/interface/src/App.tsx index a0460e3dd..1781dfe73 100644 --- a/packages/interface/src/App.tsx +++ b/packages/interface/src/App.tsx @@ -14,7 +14,7 @@ import { ExplorerScreen } from './screens/Explorer'; import { useCoreEvents } from './hooks/useCoreEvents'; import { ErrorBoundary, FallbackProps } from 'react-error-boundary'; import { OverviewScreen } from './screens/Overview'; -import { SpacesScreen } from './screens/Spaces'; +import { DebugScreen } from './screens/Debug'; import { Modal } from './components/layout/Modal'; import GeneralSettings from './screens/settings/GeneralSettings'; import SlideUp from './components/transitions/SlideUp'; @@ -27,6 +27,7 @@ import { Button } from '@sd/ui'; import { CoreEvent } from '@sd/core'; import clsx from 'clsx'; import './style.scss'; +import { ContentScreen } from './screens/Content'; const queryClient = new QueryClient(); @@ -114,7 +115,8 @@ function Router() { }> } /> } /> - } /> + } /> + } /> } /> } /> } /> diff --git a/packages/interface/src/components/file/Sidebar.tsx b/packages/interface/src/components/file/Sidebar.tsx index b0bce3b40..d6f0fe673 100644 --- a/packages/interface/src/components/file/Sidebar.tsx +++ b/packages/interface/src/components/file/Sidebar.tsx @@ -1,7 +1,7 @@ import { LockClosedIcon } from '@heroicons/react/outline'; import { CogIcon, EyeOffIcon, PlusIcon, ServerIcon } from '@heroicons/react/solid'; import clsx from 'clsx'; -import { CirclesFour, EjectSimple, MonitorPlay, Planet } from 'phosphor-react'; +import { CirclesFour, Code, EjectSimple, MonitorPlay, Planet } from 'phosphor-react'; import React, { useContext, useEffect, useState } from 'react'; import { NavLink, NavLinkProps } from 'react-router-dom'; import { TrafficLights } from '../os/TrafficLights'; @@ -111,10 +111,14 @@ export const Sidebar: React.FC = (props) => { Overview - + Content + + + Debug + {/* Explorer @@ -141,7 +145,8 @@ export const Sidebar: React.FC = (props) => { )} >
- {isActive ? : } + +
{location.name}
diff --git a/packages/interface/src/screens/Content.tsx b/packages/interface/src/screens/Content.tsx new file mode 100644 index 000000000..694dd6f96 --- /dev/null +++ b/packages/interface/src/screens/Content.tsx @@ -0,0 +1,13 @@ +// import { useBridgeCommand, useBridgeQuery } from '@sd/client'; + +import React from 'react'; + +export const ContentScreen: React.FC<{}> = (props) => { + return ( +
+
+

Content

+
+
+ ); +}; diff --git a/packages/interface/src/screens/Spaces.tsx b/packages/interface/src/screens/Debug.tsx similarity index 51% rename from packages/interface/src/screens/Spaces.tsx rename to packages/interface/src/screens/Debug.tsx index 1732e7782..c25463654 100644 --- a/packages/interface/src/screens/Spaces.tsx +++ b/packages/interface/src/screens/Debug.tsx @@ -1,18 +1,41 @@ -import { useBridgeQuery } from '@sd/client'; +import { useBridgeCommand, useBridgeQuery } from '@sd/client'; +import { Button } from '@sd/ui'; import React from 'react'; import ReactJson from 'react-json-view'; import FileItem from '../components/file/FileItem'; import CodeBlock from '../components/primitive/Codeblock'; import { Tag } from '../components/primitive/Tag'; -export const SpacesScreen: React.FC<{}> = (props) => { +export const DebugScreen: React.FC<{}> = (props) => { const { data: client } = useBridgeQuery('ClientGetState'); const { data: jobs } = useBridgeQuery('JobGetRunning'); const { data: jobHistory } = useBridgeQuery('JobGetHistory'); + const { mutate: purgeDB } = useBridgeCommand('PurgeDatabase', { + onMutate: () => { + alert('Database purged'); + } + }); + const { mutate: identifyFiles } = useBridgeCommand('IdentifyUniqueFiles'); return (

Developer Debugger

+
+ + + +

Running Jobs

Job History

diff --git a/packages/interface/src/screens/settings/GeneralSettings.tsx b/packages/interface/src/screens/settings/GeneralSettings.tsx index ae39bb288..8fc571dfa 100644 --- a/packages/interface/src/screens/settings/GeneralSettings.tsx +++ b/packages/interface/src/screens/settings/GeneralSettings.tsx @@ -11,15 +11,6 @@ import { useBridgeCommand, useBridgeQuery } from '@sd/client'; export default function GeneralSettings() { const { data: volumes } = useBridgeQuery('SysGetVolumes'); - const [fakeSliderVal, setFakeSliderVal] = useState([30, 0]); - - const { mutate: purgeDB } = useBridgeCommand('PurgeDatabase', { - onMutate: () => { - alert('Database purged'); - } - }); - const { mutate: identifyFiles } = useBridgeCommand('IdentifyUniqueFiles'); - return (
@@ -31,17 +22,7 @@ export default function GeneralSettings() { Note: This is a pre-alpha build of Spacedrive, many features are yet to be functional.

-
- - - -
+ {/*
- -
- - - - - -
- -
-
- -
- - {/**/}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index e467482465f45cc79ce7bff2fa011c9f9cdd0980..78c0b1fbb4eb7aa2bebaa21452e0de419fa7bbc1 100644 GIT binary patch delta 5462 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