link image with user

This commit is contained in:
rubikscraft
2022-05-02 15:03:01 +02:00
parent 852391c3ce
commit 71beb9f41c
18 changed files with 110 additions and 34 deletions

View File

@@ -19,11 +19,13 @@ export class ImageDBService {
private imageDerivativeRepo: Repository<EImageDerivativeBackend>, private imageDerivativeRepo: Repository<EImageDerivativeBackend>,
) {} ) {}
public async create(): AsyncFailable<EImageBackend> { public async create(userid: string): AsyncFailable<EImageBackend> {
let imageEntity = new EImageBackend(); let imageEntity = new EImageBackend();
imageEntity.user_id = userid;
imageEntity.created = new Date();
try { try {
imageEntity = await this.imageRepo.save(imageEntity); imageEntity = await this.imageRepo.save(imageEntity, { reload: true });
} catch (e) { } catch (e) {
return Fail(e); return Fail(e);
} }

View File

@@ -86,10 +86,7 @@ export class ImageFileDBService {
if (!found) return Fail('Image not found'); if (!found) return Fail('Image not found');
return Object.fromEntries( return Object.fromEntries(
types.map((type) => [ types.map((type) => [type, found.find((f) => f.type === type)?.mime]),
type,
found.find((f) => f.type === type)?.mime,
]),
); );
} catch (e) { } catch (e) {
return Fail(e); return Fail(e);
@@ -107,7 +104,7 @@ export class ImageFileDBService {
imageDerivative.key = key; imageDerivative.key = key;
imageDerivative.mime = mime; imageDerivative.mime = mime;
imageDerivative.data = file; imageDerivative.data = file;
imageDerivative.last_read_unix_sec = Math.floor(Date.now() / 1000); imageDerivative.last_read = new Date();
try { try {
return await this.imageDerivativeRepo.save(imageDerivative); return await this.imageDerivativeRepo.save(imageDerivative);
@@ -126,9 +123,9 @@ export class ImageFileDBService {
}); });
if (!derivative) return null; if (!derivative) return null;
const unix_seconds = Math.floor(Date.now() / 1000); const yesterday = new Date(Date.now() - A_DAY_IN_SECONDS * 1000);
if (derivative.last_read_unix_sec > unix_seconds - A_DAY_IN_SECONDS) { if (derivative.last_read > yesterday) {
derivative.last_read_unix_sec = unix_seconds; derivative.last_read = new Date();
return await this.imageDerivativeRepo.save(derivative); return await this.imageDerivativeRepo.save(derivative);
} }
@@ -159,9 +156,8 @@ export class ImageFileDBService {
olderThanSeconds: number, olderThanSeconds: number,
): AsyncFailable<number> { ): AsyncFailable<number> {
try { try {
const unix_seconds = Math.floor(Date.now() / 1000);
const result = await this.imageDerivativeRepo.delete({ const result = await this.imageDerivativeRepo.delete({
last_read_unix_sec: LessThan(unix_seconds - olderThanSeconds), last_read: LessThan(new Date()),
}); });
return result.affected ?? 0; return result.affected ?? 0;

View File

@@ -66,7 +66,7 @@ export class SuccessInterceptor<T> implements NestInterceptor {
const parseResult = schema.safeParse(data); const parseResult = schema.safeParse(data);
if (!parseResult.success) { if (!parseResult.success) {
this.logger.warn( this.logger.warn(
`Function ${context.getHandler().name} failed validation`, `Function ${context.getHandler().name} failed validation: ${parseResult.error}`,
); );
throw new InternalServerErrorException( throw new InternalServerErrorException(
'Server produced invalid response', 'Server produced invalid response',

View File

@@ -37,12 +37,6 @@ export class ImageManagerService {
return await this.imagesService.findOne(id); 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( public async upload(
image: Buffer, image: Buffer,
userid: string, userid: string,
@@ -62,7 +56,7 @@ export class ImageManagerService {
if (HasFailed(processResult)) return processResult; if (HasFailed(processResult)) return processResult;
// Save processed to db // Save processed to db
const imageEntity = await this.imagesService.create(); const imageEntity = await this.imagesService.create(userid);
if (HasFailed(imageEntity)) return imageEntity; if (HasFailed(imageEntity)) return imageEntity;
const imageFileEntity = await this.imageFilesService.setFile( const imageFileEntity = await this.imageFilesService.setFile(

View File

@@ -18,7 +18,7 @@ export class EImageDerivativeBackend {
mime: string; mime: string;
@Column({ name: 'last_read', nullable: false }) @Column({ name: 'last_read', nullable: false })
last_read_unix_sec: number; last_read: Date;
// Binary data // Binary data
@Column({ type: 'bytea', nullable: false }) @Column({ type: 'bytea', nullable: false })

View File

@@ -1,8 +1,18 @@
import { EImage } from 'picsur-shared/dist/entities/image.entity'; import { EImage } from 'picsur-shared/dist/entities/image.entity';
import { Entity, PrimaryGeneratedColumn } from 'typeorm'; import { Column, Entity, PrimaryGeneratedColumn } from 'typeorm';
@Entity() @Entity()
export class EImageBackend implements EImage { export class EImageBackend implements EImage {
@PrimaryGeneratedColumn('uuid') @PrimaryGeneratedColumn('uuid')
id: string; id: string;
@Column({
nullable: false,
})
user_id: string;
@Column({
nullable: false
})
created: Date;
} }

View File

@@ -13,7 +13,7 @@ type OverriddenEUser = z.infer<typeof OverriddenEUserSchema>;
@Entity() @Entity()
export class EUserBackend implements OverriddenEUser { export class EUserBackend implements OverriddenEUser {
@PrimaryGeneratedColumn('uuid', {}) @PrimaryGeneratedColumn('uuid', {})
id?: string; id: string;
@Index() @Index()
@Column({ nullable: false, unique: true }) @Column({ nullable: false, unique: true })

View File

@@ -7,15 +7,16 @@ import {
NotFoundException, NotFoundException,
Post, Post,
Query, Query,
Res Res,
} from '@nestjs/common'; } from '@nestjs/common';
import { FastifyReply } from 'fastify'; import { FastifyReply } from 'fastify';
import { import {
ImageMetaResponse, ImageMetaResponse,
ImageRequestParams, ImageRequestParams,
ImageUploadResponse ImageUploadResponse,
} from 'picsur-shared/dist/dto/api/image.dto'; } from 'picsur-shared/dist/dto/api/image.dto';
import { HasFailed } from 'picsur-shared/dist/types'; 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 { ImageFullIdParam } from '../../decorators/image-id/image-full-id.decorator';
import { ImageIdParam } from '../../decorators/image-id/image-id.decorator'; import { ImageIdParam } from '../../decorators/image-id/image-id.decorator';
import { MultiPart } from '../../decorators/multipart/multipart.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 { ImageFullId } from '../../models/constants/image-full-id.const';
import { Permission } from '../../models/constants/permissions.const'; import { Permission } from '../../models/constants/permissions.const';
import { ImageUploadDto } from '../../models/dto/image-upload.dto'; import { ImageUploadDto } from '../../models/dto/image-upload.dto';
import { EUserBackend2EUser } from '../../models/transformers/user.transformer';
// This is the only controller with CORS enabled // This is the only controller with CORS enabled
@Controller('i') @Controller('i')
@@ -33,7 +35,10 @@ import { ImageUploadDto } from '../../models/dto/image-upload.dto';
export class ImageController { export class ImageController {
private readonly logger = new Logger('ImageController'); private readonly logger = new Logger('ImageController');
constructor(private readonly imagesService: ImageManagerService) {} constructor(
private readonly imagesService: ImageManagerService,
private readonly userService: UsersService,
) {}
@Get(':id') @Get(':id')
async getImage( async getImage(
@@ -96,13 +101,20 @@ export class ImageController {
throw new NotFoundException('Could not find image'); 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)) { if (HasFailed(fileMimes)) {
this.logger.warn(fileMimes.getReason()); this.logger.warn(fileMimes.getReason());
throw new InternalServerErrorException('Could not get image mime'); 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() @Post()

View File

@@ -1,10 +1,11 @@
import { Module } from '@nestjs/common'; import { Module } from '@nestjs/common';
import { UsersModule } from '../../collections/user-db/user-db.module';
import { DecoratorsModule } from '../../decorators/decorators.module'; import { DecoratorsModule } from '../../decorators/decorators.module';
import { ImageManagerModule } from '../../managers/image/image.module'; import { ImageManagerModule } from '../../managers/image/image.module';
import { ImageController } from './image.controller'; import { ImageController } from './image.controller';
@Module({ @Module({
imports: [ImageManagerModule, DecoratorsModule], imports: [ImageManagerModule, UsersModule, DecoratorsModule],
controllers: [ImageController], controllers: [ImageController],
}) })
export class ImageModule {} export class ImageModule {}

View File

@@ -27,6 +27,7 @@
"bootstrap": "^5.1.3", "bootstrap": "^5.1.3",
"fuse.js": "^6.5.3", "fuse.js": "^6.5.3",
"jwt-decode": "^3.1.2", "jwt-decode": "^3.1.2",
"moment": "^2.29.3",
"ngx-auto-unsubscribe-decorator": "^1.1.0", "ngx-auto-unsubscribe-decorator": "^1.1.0",
"ngx-dropzone": "^3.1.0", "ngx-dropzone": "^3.1.0",
"picsur-shared": "*", "picsur-shared": "*",

View File

@@ -1,7 +1,11 @@
<div class="container centered"> <div class="container centered">
<div class="row"> <div class="row">
<div class="col-12"> <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>
<div class="col-12 py-3"> <div class="col-12 py-3">

View File

@@ -1,5 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import moment from 'moment';
import { ImageLinks } from 'picsur-shared/dist/dto/image-links.dto'; import { ImageLinks } from 'picsur-shared/dist/dto/image-links.dto';
import { import {
AnimMime, AnimMime,
@@ -10,10 +11,13 @@ import {
SupportedImageMimes, SupportedImageMimes,
SupportedMimeCategory SupportedMimeCategory
} from 'picsur-shared/dist/dto/mimes.dto'; } 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 { HasFailed, HasSuccess } from 'picsur-shared/dist/types';
import { UUIDRegex } from 'picsur-shared/dist/util/common-regex'; import { UUIDRegex } from 'picsur-shared/dist/util/common-regex';
import { ParseMime } from 'picsur-shared/dist/util/parse-mime'; import { ParseMime } from 'picsur-shared/dist/util/parse-mime';
import { ImageService } from 'src/app/services/api/image.service'; 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'; import { UtilService } from 'src/app/util/util-module/util.service';
@Component({ @Component({
@@ -45,6 +49,14 @@ export class ViewComponent implements OnInit {
public previewLink = ''; public previewLink = '';
public imageLinks = new ImageLinks(); 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() { async ngOnInit() {
const params = this.route.snapshot.paramMap; const params = this.route.snapshot.paramMap;
@@ -64,6 +76,9 @@ export class ViewComponent implements OnInit {
this.hasOriginal = metadata.fileMimes.original !== undefined; this.hasOriginal = metadata.fileMimes.original !== undefined;
this.imageUser = metadata.user;
this.image = metadata.image;
const masterMime = ParseMime(metadata.fileMimes.master); const masterMime = ParseMime(metadata.fileMimes.master);
if (HasSuccess(masterMime)) { if (HasSuccess(masterMime)) {
this.masterMime = masterMime; this.masterMime = masterMime;

View 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()));
}

View File

@@ -9,4 +9,4 @@ if (Environment.production) {
platformBrowserDynamic() platformBrowserDynamic()
.bootstrapModule(AppModule) .bootstrapModule(AppModule)
.catch((err) => console.error(err)); .catch(console.error);

View File

@@ -1,5 +1,6 @@
import { z } from 'zod'; import { z } from 'zod';
import { EImageSchema } from '../../entities/image.entity'; import { EImageSchema } from '../../entities/image.entity';
import { EUserSchema } from '../../entities/user.entity';
import { createZodDto } from '../../util/create-zod-dto'; import { createZodDto } from '../../util/create-zod-dto';
import { ImageFileType } from '../image-file-types.dto'; import { ImageFileType } from '../image-file-types.dto';
@@ -32,6 +33,7 @@ export class ImageRequestParams extends createZodDto(
export const ImageMetaResponseSchema = z.object({ export const ImageMetaResponseSchema = z.object({
image: EImageSchema, image: EImageSchema,
user: EUserSchema,
fileMimes: z.object({ fileMimes: z.object({
[ImageFileType.MASTER]: z.string(), [ImageFileType.MASTER]: z.string(),
[ImageFileType.ORIGINAL]: z.union([z.string(), z.undefined()]), [ImageFileType.ORIGINAL]: z.union([z.string(), z.undefined()]),

View File

@@ -3,5 +3,7 @@ import { IsEntityID } from '../validators/entity-id.validator';
export const EImageSchema = z.object({ export const EImageSchema = z.object({
id: IsEntityID(), id: IsEntityID(),
user_id: z.string(),
created: z.preprocess((data: any) => new Date(data), z.date()),
}); });
export type EImage = z.infer<typeof EImageSchema>; export type EImage = z.infer<typeof EImageSchema>;

View File

@@ -11,7 +11,7 @@ export const SimpleUserSchema = z.object({
export type SimpleUser = z.infer<typeof SimpleUserSchema>; export type SimpleUser = z.infer<typeof SimpleUserSchema>;
export const EUserSchema = z.object({ export const EUserSchema = z.object({
id: IsEntityID().optional(), id: IsEntityID(),
username: IsUsername(), username: IsUsername(),
roles: IsStringList(), roles: IsStringList(),
hashedPassword: z.undefined(), hashedPassword: z.undefined(),

View File

@@ -1331,7 +1331,8 @@
secure-json-parse "^2.4.0" secure-json-parse "^2.4.0"
stream-wormhole "^1.1.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" version "5.0.1"
resolved "https://registry.yarnpkg.com/@fastify/static/-/static-5.0.1.tgz#6bb6b94c2b51d9fafb06261b79ed5da5038dd5a1" resolved "https://registry.yarnpkg.com/@fastify/static/-/static-5.0.1.tgz#6bb6b94c2b51d9fafb06261b79ed5da5038dd5a1"
integrity sha512-0lReUKWVOt2i5i1KoBLYsbcMBthq5eiEoUQrFceoXJkCv+OyA0iLl6hGxcmnMRxLsl/Netrzt1NNOpg4muCcuw== 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" resolved "https://registry.yarnpkg.com/fastify-plugin/-/fastify-plugin-3.0.1.tgz#79e84c29f401020f38b524f59f2402103fd21ed2"
integrity sha512-qKcDXmuZadJqdTm6vlCqioEbyewF60b/0LOFCcYN1B6BIZGlYJumWWOYs70SFYLDAH4YqdE1cxH/RKMG7rFxgA== 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: fastify@3.28.0:
version "3.28.0" version "3.28.0"
resolved "https://registry.yarnpkg.com/fastify/-/fastify-3.28.0.tgz#14d939a2f176b82af1094de7abcb0b2d83bcff8f" resolved "https://registry.yarnpkg.com/fastify/-/fastify-3.28.0.tgz#14d939a2f176b82af1094de7abcb0b2d83bcff8f"
@@ -5333,7 +5355,12 @@ minimatch@^3.0.4:
dependencies: dependencies:
brace-expansion "^1.1.7" 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" version "2.2.1"
resolved "https://registry.yarnpkg.com/minimist-lite/-/minimist-lite-2.2.1.tgz#abb71db2c9b454d7cf4496868c03e9802de9934d" resolved "https://registry.yarnpkg.com/minimist-lite/-/minimist-lite-2.2.1.tgz#abb71db2c9b454d7cf4496868c03e9802de9934d"
integrity sha512-RSrWIRWGYoM2TDe102s7aIyeSipXMIXKb1fSHYx1tAbxAV0z4g2xR6ra3oPzkTqFb0EIUz1H3A/qvYYeDd+/qQ== 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" resolved "https://registry.yarnpkg.com/mkdirp/-/mkdirp-1.0.4.tgz#3eb5ed62622756d79a5f0e2a221dfebad75c2f7e"
integrity sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw== 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: ms@2.0.0:
version "2.0.0" version "2.0.0"
resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8" resolved "https://registry.yarnpkg.com/ms/-/ms-2.0.0.tgz#5608aeadfc00be6c2901df5f9861788de0d597c8"