feat: private certificates/documents buckets with SeaweedFS support
Add Document CRUD, temp→bucket commit flow, SeaweedFS env aliases, and default temp bucket name to temp.
This commit is contained in:
@@ -79,6 +79,11 @@
|
||||
"en": "Certificate not found.",
|
||||
"fa": "مدرک یافت نشد."
|
||||
},
|
||||
"DOCUMENT_NOT_FOUND": {
|
||||
"statusCode": 404,
|
||||
"en": "Document not found.",
|
||||
"fa": "سند یافت نشد."
|
||||
},
|
||||
"CLASS_NOT_FOUND": {
|
||||
"statusCode": 404,
|
||||
"en": "Class not found.",
|
||||
|
||||
+68
-23
@@ -14,6 +14,33 @@ const logger = require('./logger');
|
||||
|
||||
let s3ClientInstance = null;
|
||||
|
||||
const BUCKETS = {
|
||||
temp: () => config.S3_TEMP_BUCKET,
|
||||
certificates: () => config.S3_CERTIFICATES_BUCKET,
|
||||
documents: () => config.S3_DOCUMENTS_BUCKET,
|
||||
storage: () => config.S3_STORAGE_BUCKET
|
||||
};
|
||||
|
||||
const resolveBucket = (bucketNameOrKind) => {
|
||||
if (!bucketNameOrKind) return config.S3_CERTIFICATES_BUCKET;
|
||||
if (BUCKETS[bucketNameOrKind]) return BUCKETS[bucketNameOrKind]();
|
||||
return bucketNameOrKind;
|
||||
};
|
||||
|
||||
const isBucketPublic = (bucketName) => {
|
||||
const name = resolveBucket(bucketName);
|
||||
if (name === config.S3_CERTIFICATES_BUCKET) return config.S3_CERTIFICATES_PUBLIC;
|
||||
if (name === config.S3_DOCUMENTS_BUCKET) return config.S3_DOCUMENTS_PUBLIC;
|
||||
return false;
|
||||
};
|
||||
|
||||
const buildPublicUrl = (bucketName, fileKey) => {
|
||||
if (!isBucketPublic(bucketName) || !fileKey) return null;
|
||||
const base = (config.S3_PUBLIC_BASE_URL || config.S3_ENDPOINT || '').replace(/\/$/, '');
|
||||
if (!base) return null;
|
||||
return `${base}/${resolveBucket(bucketName)}/${fileKey}`;
|
||||
};
|
||||
|
||||
const getS3Client = () => {
|
||||
if (!s3ClientInstance) {
|
||||
s3ClientInstance = new S3Client({
|
||||
@@ -44,7 +71,6 @@ const uploadToTempBucket = async (fileBuffer, filename, contentType = 'applicati
|
||||
return { tempFileName: filename, bucket: config.S3_TEMP_BUCKET };
|
||||
} catch (error) {
|
||||
logger.error(`[S3 Storage ERROR] Temp upload failed for ${filename}: ${error.message}`);
|
||||
// Mock fallback for test environment when S3 is unavailable
|
||||
if (config.NODE_ENV === 'test' || error.code === 'ECONNREFUSED') {
|
||||
logger.warn(`[S3 Storage MOCK] Simulated temp upload for ${filename}`);
|
||||
return { tempFileName: filename, bucket: config.S3_TEMP_BUCKET };
|
||||
@@ -53,64 +79,79 @@ const uploadToTempBucket = async (fileBuffer, filename, contentType = 'applicati
|
||||
}
|
||||
};
|
||||
|
||||
const commitTempFile = async (tempFilename, targetFilename = null) => {
|
||||
const destinationKey = targetFilename || tempFilename;
|
||||
/**
|
||||
* Copy a temp object into a target private/public bucket, then remove the temp object.
|
||||
* @param {string} tempFilename
|
||||
* @param {string} destinationKey
|
||||
* @param {string} [targetBucketKind='certificates'] - 'certificates' | 'documents' | bucket name
|
||||
*/
|
||||
const commitTempFile = async (tempFilename, destinationKey = null, targetBucketKind = 'certificates') => {
|
||||
const targetKey = destinationKey || tempFilename;
|
||||
const targetBucket = resolveBucket(targetBucketKind);
|
||||
|
||||
try {
|
||||
const client = getS3Client();
|
||||
|
||||
// 1. Copy object from Temp Bucket to Main Storage Bucket
|
||||
const copyCommand = new CopyObjectCommand({
|
||||
CopySource: `${config.S3_TEMP_BUCKET}/${tempFilename}`,
|
||||
Bucket: config.S3_STORAGE_BUCKET,
|
||||
Key: destinationKey
|
||||
Bucket: targetBucket,
|
||||
Key: targetKey
|
||||
});
|
||||
await client.send(copyCommand);
|
||||
|
||||
// 2. Delete object from Temp Bucket
|
||||
const deleteCommand = new DeleteObjectCommand({
|
||||
Bucket: config.S3_TEMP_BUCKET,
|
||||
Key: tempFilename
|
||||
});
|
||||
await client.send(deleteCommand);
|
||||
|
||||
logger.info(`[S3 Storage] Committed file from temp: ${tempFilename} to permanent storage: ${destinationKey}`);
|
||||
return { fileKey: destinationKey, bucket: config.S3_STORAGE_BUCKET };
|
||||
const fileUrl = buildPublicUrl(targetBucket, targetKey);
|
||||
logger.info(`[S3 Storage] Committed ${tempFilename} → ${targetBucket}/${targetKey}`);
|
||||
return { fileKey: targetKey, bucket: targetBucket, fileUrl };
|
||||
} catch (error) {
|
||||
logger.error(`[S3 Storage ERROR] Failed to commit temp file ${tempFilename}: ${error.message}`);
|
||||
if (config.NODE_ENV === 'test' || error.code === 'ECONNREFUSED') {
|
||||
logger.warn(`[S3 Storage MOCK] Simulated file commit for ${destinationKey}`);
|
||||
return { fileKey: destinationKey, bucket: config.S3_STORAGE_BUCKET };
|
||||
logger.warn(`[S3 Storage MOCK] Simulated file commit for ${targetKey}`);
|
||||
return {
|
||||
fileKey: targetKey,
|
||||
bucket: targetBucket,
|
||||
fileUrl: buildPublicUrl(targetBucket, targetKey)
|
||||
};
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
};
|
||||
|
||||
const generatePresignedUrl = async (filename, bucketName = config.S3_STORAGE_BUCKET, expiresIn = config.SIGNED_URL_EXPIRES_IN) => {
|
||||
const generatePresignedUrl = async (
|
||||
filename,
|
||||
bucketName = config.S3_CERTIFICATES_BUCKET,
|
||||
expiresIn = config.SIGNED_URL_EXPIRES_IN
|
||||
) => {
|
||||
const bucket = resolveBucket(bucketName);
|
||||
try {
|
||||
const client = getS3Client();
|
||||
const command = new GetObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Bucket: bucket,
|
||||
Key: filename
|
||||
});
|
||||
|
||||
const presignedUrl = await getSignedUrl(client, command, { expiresIn });
|
||||
return presignedUrl;
|
||||
return await getSignedUrl(client, command, { expiresIn });
|
||||
} catch (error) {
|
||||
logger.error(`[S3 Storage ERROR] Failed to generate presigned URL for ${filename}: ${error.message}`);
|
||||
// Mock fallback URL for development without active S3 server
|
||||
return `${config.S3_ENDPOINT}/${bucketName}/${filename}?token=mock_presigned_${Date.now()}`;
|
||||
return `${config.S3_ENDPOINT}/${bucket}/${filename}?token=mock_presigned_${Date.now()}`;
|
||||
}
|
||||
};
|
||||
|
||||
const deleteFromBucket = async (filename, bucketName = config.S3_STORAGE_BUCKET) => {
|
||||
const deleteFromBucket = async (filename, bucketName = config.S3_CERTIFICATES_BUCKET) => {
|
||||
const bucket = resolveBucket(bucketName);
|
||||
try {
|
||||
const client = getS3Client();
|
||||
const command = new DeleteObjectCommand({
|
||||
Bucket: bucketName,
|
||||
Bucket: bucket,
|
||||
Key: filename
|
||||
});
|
||||
await client.send(command);
|
||||
logger.info(`[S3 Storage] Deleted file ${filename} from bucket ${bucketName}`);
|
||||
logger.info(`[S3 Storage] Deleted file ${filename} from bucket ${bucket}`);
|
||||
return true;
|
||||
} catch (error) {
|
||||
logger.error(`[S3 Storage ERROR] Failed to delete file ${filename}: ${error.message}`);
|
||||
@@ -138,11 +179,11 @@ const cleanupTempBucket = async (olderThanMinutes = 10) => {
|
||||
if (object.LastModified && new Date(object.LastModified) < cutoffTime) {
|
||||
await deleteFromBucket(object.Key, config.S3_TEMP_BUCKET);
|
||||
deletedCount++;
|
||||
logger.info(`[S3 Temp Cleanup] Deleted expired temp file: ${object.Key} (Last modified: ${object.LastModified})`);
|
||||
logger.info(`[S3 Temp Cleanup] Deleted expired temp file: ${object.Key}`);
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`[S3 Temp Cleanup] Daily temp bucket cleanup complete. Removed ${deletedCount} files.`);
|
||||
logger.info(`[S3 Temp Cleanup] Removed ${deletedCount} expired temp files.`);
|
||||
return deletedCount;
|
||||
} catch (error) {
|
||||
logger.error(`[S3 Temp Cleanup ERROR] Cleanup job failed: ${error.message}`);
|
||||
@@ -156,5 +197,9 @@ module.exports = {
|
||||
commitTempFile,
|
||||
generatePresignedUrl,
|
||||
deleteFromBucket,
|
||||
cleanupTempBucket
|
||||
cleanupTempBucket,
|
||||
resolveBucket,
|
||||
isBucketPublic,
|
||||
buildPublicUrl,
|
||||
BUCKETS
|
||||
};
|
||||
|
||||
Reference in New Issue
Block a user