change the way file types are handled

This commit is contained in:
rubikscraft
2022-08-27 14:24:26 +02:00
parent ab600c20b7
commit 0a81b3c25d
27 changed files with 405 additions and 375 deletions

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; 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 { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types';
import { LessThan, Repository } from 'typeorm'; import { LessThan, Repository } from 'typeorm';
import { EImageDerivativeBackend } from '../../models/entities/image-derivative.entity'; import { EImageDerivativeBackend } from '../../models/entities/image-derivative.entity';
@@ -20,19 +20,19 @@ export class ImageFileDBService {
public async setFile( public async setFile(
imageId: string, imageId: string,
type: ImageFileType, variant: ImageEntryVariant,
file: Buffer, file: Buffer,
mime: string, filetype: string,
): AsyncFailable<true> { ): AsyncFailable<true> {
const imageFile = new EImageFileBackend(); const imageFile = new EImageFileBackend();
imageFile.image_id = imageId; imageFile.image_id = imageId;
imageFile.type = type; imageFile.variant = variant;
imageFile.mime = mime; imageFile.filetype = filetype;
imageFile.data = file; imageFile.data = file;
try { try {
await this.imageFileRepo.upsert(imageFile, { await this.imageFileRepo.upsert(imageFile, {
conflictPaths: ['image_id', 'type'], conflictPaths: ['image_id', 'variant'],
}); });
} catch (e) { } catch (e) {
return Fail(FT.Database, e); return Fail(FT.Database, e);
@@ -43,11 +43,11 @@ export class ImageFileDBService {
public async getFile( public async getFile(
imageId: string, imageId: string,
type: ImageFileType, variant: ImageEntryVariant,
): AsyncFailable<EImageFileBackend> { ): AsyncFailable<EImageFileBackend> {
try { try {
const found = await this.imageFileRepo.findOne({ 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'); 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 // This is useful because you dont have to pull the whole image file
public async getFileMimes( public async getFileTypes(
imageId: string, imageId: string,
): AsyncFailable<{ [key in ImageFileType]?: string }> { ): AsyncFailable<{ [key in ImageEntryVariant]?: string }> {
try { try {
const found = await this.imageFileRepo.find({ const found = await this.imageFileRepo.find({
where: { image_id: imageId }, where: { image_id: imageId },
select: ['type', 'mime'], select: ['variant', 'filetype'],
}); });
if (!found) return Fail(FT.NotFound, 'Image not found'); 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) { for (const file of found) {
result[file.type] = file.mime; result[file.variant] = file.filetype;
} }
return result; return result;
@@ -83,13 +83,13 @@ export class ImageFileDBService {
public async addDerivative( public async addDerivative(
imageId: string, imageId: string,
key: string, key: string,
mime: string, filetype: string,
file: Buffer, file: Buffer,
): AsyncFailable<EImageDerivativeBackend> { ): AsyncFailable<EImageDerivativeBackend> {
const imageDerivative = new EImageDerivativeBackend(); const imageDerivative = new EImageDerivativeBackend();
imageDerivative.image_id = imageId; imageDerivative.image_id = imageId;
imageDerivative.key = key; imageDerivative.key = key;
imageDerivative.mime = mime; imageDerivative.filetype = filetype;
imageDerivative.data = file; imageDerivative.data = file;
imageDerivative.last_read = new Date(); imageDerivative.last_read = new Date();

View File

@@ -1,9 +1,6 @@
import { import { ArgumentMetadata, Injectable, PipeTransform } from '@nestjs/common';
ArgumentMetadata, Injectable, import { Ext2FileType } from 'picsur-shared/dist/dto/mimes.dto';
PipeTransform import { Fail, FT, HasFailed } from 'picsur-shared/dist/types';
} from '@nestjs/common';
import { Ext2Mime } from 'picsur-shared/dist/dto/mimes.dto';
import { Fail, FT } from 'picsur-shared/dist/types';
import { UUIDRegex } from 'picsur-shared/dist/util/common-regex'; import { UUIDRegex } from 'picsur-shared/dist/util/common-regex';
import { ImageFullId } from '../../models/constants/image-full-id.const'; import { ImageFullId } from '../../models/constants/image-full-id.const';
@@ -16,19 +13,19 @@ export class ImageFullIdPipe implements PipeTransform<string, ImageFullId> {
if (!UUIDRegex.test(id)) if (!UUIDRegex.test(id))
throw Fail(FT.UsrValidation, 'Invalid image identifier'); 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'); throw Fail(FT.UsrValidation, 'Invalid image identifier');
return { type: 'normal', id, ext, mime }; return { variant: 'normal', id, ext, filetype };
} else if (split.length === 1) { } else if (split.length === 1) {
const [id] = split; const [id] = split;
if (!UUIDRegex.test(id)) if (!UUIDRegex.test(id))
throw Fail(FT.UsrValidation, 'Invalid image identifier'); throw Fail(FT.UsrValidation, 'Invalid image identifier');
return { type: 'original', id, ext: null, mime: null }; return { variant: 'original', id, ext: null, filetype: null };
} else { } else {
throw Fail(FT.UsrValidation, 'Invalid image identifier'); throw Fail(FT.UsrValidation, 'Invalid image identifier');
} }

View File

@@ -27,7 +27,7 @@ async function bootstrap() {
AppModule, AppModule,
fastifyAdapter, fastifyAdapter,
{ {
bufferLogs: true, bufferLogs: false,
}, },
); );

View File

@@ -2,8 +2,8 @@ import { Injectable } from '@nestjs/common';
import ms from 'ms'; import ms from 'ms';
import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto'; import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto';
import { import {
FullMime, FileType,
SupportedMimeCategory SupportedFileTypeCategory
} from 'picsur-shared/dist/dto/mimes.dto'; } from 'picsur-shared/dist/dto/mimes.dto';
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum'; import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types'; import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
@@ -17,28 +17,29 @@ export class ImageConverterService {
public async convert( public async convert(
image: Buffer, image: Buffer,
sourcemime: FullMime, sourceFiletype: FileType,
targetmime: FullMime, targetFiletype: FileType,
options: ImageRequestParams, options: ImageRequestParams,
): AsyncFailable<ImageResult> { ): AsyncFailable<ImageResult> {
if (sourcemime.type !== targetmime.type) { if (sourceFiletype.category !== sourceFiletype.category) {
return Fail( return Fail(
FT.Impossible, FT.Impossible,
"Can't convert from animated to still or vice versa", "Can't convert from animated to still or vice versa",
); );
} }
if (sourcemime.mime === targetmime.mime) { if (sourceFiletype.identifier === targetFiletype.identifier) {
return { return {
mime: targetmime.mime, filetype: targetFiletype.identifier,
image, image,
}; };
} }
if (targetmime.type === SupportedMimeCategory.Image) { if (targetFiletype.category === SupportedFileTypeCategory.Image) {
return this.convertStill(image, sourcemime, targetmime, options); return this.convertStill(image, sourceFiletype, targetFiletype, options);
} else if (targetmime.type === SupportedMimeCategory.Animation) { } else if (targetFiletype.category === SupportedFileTypeCategory.Animation) {
return this.convertAnimation(image, targetmime, options); return this.convertStill(image, sourceFiletype, targetFiletype, options);
//return this.convertAnimation(image, targetmime, options);
} else { } else {
return Fail(FT.SysValidation, 'Unsupported mime type'); return Fail(FT.SysValidation, 'Unsupported mime type');
} }
@@ -46,8 +47,8 @@ export class ImageConverterService {
private async convertStill( private async convertStill(
image: Buffer, image: Buffer,
sourcemime: FullMime, sourceFiletype: FileType,
targetmime: FullMime, targetFiletype: FileType,
options: ImageRequestParams, options: ImageRequestParams,
): AsyncFailable<ImageResult> { ): AsyncFailable<ImageResult> {
const [memLimit, timeLimit] = await Promise.all([ const [memLimit, timeLimit] = await Promise.all([
@@ -60,7 +61,7 @@ export class ImageConverterService {
const timeLimitMS = ms(timeLimit); const timeLimitMS = ms(timeLimit);
const sharpWrapper = new SharpWrapper(timeLimitMS, memLimit); 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; if (HasFailed(hasStarted)) return hasStarted;
// Do modifications // Do modifications
@@ -103,24 +104,24 @@ export class ImageConverterService {
} }
// Export // Export
const result = await sharpWrapper.finish(targetmime, options); const result = await sharpWrapper.finish(targetFiletype, options);
if (HasFailed(result)) return result; if (HasFailed(result)) return result;
return { return {
image: result.data, image: result.data,
mime: targetmime.mime, filetype: targetFiletype.identifier,
}; };
} }
private async convertAnimation( private async convertAnimation(
image: Buffer, image: Buffer,
targetmime: FullMime, targetFiletype: FileType,
options: ImageRequestParams, options: ImageRequestParams,
): AsyncFailable<ImageResult> { ): AsyncFailable<ImageResult> {
// Apng and gif are stored as is for now // Apng and gif are stored as is for now
return { return {
image: image, image: image,
mime: targetmime.mime, filetype: targetFiletype.identifier,
}; };
} }
} }

View File

@@ -1,24 +1,27 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { import {
FullMime, FileType,
ImageMime, ImageFileType,
SupportedMimeCategory SupportedFileTypeCategory
} from 'picsur-shared/dist/dto/mimes.dto'; } 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 { ImageResult } from './imageresult';
import { UniversalSharp } from './universal-sharp';
@Injectable() @Injectable()
export class ImageProcessorService { export class ImageProcessorService {
constructor(private readonly imageConverter: ImageConverterService) {}
public async process( public async process(
image: Buffer, image: Buffer,
mime: FullMime, filetype: FileType,
): AsyncFailable<ImageResult> { ): AsyncFailable<ImageResult> {
if (mime.type === SupportedMimeCategory.Image) { if (filetype.category === SupportedFileTypeCategory.Image) {
return await this.processStill(image, mime); return await this.processStill(image, filetype);
} else if (mime.type === SupportedMimeCategory.Animation) { } else if (filetype.category === SupportedFileTypeCategory.Animation) {
return await this.processAnimation(image, mime); return await this.processAnimation(image, filetype);
} else { } else {
return Fail(FT.SysValidation, 'Unsupported mime type'); return Fail(FT.SysValidation, 'Unsupported mime type');
} }
@@ -26,48 +29,22 @@ export class ImageProcessorService {
private async processStill( private async processStill(
image: Buffer, image: Buffer,
mime: FullMime, filetype: FileType,
): AsyncFailable<ImageResult> { ): AsyncFailable<ImageResult> {
let processedMime = mime.mime; const outputFileType = ParseFileType(ImageFileType.QOI);
if (HasFailed(outputFileType)) return outputFileType;
let sharpImage = UniversalSharp(image, mime); return this.imageConverter.convert(image, filetype, outputFileType, {});
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,
};
} }
private async processAnimation( private async processAnimation(
image: Buffer, image: Buffer,
mime: FullMime, filetype: FileType,
): AsyncFailable<ImageResult> { ): AsyncFailable<ImageResult> {
// Apng and gif are stored as is for now // Apng and gif are stored as is for now
return { return {
image: image, image: image,
mime: mime.mime, filetype: filetype.identifier,
}; };
} }
} }

View File

@@ -2,13 +2,16 @@ import { Injectable, Logger } from '@nestjs/common';
import Crypto from 'crypto'; import Crypto from 'crypto';
import { fileTypeFromBuffer, FileTypeResult } from 'file-type'; import { fileTypeFromBuffer, FileTypeResult } from 'file-type';
import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto'; import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto';
import { ImageFileType } from 'picsur-shared/dist/dto/image-file-types.enum'; import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum';
import { FullMime } from 'picsur-shared/dist/dto/mimes.dto'; import { FileType } from 'picsur-shared/dist/dto/mimes.dto';
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum'; import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.enum'; import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.enum';
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types'; import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
import { FindResult } from 'picsur-shared/dist/types/find-result'; 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 { IsQOI } from 'qoi-img';
import { ImageDBService } from '../../collections/image-db/image-db.service'; import { ImageDBService } from '../../collections/image-db/image-db.service';
import { ImageFileDBService } from '../../collections/image-db/image-file-db.service'; import { ImageFileDBService } from '../../collections/image-db/image-file-db.service';
@@ -57,8 +60,8 @@ export class ImageManagerService {
image: Buffer, image: Buffer,
userid: string, userid: string,
): AsyncFailable<EImageBackend> { ): AsyncFailable<EImageBackend> {
const fullMime = await this.getFullMimeFromBuffer(image); const fileType = await this.getFileTypeFromBuffer(image);
if (HasFailed(fullMime)) return fullMime; if (HasFailed(fileType)) return fileType;
// Check if need to save orignal // Check if need to save orignal
const keepOriginal = await this.userPref.getBooleanPreference( const keepOriginal = await this.userPref.getBooleanPreference(
@@ -68,7 +71,7 @@ export class ImageManagerService {
if (HasFailed(keepOriginal)) return keepOriginal; if (HasFailed(keepOriginal)) return keepOriginal;
// Process // Process
const processResult = await this.processService.process(image, fullMime); const processResult = await this.processService.process(image, fileType);
if (HasFailed(processResult)) return processResult; if (HasFailed(processResult)) return processResult;
// Save processed to db // Save processed to db
@@ -77,18 +80,18 @@ export class ImageManagerService {
const imageFileEntity = await this.imageFilesService.setFile( const imageFileEntity = await this.imageFilesService.setFile(
imageEntity.id, imageEntity.id,
ImageFileType.MASTER, ImageEntryVariant.MASTER,
processResult.image, processResult.image,
processResult.mime, processResult.filetype,
); );
if (HasFailed(imageFileEntity)) return imageFileEntity; if (HasFailed(imageFileEntity)) return imageFileEntity;
if (keepOriginal) { if (keepOriginal) {
const originalFileEntity = await this.imageFilesService.setFile( const originalFileEntity = await this.imageFilesService.setFile(
imageEntity.id, imageEntity.id,
ImageFileType.ORIGINAL, ImageEntryVariant.ORIGINAL,
image, image,
fullMime.mime, fileType.identifier,
); );
if (HasFailed(originalFileEntity)) return originalFileEntity; if (HasFailed(originalFileEntity)) return originalFileEntity;
} }
@@ -98,13 +101,13 @@ export class ImageManagerService {
public async getConverted( public async getConverted(
imageId: string, imageId: string,
mime: string, fileType: string,
options: ImageRequestParams, options: ImageRequestParams,
): AsyncFailable<EImageDerivativeBackend> { ): AsyncFailable<EImageDerivativeBackend> {
const targetMime = ParseMime(mime); const targetFileType = ParseFileType(fileType);
if (HasFailed(targetMime)) return targetMime; 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([ const [save_derivatives, allow_editing] = await Promise.all([
this.sysPref.getBooleanPreference(SysPreference.SaveDerivatives), this.sysPref.getBooleanPreference(SysPreference.SaveDerivatives),
@@ -124,21 +127,21 @@ export class ImageManagerService {
const masterImage = await this.getMaster(imageId); const masterImage = await this.getMaster(imageId);
if (HasFailed(masterImage)) return masterImage; if (HasFailed(masterImage)) return masterImage;
const sourceMime = ParseMime(masterImage.mime); const sourceFileType = ParseFileType(masterImage.filetype);
if (HasFailed(sourceMime)) return sourceMime; if (HasFailed(sourceFileType)) return sourceFileType;
const startTime = Date.now(); const startTime = Date.now();
const convertResult = await this.convertService.convert( const convertResult = await this.convertService.convert(
masterImage.data, masterImage.data,
sourceMime, sourceFileType,
targetMime, targetFileType,
allow_editing ? options : {}, allow_editing ? options : {},
); );
if (HasFailed(convertResult)) return convertResult; if (HasFailed(convertResult)) return convertResult;
this.logger.verbose( this.logger.verbose(
`Converted ${imageId} from ${sourceMime.mime} to ${ `Converted ${imageId} from ${sourceFileType.identifier} to ${
targetMime.mime targetFileType.identifier
} in ${Date.now() - startTime}ms`, } in ${Date.now() - startTime}ms`,
); );
@@ -146,12 +149,12 @@ export class ImageManagerService {
return await this.imageFilesService.addDerivative( return await this.imageFilesService.addDerivative(
imageId, imageId,
converted_key, converted_key,
convertResult.mime, convertResult.filetype,
convertResult.image, convertResult.image,
); );
} else { } else {
const derivative = new EImageDerivativeBackend(); const derivative = new EImageDerivativeBackend();
derivative.mime = convertResult.mime; derivative.filetype = convertResult.filetype;
derivative.data = convertResult.image; derivative.data = convertResult.image;
derivative.image_id = imageId; derivative.image_id = imageId;
derivative.key = converted_key; derivative.key = converted_key;
@@ -164,52 +167,52 @@ export class ImageManagerService {
// File getters ============================================================== // File getters ==============================================================
public async getMaster(imageId: string): AsyncFailable<EImageFileBackend> { public async getMaster(imageId: string): AsyncFailable<EImageFileBackend> {
return this.imageFilesService.getFile(imageId, ImageFileType.MASTER); return this.imageFilesService.getFile(imageId, ImageEntryVariant.MASTER);
} }
public async getMasterMime(imageId: string): AsyncFailable<FullMime> { public async getMasterFileType(imageId: string): AsyncFailable<FileType> {
const mime = await this.imageFilesService.getFileMimes(imageId); const mime = await this.imageFilesService.getFileTypes(imageId);
if (HasFailed(mime)) return mime; 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<EImageFileBackend> { public async getOriginal(imageId: string): AsyncFailable<EImageFileBackend> {
return this.imageFilesService.getFile(imageId, ImageFileType.ORIGINAL); return this.imageFilesService.getFile(imageId, ImageEntryVariant.ORIGINAL);
} }
public async getOriginalMime(imageId: string): AsyncFailable<FullMime> { public async getOriginalFileType(imageId: string): AsyncFailable<FileType> {
const mime = await this.imageFilesService.getFileMimes(imageId); const filetypes = await this.imageFilesService.getFileTypes(imageId);
if (HasFailed(mime)) return mime; if (HasFailed(filetypes)) return filetypes;
if (mime.original === undefined) if (filetypes['original'] === undefined)
return Fail(FT.NotFound, 'No original file'); return Fail(FT.NotFound, 'No original file');
return ParseMime(mime.original); return ParseFileType(filetypes['original']);
} }
public async getFileMimes(imageId: string): AsyncFailable<{ public async getFileMimes(imageId: string): AsyncFailable<{
[ImageFileType.MASTER]: string; [ImageEntryVariant.MASTER]: string;
[ImageFileType.ORIGINAL]: string | undefined; [ImageEntryVariant.ORIGINAL]: string | undefined;
}> { }> {
const result = await this.imageFilesService.getFileMimes(imageId); const result = await this.imageFilesService.getFileTypes(imageId);
if (HasFailed(result)) return result; if (HasFailed(result)) return result;
if (result[ImageFileType.MASTER] === undefined) { if (result[ImageEntryVariant.MASTER] === undefined) {
return Fail(FT.NotFound, 'No master file found'); return Fail(FT.NotFound, 'No master file found');
} }
return { return {
[ImageFileType.MASTER]: result[ImageFileType.MASTER]!, [ImageEntryVariant.MASTER]: result[ImageEntryVariant.MASTER]!,
[ImageFileType.ORIGINAL]: result[ImageFileType.ORIGINAL], [ImageEntryVariant.ORIGINAL]: result[ImageEntryVariant.ORIGINAL],
}; };
} }
// Util stuff ================================================================== // Util stuff ==================================================================
private async getFullMimeFromBuffer(image: Buffer): AsyncFailable<FullMime> { private async getFileTypeFromBuffer(image: Buffer): AsyncFailable<FileType> {
const filetypeResult: FileTypeResult | undefined = await fileTypeFromBuffer( const filetypeResult: FileTypeResult | undefined = await fileTypeFromBuffer(
image, image,
); );
@@ -221,8 +224,7 @@ export class ImageManagerService {
mime = filetypeResult.mime; mime = filetypeResult.mime;
} }
const fullMime = ParseMime(mime ?? 'other/unknown'); return ParseMime2FileType(mime ?? 'other/unknown');
return fullMime;
} }
private getConvertHash(options: object) { private getConvertHash(options: object) {

View File

@@ -1,4 +1,4 @@
export interface ImageResult { export interface ImageResult {
image: Buffer; image: Buffer;
mime: string; filetype: string;
} }

View File

@@ -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,
},
});
}

View File

@@ -1,15 +1,15 @@
interface NormalImage { interface NormalImage {
type: 'normal'; variant: 'normal';
id: string; id: string;
ext: string; ext: string;
mime: string; filetype: string;
} }
interface OriginalImage { interface OriginalImage {
type: 'original'; variant: 'original';
id: string; id: string;
ext: null; ext: null;
mime: null; filetype: null;
} }
export type ImageFullId = NormalImage | OriginalImage; export type ImageFullId = NormalImage | OriginalImage;

View File

@@ -15,7 +15,7 @@ export class EImageDerivativeBackend {
key: string; key: string;
@Column({ nullable: false }) @Column({ nullable: false })
mime: string; filetype: string;
@Column({ name: 'last_read', nullable: false }) @Column({ name: 'last_read', nullable: false })
last_read: Date; last_read: Date;

View File

@@ -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'; import { Column, Entity, Index, PrimaryGeneratedColumn, Unique } from 'typeorm';
@Entity() @Entity()
@Unique(['image_id', 'type']) @Unique(['image_id', 'variant'])
export class EImageFileBackend { export class EImageFileBackend {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
private _id?: string; private _id?: string;
@@ -12,11 +12,11 @@ export class EImageFileBackend {
image_id: string; image_id: string;
@Index() @Index()
@Column({ nullable: false, enum: ImageFileType }) @Column({ nullable: false, enum: ImageEntryVariant })
type: ImageFileType; variant: ImageEntryVariant;
@Column({ nullable: false }) @Column({ nullable: false })
mime: string; filetype: string;
// Binary data // Binary data
@Column({ type: 'bytea', nullable: false }) @Column({ type: 'bytea', nullable: false })

View File

@@ -4,10 +4,7 @@ import {
AllPermissionsResponse, AllPermissionsResponse,
InfoResponse InfoResponse
} from 'picsur-shared/dist/dto/api/info.dto'; } from 'picsur-shared/dist/dto/api/info.dto';
import { import { FileType2Ext, FileType2Mime, SupportedAnimFileTypes, SupportedImageFileTypes } from 'picsur-shared/dist/dto/mimes.dto';
AnimMime2ExtMap,
ImageMime2ExtMap
} from 'picsur-shared/dist/dto/mimes.dto';
import { HostConfigService } from '../../../config/early/host.config.service'; import { HostConfigService } from '../../../config/early/host.config.service';
import { NoPermissions } from '../../../decorators/permissions.decorator'; import { NoPermissions } from '../../../decorators/permissions.decorator';
import { Returns } from '../../../decorators/returns.decorator'; import { Returns } from '../../../decorators/returns.decorator';
@@ -42,8 +39,18 @@ export class InfoController {
@Returns(AllFormatsResponse) @Returns(AllFormatsResponse)
async getFormats(): Promise<AllFormatsResponse> { async getFormats(): Promise<AllFormatsResponse> {
return { return {
image: ImageMime2ExtMap, image: Object.fromEntries(
anim: AnimMime2ExtMap, SupportedImageFileTypes.map((filetype) => [
FileType2Mime(filetype),
FileType2Ext(filetype),
]),
),
anim: Object.fromEntries(
SupportedAnimFileTypes.map((filetype) => [
FileType2Mime(filetype),
FileType2Ext(filetype),
]),
),
}; };
} }
} }

View File

@@ -1,14 +1,11 @@
import { import { Controller, Get, Head, Logger, Query, Res } from '@nestjs/common';
Controller,
Get,
Head, Logger, Query,
Res
} from '@nestjs/common';
import type { FastifyReply } from 'fastify'; import type { FastifyReply } from 'fastify';
import { import {
ImageMetaResponse, ImageMetaResponse,
ImageRequestParams ImageRequestParams
} from 'picsur-shared/dist/dto/api/image.dto'; } 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 { ThrowIfFailed } from 'picsur-shared/dist/types';
import { UsersService } from '../../collections/user-db/user-db.service'; import { UsersService } from '../../collections/user-db/user-db.service';
import { ImageFullIdParam } from '../../decorators/image-id/image-full-id.decorator'; import { ImageFullIdParam } from '../../decorators/image-id/image-full-id.decorator';
@@ -36,16 +33,16 @@ export class ImageController {
@Res({ passthrough: true }) res: FastifyReply, @Res({ passthrough: true }) res: FastifyReply,
@ImageFullIdParam() fullid: ImageFullId, @ImageFullIdParam() fullid: ImageFullId,
) { ) {
if (fullid.type === 'original') { if (fullid.variant === ImageEntryVariant.ORIGINAL) {
const fullmime = ThrowIfFailed( const filetype = ThrowIfFailed(
await this.imagesService.getOriginalMime(fullid.id), await this.imagesService.getOriginalFileType(fullid.id),
); );
res.type(fullmime.mime); res.type(ThrowIfFailed(FileType2Mime(filetype.identifier)));
return; return;
} }
res.type(fullid.mime); res.type(ThrowIfFailed(FileType2Mime(fullid.filetype)));
} }
@Get(':id') @Get(':id')
@@ -56,20 +53,20 @@ export class ImageController {
@ImageFullIdParam() fullid: ImageFullId, @ImageFullIdParam() fullid: ImageFullId,
@Query() params: ImageRequestParams, @Query() params: ImageRequestParams,
): Promise<Buffer> { ): Promise<Buffer> {
if (fullid.type === 'original') { if (fullid.variant === ImageEntryVariant.ORIGINAL) {
const image = ThrowIfFailed( const image = ThrowIfFailed(
await this.imagesService.getOriginal(fullid.id), await this.imagesService.getOriginal(fullid.id),
); );
res.type(image.mime); res.type(ThrowIfFailed(FileType2Mime(image.filetype)));
return image.data; return image.data;
} }
const image = ThrowIfFailed( 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; return image.data;
} }
@@ -81,11 +78,11 @@ export class ImageController {
const [fileMimesRes, imageUserRes] = await Promise.all([ const [fileMimesRes, imageUserRes] = await Promise.all([
this.imagesService.getFileMimes(id), this.imagesService.getFileMimes(id),
this.userService.findOne(image.user_id), this.userService.findOne(image.user_id),
]) ]);
const fileMimes = ThrowIfFailed(fileMimesRes); const fileTypes = ThrowIfFailed(fileMimesRes);
const imageUser = ThrowIfFailed(imageUserRes); const imageUser = ThrowIfFailed(imageUserRes);
return { image, user: EUserBackend2EUser(imageUser), fileMimes }; return { image, user: EUserBackend2EUser(imageUser), fileTypes };
} }
} }

View File

@@ -2,7 +2,7 @@ import { Logger } from '@nestjs/common';
import { ChildProcess, fork } from 'child_process'; import { ChildProcess, fork } from 'child_process';
import pTimeout from 'p-timeout'; import pTimeout from 'p-timeout';
import path from 'path'; import path from 'path';
import { FullMime } from 'picsur-shared/dist/dto/mimes.dto'; import { FileType } from 'picsur-shared/dist/dto/mimes.dto';
import { import {
AsyncFailable, AsyncFailable,
Fail, Fail,
@@ -41,7 +41,7 @@ export class SharpWrapper {
private readonly memory_limit: number, private readonly memory_limit: number,
) {} ) {}
public async start(image: Buffer, mime: FullMime): AsyncFailable<true> { public async start(image: Buffer, filetype: FileType): AsyncFailable<true> {
this.worker = fork(SharpWrapper.WORKER_PATH, { this.worker = fork(SharpWrapper.WORKER_PATH, {
serialization: 'advanced', serialization: 'advanced',
timeout: this.instance_timeout, timeout: this.instance_timeout,
@@ -79,7 +79,7 @@ export class SharpWrapper {
const hasSent = this.sendToWorker({ const hasSent = this.sendToWorker({
type: 'init', type: 'init',
image, image,
mime, filetype,
}); });
if (HasFailed(hasSent)) { if (HasFailed(hasSent)) {
this.purge(); this.purge();
@@ -117,7 +117,7 @@ export class SharpWrapper {
} }
public async finish( public async finish(
targetMime: FullMime, targetFiletype: FileType,
options?: SharpWorkerFinishOptions, options?: SharpWorkerFinishOptions,
): AsyncFailable<SharpResult> { ): AsyncFailable<SharpResult> {
if (!this.worker) { if (!this.worker) {
@@ -126,7 +126,7 @@ export class SharpWrapper {
const hasSent = this.sendToWorker({ const hasSent = this.sendToWorker({
type: 'finish', type: 'finish',
mime: targetMime, filetype: targetFiletype,
options: options ?? {}, options: options ?? {},
}); });
if (HasFailed(hasSent)) { if (HasFailed(hasSent)) {

View File

@@ -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 { Sharp } from 'sharp';
import { SharpResult } from './universal-sharp'; import { SharpResult } from './universal-sharp';
@@ -33,7 +33,7 @@ export interface SharpWorkerFinishOptions {
export interface SharpWorkerInitMessage { export interface SharpWorkerInitMessage {
type: 'init'; type: 'init';
image: Buffer; image: Buffer;
mime: FullMime; filetype: FileType;
} }
export interface SharpWorkerOperationMessage { export interface SharpWorkerOperationMessage {
@@ -43,7 +43,7 @@ export interface SharpWorkerOperationMessage {
export interface SharpWorkerFinishMessage { export interface SharpWorkerFinishMessage {
type: 'finish'; type: 'finish';
mime: FullMime; filetype: FileType;
options: SharpWorkerFinishOptions; options: SharpWorkerFinishOptions;
} }

View File

@@ -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 posix from 'posix.js';
import { Sharp } from 'sharp'; import { Sharp } from 'sharp';
import { import {
@@ -47,7 +47,7 @@ export class SharpWorker {
} else if (message.type === 'operation') { } else if (message.type === 'operation') {
this.operation(message); this.operation(message);
} else if (message.type === 'finish') { } else if (message.type === 'finish') {
this.finish(message.mime, message.options); this.finish(message.filetype, message.options);
} else { } else {
return this.purge('Unknown message type'); return this.purge('Unknown message type');
} }
@@ -59,7 +59,7 @@ export class SharpWorker {
} }
this.startTime = Date.now(); this.startTime = Date.now();
this.sharpi = UniversalSharpIn(message.image, message.mime); this.sharpi = UniversalSharpIn(message.image, message.filetype);
} }
private operation(message: SharpWorkerOperationMessage): void { private operation(message: SharpWorkerOperationMessage): void {
@@ -74,7 +74,7 @@ export class SharpWorker {
} }
private async finish( private async finish(
mime: FullMime, filetype: FileType,
options: SharpWorkerFinishOptions, options: SharpWorkerFinishOptions,
): Promise<void> { ): Promise<void> {
if (this.sharpi === null) { if (this.sharpi === null) {
@@ -85,7 +85,7 @@ export class SharpWorker {
this.sharpi = null; this.sharpi = null;
try { try {
const result = await UniversalSharpOut(sharpi, mime, options); const result = await UniversalSharpOut(sharpi, filetype, options);
const processingTime = Date.now() - this.startTime; const processingTime = Date.now() - this.startTime;
this.sendMessage({ this.sendMessage({

View File

@@ -1,5 +1,5 @@
import { BMPdecode, BMPencode } from 'bmp-img'; 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 { QOIdecode, QOIencode } from 'qoi-img';
import sharp, { Sharp, SharpOptions } from 'sharp'; import sharp, { Sharp, SharpOptions } from 'sharp';
@@ -10,16 +10,21 @@ export interface SharpResult {
export function UniversalSharpIn( export function UniversalSharpIn(
image: Buffer, image: Buffer,
mime: FullMime, filetype: FileType,
options?: SharpOptions, options?: SharpOptions,
): Sharp { ): Sharp {
// if (mime.mime === ImageMime.ICO) { // if (mime.mime === ImageFileType.ICO) {
// return icoSharpIn(image, options); // return icoSharpIn(image, options);
// } else // } else
if (mime.mime === ImageMime.BMP) { if (filetype.identifier === ImageFileType.BMP) {
return bmpSharpIn(image, options); return bmpSharpIn(image, options);
} else if (mime.mime === ImageMime.QOI) { } else if (filetype.identifier === ImageFileType.QOI) {
return qoiSharpIn(image, options); return qoiSharpIn(image, options);
// } else if (filetype.identifier === AnimFileType.GIF) {
// return sharp(image, {
// ...options,
// animated: true,
// });
} else { } else {
return sharp(image, options); return sharp(image, options);
} }
@@ -67,40 +72,43 @@ function qoiSharpIn(image: Buffer, options?: SharpOptions) {
export async function UniversalSharpOut( export async function UniversalSharpOut(
image: Sharp, image: Sharp,
mime: FullMime, filetype: FileType,
options?: { options?: {
quality?: number; quality?: number;
}, },
): Promise<SharpResult> { ): Promise<SharpResult> {
let result: SharpResult | undefined; let result: SharpResult | undefined;
switch (mime.mime) { switch (filetype.identifier) {
case ImageMime.PNG: case ImageFileType.PNG:
result = await image result = await image
.png({ quality: options?.quality }) .png({ quality: options?.quality })
.toBuffer({ resolveWithObject: true }); .toBuffer({ resolveWithObject: true });
break; break;
case ImageMime.JPEG: case ImageFileType.JPEG:
result = await image result = await image
.jpeg({ quality: options?.quality }) .jpeg({ quality: options?.quality })
.toBuffer({ resolveWithObject: true }); .toBuffer({ resolveWithObject: true });
break; break;
case ImageMime.TIFF: case ImageFileType.TIFF:
result = await image result = await image
.tiff({ quality: options?.quality }) .tiff({ quality: options?.quality })
.toBuffer({ resolveWithObject: true }); .toBuffer({ resolveWithObject: true });
break; break;
case ImageMime.WEBP: case ImageFileType.WEBP:
result = await image result = await image
.webp({ quality: options?.quality }) .webp({ quality: options?.quality })
.toBuffer({ resolveWithObject: true }); .toBuffer({ resolveWithObject: true });
break; break;
case ImageMime.BMP: case ImageFileType.BMP:
result = await bmpSharpOut(image); result = await bmpSharpOut(image);
break; break;
case ImageMime.QOI: case ImageFileType.QOI:
result = await qoiSharpOut(image); result = await qoiSharpOut(image);
break; break;
// case AnimFileType.GIF:
// result = await image.gif().toBuffer({ resolveWithObject: true });
// break;
default: default:
throw new Error('Unsupported mime type'); throw new Error('Unsupported mime type');
} }

View File

@@ -8,10 +8,10 @@ import {
SimpleChanges, SimpleChanges,
ViewChild ViewChild
} from '@angular/core'; } 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 { AsyncFailable, HasFailed } from 'picsur-shared/dist/types';
import { URLRegex } from 'picsur-shared/dist/util/common-regex'; 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 { ApiService } from 'src/app/services/api/api.service';
import { Logger } from 'src/app/services/logger/logger.service'; import { Logger } from 'src/app/services/logger/logger.service';
import { QoiWorkerService } from 'src/app/workers/qoi-worker.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<void> { private async update(url: string): AsyncFailable<void> {
const mime = await this.getMime(url); const filetype = await this.getFileType(url);
if (HasFailed(mime)) return mime; if (HasFailed(filetype)) return filetype;
if (mime.mime === ImageMime.QOI) { if (filetype.identifier === ImageFileType.QOI) {
const result = await this.qoiWorker.decode(url); const result = await this.qoiWorker.decode(url);
if (HasFailed(result)) return result; if (HasFailed(result)) return result;
@@ -88,7 +88,7 @@ export class PicsurImgComponent implements OnChanges {
this.changeDetector.markForCheck(); this.changeDetector.markForCheck();
} }
private async getMime(url: string): AsyncFailable<FullMime> { private async getFileType(url: string): AsyncFailable<FileType> {
const response = await this.apiService.head(url); const response = await this.apiService.head(url);
if (HasFailed(response)) { if (HasFailed(response)) {
return response; return response;
@@ -97,8 +97,7 @@ export class PicsurImgComponent implements OnChanges {
const mimeHeader = response.get('content-type') ?? ''; const mimeHeader = response.get('content-type') ?? '';
const mime = mimeHeader.split(';')[0]; const mime = mimeHeader.split(';')[0];
const fullMime = ParseMime(mime); return ParseMime2FileType(mime);
return fullMime;
} }
onInview(e: any) { onInview(e: any) {

View File

@@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { AutoUnsubscribe } from 'ngx-auto-unsubscribe-decorator'; 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 { EImage } from 'picsur-shared/dist/entities/image.entity';
import { HasFailed } from 'picsur-shared/dist/types/failable'; import { HasFailed } from 'picsur-shared/dist/types/failable';
import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto'; import { SnackBarType } from 'src/app/models/dto/snack-bar-type.dto';
@@ -71,7 +71,7 @@ export class ImagesComponent implements OnInit {
getThumbnailUrl(image: EImage) { getThumbnailUrl(image: EImage) {
return ( return (
this.imageService.GetImageURL(image.id, ImageMime.QOI) + '?height=480' this.imageService.GetImageURL(image.id, ImageFileType.QOI) + '?height=480'
); );
} }

View File

@@ -2,19 +2,20 @@ import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ImageLinks } from 'picsur-shared/dist/dto/image-links.class'; import { ImageLinks } from 'picsur-shared/dist/dto/image-links.class';
import { import {
AnimMime, AnimFileType,
FullMime, FileType,
ImageMime, FileType2Ext,
Mime2Ext, ImageFileType,
SupportedAnimMimes, SupportedAnimFileTypes,
SupportedImageMimes, SupportedFileTypeCategory,
SupportedMimeCategory SupportedImageFileTypes
} from 'picsur-shared/dist/dto/mimes.dto'; } from 'picsur-shared/dist/dto/mimes.dto';
import { EImage } from 'picsur-shared/dist/entities/image.entity'; import { EImage } from 'picsur-shared/dist/entities/image.entity';
import { EUser } from 'picsur-shared/dist/entities/user.entity'; import { EUser } from 'picsur-shared/dist/entities/user.entity';
import { HasFailed, HasSuccess } from 'picsur-shared/dist/types'; import { HasFailed, HasSuccess } from 'picsur-shared/dist/types';
import { UUIDRegex } from 'picsur-shared/dist/util/common-regex'; 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 { ImageService } from 'src/app/services/api/image.service';
import { UtilService } from 'src/app/util/util-module/util.service'; import { UtilService } from 'src/app/util/util-module/util.service';
import { import {
@@ -36,18 +37,18 @@ export class ViewComponent implements OnInit {
private id: string; private id: string;
private hasOriginal: boolean = false; private hasOriginal: boolean = false;
private masterMime: FullMime = { private masterFileType: FileType = {
mime: ImageMime.JPEG, identifier: ImageFileType.JPEG,
type: SupportedMimeCategory.Image, category: SupportedFileTypeCategory.Image,
}; };
private currentSelectedFormat: string = ImageMime.JPEG; private currentSelectedFormat: string = ImageFileType.JPEG;
public formatOptions: { public formatOptions: {
value: string; value: string;
key: string; key: string;
}[] = []; }[] = [];
public setSelectedFormat: string = ImageMime.JPEG; public setSelectedFormat: string = ImageFileType.JPEG;
public previewLink = ''; public previewLink = '';
public imageLinks = new ImageLinks(); public imageLinks = new ImageLinks();
@@ -69,25 +70,27 @@ export class ViewComponent implements OnInit {
this.previewLink = this.imageService.GetImageURL( this.previewLink = this.imageService.GetImageURL(
this.id, 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.imageUser = metadata.user;
this.image = metadata.image; this.image = metadata.image;
const masterMime = ParseMime(metadata.fileMimes.master); const masterFiletype = ParseFileType(metadata.fileTypes.master);
if (HasSuccess(masterMime)) { if (HasSuccess(masterFiletype)) {
this.masterMime = masterMime; this.masterFileType = masterFiletype;
} }
if (this.masterMime.type === SupportedMimeCategory.Image) { if (this.masterFileType.category === SupportedFileTypeCategory.Image) {
this.setSelectedFormat = ImageMime.JPEG; this.setSelectedFormat = ImageFileType.JPEG;
} else if (this.masterMime.type === SupportedMimeCategory.Animation) { } else if (
this.setSelectedFormat = AnimMime.GIF; this.masterFileType.category === SupportedFileTypeCategory.Animation
) {
this.setSelectedFormat = AnimFileType.GIF;
} else { } else {
this.setSelectedFormat = metadata.fileMimes.master; this.setSelectedFormat = metadata.fileTypes.master;
} }
this.selectedFormat(this.setSelectedFormat); this.selectedFormat(this.setSelectedFormat);
@@ -122,7 +125,7 @@ export class ViewComponent implements OnInit {
}; };
if (options.selectedFormat === 'original') { if (options.selectedFormat === 'original') {
options.selectedFormat = this.masterMime.mime; options.selectedFormat = this.masterFileType.identifier;
} }
await this.utilService.showCustomDialog(CustomizeDialogComponent, options, { await this.utilService.showCustomDialog(CustomizeDialogComponent, options, {
@@ -157,19 +160,29 @@ export class ViewComponent implements OnInit {
key: string; key: string;
}[] = []; }[] = [];
if (this.masterMime.type === SupportedMimeCategory.Image) { if (this.masterFileType.category === SupportedFileTypeCategory.Image) {
newOptions.push( newOptions.push(
...SupportedImageMimes.map((mime) => ({ ...SupportedImageFileTypes.map((mime) => {
value: Mime2Ext(mime)?.toUpperCase() ?? 'Error', let ext = FileType2Ext(mime);
key: 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( newOptions.push(
...SupportedAnimMimes.map((mime) => ({ ...SupportedAnimFileTypes.map((mime) => {
value: Mime2Ext(mime)?.toUpperCase() ?? 'Error', let ext = FileType2Ext(mime);
key: mime, if (HasFailed(ext)) ext = 'Error';
})), return {
value: ext.toUpperCase(),
key: mime,
};
}),
); );
} }

View File

@@ -1,9 +1,16 @@
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { WINDOW } from '@ng-web-apis/common'; import { WINDOW } from '@ng-web-apis/common';
import { ApiResponseSchema } from 'picsur-shared/dist/dto/api/api.dto'; import { ApiResponseSchema } from 'picsur-shared/dist/dto/api/api.dto';
import { Mime2Ext } from 'picsur-shared/dist/dto/mimes.dto'; import { FileType2Ext } from 'picsur-shared/dist/dto/mimes.dto';
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types'; import {
AsyncFailable,
Fail,
FT,
HasFailed,
HasSuccess
} from 'picsur-shared/dist/types';
import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto'; import { ZodDtoStatic } from 'picsur-shared/dist/util/create-zod-dto';
import { ParseMime2FileType } from 'picsur-shared/dist/util/parse-mime';
import { Subject } from 'rxjs'; import { Subject } from 'rxjs';
import { ApiBuffer } from 'src/app/models/dto/api-buffer.dto'; import { ApiBuffer } from 'src/app/models/dto/api-buffer.dto';
import { ApiError } from 'src/app/models/dto/api-error.dto'; import { ApiError } from 'src/app/models/dto/api-error.dto';
@@ -142,9 +149,14 @@ export class ApiService {
} }
} }
const mimeTypeExt = Mime2Ext(mimeType); const filetype = ParseMime2FileType(mimeType);
if (mimeTypeExt !== undefined && !name.endsWith(mimeTypeExt)) { if (HasSuccess(filetype)) {
name += '.' + mimeTypeExt; const ext = FileType2Ext(filetype.identifier);
if (HasSuccess(ext)) {
if (name.endsWith(ext)) {
name += '.' + ext;
}
}
} }
try { try {

View File

@@ -12,10 +12,10 @@ import {
ImageRequestParams ImageRequestParams
} from 'picsur-shared/dist/dto/api/image.dto'; } from 'picsur-shared/dist/dto/api/image.dto';
import { ImageLinks } from 'picsur-shared/dist/dto/image-links.class'; 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 { EImage } from 'picsur-shared/dist/entities/image.entity';
import { AsyncFailable } from 'picsur-shared/dist/types'; 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 { ImageUploadRequest } from '../../models/dto/image-upload-request.dto';
import { ApiService } from './api.service'; import { ApiService } from './api.service';
import { UserService } from './user.service'; import { UserService } from './user.service';
@@ -103,19 +103,19 @@ export class ImageService {
// Non api calls // 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 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( public GetImageURLCustomized(
image: string, image: string,
mime: string | null, filetype: string | null,
options: ImageRequestParams, options: ImageRequestParams,
): string { ): string {
const baseURL = this.GetImageURL(image, mime); const baseURL = this.GetImageURL(image, filetype);
const betterOptions = ImageRequestParams.zodSchema.safeParse(options); const betterOptions = ImageRequestParams.zodSchema.safeParse(options);
if (!betterOptions.success) return baseURL; if (!betterOptions.success) return baseURL;

View File

@@ -2,7 +2,7 @@ import { z } from 'zod';
import { EImageSchema } from '../../entities/image.entity'; import { EImageSchema } from '../../entities/image.entity';
import { EUserSchema } from '../../entities/user.entity'; import { EUserSchema } from '../../entities/user.entity';
import { createZodDto } from '../../util/create-zod-dto'; 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 => { const parseBool = (value: unknown): boolean | null => {
if (value === true || value === 'true' || value === '1' || value === 'yes') if (value === true || value === 'true' || value === '1' || value === 'yes')
@@ -36,9 +36,9 @@ export class ImageRequestParams extends createZodDto(
export const ImageMetaResponseSchema = z.object({ export const ImageMetaResponseSchema = z.object({
image: EImageSchema, image: EImageSchema,
user: EUserSchema, user: EUserSchema,
fileMimes: z.object({ fileTypes: z.object({
[ImageFileType.MASTER]: z.string(), [ImageEntryVariant.MASTER]: z.string(),
[ImageFileType.ORIGINAL]: z.union([z.string(), z.undefined()]), [ImageEntryVariant.ORIGINAL]: z.union([z.string(), z.undefined()]),
}), }),
}); });
export class ImageMetaResponse extends createZodDto(ImageMetaResponseSchema) {} export class ImageMetaResponse extends createZodDto(ImageMetaResponseSchema) {}

View File

@@ -1,4 +1,4 @@
export enum ImageFileType { export enum ImageEntryVariant {
ORIGINAL = 'original', ORIGINAL = 'original',
MASTER = 'master', MASTER = 'master',
} }

View File

@@ -1,69 +1,110 @@
import { Fail, Failable, FT } from '../types';
// Config // Config
export enum ImageMime { export enum ImageFileType {
QOI = 'image/x-qoi', QOI = 'image:qoi',
JPEG = 'image/jpeg', JPEG = 'image:jpeg',
PNG = 'image/png', PNG = 'image:png',
WEBP = 'image/webp', WEBP = 'image:webp',
TIFF = 'image/tiff', TIFF = 'image:tiff',
BMP = 'image/bmp', BMP = 'image:bmp',
// ICO = 'image/x-icon', // ICO = 'image:ico',
} }
export enum AnimMime { export enum AnimFileType {
APNG = 'image/apng', GIF = 'anim:gif',
GIF = 'image/gif', WEBP = 'anim:webp',
//APNG = 'anim:apng',
} }
// Derivatives // Derivatives
export const SupportedImageMimes: string[] = Object.values(ImageMime); export const SupportedImageFileTypes: string[] = Object.values(ImageFileType);
export const SupportedAnimMimes: string[] = Object.values(AnimMime); export const SupportedAnimFileTypes: string[] = Object.values(AnimFileType);
export const SupportedMimes: string[] = Object.values({ ...ImageMime, ...AnimMime }); export const SupportedFileTypes: string[] = Object.values({
...ImageFileType,
...AnimFileType,
});
export enum SupportedMimeCategory { export enum SupportedFileTypeCategory {
Image = 'image', Image = 'image',
Animation = 'anim', Animation = 'anim',
} }
export interface FullMime { export interface FileType {
mime: string; identifier: string;
type: SupportedMimeCategory; category: SupportedFileTypeCategory;
} }
export const ImageMime2ExtMap: { // Converters
[key in ImageMime]: string;
// -- Ext
const FileType2ExtMap: {
[key in ImageFileType | AnimFileType]: string;
} = { } = {
[ImageMime.QOI]: 'qoi', [AnimFileType.GIF]: 'gif',
[ImageMime.JPEG]: 'jpg', [AnimFileType.WEBP]: 'webp',
[ImageMime.PNG]: 'png', // [AnimFileType.APNG]: 'apng',
[ImageMime.WEBP]: 'webp', [ImageFileType.QOI]: 'qoi',
[ImageMime.TIFF]: 'tiff', [ImageFileType.JPEG]: 'jpg',
[ImageMime.BMP]: 'bmp', [ImageFileType.PNG]: 'png',
// [ImageMime.ICO]: 'ico', [ImageFileType.WEBP]: 'webp',
[ImageFileType.TIFF]: 'tiff',
[ImageFileType.BMP]: 'bmp',
// [ImageFileType.ICO]: 'ico',
}; };
export const AnimMime2ExtMap: { const Ext2FileTypeMap: {
[key in AnimMime]: string;
} = {
[AnimMime.GIF]: 'gif',
[AnimMime.APNG]: 'apng',
};
export const Mime2ExtMap: {
[key in ImageMime | AnimMime]: string;
} = {
...ImageMime2ExtMap,
...AnimMime2ExtMap,
};
export const Ext2MimeMap: {
[key: string]: string; [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 => { export const FileType2Ext = (mime: string): Failable<string> => {
return Mime2ExtMap[mime as ImageMime | AnimMime]; 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 => { export const Ext2FileType = (ext: string): Failable<string> => {
return Ext2MimeMap[ext]; 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<string> => {
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<string> => {
const result = FileType2MimeMap[filetype as ImageFileType | AnimFileType];
if (result === undefined)
return Fail(FT.Internal, undefined, `Unsupported filetype: ${filetype}`);
return result;
}; };

View File

@@ -141,10 +141,20 @@ export function Fail(type: FT, reason?: any, dbgReason?: any): Failure {
if (dbgReason === undefined || dbgReason === null) { if (dbgReason === undefined || dbgReason === null) {
if (reason === undefined || reason === null) { if (reason === undefined || reason === null) {
// If both are null, just return a default error message // 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') { } else if (typeof reason === 'string') {
// If it is a string, this was intentionally specified, so pass it through // 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) { } else if (reason instanceof Error) {
// In case of an error, we want to keep that hidden, so return the default message // In case of an error, we want to keep that hidden, so return the default message
// Only send the specifics to debug // Only send the specifics to debug
@@ -159,7 +169,7 @@ export function Fail(type: FT, reason?: any, dbgReason?: any): Failure {
return new Failure( return new Failure(
type, type,
FTProps[type].message, FTProps[type].message,
undefined, new Error(String(reason)).stack,
String(reason), String(reason),
); );
} }
@@ -168,11 +178,21 @@ export function Fail(type: FT, reason?: any, dbgReason?: any): Failure {
const strReason = reason?.toString() ?? FTProps[type].message; const strReason = reason?.toString() ?? FTProps[type].message;
if (typeof dbgReason === 'string') { 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) { } else if (dbgReason instanceof Error) {
return new Failure(type, strReason, dbgReason.stack, dbgReason.message); return new Failure(type, strReason, dbgReason.stack, dbgReason.message);
} else { } else {
return new Failure(type, strReason, undefined, String(dbgReason)); return new Failure(
type,
strReason,
new Error(String(dbgReason)).stack,
String(dbgReason),
);
} }
} }
} }

View File

@@ -1,17 +1,34 @@
import { import {
FullMime, Ext2FileType,
SupportedAnimMimes, FileType,
SupportedImageMimes, Mime2FileType,
SupportedMimeCategory SupportedAnimFileTypes,
SupportedFileTypeCategory,
SupportedImageFileTypes
} from '../dto/mimes.dto'; } from '../dto/mimes.dto';
import { Fail, Failable, FT } from '../types'; import { Fail, Failable, FT, HasFailed } from '../types';
export function ParseMime(mime: string): Failable<FullMime> { export function ParseFileType(filetype: string): Failable<FileType> {
if (SupportedImageMimes.includes(mime)) if (SupportedImageFileTypes.includes(filetype))
return { mime, type: SupportedMimeCategory.Image }; return { identifier: filetype, category: SupportedFileTypeCategory.Image };
if (SupportedAnimMimes.includes(mime)) if (SupportedAnimFileTypes.includes(filetype))
return { mime, type: SupportedMimeCategory.Animation }; 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<FileType> {
const result = Ext2FileType(ext);
if (HasFailed(result)) return result;
return ParseFileType(result);
}
export function ParseMime2FileType(mime: string): Failable<FileType> {
const result = Mime2FileType(mime);
if (HasFailed(result)) return result;
return ParseFileType(result);
} }