import { ResultAsync, errAsync, okAsync } from "neverthrow"; import { FlowExecCtx } from "@core/flow.execution.context"; import type { PresignedUploadResponse } from "./data"; import { R2StorageClient } from "@pkg/objectstorage"; import { type Err } from "@pkg/result"; import { fileErrors } from "./errors"; import { logDomainEvent } from "@pkg/logger"; export type StorageConfig = { bucketName: string; region: string; endpoint: string; accessKey: string; secretKey: string; publicUrl: string; maxFileSize: number; allowedMimeTypes: string[]; allowedExtensions: string[]; }; export type UploadOptions = { visibility: "public" | "private"; metadata?: Record; tags?: string[]; processImage?: boolean; processDocument?: boolean; processVideo?: boolean; }; export type UploadedFileMetadata = { id: string; filename: string; originalName: string; mimeType: string; size: number; hash: string; bucketName: string; objectKey: string; r2Url: string; visibility: string; userId: string; metadata?: Record; tags?: string[]; uploadedAt: Date; }; export class StorageRepository { private storageClient: R2StorageClient; constructor(config: StorageConfig) { this.storageClient = new R2StorageClient(config); } uploadFile( fctx: FlowExecCtx, buffer: Buffer, filename: string, mimeType: string, userId: string, options: UploadOptions, ): ResultAsync { const startedAt = Date.now(); logDomainEvent({ event: "files.storage.upload.started", fctx, meta: { userId, filename, mimeType, size: buffer.length, visibility: options.visibility, }, }); return ResultAsync.fromPromise( this.storageClient.uploadFile( buffer, filename, mimeType, userId, options, ), (error) => { logDomainEvent({ level: "error", event: "files.storage.upload.failed", fctx, durationMs: Date.now() - startedAt, error, meta: { userId, filename }, }); return fileErrors.uploadFailed( fctx, error instanceof Error ? error.message : String(error), ); }, ).andThen((uploadResult) => { if (uploadResult.error) { logDomainEvent({ level: "error", event: "files.storage.upload.failed", fctx, durationMs: Date.now() - startedAt, error: uploadResult.error, meta: { userId, filename, stage: "storage_response" }, }); return errAsync( fileErrors.uploadFailed(fctx, String(uploadResult.error)), ); } const uploadData = uploadResult.data; if (!uploadData || !uploadData.file) { logDomainEvent({ level: "error", event: "files.storage.upload.failed", fctx, durationMs: Date.now() - startedAt, error: { code: "NO_FILE_METADATA", message: "Storage upload returned no file metadata", }, meta: { userId, filename }, }); return errAsync(fileErrors.noFileMetadata(fctx)); } logDomainEvent({ event: "files.storage.upload.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { userId, fileId: uploadData.file.id, filename }, }); return okAsync(uploadData.file as UploadedFileMetadata); }); } generatePresignedUploadUrl( fctx: FlowExecCtx, objectKey: string, mimeType: string, expiresIn: number, ): ResultAsync { const startedAt = Date.now(); logDomainEvent({ event: "files.storage.presigned.started", fctx, meta: { objectKey, mimeType, expiresIn }, }); return ResultAsync.fromPromise( this.storageClient.generatePresignedUploadUrl( objectKey, mimeType, expiresIn, ), (error) => { logDomainEvent({ level: "error", event: "files.storage.presigned.failed", fctx, durationMs: Date.now() - startedAt, error, meta: { objectKey }, }); return fileErrors.presignedUrlFailed( fctx, error instanceof Error ? error.message : String(error), ); }, ).andThen((result) => { if (result.error) { logDomainEvent({ level: "error", event: "files.storage.presigned.failed", fctx, durationMs: Date.now() - startedAt, error: result.error, meta: { objectKey, stage: "storage_response" }, }); return errAsync( fileErrors.presignedUrlFailed(fctx, String(result.error)), ); } const presignedData = result.data; if (!presignedData) { logDomainEvent({ level: "error", event: "files.storage.presigned.failed", fctx, durationMs: Date.now() - startedAt, error: { code: "NO_PRESIGNED_DATA", message: "No presigned data returned", }, meta: { objectKey }, }); return errAsync(fileErrors.noPresignedData(fctx)); } logDomainEvent({ event: "files.storage.presigned.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { objectKey }, }); return okAsync(presignedData as PresignedUploadResponse); }); } deleteFile( fctx: FlowExecCtx, objectKey: string, ): ResultAsync { const startedAt = Date.now(); logDomainEvent({ event: "files.storage.delete.started", fctx, meta: { objectKey }, }); return ResultAsync.fromPromise( this.storageClient.deleteFile(objectKey), (error) => { logDomainEvent({ level: "error", event: "files.storage.delete.failed", fctx, durationMs: Date.now() - startedAt, error, meta: { objectKey }, }); return fileErrors.storageError( fctx, error instanceof Error ? error.message : String(error), ); }, ).andThen((result) => { if (result.error) { logDomainEvent({ level: "error", event: "files.storage.delete.failed", fctx, durationMs: Date.now() - startedAt, error: result.error, meta: { objectKey, stage: "storage_response" }, }); return errAsync( fileErrors.storageError(fctx, String(result.error)), ); } logDomainEvent({ event: "files.storage.delete.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { objectKey }, }); return okAsync(true); }); } deleteFiles( fctx: FlowExecCtx, objectKeys: string[], ): ResultAsync { const startedAt = Date.now(); logDomainEvent({ event: "files.storage.delete_many.started", fctx, meta: { fileCount: objectKeys.length }, }); return ResultAsync.combine( objectKeys.map((key) => this.deleteFile(fctx, key)), ).map(() => { logDomainEvent({ event: "files.storage.delete_many.succeeded", fctx, durationMs: Date.now() - startedAt, meta: { fileCount: objectKeys.length }, }); return true; }); } }