mirror of
https://github.com/immich-app/immich.git
synced 2025-01-12 15:32:36 +02:00
refactor(server): asset service - upload asset (#1438)
* refactor: asset upload * refactor: background service * chore: tests * Regenerate api --------- Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
This commit is contained in:
parent
3210302ecd
commit
9428b2576b
BIN
mobile/openapi/README.md
generated
BIN
mobile/openapi/README.md
generated
Binary file not shown.
BIN
mobile/openapi/doc/AssetFileUploadResponseDto.md
generated
BIN
mobile/openapi/doc/AssetFileUploadResponseDto.md
generated
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,10 +1,9 @@
|
|||||||
import { SearchPropertiesDto } from './dto/search-properties.dto';
|
import { SearchPropertiesDto } from './dto/search-properties.dto';
|
||||||
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
||||||
import { AssetEntity, AssetType } from '@app/infra';
|
import { AssetEntity, AssetType } from '@app/infra';
|
||||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
import { Inject, Injectable } from '@nestjs/common';
|
||||||
import { InjectRepository } from '@nestjs/typeorm';
|
import { InjectRepository } from '@nestjs/typeorm';
|
||||||
import { Repository } from 'typeorm/repository/Repository';
|
import { Repository } from 'typeorm/repository/Repository';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
|
||||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
||||||
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
|
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
|
||||||
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
||||||
@ -19,15 +18,10 @@ import { IsNull, Not } from 'typeorm';
|
|||||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||||
|
|
||||||
export interface IAssetRepository {
|
export interface IAssetRepository {
|
||||||
create(
|
get(id: string): Promise<AssetEntity | null>;
|
||||||
createAssetDto: CreateAssetDto,
|
create(asset: Omit<AssetEntity, 'id'>): Promise<AssetEntity>;
|
||||||
ownerId: string,
|
remove(asset: AssetEntity): Promise<void>;
|
||||||
originalPath: string,
|
|
||||||
mimeType: string,
|
|
||||||
isVisible: boolean,
|
|
||||||
checksum?: Buffer,
|
|
||||||
livePhotoAssetEntity?: AssetEntity,
|
|
||||||
): Promise<AssetEntity>;
|
|
||||||
update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity>;
|
update(userId: string, asset: AssetEntity, dto: UpdateAssetDto): Promise<AssetEntity>;
|
||||||
getAll(): Promise<AssetEntity[]>;
|
getAll(): Promise<AssetEntity[]>;
|
||||||
getAllVideos(): Promise<AssetEntity[]>;
|
getAllVideos(): Promise<AssetEntity[]>;
|
||||||
@ -282,44 +276,16 @@ export class AssetRepository implements IAssetRepository {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
get(id: string): Promise<AssetEntity | null> {
|
||||||
* Create new asset information in database
|
return this.assetRepository.findOne({ where: { id } });
|
||||||
* @param createAssetDto
|
|
||||||
* @param ownerId
|
|
||||||
* @param originalPath
|
|
||||||
* @param mimeType
|
|
||||||
* @returns Promise<AssetEntity>
|
|
||||||
*/
|
|
||||||
async create(
|
|
||||||
createAssetDto: CreateAssetDto,
|
|
||||||
ownerId: string,
|
|
||||||
originalPath: string,
|
|
||||||
mimeType: string,
|
|
||||||
isVisible: boolean,
|
|
||||||
checksum?: Buffer,
|
|
||||||
livePhotoAssetEntity?: AssetEntity,
|
|
||||||
): Promise<AssetEntity> {
|
|
||||||
const asset = new AssetEntity();
|
|
||||||
asset.deviceAssetId = createAssetDto.deviceAssetId;
|
|
||||||
asset.userId = ownerId;
|
|
||||||
asset.deviceId = createAssetDto.deviceId;
|
|
||||||
asset.type = !isVisible ? AssetType.VIDEO : createAssetDto.assetType || AssetType.OTHER; // If an asset is not visible, it is a LivePhotos video portion, therefore we can confidently assign the type as VIDEO here
|
|
||||||
asset.originalPath = originalPath;
|
|
||||||
asset.createdAt = createAssetDto.createdAt;
|
|
||||||
asset.modifiedAt = createAssetDto.modifiedAt;
|
|
||||||
asset.isFavorite = createAssetDto.isFavorite;
|
|
||||||
asset.mimeType = mimeType;
|
|
||||||
asset.duration = createAssetDto.duration || null;
|
|
||||||
asset.checksum = checksum || null;
|
|
||||||
asset.isVisible = isVisible;
|
|
||||||
asset.livePhotoVideoId = livePhotoAssetEntity ? livePhotoAssetEntity.id : null;
|
|
||||||
|
|
||||||
const createdAsset = await this.assetRepository.save(asset);
|
|
||||||
|
|
||||||
if (!createdAsset) {
|
|
||||||
throw new BadRequestException('Asset not created');
|
|
||||||
}
|
}
|
||||||
return createdAsset;
|
|
||||||
|
async create(asset: Omit<AssetEntity, 'id'>): Promise<AssetEntity> {
|
||||||
|
return this.assetRepository.save(asset);
|
||||||
|
}
|
||||||
|
|
||||||
|
async remove(asset: AssetEntity): Promise<void> {
|
||||||
|
await this.assetRepository.remove(asset);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -19,11 +19,9 @@ import {
|
|||||||
import { Authenticated } from '../../decorators/authenticated.decorator';
|
import { Authenticated } from '../../decorators/authenticated.decorator';
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
import { FileFieldsInterceptor } from '@nestjs/platform-express';
|
import { FileFieldsInterceptor } from '@nestjs/platform-express';
|
||||||
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
|
|
||||||
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
import { AuthUserDto, GetAuthUser } from '../../decorators/auth-user.decorator';
|
||||||
import { ServeFileDto } from './dto/serve-file.dto';
|
import { ServeFileDto } from './dto/serve-file.dto';
|
||||||
import { Response as Res } from 'express';
|
import { Response as Res } from 'express';
|
||||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
|
||||||
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
import { DeleteAssetDto } from './dto/delete-asset.dto';
|
||||||
import { SearchAssetDto } from './dto/search-asset.dto';
|
import { SearchAssetDto } from './dto/search-asset.dto';
|
||||||
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||||
@ -33,9 +31,9 @@ import { CuratedLocationsResponseDto } from './response-dto/curated-locations-re
|
|||||||
import { AssetResponseDto } from '@app/domain';
|
import { AssetResponseDto } from '@app/domain';
|
||||||
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
|
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
|
||||||
import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
|
import { AssetFileUploadDto } from './dto/asset-file-upload.dto';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto, mapToUploadFile } from './dto/create-asset.dto';
|
||||||
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
|
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
|
||||||
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
import { DeleteAssetResponseDto } from './response-dto/delete-asset-response.dto';
|
||||||
import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
|
import { GetAssetThumbnailDto } from './dto/get-asset-thumbnail.dto';
|
||||||
import { AssetCountByTimeBucketResponseDto } from './response-dto/asset-count-by-time-group-response.dto';
|
import { AssetCountByTimeBucketResponseDto } from './response-dto/asset-count-by-time-group-response.dto';
|
||||||
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
|
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
|
||||||
@ -55,12 +53,13 @@ import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
|||||||
import { SharedLinkResponseDto } from '@app/domain';
|
import { SharedLinkResponseDto } from '@app/domain';
|
||||||
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
|
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
|
||||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||||
|
import { assetUploadOption, ImmichFile } from '../../config/asset-upload.config';
|
||||||
|
|
||||||
@ApiBearerAuth()
|
@ApiBearerAuth()
|
||||||
@ApiTags('Asset')
|
@ApiTags('Asset')
|
||||||
@Controller('asset')
|
@Controller('asset')
|
||||||
export class AssetController {
|
export class AssetController {
|
||||||
constructor(private assetService: AssetService, private backgroundTaskService: BackgroundTaskService) {}
|
constructor(private assetService: AssetService) {}
|
||||||
|
|
||||||
@Authenticated({ isShared: true })
|
@Authenticated({ isShared: true })
|
||||||
@Post('upload')
|
@Post('upload')
|
||||||
@ -81,13 +80,22 @@ export class AssetController {
|
|||||||
async uploadFile(
|
async uploadFile(
|
||||||
@GetAuthUser() authUser: AuthUserDto,
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
@UploadedFiles() files: { assetData: ImmichFile[]; livePhotoData?: ImmichFile[] },
|
@UploadedFiles() files: { assetData: ImmichFile[]; livePhotoData?: ImmichFile[] },
|
||||||
@Body(ValidationPipe) createAssetDto: CreateAssetDto,
|
@Body(ValidationPipe) dto: CreateAssetDto,
|
||||||
@Response({ passthrough: true }) res: Res,
|
@Response({ passthrough: true }) res: Res,
|
||||||
): Promise<AssetFileUploadResponseDto> {
|
): Promise<AssetFileUploadResponseDto> {
|
||||||
const originalAssetData = files.assetData[0];
|
const file = mapToUploadFile(files.assetData[0]);
|
||||||
const livePhotoAssetData = files.livePhotoData?.[0];
|
const _livePhotoFile = files.livePhotoData?.[0];
|
||||||
|
let livePhotoFile;
|
||||||
|
if (_livePhotoFile) {
|
||||||
|
livePhotoFile = mapToUploadFile(_livePhotoFile);
|
||||||
|
}
|
||||||
|
|
||||||
return this.assetService.handleUploadedAsset(authUser, createAssetDto, res, originalAssetData, livePhotoAssetData);
|
const responseDto = await this.assetService.uploadFile(authUser, dto, file, livePhotoFile);
|
||||||
|
if (responseDto.duplicate) {
|
||||||
|
res.send(200);
|
||||||
|
}
|
||||||
|
|
||||||
|
return responseDto;
|
||||||
}
|
}
|
||||||
|
|
||||||
@Authenticated({ isShared: true })
|
@Authenticated({ isShared: true })
|
||||||
@ -276,37 +284,10 @@ export class AssetController {
|
|||||||
@Delete('/')
|
@Delete('/')
|
||||||
async deleteAsset(
|
async deleteAsset(
|
||||||
@GetAuthUser() authUser: AuthUserDto,
|
@GetAuthUser() authUser: AuthUserDto,
|
||||||
@Body(ValidationPipe) assetIds: DeleteAssetDto,
|
@Body(ValidationPipe) dto: DeleteAssetDto,
|
||||||
): Promise<DeleteAssetResponseDto[]> {
|
): Promise<DeleteAssetResponseDto[]> {
|
||||||
await this.assetService.checkAssetsAccess(authUser, assetIds.ids, true);
|
await this.assetService.checkAssetsAccess(authUser, dto.ids, true);
|
||||||
|
return this.assetService.deleteAll(authUser, dto);
|
||||||
const deleteAssetList: AssetResponseDto[] = [];
|
|
||||||
|
|
||||||
for (const id of assetIds.ids) {
|
|
||||||
const assets = await this.assetService.getAssetById(authUser, id);
|
|
||||||
if (!assets) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
deleteAssetList.push(assets);
|
|
||||||
|
|
||||||
if (assets.livePhotoVideoId) {
|
|
||||||
const livePhotoVideo = await this.assetService.getAssetById(authUser, assets.livePhotoVideoId);
|
|
||||||
if (livePhotoVideo) {
|
|
||||||
deleteAssetList.push(livePhotoVideo);
|
|
||||||
assetIds.ids = [...assetIds.ids, livePhotoVideo.id];
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const result = await this.assetService.deleteAssetById(assetIds);
|
|
||||||
|
|
||||||
result.forEach((res) => {
|
|
||||||
deleteAssetList.filter((a) => a.id == res.id && res.status == DeleteAssetStatusEnum.SUCCESS);
|
|
||||||
});
|
|
||||||
|
|
||||||
await this.backgroundTaskService.deleteFileOnDisk(deleteAssetList as any[]);
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
52
server/apps/immich/src/api-v1/asset/asset.core.ts
Normal file
52
server/apps/immich/src/api-v1/asset/asset.core.ts
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
import { timeUtils } from '@app/common';
|
||||||
|
import { AuthUserDto, IJobRepository, JobName } from '@app/domain';
|
||||||
|
import { AssetEntity } from '@app/infra/db/entities';
|
||||||
|
import { StorageService } from '@app/storage';
|
||||||
|
import { IAssetRepository } from './asset-repository';
|
||||||
|
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
|
||||||
|
|
||||||
|
export class AssetCore {
|
||||||
|
constructor(
|
||||||
|
private repository: IAssetRepository,
|
||||||
|
private jobRepository: IJobRepository,
|
||||||
|
private storageService: StorageService,
|
||||||
|
) {}
|
||||||
|
|
||||||
|
async create(
|
||||||
|
authUser: AuthUserDto,
|
||||||
|
dto: CreateAssetDto,
|
||||||
|
file: UploadFile,
|
||||||
|
livePhotoAssetId?: string,
|
||||||
|
): Promise<AssetEntity> {
|
||||||
|
let asset = await this.repository.create({
|
||||||
|
userId: authUser.id,
|
||||||
|
|
||||||
|
mimeType: file.mimeType,
|
||||||
|
checksum: file.checksum || null,
|
||||||
|
originalPath: file.originalPath,
|
||||||
|
|
||||||
|
createdAt: timeUtils.checkValidTimestamp(dto.createdAt) ? dto.createdAt : new Date().toISOString(),
|
||||||
|
modifiedAt: timeUtils.checkValidTimestamp(dto.modifiedAt) ? dto.modifiedAt : new Date().toISOString(),
|
||||||
|
|
||||||
|
deviceAssetId: dto.deviceAssetId,
|
||||||
|
deviceId: dto.deviceId,
|
||||||
|
|
||||||
|
type: dto.assetType,
|
||||||
|
isFavorite: dto.isFavorite,
|
||||||
|
duration: dto.duration || null,
|
||||||
|
isVisible: dto.isVisible ?? true,
|
||||||
|
livePhotoVideoId: livePhotoAssetId || null,
|
||||||
|
resizePath: null,
|
||||||
|
webpPath: null,
|
||||||
|
encodedVideoPath: null,
|
||||||
|
tags: [],
|
||||||
|
sharedLinks: [],
|
||||||
|
});
|
||||||
|
|
||||||
|
asset = await this.storageService.moveAsset(asset, file.originalName);
|
||||||
|
|
||||||
|
await this.jobRepository.add({ name: JobName.ASSET_UPLOADED, data: { asset, fileName: file.originalName } });
|
||||||
|
|
||||||
|
return asset;
|
||||||
|
}
|
||||||
|
}
|
@ -3,8 +3,6 @@ import { AssetService } from './asset.service';
|
|||||||
import { AssetController } from './asset.controller';
|
import { AssetController } from './asset.controller';
|
||||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||||
import { AssetEntity } from '@app/infra';
|
import { AssetEntity } from '@app/infra';
|
||||||
import { BackgroundTaskModule } from '../../modules/background-task/background-task.module';
|
|
||||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
|
||||||
import { CommunicationModule } from '../communication/communication.module';
|
import { CommunicationModule } from '../communication/communication.module';
|
||||||
import { AssetRepository, IAssetRepository } from './asset-repository';
|
import { AssetRepository, IAssetRepository } from './asset-repository';
|
||||||
import { DownloadModule } from '../../modules/download/download.module';
|
import { DownloadModule } from '../../modules/download/download.module';
|
||||||
@ -21,14 +19,13 @@ const ASSET_REPOSITORY_PROVIDER = {
|
|||||||
imports: [
|
imports: [
|
||||||
TypeOrmModule.forFeature([AssetEntity]),
|
TypeOrmModule.forFeature([AssetEntity]),
|
||||||
CommunicationModule,
|
CommunicationModule,
|
||||||
BackgroundTaskModule,
|
|
||||||
DownloadModule,
|
DownloadModule,
|
||||||
TagModule,
|
TagModule,
|
||||||
StorageModule,
|
StorageModule,
|
||||||
forwardRef(() => AlbumModule),
|
forwardRef(() => AlbumModule),
|
||||||
],
|
],
|
||||||
controllers: [AssetController],
|
controllers: [AssetController],
|
||||||
providers: [AssetService, BackgroundTaskService, ASSET_REPOSITORY_PROVIDER],
|
providers: [AssetService, ASSET_REPOSITORY_PROVIDER],
|
||||||
exports: [ASSET_REPOSITORY_PROVIDER],
|
exports: [ASSET_REPOSITORY_PROVIDER],
|
||||||
})
|
})
|
||||||
export class AssetModule {}
|
export class AssetModule {}
|
||||||
|
@ -1,17 +1,15 @@
|
|||||||
import { IAssetRepository } from './asset-repository';
|
import { IAssetRepository } from './asset-repository';
|
||||||
import { AuthUserDto } from '../../decorators/auth-user.decorator';
|
|
||||||
import { AssetService } from './asset.service';
|
import { AssetService } from './asset.service';
|
||||||
import { Repository } from 'typeorm';
|
import { QueryFailedError, Repository } from 'typeorm';
|
||||||
import { AssetEntity, AssetType } from '@app/infra';
|
import { AssetEntity, AssetType } from '@app/infra';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||||
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
|
import { AssetCountByTimeBucket } from './response-dto/asset-count-by-time-group-response.dto';
|
||||||
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
import { TimeGroupEnum } from './dto/get-asset-count-by-time-bucket.dto';
|
||||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||||
import { DownloadService } from '../../modules/download/download.service';
|
import { DownloadService } from '../../modules/download/download.service';
|
||||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
|
||||||
import { AlbumRepository, IAlbumRepository } from '../album/album-repository';
|
import { AlbumRepository, IAlbumRepository } from '../album/album-repository';
|
||||||
import { StorageService } from '@app/storage';
|
import { StorageService } from '@app/storage';
|
||||||
import { ICryptoRepository, IJobRepository, ISharedLinkRepository } from '@app/domain';
|
import { ICryptoRepository, IJobRepository, ISharedLinkRepository, JobName } from '@app/domain';
|
||||||
import {
|
import {
|
||||||
authStub,
|
authStub,
|
||||||
newCryptoRepositoryMock,
|
newCryptoRepositoryMock,
|
||||||
@ -23,24 +21,7 @@ import {
|
|||||||
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
||||||
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||||
|
|
||||||
describe('AssetService', () => {
|
const _getCreateAssetDto = (): CreateAssetDto => {
|
||||||
let sui: AssetService;
|
|
||||||
let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
|
|
||||||
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
|
|
||||||
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
|
||||||
let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
|
|
||||||
let backgroundTaskServiceMock: jest.Mocked<BackgroundTaskService>;
|
|
||||||
let storageSeriveMock: jest.Mocked<StorageService>;
|
|
||||||
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
|
|
||||||
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
|
||||||
let jobMock: jest.Mocked<IJobRepository>;
|
|
||||||
const authUser: AuthUserDto = Object.freeze({
|
|
||||||
id: 'user_id_1',
|
|
||||||
email: 'auth@test.com',
|
|
||||||
isAdmin: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const _getCreateAssetDto = (): CreateAssetDto => {
|
|
||||||
const createAssetDto = new CreateAssetDto();
|
const createAssetDto = new CreateAssetDto();
|
||||||
createAssetDto.deviceAssetId = 'deviceAssetId';
|
createAssetDto.deviceAssetId = 'deviceAssetId';
|
||||||
createAssetDto.deviceId = 'deviceId';
|
createAssetDto.deviceId = 'deviceId';
|
||||||
@ -51,9 +32,9 @@ describe('AssetService', () => {
|
|||||||
createAssetDto.duration = '0:00:00.000000';
|
createAssetDto.duration = '0:00:00.000000';
|
||||||
|
|
||||||
return createAssetDto;
|
return createAssetDto;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _getAsset_1 = () => {
|
const _getAsset_1 = () => {
|
||||||
const asset_1 = new AssetEntity();
|
const asset_1 = new AssetEntity();
|
||||||
|
|
||||||
asset_1.id = 'id_1';
|
asset_1.id = 'id_1';
|
||||||
@ -71,9 +52,9 @@ describe('AssetService', () => {
|
|||||||
asset_1.encodedVideoPath = '';
|
asset_1.encodedVideoPath = '';
|
||||||
asset_1.duration = '0:00:00.000000';
|
asset_1.duration = '0:00:00.000000';
|
||||||
return asset_1;
|
return asset_1;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _getAsset_2 = () => {
|
const _getAsset_2 = () => {
|
||||||
const asset_2 = new AssetEntity();
|
const asset_2 = new AssetEntity();
|
||||||
|
|
||||||
asset_2.id = 'id_2';
|
asset_2.id = 'id_2';
|
||||||
@ -92,13 +73,13 @@ describe('AssetService', () => {
|
|||||||
asset_2.duration = '0:00:00.000000';
|
asset_2.duration = '0:00:00.000000';
|
||||||
|
|
||||||
return asset_2;
|
return asset_2;
|
||||||
};
|
};
|
||||||
|
|
||||||
const _getAssets = () => {
|
const _getAssets = () => {
|
||||||
return [_getAsset_1(), _getAsset_2()];
|
return [_getAsset_1(), _getAsset_2()];
|
||||||
};
|
};
|
||||||
|
|
||||||
const _getAssetCountByTimeBucket = (): AssetCountByTimeBucket[] => {
|
const _getAssetCountByTimeBucket = (): AssetCountByTimeBucket[] => {
|
||||||
const result1 = new AssetCountByTimeBucket();
|
const result1 = new AssetCountByTimeBucket();
|
||||||
result1.count = 2;
|
result1.count = 2;
|
||||||
result1.timeBucket = '2022-06-01T00:00:00.000Z';
|
result1.timeBucket = '2022-06-01T00:00:00.000Z';
|
||||||
@ -108,20 +89,34 @@ describe('AssetService', () => {
|
|||||||
result1.timeBucket = '2022-07-01T00:00:00.000Z';
|
result1.timeBucket = '2022-07-01T00:00:00.000Z';
|
||||||
|
|
||||||
return [result1, result2];
|
return [result1, result2];
|
||||||
};
|
};
|
||||||
|
|
||||||
const _getAssetCountByUserId = (): AssetCountByUserIdResponseDto => {
|
const _getAssetCountByUserId = (): AssetCountByUserIdResponseDto => {
|
||||||
const result = new AssetCountByUserIdResponseDto();
|
const result = new AssetCountByUserIdResponseDto();
|
||||||
|
|
||||||
result.videos = 2;
|
result.videos = 2;
|
||||||
result.photos = 2;
|
result.photos = 2;
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
};
|
};
|
||||||
|
|
||||||
beforeAll(() => {
|
describe('AssetService', () => {
|
||||||
|
let sut: AssetService;
|
||||||
|
let a: Repository<AssetEntity>; // TO BE DELETED AFTER FINISHED REFACTORING
|
||||||
|
let assetRepositoryMock: jest.Mocked<IAssetRepository>;
|
||||||
|
let albumRepositoryMock: jest.Mocked<IAlbumRepository>;
|
||||||
|
let downloadServiceMock: jest.Mocked<Partial<DownloadService>>;
|
||||||
|
let storageServiceMock: jest.Mocked<StorageService>;
|
||||||
|
let sharedLinkRepositoryMock: jest.Mocked<ISharedLinkRepository>;
|
||||||
|
let cryptoMock: jest.Mocked<ICryptoRepository>;
|
||||||
|
let jobMock: jest.Mocked<IJobRepository>;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
assetRepositoryMock = {
|
assetRepositoryMock = {
|
||||||
|
get: jest.fn(),
|
||||||
create: jest.fn(),
|
create: jest.fn(),
|
||||||
|
remove: jest.fn(),
|
||||||
|
|
||||||
update: jest.fn(),
|
update: jest.fn(),
|
||||||
getAll: jest.fn(),
|
getAll: jest.fn(),
|
||||||
getAllVideos: jest.fn(),
|
getAllVideos: jest.fn(),
|
||||||
@ -151,18 +146,21 @@ describe('AssetService', () => {
|
|||||||
downloadArchive: jest.fn(),
|
downloadArchive: jest.fn(),
|
||||||
};
|
};
|
||||||
|
|
||||||
sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
|
storageServiceMock = {
|
||||||
|
moveAsset: jest.fn(),
|
||||||
|
removeEmptyDirectories: jest.fn(),
|
||||||
|
} as unknown as jest.Mocked<StorageService>;
|
||||||
|
|
||||||
|
sharedLinkRepositoryMock = newSharedLinkRepositoryMock();
|
||||||
jobMock = newJobRepositoryMock();
|
jobMock = newJobRepositoryMock();
|
||||||
cryptoMock = newCryptoRepositoryMock();
|
cryptoMock = newCryptoRepositoryMock();
|
||||||
|
|
||||||
sui = new AssetService(
|
sut = new AssetService(
|
||||||
assetRepositoryMock,
|
assetRepositoryMock,
|
||||||
albumRepositoryMock,
|
albumRepositoryMock,
|
||||||
a,
|
a,
|
||||||
backgroundTaskServiceMock,
|
|
||||||
downloadServiceMock as DownloadService,
|
downloadServiceMock as DownloadService,
|
||||||
storageSeriveMock,
|
storageServiceMock,
|
||||||
sharedLinkRepositoryMock,
|
sharedLinkRepositoryMock,
|
||||||
jobMock,
|
jobMock,
|
||||||
cryptoMock,
|
cryptoMock,
|
||||||
@ -178,7 +176,7 @@ describe('AssetService', () => {
|
|||||||
assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
|
assetRepositoryMock.countByIdAndUser.mockResolvedValue(1);
|
||||||
sharedLinkRepositoryMock.create.mockResolvedValue(sharedLinkStub.valid);
|
sharedLinkRepositoryMock.create.mockResolvedValue(sharedLinkStub.valid);
|
||||||
|
|
||||||
await expect(sui.createAssetsSharedLink(authStub.user1, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
|
await expect(sut.createAssetsSharedLink(authStub.user1, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||||
|
|
||||||
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
||||||
expect(assetRepositoryMock.countByIdAndUser).toHaveBeenCalledWith(asset1.id, authStub.user1.id);
|
expect(assetRepositoryMock.countByIdAndUser).toHaveBeenCalledWith(asset1.id, authStub.user1.id);
|
||||||
@ -196,7 +194,7 @@ describe('AssetService', () => {
|
|||||||
sharedLinkRepositoryMock.get.mockResolvedValue(null);
|
sharedLinkRepositoryMock.get.mockResolvedValue(null);
|
||||||
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
|
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
|
||||||
|
|
||||||
await expect(sui.updateAssetsInSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException);
|
await expect(sut.updateAssetsInSharedLink(authDto, dto)).rejects.toBeInstanceOf(BadRequestException);
|
||||||
|
|
||||||
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
||||||
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
||||||
@ -215,7 +213,7 @@ describe('AssetService', () => {
|
|||||||
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
|
sharedLinkRepositoryMock.hasAssetAccess.mockResolvedValue(true);
|
||||||
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
|
sharedLinkRepositoryMock.save.mockResolvedValue(sharedLinkStub.valid);
|
||||||
|
|
||||||
await expect(sui.updateAssetsInSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
|
await expect(sut.updateAssetsInSharedLink(authDto, dto)).resolves.toEqual(sharedLinkResponseStub.valid);
|
||||||
|
|
||||||
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
expect(assetRepositoryMock.getById).toHaveBeenCalledWith(asset1.id);
|
||||||
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
expect(sharedLinkRepositoryMock.get).toHaveBeenCalledWith(authDto.id, authDto.sharedLinkId);
|
||||||
@ -223,27 +221,94 @@ describe('AssetService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
// Currently failing due to calculate checksum from a file
|
describe('uploadFile', () => {
|
||||||
it('create an asset', async () => {
|
it('should handle a file upload', async () => {
|
||||||
const assetEntity = _getAsset_1();
|
const assetEntity = _getAsset_1();
|
||||||
|
const file = {
|
||||||
|
originalPath: 'fake_path/asset_1.jpeg',
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
checksum: Buffer.from('file hash', 'utf8'),
|
||||||
|
originalName: 'asset_1.jpeg',
|
||||||
|
};
|
||||||
|
const dto = _getCreateAssetDto();
|
||||||
|
|
||||||
assetRepositoryMock.create.mockImplementation(() => Promise.resolve<AssetEntity>(assetEntity));
|
assetRepositoryMock.create.mockImplementation(() => Promise.resolve(assetEntity));
|
||||||
|
storageServiceMock.moveAsset.mockResolvedValue({ ...assetEntity, originalPath: 'fake_new_path/asset_123.jpeg' });
|
||||||
|
|
||||||
const originalPath = 'fake_path/asset_1.jpeg';
|
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: false, id: 'id_1' });
|
||||||
const mimeType = 'image/jpeg';
|
});
|
||||||
const createAssetDto = _getCreateAssetDto();
|
|
||||||
const result = await sui.createUserAsset(
|
|
||||||
authUser,
|
|
||||||
createAssetDto,
|
|
||||||
originalPath,
|
|
||||||
mimeType,
|
|
||||||
Buffer.from('0x5041E6328F7DF8AFF650BEDAED9251897D9A6241', 'hex'),
|
|
||||||
true,
|
|
||||||
);
|
|
||||||
|
|
||||||
expect(result.userId).toEqual(authUser.id);
|
it('should handle a duplicate', async () => {
|
||||||
expect(result.resizePath).toEqual('');
|
const file = {
|
||||||
expect(result.webpPath).toEqual('');
|
originalPath: 'fake_path/asset_1.jpeg',
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
checksum: Buffer.from('file hash', 'utf8'),
|
||||||
|
originalName: 'asset_1.jpeg',
|
||||||
|
};
|
||||||
|
const dto = _getCreateAssetDto();
|
||||||
|
const error = new QueryFailedError('', [], '');
|
||||||
|
(error as any).constraint = 'UQ_userid_checksum';
|
||||||
|
|
||||||
|
assetRepositoryMock.create.mockRejectedValue(error);
|
||||||
|
assetRepositoryMock.getAssetByChecksum.mockResolvedValue(_getAsset_1());
|
||||||
|
|
||||||
|
await expect(sut.uploadFile(authStub.user1, dto, file)).resolves.toEqual({ duplicate: true, id: 'id_1' });
|
||||||
|
|
||||||
|
expect(jobMock.add).toHaveBeenCalledWith({
|
||||||
|
name: JobName.DELETE_FILE_ON_DISK,
|
||||||
|
data: { assets: [{ originalPath: 'fake_path/asset_1.jpeg', resizePath: null }] },
|
||||||
|
});
|
||||||
|
expect(storageServiceMock.moveAsset).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should handle a live photo', async () => {
|
||||||
|
const file = {
|
||||||
|
originalPath: 'fake_path/asset_1.jpeg',
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
checksum: Buffer.from('file hash', 'utf8'),
|
||||||
|
originalName: 'asset_1.jpeg',
|
||||||
|
};
|
||||||
|
const asset = {
|
||||||
|
id: 'live-photo-asset',
|
||||||
|
originalPath: file.originalPath,
|
||||||
|
userId: authStub.user1.id,
|
||||||
|
type: AssetType.IMAGE,
|
||||||
|
isVisible: true,
|
||||||
|
} as AssetEntity;
|
||||||
|
|
||||||
|
const livePhotoFile = {
|
||||||
|
originalPath: 'fake_path/asset_1.mp4',
|
||||||
|
mimeType: 'image/jpeg',
|
||||||
|
checksum: Buffer.from('live photo file hash', 'utf8'),
|
||||||
|
originalName: 'asset_1.jpeg',
|
||||||
|
};
|
||||||
|
|
||||||
|
const livePhotoAsset = {
|
||||||
|
id: 'live-photo-motion',
|
||||||
|
originalPath: livePhotoFile.originalPath,
|
||||||
|
userId: authStub.user1.id,
|
||||||
|
type: AssetType.VIDEO,
|
||||||
|
isVisible: false,
|
||||||
|
} as AssetEntity;
|
||||||
|
|
||||||
|
const dto = _getCreateAssetDto();
|
||||||
|
const error = new QueryFailedError('', [], '');
|
||||||
|
(error as any).constraint = 'UQ_userid_checksum';
|
||||||
|
|
||||||
|
assetRepositoryMock.create.mockResolvedValueOnce(livePhotoAsset);
|
||||||
|
assetRepositoryMock.create.mockResolvedValueOnce(asset);
|
||||||
|
storageServiceMock.moveAsset.mockImplementation((asset) => Promise.resolve(asset));
|
||||||
|
|
||||||
|
await expect(sut.uploadFile(authStub.user1, dto, file, livePhotoFile)).resolves.toEqual({
|
||||||
|
duplicate: false,
|
||||||
|
id: 'live-photo-asset',
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(jobMock.add.mock.calls).toEqual([
|
||||||
|
[{ name: JobName.ASSET_UPLOADED, data: { asset: livePhotoAsset, fileName: file.originalName } }],
|
||||||
|
[{ name: JobName.ASSET_UPLOADED, data: { asset, fileName: file.originalName } }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it('get assets by device id', async () => {
|
it('get assets by device id', async () => {
|
||||||
@ -254,7 +319,7 @@ describe('AssetService', () => {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const deviceId = 'device_id_1';
|
const deviceId = 'device_id_1';
|
||||||
const result = await sui.getUserAssetsByDeviceId(authUser, deviceId);
|
const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId);
|
||||||
|
|
||||||
expect(result.length).toEqual(2);
|
expect(result.length).toEqual(2);
|
||||||
expect(result).toEqual(assets.map((asset) => asset.deviceAssetId));
|
expect(result).toEqual(assets.map((asset) => asset.deviceAssetId));
|
||||||
@ -267,7 +332,7 @@ describe('AssetService', () => {
|
|||||||
Promise.resolve<AssetCountByTimeBucket[]>(assetCountByTimeBucket),
|
Promise.resolve<AssetCountByTimeBucket[]>(assetCountByTimeBucket),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await sui.getAssetCountByTimeBucket(authUser, {
|
const result = await sut.getAssetCountByTimeBucket(authStub.user1, {
|
||||||
timeGroup: TimeGroupEnum.Month,
|
timeGroup: TimeGroupEnum.Month,
|
||||||
});
|
});
|
||||||
|
|
||||||
@ -282,18 +347,70 @@ describe('AssetService', () => {
|
|||||||
Promise.resolve<AssetCountByUserIdResponseDto>(assetCount),
|
Promise.resolve<AssetCountByUserIdResponseDto>(assetCount),
|
||||||
);
|
);
|
||||||
|
|
||||||
const result = await sui.getAssetCountByUserId(authUser);
|
const result = await sut.getAssetCountByUserId(authStub.user1);
|
||||||
|
|
||||||
expect(result).toEqual(assetCount);
|
expect(result).toEqual(assetCount);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('deleteAll', () => {
|
||||||
|
it('should return failed status when an asset is missing', async () => {
|
||||||
|
assetRepositoryMock.get.mockResolvedValue(null);
|
||||||
|
|
||||||
|
await expect(sut.deleteAll(authStub.user1, { ids: ['asset1'] })).resolves.toEqual([
|
||||||
|
{ id: 'asset1', status: 'FAILED' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(jobMock.add).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return failed status a delete fails', async () => {
|
||||||
|
assetRepositoryMock.get.mockResolvedValue({ id: 'asset1' } as AssetEntity);
|
||||||
|
assetRepositoryMock.remove.mockRejectedValue('delete failed');
|
||||||
|
|
||||||
|
await expect(sut.deleteAll(authStub.user1, { ids: ['asset1'] })).resolves.toEqual([
|
||||||
|
{ id: 'asset1', status: 'FAILED' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(jobMock.add).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete a live photo', async () => {
|
||||||
|
assetRepositoryMock.get.mockResolvedValueOnce({ id: 'asset1', livePhotoVideoId: 'live-photo' } as AssetEntity);
|
||||||
|
assetRepositoryMock.get.mockResolvedValueOnce({ id: 'live-photo' } as AssetEntity);
|
||||||
|
|
||||||
|
await expect(sut.deleteAll(authStub.user1, { ids: ['asset1'] })).resolves.toEqual([
|
||||||
|
{ id: 'asset1', status: 'SUCCESS' },
|
||||||
|
{ id: 'live-photo', status: 'SUCCESS' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(jobMock.add).toHaveBeenCalledWith({
|
||||||
|
name: JobName.DELETE_FILE_ON_DISK,
|
||||||
|
data: { assets: [{ id: 'asset1', livePhotoVideoId: 'live-photo' }, { id: 'live-photo' }] },
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should delete a batch of assets', async () => {
|
||||||
|
assetRepositoryMock.get.mockImplementation((id) => Promise.resolve({ id } as AssetEntity));
|
||||||
|
assetRepositoryMock.remove.mockImplementation(() => Promise.resolve());
|
||||||
|
|
||||||
|
await expect(sut.deleteAll(authStub.user1, { ids: ['asset1', 'asset2'] })).resolves.toEqual([
|
||||||
|
{ id: 'asset1', status: 'SUCCESS' },
|
||||||
|
{ id: 'asset2', status: 'SUCCESS' },
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(jobMock.add.mock.calls).toEqual([
|
||||||
|
[{ name: JobName.DELETE_FILE_ON_DISK, data: { assets: [{ id: 'asset1' }, { id: 'asset2' }] } }],
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('checkDownloadAccess', () => {
|
describe('checkDownloadAccess', () => {
|
||||||
it('should validate download access', async () => {
|
it('should validate download access', async () => {
|
||||||
await sui.checkDownloadAccess(authStub.adminSharedLink);
|
await sut.checkDownloadAccess(authStub.adminSharedLink);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should not allow when user is not allowed to download', async () => {
|
it('should not allow when user is not allowed to download', async () => {
|
||||||
expect(() => sui.checkDownloadAccess(authStub.readonlySharedLink)).toThrow(ForbiddenException);
|
expect(() => sut.checkDownloadAccess(authStub.readonlySharedLink)).toThrow(ForbiddenException);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@ -23,8 +23,8 @@ import { SearchAssetDto } from './dto/search-asset.dto';
|
|||||||
import fs from 'fs/promises';
|
import fs from 'fs/promises';
|
||||||
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
import { CheckDuplicateAssetDto } from './dto/check-duplicate-asset.dto';
|
||||||
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
||||||
import { AssetResponseDto, mapAsset, mapAssetWithoutExif } from '@app/domain';
|
import { AssetResponseDto, JobName, mapAsset, mapAssetWithoutExif } from '@app/domain';
|
||||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
import { CreateAssetDto, UploadFile } from './dto/create-asset.dto';
|
||||||
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
import { DeleteAssetResponseDto, DeleteAssetStatusEnum } from './response-dto/delete-asset-response.dto';
|
||||||
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
|
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
|
||||||
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
|
import { CheckDuplicateAssetResponseDto } from './response-dto/check-duplicate-asset-response.dto';
|
||||||
@ -37,13 +37,12 @@ import {
|
|||||||
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
|
import { GetAssetCountByTimeBucketDto } from './dto/get-asset-count-by-time-bucket.dto';
|
||||||
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
import { GetAssetByTimeBucketDto } from './dto/get-asset-by-time-bucket.dto';
|
||||||
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
import { AssetCountByUserIdResponseDto } from './response-dto/asset-count-by-user-id-response.dto';
|
||||||
import { timeUtils } from '@app/common/utils';
|
import { AssetCore } from './asset.core';
|
||||||
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||||
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
import { CheckExistingAssetsResponseDto } from './response-dto/check-existing-assets-response.dto';
|
||||||
import { UpdateAssetDto } from './dto/update-asset.dto';
|
import { UpdateAssetDto } from './dto/update-asset.dto';
|
||||||
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
|
import { AssetFileUploadResponseDto } from './response-dto/asset-file-upload-response.dto';
|
||||||
import { BackgroundTaskService } from '../../modules/background-task/background-task.service';
|
import { ICryptoRepository, IJobRepository } from '@app/domain';
|
||||||
import { ICryptoRepository, IJobRepository, JobName } from '@app/domain';
|
|
||||||
import { DownloadService } from '../../modules/download/download.service';
|
import { DownloadService } from '../../modules/download/download.service';
|
||||||
import { DownloadDto } from './dto/download-library.dto';
|
import { DownloadDto } from './dto/download-library.dto';
|
||||||
import { IAlbumRepository } from '../album/album-repository';
|
import { IAlbumRepository } from '../album/album-repository';
|
||||||
@ -55,7 +54,6 @@ import { CreateAssetsShareLinkDto } from './dto/create-asset-shared-link.dto';
|
|||||||
import { mapSharedLink, SharedLinkResponseDto } from '@app/domain';
|
import { mapSharedLink, SharedLinkResponseDto } from '@app/domain';
|
||||||
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
|
import { UpdateAssetsToSharedLinkDto } from './dto/add-assets-to-shared-link.dto';
|
||||||
import { AssetSearchDto } from './dto/asset-search.dto';
|
import { AssetSearchDto } from './dto/asset-search.dto';
|
||||||
import { ImmichFile } from '../../config/asset-upload.config';
|
|
||||||
|
|
||||||
const fileInfo = promisify(stat);
|
const fileInfo = promisify(stat);
|
||||||
|
|
||||||
@ -63,140 +61,67 @@ const fileInfo = promisify(stat);
|
|||||||
export class AssetService {
|
export class AssetService {
|
||||||
readonly logger = new Logger(AssetService.name);
|
readonly logger = new Logger(AssetService.name);
|
||||||
private shareCore: ShareCore;
|
private shareCore: ShareCore;
|
||||||
|
private assetCore: AssetCore;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(IAssetRepository) private _assetRepository: IAssetRepository,
|
@Inject(IAssetRepository) private _assetRepository: IAssetRepository,
|
||||||
@Inject(IAlbumRepository) private _albumRepository: IAlbumRepository,
|
@Inject(IAlbumRepository) private _albumRepository: IAlbumRepository,
|
||||||
@InjectRepository(AssetEntity)
|
@InjectRepository(AssetEntity)
|
||||||
private assetRepository: Repository<AssetEntity>,
|
private assetRepository: Repository<AssetEntity>,
|
||||||
private backgroundTaskService: BackgroundTaskService,
|
|
||||||
private downloadService: DownloadService,
|
private downloadService: DownloadService,
|
||||||
private storageService: StorageService,
|
storageService: StorageService,
|
||||||
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
|
@Inject(ISharedLinkRepository) sharedLinkRepository: ISharedLinkRepository,
|
||||||
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
@Inject(IJobRepository) private jobRepository: IJobRepository,
|
||||||
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
@Inject(ICryptoRepository) cryptoRepository: ICryptoRepository,
|
||||||
) {
|
) {
|
||||||
|
this.assetCore = new AssetCore(_assetRepository, jobRepository, storageService);
|
||||||
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
|
this.shareCore = new ShareCore(sharedLinkRepository, cryptoRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
public async handleUploadedAsset(
|
public async uploadFile(
|
||||||
authUser: AuthUserDto,
|
authUser: AuthUserDto,
|
||||||
createAssetDto: CreateAssetDto,
|
dto: CreateAssetDto,
|
||||||
res: Res,
|
file: UploadFile,
|
||||||
originalAssetData: ImmichFile,
|
livePhotoFile?: UploadFile,
|
||||||
livePhotoAssetData?: ImmichFile,
|
): Promise<AssetFileUploadResponseDto> {
|
||||||
) {
|
if (livePhotoFile) {
|
||||||
const checksum = originalAssetData.checksum;
|
livePhotoFile.originalName = file.originalName;
|
||||||
const isLivePhoto = livePhotoAssetData !== undefined;
|
}
|
||||||
let livePhotoAssetEntity: AssetEntity | undefined;
|
|
||||||
|
let livePhotoAsset: AssetEntity | null = null;
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (isLivePhoto) {
|
if (livePhotoFile) {
|
||||||
const livePhotoChecksum = livePhotoAssetData.checksum;
|
const livePhotoDto = { ...dto, assetType: AssetType.VIDEO, isVisible: false };
|
||||||
livePhotoAssetEntity = await this.createUserAsset(
|
livePhotoAsset = await this.assetCore.create(authUser, livePhotoDto, livePhotoFile);
|
||||||
authUser,
|
|
||||||
createAssetDto,
|
|
||||||
livePhotoAssetData.path,
|
|
||||||
livePhotoAssetData.mimetype,
|
|
||||||
livePhotoChecksum,
|
|
||||||
false,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!livePhotoAssetEntity) {
|
|
||||||
await this.backgroundTaskService.deleteFileOnDisk([
|
|
||||||
{
|
|
||||||
originalPath: livePhotoAssetData.path,
|
|
||||||
} as any,
|
|
||||||
]);
|
|
||||||
throw new BadRequestException('Asset not created');
|
|
||||||
}
|
}
|
||||||
|
|
||||||
await this.storageService.moveAsset(livePhotoAssetEntity, originalAssetData.originalname);
|
const asset = await this.assetCore.create(authUser, dto, file, livePhotoAsset?.id);
|
||||||
|
|
||||||
await this.jobRepository.add({ name: JobName.VIDEO_CONVERSION, data: { asset: livePhotoAssetEntity } });
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetEntity = await this.createUserAsset(
|
|
||||||
authUser,
|
|
||||||
createAssetDto,
|
|
||||||
originalAssetData.path,
|
|
||||||
originalAssetData.mimetype,
|
|
||||||
checksum,
|
|
||||||
true,
|
|
||||||
livePhotoAssetEntity,
|
|
||||||
);
|
|
||||||
|
|
||||||
if (!assetEntity) {
|
|
||||||
await this.backgroundTaskService.deleteFileOnDisk([
|
|
||||||
{
|
|
||||||
originalPath: originalAssetData.path,
|
|
||||||
} as any,
|
|
||||||
]);
|
|
||||||
throw new BadRequestException('Asset not created');
|
|
||||||
}
|
|
||||||
|
|
||||||
const movedAsset = await this.storageService.moveAsset(assetEntity, originalAssetData.originalname);
|
|
||||||
|
|
||||||
|
return { id: asset.id, duplicate: false };
|
||||||
|
} catch (error: any) {
|
||||||
|
// clean up files
|
||||||
await this.jobRepository.add({
|
await this.jobRepository.add({
|
||||||
name: JobName.ASSET_UPLOADED,
|
name: JobName.DELETE_FILE_ON_DISK,
|
||||||
data: { asset: movedAsset, fileName: originalAssetData.originalname },
|
data: {
|
||||||
|
assets: [
|
||||||
|
{
|
||||||
|
originalPath: file.originalPath,
|
||||||
|
resizePath: livePhotoFile?.originalPath || null,
|
||||||
|
} as AssetEntity,
|
||||||
|
],
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
return new AssetFileUploadResponseDto(movedAsset.id);
|
// handle duplicates with a success response
|
||||||
} catch (err) {
|
if (error instanceof QueryFailedError && (error as any).constraint === 'UQ_userid_checksum') {
|
||||||
await this.backgroundTaskService.deleteFileOnDisk([
|
const duplicate = await this.getAssetByChecksum(authUser.id, file.checksum);
|
||||||
{
|
return { id: duplicate.id, duplicate: true };
|
||||||
originalPath: originalAssetData.path,
|
|
||||||
} as any,
|
|
||||||
]); // simulate asset to make use of delete queue (or use fs.unlink instead)
|
|
||||||
|
|
||||||
if (isLivePhoto) {
|
|
||||||
await this.backgroundTaskService.deleteFileOnDisk([
|
|
||||||
{
|
|
||||||
originalPath: livePhotoAssetData.path,
|
|
||||||
} as any,
|
|
||||||
]);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if (err instanceof QueryFailedError && (err as any).constraint === 'UQ_userid_checksum') {
|
this.logger.error(`Error uploading file ${error}`, error?.stack);
|
||||||
const existedAsset = await this.getAssetByChecksum(authUser.id, checksum);
|
throw new BadRequestException(`Error uploading file`, `${error}`);
|
||||||
res.status(200); // normal POST is 201. we use 200 to indicate the asset already exists
|
|
||||||
return new AssetFileUploadResponseDto(existedAsset.id);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Logger.error(`Error uploading file ${err}`);
|
|
||||||
throw new BadRequestException(`Error uploading file`, `${err}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
public async createUserAsset(
|
|
||||||
authUser: AuthUserDto,
|
|
||||||
createAssetDto: CreateAssetDto,
|
|
||||||
originalPath: string,
|
|
||||||
mimeType: string,
|
|
||||||
checksum: Buffer,
|
|
||||||
isVisible: boolean,
|
|
||||||
livePhotoAssetEntity?: AssetEntity,
|
|
||||||
): Promise<AssetEntity> {
|
|
||||||
if (!timeUtils.checkValidTimestamp(createAssetDto.createdAt)) {
|
|
||||||
createAssetDto.createdAt = new Date().toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!timeUtils.checkValidTimestamp(createAssetDto.modifiedAt)) {
|
|
||||||
createAssetDto.modifiedAt = new Date().toISOString();
|
|
||||||
}
|
|
||||||
|
|
||||||
const assetEntity = await this._assetRepository.create(
|
|
||||||
createAssetDto,
|
|
||||||
authUser.id,
|
|
||||||
originalPath,
|
|
||||||
mimeType,
|
|
||||||
isVisible,
|
|
||||||
checksum,
|
|
||||||
livePhotoAssetEntity,
|
|
||||||
);
|
|
||||||
|
|
||||||
return assetEntity;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) {
|
public async getUserAssetsByDeviceId(authUser: AuthUserDto, deviceId: string) {
|
||||||
@ -520,26 +445,35 @@ export class AssetService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
public async deleteAssetById(assetIds: DeleteAssetDto): Promise<DeleteAssetResponseDto[]> {
|
public async deleteAll(authUser: AuthUserDto, dto: DeleteAssetDto): Promise<DeleteAssetResponseDto[]> {
|
||||||
|
const deleteQueue: AssetEntity[] = [];
|
||||||
const result: DeleteAssetResponseDto[] = [];
|
const result: DeleteAssetResponseDto[] = [];
|
||||||
|
|
||||||
const target = assetIds.ids;
|
const ids = dto.ids.slice();
|
||||||
for (const assetId of target) {
|
for (const id of ids) {
|
||||||
const res = await this.assetRepository.delete({
|
const asset = await this._assetRepository.get(id);
|
||||||
id: assetId,
|
if (!asset) {
|
||||||
});
|
result.push({ id, status: DeleteAssetStatusEnum.FAILED });
|
||||||
|
continue;
|
||||||
if (res.affected) {
|
|
||||||
result.push({
|
|
||||||
id: assetId,
|
|
||||||
status: DeleteAssetStatusEnum.SUCCESS,
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
result.push({
|
|
||||||
id: assetId,
|
|
||||||
status: DeleteAssetStatusEnum.FAILED,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await this._assetRepository.remove(asset);
|
||||||
|
|
||||||
|
result.push({ id: asset.id, status: DeleteAssetStatusEnum.SUCCESS });
|
||||||
|
deleteQueue.push(asset as any);
|
||||||
|
|
||||||
|
// TODO refactor this to use cascades
|
||||||
|
if (asset.livePhotoVideoId && !ids.includes(asset.livePhotoVideoId)) {
|
||||||
|
ids.push(asset.livePhotoVideoId);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
result.push({ id, status: DeleteAssetStatusEnum.FAILED });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (deleteQueue.length > 0) {
|
||||||
|
await this.jobRepository.add({ name: JobName.DELETE_FILE_ON_DISK, data: { assets: deleteQueue } });
|
||||||
}
|
}
|
||||||
|
|
||||||
return result;
|
return result;
|
||||||
|
@ -1,6 +1,7 @@
|
|||||||
import { IsNotEmpty, IsOptional } from 'class-validator';
|
|
||||||
import { AssetType } from '@app/infra';
|
import { AssetType } from '@app/infra';
|
||||||
import { ApiProperty } from '@nestjs/swagger';
|
import { ApiProperty } from '@nestjs/swagger';
|
||||||
|
import { IsBoolean, IsNotEmpty, IsOptional } from 'class-validator';
|
||||||
|
import { ImmichFile } from '../../../config/asset-upload.config';
|
||||||
|
|
||||||
export class CreateAssetDto {
|
export class CreateAssetDto {
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
@ -22,9 +23,29 @@ export class CreateAssetDto {
|
|||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
isFavorite!: boolean;
|
isFavorite!: boolean;
|
||||||
|
|
||||||
|
@IsOptional()
|
||||||
|
@IsBoolean()
|
||||||
|
isVisible?: boolean;
|
||||||
|
|
||||||
@IsNotEmpty()
|
@IsNotEmpty()
|
||||||
fileExtension!: string;
|
fileExtension!: string;
|
||||||
|
|
||||||
@IsOptional()
|
@IsOptional()
|
||||||
duration?: string;
|
duration?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface UploadFile {
|
||||||
|
mimeType: string;
|
||||||
|
checksum: Buffer;
|
||||||
|
originalPath: string;
|
||||||
|
originalName: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapToUploadFile(file: ImmichFile): UploadFile {
|
||||||
|
return {
|
||||||
|
checksum: file.checksum,
|
||||||
|
mimeType: file.mimetype,
|
||||||
|
originalPath: file.path,
|
||||||
|
originalName: file.originalname,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
@ -1,7 +1,4 @@
|
|||||||
export class AssetFileUploadResponseDto {
|
export class AssetFileUploadResponseDto {
|
||||||
constructor(id: string) {
|
id!: string;
|
||||||
this.id = id;
|
duplicate!: boolean;
|
||||||
}
|
|
||||||
|
|
||||||
id: string;
|
|
||||||
}
|
}
|
||||||
|
@ -4,7 +4,6 @@ import { AssetModule } from './api-v1/asset/asset.module';
|
|||||||
import { DeviceInfoModule } from './api-v1/device-info/device-info.module';
|
import { DeviceInfoModule } from './api-v1/device-info/device-info.module';
|
||||||
import { ConfigModule } from '@nestjs/config';
|
import { ConfigModule } from '@nestjs/config';
|
||||||
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
|
import { ServerInfoModule } from './api-v1/server-info/server-info.module';
|
||||||
import { BackgroundTaskModule } from './modules/background-task/background-task.module';
|
|
||||||
import { CommunicationModule } from './api-v1/communication/communication.module';
|
import { CommunicationModule } from './api-v1/communication/communication.module';
|
||||||
import { AlbumModule } from './api-v1/album/album.module';
|
import { AlbumModule } from './api-v1/album/album.module';
|
||||||
import { AppController } from './app.controller';
|
import { AppController } from './app.controller';
|
||||||
@ -40,8 +39,6 @@ import { UserAuthStrategy } from './modules/immich-auth/strategies/user-auth.str
|
|||||||
|
|
||||||
ServerInfoModule,
|
ServerInfoModule,
|
||||||
|
|
||||||
BackgroundTaskModule,
|
|
||||||
|
|
||||||
CommunicationModule,
|
CommunicationModule,
|
||||||
|
|
||||||
AlbumModule,
|
AlbumModule,
|
||||||
|
@ -1,9 +0,0 @@
|
|||||||
import { Module } from '@nestjs/common';
|
|
||||||
import { BackgroundTaskProcessor } from './background-task.processor';
|
|
||||||
import { BackgroundTaskService } from './background-task.service';
|
|
||||||
|
|
||||||
@Module({
|
|
||||||
providers: [BackgroundTaskService, BackgroundTaskProcessor],
|
|
||||||
exports: [BackgroundTaskService],
|
|
||||||
})
|
|
||||||
export class BackgroundTaskModule {}
|
|
@ -1,12 +0,0 @@
|
|||||||
import { IJobRepository, JobName } from '@app/domain';
|
|
||||||
import { AssetEntity } from '@app/infra';
|
|
||||||
import { Inject, Injectable } from '@nestjs/common';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class BackgroundTaskService {
|
|
||||||
constructor(@Inject(IJobRepository) private jobRepository: IJobRepository) {}
|
|
||||||
|
|
||||||
async deleteFileOnDisk(assets: AssetEntity[]) {
|
|
||||||
await this.jobRepository.add({ name: JobName.DELETE_FILE_ON_DISK, data: { assets } });
|
|
||||||
}
|
|
||||||
}
|
|
@ -14,6 +14,7 @@ import { StorageMigrationProcessor } from './processors/storage-migration.proces
|
|||||||
import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor';
|
import { ThumbnailGeneratorProcessor } from './processors/thumbnail.processor';
|
||||||
import { UserDeletionProcessor } from './processors/user-deletion.processor';
|
import { UserDeletionProcessor } from './processors/user-deletion.processor';
|
||||||
import { VideoTranscodeProcessor } from './processors/video-transcode.processor';
|
import { VideoTranscodeProcessor } from './processors/video-transcode.processor';
|
||||||
|
import { BackgroundTaskProcessor } from './processors/background-task.processor';
|
||||||
import { DomainModule } from '@app/domain';
|
import { DomainModule } from '@app/domain';
|
||||||
|
|
||||||
@Module({
|
@Module({
|
||||||
@ -37,6 +38,7 @@ import { DomainModule } from '@app/domain';
|
|||||||
MachineLearningProcessor,
|
MachineLearningProcessor,
|
||||||
UserDeletionProcessor,
|
UserDeletionProcessor,
|
||||||
StorageMigrationProcessor,
|
StorageMigrationProcessor,
|
||||||
|
BackgroundTaskProcessor,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class MicroservicesModule {}
|
export class MicroservicesModule {}
|
||||||
|
@ -2,7 +2,7 @@ import { assetUtils } from '@app/common/utils';
|
|||||||
import { Process, Processor } from '@nestjs/bull';
|
import { Process, Processor } from '@nestjs/bull';
|
||||||
import { Job } from 'bull';
|
import { Job } from 'bull';
|
||||||
import { JobName, QueueName } from '@app/domain';
|
import { JobName, QueueName } from '@app/domain';
|
||||||
import { AssetEntity } from '@app/infra';
|
import { AssetEntity } from '@app/infra/db/entities';
|
||||||
|
|
||||||
@Processor(QueueName.BACKGROUND_TASK)
|
@Processor(QueueName.BACKGROUND_TASK)
|
||||||
export class BackgroundTaskProcessor {
|
export class BackgroundTaskProcessor {
|
@ -235,6 +235,10 @@ export class MetadataExtractionProcessor {
|
|||||||
async extractVideoMetadata(job: Job<IVideoLengthExtractionProcessor>) {
|
async extractVideoMetadata(job: Job<IVideoLengthExtractionProcessor>) {
|
||||||
const { asset, fileName } = job.data;
|
const { asset, fileName } = job.data;
|
||||||
|
|
||||||
|
if (!asset.isVisible) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const data = await new Promise<ffmpeg.FfprobeData>((resolve, reject) =>
|
const data = await new Promise<ffmpeg.FfprobeData>((resolve, reject) =>
|
||||||
ffmpeg.ffprobe(asset.originalPath, (err, data) => {
|
ffmpeg.ffprobe(asset.originalPath, (err, data) => {
|
||||||
|
@ -3725,10 +3725,14 @@
|
|||||||
"properties": {
|
"properties": {
|
||||||
"id": {
|
"id": {
|
||||||
"type": "string"
|
"type": "string"
|
||||||
|
},
|
||||||
|
"duplicate": {
|
||||||
|
"type": "boolean"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"required": [
|
"required": [
|
||||||
"id"
|
"id",
|
||||||
|
"duplicate"
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
"DownloadFilesDto": {
|
"DownloadFilesDto": {
|
||||||
|
@ -32,7 +32,7 @@ export class AssetEntity {
|
|||||||
webpPath!: string | null;
|
webpPath!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'varchar', nullable: true, default: '' })
|
@Column({ type: 'varchar', nullable: true, default: '' })
|
||||||
encodedVideoPath!: string;
|
encodedVideoPath!: string | null;
|
||||||
|
|
||||||
@Column({ type: 'timestamptz' })
|
@Column({ type: 'timestamptz' })
|
||||||
createdAt!: string;
|
createdAt!: string;
|
||||||
|
@ -25,7 +25,7 @@ const moveFile = promisify<string, string, mv.Options>(mv);
|
|||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class StorageService {
|
export class StorageService {
|
||||||
readonly logger = new Logger(StorageService.name);
|
private readonly logger = new Logger(StorageService.name);
|
||||||
|
|
||||||
private storageTemplate: HandlebarsTemplateDelegate<any>;
|
private storageTemplate: HandlebarsTemplateDelegate<any>;
|
||||||
|
|
||||||
|
8
web/src/api/open-api/api.ts
generated
8
web/src/api/open-api/api.ts
generated
@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.42.0
|
* The version of the OpenAPI document: 1.43.0
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
@ -395,6 +395,12 @@ export interface AssetFileUploadResponseDto {
|
|||||||
* @memberof AssetFileUploadResponseDto
|
* @memberof AssetFileUploadResponseDto
|
||||||
*/
|
*/
|
||||||
'id': string;
|
'id': string;
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @type {boolean}
|
||||||
|
* @memberof AssetFileUploadResponseDto
|
||||||
|
*/
|
||||||
|
'duplicate': boolean;
|
||||||
}
|
}
|
||||||
/**
|
/**
|
||||||
*
|
*
|
||||||
|
2
web/src/api/open-api/base.ts
generated
2
web/src/api/open-api/base.ts
generated
@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.42.0
|
* The version of the OpenAPI document: 1.43.0
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
2
web/src/api/open-api/common.ts
generated
2
web/src/api/open-api/common.ts
generated
@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.42.0
|
* The version of the OpenAPI document: 1.43.0
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
2
web/src/api/open-api/configuration.ts
generated
2
web/src/api/open-api/configuration.ts
generated
@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.42.0
|
* The version of the OpenAPI document: 1.43.0
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
2
web/src/api/open-api/index.ts
generated
2
web/src/api/open-api/index.ts
generated
@ -4,7 +4,7 @@
|
|||||||
* Immich
|
* Immich
|
||||||
* Immich API
|
* Immich API
|
||||||
*
|
*
|
||||||
* The version of the OpenAPI document: 1.42.0
|
* The version of the OpenAPI document: 1.43.0
|
||||||
*
|
*
|
||||||
*
|
*
|
||||||
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
* NOTE: This class is auto generated by OpenAPI Generator (https://openapi-generator.tech).
|
||||||
|
Loading…
Reference in New Issue
Block a user