feat: upload module (#486)

* feat: wip upload module

* feat: local storage and serve local images

* feat: protect against injections

* feat: server local and s3 files

* fix: use storage location when serving local files

* feat: cross field env validation
This commit is contained in:
Jérémy M
2023-07-04 16:02:44 +02:00
committed by GitHub
parent 820ef184d3
commit 5e1fc1ad11
52 changed files with 2632 additions and 64 deletions

View File

@ -0,0 +1,135 @@
import { Injectable } from '@nestjs/common';
import sharp from 'sharp';
import { S3StorageService } from 'src/integrations/s3-storage/s3-storage.service';
import { kebabCase } from 'src/utils/kebab-case';
import { EnvironmentService } from 'src/integrations/environment/environment.service';
import { LocalStorageService } from 'src/integrations/local-storage/local-storage.service';
import { getCropSize } from 'src/utils/image';
import { settings } from 'src/constants/settings';
import { FileFolder } from '../interfaces/file-folder.interface';
@Injectable()
export class FileUploadService {
constructor(
private readonly s3Storage: S3StorageService,
private readonly localStorage: LocalStorageService,
private readonly environmentService: EnvironmentService,
) {}
async uploadFile({
file,
name,
mimeType,
fileFolder,
}: {
file: Buffer | Uint8Array | string;
name: string;
mimeType: string | undefined;
fileFolder: FileFolder;
}) {
const storageType = this.environmentService.getStorageType();
switch (storageType) {
case 's3': {
await this.uploadFileToS3(file, name, mimeType, fileFolder);
return {
name: `/${name}`,
};
}
case 'local':
default: {
await this.uploadToLocal(file, name, fileFolder);
return {
name: `/${name}`,
};
}
}
}
async uploadImage({
file,
name,
mimeType,
fileFolder,
}: {
file: Buffer | Uint8Array | string;
name: string;
mimeType: string | undefined;
fileFolder: FileFolder;
}) {
// Get all cropSizes for this fileFolder
const cropSizes = settings.storage.imageCropSizes[fileFolder];
// Extract the values from ShortCropSize
const sizes = cropSizes.map((shortSize) => getCropSize(shortSize));
// Crop images based on sizes
const images = await Promise.all(
sizes.map((size) =>
sharp(file).resize({
[size?.type || 'width']: size?.value ?? undefined,
}),
),
);
// Upload all images to corresponding folders
await Promise.all(
images.map(async (image, index) => {
const buffer = await image.toBuffer();
return this.uploadFile({
file: buffer,
name: `${cropSizes[index]}/${name}`,
mimeType,
fileFolder,
});
}),
);
return {
name: `/${name}`,
};
}
private async uploadToLocal(
file: Buffer | Uint8Array | string,
name: string,
fileFolder: FileFolder,
): Promise<void> {
const folderName = kebabCase(fileFolder.toString());
try {
const result = await this.localStorage.uploadFile({
file,
name,
folder: folderName,
});
return result;
} catch (err) {
console.log('uploadFile error: ', err);
throw err;
}
}
private async uploadFileToS3(
file: Buffer | Uint8Array | string,
name: string,
mimeType: string | undefined,
fileFolder: FileFolder,
) {
// Aws only accept bucket with kebab-case name
const bucketFolderName = kebabCase(fileFolder.toString());
try {
const result = await this.s3Storage.uploadFile({
Key: `${bucketFolderName}/${name}`,
Body: file,
ContentType: mimeType,
});
return result;
} catch (err) {
console.log('uploadFile error: ', err);
throw err;
}
}
}