파일을 삭제할 경우 서버와 클라이언트에 저장된 썸네일을 함께 삭제하도록 개선

This commit is contained in:
static
2025-07-06 17:38:04 +09:00
parent 781642fed6
commit 8975a0200d
5 changed files with 59 additions and 29 deletions

View File

@@ -163,16 +163,24 @@ export const unregisterDirectory = async (userId: number, directoryId: number) =
.setIsolationLevel("repeatable read") // TODO: Sufficient? .setIsolationLevel("repeatable read") // TODO: Sufficient?
.execute(async (trx) => { .execute(async (trx) => {
const unregisterFiles = async (parentId: number) => { 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") .deleteFrom("file")
.where("parent_id", "=", parentId) .where("parent_id", "=", parentId)
.where("user_id", "=", userId) .where("user_id", "=", userId)
.returning(["id", "path"])
.execute(); .execute();
return files;
}; };
const unregisterDirectoryRecursively = async ( const unregisterDirectoryRecursively = async (
directoryId: number, directoryId: number,
): Promise<{ id: number; path: string }[]> => { ): Promise<{ id: number; path: string; thumbnailPath: string | null }[]> => {
const files = await unregisterFiles(directoryId); const files = await unregisterFiles(directoryId);
const subDirectories = await trx const subDirectories = await trx
.selectFrom("directory") .selectFrom("directory")
@@ -417,16 +425,22 @@ export const setFileEncName = async (
}; };
export const unregisterFile = async (userId: number, fileId: number) => { export const unregisterFile = async (userId: number, fileId: number) => {
const file = await db return await db.transaction().execute(async (trx) => {
.deleteFrom("file") const file = await trx
.where("id", "=", fileId) .selectFrom("file")
.where("user_id", "=", userId) .leftJoin("thumbnail", "file.id", "thumbnail.file_id")
.returning("path") .select(["file.path", "thumbnail.path as thumbnailPath"])
.where("file.id", "=", fileId)
.where("file.user_id", "=", userId)
.forUpdate("file")
.executeTakeFirst(); .executeTakeFirst();
if (!file) { if (!file) {
throw new IntegrityError("File not found"); throw new IntegrityError("File not found");
} }
return { path: file.path };
await trx.deleteFrom("file").where("id", "=", fileId).execute();
return file;
});
}; };
export const addFileToCategory = async (fileId: number, categoryId: number) => { export const addFileToCategory = async (fileId: number, categoryId: number) => {

View File

@@ -37,7 +37,7 @@ export const updateFileThumbnail = async (
const thumbnail = await trx const thumbnail = await trx
.selectFrom("thumbnail") .selectFrom("thumbnail")
.select("path as old_path") .select("path as oldPath")
.where("file_id", "=", fileId) .where("file_id", "=", fileId)
.limit(1) .limit(1)
.forUpdate() .forUpdate()
@@ -60,7 +60,7 @@ export const updateFileThumbnail = async (
}), }),
) )
.execute(); .execute();
return thumbnail?.old_path; return thumbnail?.oldPath ?? null;
}); });
}; };

View File

@@ -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) => { export const deleteDirectory = async (userId: number, directoryId: number) => {
try { try {
const files = await unregisterDirectory(userId, directoryId); const files = await unregisterDirectory(userId, directoryId);
return { return {
files: files.map(({ id, path }) => { files: files.map(({ id, path, thumbnailPath }) => {
unlink(path); // Intended safeUnlink(path); // Intended
safeUnlink(thumbnailPath); // Intended
return id; return id;
}), }),
}; };

View File

@@ -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) => { export const deleteFile = async (userId: number, fileId: number) => {
try { try {
const { path } = await unregisterFile(userId, fileId); const { path, thumbnailPath } = await unregisterFile(userId, fileId);
unlink(path); // Intended safeUnlink(path); // Intended
safeUnlink(thumbnailPath); // Intended
} catch (e) { } catch (e) {
if (e instanceof IntegrityError && e.message === "File not found") { if (e instanceof IntegrityError && e.message === "File not found") {
error(404, "Invalid file id"); error(404, "Invalid file id");
@@ -126,9 +133,7 @@ export const uploadFileThumbnail = async (
await pipeline(encContentStream, createWriteStream(path, { flags: "wx", mode: 0o600 })); await pipeline(encContentStream, createWriteStream(path, { flags: "wx", mode: 0o600 }));
const oldPath = await updateFileThumbnail(userId, fileId, dekVersion, path, encContentIv); const oldPath = await updateFileThumbnail(userId, fileId, dekVersion, path, encContentIv);
if (oldPath) {
safeUnlink(oldPath); // Intended safeUnlink(oldPath); // Intended
}
} catch (e) { } catch (e) {
await safeUnlink(path); await safeUnlink(path);
@@ -157,10 +162,6 @@ export const scanMissingFileThumbnails = async (userId: number) => {
return { files: fileIds }; return { files: fileIds };
}; };
const safeUnlink = async (path: string) => {
await unlink(path).catch(console.error);
};
export const uploadFile = async ( export const uploadFile = async (
params: Omit<NewFile, "path" | "encContentHash">, params: Omit<NewFile, "path" | "encContentHash">,
encContentStream: Readable, encContentStream: Readable,

View File

@@ -2,7 +2,13 @@ import { getContext, setContext } from "svelte";
import { callGetApi, callPostApi } from "$lib/hooks"; import { callGetApi, callPostApi } from "$lib/hooks";
import { storeHmacSecrets } from "$lib/indexedDB"; import { storeHmacSecrets } from "$lib/indexedDB";
import { generateDataKey, wrapDataKey, unwrapHmacSecret, encryptString } from "$lib/modules/crypto"; 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 { import type {
DirectoryRenameRequest, DirectoryRenameRequest,
DirectoryCreateRequest, DirectoryCreateRequest,
@@ -114,10 +120,12 @@ export const requestEntryDeletion = async (entry: SelectedEntry) => {
if (entry.type === "directory") { if (entry.type === "directory") {
const { deletedFiles }: DirectoryDeleteResponse = await res.json(); const { deletedFiles }: DirectoryDeleteResponse = await res.json();
await Promise.all(deletedFiles.map(deleteFileCache)); await Promise.all(
deletedFiles.flatMap((fileId) => [deleteFileCache(fileId), deleteFileThumbnail(fileId)]),
);
return true; return true;
} else { } else {
await deleteFileCache(entry.id); await Promise.all([deleteFileCache(entry.id), deleteFileThumbnail(entry.id)]);
return true; return true;
} }
}; };