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

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?
.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) => {

View File

@@ -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;
});
};

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) => {
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;
}),
};

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) => {
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<NewFile, "path" | "encContentHash">,
encContentStream: Readable,

View File

@@ -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;
}
};