refactor: asset v1, app.utils (#8152)
This commit is contained in:
220
server/src/services/asset-v1.service.spec.ts
Normal file
220
server/src/services/asset-v1.service.spec.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import { when } from 'jest-when';
|
||||
import { AssetRejectReason, AssetUploadAction } from 'src/dtos/asset-v1-response.dto';
|
||||
import { CreateAssetDto } from 'src/dtos/asset-v1.dto';
|
||||
import { ASSET_CHECKSUM_CONSTRAINT, AssetEntity, AssetType } from 'src/entities/asset.entity';
|
||||
import { ExifEntity } from 'src/entities/exif.entity';
|
||||
import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { AssetServiceV1 } from 'src/services/asset-v1.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
import { authStub } from 'test/fixtures/auth.stub';
|
||||
import { fileStub } from 'test/fixtures/file.stub';
|
||||
import { IAccessRepositoryMock, newAccessRepositoryMock } from 'test/repositories/access.repository.mock';
|
||||
import { newAssetRepositoryMock } from 'test/repositories/asset.repository.mock';
|
||||
import { newJobRepositoryMock } from 'test/repositories/job.repository.mock';
|
||||
import { newLibraryRepositoryMock } from 'test/repositories/library.repository.mock';
|
||||
import { newStorageRepositoryMock } from 'test/repositories/storage.repository.mock';
|
||||
import { newUserRepositoryMock } from 'test/repositories/user.repository.mock';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
|
||||
const _getCreateAssetDto = (): CreateAssetDto => {
|
||||
const createAssetDto = new CreateAssetDto();
|
||||
createAssetDto.deviceAssetId = 'deviceAssetId';
|
||||
createAssetDto.deviceId = 'deviceId';
|
||||
createAssetDto.fileCreatedAt = new Date('2022-06-19T23:41:36.910Z');
|
||||
createAssetDto.fileModifiedAt = new Date('2022-06-19T23:41:36.910Z');
|
||||
createAssetDto.isFavorite = false;
|
||||
createAssetDto.isArchived = false;
|
||||
createAssetDto.duration = '0:00:00.000000';
|
||||
createAssetDto.libraryId = 'libraryId';
|
||||
|
||||
return createAssetDto;
|
||||
};
|
||||
|
||||
const _getAsset_1 = () => {
|
||||
const asset_1 = new AssetEntity();
|
||||
|
||||
asset_1.id = 'id_1';
|
||||
asset_1.ownerId = 'user_id_1';
|
||||
asset_1.deviceAssetId = 'device_asset_id_1';
|
||||
asset_1.deviceId = 'device_id_1';
|
||||
asset_1.type = AssetType.VIDEO;
|
||||
asset_1.originalPath = 'fake_path/asset_1.jpeg';
|
||||
asset_1.resizePath = '';
|
||||
asset_1.fileModifiedAt = new Date('2022-06-19T23:41:36.910Z');
|
||||
asset_1.fileCreatedAt = new Date('2022-06-19T23:41:36.910Z');
|
||||
asset_1.updatedAt = new Date('2022-06-19T23:41:36.910Z');
|
||||
asset_1.isFavorite = false;
|
||||
asset_1.isArchived = false;
|
||||
asset_1.webpPath = '';
|
||||
asset_1.encodedVideoPath = '';
|
||||
asset_1.duration = '0:00:00.000000';
|
||||
asset_1.exifInfo = new ExifEntity();
|
||||
asset_1.exifInfo.latitude = 49.533_547;
|
||||
asset_1.exifInfo.longitude = 10.703_075;
|
||||
return asset_1;
|
||||
};
|
||||
|
||||
describe('AssetService', () => {
|
||||
let sut: AssetServiceV1;
|
||||
let accessMock: IAccessRepositoryMock;
|
||||
let assetRepositoryMockV1: jest.Mocked<IAssetRepositoryV1>;
|
||||
let assetMock: jest.Mocked<IAssetRepository>;
|
||||
let jobMock: jest.Mocked<IJobRepository>;
|
||||
let libraryMock: jest.Mocked<ILibraryRepository>;
|
||||
let storageMock: jest.Mocked<IStorageRepository>;
|
||||
let userMock: jest.Mocked<IUserRepository>;
|
||||
|
||||
beforeEach(() => {
|
||||
assetRepositoryMockV1 = {
|
||||
get: jest.fn(),
|
||||
getAllByUserId: jest.fn(),
|
||||
getDetectedObjectsByUserId: jest.fn(),
|
||||
getLocationsByUserId: jest.fn(),
|
||||
getSearchPropertiesByUserId: jest.fn(),
|
||||
getAssetsByChecksums: jest.fn(),
|
||||
getExistingAssets: jest.fn(),
|
||||
getByOriginalPath: jest.fn(),
|
||||
};
|
||||
|
||||
accessMock = newAccessRepositoryMock();
|
||||
assetMock = newAssetRepositoryMock();
|
||||
jobMock = newJobRepositoryMock();
|
||||
libraryMock = newLibraryRepositoryMock();
|
||||
storageMock = newStorageRepositoryMock();
|
||||
userMock = newUserRepositoryMock();
|
||||
|
||||
sut = new AssetServiceV1(accessMock, assetRepositoryMockV1, assetMock, jobMock, libraryMock, storageMock, userMock);
|
||||
|
||||
when(assetRepositoryMockV1.get)
|
||||
.calledWith(assetStub.livePhotoStillAsset.id)
|
||||
.mockResolvedValue(assetStub.livePhotoStillAsset);
|
||||
when(assetRepositoryMockV1.get)
|
||||
.calledWith(assetStub.livePhotoMotionAsset.id)
|
||||
.mockResolvedValue(assetStub.livePhotoMotionAsset);
|
||||
});
|
||||
|
||||
describe('uploadFile', () => {
|
||||
it('should handle a file upload', async () => {
|
||||
const assetEntity = _getAsset_1();
|
||||
const file = {
|
||||
uuid: 'random-uuid',
|
||||
originalPath: 'fake_path/asset_1.jpeg',
|
||||
mimeType: 'image/jpeg',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
originalName: 'asset_1.jpeg',
|
||||
size: 42,
|
||||
};
|
||||
const dto = _getCreateAssetDto();
|
||||
|
||||
assetMock.create.mockResolvedValue(assetEntity);
|
||||
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
||||
|
||||
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' });
|
||||
|
||||
expect(assetMock.create).toHaveBeenCalled();
|
||||
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, file.size);
|
||||
expect(storageMock.utimes).toHaveBeenCalledWith(
|
||||
file.originalPath,
|
||||
expect.any(Date),
|
||||
new Date(dto.fileModifiedAt),
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle a duplicate', async () => {
|
||||
const file = {
|
||||
uuid: 'random-uuid',
|
||||
originalPath: 'fake_path/asset_1.jpeg',
|
||||
mimeType: 'image/jpeg',
|
||||
checksum: Buffer.from('file hash', 'utf8'),
|
||||
originalName: 'asset_1.jpeg',
|
||||
size: 0,
|
||||
};
|
||||
const dto = _getCreateAssetDto();
|
||||
const error = new QueryFailedError('', [], new Error('unique key violation'));
|
||||
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
|
||||
|
||||
assetMock.create.mockRejectedValue(error);
|
||||
assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([_getAsset_1()]);
|
||||
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
||||
|
||||
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' });
|
||||
|
||||
expect(jobMock.queue).toHaveBeenCalledWith({
|
||||
name: JobName.DELETE_FILES,
|
||||
data: { files: ['fake_path/asset_1.jpeg', undefined, undefined] },
|
||||
});
|
||||
expect(userMock.updateUsage).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle a live photo', async () => {
|
||||
const dto = _getCreateAssetDto();
|
||||
const error = new QueryFailedError('', [], new Error('unique key violation'));
|
||||
(error as any).constraint = ASSET_CHECKSUM_CONSTRAINT;
|
||||
|
||||
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoMotionAsset);
|
||||
assetMock.create.mockResolvedValueOnce(assetStub.livePhotoStillAsset);
|
||||
accessMock.library.checkOwnerAccess.mockResolvedValue(new Set([dto.libraryId!]));
|
||||
|
||||
await expect(
|
||||
sut.uploadFile(authStub.user1, dto, fileStub.livePhotoStill, fileStub.livePhotoMotion),
|
||||
).resolves.toEqual({
|
||||
duplicate: false,
|
||||
id: 'live-photo-still-asset',
|
||||
});
|
||||
|
||||
expect(jobMock.queue.mock.calls).toEqual([
|
||||
[
|
||||
{
|
||||
name: JobName.METADATA_EXTRACTION,
|
||||
data: { id: assetStub.livePhotoMotionAsset.id, source: 'upload' },
|
||||
},
|
||||
],
|
||||
[{ name: JobName.METADATA_EXTRACTION, data: { id: assetStub.livePhotoStillAsset.id, source: 'upload' } }],
|
||||
]);
|
||||
expect(userMock.updateUsage).toHaveBeenCalledWith(authStub.user1.user.id, 111);
|
||||
expect(storageMock.utimes).toHaveBeenCalledWith(
|
||||
fileStub.livePhotoStill.originalPath,
|
||||
expect.any(Date),
|
||||
new Date(dto.fileModifiedAt),
|
||||
);
|
||||
expect(storageMock.utimes).toHaveBeenCalledWith(
|
||||
fileStub.livePhotoMotion.originalPath,
|
||||
expect.any(Date),
|
||||
new Date(dto.fileModifiedAt),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('bulkUploadCheck', () => {
|
||||
it('should accept hex and base64 checksums', async () => {
|
||||
const file1 = Buffer.from('d2947b871a706081be194569951b7db246907957', 'hex');
|
||||
const file2 = Buffer.from('53be335e99f18a66ff12e9a901c7a6171dd76573', 'hex');
|
||||
|
||||
assetRepositoryMockV1.getAssetsByChecksums.mockResolvedValue([
|
||||
{ id: 'asset-1', checksum: file1 },
|
||||
{ id: 'asset-2', checksum: file2 },
|
||||
]);
|
||||
|
||||
await expect(
|
||||
sut.bulkUploadCheck(authStub.admin, {
|
||||
assets: [
|
||||
{ id: '1', checksum: file1.toString('hex') },
|
||||
{ id: '2', checksum: file2.toString('base64') },
|
||||
],
|
||||
}),
|
||||
).resolves.toEqual({
|
||||
results: [
|
||||
{ id: '1', assetId: 'asset-1', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE },
|
||||
{ id: '2', assetId: 'asset-2', action: AssetUploadAction.REJECT, reason: AssetRejectReason.DUPLICATE },
|
||||
],
|
||||
});
|
||||
|
||||
expect(assetRepositoryMockV1.getAssetsByChecksums).toHaveBeenCalledWith(authStub.admin.user.id, [file1, file2]);
|
||||
});
|
||||
});
|
||||
});
|
||||
366
server/src/services/asset-v1.service.ts
Normal file
366
server/src/services/asset-v1.service.ts
Normal file
@@ -0,0 +1,366 @@
|
||||
import {
|
||||
BadRequestException,
|
||||
Inject,
|
||||
Injectable,
|
||||
InternalServerErrorException,
|
||||
NotFoundException,
|
||||
} from '@nestjs/common';
|
||||
import { AccessCore, Permission } from 'src/cores/access.core';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import {
|
||||
AssetBulkUploadCheckResponseDto,
|
||||
AssetFileUploadResponseDto,
|
||||
AssetRejectReason,
|
||||
AssetUploadAction,
|
||||
CheckExistingAssetsResponseDto,
|
||||
CuratedLocationsResponseDto,
|
||||
CuratedObjectsResponseDto,
|
||||
} 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';
|
||||
import { IAccessRepository } from 'src/interfaces/access.interface';
|
||||
import { IAssetRepositoryV1 } from 'src/interfaces/asset-v1.interface';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { IJobRepository, JobName } from 'src/interfaces/job.interface';
|
||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
import { UploadFile } from 'src/services/asset.service';
|
||||
import { CacheControl, ImmichFileResponse, getLivePhotoMotionFilename } from 'src/utils/file';
|
||||
import { ImmichLogger } from 'src/utils/logger';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
import { QueryFailedError } from 'typeorm';
|
||||
|
||||
@Injectable()
|
||||
/** @deprecated */
|
||||
export class AssetServiceV1 {
|
||||
readonly logger = new ImmichLogger(AssetServiceV1.name);
|
||||
private access: AccessCore;
|
||||
|
||||
constructor(
|
||||
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
||||
@Inject(IAssetRepositoryV1) private assetRepositoryV1: IAssetRepositoryV1,
|
||||
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||
@Inject(ILibraryRepository) private libraryRepository: ILibraryRepository,
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(IUserRepository) private userRepository: IUserRepository,
|
||||
) {
|
||||
this.access = AccessCore.create(accessRepository);
|
||||
}
|
||||
|
||||
public async uploadFile(
|
||||
auth: AuthDto,
|
||||
dto: CreateAssetDto,
|
||||
file: UploadFile,
|
||||
livePhotoFile?: UploadFile,
|
||||
sidecarFile?: UploadFile,
|
||||
): Promise<AssetFileUploadResponseDto> {
|
||||
if (livePhotoFile) {
|
||||
livePhotoFile = {
|
||||
...livePhotoFile,
|
||||
originalName: getLivePhotoMotionFilename(file.originalName, livePhotoFile.originalName),
|
||||
};
|
||||
}
|
||||
|
||||
let livePhotoAsset: AssetEntity | null = null;
|
||||
|
||||
try {
|
||||
const libraryId = await this.getLibraryId(auth, dto.libraryId);
|
||||
await this.access.requirePermission(auth, Permission.ASSET_UPLOAD, libraryId);
|
||||
this.requireQuota(auth, file.size);
|
||||
if (livePhotoFile) {
|
||||
const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false, libraryId };
|
||||
livePhotoAsset = await this.create(auth, livePhotoDto, livePhotoFile);
|
||||
}
|
||||
|
||||
const asset = await this.create(auth, { ...dto, libraryId }, file, livePhotoAsset?.id, sidecarFile?.originalPath);
|
||||
|
||||
await this.userRepository.updateUsage(auth.user.id, (livePhotoFile?.size || 0) + file.size);
|
||||
|
||||
return { id: asset.id, duplicate: false };
|
||||
} catch (error: any) {
|
||||
// clean up files
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.DELETE_FILES,
|
||||
data: { files: [file.originalPath, livePhotoFile?.originalPath, sidecarFile?.originalPath] },
|
||||
});
|
||||
|
||||
// handle duplicates with a success response
|
||||
if (error instanceof QueryFailedError && (error as any).constraint === ASSET_CHECKSUM_CONSTRAINT) {
|
||||
const checksums = [file.checksum, livePhotoFile?.checksum].filter((checksum): checksum is Buffer => !!checksum);
|
||||
const [duplicate] = await this.assetRepositoryV1.getAssetsByChecksums(auth.user.id, checksums);
|
||||
return { id: duplicate.id, duplicate: true };
|
||||
}
|
||||
|
||||
this.logger.error(`Error uploading file ${error}`, error?.stack);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
const assets = await this.assetRepositoryV1.getAllByUserId(userId, dto);
|
||||
return assets.map((asset) => mapAsset(asset, { withStack: true, auth }));
|
||||
}
|
||||
|
||||
async serveThumbnail(auth: AuthDto, assetId: string, dto: GetAssetThumbnailDto): Promise<ImmichFileResponse> {
|
||||
await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId);
|
||||
|
||||
const asset = await this.assetRepositoryV1.get(assetId);
|
||||
if (!asset) {
|
||||
throw new NotFoundException('Asset not found');
|
||||
}
|
||||
|
||||
const filepath = this.getThumbnailPath(asset, dto.format);
|
||||
|
||||
return new ImmichFileResponse({
|
||||
path: filepath,
|
||||
contentType: mimeTypes.lookup(filepath),
|
||||
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
|
||||
});
|
||||
}
|
||||
|
||||
public async serveFile(auth: AuthDto, assetId: string, dto: ServeFileDto): Promise<ImmichFileResponse> {
|
||||
// this is not quite right as sometimes this returns the original still
|
||||
await this.access.requirePermission(auth, Permission.ASSET_VIEW, assetId);
|
||||
|
||||
const asset = await this.assetRepository.getById(assetId);
|
||||
if (!asset) {
|
||||
throw new NotFoundException('Asset does not exist');
|
||||
}
|
||||
|
||||
const allowOriginalFile = !!(!auth.sharedLink || auth.sharedLink?.allowDownload);
|
||||
|
||||
const filepath =
|
||||
asset.type === AssetType.IMAGE
|
||||
? this.getServePath(asset, dto, allowOriginalFile)
|
||||
: asset.encodedVideoPath || asset.originalPath;
|
||||
|
||||
return new ImmichFileResponse({
|
||||
path: filepath,
|
||||
contentType: mimeTypes.lookup(filepath),
|
||||
cacheControl: CacheControl.PRIVATE_WITH_CACHE,
|
||||
});
|
||||
}
|
||||
|
||||
async getAssetSearchTerm(auth: AuthDto): Promise<string[]> {
|
||||
const possibleSearchTerm = new Set<string>();
|
||||
|
||||
const rows = await this.assetRepositoryV1.getSearchPropertiesByUserId(auth.user.id);
|
||||
|
||||
for (const row of rows) {
|
||||
// tags
|
||||
row.tags?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase()));
|
||||
|
||||
// objects
|
||||
row.objects?.map((object: string) => possibleSearchTerm.add(object?.toLowerCase()));
|
||||
|
||||
// asset's tyoe
|
||||
possibleSearchTerm.add(row.assetType?.toLowerCase() || '');
|
||||
|
||||
// image orientation
|
||||
possibleSearchTerm.add(row.orientation?.toLowerCase() || '');
|
||||
|
||||
// Lens model
|
||||
possibleSearchTerm.add(row.lensModel?.toLowerCase() || '');
|
||||
|
||||
// Make and model
|
||||
possibleSearchTerm.add(row.make?.toLowerCase() || '');
|
||||
possibleSearchTerm.add(row.model?.toLowerCase() || '');
|
||||
|
||||
// Location
|
||||
possibleSearchTerm.add(row.city?.toLowerCase() || '');
|
||||
possibleSearchTerm.add(row.state?.toLowerCase() || '');
|
||||
possibleSearchTerm.add(row.country?.toLowerCase() || '');
|
||||
}
|
||||
|
||||
return [...possibleSearchTerm].filter((x) => x != null && x != '');
|
||||
}
|
||||
|
||||
async getCuratedLocation(auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
|
||||
return this.assetRepositoryV1.getLocationsByUserId(auth.user.id);
|
||||
}
|
||||
|
||||
async getCuratedObject(auth: AuthDto): Promise<CuratedObjectsResponseDto[]> {
|
||||
return this.assetRepositoryV1.getDetectedObjectsByUserId(auth.user.id);
|
||||
}
|
||||
|
||||
async checkExistingAssets(
|
||||
auth: AuthDto,
|
||||
checkExistingAssetsDto: CheckExistingAssetsDto,
|
||||
): Promise<CheckExistingAssetsResponseDto> {
|
||||
return {
|
||||
existingIds: await this.assetRepositoryV1.getExistingAssets(auth.user.id, checkExistingAssetsDto),
|
||||
};
|
||||
}
|
||||
|
||||
async bulkUploadCheck(auth: AuthDto, dto: AssetBulkUploadCheckDto): Promise<AssetBulkUploadCheckResponseDto> {
|
||||
// support base64 and hex checksums
|
||||
for (const asset of dto.assets) {
|
||||
if (asset.checksum.length === 28) {
|
||||
asset.checksum = Buffer.from(asset.checksum, 'base64').toString('hex');
|
||||
}
|
||||
}
|
||||
|
||||
const checksums: Buffer[] = dto.assets.map((asset) => Buffer.from(asset.checksum, 'hex'));
|
||||
const results = await this.assetRepositoryV1.getAssetsByChecksums(auth.user.id, checksums);
|
||||
const checksumMap: Record<string, string> = {};
|
||||
|
||||
for (const { id, checksum } of results) {
|
||||
checksumMap[checksum.toString('hex')] = id;
|
||||
}
|
||||
|
||||
return {
|
||||
results: dto.assets.map(({ id, checksum }) => {
|
||||
const duplicate = checksumMap[checksum];
|
||||
if (duplicate) {
|
||||
return {
|
||||
id,
|
||||
assetId: duplicate,
|
||||
action: AssetUploadAction.REJECT,
|
||||
reason: AssetRejectReason.DUPLICATE,
|
||||
};
|
||||
}
|
||||
|
||||
// TODO mime-check
|
||||
|
||||
return {
|
||||
id,
|
||||
action: AssetUploadAction.ACCEPT,
|
||||
};
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) {
|
||||
switch (format) {
|
||||
case GetAssetThumbnailFormatEnum.WEBP: {
|
||||
if (asset.webpPath) {
|
||||
return asset.webpPath;
|
||||
}
|
||||
this.logger.warn(`WebP thumbnail requested but not found for asset ${asset.id}, falling back to JPEG`);
|
||||
}
|
||||
case GetAssetThumbnailFormatEnum.JPEG: {
|
||||
if (!asset.resizePath) {
|
||||
throw new NotFoundException(`No thumbnail found for asset ${asset.id}`);
|
||||
}
|
||||
return asset.resizePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getServePath(asset: AssetEntity, dto: ServeFileDto, allowOriginalFile: boolean): string {
|
||||
const mimeType = mimeTypes.lookup(asset.originalPath);
|
||||
|
||||
/**
|
||||
* Serve file viewer on the web
|
||||
*/
|
||||
if (dto.isWeb && mimeType != 'image/gif') {
|
||||
if (!asset.resizePath) {
|
||||
this.logger.error('Error serving IMAGE asset for web');
|
||||
throw new InternalServerErrorException(`Failed to serve image asset for web`, 'ServeFile');
|
||||
}
|
||||
|
||||
return asset.resizePath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Serve thumbnail image for both web and mobile app
|
||||
*/
|
||||
if ((!dto.isThumb && allowOriginalFile) || (dto.isWeb && mimeType === 'image/gif')) {
|
||||
return asset.originalPath;
|
||||
}
|
||||
|
||||
if (asset.webpPath && asset.webpPath.length > 0) {
|
||||
return asset.webpPath;
|
||||
}
|
||||
|
||||
if (!asset.resizePath) {
|
||||
throw new Error('resizePath not set');
|
||||
}
|
||||
|
||||
return asset.resizePath;
|
||||
}
|
||||
|
||||
private async getLibraryId(auth: AuthDto, libraryId?: string) {
|
||||
if (libraryId) {
|
||||
return libraryId;
|
||||
}
|
||||
|
||||
let library = await this.libraryRepository.getDefaultUploadLibrary(auth.user.id);
|
||||
if (!library) {
|
||||
library = await this.libraryRepository.create({
|
||||
ownerId: auth.user.id,
|
||||
name: 'Default Library',
|
||||
assets: [],
|
||||
type: LibraryType.UPLOAD,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
isVisible: true,
|
||||
});
|
||||
}
|
||||
|
||||
return library.id;
|
||||
}
|
||||
|
||||
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),
|
||||
originalFileName: file.originalName,
|
||||
sidecarPath: sidecarPath || null,
|
||||
isReadOnly: dto.isReadOnly ?? false,
|
||||
isOffline: dto.isOffline ?? false,
|
||||
});
|
||||
|
||||
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));
|
||||
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!');
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user