fixed image previews
This commit is contained in:
@@ -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 });
|
||||||
|
|||||||
@@ -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({
|
||||||
|
|||||||
@@ -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,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user