diff --git a/backend/src/collections/image-db/image-file-db.service.ts b/backend/src/collections/image-db/image-file-db.service.ts index b6afa4d..5f67a77 100644 --- a/backend/src/collections/image-db/image-file-db.service.ts +++ b/backend/src/collections/image-db/image-file-db.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@nestjs/common'; import { InjectRepository } from '@nestjs/typeorm'; -import { ImageFileType } from 'picsur-shared/dist/dto/image-file-types.enum'; +import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum'; import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types'; import { LessThan, Repository } from 'typeorm'; import { EImageDerivativeBackend } from '../../models/entities/image-derivative.entity'; @@ -20,19 +20,19 @@ export class ImageFileDBService { public async setFile( imageId: string, - type: ImageFileType, + variant: ImageEntryVariant, file: Buffer, - mime: string, + filetype: string, ): AsyncFailable { const imageFile = new EImageFileBackend(); imageFile.image_id = imageId; - imageFile.type = type; - imageFile.mime = mime; + imageFile.variant = variant; + imageFile.filetype = filetype; imageFile.data = file; try { await this.imageFileRepo.upsert(imageFile, { - conflictPaths: ['image_id', 'type'], + conflictPaths: ['image_id', 'variant'], }); } catch (e) { return Fail(FT.Database, e); @@ -43,11 +43,11 @@ export class ImageFileDBService { public async getFile( imageId: string, - type: ImageFileType, + variant: ImageEntryVariant, ): AsyncFailable { try { const found = await this.imageFileRepo.findOne({ - where: { image_id: imageId ?? '', type: type ?? '' }, + where: { image_id: imageId ?? '', variant: variant ?? '' }, }); if (!found) return Fail(FT.NotFound, 'Image not found'); @@ -58,20 +58,20 @@ export class ImageFileDBService { } // This is useful because you dont have to pull the whole image file - public async getFileMimes( + public async getFileTypes( imageId: string, - ): AsyncFailable<{ [key in ImageFileType]?: string }> { + ): AsyncFailable<{ [key in ImageEntryVariant]?: string }> { try { const found = await this.imageFileRepo.find({ where: { image_id: imageId }, - select: ['type', 'mime'], + select: ['variant', 'filetype'], }); if (!found) return Fail(FT.NotFound, 'Image not found'); - const result: { [key in ImageFileType]?: string } = {}; + const result: { [key in ImageEntryVariant]?: string } = {}; for (const file of found) { - result[file.type] = file.mime; + result[file.variant] = file.filetype; } return result; @@ -83,13 +83,13 @@ export class ImageFileDBService { public async addDerivative( imageId: string, key: string, - mime: string, + filetype: string, file: Buffer, ): AsyncFailable { const imageDerivative = new EImageDerivativeBackend(); imageDerivative.image_id = imageId; imageDerivative.key = key; - imageDerivative.mime = mime; + imageDerivative.filetype = filetype; imageDerivative.data = file; imageDerivative.last_read = new Date(); diff --git a/backend/src/decorators/image-id/image-full-id.pipe.ts b/backend/src/decorators/image-id/image-full-id.pipe.ts index 8cf542c..b76ae25 100644 --- a/backend/src/decorators/image-id/image-full-id.pipe.ts +++ b/backend/src/decorators/image-id/image-full-id.pipe.ts @@ -1,9 +1,6 @@ -import { - ArgumentMetadata, Injectable, - PipeTransform -} from '@nestjs/common'; -import { Ext2Mime } from 'picsur-shared/dist/dto/mimes.dto'; -import { Fail, FT } from 'picsur-shared/dist/types'; +import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common'; +import { Ext2FileType } from 'picsur-shared/dist/dto/mimes.dto'; +import { Fail, FT, HasFailed } from 'picsur-shared/dist/types'; import { UUIDRegex } from 'picsur-shared/dist/util/common-regex'; import { ImageFullId } from '../../models/constants/image-full-id.const'; @@ -16,19 +13,19 @@ export class ImageFullIdPipe implements PipeTransform { if (!UUIDRegex.test(id)) throw Fail(FT.UsrValidation, 'Invalid image identifier'); - const mime = Ext2Mime(ext); + const filetype = Ext2FileType(ext); - if (mime === undefined) + if (HasFailed(filetype)) throw Fail(FT.UsrValidation, 'Invalid image identifier'); - return { type: 'normal', id, ext, mime }; + return { variant: 'normal', id, ext, filetype }; } else if (split.length === 1) { const [id] = split; if (!UUIDRegex.test(id)) throw Fail(FT.UsrValidation, 'Invalid image identifier'); - return { type: 'original', id, ext: null, mime: null }; + return { variant: 'original', id, ext: null, filetype: null }; } else { throw Fail(FT.UsrValidation, 'Invalid image identifier'); } diff --git a/backend/src/main.ts b/backend/src/main.ts index fcdcfb2..45f768a 100644 --- a/backend/src/main.ts +++ b/backend/src/main.ts @@ -27,7 +27,7 @@ async function bootstrap() { AppModule, fastifyAdapter, { - bufferLogs: true, + bufferLogs: false, }, ); diff --git a/backend/src/managers/image/image-converter.service.ts b/backend/src/managers/image/image-converter.service.ts index 66a32a8..24fc49f 100644 --- a/backend/src/managers/image/image-converter.service.ts +++ b/backend/src/managers/image/image-converter.service.ts @@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common'; import ms from 'ms'; import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto'; import { - FullMime, - SupportedMimeCategory + FileType, + SupportedFileTypeCategory } from 'picsur-shared/dist/dto/mimes.dto'; import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum'; import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types'; @@ -17,28 +17,29 @@ export class ImageConverterService { public async convert( image: Buffer, - sourcemime: FullMime, - targetmime: FullMime, + sourceFiletype: FileType, + targetFiletype: FileType, options: ImageRequestParams, ): AsyncFailable { - if (sourcemime.type !== targetmime.type) { + if (sourceFiletype.category !== sourceFiletype.category) { return Fail( FT.Impossible, "Can't convert from animated to still or vice versa", ); } - if (sourcemime.mime === targetmime.mime) { + if (sourceFiletype.identifier === targetFiletype.identifier) { return { - mime: targetmime.mime, + filetype: targetFiletype.identifier, image, }; } - if (targetmime.type === SupportedMimeCategory.Image) { - return this.convertStill(image, sourcemime, targetmime, options); - } else if (targetmime.type === SupportedMimeCategory.Animation) { - return this.convertAnimation(image, targetmime, options); + if (targetFiletype.category === SupportedFileTypeCategory.Image) { + return this.convertStill(image, sourceFiletype, targetFiletype, options); + } else if (targetFiletype.category === SupportedFileTypeCategory.Animation) { + return this.convertStill(image, sourceFiletype, targetFiletype, options); + //return this.convertAnimation(image, targetmime, options); } else { return Fail(FT.SysValidation, 'Unsupported mime type'); } @@ -46,8 +47,8 @@ export class ImageConverterService { private async convertStill( image: Buffer, - sourcemime: FullMime, - targetmime: FullMime, + sourceFiletype: FileType, + targetFiletype: FileType, options: ImageRequestParams, ): AsyncFailable { const [memLimit, timeLimit] = await Promise.all([ @@ -60,7 +61,7 @@ export class ImageConverterService { const timeLimitMS = ms(timeLimit); const sharpWrapper = new SharpWrapper(timeLimitMS, memLimit); - const hasStarted = await sharpWrapper.start(image, sourcemime); + const hasStarted = await sharpWrapper.start(image, sourceFiletype); if (HasFailed(hasStarted)) return hasStarted; // Do modifications @@ -103,24 +104,24 @@ export class ImageConverterService { } // Export - const result = await sharpWrapper.finish(targetmime, options); + const result = await sharpWrapper.finish(targetFiletype, options); if (HasFailed(result)) return result; return { image: result.data, - mime: targetmime.mime, + filetype: targetFiletype.identifier, }; } private async convertAnimation( image: Buffer, - targetmime: FullMime, + targetFiletype: FileType, options: ImageRequestParams, ): AsyncFailable { // Apng and gif are stored as is for now return { image: image, - mime: targetmime.mime, + filetype: targetFiletype.identifier, }; } } diff --git a/backend/src/managers/image/image-processor.service.ts b/backend/src/managers/image/image-processor.service.ts index d358c13..cad4253 100644 --- a/backend/src/managers/image/image-processor.service.ts +++ b/backend/src/managers/image/image-processor.service.ts @@ -1,24 +1,27 @@ import { Injectable } from '@nestjs/common'; import { - FullMime, - ImageMime, - SupportedMimeCategory + FileType, + ImageFileType, + SupportedFileTypeCategory } from 'picsur-shared/dist/dto/mimes.dto'; -import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types'; -import { QOIColorSpace, QOIencode } from 'qoi-img'; + +import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types'; +import { ParseFileType } from 'picsur-shared/dist/util/parse-mime'; +import { ImageConverterService } from './image-converter.service'; import { ImageResult } from './imageresult'; -import { UniversalSharp } from './universal-sharp'; @Injectable() export class ImageProcessorService { + constructor(private readonly imageConverter: ImageConverterService) {} + public async process( image: Buffer, - mime: FullMime, + filetype: FileType, ): AsyncFailable { - if (mime.type === SupportedMimeCategory.Image) { - return await this.processStill(image, mime); - } else if (mime.type === SupportedMimeCategory.Animation) { - return await this.processAnimation(image, mime); + if (filetype.category === SupportedFileTypeCategory.Image) { + return await this.processStill(image, filetype); + } else if (filetype.category === SupportedFileTypeCategory.Animation) { + return await this.processAnimation(image, filetype); } else { return Fail(FT.SysValidation, 'Unsupported mime type'); } @@ -26,48 +29,22 @@ export class ImageProcessorService { private async processStill( image: Buffer, - mime: FullMime, + filetype: FileType, ): AsyncFailable { - let processedMime = mime.mime; + const outputFileType = ParseFileType(ImageFileType.QOI); + if (HasFailed(outputFileType)) return outputFileType; - let sharpImage = UniversalSharp(image, mime); - processedMime = ImageMime.QOI; - - sharpImage = sharpImage.toColorspace('srgb'); - - const processedImage = await sharpImage.raw().toBuffer({ - resolveWithObject: true, - }); - - if ( - processedImage.info.width >= 32768 || - processedImage.info.height >= 32768 - ) { - return Fail(FT.UsrValidation, 'Image too large'); - } - - // Png can be more efficient than QOI, but its just sooooooo slow - const qoiImage = QOIencode(processedImage.data, { - channels: processedImage.info.channels, - colorspace: QOIColorSpace.SRGB, - height: processedImage.info.height, - width: processedImage.info.width, - }); - - return { - image: qoiImage, - mime: processedMime, - }; + return this.imageConverter.convert(image, filetype, outputFileType, {}); } private async processAnimation( image: Buffer, - mime: FullMime, + filetype: FileType, ): AsyncFailable { // Apng and gif are stored as is for now return { image: image, - mime: mime.mime, + filetype: filetype.identifier, }; } } diff --git a/backend/src/managers/image/image.service.ts b/backend/src/managers/image/image.service.ts index 55c3de0..aa3e239 100644 --- a/backend/src/managers/image/image.service.ts +++ b/backend/src/managers/image/image.service.ts @@ -2,13 +2,16 @@ import { Injectable, Logger } from '@nestjs/common'; import Crypto from 'crypto'; import { fileTypeFromBuffer, FileTypeResult } from 'file-type'; import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto'; -import { ImageFileType } from 'picsur-shared/dist/dto/image-file-types.enum'; -import { FullMime } from 'picsur-shared/dist/dto/mimes.dto'; +import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum'; +import { FileType } from 'picsur-shared/dist/dto/mimes.dto'; import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum'; import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.enum'; import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types'; import { FindResult } from 'picsur-shared/dist/types/find-result'; -import { ParseMime } from 'picsur-shared/dist/util/parse-mime'; +import { + ParseFileType, + ParseMime2FileType +} from 'picsur-shared/dist/util/parse-mime'; import { IsQOI } from 'qoi-img'; import { ImageDBService } from '../../collections/image-db/image-db.service'; import { ImageFileDBService } from '../../collections/image-db/image-file-db.service'; @@ -57,8 +60,8 @@ export class ImageManagerService { image: Buffer, userid: string, ): AsyncFailable { - const fullMime = await this.getFullMimeFromBuffer(image); - if (HasFailed(fullMime)) return fullMime; + const fileType = await this.getFileTypeFromBuffer(image); + if (HasFailed(fileType)) return fileType; // Check if need to save orignal const keepOriginal = await this.userPref.getBooleanPreference( @@ -68,7 +71,7 @@ export class ImageManagerService { if (HasFailed(keepOriginal)) return keepOriginal; // Process - const processResult = await this.processService.process(image, fullMime); + const processResult = await this.processService.process(image, fileType); if (HasFailed(processResult)) return processResult; // Save processed to db @@ -77,18 +80,18 @@ export class ImageManagerService { const imageFileEntity = await this.imageFilesService.setFile( imageEntity.id, - ImageFileType.MASTER, + ImageEntryVariant.MASTER, processResult.image, - processResult.mime, + processResult.filetype, ); if (HasFailed(imageFileEntity)) return imageFileEntity; if (keepOriginal) { const originalFileEntity = await this.imageFilesService.setFile( imageEntity.id, - ImageFileType.ORIGINAL, + ImageEntryVariant.ORIGINAL, image, - fullMime.mime, + fileType.identifier, ); if (HasFailed(originalFileEntity)) return originalFileEntity; } @@ -98,13 +101,13 @@ export class ImageManagerService { public async getConverted( imageId: string, - mime: string, + fileType: string, options: ImageRequestParams, ): AsyncFailable { - const targetMime = ParseMime(mime); - if (HasFailed(targetMime)) return targetMime; + const targetFileType = ParseFileType(fileType); + if (HasFailed(targetFileType)) return targetFileType; - const converted_key = this.getConvertHash({ mime, ...options }); + const converted_key = this.getConvertHash({ mime: fileType, ...options }); const [save_derivatives, allow_editing] = await Promise.all([ this.sysPref.getBooleanPreference(SysPreference.SaveDerivatives), @@ -124,21 +127,21 @@ export class ImageManagerService { const masterImage = await this.getMaster(imageId); if (HasFailed(masterImage)) return masterImage; - const sourceMime = ParseMime(masterImage.mime); - if (HasFailed(sourceMime)) return sourceMime; + const sourceFileType = ParseFileType(masterImage.filetype); + if (HasFailed(sourceFileType)) return sourceFileType; const startTime = Date.now(); const convertResult = await this.convertService.convert( masterImage.data, - sourceMime, - targetMime, + sourceFileType, + targetFileType, allow_editing ? options : {}, ); if (HasFailed(convertResult)) return convertResult; this.logger.verbose( - `Converted ${imageId} from ${sourceMime.mime} to ${ - targetMime.mime + `Converted ${imageId} from ${sourceFileType.identifier} to ${ + targetFileType.identifier } in ${Date.now() - startTime}ms`, ); @@ -146,12 +149,12 @@ export class ImageManagerService { return await this.imageFilesService.addDerivative( imageId, converted_key, - convertResult.mime, + convertResult.filetype, convertResult.image, ); } else { const derivative = new EImageDerivativeBackend(); - derivative.mime = convertResult.mime; + derivative.filetype = convertResult.filetype; derivative.data = convertResult.image; derivative.image_id = imageId; derivative.key = converted_key; @@ -164,52 +167,52 @@ export class ImageManagerService { // File getters ============================================================== public async getMaster(imageId: string): AsyncFailable { - return this.imageFilesService.getFile(imageId, ImageFileType.MASTER); + return this.imageFilesService.getFile(imageId, ImageEntryVariant.MASTER); } - public async getMasterMime(imageId: string): AsyncFailable { - const mime = await this.imageFilesService.getFileMimes(imageId); + public async getMasterFileType(imageId: string): AsyncFailable { + const mime = await this.imageFilesService.getFileTypes(imageId); if (HasFailed(mime)) return mime; - if (mime.master === undefined) return Fail(FT.NotFound, 'No master file'); + if (mime['master'] === undefined) return Fail(FT.NotFound, 'No master file'); - return ParseMime(mime.master); + return ParseFileType(mime['master']); } public async getOriginal(imageId: string): AsyncFailable { - return this.imageFilesService.getFile(imageId, ImageFileType.ORIGINAL); + return this.imageFilesService.getFile(imageId, ImageEntryVariant.ORIGINAL); } - public async getOriginalMime(imageId: string): AsyncFailable { - const mime = await this.imageFilesService.getFileMimes(imageId); - if (HasFailed(mime)) return mime; + public async getOriginalFileType(imageId: string): AsyncFailable { + const filetypes = await this.imageFilesService.getFileTypes(imageId); + if (HasFailed(filetypes)) return filetypes; - if (mime.original === undefined) + if (filetypes['original'] === undefined) return Fail(FT.NotFound, 'No original file'); - return ParseMime(mime.original); + return ParseFileType(filetypes['original']); } public async getFileMimes(imageId: string): AsyncFailable<{ - [ImageFileType.MASTER]: string; - [ImageFileType.ORIGINAL]: string | undefined; + [ImageEntryVariant.MASTER]: string; + [ImageEntryVariant.ORIGINAL]: string | undefined; }> { - const result = await this.imageFilesService.getFileMimes(imageId); + const result = await this.imageFilesService.getFileTypes(imageId); if (HasFailed(result)) return result; - if (result[ImageFileType.MASTER] === undefined) { + if (result[ImageEntryVariant.MASTER] === undefined) { return Fail(FT.NotFound, 'No master file found'); } return { - [ImageFileType.MASTER]: result[ImageFileType.MASTER]!, - [ImageFileType.ORIGINAL]: result[ImageFileType.ORIGINAL], + [ImageEntryVariant.MASTER]: result[ImageEntryVariant.MASTER]!, + [ImageEntryVariant.ORIGINAL]: result[ImageEntryVariant.ORIGINAL], }; } // Util stuff ================================================================== - private async getFullMimeFromBuffer(image: Buffer): AsyncFailable { + private async getFileTypeFromBuffer(image: Buffer): AsyncFailable { const filetypeResult: FileTypeResult | undefined = await fileTypeFromBuffer( image, ); @@ -221,8 +224,7 @@ export class ImageManagerService { mime = filetypeResult.mime; } - const fullMime = ParseMime(mime ?? 'other/unknown'); - return fullMime; + return ParseMime2FileType(mime ?? 'other/unknown'); } private getConvertHash(options: object) { diff --git a/backend/src/managers/image/imageresult.ts b/backend/src/managers/image/imageresult.ts index a40dfd9..73204ba 100644 --- a/backend/src/managers/image/imageresult.ts +++ b/backend/src/managers/image/imageresult.ts @@ -1,4 +1,4 @@ export interface ImageResult { image: Buffer; - mime: string; + filetype: string; } diff --git a/backend/src/managers/image/universal-sharp.ts b/backend/src/managers/image/universal-sharp.ts deleted file mode 100644 index bf1e6ae..0000000 --- a/backend/src/managers/image/universal-sharp.ts +++ /dev/null @@ -1,61 +0,0 @@ -import { BMPdecode } from 'bmp-img'; -import { FullMime, ImageMime } from 'picsur-shared/dist/dto/mimes.dto'; -import { QOIdecode } from 'qoi-img'; -import sharp, { Sharp, SharpOptions } from 'sharp'; - -export function UniversalSharp( - image: Buffer, - mime: FullMime, - options?: SharpOptions, -): Sharp { - // if (mime.mime === ImageMime.ICO) { - // return icoSharp(image, options); - // } else - if (mime.mime === ImageMime.BMP) { - return bmpSharp(image, options); - } else if (mime.mime === ImageMime.QOI) { - return qoiSharp(image, options); - } else { - return sharp(image, options); - } -} - -function bmpSharp(image: Buffer, options?: SharpOptions) { - const bitmap = BMPdecode(image); - return sharp(bitmap.pixels, { - ...options, - raw: { - width: bitmap.width, - height: bitmap.height, - channels: bitmap.channels, - }, - }); -} - -// function icoSharp(image: Buffer, options?: SharpOptions) { -// const result = decodeico(image); -// // Get biggest image -// const best = result.sort((a, b) => b.width - a.width)[0]; - -// return sharp(best.data, { -// ...options, -// raw: { -// width: best.width, -// height: best.height, -// channels: 4, -// }, -// }); -// } - -function qoiSharp(image: Buffer, options?: SharpOptions) { - const result = QOIdecode(image); - - return sharp(result.pixels, { - ...options, - raw: { - width: result.width, - height: result.height, - channels: result.channels, - }, - }); -} diff --git a/backend/src/models/constants/image-full-id.const.ts b/backend/src/models/constants/image-full-id.const.ts index cf9715f..58f6a03 100644 --- a/backend/src/models/constants/image-full-id.const.ts +++ b/backend/src/models/constants/image-full-id.const.ts @@ -1,15 +1,15 @@ interface NormalImage { - type: 'normal'; + variant: 'normal'; id: string; ext: string; - mime: string; + filetype: string; } interface OriginalImage { - type: 'original'; + variant: 'original'; id: string; ext: null; - mime: null; + filetype: null; } export type ImageFullId = NormalImage | OriginalImage; diff --git a/backend/src/models/entities/image-derivative.entity.ts b/backend/src/models/entities/image-derivative.entity.ts index 9e866fe..7e3f699 100644 --- a/backend/src/models/entities/image-derivative.entity.ts +++ b/backend/src/models/entities/image-derivative.entity.ts @@ -15,7 +15,7 @@ export class EImageDerivativeBackend { key: string; @Column({ nullable: false }) - mime: string; + filetype: string; @Column({ name: 'last_read', nullable: false }) last_read: Date; diff --git a/backend/src/models/entities/image-file.entity.ts b/backend/src/models/entities/image-file.entity.ts index 1cb2a12..7a7fab4 100644 --- a/backend/src/models/entities/image-file.entity.ts +++ b/backend/src/models/entities/image-file.entity.ts @@ -1,8 +1,8 @@ -import { ImageFileType } from 'picsur-shared/dist/dto/image-file-types.enum'; +import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum'; import { Column, Entity, Index, PrimaryGeneratedColumn, Unique } from 'typeorm'; @Entity() -@Unique(['image_id', 'type']) +@Unique(['image_id', 'variant']) export class EImageFileBackend { @PrimaryGeneratedColumn('uuid') private _id?: string; @@ -12,11 +12,11 @@ export class EImageFileBackend { image_id: string; @Index() - @Column({ nullable: false, enum: ImageFileType }) - type: ImageFileType; + @Column({ nullable: false, enum: ImageEntryVariant }) + variant: ImageEntryVariant; @Column({ nullable: false }) - mime: string; + filetype: string; // Binary data @Column({ type: 'bytea', nullable: false }) diff --git a/backend/src/routes/api/info/info.controller.ts b/backend/src/routes/api/info/info.controller.ts index a717c2a..b83dcdb 100644 --- a/backend/src/routes/api/info/info.controller.ts +++ b/backend/src/routes/api/info/info.controller.ts @@ -4,10 +4,7 @@ import { AllPermissionsResponse, InfoResponse } from 'picsur-shared/dist/dto/api/info.dto'; -import { - AnimMime2ExtMap, - ImageMime2ExtMap -} from 'picsur-shared/dist/dto/mimes.dto'; +import { FileType2Ext, FileType2Mime, SupportedAnimFileTypes, SupportedImageFileTypes } from 'picsur-shared/dist/dto/mimes.dto'; import { HostConfigService } from '../../../config/early/host.config.service'; import { NoPermissions } from '../../../decorators/permissions.decorator'; import { Returns } from '../../../decorators/returns.decorator'; @@ -42,8 +39,18 @@ export class InfoController { @Returns(AllFormatsResponse) async getFormats(): Promise { return { - image: ImageMime2ExtMap, - anim: AnimMime2ExtMap, + image: Object.fromEntries( + SupportedImageFileTypes.map((filetype) => [ + FileType2Mime(filetype), + FileType2Ext(filetype), + ]), + ), + anim: Object.fromEntries( + SupportedAnimFileTypes.map((filetype) => [ + FileType2Mime(filetype), + FileType2Ext(filetype), + ]), + ), }; } } diff --git a/backend/src/routes/image/image.controller.ts b/backend/src/routes/image/image.controller.ts index c3b12f6..754708f 100644 --- a/backend/src/routes/image/image.controller.ts +++ b/backend/src/routes/image/image.controller.ts @@ -1,14 +1,11 @@ -import { - Controller, - Get, - Head, Logger, Query, - Res -} from '@nestjs/common'; +import { Controller, Get, Head, Logger, Query, Res } from '@nestjs/common'; import type { FastifyReply } from 'fastify'; import { ImageMetaResponse, ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto'; +import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum'; +import { FileType2Mime } from 'picsur-shared/dist/dto/mimes.dto'; import { ThrowIfFailed } from 'picsur-shared/dist/types'; import { UsersService } from '../../collections/user-db/user-db.service'; import { ImageFullIdParam } from '../../decorators/image-id/image-full-id.decorator'; @@ -36,16 +33,16 @@ export class ImageController { @Res({ passthrough: true }) res: FastifyReply, @ImageFullIdParam() fullid: ImageFullId, ) { - if (fullid.type === 'original') { - const fullmime = ThrowIfFailed( - await this.imagesService.getOriginalMime(fullid.id), + if (fullid.variant === ImageEntryVariant.ORIGINAL) { + const filetype = ThrowIfFailed( + await this.imagesService.getOriginalFileType(fullid.id), ); - res.type(fullmime.mime); + res.type(ThrowIfFailed(FileType2Mime(filetype.identifier))); return; } - res.type(fullid.mime); + res.type(ThrowIfFailed(FileType2Mime(fullid.filetype))); } @Get(':id') @@ -56,20 +53,20 @@ export class ImageController { @ImageFullIdParam() fullid: ImageFullId, @Query() params: ImageRequestParams, ): Promise { - if (fullid.type === 'original') { + if (fullid.variant === ImageEntryVariant.ORIGINAL) { const image = ThrowIfFailed( await this.imagesService.getOriginal(fullid.id), ); - res.type(image.mime); + res.type(ThrowIfFailed(FileType2Mime(image.filetype))); return image.data; } const image = ThrowIfFailed( - await this.imagesService.getConverted(fullid.id, fullid.mime, params), + await this.imagesService.getConverted(fullid.id, fullid.filetype, params), ); - res.type(image.mime); + res.type(ThrowIfFailed(FileType2Mime(image.filetype))); return image.data; } @@ -81,11 +78,11 @@ export class ImageController { const [fileMimesRes, imageUserRes] = await Promise.all([ this.imagesService.getFileMimes(id), this.userService.findOne(image.user_id), - ]) + ]); - const fileMimes = ThrowIfFailed(fileMimesRes); + const fileTypes = ThrowIfFailed(fileMimesRes); const imageUser = ThrowIfFailed(imageUserRes); - return { image, user: EUserBackend2EUser(imageUser), fileMimes }; + return { image, user: EUserBackend2EUser(imageUser), fileTypes }; } } diff --git a/backend/src/workers/sharp.wrapper.ts b/backend/src/workers/sharp.wrapper.ts index c135492..c8b167d 100644 --- a/backend/src/workers/sharp.wrapper.ts +++ b/backend/src/workers/sharp.wrapper.ts @@ -2,7 +2,7 @@ import { Logger } from '@nestjs/common'; import { ChildProcess, fork } from 'child_process'; import pTimeout from 'p-timeout'; import path from 'path'; -import { FullMime } from 'picsur-shared/dist/dto/mimes.dto'; +import { FileType } from 'picsur-shared/dist/dto/mimes.dto'; import { AsyncFailable, Fail, @@ -41,7 +41,7 @@ export class SharpWrapper { private readonly memory_limit: number, ) {} - public async start(image: Buffer, mime: FullMime): AsyncFailable { + public async start(image: Buffer, filetype: FileType): AsyncFailable { this.worker = fork(SharpWrapper.WORKER_PATH, { serialization: 'advanced', timeout: this.instance_timeout, @@ -79,7 +79,7 @@ export class SharpWrapper { const hasSent = this.sendToWorker({ type: 'init', image, - mime, + filetype, }); if (HasFailed(hasSent)) { this.purge(); @@ -117,7 +117,7 @@ export class SharpWrapper { } public async finish( - targetMime: FullMime, + targetFiletype: FileType, options?: SharpWorkerFinishOptions, ): AsyncFailable { if (!this.worker) { @@ -126,7 +126,7 @@ export class SharpWrapper { const hasSent = this.sendToWorker({ type: 'finish', - mime: targetMime, + filetype: targetFiletype, options: options ?? {}, }); if (HasFailed(hasSent)) { diff --git a/backend/src/workers/sharp/sharp.message.ts b/backend/src/workers/sharp/sharp.message.ts index e92c173..b74e451 100644 --- a/backend/src/workers/sharp/sharp.message.ts +++ b/backend/src/workers/sharp/sharp.message.ts @@ -1,4 +1,4 @@ -import { FullMime } from 'picsur-shared/dist/dto/mimes.dto'; +import { FileType } from 'picsur-shared/dist/dto/mimes.dto'; import { Sharp } from 'sharp'; import { SharpResult } from './universal-sharp'; @@ -33,7 +33,7 @@ export interface SharpWorkerFinishOptions { export interface SharpWorkerInitMessage { type: 'init'; image: Buffer; - mime: FullMime; + filetype: FileType; } export interface SharpWorkerOperationMessage { @@ -43,7 +43,7 @@ export interface SharpWorkerOperationMessage { export interface SharpWorkerFinishMessage { type: 'finish'; - mime: FullMime; + filetype: FileType; options: SharpWorkerFinishOptions; } diff --git a/backend/src/workers/sharp/sharp.worker.ts b/backend/src/workers/sharp/sharp.worker.ts index 41bee1f..2b76a16 100644 --- a/backend/src/workers/sharp/sharp.worker.ts +++ b/backend/src/workers/sharp/sharp.worker.ts @@ -1,4 +1,4 @@ -import { FullMime } from 'picsur-shared/dist/dto/mimes.dto'; +import { FileType } from 'picsur-shared/dist/dto/mimes.dto'; import posix from 'posix.js'; import { Sharp } from 'sharp'; import { @@ -47,7 +47,7 @@ export class SharpWorker { } else if (message.type === 'operation') { this.operation(message); } else if (message.type === 'finish') { - this.finish(message.mime, message.options); + this.finish(message.filetype, message.options); } else { return this.purge('Unknown message type'); } @@ -59,7 +59,7 @@ export class SharpWorker { } this.startTime = Date.now(); - this.sharpi = UniversalSharpIn(message.image, message.mime); + this.sharpi = UniversalSharpIn(message.image, message.filetype); } private operation(message: SharpWorkerOperationMessage): void { @@ -74,7 +74,7 @@ export class SharpWorker { } private async finish( - mime: FullMime, + filetype: FileType, options: SharpWorkerFinishOptions, ): Promise { if (this.sharpi === null) { @@ -85,7 +85,7 @@ export class SharpWorker { this.sharpi = null; try { - const result = await UniversalSharpOut(sharpi, mime, options); + const result = await UniversalSharpOut(sharpi, filetype, options); const processingTime = Date.now() - this.startTime; this.sendMessage({ diff --git a/backend/src/workers/sharp/universal-sharp.ts b/backend/src/workers/sharp/universal-sharp.ts index 5657334..beb256f 100644 --- a/backend/src/workers/sharp/universal-sharp.ts +++ b/backend/src/workers/sharp/universal-sharp.ts @@ -1,5 +1,5 @@ import { BMPdecode, BMPencode } from 'bmp-img'; -import { FullMime, ImageMime } from 'picsur-shared/dist/dto/mimes.dto'; +import { FileType, ImageFileType } from 'picsur-shared/dist/dto/mimes.dto'; import { QOIdecode, QOIencode } from 'qoi-img'; import sharp, { Sharp, SharpOptions } from 'sharp'; @@ -10,16 +10,21 @@ export interface SharpResult { export function UniversalSharpIn( image: Buffer, - mime: FullMime, + filetype: FileType, options?: SharpOptions, ): Sharp { - // if (mime.mime === ImageMime.ICO) { + // if (mime.mime === ImageFileType.ICO) { // return icoSharpIn(image, options); // } else - if (mime.mime === ImageMime.BMP) { + if (filetype.identifier === ImageFileType.BMP) { return bmpSharpIn(image, options); - } else if (mime.mime === ImageMime.QOI) { + } else if (filetype.identifier === ImageFileType.QOI) { return qoiSharpIn(image, options); + // } else if (filetype.identifier === AnimFileType.GIF) { + // return sharp(image, { + // ...options, + // animated: true, + // }); } else { return sharp(image, options); } @@ -67,40 +72,43 @@ function qoiSharpIn(image: Buffer, options?: SharpOptions) { export async function UniversalSharpOut( image: Sharp, - mime: FullMime, + filetype: FileType, options?: { quality?: number; }, ): Promise { let result: SharpResult | undefined; - switch (mime.mime) { - case ImageMime.PNG: + switch (filetype.identifier) { + case ImageFileType.PNG: result = await image .png({ quality: options?.quality }) .toBuffer({ resolveWithObject: true }); break; - case ImageMime.JPEG: + case ImageFileType.JPEG: result = await image .jpeg({ quality: options?.quality }) .toBuffer({ resolveWithObject: true }); break; - case ImageMime.TIFF: + case ImageFileType.TIFF: result = await image .tiff({ quality: options?.quality }) .toBuffer({ resolveWithObject: true }); break; - case ImageMime.WEBP: + case ImageFileType.WEBP: result = await image .webp({ quality: options?.quality }) .toBuffer({ resolveWithObject: true }); break; - case ImageMime.BMP: + case ImageFileType.BMP: result = await bmpSharpOut(image); break; - case ImageMime.QOI: + case ImageFileType.QOI: result = await qoiSharpOut(image); break; + // case AnimFileType.GIF: + // result = await image.gif().toBuffer({ resolveWithObject: true }); + // break; default: throw new Error('Unsupported mime type'); } diff --git a/frontend/src/app/components/picsur-img/picsur-img.component.ts b/frontend/src/app/components/picsur-img/picsur-img.component.ts index 1494326..91d863d 100644 --- a/frontend/src/app/components/picsur-img/picsur-img.component.ts +++ b/frontend/src/app/components/picsur-img/picsur-img.component.ts @@ -8,10 +8,10 @@ import { SimpleChanges, ViewChild } from '@angular/core'; -import { FullMime, ImageMime } from 'picsur-shared/dist/dto/mimes.dto'; +import { FileType, ImageFileType } from 'picsur-shared/dist/dto/mimes.dto'; import { AsyncFailable, HasFailed } from 'picsur-shared/dist/types'; import { URLRegex } from 'picsur-shared/dist/util/common-regex'; -import { ParseMime } from 'picsur-shared/dist/util/parse-mime'; +import { ParseMime2FileType } from 'picsur-shared/dist/util/parse-mime'; import { ApiService } from 'src/app/services/api/api.service'; import { Logger } from 'src/app/services/logger/logger.service'; import { QoiWorkerService } from 'src/app/workers/qoi-worker.service'; @@ -69,10 +69,10 @@ export class PicsurImgComponent implements OnChanges { } private async update(url: string): AsyncFailable { - const mime = await this.getMime(url); - if (HasFailed(mime)) return mime; + const filetype = await this.getFileType(url); + if (HasFailed(filetype)) return filetype; - if (mime.mime === ImageMime.QOI) { + if (filetype.identifier === ImageFileType.QOI) { const result = await this.qoiWorker.decode(url); if (HasFailed(result)) return result; @@ -88,7 +88,7 @@ export class PicsurImgComponent implements OnChanges { this.changeDetector.markForCheck(); } - private async getMime(url: string): AsyncFailable { + private async getFileType(url: string): AsyncFailable { const response = await this.apiService.head(url); if (HasFailed(response)) { return response; @@ -97,8 +97,7 @@ export class PicsurImgComponent implements OnChanges { const mimeHeader = response.get('content-type') ?? ''; const mime = mimeHeader.split(';')[0]; - const fullMime = ParseMime(mime); - return fullMime; + return ParseMime2FileType(mime); } onInview(e: any) { diff --git a/frontend/src/app/routes/images/images.component.ts b/frontend/src/app/routes/images/images.component.ts index d4eb50c..43abe05 100644 --- a/frontend/src/app/routes/images/images.component.ts +++ b/frontend/src/app/routes/images/images.component.ts @@ -1,7 +1,7 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; -import { ImageMime } from 'picsur-shared/dist/dto/mimes.dto'; +import { ImageFileType } from 'picsur-shared/dist/dto/mimes.dto'; import { EImage } from 'picsur-shared/dist/entities/image.entity'; import { HasFailed } from 'picsur-shared/dist/types/failable'; import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto'; @@ -71,7 +71,7 @@ export class ImagesComponent implements OnInit { getThumbnailUrl(image: EImage) { return ( - this.imageService.GetImageURL(image.id, ImageMime.QOI) + '?height=480' + this.imageService.GetImageURL(image.id, ImageFileType.QOI) + '?height=480' ); } diff --git a/frontend/src/app/routes/view/view.component.ts b/frontend/src/app/routes/view/view.component.ts index aa91174..861232f 100644 --- a/frontend/src/app/routes/view/view.component.ts +++ b/frontend/src/app/routes/view/view.component.ts @@ -2,19 +2,20 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { ImageLinks } from 'picsur-shared/dist/dto/image-links.class'; import { - AnimMime, - FullMime, - ImageMime, - Mime2Ext, - SupportedAnimMimes, - SupportedImageMimes, - SupportedMimeCategory + AnimFileType, + FileType, + FileType2Ext, + ImageFileType, + SupportedAnimFileTypes, + SupportedFileTypeCategory, + SupportedImageFileTypes } from 'picsur-shared/dist/dto/mimes.dto'; + import { EImage } from 'picsur-shared/dist/entities/image.entity'; import { EUser } from 'picsur-shared/dist/entities/user.entity'; import { HasFailed, HasSuccess } from 'picsur-shared/dist/types'; import { UUIDRegex } from 'picsur-shared/dist/util/common-regex'; -import { ParseMime } from 'picsur-shared/dist/util/parse-mime'; +import { ParseFileType } from 'picsur-shared/dist/util/parse-mime'; import { ImageService } from 'src/app/services/api/image.service'; import { UtilService } from 'src/app/util/util-module/util.service'; import { @@ -36,18 +37,18 @@ export class ViewComponent implements OnInit { private id: string; private hasOriginal: boolean = false; - private masterMime: FullMime = { - mime: ImageMime.JPEG, - type: SupportedMimeCategory.Image, + private masterFileType: FileType = { + identifier: ImageFileType.JPEG, + category: SupportedFileTypeCategory.Image, }; - private currentSelectedFormat: string = ImageMime.JPEG; + private currentSelectedFormat: string = ImageFileType.JPEG; public formatOptions: { value: string; key: string; }[] = []; - public setSelectedFormat: string = ImageMime.JPEG; + public setSelectedFormat: string = ImageFileType.JPEG; public previewLink = ''; public imageLinks = new ImageLinks(); @@ -69,25 +70,27 @@ export class ViewComponent implements OnInit { this.previewLink = this.imageService.GetImageURL( this.id, - metadata.fileMimes.master, + metadata.fileTypes.master, ); - this.hasOriginal = metadata.fileMimes.original !== undefined; + this.hasOriginal = metadata.fileTypes.original !== undefined; this.imageUser = metadata.user; this.image = metadata.image; - const masterMime = ParseMime(metadata.fileMimes.master); - if (HasSuccess(masterMime)) { - this.masterMime = masterMime; + const masterFiletype = ParseFileType(metadata.fileTypes.master); + if (HasSuccess(masterFiletype)) { + this.masterFileType = masterFiletype; } - if (this.masterMime.type === SupportedMimeCategory.Image) { - this.setSelectedFormat = ImageMime.JPEG; - } else if (this.masterMime.type === SupportedMimeCategory.Animation) { - this.setSelectedFormat = AnimMime.GIF; + if (this.masterFileType.category === SupportedFileTypeCategory.Image) { + this.setSelectedFormat = ImageFileType.JPEG; + } else if ( + this.masterFileType.category === SupportedFileTypeCategory.Animation + ) { + this.setSelectedFormat = AnimFileType.GIF; } else { - this.setSelectedFormat = metadata.fileMimes.master; + this.setSelectedFormat = metadata.fileTypes.master; } this.selectedFormat(this.setSelectedFormat); @@ -122,7 +125,7 @@ export class ViewComponent implements OnInit { }; if (options.selectedFormat === 'original') { - options.selectedFormat = this.masterMime.mime; + options.selectedFormat = this.masterFileType.identifier; } await this.utilService.showCustomDialog(CustomizeDialogComponent, options, { @@ -157,19 +160,29 @@ export class ViewComponent implements OnInit { key: string; }[] = []; - if (this.masterMime.type === SupportedMimeCategory.Image) { + if (this.masterFileType.category === SupportedFileTypeCategory.Image) { newOptions.push( - ...SupportedImageMimes.map((mime) => ({ - value: Mime2Ext(mime)?.toUpperCase() ?? 'Error', - key: mime, - })), + ...SupportedImageFileTypes.map((mime) => { + let ext = FileType2Ext(mime); + if (HasFailed(ext)) ext = 'Error'; + return { + value: ext.toUpperCase(), + key: mime, + }; + }), ); - } else if (this.masterMime.type === SupportedMimeCategory.Animation) { + } else if ( + this.masterFileType.category === SupportedFileTypeCategory.Animation + ) { newOptions.push( - ...SupportedAnimMimes.map((mime) => ({ - value: Mime2Ext(mime)?.toUpperCase() ?? 'Error', - key: mime, - })), + ...SupportedAnimFileTypes.map((mime) => { + let ext = FileType2Ext(mime); + if (HasFailed(ext)) ext = 'Error'; + return { + value: ext.toUpperCase(), + key: mime, + }; + }), ); } diff --git a/frontend/src/app/services/api/api.service.ts b/frontend/src/app/services/api/api.service.ts index 808f861..42c9ac7 100644 --- a/frontend/src/app/services/api/api.service.ts +++ b/frontend/src/app/services/api/api.service.ts @@ -1,9 +1,16 @@ import { Inject, Injectable } from '@angular/core'; import { WINDOW } from '@ng-web-apis/common'; import { ApiResponseSchema } from 'picsur-shared/dist/dto/api/api.dto'; -import { Mime2Ext } from 'picsur-shared/dist/dto/mimes.dto'; -import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types'; +import { FileType2Ext } from 'picsur-shared/dist/dto/mimes.dto'; +import { + AsyncFailable, + Fail, + FT, + HasFailed, + HasSuccess +} from 'picsur-shared/dist/types'; import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto'; +import { ParseMime2FileType } from 'picsur-shared/dist/util/parse-mime'; import { Subject } from 'rxjs'; import { ApiBuffer } from 'src/app/models/dto/api-buffer.dto'; import { ApiError } from 'src/app/models/dto/api-error.dto'; @@ -142,9 +149,14 @@ export class ApiService { } } - const mimeTypeExt = Mime2Ext(mimeType); - if (mimeTypeExt !== undefined && !name.endsWith(mimeTypeExt)) { - name += '.' + mimeTypeExt; + const filetype = ParseMime2FileType(mimeType); + if (HasSuccess(filetype)) { + const ext = FileType2Ext(filetype.identifier); + if (HasSuccess(ext)) { + if (name.endsWith(ext)) { + name += '.' + ext; + } + } } try { diff --git a/frontend/src/app/services/api/image.service.ts b/frontend/src/app/services/api/image.service.ts index 3699166..58394d6 100644 --- a/frontend/src/app/services/api/image.service.ts +++ b/frontend/src/app/services/api/image.service.ts @@ -12,10 +12,10 @@ import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto'; import { ImageLinks } from 'picsur-shared/dist/dto/image-links.class'; -import { Mime2Ext } from 'picsur-shared/dist/dto/mimes.dto'; +import { FileType2Ext } from 'picsur-shared/dist/dto/mimes.dto'; import { EImage } from 'picsur-shared/dist/entities/image.entity'; import { AsyncFailable } from 'picsur-shared/dist/types'; -import { Fail, FT, HasFailed, Open } from 'picsur-shared/dist/types/failable'; +import { Fail, FT, HasFailed, HasSuccess, Open } from 'picsur-shared/dist/types/failable'; import { ImageUploadRequest } from '../../models/dto/image-upload-request.dto'; import { ApiService } from './api.service'; import { UserService } from './user.service'; @@ -103,19 +103,19 @@ export class ImageService { // Non api calls - public GetImageURL(image: string, mime: string | null): string { + public GetImageURL(image: string, filetype: string | null): string { const baseURL = this.location.protocol + '//' + this.location.host; - const extension = mime !== null ? Mime2Ext(mime) : null; + const extension = FileType2Ext(filetype ?? ''); - return `${baseURL}/i/${image}${extension !== null ? '.' + extension : ''}`; + return `${baseURL}/i/${image}${HasSuccess(extension) ? '.' + extension : ''}`; } public GetImageURLCustomized( image: string, - mime: string | null, + filetype: string | null, options: ImageRequestParams, ): string { - const baseURL = this.GetImageURL(image, mime); + const baseURL = this.GetImageURL(image, filetype); const betterOptions = ImageRequestParams.zodSchema.safeParse(options); if (!betterOptions.success) return baseURL; diff --git a/shared/src/dto/api/image.dto.ts b/shared/src/dto/api/image.dto.ts index 5bc3625..9285713 100644 --- a/shared/src/dto/api/image.dto.ts +++ b/shared/src/dto/api/image.dto.ts @@ -2,7 +2,7 @@ import { z } from 'zod'; import { EImageSchema } from '../../entities/image.entity'; import { EUserSchema } from '../../entities/user.entity'; import { createZodDto } from '../../util/create-zod-dto'; -import { ImageFileType } from '../image-file-types.enum'; +import { ImageEntryVariant } from '../image-entry-variant.enum'; const parseBool = (value: unknown): boolean | null => { if (value === true || value === 'true' || value === '1' || value === 'yes') @@ -36,9 +36,9 @@ export class ImageRequestParams extends createZodDto( export const ImageMetaResponseSchema = z.object({ image: EImageSchema, user: EUserSchema, - fileMimes: z.object({ - [ImageFileType.MASTER]: z.string(), - [ImageFileType.ORIGINAL]: z.union([z.string(), z.undefined()]), + fileTypes: z.object({ + [ImageEntryVariant.MASTER]: z.string(), + [ImageEntryVariant.ORIGINAL]: z.union([z.string(), z.undefined()]), }), }); export class ImageMetaResponse extends createZodDto(ImageMetaResponseSchema) {} diff --git a/shared/src/dto/image-file-types.enum.ts b/shared/src/dto/image-entry-variant.enum.ts similarity index 60% rename from shared/src/dto/image-file-types.enum.ts rename to shared/src/dto/image-entry-variant.enum.ts index ae42eae..e9c77b6 100644 --- a/shared/src/dto/image-file-types.enum.ts +++ b/shared/src/dto/image-entry-variant.enum.ts @@ -1,4 +1,4 @@ -export enum ImageFileType { +export enum ImageEntryVariant { ORIGINAL = 'original', MASTER = 'master', } diff --git a/shared/src/dto/mimes.dto.ts b/shared/src/dto/mimes.dto.ts index 0228936..ee1e4bd 100644 --- a/shared/src/dto/mimes.dto.ts +++ b/shared/src/dto/mimes.dto.ts @@ -1,69 +1,110 @@ +import { Fail, Failable, FT } from '../types'; + // Config -export enum ImageMime { - QOI = 'image/x-qoi', - JPEG = 'image/jpeg', - PNG = 'image/png', - WEBP = 'image/webp', - TIFF = 'image/tiff', - BMP = 'image/bmp', - // ICO = 'image/x-icon', +export enum ImageFileType { + QOI = 'image:qoi', + JPEG = 'image:jpeg', + PNG = 'image:png', + WEBP = 'image:webp', + TIFF = 'image:tiff', + BMP = 'image:bmp', + // ICO = 'image:ico', } -export enum AnimMime { - APNG = 'image/apng', - GIF = 'image/gif', +export enum AnimFileType { + GIF = 'anim:gif', + WEBP = 'anim:webp', + //APNG = 'anim:apng', } // Derivatives -export const SupportedImageMimes: string[] = Object.values(ImageMime); -export const SupportedAnimMimes: string[] = Object.values(AnimMime); -export const SupportedMimes: string[] = Object.values({ ...ImageMime, ...AnimMime }); +export const SupportedImageFileTypes: string[] = Object.values(ImageFileType); +export const SupportedAnimFileTypes: string[] = Object.values(AnimFileType); +export const SupportedFileTypes: string[] = Object.values({ + ...ImageFileType, + ...AnimFileType, +}); -export enum SupportedMimeCategory { +export enum SupportedFileTypeCategory { Image = 'image', Animation = 'anim', } -export interface FullMime { - mime: string; - type: SupportedMimeCategory; +export interface FileType { + identifier: string; + category: SupportedFileTypeCategory; } -export const ImageMime2ExtMap: { - [key in ImageMime]: string; +// Converters + +// -- Ext + +const FileType2ExtMap: { + [key in ImageFileType | AnimFileType]: string; } = { - [ImageMime.QOI]: 'qoi', - [ImageMime.JPEG]: 'jpg', - [ImageMime.PNG]: 'png', - [ImageMime.WEBP]: 'webp', - [ImageMime.TIFF]: 'tiff', - [ImageMime.BMP]: 'bmp', - // [ImageMime.ICO]: 'ico', + [AnimFileType.GIF]: 'gif', + [AnimFileType.WEBP]: 'webp', + // [AnimFileType.APNG]: 'apng', + [ImageFileType.QOI]: 'qoi', + [ImageFileType.JPEG]: 'jpg', + [ImageFileType.PNG]: 'png', + [ImageFileType.WEBP]: 'webp', + [ImageFileType.TIFF]: 'tiff', + [ImageFileType.BMP]: 'bmp', + // [ImageFileType.ICO]: 'ico', }; -export const AnimMime2ExtMap: { - [key in AnimMime]: string; -} = { - [AnimMime.GIF]: 'gif', - [AnimMime.APNG]: 'apng', -}; - -export const Mime2ExtMap: { - [key in ImageMime | AnimMime]: string; -} = { - ...ImageMime2ExtMap, - ...AnimMime2ExtMap, -}; - -export const Ext2MimeMap: { +const Ext2FileTypeMap: { [key: string]: string; -} = Object.fromEntries(Object.entries(Mime2ExtMap).map(([k, v]) => [v, k])); +} = Object.fromEntries(Object.entries(FileType2ExtMap).map(([k, v]) => [v, k])); -export const Mime2Ext = (mime: string): string | undefined => { - return Mime2ExtMap[mime as ImageMime | AnimMime]; +export const FileType2Ext = (mime: string): Failable => { + const result = FileType2ExtMap[mime as ImageFileType | AnimFileType]; + if (result === undefined) + return Fail(FT.Internal, undefined, `Unsupported mime type: ${mime}`); + return result; }; -export const Ext2Mime = (ext: string): string | undefined => { - return Ext2MimeMap[ext]; +export const Ext2FileType = (ext: string): Failable => { + const result = Ext2FileTypeMap[ext]; + if (result === undefined) + return Fail(FT.Internal, undefined, `Unsupported ext: ${ext}`); + return result; +}; + +// -- Mime + +const FileType2MimeMap: { + [key in ImageFileType | AnimFileType]: string; +} = { + [AnimFileType.GIF]: 'image/gif', + [AnimFileType.WEBP]: 'image/webp', + // [AnimFileType.APNG]: 'image/apng', + [ImageFileType.QOI]: 'image/x-qoi', + [ImageFileType.JPEG]: 'image/jpeg', + [ImageFileType.PNG]: 'image/png', + [ImageFileType.WEBP]: 'image/webp', + [ImageFileType.TIFF]: 'image/tiff', + [ImageFileType.BMP]: 'image/bmp', + // [ImageFileType.ICO]: 'image/x-icon', +}; + +const Mime2FileTypeMap: { + [key: string]: string; +} = Object.fromEntries( + Object.entries(FileType2MimeMap).map(([k, v]) => [v, k]), +); + +export const Mime2FileType = (mime: string): Failable => { + const result = Mime2FileTypeMap[mime as ImageFileType | AnimFileType]; + if (result === undefined) + return Fail(FT.Internal, undefined, `Unsupported mime type: ${mime}`); + return result; +}; +export const FileType2Mime = (filetype: string): Failable => { + const result = FileType2MimeMap[filetype as ImageFileType | AnimFileType]; + if (result === undefined) + return Fail(FT.Internal, undefined, `Unsupported filetype: ${filetype}`); + return result; }; diff --git a/shared/src/types/failable.ts b/shared/src/types/failable.ts index 0a80da3..bf67e5a 100644 --- a/shared/src/types/failable.ts +++ b/shared/src/types/failable.ts @@ -141,10 +141,20 @@ export function Fail(type: FT, reason?: any, dbgReason?: any): Failure { if (dbgReason === undefined || dbgReason === null) { if (reason === undefined || reason === null) { // If both are null, just return a default error message - return new Failure(type, FTProps[type].message, undefined, undefined); + return new Failure( + type, + FTProps[type].message, + new Error(String(FTProps[type].message)).stack, + undefined, + ); } else if (typeof reason === 'string') { // If it is a string, this was intentionally specified, so pass it through - return new Failure(type, reason, undefined, undefined); + return new Failure( + type, + reason, + new Error(String(reason)).stack, + undefined, + ); } else if (reason instanceof Error) { // In case of an error, we want to keep that hidden, so return the default message // Only send the specifics to debug @@ -159,7 +169,7 @@ export function Fail(type: FT, reason?: any, dbgReason?: any): Failure { return new Failure( type, FTProps[type].message, - undefined, + new Error(String(reason)).stack, String(reason), ); } @@ -168,11 +178,21 @@ export function Fail(type: FT, reason?: any, dbgReason?: any): Failure { const strReason = reason?.toString() ?? FTProps[type].message; if (typeof dbgReason === 'string') { - return new Failure(type, strReason, undefined, dbgReason); + return new Failure( + type, + strReason, + new Error(String(dbgReason)).stack, + dbgReason, + ); } else if (dbgReason instanceof Error) { return new Failure(type, strReason, dbgReason.stack, dbgReason.message); } else { - return new Failure(type, strReason, undefined, String(dbgReason)); + return new Failure( + type, + strReason, + new Error(String(dbgReason)).stack, + String(dbgReason), + ); } } } diff --git a/shared/src/util/parse-mime.ts b/shared/src/util/parse-mime.ts index f4e1296..2da29b8 100644 --- a/shared/src/util/parse-mime.ts +++ b/shared/src/util/parse-mime.ts @@ -1,17 +1,34 @@ import { - FullMime, - SupportedAnimMimes, - SupportedImageMimes, - SupportedMimeCategory + Ext2FileType, + FileType, + Mime2FileType, + SupportedAnimFileTypes, + SupportedFileTypeCategory, + SupportedImageFileTypes } from '../dto/mimes.dto'; -import { Fail, Failable, FT } from '../types'; +import { Fail, Failable, FT, HasFailed } from '../types'; -export function ParseMime(mime: string): Failable { - if (SupportedImageMimes.includes(mime)) - return { mime, type: SupportedMimeCategory.Image }; +export function ParseFileType(filetype: string): Failable { + if (SupportedImageFileTypes.includes(filetype)) + return { identifier: filetype, category: SupportedFileTypeCategory.Image }; - if (SupportedAnimMimes.includes(mime)) - return { mime, type: SupportedMimeCategory.Animation }; + if (SupportedAnimFileTypes.includes(filetype)) + return { + identifier: filetype, + category: SupportedFileTypeCategory.Animation, + }; - return Fail(FT.UsrValidation, 'Unsupported mime type'); + return Fail(FT.UsrValidation, 'Unsupported file type'); +} + +export function ParseExt2FileType(ext: string): Failable { + const result = Ext2FileType(ext); + if (HasFailed(result)) return result; + return ParseFileType(result); +} + +export function ParseMime2FileType(mime: string): Failable { + const result = Mime2FileType(mime); + if (HasFailed(result)) return result; + return ParseFileType(result); }