253 lines
9.3 KiB
TypeScript
253 lines
9.3 KiB
TypeScript
import {
|
|
FileFilters,
|
|
FileShareRequest,
|
|
FileUpdateRequest,
|
|
FileUploadRequest,
|
|
PaginationOptions,
|
|
PresignedUploadRequest,
|
|
} from "./data";
|
|
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 { traceResultAsync } from "@core/observability";
|
|
import { db } from "@pkg/db";
|
|
|
|
export class FileController {
|
|
constructor(
|
|
private fileRepo: FileRepository,
|
|
private storageRepo: StorageRepository,
|
|
private publicUrl: string,
|
|
) {}
|
|
|
|
getFiles(
|
|
fctx: FlowExecCtx,
|
|
filters: FileFilters,
|
|
pagination: PaginationOptions,
|
|
) {
|
|
return traceResultAsync({
|
|
name: "logic.files.controller.getFiles",
|
|
fctx,
|
|
attributes: { "app.user.id": filters.userId },
|
|
fn: () => this.fileRepo.getFiles(fctx, filters, pagination),
|
|
});
|
|
}
|
|
|
|
getFile(fctx: FlowExecCtx, fileId: string, userId: string) {
|
|
return traceResultAsync({
|
|
name: "logic.files.controller.getFile",
|
|
fctx,
|
|
attributes: { "app.user.id": userId, "app.file.id": fileId },
|
|
fn: () => this.fileRepo.getFileById(fctx, fileId, userId),
|
|
});
|
|
}
|
|
|
|
uploadFile(
|
|
fctx: FlowExecCtx,
|
|
userId: string,
|
|
file: globalThis.File,
|
|
uploadRequest: FileUploadRequest,
|
|
) {
|
|
return traceResultAsync({
|
|
name: "logic.files.controller.uploadFile",
|
|
fctx,
|
|
attributes: { "app.user.id": userId, "app.file.name": file.name },
|
|
fn: () =>
|
|
ResultAsync.fromPromise(file.arrayBuffer(), (error) => ({
|
|
code: "INTERNAL_ERROR",
|
|
message: "Failed to read file buffer",
|
|
description: "Please try again",
|
|
detail: error instanceof Error ? error.message : String(error),
|
|
}))
|
|
.map((arrayBuffer) => Buffer.from(arrayBuffer))
|
|
.andThen((buffer) =>
|
|
this.storageRepo.uploadFile(
|
|
fctx,
|
|
buffer,
|
|
file.name,
|
|
file.type,
|
|
userId,
|
|
{
|
|
visibility:
|
|
(uploadRequest.visibility as
|
|
| "public"
|
|
| "private") || "private",
|
|
metadata: uploadRequest.metadata,
|
|
tags: uploadRequest.tags,
|
|
processImage: uploadRequest.processImage,
|
|
processDocument: uploadRequest.processDocument,
|
|
processVideo: uploadRequest.processVideo,
|
|
},
|
|
),
|
|
)
|
|
.andThen((fileMetadata) =>
|
|
this.fileRepo
|
|
.createFile(fctx, {
|
|
id: fileMetadata.id,
|
|
filename: fileMetadata.filename,
|
|
originalName: fileMetadata.originalName,
|
|
mimeType: fileMetadata.mimeType,
|
|
size: fileMetadata.size,
|
|
hash: fileMetadata.hash,
|
|
bucketName: fileMetadata.bucketName,
|
|
objectKey: fileMetadata.objectKey,
|
|
r2Url: fileMetadata.r2Url,
|
|
visibility: fileMetadata.visibility,
|
|
userId: fileMetadata.userId,
|
|
metadata: fileMetadata.metadata,
|
|
tags: fileMetadata.tags
|
|
? [...fileMetadata.tags]
|
|
: undefined,
|
|
status: "ready",
|
|
uploadedAt: fileMetadata.uploadedAt,
|
|
})
|
|
.map((dbFile) => ({
|
|
success: true,
|
|
file: dbFile,
|
|
uploadId: fileMetadata.id,
|
|
})),
|
|
),
|
|
});
|
|
}
|
|
|
|
generatePresignedUrl(
|
|
fctx: FlowExecCtx,
|
|
userId: string,
|
|
bucketName: string,
|
|
request: PresignedUploadRequest,
|
|
) {
|
|
const fileId = crypto.randomUUID();
|
|
const extension = request.filename.split(".").pop() || "";
|
|
const filename = `${fileId}.${extension}`;
|
|
const objectKey = `uploads/${userId}/${filename}`;
|
|
|
|
return traceResultAsync({
|
|
name: "logic.files.controller.generatePresignedUrl",
|
|
fctx,
|
|
attributes: { "app.user.id": userId, "app.file.id": fileId },
|
|
fn: () =>
|
|
this.storageRepo
|
|
.generatePresignedUploadUrl(
|
|
fctx,
|
|
objectKey,
|
|
request.mimeType,
|
|
3600,
|
|
)
|
|
.andThen((presignedData) =>
|
|
this.fileRepo
|
|
.createFile(fctx, {
|
|
id: fileId,
|
|
filename,
|
|
originalName: request.filename,
|
|
mimeType: request.mimeType,
|
|
size: request.size,
|
|
hash: "",
|
|
bucketName,
|
|
objectKey,
|
|
r2Url: `${this.publicUrl}/${bucketName}/${objectKey}`,
|
|
visibility: request.visibility || "private",
|
|
userId,
|
|
status: "processing",
|
|
uploadedAt: new Date(),
|
|
})
|
|
.map(() => ({
|
|
...presignedData,
|
|
fileId,
|
|
objectKey,
|
|
})),
|
|
),
|
|
});
|
|
}
|
|
|
|
updateFile(
|
|
fctx: FlowExecCtx,
|
|
fileId: string,
|
|
userId: string,
|
|
updates: FileUpdateRequest,
|
|
) {
|
|
return traceResultAsync({
|
|
name: "logic.files.controller.updateFile",
|
|
fctx,
|
|
attributes: { "app.user.id": userId, "app.file.id": fileId },
|
|
fn: () => this.fileRepo.updateFile(fctx, fileId, userId, updates),
|
|
});
|
|
}
|
|
|
|
deleteFiles(fctx: FlowExecCtx, fileIds: readonly string[], userId: string) {
|
|
return traceResultAsync({
|
|
name: "logic.files.controller.deleteFiles",
|
|
fctx,
|
|
attributes: {
|
|
"app.user.id": userId,
|
|
"app.files.count": fileIds.length,
|
|
},
|
|
fn: () =>
|
|
ResultAsync.combine(
|
|
[...fileIds].map((fileId) =>
|
|
this.fileRepo.getFileById(fctx, fileId, userId),
|
|
),
|
|
)
|
|
.map((files) => files.map((file) => file.objectKey))
|
|
.andThen((objectKeys) =>
|
|
this.storageRepo.deleteFiles(fctx, objectKeys),
|
|
)
|
|
.andThen(() =>
|
|
this.fileRepo.deleteFiles(fctx, fileIds, userId),
|
|
),
|
|
});
|
|
}
|
|
|
|
shareFile(
|
|
fctx: FlowExecCtx,
|
|
fileId: string,
|
|
ownerId: string,
|
|
shareRequest: FileShareRequest,
|
|
) {
|
|
return traceResultAsync({
|
|
name: "logic.files.controller.shareFile",
|
|
fctx,
|
|
attributes: { "app.user.id": ownerId, "app.file.id": fileId },
|
|
fn: () => this.fileRepo.shareFile(fctx, fileId, ownerId, shareRequest),
|
|
});
|
|
}
|
|
|
|
updateFileStatus(
|
|
fctx: FlowExecCtx,
|
|
fileId: string,
|
|
status: string,
|
|
processingError?: string,
|
|
) {
|
|
return traceResultAsync({
|
|
name: "logic.files.controller.updateFileStatus",
|
|
fctx,
|
|
attributes: { "app.file.id": fileId },
|
|
fn: () =>
|
|
this.fileRepo.updateFileStatus(
|
|
fctx,
|
|
fileId,
|
|
status,
|
|
processingError,
|
|
),
|
|
});
|
|
}
|
|
}
|
|
|
|
export function getFileController(): FileController {
|
|
return new FileController(
|
|
new FileRepository(db),
|
|
new StorageRepository({
|
|
bucketName: settings.r2BucketName || "",
|
|
region: settings.r2Region || "",
|
|
endpoint: settings.r2Endpoint || "",
|
|
accessKey: settings.r2AccessKey || "",
|
|
secretKey: settings.r2SecretKey || "",
|
|
publicUrl: settings.r2PublicUrl || "",
|
|
maxFileSize: settings.maxFileSize,
|
|
allowedMimeTypes: settings.allowedMimeTypes,
|
|
allowedExtensions: settings.allowedExtensions,
|
|
}),
|
|
settings.r2PublicUrl || "",
|
|
);
|
|
}
|