diff --git a/backend/src/config/early/type-orm.config.service.ts b/backend/src/config/early/type-orm.config.service.ts index 8a639ee..da8e9b2 100644 --- a/backend/src/config/early/type-orm.config.service.ts +++ b/backend/src/config/early/type-orm.config.service.ts @@ -1,6 +1,6 @@ import { Injectable, Logger } from '@nestjs/common'; import { ConfigService } from '@nestjs/config'; -import { TypeOrmOptionsFactory } from '@nestjs/typeorm'; +import { TypeOrmModuleOptions, TypeOrmOptionsFactory } from '@nestjs/typeorm'; import { ParseInt, ParseString } from 'picsur-shared/dist/util/parse-simple'; import { EntityList } from '../../database/entities'; import { MigrationList } from '../../database/migrations'; @@ -52,19 +52,21 @@ export class TypeOrmConfigService implements TypeOrmOptionsFactory { const varOptions = this.getTypeOrmServerOptions(); return { type: 'postgres' as 'postgres', - synchronize: false, //!this.hostService.isProduction(), + synchronize: !this.hostService.isProduction(), migrationsRun: true, entities: EntityList, migrations: MigrationList, + useUTC: true, + cli: { migrationsDir: 'src/database/migrations', entitiesDir: 'src/database/entities', }, ...varOptions, - }; + } as TypeOrmModuleOptions; } } diff --git a/backend/src/database/entities/apikey.entity.ts b/backend/src/database/entities/apikey.entity.ts index c425256..32c1125 100644 --- a/backend/src/database/entities/apikey.entity.ts +++ b/backend/src/database/entities/apikey.entity.ts @@ -41,13 +41,13 @@ export class EApiKeyBackend< name: string; @Column({ - type: 'timestamp', + type: 'timestamptz', nullable: false, }) created: Date; @Column({ - type: 'timestamp', + type: 'timestamptz', nullable: true, }) last_used: Date; diff --git a/backend/src/database/entities/image-derivative.entity.ts b/backend/src/database/entities/image-derivative.entity.ts index d3dfd0d..51ca6d3 100644 --- a/backend/src/database/entities/image-derivative.entity.ts +++ b/backend/src/database/entities/image-derivative.entity.ts @@ -36,7 +36,11 @@ export class EImageDerivativeBackend { @Column({ nullable: false }) filetype: string; - @Column({ type: 'timestamp', name: 'last_read', nullable: false }) + @Column({ + type: 'timestamptz', + name: 'last_read', + nullable: false, + }) last_read: Date; // Binary data diff --git a/backend/src/database/entities/image.entity.ts b/backend/src/database/entities/image.entity.ts index 1fb954d..e28ac67 100644 --- a/backend/src/database/entities/image.entity.ts +++ b/backend/src/database/entities/image.entity.ts @@ -15,7 +15,7 @@ export class EImageBackend implements EImage { user_id: string; @Column({ - type: 'timestamp', + type: 'timestamptz', nullable: false, }) created: Date; @@ -27,7 +27,7 @@ export class EImageBackend implements EImage { file_name: string; @Column({ - type: 'timestamp', + type: "timestamptz", nullable: true, }) expires_at: Date | null; diff --git a/backend/src/database/migrations/1662728275448-V_0_4_0_d.ts b/backend/src/database/migrations/1662728275448-V_0_4_0_d.ts new file mode 100644 index 0000000..f561d79 --- /dev/null +++ b/backend/src/database/migrations/1662728275448-V_0_4_0_d.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; + +export class V040D1662728275448 implements MigrationInterface { + name = 'V040D1662728275448' + + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "e_api_key_backend" ALTER COLUMN "created" SET DATA TYPE TIMESTAMP WITH TIME ZONE, ALTER COLUMN "created" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "e_api_key_backend" ALTER COLUMN "last_used" SET DATA TYPE TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`ALTER TABLE "e_image_backend" ALTER COLUMN "created" SET DATA TYPE TIMESTAMP WITH TIME ZONE, ALTER COLUMN "created" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "e_image_backend" ALTER COLUMN "expires_at" SET DATA TYPE TIMESTAMP WITH TIME ZONE`); + await queryRunner.query(`ALTER TABLE "e_image_derivative_backend" ALTER COLUMN "last_read" SET DATA TYPE TIMESTAMP WITH TIME ZONE, ALTER COLUMN "last_read" SET NOT NULL`); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.query(`ALTER TABLE "e_image_derivative_backend" ALTER COLUMN "last_read" SET DATA TYPE TIMESTAMP, ALTER COLUMN "last_read" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "e_image_backend" ALTER COLUMN "expires_at" SET DATA TYPE TIMESTAMP`); + await queryRunner.query(`ALTER TABLE "e_image_backend" ALTER COLUMN "created" SET DATA TYPE TIMESTAMP, ALTER COLUMN "created" SET NOT NULL`); + await queryRunner.query(`ALTER TABLE "e_api_key_backend" ALTER COLUMN "last_used" SET DATA TYPE TIMESTAMP`); + await queryRunner.query(`ALTER TABLE "e_api_key_backend" ALTER COLUMN "created" SET DATA TYPE TIMESTAMP, ALTER COLUMN "created" SET NOT NULL`); + } + +} diff --git a/backend/src/database/migrations/index.ts b/backend/src/database/migrations/index.ts index f99d4f1..07f9fd8 100644 --- a/backend/src/database/migrations/index.ts +++ b/backend/src/database/migrations/index.ts @@ -3,6 +3,7 @@ import { V032A1662029904716 } from './1662029904716-V_0_3_2_a'; import { V040A1662314197741 } from './1662314197741-V_0_4_0_a'; import { V040B1662485374471 } from './1662485374471-V_0_4_0_b'; import { V040C1662535484200 } from './1662535484200-V_0_4_0_c'; +import { V040D1662728275448 } from './1662728275448-V_0_4_0_d'; export const MigrationList: Function[] = [ V030A1661692206479, @@ -10,4 +11,5 @@ export const MigrationList: Function[] = [ V040A1662314197741, V040B1662485374471, V040C1662535484200, + V040D1662728275448, ]; diff --git a/backend/src/datasource.ts b/backend/src/datasource.ts index 0ab7ffa..60fe2d7 100644 --- a/backend/src/datasource.ts +++ b/backend/src/datasource.ts @@ -17,7 +17,7 @@ async function createDataSource() { const configFactory = app.get(TypeOrmConfigService); const config = await configFactory.createTypeOrmOptions(); - return new DataSource(config); + return new DataSource(config as any); } export default createDataSource(); diff --git a/backend/src/managers/image/image.module.ts b/backend/src/managers/image/image.module.ts index 951c7a5..fafb256 100644 --- a/backend/src/managers/image/image.module.ts +++ b/backend/src/managers/image/image.module.ts @@ -61,7 +61,7 @@ export class ImageManagerModule implements OnModuleInit, OnModuleDestroy { const result = await this.imageFileDB.cleanupDerivatives(after_ms / 1000); if (HasFailed(result)) { - this.logger.warn(`Failed to cleanup derivatives`); + this.logger.warn(result.print()); } this.logger.log(`Cleaned up ${result} derivatives`); @@ -71,7 +71,7 @@ export class ImageManagerModule implements OnModuleInit, OnModuleDestroy { const cleanedUp = await this.imageDB.cleanupExpired(); if (HasFailed(cleanedUp)) { - this.logger.warn(`Failed to cleanup expired images`); + this.logger.warn(cleanedUp.print()); } this.logger.log(`Cleaned up ${cleanedUp} expired images`); diff --git a/backend/src/managers/image/image.service.ts b/backend/src/managers/image/image.service.ts index 23bba17..aafa75e 100644 --- a/backend/src/managers/image/image.service.ts +++ b/backend/src/managers/image/image.service.ts @@ -57,6 +57,11 @@ export class ImageManagerService { userid: string | undefined, options: Partial>, ): AsyncFailable { + if (options.expires_at !== undefined && options.expires_at !== null) { + if (options.expires_at < new Date()) { + return Fail(FT.UsrValidation, 'Expiration date must be in the future'); + } + } return await this.imagesService.update(id, userid, options); } diff --git a/frontend/src/app/routes/images/images.component.html b/frontend/src/app/routes/images/images.component.html index 21eedd5..d638db2 100644 --- a/frontend/src/app/routes/images/images.component.html +++ b/frontend/src/app/routes/images/images.component.html @@ -12,6 +12,11 @@ {{ image.file_name | truncate }} Uploaded {{ image.created | amTimeAgo }} + {{ + image.expires_at === null + ? '' + : '| Expires ' + (image.expires_at | amTimeAgo) + }} { + const list = await this.imageService.ListMyImages(24, this.page - 1); + if (HasFailed(list)) { + return this.logger.error(list.getReason()); + } + + this.pages = list.pages; + this.images = list.results; + }); + } + getThumbnailUrl(image: EImage) { return ( this.imageService.GetImageURL(image.id, ImageFileType.QOI) + diff --git a/frontend/src/app/routes/view/customize-dialog/customize-dialog.component.html b/frontend/src/app/routes/view/customize-dialog/customize-dialog.component.html index 6c2f41e..0193a33 100644 --- a/frontend/src/app/routes/view/customize-dialog/customize-dialog.component.html +++ b/frontend/src/app/routes/view/customize-dialog/customize-dialog.component.html @@ -91,6 +91,6 @@
- +
diff --git a/frontend/src/app/routes/view/edit-dialog/edit-dialog.component.html b/frontend/src/app/routes/view/edit-dialog/edit-dialog.component.html new file mode 100644 index 0000000..39ef3fe --- /dev/null +++ b/frontend/src/app/routes/view/edit-dialog/edit-dialog.component.html @@ -0,0 +1,27 @@ +
+
+
+

Edit Image Properties

+
+
+ + Title + + +
+
+ + Expires After + + + {{ option[0] }} + + + +
+
+
+ + +
+
diff --git a/frontend/src/app/routes/view/edit-dialog/edit-dialog.component.scss b/frontend/src/app/routes/view/edit-dialog/edit-dialog.component.scss new file mode 100644 index 0000000..53a4bc5 --- /dev/null +++ b/frontend/src/app/routes/view/edit-dialog/edit-dialog.component.scss @@ -0,0 +1,3 @@ +mat-form-field { + width: 100%; +} \ No newline at end of file diff --git a/frontend/src/app/routes/view/edit-dialog/edit-dialog.component.ts b/frontend/src/app/routes/view/edit-dialog/edit-dialog.component.ts new file mode 100644 index 0000000..7deaf41 --- /dev/null +++ b/frontend/src/app/routes/view/edit-dialog/edit-dialog.component.ts @@ -0,0 +1,76 @@ +import { Component, Inject, OnInit } from '@angular/core'; +import { MatDialogRef, MAT_DIALOG_DATA } from '@angular/material/dialog'; +import { EImage } from 'picsur-shared/dist/entities/image.entity'; +import { HasFailed } from 'picsur-shared/dist/types'; +import { ImageService } from 'src/app/services/api/image.service'; +import { Logger } from 'src/app/services/logger/logger.service'; +import { ErrorService } from 'src/app/util/error-manager/error.service'; + +export interface EditDialogData { + image: EImage; +} + +@Component({ + selector: 'edit-dialog', + templateUrl: './edit-dialog.component.html', + styleUrls: ['./edit-dialog.component.scss'], +}) +export class EditDialogComponent implements OnInit { + private readonly logger = new Logger(EditDialogComponent.name); + + public readonly ExpireOptions: Array<[string, null | number]> = [ + ['Never', 0], + ['5 Minutes', 5 * 60], + ['10 Minutes', 10 * 60], + ['30 Minutes', 15 * 60], + ['1 Hour', 60 * 60], + ['6 Hours', 2 * 60 * 60], + ['12 Hours', 12 * 60 * 60], + ['1 Day', 24 * 60 * 60], + ['1 Week', 7 * 24 * 60 * 60], + ['1 Month', 30 * 24 * 60 * 60], + ]; + + public expiresAfter: number = 0; + public image: EImage; + + constructor( + public readonly dialogRef: MatDialogRef, + private readonly imageService: ImageService, + private readonly errorService: ErrorService, + @Inject(MAT_DIALOG_DATA) data: EditDialogData, + ) { + if (!data.image) { + throw new Error('imageID is required'); + } + + this.image = data.image; + } + + async ngOnInit() { + console.log(this.image); + } + + close() { + this.dialogRef.close(null); + } + + async save() { + const result = await this.imageService.UpdateImage(this.image.id, { + file_name: this.image.file_name, + expires_at: + this.expiresAfter === 0 + ? null + : new Date(Date.now() + this.expiresAfter * 1000), + }); + + if (HasFailed(result)) { + this.errorService.showFailure(result, this.logger); + return this.close(); + } + + this.errorService.success('Image successfully updated'); + + this.dialogRef.close(result); + } +} diff --git a/frontend/src/app/routes/view/view.component.html b/frontend/src/app/routes/view/view.component.html index 8d27a00..9d7d43a 100644 --- a/frontend/src/app/routes/view/view.component.html +++ b/frontend/src/app/routes/view/view.component.html @@ -8,7 +8,14 @@
-

Uploaded {{ image.created | amTimeAgo }}

+

+ Uploaded {{ image.created | amTimeAgo }} + {{ + image.expires_at === null + ? '' + : '| Expires ' + (image.expires_at | amTimeAgo) + }} +

@@ -66,7 +73,15 @@ share +