From ea9766a60b26c2af9b41d338670901a925bd160f Mon Sep 17 00:00:00 2001 From: Alex Tran Date: Wed, 9 Feb 2022 22:59:32 -0600 Subject: [PATCH] Create database and model for EXIF --- server/src/api-v1/asset/asset.controller.ts | 7 ++- server/src/api-v1/asset/asset.module.ts | 13 ++++- .../src/api-v1/asset/dto/create-exif.dto.ts | 48 ++++++++++++++++ .../src/api-v1/asset/dto/update-exif.dto.ts | 4 ++ .../src/api-v1/asset/entities/asset.entity.ts | 7 ++- .../src/api-v1/asset/entities/exif.entity.ts | 56 +++++++++++++++++++ .../background-task/background-task.module.ts | 4 +- .../background-task.processor.ts | 22 +++++++- .../background-task.service.ts | 21 ++++++- 9 files changed, 175 insertions(+), 7 deletions(-) create mode 100644 server/src/api-v1/asset/dto/create-exif.dto.ts create mode 100644 server/src/api-v1/asset/dto/update-exif.dto.ts create mode 100644 server/src/api-v1/asset/entities/exif.entity.ts diff --git a/server/src/api-v1/asset/asset.controller.ts b/server/src/api-v1/asset/asset.controller.ts index 1db90a7802..36ab115105 100644 --- a/server/src/api-v1/asset/asset.controller.ts +++ b/server/src/api-v1/asset/asset.controller.ts @@ -30,6 +30,7 @@ import { promisify } from 'util'; import { stat } from 'fs'; import { pipeline } from 'stream'; import { GetNewAssetQueryDto } from './dto/get-new-asset-query.dto'; +import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; const fileInfo = promisify(stat); @@ -37,8 +38,9 @@ const fileInfo = promisify(stat); @Controller('asset') export class AssetController { constructor( - private readonly assetService: AssetService, - private readonly assetOptimizeService: AssetOptimizeService, + private assetService: AssetService, + private assetOptimizeService: AssetOptimizeService, + private backgroundTaskService: BackgroundTaskService, ) {} @Post('upload') @@ -53,6 +55,7 @@ export class AssetController { if (savedAsset && savedAsset.type == AssetType.IMAGE) { await this.assetOptimizeService.resizeImage(savedAsset); + await this.backgroundTaskService.extractExif(savedAsset); } if (savedAsset && savedAsset.type == AssetType.VIDEO) { diff --git a/server/src/api-v1/asset/asset.module.ts b/server/src/api-v1/asset/asset.module.ts index 161e00748a..0d4466b9a5 100644 --- a/server/src/api-v1/asset/asset.module.ts +++ b/server/src/api-v1/asset/asset.module.ts @@ -6,6 +6,8 @@ import { AssetEntity } from './entities/asset.entity'; import { ImageOptimizeModule } from '../../modules/image-optimize/image-optimize.module'; import { AssetOptimizeService } from '../../modules/image-optimize/image-optimize.service'; import { BullModule } from '@nestjs/bull'; +import { BackgroundTaskModule } from '../../modules/background-task/background-task.module'; +import { BackgroundTaskService } from '../../modules/background-task/background-task.service'; @Module({ imports: [ @@ -17,11 +19,20 @@ import { BullModule } from '@nestjs/bull'; removeOnFail: false, }, }), + BullModule.registerQueue({ + name: 'background-task', + defaultJobOptions: { + attempts: 3, + removeOnComplete: true, + removeOnFail: false, + }, + }), TypeOrmModule.forFeature([AssetEntity]), ImageOptimizeModule, + BackgroundTaskModule, ], controllers: [AssetController], - providers: [AssetService, AssetOptimizeService], + providers: [AssetService, AssetOptimizeService, BackgroundTaskService], exports: [], }) export class AssetModule {} diff --git a/server/src/api-v1/asset/dto/create-exif.dto.ts b/server/src/api-v1/asset/dto/create-exif.dto.ts new file mode 100644 index 0000000000..0214c1ee57 --- /dev/null +++ b/server/src/api-v1/asset/dto/create-exif.dto.ts @@ -0,0 +1,48 @@ +import { IsNotEmpty, IsOptional } from 'class-validator'; + +export class CreateExifDto { + @IsNotEmpty() + assetId: string; + + @IsOptional() + make: string; + + @IsOptional() + model: string; + + @IsOptional() + imageName: string; + + @IsOptional() + exifImageWidth: number; + + @IsOptional() + exifImageHeight: number; + + @IsOptional() + fileSizeInByte: number; + + @IsOptional() + orientation: string; + + @IsOptional() + dateTimeOriginal: Date; + + @IsOptional() + modifiedDate: Date; + + @IsOptional() + lensModel: string; + + @IsOptional() + fNumber: number; + + @IsOptional() + focalLenght: number; + + @IsOptional() + iso: number; + + @IsOptional() + exposureTime: number; +} diff --git a/server/src/api-v1/asset/dto/update-exif.dto.ts b/server/src/api-v1/asset/dto/update-exif.dto.ts new file mode 100644 index 0000000000..1bf5066649 --- /dev/null +++ b/server/src/api-v1/asset/dto/update-exif.dto.ts @@ -0,0 +1,4 @@ +import { PartialType } from '@nestjs/mapped-types'; +import { CreateExifDto } from './create-exif.dto'; + +export class UpdateExifDto extends PartialType(CreateExifDto) {} diff --git a/server/src/api-v1/asset/entities/asset.entity.ts b/server/src/api-v1/asset/entities/asset.entity.ts index 4d78096d25..640783ed99 100644 --- a/server/src/api-v1/asset/entities/asset.entity.ts +++ b/server/src/api-v1/asset/entities/asset.entity.ts @@ -1,4 +1,5 @@ -import { Column, Entity, PrimaryColumn, PrimaryGeneratedColumn, Unique } from 'typeorm'; +import { Column, Entity, JoinColumn, OneToOne, PrimaryColumn, PrimaryGeneratedColumn, Unique } from 'typeorm'; +import { ExifEntity } from './exif.entity'; @Entity('assets') @Unique(['deviceAssetId', 'userId', 'deviceId']) @@ -38,6 +39,10 @@ export class AssetEntity { @Column({ nullable: true }) duration: string; + + @OneToOne(() => ExifEntity, { cascade: true }) + @JoinColumn({ name: 'assetId', referencedColumnName: 'id' }) + exifData?: ExifEntity; } export enum AssetType { diff --git a/server/src/api-v1/asset/entities/exif.entity.ts b/server/src/api-v1/asset/entities/exif.entity.ts new file mode 100644 index 0000000000..ac4ec9d7e5 --- /dev/null +++ b/server/src/api-v1/asset/entities/exif.entity.ts @@ -0,0 +1,56 @@ +import { Index } from 'typeorm'; +import { Column } from 'typeorm/decorator/columns/Column'; +import { PrimaryGeneratedColumn } from 'typeorm/decorator/columns/PrimaryGeneratedColumn'; +import { Entity } from 'typeorm/decorator/entity/Entity'; + +@Entity('exif') +export class ExifEntity { + @PrimaryGeneratedColumn() + id: string; + + @Index({ unique: true }) + @Column({ type: 'uuid' }) + assetId: string; + + @Column({ nullable: true }) + make: string; + + @Column({ nullable: true }) + model: string; + + @Column({ nullable: true }) + imageName: string; + + @Column({ nullable: true }) + exifImageWidth: number; + + @Column({ nullable: true }) + exifImageHeight: number; + + @Column({ nullable: true }) + fileSizeInByte: number; + + @Column({ nullable: true }) + orientation: string; + + @Column({ type: 'timestamptz', nullable: true }) + dateTimeOriginal: Date; + + @Column({ type: 'timestamptz', nullable: true }) + modifiedDate: Date; + + @Column({ nullable: true }) + lensModel: string; + + @Column({ nullable: true }) + fNumber: number; + + @Column({ nullable: true }) + focalLenght: number; + + @Column({ nullable: true }) + iso: number; + + @Column({ nullable: true }) + exposureTime: number; +} diff --git a/server/src/modules/background-task/background-task.module.ts b/server/src/modules/background-task/background-task.module.ts index 795030666e..9c5d8645fb 100644 --- a/server/src/modules/background-task/background-task.module.ts +++ b/server/src/modules/background-task/background-task.module.ts @@ -2,6 +2,7 @@ import { BullModule } from '@nestjs/bull'; import { Module } from '@nestjs/common'; import { TypeOrmModule } from '@nestjs/typeorm'; import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; +import { BackgroundTaskProcessor } from './background-task.processor'; import { BackgroundTaskService } from './background-task.service'; @Module({ @@ -16,6 +17,7 @@ import { BackgroundTaskService } from './background-task.service'; }), TypeOrmModule.forFeature([AssetEntity]), ], - providers: [BackgroundTaskService], + providers: [BackgroundTaskService, BackgroundTaskProcessor], + exports: [BackgroundTaskService], }) export class BackgroundTaskModule {} diff --git a/server/src/modules/background-task/background-task.processor.ts b/server/src/modules/background-task/background-task.processor.ts index ecb7852bb0..f2b9d33ef1 100644 --- a/server/src/modules/background-task/background-task.processor.ts +++ b/server/src/modules/background-task/background-task.processor.ts @@ -4,13 +4,33 @@ import { Job, Queue } from 'bull'; import { Repository } from 'typeorm'; import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; import { ConfigService } from '@nestjs/config'; +import exifr from 'exifr'; +import { readFile } from 'fs/promises'; +import { Logger } from '@nestjs/common'; @Processor('background-task') -export class ImageOptimizeProcessor { +export class BackgroundTaskProcessor { constructor( @InjectRepository(AssetEntity) private assetRepository: Repository, private configService: ConfigService, ) {} + + @Process('extract-exif') + async extractExif(job: Job) { + const { savedAsset }: { savedAsset: AssetEntity } = job.data; + + const fileBuffer = await readFile(savedAsset.originalPath); + + const exifData = await exifr.parse(fileBuffer); + + console.log('====================='); + console.log(exifData); + + try { + } catch (e) { + Logger.error(`Error extracting EXIF ${e.toString()}`, 'extractExif'); + } + } } diff --git a/server/src/modules/background-task/background-task.service.ts b/server/src/modules/background-task/background-task.service.ts index 3df15fb034..507840ea30 100644 --- a/server/src/modules/background-task/background-task.service.ts +++ b/server/src/modules/background-task/background-task.service.ts @@ -1,4 +1,23 @@ +import { InjectQueue } from '@nestjs/bull/dist/decorators'; import { Injectable } from '@nestjs/common'; +import { Queue } from 'bull'; +import { randomUUID } from 'node:crypto'; +import { AssetEntity } from '../../api-v1/asset/entities/asset.entity'; @Injectable() -export class BackgroundTaskService {} +export class BackgroundTaskService { + constructor( + @InjectQueue('background-task') + private backgroundTaskQueue: Queue, + ) {} + + async extractExif(savedAsset: AssetEntity) { + const job = await this.backgroundTaskQueue.add( + 'extract-exif', + { + savedAsset, + }, + { jobId: randomUUID() }, + ); + } +}