mirror of
https://github.com/immich-app/immich.git
synced 2024-11-21 18:16:55 +02:00
chore(server): cleanup library watching (#8835)
chore: clean up library watching
This commit is contained in:
parent
1c1e461936
commit
dba365634a
13
.github/workflows/test.yml
vendored
13
.github/workflows/test.yml
vendored
@ -10,19 +10,6 @@ concurrency:
|
||||
cancel-in-progress: true
|
||||
|
||||
jobs:
|
||||
server-e2e-jobs:
|
||||
name: Server (e2e-jobs)
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout code
|
||||
uses: actions/checkout@v4
|
||||
with:
|
||||
submodules: 'recursive'
|
||||
|
||||
- name: Run e2e tests
|
||||
run: make server-e2e-jobs
|
||||
|
||||
doc-tests:
|
||||
name: Docs
|
||||
runs-on: ubuntu-latest
|
||||
|
3
Makefile
3
Makefile
@ -16,9 +16,6 @@ stage:
|
||||
pull-stage:
|
||||
docker compose -f ./docker/docker-compose.staging.yml pull
|
||||
|
||||
server-e2e-jobs:
|
||||
docker compose -f ./server/e2e/docker-compose.server-e2e.yml up --renew-anon-volumes --abort-on-container-exit --exit-code-from immich-server --remove-orphans --build
|
||||
|
||||
.PHONY: e2e
|
||||
e2e:
|
||||
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
||||
|
@ -8,15 +8,24 @@ Unit are run by calling `npm run test` from the `server` directory.
|
||||
|
||||
### End to end tests
|
||||
|
||||
The backend has two end-to-end test suites that can be called with the following two commands from the project root directory:
|
||||
The e2e tests can be run by first starting up a test production environment via:
|
||||
|
||||
- `make server-e2e-api`
|
||||
- `make server-e2e-jobs`
|
||||
```bash
|
||||
make e2e
|
||||
```
|
||||
|
||||
#### API (e2e)
|
||||
Once the test environment is running, the e2e tests can be run via:
|
||||
|
||||
The API e2e tests spin up a test database and execute http requests against the server, validating the expected response codes and functionality for API endpoints.
|
||||
```bash
|
||||
cd e2e/
|
||||
npm test
|
||||
```
|
||||
|
||||
#### Jobs (e2e)
|
||||
The tests check various things including:
|
||||
|
||||
The Jobs e2e tests spin up a docker test environment where thumbnail generation, library scanning, and other _job_ workflows are validated.
|
||||
- Authentication and authorization
|
||||
- Query param, body, and url validation
|
||||
- Response codes
|
||||
- Thumbnail generation
|
||||
- Metadata extraction
|
||||
- Library scanning
|
||||
|
@ -27,6 +27,7 @@ describe('/library', () => {
|
||||
beforeAll(async () => {
|
||||
await utils.resetDatabase();
|
||||
admin = await utils.adminSetup();
|
||||
await utils.resetAdminConfig(admin.accessToken);
|
||||
user = await utils.userSetup(admin.accessToken, userDto.user1);
|
||||
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External });
|
||||
websocket = await utils.connectWebsocket(admin.accessToken);
|
||||
@ -36,7 +37,7 @@ describe('/library', () => {
|
||||
|
||||
afterAll(() => {
|
||||
utils.disconnectWebsocket(websocket);
|
||||
utils.deleteTempFolder();
|
||||
utils.resetTempFolder();
|
||||
});
|
||||
|
||||
beforeEach(() => {
|
||||
@ -816,40 +817,4 @@ describe('/library', () => {
|
||||
expect(existsSync(`${testAssetDir}/temp/directoryB/assetB.png`)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
// describe('Watching', () => {
|
||||
// beforeAll(async () => {
|
||||
// const config = await getConfigDefaults({ headers: asBearerAuth(admin.accessToken) });
|
||||
// await updateConfig(
|
||||
// { systemConfigDto: { ...config, library: { ...config.library, watch: { enabled: true } } } },
|
||||
// { headers: asBearerAuth(admin.accessToken) },
|
||||
// );
|
||||
// });
|
||||
|
||||
// afterAll(async () => {
|
||||
// const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(admin.accessToken) });
|
||||
// await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(admin.accessToken) });
|
||||
// rmSync(`${testAssetDir}/temp/watch`, { recursive: true });
|
||||
// });
|
||||
|
||||
// describe('Single import path', () => {
|
||||
// let library: LibraryResponseDto;
|
||||
// beforeEach(async () => {
|
||||
// library = await utils.createLibrary(admin.accessToken, {
|
||||
// ownerId: admin.userId,
|
||||
// type: LibraryType.External,
|
||||
// importPaths: [`${testAssetDirInternal}/temp`],
|
||||
// });
|
||||
// });
|
||||
|
||||
// it('should import a new file', async () => {
|
||||
// utils.createImageFile(`${testAssetDir}/temp/watch/assetA.png`);
|
||||
|
||||
// await utils.waitForWebsocketEvent({ event: 'assetUpload', total: 1 });
|
||||
|
||||
// const { assets } = await utils.metadataSearch(admin.accessToken, { libraryId: library.id });
|
||||
// expect(assets.count).toEqual(3);
|
||||
// });
|
||||
// });
|
||||
// });
|
||||
});
|
||||
|
@ -21,10 +21,12 @@ import {
|
||||
getAllAssets,
|
||||
getAllJobsStatus,
|
||||
getAssetInfo,
|
||||
getConfigDefaults,
|
||||
login,
|
||||
searchMetadata,
|
||||
setAdminOnboarding,
|
||||
signUpAdmin,
|
||||
updateConfig,
|
||||
validate,
|
||||
} from '@immich/sdk';
|
||||
import { BrowserContext } from '@playwright/test';
|
||||
@ -139,6 +141,7 @@ export const utils = {
|
||||
'user_token',
|
||||
'users',
|
||||
'system_metadata',
|
||||
'system_config',
|
||||
];
|
||||
|
||||
const sql: string[] = [];
|
||||
@ -148,7 +151,12 @@ export const utils = {
|
||||
}
|
||||
|
||||
for (const table of tables) {
|
||||
sql.push(`DELETE FROM ${table} CASCADE;`);
|
||||
if (table === 'system_metadata') {
|
||||
// prevent reverse geocoder from being re-initialized
|
||||
sql.push(`DELETE FROM "system_metadata" where "key" != 'reverse-geocoding-state';`);
|
||||
} else {
|
||||
sql.push(`DELETE FROM ${table} CASCADE;`);
|
||||
}
|
||||
}
|
||||
|
||||
await client.query(sql.join('\n'));
|
||||
@ -310,9 +318,7 @@ export const utils = {
|
||||
if (!existsSync(dirname(path))) {
|
||||
mkdirSync(dirname(path), { recursive: true });
|
||||
}
|
||||
if (!existsSync(path)) {
|
||||
writeFileSync(path, makeRandomImage());
|
||||
}
|
||||
writeFileSync(path, makeRandomImage());
|
||||
},
|
||||
|
||||
removeImageFile: (path: string) => {
|
||||
@ -407,8 +413,14 @@ export const utils = {
|
||||
},
|
||||
]),
|
||||
|
||||
deleteTempFolder: () => {
|
||||
resetTempFolder: () => {
|
||||
rmSync(`${testAssetDir}/temp`, { recursive: true, force: true });
|
||||
mkdirSync(`${testAssetDir}/temp`, { recursive: true });
|
||||
},
|
||||
|
||||
resetAdminConfig: async (accessToken: string) => {
|
||||
const defaultConfig = await getConfigDefaults({ headers: asBearerAuth(accessToken) });
|
||||
await updateConfig({ systemConfigDto: defaultConfig }, { headers: asBearerAuth(accessToken) });
|
||||
},
|
||||
|
||||
isQueueEmpty: async (accessToken: string, queue: keyof AllJobStatusResponseDto) => {
|
||||
|
@ -1,10 +0,0 @@
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import request from 'supertest';
|
||||
|
||||
export const assetApi = {
|
||||
getAllAssets: async (server: any, accessToken: string) => {
|
||||
const { body, status } = await request(server).get(`/asset/`).set('Authorization', `Bearer ${accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
return body as AssetResponseDto[];
|
||||
},
|
||||
};
|
@ -1,23 +0,0 @@
|
||||
import { LoginResponseDto } from 'src/dtos/auth.dto';
|
||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||
import request from 'supertest';
|
||||
import { adminSignupStub, loginResponseStub, loginStub } from 'test/fixtures/auth.stub';
|
||||
|
||||
export const authApi = {
|
||||
adminSignUp: async (server: any) => {
|
||||
const { status, body } = await request(server).post('/auth/admin-sign-up').send(adminSignupStub);
|
||||
|
||||
expect(status).toBe(201);
|
||||
|
||||
return body as UserResponseDto;
|
||||
},
|
||||
adminLogin: async (server: any) => {
|
||||
const { status, body } = await request(server).post('/auth/login').send(loginStub.admin);
|
||||
|
||||
expect(body).toEqual(loginResponseStub.admin.response);
|
||||
expect(body).toMatchObject({ accessToken: expect.any(String) });
|
||||
expect(status).toBe(201);
|
||||
|
||||
return body as LoginResponseDto;
|
||||
},
|
||||
};
|
@ -1,9 +0,0 @@
|
||||
import { assetApi } from 'e2e/client/asset-api';
|
||||
import { authApi } from 'e2e/client/auth-api';
|
||||
import { libraryApi } from 'e2e/client/library-api';
|
||||
|
||||
export const api = {
|
||||
authApi,
|
||||
assetApi,
|
||||
libraryApi,
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
import { CreateLibraryDto, LibraryResponseDto, ScanLibraryDto } from 'src/dtos/library.dto';
|
||||
import request from 'supertest';
|
||||
|
||||
export const libraryApi = {
|
||||
getAll: async (server: any, accessToken: string) => {
|
||||
const { body, status } = await request(server).get(`/library/`).set('Authorization', `Bearer ${accessToken}`);
|
||||
expect(status).toBe(200);
|
||||
return body as LibraryResponseDto[];
|
||||
},
|
||||
create: async (server: any, accessToken: string, dto: CreateLibraryDto) => {
|
||||
const { body, status } = await request(server)
|
||||
.post(`/library/`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto);
|
||||
expect(status).toBe(201);
|
||||
return body as LibraryResponseDto;
|
||||
},
|
||||
setImportPaths: async (server: any, accessToken: string, id: string, importPaths: string[]) => {
|
||||
const { body, status } = await request(server)
|
||||
.put(`/library/${id}`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send({ importPaths });
|
||||
expect(status).toBe(200);
|
||||
return body as LibraryResponseDto;
|
||||
},
|
||||
scanLibrary: async (server: any, accessToken: string, id: string, dto: ScanLibraryDto = {}) => {
|
||||
const { status } = await request(server)
|
||||
.post(`/library/${id}/scan`)
|
||||
.set('Authorization', `Bearer ${accessToken}`)
|
||||
.send(dto);
|
||||
expect(status).toBe(204);
|
||||
},
|
||||
};
|
@ -1,33 +0,0 @@
|
||||
version: '3.8'
|
||||
|
||||
name: 'immich-test-e2e'
|
||||
|
||||
services:
|
||||
immich-server:
|
||||
image: immich-server-dev:latest
|
||||
build:
|
||||
context: ../../
|
||||
dockerfile: server/Dockerfile
|
||||
target: dev
|
||||
command: ['/usr/src/app/bin/immich-test', 'jobs']
|
||||
volumes:
|
||||
- /usr/src/app/node_modules
|
||||
- ../test/assets:/usr/src/app/test/assets:ro
|
||||
environment:
|
||||
- DB_HOSTNAME=database
|
||||
- DB_USERNAME=postgres
|
||||
- DB_PASSWORD=postgres
|
||||
- DB_DATABASE_NAME=e2e_test
|
||||
- IMMICH_METRICS=true
|
||||
depends_on:
|
||||
- database
|
||||
|
||||
database:
|
||||
image: tensorchord/pgvecto-rs:pg14-v0.2.0@sha256:90724186f0a3517cf6914295b5ab410db9ce23190a2d9d0b9dd6463e3fa298f0
|
||||
command: -c fsync=off -c shared_preload_libraries=vectors.so
|
||||
environment:
|
||||
POSTGRES_PASSWORD: postgres
|
||||
POSTGRES_USER: postgres
|
||||
POSTGRES_DB: e2e_test
|
||||
logging:
|
||||
driver: none
|
@ -1,17 +0,0 @@
|
||||
{
|
||||
"reverseGeocoding": {
|
||||
"enabled": false
|
||||
},
|
||||
"machineLearning": {
|
||||
"enabled": false
|
||||
},
|
||||
"logging": {
|
||||
"enabled": false,
|
||||
"level": "debug"
|
||||
},
|
||||
"library": {
|
||||
"watch": {
|
||||
"enabled": true
|
||||
}
|
||||
}
|
||||
}
|
@ -1,12 +0,0 @@
|
||||
{
|
||||
"reverseGeocoding": {
|
||||
"enabled": false
|
||||
},
|
||||
"machineLearning": {
|
||||
"enabled": false
|
||||
},
|
||||
"logging": {
|
||||
"enabled": false,
|
||||
"level": "debug"
|
||||
}
|
||||
}
|
@ -1,22 +0,0 @@
|
||||
{
|
||||
"moduleFileExtensions": ["js", "json", "ts"],
|
||||
"modulePaths": ["<rootDir>"],
|
||||
"rootDir": "../..",
|
||||
"globalSetup": "<rootDir>/e2e/jobs/setup.ts",
|
||||
"testEnvironment": "node",
|
||||
"testMatch": ["**/e2e/jobs/specs/*.e2e-spec.[tj]s"],
|
||||
"testTimeout": 10000,
|
||||
"transform": {
|
||||
"^.+\\.(t|j)s$": "ts-jest"
|
||||
},
|
||||
"collectCoverageFrom": [
|
||||
"<rootDir>/src/**/*.(t|j)s",
|
||||
"!<rootDir>/src/**/*.spec.(t|s)s",
|
||||
"!<rootDir>/src/migrations/**"
|
||||
],
|
||||
"coverageDirectory": "./coverage",
|
||||
"moduleNameMapper": {
|
||||
"^test(|/.*)$": "<rootDir>/test/$1",
|
||||
"^src(|/.*)$": "<rootDir>/src/$1"
|
||||
}
|
||||
}
|
@ -1,42 +0,0 @@
|
||||
import { PostgreSqlContainer } from '@testcontainers/postgresql';
|
||||
import { access } from 'fs/promises';
|
||||
import path from 'path';
|
||||
|
||||
export default async () => {
|
||||
let IMMICH_TEST_ASSET_PATH: string = '';
|
||||
|
||||
if (process.env.IMMICH_TEST_ASSET_PATH === undefined) {
|
||||
IMMICH_TEST_ASSET_PATH = path.normalize(`${__dirname}/../../test/assets/`);
|
||||
process.env.IMMICH_TEST_ASSET_PATH = IMMICH_TEST_ASSET_PATH;
|
||||
} else {
|
||||
IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH;
|
||||
}
|
||||
|
||||
const directoryExists = async (dirPath: string) =>
|
||||
await access(dirPath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
if (!(await directoryExists(`${IMMICH_TEST_ASSET_PATH}/albums`))) {
|
||||
throw new Error(
|
||||
`Test assets not found. Please checkout https://github.com/immich-app/test-assets into ${IMMICH_TEST_ASSET_PATH} before testing`,
|
||||
);
|
||||
}
|
||||
|
||||
if (process.env.DB_HOSTNAME === undefined) {
|
||||
// DB hostname not set which likely means we're not running e2e through docker compose. Start a local postgres container.
|
||||
const pg = await new PostgreSqlContainer('tensorchord/pgvecto-rs:pg14-v0.2.0')
|
||||
.withExposedPorts(5432)
|
||||
.withDatabase('immich')
|
||||
.withUsername('postgres')
|
||||
.withPassword('postgres')
|
||||
.withReuse()
|
||||
.start();
|
||||
|
||||
process.env.DB_URL = pg.getConnectionUri();
|
||||
}
|
||||
|
||||
process.env.NODE_ENV = 'development';
|
||||
process.env.IMMICH_CONFIG_FILE = path.normalize(`${__dirname}/immich-e2e-config.json`);
|
||||
process.env.TZ = 'Z';
|
||||
};
|
@ -1,231 +0,0 @@
|
||||
import { api } from 'e2e/client';
|
||||
import fs from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import { LoginResponseDto } from 'src/dtos/auth.dto';
|
||||
import { LibraryResponseDto } from 'src/dtos/library.dto';
|
||||
import { AssetType } from 'src/entities/asset.entity';
|
||||
import { LibraryType } from 'src/entities/library.entity';
|
||||
import { StorageEventType } from 'src/interfaces/storage.interface';
|
||||
import { LibraryService } from 'src/services/library.service';
|
||||
import {
|
||||
IMMICH_TEST_ASSET_PATH,
|
||||
IMMICH_TEST_ASSET_TEMP_PATH,
|
||||
restoreTempFolder,
|
||||
testApp,
|
||||
waitForEvent,
|
||||
} from 'test/utils';
|
||||
|
||||
describe(`Library watcher (e2e)`, () => {
|
||||
let server: any;
|
||||
let admin: LoginResponseDto;
|
||||
let libraryService: LibraryService;
|
||||
const configFilePath = process.env.IMMICH_CONFIG_FILE;
|
||||
|
||||
beforeAll(async () => {
|
||||
process.env.IMMICH_CONFIG_FILE = path.normalize(`${__dirname}/../config/library-watcher-e2e-config.json`);
|
||||
|
||||
const app = await testApp.create();
|
||||
server = app.getHttpServer();
|
||||
libraryService = testApp.get(LibraryService);
|
||||
});
|
||||
|
||||
beforeEach(async () => {
|
||||
await testApp.reset();
|
||||
await restoreTempFolder();
|
||||
await api.authApi.adminSignUp(server);
|
||||
admin = await api.authApi.adminLogin(server);
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
await libraryService.teardown();
|
||||
});
|
||||
|
||||
afterAll(async () => {
|
||||
await testApp.teardown();
|
||||
await restoreTempFolder();
|
||||
process.env.IMMICH_CONFIG_FILE = configFilePath;
|
||||
});
|
||||
|
||||
describe('Event handling', () => {
|
||||
describe('Single import path', () => {
|
||||
beforeEach(async () => {
|
||||
await api.libraryApi.create(server, admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [`${IMMICH_TEST_ASSET_TEMP_PATH}`],
|
||||
});
|
||||
});
|
||||
|
||||
it('should import a new file', async () => {
|
||||
await fs.copyFile(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`,
|
||||
);
|
||||
|
||||
await waitForEvent(libraryService, StorageEventType.ADD);
|
||||
|
||||
const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
expect(afterAssets.length).toEqual(1);
|
||||
});
|
||||
|
||||
it('should import new files with case insensitive extensions', async () => {
|
||||
await fs.copyFile(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/file2.JPG`,
|
||||
);
|
||||
|
||||
await fs.copyFile(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/file3.Jpg`,
|
||||
);
|
||||
|
||||
await fs.copyFile(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/file4.jpG`,
|
||||
);
|
||||
|
||||
await fs.copyFile(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/file5.jPg`,
|
||||
);
|
||||
|
||||
await waitForEvent(libraryService, StorageEventType.ADD, 4);
|
||||
|
||||
const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
expect(afterAssets.length).toEqual(4);
|
||||
});
|
||||
|
||||
it('should update a changed file', async () => {
|
||||
await fs.copyFile(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`,
|
||||
);
|
||||
|
||||
await waitForEvent(libraryService, StorageEventType.ADD);
|
||||
|
||||
const originalAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
expect(originalAssets.length).toEqual(1);
|
||||
|
||||
await fs.copyFile(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/prairie_falcon.jpg`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/file.jpg`,
|
||||
);
|
||||
|
||||
await waitForEvent(libraryService, StorageEventType.CHANGE);
|
||||
|
||||
const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
expect(afterAssets).toEqual([
|
||||
expect.objectContaining({
|
||||
// Make sure we keep the original asset id
|
||||
id: originalAssets[0].id,
|
||||
type: AssetType.IMAGE,
|
||||
exifInfo: expect.objectContaining({
|
||||
make: 'Canon',
|
||||
model: 'Canon EOS R5',
|
||||
exifImageWidth: 800,
|
||||
exifImageHeight: 533,
|
||||
exposureTime: '1/4000',
|
||||
}),
|
||||
}),
|
||||
]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('Multiple import paths', () => {
|
||||
beforeEach(async () => {
|
||||
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`, { recursive: true });
|
||||
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`, { recursive: true });
|
||||
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true });
|
||||
|
||||
await api.libraryApi.create(server, admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`,
|
||||
],
|
||||
});
|
||||
});
|
||||
|
||||
it('should add new files in multiple import paths', async () => {
|
||||
await fs.copyFile(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/el_torcal_rocks.jpg`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file2.jpg`,
|
||||
);
|
||||
|
||||
await fs.copyFile(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/polemonium_reptans.jpg`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir2/file3.jpg`,
|
||||
);
|
||||
|
||||
await fs.copyFile(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/tanners_ridge.jpg`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3/file4.jpg`,
|
||||
);
|
||||
|
||||
await waitForEvent(libraryService, StorageEventType.ADD, 3);
|
||||
|
||||
const assets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
expect(assets.length).toEqual(3);
|
||||
});
|
||||
|
||||
it('should offline a removed file', async () => {
|
||||
await fs.copyFile(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/polemonium_reptans.jpg`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file.jpg`,
|
||||
);
|
||||
|
||||
await waitForEvent(libraryService, StorageEventType.ADD);
|
||||
|
||||
const addedAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
expect(addedAssets.length).toEqual(1);
|
||||
|
||||
await fs.unlink(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1/file.jpg`);
|
||||
|
||||
await waitForEvent(libraryService, StorageEventType.UNLINK);
|
||||
|
||||
const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
expect(afterAssets[0].isOffline).toEqual(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Configuration', () => {
|
||||
let library: LibraryResponseDto;
|
||||
|
||||
beforeEach(async () => {
|
||||
library = await api.libraryApi.create(server, admin.accessToken, {
|
||||
ownerId: admin.userId,
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: [
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`,
|
||||
],
|
||||
});
|
||||
|
||||
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir1`, { recursive: true });
|
||||
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir2`, { recursive: true });
|
||||
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir3`, { recursive: true });
|
||||
});
|
||||
|
||||
it('should use an updated import path', async () => {
|
||||
await fs.mkdir(`${IMMICH_TEST_ASSET_TEMP_PATH}/dir4`, { recursive: true });
|
||||
|
||||
await api.libraryApi.setImportPaths(server, admin.accessToken, library.id, [
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir4`,
|
||||
]);
|
||||
|
||||
await fs.copyFile(
|
||||
`${IMMICH_TEST_ASSET_PATH}/albums/nature/polemonium_reptans.jpg`,
|
||||
`${IMMICH_TEST_ASSET_TEMP_PATH}/dir4/file.jpg`,
|
||||
);
|
||||
|
||||
await waitForEvent(libraryService, StorageEventType.ADD);
|
||||
|
||||
const afterAssets = await api.assetApi.getAllAssets(server, admin.accessToken);
|
||||
expect(afterAssets.length).toEqual(1);
|
||||
});
|
||||
});
|
||||
});
|
322
server/package-lock.json
generated
322
server/package-lock.json
generated
@ -81,7 +81,6 @@
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.5.7",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
@ -98,8 +97,6 @@
|
||||
"rimraf": "^5.0.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"sql-formatter": "^15.0.0",
|
||||
"supertest": "^6.3.3",
|
||||
"testcontainers": "^10.2.1",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.4.4",
|
||||
"ts-node": "^10.9.1",
|
||||
@ -4390,12 +4387,6 @@
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/cookiejar": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
|
||||
"integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/cookies": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz",
|
||||
@ -4685,12 +4676,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||
"integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/mime": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
|
||||
@ -4890,27 +4875,6 @@
|
||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/superagent": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.1.tgz",
|
||||
"integrity": "sha512-YQyEXA4PgCl7EVOoSAS3o0fyPFU6erv5mMixztQYe1bqbWmmn8c+IrqoxjQeZe4MgwXikgcaZPiI/DsbmOVlzA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/cookiejar": "^2.1.5",
|
||||
"@types/methods": "^1.1.4",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/supertest": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz",
|
||||
"integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/methods": "^1.1.4",
|
||||
"@types/superagent": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/tedious": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz",
|
||||
@ -5709,12 +5673,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
@ -5734,12 +5692,6 @@
|
||||
"resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz",
|
||||
"integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="
|
||||
},
|
||||
"node_modules/asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/b4a": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
|
||||
@ -6587,18 +6539,6 @@
|
||||
"color-support": "bin.js"
|
||||
}
|
||||
},
|
||||
"node_modules/combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 0.8"
|
||||
}
|
||||
},
|
||||
"node_modules/commander": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
@ -6624,12 +6564,6 @@
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/component-emitter": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/compress-commons": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
|
||||
@ -6762,12 +6696,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||
},
|
||||
"node_modules/cookiejar": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
|
||||
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/core-js-compat": {
|
||||
"version": "3.35.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
|
||||
@ -7037,15 +6965,6 @@
|
||||
"url": "https://github.com/sponsors/ljharb"
|
||||
}
|
||||
},
|
||||
"node_modules/delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/delegates": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
@ -7093,16 +7012,6 @@
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/dezalgo": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asap": "^2.0.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"node_modules/diacritics": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
|
||||
@ -8172,35 +8081,6 @@
|
||||
"webpack": "^5.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">= 6"
|
||||
}
|
||||
},
|
||||
"node_modules/formidable": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz",
|
||||
"integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"dezalgo": "^1.0.4",
|
||||
"hexoid": "^1.0.0",
|
||||
"once": "^1.4.0",
|
||||
"qs": "^6.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://ko-fi.com/tunnckoCore/commissions"
|
||||
}
|
||||
},
|
||||
"node_modules/forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@ -8711,15 +8591,6 @@
|
||||
"he": "bin/he"
|
||||
}
|
||||
},
|
||||
"node_modules/hexoid": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
|
||||
"integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/highlight.js": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
@ -12922,52 +12793,6 @@
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/superagent": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz",
|
||||
"integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"component-emitter": "^1.3.0",
|
||||
"cookiejar": "^2.1.4",
|
||||
"debug": "^4.3.4",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"form-data": "^4.0.0",
|
||||
"formidable": "^2.1.2",
|
||||
"methods": "^1.1.2",
|
||||
"mime": "2.6.0",
|
||||
"qs": "^6.11.0",
|
||||
"semver": "^7.3.8"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.4.0 <13 || >=14"
|
||||
}
|
||||
},
|
||||
"node_modules/superagent/node_modules/mime": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
||||
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"mime": "cli.js"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supertest": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz",
|
||||
"integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"methods": "^1.1.2",
|
||||
"superagent": "^8.1.2"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6.4.0"
|
||||
}
|
||||
},
|
||||
"node_modules/supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
@ -17435,12 +17260,6 @@
|
||||
"@types/express": "*"
|
||||
}
|
||||
},
|
||||
"@types/cookiejar": {
|
||||
"version": "2.1.5",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookiejar/-/cookiejar-2.1.5.tgz",
|
||||
"integrity": "sha512-he+DHOWReW0nghN24E1WUqM0efK4kI9oTqDm6XmK8ZPe2djZ90BSNdGnIyCLzCPw7/pogPlGbzI2wHGGmi4O/Q==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/cookies": {
|
||||
"version": "0.9.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz",
|
||||
@ -17729,12 +17548,6 @@
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/methods": {
|
||||
"version": "1.1.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/methods/-/methods-1.1.4.tgz",
|
||||
"integrity": "sha512-ymXWVrDiCxTBE3+RIrrP533E70eA+9qu7zdWoHuOmGujkYtzf4HQF96b8nwHLqhuf4ykX61IGRIB38CC6/sImQ==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/mime": {
|
||||
"version": "1.3.3",
|
||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
|
||||
@ -17921,27 +17734,6 @@
|
||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/superagent": {
|
||||
"version": "8.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/superagent/-/superagent-8.1.1.tgz",
|
||||
"integrity": "sha512-YQyEXA4PgCl7EVOoSAS3o0fyPFU6erv5mMixztQYe1bqbWmmn8c+IrqoxjQeZe4MgwXikgcaZPiI/DsbmOVlzA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/cookiejar": "^2.1.5",
|
||||
"@types/methods": "^1.1.4",
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"@types/supertest": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/@types/supertest/-/supertest-6.0.2.tgz",
|
||||
"integrity": "sha512-137ypx2lk/wTQbW6An6safu9hXmajAifU/s7szAHLN/FeIm5w7yR0Wkl9fdJMRSHwOn4HLAI0DaB2TOORuhPDg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/methods": "^1.1.4",
|
||||
"@types/superagent": "^8.1.0"
|
||||
}
|
||||
},
|
||||
"@types/tedious": {
|
||||
"version": "4.0.14",
|
||||
"resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz",
|
||||
@ -18536,12 +18328,6 @@
|
||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||
"dev": true
|
||||
},
|
||||
"asap": {
|
||||
"version": "2.0.6",
|
||||
"resolved": "https://registry.npmjs.org/asap/-/asap-2.0.6.tgz",
|
||||
"integrity": "sha512-BSHWgDSAiKs50o2Re8ppvp3seVHXSRM44cdSsT9FfNEUUZLOGWVCsiWaRPWM1Znn+mqZ1OfVZ3z3DWEzSp7hRA==",
|
||||
"dev": true
|
||||
},
|
||||
"asn1": {
|
||||
"version": "0.2.6",
|
||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
||||
@ -18561,12 +18347,6 @@
|
||||
"resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz",
|
||||
"integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="
|
||||
},
|
||||
"asynckit": {
|
||||
"version": "0.4.0",
|
||||
"resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz",
|
||||
"integrity": "sha512-Oei9OH4tRh0YqU3GxhX79dM/mwVgvbZJaSNaRk+bshkj0S5cfHcgYakreBjrHwatXKbz+IoIdYLxrKim2MjW0Q==",
|
||||
"dev": true
|
||||
},
|
||||
"b4a": {
|
||||
"version": "1.6.4",
|
||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
|
||||
@ -19182,15 +18962,6 @@
|
||||
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="
|
||||
},
|
||||
"combined-stream": {
|
||||
"version": "1.0.8",
|
||||
"resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.8.tgz",
|
||||
"integrity": "sha512-FQN4MRfuJeHf7cBbBMJFXhKSDq+2kAArBlmRBvcvFE5BB1HZKXtSFASDhdlz9zOYwxh8lDdnvmMOe/+5cdoEdg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"delayed-stream": "~1.0.0"
|
||||
}
|
||||
},
|
||||
"commander": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||
@ -19210,12 +18981,6 @@
|
||||
"repeat-string": "^1.6.1"
|
||||
}
|
||||
},
|
||||
"component-emitter": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/component-emitter/-/component-emitter-1.3.0.tgz",
|
||||
"integrity": "sha512-Rd3se6QB+sO1TwqZjscQrurpEPIfO0/yYnSin6Q/rD3mOutHvUrCAhJub3r90uNb+SESBuE0QYoB90YdfatsRg==",
|
||||
"dev": true
|
||||
},
|
||||
"compress-commons": {
|
||||
"version": "6.0.2",
|
||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
|
||||
@ -19315,12 +19080,6 @@
|
||||
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
||||
},
|
||||
"cookiejar": {
|
||||
"version": "2.1.4",
|
||||
"resolved": "https://registry.npmjs.org/cookiejar/-/cookiejar-2.1.4.tgz",
|
||||
"integrity": "sha512-LDx6oHrK+PhzLKJU9j5S7/Y3jM/mUHvD/DeI1WQmJn652iPC5Y4TBzC9l+5OMOXlyTTA+SmVUPm0HQUwpD5Jqw==",
|
||||
"dev": true
|
||||
},
|
||||
"core-js-compat": {
|
||||
"version": "3.35.1",
|
||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
|
||||
@ -19502,12 +19261,6 @@
|
||||
"has-property-descriptors": "^1.0.1"
|
||||
}
|
||||
},
|
||||
"delayed-stream": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz",
|
||||
"integrity": "sha512-ZySD7Nf91aLB0RxL4KGrKHBXl7Eds1DAmEdcoVawXnLD7SDhpNgtuII2aAkg7a7QS41jxPSZ17p4VdGnMHk3MQ==",
|
||||
"dev": true
|
||||
},
|
||||
"delegates": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||
@ -19539,16 +19292,6 @@
|
||||
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
|
||||
"dev": true
|
||||
},
|
||||
"dezalgo": {
|
||||
"version": "1.0.4",
|
||||
"resolved": "https://registry.npmjs.org/dezalgo/-/dezalgo-1.0.4.tgz",
|
||||
"integrity": "sha512-rXSP0bf+5n0Qonsb+SVVfNfIsimO4HEtmnIpPHY8Q1UCzKlQrDMfdobr8nJOOsRgWCyMRqeSBQzmWUMq7zvVig==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asap": "^2.0.0",
|
||||
"wrappy": "1"
|
||||
}
|
||||
},
|
||||
"diacritics": {
|
||||
"version": "1.3.0",
|
||||
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
|
||||
@ -20373,29 +20116,6 @@
|
||||
"tapable": "^2.2.1"
|
||||
}
|
||||
},
|
||||
"form-data": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/form-data/-/form-data-4.0.0.tgz",
|
||||
"integrity": "sha512-ETEklSGi5t0QMZuiXoA/Q6vcnxcLQP5vdugSpuAyi6SVGi2clPPp+xgEhuMaHC+zGgn31Kd235W35f7Hykkaww==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"asynckit": "^0.4.0",
|
||||
"combined-stream": "^1.0.8",
|
||||
"mime-types": "^2.1.12"
|
||||
}
|
||||
},
|
||||
"formidable": {
|
||||
"version": "2.1.2",
|
||||
"resolved": "https://registry.npmjs.org/formidable/-/formidable-2.1.2.tgz",
|
||||
"integrity": "sha512-CM3GuJ57US06mlpQ47YcunuUZ9jpm8Vx+P2CGt2j7HpgkKZO/DJYQ0Bobim8G6PFQmK5lOqOOdUXboU+h73A4g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"dezalgo": "^1.0.4",
|
||||
"hexoid": "^1.0.0",
|
||||
"once": "^1.4.0",
|
||||
"qs": "^6.11.0"
|
||||
}
|
||||
},
|
||||
"forwarded": {
|
||||
"version": "0.2.0",
|
||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||
@ -20758,12 +20478,6 @@
|
||||
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
|
||||
},
|
||||
"hexoid": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/hexoid/-/hexoid-1.0.0.tgz",
|
||||
"integrity": "sha512-QFLV0taWQOZtvIRIAdBChesmogZrtuXvVWsFHZTk2SU+anspqZ2vMnoLg7IE1+Uk16N19APic1BuF8bC8c2m5g==",
|
||||
"dev": true
|
||||
},
|
||||
"highlight.js": {
|
||||
"version": "10.7.3",
|
||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
@ -23956,42 +23670,6 @@
|
||||
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
|
||||
"dev": true
|
||||
},
|
||||
"superagent": {
|
||||
"version": "8.1.2",
|
||||
"resolved": "https://registry.npmjs.org/superagent/-/superagent-8.1.2.tgz",
|
||||
"integrity": "sha512-6WTxW1EB6yCxV5VFOIPQruWGHqc3yI7hEmZK6h+pyk69Lk/Ut7rLUY6W/ONF2MjBuGjvmMiIpsrVJ2vjrHlslA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"component-emitter": "^1.3.0",
|
||||
"cookiejar": "^2.1.4",
|
||||
"debug": "^4.3.4",
|
||||
"fast-safe-stringify": "^2.1.1",
|
||||
"form-data": "^4.0.0",
|
||||
"formidable": "^2.1.2",
|
||||
"methods": "^1.1.2",
|
||||
"mime": "2.6.0",
|
||||
"qs": "^6.11.0",
|
||||
"semver": "^7.3.8"
|
||||
},
|
||||
"dependencies": {
|
||||
"mime": {
|
||||
"version": "2.6.0",
|
||||
"resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz",
|
||||
"integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"supertest": {
|
||||
"version": "6.3.4",
|
||||
"resolved": "https://registry.npmjs.org/supertest/-/supertest-6.3.4.tgz",
|
||||
"integrity": "sha512-erY3HFDG0dPnhw4U+udPfrzXa4xhSG+n4rxfRuZWCUvjFWwKl+OxWf/7zk50s84/fAAs7vf5QAb9uRa0cCykxw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"methods": "^1.1.2",
|
||||
"superagent": "^8.1.2"
|
||||
}
|
||||
},
|
||||
"supports-color": {
|
||||
"version": "7.2.0",
|
||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||
|
@ -105,7 +105,6 @@
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.5.7",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
"@typescript-eslint/parser": "^7.0.0",
|
||||
@ -122,8 +121,6 @@
|
||||
"rimraf": "^5.0.1",
|
||||
"source-map-support": "^0.5.21",
|
||||
"sql-formatter": "^15.0.0",
|
||||
"supertest": "^6.3.3",
|
||||
"testcontainers": "^10.2.1",
|
||||
"ts-jest": "^29.1.1",
|
||||
"ts-loader": "^9.4.4",
|
||||
"ts-node": "^10.9.1",
|
||||
|
@ -31,14 +31,6 @@ export interface WatchEvents {
|
||||
onError(error: Error): void;
|
||||
}
|
||||
|
||||
export enum StorageEventType {
|
||||
READY = 'ready',
|
||||
ADD = 'add',
|
||||
CHANGE = 'change',
|
||||
UNLINK = 'unlink',
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
export interface IStorageRepository {
|
||||
createZipStream(): ImmichZipStream;
|
||||
createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream>;
|
||||
|
@ -10,7 +10,6 @@ import {
|
||||
IStorageRepository,
|
||||
ImmichReadStream,
|
||||
ImmichZipStream,
|
||||
StorageEventType,
|
||||
WatchEvents,
|
||||
} from 'src/interfaces/storage.interface';
|
||||
import { Instrumentation } from 'src/utils/instrumentation';
|
||||
@ -173,11 +172,11 @@ export class StorageRepository implements IStorageRepository {
|
||||
watch(paths: string[], options: WatchOptions, events: Partial<WatchEvents>) {
|
||||
const watcher = chokidar.watch(paths, options);
|
||||
|
||||
watcher.on(StorageEventType.READY, () => events.onReady?.());
|
||||
watcher.on(StorageEventType.ADD, (path) => events.onAdd?.(path));
|
||||
watcher.on(StorageEventType.CHANGE, (path) => events.onChange?.(path));
|
||||
watcher.on(StorageEventType.UNLINK, (path) => events.onUnlink?.(path));
|
||||
watcher.on(StorageEventType.ERROR, (error) => events.onError?.(error));
|
||||
watcher.on('ready', () => events.onReady?.());
|
||||
watcher.on('add', (path) => events.onAdd?.(path));
|
||||
watcher.on('change', (path) => events.onChange?.(path));
|
||||
watcher.on('unlink', (path) => events.onUnlink?.(path));
|
||||
watcher.on('error', (error) => events.onError?.(error));
|
||||
|
||||
return () => watcher.close();
|
||||
}
|
||||
|
@ -13,7 +13,7 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||
import { IJobRepository, ILibraryFileJob, ILibraryRefreshJob, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { LibraryService } from 'src/services/library.service';
|
||||
import { assetStub } from 'test/fixtures/asset.stub';
|
||||
@ -933,12 +933,6 @@ describe(LibraryService.name, () => {
|
||||
type: LibraryType.EXTERNAL,
|
||||
importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths,
|
||||
});
|
||||
|
||||
expect(storageMock.watch).toHaveBeenCalledWith(
|
||||
libraryStub.externalLibraryWithImportPaths1.importPaths,
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should create with exclusion patterns', async () => {
|
||||
@ -1087,45 +1081,6 @@ describe(LibraryService.name, () => {
|
||||
await expect(sut.update('library-id', {})).resolves.toEqual(mapLibrary(libraryStub.uploadLibrary1));
|
||||
expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
|
||||
});
|
||||
|
||||
it('should re-watch library when updating import paths', async () => {
|
||||
libraryMock.update.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
|
||||
storageMock.stat.mockResolvedValue({
|
||||
isDirectory: () => true,
|
||||
} as Stats);
|
||||
|
||||
storageMock.checkFileExists.mockResolvedValue(true);
|
||||
|
||||
await expect(sut.update('library-id', { importPaths: ['/data/user1/foo'] })).resolves.toEqual(
|
||||
mapLibrary(libraryStub.externalLibraryWithImportPaths1),
|
||||
);
|
||||
|
||||
expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
|
||||
expect(storageMock.watch).toHaveBeenCalledWith(
|
||||
libraryStub.externalLibraryWithImportPaths1.importPaths,
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
|
||||
it('should re-watch library when updating exclusion patterns', async () => {
|
||||
libraryMock.update.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
configMock.load.mockResolvedValue(systemConfigStub.libraryWatchEnabled);
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
|
||||
await expect(sut.update('library-id', { exclusionPatterns: ['bar'] })).resolves.toEqual(
|
||||
mapLibrary(libraryStub.externalLibraryWithImportPaths1),
|
||||
);
|
||||
|
||||
expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
|
||||
expect(storageMock.watch).toHaveBeenCalledWith(
|
||||
expect.arrayContaining([expect.any(String)]),
|
||||
expect.anything(),
|
||||
expect.anything(),
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('watchAll', () => {
|
||||
@ -1198,9 +1153,7 @@ describe(LibraryService.name, () => {
|
||||
it('should handle a new file event', async () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||
storageMock.watch.mockImplementation(
|
||||
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/foo/photo.jpg' }] }),
|
||||
);
|
||||
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] }));
|
||||
|
||||
await sut.watchAll();
|
||||
|
||||
@ -1221,7 +1174,7 @@ describe(LibraryService.name, () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||
storageMock.watch.mockImplementation(
|
||||
makeMockWatcher({ items: [{ event: StorageEventType.CHANGE, value: '/foo/photo.jpg' }] }),
|
||||
makeMockWatcher({ items: [{ event: 'change', value: '/foo/photo.jpg' }] }),
|
||||
);
|
||||
|
||||
await sut.watchAll();
|
||||
@ -1244,7 +1197,7 @@ describe(LibraryService.name, () => {
|
||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external);
|
||||
storageMock.watch.mockImplementation(
|
||||
makeMockWatcher({ items: [{ event: StorageEventType.UNLINK, value: '/foo/photo.jpg' }] }),
|
||||
makeMockWatcher({ items: [{ event: 'unlink', value: '/foo/photo.jpg' }] }),
|
||||
);
|
||||
|
||||
await sut.watchAll();
|
||||
@ -1258,19 +1211,17 @@ describe(LibraryService.name, () => {
|
||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||
storageMock.watch.mockImplementation(
|
||||
makeMockWatcher({
|
||||
items: [{ event: StorageEventType.ERROR, value: 'Error!' }],
|
||||
items: [{ event: 'error', value: 'Error!' }],
|
||||
}),
|
||||
);
|
||||
|
||||
await expect(sut.watchAll()).rejects.toThrow('Error!');
|
||||
await expect(sut.watchAll()).resolves.toBeUndefined();
|
||||
});
|
||||
|
||||
it('should ignore unknown extensions', async () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||
storageMock.watch.mockImplementation(
|
||||
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/foo/photo.jpg' }] }),
|
||||
);
|
||||
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] }));
|
||||
|
||||
await sut.watchAll();
|
||||
|
||||
@ -1280,9 +1231,7 @@ describe(LibraryService.name, () => {
|
||||
it('should ignore excluded paths', async () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.patternPath);
|
||||
libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]);
|
||||
storageMock.watch.mockImplementation(
|
||||
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/dir1/photo.txt' }] }),
|
||||
);
|
||||
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/dir1/photo.txt' }] }));
|
||||
|
||||
await sut.watchAll();
|
||||
|
||||
@ -1292,9 +1241,7 @@ describe(LibraryService.name, () => {
|
||||
it('should ignore excluded paths without case sensitivity', async () => {
|
||||
libraryMock.get.mockResolvedValue(libraryStub.patternPath);
|
||||
libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]);
|
||||
storageMock.watch.mockImplementation(
|
||||
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/DIR1/photo.txt' }] }),
|
||||
);
|
||||
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/DIR1/photo.txt' }] }));
|
||||
|
||||
await sut.watchAll();
|
||||
|
||||
|
@ -1,7 +1,6 @@
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { Trie } from 'mnemonist';
|
||||
import { R_OK } from 'node:constants';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { Stats } from 'node:fs';
|
||||
import path, { basename, parse } from 'node:path';
|
||||
import picomatch from 'picomatch';
|
||||
@ -37,7 +36,7 @@ import {
|
||||
JobStatus,
|
||||
} from 'src/interfaces/job.interface';
|
||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { IStorageRepository, StorageEventType } from 'src/interfaces/storage.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ImmichLogger } from 'src/utils/logger';
|
||||
import { mimeTypes } from 'src/utils/mime-types';
|
||||
@ -48,7 +47,7 @@ import { validateCronExpression } from 'src/validation';
|
||||
const LIBRARY_SCAN_BATCH_SIZE = 5000;
|
||||
|
||||
@Injectable()
|
||||
export class LibraryService extends EventEmitter {
|
||||
export class LibraryService {
|
||||
readonly logger = new ImmichLogger(LibraryService.name);
|
||||
private configCore: SystemConfigCore;
|
||||
private watchLibraries = false;
|
||||
@ -64,7 +63,6 @@ export class LibraryService extends EventEmitter {
|
||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||
) {
|
||||
super();
|
||||
this.configCore = SystemConfigCore.create(configRepository);
|
||||
}
|
||||
|
||||
@ -152,7 +150,6 @@ export class LibraryService extends EventEmitter {
|
||||
if (matcher(path)) {
|
||||
await this.scanAssets(library.id, [path], library.ownerId, false);
|
||||
}
|
||||
this.emit(StorageEventType.ADD, path);
|
||||
};
|
||||
return handlePromiseError(handler(), this.logger);
|
||||
},
|
||||
@ -163,7 +160,6 @@ export class LibraryService extends EventEmitter {
|
||||
// Note: if the changed file was not previously imported, it will be imported now.
|
||||
await this.scanAssets(library.id, [path], library.ownerId, false);
|
||||
}
|
||||
this.emit(StorageEventType.CHANGE, path);
|
||||
};
|
||||
return handlePromiseError(handler(), this.logger);
|
||||
},
|
||||
@ -174,13 +170,11 @@ export class LibraryService extends EventEmitter {
|
||||
if (asset && matcher(path)) {
|
||||
await this.assetRepository.update({ id: asset.id, isOffline: true });
|
||||
}
|
||||
this.emit(StorageEventType.UNLINK, path);
|
||||
};
|
||||
return handlePromiseError(handler(), this.logger);
|
||||
},
|
||||
onError: (error) => {
|
||||
this.logger.error(`Library watcher for library ${library.id} encountered error: ${error}`);
|
||||
this.emit(StorageEventType.ERROR, error);
|
||||
},
|
||||
},
|
||||
);
|
||||
@ -281,10 +275,6 @@ export class LibraryService extends EventEmitter {
|
||||
|
||||
this.logger.log(`Creating ${dto.type} library for ${dto.ownerId}}`);
|
||||
|
||||
if (dto.type === LibraryType.EXTERNAL) {
|
||||
await this.watch(library.id);
|
||||
}
|
||||
|
||||
return mapLibrary(library);
|
||||
}
|
||||
|
||||
@ -368,11 +358,6 @@ export class LibraryService extends EventEmitter {
|
||||
}
|
||||
}
|
||||
|
||||
if (dto.importPaths || dto.exclusionPatterns) {
|
||||
// Re-watch library to use new paths and/or exclusion patterns
|
||||
await this.watch(id);
|
||||
}
|
||||
|
||||
return mapLibrary(library);
|
||||
}
|
||||
|
||||
|
@ -1,6 +1,6 @@
|
||||
import { WatchOptions } from 'chokidar';
|
||||
import { StorageCore } from 'src/cores/storage.core';
|
||||
import { IStorageRepository, StorageEventType, WatchEvents } from 'src/interfaces/storage.interface';
|
||||
import { IStorageRepository, WatchEvents } from 'src/interfaces/storage.interface';
|
||||
|
||||
interface MockWatcherOptions {
|
||||
items?: Array<{ event: 'change' | 'add' | 'unlink' | 'error'; value: string }>;
|
||||
@ -13,19 +13,19 @@ export const makeMockWatcher =
|
||||
events.onReady?.();
|
||||
for (const item of items || []) {
|
||||
switch (item.event) {
|
||||
case StorageEventType.ADD: {
|
||||
case 'add': {
|
||||
events.onAdd?.(item.value);
|
||||
break;
|
||||
}
|
||||
case StorageEventType.CHANGE: {
|
||||
case 'change': {
|
||||
events.onChange?.(item.value);
|
||||
break;
|
||||
}
|
||||
case StorageEventType.UNLINK: {
|
||||
case 'unlink': {
|
||||
events.onUnlink?.(item.value);
|
||||
break;
|
||||
}
|
||||
case StorageEventType.ERROR: {
|
||||
case 'error': {
|
||||
events.onError?.(new Error(item.value));
|
||||
}
|
||||
}
|
||||
|
@ -1,168 +0,0 @@
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { DateTime } from 'luxon';
|
||||
import fs from 'node:fs';
|
||||
import { tmpdir } from 'node:os';
|
||||
import { join } from 'node:path';
|
||||
import { EventEmitter } from 'node:stream';
|
||||
import { AppTestModule } from 'src/app.module';
|
||||
import { dataSource } from 'src/database.config';
|
||||
import { IJobRepository, JobItem, JobItemHandler, QueueName } from 'src/interfaces/job.interface';
|
||||
import { IMediaRepository } from 'src/interfaces/media.interface';
|
||||
import { StorageEventType } from 'src/interfaces/storage.interface';
|
||||
import { MediaRepository } from 'src/repositories/media.repository';
|
||||
import { ApiService } from 'src/services/api.service';
|
||||
import { MicroservicesService } from 'src/services/microservices.service';
|
||||
import { EntityTarget, ObjectLiteral } from 'typeorm';
|
||||
|
||||
export const IMMICH_TEST_ASSET_PATH = process.env.IMMICH_TEST_ASSET_PATH as string;
|
||||
export const IMMICH_TEST_ASSET_TEMP_PATH = join(tmpdir(), 'immich');
|
||||
|
||||
export const today = DateTime.fromObject({ year: 2023, month: 11, day: 3 });
|
||||
export const yesterday = today.minus({ days: 1 });
|
||||
|
||||
export interface ResetOptions {
|
||||
entities?: EntityTarget<ObjectLiteral>[];
|
||||
}
|
||||
export const db = {
|
||||
reset: async (options?: ResetOptions) => {
|
||||
if (!dataSource.isInitialized) {
|
||||
await dataSource.initialize();
|
||||
}
|
||||
await dataSource.transaction(async (em) => {
|
||||
const entities = options?.entities || [];
|
||||
const tableNames =
|
||||
entities.length > 0
|
||||
? entities.map((entity) => em.getRepository(entity).metadata.tableName)
|
||||
: dataSource.entityMetadatas
|
||||
.map((entity) => entity.tableName)
|
||||
.filter((tableName) => !tableName.startsWith('geodata'));
|
||||
|
||||
let deleteUsers = false;
|
||||
for (const tableName of tableNames) {
|
||||
if (tableName === 'users') {
|
||||
deleteUsers = true;
|
||||
continue;
|
||||
}
|
||||
await em.query(`DELETE FROM ${tableName} CASCADE;`);
|
||||
}
|
||||
if (deleteUsers) {
|
||||
await em.query(`DELETE FROM "users" CASCADE;`);
|
||||
}
|
||||
|
||||
// Release all locks
|
||||
await em.query('SELECT pg_advisory_unlock_all()');
|
||||
});
|
||||
},
|
||||
disconnect: async () => {
|
||||
if (dataSource.isInitialized) {
|
||||
await dataSource.destroy();
|
||||
}
|
||||
},
|
||||
};
|
||||
|
||||
class JobMock implements IJobRepository {
|
||||
private _handler: JobItemHandler = () => Promise.resolve();
|
||||
addHandler(_queueName: QueueName, _concurrency: number, handler: JobItemHandler) {
|
||||
this._handler = handler;
|
||||
}
|
||||
addCronJob() {}
|
||||
updateCronJob() {}
|
||||
deleteCronJob() {}
|
||||
validateCronExpression() {}
|
||||
queue(item: JobItem) {
|
||||
return this._handler(item);
|
||||
}
|
||||
queueAll(items: JobItem[]) {
|
||||
return Promise.all(items.map((arg) => this._handler(arg))).then(() => {});
|
||||
}
|
||||
async resume() {}
|
||||
async empty() {}
|
||||
async setConcurrency() {}
|
||||
getQueueStatus() {
|
||||
return Promise.resolve(null) as any;
|
||||
}
|
||||
getJobCounts() {
|
||||
return Promise.resolve(null) as any;
|
||||
}
|
||||
async pause() {}
|
||||
clear() {
|
||||
return Promise.resolve([]);
|
||||
}
|
||||
async waitForQueueCompletion() {}
|
||||
}
|
||||
|
||||
class MediaMockRepository extends MediaRepository {
|
||||
generateThumbhash() {
|
||||
return Promise.resolve(Buffer.from('mock-thumbhash'));
|
||||
}
|
||||
}
|
||||
|
||||
let app: INestApplication;
|
||||
|
||||
export const testApp = {
|
||||
create: async (): Promise<INestApplication> => {
|
||||
const moduleFixture = await Test.createTestingModule({ imports: [AppTestModule] })
|
||||
.overrideProvider(IJobRepository)
|
||||
.useClass(JobMock)
|
||||
.overrideProvider(IMediaRepository)
|
||||
.useClass(MediaMockRepository)
|
||||
.compile();
|
||||
|
||||
app = await moduleFixture.createNestApplication().init();
|
||||
await app.get(ApiService).init();
|
||||
await db.reset();
|
||||
await app.get(ApiService).init();
|
||||
await app.get(MicroservicesService).init();
|
||||
|
||||
return app;
|
||||
},
|
||||
reset: async (options?: ResetOptions) => {
|
||||
await db.reset(options);
|
||||
},
|
||||
get: (member: any) => app.get(member),
|
||||
teardown: async () => {
|
||||
if (app) {
|
||||
await app.get(MicroservicesService).teardown();
|
||||
await app.close();
|
||||
}
|
||||
await db.disconnect();
|
||||
},
|
||||
};
|
||||
|
||||
export function waitForEvent(emitter: EventEmitter, event: string, times = 1): Promise<void[]> {
|
||||
const promises: Promise<void>[] = [];
|
||||
|
||||
for (let i = 1; i <= times; i++) {
|
||||
promises.push(
|
||||
new Promise((resolve, reject) => {
|
||||
const success = (value: any) => {
|
||||
emitter.off(StorageEventType.ERROR, fail);
|
||||
resolve(value);
|
||||
};
|
||||
const fail = (error: Error) => {
|
||||
emitter.off(event, success);
|
||||
reject(error);
|
||||
};
|
||||
emitter.once(event, success);
|
||||
emitter.once(StorageEventType.ERROR, fail);
|
||||
}),
|
||||
);
|
||||
}
|
||||
return Promise.all(promises);
|
||||
}
|
||||
|
||||
const directoryExists = async (dirPath: string) =>
|
||||
await fs.promises
|
||||
.access(dirPath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
export async function restoreTempFolder(): Promise<void> {
|
||||
if (await directoryExists(`${IMMICH_TEST_ASSET_TEMP_PATH}`)) {
|
||||
// Temp directory exists, delete all files inside it
|
||||
await fs.promises.rm(IMMICH_TEST_ASSET_TEMP_PATH, { recursive: true });
|
||||
}
|
||||
// Create temp folder
|
||||
await fs.promises.mkdir(IMMICH_TEST_ASSET_TEMP_PATH);
|
||||
}
|
Loading…
Reference in New Issue
Block a user