1
0
Fork 0
mirror of https://github.com/immich-app/immich.git synced 2024-12-29 15:11:58 +00:00

fix(server): external library motion photo video asset handling (#8721)

* added "isExternal" to the getLibraryAssetPaths query

* handleQueueAssetRefresh skip "non external" video asset, closes #8562

* correctly implements live photo deletion for external library

* use "external asset" for external library tests

* minor: external library asset checksum is "path hash" not file hash

* renamed to getExternalLibraryAssetPaths and added isExternal where clause

* generated sql

* reverted leftover change
This commit is contained in:
Kevin Huang 2024-04-14 16:55:44 -07:00 committed by GitHub
parent a903898781
commit 85df3f1e99
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
8 changed files with 61 additions and 17 deletions

View file

@ -155,7 +155,7 @@ export interface IAssetRepository {
getRandom(userId: string, count: number): Promise<AssetEntity[]>;
getFirstAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
getLastUpdatedAssetForAlbumId(albumId: string): Promise<AssetEntity | null>;
getLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated<AssetPathEntity>;
getExternalLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated<AssetPathEntity>;
getByLibraryIdAndOriginalPath(libraryId: string, originalPath: string): Promise<AssetEntity | null>;
deleteAll(ownerId: string): Promise<void>;
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;

View file

@ -253,7 +253,7 @@ DELETE FROM "assets"
WHERE
"ownerId" = $1
-- AssetRepository.getLibraryAssetPaths
-- AssetRepository.getExternalLibraryAssetPaths
SELECT DISTINCT
"distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id"
FROM
@ -272,6 +272,7 @@ FROM
(
(
((("AssetEntity__AssetEntity_library"."id" = $1)))
AND ("AssetEntity"."isExternal" = $2)
)
)
AND ("AssetEntity"."deletedAt" IS NULL)

View file

@ -160,10 +160,10 @@ export class AssetRepository implements IAssetRepository {
}
@GenerateSql({ params: [{ take: 1, skip: 0 }, DummyValue.UUID] })
getLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated<AssetPathEntity> {
getExternalLibraryAssetPaths(pagination: PaginationOptions, libraryId: string): Paginated<AssetPathEntity> {
return paginate(this.repository, pagination, {
select: { id: true, originalPath: true, isOffline: true },
where: { library: { id: libraryId } },
where: { library: { id: libraryId }, isExternal: true },
});
}

View file

@ -396,7 +396,10 @@ export class AssetService {
// TODO refactor this to use cascades
if (asset.livePhotoVideoId) {
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } });
await this.jobRepository.queue({
name: JobName.ASSET_DELETION,
data: { id: asset.livePhotoVideoId, fromExternal },
});
}
const files = [asset.thumbnailPath, asset.previewPath, asset.encodedVideoPath];

View file

@ -160,7 +160,7 @@ describe(LibraryService.name, () => {
storageMock.walk.mockImplementation(async function* generator() {
yield '/data/user1/photo.jpg';
});
assetMock.getLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
await sut.handleQueueAssetRefresh(mockLibraryJob);
@ -189,7 +189,7 @@ describe(LibraryService.name, () => {
storageMock.walk.mockImplementation(async function* generator() {
yield '/data/user1/photo.jpg';
});
assetMock.getLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
await sut.handleQueueAssetRefresh(mockLibraryJob);
@ -238,7 +238,7 @@ describe(LibraryService.name, () => {
};
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
assetMock.getLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
assetMock.getExternalLibraryAssetPaths.mockResolvedValue({ items: [], hasNextPage: false });
await sut.handleQueueAssetRefresh(mockLibraryJob);
@ -256,8 +256,8 @@ describe(LibraryService.name, () => {
};
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
assetMock.getLibraryAssetPaths.mockResolvedValue({
items: [assetStub.image],
assetMock.getExternalLibraryAssetPaths.mockResolvedValue({
items: [assetStub.external],
hasNextPage: false,
});
@ -278,16 +278,16 @@ describe(LibraryService.name, () => {
libraryMock.get.mockResolvedValue(libraryStub.externalLibrary1);
// eslint-disable-next-line @typescript-eslint/require-await
storageMock.walk.mockImplementation(async function* generator() {
yield assetStub.offline.originalPath;
yield assetStub.externalOffline.originalPath;
});
assetMock.getLibraryAssetPaths.mockResolvedValue({
items: [assetStub.offline],
assetMock.getExternalLibraryAssetPaths.mockResolvedValue({
items: [assetStub.externalOffline],
hasNextPage: false,
});
await sut.handleQueueAssetRefresh(mockLibraryJob);
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.offline.id], { isOffline: false });
expect(assetMock.updateAll).toHaveBeenCalledWith([assetStub.externalOffline.id], { isOffline: false });
expect(assetMock.updateAll).not.toHaveBeenCalledWith(expect.anything(), { isOffline: true });
expect(jobMock.queueAll).not.toHaveBeenCalled();
});

View file

@ -616,7 +616,7 @@ export class LibraryService extends EventEmitter {
const assetIdsToMarkOffline = [];
const assetIdsToMarkOnline = [];
const pagination = usePagination(LIBRARY_SCAN_BATCH_SIZE, (pagination) =>
this.assetRepository.getLibraryAssetPaths(pagination, library.id),
this.assetRepository.getExternalLibraryAssetPaths(pagination, library.id),
);
this.logger.verbose(`Crawled asset paths paginated`);

View file

@ -225,7 +225,7 @@ export const assetStub = {
deviceId: 'device-id',
originalPath: '/data/user1/photo.jpg',
previewPath: '/uploads/user-id/thumbs/path.jpg',
checksum: Buffer.from('file hash', 'utf8'),
checksum: Buffer.from('path hash', 'utf8'),
type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'),
@ -295,6 +295,46 @@ export const assetStub = {
deletedAt: null,
}),
externalOffline: Object.freeze<AssetEntity>({
id: 'asset-id',
deviceAssetId: 'device-asset-id',
fileModifiedAt: new Date('2023-02-23T05:06:29.716Z'),
fileCreatedAt: new Date('2023-02-23T05:06:29.716Z'),
owner: userStub.user1,
ownerId: 'user-id',
deviceId: 'device-id',
originalPath: '/data/user1/photo.jpg',
previewPath: '/uploads/user-id/thumbs/path.jpg',
checksum: Buffer.from('path hash', 'utf8'),
type: AssetType.IMAGE,
thumbnailPath: '/uploads/user-id/webp/path.ext',
thumbhash: Buffer.from('blablabla', 'base64'),
encodedVideoPath: null,
createdAt: new Date('2023-02-23T05:06:29.716Z'),
updatedAt: new Date('2023-02-23T05:06:29.716Z'),
localDateTime: new Date('2023-02-23T05:06:29.716Z'),
isFavorite: true,
isArchived: false,
isReadOnly: false,
isExternal: true,
duration: null,
isVisible: true,
livePhotoVideo: null,
livePhotoVideoId: null,
isOffline: true,
libraryId: 'library-id',
library: libraryStub.externalLibrary1,
tags: [],
sharedLinks: [],
originalFileName: 'asset-id.jpg',
faces: [],
sidecarPath: null,
exifInfo: {
fileSizeInByte: 5000,
} as ExifEntity,
deletedAt: null,
}),
image1: Object.freeze<AssetEntity>({
id: 'asset-id-1',
deviceAssetId: 'device-asset-id',

View file

@ -20,7 +20,7 @@ export const newAssetRepositoryMock = (): jest.Mocked<IAssetRepository> => {
getAll: jest.fn().mockResolvedValue({ items: [], hasNextPage: false }),
getAllByDeviceId: jest.fn(),
updateAll: jest.fn(),
getLibraryAssetPaths: jest.fn(),
getExternalLibraryAssetPaths: jest.fn(),
getByLibraryIdAndOriginalPath: jest.fn(),
deleteAll: jest.fn(),
update: jest.fn(),