mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-12 14:55:39 +01:00
cache converted images
This commit is contained in:
@@ -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);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
Reference in New Issue
Block a user