mirror of
https://github.com/immich-app/immich.git
synced 2026-03-25 19:44:21 -07:00
Compare commits
1 Commits
fix/motion
...
chore/libr
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
21c54c740a |
@@ -1,5 +1,5 @@
|
||||
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 { userDto, uuidDto } from 'src/fixtures';
|
||||
import { errorDto } from 'src/responses';
|
||||
@@ -768,553 +768,6 @@ describe('/libraries', () => {
|
||||
|
||||
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', () => {
|
||||
|
||||
@@ -866,7 +866,6 @@
|
||||
"crop_aspect_ratio_fixed": "Fixed",
|
||||
"crop_aspect_ratio_free": "Free",
|
||||
"crop_aspect_ratio_original": "Original",
|
||||
"crop_aspect_ratio_square": "Square",
|
||||
"curated_object_page_title": "Things",
|
||||
"current_device": "Current device",
|
||||
"current_pin_code": "Current PIN code",
|
||||
|
||||
@@ -27,10 +27,6 @@
|
||||
"matchUpdateTypes": ["major"],
|
||||
"enabled": false
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["ghcr.io/immich-app/base-server-*"],
|
||||
"maxMajorIncrement": 0
|
||||
},
|
||||
{
|
||||
"matchPackageNames": ["ruby"],
|
||||
"groupName": "ruby",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
FROM ghcr.io/immich-app/base-server-dev:202603251709@sha256:2bf3053c732fcb87ec90c3c614632ac44847423468ccc57fd935bff771828d9d AS builder
|
||||
FROM ghcr.io/immich-app/base-server-dev:202603031112@sha256:837536db5fd9e432f0f474ef9b61712fe3b3815821c3e4edf5e5b0b1f1ed30ad AS builder
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
CI=1 \
|
||||
COREPACK_HOME=/tmp \
|
||||
@@ -71,7 +71,7 @@ RUN --mount=type=cache,id=pnpm-plugins,target=/buildcache/pnpm-store \
|
||||
--mount=type=cache,id=mise-tools-${TARGETPLATFORM},target=/buildcache/mise \
|
||||
cd plugins && mise run build
|
||||
|
||||
FROM ghcr.io/immich-app/base-server-prod:202603251709@sha256:17de30977ff87aa06758a56ad7f10d6b5c97bf9dab76e4ec4177a2a8d1b2b5f3
|
||||
FROM ghcr.io/immich-app/base-server-prod:202603031112@sha256:bb8c8645ee61977140121e56ba09db7ae656a7506f9a6af1be8461b4d81fdf03
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:202603251709@sha256:2bf3053c732fcb87ec90c3c614632ac44847423468ccc57fd935bff771828d9d AS dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:202603031112@sha256:837536db5fd9e432f0f474ef9b61712fe3b3815821c3e4edf5e5b0b1f1ed30ad AS dev
|
||||
|
||||
ENV COREPACK_ENABLE_DOWNLOAD_PROMPT=0 \
|
||||
CI=1 \
|
||||
|
||||
@@ -30,6 +30,7 @@ import { DatabaseRepository } from 'src/repositories/database.repository';
|
||||
import { EmailRepository } from 'src/repositories/email.repository';
|
||||
import { EventRepository } from 'src/repositories/event.repository';
|
||||
import { JobRepository } from 'src/repositories/job.repository';
|
||||
import { LibraryRepository } from 'src/repositories/library.repository';
|
||||
import { LoggingRepository } from 'src/repositories/logging.repository';
|
||||
import { MachineLearningRepository } from 'src/repositories/machine-learning.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 AssetJobRepository:
|
||||
case MemoryRepository:
|
||||
case LibraryRepository:
|
||||
case NotificationRepository:
|
||||
case OcrRepository:
|
||||
case PartnerRepository:
|
||||
@@ -468,6 +470,7 @@ const newMockRepository = <T>(key: ClassConstructor<T>) => {
|
||||
case AssetJobRepository:
|
||||
case ConfigRepository:
|
||||
case CryptoRepository:
|
||||
case LibraryRepository:
|
||||
case MemoryRepository:
|
||||
case NotificationRepository:
|
||||
case OcrRepository:
|
||||
|
||||
456
server/test/medium/specs/services/library.service.spec.ts
Normal file
456
server/test/medium/specs/services/library.service.spec.ts
Normal file
@@ -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();
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -120,10 +120,10 @@
|
||||
<ActionButton action={Cast} />
|
||||
<ActionButton action={Actions.Share} />
|
||||
<ActionButton action={Actions.Offline} />
|
||||
<ActionButton action={Actions.ZoomIn} />
|
||||
<ActionButton action={Actions.ZoomOut} />
|
||||
<ActionButton action={Actions.PlayMotionPhoto} />
|
||||
<ActionButton action={Actions.StopMotionPhoto} />
|
||||
<ActionButton action={Actions.ZoomIn} />
|
||||
<ActionButton action={Actions.ZoomOut} />
|
||||
<ActionButton action={Actions.Copy} />
|
||||
<ActionButton action={Actions.SharedLinkDownload} />
|
||||
<ActionButton action={Actions.Info} />
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
{ label: '2:3', value: '2:3', width: 16, height: 24 },
|
||||
{ label: '16:9', value: '16:9', width: 24, height: 14 },
|
||||
{ label: '9:16', value: '9:16', width: 14, height: 24 },
|
||||
{ label: $t('crop_aspect_ratio_square'), value: '1:1', width: 20, height: 20 },
|
||||
{ label: 'Square', value: '1:1', width: 20, height: 20 },
|
||||
];
|
||||
|
||||
let isRotated = $derived(transformManager.normalizedRotation % 180 !== 0);
|
||||
|
||||
Reference in New Issue
Block a user