fixed image previews

This commit is contained in:
jackiettran
2025-12-30 22:49:34 -05:00
parent 807082eebf
commit 1b4e86be29
3 changed files with 41 additions and 14 deletions

View File

@@ -4,6 +4,7 @@ const { authenticateToken } = require("../middleware/auth");
const { uploadPresignLimiter } = require("../middleware/rateLimiter"); const { uploadPresignLimiter } = require("../middleware/rateLimiter");
const s3Service = require("../services/s3Service"); const s3Service = require("../services/s3Service");
const S3OwnershipService = require("../services/s3OwnershipService"); const S3OwnershipService = require("../services/s3OwnershipService");
const { v4: uuidv4 } = require("uuid");
const logger = require("../utils/logger"); const logger = require("../utils/logger");
const MAX_BATCH_SIZE = 20; const MAX_BATCH_SIZE = 20;
@@ -63,6 +64,7 @@ router.post(
/** /**
* POST /api/upload/presign-batch * POST /api/upload/presign-batch
* Get presigned URLs for uploading multiple files to S3 * Get presigned URLs for uploading multiple files to S3
* All files in a batch share the same UUID base for coordinated variant uploads
*/ */
router.post( router.post(
"/presign-batch", "/presign-batch",
@@ -96,13 +98,17 @@ router.post(
} }
} }
// Generate one shared UUID for all files in this batch
const sharedBaseKey = uuidv4();
const results = await Promise.all( const results = await Promise.all(
files.map((f) => files.map((f) =>
s3Service.getPresignedUploadUrl( s3Service.getPresignedUploadUrl(
uploadType, uploadType,
f.contentType, f.contentType,
f.fileName, f.fileName,
f.fileSize f.fileSize,
sharedBaseKey
) )
) )
); );
@@ -111,9 +117,10 @@ router.post(
userId: req.user.id, userId: req.user.id,
uploadType, uploadType,
count: results.length, count: results.length,
baseKey: sharedBaseKey,
}); });
res.json({ uploads: results }); res.json({ uploads: results, baseKey: sharedBaseKey });
} catch (error) { } catch (error) {
if (error.message.includes("Invalid")) { if (error.message.includes("Invalid")) {
return res.status(400).json({ error: error.message }); return res.status(400).json({ error: error.message });

View File

@@ -112,9 +112,10 @@ class S3Service {
* @param {string} contentType - MIME type of the file * @param {string} contentType - MIME type of the file
* @param {string} fileName - Original filename (used for extension) * @param {string} fileName - Original filename (used for extension)
* @param {number} fileSize - File size in bytes (required for size enforcement) * @param {number} fileSize - File size in bytes (required for size enforcement)
* @param {string} [baseKey] - Optional base key (UUID) for coordinated variant uploads
* @returns {Promise<{uploadUrl: string, key: string, publicUrl: string, expiresAt: Date}>} * @returns {Promise<{uploadUrl: string, key: string, publicUrl: string, expiresAt: Date}>}
*/ */
async getPresignedUploadUrl(uploadType, contentType, fileName, fileSize) { async getPresignedUploadUrl(uploadType, contentType, fileName, fileSize, baseKey = null) {
if (!this.enabled) { if (!this.enabled) {
throw new Error("S3 storage is not enabled"); throw new Error("S3 storage is not enabled");
} }
@@ -135,8 +136,21 @@ class S3Service {
); );
} }
// Extract known variant suffix from fileName if present (e.g., "photo_th.jpg" -> "_th")
const ext = path.extname(fileName) || this.getExtFromMime(contentType); const ext = path.extname(fileName) || this.getExtFromMime(contentType);
const key = `${config.folder}/${uuidv4()}${ext}`; const baseName = path.basename(fileName, ext);
// Only recognize known variant suffixes
let suffix = "";
if (baseName.endsWith("_th")) {
suffix = "_th";
} else if (baseName.endsWith("_md")) {
suffix = "_md";
}
// Use provided baseKey or generate new UUID
const uuid = baseKey || uuidv4();
const key = `${config.folder}/${uuid}${suffix}${ext}`;
const cacheDirective = config.public ? "public" : "private"; const cacheDirective = config.public ? "public" : "private";
const command = new PutObjectCommand({ const command = new PutObjectCommand({

View File

@@ -59,13 +59,19 @@ export async function getPresignedUrl(
return response.data; return response.data;
} }
interface BatchPresignResponse {
uploads: PresignedUrlResponse[];
baseKey: string;
}
/** /**
* Get presigned URLs for uploading multiple files * Get presigned URLs for uploading multiple files
* All files share the same base UUID for coordinated variant uploads
*/ */
export async function getPresignedUrls( export async function getPresignedUrls(
uploadType: UploadType, uploadType: UploadType,
files: File[] files: File[]
): Promise<PresignedUrlResponse[]> { ): Promise<BatchPresignResponse> {
const response = await api.post("/upload/presign-batch", { const response = await api.post("/upload/presign-batch", {
uploadType, uploadType,
files: files.map((f) => ({ files: files.map((f) => ({
@@ -74,7 +80,7 @@ export async function getPresignedUrls(
fileSize: f.size, fileSize: f.size,
})), })),
}); });
return response.data.uploads; return { uploads: response.data.uploads, baseKey: response.data.baseKey };
} }
/** /**
@@ -223,9 +229,9 @@ export async function uploadImageWithVariants(
throw new Error("Failed to resize image"); throw new Error("Failed to resize image");
} }
// Get presigned URLs for all variants // Get presigned URLs for all variants (all share same base UUID)
const files = resizedImages.map((r) => r.file); const files = resizedImages.map((r) => r.file);
const presignedUrls = await getPresignedUrls(uploadType, files); const { uploads: presignedUrls } = await getPresignedUrls(uploadType, files);
// Upload all variants in parallel with combined progress // Upload all variants in parallel with combined progress
const totalBytes = files.reduce((sum, f) => sum + f.size, 0); const totalBytes = files.reduce((sum, f) => sum + f.size, 0);
@@ -251,15 +257,15 @@ export async function uploadImageWithVariants(
const keys = presignedUrls.map((p) => p.key); const keys = presignedUrls.map((p) => p.key);
await confirmUploads(keys); await confirmUploads(keys);
// Find the original variant key (no suffix) for database storage
const originalIdx = resizedImages.findIndex((r) => r.variant.size === "original");
const baseKey = presignedUrls[originalIdx]?.key || presignedUrls[0].key;
if (onProgress) onProgress(100); if (onProgress) onProgress(100);
// Use the baseKey returned by the backend (shared UUID for all variants)
// The stored key format is: items/uuid.jpg (original), and variants are items/uuid_th.jpg, items/uuid_md.jpg
const originalKey = keys.find((k) => !k.includes("_th") && !k.includes("_md")) || keys[0];
return { return {
baseKey, baseKey: originalKey,
publicUrl: getPublicImageUrl(baseKey), publicUrl: getPublicImageUrl(originalKey),
variants: keys, variants: keys,
}; };
} }