cache converted images

This commit is contained in:
rubikscraft
2022-04-25 15:31:25 +02:00
parent f876253b05
commit f22245904f
6 changed files with 118 additions and 88 deletions

View File

@@ -77,7 +77,7 @@ export class ImageFileDBService {
key: string, key: string,
mime: string, mime: string,
file: Buffer, file: Buffer,
): AsyncFailable<true> { ): AsyncFailable<EImageDerivativeBackend> {
const imageDerivative = new EImageDerivativeBackend(); const imageDerivative = new EImageDerivativeBackend();
imageDerivative.imageId = imageId; imageDerivative.imageId = imageId;
imageDerivative.key = key; imageDerivative.key = key;
@@ -85,25 +85,20 @@ export class ImageFileDBService {
imageDerivative.data = file; imageDerivative.data = file;
try { try {
await this.imageDerivativeRepo.save(imageDerivative); return await this.imageDerivativeRepo.save(imageDerivative);
} catch (e) { } catch (e) {
return Fail(e); return Fail(e);
} }
return true;
} }
public async getDerivative( public async getDerivative(
imageId: string, imageId: string,
key: string, key: string,
): AsyncFailable<EImageDerivativeBackend> { ): AsyncFailable<EImageDerivativeBackend | null> {
try { try {
const found = await this.imageDerivativeRepo.findOne({ return await this.imageDerivativeRepo.findOne({
where: { imageId, key }, where: { imageId, key },
}); });
if (!found) return Fail('Image not found');
return found;
} catch (e) { } catch (e) {
return Fail(e); return Fail(e);
} }

View File

@@ -63,32 +63,34 @@ export class SysPreferenceService {
let validatedKey = this.prefCommon.validatePrefKey(key, SysPreference); let validatedKey = this.prefCommon.validatePrefKey(key, SysPreference);
if (HasFailed(validatedKey)) return validatedKey; if (HasFailed(validatedKey)) return validatedKey;
let foundSysPreference: ESysPreferenceBackend; return MutexFallBack(
try { 'fetchSysPrefrence',
foundSysPreference = await MutexFallBack( async () => {
'fetchSysPrefrence', let existing: ESysPreferenceBackend | null;
() => try {
this.sysPreferenceRepository.findOne({ existing = await this.sysPreferenceRepository.findOne({
where: { key: validatedKey as SysPreference }, where: { key: validatedKey as SysPreference },
cache: 60000, cache: 60000,
}), });
() => this.saveDefault(validatedKey as SysPreference), if (!existing) return null;
); } catch (e) {
} catch (e) { return Fail(e);
return Fail(e); }
}
// Validate // Validate
const result = ESysPreferenceSchema.safeParse(foundSysPreference); const result = ESysPreferenceSchema.safeParse(existing);
if (!result.success) { if (!result.success) {
return Fail(result.error); return Fail(result.error);
} }
// Return // Return
return this.prefCommon.validateAndUnpackPref( return this.prefCommon.validateAndUnpackPref(
result.data, result.data,
SysPreference, SysPreference,
SysPreferenceValueTypes, SysPreferenceValueTypes,
);
},
() => this.saveDefault(validatedKey as SysPreference),
); );
} }

View File

@@ -68,38 +68,40 @@ export class UsrPreferenceService {
let validatedKey = this.prefCommon.validatePrefKey(key, UsrPreference); let validatedKey = this.prefCommon.validatePrefKey(key, UsrPreference);
if (HasFailed(validatedKey)) return validatedKey; if (HasFailed(validatedKey)) return validatedKey;
let foundUsrPreference: EUsrPreferenceBackend; return MutexFallBack(
try { 'fetchUsrPrefrence',
foundUsrPreference = await MutexFallBack( async () => {
'fetchUsrPrefrence', let existing: EUsrPreferenceBackend | null;
() => try {
this.usrPreferenceRepository.findOne({ existing = await this.usrPreferenceRepository.findOne({
where: { key: validatedKey as UsrPreference, userId: userid }, where: { key: validatedKey as UsrPreference, userId: userid },
cache: 60000, cache: 60000,
}), });
() => this.saveDefault(userid, validatedKey as UsrPreference), if (!existing) return null;
); } catch (e) {
} catch (e) { return Fail(e);
return Fail(e); }
}
// Validate // Validate
const result = EUsrPreferenceSchema.safeParse(foundUsrPreference); const result = EUsrPreferenceSchema.safeParse(existing);
if (!result.success) { if (!result.success) {
return Fail(result.error); return Fail(result.error);
} }
// Return // Return
const unpacked = this.prefCommon.validateAndUnpackPref( const unpacked = this.prefCommon.validateAndUnpackPref(
result.data, result.data,
UsrPreference, UsrPreference,
UsrPreferenceValueTypes, UsrPreferenceValueTypes,
);
if (HasFailed(unpacked)) return unpacked;
return {
...unpacked,
user: result.data.userId,
};
},
() => this.saveDefault(userid, validatedKey as UsrPreference),
); );
if (HasFailed(unpacked)) return unpacked;
return {
...unpacked,
user: result.data.userId,
};
} }
public async getStringPreference( public async getStringPreference(

View File

@@ -1,4 +1,5 @@
import { Injectable } from '@nestjs/common'; import { Injectable, Logger } from '@nestjs/common';
import Crypto from 'crypto';
import { fileTypeFromBuffer, FileTypeResult } from 'file-type'; import { fileTypeFromBuffer, FileTypeResult } from 'file-type';
import { FullMime } from 'picsur-shared/dist/dto/mimes.dto'; import { FullMime } from 'picsur-shared/dist/dto/mimes.dto';
import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.dto'; import { UsrPreference } from 'picsur-shared/dist/dto/usr-preferences.dto';
@@ -12,15 +13,14 @@ import { ImageFileType } from '../../models/constants/image-file-types.const';
import { EImageDerivativeBackend } from '../../models/entities/image-derivative.entity'; import { EImageDerivativeBackend } from '../../models/entities/image-derivative.entity';
import { EImageFileBackend } from '../../models/entities/image-file.entity'; import { EImageFileBackend } from '../../models/entities/image-file.entity';
import { EImageBackend } from '../../models/entities/image.entity'; import { EImageBackend } from '../../models/entities/image.entity';
import { MutexFallBack } from '../../models/util/mutex-fallback';
import { ImageConverterService } from './image-converter.service'; import { ImageConverterService } from './image-converter.service';
import { ImageProcessorService } from './image-processor.service'; import { ImageProcessorService } from './image-processor.service';
// Right now this service is mostly a wrapper for the imagedbservice.
// But in the future the actual image logic will happend here
// And the image storing part will stay in the imagedbservice
@Injectable() @Injectable()
export class ImageManagerService { export class ImageManagerService {
private readonly logger = new Logger(ImageManagerService.name);
constructor( constructor(
private readonly imagesService: ImageDBService, private readonly imagesService: ImageDBService,
private readonly imageFilesService: ImageFileDBService, private readonly imageFilesService: ImageFileDBService,
@@ -84,31 +84,47 @@ export class ImageManagerService {
public async getConverted( public async getConverted(
imageId: string, imageId: string,
mime: string, options: {
mime: string;
},
): AsyncFailable<EImageDerivativeBackend> { ): AsyncFailable<EImageDerivativeBackend> {
const targetMime = ParseMime(mime); const targetMime = ParseMime(options.mime);
if (HasFailed(targetMime)) return targetMime; if (HasFailed(targetMime)) return targetMime;
const masterImage = await this.getMaster(imageId);
if (HasFailed(masterImage)) return masterImage;
const sourceMime = ParseMime(masterImage.mime); const converted_key = this.getConvertHash(options);
if (HasFailed(sourceMime)) return sourceMime;
const convertResult = await this.convertService.convert( return MutexFallBack(
masterImage.data, converted_key,
sourceMime, () => this.imageFilesService.getDerivative(imageId, converted_key),
targetMime, async () => {
const masterImage = await this.getMaster(imageId);
if (HasFailed(masterImage)) return masterImage;
const sourceMime = ParseMime(masterImage.mime);
if (HasFailed(sourceMime)) return sourceMime;
const startTime = Date.now();
const convertResult = await this.convertService.convert(
masterImage.data,
sourceMime,
targetMime,
);
if (HasFailed(convertResult)) return convertResult;
this.logger.verbose(
`Converted ${imageId} from ${sourceMime.mime} to ${
targetMime.mime
} in ${Date.now() - startTime}ms`,
);
return await this.imageFilesService.addDerivative(
imageId,
converted_key,
convertResult.mime,
convertResult.image,
);
},
); );
if (HasFailed(convertResult)) return convertResult;
const returned = new EImageDerivativeBackend();
returned.data = convertResult.image;
returned.mime = convertResult.mime;
returned.imageId = imageId;
returned.key = 'aight';
return returned;
} }
// File getters ============================================================== // File getters ==============================================================
@@ -158,4 +174,13 @@ export class ImageManagerService {
const fullMime = ParseMime(mime ?? 'other/unknown'); const fullMime = ParseMime(mime ?? 'other/unknown');
return fullMime; return fullMime;
} }
private getConvertHash(options: object) {
// Return a sha256 hash of the stringified options
const stringified = JSON.stringify(options);
const hash = Crypto.createHash('sha256');
hash.update(stringified);
const digest = hash.digest('hex');
return digest;
}
} }

View File

@@ -14,7 +14,7 @@ const fallBackMap: Record<string, Promise<unknown>> = {};
export async function MutexFallBack< export async function MutexFallBack<
MF extends () => Promise<O | null | undefined>, MF extends () => Promise<O | null | undefined>,
FF extends () => Promise<unknown>, FF extends () => Promise<O>,
O, O,
>(key: string, mainFunc: MF, fallBackFunc: FF): Promise<O> { >(key: string, mainFunc: MF, fallBackFunc: FF): Promise<O> {
const try_it = await mainFunc(); const try_it = await mainFunc();
@@ -25,7 +25,9 @@ export async function MutexFallBack<
return MutexFallBack(key, mainFunc, fallBackFunc); return MutexFallBack(key, mainFunc, fallBackFunc);
} }
fallBackMap[key] = fallBackFunc(); const fallBackPromise = fallBackFunc();
fallBackMap[key] = fallBackPromise;
fallBackMap[key] fallBackMap[key]
.then(() => { .then(() => {
delete fallBackMap[key]; delete fallBackMap[key];
@@ -33,5 +35,6 @@ export async function MutexFallBack<
.catch(() => { .catch(() => {
delete fallBackMap[key]; delete fallBackMap[key];
}); });
return MutexFallBack(key, mainFunc, fallBackFunc);
return fallBackPromise;
} }

View File

@@ -5,7 +5,8 @@ import {
InternalServerErrorException, InternalServerErrorException,
Logger, Logger,
NotFoundException, NotFoundException,
Post, Res Post,
Res
} from '@nestjs/common'; } from '@nestjs/common';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto'; import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto';
@@ -47,7 +48,9 @@ export class ImageController {
return image.data; return image.data;
} }
const image = await this.imagesService.getConverted(fullid.id, fullid.mime); const image = await this.imagesService.getConverted(fullid.id, {
mime: fullid.mime,
});
if (HasFailed(image)) { if (HasFailed(image)) {
this.logger.warn(image.getReason()); this.logger.warn(image.getReason());
throw new NotFoundException('Could not find image'); throw new NotFoundException('Could not find image');