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:
2026-08-15 18:46:26 +03:30
parent 7ebf9409f0
commit 1ef54ad414
9 changed files with 173 additions and 30 deletions
+10
View File
@@ -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
View File
@@ -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,