2023-07-09 00:37:40 -04:00
|
|
|
import { AssetEntity } from '@app/infra/entities';
|
2023-06-30 12:24:28 -04:00
|
|
|
import { BadRequestException, Inject } from '@nestjs/common';
|
2023-06-15 14:05:30 -04:00
|
|
|
import { DateTime } from 'luxon';
|
2023-06-30 12:24:28 -04:00
|
|
|
import { extname } from 'path';
|
2023-07-09 00:37:40 -04:00
|
|
|
import { AccessCore, IAccessRepository, Permission } from '../access';
|
2023-05-21 08:26:06 +02:00
|
|
|
import { AuthUserDto } from '../auth';
|
2023-07-10 13:56:45 -04:00
|
|
|
import { mimeTypes } from '../domain.constant';
|
2023-06-30 12:24:28 -04:00
|
|
|
import { HumanReadableSize, usePagination } from '../domain.util';
|
|
|
|
|
import { ImmichReadStream, IStorageRepository } from '../storage';
|
2023-03-02 21:47:08 -05:00
|
|
|
import { IAssetRepository } from './asset.repository';
|
2023-06-30 12:24:28 -04:00
|
|
|
import { AssetIdsDto, DownloadArchiveInfo, DownloadDto, DownloadResponseDto, MemoryLaneDto } from './dto';
|
2023-05-21 08:26:06 +02:00
|
|
|
import { MapMarkerDto } from './dto/map-marker.dto';
|
2023-06-15 14:05:30 -04:00
|
|
|
import { mapAsset, MapMarkerResponseDto } from './response-dto';
|
2023-06-14 20:47:18 -05:00
|
|
|
import { MemoryLaneResponseDto } from './response-dto/memory-lane-response.dto';
|
2023-02-25 09:12:03 -05:00
|
|
|
|
2023-07-09 00:37:40 -04:00
|
|
|
export enum UploadFieldName {
|
|
|
|
|
ASSET_DATA = 'assetData',
|
|
|
|
|
LIVE_PHOTO_DATA = 'livePhotoData',
|
|
|
|
|
SIDECAR_DATA = 'sidecarData',
|
|
|
|
|
PROFILE_DATA = 'file',
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export interface UploadFile {
|
|
|
|
|
checksum: Buffer;
|
|
|
|
|
originalPath: string;
|
|
|
|
|
originalName: string;
|
|
|
|
|
}
|
|
|
|
|
|
2023-02-25 09:12:03 -05:00
|
|
|
export class AssetService {
|
2023-06-30 12:24:28 -04:00
|
|
|
private access: AccessCore;
|
|
|
|
|
|
|
|
|
|
constructor(
|
|
|
|
|
@Inject(IAccessRepository) accessRepository: IAccessRepository,
|
|
|
|
|
@Inject(IAssetRepository) private assetRepository: IAssetRepository,
|
|
|
|
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
|
|
|
|
) {
|
|
|
|
|
this.access = new AccessCore(accessRepository);
|
|
|
|
|
}
|
2023-05-21 08:26:06 +02:00
|
|
|
|
|
|
|
|
getMapMarkers(authUser: AuthUserDto, options: MapMarkerDto): Promise<MapMarkerResponseDto[]> {
|
|
|
|
|
return this.assetRepository.getMapMarkers(authUser.id, options);
|
|
|
|
|
}
|
2023-06-14 20:47:18 -05:00
|
|
|
|
2023-06-15 14:05:30 -04:00
|
|
|
async getMemoryLane(authUser: AuthUserDto, dto: MemoryLaneDto): Promise<MemoryLaneResponseDto[]> {
|
|
|
|
|
const target = DateTime.fromJSDate(dto.timestamp);
|
2023-06-14 20:47:18 -05:00
|
|
|
|
2023-06-15 14:05:30 -04:00
|
|
|
const onRequest = async (yearsAgo: number): Promise<MemoryLaneResponseDto> => {
|
|
|
|
|
const assets = await this.assetRepository.getByDate(authUser.id, target.minus({ years: yearsAgo }).toJSDate());
|
|
|
|
|
return {
|
|
|
|
|
title: `${yearsAgo} year${yearsAgo > 1 ? 's' : ''} since...`,
|
|
|
|
|
assets: assets.map((a) => mapAsset(a)),
|
|
|
|
|
};
|
|
|
|
|
};
|
2023-06-14 20:47:18 -05:00
|
|
|
|
2023-06-15 14:05:30 -04:00
|
|
|
const requests: Promise<MemoryLaneResponseDto>[] = [];
|
|
|
|
|
for (let i = 1; i <= dto.years; i++) {
|
|
|
|
|
requests.push(onRequest(i));
|
2023-06-14 20:47:18 -05:00
|
|
|
}
|
|
|
|
|
|
2023-06-15 14:05:30 -04:00
|
|
|
return Promise.all(requests).then((results) => results.filter((result) => result.assets.length > 0));
|
2023-06-14 20:47:18 -05:00
|
|
|
}
|
2023-06-30 12:24:28 -04:00
|
|
|
|
|
|
|
|
async downloadFile(authUser: AuthUserDto, id: string): Promise<ImmichReadStream> {
|
|
|
|
|
await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, id);
|
|
|
|
|
|
|
|
|
|
const [asset] = await this.assetRepository.getByIds([id]);
|
|
|
|
|
if (!asset) {
|
|
|
|
|
throw new BadRequestException('Asset not found');
|
|
|
|
|
}
|
|
|
|
|
|
2023-07-10 13:56:45 -04:00
|
|
|
return this.storageRepository.createReadStream(asset.originalPath, mimeTypes.lookup(asset.originalPath));
|
2023-06-30 12:24:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async getDownloadInfo(authUser: AuthUserDto, dto: DownloadDto): Promise<DownloadResponseDto> {
|
|
|
|
|
const targetSize = dto.archiveSize || HumanReadableSize.GiB * 4;
|
|
|
|
|
const archives: DownloadArchiveInfo[] = [];
|
|
|
|
|
let archive: DownloadArchiveInfo = { size: 0, assetIds: [] };
|
|
|
|
|
|
|
|
|
|
const assetPagination = await this.getDownloadAssets(authUser, dto);
|
|
|
|
|
for await (const assets of assetPagination) {
|
|
|
|
|
// motion part of live photos
|
|
|
|
|
const motionIds = assets.map((asset) => asset.livePhotoVideoId).filter<string>((id): id is string => !!id);
|
|
|
|
|
if (motionIds.length > 0) {
|
|
|
|
|
assets.push(...(await this.assetRepository.getByIds(motionIds)));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for (const asset of assets) {
|
|
|
|
|
archive.size += Number(asset.exifInfo?.fileSizeInByte || 0);
|
|
|
|
|
archive.assetIds.push(asset.id);
|
|
|
|
|
|
|
|
|
|
if (archive.size > targetSize) {
|
|
|
|
|
archives.push(archive);
|
|
|
|
|
archive = { size: 0, assetIds: [] };
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (archive.assetIds.length > 0) {
|
|
|
|
|
archives.push(archive);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
totalSize: archives.reduce((total, item) => (total += item.size), 0),
|
|
|
|
|
archives,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async downloadArchive(authUser: AuthUserDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
|
|
|
|
|
await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, dto.assetIds);
|
|
|
|
|
|
|
|
|
|
const zip = this.storageRepository.createZipStream();
|
|
|
|
|
const assets = await this.assetRepository.getByIds(dto.assetIds);
|
2023-07-01 00:44:55 -04:00
|
|
|
const paths: Record<string, number> = {};
|
2023-06-30 12:24:28 -04:00
|
|
|
|
|
|
|
|
for (const { originalPath, originalFileName } of assets) {
|
|
|
|
|
const ext = extname(originalPath);
|
|
|
|
|
let filename = `${originalFileName}${ext}`;
|
2023-07-01 00:44:55 -04:00
|
|
|
const count = paths[filename] || 0;
|
|
|
|
|
paths[filename] = count + 1;
|
|
|
|
|
if (count !== 0) {
|
|
|
|
|
filename = `${originalFileName}+${count}${ext}`;
|
2023-06-30 12:24:28 -04:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
zip.addFile(originalPath, filename);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
zip.finalize();
|
|
|
|
|
|
|
|
|
|
return { stream: zip.stream };
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
private async getDownloadAssets(authUser: AuthUserDto, dto: DownloadDto): Promise<AsyncGenerator<AssetEntity[]>> {
|
|
|
|
|
const PAGINATION_SIZE = 2500;
|
|
|
|
|
|
|
|
|
|
if (dto.assetIds) {
|
|
|
|
|
const assetIds = dto.assetIds;
|
|
|
|
|
await this.access.requirePermission(authUser, Permission.ASSET_DOWNLOAD, assetIds);
|
|
|
|
|
const assets = await this.assetRepository.getByIds(assetIds);
|
|
|
|
|
return (async function* () {
|
|
|
|
|
yield assets;
|
|
|
|
|
})();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dto.albumId) {
|
|
|
|
|
const albumId = dto.albumId;
|
|
|
|
|
await this.access.requirePermission(authUser, Permission.ALBUM_DOWNLOAD, albumId);
|
|
|
|
|
return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByAlbumId(pagination, albumId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (dto.userId) {
|
|
|
|
|
const userId = dto.userId;
|
|
|
|
|
await this.access.requirePermission(authUser, Permission.LIBRARY_DOWNLOAD, userId);
|
|
|
|
|
return usePagination(PAGINATION_SIZE, (pagination) => this.assetRepository.getByUserId(pagination, userId));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
throw new BadRequestException('assetIds, albumId, or userId is required');
|
|
|
|
|
}
|
2023-02-25 09:12:03 -05:00
|
|
|
}
|