change images to use ids instead of hashes

This commit is contained in:
rubikscraft
2022-04-16 17:22:50 +02:00
parent bd20c99f84
commit 84aabbd49a
10 changed files with 37 additions and 51 deletions

View File

@@ -1,9 +1,8 @@
import { Injectable } from '@nestjs/common'; import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm'; import { InjectRepository } from '@nestjs/typeorm';
import Crypto from 'crypto';
import { import {
AsyncFailable, AsyncFailable,
Fail, HasSuccess Fail
} from 'picsur-shared/dist/types'; } from 'picsur-shared/dist/types';
import { Repository } from 'typeorm'; import { Repository } from 'typeorm';
import { EImageBackend } from '../../models/entities/image.entity'; import { EImageBackend } from '../../models/entities/image.entity';
@@ -20,14 +19,9 @@ export class ImageDBService {
image: Buffer, image: Buffer,
type: string, type: string,
): AsyncFailable<EImageBackend> { ): AsyncFailable<EImageBackend> {
const hash = this.hash(image);
const find = await this.findOne(hash);
if (HasSuccess(find)) return find;
let imageEntity = new EImageBackend(); let imageEntity = new EImageBackend();
imageEntity.data = image; imageEntity.data = image;
imageEntity.mime = type; imageEntity.mime = type;
imageEntity.hash = hash;
try { try {
imageEntity = await this.imageRepository.save(imageEntity); imageEntity = await this.imageRepository.save(imageEntity);
@@ -39,14 +33,14 @@ export class ImageDBService {
} }
public async findOne<B extends true | undefined = undefined>( public async findOne<B extends true | undefined = undefined>(
hash: string, id: string,
getPrivate?: B, getPrivate?: B,
): AsyncFailable< ): AsyncFailable<
B extends undefined ? EImageBackend : Required<EImageBackend> B extends undefined ? EImageBackend : Required<EImageBackend>
> { > {
try { try {
const found = await this.imageRepository.findOne({ const found = await this.imageRepository.findOne({
where: { hash }, where: { id },
select: getPrivate ? GetCols(this.imageRepository) : undefined, select: getPrivate ? GetCols(this.imageRepository) : undefined,
}); });
@@ -79,9 +73,9 @@ export class ImageDBService {
} }
} }
public async delete(hash: string): AsyncFailable<true> { public async delete(id: string): AsyncFailable<true> {
try { try {
const result = await this.imageRepository.delete({ hash }); const result = await this.imageRepository.delete({ id });
if (result.affected === 0) return Fail('Image not found'); if (result.affected === 0) return Fail('Image not found');
} catch (e: any) { } catch (e: any) {
return Fail(e?.message); return Fail(e?.message);
@@ -100,8 +94,4 @@ export class ImageDBService {
} }
return true; return true;
} }
private hash(image: Buffer): string {
return Crypto.createHash('sha256').update(image).digest('hex');
}
} }

View File

@@ -19,16 +19,16 @@ export class ImageManagerService {
private readonly processService: ImageProcessorService, private readonly processService: ImageProcessorService,
) {} ) {}
public async retrieveInfo(hash: string): AsyncFailable<EImageBackend> { public async retrieveInfo(id: string): AsyncFailable<EImageBackend> {
return await this.imagesService.findOne(hash); return await this.imagesService.findOne(id);
} }
// Image data buffer is not included by default, this also returns that buffer // Image data buffer is not included by default, this also returns that buffer
// Dont send to client, keep in backend // Dont send to client, keep in backend
public async retrieveComplete( public async retrieveComplete(
hash: string, id: string,
): AsyncFailable<Required<EImageBackend>> { ): AsyncFailable<Required<EImageBackend>> {
return await this.imagesService.findOne(hash, true); return await this.imagesService.findOne(id, true);
} }
public async upload( public async upload(

View File

@@ -1,5 +1,5 @@
import { EImageSchema } from 'picsur-shared/dist/entities/image.entity'; import { EImageSchema } from 'picsur-shared/dist/entities/image.entity';
import { Column, Entity, Index, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
import { z } from 'zod'; import { z } from 'zod';
const OverriddenEImageSchema = EImageSchema.omit({ data: true }).merge( const OverriddenEImageSchema = EImageSchema.omit({ data: true }).merge(
@@ -12,11 +12,7 @@ type OverriddenEImage = z.infer<typeof OverriddenEImageSchema>;
@Entity() @Entity()
export class EImageBackend implements OverriddenEImage { export class EImageBackend implements OverriddenEImage {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id?: string; id: string;
@Index()
@Column({ unique: true, nullable: false })
hash: string;
@Column({ nullable: false }) @Column({ nullable: false })
mime: string; mime: string;
@@ -24,4 +20,7 @@ export class EImageBackend implements OverriddenEImage {
// Binary data // Binary data
@Column({ type: 'bytea', nullable: false, select: false }) @Column({ type: 'bytea', nullable: false, select: false })
data?: Buffer; data?: Buffer;
@Column({ type: 'bytea', nullable: true, select: false })
originaldata?: Buffer;
} }

View File

@@ -4,13 +4,12 @@ import {
Injectable, Injectable,
PipeTransform PipeTransform
} from '@nestjs/common'; } from '@nestjs/common';
import { SHA256Regex } from 'picsur-shared/dist/util/common-regex'; import { UUIDRegex } from 'picsur-shared/dist/util/common-regex';
@Injectable() @Injectable()
export class ImageIdValidator implements PipeTransform<string, string> { export class ImageIdValidator implements PipeTransform<string, string> {
transform(value: string, metadata: ArgumentMetadata): string { transform(value: string, metadata: ArgumentMetadata): string {
// Check regex for sha256 if (UUIDRegex.test(value)) return value;
if (SHA256Regex.test(value)) return value;
throw new BadRequestException('Invalid image id'); throw new BadRequestException('Invalid image id');
} }
} }

View File

@@ -30,14 +30,14 @@ export class ImageController {
constructor(private readonly imagesService: ImageManagerService) {} constructor(private readonly imagesService: ImageManagerService) {}
@Get(':hash') @Get(':id')
async getImage( async getImage(
// Usually passthrough is for manually sending the response, // Usually passthrough is for manually sending the response,
// But we need it here to set the mime type // But we need it here to set the mime type
@Res({ passthrough: true }) res: FastifyReply, @Res({ passthrough: true }) res: FastifyReply,
@Param('hash', ImageIdValidator) hash: string, @Param('id', ImageIdValidator) id: string,
): Promise<Buffer> { ): Promise<Buffer> {
const image = await this.imagesService.retrieveComplete(hash); const image = await this.imagesService.retrieveComplete(id);
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');
@@ -47,12 +47,12 @@ export class ImageController {
return image.data; return image.data;
} }
@Head(':hash') @Head(':id')
async headImage( async headImage(
@Res({ passthrough: true }) res: FastifyReply, @Res({ passthrough: true }) res: FastifyReply,
@Param('hash', ImageIdValidator) hash: string, @Param('id', ImageIdValidator) id: string,
) { ) {
const image = await this.imagesService.retrieveInfo(hash); const image = await this.imagesService.retrieveInfo(id);
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');
@@ -61,12 +61,12 @@ export class ImageController {
res.type(image.mime); res.type(image.mime);
} }
@Get('meta/:hash') @Get('meta/:id')
@Returns(ImageMetaResponse) @Returns(ImageMetaResponse)
async getImageMeta( async getImageMeta(
@Param('hash', ImageIdValidator) hash: string, @Param('id', ImageIdValidator) id: string,
): Promise<ImageMetaResponse> { ): Promise<ImageMetaResponse> {
const image = await this.imagesService.retrieveInfo(hash); const image = await this.imagesService.retrieveInfo(id);
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');

View File

@@ -23,11 +23,11 @@ export class ProcessingComponent implements OnInit {
history.replaceState(null, ''); history.replaceState(null, '');
const hash = await this.imageService.UploadImage(state.imageFile); const id = await this.imageService.UploadImage(state.imageFile);
if (HasFailed(hash)) { if (HasFailed(id)) {
return this.utilService.quitError(hash.getReason()); return this.utilService.quitError(id.getReason());
} }
this.router.navigate([`/view/`, hash], { replaceUrl: true }); this.router.navigate([`/view/`, id], { replaceUrl: true });
} }
} }

View File

@@ -2,7 +2,7 @@ import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { ImageLinks } from 'picsur-shared/dist/dto/imagelinks.dto'; import { ImageLinks } from 'picsur-shared/dist/dto/imagelinks.dto';
import { HasFailed } from 'picsur-shared/dist/types'; import { HasFailed } from 'picsur-shared/dist/types';
import { SHA256Regex } from 'picsur-shared/dist/util/common-regex'; import { UUIDRegex } from 'picsur-shared/dist/util/common-regex';
import { ImageService } from 'src/app/services/api/image.service'; import { ImageService } from 'src/app/services/api/image.service';
import { UtilService } from 'src/app/util/util.service'; import { UtilService } from 'src/app/util/util.service';
@@ -22,17 +22,17 @@ export class ViewComponent implements OnInit {
async ngOnInit() { async ngOnInit() {
const params = this.route.snapshot.paramMap; const params = this.route.snapshot.paramMap;
const hash = params.get('hash') ?? ''; const id = params.get('id') ?? '';
if (!SHA256Regex.test(hash)) { if (!UUIDRegex.test(id)) {
return this.utilService.quitError('Invalid image link'); return this.utilService.quitError('Invalid image link');
} }
const metadata = await this.imageService.GetImageMeta(hash); const metadata = await this.imageService.GetImageMeta(id);
if (HasFailed(metadata)) { if (HasFailed(metadata)) {
return this.utilService.quitError(metadata.getReason()); return this.utilService.quitError(metadata.getReason());
} }
this.imageLinks = this.imageService.CreateImageLinksFromID(hash); this.imageLinks = this.imageService.CreateImageLinksFromID(id);
} }
downloadImage() { downloadImage() {

View File

@@ -7,7 +7,7 @@ import { ViewComponent } from './view.component';
const routes: PRoutes = [ const routes: PRoutes = [
{ {
path: ':hash', path: ':id',
component: ViewComponent, component: ViewComponent,
canActivate: [PermissionGuard], canActivate: [PermissionGuard],
data: { permissions: [Permission.ImageView] }, data: { permissions: [Permission.ImageView] },

View File

@@ -19,7 +19,7 @@ export class ImageService {
new ImageUploadRequest(image) new ImageUploadRequest(image)
); );
return Open(result, 'hash'); return Open(result, 'id');
} }
public async GetImageMeta(image: string): AsyncFailable<EImage> { public async GetImageMeta(image: string): AsyncFailable<EImage> {

View File

@@ -1,10 +1,8 @@
import { z } from 'zod'; import { z } from 'zod';
import { SHA256Regex } from '../util/common-regex';
import { IsEntityID } from '../validators/entity-id.validator'; import { IsEntityID } from '../validators/entity-id.validator';
export const EImageSchema = z.object({ export const EImageSchema = z.object({
id: IsEntityID().optional(), id: IsEntityID(),
hash: z.string().regex(SHA256Regex),
data: z.undefined(), data: z.undefined(),
mime: z.string(), mime: z.string(),
}); });