mirror of
https://github.com/CaramelFur/Picsur.git
synced 2026-05-06 11:46:26 +02:00
make image converting a job too
This commit is contained in:
@@ -12,7 +12,6 @@ import { PicsurLayersModule } from './layers/PicsurLayers.module';
|
||||
import { PicsurLoggerModule } from './logger/logger.module';
|
||||
import { AuthManagerModule } from './managers/auth/auth.module';
|
||||
import { DemoManagerModule } from './managers/demo/demo.module';
|
||||
import { IngestManagerModule } from './managers/ingest/ingest.module';
|
||||
import { UsageManagerModule } from './managers/usage/usage.module';
|
||||
import { PicsurRoutesModule } from './routes/routes.module';
|
||||
|
||||
@@ -59,7 +58,6 @@ const imageCorsOverride = (
|
||||
DemoManagerModule,
|
||||
PicsurRoutesModule,
|
||||
PicsurLayersModule,
|
||||
IngestManagerModule,
|
||||
],
|
||||
})
|
||||
export class AppModule implements NestModule {
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { EIngestFileBackend } from '../../database/entities/ingest-file.entity';
|
||||
import { IngestFileDbService } from './ingest-file-db.service';
|
||||
|
||||
@Module({
|
||||
imports: [TypeOrmModule.forFeature([EIngestFileBackend])],
|
||||
providers: [IngestFileDbService],
|
||||
exports: [IngestFileDbService],
|
||||
})
|
||||
export class IngestFileDbModule {}
|
||||
@@ -1,32 +0,0 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import { AsyncFailable, Fail, FT } from 'picsur-shared/dist/types';
|
||||
import { Repository } from 'typeorm';
|
||||
import { EIngestFileBackend } from '../../database/entities/ingest-file.entity';
|
||||
|
||||
@Injectable()
|
||||
export class IngestFileDbService {
|
||||
private readonly logger = new Logger(IngestFileDbService.name);
|
||||
|
||||
constructor(
|
||||
@InjectRepository(EIngestFileBackend)
|
||||
private readonly ingressFileRepo: Repository<EIngestFileBackend>,
|
||||
) {}
|
||||
|
||||
public async uploadFile(
|
||||
filename: string,
|
||||
file: Buffer,
|
||||
): AsyncFailable<string> {
|
||||
const ingressFile = new EIngestFileBackend();
|
||||
ingressFile.filename = filename;
|
||||
ingressFile.data = file;
|
||||
|
||||
try {
|
||||
await this.ingressFileRepo.save(ingressFile);
|
||||
} catch (e) {
|
||||
return Fail(FT.Database, e);
|
||||
}
|
||||
|
||||
return ingressFile.id;
|
||||
}
|
||||
}
|
||||
@@ -42,7 +42,6 @@ export class PreferenceDefaultsService {
|
||||
[SysPreference.BCryptStrength]: 10,
|
||||
|
||||
[SysPreference.RemoveDerivativesAfter]: '7d',
|
||||
[SysPreference.SaveDerivatives]: true,
|
||||
[SysPreference.AllowEditing]: true,
|
||||
|
||||
[SysPreference.ConversionTimeLimit]: '15s',
|
||||
|
||||
@@ -2,7 +2,6 @@ import { EApiKeyBackend } from './apikey.entity';
|
||||
import { EImageDerivativeBackend } from './images/image-derivative.entity';
|
||||
import { EImageFileBackend } from './images/image-file.entity';
|
||||
import { EImageBackend } from './images/image.entity';
|
||||
import { EIngestFileBackend } from './ingest-file.entity';
|
||||
import { ESysPreferenceBackend } from './system/sys-preference.entity';
|
||||
import { ESystemStateBackend } from './system/system-state.entity';
|
||||
import { EUsrPreferenceBackend } from './system/usr-preference.entity';
|
||||
@@ -19,5 +18,4 @@ export const EntityList = [
|
||||
EUsrPreferenceBackend,
|
||||
EApiKeyBackend,
|
||||
ESystemStateBackend,
|
||||
EIngestFileBackend,
|
||||
];
|
||||
|
||||
@@ -1,16 +0,0 @@
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class EIngestFileBackend {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({ nullable: false })
|
||||
filename: string;
|
||||
|
||||
@Column({ type: 'bytea', nullable: false })
|
||||
data: Buffer;
|
||||
|
||||
@Column({ nullable: false, default: false })
|
||||
in_use: boolean;
|
||||
}
|
||||
@@ -4,7 +4,7 @@ import {
|
||||
Injectable,
|
||||
Logger,
|
||||
NestInterceptor,
|
||||
Optional,
|
||||
Optional
|
||||
} from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { FastifyReply } from 'fastify';
|
||||
@@ -46,6 +46,20 @@ export class SuccessInterceptor<T> implements NestInterceptor {
|
||||
return data;
|
||||
}
|
||||
}),
|
||||
map((data) => {
|
||||
const request = context.switchToHttp().getRequest();
|
||||
const response = context.switchToHttp().getResponse<FastifyReply>();
|
||||
const traceString = `(${request.ip} -> ${request.method} ${request.url})`;
|
||||
|
||||
this.logger.verbose(
|
||||
`Handled ${traceString} with ${response.statusCode} in ${Math.ceil(
|
||||
response.getResponseTime(),
|
||||
)}ms`,
|
||||
SuccessInterceptor.name,
|
||||
);
|
||||
|
||||
return data;
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -44,12 +44,9 @@ async function bootstrap() {
|
||||
);
|
||||
|
||||
// Configure logger
|
||||
const logger = app.get(PicsurLoggerService)
|
||||
app.useLogger(logger);
|
||||
app.useLogger(app.get(PicsurLoggerService));
|
||||
app.flushLogs();
|
||||
|
||||
console.log(logger);
|
||||
|
||||
app.useGlobalFilters(app.get(MainExceptionFilter));
|
||||
app.useGlobalInterceptors(app.get(SuccessInterceptor));
|
||||
app.useGlobalPipes(app.get(ZodValidationPipe));
|
||||
|
||||
94
backend/src/managers/image/convert.consumer.ts
Normal file
94
backend/src/managers/image/convert.consumer.ts
Normal file
@@ -0,0 +1,94 @@
|
||||
import { OnQueueError, OnQueueFailed, Process, Processor } from '@nestjs/bull';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import type { Job } from 'bull';
|
||||
import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto';
|
||||
import { SysPreference } from 'picsur-shared/dist/dto/sys-preferences.enum';
|
||||
import { IsFailure, ThrowIfFailed } from 'picsur-shared/dist/types';
|
||||
import { ParseFileType } from 'picsur-shared/dist/util/parse-mime';
|
||||
import { ImageFileDBService } from '../../collections/image-db/image-file-db.service';
|
||||
import { SysPreferenceDbService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
import { ImageConverterService } from './image-converter.service';
|
||||
import { ImageManagerService } from './image-manager.service';
|
||||
import { ImageQueueID, ImageQueueSubject } from './image.queue';
|
||||
|
||||
// This contains the job to convert an image to a derivative and store it
|
||||
|
||||
export interface ImageConvertJobData {
|
||||
uniqueKey: string;
|
||||
imageId: string;
|
||||
fileType: string;
|
||||
options: ImageRequestParams;
|
||||
}
|
||||
export type ImageConvertJob = Job<ImageConvertJobData>;
|
||||
|
||||
@Processor(ImageQueueID)
|
||||
export class ConvertConsumer {
|
||||
private readonly logger = new Logger(ConvertConsumer.name);
|
||||
|
||||
constructor(
|
||||
private readonly imageFilesService: ImageFileDBService,
|
||||
private readonly imageConverter: ImageConverterService,
|
||||
private readonly sysPref: SysPreferenceDbService,
|
||||
private readonly imageService: ImageManagerService,
|
||||
) {}
|
||||
|
||||
@Process(ImageQueueSubject.CONVERT)
|
||||
async convertImage(job: ImageConvertJob): Promise<void> {
|
||||
const { imageId, fileType, options, uniqueKey } = job.data;
|
||||
|
||||
// Get file type
|
||||
const targetFileType = ThrowIfFailed(ParseFileType(fileType));
|
||||
|
||||
// Get preferences
|
||||
const allow_editing = ThrowIfFailed(
|
||||
await this.sysPref.getBooleanPreference(SysPreference.AllowEditing),
|
||||
);
|
||||
|
||||
// Get master image
|
||||
const masterImage = ThrowIfFailed(
|
||||
await this.imageService.getMaster(imageId),
|
||||
);
|
||||
const sourceFileType = ThrowIfFailed(ParseFileType(masterImage.filetype));
|
||||
|
||||
// Conver timage
|
||||
const startTime = Date.now();
|
||||
const convertResult = ThrowIfFailed(
|
||||
await this.imageConverter.convert(
|
||||
masterImage.data,
|
||||
sourceFileType,
|
||||
targetFileType,
|
||||
allow_editing ? options : {},
|
||||
),
|
||||
);
|
||||
|
||||
this.logger.verbose(
|
||||
`Converted ${imageId} from ${sourceFileType.identifier} to ${
|
||||
targetFileType.identifier
|
||||
} in ${Date.now() - startTime}ms`,
|
||||
);
|
||||
|
||||
ThrowIfFailed(
|
||||
await this.imageFilesService.addDerivative(
|
||||
imageId,
|
||||
uniqueKey,
|
||||
convertResult.filetype,
|
||||
convertResult.image,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
@OnQueueError()
|
||||
async handleError(error: any) {
|
||||
if (IsFailure(error)) error.print(this.logger);
|
||||
else this.logger.error(error);
|
||||
}
|
||||
|
||||
@OnQueueFailed()
|
||||
async handleFailed(job: Job, error: any) {
|
||||
if (IsFailure(error))
|
||||
error.print(this.logger, {
|
||||
prefix: `[JOB ${job.id}]`,
|
||||
});
|
||||
else this.logger.error(error);
|
||||
}
|
||||
}
|
||||
97
backend/src/managers/image/convert.service.ts
Normal file
97
backend/src/managers/image/convert.service.ts
Normal file
@@ -0,0 +1,97 @@
|
||||
import { InjectQueue } from '@nestjs/bull';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import Crypto from 'crypto';
|
||||
import { ImageRequestParams } from 'picsur-shared/dist/dto/api/image.dto';
|
||||
import {
|
||||
AsyncFailable,
|
||||
Fail,
|
||||
FT,
|
||||
HasFailed,
|
||||
ThrowIfFailed
|
||||
} from 'picsur-shared/dist/types';
|
||||
import { ImageFileDBService } from '../../collections/image-db/image-file-db.service';
|
||||
import { EImageDerivativeBackend } from '../../database/entities/images/image-derivative.entity';
|
||||
import { ImageConvertJob } from './convert.consumer';
|
||||
import * as ImageQueue from './image.queue';
|
||||
|
||||
@Injectable()
|
||||
export class ConvertService {
|
||||
constructor(
|
||||
@InjectQueue(ImageQueue.ImageQueueID)
|
||||
private readonly imageQueue: ImageQueue.ImageQueueType,
|
||||
private readonly imageFilesService: ImageFileDBService,
|
||||
) {}
|
||||
|
||||
public async convertJob(
|
||||
imageId: string,
|
||||
fileType: string,
|
||||
options: ImageRequestParams,
|
||||
): AsyncFailable<ImageConvertJob> {
|
||||
const jobID = this.getConvertHash(imageId, { fileType, ...options });
|
||||
|
||||
/*
|
||||
Jobs with the same ID don't get executed, we abuse this by passing it a hash of the input parameters.
|
||||
This way, if the same image is requested with the same parameters, we don't have to convert it again.
|
||||
Since it will always produce the same output with the same inputs
|
||||
*/
|
||||
|
||||
let job: ImageConvertJob;
|
||||
try {
|
||||
job = (await this.imageQueue.add(
|
||||
ImageQueue.ImageQueueSubject.CONVERT,
|
||||
{
|
||||
imageId,
|
||||
fileType,
|
||||
options,
|
||||
uniqueKey: jobID,
|
||||
},
|
||||
{
|
||||
jobId: jobID,
|
||||
},
|
||||
)) as ImageConvertJob;
|
||||
} catch (e) {
|
||||
return Fail(FT.Internal, e);
|
||||
}
|
||||
|
||||
if (!job.id) return Fail(FT.Internal, undefined, 'Failed to queue job');
|
||||
return job;
|
||||
}
|
||||
|
||||
public async convertPromise(
|
||||
imageId: string,
|
||||
fileType: string,
|
||||
options: ImageRequestParams,
|
||||
): AsyncFailable<EImageDerivativeBackend> {
|
||||
const uniqueKey = this.getConvertHash(imageId, { fileType, ...options });
|
||||
|
||||
const findExisting = ThrowIfFailed(
|
||||
await this.imageFilesService.getDerivative(imageId, uniqueKey),
|
||||
);
|
||||
if (findExisting !== null) return findExisting;
|
||||
|
||||
const job = await this.convertJob(imageId, fileType, options);
|
||||
if (HasFailed(job)) return job;
|
||||
|
||||
try {
|
||||
await job.finished();
|
||||
} catch (e) {
|
||||
return Fail(FT.Internal, 'Failed to convert image', e);
|
||||
}
|
||||
|
||||
const findResult = ThrowIfFailed(
|
||||
await this.imageFilesService.getDerivative(imageId, uniqueKey),
|
||||
);
|
||||
if (findResult !== null) return findResult;
|
||||
|
||||
return Fail(FT.Internal, 'Failed to convert image');
|
||||
}
|
||||
|
||||
private getConvertHash(imageID: string, options: object) {
|
||||
// Return a sha256 hash of the stringified options
|
||||
const stringified = JSON.stringify(options) + '-' + imageID;
|
||||
const hash = Crypto.createHash('sha256');
|
||||
hash.update(stringified);
|
||||
const digest = hash.digest('hex');
|
||||
return digest;
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { Logger, Module, OnModuleInit } from '@nestjs/common';
|
||||
import { Interval } from '@nestjs/schedule';
|
||||
import ms from 'ms';
|
||||
@@ -8,18 +9,36 @@ import { ImageDBService } from '../../collections/image-db/image-db.service';
|
||||
import { ImageFileDBService } from '../../collections/image-db/image-file-db.service';
|
||||
import { PreferenceDbModule } from '../../collections/preference-db/preference-db.module';
|
||||
import { SysPreferenceDbService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
import { ConvertConsumer } from './convert.consumer';
|
||||
import { ConvertService } from './convert.service';
|
||||
import { ImageConverterService } from './image-converter.service';
|
||||
import { ImageProcessorService } from './image-processor.service';
|
||||
import { ImageManagerService } from './image.service';
|
||||
import { ImageManagerService } from './image-manager.service';
|
||||
import { ImageQueueID } from './image.queue';
|
||||
import { IngestConsumer } from './ingest.consumer';
|
||||
import { IngestService } from './ingest.service';
|
||||
|
||||
@Module({
|
||||
imports: [ImageDBModule, PreferenceDbModule],
|
||||
imports: [
|
||||
ImageDBModule,
|
||||
PreferenceDbModule,
|
||||
BullModule.registerQueue({
|
||||
name: ImageQueueID,
|
||||
}),
|
||||
],
|
||||
providers: [
|
||||
ImageManagerService,
|
||||
ImageProcessorService,
|
||||
ImageConverterService,
|
||||
IngestConsumer,
|
||||
ConvertConsumer,
|
||||
IngestService,
|
||||
ConvertService,
|
||||
],
|
||||
exports: [
|
||||
ImageManagerService,
|
||||
ImageConverterService,
|
||||
IngestService,
|
||||
ConvertService,
|
||||
],
|
||||
exports: [ImageManagerService, ImageConverterService],
|
||||
})
|
||||
export class ImageManagerModule implements OnModuleInit {
|
||||
private readonly logger = new Logger(ImageManagerModule.name);
|
||||
@@ -49,11 +68,12 @@ export class ImageManagerModule implements OnModuleInit {
|
||||
return;
|
||||
}
|
||||
|
||||
const after_ms = ms(remove_derivatives_after as any);
|
||||
let after_ms = ms(remove_derivatives_after as any);
|
||||
if (isNaN(after_ms) || after_ms === 0) {
|
||||
this.logger.log('remove_derivatives_after is 0, skipping cron');
|
||||
return;
|
||||
}
|
||||
if (after_ms < 60000) after_ms = 60000;
|
||||
|
||||
const result = await this.imageFileDB.cleanupDerivatives(after_ms / 1000);
|
||||
if (HasFailed(result)) {
|
||||
106
backend/src/managers/image/image-manager.service.ts
Normal file
106
backend/src/managers/image/image-manager.service.ts
Normal file
@@ -0,0 +1,106 @@
|
||||
import { Injectable, Logger } from '@nestjs/common';
|
||||
import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum';
|
||||
import { FileType } from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { AsyncFailable, Fail, FT, HasFailed } from 'picsur-shared/dist/types';
|
||||
import { FindResult } from 'picsur-shared/dist/types/find-result';
|
||||
import { ParseFileType } from 'picsur-shared/dist/util/parse-mime';
|
||||
import { ImageDBService } from '../../collections/image-db/image-db.service';
|
||||
import { ImageFileDBService } from '../../collections/image-db/image-file-db.service';
|
||||
import { EImageFileBackend } from '../../database/entities/images/image-file.entity';
|
||||
import { EImageBackend } from '../../database/entities/images/image.entity';
|
||||
|
||||
@Injectable()
|
||||
export class ImageManagerService {
|
||||
private readonly logger = new Logger(ImageManagerService.name);
|
||||
|
||||
constructor(
|
||||
private readonly imagesService: ImageDBService,
|
||||
private readonly imageFilesService: ImageFileDBService,
|
||||
) {}
|
||||
|
||||
public async findOne(id: string): AsyncFailable<EImageBackend> {
|
||||
return await this.imagesService.findOne(id, undefined);
|
||||
}
|
||||
|
||||
public async findMany(
|
||||
count: number,
|
||||
page: number,
|
||||
userid: string | undefined,
|
||||
): AsyncFailable<FindResult<EImageBackend>> {
|
||||
return await this.imagesService.findMany(count, page, userid);
|
||||
}
|
||||
|
||||
public async update(
|
||||
id: string,
|
||||
userid: string | undefined,
|
||||
options: Partial<Pick<EImageBackend, 'file_name' | 'expires_at'>>,
|
||||
): AsyncFailable<EImageBackend> {
|
||||
if (options.expires_at !== undefined && options.expires_at !== null) {
|
||||
if (options.expires_at < new Date()) {
|
||||
return Fail(FT.UsrValidation, 'Expiration date must be in the future');
|
||||
}
|
||||
}
|
||||
return await this.imagesService.update(id, userid, options);
|
||||
}
|
||||
|
||||
public async deleteMany(
|
||||
ids: string[],
|
||||
userid: string | undefined,
|
||||
): AsyncFailable<EImageBackend[]> {
|
||||
return await this.imagesService.delete(ids, userid);
|
||||
}
|
||||
|
||||
public async deleteWithKey(
|
||||
imageId: string,
|
||||
key: string,
|
||||
): AsyncFailable<EImageBackend> {
|
||||
return await this.imagesService.deleteWithKey(imageId, key);
|
||||
}
|
||||
|
||||
// File getters ==============================================================
|
||||
|
||||
public async getMaster(imageId: string): AsyncFailable<EImageFileBackend> {
|
||||
return this.imageFilesService.getFile(imageId, ImageEntryVariant.MASTER);
|
||||
}
|
||||
|
||||
public async getMasterFileType(imageId: string): AsyncFailable<FileType> {
|
||||
const mime = await this.imageFilesService.getFileTypes(imageId);
|
||||
if (HasFailed(mime)) return mime;
|
||||
|
||||
if (mime['master'] === undefined)
|
||||
return Fail(FT.NotFound, 'No master file');
|
||||
|
||||
return ParseFileType(mime['master']);
|
||||
}
|
||||
|
||||
public async getOriginal(imageId: string): AsyncFailable<EImageFileBackend> {
|
||||
return this.imageFilesService.getFile(imageId, ImageEntryVariant.ORIGINAL);
|
||||
}
|
||||
|
||||
public async getOriginalFileType(imageId: string): AsyncFailable<FileType> {
|
||||
const filetypes = await this.imageFilesService.getFileTypes(imageId);
|
||||
if (HasFailed(filetypes)) return filetypes;
|
||||
|
||||
if (filetypes['original'] === undefined)
|
||||
return Fail(FT.NotFound, 'No original file');
|
||||
|
||||
return ParseFileType(filetypes['original']);
|
||||
}
|
||||
|
||||
public async getFileMimes(imageId: string): AsyncFailable<{
|
||||
[ImageEntryVariant.MASTER]: string;
|
||||
[ImageEntryVariant.ORIGINAL]: string | undefined;
|
||||
}> {
|
||||
const result = await this.imageFilesService.getFileTypes(imageId);
|
||||
if (HasFailed(result)) return result;
|
||||
|
||||
if (result[ImageEntryVariant.MASTER] === undefined) {
|
||||
return Fail(FT.NotFound, 'No master file found');
|
||||
}
|
||||
|
||||
return {
|
||||
[ImageEntryVariant.MASTER]: result[ImageEntryVariant.MASTER]!,
|
||||
[ImageEntryVariant.ORIGINAL]: result[ImageEntryVariant.ORIGINAL],
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -1,50 +0,0 @@
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import {
|
||||
FileType,
|
||||
ImageFileType,
|
||||
SupportedFileTypeCategory
|
||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||
|
||||
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';
|
||||
|
||||
@Injectable()
|
||||
export class ImageProcessorService {
|
||||
constructor(private readonly imageConverter: ImageConverterService) {}
|
||||
|
||||
public async process(
|
||||
image: Buffer,
|
||||
filetype: FileType,
|
||||
): AsyncFailable<ImageResult> {
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
private async processStill(
|
||||
image: Buffer,
|
||||
filetype: FileType,
|
||||
): AsyncFailable<ImageResult> {
|
||||
const outputFileType = ParseFileType(ImageFileType.QOI);
|
||||
if (HasFailed(outputFileType)) return outputFileType;
|
||||
|
||||
return this.imageConverter.convert(image, filetype, outputFileType, {});
|
||||
}
|
||||
|
||||
private async processAnimation(
|
||||
image: Buffer,
|
||||
filetype: FileType,
|
||||
): AsyncFailable<ImageResult> {
|
||||
// Webps and gifs are stored as is for now
|
||||
return {
|
||||
image: image,
|
||||
filetype: filetype.identifier,
|
||||
};
|
||||
}
|
||||
}
|
||||
12
backend/src/managers/image/image.queue.ts
Normal file
12
backend/src/managers/image/image.queue.ts
Normal file
@@ -0,0 +1,12 @@
|
||||
import { Queue } from 'bull';
|
||||
import { ImageConvertJobData } from './convert.consumer';
|
||||
import { ImageIngestJobData } from './ingest.consumer';
|
||||
|
||||
export const ImageQueueID = 'image-queue';
|
||||
export type ImageQueueType = Queue<ImageIngestJobData | ImageConvertJobData>;
|
||||
|
||||
export enum ImageQueueSubject {
|
||||
INGEST = 'ingest',
|
||||
CONVERT = 'convert',
|
||||
}
|
||||
|
||||
@@ -1,288 +0,0 @@
|
||||
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 { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum';
|
||||
import {
|
||||
AnimFileType,
|
||||
FileType, ImageFileType,
|
||||
Mime2FileType
|
||||
} 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 { ParseFileType } 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';
|
||||
import { SysPreferenceDbService } from '../../collections/preference-db/sys-preference-db.service';
|
||||
import { UsrPreferenceDbService } from '../../collections/preference-db/usr-preference-db.service';
|
||||
import { EImageDerivativeBackend } from '../../database/entities/images/image-derivative.entity';
|
||||
import { EImageFileBackend } from '../../database/entities/images/image-file.entity';
|
||||
import { EImageBackend } from '../../database/entities/images/image.entity';
|
||||
import { MutexFallBack } from '../../util/mutex-fallback';
|
||||
import { ImageConverterService } from './image-converter.service';
|
||||
import { ImageProcessorService } from './image-processor.service';
|
||||
import { WebPInfo } from './webpinfo/webpinfo';
|
||||
|
||||
@Injectable()
|
||||
export class ImageManagerService {
|
||||
private readonly logger = new Logger(ImageManagerService.name);
|
||||
|
||||
constructor(
|
||||
private readonly imagesService: ImageDBService,
|
||||
private readonly imageFilesService: ImageFileDBService,
|
||||
private readonly processService: ImageProcessorService,
|
||||
private readonly convertService: ImageConverterService,
|
||||
private readonly userPref: UsrPreferenceDbService,
|
||||
private readonly sysPref: SysPreferenceDbService,
|
||||
) {}
|
||||
|
||||
public async findOne(id: string): AsyncFailable<EImageBackend> {
|
||||
return await this.imagesService.findOne(id, undefined);
|
||||
}
|
||||
|
||||
public async findMany(
|
||||
count: number,
|
||||
page: number,
|
||||
userid: string | undefined,
|
||||
): AsyncFailable<FindResult<EImageBackend>> {
|
||||
return await this.imagesService.findMany(count, page, userid);
|
||||
}
|
||||
|
||||
public async update(
|
||||
id: string,
|
||||
userid: string | undefined,
|
||||
options: Partial<Pick<EImageBackend, 'file_name' | 'expires_at'>>,
|
||||
): AsyncFailable<EImageBackend> {
|
||||
if (options.expires_at !== undefined && options.expires_at !== null) {
|
||||
if (options.expires_at < new Date()) {
|
||||
return Fail(FT.UsrValidation, 'Expiration date must be in the future');
|
||||
}
|
||||
}
|
||||
return await this.imagesService.update(id, userid, options);
|
||||
}
|
||||
|
||||
public async deleteMany(
|
||||
ids: string[],
|
||||
userid: string | undefined,
|
||||
): AsyncFailable<EImageBackend[]> {
|
||||
return await this.imagesService.delete(ids, userid);
|
||||
}
|
||||
|
||||
public async deleteWithKey(
|
||||
imageId: string,
|
||||
key: string,
|
||||
): AsyncFailable<EImageBackend> {
|
||||
return await this.imagesService.deleteWithKey(imageId, key);
|
||||
}
|
||||
|
||||
public async upload(
|
||||
userid: string,
|
||||
filename: string,
|
||||
image: Buffer,
|
||||
withDeleteKey: boolean,
|
||||
): AsyncFailable<EImageBackend> {
|
||||
const fileType = await this.getFileTypeFromBuffer(image);
|
||||
if (HasFailed(fileType)) return fileType;
|
||||
|
||||
// Check if need to save orignal
|
||||
const keepOriginal = await this.userPref.getBooleanPreference(
|
||||
userid,
|
||||
UsrPreference.KeepOriginal,
|
||||
);
|
||||
if (HasFailed(keepOriginal)) return keepOriginal;
|
||||
|
||||
// Process
|
||||
const processResult = await this.processService.process(image, fileType);
|
||||
if (HasFailed(processResult)) return processResult;
|
||||
|
||||
// Strip extension from filename
|
||||
const name = (() => {
|
||||
const index = filename.lastIndexOf('.');
|
||||
if (index === -1) return filename;
|
||||
return filename.substring(0, index);
|
||||
})();
|
||||
|
||||
// Save processed to db
|
||||
const imageEntity = await this.imagesService.create(
|
||||
userid,
|
||||
name,
|
||||
withDeleteKey,
|
||||
);
|
||||
if (HasFailed(imageEntity)) return imageEntity;
|
||||
|
||||
const imageFileEntity = await this.imageFilesService.setFile(
|
||||
imageEntity.id,
|
||||
ImageEntryVariant.MASTER,
|
||||
processResult.image,
|
||||
processResult.filetype,
|
||||
);
|
||||
if (HasFailed(imageFileEntity)) return imageFileEntity;
|
||||
|
||||
if (keepOriginal) {
|
||||
const originalFileEntity = await this.imageFilesService.setFile(
|
||||
imageEntity.id,
|
||||
ImageEntryVariant.ORIGINAL,
|
||||
image,
|
||||
fileType.identifier,
|
||||
);
|
||||
if (HasFailed(originalFileEntity)) return originalFileEntity;
|
||||
}
|
||||
|
||||
return imageEntity;
|
||||
}
|
||||
|
||||
public async getConverted(
|
||||
imageId: string,
|
||||
fileType: string,
|
||||
options: ImageRequestParams,
|
||||
): AsyncFailable<EImageDerivativeBackend> {
|
||||
const targetFileType = ParseFileType(fileType);
|
||||
if (HasFailed(targetFileType)) return targetFileType;
|
||||
|
||||
const converted_key = this.getConvertHash({ mime: fileType, ...options });
|
||||
|
||||
const [save_derivatives, allow_editing] = await Promise.all([
|
||||
this.sysPref.getBooleanPreference(SysPreference.SaveDerivatives),
|
||||
this.sysPref.getBooleanPreference(SysPreference.AllowEditing),
|
||||
]);
|
||||
if (HasFailed(save_derivatives)) return save_derivatives;
|
||||
if (HasFailed(allow_editing)) return allow_editing;
|
||||
|
||||
return MutexFallBack(
|
||||
converted_key,
|
||||
() => {
|
||||
if (save_derivatives)
|
||||
return this.imageFilesService.getDerivative(imageId, converted_key);
|
||||
else return Promise.resolve(null);
|
||||
},
|
||||
async () => {
|
||||
const masterImage = await this.getMaster(imageId);
|
||||
if (HasFailed(masterImage)) return masterImage;
|
||||
|
||||
const sourceFileType = ParseFileType(masterImage.filetype);
|
||||
if (HasFailed(sourceFileType)) return sourceFileType;
|
||||
|
||||
const startTime = Date.now();
|
||||
const convertResult = await this.convertService.convert(
|
||||
masterImage.data,
|
||||
sourceFileType,
|
||||
targetFileType,
|
||||
allow_editing ? options : {},
|
||||
);
|
||||
if (HasFailed(convertResult)) return convertResult;
|
||||
|
||||
this.logger.verbose(
|
||||
`Converted ${imageId} from ${sourceFileType.identifier} to ${
|
||||
targetFileType.identifier
|
||||
} in ${Date.now() - startTime}ms`,
|
||||
);
|
||||
|
||||
if (save_derivatives) {
|
||||
return await this.imageFilesService.addDerivative(
|
||||
imageId,
|
||||
converted_key,
|
||||
convertResult.filetype,
|
||||
convertResult.image,
|
||||
);
|
||||
} else {
|
||||
const derivative = new EImageDerivativeBackend();
|
||||
derivative.filetype = convertResult.filetype;
|
||||
derivative.data = convertResult.image;
|
||||
derivative.image_id = imageId;
|
||||
derivative.key = converted_key;
|
||||
return derivative;
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
// File getters ==============================================================
|
||||
|
||||
public async getMaster(imageId: string): AsyncFailable<EImageFileBackend> {
|
||||
return this.imageFilesService.getFile(imageId, ImageEntryVariant.MASTER);
|
||||
}
|
||||
|
||||
public async getMasterFileType(imageId: string): AsyncFailable<FileType> {
|
||||
const mime = await this.imageFilesService.getFileTypes(imageId);
|
||||
if (HasFailed(mime)) return mime;
|
||||
|
||||
if (mime['master'] === undefined)
|
||||
return Fail(FT.NotFound, 'No master file');
|
||||
|
||||
return ParseFileType(mime['master']);
|
||||
}
|
||||
|
||||
public async getOriginal(imageId: string): AsyncFailable<EImageFileBackend> {
|
||||
return this.imageFilesService.getFile(imageId, ImageEntryVariant.ORIGINAL);
|
||||
}
|
||||
|
||||
public async getOriginalFileType(imageId: string): AsyncFailable<FileType> {
|
||||
const filetypes = await this.imageFilesService.getFileTypes(imageId);
|
||||
if (HasFailed(filetypes)) return filetypes;
|
||||
|
||||
if (filetypes['original'] === undefined)
|
||||
return Fail(FT.NotFound, 'No original file');
|
||||
|
||||
return ParseFileType(filetypes['original']);
|
||||
}
|
||||
|
||||
public async getFileMimes(imageId: string): AsyncFailable<{
|
||||
[ImageEntryVariant.MASTER]: string;
|
||||
[ImageEntryVariant.ORIGINAL]: string | undefined;
|
||||
}> {
|
||||
const result = await this.imageFilesService.getFileTypes(imageId);
|
||||
if (HasFailed(result)) return result;
|
||||
|
||||
if (result[ImageEntryVariant.MASTER] === undefined) {
|
||||
return Fail(FT.NotFound, 'No master file found');
|
||||
}
|
||||
|
||||
return {
|
||||
[ImageEntryVariant.MASTER]: result[ImageEntryVariant.MASTER]!,
|
||||
[ImageEntryVariant.ORIGINAL]: result[ImageEntryVariant.ORIGINAL],
|
||||
};
|
||||
}
|
||||
|
||||
// Util stuff ==================================================================
|
||||
|
||||
private async getFileTypeFromBuffer(image: Buffer): AsyncFailable<FileType> {
|
||||
const filetypeResult: FileTypeResult | undefined = await fileTypeFromBuffer(
|
||||
image,
|
||||
);
|
||||
|
||||
let mime: string | undefined;
|
||||
if (filetypeResult === undefined) {
|
||||
if (IsQOI(image)) mime = 'image/x-qoi';
|
||||
} else {
|
||||
mime = filetypeResult.mime;
|
||||
}
|
||||
|
||||
if (mime === undefined) mime = 'other/unknown';
|
||||
|
||||
let filetype: string | undefined;
|
||||
if (mime === 'image/webp') {
|
||||
const header = await WebPInfo.from(image);
|
||||
if (header.summary.isAnimated) filetype = AnimFileType.WEBP;
|
||||
else filetype = ImageFileType.WEBP;
|
||||
}
|
||||
if (filetype === undefined) {
|
||||
const parsed = Mime2FileType(mime);
|
||||
if (HasFailed(parsed)) return parsed;
|
||||
filetype = parsed;
|
||||
}
|
||||
|
||||
return ParseFileType(filetype);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,6 @@
|
||||
import {
|
||||
InjectQueue,
|
||||
OnQueueError,
|
||||
OnQueueFailed,
|
||||
Process,
|
||||
Processor
|
||||
} from '@nestjs/bull';
|
||||
import { OnQueueError, OnQueueFailed, Process, Processor } from '@nestjs/bull';
|
||||
import { Logger } from '@nestjs/common';
|
||||
import type { Job, Queue } from 'bull';
|
||||
import type { Job } from 'bull';
|
||||
import { ImageEntryVariant } from 'picsur-shared/dist/dto/image-entry-variant.enum';
|
||||
import {
|
||||
FileType,
|
||||
@@ -27,47 +21,28 @@ import { ImageFileDBService } from '../../collections/image-db/image-file-db.ser
|
||||
import { EImageBackend } from '../../database/entities/images/image.entity';
|
||||
import { ImageConverterService } from '../image/image-converter.service';
|
||||
import { ImageResult } from '../image/imageresult';
|
||||
import { ImageQueueID, ImageQueueSubject } from './image.queue';
|
||||
|
||||
interface ImageIngestJobData {
|
||||
export interface ImageIngestJobData {
|
||||
imageID: string;
|
||||
storeOriginal: boolean;
|
||||
}
|
||||
export type ImageIngestQueue = Queue<ImageIngestJobData>;
|
||||
export type ImageIngestJob = Job<ImageIngestJobData>;
|
||||
|
||||
@Processor('image-ingest')
|
||||
@Processor(ImageQueueID)
|
||||
export class IngestConsumer {
|
||||
private readonly logger = new Logger(IngestConsumer.name);
|
||||
private i = 0;
|
||||
|
||||
constructor(
|
||||
@InjectQueue('image-ingest') private readonly ingestQueue: Queue,
|
||||
private readonly imagesService: ImageDBService,
|
||||
private readonly imageFilesService: ImageFileDBService,
|
||||
private readonly imageConverter: ImageConverterService,
|
||||
) {
|
||||
this.logger.log('Ingest consumer started');
|
||||
this.logger.error('Ingest consumer started');
|
||||
}
|
||||
) {}
|
||||
|
||||
// @Process('group')
|
||||
// async processJob(job: Job<GroupIngestJob>) {
|
||||
// console.log('Received', job.data);
|
||||
// await new Promise((resolve) => setTimeout(resolve, 4000));
|
||||
// console.log('Done');
|
||||
// return 'big chungus';
|
||||
// }
|
||||
|
||||
@Process('image')
|
||||
async processImage(job: ImageIngestJob): Promise<EImageBackend> {
|
||||
@Process(ImageQueueSubject.INGEST)
|
||||
async ingestImage(job: ImageIngestJob): Promise<EImageBackend> {
|
||||
const { imageID, storeOriginal } = job.data;
|
||||
|
||||
job.failedReason = 'Not implemented';
|
||||
|
||||
if (this.i === 0) {
|
||||
throw Fail(FT.Internal, undefined, 'oops');
|
||||
}
|
||||
|
||||
// Already start the query for the image, we only need it when returning
|
||||
const imagePromise = this.imagesService.findOne(imageID, undefined);
|
||||
|
||||
@@ -110,7 +85,7 @@ export class IngestConsumer {
|
||||
return image;
|
||||
}
|
||||
|
||||
public async process(
|
||||
private async process(
|
||||
image: Buffer,
|
||||
filetype: FileType,
|
||||
): AsyncFailable<ImageResult> {
|
||||
@@ -17,14 +17,16 @@ import { ImageFileDBService } from '../../collections/image-db/image-file-db.ser
|
||||
import { UsrPreferenceDbService } from '../../collections/preference-db/usr-preference-db.service';
|
||||
import { EImageBackend } from '../../database/entities/images/image.entity';
|
||||
import { WebPInfo } from '../image/webpinfo/webpinfo';
|
||||
import type { ImageIngestJob, ImageIngestQueue } from './ingest.consumer';
|
||||
import * as ImageQueue from './image.queue';
|
||||
import { ImageIngestJob } from './ingest.consumer';
|
||||
|
||||
@Injectable()
|
||||
export class IngestService {
|
||||
private readonly logger = new Logger(IngestService.name);
|
||||
|
||||
constructor(
|
||||
@InjectQueue('image-ingest') private readonly ingestQueue: ImageIngestQueue,
|
||||
@InjectQueue(ImageQueue.ImageQueueID)
|
||||
private readonly imageQueue: ImageQueue.ImageQueueType,
|
||||
private readonly imagesService: ImageDBService,
|
||||
private readonly imageFilesService: ImageFileDBService,
|
||||
private readonly userPref: UsrPreferenceDbService,
|
||||
@@ -35,7 +37,7 @@ export class IngestService {
|
||||
filename: string,
|
||||
image: Buffer,
|
||||
withDeleteKey: boolean,
|
||||
): AsyncFailable<ImageIngestJob> {
|
||||
): AsyncFailable<[ImageIngestJob, EImageBackend]> {
|
||||
const fileType = await this.getFileTypeFromBuffer(image);
|
||||
if (HasFailed(fileType)) return fileType;
|
||||
|
||||
@@ -69,13 +71,20 @@ export class IngestService {
|
||||
);
|
||||
if (HasFailed(imageFileEntity)) return imageFileEntity;
|
||||
|
||||
const job = await this.ingestQueue.add('image', {
|
||||
imageID: imageEntity.id,
|
||||
storeOriginal: keepOriginal,
|
||||
});
|
||||
if (!job.id) return Fail(FT.Internal, undefined, 'Failed to queue job');
|
||||
try {
|
||||
const job = (await this.imageQueue.add(
|
||||
ImageQueue.ImageQueueSubject.INGEST,
|
||||
{
|
||||
imageID: imageEntity.id,
|
||||
storeOriginal: keepOriginal,
|
||||
},
|
||||
)) as ImageIngestJob;
|
||||
if (!job.id) return Fail(FT.Internal, undefined, 'Failed to queue job');
|
||||
|
||||
return job;
|
||||
return [job, imageEntity];
|
||||
} catch (e) {
|
||||
return Fail(FT.Internal, e);
|
||||
}
|
||||
}
|
||||
|
||||
public async uploadPromise(
|
||||
@@ -84,11 +93,13 @@ export class IngestService {
|
||||
image: Buffer,
|
||||
withDeleteKey: boolean,
|
||||
): AsyncFailable<EImageBackend> {
|
||||
const job = await this.uploadJob(userid, filename, image, withDeleteKey);
|
||||
if (HasFailed(job)) return job;
|
||||
const result = await this.uploadJob(userid, filename, image, withDeleteKey);
|
||||
if (HasFailed(result)) return result;
|
||||
|
||||
const [job, imageEntity] = result;
|
||||
|
||||
try {
|
||||
const imageEntity: EImageBackend = await job.finished();
|
||||
await job.finished();
|
||||
return imageEntity;
|
||||
} catch (e) {
|
||||
return Fail(FT.Internal, 'Failed to process image', e);
|
||||
@@ -1,21 +0,0 @@
|
||||
import { BullModule } from '@nestjs/bull';
|
||||
import { Module } from '@nestjs/common';
|
||||
import { ImageDBModule } from '../../collections/image-db/image-db.module';
|
||||
import { PreferenceDbModule } from '../../collections/preference-db/preference-db.module';
|
||||
import { ImageManagerModule } from '../image/image.module';
|
||||
import { IngestConsumer } from './ingest.consumer';
|
||||
import { IngestService } from './ingest.service';
|
||||
|
||||
@Module({
|
||||
imports: [
|
||||
ImageDBModule,
|
||||
ImageManagerModule,
|
||||
PreferenceDbModule,
|
||||
BullModule.registerQueue({
|
||||
name: 'image-ingest',
|
||||
}),
|
||||
],
|
||||
providers: [IngestConsumer, IngestService],
|
||||
exports: [BullModule, IngestService],
|
||||
})
|
||||
export class IngestManagerModule {}
|
||||
@@ -1,6 +1,4 @@
|
||||
import { InjectQueue } from '@nestjs/bull';
|
||||
import { Controller, Get, Logger } from '@nestjs/common';
|
||||
import type { Queue } from 'bull';
|
||||
import { NoPermissions } from '../../../decorators/permissions.decorator';
|
||||
import { ReturnsAnything } from '../../../decorators/returns.decorator';
|
||||
|
||||
@@ -9,27 +7,11 @@ import { ReturnsAnything } from '../../../decorators/returns.decorator';
|
||||
export class ExperimentController {
|
||||
private readonly logger = new Logger(ExperimentController.name);
|
||||
|
||||
constructor(
|
||||
@InjectQueue('image-ingest') private readonly ingestQueue: Queue,
|
||||
) {
|
||||
this.logger.log('experiment consumer started');
|
||||
this.logger.error('experiment consumer started');
|
||||
console.log(this.logger);
|
||||
}
|
||||
constructor() {}
|
||||
|
||||
@Get()
|
||||
@ReturnsAnything()
|
||||
async testRoute(): Promise<any> {
|
||||
console.log('Create job');
|
||||
const job = await this.ingestQueue.add({
|
||||
foo: Buffer.from('aaaaaheleool'),
|
||||
});
|
||||
console.log('Job created', job.id);
|
||||
|
||||
const result = await job.finished();
|
||||
|
||||
console.log('Job finished', result);
|
||||
|
||||
return 'ok';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,13 +1,13 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { PicsurLoggerModule } from '../../../logger/logger.module';
|
||||
import { IngestManagerModule } from '../../../managers/ingest/ingest.module';
|
||||
import { ImageManagerModule } from '../../../managers/image/image-manager.module';
|
||||
import { ExperimentController } from './experiment.controller';
|
||||
|
||||
// This is comletely useless module, but is used for testing
|
||||
// TODO: remove when out of beta
|
||||
|
||||
@Module({
|
||||
imports: [IngestManagerModule, PicsurLoggerModule],
|
||||
imports: [ImageManagerModule, PicsurLoggerModule],
|
||||
controllers: [ExperimentController]
|
||||
})
|
||||
export class ExperimentModule {}
|
||||
|
||||
@@ -30,8 +30,8 @@ import {
|
||||
} from '../../decorators/permissions.decorator';
|
||||
import { ReqUserID } from '../../decorators/request-user.decorator';
|
||||
import { Returns, ReturnsAnything } from '../../decorators/returns.decorator';
|
||||
import { ImageManagerService } from '../../managers/image/image.service';
|
||||
import { IngestService } from '../../managers/ingest/ingest.service';
|
||||
import { ImageManagerService } from '../../managers/image/image-manager.service';
|
||||
import { IngestService } from '../../managers/image/ingest.service';
|
||||
import { GetNextAsync } from '../../util/iterator';
|
||||
@Controller('api/image')
|
||||
@RequiredPermissions(Permission.ImageUpload)
|
||||
|
||||
@@ -13,7 +13,8 @@ import { ImageFullIdParam } from '../../decorators/image-id/image-full-id.decora
|
||||
import { ImageIdParam } from '../../decorators/image-id/image-id.decorator';
|
||||
import { RequiredPermissions } from '../../decorators/permissions.decorator';
|
||||
import { Returns } from '../../decorators/returns.decorator';
|
||||
import { ImageManagerService } from '../../managers/image/image.service';
|
||||
import { ConvertService } from '../../managers/image/convert.service';
|
||||
import { ImageManagerService } from '../../managers/image/image-manager.service';
|
||||
import type { ImageFullId } from '../../models/constants/image-full-id.const';
|
||||
import { Permission } from '../../models/constants/permissions.const';
|
||||
import { EUserBackend2EUser } from '../../models/transformers/user.transformer';
|
||||
@@ -29,6 +30,7 @@ export class ImageController {
|
||||
constructor(
|
||||
private readonly imagesService: ImageManagerService,
|
||||
private readonly userService: UserDbService,
|
||||
private readonly convertService: ConvertService,
|
||||
) {}
|
||||
|
||||
@Head(':id')
|
||||
@@ -67,7 +69,7 @@ export class ImageController {
|
||||
}
|
||||
|
||||
const image = ThrowIfFailed(
|
||||
await this.imagesService.getConverted(
|
||||
await this.convertService.convertPromise(
|
||||
fullid.id,
|
||||
fullid.filetype,
|
||||
params,
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { IngestFileDbModule } from '../../collections/ingest-file-db/ingest-file-db.module';
|
||||
import { UserDbModule } from '../../collections/user-db/user-db.module';
|
||||
import { DecoratorsModule } from '../../decorators/decorators.module';
|
||||
import { ImageManagerModule } from '../../managers/image/image.module';
|
||||
import { IngestManagerModule } from '../../managers/ingest/ingest.module';
|
||||
import { ImageManagerModule } from '../../managers/image/image-manager.module';
|
||||
import { ImageManageController } from './image-manage.controller';
|
||||
import { ImageController } from './image.controller';
|
||||
|
||||
@@ -11,8 +9,6 @@ import { ImageController } from './image.controller';
|
||||
imports: [
|
||||
ImageManagerModule,
|
||||
UserDbModule,
|
||||
IngestFileDbModule,
|
||||
IngestManagerModule,
|
||||
DecoratorsModule,
|
||||
],
|
||||
controllers: [ImageController, ImageManageController],
|
||||
|
||||
@@ -17,13 +17,7 @@ export const SysPreferenceUI: {
|
||||
[SysPreference.RemoveDerivativesAfter]: {
|
||||
name: 'Cached Images Expiry Time',
|
||||
helpText:
|
||||
'Time before cached images are deleted. This does not affect the original image. Set to 0 to disable.',
|
||||
category: 'Image Processing',
|
||||
},
|
||||
[SysPreference.SaveDerivatives]: {
|
||||
name: 'Cache Converted Images',
|
||||
helpText:
|
||||
'Cache converted images, this will reduce the time it takes to load images. It does however use more disk space.',
|
||||
'Time before cached converted images are deleted. This does not affect the original image. A lower cache time will save on disk space but cost more cpu. Set to 0 to disable.',
|
||||
category: 'Image Processing',
|
||||
},
|
||||
[SysPreference.AllowEditing]: {
|
||||
|
||||
@@ -13,7 +13,6 @@ export enum SysPreference {
|
||||
JwtExpiresIn = 'jwt_expires_in',
|
||||
BCryptStrength = 'bcrypt_strength',
|
||||
|
||||
SaveDerivatives = 'save_derivatives',
|
||||
RemoveDerivativesAfter = 'remove_derivatives_after',
|
||||
AllowEditing = 'allow_editing',
|
||||
|
||||
@@ -41,7 +40,6 @@ export const SysPreferenceValueTypes: {
|
||||
[SysPreference.BCryptStrength]: 'number',
|
||||
|
||||
[SysPreference.RemoveDerivativesAfter]: 'string',
|
||||
[SysPreference.SaveDerivatives]: 'boolean',
|
||||
[SysPreference.AllowEditing]: 'boolean',
|
||||
|
||||
[SysPreference.ConversionTimeLimit]: 'string',
|
||||
@@ -63,8 +61,7 @@ export const SysPreferenceValidators: {
|
||||
[SysPreference.JwtExpiresIn]: IsValidMS(),
|
||||
|
||||
[SysPreference.BCryptStrength]: IsPosInt(),
|
||||
[SysPreference.RemoveDerivativesAfter]: IsValidMS(),
|
||||
[SysPreference.SaveDerivatives]: z.boolean(),
|
||||
[SysPreference.RemoveDerivativesAfter]: IsValidMS(60000),
|
||||
|
||||
[SysPreference.AllowEditing]: z.boolean(),
|
||||
[SysPreference.ConversionTimeLimit]: IsValidMS(),
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import ms from 'ms';
|
||||
import { z } from 'zod';
|
||||
|
||||
export const IsValidMS = () =>
|
||||
export const IsValidMS = (min = 0) =>
|
||||
z.preprocess(
|
||||
(v: any) => {
|
||||
try {
|
||||
@@ -17,5 +17,5 @@ export const IsValidMS = () =>
|
||||
}),
|
||||
})
|
||||
.int()
|
||||
.min(0),
|
||||
.min(min),
|
||||
);
|
||||
|
||||
147
yarn.lock
147
yarn.lock
@@ -39,17 +39,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@angular-devkit/architect@npm:0.1402.2, @angular-devkit/architect@npm:>=0.1400.0 < 0.1500.0":
|
||||
version: 0.1402.2
|
||||
resolution: "@angular-devkit/architect@npm:0.1402.2"
|
||||
dependencies:
|
||||
"@angular-devkit/core": 14.2.2
|
||||
rxjs: 6.6.7
|
||||
checksum: a014bbd941582ad4a263b9d8870accef78c5e26528ba3f7a760c860dbb4ba6b66216361f562da591b579662f2710fb5810d748610aa17b7b4c17f4caa08794e3
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@angular-devkit/architect@npm:0.1402.3":
|
||||
"@angular-devkit/architect@npm:0.1402.3, @angular-devkit/architect@npm:>=0.1400.0 < 0.1500.0":
|
||||
version: 0.1402.3
|
||||
resolution: "@angular-devkit/architect@npm:0.1402.3"
|
||||
dependencies:
|
||||
@@ -59,7 +49,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@angular-devkit/build-angular@npm:14.2.3":
|
||||
"@angular-devkit/build-angular@npm:14.2.3, @angular-devkit/build-angular@npm:^14.0.0":
|
||||
version: 14.2.3
|
||||
resolution: "@angular-devkit/build-angular@npm:14.2.3"
|
||||
dependencies:
|
||||
@@ -155,115 +145,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@angular-devkit/build-angular@npm:^14.0.0":
|
||||
version: 14.2.2
|
||||
resolution: "@angular-devkit/build-angular@npm:14.2.2"
|
||||
dependencies:
|
||||
"@ampproject/remapping": 2.2.0
|
||||
"@angular-devkit/architect": 0.1402.2
|
||||
"@angular-devkit/build-webpack": 0.1402.2
|
||||
"@angular-devkit/core": 14.2.2
|
||||
"@babel/core": 7.18.10
|
||||
"@babel/generator": 7.18.12
|
||||
"@babel/helper-annotate-as-pure": 7.18.6
|
||||
"@babel/plugin-proposal-async-generator-functions": 7.18.10
|
||||
"@babel/plugin-transform-async-to-generator": 7.18.6
|
||||
"@babel/plugin-transform-runtime": 7.18.10
|
||||
"@babel/preset-env": 7.18.10
|
||||
"@babel/runtime": 7.18.9
|
||||
"@babel/template": 7.18.10
|
||||
"@discoveryjs/json-ext": 0.5.7
|
||||
"@ngtools/webpack": 14.2.2
|
||||
ansi-colors: 4.1.3
|
||||
babel-loader: 8.2.5
|
||||
babel-plugin-istanbul: 6.1.1
|
||||
browserslist: ^4.9.1
|
||||
cacache: 16.1.2
|
||||
copy-webpack-plugin: 11.0.0
|
||||
critters: 0.0.16
|
||||
css-loader: 6.7.1
|
||||
esbuild: 0.15.5
|
||||
esbuild-wasm: 0.15.5
|
||||
glob: 8.0.3
|
||||
https-proxy-agent: 5.0.1
|
||||
inquirer: 8.2.4
|
||||
jsonc-parser: 3.1.0
|
||||
karma-source-map-support: 1.4.0
|
||||
less: 4.1.3
|
||||
less-loader: 11.0.0
|
||||
license-webpack-plugin: 4.0.2
|
||||
loader-utils: 3.2.0
|
||||
mini-css-extract-plugin: 2.6.1
|
||||
minimatch: 5.1.0
|
||||
open: 8.4.0
|
||||
ora: 5.4.1
|
||||
parse5-html-rewriting-stream: 6.0.1
|
||||
piscina: 3.2.0
|
||||
postcss: 8.4.16
|
||||
postcss-import: 15.0.0
|
||||
postcss-loader: 7.0.1
|
||||
postcss-preset-env: 7.8.0
|
||||
regenerator-runtime: 0.13.9
|
||||
resolve-url-loader: 5.0.0
|
||||
rxjs: 6.6.7
|
||||
sass: 1.54.4
|
||||
sass-loader: 13.0.2
|
||||
semver: 7.3.7
|
||||
source-map-loader: 4.0.0
|
||||
source-map-support: 0.5.21
|
||||
stylus: 0.59.0
|
||||
stylus-loader: 7.0.0
|
||||
terser: 5.14.2
|
||||
text-table: 0.2.0
|
||||
tree-kill: 1.2.2
|
||||
tslib: 2.4.0
|
||||
webpack: 5.74.0
|
||||
webpack-dev-middleware: 5.3.3
|
||||
webpack-dev-server: 4.11.0
|
||||
webpack-merge: 5.8.0
|
||||
webpack-subresource-integrity: 5.1.0
|
||||
peerDependencies:
|
||||
"@angular/compiler-cli": ^14.0.0
|
||||
"@angular/localize": ^14.0.0
|
||||
"@angular/service-worker": ^14.0.0
|
||||
karma: ^6.3.0
|
||||
ng-packagr: ^14.0.0
|
||||
protractor: ^7.0.0
|
||||
tailwindcss: ^2.0.0 || ^3.0.0
|
||||
typescript: ">=4.6.2 <4.9"
|
||||
dependenciesMeta:
|
||||
esbuild:
|
||||
optional: true
|
||||
peerDependenciesMeta:
|
||||
"@angular/localize":
|
||||
optional: true
|
||||
"@angular/service-worker":
|
||||
optional: true
|
||||
karma:
|
||||
optional: true
|
||||
ng-packagr:
|
||||
optional: true
|
||||
protractor:
|
||||
optional: true
|
||||
tailwindcss:
|
||||
optional: true
|
||||
checksum: 3829654f52b26bc7bd5c753f867a0b24d649e0b650c24cd6ff0ecbfc0708bab8403c64ece40a349147d2104dd21472ac71e7cf7b8e58e1622f430fab99701086
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@angular-devkit/build-webpack@npm:0.1402.2":
|
||||
version: 0.1402.2
|
||||
resolution: "@angular-devkit/build-webpack@npm:0.1402.2"
|
||||
dependencies:
|
||||
"@angular-devkit/architect": 0.1402.2
|
||||
rxjs: 6.6.7
|
||||
peerDependencies:
|
||||
webpack: ^5.30.0
|
||||
webpack-dev-server: ^4.0.0
|
||||
checksum: fb97fa5eceb043cefce63ee4a8a598496002506c05423437914b721c41e78408750d28b42b9dc1d4594c1c599ea5c28b7be1903b8a3041c46c1e9dd1f2d7575c
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@angular-devkit/build-webpack@npm:0.1402.3":
|
||||
version: 0.1402.3
|
||||
resolution: "@angular-devkit/build-webpack@npm:0.1402.3"
|
||||
@@ -295,7 +176,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@angular-devkit/core@npm:14.2.2, @angular-devkit/core@npm:^14.0.0":
|
||||
"@angular-devkit/core@npm:14.2.2":
|
||||
version: 14.2.2
|
||||
resolution: "@angular-devkit/core@npm:14.2.2"
|
||||
dependencies:
|
||||
@@ -313,7 +194,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@angular-devkit/core@npm:14.2.3":
|
||||
"@angular-devkit/core@npm:14.2.3, @angular-devkit/core@npm:^14.0.0":
|
||||
version: 14.2.3
|
||||
resolution: "@angular-devkit/core@npm:14.2.3"
|
||||
dependencies:
|
||||
@@ -2745,17 +2626,6 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ngtools/webpack@npm:14.2.2":
|
||||
version: 14.2.2
|
||||
resolution: "@ngtools/webpack@npm:14.2.2"
|
||||
peerDependencies:
|
||||
"@angular/compiler-cli": ^14.0.0
|
||||
typescript: ">=4.6.2 <4.9"
|
||||
webpack: ^5.54.0
|
||||
checksum: fce1268f3686ed1f974161bed3662915a98a90c75b7bc8fb166fea346794351ef92bc5bb3cb95714f9d235beaa1b14c8856d4ca8d7eac431d30918a96d404df8
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@ngtools/webpack@npm:14.2.3":
|
||||
version: 14.2.3
|
||||
resolution: "@ngtools/webpack@npm:14.2.3"
|
||||
@@ -3177,14 +3047,7 @@ __metadata:
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:*":
|
||||
version: 18.7.16
|
||||
resolution: "@types/node@npm:18.7.16"
|
||||
checksum: 01a3d35c764a3f0e7370b56e1ad4203731131883c65784e020009014171b3f53c4649cde6c7aa4f1026b907ee87ef6ae6ece2bc518151dc7b81100fe8b1db3ad
|
||||
languageName: node
|
||||
linkType: hard
|
||||
|
||||
"@types/node@npm:>=10.0.0, @types/node@npm:^18.7.18":
|
||||
"@types/node@npm:*, @types/node@npm:>=10.0.0, @types/node@npm:^18.7.18":
|
||||
version: 18.7.18
|
||||
resolution: "@types/node@npm:18.7.18"
|
||||
checksum: 8aec61f0f96e2a69ce51f1f40f949ca578bbb4fe05d7c0b8ce3aeeb848e90f755837f17f6ac132ca404d974fe9b2974150ad3b4984fc9dc7c3ceddb10bae0167
|
||||
|
||||
Reference in New Issue
Block a user