import path from "path"; import { DeleteObjectCommand, DeleteObjectCommandInput, GetObjectCommand, ListObjectsV2Command, PutObjectCommand, PutObjectCommandInput, S3Client, } from "@aws-sdk/client-s3"; import { getSignedUrl } from "@aws-sdk/s3-request-presigner"; import { BlobServiceClient, StorageSharedKeyCredential, ContainerClient, BlockBlobClient, generateBlobSASQueryParameters, BlobSASPermissions, } from "@azure/storage-blob"; // Import mime type library const mimeTypes = require("mime"); import { isProduction, isTestServer, log_error } from "./utils"; import { AZURE_STORAGE_ACCOUNT_KEY, AZURE_STORAGE_ACCOUNT_NAME, AZURE_STORAGE_CONTAINER, AZURE_STORAGE_URL, BUCKET, REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_URL, STORAGE_PROVIDER, } from "./constants"; // Parse the endpoint URL from S3_URL if it exists const getEndpointFromUrl = () => { try { if (!S3_URL) return undefined; // Extract the endpoint URL (e.g., http://minio:9000 from http://minio:9000/bucket) const url = new URL(S3_URL); return `${url.protocol}//${url.host}`; } catch (error) { console.warn("Error parsing S3_URL:", error); return undefined; } }; // Initialize S3 Client with support for MinIO const s3Client = new S3Client({ region: REGION, credentials: { accessKeyId: S3_ACCESS_KEY_ID || "", secretAccessKey: S3_SECRET_ACCESS_KEY || "", }, endpoint: getEndpointFromUrl(), forcePathStyle: true, // Required for MinIO }); // Log the storage configuration console.log(`Storage provider initialized: ${STORAGE_PROVIDER}`); console.log(`Using endpoint: ${getEndpointFromUrl() || "AWS default"}`); console.log(`Bucket: ${BUCKET}`); // Initialize Azure Blob Storage Client let azureBlobServiceClient: BlobServiceClient | null = null; let azureContainerClient: ContainerClient | null = null; if (STORAGE_PROVIDER === "azure") { try { if (!AZURE_STORAGE_ACCOUNT_NAME || !AZURE_STORAGE_ACCOUNT_KEY) { console.error("Azure Blob Storage credentials are missing"); } else { const sharedKeyCredential = new StorageSharedKeyCredential( AZURE_STORAGE_ACCOUNT_NAME, AZURE_STORAGE_ACCOUNT_KEY ); azureBlobServiceClient = new BlobServiceClient( `https://${AZURE_STORAGE_ACCOUNT_NAME}.blob.core.windows.net`, sharedKeyCredential ); const containerName = AZURE_STORAGE_CONTAINER || "ifinitycdn"; azureContainerClient = azureBlobServiceClient.getContainerClient(containerName); console.log(`Azure Blob Storage initialized with account: ${AZURE_STORAGE_ACCOUNT_NAME}, container: ${containerName}`); } } catch (error) { console.error("Failed to initialize Azure Blob Storage:", error); } } export function getRootDir() { if (isTestServer()) return "test-server"; if (isProduction()) return "secure"; return "local-server"; } export function getKey( teamId: string, projectId: string, attachmentId: string, type: string ) { const keyPath = path .join(getRootDir(), teamId, projectId, `${attachmentId}.${type}`) .replace(/\\/g, "/"); return keyPath; } export function getTaskAttachmentKey( teamId: string, projectId: string, taskId: string, commentId: string, attachmentId: string, type: string ) { const keyPath = path .join( getRootDir(), teamId, projectId, taskId, commentId, `${attachmentId}.${type}` ) .replace(/\\/g, "/"); return keyPath; } export function getAvatarKey(userId: string, type: string) { const keyPath = path .join("avatars", getRootDir(), `${userId}.${type}`) .replace(/\\/g, "/"); return keyPath; } async function uploadBufferToS3( buffer: Buffer, type: string, location: string ): Promise { try { const bucketParams: PutObjectCommandInput = { Bucket: BUCKET, Key: location, Body: buffer, ContentEncoding: "base64", ContentType: type, }; await s3Client.send(new PutObjectCommand(bucketParams)); // Create proper URL depending on whether we're using S3 or MinIO const endpointUrl = getEndpointFromUrl(); if (endpointUrl) { // For MinIO or custom S3 endpoint return `${endpointUrl}/${BUCKET}/${location}`; } // For standard AWS S3 return `${S3_URL}/${location}`; } catch (error) { log_error(error); return null; } } async function uploadBufferToAzure( buffer: Buffer, type: string, location: string ): Promise { try { if (!azureContainerClient) { throw new Error("Azure Blob Storage not configured properly"); } const blobClient = azureContainerClient.getBlockBlobClient(location); await blobClient.uploadData(buffer, { blobHTTPHeaders: { blobContentType: type, }, }); // Format URL with container name in the path const containerName = AZURE_STORAGE_CONTAINER || "ifinitycdn"; return `${AZURE_STORAGE_URL}/${containerName}/${location}`; } catch (error) { log_error(error); return null; } } export async function uploadBuffer( buffer: Buffer, type: string, location: string ): Promise { if (STORAGE_PROVIDER === "azure") { return uploadBufferToAzure(buffer, type, location); } return uploadBufferToS3(buffer, type, location); } export async function uploadBase64(base64Data: string, location: string) { try { const buffer = Buffer.from( base64Data.replace(/^data:(.*?);base64,/, ""), "base64" ); const type = base64Data.split(";")[0].split(":")[1] || null; if (!type) return null; return await uploadBuffer(buffer, type, location); } catch (error) { log_error(error); return null; } } async function deleteObjectFromS3(key: string) { try { const input: DeleteObjectCommandInput = { Bucket: BUCKET, Key: key, }; return await s3Client.send(new DeleteObjectCommand(input)); } catch (error) { log_error(error); return null; } } async function deleteObjectFromAzure(key: string) { try { if (!azureContainerClient) { throw new Error("Azure Blob Storage not configured properly"); } const blobClient = azureContainerClient.getBlockBlobClient(key); return await blobClient.delete(); } catch (error) { log_error(error); return null; } } export async function deleteObject(key: string) { if (STORAGE_PROVIDER === "azure") { return deleteObjectFromAzure(key); } return deleteObjectFromS3(key); } async function calculateStorageS3(prefix: string) { try { let totalSize = 0; let continuationToken; let response: any | null = null; do { const command: any = new ListObjectsV2Command({ Bucket: BUCKET, Prefix: `${getRootDir()}/${prefix}`, ContinuationToken: continuationToken, }); response = await s3Client.send(command); if (response?.Contents) { for (const obj of response.Contents) { totalSize += obj.Size; } } continuationToken = response.NextContinuationToken; } while (continuationToken); return totalSize; } catch (error) { log_error(error); return 0; } } async function calculateStorageAzure(prefix: string) { try { if (!azureContainerClient) { throw new Error("Azure Blob Storage not configured properly"); } let totalSize = 0; const fullPrefix = `${getRootDir()}/${prefix}`; // List all blobs with the specified prefix for await (const blob of azureContainerClient.listBlobsFlat({ prefix: fullPrefix, })) { if (blob.properties.contentLength) { totalSize += blob.properties.contentLength; } } return totalSize; } catch (error) { log_error(error); return 0; } } export async function calculateStorage(prefix: string) { if (STORAGE_PROVIDER === "azure") { return calculateStorageAzure(prefix); } return calculateStorageS3(prefix); } async function createPresignedUrlWithS3Client(key: string, file: string) { const fileExtension = path.extname(key).toLowerCase(); const contentType = mimeTypes.lookup(fileExtension); const command = new GetObjectCommand({ Bucket: BUCKET, Key: key, ResponseContentType: `${contentType}`, ResponseContentDisposition: `attachment; filename=${file}`, }); return getSignedUrl(s3Client, command, { expiresIn: 3600 }); } async function createPresignedUrlWithAzureClient(key: string, file: string) { try { if ( !azureContainerClient || !AZURE_STORAGE_ACCOUNT_NAME || !AZURE_STORAGE_ACCOUNT_KEY ) { throw new Error("Azure Blob Storage not configured properly"); } const blobClient = azureContainerClient.getBlockBlobClient(key); // Create a SAS token that's valid for one hour const sharedKeyCredential = new StorageSharedKeyCredential( AZURE_STORAGE_ACCOUNT_NAME, AZURE_STORAGE_ACCOUNT_KEY ); const fileExtension = path.extname(key).toLowerCase(); const contentType = mimeTypes.lookup(fileExtension); const containerName = AZURE_STORAGE_CONTAINER || "ifinitycdn"; const sasOptions = { containerName, blobName: key, permissions: BlobSASPermissions.parse("r"), // Read permission startsOn: new Date(), expiresOn: new Date(new Date().valueOf() + 3600 * 1000), contentDisposition: `attachment; filename=${file}`, contentType: contentType || undefined, }; const sasToken = generateBlobSASQueryParameters( sasOptions, sharedKeyCredential ).toString(); // Generate URL with container name in the path return `${AZURE_STORAGE_URL}/${containerName}/${key}?${sasToken}`; } catch (error) { log_error(error); return null; } } export async function createPresignedUrlWithClient(key: string, file: string) { if (STORAGE_PROVIDER === "azure") { return createPresignedUrlWithAzureClient(key, file); } return createPresignedUrlWithS3Client(key, file); }