chore(server): Store generated files (thumbnails, encoded video) in subdirectories (#4112)

* save thumbnails in subdirectories

* migration job, migrate assets and face thumbnails

* fix tests

* directory depth of two instead of three

* cleanup empty dirs after migration

* clean up empty dirs after migration, migrate people without assetId

* add job card for new migration job

* fix removeEmptyDirs race condition because of missing await

* cleanup empty directories after asset deletion

* move ensurePath to storage core

* rename jobs

* remove unnecessary property of IEntityJob

* use updated person getById, minor refactoring

* ensure that directory cleanup doesn't interfere with migration

* better description for job in ui

* fix remove directories when migration is done

* cleanup empty folders at start of migration

* fix: actually persist concurrency setting

* add comment explaining regex

* chore: cleanup

---------

Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
Daniel Dietzler
2023-09-25 17:07:21 +02:00
committed by GitHub
parent 07069c3b1e
commit 3053cbd4c8
36 changed files with 310 additions and 102 deletions
+21
View File
@@ -1,5 +1,6 @@
import { join } from 'node:path';
import { APP_MEDIA_LOCATION } from '../domain.constant';
import { IStorageRepository } from './storage.repository';
export enum StorageFolder {
ENCODED_VIDEO = 'encoded-video',
@@ -10,6 +11,8 @@ export enum StorageFolder {
}
export class StorageCore {
constructor(private repository: IStorageRepository) {}
getFolderLocation(
folder: StorageFolder.ENCODED_VIDEO | StorageFolder.UPLOAD | StorageFolder.PROFILE | StorageFolder.THUMBNAILS,
userId: string,
@@ -24,4 +27,22 @@ export class StorageCore {
getBaseFolder(folder: StorageFolder) {
return join(APP_MEDIA_LOCATION, folder);
}
ensurePath(
folder: StorageFolder.ENCODED_VIDEO | StorageFolder.UPLOAD | StorageFolder.PROFILE | StorageFolder.THUMBNAILS,
ownerId: string,
fileName: string,
): string {
const folderPath = join(
this.getFolderLocation(folder, ownerId),
fileName.substring(0, 2),
fileName.substring(2, 4),
);
this.repository.mkdirSync(folderPath);
return join(folderPath, fileName);
}
removeEmptyDirs(folder: StorageFolder) {
return this.repository.removeEmptyDirs(this.getBaseFolder(folder));
}
}
@@ -26,7 +26,7 @@ export interface IStorageRepository {
createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream>;
unlink(filepath: string): Promise<void>;
unlinkDir(folder: string, options?: { recursive?: boolean; force?: boolean }): Promise<void>;
removeEmptyDirs(folder: string): Promise<void>;
removeEmptyDirs(folder: string, self?: boolean): Promise<void>;
moveFile(source: string, target: string): Promise<void>;
checkFileExists(filepath: string, mode?: number): Promise<boolean>;
mkdirSync(filepath: string): void;
+4 -2
View File
@@ -6,9 +6,11 @@ import { IStorageRepository } from './storage.repository';
@Injectable()
export class StorageService {
private logger = new Logger(StorageService.name);
private storageCore = new StorageCore();
private storageCore: StorageCore;
constructor(@Inject(IStorageRepository) private storageRepository: IStorageRepository) {}
constructor(@Inject(IStorageRepository) private storageRepository: IStorageRepository) {
this.storageCore = new StorageCore(storageRepository);
}
init() {
const libraryBase = this.storageCore.getBaseFolder(StorageFolder.LIBRARY);