add ability to add extensions to image api
This commit is contained in:
parent
1d7a18b2bb
commit
6c1fa47a77
|
@ -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');
|
||||
|
|
|
@ -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);
|
29
backend/src/decorators/image-id/image-full-id.pipe.ts
Normal file
29
backend/src/decorators/image-id/image-full-id.pipe.ts
Normal 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 };
|
||||
}
|
||||
}
|
5
backend/src/models/constants/image-full-id.const.ts
Normal file
5
backend/src/models/constants/image-full-id.const.ts
Normal file
|
@ -0,0 +1,5 @@
|
|||
export interface ImageFullId {
|
||||
id: string;
|
||||
ext: string;
|
||||
mime: string;
|
||||
}
|
|
@ -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');
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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 {}
|
||||
|
|
|
@ -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() {
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
|
|
@ -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>;
|
||||
|
|
|
@ -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 });
|
||||
|
|
|
@ -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,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -14,7 +14,7 @@ addEventListener('message', async (msg) => {
|
|||
|
||||
const returned: QOIWorkerOut = {
|
||||
id,
|
||||
...result,
|
||||
result,
|
||||
};
|
||||
|
||||
postMessage(returned);
|
||||
|
|
|
@ -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];
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue