forked from Cutlery/immich
* refactor(server): delete assets endpoint * fix: formatting * chore: cleanup * chore: open api * chore(mobile): replace DeleteAssetDTO with BulkIdsDTOs * feat: trash an asset * chore(server): formatting * chore: open api * chore: wording * chore: open-api * feat(server): add withDeleted to getAssets queries * WIP: mobile-recycle-bin * feat(server): recycle-bin to system config * feat(web): use recycle-bin system config * chore(server): domain assetcore removed * chore(server): rename recycle-bin to trash * chore(web): rename recycle-bin to trash * chore(server): always send soft deleted assets for getAllByUserId * chore(web): formatting * feat(server): permanent delete assets older than trashed period * feat(web): trash empty placeholder image * feat(server): empty trash * feat(web): empty trash * WIP: mobile-recycle-bin * refactor(server): empty / restore trash to separate endpoint * test(server): handle failures * test(server): fix e2e server-info test * test(server): deletion test refactor * feat(mobile): use map settings from server-config to enable / disable map * feat(mobile): trash asset * fix(server): operations on assets in trash * feat(web): show trash statistics * fix(web): handle trash enabled * fix(mobile): restore updates from trash * fix(server): ignore trashed assets for person * fix(server): add / remove search index when trashed / restored * chore(web): format * fix(server): asset service test * fix(server): include trashed assts for duplicates from uploads * feat(mobile): no dialog for trash, always dialog for permanent delete * refactor(mobile): use isar where instead of dart filter * refactor(mobile): asset provide - handle deletes in single db txn * chore(mobile): review changes * feat(web): confirmation before empty trash * server: review changes * fix(server): handle library changes * fix: filter external assets from getting trashed / deleted * fix(server): empty-bin * feat: broadcast config update events through ws * change order of trash button on mobile * styling * fix(mobile): do not show trashed toast for local only assets --------- Co-authored-by: Jason Rasmussen <jrasm91@gmail.com> Co-authored-by: Alex Tran <alex.tran1502@gmail.com>
225 lines
6.7 KiB
TypeScript
225 lines
6.7 KiB
TypeScript
import { AssetCreate } from '@app/domain';
|
|
import { AssetEntity } from '@app/infra/entities';
|
|
import { Injectable } from '@nestjs/common';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { MoreThan } from 'typeorm';
|
|
import { In } from 'typeorm/find-options/operator/In';
|
|
import { Repository } from 'typeorm/repository/Repository';
|
|
import { AssetSearchDto } from './dto/asset-search.dto';
|
|
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
|
import { SearchPropertiesDto } from './dto/search-properties.dto';
|
|
import { CuratedLocationsResponseDto } from './response-dto/curated-locations-response.dto';
|
|
import { CuratedObjectsResponseDto } from './response-dto/curated-objects-response.dto';
|
|
|
|
export interface AssetCheck {
|
|
id: string;
|
|
checksum: Buffer;
|
|
}
|
|
|
|
export interface AssetOwnerCheck extends AssetCheck {
|
|
ownerId: string;
|
|
}
|
|
|
|
export interface IAssetRepository {
|
|
get(id: string): Promise<AssetEntity | null>;
|
|
create(asset: AssetCreate): Promise<AssetEntity>;
|
|
getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
|
|
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
|
|
getById(assetId: string): Promise<AssetEntity>;
|
|
getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]>;
|
|
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]>;
|
|
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]>;
|
|
getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
|
|
getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
|
|
getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null>;
|
|
}
|
|
|
|
export const IAssetRepository = 'IAssetRepository';
|
|
|
|
@Injectable()
|
|
export class AssetRepository implements IAssetRepository {
|
|
constructor(@InjectRepository(AssetEntity) private assetRepository: Repository<AssetEntity>) {}
|
|
|
|
getSearchPropertiesByUserId(userId: string): Promise<SearchPropertiesDto[]> {
|
|
return this.assetRepository
|
|
.createQueryBuilder('asset')
|
|
.where('asset.ownerId = :userId', { userId: userId })
|
|
.andWhere('asset.isVisible = true')
|
|
.leftJoin('asset.exifInfo', 'ei')
|
|
.leftJoin('asset.smartInfo', 'si')
|
|
.select('si.tags', 'tags')
|
|
.addSelect('si.objects', 'objects')
|
|
.addSelect('asset.type', 'assetType')
|
|
.addSelect('ei.orientation', 'orientation')
|
|
.addSelect('ei."lensModel"', 'lensModel')
|
|
.addSelect('ei.make', 'make')
|
|
.addSelect('ei.model', 'model')
|
|
.addSelect('ei.city', 'city')
|
|
.addSelect('ei.state', 'state')
|
|
.addSelect('ei.country', 'country')
|
|
.distinctOn(['si.tags'])
|
|
.getRawMany();
|
|
}
|
|
|
|
getDetectedObjectsByUserId(userId: string): Promise<CuratedObjectsResponseDto[]> {
|
|
return this.assetRepository.query(
|
|
`
|
|
SELECT DISTINCT ON (unnest(si.objects)) a.id, unnest(si.objects) as "object", a."resizePath", a."deviceAssetId", a."deviceId"
|
|
FROM assets a
|
|
LEFT JOIN smart_info si ON a.id = si."assetId"
|
|
WHERE a."ownerId" = $1
|
|
AND a."isVisible" = true
|
|
AND si.objects IS NOT NULL
|
|
`,
|
|
[userId],
|
|
);
|
|
}
|
|
|
|
getLocationsByUserId(userId: string): Promise<CuratedLocationsResponseDto[]> {
|
|
return this.assetRepository.query(
|
|
`
|
|
SELECT DISTINCT ON (e.city) a.id, e.city, a."resizePath", a."deviceAssetId", a."deviceId"
|
|
FROM assets a
|
|
LEFT JOIN exif e ON a.id = e."assetId"
|
|
WHERE a."ownerId" = $1
|
|
AND a."isVisible" = true
|
|
AND e.city IS NOT NULL
|
|
AND a.type = 'IMAGE';
|
|
`,
|
|
[userId],
|
|
);
|
|
}
|
|
|
|
/**
|
|
* Get a single asset information by its ID
|
|
* - include exif info
|
|
* @param assetId
|
|
*/
|
|
getById(assetId: string): Promise<AssetEntity> {
|
|
return this.assetRepository.findOneOrFail({
|
|
where: {
|
|
id: assetId,
|
|
},
|
|
relations: {
|
|
exifInfo: true,
|
|
tags: true,
|
|
sharedLinks: true,
|
|
smartInfo: true,
|
|
owner: true,
|
|
faces: {
|
|
person: true,
|
|
},
|
|
},
|
|
// We are specifically asking for this asset. Return it even if it is soft deleted
|
|
withDeleted: true,
|
|
});
|
|
}
|
|
|
|
/**
|
|
* Get all assets belong to the user on the database
|
|
* @param ownerId
|
|
*/
|
|
getAllByUserId(ownerId: string, dto: AssetSearchDto): Promise<AssetEntity[]> {
|
|
return this.assetRepository.find({
|
|
where: {
|
|
ownerId,
|
|
isVisible: true,
|
|
isFavorite: dto.isFavorite,
|
|
isArchived: dto.isArchived,
|
|
updatedAt: dto.updatedAfter ? MoreThan(dto.updatedAfter) : undefined,
|
|
},
|
|
relations: {
|
|
exifInfo: true,
|
|
tags: true,
|
|
},
|
|
skip: dto.skip || 0,
|
|
order: {
|
|
fileCreatedAt: 'DESC',
|
|
},
|
|
withDeleted: true,
|
|
});
|
|
}
|
|
|
|
get(id: string): Promise<AssetEntity | null> {
|
|
return this.assetRepository.findOne({
|
|
where: { id },
|
|
relations: {
|
|
faces: {
|
|
person: true,
|
|
},
|
|
library: true,
|
|
},
|
|
withDeleted: true,
|
|
});
|
|
}
|
|
|
|
create(asset: AssetCreate): Promise<AssetEntity> {
|
|
return this.assetRepository.save(asset);
|
|
}
|
|
|
|
/**
|
|
* Get assets by device's Id on the database
|
|
* @param ownerId
|
|
* @param deviceId
|
|
*
|
|
* @returns Promise<string[]> - Array of assetIds belong to the device
|
|
*/
|
|
async getAllByDeviceId(ownerId: string, deviceId: string): Promise<string[]> {
|
|
const items = await this.assetRepository.find({
|
|
select: { deviceAssetId: true },
|
|
where: {
|
|
ownerId,
|
|
deviceId,
|
|
isVisible: true,
|
|
},
|
|
});
|
|
|
|
return items.map((asset) => asset.deviceAssetId);
|
|
}
|
|
|
|
/**
|
|
* Get assets by checksums on the database
|
|
* @param ownerId
|
|
* @param checksums
|
|
*
|
|
*/
|
|
getAssetsByChecksums(ownerId: string, checksums: Buffer[]): Promise<AssetCheck[]> {
|
|
return this.assetRepository.find({
|
|
select: {
|
|
id: true,
|
|
checksum: true,
|
|
},
|
|
where: {
|
|
ownerId,
|
|
checksum: In(checksums),
|
|
},
|
|
withDeleted: true,
|
|
});
|
|
}
|
|
|
|
async getExistingAssets(ownerId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]> {
|
|
const assets = await this.assetRepository.find({
|
|
select: { deviceAssetId: true },
|
|
where: {
|
|
deviceAssetId: In(checkDuplicateAssetDto.deviceAssetIds),
|
|
deviceId: checkDuplicateAssetDto.deviceId,
|
|
ownerId,
|
|
},
|
|
});
|
|
return assets.map((asset) => asset.deviceAssetId);
|
|
}
|
|
|
|
getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null> {
|
|
return this.assetRepository.findOne({
|
|
select: {
|
|
id: true,
|
|
ownerId: true,
|
|
checksum: true,
|
|
},
|
|
where: {
|
|
originalPath,
|
|
},
|
|
});
|
|
}
|
|
}
|