mirror of
https://github.com/KevinMidboe/immich.git
synced 2025-12-08 20:29:05 +00:00
feat(server): harden move file (#4361)
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
@@ -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,
|
||||
|
||||
37
server/src/infra/entities/move.entity.ts
Normal file
37
server/src/infra/entities/move.entity.ts
Normal 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;
|
||||
@@ -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 },
|
||||
|
||||
14
server/src/infra/migrations/1696968880063-AddMoveTable.ts
Normal file
14
server/src/infra/migrations/1696968880063-AddMoveTable.ts
Normal 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"`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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}`);
|
||||
}
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -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 };
|
||||
|
||||
|
||||
26
server/src/infra/repositories/move.repository.ts
Normal file
26
server/src/infra/repositories/move.repository.ts
Normal 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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user