mirror of
https://github.com/immich-app/immich.git
synced 2024-12-25 10:43:13 +02:00
refactor(server): make storage core singleton (#4608)
This commit is contained in:
parent
2288b022bc
commit
6b25435b4f
@ -10,8 +10,6 @@ import {
|
||||
newCommunicationRepositoryMock,
|
||||
newCryptoRepositoryMock,
|
||||
newJobRepositoryMock,
|
||||
newMoveRepositoryMock,
|
||||
newPersonRepositoryMock,
|
||||
newStorageRepositoryMock,
|
||||
newSystemConfigRepositoryMock,
|
||||
} from '@test';
|
||||
@ -25,8 +23,6 @@ import {
|
||||
ICommunicationRepository,
|
||||
ICryptoRepository,
|
||||
IJobRepository,
|
||||
IMoveRepository,
|
||||
IPersonRepository,
|
||||
IStorageRepository,
|
||||
ISystemConfigRepository,
|
||||
JobItem,
|
||||
@ -165,8 +161,6 @@ describe(AssetService.name, () => {
|
||||
let assetMock: jest.Mocked<IAssetRepository>;
|
||||
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
||||
let jobMock: jest.Mocked<IJobRepository>;
|
||||
let moveMock: jest.Mocked<IMoveRepository>;
|
||||
let personMock: jest.Mocked<IPersonRepository>;
|
||||
let storageMock: jest.Mocked<IStorageRepository>;
|
||||
let communicationMock: jest.Mocked<ICommunicationRepository>;
|
||||
let configMock: jest.Mocked<ISystemConfigRepository>;
|
||||
@ -181,21 +175,9 @@ describe(AssetService.name, () => {
|
||||
communicationMock = newCommunicationRepositoryMock();
|
||||
cryptoMock = newCryptoRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
moveMock = newMoveRepositoryMock();
|
||||
personMock = newPersonRepositoryMock();
|
||||
storageMock = newStorageRepositoryMock();
|
||||
configMock = newSystemConfigRepositoryMock();
|
||||
sut = new AssetService(
|
||||
accessMock,
|
||||
assetMock,
|
||||
cryptoMock,
|
||||
jobMock,
|
||||
configMock,
|
||||
moveMock,
|
||||
personMock,
|
||||
storageMock,
|
||||
communicationMock,
|
||||
);
|
||||
sut = new AssetService(accessMock, assetMock, cryptoMock, jobMock, configMock, storageMock, communicationMock);
|
||||
|
||||
when(assetMock.getById)
|
||||
.calledWith(assetStub.livePhotoStillAsset.id)
|
||||
|
@ -16,8 +16,6 @@ import {
|
||||
ICommunicationRepository,
|
||||
ICryptoRepository,
|
||||
IJobRepository,
|
||||
IMoveRepository,
|
||||
IPersonRepository,
|
||||
IStorageRepository,
|
||||
ISystemConfigRepository,
|
||||
ImmichReadStream,
|
||||
@ -76,7 +74,6 @@ export class AssetService {
|
||||
private logger = new Logger(AssetService.name);
|
||||
private access: AccessCore;
|
||||
private configCore: SystemConfigCore;
|
||||
private storageCore: StorageCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@ -84,14 +81,11 @@ export class AssetService {
|
||||
@Inject(ICryptoRepository) private cryptoRepository: ICryptoRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||
@Inject(IPersonRepository) personRepository: IPersonRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(ICommunicationRepository) private communicationRepository: ICommunicationRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
this.configCore = SystemConfigCore.create(configRepository);
|
||||
this.storageCore = new StorageCore(storageRepository, assetRepository, moveRepository, personRepository);
|
||||
}
|
||||
|
||||
canUploadFile({ authUser, fieldName, file }: UploadRequest): true {
|
||||
@ -147,9 +141,9 @@ export class AssetService {
|
||||
getUploadFolder({ authUser, fieldName }: UploadRequest): string {
|
||||
authUser = this.access.requireUploadAccess(authUser);
|
||||
|
||||
let folder = this.storageCore.getFolderLocation(StorageFolder.UPLOAD, authUser.id);
|
||||
let folder = StorageCore.getFolderLocation(StorageFolder.UPLOAD, authUser.id);
|
||||
if (fieldName === UploadFieldName.PROFILE_DATA) {
|
||||
folder = this.storageCore.getFolderLocation(StorageFolder.PROFILE, authUser.id);
|
||||
folder = StorageCore.getFolderLocation(StorageFolder.PROFILE, authUser.id);
|
||||
}
|
||||
|
||||
this.storageRepository.mkdirSync(folder);
|
||||
|
@ -44,7 +44,7 @@ export class MediaService {
|
||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||
) {
|
||||
this.configCore = SystemConfigCore.create(configRepository);
|
||||
this.storageCore = new StorageCore(this.storageRepository, assetRepository, moveRepository, personRepository);
|
||||
this.storageCore = StorageCore.create(assetRepository, moveRepository, personRepository, storageRepository);
|
||||
}
|
||||
|
||||
async handleQueueGenerateThumbnails({ force }: IBaseJob) {
|
||||
@ -140,7 +140,7 @@ export class MediaService {
|
||||
const { thumbnail, ffmpeg } = await this.configCore.getConfig();
|
||||
const size = format === 'jpeg' ? thumbnail.jpegSize : thumbnail.webpSize;
|
||||
const path =
|
||||
format === 'jpeg' ? this.storageCore.getLargeThumbnailPath(asset) : this.storageCore.getSmallThumbnailPath(asset);
|
||||
format === 'jpeg' ? StorageCore.getLargeThumbnailPath(asset) : StorageCore.getSmallThumbnailPath(asset);
|
||||
this.storageCore.ensureFolders(path);
|
||||
|
||||
switch (asset.type) {
|
||||
@ -220,7 +220,7 @@ export class MediaService {
|
||||
}
|
||||
|
||||
const input = asset.originalPath;
|
||||
const output = this.storageCore.getEncodedVideoPath(asset);
|
||||
const output = StorageCore.getEncodedVideoPath(asset);
|
||||
this.storageCore.ensureFolders(output);
|
||||
|
||||
const { videoStreams, audioStreams, format } = await this.mediaRepository.probe(input);
|
||||
|
@ -80,7 +80,7 @@ export class MetadataService {
|
||||
@Inject(IPersonRepository) personRepository: IPersonRepository,
|
||||
) {
|
||||
this.configCore = SystemConfigCore.create(configRepository);
|
||||
this.storageCore = new StorageCore(storageRepository, assetRepository, moveRepository, personRepository);
|
||||
this.storageCore = StorageCore.create(assetRepository, moveRepository, personRepository, storageRepository);
|
||||
this.configCore.config$.subscribe(() => this.init());
|
||||
}
|
||||
|
||||
@ -294,7 +294,7 @@ export class MetadataService {
|
||||
});
|
||||
const checksum = this.cryptoRepository.hashSha1(video);
|
||||
|
||||
const motionPath = this.storageCore.getAndroidMotionPath(asset);
|
||||
const motionPath = StorageCore.getAndroidMotionPath(asset);
|
||||
this.storageCore.ensureFolders(motionPath);
|
||||
|
||||
let motionAsset = await this.assetRepository.getByChecksum(asset.ownerId, checksum);
|
||||
|
@ -58,7 +58,7 @@ export class PersonService {
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
this.configCore = SystemConfigCore.create(configRepository);
|
||||
this.storageCore = new StorageCore(storageRepository, assetRepository, moveRepository, repository);
|
||||
this.storageCore = StorageCore.create(assetRepository, moveRepository, repository, storageRepository);
|
||||
}
|
||||
|
||||
async getAll(authUser: AuthUserDto, dto: PersonSearchDto): Promise<PeopleResponseDto> {
|
||||
@ -309,7 +309,7 @@ export class PersonService {
|
||||
}
|
||||
|
||||
this.logger.verbose(`Cropping face for person: ${personId}`);
|
||||
const thumbnailPath = this.storageCore.getPersonThumbnailPath(person);
|
||||
const thumbnailPath = StorageCore.getPersonThumbnailPath(person);
|
||||
this.storageCore.ensureFolders(thumbnailPath);
|
||||
|
||||
const halfWidth = (x2 - x1) / 2;
|
||||
|
@ -52,7 +52,7 @@ export class StorageTemplateService {
|
||||
this.configCore = SystemConfigCore.create(configRepository);
|
||||
this.configCore.addValidator((config) => this.validate(config));
|
||||
this.configCore.config$.subscribe((config) => this.onConfig(config));
|
||||
this.storageCore = new StorageCore(storageRepository, assetRepository, moveRepository, personRepository);
|
||||
this.storageCore = StorageCore.create(assetRepository, moveRepository, personRepository, storageRepository);
|
||||
}
|
||||
|
||||
async handleMigrationSingle({ id }: IEntityJob) {
|
||||
@ -99,7 +99,7 @@ export class StorageTemplateService {
|
||||
}
|
||||
|
||||
async moveAsset(asset: AssetEntity, metadata: MoveAssetMetadata) {
|
||||
if (asset.isReadOnly || asset.isExternal || this.storageCore.isAndroidMotionPath(asset.originalPath)) {
|
||||
if (asset.isReadOnly || asset.isExternal || StorageCore.isAndroidMotionPath(asset.originalPath)) {
|
||||
// External assets are not affected by storage template
|
||||
// TODO: shouldn't this only apply to external assets?
|
||||
return;
|
||||
@ -131,7 +131,7 @@ export class StorageTemplateService {
|
||||
const source = asset.originalPath;
|
||||
const ext = path.extname(source).split('.').pop() as string;
|
||||
const sanitized = sanitize(path.basename(filename, `.${ext}`));
|
||||
const rootPath = this.storageCore.getLibraryFolder({ id: asset.ownerId, storageLabel });
|
||||
const rootPath = StorageCore.getLibraryFolder({ id: asset.ownerId, storageLabel });
|
||||
const storagePath = this.render(this.storageTemplate, asset, sanitized, ext);
|
||||
const fullPath = path.normalize(path.join(rootPath, storagePath));
|
||||
let destination = `${fullPath}.${ext}`;
|
||||
|
@ -21,21 +21,40 @@ export interface MoveRequest {
|
||||
|
||||
type GeneratedAssetPath = AssetPathType.JPEG_THUMBNAIL | AssetPathType.WEBP_THUMBNAIL | AssetPathType.ENCODED_VIDEO;
|
||||
|
||||
let instance: StorageCore | null;
|
||||
|
||||
export class StorageCore {
|
||||
private logger = new Logger(StorageCore.name);
|
||||
|
||||
constructor(
|
||||
private repository: IStorageRepository,
|
||||
private constructor(
|
||||
private assetRepository: IAssetRepository,
|
||||
private moveRepository: IMoveRepository,
|
||||
private personRepository: IPersonRepository,
|
||||
private repository: IStorageRepository,
|
||||
) {}
|
||||
|
||||
getFolderLocation(folder: StorageFolder, userId: string) {
|
||||
static create(
|
||||
assetRepository: IAssetRepository,
|
||||
moveRepository: IMoveRepository,
|
||||
personRepository: IPersonRepository,
|
||||
repository: IStorageRepository,
|
||||
) {
|
||||
if (!instance) {
|
||||
instance = new StorageCore(assetRepository, moveRepository, personRepository, repository);
|
||||
}
|
||||
|
||||
return instance;
|
||||
}
|
||||
|
||||
static reset() {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
static getFolderLocation(folder: StorageFolder, userId: string) {
|
||||
return join(StorageCore.getBaseFolder(folder), userId);
|
||||
}
|
||||
|
||||
getLibraryFolder(user: { storageLabel: string | null; id: string }) {
|
||||
static getLibraryFolder(user: { storageLabel: string | null; id: string }) {
|
||||
return join(StorageCore.getBaseFolder(StorageFolder.LIBRARY), user.storageLabel || user.id);
|
||||
}
|
||||
|
||||
@ -43,27 +62,27 @@ export class StorageCore {
|
||||
return join(APP_MEDIA_LOCATION, folder);
|
||||
}
|
||||
|
||||
getPersonThumbnailPath(person: PersonEntity) {
|
||||
return this.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`);
|
||||
static getPersonThumbnailPath(person: PersonEntity) {
|
||||
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, person.ownerId, `${person.id}.jpeg`);
|
||||
}
|
||||
|
||||
getLargeThumbnailPath(asset: AssetEntity) {
|
||||
return this.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}.jpeg`);
|
||||
static getLargeThumbnailPath(asset: AssetEntity) {
|
||||
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}.jpeg`);
|
||||
}
|
||||
|
||||
getSmallThumbnailPath(asset: AssetEntity) {
|
||||
return this.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}.webp`);
|
||||
static getSmallThumbnailPath(asset: AssetEntity) {
|
||||
return StorageCore.getNestedPath(StorageFolder.THUMBNAILS, asset.ownerId, `${asset.id}.webp`);
|
||||
}
|
||||
|
||||
getEncodedVideoPath(asset: AssetEntity) {
|
||||
return this.getNestedPath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${asset.id}.mp4`);
|
||||
static getEncodedVideoPath(asset: AssetEntity) {
|
||||
return StorageCore.getNestedPath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${asset.id}.mp4`);
|
||||
}
|
||||
|
||||
getAndroidMotionPath(asset: AssetEntity) {
|
||||
return this.getNestedPath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${asset.id}-MP.mp4`);
|
||||
static getAndroidMotionPath(asset: AssetEntity) {
|
||||
return StorageCore.getNestedPath(StorageFolder.ENCODED_VIDEO, asset.ownerId, `${asset.id}-MP.mp4`);
|
||||
}
|
||||
|
||||
isAndroidMotionPath(originalPath: string) {
|
||||
static isAndroidMotionPath(originalPath: string) {
|
||||
return originalPath.startsWith(StorageCore.getBaseFolder(StorageFolder.ENCODED_VIDEO));
|
||||
}
|
||||
|
||||
@ -75,15 +94,25 @@ export class StorageCore {
|
||||
const { id: entityId, resizePath, webpPath, encodedVideoPath } = asset;
|
||||
switch (pathType) {
|
||||
case AssetPathType.JPEG_THUMBNAIL:
|
||||
return this.moveFile({ entityId, pathType, oldPath: resizePath, newPath: this.getLargeThumbnailPath(asset) });
|
||||
return this.moveFile({
|
||||
entityId,
|
||||
pathType,
|
||||
oldPath: resizePath,
|
||||
newPath: StorageCore.getLargeThumbnailPath(asset),
|
||||
});
|
||||
case AssetPathType.WEBP_THUMBNAIL:
|
||||
return this.moveFile({ entityId, pathType, oldPath: webpPath, newPath: this.getSmallThumbnailPath(asset) });
|
||||
return this.moveFile({
|
||||
entityId,
|
||||
pathType,
|
||||
oldPath: webpPath,
|
||||
newPath: StorageCore.getSmallThumbnailPath(asset),
|
||||
});
|
||||
case AssetPathType.ENCODED_VIDEO:
|
||||
return this.moveFile({
|
||||
entityId,
|
||||
pathType,
|
||||
oldPath: encodedVideoPath,
|
||||
newPath: this.getEncodedVideoPath(asset),
|
||||
newPath: StorageCore.getEncodedVideoPath(asset),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -96,7 +125,7 @@ export class StorageCore {
|
||||
entityId,
|
||||
pathType,
|
||||
oldPath: thumbnailPath,
|
||||
newPath: this.getPersonThumbnailPath(person),
|
||||
newPath: StorageCore.getPersonThumbnailPath(person),
|
||||
});
|
||||
}
|
||||
}
|
||||
@ -159,7 +188,12 @@ export class StorageCore {
|
||||
}
|
||||
}
|
||||
|
||||
private getNestedPath(folder: StorageFolder, ownerId: string, filename: string): string {
|
||||
return join(this.getFolderLocation(folder, ownerId), filename.substring(0, 2), filename.substring(2, 4), filename);
|
||||
private static getNestedPath(folder: StorageFolder, ownerId: string, filename: string): string {
|
||||
return join(
|
||||
StorageCore.getFolderLocation(folder, ownerId),
|
||||
filename.substring(0, 2),
|
||||
filename.substring(2, 4),
|
||||
filename,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@ -11,8 +11,6 @@ import {
|
||||
newCryptoRepositoryMock,
|
||||
newJobRepositoryMock,
|
||||
newLibraryRepositoryMock,
|
||||
newMoveRepositoryMock,
|
||||
newPersonRepositoryMock,
|
||||
newStorageRepositoryMock,
|
||||
newUserRepositoryMock,
|
||||
userStub,
|
||||
@ -26,8 +24,6 @@ import {
|
||||
ICryptoRepository,
|
||||
IJobRepository,
|
||||
ILibraryRepository,
|
||||
IMoveRepository,
|
||||
IPersonRepository,
|
||||
IStorageRepository,
|
||||
IUserRepository,
|
||||
} from '../repositories';
|
||||
@ -139,8 +135,6 @@ describe(UserService.name, () => {
|
||||
let assetMock: jest.Mocked<IAssetRepository>;
|
||||
let jobMock: jest.Mocked<IJobRepository>;
|
||||
let libraryMock: jest.Mocked<ILibraryRepository>;
|
||||
let moveMock: jest.Mocked<IMoveRepository>;
|
||||
let personMock: jest.Mocked<IPersonRepository>;
|
||||
let storageMock: jest.Mocked<IStorageRepository>;
|
||||
|
||||
beforeEach(async () => {
|
||||
@ -149,22 +143,10 @@ describe(UserService.name, () => {
|
||||
cryptoRepositoryMock = newCryptoRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
libraryMock = newLibraryRepositoryMock();
|
||||
moveMock = newMoveRepositoryMock();
|
||||
personMock = newPersonRepositoryMock();
|
||||
storageMock = newStorageRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
|
||||
sut = new UserService(
|
||||
albumMock,
|
||||
assetMock,
|
||||
cryptoRepositoryMock,
|
||||
jobMock,
|
||||
libraryMock,
|
||||
moveMock,
|
||||
personMock,
|
||||
storageMock,
|
||||
userMock,
|
||||
);
|
||||
sut = new UserService(albumMock, assetMock, cryptoRepositoryMock, jobMock, libraryMock, storageMock, userMock);
|
||||
|
||||
when(userMock.get).calledWith(adminUser.id).mockResolvedValue(adminUser);
|
||||
when(userMock.get).calledWith(adminUser.id, undefined).mockResolvedValue(adminUser);
|
||||
|
@ -10,8 +10,6 @@ import {
|
||||
ICryptoRepository,
|
||||
IJobRepository,
|
||||
ILibraryRepository,
|
||||
IMoveRepository,
|
||||
IPersonRepository,
|
||||
IStorageRepository,
|
||||
IUserRepository,
|
||||
} from '../repositories';
|
||||
@ -30,7 +28,6 @@ import { UserCore } from './user.core';
|
||||
@Injectable()
|
||||
export class UserService {
|
||||
private logger = new Logger(UserService.name);
|
||||
private storageCore: StorageCore;
|
||||
private userCore: UserCore;
|
||||
|
||||
constructor(
|
||||
@ -39,12 +36,9 @@ export class UserService {
|
||||
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ILibraryRepository) libraryRepository: ILibraryRepository,
|
||||
@Inject(IMoveRepository) moveRepository: IMoveRepository,
|
||||
@Inject(IPersonRepository) personRepository: IPersonRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
) {
|
||||
this.storageCore = new StorageCore(storageRepository, assetRepository, moveRepository, personRepository);
|
||||
this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository);
|
||||
}
|
||||
|
||||
@ -171,11 +165,11 @@ export class UserService {
|
||||
this.logger.log(`Deleting user: ${user.id}`);
|
||||
|
||||
const folders = [
|
||||
this.storageCore.getLibraryFolder(user),
|
||||
this.storageCore.getFolderLocation(StorageFolder.UPLOAD, user.id),
|
||||
this.storageCore.getFolderLocation(StorageFolder.PROFILE, user.id),
|
||||
this.storageCore.getFolderLocation(StorageFolder.THUMBNAILS, user.id),
|
||||
this.storageCore.getFolderLocation(StorageFolder.ENCODED_VIDEO, user.id),
|
||||
StorageCore.getLibraryFolder(user),
|
||||
StorageCore.getFolderLocation(StorageFolder.UPLOAD, user.id),
|
||||
StorageCore.getFolderLocation(StorageFolder.PROFILE, user.id),
|
||||
StorageCore.getFolderLocation(StorageFolder.THUMBNAILS, user.id),
|
||||
StorageCore.getFolderLocation(StorageFolder.ENCODED_VIDEO, user.id),
|
||||
];
|
||||
|
||||
for (const folder of folders) {
|
||||
|
@ -1,6 +1,10 @@
|
||||
import { IStorageRepository } from '@app/domain';
|
||||
import { IStorageRepository, StorageCore } from '@app/domain';
|
||||
|
||||
export const newStorageRepositoryMock = (reset = true): jest.Mocked<IStorageRepository> => {
|
||||
if (reset) {
|
||||
StorageCore.reset();
|
||||
}
|
||||
|
||||
export const newStorageRepositoryMock = (): jest.Mocked<IStorageRepository> => {
|
||||
return {
|
||||
createZipStream: jest.fn(),
|
||||
createReadStream: jest.fn(),
|
||||
|
Loading…
Reference in New Issue
Block a user