2024-02-08 16:56:06 -05:00
|
|
|
import {
|
|
|
|
|
BadRequestException,
|
|
|
|
|
Inject,
|
|
|
|
|
Injectable,
|
|
|
|
|
InternalServerErrorException,
|
|
|
|
|
NotFoundException,
|
|
|
|
|
} from '@nestjs/common';
|
2024-03-20 21:20:38 +01:00
|
|
|
import { AccessCore, Permission } from 'src/cores/access.core';
|
2024-03-20 23:53:07 +01:00
|
|
|
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
2023-06-16 15:54:17 -04:00
|
|
|
import {
|
|
|
|
|
AssetBulkUploadCheckResponseDto,
|
2024-03-21 08:07:47 -05:00
|
|
|
AssetFileUploadResponseDto,
|
2023-06-16 15:54:17 -04:00
|
|
|
AssetRejectReason,
|
|
|
|
|
AssetUploadAction,
|
2024-03-21 08:07:47 -05:00
|
|
|
CheckExistingAssetsResponseDto,
|
|
|
|
|
} from 'src/dtos/asset-v1-response.dto';
|
|
|
|
|
import {
|
|
|
|
|
AssetBulkUploadCheckDto,
|
|
|
|
|
AssetSearchDto,
|
|
|
|
|
CheckExistingAssetsDto,
|
|
|
|
|
CreateAssetDto,
|
|
|
|
|
GetAssetThumbnailDto,
|
|
|
|
|
GetAssetThumbnailFormatEnum,
|
|
|
|
|
ServeFileDto,
|
|
|
|
|
} from 'src/dtos/asset-v1.dto';
|
|
|
|
|
import { AuthDto } from 'src/dtos/auth.dto';
|
|
|
|
|
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
|
|
|
|
|
import { LibraryType } from 'src/entities/library.entity';
|
2024-03-21 12:59:49 +01:00
|
|
|
import { IAccessRepository } from 'src/interfaces/access.interface';
|
2024-03-21 08:07:47 -05:00
|
|
|
import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
|
2024-03-21 12:59:49 +01:00
|
|
|
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
|
|
|
|
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
|
|
|
|
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
2024-04-17 03:00:31 +05:30
|
|
|
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
2024-03-21 12:59:49 +01:00
|
|
|
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
|
|
|
|
import { IUserRepository } from 'src/interfaces/user.interface';
|
2024-03-21 00:07:30 +01:00
|
|
|
import { UploadFile } from 'src/services/asset.service';
|
2024-03-20 22:15:09 -05:00
|
|
|
import { CacheControl, ImmichFileResponse, getLivePhotoMotionFilename } from 'src/utils/file';
|
|
|
|
|
import { mimeTypes } from 'src/utils/mime-types';
|
2024-05-02 15:42:26 -04:00
|
|
|
import { fromChecksum } from 'src/utils/request';
|
2024-03-20 19:32:04 +01:00
|
|
|
import { QueryFailedError } from 'typeorm';
|
2022-02-13 15:10:42 -06:00
|
|
|
|
2022-02-03 10:06:44 -06:00
|
|
|
@Injectable()
|
2024-03-21 08:07:47 -05:00
|
|
|
/** @deprecated */
|
|
|
|
|
export class AssetServiceV1 {
|
2023-06-28 09:56:24 -04:00
|
|
|
private access: AccessCore;
|
2023-01-09 14:16:08 -06:00
|
|
|
|
2022-02-03 10:06:44 -06:00
|
|
|
constructor(
|
2023-06-28 09:56:24 -04:00
|
|
|
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
2024-01-25 12:52:21 -05:00
|
|
|
@Inject(IAssetRepositoryV1) private assetRepositoryV1: IAssetRepositoryV1,
|
|
|
|
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
2023-01-21 23:13:36 -05:00
|
|
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
2023-09-20 13:16:33 +02:00
|
|
|
@Inject(ILibraryRepository) private libraryRepository: ILibraryRepository,
|
2024-02-11 21:40:34 -07:00
|
|
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
2024-01-12 18:43:36 -06:00
|
|
|
@Inject(IUserRepository) private userRepository: IUserRepository,
|
2024-04-17 03:00:31 +05:30
|
|
|
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
2023-01-09 14:16:08 -06:00
|
|
|
) {
|
2023-10-23 14:37:51 +02:00
|
|
|
this.access = AccessCore.create(accessRepository);
|
2024-04-17 03:00:31 +05:30
|
|
|
this.logger.setContext(AssetServiceV1.name);
|
2023-01-09 14:16:08 -06:00
|
|
|
}
|
2022-06-11 16:12:06 -05:00
|
|
|
|
2023-01-30 11:14:13 -05:00
|
|
|
public async uploadFile(
|
2023-12-09 23:34:12 -05:00
|
|
|
auth: AuthDto,
|
2023-01-30 11:14:13 -05:00
|
|
|
dto: CreateAssetDto,
|
|
|
|
|
file: UploadFile,
|
|
|
|
|
livePhotoFile?: UploadFile,
|
feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support
* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards
* didn't mean to commit default log level during testing
* new sidecar logic for video metadata as well
* Added xml mimetype for sidecars only
* don't need capture group for this regex
* wrong default value reverted
* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway
* simplified setter logic
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
* simplified logic per suggestions
* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing
* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar
* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync
* simplified logic of filename extraction and asset instantiation
* not sure how that got deleted..
* updated code per suggestions and comments in the PR
* stat was not being used, removed the variable set
* better type checking, using in-scope variables for exif getter instead of passing in every time
* removed commented out test
* ran and resolved all lints, formats, checks, and tests
* resolved suggested change in PR
* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking
* better error handling and moving files back to positions on move or save failure
* regenerated api
* format fixes
* Added XMP documentation
* documentation typo
* Merged in main
* missed merge conflict
* more changes due to a merge
* Resolving conflicts
* added icon for sidecar jobs
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-24 21:59:30 -04:00
|
|
|
sidecarFile?: UploadFile,
|
2023-01-30 11:14:13 -05:00
|
|
|
): Promise<AssetFileUploadResponseDto> {
|
|
|
|
|
if (livePhotoFile) {
|
2023-02-25 09:12:03 -05:00
|
|
|
livePhotoFile = {
|
|
|
|
|
...livePhotoFile,
|
2023-03-28 16:04:11 -04:00
|
|
|
originalName: getLivePhotoMotionFilename(file.originalName, livePhotoFile.originalName),
|
2023-02-25 09:12:03 -05:00
|
|
|
};
|
2023-01-30 11:14:13 -05:00
|
|
|
}
|
2022-12-16 14:26:12 -06:00
|
|
|
|
2023-01-30 11:14:13 -05:00
|
|
|
let livePhotoAsset: AssetEntity | null = null;
|
2022-11-18 23:12:54 -06:00
|
|
|
|
2023-01-30 11:14:13 -05:00
|
|
|
try {
|
2023-12-09 23:34:12 -05:00
|
|
|
const libraryId = await this.getLibraryId(auth, dto.libraryId);
|
|
|
|
|
await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId);
|
2024-02-08 16:56:06 -05:00
|
|
|
this.requireQuota(auth, file.size);
|
2023-01-30 11:14:13 -05:00
|
|
|
if (livePhotoFile) {
|
2023-09-21 21:35:25 -04:00
|
|
|
const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId };
|
2024-02-08 16:56:06 -05:00
|
|
|
livePhotoAsset = await this.create(auth, livePhotoDto, livePhotoFile);
|
2022-11-18 23:12:54 -06:00
|
|
|
}
|
|
|
|
|
|
2024-02-08 16:56:06 -05:00
|
|
|
const asset = await this.create(auth, { ...dto, libraryId }, file, livePhotoAsset?.id, sidecarFile?.originalPath);
|
2022-12-16 14:26:12 -06:00
|
|
|
|
2024-01-12 18:43:36 -06:00
|
|
|
await this.userRepository.updateUsage(auth.user.id, (livePhotoFile?.size || 0) + file.size);
|
|
|
|
|
|
2023-01-30 11:14:13 -05:00
|
|
|
return { id: asset.id, duplicate: false };
|
|
|
|
|
} catch (error: any) {
|
|
|
|
|
// clean up files
|
2023-02-25 09:12:03 -05:00
|
|
|
await this.jobRepository.queue({
|
|
|
|
|
name: JobName.DELETE_FILES,
|
feat(server): xmp sidecar metadata (#2466)
* initial commit for XMP sidecar support
* Added support for 'missing' metadata files to include those without sidecar files, now detects sidecar files in the filesystem for media already ingested but the sidecar was created afterwards
* didn't mean to commit default log level during testing
* new sidecar logic for video metadata as well
* Added xml mimetype for sidecars only
* don't need capture group for this regex
* wrong default value reverted
* simplified the move here - keep it in the same try catch since the outcome is to move the media back anyway
* simplified setter logic
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
* simplified logic per suggestions
* sidecar is now its own queue with a discover and sync, updated UI for the new job queueing
* queue a sidecar job for every asset based on discovery or sync, though the logic is almost identical aside from linking the sidecar
* now queue sidecar jobs for each assset, though logic is mostly the same between discovery and sync
* simplified logic of filename extraction and asset instantiation
* not sure how that got deleted..
* updated code per suggestions and comments in the PR
* stat was not being used, removed the variable set
* better type checking, using in-scope variables for exif getter instead of passing in every time
* removed commented out test
* ran and resolved all lints, formats, checks, and tests
* resolved suggested change in PR
* made getExifProperty more dynamic with multiple possible args for fallbacks, fixed typo, used generic in function for better type checking
* better error handling and moving files back to positions on move or save failure
* regenerated api
* format fixes
* Added XMP documentation
* documentation typo
* Merged in main
* missed merge conflict
* more changes due to a merge
* Resolving conflicts
* added icon for sidecar jobs
---------
Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
2023-05-24 21:59:30 -04:00
|
|
|
data: { files: [file.originalPath, livePhotoFile?.originalPath, sidecarFile?.originalPath] },
|
2023-01-21 23:13:36 -05:00
|
|
|
});
|
2022-11-18 23:12:54 -06:00
|
|
|
|
2023-01-30 11:14:13 -05:00
|
|
|
// handle duplicates with a success response
|
2023-09-20 13:16:33 +02:00
|
|
|
if (error instanceof QueryFailedError && (error as any).constraint === ASSET_CHECKSUM_CONSTRAINT) {
|
2023-05-24 23:08:21 +02:00
|
|
|
const checksums = [file.checksum, livePhotoFile?.checksum].filter((checksum): checksum is Buffer => !!checksum);
|
2024-01-25 12:52:21 -05:00
|
|
|
const [duplicate] = await this.assetRepositoryV1.getAssetsByChecksums(auth.user.id, checksums);
|
2023-01-30 11:14:13 -05:00
|
|
|
return { id: duplicate.id, duplicate: true };
|
2022-11-18 23:12:54 -06:00
|
|
|
}
|
|
|
|
|
|
2023-01-30 11:14:13 -05:00
|
|
|
this.logger.error(`Error uploading file ${error}`, error?.stack);
|
2023-10-06 17:32:28 -04:00
|
|
|
throw error;
|
2022-11-18 23:12:54 -06:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
public async getAllAssets(auth: AuthDto, dto: AssetSearchDto): Promise<AssetResponseDto[]> {
|
|
|
|
|
const userId = dto.userId || auth.user.id;
|
|
|
|
|
await this.access.requirePermission(auth, Permission.TIMELINE_READ, userId);
|
2024-03-08 17:16:32 -06:00
|
|
|
const assets = await this.assetRepositoryV1.getAllByUserId(userId, dto);
|
|
|
|
|
return assets.map((asset) => mapAsset(asset, { withStack: true, auth }));
|
2022-02-13 15:10:42 -06:00
|
|
|
}
|
|
|
|
|
|
2023-12-12 09:58:25 -05:00
|
|
|
async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise<ImmichFileResponse> {
|
2023-12-09 23:34:12 -05:00
|
|
|
await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId);
|
2023-06-28 09:56:24 -04:00
|
|
|
|
2024-01-25 12:52:21 -05:00
|
|
|
const asset = await this.assetRepositoryV1.get(assetId);
|
2022-07-01 12:00:12 -05:00
|
|
|
if (!asset) {
|
|
|
|
|
throw new NotFoundException('Asset not found');
|
|
|
|
|
}
|
2022-04-23 21:08:45 -05:00
|
|
|
|
2023-12-12 09:58:25 -05:00
|
|
|
const filepath = this.getThumbnailPath(asset, dto.format);
|
|
|
|
|
|
2023-12-18 11:33:46 -05:00
|
|
|
return new ImmichFileResponse({
|
|
|
|
|
path: filepath,
|
|
|
|
|
contentType: mimeTypes.lookup(filepath),
|
|
|
|
|
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
|
|
|
|
|
});
|
2022-04-23 21:08:45 -05:00
|
|
|
}
|
|
|
|
|
|
2023-12-12 09:58:25 -05:00
|
|
|
public async serveFile(auth: AuthDto, assetId: string, dto: ServeFileDto): Promise<ImmichFileResponse> {
|
2023-06-28 09:56:24 -04:00
|
|
|
// this is not quite right as sometimes this returns the original still
|
2023-12-09 23:34:12 -05:00
|
|
|
await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId);
|
2023-06-06 15:17:15 -04:00
|
|
|
|
2024-01-25 12:52:21 -05:00
|
|
|
const asset = await this.assetRepository.getById(assetId);
|
2022-04-02 12:31:53 -05:00
|
|
|
if (!asset) {
|
2022-07-01 12:00:12 -05:00
|
|
|
throw new NotFoundException('Asset does not exist');
|
2022-04-02 12:31:53 -05:00
|
|
|
}
|
2022-05-27 14:02:06 -05:00
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
const allowOriginalFile = !!(!auth.sharedLink || auth.sharedLink?.allowDownload);
|
2023-07-13 17:02:49 -04:00
|
|
|
|
|
|
|
|
const filepath =
|
|
|
|
|
asset.type === AssetType.IMAGE
|
2023-12-12 09:58:25 -05:00
|
|
|
? this.getServePath(asset, dto, allowOriginalFile)
|
2023-07-13 17:02:49 -04:00
|
|
|
: asset.encodedVideoPath || asset.originalPath;
|
|
|
|
|
|
2023-12-18 11:33:46 -05:00
|
|
|
return new ImmichFileResponse({
|
|
|
|
|
path: filepath,
|
|
|
|
|
contentType: mimeTypes.lookup(filepath),
|
|
|
|
|
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
|
|
|
|
|
});
|
2022-02-13 15:10:42 -06:00
|
|
|
}
|
|
|
|
|
|
2022-10-25 09:51:03 -05:00
|
|
|
async checkExistingAssets(
|
2023-12-09 23:34:12 -05:00
|
|
|
auth: AuthDto,
|
2022-10-25 09:51:03 -05:00
|
|
|
checkExistingAssetsDto: CheckExistingAssetsDto,
|
|
|
|
|
): Promise<CheckExistingAssetsResponseDto> {
|
2023-05-24 23:08:21 +02:00
|
|
|
return {
|
2024-01-25 12:52:21 -05:00
|
|
|
existingIds: await this.assetRepositoryV1.getExistingAssets(auth.user.id, checkExistingAssetsDto),
|
2023-05-24 23:08:21 +02:00
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
|
2024-05-02 15:42:26 -04:00
|
|
|
const checksums: Buffer[] = dto.assets.map((asset) => fromChecksum(asset.checksum));
|
2024-01-25 12:52:21 -05:00
|
|
|
const results = await this.assetRepositoryV1.getAssetsByChecksums(auth.user.id, checksums);
|
2023-05-27 21:56:17 -04:00
|
|
|
const checksumMap: Record<string, string> = {};
|
2023-05-24 23:08:21 +02:00
|
|
|
|
|
|
|
|
for (const { id, checksum } of results) {
|
2023-05-27 21:56:17 -04:00
|
|
|
checksumMap[checksum.toString('hex')] = id;
|
2023-05-24 23:08:21 +02:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
results: dto.assets.map(({ id, checksum }) => {
|
2024-05-02 15:42:26 -04:00
|
|
|
const duplicate = checksumMap[fromChecksum(checksum).toString('hex')];
|
2023-05-24 23:08:21 +02:00
|
|
|
if (duplicate) {
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
assetId: duplicate,
|
|
|
|
|
action: AssetUploadAction.REJECT,
|
|
|
|
|
reason: AssetRejectReason.DUPLICATE,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// TODO mime-check
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
id,
|
|
|
|
|
action: AssetUploadAction.ACCEPT,
|
|
|
|
|
};
|
|
|
|
|
}),
|
|
|
|
|
};
|
2022-10-25 09:51:03 -05:00
|
|
|
}
|
|
|
|
|
|
2023-03-23 22:40:30 -04:00
|
|
|
private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) {
|
|
|
|
|
switch (format) {
|
2024-02-02 04:18:00 +01:00
|
|
|
case GetAssetThumbnailFormatEnum.WEBP: {
|
2024-04-02 00:56:56 -04:00
|
|
|
if (asset.thumbnailPath) {
|
|
|
|
|
return asset.thumbnailPath;
|
2023-03-23 22:40:30 -04:00
|
|
|
}
|
2023-07-08 16:07:56 -04:00
|
|
|
this.logger.warn(`WebP thumbnail requested but not found for asset ${asset.id}, falling back to JPEG`);
|
2024-02-02 04:18:00 +01:00
|
|
|
}
|
|
|
|
|
case GetAssetThumbnailFormatEnum.JPEG: {
|
2024-04-02 00:56:56 -04:00
|
|
|
if (!asset.previewPath) {
|
2023-07-08 16:07:56 -04:00
|
|
|
throw new NotFoundException(`No thumbnail found for asset ${asset.id}`);
|
2023-03-23 22:40:30 -04:00
|
|
|
}
|
2024-04-02 00:56:56 -04:00
|
|
|
return asset.previewPath;
|
2024-02-02 04:18:00 +01:00
|
|
|
}
|
2023-03-23 22:40:30 -04:00
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-12 09:58:25 -05:00
|
|
|
private getServePath(asset: AssetEntity, dto: ServeFileDto, allowOriginalFile: boolean): string {
|
2023-07-10 13:56:45 -04:00
|
|
|
const mimeType = mimeTypes.lookup(asset.originalPath);
|
|
|
|
|
|
2023-03-23 22:40:30 -04:00
|
|
|
/**
|
|
|
|
|
* Serve file viewer on the web
|
|
|
|
|
*/
|
2023-12-12 09:58:25 -05:00
|
|
|
if (dto.isWeb && mimeType != 'image/gif') {
|
2024-04-02 00:56:56 -04:00
|
|
|
if (!asset.previewPath) {
|
2023-03-23 22:40:30 -04:00
|
|
|
this.logger.error('Error serving IMAGE asset for web');
|
|
|
|
|
throw new InternalServerErrorException(`Failed to serve image asset for web`, 'ServeFile');
|
|
|
|
|
}
|
|
|
|
|
|
2024-04-02 00:56:56 -04:00
|
|
|
return asset.previewPath;
|
2023-03-23 22:40:30 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* Serve thumbnail image for both web and mobile app
|
|
|
|
|
*/
|
2023-12-12 09:58:25 -05:00
|
|
|
if ((!dto.isThumb && allowOriginalFile) || (dto.isWeb && mimeType === 'image/gif')) {
|
2023-07-10 13:56:45 -04:00
|
|
|
return asset.originalPath;
|
2023-03-23 22:40:30 -04:00
|
|
|
}
|
|
|
|
|
|
2024-04-02 00:56:56 -04:00
|
|
|
if (asset.thumbnailPath && asset.thumbnailPath.length > 0) {
|
|
|
|
|
return asset.thumbnailPath;
|
2023-03-23 22:40:30 -04:00
|
|
|
}
|
|
|
|
|
|
2024-04-02 00:56:56 -04:00
|
|
|
if (!asset.previewPath) {
|
|
|
|
|
throw new Error('previewPath not set');
|
2023-03-23 22:40:30 -04:00
|
|
|
}
|
|
|
|
|
|
2024-04-02 00:56:56 -04:00
|
|
|
return asset.previewPath;
|
2023-03-23 22:40:30 -04:00
|
|
|
}
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
private async getLibraryId(auth: AuthDto, libraryId?: string) {
|
2023-09-21 21:35:25 -04:00
|
|
|
if (libraryId) {
|
|
|
|
|
return libraryId;
|
|
|
|
|
}
|
|
|
|
|
|
2023-12-09 23:34:12 -05:00
|
|
|
let library = await this.libraryRepository.getDefaultUploadLibrary(auth.user.id);
|
2023-09-21 21:35:25 -04:00
|
|
|
if (!library) {
|
|
|
|
|
library = await this.libraryRepository.create({
|
2023-12-09 23:34:12 -05:00
|
|
|
ownerId: auth.user.id,
|
2023-09-21 21:35:25 -04:00
|
|
|
name: 'Default Library',
|
|
|
|
|
assets: [],
|
|
|
|
|
type: LibraryType.UPLOAD,
|
|
|
|
|
importPaths: [],
|
|
|
|
|
exclusionPatterns: [],
|
|
|
|
|
isVisible: true,
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return library.id;
|
|
|
|
|
}
|
2024-02-08 16:56:06 -05:00
|
|
|
|
|
|
|
|
private async create(
|
|
|
|
|
auth: AuthDto,
|
|
|
|
|
dto: CreateAssetDto & { libraryId: string },
|
|
|
|
|
file: UploadFile,
|
|
|
|
|
livePhotoAssetId?: string,
|
|
|
|
|
sidecarPath?: string,
|
|
|
|
|
): Promise<AssetEntity> {
|
|
|
|
|
const asset = await this.assetRepository.create({
|
|
|
|
|
ownerId: auth.user.id,
|
|
|
|
|
libraryId: dto.libraryId,
|
|
|
|
|
|
|
|
|
|
checksum: file.checksum,
|
|
|
|
|
originalPath: file.originalPath,
|
|
|
|
|
|
|
|
|
|
deviceAssetId: dto.deviceAssetId,
|
|
|
|
|
deviceId: dto.deviceId,
|
|
|
|
|
|
|
|
|
|
fileCreatedAt: dto.fileCreatedAt,
|
|
|
|
|
fileModifiedAt: dto.fileModifiedAt,
|
|
|
|
|
localDateTime: dto.fileCreatedAt,
|
|
|
|
|
|
|
|
|
|
type: mimeTypes.assetType(file.originalPath),
|
|
|
|
|
isFavorite: dto.isFavorite,
|
|
|
|
|
isArchived: dto.isArchived ?? false,
|
|
|
|
|
duration: dto.duration || null,
|
|
|
|
|
isVisible: dto.isVisible ?? true,
|
|
|
|
|
livePhotoVideo: livePhotoAssetId === null ? null : ({ id: livePhotoAssetId } as AssetEntity),
|
2024-03-06 20:34:55 -06:00
|
|
|
originalFileName: file.originalName,
|
2024-02-08 16:56:06 -05:00
|
|
|
sidecarPath: sidecarPath || null,
|
|
|
|
|
isReadOnly: dto.isReadOnly ?? false,
|
|
|
|
|
isOffline: dto.isOffline ?? false,
|
|
|
|
|
});
|
|
|
|
|
|
2024-02-11 21:40:34 -07:00
|
|
|
if (sidecarPath) {
|
|
|
|
|
await this.storageRepository.utimes(sidecarPath, new Date(), new Date(dto.fileModifiedAt));
|
|
|
|
|
}
|
|
|
|
|
await this.storageRepository.utimes(file.originalPath, new Date(), new Date(dto.fileModifiedAt));
|
2024-02-08 16:56:06 -05:00
|
|
|
await this.assetRepository.upsertExif({ assetId: asset.id, fileSizeInByte: file.size });
|
|
|
|
|
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: { id: asset.id, source: 'upload' } });
|
|
|
|
|
|
|
|
|
|
return asset;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private requireQuota(auth: AuthDto, size: number) {
|
|
|
|
|
if (auth.user.quotaSizeInBytes && auth.user.quotaSizeInBytes < auth.user.quotaUsageInBytes + size) {
|
|
|
|
|
throw new BadRequestException('Quota has been exceeded!');
|
|
|
|
|
}
|
|
|
|
|
}
|
2022-11-29 22:45:47 +01:00
|
|
|
}
|