mirror of
https://github.com/immich-app/immich.git
synced 2026-05-17 13:02:14 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 21c54c740a |
@@ -1,5 +1,5 @@
|
|||||||
import { LibraryResponseDto, LoginResponseDto, getAllLibraries } from '@immich/sdk';
|
import { LibraryResponseDto, LoginResponseDto, getAllLibraries } from '@immich/sdk';
|
||||||
import { cpSync, existsSync, rmSync, unlinkSync } from 'node:fs';
|
import { cpSync, existsSync } from 'node:fs';
|
||||||
import { Socket } from 'socket.io-client';
|
import { Socket } from 'socket.io-client';
|
||||||
import { userDto, uuidDto } from 'src/fixtures';
|
import { userDto, uuidDto } from 'src/fixtures';
|
||||||
import { errorDto } from 'src/responses';
|
import { errorDto } from 'src/responses';
|
||||||
@@ -768,553 +768,6 @@ describe('/libraries', () => {
|
|||||||
|
|
||||||
utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
utils.removeImageFile(`${testAssetDir}/temp/reimport/asset.jpg`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should set an asset offline if its file is missing', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
|
||||||
});
|
|
||||||
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
expect(assets.count).toBe(1);
|
|
||||||
|
|
||||||
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
|
||||||
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
|
||||||
expect(trashedAsset.isOffline).toEqual(true);
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
expect(newAssets.items).toEqual([]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set an asset offline if its file is not in any import path', async () => {
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
|
||||||
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
expect(assets.count).toBe(1);
|
|
||||||
|
|
||||||
utils.createDirectory(`${testAssetDir}/temp/another-path/`);
|
|
||||||
|
|
||||||
await utils.updateLibrary(admin.accessToken, library.id, {
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/another-path/`],
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
|
||||||
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
|
||||||
expect(trashedAsset.isOffline).toBe(true);
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
expect(newAssets.items).toEqual([]);
|
|
||||||
|
|
||||||
utils.removeImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
|
||||||
utils.removeDirectory(`${testAssetDir}/temp/another-path/`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set an asset offline if its file is covered by an exclusion pattern', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, {
|
|
||||||
libraryId: library.id,
|
|
||||||
originalFileName: 'assetB.png',
|
|
||||||
});
|
|
||||||
expect(assets.count).toBe(1);
|
|
||||||
|
|
||||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/directoryB/**'] });
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
|
||||||
expect(trashedAsset.isTrashed).toBe(true);
|
|
||||||
expect(trashedAsset.originalPath).toBe(`${testAssetDirInternal}/temp/directoryB/assetB.png`);
|
|
||||||
expect(trashedAsset.isOffline).toBe(true);
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
expect(newAssets.items).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
originalFileName: 'assetA.png',
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set an asset offline if its file exists, is in an import path, and not covered by an exclusion pattern', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp`],
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets: assetsBefore } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
expect(assetsBefore.count).toBeGreaterThan(1);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
expect(assets).toEqual(assetsBefore);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('xmp metadata', async () => {
|
|
||||||
it('should import metadata from file.xmp', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
|
||||||
});
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
|
||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
expect(newAssets.items).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
originalFileName: 'glarus.nef',
|
|
||||||
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should import metadata from file.ext.xmp', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
|
||||||
});
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
|
||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
expect(newAssets.items).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
originalFileName: 'glarus.nef',
|
|
||||||
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should import metadata in file.ext.xmp before file.xmp if both exist', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
|
||||||
});
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
|
||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
expect(newAssets.items).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
originalFileName: 'glarus.nef',
|
|
||||||
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should switch from using file.xmp to file.ext.xmp when asset refreshes', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
|
||||||
});
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
|
||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
|
||||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
|
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
expect(newAssets.items).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
originalFileName: 'glarus.nef',
|
|
||||||
fileCreatedAt: '2010-09-27T12:35:33.000Z',
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should switch from using file metadata to file.xmp metadata when asset refreshes', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
|
||||||
});
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
expect(newAssets.items).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
originalFileName: 'glarus.nef',
|
|
||||||
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should switch from using file metadata to file.ext.xmp metadata when asset refreshes', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
|
||||||
});
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
expect(newAssets.items).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
originalFileName: 'glarus.nef',
|
|
||||||
fileCreatedAt: '2000-09-27T12:35:33.000Z',
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should switch from using file.ext.xmp to file.xmp when asset refreshes', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
|
||||||
});
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
|
||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2010.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
|
||||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
expect(newAssets.items).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
originalFileName: 'glarus.nef',
|
|
||||||
fileCreatedAt: '2010-09-27T12:35:33.000Z',
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should switch from using file.ext.xmp to file metadata', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
|
||||||
});
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
|
||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.nef.xmp`);
|
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
expect(newAssets.items).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
originalFileName: 'glarus.nef',
|
|
||||||
fileCreatedAt: '2010-07-20T17:27:12.000Z',
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should switch from using file.xmp to file metadata', async () => {
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/xmp`],
|
|
||||||
});
|
|
||||||
|
|
||||||
cpSync(`${testAssetDir}/metadata/xmp/dates/2000.xmp`, `${testAssetDir}/temp/xmp/glarus.xmp`);
|
|
||||||
cpSync(`${testAssetDir}/formats/raw/Nikon/D80/glarus.nef`, `${testAssetDir}/temp/xmp/glarus.nef`);
|
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_000);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
unlinkSync(`${testAssetDir}/temp/xmp/glarus.xmp`);
|
|
||||||
await utimes(`${testAssetDir}/temp/xmp/glarus.nef`, 447_775_200_001);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets: newAssets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
expect(newAssets.items).toEqual([
|
|
||||||
expect.objectContaining({
|
|
||||||
originalFileName: 'glarus.nef',
|
|
||||||
fileCreatedAt: '2010-07-20T17:27:12.000Z',
|
|
||||||
}),
|
|
||||||
]);
|
|
||||||
|
|
||||||
rmSync(`${testAssetDir}/temp/xmp`, { recursive: true, force: true });
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set an offline asset to online if its file exists, is in an import path, and not covered by an exclusion pattern', async () => {
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
|
||||||
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
expect(assets.count).toBe(1);
|
|
||||||
|
|
||||||
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
|
||||||
expect(offlineAsset.isTrashed).toBe(true);
|
|
||||||
expect(offlineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
|
||||||
expect(offlineAsset.isOffline).toBe(true);
|
|
||||||
|
|
||||||
{
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
|
||||||
expect(assets.count).toBe(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const backOnlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
|
||||||
|
|
||||||
expect(backOnlineAsset.isTrashed).toBe(false);
|
|
||||||
expect(backOnlineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
|
||||||
expect(backOnlineAsset.isOffline).toBe(false);
|
|
||||||
|
|
||||||
{
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
expect(assets.count).toBe(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set a trashed offline asset to online but keep it in trash', async () => {
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
|
||||||
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
expect(assets.count).toBe(1);
|
|
||||||
|
|
||||||
await utils.deleteAssets(admin.accessToken, [assets.items[0].id]);
|
|
||||||
|
|
||||||
{
|
|
||||||
const trashedAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
|
||||||
|
|
||||||
expect(trashedAsset.isTrashed).toBe(true);
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
|
||||||
expect(offlineAsset.isTrashed).toBe(true);
|
|
||||||
expect(offlineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
|
||||||
expect(offlineAsset.isOffline).toBe(true);
|
|
||||||
|
|
||||||
{
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
|
||||||
expect(assets.count).toBe(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const backOnlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
|
||||||
|
|
||||||
expect(backOnlineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
|
||||||
expect(backOnlineAsset.isOffline).toBe(false);
|
|
||||||
expect(backOnlineAsset.isTrashed).toBe(true);
|
|
||||||
|
|
||||||
{
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
|
||||||
expect(assets.count).toBe(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set an offline asset to online if its file exists, is not covered by an exclusion pattern, but is outside of all import paths', async () => {
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
|
||||||
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
|
|
||||||
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
{
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
|
||||||
expect(assets.count).toBe(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
|
||||||
|
|
||||||
expect(offlineAsset.isTrashed).toBe(true);
|
|
||||||
expect(offlineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
|
||||||
expect(offlineAsset.isOffline).toBe(true);
|
|
||||||
|
|
||||||
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
|
|
||||||
|
|
||||||
utils.createDirectory(`${testAssetDir}/temp/another-path/`);
|
|
||||||
|
|
||||||
await utils.updateLibrary(admin.accessToken, library.id, {
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/another-path`],
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
|
||||||
|
|
||||||
expect(stillOfflineAsset.isTrashed).toBe(true);
|
|
||||||
expect(stillOfflineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
|
||||||
expect(stillOfflineAsset.isOffline).toBe(true);
|
|
||||||
|
|
||||||
{
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
|
||||||
expect(assets.count).toBe(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.removeDirectory(`${testAssetDir}/temp/another-path/`);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not set an offline asset to online if its file exists, is in an import path, but is covered by an exclusion pattern', async () => {
|
|
||||||
utils.createImageFile(`${testAssetDir}/temp/offline/offline.png`);
|
|
||||||
|
|
||||||
const library = await utils.createLibrary(admin.accessToken, {
|
|
||||||
ownerId: admin.userId,
|
|
||||||
importPaths: [`${testAssetDirInternal}/temp/offline`],
|
|
||||||
});
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
{
|
|
||||||
const { assets: assetsBefore } = await utils.searchAssets(admin.accessToken, { libraryId: library.id });
|
|
||||||
expect(assetsBefore.count).toBe(1);
|
|
||||||
}
|
|
||||||
|
|
||||||
utils.renameImageFile(`${testAssetDir}/temp/offline/offline.png`, `${testAssetDir}/temp/offline.png`);
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
|
||||||
expect(assets.count).toBe(1);
|
|
||||||
|
|
||||||
const offlineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
|
||||||
|
|
||||||
expect(offlineAsset.isTrashed).toBe(true);
|
|
||||||
expect(offlineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
|
||||||
expect(offlineAsset.isOffline).toBe(true);
|
|
||||||
|
|
||||||
utils.renameImageFile(`${testAssetDir}/temp/offline.png`, `${testAssetDir}/temp/offline/offline.png`);
|
|
||||||
|
|
||||||
await utils.updateLibrary(admin.accessToken, library.id, { exclusionPatterns: ['**/offline/**'] });
|
|
||||||
|
|
||||||
await utils.scan(admin.accessToken, library.id);
|
|
||||||
|
|
||||||
const stillOfflineAsset = await utils.getAssetInfo(admin.accessToken, assets.items[0].id);
|
|
||||||
|
|
||||||
expect(stillOfflineAsset.isTrashed).toBe(true);
|
|
||||||
expect(stillOfflineAsset.originalPath).toBe(`${testAssetDirInternal}/temp/offline/offline.png`);
|
|
||||||
expect(stillOfflineAsset.isOffline).toBe(true);
|
|
||||||
|
|
||||||
{
|
|
||||||
const { assets } = await utils.searchAssets(admin.accessToken, { libraryId: library.id, withDeleted: true });
|
|
||||||
expect(assets.count).toBe(1);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('POST /libraries/:id/validate', () => {
|
describe('POST /libraries/:id/validate', () => {
|
||||||
|
|||||||
@@ -30,6 +30,7 @@ import { DatabaseRepository } from 'src/repositories/database.repository';
|
|||||||
import { EmailRepository } from 'src/repositories/email.repository';
|
import { EmailRepository } from 'src/repositories/email.repository';
|
||||||
import { EventRepository } from 'src/repositories/event.repository';
|
import { EventRepository } from 'src/repositories/event.repository';
|
||||||
import { JobRepository } from 'src/repositories/job.repository';
|
import { JobRepository } from 'src/repositories/job.repository';
|
||||||
|
import { LibraryRepository } from 'src/repositories/library.repository';
|
||||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
|
import { MachineLearningRepository } from 'src/repositories/machine-learning.repository';
|
||||||
import { MapRepository } from 'src/repositories/map.repository';
|
import { MapRepository } from 'src/repositories/map.repository';
|
||||||
@@ -406,6 +407,7 @@ const newRealRepository = <T>(key: ClassConstructor<T>, db: Kysely<DB>): T => {
|
|||||||
case AssetEditRepository:
|
case AssetEditRepository:
|
||||||
case AssetJobRepository:
|
case AssetJobRepository:
|
||||||
case MemoryRepository:
|
case MemoryRepository:
|
||||||
|
case LibraryRepository:
|
||||||
case NotificationRepository:
|
case NotificationRepository:
|
||||||
case OcrRepository:
|
case OcrRepository:
|
||||||
case PartnerRepository:
|
case PartnerRepository:
|
||||||
@@ -468,6 +470,7 @@ const newMockRepository = <T>(key: ClassConstructor<T>) => {
|
|||||||
case AssetJobRepository:
|
case AssetJobRepository:
|
||||||
case ConfigRepository:
|
case ConfigRepository:
|
||||||
case CryptoRepository:
|
case CryptoRepository:
|
||||||
|
case LibraryRepository:
|
||||||
case MemoryRepository:
|
case MemoryRepository:
|
||||||
case NotificationRepository:
|
case NotificationRepository:
|
||||||
case OcrRepository:
|
case OcrRepository:
|
||||||
|
|||||||
@@ -0,0 +1,456 @@
|
|||||||
|
import { Kysely } from 'kysely';
|
||||||
|
import { Stats } from 'node:fs';
|
||||||
|
import { join } from 'node:path';
|
||||||
|
import { AssetStatus, JobName, JobStatus } from 'src/enum';
|
||||||
|
import { AssetJobRepository } from 'src/repositories/asset-job.repository';
|
||||||
|
import { AssetRepository } from 'src/repositories/asset.repository';
|
||||||
|
import { CryptoRepository } from 'src/repositories/crypto.repository';
|
||||||
|
import { JobRepository } from 'src/repositories/job.repository';
|
||||||
|
import { LibraryRepository } from 'src/repositories/library.repository';
|
||||||
|
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||||
|
import { StorageRepository } from 'src/repositories/storage.repository';
|
||||||
|
import { DB } from 'src/schema';
|
||||||
|
import { LibraryService } from 'src/services/library.service';
|
||||||
|
import { newMediumService, testAssetsDir } from 'test/medium.factory';
|
||||||
|
import { getKyselyDB } from 'test/utils';
|
||||||
|
|
||||||
|
let defaultDatabase: Kysely<DB>;
|
||||||
|
|
||||||
|
const createFileStats = (mtimeMs: number): Stats => {
|
||||||
|
return { mtime: new Date(mtimeMs) } as Stats;
|
||||||
|
};
|
||||||
|
|
||||||
|
const setup = (db?: Kysely<DB>) => {
|
||||||
|
const context = newMediumService(LibraryService, {
|
||||||
|
database: db || defaultDatabase,
|
||||||
|
real: [AssetRepository, AssetJobRepository, CryptoRepository, LibraryRepository],
|
||||||
|
mock: [StorageRepository, JobRepository, LoggingRepository],
|
||||||
|
});
|
||||||
|
|
||||||
|
const jobs = context.ctx.getMock(JobRepository);
|
||||||
|
jobs.queue.mockResolvedValue();
|
||||||
|
jobs.queueAll.mockResolvedValue();
|
||||||
|
|
||||||
|
return context;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeAll(async () => {
|
||||||
|
defaultDatabase = await getKyselyDB();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe(LibraryService.name, () => {
|
||||||
|
const importRoot = '/libraries/offline';
|
||||||
|
const importPath = `${importRoot}/in-path`;
|
||||||
|
const excludedPath = `${importRoot}/excluded`;
|
||||||
|
const outsidePath = '/libraries/outside';
|
||||||
|
|
||||||
|
const createLibrary = async (
|
||||||
|
ctx: ReturnType<typeof setup>['ctx'],
|
||||||
|
options: { importPaths?: string[]; exclusionPatterns?: string[] } = {},
|
||||||
|
) => {
|
||||||
|
const { user } = await ctx.newUser();
|
||||||
|
return ctx.get(LibraryRepository).create({
|
||||||
|
ownerId: user.id,
|
||||||
|
name: 'Medium test library',
|
||||||
|
importPaths: options.importPaths ?? [importPath],
|
||||||
|
exclusionPatterns: options.exclusionPatterns ?? [],
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
describe('offline asset handling', () => {
|
||||||
|
it('should set an asset offline if its file is missing', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const storage = ctx.getMock(StorageRepository);
|
||||||
|
const assetRepo = ctx.get(AssetRepository);
|
||||||
|
|
||||||
|
const library = await createLibrary(ctx);
|
||||||
|
const { asset } = await ctx.newAsset({
|
||||||
|
ownerId: library.ownerId,
|
||||||
|
libraryId: library.id,
|
||||||
|
originalPath: `${importPath}/offline.png`,
|
||||||
|
isExternal: true,
|
||||||
|
isOffline: false,
|
||||||
|
status: AssetStatus.Active,
|
||||||
|
});
|
||||||
|
|
||||||
|
storage.stat.mockRejectedValue(new Error('ENOENT'));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.handleSyncAssets({
|
||||||
|
libraryId: library.id,
|
||||||
|
importPaths: library.importPaths,
|
||||||
|
exclusionPatterns: library.exclusionPatterns,
|
||||||
|
assetIds: [asset.id],
|
||||||
|
progressCounter: 1,
|
||||||
|
totalAssets: 1,
|
||||||
|
}),
|
||||||
|
).resolves.toBe(JobStatus.Success);
|
||||||
|
|
||||||
|
const updated = await assetRepo.getById(asset.id);
|
||||||
|
expect(updated).toEqual(expect.objectContaining({ isOffline: true }));
|
||||||
|
expect(updated?.deletedAt).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set an asset offline if its file is not in any import path', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const assetRepo = ctx.get(AssetRepository);
|
||||||
|
|
||||||
|
const library = await createLibrary(ctx, { importPaths: [importPath] });
|
||||||
|
const { asset } = await ctx.newAsset({
|
||||||
|
ownerId: library.ownerId,
|
||||||
|
libraryId: library.id,
|
||||||
|
originalPath: `${outsidePath}/offline.png`,
|
||||||
|
isExternal: true,
|
||||||
|
isOffline: false,
|
||||||
|
status: AssetStatus.Active,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(sut.handleQueueSyncAssets({ id: library.id })).resolves.toBe(JobStatus.Success);
|
||||||
|
|
||||||
|
const updated = await assetRepo.getById(asset.id);
|
||||||
|
expect(updated).toEqual(expect.objectContaining({ isOffline: true }));
|
||||||
|
expect(updated?.deletedAt).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set an asset offline if its file is covered by an exclusion pattern', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const assetRepo = ctx.get(AssetRepository);
|
||||||
|
|
||||||
|
const library = await createLibrary(ctx, {
|
||||||
|
importPaths: [importRoot],
|
||||||
|
exclusionPatterns: ['**/excluded/**'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { asset } = await ctx.newAsset({
|
||||||
|
ownerId: library.ownerId,
|
||||||
|
libraryId: library.id,
|
||||||
|
originalPath: `${excludedPath}/offline.png`,
|
||||||
|
isExternal: true,
|
||||||
|
isOffline: false,
|
||||||
|
status: AssetStatus.Active,
|
||||||
|
});
|
||||||
|
|
||||||
|
await expect(sut.handleQueueSyncAssets({ id: library.id })).resolves.toBe(JobStatus.Success);
|
||||||
|
|
||||||
|
const updated = await assetRepo.getById(asset.id);
|
||||||
|
expect(updated).toEqual(expect.objectContaining({ isOffline: true }));
|
||||||
|
expect(updated?.deletedAt).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set an asset offline if file exists in import path and is not excluded', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const storage = ctx.getMock(StorageRepository);
|
||||||
|
const assetRepo = ctx.get(AssetRepository);
|
||||||
|
|
||||||
|
const library = await createLibrary(ctx, {
|
||||||
|
importPaths: [importRoot],
|
||||||
|
exclusionPatterns: ['**/excluded/**'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { asset } = await ctx.newAsset({
|
||||||
|
ownerId: library.ownerId,
|
||||||
|
libraryId: library.id,
|
||||||
|
originalPath: `${importPath}/online.png`,
|
||||||
|
isExternal: true,
|
||||||
|
isOffline: false,
|
||||||
|
status: AssetStatus.Active,
|
||||||
|
});
|
||||||
|
|
||||||
|
storage.stat.mockResolvedValue(createFileStats(1_700_000_000_000));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.handleSyncAssets({
|
||||||
|
libraryId: library.id,
|
||||||
|
importPaths: library.importPaths,
|
||||||
|
exclusionPatterns: library.exclusionPatterns,
|
||||||
|
assetIds: [asset.id],
|
||||||
|
progressCounter: 1,
|
||||||
|
totalAssets: 1,
|
||||||
|
}),
|
||||||
|
).resolves.toBe(JobStatus.Success);
|
||||||
|
|
||||||
|
const updated = await assetRepo.getById(asset.id);
|
||||||
|
expect(updated).toEqual(expect.objectContaining({ isOffline: false }));
|
||||||
|
expect(updated?.deletedAt).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set an offline asset to online if its file exists in an import path and is not excluded', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const storage = ctx.getMock(StorageRepository);
|
||||||
|
const assetRepo = ctx.get(AssetRepository);
|
||||||
|
|
||||||
|
const library = await createLibrary(ctx, { importPaths: [importPath] });
|
||||||
|
const { asset } = await ctx.newAsset({
|
||||||
|
ownerId: library.ownerId,
|
||||||
|
libraryId: library.id,
|
||||||
|
originalPath: `${importPath}/offline.png`,
|
||||||
|
isExternal: true,
|
||||||
|
isOffline: true,
|
||||||
|
deletedAt: new Date(),
|
||||||
|
status: AssetStatus.Active,
|
||||||
|
});
|
||||||
|
|
||||||
|
storage.stat.mockResolvedValue(createFileStats(1_700_000_000_000));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.handleSyncAssets({
|
||||||
|
libraryId: library.id,
|
||||||
|
importPaths: library.importPaths,
|
||||||
|
exclusionPatterns: library.exclusionPatterns,
|
||||||
|
assetIds: [asset.id],
|
||||||
|
progressCounter: 1,
|
||||||
|
totalAssets: 1,
|
||||||
|
}),
|
||||||
|
).resolves.toBe(JobStatus.Success);
|
||||||
|
|
||||||
|
const updated = await assetRepo.getById(asset.id);
|
||||||
|
expect(updated).toEqual(expect.objectContaining({ isOffline: false }));
|
||||||
|
expect(updated?.deletedAt).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not set an offline asset to online if its file exists in an import path but is excluded', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const storage = ctx.getMock(StorageRepository);
|
||||||
|
const assetRepo = ctx.get(AssetRepository);
|
||||||
|
|
||||||
|
const library = await createLibrary(ctx, {
|
||||||
|
importPaths: [importRoot],
|
||||||
|
exclusionPatterns: ['**/offline/**'],
|
||||||
|
});
|
||||||
|
|
||||||
|
const { asset } = await ctx.newAsset({
|
||||||
|
ownerId: library.ownerId,
|
||||||
|
libraryId: library.id,
|
||||||
|
originalPath: `${importRoot}/offline/offline.png`,
|
||||||
|
isExternal: true,
|
||||||
|
isOffline: true,
|
||||||
|
deletedAt: new Date(),
|
||||||
|
status: AssetStatus.Active,
|
||||||
|
});
|
||||||
|
|
||||||
|
storage.stat.mockResolvedValue(createFileStats(1_700_000_000_000));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.handleSyncAssets({
|
||||||
|
libraryId: library.id,
|
||||||
|
importPaths: library.importPaths,
|
||||||
|
exclusionPatterns: library.exclusionPatterns,
|
||||||
|
assetIds: [asset.id],
|
||||||
|
progressCounter: 1,
|
||||||
|
totalAssets: 1,
|
||||||
|
}),
|
||||||
|
).resolves.toBe(JobStatus.Success);
|
||||||
|
|
||||||
|
const updated = await assetRepo.getById(asset.id);
|
||||||
|
expect(updated).toEqual(expect.objectContaining({ isOffline: true }));
|
||||||
|
expect(updated?.deletedAt).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should keep an offline asset offline if it is outside import paths', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const storage = ctx.getMock(StorageRepository);
|
||||||
|
const assetRepo = ctx.get(AssetRepository);
|
||||||
|
|
||||||
|
const library = await createLibrary(ctx, { importPaths: [importPath] });
|
||||||
|
const { asset } = await ctx.newAsset({
|
||||||
|
ownerId: library.ownerId,
|
||||||
|
libraryId: library.id,
|
||||||
|
originalPath: `${outsidePath}/offline.png`,
|
||||||
|
isExternal: true,
|
||||||
|
isOffline: true,
|
||||||
|
deletedAt: new Date(),
|
||||||
|
status: AssetStatus.Active,
|
||||||
|
});
|
||||||
|
|
||||||
|
storage.stat.mockResolvedValue(createFileStats(1_700_000_000_000));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.handleSyncAssets({
|
||||||
|
libraryId: library.id,
|
||||||
|
importPaths: library.importPaths,
|
||||||
|
exclusionPatterns: library.exclusionPatterns,
|
||||||
|
assetIds: [asset.id],
|
||||||
|
progressCounter: 1,
|
||||||
|
totalAssets: 1,
|
||||||
|
}),
|
||||||
|
).resolves.toBe(JobStatus.Success);
|
||||||
|
|
||||||
|
const updated = await assetRepo.getById(asset.id);
|
||||||
|
expect(updated).toEqual(expect.objectContaining({ isOffline: true }));
|
||||||
|
expect(updated?.deletedAt).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set a trashed asset offline if its file is missing', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const storage = ctx.getMock(StorageRepository);
|
||||||
|
const assetRepo = ctx.get(AssetRepository);
|
||||||
|
|
||||||
|
const library = await createLibrary(ctx, { importPaths: [importPath] });
|
||||||
|
const { asset } = await ctx.newAsset({
|
||||||
|
ownerId: library.ownerId,
|
||||||
|
libraryId: library.id,
|
||||||
|
originalPath: `${importPath}/offline.png`,
|
||||||
|
isExternal: true,
|
||||||
|
isOffline: false,
|
||||||
|
deletedAt: new Date(),
|
||||||
|
status: AssetStatus.Trashed,
|
||||||
|
});
|
||||||
|
|
||||||
|
storage.stat.mockRejectedValue(new Error('ENOENT'));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.handleSyncAssets({
|
||||||
|
libraryId: library.id,
|
||||||
|
importPaths: library.importPaths,
|
||||||
|
exclusionPatterns: library.exclusionPatterns,
|
||||||
|
assetIds: [asset.id],
|
||||||
|
progressCounter: 1,
|
||||||
|
totalAssets: 1,
|
||||||
|
}),
|
||||||
|
).resolves.toBe(JobStatus.Success);
|
||||||
|
|
||||||
|
const updated = await assetRepo.getById(asset.id);
|
||||||
|
expect(updated).toEqual(expect.objectContaining({ isOffline: true }));
|
||||||
|
expect(updated?.deletedAt).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set a trashed offline asset to online but keep it in trash', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const storage = ctx.getMock(StorageRepository);
|
||||||
|
const assetRepo = ctx.get(AssetRepository);
|
||||||
|
|
||||||
|
const library = await createLibrary(ctx, { importPaths: [importPath] });
|
||||||
|
const { asset } = await ctx.newAsset({
|
||||||
|
ownerId: library.ownerId,
|
||||||
|
libraryId: library.id,
|
||||||
|
originalPath: `${importPath}/offline.png`,
|
||||||
|
isExternal: true,
|
||||||
|
isOffline: true,
|
||||||
|
deletedAt: new Date(),
|
||||||
|
status: AssetStatus.Trashed,
|
||||||
|
});
|
||||||
|
|
||||||
|
storage.stat.mockResolvedValue(createFileStats(1_700_000_000_000));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.handleSyncAssets({
|
||||||
|
libraryId: library.id,
|
||||||
|
importPaths: library.importPaths,
|
||||||
|
exclusionPatterns: library.exclusionPatterns,
|
||||||
|
assetIds: [asset.id],
|
||||||
|
progressCounter: 1,
|
||||||
|
totalAssets: 1,
|
||||||
|
}),
|
||||||
|
).resolves.toBe(JobStatus.Success);
|
||||||
|
|
||||||
|
const updated = await assetRepo.getById(asset.id);
|
||||||
|
expect(updated).toEqual(expect.objectContaining({ isOffline: false }));
|
||||||
|
expect(updated?.deletedAt).toBeInstanceOf(Date);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('xmp scan behavior', () => {
|
||||||
|
it('should queue sidecar checks for newly imported assets', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const storage = ctx.getMock(StorageRepository);
|
||||||
|
const jobs = ctx.getMock(JobRepository);
|
||||||
|
jobs.queueAll.mockResolvedValue();
|
||||||
|
|
||||||
|
const library = await createLibrary(ctx, { importPaths: ['/libraries/xmp'] });
|
||||||
|
const rawPath = join(testAssetsDir, 'formats/raw/Nikon/D80/glarus.nef');
|
||||||
|
|
||||||
|
storage.stat.mockResolvedValue(createFileStats(1_700_000_000_000));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.handleSyncFiles({
|
||||||
|
libraryId: library.id,
|
||||||
|
paths: [rawPath],
|
||||||
|
progressCounter: 1,
|
||||||
|
}),
|
||||||
|
).resolves.toBe(JobStatus.Success);
|
||||||
|
|
||||||
|
expect(jobs.queueAll).toHaveBeenCalledWith([
|
||||||
|
expect.objectContaining({
|
||||||
|
name: JobName.SidecarCheck,
|
||||||
|
data: expect.objectContaining({ id: expect.any(String) }),
|
||||||
|
}),
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should queue sidecar checks for assets whose file changed', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const storage = ctx.getMock(StorageRepository);
|
||||||
|
const jobs = ctx.getMock(JobRepository);
|
||||||
|
jobs.queueAll.mockResolvedValue();
|
||||||
|
|
||||||
|
const library = await createLibrary(ctx, { importPaths: ['/libraries/xmp'] });
|
||||||
|
const rawPath = join(testAssetsDir, 'formats/raw/Nikon/D80/glarus.nef');
|
||||||
|
|
||||||
|
const { asset } = await ctx.newAsset({
|
||||||
|
ownerId: library.ownerId,
|
||||||
|
libraryId: library.id,
|
||||||
|
originalPath: rawPath,
|
||||||
|
fileModifiedAt: new Date(1_700_000_000_000),
|
||||||
|
isExternal: true,
|
||||||
|
isOffline: false,
|
||||||
|
status: AssetStatus.Active,
|
||||||
|
});
|
||||||
|
|
||||||
|
storage.stat.mockResolvedValue(createFileStats(1_700_000_000_001));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.handleSyncAssets({
|
||||||
|
libraryId: library.id,
|
||||||
|
importPaths: library.importPaths,
|
||||||
|
exclusionPatterns: library.exclusionPatterns,
|
||||||
|
assetIds: [asset.id],
|
||||||
|
progressCounter: 1,
|
||||||
|
totalAssets: 1,
|
||||||
|
}),
|
||||||
|
).resolves.toBe(JobStatus.Success);
|
||||||
|
|
||||||
|
expect(jobs.queueAll).toHaveBeenCalledWith([
|
||||||
|
{
|
||||||
|
name: JobName.SidecarCheck,
|
||||||
|
data: { id: asset.id, source: 'upload' },
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not queue sidecar checks for unchanged assets', async () => {
|
||||||
|
const { sut, ctx } = setup();
|
||||||
|
const storage = ctx.getMock(StorageRepository);
|
||||||
|
const jobs = ctx.getMock(JobRepository);
|
||||||
|
jobs.queueAll.mockResolvedValue();
|
||||||
|
|
||||||
|
const library = await createLibrary(ctx, { importPaths: ['/libraries/xmp'] });
|
||||||
|
const rawPath = join(testAssetsDir, 'formats/raw/Nikon/D80/glarus.nef');
|
||||||
|
|
||||||
|
const mtimeMs = 1_700_000_000_000;
|
||||||
|
const { asset } = await ctx.newAsset({
|
||||||
|
ownerId: library.ownerId,
|
||||||
|
libraryId: library.id,
|
||||||
|
originalPath: rawPath,
|
||||||
|
fileModifiedAt: new Date(mtimeMs),
|
||||||
|
isExternal: true,
|
||||||
|
isOffline: false,
|
||||||
|
status: AssetStatus.Active,
|
||||||
|
});
|
||||||
|
|
||||||
|
storage.stat.mockResolvedValue(createFileStats(mtimeMs));
|
||||||
|
|
||||||
|
await expect(
|
||||||
|
sut.handleSyncAssets({
|
||||||
|
libraryId: library.id,
|
||||||
|
importPaths: library.importPaths,
|
||||||
|
exclusionPatterns: library.exclusionPatterns,
|
||||||
|
assetIds: [asset.id],
|
||||||
|
progressCounter: 1,
|
||||||
|
totalAssets: 1,
|
||||||
|
}),
|
||||||
|
).resolves.toBe(JobStatus.Success);
|
||||||
|
|
||||||
|
expect(jobs.queueAll).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
Reference in New Issue
Block a user