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:
2026-08-15 02:53:59 +03:30
parent 5c2fad4b44
commit 4603b45208
18 changed files with 585 additions and 112 deletions
+68 -23
View File
@@ -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
};