388 lines
9.9 KiB
TypeScript
388 lines
9.9 KiB
TypeScript
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<string | null> {
|
|
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<string | null> {
|
|
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<string | null> {
|
|
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);
|
|
}
|