add ability to add extensions to image api

This commit is contained in:
rubikscraft 2022-04-21 19:35:11 +02:00
parent 1d7a18b2bb
commit 6c1fa47a77
No known key found for this signature in database
GPG key ID: 1463EBE9200A5CD4
16 changed files with 141 additions and 59 deletions

View file

@ -45,7 +45,7 @@ export class ImageFileDBService {
): AsyncFailable<EImageFileBackend> {
try {
const found = await this.imageFileRepo.findOne({
where: { imageId, type },
where: { imageId: imageId ?? '', type: type ?? '' },
});
if (!found) return Fail('Image not found');

View file

@ -0,0 +1,6 @@
import { Param, PipeTransform, Type } from '@nestjs/common';
import { ImageFullIdPipe } from './image-full-id.pipe';
export const ImageFullIdParam = (
...pipes: (PipeTransform<any, any> | Type<PipeTransform<any, any>>)[]
) => Param('id', ImageFullIdPipe, ...pipes);

View file

@ -0,0 +1,29 @@
import {
ArgumentMetadata,
BadRequestException,
Injectable,
PipeTransform
} from '@nestjs/common';
import { Ext2Mime } from 'picsur-shared/dist/dto/mimes.dto';
import { UUIDRegex } from 'picsur-shared/dist/util/common-regex';
import { ImageFullId } from '../../models/constants/image-full-id.const';
@Injectable()
export class ImageFullIdPipe implements PipeTransform<string, ImageFullId> {
transform(value: string, metadata: ArgumentMetadata): ImageFullId {
const split = value.split('.');
if (split.length !== 2)
throw new BadRequestException('Invalid image identifier');
const [id, ext] = split;
if (!UUIDRegex.test(id))
throw new BadRequestException('Invalid image identifier');
const mime = Ext2Mime(ext);
if (mime === undefined)
throw new BadRequestException('Invalid image identifier');
return { id, ext, mime };
}
}

View file

@ -0,0 +1,5 @@
export interface ImageFullId {
id: string;
ext: string;
mime: string;
}

View file

@ -11,12 +11,14 @@ import {
import { FastifyReply } from 'fastify';
import { ImageMetaResponse } from 'picsur-shared/dist/dto/api/image.dto';
import { HasFailed } from 'picsur-shared/dist/types';
import { ImageFullIdParam } from '../../decorators/image-id/image-full-id.decorator';
import { ImageIdParam } from '../../decorators/image-id/image-id.decorator';
import { MultiPart } from '../../decorators/multipart/multipart.decorator';
import { RequiredPermissions } from '../../decorators/permissions.decorator';
import { ReqUserID } from '../../decorators/request-user.decorator';
import { Returns } from '../../decorators/returns.decorator';
import { ImageManagerService } from '../../managers/image/image.service';
import { ImageFullId } from '../../models/constants/image-full-id.const';
import { Permission } from '../../models/constants/permissions.const';
import { ImageUploadDto } from '../../models/dto/image-upload.dto';
@ -33,9 +35,9 @@ export class ImageController {
// Usually passthrough is for manually sending the response,
// But we need it here to set the mime type
@Res({ passthrough: true }) res: FastifyReply,
@ImageIdParam() id: string,
@ImageFullIdParam() fullid: ImageFullId,
): Promise<Buffer> {
const image = await this.imagesService.getMaster(id);
const image = await this.imagesService.getMaster(fullid.id);
if (HasFailed(image)) {
this.logger.warn(image.getReason());
throw new NotFoundException('Could not find image');
@ -48,9 +50,9 @@ export class ImageController {
@Head(':id')
async headImage(
@Res({ passthrough: true }) res: FastifyReply,
@ImageIdParam() id: string,
@ImageFullIdParam() fullid: ImageFullId,
) {
const fullmime = await this.imagesService.getMasterMime(id);
const fullmime = await this.imagesService.getMasterMime(fullid.id);
if (HasFailed(fullmime)) {
this.logger.warn(fullmime.getReason());
throw new NotFoundException('Could not find image');

View file

@ -7,3 +7,4 @@
></canvas>
<img *ngIf="state === 'image'" [src]="imageURL" />
<mat-spinner *ngIf="state === 'loading'" color="accent"></mat-spinner>
<mat-icon *ngIf="state === 'error'">broken_image</mat-icon>

View file

@ -18,6 +18,7 @@ enum PicsurImgState {
Loading = 'loading',
Canvas = 'canvas',
Image = 'image',
Error = 'error',
}
@Component({
@ -41,22 +42,28 @@ export class PicsurImgComponent implements OnChanges {
ngOnChanges(changes: SimpleChanges): void {
let url = this.imageURL ?? '';
if (URLRegex.test(url)) {
this.update(url).catch(this.logger.error);
} else {
if (!URLRegex.test(url)) {
this.state = PicsurImgState.Loading;
}
}
private async update(url: string) {
const mime = await this.getMime(url);
if (HasFailed(mime)) {
this.logger.error(mime.getReason());
return;
}
this.update(url)
.then((result) => {
if (HasFailed(result)) {
this.state = PicsurImgState.Error;
this.logger.error(result.getReason());
}
})
.catch((e) => this.logger.error);
}
private async update(url: string): AsyncFailable<void> {
const mime = await this.getMime(url);
if (HasFailed(mime)) return mime;
if (mime.mime === SupportedMime.QOI) {
const result = await this.qoiWorker.decode(url);
if (HasFailed(result)) return result;
const canvas = this.canvas.nativeElement;
canvas.height = result.height;

View file

@ -1,11 +1,12 @@
import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { MatIconModule } from '@angular/material/icon';
import { MatProgressSpinnerModule } from '@angular/material/progress-spinner';
import { PicsurImgComponent } from './picsur-img.component';
@NgModule({
declarations: [PicsurImgComponent],
imports: [CommonModule, MatProgressSpinnerModule],
imports: [CommonModule, MatProgressSpinnerModule, MatIconModule],
exports: [PicsurImgComponent],
})
export class PicsurImgModule {}

View file

@ -32,7 +32,7 @@ export class ViewComponent implements OnInit {
return this.utilService.quitError(metadata.getReason());
}
this.imageLinks = this.imageService.CreateImageLinksFromID(id);
this.imageLinks = this.imageService.CreateImageLinksFromID(id, 'qoi');
}
download() {

View file

@ -120,6 +120,8 @@ export class ApiService {
const response = await this.fetch(url, options);
if (HasFailed(response)) return response;
if (!response.ok) return Fail('Recieved a non-ok response');
const mimeType = response.headers.get('Content-Type') ?? 'other/unknown';
let name = response.headers.get('Content-Disposition');
if (!name) {
@ -155,6 +157,8 @@ export class ApiService {
const response = await this.fetch(url, options);
if (HasFailed(response)) return response;
if (!response.ok) return Fail('Recieved a non-ok response');
return response.headers;
}

View file

@ -42,7 +42,7 @@ export class ImageService {
};
}
public CreateImageLinksFromID(imageID: string): ImageLinks {
return this.CreateImageLinks(this.GetImageURL(imageID));
public CreateImageLinksFromID(imageID: string, format: string): ImageLinks {
return this.CreateImageLinks(this.GetImageURL(imageID + '.' + format));
}
}

View file

@ -1,3 +1,5 @@
import { AsyncFailable, Failable } from 'picsur-shared/dist/types';
export interface QOIImage {
data: ImageData;
width: number;
@ -10,8 +12,9 @@ export interface QOIWorkerIn {
authorization: string;
}
export interface QOIWorkerOut extends QOIImage {
export interface QOIWorkerOut {
id: number;
result: Failable<QOIImage>;
}
export type QOIJob = (url: string, authorization: string) => Promise<QOIImage>;
export type QOIJob = (url: string, authorization: string) => AsyncFailable<QOIImage>;

View file

@ -1,4 +1,5 @@
import { Injectable } from '@angular/core';
import { AsyncFailable, Failure, HasFailed } from 'picsur-shared/dist/types';
import { KeyService } from '../services/storage/key.service';
import { QOIImage, QOIJob, QOIWorkerOut } from './qoi-worker.dto';
@ -17,7 +18,7 @@ export class QoiWorkerService {
}
}
public async decode(url: string): Promise<QOIImage> {
public async decode(url: string): AsyncFailable<QOIImage> {
const authorization = 'Bearer ' + (this.keyService.get() ?? '');
if (this.worker && !this.job) {
@ -27,11 +28,10 @@ export class QoiWorkerService {
if (data.id !== id) return;
this.worker!.removeEventListener('message', listener);
resolve({
data: data.data,
width: data.width,
height: data.height,
});
let result = data.result;
if (HasFailed(result)) result = Failure.deserialize(data.result);
resolve(result);
};
this.worker!.addEventListener('message', listener);
this.worker!.postMessage({ id, url, authorization });

View file

@ -1,32 +1,37 @@
import { AsyncFailable, Fail } from 'picsur-shared/dist/types';
import { QOIdecodeJS } from '../util/qoi/qoi-decode';
import { QOIImage } from './qoi-worker.dto';
export default async function qoiDecodeJob(
url: string,
authorization: string
): Promise<QOIImage> {
const response = await fetch(url, {
headers: {
Authorization: authorization,
},
});
if (!response.ok) {
throw new Error(`Failed to fetch image: ${url}`);
): AsyncFailable<QOIImage> {
try {
const response = await fetch(url, {
headers: {
Authorization: authorization,
},
});
if (!response.ok) {
return Fail('Could not fetch image');
}
const buffer = await response.arrayBuffer();
const image = QOIdecodeJS(buffer, null, null, 4);
const imageData = new ImageData(
new Uint8ClampedArray(image.data.buffer),
image.width,
image.height
);
return {
data: imageData,
width: image.width,
height: image.height,
};
} catch (e) {
return Fail(e);
}
const buffer = await response.arrayBuffer();
const image = QOIdecodeJS(buffer, null, null, 4);
const imageData = new ImageData(
new Uint8ClampedArray(image.data.buffer),
image.width,
image.height
);
return {
data: imageData,
width: image.width,
height: image.height,
};
}

View file

@ -14,7 +14,7 @@ addEventListener('message', async (msg) => {
const returned: QOIWorkerOut = {
id,
...result,
result,
};
postMessage(returned);

View file

@ -4,7 +4,12 @@
// -> Side effects go brrr
export class Failure {
constructor(private readonly reason?: string, private readonly stack?: string) {}
private __68351953531423479708__id_failure = 1148363914;
constructor(
private readonly reason?: string,
private readonly stack?: string,
) {}
getReason(): string {
return this.reason ?? 'Unknown';
@ -13,14 +18,22 @@ export class Failure {
getStack(): string {
return this.stack ?? 'None';
}
static deserialize(data: any): Failure {
if (data.__68351953531423479708__id_failure !== 1148363914) {
throw new Error('Invalid failure data');
}
return new Failure(data.reason, data.stack);
}
}
export function Fail(reason?: any): Failure {
if (typeof reason === 'string') {
return new Failure(reason);
} else if(reason instanceof Error) {
} else if (reason instanceof Error) {
return new Failure(reason.message, reason.stack);
} else if(reason instanceof Failure) {
} else if (reason instanceof Failure) {
return reason;
} else {
return new Failure('Converted(' + reason + ')');
@ -33,20 +46,26 @@ export type AsyncFailable<T> = Promise<Failable<T>>;
export function HasFailed<T>(failable: Failable<T>): failable is Failure {
if (failable instanceof Promise) throw new Error('Invalid use of HasFailed');
return failable instanceof Failure;
return (failable as any).__68351953531423479708__id_failure === 1148363914;
}
export function HasSuccess<T>(failable: Failable<T>): failable is T {
if (failable instanceof Promise) throw new Error('Invalid use of HasSuccess');
return !(failable instanceof Failure);
return (failable as any).__68351953531423479708__id_failure !== 1148363914;
}
export function Map<T, U>(failable: Failable<T>, mapper: (value: T) => U): Failable<U> {
export function Map<T, U>(
failable: Failable<T>,
mapper: (value: T) => U,
): Failable<U> {
if (HasFailed(failable)) return failable;
return mapper(failable);
}
export function Open<T, U extends keyof T>(failable: Failable<T>, key: U): Failable<T[U]> {
export function Open<T, U extends keyof T>(
failable: Failable<T>,
key: U,
): Failable<T[U]> {
if (HasFailed(failable)) return failable;
return failable[key];
}