Rename Files to Objects (#396)

* backend rename complete

* finalize Object refactor frontend
+ remove some exhaustive dep suppression
+ FIX INVALIDATE QUERY BUG
This commit is contained in:
Jamie Pine 2022-10-04 09:16:49 -07:00 committed by GitHub
parent ed06e3051e
commit cbd58ee438
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
32 changed files with 313 additions and 375 deletions

1
.gitignore vendored
View file

@ -66,3 +66,4 @@ examples/*/*.lock
/sdserver_data /sdserver_data
.spacedrive .spacedrive
dev.db-journal

View file

@ -14,7 +14,7 @@ const placeholderFileItems: FilePath[] = [
date_indexed: '2020-01-01T00:00:00.000Z', date_indexed: '2020-01-01T00:00:00.000Z',
date_modified: '2020-01-01T00:00:00.000Z', date_modified: '2020-01-01T00:00:00.000Z',
extension: '', extension: '',
file_id: 1, object_id: 1,
id: 1, id: 1,
location_id: 1, location_id: 1,
materialized_path: '', materialized_path: '',
@ -28,7 +28,7 @@ const placeholderFileItems: FilePath[] = [
date_indexed: '2020-01-01T00:00:00.000Z', date_indexed: '2020-01-01T00:00:00.000Z',
date_modified: '2020-01-01T00:00:00.000Z', date_modified: '2020-01-01T00:00:00.000Z',
extension: '', extension: '',
file_id: 2, object_id: 2,
id: 2, id: 2,
location_id: 2, location_id: 2,
materialized_path: '', materialized_path: '',
@ -42,7 +42,7 @@ const placeholderFileItems: FilePath[] = [
date_indexed: '2020-01-01T00:00:00.000Z', date_indexed: '2020-01-01T00:00:00.000Z',
date_modified: '2020-01-01T00:00:00.000Z', date_modified: '2020-01-01T00:00:00.000Z',
extension: 'tsx', extension: 'tsx',
file_id: 3, object_id: 3,
id: 3, id: 3,
location_id: 3, location_id: 3,
materialized_path: '', materialized_path: '',
@ -56,7 +56,7 @@ const placeholderFileItems: FilePath[] = [
date_indexed: '2020-01-01T00:00:00.000Z', date_indexed: '2020-01-01T00:00:00.000Z',
date_modified: '2020-01-01T00:00:00.000Z', date_modified: '2020-01-01T00:00:00.000Z',
extension: 'vite', extension: 'vite',
file_id: 4, object_id: 4,
id: 4, id: 4,
location_id: 4, location_id: 4,
materialized_path: '', materialized_path: '',
@ -70,7 +70,7 @@ const placeholderFileItems: FilePath[] = [
date_indexed: '2020-01-01T00:00:00.000Z', date_indexed: '2020-01-01T00:00:00.000Z',
date_modified: '2020-01-01T00:00:00.000Z', date_modified: '2020-01-01T00:00:00.000Z',
extension: 'docker', extension: 'docker',
file_id: 5, object_id: 5,
id: 5, id: 5,
location_id: 5, location_id: 5,
materialized_path: '', materialized_path: '',
@ -90,7 +90,7 @@ export interface DeviceProps {
const Device = ({ name, locations, size, type }: DeviceProps) => { const Device = ({ name, locations, size, type }: DeviceProps) => {
return ( return (
<View style={tw`bg-gray-600 border rounded-md border-gray-550 my-2`}> <View style={tw`my-2 bg-gray-600 border rounded-md border-gray-550`}>
<View style={tw`flex flex-row items-center px-3.5 pt-3 pb-2`}> <View style={tw`flex flex-row items-center px-3.5 pt-3 pb-2`}>
<View style={tw`flex flex-row items-center`}> <View style={tw`flex flex-row items-center`}>
{type === 'phone' && ( {type === 'phone' && (
@ -107,7 +107,7 @@ const Device = ({ name, locations, size, type }: DeviceProps) => {
</View> </View>
</View> </View>
{/* Size */} {/* Size */}
<Text style={tw`font-semibold text-sm ml-2 text-gray-400`}>{size}</Text> <Text style={tw`ml-2 text-sm font-semibold text-gray-400`}>{size}</Text>
</View> </View>
{/* Locations/Files TODO: Maybe use FlashList? */} {/* Locations/Files TODO: Maybe use FlashList? */}
<FlatList <FlatList

View file

