diff --git a/src/lib/server/db/file.ts b/src/lib/server/db/file.ts index 0a76b6d..db450c7 100644 --- a/src/lib/server/db/file.ts +++ b/src/lib/server/db/file.ts @@ -163,16 +163,24 @@ export const unregisterDirectory = async (userId: number, directoryId: number) = .setIsolationLevel("repeatable read") // TODO: Sufficient? .execute(async (trx) => { const unregisterFiles = async (parentId: number) => { - return await trx + const files = await trx + .selectFrom("file") + .leftJoin("thumbnail", "file.id", "thumbnail.file_id") + .select(["file.id", "file.path", "thumbnail.path as thumbnailPath"]) + .where("file.parent_id", "=", parentId) + .where("file.user_id", "=", userId) + .forUpdate("file") + .execute(); + await trx .deleteFrom("file") .where("parent_id", "=", parentId) .where("user_id", "=", userId) - .returning(["id", "path"]) .execute(); + return files; }; const unregisterDirectoryRecursively = async ( directoryId: number, - ): Promise<{ id: number; path: string }[]> => { + ): Promise<{ id: number; path: string; thumbnailPath: string | null }[]> => { const files = await unregisterFiles(directoryId); const subDirectories = await trx .selectFrom("directory") @@ -417,16 +425,22 @@ export const setFileEncName = async ( }; export const unregisterFile = async (userId: number, fileId: number) => { - const file = await db - .deleteFrom("file") - .where("id", "=", fileId) - .where("user_id", "=", userId) - .returning("path") - .executeTakeFirst(); - if (!file) { - throw new IntegrityError("File not found"); - } - return { path: file.path }; + return await db.transaction().execute(async (trx) => { + const file = await trx + .selectFrom("file") + .leftJoin("thumbnail", "file.id", "thumbnail.file_id") + .select(["file.path", "thumbnail.path as thumbnailPath"]) + .where("file.id", "=", fileId) + .where("file.user_id", "=", userId) + .forUpdate("file") + .executeTakeFirst(); + if (!file) { + throw new IntegrityError("File not found"); + } + + await trx.deleteFrom("file").where("id", "=", fileId).execute(); + return file; + }); }; export const addFileToCategory = async (fileId: number, categoryId: number) => { diff --git a/src/lib/server/db/media.ts b/src/lib/server/db/media.ts index 360ed49..8386ffc 100644 --- a/src/lib/server/db/media.ts +++ b/src/lib/server/db/media.ts @@ -37,7 +37,7 @@ export const updateFileThumbnail = async ( const thumbnail = await trx .selectFrom("thumbnail") - .select("path as old_path") + .select("path as oldPath") .where("file_id", "=", fileId) .limit(1) .forUpdate() @@ -60,7 +60,7 @@ export const updateFileThumbnail = async ( }), ) .execute(); - return thumbnail?.old_path; + return thumbnail?.oldPath ?? null; }); }; diff --git a/src/lib/server/services/directory.ts b/src/lib/server/services/directory.ts index 2525069..fdab587 100644 --- a/src/lib/server/services/directory.ts +++ b/src/lib/server/services/directory.ts @@ -34,12 +34,19 @@ export const getDirectoryInformation = async (userId: number, directoryId: Direc }; }; +const safeUnlink = async (path: string | null) => { + if (path) { + await unlink(path).catch(console.error); + } +}; + export const deleteDirectory = async (userId: number, directoryId: number) => { try { const files = await unregisterDirectory(userId, directoryId); return { - files: files.map(({ id, path }) => { - unlink(path); // Intended + files: files.map(({ id, path, thumbnailPath }) => { + safeUnlink(path); // Intended + safeUnlink(thumbnailPath); // Intended return id; }), }; diff --git a/src/lib/server/services/file.ts b/src/lib/server/services/file.ts index 6f5af03..0e20676 100644 --- a/src/lib/server/services/file.ts +++ b/src/lib/server/services/file.ts @@ -45,10 +45,17 @@ export const getFileInformation = async (userId: number, fileId: number) => { }; }; +const safeUnlink = async (path: string | null) => { + if (path) { + await unlink(path).catch(console.error); + } +}; + export const deleteFile = async (userId: number, fileId: number) => { try { - const { path } = await unregisterFile(userId, fileId); - unlink(path); // Intended + const { path, thumbnailPath } = await unregisterFile(userId, fileId); + safeUnlink(path); // Intended + safeUnlink(thumbnailPath); // Intended } catch (e) { if (e instanceof IntegrityError && e.message === "File not found") { error(404, "Invalid file id"); @@ -126,9 +133,7 @@ export const uploadFileThumbnail = async ( await pipeline(encContentStream, createWriteStream(path, { flags: "wx", mode: 0o600 })); const oldPath = await updateFileThumbnail(userId, fileId, dekVersion, path, encContentIv); - if (oldPath) { - safeUnlink(oldPath); // Intended - } + safeUnlink(oldPath); // Intended } catch (e) { await safeUnlink(path); @@ -157,10 +162,6 @@ export const scanMissingFileThumbnails = async (userId: number) => { return { files: fileIds }; }; -const safeUnlink = async (path: string) => { - await unlink(path).catch(console.error); -}; - export const uploadFile = async ( params: Omit, encContentStream: Readable, diff --git a/src/routes/(main)/directory/[[id]]/service.svelte.ts b/src/routes/(main)/directory/[[id]]/service.svelte.ts index d4a0556..b29630a 100644 --- a/src/routes/(main)/directory/[[id]]/service.svelte.ts +++ b/src/routes/(main)/directory/[[id]]/service.svelte.ts @@ -2,7 +2,13 @@ import { getContext, setContext } from "svelte"; import { callGetApi, callPostApi } from "$lib/hooks"; import { storeHmacSecrets } from "$lib/indexedDB"; import { generateDataKey, wrapDataKey, unwrapHmacSecret, encryptString } from "$lib/modules/crypto"; -import { storeFileCache, deleteFileCache, storeFileThumbnail, uploadFile } from "$lib/modules/file"; +import { + storeFileCache, + deleteFileCache, + storeFileThumbnail, + deleteFileThumbnail, + uploadFile, +} from "$lib/modules/file"; import type { DirectoryRenameRequest, DirectoryCreateRequest, @@ -114,10 +120,12 @@ export const requestEntryDeletion = async (entry: SelectedEntry) => { if (entry.type === "directory") { const { deletedFiles }: DirectoryDeleteResponse = await res.json(); - await Promise.all(deletedFiles.map(deleteFileCache)); + await Promise.all( + deletedFiles.flatMap((fileId) => [deleteFileCache(fileId), deleteFileThumbnail(fileId)]), + ); return true; } else { - await deleteFileCache(entry.id); + await Promise.all([deleteFileCache(entry.id), deleteFileThumbnail(entry.id)]); return true; } };