feat(server): harden move file (#4361)

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Daniel Dietzler
2023-10-11 04:14:44 +02:00
committed by GitHub
parent 332a8d80f2
commit 09bf1c9175
31 changed files with 564 additions and 190 deletions

View File

@@ -5,6 +5,7 @@ import { AssetEntity } from './asset.entity';
import { AuditEntity } from './audit.entity';
import { ExifEntity } from './exif.entity';
import { LibraryEntity } from './library.entity';
import { MoveEntity } from './move.entity';
import { PartnerEntity } from './partner.entity';
import { PersonEntity } from './person.entity';
import { SharedLinkEntity } from './shared-link.entity';
@@ -21,6 +22,7 @@ export * from './asset.entity';
export * from './audit.entity';
export * from './exif.entity';
export * from './library.entity';
export * from './move.entity';
export * from './partner.entity';
export * from './person.entity';
export * from './shared-link.entity';
@@ -37,6 +39,7 @@ export const databaseEntities = [
AssetFaceEntity,
AuditEntity,
ExifEntity,
MoveEntity,
PartnerEntity,
PersonEntity,
SharedLinkEntity,

View File

@@ -0,0 +1,37 @@
import { Column, Entity, PrimaryGeneratedColumn, Unique } from 'typeorm';
@Entity('move_history')
// path lock (per entity)
@Unique('UQ_entityId_pathType', ['entityId', 'pathType'])
// new path lock (global)
@Unique('UQ_newPath', ['newPath'])
export class MoveEntity {
@PrimaryGeneratedColumn('uuid')
id!: string;
@Column({ type: 'varchar' })
entityId!: string;
@Column({ type: 'varchar' })
pathType!: PathType;
@Column({ type: 'varchar' })
oldPath!: string;
@Column({ type: 'varchar' })
newPath!: string;
}
export enum AssetPathType {
ORIGINAL = 'original',
JPEG_THUMBNAIL = 'jpeg_thumbnail',
WEBP_THUMBNAIL = 'webp_thumbnail',
ENCODED_VIDEO = 'encoded_video',
SIDECAR = 'sidecar',
}
export enum PersonPathType {
FACE = 'face',
}
export type PathType = AssetPathType | PersonPathType;

View File

@@ -11,6 +11,7 @@ import {
IMachineLearningRepository,
IMediaRepository,
IMetadataRepository,
IMoveRepository,
IPartnerRepository,
IPersonRepository,
ISearchRepository,
@@ -44,6 +45,7 @@ import {
MachineLearningRepository,
MediaRepository,
MetadataRepository,
MoveRepository,
PartnerRepository,
PersonRepository,
SharedLinkRepository,
@@ -67,6 +69,7 @@ const providers: Provider[] = [
{ provide: IKeyRepository, useClass: APIKeyRepository },
{ provide: IMachineLearningRepository, useClass: MachineLearningRepository },
{ provide: IMetadataRepository, useClass: MetadataRepository },
{ provide: IMoveRepository, useClass: MoveRepository },
{ provide: IPartnerRepository, useClass: PartnerRepository },
{ provide: IPersonRepository, useClass: PersonRepository },
{ provide: ISearchRepository, useClass: TypesenseRepository },

View File

@@ -0,0 +1,14 @@
import { MigrationInterface, QueryRunner } from "typeorm";
export class AddMoveTable1696968880063 implements MigrationInterface {
name = 'AddMoveTable1696968880063'
public async up(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`CREATE TABLE "move_history" ("id" uuid NOT NULL DEFAULT uuid_generate_v4(), "entityId" character varying NOT NULL, "pathType" character varying NOT NULL, "oldPath" character varying NOT NULL, "newPath" character varying NOT NULL, CONSTRAINT "UQ_newPath" UNIQUE ("newPath"), CONSTRAINT "UQ_entityId_pathType" UNIQUE ("entityId", "pathType"), CONSTRAINT "PK_af608f132233acf123f2949678d" PRIMARY KEY ("id"))`);
}
public async down(queryRunner: QueryRunner): Promise<void> {
await queryRunner.query(`DROP TABLE "move_history"`);
}
}

View File

@@ -6,6 +6,7 @@ import {
IStorageRepository,
mimeTypes,
} from '@app/domain';
import { Logger } from '@nestjs/common';
import archiver from 'archiver';
import { constants, createReadStream, existsSync, mkdirSync } from 'fs';
import fs, { readdir, writeFile } from 'fs/promises';
@@ -17,6 +18,8 @@ import path from 'path';
const moveFile = promisify<string, string, mv.Options>(mv);
export class FilesystemProvider implements IStorageRepository {
private logger = new Logger(FilesystemProvider.name);
createZipStream(): ImmichZipStream {
const archive = archiver('zip', { store: true });
@@ -52,6 +55,8 @@ export class FilesystemProvider implements IStorageRepository {
writeFile = writeFile;
async moveFile(source: string, destination: string): Promise<void> {
this.logger.verbose(`Moving ${source} to ${destination}`);
if (await this.checkFileExists(destination)) {
throw new Error(`Destination file already exists: ${destination}`);
}

View File

@@ -11,6 +11,7 @@ export * from './library.repository';
export * from './machine-learning.repository';
export * from './media.repository';
export * from './metadata.repository';
export * from './move.repository';
export * from './partner.repository';
export * from './person.repository';
export * from './shared-link.repository';

View File

@@ -70,6 +70,8 @@ export class JobRepository implements IJobRepository {
private getJobOptions(item: JobItem): JobsOptions | null {
switch (item.name) {
case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE:
return { jobId: item.data.id };
case JobName.GENERATE_PERSON_THUMBNAIL:
return { priority: 1 };

View File

@@ -0,0 +1,26 @@
import { IMoveRepository, MoveCreate } from '@app/domain';
import { Injectable } from '@nestjs/common';
import { InjectRepository } from '@nestjs/typeorm';
import { Repository } from 'typeorm';
import { MoveEntity, PathType } from '../entities';
@Injectable()
export class MoveRepository implements IMoveRepository {
constructor(@InjectRepository(MoveEntity) private repository: Repository<MoveEntity>) {}
create(entity: MoveCreate): Promise<MoveEntity> {
return this.repository.save(entity);
}
getByEntity(entityId: string, pathType: PathType): Promise<MoveEntity | null> {
return this.repository.findOne({ where: { entityId, pathType } });
}
update(entity: Partial<MoveEntity>): Promise<MoveEntity> {
return this.repository.save(entity);
}
delete(move: MoveEntity): Promise<MoveEntity> {
return this.repository.remove(move);
}
}