@ -53,11 +53,9 @@ export type ExplorerContext = { type: "Location" } & Location | { type: "Tag" }
export interface ExplorerData { context: ExplorerContext, items: Array<ExplorerItem> } export interface ExplorerData { context: ExplorerContext, items: Array<ExplorerItem> }
export type ExplorerItem = { type: "Path" } & { id: number, is_dir: boolean, location_id: number, materialized_path: string, name: string, extension: string | null, file_id: number | null, parent_id: number | null, key_id: number | null, date_created: string, date_modified: string, date_indexed: string, file: File | null } | { type: "Object" } & { id: number, cas_id: string, integrity_checksum: string | null, name: string | null, extension: string | null, kind: number, size_in_bytes: string, 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_modified: string, date_indexed: string, paths: Array<FilePath> } export type ExplorerItem = { type: "Path" } & { id: number, is_dir: boolean, location_id: number, materialized_path: string, name: string, extension: string | null, object_id: number | null, parent_id: number | null, key_id: number | null, date_created: string, date_modified: string, date_indexed: string, object: Object | null } | { type: "Object" } & { id: number, cas_id: string, integrity_checksum: string | null, name: string | null, extension: string | null, kind: number, size_in_bytes: string, 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_modified: string, date_indexed: string, file_paths: Array<FilePath> }
export interface File { id: number, cas_id: string, integrity_checksum: string | null, name: string | null, extension: string | null, kind: number, size_in_bytes: string, 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_modified: string, date_indexed: string } export interface FilePath { id: number, is_dir: boolean, location_id: number, materialized_path: string, name: string, extension: string | null, object_id: number | null, parent_id: number | null, key_id: number | null, date_created: string, date_modified: string, date_indexed: string }
export interface FilePath { id: number, is_dir: boolean, location_id: number, materialized_path: string, name: string, extension: string | null, file_id: number | null, parent_id: number | null, key_id: number | null, date_created: string, date_modified: string, date_indexed: string }
export interface GenerateThumbsForLocationArgs { id: number, path: string } export interface GenerateThumbsForLocationArgs { id: number, path: string }
@ -93,17 +91,19 @@ export interface NodeConfig { version: string | null, id: string, name: string,
export interface NodeState { version: string | null, id: string, name: string, p2p_port: number | null, data_path: string } export interface NodeState { version: string | null, id: string, name: string, p2p_port: number | null, data_path: string }
export interface Object { id: number, cas_id: string, integrity_checksum: string | null, name: string | null, extension: string | null, kind: number, size_in_bytes: string, 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_modified: string, date_indexed: string }
export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent" export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"
export interface SetFavoriteArgs { id: number, favorite: boolean } export interface SetFavoriteArgs { id: number, favorite: boolean }
export interface SetNoteArgs { id: number, note: string | null } export interface SetNoteArgs { id: number, note: string | null }
export interface Statistics { id: number, date_captured: string, total_file_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string } export interface Statistics { id: number, date_captured: string, total_object_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string }
export interface Tag { id: number, pub_id: Array<number>, name: string | null, color: string | null, total_files: number | null, redundancy_goal: number | null, date_created: string, date_modified: string } export interface Tag { id: number, pub_id: Array<number>, name: string | null, color: string | null, total_objects: number | null, redundancy_goal: number | null, date_created: string, date_modified: string }
export interface TagAssignArgs { file_id: number, tag_id: number, unassign: boolean } export interface TagAssignArgs { object_id: number, tag_id: number, unassign: boolean }
export interface TagCreateArgs { name: string, color: string } export interface TagCreateArgs { name: string, color: string }

View file

@ -1,14 +1,5 @@
-- CreateTable -- CreateTable
CREATE TABLE "_migrations" ( CREATE TABLE "sync_event" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"name" TEXT NOT NULL,
"checksum" TEXT NOT NULL,
"steps_applied" INTEGER NOT NULL DEFAULT 0,
"applied_at" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- CreateTable
CREATE TABLE "sync_events" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"node_id" INTEGER NOT NULL, "node_id" INTEGER NOT NULL,
"timestamp" TEXT NOT NULL, "timestamp" TEXT NOT NULL,
@ -16,14 +7,14 @@ CREATE TABLE "sync_events" (
"kind" INTEGER NOT NULL, "kind" INTEGER NOT NULL,
"column" TEXT, "column" TEXT,
"value" TEXT NOT NULL, "value" TEXT NOT NULL,
CONSTRAINT "sync_events_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "nodes" ("id") ON DELETE RESTRICT ON UPDATE CASCADE CONSTRAINT "sync_event_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "node" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
); );
-- CreateTable -- CreateTable
CREATE TABLE "statistics" ( CREATE TABLE "statistics" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"date_captured" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_captured" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"total_file_count" INTEGER NOT NULL DEFAULT 0, "total_object_count" INTEGER NOT NULL DEFAULT 0,
"library_db_size" TEXT NOT NULL DEFAULT '0', "library_db_size" TEXT NOT NULL DEFAULT '0',
"total_bytes_used" TEXT NOT NULL DEFAULT '0', "total_bytes_used" TEXT NOT NULL DEFAULT '0',
"total_bytes_capacity" TEXT NOT NULL DEFAULT '0', "total_bytes_capacity" TEXT NOT NULL DEFAULT '0',
@ -33,7 +24,7 @@ CREATE TABLE "statistics" (
); );
-- CreateTable -- CreateTable
CREATE TABLE "nodes" ( CREATE TABLE "node" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pub_id" BLOB NOT NULL, "pub_id" BLOB NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
@ -45,7 +36,7 @@ CREATE TABLE "nodes" (
); );
-- CreateTable -- CreateTable
CREATE TABLE "volumes" ( CREATE TABLE "volume" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"node_id" INTEGER NOT NULL, "node_id" INTEGER NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
@ -59,7 +50,7 @@ CREATE TABLE "volumes" (
); );
-- CreateTable -- CreateTable
CREATE TABLE "locations" ( CREATE TABLE "location" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pub_id" BLOB NOT NULL, "pub_id" BLOB NOT NULL,
"node_id" INTEGER NOT NULL, "node_id" INTEGER NOT NULL,
@ -73,11 +64,11 @@ CREATE TABLE "locations" (
"is_online" BOOLEAN NOT NULL DEFAULT true, "is_online" BOOLEAN NOT NULL DEFAULT true,
"is_archived" BOOLEAN NOT NULL DEFAULT false, "is_archived" BOOLEAN NOT NULL DEFAULT false,
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "locations_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "nodes" ("id") ON DELETE RESTRICT ON UPDATE CASCADE CONSTRAINT "location_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "node" ("id") ON DELETE RESTRICT ON UPDATE CASCADE
); );
-- CreateTable -- CreateTable
CREATE TABLE "files" ( CREATE TABLE "object" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"cas_id" TEXT NOT NULL, "cas_id" TEXT NOT NULL,
"integrity_checksum" TEXT, "integrity_checksum" TEXT,
@ -97,18 +88,18 @@ CREATE TABLE "files" (
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_indexed" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_indexed" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
CONSTRAINT "files_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "keys" ("id") ON DELETE SET NULL ON UPDATE CASCADE CONSTRAINT "object_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "key" ("id") ON DELETE SET NULL ON UPDATE CASCADE
); );
-- CreateTable -- CreateTable
CREATE TABLE "file_paths" ( CREATE TABLE "file_path" (
"id" INTEGER NOT NULL, "id" INTEGER NOT NULL,
"is_dir" BOOLEAN NOT NULL DEFAULT false, "is_dir" BOOLEAN NOT NULL DEFAULT false,
"location_id" INTEGER NOT NULL, "location_id" INTEGER NOT NULL,
"materialized_path" TEXT NOT NULL, "materialized_path" TEXT NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"extension" TEXT, "extension" TEXT,
"file_id" INTEGER, "object_id" INTEGER,
"parent_id" INTEGER, "parent_id" INTEGER,
"key_id" INTEGER, "key_id" INTEGER,
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
@ -116,19 +107,19 @@ CREATE TABLE "file_paths" (
"date_indexed" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_indexed" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY ("location_id", "id"), PRIMARY KEY ("location_id", "id"),
CONSTRAINT "file_paths_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "file_path_object_id_fkey" FOREIGN KEY ("object_id") REFERENCES "object" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "file_paths_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "locations" ("id") ON DELETE CASCADE ON UPDATE CASCADE, CONSTRAINT "file_path_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "location" ("id") ON DELETE CASCADE ON UPDATE CASCADE,
CONSTRAINT "file_paths_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "keys" ("id") ON DELETE SET NULL ON UPDATE CASCADE CONSTRAINT "file_path_key_id_fkey" FOREIGN KEY ("key_id") REFERENCES "key" ("id") ON DELETE SET NULL ON UPDATE CASCADE
); );
-- CreateTable -- CreateTable
CREATE TABLE "file_conflicts" ( CREATE TABLE "file_conflict" (
"original_file_id" INTEGER NOT NULL, "original_object_id" INTEGER NOT NULL,
"detactched_file_id" INTEGER NOT NULL "detactched_object_id" INTEGER NOT NULL
); );
-- CreateTable -- CreateTable
CREATE TABLE "keys" ( CREATE TABLE "key" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"checksum" TEXT NOT NULL, "checksum" TEXT NOT NULL,
"name" TEXT, "name" TEXT,
@ -150,34 +141,34 @@ CREATE TABLE "media_data" (
"duration_seconds" INTEGER, "duration_seconds" INTEGER,
"codecs" TEXT, "codecs" TEXT,
"streams" INTEGER, "streams" INTEGER,
CONSTRAINT "media_data_id_fkey" FOREIGN KEY ("id") REFERENCES "files" ("id") ON DELETE CASCADE ON UPDATE CASCADE CONSTRAINT "media_data_id_fkey" FOREIGN KEY ("id") REFERENCES "object" ("id") ON DELETE CASCADE ON UPDATE CASCADE
); );
-- CreateTable -- CreateTable
CREATE TABLE "tags" ( CREATE TABLE "tag" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pub_id" BLOB NOT NULL, "pub_id" BLOB NOT NULL,
"name" TEXT, "name" TEXT,
"color" TEXT, "color" TEXT,
"total_files" INTEGER DEFAULT 0, "total_objects" INTEGER DEFAULT 0,
"redundancy_goal" INTEGER DEFAULT 1, "redundancy_goal" INTEGER DEFAULT 1,
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP "date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
); );
-- CreateTable -- CreateTable
CREATE TABLE "tags_on_file" ( CREATE TABLE "tag_on_object" (
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"tag_id" INTEGER NOT NULL, "tag_id" INTEGER NOT NULL,
"file_id" INTEGER NOT NULL, "object_id" INTEGER NOT NULL,
PRIMARY KEY ("tag_id", "file_id"), PRIMARY KEY ("tag_id", "object_id"),
CONSTRAINT "tags_on_file_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "tags" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "tag_on_object_tag_id_fkey" FOREIGN KEY ("tag_id") REFERENCES "tag" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT "tags_on_file_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION CONSTRAINT "tag_on_object_object_id_fkey" FOREIGN KEY ("object_id") REFERENCES "object" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
); );
-- CreateTable -- CreateTable
CREATE TABLE "labels" ( CREATE TABLE "label" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pub_id" BLOB NOT NULL, "pub_id" BLOB NOT NULL,
"name" TEXT, "name" TEXT,
@ -186,18 +177,18 @@ CREATE TABLE "labels" (
); );
-- CreateTable -- CreateTable
CREATE TABLE "label_on_file" ( CREATE TABLE "label_on_object" (
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"label_id" INTEGER NOT NULL, "label_id" INTEGER NOT NULL,
"file_id" INTEGER NOT NULL, "object_id" INTEGER NOT NULL,
PRIMARY KEY ("label_id", "file_id"), PRIMARY KEY ("label_id", "object_id"),
CONSTRAINT "label_on_file_label_id_fkey" FOREIGN KEY ("label_id") REFERENCES "labels" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "label_on_object_label_id_fkey" FOREIGN KEY ("label_id") REFERENCES "label" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT "label_on_file_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION CONSTRAINT "label_on_object_object_id_fkey" FOREIGN KEY ("object_id") REFERENCES "object" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
); );
-- CreateTable -- CreateTable
CREATE TABLE "spaces" ( CREATE TABLE "space" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pub_id" BLOB NOT NULL, "pub_id" BLOB NOT NULL,
"name" TEXT, "name" TEXT,
@ -207,18 +198,18 @@ CREATE TABLE "spaces" (
); );
-- CreateTable -- CreateTable
CREATE TABLE "file_in_space" ( CREATE TABLE "object_in_space" (
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"space_id" INTEGER NOT NULL, "space_id" INTEGER NOT NULL,
"file_id" INTEGER NOT NULL, "object_id" INTEGER NOT NULL,
PRIMARY KEY ("space_id", "file_id"), PRIMARY KEY ("space_id", "object_id"),
CONSTRAINT "file_in_space_space_id_fkey" FOREIGN KEY ("space_id") REFERENCES "spaces" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "object_in_space_space_id_fkey" FOREIGN KEY ("space_id") REFERENCES "space" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT "file_in_space_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION CONSTRAINT "object_in_space_object_id_fkey" FOREIGN KEY ("object_id") REFERENCES "object" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
); );
-- CreateTable -- CreateTable
CREATE TABLE "jobs" ( CREATE TABLE "job" (
"id" BLOB NOT NULL PRIMARY KEY, "id" BLOB NOT NULL PRIMARY KEY,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
"node_id" INTEGER NOT NULL, "node_id" INTEGER NOT NULL,
@ -231,11 +222,11 @@ CREATE TABLE "jobs" (
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"seconds_elapsed" INTEGER NOT NULL DEFAULT 0, "seconds_elapsed" INTEGER NOT NULL DEFAULT 0,
CONSTRAINT "jobs_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "nodes" ("id") ON DELETE CASCADE ON UPDATE CASCADE CONSTRAINT "job_node_id_fkey" FOREIGN KEY ("node_id") REFERENCES "node" ("id") ON DELETE CASCADE ON UPDATE CASCADE
); );
-- CreateTable -- CreateTable
CREATE TABLE "albums" ( CREATE TABLE "album" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pub_id" BLOB NOT NULL, "pub_id" BLOB NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
@ -245,29 +236,29 @@ CREATE TABLE "albums" (
); );
-- CreateTable -- CreateTable
CREATE TABLE "files_in_albums" ( CREATE TABLE "object_in_album" (
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"album_id" INTEGER NOT NULL, "album_id" INTEGER NOT NULL,
"file_id" INTEGER NOT NULL, "object_id" INTEGER NOT NULL,
PRIMARY KEY ("album_id", "file_id"), PRIMARY KEY ("album_id", "object_id"),
CONSTRAINT "files_in_albums_album_id_fkey" FOREIGN KEY ("album_id") REFERENCES "albums" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "object_in_album_album_id_fkey" FOREIGN KEY ("album_id") REFERENCES "album" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT "files_in_albums_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION CONSTRAINT "object_in_album_object_id_fkey" FOREIGN KEY ("object_id") REFERENCES "object" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
); );
-- CreateTable -- CreateTable
CREATE TABLE "comments" ( CREATE TABLE "comment" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"pub_id" BLOB NOT NULL, "pub_id" BLOB NOT NULL,
"content" TEXT NOT NULL, "content" TEXT NOT NULL,
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_modified" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"file_id" INTEGER, "object_id" INTEGER,
CONSTRAINT "comments_file_id_fkey" FOREIGN KEY ("file_id") REFERENCES "files" ("id") ON DELETE SET NULL ON UPDATE CASCADE CONSTRAINT "comment_object_id_fkey" FOREIGN KEY ("object_id") REFERENCES "object" ("id") ON DELETE SET NULL ON UPDATE CASCADE
); );
-- CreateTable -- CreateTable
CREATE TABLE "indexer_rules" ( CREATE TABLE "indexer_rule" (
"id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT, "id" INTEGER NOT NULL PRIMARY KEY AUTOINCREMENT,
"kind" INTEGER NOT NULL, "kind" INTEGER NOT NULL,
"name" TEXT NOT NULL, "name" TEXT NOT NULL,
@ -277,60 +268,57 @@ CREATE TABLE "indexer_rules" (
); );
-- CreateTable -- CreateTable
CREATE TABLE "indexer_rules_in_location" ( CREATE TABLE "indexer_rule_in_location" (
"date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, "date_created" DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
"location_id" INTEGER NOT NULL, "location_id" INTEGER NOT NULL,
"indexer_rule_id" INTEGER NOT NULL, "indexer_rule_id" INTEGER NOT NULL,
PRIMARY KEY ("location_id", "indexer_rule_id"), PRIMARY KEY ("location_id", "indexer_rule_id"),
CONSTRAINT "indexer_rules_in_location_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "locations" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION, CONSTRAINT "indexer_rule_in_location_location_id_fkey" FOREIGN KEY ("location_id") REFERENCES "location" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION,
CONSTRAINT "indexer_rules_in_location_indexer_rule_id_fkey" FOREIGN KEY ("indexer_rule_id") REFERENCES "indexer_rules" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION CONSTRAINT "indexer_rule_in_location_indexer_rule_id_fkey" FOREIGN KEY ("indexer_rule_id") REFERENCES "indexer_rule" ("id") ON DELETE NO ACTION ON UPDATE NO ACTION
); );
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "_migrations_checksum_key" ON "_migrations"("checksum"); CREATE UNIQUE INDEX "node_pub_id_key" ON "node"("pub_id");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "nodes_pub_id_key" ON "nodes"("pub_id"); CREATE UNIQUE INDEX "volume_node_id_mount_point_name_key" ON "volume"("node_id", "mount_point", "name");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "volumes_node_id_mount_point_name_key" ON "volumes"("node_id", "mount_point", "name"); CREATE UNIQUE INDEX "location_pub_id_key" ON "location"("pub_id");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "locations_pub_id_key" ON "locations"("pub_id"); CREATE UNIQUE INDEX "object_cas_id_key" ON "object"("cas_id");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "files_cas_id_key" ON "files"("cas_id"); CREATE UNIQUE INDEX "object_integrity_checksum_key" ON "object"("integrity_checksum");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "files_integrity_checksum_key" ON "files"("integrity_checksum"); CREATE INDEX "file_path_location_id_idx" ON "file_path"("location_id");
-- CreateIndex -- CreateIndex
CREATE INDEX "file_paths_location_id_idx" ON "file_paths"("location_id"); CREATE UNIQUE INDEX "file_path_location_id_materialized_path_name_extension_key" ON "file_path"("location_id", "materialized_path", "name", "extension");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "file_paths_location_id_materialized_path_name_extension_key" ON "file_paths"("location_id", "materialized_path", "name", "extension"); CREATE UNIQUE INDEX "file_conflict_original_object_id_key" ON "file_conflict"("original_object_id");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "file_conflicts_original_file_id_key" ON "file_conflicts"("original_file_id"); CREATE UNIQUE INDEX "file_conflict_detactched_object_id_key" ON "file_conflict"("detactched_object_id");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "file_conflicts_detactched_file_id_key" ON "file_conflicts"("detactched_file_id"); CREATE UNIQUE INDEX "key_checksum_key" ON "key"("checksum");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "keys_checksum_key" ON "keys"("checksum"); CREATE UNIQUE INDEX "tag_pub_id_key" ON "tag"("pub_id");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "tags_pub_id_key" ON "tags"("pub_id"); CREATE UNIQUE INDEX "label_pub_id_key" ON "label"("pub_id");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "labels_pub_id_key" ON "labels"("pub_id"); CREATE UNIQUE INDEX "space_pub_id_key" ON "space"("pub_id");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "spaces_pub_id_key" ON "spaces"("pub_id"); CREATE UNIQUE INDEX "album_pub_id_key" ON "album"("pub_id");
-- CreateIndex -- CreateIndex
CREATE UNIQUE INDEX "albums_pub_id_key" ON "albums"("pub_id"); CREATE UNIQUE INDEX "comment_pub_id_key" ON "comment"("pub_id");
-- CreateIndex
CREATE UNIQUE INDEX "comments_pub_id_key" ON "comments"("pub_id");

View file

@ -8,16 +8,6 @@ generator client {
output = "../src/prisma.rs" output = "../src/prisma.rs"
} }
model Migration {
id Int @id @default(autoincrement())
name String
checksum String @unique
steps_applied Int @default(0)
applied_at DateTime @default(now())
@@map("_migrations")
}
model SyncEvent { model SyncEvent {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
node_id Int node_id Int
@ -33,13 +23,13 @@ model SyncEvent {
node Node @relation(fields: [node_id], references: [id]) node Node @relation(fields: [node_id], references: [id])
@@map("sync_events") @@map("sync_event")
} }
model Statistics { model Statistics {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
date_captured DateTime @default(now()) date_captured DateTime @default(now())
total_file_count Int @default(0) total_object_count Int @default(0)
library_db_size String @default("0") library_db_size String @default("0")
total_bytes_used String @default("0") total_bytes_used String @default("0")
total_bytes_capacity String @default("0") total_bytes_capacity String @default("0")
@ -65,7 +55,7 @@ model Node {
Location Location[] Location Location[]
@@map("nodes") @@map("node")
} }
model Volume { model Volume {
@ -81,7 +71,7 @@ model Volume {
date_modified DateTime @default(now()) date_modified DateTime @default(now())
@@unique([node_id, mount_point, name]) @@unique([node_id, mount_point, name])
@@map("volumes") @@map("volume")
} }
model Location { model Location {
@ -103,10 +93,10 @@ model Location {
file_paths FilePath[] file_paths FilePath[]
indexer_rules IndexerRulesInLocation[] indexer_rules IndexerRulesInLocation[]
@@map("locations") @@map("location")
} }
model File { model Object {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
// content addressable storage id - sha256 sampled checksum // content addressable storage id - sha256 sampled checksum
cas_id String @unique cas_id String @unique
@ -118,11 +108,11 @@ model File {
kind Int @default(0) kind Int @default(0)
size_in_bytes String size_in_bytes String
key_id Int? key_id Int?
// handy ways to mark a file // handy ways to mark an object
hidden Boolean @default(false) hidden Boolean @default(false)
favorite Boolean @default(false) favorite Boolean @default(false)
important Boolean @default(false) important Boolean @default(false)
// if we have generated preview media for this file // if we have generated preview media for this object
has_thumbnail Boolean @default(false) has_thumbnail Boolean @default(false)
has_thumbstrip Boolean @default(false) has_thumbstrip Boolean @default(false)
has_video_preview Boolean @default(false) has_video_preview Boolean @default(false)
@ -130,24 +120,24 @@ model File {
ipfs_id String? ipfs_id String?
// plain text note // plain text note
note String? note String?
// the original known creation date of this file // the original known creation date of this object
date_created DateTime @default(now()) date_created DateTime @default(now())
// the last time this file was modified // the last time this object was modified
date_modified DateTime @default(now()) date_modified DateTime @default(now())
// when this file was first indexed // when this object was first indexed
date_indexed DateTime @default(now()) date_indexed DateTime @default(now())
tags TagOnFile[] tags TagOnObject[]
labels LabelOnFile[] labels LabelOnObject[]
albums FileInAlbum[] albums ObjectInAlbum[]
spaces FileInSpace[] spaces ObjectInSpace[]
paths FilePath[] file_paths FilePath[]
comments Comment[] comments Comment[]
media_data MediaData? media_data MediaData?
key Key? @relation(fields: [key_id], references: [id]) key Key? @relation(fields: [key_id], references: [id])
@@map("files") @@map("object")
} }
model FilePath { model FilePath {
@ -160,8 +150,8 @@ model FilePath {
// the name and extension // the name and extension
name String name String
extension String? extension String?
// the unique File for this file path // the unique Object for this file path
file_id Int? object_id Int?
// the parent in the file tree // the parent in the file tree
parent_id Int? parent_id Int?
key_id Int? // replacement for encryption key_id Int? // replacement for encryption
@ -172,7 +162,7 @@ model FilePath {
date_modified DateTime @default(now()) date_modified DateTime @default(now())
date_indexed DateTime @default(now()) date_indexed DateTime @default(now())
file File? @relation(fields: [file_id], references: [id], onDelete: Cascade, onUpdate: Cascade) object Object? @relation(fields: [object_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
location Location? @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: Cascade) location Location? @relation(fields: [location_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
// NOTE: this self relation for the file tree was causing SQLite to go to forever bed, disabling until workaround // NOTE: this self relation for the file tree was causing SQLite to go to forever bed, disabling until workaround
@ -184,15 +174,15 @@ model FilePath {
@@id([location_id, id]) @@id([location_id, id])
@@unique([location_id, materialized_path, name, extension]) @@unique([location_id, materialized_path, name, extension])
@@index([location_id]) @@index([location_id])
@@map("file_paths") @@map("file_path")
} }
// if there is a conflicting cas_id, the conficting file should be updated to have a larger cas_id as the field is unique, however this record is kept to tell the indexer (upon discovering this CAS) that there is alternate versions of the file and to check by a full integrity hash to define for which to associate with. // if there is a conflicting cas_id, the conficting file should be updated to have a larger cas_id as the field is unique, however this record is kept to tell the indexer (upon discovering this CAS) that there is alternate versions of the file and to check by a full integrity hash to define for which to associate with.
model FileConflict { model FileConflict {
original_file_id Int @unique original_object_id Int @unique
detactched_file_id Int @unique detactched_object_id Int @unique
@@map("file_conflicts") @@map("file_conflict")
} }
// keys allow us to know exactly which files can be decrypted with a given key // keys allow us to know exactly which files can be decrypted with a given key
@ -207,10 +197,10 @@ model Key {
// so we know which algorithm to use, can be null if user must select // so we know which algorithm to use, can be null if user must select
algorithm Int? @default(0) algorithm Int? @default(0)
files File[] objects Object[]
file_paths FilePath[] file_paths FilePath[]
@@map("keys") @@map("key")
} }
model MediaData { model MediaData {
@ -227,8 +217,8 @@ model MediaData {
codecs String? // eg: "h264,acc" codecs String? // eg: "h264,acc"
streams Int? streams Int?
// change this relation to File after testing // change this relation to Object after testing
files File? @relation(fields: [id], references: [id], onDelete: Cascade, onUpdate: Cascade) objects Object? @relation(fields: [id], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@map("media_data") @@map("media_data")
} }
@ -238,27 +228,27 @@ model Tag {
pub_id Bytes @unique pub_id Bytes @unique
name String? name String?
color String? color String?
total_files Int? @default(0) total_objects Int? @default(0)
redundancy_goal Int? @default(1) redundancy_goal Int? @default(1)
date_created DateTime @default(now()) date_created DateTime @default(now())
date_modified DateTime @default(now()) date_modified DateTime @default(now())
tag_files TagOnFile[] tag_objects TagOnObject[]
@@map("tags") @@map("tag")
} }
model TagOnFile { model TagOnObject {
date_created DateTime @default(now()) date_created DateTime @default(now())
tag_id Int tag_id Int
tag Tag @relation(fields: [tag_id], references: [id], onDelete: NoAction, onUpdate: NoAction) tag Tag @relation(fields: [tag_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
file_id Int object_id Int
file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction) object Object @relation(fields: [object_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
@@id([tag_id, file_id]) @@id([tag_id, object_id])
@@map("tags_on_file") @@map("tag_on_object")
} }
model Label { model Label {
@ -268,22 +258,22 @@ model Label {
date_created DateTime @default(now()) date_created DateTime @default(now())
date_modified DateTime @default(now()) date_modified DateTime @default(now())
label_files LabelOnFile[] label_objects LabelOnObject[]
@@map("labels") @@map("label")
} }
model LabelOnFile { model LabelOnObject {
date_created DateTime @default(now()) date_created DateTime @default(now())
label_id Int label_id Int
label Label @relation(fields: [label_id], references: [id], onDelete: NoAction, onUpdate: NoAction) label Label @relation(fields: [label_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
file_id Int object_id Int
file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction) object Object @relation(fields: [object_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
@@id([label_id, file_id]) @@id([label_id, object_id])
@@map("label_on_file") @@map("label_on_object")
} }
model Space { model Space {
@ -294,22 +284,22 @@ model Space {
date_created DateTime @default(now()) date_created DateTime @default(now())
date_modified DateTime @default(now()) date_modified DateTime @default(now())
files FileInSpace[] objects ObjectInSpace[]
@@map("spaces") @@map("space")
} }
model FileInSpace { model ObjectInSpace {
date_created DateTime @default(now()) date_created DateTime @default(now())
space_id Int space_id Int
space Space @relation(fields: [space_id], references: [id], onDelete: NoAction, onUpdate: NoAction) space Space @relation(fields: [space_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
file_id Int object_id Int
file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction) object Object @relation(fields: [object_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
@@id([space_id, file_id]) @@id([space_id, object_id])
@@map("file_in_space") @@map("object_in_space")
} }
model Job { model Job {
@ -329,7 +319,7 @@ model Job {
nodes Node @relation(fields: [node_id], references: [id], onDelete: Cascade, onUpdate: Cascade) nodes Node @relation(fields: [node_id], references: [id], onDelete: Cascade, onUpdate: Cascade)
@@map("jobs") @@map("job")
} }
model Album { model Album {
@ -341,22 +331,22 @@ model Album {
date_created DateTime @default(now()) date_created DateTime @default(now())
date_modified DateTime @default(now()) date_modified DateTime @default(now())
files FileInAlbum[] objects ObjectInAlbum[]
@@map("albums") @@map("album")
} }
model FileInAlbum { model ObjectInAlbum {
date_created DateTime @default(now()) date_created DateTime @default(now())
album_id Int album_id Int
album Album @relation(fields: [album_id], references: [id], onDelete: NoAction, onUpdate: NoAction) album Album @relation(fields: [album_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
file_id Int object_id Int
file File @relation(fields: [file_id], references: [id], onDelete: NoAction, onUpdate: NoAction) object Object @relation(fields: [object_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
@@id([album_id, file_id]) @@id([album_id, object_id])
@@map("files_in_albums") @@map("object_in_album")
} }
model Comment { model Comment {
@ -365,10 +355,10 @@ model Comment {
content String content String
date_created DateTime @default(now()) date_created DateTime @default(now())
date_modified DateTime @default(now()) date_modified DateTime @default(now())
file_id Int? object_id Int?
file File? @relation(fields: [file_id], references: [id]) object Object? @relation(fields: [object_id], references: [id])
@@map("comments") @@map("comment")
} }
model IndexerRule { model IndexerRule {
@ -381,7 +371,7 @@ model IndexerRule {
locations IndexerRulesInLocation[] locations IndexerRulesInLocation[]
@@map("indexer_rules") @@map("indexer_rule")
} }
model IndexerRulesInLocation { model IndexerRulesInLocation {
@ -394,5 +384,5 @@ model IndexerRulesInLocation {
indexer_rule IndexerRule @relation(fields: [indexer_rule_id], references: [id], onDelete: NoAction, onUpdate: NoAction) indexer_rule IndexerRule @relation(fields: [indexer_rule_id], references: [id], onDelete: NoAction, onUpdate: NoAction)
@@id([location_id, indexer_rule_id]) @@id([location_id, indexer_rule_id])
@@map("indexer_rules_in_location") @@map("indexer_rule_in_location")
} }

View file

@ -1,4 +1,4 @@
use crate::{invalidate_query, prisma::file}; use crate::{invalidate_query, prisma::object};
use rspc::Type; use rspc::Type;
use serde::Deserialize; use serde::Deserialize;
@ -26,8 +26,11 @@ pub(crate) fn mount() -> RouterBuilder {
.library_mutation("setNote", |_, args: SetNoteArgs, library| async move { .library_mutation("setNote", |_, args: SetNoteArgs, library| async move {
library library
.db .db
.file() .object()
.update(file::id::equals(args.id), vec![file::note::set(args.note)]) .update(
object::id::equals(args.id),
vec![object::note::set(args.note)],
)
.exec() .exec()
.await?; .await?;
@ -40,10 +43,10 @@ pub(crate) fn mount() -> RouterBuilder {
|_, args: SetFavoriteArgs, library| async move { |_, args: SetFavoriteArgs, library| async move {
library library
.db .db
.file() .object()
.update( .update(
file::id::equals(args.id), object::id::equals(args.id),
vec![file::favorite::set(args.favorite)], vec![object::favorite::set(args.favorite)],
) )
.exec() .exec()
.await?; .await?;
@ -56,8 +59,8 @@ pub(crate) fn mount() -> RouterBuilder {
.library_mutation("delete", |_, id: i32, library| async move { .library_mutation("delete", |_, id: i32, library| async move {
library library
.db .db
.file() .object()
.delete(file::id::equals(id)) .delete(object::id::equals(id))
.exec() .exec()
.await?; .await?;

View file

@ -57,7 +57,7 @@ pub(crate) fn mount() -> RouterBuilder {
let params = vec![ let params = vec![
id::set(1), // Each library is a database so only one of these ever exists id::set(1), // Each library is a database so only one of these ever exists
date_captured::set(Utc::now().into()), date_captured::set(Utc::now().into()),
total_file_count::set(0), total_object_count::set(0),
library_db_size::set(library_db_size.to_string()), library_db_size::set(library_db_size.to_string()),
total_bytes_used::set(0.to_string()), total_bytes_used::set(0.to_string()),
total_bytes_capacity::set(total_capacity.to_string()), total_bytes_capacity::set(total_capacity.to_string()),

View file

@ -6,7 +6,7 @@ use crate::{
scan_location, LocationCreateArgs, LocationError, LocationUpdateArgs, scan_location, LocationCreateArgs, LocationError, LocationUpdateArgs,
}, },
object::preview::THUMBNAIL_CACHE_DIR_NAME, object::preview::THUMBNAIL_CACHE_DIR_NAME,
prisma::{file, file_path, indexer_rule, indexer_rules_in_location, location, tag}, prisma::{file_path, indexer_rule, indexer_rules_in_location, location, object, tag},
}; };
use rspc::{self, ErrorCode, Type}; use rspc::{self, ErrorCode, Type};
@ -29,14 +29,14 @@ pub enum ExplorerContext {
// Space(object_in_space::Data), // Space(object_in_space::Data),
} }
file_path::include!(file_path_with_file { file }); file_path::include!(file_path_with_object { object });
file::include!(file_with_paths { paths }); object::include!(object_with_file_paths { file_paths });
#[derive(Serialize, Deserialize, Type, Debug)] #[derive(Serialize, Deserialize, Type, Debug)]
#[serde(tag = "type")] #[serde(tag = "type")]
pub enum ExplorerItem { pub enum ExplorerItem {
Path(Box<file_path_with_file::Data>), Path(Box<file_path_with_object::Data>),
Object(Box<file_with_paths::Data>), Object(Box<object_with_file_paths::Data>),
} }
#[derive(Clone, Serialize, Deserialize, Type, Debug)] #[derive(Clone, Serialize, Deserialize, Type, Debug)]
@ -100,7 +100,7 @@ pub(crate) fn mount() -> RouterBuilder {
file_path::location_id::equals(location.id), file_path::location_id::equals(location.id),
file_path::parent_id::equals(Some(directory.id)), file_path::parent_id::equals(Some(directory.id)),
]) ])
.include(file_path_with_file::include()) .include(file_path_with_object::include())
.exec() .exec()
.await?; .await?;
@ -109,16 +109,16 @@ pub(crate) fn mount() -> RouterBuilder {
items: file_paths items: file_paths
.into_iter() .into_iter()
.map(|mut file_path| { .map(|mut file_path| {
if let Some(file) = &mut file_path.file.as_mut() { if let Some(object) = &mut file_path.object.as_mut() {
// TODO: Use helper function to build this url as as the Rust file loading layer // TODO: Use helper function to build this url as as the Rust file loading layer
let thumb_path = library let thumb_path = library
.config() .config()
.data_directory() .data_directory()
.join(THUMBNAIL_CACHE_DIR_NAME) .join(THUMBNAIL_CACHE_DIR_NAME)
.join(&file.cas_id) .join(&object.cas_id)
.with_extension("webp"); .with_extension("webp");
file.has_thumbnail = thumb_path.exists(); object.has_thumbnail = thumb_path.exists();
} }
ExplorerItem::Path(Box::new(file_path)) ExplorerItem::Path(Box::new(file_path))
}) })

View file

@ -4,10 +4,10 @@ use tracing::info;
use uuid::Uuid; use uuid::Uuid;
use crate::{ use crate::{
api::locations::{file_with_paths, ExplorerContext, ExplorerData, ExplorerItem}, api::locations::{object_with_file_paths, ExplorerContext, ExplorerData, ExplorerItem},
invalidate_query, invalidate_query,
object::preview::THUMBNAIL_CACHE_DIR_NAME, object::preview::THUMBNAIL_CACHE_DIR_NAME,
prisma::{file, tag, tag_on_file}, prisma::{object, tag, tag_on_object},
}; };
use super::{utils::LibraryRequest, RouterBuilder}; use super::{utils::LibraryRequest, RouterBuilder};
@ -20,7 +20,7 @@ pub struct TagCreateArgs {
#[derive(Debug, Type, Deserialize)] #[derive(Debug, Type, Deserialize)]
pub struct TagAssignArgs { pub struct TagAssignArgs {
pub file_id: i32, pub object_id: i32,
pub tag_id: i32, pub tag_id: i32,
pub unassign: bool, pub unassign: bool,
} }
@ -52,32 +52,32 @@ pub(crate) fn mount() -> RouterBuilder {
let files: Vec<ExplorerItem> = library let files: Vec<ExplorerItem> = library
.db .db
.file() .object()
.find_many(vec![file::tags::some(vec![tag_on_file::tag_id::equals( .find_many(vec![object::tags::some(vec![
tag_id, tag_on_object::tag_id::equals(tag_id),
)])]) ])])
.include(file_with_paths::include()) .include(object_with_file_paths::include())
.exec() .exec()
.await? .await?
.into_iter() .into_iter()
.map(|mut file| { .map(|mut object| {
// sorry brendan // sorry brendan
// grab the first path and tac on the name // grab the first path and tac on the name
let oldest_path = &file.paths[0]; let oldest_path = &object.file_paths[0];
file.name = Some(oldest_path.name.clone()); object.name = Some(oldest_path.name.clone());
file.extension = oldest_path.extension.clone(); object.extension = oldest_path.extension.clone();
// a long term fix for this would be to have the indexer give the Object a name and extension, sacrificing its own and only store newly found Path names that differ from the Object name // a long term fix for this would be to have the indexer give the Object a name and extension, sacrificing its own and only store newly found Path names that differ from the Object name
let thumb_path = library let thumb_path = library
.config() .config()
.data_directory() .data_directory()
.join(THUMBNAIL_CACHE_DIR_NAME) .join(THUMBNAIL_CACHE_DIR_NAME)
.join(&file.cas_id) .join(&object.cas_id)
.with_extension("webp"); .with_extension("webp");
file.has_thumbnail = thumb_path.exists(); object.has_thumbnail = thumb_path.exists();
ExplorerItem::Object(Box::new(file)) ExplorerItem::Object(Box::new(object))
}) })
.collect(); .collect();
@ -88,12 +88,12 @@ pub(crate) fn mount() -> RouterBuilder {
items: files, items: files,
}) })
}) })
.library_query("getForFile", |_, file_id: i32, library| async move { .library_query("getForFile", |_, object_id: i32, library| async move {
Ok(library Ok(library
.db .db
.tag() .tag()
.find_many(vec![tag::tag_files::some(vec![ .find_many(vec![tag::tag_objects::some(vec![
tag_on_file::file_id::equals(file_id), tag_on_object::object_id::equals(object_id),
])]) ])])
.exec() .exec()
.await?) .await?)
@ -128,17 +128,17 @@ pub(crate) fn mount() -> RouterBuilder {
if args.unassign { if args.unassign {
library library
.db .db
.tag_on_file() .tag_on_object()
.delete(tag_on_file::tag_id_file_id(args.tag_id, args.file_id)) .delete(tag_on_object::tag_id_object_id(args.tag_id, args.object_id))
.exec() .exec()
.await?; .await?;
} else { } else {
library library
.db .db
.tag_on_file() .tag_on_object()
.create( .create(
tag::id::equals(args.tag_id), tag::id::equals(args.tag_id),
file::id::equals(args.file_id), object::id::equals(args.object_id),
vec![], vec![],
) )
.exec() .exec()

View file

@ -92,7 +92,7 @@ impl InvalidRequests {
#[allow(clippy::crate_in_macro_def)] #[allow(clippy::crate_in_macro_def)]
macro_rules! invalidate_query { macro_rules! invalidate_query {
($ctx:expr, $key:literal) => {{ ($ctx:expr, $key:literal) => {{
let _: &crate::library::LibraryContext = &$ctx; // Assert the context is the correct type let ctx: &crate::library::LibraryContext = &$ctx; // Assert the context is the correct type
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
{ {
@ -105,13 +105,15 @@ macro_rules! invalidate_query {
.push(crate::api::utils::InvalidationRequest { .push(crate::api::utils::InvalidationRequest {
key: $key, key: $key,
arg_ty: None, arg_ty: None,
macro_src: concat!(file!(), ":", line!()), macro_src: concat!(file!(), ":", line!()),
}) })
} }
} }
// The error are ignored here because they aren't mission critical. If they fail the UI might be outdated for a bit. // The error are ignored here because they aren't mission critical. If they fail the UI might be outdated for a bit.
crate::api::utils::InvalidateOperationEvent::dangerously_create($key, serde_json::Value::Null) ctx.emit(crate::api::CoreEvent::InvalidateOperation(
crate::api::utils::InvalidateOperationEvent::dangerously_create($key, serde_json::Value::Null)
))
}}; }};
($ctx:expr, $key:literal: $arg_ty:ty, $arg:expr $(,)?) => {{ ($ctx:expr, $key:literal: $arg_ty:ty, $arg:expr $(,)?) => {{
let _: $arg_ty = $arg; // Assert the type the user provided is correct let _: $arg_ty = $arg; // Assert the type the user provided is correct

View file

@ -1,7 +1,7 @@
use crate::{ use crate::{
job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext}, job::{JobError, JobReportUpdate, JobResult, JobState, StatefulJob, WorkerContext},
library::LibraryContext, library::LibraryContext,
prisma::{file, file_path, location}, prisma::{file_path, location, object},
}; };
use chrono::{DateTime, FixedOffset}; use chrono::{DateTime, FixedOffset};
@ -162,10 +162,10 @@ impl StatefulJob for FileIdentifierJob {
for file_path in &file_paths { for file_path in &file_paths {
// get the cas_id and extract metadata // get the cas_id and extract metadata
match assemble_object_metadata(&data.location_path, file_path).await { match assemble_object_metadata(&data.location_path, file_path).await {
Ok(file) => { Ok(object) => {
let cas_id = file.cas_id.clone(); let cas_id = object.cas_id.clone();
// create entry into chunks for created file data // create entry into chunks for created file data
chunk.insert(file_path.id, file); chunk.insert(file_path.id, object);
cas_lookup.insert(cas_id, file_path.id); cas_lookup.insert(cas_id, file_path.id);
} }
Err(e) => { Err(e) => {
@ -177,23 +177,23 @@ impl StatefulJob for FileIdentifierJob {
// find all existing files by cas id // find all existing files by cas id
let generated_cas_ids = chunk.values().map(|c| c.cas_id.clone()).collect(); let generated_cas_ids = chunk.values().map(|c| c.cas_id.clone()).collect();
let existing_files = db let existing_objects = db
.file() .object()
.find_many(vec![file::cas_id::in_vec(generated_cas_ids)]) .find_many(vec![object::cas_id::in_vec(generated_cas_ids)])
.exec() .exec()
.await?; .await?;
info!("Found {} existing files", existing_files.len()); info!("Found {} existing files", existing_objects.len());
for existing_file in &existing_files { for existing_object in &existing_objects {
if let Err(e) = db if let Err(e) = db
.file_path() .file_path()
.update( .update(
file_path::location_id_id( file_path::location_id_id(
state.init.location_id, state.init.location_id,
*cas_lookup.get(&existing_file.cas_id).unwrap(), *cas_lookup.get(&existing_object.cas_id).unwrap(),
), ),
vec![file_path::file_id::set(Some(existing_file.id))], vec![file_path::object_id::set(Some(existing_object.id))],
) )
.exec() .exec()
.await .await
@ -202,26 +202,26 @@ impl StatefulJob for FileIdentifierJob {
} }
} }
let existing_files_cas_ids = existing_files let existing_object_cas_ids = existing_objects
.iter() .iter()
.map(|file| file.cas_id.clone()) .map(|object| object.cas_id.clone())
.collect::<HashSet<_>>(); .collect::<HashSet<_>>();
// extract files that don't already exist in the database // extract objects that don't already exist in the database
let new_files = chunk let new_objects = chunk
.iter() .iter()
.map(|(_id, create_file)| create_file) .map(|(_id, create_file)| create_file)
.filter(|create_file| !existing_files_cas_ids.contains(&create_file.cas_id)) .filter(|create_file| !existing_object_cas_ids.contains(&create_file.cas_id))
.collect::<Vec<_>>(); .collect::<Vec<_>>();
if !new_files.is_empty() { if !new_objects.is_empty() {
// assemble prisma values for new unique files // assemble prisma values for new unique files
let mut values = Vec::with_capacity(new_files.len() * 3); let mut values = Vec::with_capacity(new_objects.len() * 3);
for file in &new_files { for object in &new_objects {
values.extend([ values.extend([
PrismaValue::String(file.cas_id.clone()), PrismaValue::String(object.cas_id.clone()),
PrismaValue::Int(file.size_in_bytes), PrismaValue::Int(object.size_in_bytes),
PrismaValue::DateTime(file.date_created), PrismaValue::DateTime(object.date_created),
]); ]);
} }
@ -230,9 +230,9 @@ impl StatefulJob for FileIdentifierJob {
let created_files: Vec<FileCreated> = db let created_files: Vec<FileCreated> = db
._query_raw(Raw::new( ._query_raw(Raw::new(
&format!( &format!(
"INSERT INTO files (cas_id, size_in_bytes, date_created) VALUES {} "INSERT INTO object (cas_id, size_in_bytes, date_created) VALUES {}
ON CONFLICT (cas_id) DO NOTHING RETURNING id, cas_id", ON CONFLICT (cas_id) DO NOTHING RETURNING id, cas_id",
vec!["({}, {}, {})"; new_files.len()].join(",") vec!["({}, {}, {})"; new_objects.len()].join(",")
), ),
values, values,
)) ))
@ -256,7 +256,7 @@ impl StatefulJob for FileIdentifierJob {
state.init.location_id, state.init.location_id,
*cas_lookup.get(&created_file.cas_id).unwrap(), *cas_lookup.get(&created_file.cas_id).unwrap(),
), ),
vec![file_path::file_id::set(Some(created_file.id))], vec![file_path::object_id::set(Some(created_file.id))],
) )
.exec() .exec()
.await .await
@ -305,7 +305,7 @@ impl StatefulJob for FileIdentifierJob {
fn orphan_path_filters(location_id: i32, file_path_id: Option<i32>) -> Vec<file_path::WhereParam> { fn orphan_path_filters(location_id: i32, file_path_id: Option<i32>) -> Vec<file_path::WhereParam> {
let mut params = vec![ let mut params = vec![
file_path::file_id::equals(None), file_path::object_id::equals(None),
file_path::is_dir::equals(false), file_path::is_dir::equals(false),
file_path::location_id::equals(location_id), file_path::location_id::equals(location_id),
]; ];
@ -329,7 +329,7 @@ async fn count_orphan_file_paths(
.db .db
.file_path() .file_path()
.count(vec![ .count(vec![
file_path::file_id::equals(None), file_path::object_id::equals(None),
file_path::is_dir::equals(false), file_path::is_dir::equals(false),
file_path::location_id::equals(location_id), file_path::location_id::equals(location_id),
]) ])

View file

@ -28,7 +28,7 @@ pub struct ObjectsForExplorer {
#[derive(Debug, Serialize, Deserialize, Type)] #[derive(Debug, Serialize, Deserialize, Type)]
pub enum ObjectData { pub enum ObjectData {
Object(Box<prisma::file::Data>), Object(Box<prisma::object::Data>),
Path(Box<prisma::file_path::Data>), Path(Box<prisma::file_path::Data>),
} }

View file

@ -38,7 +38,7 @@ pub struct ThumbnailJobState {
root_path: PathBuf, root_path: PathBuf,
} }
file_path::include!(file_path_with_file { file }); file_path::include!(file_path_with_object { object });
#[derive(Debug, Serialize, Deserialize, Clone, Copy)] #[derive(Debug, Serialize, Deserialize, Clone, Copy)]
enum ThumbnailJobStepKind { enum ThumbnailJobStepKind {
@ -49,7 +49,7 @@ enum ThumbnailJobStepKind {
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct ThumbnailJobStep { pub struct ThumbnailJobStep {
file: file_path_with_file::Data, file_path: file_path_with_object::Data,
kind: ThumbnailJobStepKind, kind: ThumbnailJobStepKind,
} }
@ -179,7 +179,7 @@ impl StatefulJob for ThumbnailJob {
let step = &state.steps[0]; let step = &state.steps[0];
ctx.progress(vec![JobReportUpdate::Message(format!( ctx.progress(vec![JobReportUpdate::Message(format!(
"Processing {}", "Processing {}",
step.file.materialized_path step.file_path.materialized_path
))]); ))]);
let data = state let data = state
@ -188,16 +188,16 @@ impl StatefulJob for ThumbnailJob {
.expect("critical error: missing data on job state"); .expect("critical error: missing data on job state");
// assemble the file path // assemble the file path
let path = data.root_path.join(&step.file.materialized_path); let path = data.root_path.join(&step.file_path.materialized_path);
trace!("image_file {:?}", step); trace!("image_file {:?}", step);
// get cas_id, if none found skip // get cas_id, if none found skip
let cas_id = match &step.file.file { let cas_id = match &step.file_path.object {
Some(f) => f.cas_id.clone(), Some(f) => f.cas_id.clone(),
_ => { _ => {
warn!( warn!(
"skipping thumbnail generation for {}", "skipping thumbnail generation for {}",
step.file.materialized_path step.file_path.materialized_path
); );
return Ok(()); return Ok(());
} }
@ -327,10 +327,10 @@ async fn get_files_by_extension(
.db .db
.file_path() .file_path()
.find_many(params) .find_many(params)
.include(file_path_with_file::include()) .include(file_path_with_object::include())
.exec() .exec()
.await? .await?
.into_iter() .into_iter()
.map(|file| ThumbnailJobStep { file, kind }) .map(|file_path| ThumbnailJobStep { file_path, kind })
.collect()) .collect())
} }

View file

@ -8,7 +8,7 @@ pub enum MigrationError {
#[error("An error occurred while initialising a new database connection: {0}")] #[error("An error occurred while initialising a new database connection: {0}")]
NewClient(#[from] Box<NewClientError>), NewClient(#[from] Box<NewClientError>),
#[cfg(debug_assertions)] #[cfg(debug_assertions)]
#[error("An error occurred during migartion: {0}")] #[error("An error occurred during migration: {0}")]
MigrateFailed(#[from] DbPushError), MigrateFailed(#[from] DbPushError),
#[cfg(not(debug_assertions))] #[cfg(not(debug_assertions))]
#[error("An error occurred during migration: {0}")] #[error("An error occurred during migration: {0}")]

View file

@ -3,11 +3,11 @@
"version": "0.0.0", "version": "0.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"prep": "pnpm db:gen && cargo test", "prep": "pnpm gen:prisma && cargo test",
"build": "turbo run build", "build": "turbo run build",
"landing-web": "turbo run dev --parallel --filter=@sd/landing --filter=@sd/web", "landing-web": "turbo run dev --parallel --filter=@sd/landing --filter=@sd/web",
"db:migrate": "cd crates && cargo prisma migrate dev", "gen:migrations": "cd core && cargo prisma migrate dev",
"db:gen": "cd core && cargo prisma generate", "gen:prisma": "cd core && cargo prisma generate",
"format": "prettier --config .prettierrc.cli.js --write \"**/*.{ts,tsx,html,scss,json,yml,md}\"", "format": "prettier --config .prettierrc.cli.js --write \"**/*.{ts,tsx,html,scss,json,yml,md}\"",
"desktop": "pnpm --filter @sd/desktop --", "desktop": "pnpm --filter @sd/desktop --",
"web": "pnpm --filter @sd/web -- ", "web": "pnpm --filter @sd/web -- ",

View file

@ -53,11 +53,9 @@ export type ExplorerContext = { type: "Location" } & Location | { type: "Tag" }
export interface ExplorerData { context: ExplorerContext, items: Array<ExplorerItem> } export interface ExplorerData { context: ExplorerContext, items: Array<ExplorerItem> }
export type ExplorerItem = { type: "Path" } & { id: number, is_dir: boolean, location_id: number, materialized_path: string, name: string, extension: string | null, file_id: number | null, parent_id: number | null, key_id: number | null, date_created: string, date_modified: string, date_indexed: string, file: File | null } | { type: "Object" } & { id: number, cas_id: string, integrity_checksum: string | null, name: string | null, extension: string | null, kind: number, size_in_bytes: string, 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_modified: string, date_indexed: string, paths: Array<FilePath> } export type ExplorerItem = { type: "Path" } & { id: number, is_dir: boolean, location_id: number, materialized_path: string, name: string, extension: string | null, object_id: number | null, parent_id: number | null, key_id: number | null, date_created: string, date_modified: string, date_indexed: string, object: Object | null } | { type: "Object" } & { id: number, cas_id: string, integrity_checksum: string | null, name: string | null, extension: string | null, kind: number, size_in_bytes: string, 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_modified: string, date_indexed: string, file_paths: Array<FilePath> }
export interface File { id: number, cas_id: string, integrity_checksum: string | null, name: string | null, extension: string | null, kind: number, size_in_bytes: string, 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_modified: string, date_indexed: string } export interface FilePath { id: number, is_dir: boolean, location_id: number, materialized_path: string, name: string, extension: string | null, object_id: number | null, parent_id: number | null, key_id: number | null, date_created: string, date_modified: string, date_indexed: string }
export interface FilePath { id: number, is_dir: boolean, location_id: number, materialized_path: string, name: string, extension: string | null, file_id: number | null, parent_id: number | null, key_id: number | null, date_created: string, date_modified: string, date_indexed: string }
export interface GenerateThumbsForLocationArgs { id: number, path: string } export interface GenerateThumbsForLocationArgs { id: number, path: string }
@ -93,17 +91,19 @@ export interface NodeConfig { version: string | null, id: string, name: string,
export interface NodeState { version: string | null, id: string, name: string, p2p_port: number | null, data_path: string } export interface NodeState { version: string | null, id: string, name: string, p2p_port: number | null, data_path: string }
export interface Object { id: number, cas_id: string, integrity_checksum: string | null, name: string | null, extension: string | null, kind: number, size_in_bytes: string, 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_modified: string, date_indexed: string }
export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent" export type RuleKind = "AcceptFilesByGlob" | "RejectFilesByGlob" | "AcceptIfChildrenDirectoriesArePresent" | "RejectIfChildrenDirectoriesArePresent"
export interface SetFavoriteArgs { id: number, favorite: boolean } export interface SetFavoriteArgs { id: number, favorite: boolean }
export interface SetNoteArgs { id: number, note: string | null } export interface SetNoteArgs { id: number, note: string | null }
export interface Statistics { id: number, date_captured: string, total_file_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string } export interface Statistics { id: number, date_captured: string, total_object_count: number, library_db_size: string, total_bytes_used: string, total_bytes_capacity: string, total_unique_bytes: string, total_bytes_free: string, preview_media_bytes: string }
export interface Tag { id: number, pub_id: Array<number>, name: string | null, color: string | null, total_files: number | null, redundancy_goal: number | null, date_created: string, date_modified: string } export interface Tag { id: number, pub_id: Array<number>, name: string | null, color: string | null, total_objects: number | null, redundancy_goal: number | null, date_created: string, date_modified: string }
export interface TagAssignArgs { file_id: number, tag_id: number, unassign: boolean } export interface TagAssignArgs { object_id: number, tag_id: number, unassign: boolean }
export interface TagCreateArgs { name: string, color: string } export interface TagCreateArgs { name: string, color: string }

View file

@ -1,5 +1,5 @@
import { useCurrentLibrary, useInvalidateQuery } from '@sd/client'; import { useCurrentLibrary, useInvalidateQuery } from '@sd/client';
import { Route, Routes } from 'react-router-dom'; import { Navigate, Route, Routes } from 'react-router-dom';
import { AppLayout } from './AppLayout'; import { AppLayout } from './AppLayout';
import { NotFound } from './NotFound'; import { NotFound } from './NotFound';
@ -10,7 +10,6 @@ import { DebugScreen } from './screens/Debug';
import { LocationExplorer } from './screens/LocationExplorer'; import { LocationExplorer } from './screens/LocationExplorer';
import { OverviewScreen } from './screens/Overview'; import { OverviewScreen } from './screens/Overview';
import { PhotosScreen } from './screens/Photos'; import { PhotosScreen } from './screens/Photos';
import { RedirectPage } from './screens/Redirect';
import { TagExplorer } from './screens/TagExplorer'; import { TagExplorer } from './screens/TagExplorer';
import { SettingsScreen } from './screens/settings/Settings'; import { SettingsScreen } from './screens/settings/Settings';
import AppearanceSettings from './screens/settings/client/AppearanceSettings'; import AppearanceSettings from './screens/settings/client/AppearanceSettings';
@ -49,12 +48,12 @@ export function AppRouter() {
<Route <Route
path="*" path="*"
element={ element={
<h1 className="text-white p-4">Please select or create a library in the sidebar.</h1> <h1 className="p-4 text-white">Please select or create a library in the sidebar.</h1>
} }
/> />
) : ( ) : (
<> <>
<Route index element={<RedirectPage to="/overview" />} /> <Route index element={<Navigate to="/overview" />} />
<Route path="overview" element={<OverviewScreen />} /> <Route path="overview" element={<OverviewScreen />} />
<Route path="content" element={<ContentScreen />} /> <Route path="content" element={<ContentScreen />} />
<Route path="photos" element={<PhotosScreen />} /> <Route path="photos" element={<PhotosScreen />} />

View file

@ -34,7 +34,7 @@ const AssignTagMenuItems = (props: { objectId: number }) => {
assignTag({ assignTag({
tag_id: tag.id, tag_id: tag.id,
file_id: props.objectId, object_id: props.objectId,
unassign: active unassign: active
}); });
}} }}

View file

@ -21,7 +21,7 @@ function FileItem({ data, selected, index, ...rest }: Props) {
return ( return (
<div <div
onContextMenu={(e) => { onContextMenu={(e) => {
const objectId = isObject(data) ? data.id : data.file?.id; const objectId = isObject(data) ? data.id : data.object?.id;
if (objectId != undefined) { if (objectId != undefined) {
getExplorerStore().contextMenuObjectId = objectId; getExplorerStore().contextMenuObjectId = objectId;
if (index != undefined) { if (index != undefined) {

View file

@ -23,7 +23,7 @@ export default function FileThumb({ data, ...props }: Props) {
if (isPath(data) && data.is_dir) if (isPath(data) && data.is_dir)
return <Folder className={props.iconClassNames} size={props.size * 0.7} />; return <Folder className={props.iconClassNames} size={props.size * 0.7} />;
const cas_id = isObject(data) ? data.cas_id : data.file?.cas_id; const cas_id = isObject(data) ? data.cas_id : data.object?.cas_id;
if (cas_id) { if (cas_id) {
// this won't work // this won't work
@ -32,7 +32,7 @@ export default function FileThumb({ data, ...props }: Props) {
const has_thumbnail = isObject(data) const has_thumbnail = isObject(data)
? data.has_thumbnail ? data.has_thumbnail
: isPath(data) : isPath(data)
? data.file?.has_thumbnail ? data.object?.has_thumbnail
: new_thumbnail; : new_thumbnail;
const url = platform.getThumbnailUrlById(cas_id); const url = platform.getThumbnailUrlById(cas_id);

View file

@ -1,6 +1,6 @@
import { ShareIcon } from '@heroicons/react/24/solid'; import { ShareIcon } from '@heroicons/react/24/solid';
import { useLibraryQuery } from '@sd/client'; import { useLibraryQuery } from '@sd/client';
import { ExplorerContext, ExplorerItem, File, FilePath, Location } from '@sd/client'; import { ExplorerContext, ExplorerItem, FilePath, Location, Object } from '@sd/client';
import { Button, TextArea } from '@sd/ui'; import { Button, TextArea } from '@sd/ui';
import clsx from 'clsx'; import clsx from 'clsx';
import moment from 'moment'; import moment from 'moment';
@ -24,7 +24,7 @@ interface Props {
export const Inspector = (props: Props) => { export const Inspector = (props: Props) => {
const is_dir = props.data?.type === 'Path' ? props.data.is_dir : false; const is_dir = props.data?.type === 'Path' ? props.data.is_dir : false;
const objectData = isObject(props.data) ? props.data : props.data.file; const objectData = isObject(props.data) ? props.data : props.data.object;
// this prevents the inspector from fetching data when the user is navigating quickly // this prevents the inspector from fetching data when the user is navigating quickly
const [readyToFetch, setReadyToFetch] = useState(false); const [readyToFetch, setReadyToFetch] = useState(false);

View file

@ -198,6 +198,5 @@ const WrappedItem: React.FC<WrappedItemProps> = ({ item, index, isSelected, kind
// selected={isSelected} // selected={isSelected}
// /> // />
// ); // );
// // eslint-disable-next-line react-hooks/exhaustive-deps
// }, [item, index, isSelected]); // }, [item, index, isSelected]);
}; };

View file

@ -1,11 +1,11 @@
import { useLibraryMutation } from '@sd/client'; import { useLibraryMutation } from '@sd/client';
import { File } from '@sd/client'; import { Object as SDObject } from '@sd/client';
import { Button } from '@sd/ui'; import { Button } from '@sd/ui';
import { Heart } from 'phosphor-react'; import { Heart } from 'phosphor-react';
import { useEffect, useState } from 'react'; import { useEffect, useState } from 'react';
interface Props { interface Props {
data: File; data: SDObject;
} }
export default function FavoriteButton(props: Props) { export default function FavoriteButton(props: Props) {

View file

@ -1,6 +1,5 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { useLibraryMutation } from '@sd/client'; import { useLibraryMutation } from '@sd/client';
import { File } from '@sd/client'; import { Object as SDObject } from '@sd/client';
import { TextArea } from '@sd/ui'; import { TextArea } from '@sd/ui';
import { debounce } from 'lodash'; import { debounce } from 'lodash';
import { useCallback, useState } from 'react'; import { useCallback, useState } from 'react';
@ -9,25 +8,28 @@ import { Divider } from './Divider';
import { MetaItem } from './MetaItem'; import { MetaItem } from './MetaItem';
interface Props { interface Props {
data: File; data: SDObject;
} }
export default function Note(props: Props) { export default function Note(props: Props) {
// notes are cached in a store by their file id // notes are cached in a store by their file id
// this is so we can ensure every note has been sent to Rust even // this is so we can ensure every note has been sent to Rust even
// when quickly navigating files, which cancels update function // when quickly navigating files, which cancels update function
const [note, setNote] = useState((props.data as File)?.note || ''); const [note, setNote] = useState(props.data.note || '');
const { mutate: fileSetNote } = useLibraryMutation('files.setNote'); const { mutate: fileSetNote } = useLibraryMutation('files.setNote');
const debouncedNote = useCallback( const debouncedNote = useCallback(
debounce((note: string) => { (note: string) =>
fileSetNote({ debounce(
id: props.data.id, () =>
note fileSetNote({
}); id: props.data.id,
}, 2000), note
[props.data.id] }),
2000
),
[props.data.id, fileSetNote]
); );
// when input is updated, cache note // when input is updated, cache note

View file

@ -1,4 +1,4 @@
import { ExplorerItem, File, FilePath } from '@sd/client'; import { ExplorerItem, FilePath } from '@sd/client';
export function isPath(item: ExplorerItem): item is Extract<ExplorerItem, { type: 'Path' }> { export function isPath(item: ExplorerItem): item is Extract<ExplorerItem, { type: 'Path' }> {
return item.type === 'Path'; return item.type === 'Path';

View file

@ -186,10 +186,10 @@ export function Sidebar() {
buttonText={isLoadingLibraries ? 'Loading...' : library ? library.config.name : ' '} buttonText={isLoadingLibraries ? 'Loading...' : library ? library.config.name : ' '}
buttonTextClassName={library === null || isLoadingLibraries ? 'text-gray-300' : undefined} buttonTextClassName={library === null || isLoadingLibraries ? 'text-gray-300' : undefined}
items={[ items={[
libraries?.map((library) => ({ libraries?.map((lib) => ({
name: library.config.name, name: lib.config.name,
selected: library.uuid === library?.uuid, selected: lib.uuid === library?.uuid,
onPress: () => switchLibrary(library.uuid) onPress: () => switchLibrary(lib.uuid)
})) || [], })) || [],
[ [
{ {

View file

@ -53,13 +53,12 @@ const useCounter = ({ name, start = 0, end, duration = 2, saveState = true }: Us
duration, duration,
easing: 'easeOutCubic' easing: 'easeOutCubic'
}); });
useEffect(() => { useEffect(() => {
if (saveState && value == end) { if (saveState && value == end) {
setLastValue(name, end); setLastValue(name, end);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [end, name, saveState, setLastValue, value]);
}, [end, value]);
if (start === end) return end; if (start === end) return end;

View file

@ -1,4 +1,3 @@
/* eslint-disable react-hooks/exhaustive-deps */
import { getExplorerStore, useCurrentLibrary, useLibraryQuery } from '@sd/client'; import { getExplorerStore, useCurrentLibrary, useLibraryQuery } from '@sd/client';
import { useEffect } from 'react'; import { useEffect } from 'react';
import { useParams, useSearchParams } from 'react-router-dom'; import { useParams, useSearchParams } from 'react-router-dom';

View file

@ -100,42 +100,22 @@ export const OverviewScreen = () => {
// get app props from context // get app props from context
useEffect(() => { useEffect(() => {
if (platform.demoMode === true) { const newStatistics: OverviewStats = {
if (!Object.entries(overviewStats).length) total_bytes_capacity: '0',
setOverviewStats({ preview_media_bytes: '0',
total_bytes_capacity: '8093333345230', library_db_size: '0',
preview_media_bytes: '2304387532', total_object_count: '0',
library_db_size: '83345230', total_bytes_free: '0',
total_file_count: '20342345', total_bytes_used: '0',
total_bytes_free: '89734502034', total_unique_bytes: '0'
total_bytes_used: '8093333345230', };
total_unique_bytes: '9347397'
});
} else {
const newStatistics: OverviewStats = {
total_bytes_capacity: '0',
preview_media_bytes: '0',
library_db_size: '0',
total_file_count: '0',
total_bytes_free: '0',
total_bytes_used: '0',
total_unique_bytes: '0'
};
Object.entries((libraryStatistics as Statistics) || {}).forEach(([key, value]) => { Object.entries((libraryStatistics as Statistics) || {}).forEach(([key, value]) => {
newStatistics[key as keyof Statistics] = `${value}`; newStatistics[key as keyof Statistics] = `${value}`;
}); });
setOverviewStats(newStatistics); setOverviewStats(newStatistics);
} }, [platform, libraryStatistics, setOverviewStats]);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [platform, libraryStatistics]);
// useEffect(() => {
// setTimeout(() => {
// setOverviewStat('total_bytes_capacity', '4093333345230');
// }, 2000);
// }, [overviewStats]);
const displayableStatItems = Object.keys(StatItemNames) as unknown as keyof typeof StatItemNames; const displayableStatItems = Object.keys(StatItemNames) as unknown as keyof typeof StatItemNames;

View file

@ -1,19 +0,0 @@
import { useEffect } from 'react';
import { useNavigate } from 'react-router';
export interface RedirectPageProps {
to: string;
}
export const RedirectPage: React.FC<RedirectPageProps> = (props) => {
const { to: destination } = props;
const navigate = useNavigate();
useEffect(() => {
navigate(destination);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
return null;
};

View file

@ -35,16 +35,14 @@ export default function LibraryGeneralSettings() {
}); });
} }
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [nameDebounced, descriptionDebounced, library, editLibrary]);
}, [nameDebounced, descriptionDebounced]);
useEffect(() => { useEffect(() => {
if (library) { if (library) {
setName(library.config.name); setName(library.config.name);
setDescription(library.config.description); setDescription(library.config.description);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps }, [libraries, library]);
}, [libraries]);
useEffect(() => { useEffect(() => {
if (library) { if (library) {
@ -52,7 +50,6 @@ export default function LibraryGeneralSettings() {
setName(library.config.name); setName(library.config.name);
setDescription(library.config.description); setDescription(library.config.description);
} }
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [library]); }, [library]);
useEffect(() => { useEffect(() => {

View file

@ -1,5 +1,5 @@
import { TrashIcon } from '@heroicons/react/24/outline'; import { TrashIcon } from '@heroicons/react/24/outline';
import { useLibraryMutation, useLibraryQuery } from '@sd/client'; import { Tag, useLibraryMutation, useLibraryQuery } from '@sd/client';
import { TagUpdateArgs } from '@sd/client'; import { TagUpdateArgs } from '@sd/client';
import { Button, Input } from '@sd/ui'; import { Button, Input } from '@sd/ui';
import clsx from 'clsx'; import clsx from 'clsx';
@ -23,11 +23,7 @@ export default function TagsSettings() {
const { data: tags } = useLibraryQuery(['tags.list']); const { data: tags } = useLibraryQuery(['tags.list']);
const [selectedTag, setSelectedTag] = useState<null | number>(null); const [selectedTag, setSelectedTag] = useState<null | Tag>(tags?.[0] ?? null);
const currentTag = useMemo(() => {
return tags?.find((t) => t.id === selectedTag);
}, [tags, selectedTag]);
const { mutate: createTag, isLoading } = useLibraryMutation('tags.create', { const { mutate: createTag, isLoading } = useLibraryMutation('tags.create', {
onError: (e) => { onError: (e) => {
@ -38,25 +34,27 @@ export default function TagsSettings() {
} }
}); });
const { mutate: updateTag } = useLibraryMutation('tags.update'); const updateTag = useLibraryMutation('tags.update');
const { mutate: deleteTag, isLoading: tagDeleteLoading } = useLibraryMutation('tags.delete'); const deleteTag = useLibraryMutation('tags.delete', {
onSuccess: () => {
// set default selected tag setSelectedTag(null);
useEffect(() => { }
if (!currentTag && tags?.length) setSelectedTag(tags[0].id);
}, [currentTag, tags]);
useEffect(() => {
reset(currentTag);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [currentTag]);
const { register, handleSubmit, watch, reset, control } = useForm({
defaultValues: currentTag as TagUpdateArgs
}); });
const submitTagUpdate = handleSubmit((data) => updateTag(data)); const { register, handleSubmit, watch, reset, control } = useForm({
defaultValues: selectedTag as TagUpdateArgs
});
const setTag = useCallback(
(tag: Tag | null) => {
if (tag) reset(tag);
setSelectedTag(tag);
},
[setSelectedTag, reset]
);
const submitTagUpdate = handleSubmit((data) => updateTag.mutate(data));
// eslint-disable-next-line react-hooks/exhaustive-deps // eslint-disable-next-line react-hooks/exhaustive-deps
const autoUpdateTag = useCallback(useDebounce(submitTagUpdate, 500), []); const autoUpdateTag = useCallback(useDebounce(submitTagUpdate, 500), []);
@ -113,11 +111,11 @@ export default function TagsSettings() {
<div className="flex flex-wrap gap-2 m-1"> <div className="flex flex-wrap gap-2 m-1">
{tags?.map((tag) => ( {tags?.map((tag) => (
<div <div
onClick={() => setSelectedTag(tag.id === selectedTag ? null : tag.id)} onClick={() => setTag(tag.id === selectedTag?.id ? null : tag)}
key={tag.id} key={tag.id}
className={clsx( className={clsx(
'flex items-center rounded px-1.5 py-0.5', 'flex items-center rounded px-1.5 py-0.5',
selectedTag === tag.id && 'ring' selectedTag?.id === tag.id && 'ring'
)} )}
style={{ backgroundColor: tag.color + 'CC' }} style={{ backgroundColor: tag.color + 'CC' }}
> >
@ -126,7 +124,7 @@ export default function TagsSettings() {
))} ))}
</div> </div>
</Card> </Card>
{currentTag ? ( {selectedTag ? (
<form onSubmit={submitTagUpdate}> <form onSubmit={submitTagUpdate}>
<div className="flex flex-row mb-10 space-x-3"> <div className="flex flex-row mb-10 space-x-3">
<div className="flex flex-col"> <div className="flex flex-col">
@ -159,9 +157,9 @@ export default function TagsSettings() {
title="Delete Tag" title="Delete Tag"
description="Are you sure you want to delete this tag? This cannot be undone and tagged files will be unlinked." description="Are you sure you want to delete this tag? This cannot be undone and tagged files will be unlinked."
ctaAction={() => { ctaAction={() => {
deleteTag(currentTag.id); deleteTag.mutate(selectedTag.id);
}} }}
loading={tagDeleteLoading} loading={deleteTag.isLoading}
ctaDanger ctaDanger
ctaLabel="Delete" ctaLabel="Delete"
trigger={ trigger={