mirror of
https://github.com/CaramelFur/Picsur.git
synced 2025-11-12 23:05:39 +01:00
link image with user
This commit is contained in:
@@ -19,11 +19,13 @@ export class ImageDBService {
|
||||
private imageDerivativeRepo: Repository<EImageDerivativeBackend>,
|
||||
) {}
|
||||
|
||||
public async create(): AsyncFailable<EImageBackend> {
|
||||
public async create(userid: string): AsyncFailable<EImageBackend> {
|
||||
let imageEntity = new EImageBackend();
|
||||
imageEntity.user_id = userid;
|
||||
imageEntity.created = new Date();
|
||||
|
||||
try {
|
||||
imageEntity = await this.imageRepo.save(imageEntity);
|
||||
imageEntity = await this.imageRepo.save(imageEntity, { reload: true });
|
||||
} catch (e) {
|
||||
return Fail(e);
|
||||
}
|
||||
|
||||
@@ -86,10 +86,7 @@ export class ImageFileDBService {
|
||||
|
||||
if (!found) return Fail('Image not found');
|
||||
return Object.fromEntries(
|
||||
types.map((type) => [
|
||||
type,
|
||||
found.find((f) => f.type === type)?.mime,
|
||||
]),
|
||||
types.map((type) => [type, found.find((f) => f.type === type)?.mime]),
|
||||
);
|
||||
} catch (e) {
|
||||
return Fail(e);
|
||||
@@ -107,7 +104,7 @@ export class ImageFileDBService {
|
||||
imageDerivative.key = key;
|
||||
imageDerivative.mime = mime;
|
||||
imageDerivative.data = file;
|
||||
imageDerivative.last_read_unix_sec = Math.floor(Date.now() / 1000);
|
||||
imageDerivative.last_read = new Date();
|
||||
|
||||
try {
|
||||
return await this.imageDerivativeRepo.save(imageDerivative);
|
||||
@@ -126,9 +123,9 @@ export class ImageFileDBService {
|
||||
});
|
||||
if (!derivative) return null;
|
||||
|
||||
const unix_seconds = Math.floor(Date.now() / 1000);
|
||||
if (derivative.last_read_unix_sec > unix_seconds - A_DAY_IN_SECONDS) {
|
||||
derivative.last_read_unix_sec = unix_seconds;
|
||||
const yesterday = new Date(Date.now() - A_DAY_IN_SECONDS * 1000);
|
||||
if (derivative.last_read > yesterday) {
|
||||
derivative.last_read = new Date();
|
||||
return await this.imageDerivativeRepo.save(derivative);
|
||||
}
|
||||
|
||||
@@ -159,9 +156,8 @@ export class ImageFileDBService {
|
||||
olderThanSeconds: number,
|
||||
): AsyncFailable<number> {
|
||||
try {
|
||||
const unix_seconds = Math.floor(Date.now() / 1000);
|
||||
const result = await this.imageDerivativeRepo.delete({
|
||||
last_read_unix_sec: LessThan(unix_seconds - olderThanSeconds),
|
||||
last_read: LessThan(new Date()),
|
||||
});
|
||||
|
||||
return result.affected ?? 0;
|
||||
|
||||
@@ -66,7 +66,7 @@ export class SuccessInterceptor<T> implements NestInterceptor {
|
||||
const parseResult = schema.safeParse(data);
|
||||
if (!parseResult.success) {
|
||||
this.logger.warn(
|
||||
`Function ${context.getHandler().name} failed validation`,
|
||||
`Function ${context.getHandler().name} failed validation: ${parseResult.error}`,
|
||||
);
|
||||
throw new InternalServerErrorException(
|
||||
'Server produced invalid response',
|
||||
|
||||
@@ -37,12 +37,6 @@ export class ImageManagerService {
|
||||
return await this.imagesService.findOne(id);
|
||||
}
|
||||
|
||||
// Image data buffer is not included by default, this also returns that buffer
|
||||
// Dont send to client, keep in backend
|
||||
public async retrieveComplete(id: string): AsyncFailable<EImageBackend> {
|
||||
return await this.imagesService.findOne(id);
|
||||
}
|
||||
|
||||
public async upload(
|
||||
image: Buffer,
|
||||
userid: string,
|
||||
@@ -62,7 +56,7 @@ export class ImageManagerService {
|
||||
if (HasFailed(processResult)) return processResult;
|
||||
|
||||
// Save processed to db
|
||||
const imageEntity = await this.imagesService.create();
|
||||
const imageEntity = await this.imagesService.create(userid);
|
||||
if (HasFailed(imageEntity)) return imageEntity;
|
||||
|
||||
const imageFileEntity = await this.imageFilesService.setFile(
|
||||
|
||||
@@ -18,7 +18,7 @@ export class EImageDerivativeBackend {
|
||||
mime: string;
|
||||
|
||||
@Column({ name: 'last_read', nullable: false })
|
||||
last_read_unix_sec: number;
|
||||
last_read: Date;
|
||||
|
||||
// Binary data
|
||||
@Column({ type: 'bytea', nullable: false })
|
||||
|
||||
@@ -1,8 +1,18 @@
|
||||
import { EImage } from 'picsur-shared/dist/entities/image.entity';
|
||||
import { Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
|
||||
|
||||
@Entity()
|
||||
export class EImageBackend implements EImage {
|
||||
@PrimaryGeneratedColumn('uuid')
|
||||
id: string;
|
||||
|
||||
@Column({
|
||||
nullable: false,
|
||||
})
|
||||
user_id: string;
|
||||
|
||||
@Column({
|
||||
nullable: false
|
||||
})
|
||||
created: Date;
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ type OverriddenEUser = z.infer<typeof OverriddenEUserSchema>;
|
||||
@Entity()
|
||||
export class EUserBackend implements OverriddenEUser {
|
||||
@PrimaryGeneratedColumn('uuid', {})
|
||||
id?: string;
|
||||
id: string;
|
||||
|
||||
@Index()
|
||||
@Column({ nullable: false, unique: true })
|
||||
|
||||
@@ -7,15 +7,16 @@ import {
|
||||
NotFoundException,
|
||||
Post,
|
||||
Query,
|
||||
Res
|
||||
Res,
|
||||
} from '@nestjs/common';
|
||||
import { FastifyReply } from 'fastify';
|
||||
import {
|
||||
ImageMetaResponse,
|
||||
ImageRequestParams,
|
||||
ImageUploadResponse
|
||||
ImageUploadResponse,
|
||||
} from 'picsur-shared/dist/dto/api/image.dto';
|
||||
import { HasFailed } from 'picsur-shared/dist/types';
|
||||
import { UsersService } from '../../collections/user-db/user-db.service';
|
||||
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';
|
||||
@@ -26,6 +27,7 @@ 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';
|
||||
import { EUserBackend2EUser } from '../../models/transformers/user.transformer';
|
||||
|
||||
// This is the only controller with CORS enabled
|
||||
@Controller('i')
|
||||
@@ -33,7 +35,10 @@ import { ImageUploadDto } from '../../models/dto/image-upload.dto';
|
||||
export class ImageController {
|
||||
private readonly logger = new Logger('ImageController');
|
||||
|
||||
constructor(private readonly imagesService: ImageManagerService) {}
|
||||
constructor(
|
||||
private readonly imagesService: ImageManagerService,
|
||||
private readonly userService: UsersService,
|
||||
) {}
|
||||
|
||||
@Get(':id')
|
||||
async getImage(
|
||||
@@ -96,13 +101,20 @@ export class ImageController {
|
||||
throw new NotFoundException('Could not find image');
|
||||
}
|
||||
|
||||
const fileMimes = await this.imagesService.getAllFileMimes(id);
|
||||
const [fileMimes, imageUser] = await Promise.all([
|
||||
this.imagesService.getAllFileMimes(id),
|
||||
this.userService.findOne(image.user_id),
|
||||
]);
|
||||
if (HasFailed(fileMimes)) {
|
||||
this.logger.warn(fileMimes.getReason());
|
||||
throw new InternalServerErrorException('Could not get image mime');
|
||||
}
|
||||
if (HasFailed(imageUser)) {
|
||||
this.logger.warn(imageUser.getReason());
|
||||
throw new InternalServerErrorException('Could not get image user');
|
||||
}
|
||||
|
||||
return { image, fileMimes };
|
||||
return { image, user: EUserBackend2EUser(imageUser), fileMimes };
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { Module } from '@nestjs/common';
|
||||
import { UsersModule } from '../../collections/user-db/user-db.module';
|
||||
import { DecoratorsModule } from '../../decorators/decorators.module';
|
||||
import { ImageManagerModule } from '../../managers/image/image.module';
|
||||
import { ImageController } from './image.controller';
|
||||
|
||||
@Module({
|
||||
imports: [ImageManagerModule, DecoratorsModule],
|
||||
imports: [ImageManagerModule, UsersModule, DecoratorsModule],
|
||||
controllers: [ImageController],
|
||||
})
|
||||
export class ImageModule {}
|
||||
|
||||
@@ -27,6 +27,7 @@
|
||||
"bootstrap": "^5.1.3",
|
||||
"fuse.js": "^6.5.3",
|
||||
"jwt-decode": "^3.1.2",
|
||||
"moment": "^2.29.3",
|
||||
"ngx-auto-unsubscribe-decorator": "^1.1.0",
|
||||
"ngx-dropzone": "^3.1.0",
|
||||
"picsur-shared": "*",
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
<div class="container centered">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h1>Uploaded Image</h1>
|
||||
<h1>Image uploaded by {{imageUser?.username}}</h1>
|
||||
</div>
|
||||
|
||||
<div class="col-12" *ngIf="image !== null">
|
||||
<h3>{{(timeAgo | async)}}</h3>
|
||||
</div>
|
||||
|
||||
<div class="col-12 py-3">
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import moment from 'moment';
|
||||
import { ImageLinks } from 'picsur-shared/dist/dto/image-links.dto';
|
||||
import {
|
||||
AnimMime,
|
||||
@@ -10,10 +11,13 @@ import {
|
||||
SupportedImageMimes,
|
||||
SupportedMimeCategory
|
||||
} from 'picsur-shared/dist/dto/mimes.dto';
|
||||
import { EImage } from 'picsur-shared/dist/entities/image.entity';
|
||||
import { EUser } from 'picsur-shared/dist/entities/user.entity';
|
||||
import { HasFailed, HasSuccess } from 'picsur-shared/dist/types';
|
||||
import { UUIDRegex } from 'picsur-shared/dist/util/common-regex';
|
||||
import { ParseMime } from 'picsur-shared/dist/util/parse-mime';
|
||||
import { ImageService } from 'src/app/services/api/image.service';
|
||||
import { rxjs_poll } from 'src/app/util/poll';
|
||||
import { UtilService } from 'src/app/util/util-module/util.service';
|
||||
|
||||
@Component({
|
||||
@@ -45,6 +49,14 @@ export class ViewComponent implements OnInit {
|
||||
public previewLink = '';
|
||||
public imageLinks = new ImageLinks();
|
||||
|
||||
public image: EImage | null = null;
|
||||
public imageUser: EUser | null = null;
|
||||
|
||||
public timeAgo = rxjs_poll(
|
||||
1000,
|
||||
(() => moment(this.image?.created).fromNow()).bind(this)
|
||||
);
|
||||
|
||||
async ngOnInit() {
|
||||
const params = this.route.snapshot.paramMap;
|
||||
|
||||
@@ -64,6 +76,9 @@ export class ViewComponent implements OnInit {
|
||||
|
||||
this.hasOriginal = metadata.fileMimes.original !== undefined;
|
||||
|
||||
this.imageUser = metadata.user;
|
||||
this.image = metadata.image;
|
||||
|
||||
const masterMime = ParseMime(metadata.fileMimes.master);
|
||||
if (HasSuccess(masterMime)) {
|
||||
this.masterMime = masterMime;
|
||||
|
||||
5
frontend/src/app/util/poll.ts
Normal file
5
frontend/src/app/util/poll.ts
Normal file
@@ -0,0 +1,5 @@
|
||||
import { map, Observable, timer } from 'rxjs';
|
||||
|
||||
export function rxjs_poll<T>(period: number, action: () => T): Observable<T> {
|
||||
return timer(0, 1000).pipe(map(() => action()));
|
||||
}
|
||||
@@ -9,4 +9,4 @@ if (Environment.production) {
|
||||
|
||||
platformBrowserDynamic()
|
||||
.bootstrapModule(AppModule)
|
||||
.catch((err) => console.error(err));
|
||||
.catch(console.error);
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { z } from 'zod';
|
||||
import { EImageSchema } from '../../entities/image.entity';
|
||||
import { EUserSchema } from '../../entities/user.entity';
|
||||
import { createZodDto } from '../../util/create-zod-dto';
|
||||
import { ImageFileType } from '../image-file-types.dto';
|
||||
|
||||
@@ -32,6 +33,7 @@ export class ImageRequestParams extends createZodDto(
|
||||
|
||||
export const ImageMetaResponseSchema = z.object({
|
||||
image: EImageSchema,
|
||||
user: EUserSchema,
|
||||
fileMimes: z.object({
|
||||
[ImageFileType.MASTER]: z.string(),
|
||||
[ImageFileType.ORIGINAL]: z.union([z.string(), z.undefined()]),
|
||||
|
||||
@@ -3,5 +3,7 @@ import { IsEntityID } from '../validators/entity-id.validator';
|
||||
|
||||
export const EImageSchema = z.object({
|
||||
id: IsEntityID(),
|
||||
user_id: z.string(),
|
||||
created: z.preprocess((data: any) => new Date(data), z.date()),
|
||||
});
|
||||
export type EImage = z.infer<typeof EImageSchema>;
|
||||
|
||||
@@ -11,7 +11,7 @@ export const SimpleUserSchema = z.object({
|
||||
export type SimpleUser = z.infer<typeof SimpleUserSchema>;
|
||||
|
||||
export const EUserSchema = z.object({
|
||||
id: IsEntityID().optional(),
|
||||
id: IsEntityID(),
|
||||
username: IsUsername(),
|
||||
roles: IsStringList(),
|
||||
hashedPassword: z.undefined(),
|
||||
|
||||
36
yarn.lock
36
yarn.lock
@@ -1331,7 +1331,8 @@
|
||||
secure-json-parse "^2.4.0"
|
||||
stream-wormhole "^1.1.0"
|
||||
|
||||
"@fastify/static@^5.0.0", fastify-static@^4.7.0, "fastify-static@npm:@fastify/static":
|
||||
"@fastify/static@^5.0.0", "fastify-static@npm:@fastify/static":
|
||||
name fastify-static
|
||||
version "5.0.1"
|
||||
resolved "https://registry.yarnpkg.com/@fastify/static/-/static-5.0.1.tgz#6bb6b94c2b51d9fafb06261b79ed5da5038dd5a1"
|
||||
integrity sha512-0lReUKWVOt2i5i1KoBLYsbcMBthq5eiEoUQrFceoXJkCv+OyA0iLl6hGxcmnMRxLsl/Netrzt1NNOpg4muCcuw==
|
||||
@@ -3957,6 +3958,27 @@ fastify-plugin@^3.0.0:
|
||||
resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-3.0.1.tgz#79e84c29f401020f38b524f59f2402103fd21ed2"
|
||||
integrity sha512-qKcDXmuZadJqdTm6vlCqioEbyewF60b/0LOFCcYN1B6BIZGlYJumWWOYs70SFYLDAH4YqdE1cxH/RKMG7rFxgA==
|
||||
|
||||
"fastify-static-deprecated@npm:fastify-static@4.6.1":
|
||||
version "4.6.1"
|
||||
resolved "https://registry.yarnpkg.com/fastify-static/-/fastify-static-4.6.1.tgz#687131da76f1d4391fb8b47f71ea2118cdc85803"
|
||||
integrity sha512-vy7N28U4AMhuOim12ZZWHulEE6OQKtzZbHgiB8Zj4llUuUQXPka0WHAQI3njm1jTCx4W6fixUHfpITxweMtAIA==
|
||||
dependencies:
|
||||
content-disposition "^0.5.3"
|
||||
encoding-negotiator "^2.0.1"
|
||||
fastify-plugin "^3.0.0"
|
||||
glob "^7.1.4"
|
||||
p-limit "^3.1.0"
|
||||
readable-stream "^3.4.0"
|
||||
send "^0.17.1"
|
||||
|
||||
fastify-static@^4.7.0:
|
||||
version "4.7.0"
|
||||
resolved "https://registry.yarnpkg.com/fastify-static/-/fastify-static-4.7.0.tgz#e802658d69c1dcddb380b9afc2456d467a3494be"
|
||||
integrity sha512-zZhCfJv/hkmud2qhWqpU3K9XVAuy3+IV8Tp9BC5J5U+GyA2XwoB6h8lh9GqpEIqdXOw01WyWQllV7dOWVyAlXg==
|
||||
dependencies:
|
||||
fastify-static-deprecated "npm:fastify-static@4.6.1"
|
||||
process-warning "^1.0.0"
|
||||
|
||||
fastify@3.28.0:
|
||||
version "3.28.0"
|
||||
resolved "https://registry.yarnpkg.com/fastify/-/fastify-3.28.0.tgz#14d939a2f176b82af1094de7abcb0b2d83bcff8f"
|
||||
@@ -5333,7 +5355,12 @@ minimatch@^3.0.4:
|
||||
dependencies:
|
||||
brace-expansion "^1.1.7"
|
||||
|
||||
minimist@1.2.6, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6, "minimist@npm:minimist-lite":
|
||||
minimist@1.2.6, minimist@^1.2.0, minimist@^1.2.3, minimist@^1.2.6:
|
||||
version "1.2.6"
|
||||
resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.6.tgz#8637a5b759ea0d6e98702cfb3a9283323c93af44"
|
||||
integrity sha512-Jsjnk4bw3YJqYzbdyBiNsPWHPfO++UGG749Cxs6peCu5Xg4nrena6OVxOYxrQTqww0Jmwt+Ref8rggumkTLz9Q==
|
||||
|
||||
"minimist@npm:minimist-lite":
|
||||
version "2.2.1"
|
||||
resolved "https://registry.yarnpkg.com/minimist-lite/-/minimist-lite-2.2.1.tgz#abb71db2c9b454d7cf4496868c03e9802de9934d"
|
||||
integrity sha512-RSrWIRWGYoM2TDe102s7aIyeSipXMIXKb1fSHYx1tAbxAV0z4g2xR6ra3oPzkTqFb0EIUz1H3A/qvYYeDd+/qQ==
|
||||
@@ -5417,6 +5444,11 @@ mkdirp@^1.0.3, mkdirp@^1.0.4:
|
||||
resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
|
||||
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==
|
||||
|
||||
moment@^2.29.3:
|
||||
version "2.29.3"
|
||||
resolved "https://registry.yarnpkg.com/moment/-/moment-2.29.3.tgz#edd47411c322413999f7a5940d526de183c031f3"
|
||||
integrity sha512-c6YRvhEo//6T2Jz/vVtYzqBzwvPT95JBQ+smCytzf7c50oMZRsR/a4w88aD34I+/QVSfnoAnSBFPJHItlOMJVw==
|
||||
|
||||
ms@2.0.0:
|
||||
version "2.0.0"
|
||||
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"
|
||||
|
||||
Reference in New Issue
Block a user