add: delete actions for device + slightly better ui
This commit is contained in:
@@ -10,7 +10,7 @@ import { FlowExecCtx } from "@core/flow.execution.context";
|
||||
import { StorageRepository } from "./storage.repository";
|
||||
import { FileRepository } from "./repository";
|
||||
import { settings } from "@core/settings";
|
||||
import { ResultAsync } from "neverthrow";
|
||||
import { okAsync, ResultAsync } from "neverthrow";
|
||||
import { traceResultAsync } from "@core/observability";
|
||||
import { db } from "@pkg/db";
|
||||
|
||||
@@ -198,6 +198,65 @@ export class FileController {
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile(fctx: FlowExecCtx, fileId: string, userId: string) {
|
||||
return traceResultAsync({
|
||||
name: "logic.files.controller.deleteFile",
|
||||
fctx,
|
||||
attributes: { "app.user.id": userId, "app.file.id": fileId },
|
||||
fn: () => this.deleteFiles(fctx, [fileId], userId),
|
||||
});
|
||||
}
|
||||
|
||||
cleanupDanglingStorageFiles(fctx: FlowExecCtx, userId: string) {
|
||||
return traceResultAsync({
|
||||
name: "logic.files.controller.cleanupDanglingStorageFiles",
|
||||
fctx,
|
||||
attributes: { "app.user.id": userId },
|
||||
fn: () =>
|
||||
this.fileRepo
|
||||
.listReferencedObjectKeysForUser(fctx, userId)
|
||||
.andThen((referencedKeys) => {
|
||||
const referencedSet = new Set(referencedKeys);
|
||||
return ResultAsync.combine([
|
||||
this.storageRepo.listObjectKeys(
|
||||
fctx,
|
||||
`uploads/${userId}/`,
|
||||
),
|
||||
this.storageRepo.listObjectKeys(
|
||||
fctx,
|
||||
`thumbnails/${userId}/`,
|
||||
),
|
||||
]).andThen(([uploadKeys, thumbnailKeys]) => {
|
||||
const existingStorageKeys = [
|
||||
...new Set([...uploadKeys, ...thumbnailKeys]),
|
||||
];
|
||||
|
||||
const danglingKeys = existingStorageKeys.filter(
|
||||
(key) => !referencedSet.has(key),
|
||||
);
|
||||
|
||||
if (danglingKeys.length === 0) {
|
||||
return okAsync({
|
||||
scanned: existingStorageKeys.length,
|
||||
referenced: referencedKeys.length,
|
||||
dangling: 0,
|
||||
deleted: 0,
|
||||
});
|
||||
}
|
||||
|
||||
return this.storageRepo
|
||||
.deleteFiles(fctx, danglingKeys)
|
||||
.map(() => ({
|
||||
scanned: existingStorageKeys.length,
|
||||
referenced: referencedKeys.length,
|
||||
dangling: danglingKeys.length,
|
||||
deleted: danglingKeys.length,
|
||||
}));
|
||||
});
|
||||
}),
|
||||
});
|
||||
}
|
||||
|
||||
shareFile(
|
||||
fctx: FlowExecCtx,
|
||||
fileId: string,
|
||||
|
||||
@@ -381,6 +381,47 @@ export class FileRepository {
|
||||
});
|
||||
}
|
||||
|
||||
listReferencedObjectKeysForUser(
|
||||
fctx: FlowExecCtx,
|
||||
userId: string,
|
||||
): ResultAsync<string[], Err> {
|
||||
return ResultAsync.fromPromise(
|
||||
this.db
|
||||
.select({
|
||||
objectKey: file.objectKey,
|
||||
metadata: file.metadata,
|
||||
})
|
||||
.from(file)
|
||||
.where(eq(file.userId, userId)),
|
||||
(error) =>
|
||||
fileErrors.getFilesFailed(
|
||||
fctx,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
),
|
||||
).map((rows) => {
|
||||
const keys = new Set<string>();
|
||||
|
||||
for (const row of rows) {
|
||||
if (row.objectKey) {
|
||||
keys.add(row.objectKey);
|
||||
}
|
||||
|
||||
const thumbnailKey =
|
||||
row.metadata &&
|
||||
typeof row.metadata === "object" &&
|
||||
"thumbnailKey" in row.metadata
|
||||
? (row.metadata as Record<string, unknown>).thumbnailKey
|
||||
: undefined;
|
||||
|
||||
if (typeof thumbnailKey === "string" && thumbnailKey.length > 0) {
|
||||
keys.add(thumbnailKey);
|
||||
}
|
||||
}
|
||||
|
||||
return [...keys];
|
||||
});
|
||||
}
|
||||
|
||||
updateFileStatus(
|
||||
fctx: FlowExecCtx,
|
||||
fileId: string,
|
||||
|
||||
@@ -284,4 +284,55 @@ export class StorageRepository {
|
||||
return true;
|
||||
});
|
||||
}
|
||||
|
||||
listObjectKeys(
|
||||
fctx: FlowExecCtx,
|
||||
prefix?: string,
|
||||
): ResultAsync<string[], Err> {
|
||||
const startedAt = Date.now();
|
||||
logDomainEvent({
|
||||
event: "files.storage.list.started",
|
||||
fctx,
|
||||
meta: { prefix: prefix || null },
|
||||
});
|
||||
|
||||
return ResultAsync.fromPromise(
|
||||
this.storageClient.listObjectKeys(prefix),
|
||||
(error) => {
|
||||
logDomainEvent({
|
||||
level: "error",
|
||||
event: "files.storage.list.failed",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error,
|
||||
meta: { prefix: prefix || null },
|
||||
});
|
||||
return fileErrors.storageError(
|
||||
fctx,
|
||||
error instanceof Error ? error.message : String(error),
|
||||
);
|
||||
},
|
||||
).andThen((result) => {
|
||||
if (result.error) {
|
||||
logDomainEvent({
|
||||
level: "error",
|
||||
event: "files.storage.list.failed",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error: result.error,
|
||||
meta: { prefix: prefix || null, stage: "storage_response" },
|
||||
});
|
||||
return errAsync(fileErrors.storageError(fctx, String(result.error)));
|
||||
}
|
||||
|
||||
const keys = result.data || [];
|
||||
logDomainEvent({
|
||||
event: "files.storage.list.succeeded",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
meta: { prefix: prefix || null, count: keys.length },
|
||||
});
|
||||
return okAsync(keys);
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user