freshyo/apps/backend/src/lib/s3-client.ts
2026-03-07 16:24:24 +05:30

221 lines
6.8 KiB
TypeScript
Executable file

// import { s3A, awsBucketName, awsRegion, awsSecretAccessKey } from "@/src/lib/env-exporter"
import { DeleteObjectCommand, DeleteObjectsCommand, PutObjectCommand, S3Client, GetObjectCommand } from "@aws-sdk/client-s3"
import { getSignedUrl } from "@aws-sdk/s3-request-presigner"
import signedUrlCache from "@/src/lib/signed-url-cache"
import { s3AccessKeyId, s3Region, s3Url, s3SecretAccessKey, s3BucketName, assetsDomain } from "@/src/lib/env-exporter"
import { db } from "@/src/db/db_index"; // Adjust path if needed
import { uploadUrlStatus } from "@/src/db/schema"
import { and, eq } from 'drizzle-orm';
const s3Client = new S3Client({
region: s3Region,
endpoint: s3Url,
forcePathStyle: true,
credentials: {
accessKeyId: s3AccessKeyId,
secretAccessKey: s3SecretAccessKey,
},
})
export default s3Client;
export const imageUploadS3 = async(body: Buffer<ArrayBufferLike>, type: string, key:string) => {
// const key = `${category}/${Date.now()}`
const command = new PutObjectCommand({
Bucket: s3BucketName,
Key: key,
Body: body,
ContentType: type,
})
const resp = await s3Client.send(command)
const imageUrl = `${key}`
return imageUrl;
}
// export async function deleteImageUtil(...keys:string[]):Promise<boolean>;
export async function deleteImageUtil({bucket = s3BucketName, keys}:{bucket?:string, keys: string[]}) {
if (keys.length === 0) {
return true;
}
try {
const deleteParams = {
Bucket: bucket,
Delete: {
Objects: keys.map((key) => ({ Key: key })),
Quiet: false,
}
}
const deleteCommand = new DeleteObjectsCommand(deleteParams)
await s3Client.send(deleteCommand)
return true
}
catch (error) {
console.error("Error deleting image:", error)
throw new Error("Failed to delete image")
return false;
}
}
export function scaffoldAssetUrl(input: string | null): string
export function scaffoldAssetUrl(input: (string | null)[]): string[]
export function scaffoldAssetUrl(input: string | null | (string | null)[]): string | string[] {
if (Array.isArray(input)) {
return input.map(key => scaffoldAssetUrl(key) as string);
}
if (!input) {
return '';
}
const normalizedKey = input.replace(/^\/+/, '');
const domain = assetsDomain.endsWith('/')
? assetsDomain.slice(0, -1)
: assetsDomain;
return `${domain}/${normalizedKey}`;
}
/**
* Generate a signed URL from an S3 URL
* @param s3Url The full S3 URL (e.g., https://bucket-name.s3.region.amazonaws.com/path/to/object)
* @param expiresIn Expiration time in seconds (default: 259200 seconds = 3 days)
* @returns A pre-signed URL that provides temporary access to the object
*/
export async function generateSignedUrlFromS3Url(s3UrlRaw: string|null, expiresIn: number = 259200): Promise<string> {
if (!s3UrlRaw) {
return '';
}
const s3Url = s3UrlRaw
try {
// Check if we have a cached signed URL
const cachedUrl = signedUrlCache.get(s3Url);
if (cachedUrl) {
// Found in cache, return it
return cachedUrl;
}
// Create the command to get the object
const command = new GetObjectCommand({
Bucket: s3BucketName,
Key: s3Url,
});
// Generate the signed URL
const signedUrl = await getSignedUrl(s3Client, command, { expiresIn });
// Cache the signed URL with TTL matching the expiration time (convert seconds to milliseconds)
signedUrlCache.set(s3Url, signedUrl, (expiresIn * 1000) - 60000); // Subtract 1 minute to ensure it doesn't expire before use
return signedUrl;
} catch (error) {
console.error("Error generating signed URL:", error);
throw new Error("Failed to generate signed URL");
}
}
/**
* Get the original S3 URL from a signed URL
* @param signedUrl The signed URL
* @returns The original S3 URL if found in cache, otherwise null
*/
export function getOriginalUrlFromSignedUrl(signedUrl: string|null): string|null {
if (!signedUrl) {
return null;
}
// Try to find the original URL in our cache
const originalUrl = signedUrlCache.getOriginalUrl(signedUrl);
return originalUrl || null;
}
/**
* Generate signed URLs for multiple S3 URLs
* @param s3Urls Array of S3 URLs or null values
* @param expiresIn Expiration time in seconds (default: 259200 seconds = 3 days)
* @returns Array of signed URLs (empty strings for null/invalid inputs)
*/
export async function generateSignedUrlsFromS3Urls(s3Urls: (string|null)[], expiresIn: number = 259200): Promise<string[]> {
if (!s3Urls || !s3Urls.length) {
return [];
}
try {
// Process URLs in parallel for better performance
const signedUrls = await Promise.all(
s3Urls.map(url => generateSignedUrlFromS3Url(url, expiresIn).catch(() => ''))
);
return signedUrls;
} catch (error) {
console.error("Error generating multiple signed URLs:", error);
// Return an array of empty strings with the same length as input
return s3Urls.map(() => '');
}
}
export async function generateUploadUrl(key: string, mimeType: string, expiresIn: number = 180): Promise<string> {
try {
// Insert record into upload_url_status
await db.insert(uploadUrlStatus).values({
key: key,
status: 'pending',
});
// Generate signed upload URL
const command = new PutObjectCommand({
Bucket: s3BucketName,
Key: key,
ContentType: mimeType,
});
const signedUrl = await getSignedUrl(s3Client, command, { expiresIn });
return signedUrl;
} catch (error) {
console.error('Error generating upload URL:', error);
throw new Error('Failed to generate upload URL');
}
}
// export function extractKeyFromPresignedUrl(url:string) {
// const u = new URL(url);
// const rawKey = u.pathname.replace(/^\/+/, ""); // remove leading slash
// return decodeURIComponent(rawKey);
// }
// New function (excludes bucket name)
export function extractKeyFromPresignedUrl(url: string): string {
const u = new URL(url);
const rawKey = u.pathname.replace(/^\/+/, ""); // remove leading slash
const decodedKey = decodeURIComponent(rawKey);
// Remove bucket prefix
const parts = decodedKey.split('/');
parts.shift(); // Remove bucket name
return parts.join('/');
}
export async function claimUploadUrl(url: string): Promise<void> {
try {
const semiKey = extractKeyFromPresignedUrl(url);
const key = s3BucketName+'/'+ semiKey
// Update status to 'claimed' if currently 'pending'
const result = await db
.update(uploadUrlStatus)
.set({ status: 'claimed' })
.where(and(eq(uploadUrlStatus.key, semiKey), eq(uploadUrlStatus.status, 'pending')))
.returning();
if (result.length === 0) {
throw new Error('Upload URL not found or already claimed');
}
} catch (error) {
console.error('Error claiming upload URL:', error);
throw new Error('Failed to claim upload URL');
}
}