File size: 4,264 Bytes
8a49743 ab600b2 8a49743 142fc6a 8a49743 142fc6a 8a49743 142fc6a 8a49743 142fc6a 8a49743 142fc6a 8a49743 142fc6a 8a49743 142fc6a 8a49743 142fc6a 8a49743 142fc6a ab600b2 142fc6a ab600b2 142fc6a 8a49743 142fc6a ab600b2 142fc6a 8a49743 |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 |
import sharp from 'sharp';
import { collections, withTransaction } from '$lib/server/db';
import { generateId } from '$lib/utils/generateId';
import type { ClientSession } from 'mongodb';
import { error } from '@sveltejs/kit';
import { s3client } from './s3';
import { DeleteObjectCommand, PutObjectCommand } from '@aws-sdk/client-s3';
import { S3_BUCKET } from '$env/static/private';
import * as mimeTypes from 'mime-types';
import type { ImageData, Picture } from '$lib/types/Picture';
/**
* Upload picture to S3 under different formats, and create a document in db.pictures.
*
* You can associate the picture to a product, and you can pass a callback called at the end that if it fails,
* will cancel everything (remove from DB and S3)
*/
export async function generatePicture(
buffer: Buffer,
name: string,
opts?: { productId?: string; cb?: (session: ClientSession) => Promise<void> }
): Promise<void> {
const image = sharp(buffer);
const { width, height, format } = await image.metadata();
if (!width || !height) {
throw error(400, 'Invalid image: no height or width');
}
if (!format) {
throw error(400, 'Invalid image format');
}
const mime = mimeTypes.lookup(format);
if (!mime) {
throw error(400, 'Invalid image format: ' + format);
}
const _id = generateId(name);
const extension = '.' + mimeTypes.extension(mime);
const uploadedKeys: string[] = [];
const pathPrefix = picturePrefix(opts?.productId);
const path = `${pathPrefix}${_id}${extension}`;
await s3client.send(
new PutObjectCommand({
Bucket: S3_BUCKET,
Key: path,
Body: buffer,
ContentType: mime
})
);
uploadedKeys.push(path);
const original = {
key: path,
width,
height,
size: buffer.length
};
const formats: ImageData[] = [];
try {
if (width <= 2048 && height <= 2048) {
const key = `${pathPrefix}${_id}-${width}x${height}.webp`;
const buffer = await image.toFormat('webp').toBuffer();
await s3client.send(
new PutObjectCommand({
Bucket: S3_BUCKET,
Key: key,
Body: buffer,
ContentType: 'image/webp'
})
);
uploadedKeys.push(key);
formats.push({
width,
height,
key,
size: buffer.length
});
}
for (const size of [2048, 1024, 512, 256, 128]) {
if (width > size || height > size) {
const buffer = await image
.resize(width > height ? { width: size } : { height: size })
.toFormat('webp')
.toBuffer();
// Is it possible to get the new width and height as an intermediate step of the above call instead?
const { width: newWidth, height: newHeight } = await sharp(buffer).metadata();
if (!newWidth || !newHeight) {
throw error(500, 'Could not get resized width and height');
}
const key = `${pathPrefix}${_id}-${newWidth}x${newHeight}.webp`;
await s3client.send(
new PutObjectCommand({
Bucket: S3_BUCKET,
Key: key,
Body: buffer,
ContentType: 'image/webp'
})
);
uploadedKeys.push(key);
formats.push({
width: newWidth,
height: newHeight,
key,
size: buffer.length
});
}
}
await withTransaction(async (session) => {
await collections.pictures.insertOne(
{
_id,
name,
storage: {
original,
formats
},
...(opts?.productId && { productId: opts.productId }),
createdAt: new Date(),
updatedAt: new Date()
},
{ session }
);
if (opts?.cb) {
await opts.cb(session);
}
});
} catch (err) {
// Remove uploaded files
for (const key of uploadedKeys) {
s3client.send(new DeleteObjectCommand({ Bucket: S3_BUCKET, Key: key })).catch();
}
throw err;
}
}
export async function deletePicture(pictureId: Picture['_id']) {
const res = await collections.pictures.findOneAndDelete({ _id: pictureId });
if (!res.value) {
return;
}
for (const format of res.value.storage.formats) {
await s3client
.send(new DeleteObjectCommand({ Bucket: S3_BUCKET, Key: format.key }))
.catch(console.error);
}
await s3client
.send(new DeleteObjectCommand({ Bucket: S3_BUCKET, Key: res.value.storage.original.key }))
.catch(console.error);
}
export function picturePrefix(productId?: string) {
return productId ? `produits/${productId}/` : `photos/`;
}
|