mirror of
https://github.com/immich-app/immich.git
synced 2025-01-09 21:36:46 +01:00
e171fec5aa
* Added read-only flag for assets, endpoint to trigger file import vs upload * updated fixtures with new property * if upload is 'read-only', ensure there is no existing asset at the designated originalPath * added test for file import as well as detecting existing image at read-only destination location * Added storage service test for a case where it should not move read-only assets * upload doesn't need the read-only flag available, just importing * default isReadOnly on import endpoint to true * formatting fixes * create-asset dto needs isReadOnly, so set it to false by default on create, updated api generation * updated code to reflect changes in MR * fixed read stream promise return type * new index for originalPath, check for existing path on import, reglardless of user, to prevent duplicates * refactor: import asset * chore: open api * chore: tests * Added externalPath support for individual users, updated UI to allow this to be set by admin * added missing var for externalPath in ui * chore: open api * fix: compilation issues * fix: server test * built api, fixed user-response dto to include externalPath * reverted accidental commit * bad commit of duplicate externalPath in user response dto * fixed tests to include externalPath on expected result * fix: unit tests * centralized supported filetypes, perform file type checking of asset and sidecar during file import process * centralized supported filetype check method to keep regex DRY * fixed typo * combined migrations into one * update api * Removed externalPath from shared-link code, added column to admin user page whether external paths / import is enabled or not * update mimetype * Fixed detect correct mimetype * revert asset-upload config * reverted domain.constant * refactor * fix mime-type issue * fix format --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
124 lines
4.8 KiB
TypeScript
124 lines
4.8 KiB
TypeScript
import { AssetEntity, SystemConfig } from '@app/infra/entities';
|
|
import { Inject, Injectable, Logger } from '@nestjs/common';
|
|
import { IAssetRepository } from '../asset/asset.repository';
|
|
import { APP_MEDIA_LOCATION } from '../domain.constant';
|
|
import { getLivePhotoMotionFilename, usePagination } from '../domain.util';
|
|
import { IEntityJob, JOBS_ASSET_PAGINATION_SIZE } from '../job';
|
|
import { IStorageRepository } from '../storage/storage.repository';
|
|
import { INITIAL_SYSTEM_CONFIG, ISystemConfigRepository } from '../system-config';
|
|
import { IUserRepository } from '../user/user.repository';
|
|
import { StorageTemplateCore } from './storage-template.core';
|
|
|
|
export interface MoveAssetMetadata {
|
|
storageLabel: string | null;
|
|
filename: string;
|
|
}
|
|
|
|
@Injectable()
|
|
export class StorageTemplateService {
|
|
private logger = new Logger(StorageTemplateService.name);
|
|
private core: StorageTemplateCore;
|
|
|
|
constructor(
|
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
|
@Inject(ISystemConfigRepository) configRepository: ISystemConfigRepository,
|
|
@Inject(INITIAL_SYSTEM_CONFIG) config: SystemConfig,
|
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
|
) {
|
|
this.core = new StorageTemplateCore(configRepository, config, storageRepository);
|
|
}
|
|
|
|
async handleMigrationSingle({ id }: IEntityJob) {
|
|
const [asset] = await this.assetRepository.getByIds([id]);
|
|
|
|
const user = await this.userRepository.get(asset.ownerId);
|
|
const storageLabel = user?.storageLabel || null;
|
|
const filename = asset.originalFileName || asset.id;
|
|
await this.moveAsset(asset, { storageLabel, filename });
|
|
|
|
// move motion part of live photo
|
|
if (asset.livePhotoVideoId) {
|
|
const [livePhotoVideo] = await this.assetRepository.getByIds([asset.livePhotoVideoId]);
|
|
const motionFilename = getLivePhotoMotionFilename(filename, livePhotoVideo.originalPath);
|
|
await this.moveAsset(livePhotoVideo, { storageLabel, filename: motionFilename });
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
async handleMigration() {
|
|
try {
|
|
console.time('migrating-time');
|
|
|
|
const assetPagination = usePagination(JOBS_ASSET_PAGINATION_SIZE, (pagination) =>
|
|
this.assetRepository.getAll(pagination),
|
|
);
|
|
const users = await this.userRepository.getList();
|
|
|
|
for await (const assets of assetPagination) {
|
|
for (const asset of assets) {
|
|
const user = users.find((user) => user.id === asset.ownerId);
|
|
const storageLabel = user?.storageLabel || null;
|
|
const filename = asset.originalFileName || asset.id;
|
|
await this.moveAsset(asset, { storageLabel, filename });
|
|
}
|
|
}
|
|
|
|
this.logger.debug('Cleaning up empty directories...');
|
|
await this.storageRepository.removeEmptyDirs(APP_MEDIA_LOCATION);
|
|
} finally {
|
|
console.timeEnd('migrating-time');
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
// TODO: use asset core (once in domain)
|
|
async moveAsset(asset: AssetEntity, metadata: MoveAssetMetadata) {
|
|
if (asset.isReadOnly) {
|
|
this.logger.verbose(`Not moving read-only asset: ${asset.originalPath}`);
|
|
return;
|
|
}
|
|
|
|
const destination = await this.core.getTemplatePath(asset, metadata);
|
|
if (asset.originalPath !== destination) {
|
|
const source = asset.originalPath;
|
|
|
|
let sidecarMoved = false;
|
|
try {
|
|
await this.storageRepository.moveFile(asset.originalPath, destination);
|
|
|
|
let sidecarDestination;
|
|
try {
|
|
if (asset.sidecarPath) {
|
|
sidecarDestination = `${destination}.xmp`;
|
|
await this.storageRepository.moveFile(asset.sidecarPath, sidecarDestination);
|
|
sidecarMoved = true;
|
|
}
|
|
|
|
await this.assetRepository.save({ id: asset.id, originalPath: destination, sidecarPath: sidecarDestination });
|
|
asset.originalPath = destination;
|
|
asset.sidecarPath = sidecarDestination || null;
|
|
} catch (error: any) {
|
|
this.logger.warn(
|
|
`Unable to save new originalPath to database, undoing move for path ${asset.originalPath} - filename ${asset.originalFileName} - id ${asset.id}`,
|
|
error?.stack,
|
|
);
|
|
|
|
// Either sidecar move failed or the save failed. Eithr way, move media back
|
|
await this.storageRepository.moveFile(destination, source);
|
|
|
|
if (asset.sidecarPath && sidecarDestination && sidecarMoved) {
|
|
// If the sidecar was moved, that means the saved failed. So move both the sidecar and the
|
|
// media back into their original positions
|
|
await this.storageRepository.moveFile(sidecarDestination, asset.sidecarPath);
|
|
}
|
|
}
|
|
} catch (error: any) {
|
|
this.logger.error(`Problem applying storage template`, error?.stack, { id: asset.id, source, destination });
|
|
}
|
|
}
|
|
return asset;
|
|
}
|
|
}
|