mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-01 01:45:48 +01:00
change images to use ids instead of hashes
This commit is contained in:
@@ -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');
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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(
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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');
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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() {
|
||||||
|
|||||||
@@ -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] },
|
||||||
|
|||||||
@@ -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> {
|
||||||
|
|||||||
@@ -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(),
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user