2023-10-06 23:32:28 +02:00
|
|
|
import { LibraryResponseDto, LoginResponseDto } from '@app/domain';
|
|
|
|
import { LibraryController } from '@app/immich';
|
|
|
|
import { AssetType, LibraryType } from '@app/infra/entities';
|
2024-01-10 06:04:16 +02:00
|
|
|
import { errorStub, uuidStub } from '@test/fixtures';
|
2024-02-13 15:48:47 +02:00
|
|
|
import * as fs from 'node:fs';
|
2023-09-20 13:16:33 +02:00
|
|
|
import request from 'supertest';
|
2023-10-06 23:32:28 +02:00
|
|
|
import { utimes } from 'utimes';
|
2024-01-31 01:23:33 +02:00
|
|
|
import {
|
|
|
|
IMMICH_TEST_ASSET_PATH,
|
|
|
|
IMMICH_TEST_ASSET_TEMP_PATH,
|
|
|
|
restoreTempFolder,
|
|
|
|
testApp,
|
|
|
|
} from '../../../src/test-utils/utils';
|
2024-01-25 00:24:53 +02:00
|
|
|
import { api } from '../../client';
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
describe(`${LibraryController.name} (e2e)`, () => {
|
|
|
|
let server: any;
|
2023-10-06 23:32:28 +02:00
|
|
|
let admin: LoginResponseDto;
|
|
|
|
|
2023-09-20 13:16:33 +02:00
|
|
|
beforeAll(async () => {
|
2024-02-13 15:48:47 +02:00
|
|
|
const app = await testApp.create();
|
|
|
|
server = app.getHttpServer();
|
2023-10-19 00:02:42 +02:00
|
|
|
});
|
|
|
|
|
2023-09-20 13:16:33 +02:00
|
|
|
beforeEach(async () => {
|
2023-11-15 03:31:06 +02:00
|
|
|
await testApp.reset();
|
2023-10-11 00:59:13 +02:00
|
|
|
await restoreTempFolder();
|
2023-10-06 23:32:28 +02:00
|
|
|
await api.authApi.adminSignUp(server);
|
|
|
|
admin = await api.authApi.adminLogin(server);
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
|
2024-01-31 10:15:54 +02:00
|
|
|
afterAll(async () => {
|
|
|
|
await testApp.teardown();
|
|
|
|
await restoreTempFolder();
|
|
|
|
});
|
|
|
|
|
2023-09-20 13:16:33 +02:00
|
|
|
describe('DELETE /library/:id', () => {
|
2023-10-11 00:59:13 +02:00
|
|
|
it('should delete an external library with assets', async () => {
|
2023-10-06 23:32:28 +02:00
|
|
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
|
|
|
});
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
|
|
|
|
|
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
|
|
expect(assets.length).toBeGreaterThan(2);
|
|
|
|
|
|
|
|
const { status, body } = await request(server)
|
|
|
|
.delete(`/library/${library.id}`)
|
|
|
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
|
|
|
|
|
|
|
expect(status).toBe(200);
|
|
|
|
expect(body).toEqual({});
|
|
|
|
|
|
|
|
const libraries = await api.libraryApi.getAll(server, admin.accessToken);
|
|
|
|
expect(libraries).toHaveLength(1);
|
|
|
|
expect(libraries).not.toEqual(
|
|
|
|
expect.arrayContaining([
|
|
|
|
expect.objectContaining({
|
|
|
|
id: library.id,
|
|
|
|
}),
|
|
|
|
]),
|
|
|
|
);
|
|
|
|
});
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
|
|
|
|
describe('POST /library/:id/scan', () => {
|
|
|
|
it('should require authentication', async () => {
|
|
|
|
const { status, body } = await request(server).post(`/library/${uuidStub.notFound}/scan`).send({});
|
2023-10-06 23:32:28 +02:00
|
|
|
|
2023-09-20 13:16:33 +02:00
|
|
|
expect(status).toBe(401);
|
|
|
|
expect(body).toEqual(errorStub.unauthorized);
|
|
|
|
});
|
|
|
|
|
2023-10-06 23:32:28 +02:00
|
|
|
it('should scan external library with import paths', async () => {
|
|
|
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
|
|
|
});
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
|
|
|
|
|
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
|
|
|
|
|
|
expect(assets).toEqual(
|
|
|
|
expect.arrayContaining([
|
|
|
|
expect.objectContaining({
|
|
|
|
type: AssetType.IMAGE,
|
|
|
|
originalFileName: 'el_torcal_rocks',
|
|
|
|
libraryId: library.id,
|
|
|
|
resized: true,
|
|
|
|
thumbhash: expect.any(String),
|
|
|
|
exifInfo: expect.objectContaining({
|
|
|
|
exifImageWidth: 512,
|
|
|
|
exifImageHeight: 341,
|
|
|
|
latitude: null,
|
|
|
|
longitude: null,
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
expect.objectContaining({
|
|
|
|
type: AssetType.IMAGE,
|
|
|
|
originalFileName: 'silver_fir',
|
|
|
|
libraryId: library.id,
|
|
|
|
resized: true,
|
|
|
|
thumbhash: expect.any(String),
|
|
|
|
exifInfo: expect.objectContaining({
|
|
|
|
exifImageWidth: 511,
|
|
|
|
exifImageHeight: 323,
|
|
|
|
latitude: null,
|
|
|
|
longitude: null,
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
]),
|
|
|
|
);
|
|
|
|
});
|
2023-09-20 13:16:33 +02:00
|
|
|
|
2023-10-06 23:32:28 +02:00
|
|
|
it('should scan external library with exclusion pattern', async () => {
|
|
|
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
|
|
|
exclusionPatterns: ['**/el_corcal*'],
|
|
|
|
});
|
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
|
|
|
|
|
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
|
|
|
|
|
|
expect(assets).toEqual(
|
|
|
|
expect.arrayContaining([
|
|
|
|
expect.not.objectContaining({
|
|
|
|
// Excluded by exclusion pattern
|
|
|
|
originalFileName: 'el_torcal_rocks',
|
|
|
|
}),
|
|
|
|
expect.objectContaining({
|
|
|
|
type: AssetType.IMAGE,
|
|
|
|
originalFileName: 'silver_fir',
|
|
|
|
libraryId: library.id,
|
|
|
|
resized: true,
|
|
|
|
exifInfo: expect.objectContaining({
|
|
|
|
exifImageWidth: 511,
|
|
|
|
exifImageHeight: 323,
|
|
|
|
latitude: null,
|
|
|
|
longitude: null,
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
]),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should offline missing files', async () => {
|
|
|
|
await fs.promises.cp(`${IMMICH_TEST_ASSET_PATH}/albums/nature`, `${IMMICH_TEST_ASSET_TEMP_PATH}/albums/nature`, {
|
|
|
|
recursive: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
|
|
|
});
|
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
|
|
|
|
|
|
const onlineAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
|
|
expect(onlineAssets.length).toBeGreaterThan(1);
|
|
|
|
|
|
|
|
await restoreTempFolder();
|
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
|
|
|
|
|
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
|
|
|
|
|
|
expect(assets).toEqual(
|
|
|
|
expect.arrayContaining([
|
|
|
|
expect.objectContaining({
|
|
|
|
isOffline: true,
|
|
|
|
originalFileName: 'el_torcal_rocks',
|
|
|
|
}),
|
|
|
|
expect.objectContaining({
|
|
|
|
isOffline: true,
|
|
|
|
originalFileName: 'tanners_ridge',
|
|
|
|
}),
|
|
|
|
]),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should scan new files', async () => {
|
|
|
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
|
|
|
});
|
|
|
|
|
|
|
|
await fs.promises.cp(
|
|
|
|
`${IMMICH_TEST_ASSET_PATH}/albums/nature/silver_fir.jpg`,
|
|
|
|
`${IMMICH_TEST_ASSET_TEMP_PATH}/silver_fir.jpg`,
|
|
|
|
);
|
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
|
|
|
|
|
|
await fs.promises.cp(
|
|
|
|
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
|
|
|
`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`,
|
|
|
|
);
|
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
|
|
|
|
|
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
|
|
|
|
|
|
expect(assets).toEqual(
|
|
|
|
expect.arrayContaining([
|
|
|
|
expect.objectContaining({
|
|
|
|
originalFileName: 'el_torcal_rocks',
|
|
|
|
}),
|
|
|
|
expect.objectContaining({
|
|
|
|
originalFileName: 'silver_fir',
|
|
|
|
}),
|
|
|
|
]),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('with refreshModifiedFiles=true', () => {
|
|
|
|
it('should reimport modified files', async () => {
|
|
|
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
|
|
|
});
|
|
|
|
|
|
|
|
await fs.promises.cp(
|
|
|
|
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
|
|
|
`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`,
|
|
|
|
);
|
|
|
|
|
2024-02-13 15:48:47 +02:00
|
|
|
await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447_775_200_000);
|
2023-10-06 23:32:28 +02:00
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
|
|
|
|
|
|
await fs.promises.cp(
|
|
|
|
`${IMMICH_TEST_ASSET_PATH}/albums/nature/tanners_ridge.jpg`,
|
|
|
|
`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`,
|
|
|
|
);
|
|
|
|
|
2024-02-13 15:48:47 +02:00
|
|
|
await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447_775_200_001);
|
2023-10-06 23:32:28 +02:00
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, { refreshModifiedFiles: true });
|
|
|
|
|
|
|
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
|
|
expect(assets.length).toBe(1);
|
|
|
|
|
|
|
|
expect(assets[0]).toEqual(
|
|
|
|
expect.objectContaining({
|
|
|
|
originalFileName: 'el_torcal_rocks',
|
|
|
|
exifInfo: expect.objectContaining({
|
|
|
|
dateTimeOriginal: '2023-09-25T08:33:30.880Z',
|
|
|
|
exifImageHeight: 534,
|
|
|
|
exifImageWidth: 800,
|
|
|
|
exposureTime: '1/15',
|
|
|
|
fNumber: 22,
|
2024-02-13 15:48:47 +02:00
|
|
|
fileSizeInByte: 114_225,
|
2023-10-06 23:32:28 +02:00
|
|
|
focalLength: 35,
|
|
|
|
iso: 1000,
|
|
|
|
make: 'NIKON CORPORATION',
|
|
|
|
model: 'NIKON D750',
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
|
|
|
|
it('should not reimport unmodified files', async () => {
|
|
|
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
|
|
|
});
|
|
|
|
|
|
|
|
await fs.promises.cp(
|
|
|
|
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
|
|
|
`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`,
|
|
|
|
);
|
|
|
|
|
2024-02-13 15:48:47 +02:00
|
|
|
await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447_775_200_000);
|
2023-10-06 23:32:28 +02:00
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
|
|
|
|
|
|
await fs.promises.cp(
|
|
|
|
`${IMMICH_TEST_ASSET_PATH}/albums/nature/tanners_ridge.jpg`,
|
|
|
|
`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`,
|
|
|
|
);
|
|
|
|
|
2024-02-13 15:48:47 +02:00
|
|
|
await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447_775_200_000);
|
2023-10-06 23:32:28 +02:00
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, { refreshModifiedFiles: true });
|
|
|
|
|
|
|
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
|
|
expect(assets.length).toBe(1);
|
|
|
|
|
|
|
|
expect(assets[0]).toEqual(
|
|
|
|
expect.objectContaining({
|
|
|
|
originalFileName: 'el_torcal_rocks',
|
|
|
|
exifInfo: expect.objectContaining({
|
|
|
|
dateTimeOriginal: '2012-08-05T11:39:59.000Z',
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('with refreshAllFiles=true', () => {
|
|
|
|
it('should reimport all files', async () => {
|
|
|
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
|
|
|
});
|
|
|
|
|
|
|
|
await fs.promises.cp(
|
|
|
|
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
|
|
|
`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`,
|
|
|
|
);
|
|
|
|
|
2024-02-13 15:48:47 +02:00
|
|
|
await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447_775_200_000);
|
2023-10-06 23:32:28 +02:00
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
|
|
|
|
|
|
await fs.promises.cp(
|
|
|
|
`${IMMICH_TEST_ASSET_PATH}/albums/nature/tanners_ridge.jpg`,
|
|
|
|
`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`,
|
|
|
|
);
|
|
|
|
|
2024-02-13 15:48:47 +02:00
|
|
|
await utimes(`${IMMICH_TEST_ASSET_TEMP_PATH}/el_torcal_rocks.jpg`, 447_775_200_000);
|
2023-10-06 23:32:28 +02:00
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id, { refreshAllFiles: true });
|
|
|
|
|
|
|
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
|
|
expect(assets.length).toBe(1);
|
|
|
|
|
|
|
|
expect(assets[0]).toEqual(
|
|
|
|
expect.objectContaining({
|
|
|
|
originalFileName: 'el_torcal_rocks',
|
|
|
|
exifInfo: expect.objectContaining({
|
|
|
|
exifImageHeight: 534,
|
|
|
|
exifImageWidth: 800,
|
|
|
|
exposureTime: '1/15',
|
|
|
|
fNumber: 22,
|
2024-02-13 15:48:47 +02:00
|
|
|
fileSizeInByte: 114_225,
|
2023-10-06 23:32:28 +02:00
|
|
|
focalLength: 35,
|
|
|
|
iso: 1000,
|
|
|
|
make: 'NIKON CORPORATION',
|
|
|
|
model: 'NIKON D750',
|
|
|
|
}),
|
|
|
|
}),
|
|
|
|
);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
2023-09-20 13:16:33 +02:00
|
|
|
it('should not scan an upload library', async () => {
|
2023-10-06 23:32:28 +02:00
|
|
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
|
|
|
type: LibraryType.UPLOAD,
|
|
|
|
});
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
const { status, body } = await request(server)
|
2023-10-06 23:32:28 +02:00
|
|
|
.post(`/library/${library.id}/scan`)
|
|
|
|
.set('Authorization', `Bearer ${admin.accessToken}`);
|
2023-09-20 13:16:33 +02:00
|
|
|
|
|
|
|
expect(status).toBe(400);
|
|
|
|
expect(body).toEqual(errorStub.badRequest('Can only refresh external libraries'));
|
|
|
|
});
|
|
|
|
});
|
|
|
|
|
|
|
|
describe('POST /library/:id/removeOffline', () => {
|
|
|
|
it('should require authentication', async () => {
|
|
|
|
const { status, body } = await request(server).post(`/library/${uuidStub.notFound}/removeOffline`).send({});
|
2023-10-06 23:32:28 +02:00
|
|
|
|
2023-09-20 13:16:33 +02:00
|
|
|
expect(status).toBe(401);
|
|
|
|
expect(body).toEqual(errorStub.unauthorized);
|
|
|
|
});
|
2023-10-06 23:32:28 +02:00
|
|
|
|
|
|
|
it('should remvove offline files', async () => {
|
|
|
|
await fs.promises.cp(`${IMMICH_TEST_ASSET_PATH}/albums/nature`, `${IMMICH_TEST_ASSET_TEMP_PATH}/albums/nature`, {
|
|
|
|
recursive: true,
|
|
|
|
});
|
|
|
|
|
|
|
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
|
|
|
});
|
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
|
|
|
|
|
|
const onlineAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
|
|
expect(onlineAssets.length).toBeGreaterThan(1);
|
|
|
|
|
|
|
|
await restoreTempFolder();
|
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
|
|
|
|
|
|
const { status } = await request(server)
|
|
|
|
.post(`/library/${library.id}/removeOffline`)
|
|
|
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
|
|
.send();
|
|
|
|
expect(status).toBe(201);
|
|
|
|
|
|
|
|
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
|
|
|
|
|
|
expect(assets).toEqual([]);
|
|
|
|
});
|
|
|
|
|
2024-02-29 20:35:37 +02:00
|
|
|
it('should not remove online files', async () => {
|
2023-10-06 23:32:28 +02:00
|
|
|
const library = await api.libraryApi.create(server, admin.accessToken, {
|
|
|
|
type: LibraryType.EXTERNAL,
|
|
|
|
importPaths: [`${IMMICH_TEST_ASSET_PATH}/albums/nature`],
|
|
|
|
});
|
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
|
|
|
|
|
|
const assetsBefore = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
|
|
expect(assetsBefore.length).toBeGreaterThan(1);
|
|
|
|
|
|
|
|
await api.libraryApi.scanLibrary(server, admin.accessToken, library.id);
|
|
|
|
|
|
|
|
const { status } = await request(server)
|
|
|
|
.post(`/library/${library.id}/removeOffline`)
|
|
|
|
.set('Authorization', `Bearer ${admin.accessToken}`)
|
|
|
|
.send();
|
|
|
|
expect(status).toBe(201);
|
|
|
|
|
|
|
|
const assetsAfter = await api.assetApi.getAllAssets(server, admin.accessToken);
|
|
|
|
|
|
|
|
expect(assetsAfter).toEqual(assetsBefore);
|
|
|
|
});
|
2023-09-20 13:16:33 +02:00
|
|
|
});
|
|
|
|
});
|