now files do presigned url setup
This commit is contained in:
@@ -43,6 +43,33 @@ export class FileController {
|
||||
});
|
||||
}
|
||||
|
||||
getFileAccessUrl(
|
||||
fctx: FlowExecCtx,
|
||||
fileId: string,
|
||||
userId: string,
|
||||
expiresIn: number = 3600,
|
||||
) {
|
||||
return traceResultAsync({
|
||||
name: "logic.files.controller.getFileAccessUrl",
|
||||
fctx,
|
||||
attributes: {
|
||||
"app.user.id": userId,
|
||||
"app.file.id": fileId,
|
||||
"app.file.access_ttl_sec": expiresIn,
|
||||
},
|
||||
fn: () =>
|
||||
this.fileRepo
|
||||
.getFileById(fctx, fileId, userId)
|
||||
.andThen((file) =>
|
||||
this.storageRepo.generatePresignedDownloadUrl(
|
||||
fctx,
|
||||
file.objectKey,
|
||||
expiresIn,
|
||||
),
|
||||
),
|
||||
});
|
||||
}
|
||||
|
||||
uploadFile(
|
||||
fctx: FlowExecCtx,
|
||||
userId: string,
|
||||
|
||||
@@ -91,6 +91,14 @@ export type PresignedUploadResponse = v.InferOutput<
|
||||
typeof presignedUploadResponseSchema
|
||||
>;
|
||||
|
||||
export const presignedFileAccessResponseSchema = v.object({
|
||||
url: v.string(),
|
||||
expiresIn: v.pipe(v.number(), v.integer()),
|
||||
});
|
||||
export type PresignedFileAccessResponse = v.InferOutput<
|
||||
typeof presignedFileAccessResponseSchema
|
||||
>;
|
||||
|
||||
export const fileUploadResultSchema = v.object({
|
||||
success: v.boolean(),
|
||||
file: v.optional(fileSchema),
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
import { ResultAsync, errAsync, okAsync } from "neverthrow";
|
||||
import { FlowExecCtx } from "@core/flow.execution.context";
|
||||
import type { PresignedUploadResponse } from "./data";
|
||||
import type {
|
||||
PresignedFileAccessResponse,
|
||||
PresignedUploadResponse,
|
||||
} from "./data";
|
||||
import { R2StorageClient } from "@pkg/objectstorage";
|
||||
import { type Err } from "@pkg/result";
|
||||
import { fileErrors } from "./errors";
|
||||
@@ -209,6 +212,78 @@ export class StorageRepository {
|
||||
});
|
||||
}
|
||||
|
||||
generatePresignedDownloadUrl(
|
||||
fctx: FlowExecCtx,
|
||||
objectKey: string,
|
||||
expiresIn: number,
|
||||
): ResultAsync<PresignedFileAccessResponse, Err> {
|
||||
const startedAt = Date.now();
|
||||
logDomainEvent({
|
||||
event: "files.storage.presigned_download.started",
|
||||
fctx,
|
||||
meta: { objectKey, expiresIn },
|
||||
});
|
||||
|
||||
return ResultAsync.fromPromise(
|
||||
this.storageClient.generatePresignedDownloadUrl(objectKey, expiresIn),
|
||||
(error) => {
|
||||
logDomainEvent({
|
||||
level: "error",
|
||||
event: "files.storage.presigned_download.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_download.failed",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error: result.error,
|
||||
meta: { objectKey, stage: "storage_response" },
|
||||
});
|
||||
return errAsync(
|
||||
fileErrors.presignedUrlFailed(fctx, String(result.error)),
|
||||
);
|
||||
}
|
||||
|
||||
const data = result.data;
|
||||
if (!data?.downloadUrl) {
|
||||
logDomainEvent({
|
||||
level: "error",
|
||||
event: "files.storage.presigned_download.failed",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
error: {
|
||||
code: "NO_PRESIGNED_DATA",
|
||||
message: "No presigned download data returned",
|
||||
},
|
||||
meta: { objectKey },
|
||||
});
|
||||
return errAsync(fileErrors.noPresignedData(fctx));
|
||||
}
|
||||
|
||||
logDomainEvent({
|
||||
event: "files.storage.presigned_download.succeeded",
|
||||
fctx,
|
||||
durationMs: Date.now() - startedAt,
|
||||
meta: { objectKey, expiresIn },
|
||||
});
|
||||
return okAsync({
|
||||
url: data.downloadUrl,
|
||||
expiresIn: data.expiresIn,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
deleteFile(
|
||||
fctx: FlowExecCtx,
|
||||
objectKey: string,
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
import type {
|
||||
FileMetadata,
|
||||
FileUploadConfig,
|
||||
PresignedDownloadResult,
|
||||
PresignedUrlResult,
|
||||
UploadOptions,
|
||||
UploadResult,
|
||||
@@ -266,6 +267,46 @@ export class R2StorageClient {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate presigned URL for private object download/view
|
||||
*/
|
||||
async generatePresignedDownloadUrl(
|
||||
objectKey: string,
|
||||
expiresIn: number = 3600,
|
||||
): Promise<Result<PresignedDownloadResult>> {
|
||||
try {
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: this.config.bucketName,
|
||||
Key: objectKey,
|
||||
});
|
||||
|
||||
const downloadUrl = await getSignedUrl(this.s3Client, command, {
|
||||
expiresIn,
|
||||
});
|
||||
|
||||
logger.info(`Generated presigned download URL for ${objectKey}`);
|
||||
return {
|
||||
data: {
|
||||
downloadUrl,
|
||||
expiresIn,
|
||||
},
|
||||
};
|
||||
} catch (error) {
|
||||
logger.error("Failed to generate presigned download URL:", error);
|
||||
return {
|
||||
error: getError(
|
||||
{
|
||||
code: ERROR_CODES.STORAGE_ERROR,
|
||||
message: "Failed to generate presigned download URL",
|
||||
description: "Could not create download URL",
|
||||
detail: "S3 presigned URL generation failed",
|
||||
},
|
||||
error,
|
||||
),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get file from R2
|
||||
*/
|
||||
|
||||
@@ -60,6 +60,14 @@ export const presignedUrlResultSchema = v.object({
|
||||
});
|
||||
export type PresignedUrlResult = v.InferOutput<typeof presignedUrlResultSchema>;
|
||||
|
||||
export const presignedDownloadResultSchema = v.object({
|
||||
downloadUrl: v.string(),
|
||||
expiresIn: v.pipe(v.number(), v.integer()),
|
||||
});
|
||||
export type PresignedDownloadResult = v.InferOutput<
|
||||
typeof presignedDownloadResultSchema
|
||||
>;
|
||||
|
||||
// File Validation Result Schema
|
||||
export const fileValidationResultSchema = v.object({
|
||||
isValid: v.boolean(),
|
||||
|
||||
Reference in New Issue
Block a user