fix: stream FilePond uploads to S3 and proxy private file previews
Disk-based multer plus an authenticated content endpoint match the working Node.js uploader and avoid browser signed-URL failures on SeaweedFS.
This commit is contained in:
@@ -144,6 +144,16 @@
|
||||
"en": "File is required.",
|
||||
"fa": "ارسال فایل الزامی است."
|
||||
},
|
||||
"FILE_NOT_FOUND": {
|
||||
"statusCode": 404,
|
||||
"en": "The requested file was not found.",
|
||||
"fa": "فایل مورد نظر یافت نشد."
|
||||
},
|
||||
"UNSUPPORTED_FILE_TYPE": {
|
||||
"statusCode": 400,
|
||||
"en": "This file type is not allowed. Use JPEG, PNG, WebP, GIF, or PDF.",
|
||||
"fa": "این نوع فایل مجاز نیست. از JPEG، PNG، WebP، GIF یا PDF استفاده کنید."
|
||||
},
|
||||
"FILE_COMMIT_FAILED": {
|
||||
"statusCode": 502,
|
||||
"en": "Failed to store the uploaded file. Please try again.",
|
||||
|
||||
+39
-3
@@ -1,5 +1,6 @@
|
||||
// /utils/s3Client.js
|
||||
|
||||
const fs = require('fs');
|
||||
const {
|
||||
S3Client,
|
||||
PutObjectCommand,
|
||||
@@ -161,14 +162,21 @@ const ensureAppBuckets = async () => {
|
||||
}
|
||||
};
|
||||
|
||||
const uploadToTempBucket = async (fileBuffer, filename, contentType = 'application/octet-stream') => {
|
||||
const toPutBody = (fileBufferOrPath) => {
|
||||
if (typeof fileBufferOrPath === 'string') {
|
||||
return fs.createReadStream(fileBufferOrPath);
|
||||
}
|
||||
return fileBufferOrPath;
|
||||
};
|
||||
|
||||
const uploadToTempBucket = async (fileBufferOrPath, filename, contentType = 'application/octet-stream') => {
|
||||
try {
|
||||
await ensureBucket(config.S3_TEMP_BUCKET);
|
||||
const client = getS3Client();
|
||||
const command = new PutObjectCommand({
|
||||
Bucket: config.S3_TEMP_BUCKET,
|
||||
Key: filename,
|
||||
Body: fileBuffer,
|
||||
Body: toPutBody(fileBufferOrPath),
|
||||
ContentType: contentType
|
||||
});
|
||||
|
||||
@@ -186,6 +194,30 @@ const uploadToTempBucket = async (fileBuffer, filename, contentType = 'applicati
|
||||
}
|
||||
};
|
||||
|
||||
const getObjectBuffer = async (filename, bucketName = config.S3_CERTIFICATES_BUCKET) => {
|
||||
if (!filename) {
|
||||
throw new AppError('VALIDATION_FAILED', null, 'Filename is required');
|
||||
}
|
||||
|
||||
const bucket = resolveBucket(bucketName);
|
||||
try {
|
||||
const client = getS3Client();
|
||||
const result = await client.send(new GetObjectCommand({
|
||||
Bucket: bucket,
|
||||
Key: filename
|
||||
}));
|
||||
const buffer = await streamToBuffer(result.Body);
|
||||
return {
|
||||
buffer,
|
||||
contentType: result.ContentType || 'application/octet-stream',
|
||||
bucket
|
||||
};
|
||||
} catch (error) {
|
||||
logS3Error(`GetObject failed for ${bucket}/${filename}`, error);
|
||||
throw new AppError('FILE_NOT_FOUND');
|
||||
}
|
||||
};
|
||||
|
||||
const rewriteTempObject = async (client, tempFilename, targetBucket, targetKey) => {
|
||||
const source = await client.send(new GetObjectCommand({
|
||||
Bucket: config.S3_TEMP_BUCKET,
|
||||
@@ -256,7 +288,10 @@ const generatePresignedUrl = async (
|
||||
return await getSignedUrl(client, command, { expiresIn });
|
||||
} catch (error) {
|
||||
logger.error(`[S3 Storage ERROR] Failed to generate presigned URL for ${filename}: ${error.message}`);
|
||||
return `${config.S3_ENDPOINT}/${bucket}/${filename}?token=mock_presigned_${Date.now()}`;
|
||||
if (config.NODE_ENV === 'test') {
|
||||
return `${config.S3_ENDPOINT}/${bucket}/${filename}?token=mock_presigned_${Date.now()}`;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
@@ -314,6 +349,7 @@ module.exports = {
|
||||
uploadToTempBucket,
|
||||
commitTempFile,
|
||||
generatePresignedUrl,
|
||||
getObjectBuffer,
|
||||
deleteFromBucket,
|
||||
cleanupTempBucket,
|
||||
resolveBucket,
|
||||
|
||||
Reference in New Issue
Block a user