mirror of
https://github.com/immich-app/immich.git
synced 2025-01-23 12:12:45 +01:00
chore(server): cleanup library watching (#8835)
chore: clean up library watching
This commit is contained in:
parent
1c1e461936
commit
dba365634a
23 changed files with 56 additions and 1088 deletions
.github/workflows
Makefiledocs/docs/developer
e2e/src
server
e2e
package-lock.jsonpackage.jsonsrc
interfaces
repositories
services
test
13
.github/workflows/test.yml
vendored
13
.github/workflows/test.yml
vendored
|
@ -10,19 +10,6 @@ concurrency:
|
||||||
cancel-in-progress: true
|
cancel-in-progress: true
|
||||||
|
|
||||||
jobs:
|
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:
|
doc-tests:
|
||||||
name: Docs
|
name: Docs
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
|
3
Makefile
3
Makefile
|
@ -16,9 +16,6 @@ stage:
|
||||||
pull-stage:
|
pull-stage:
|
||||||
docker compose -f ./docker/docker-compose.staging.yml pull
|
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
|
.PHONY: e2e
|
||||||
e2e:
|
e2e:
|
||||||
docker compose -f ./e2e/docker-compose.yml up --build -V --remove-orphans
|
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
|
### 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`
|
```bash
|
||||||
- `make server-e2e-jobs`
|
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 () => {
|
beforeAll(async () => {
|
||||||
await utils.resetDatabase();
|
await utils.resetDatabase();
|
||||||
admin = await utils.adminSetup();
|
admin = await utils.adminSetup();
|
||||||
|
await utils.resetAdminConfig(admin.accessToken);
|
||||||
user = await utils.userSetup(admin.accessToken, userDto.user1);
|
user = await utils.userSetup(admin.accessToken, userDto.user1);
|
||||||
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External });
|
library = await utils.createLibrary(admin.accessToken, { ownerId: admin.userId, type: LibraryType.External });
|
||||||
websocket = await utils.connectWebsocket(admin.accessToken);
|
websocket = await utils.connectWebsocket(admin.accessToken);
|
||||||
|
@ -36,7 +37,7 @@ describe('/library', () => {
|
||||||
|
|
||||||
afterAll(() => {
|
afterAll(() => {
|
||||||
utils.disconnectWebsocket(websocket);
|
utils.disconnectWebsocket(websocket);
|
||||||
utils.deleteTempFolder();
|
utils.resetTempFolder();
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
@ -816,40 +817,4 @@ describe('/library', () => {
|
||||||
expect(existsSync(`${testAssetDir}/temp/directoryB/assetB.png`)).toBe(true);
|
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,
|
getAllAssets,
|
||||||
getAllJobsStatus,
|
getAllJobsStatus,
|
||||||
getAssetInfo,
|
getAssetInfo,
|
||||||
|
getConfigDefaults,
|
||||||
login,
|
login,
|
||||||
searchMetadata,
|
searchMetadata,
|
||||||
setAdminOnboarding,
|
setAdminOnboarding,
|
||||||
signUpAdmin,
|
signUpAdmin,
|
||||||
|
updateConfig,
|
||||||
validate,
|
validate,
|
||||||
} from '@immich/sdk';
|
} from '@immich/sdk';
|
||||||
import { BrowserContext } from '@playwright/test';
|
import { BrowserContext } from '@playwright/test';
|
||||||
|
@ -139,6 +141,7 @@ export const utils = {
|
||||||
'user_token',
|
'user_token',
|
||||||
'users',
|
'users',
|
||||||
'system_metadata',
|
'system_metadata',
|
||||||
|
'system_config',
|
||||||
];
|
];
|
||||||
|
|
||||||
const sql: string[] = [];
|
const sql: string[] = [];
|
||||||
|
@ -148,7 +151,12 @@ export const utils = {
|
||||||
}
|
}
|
||||||
|
|
||||||
for (const table of tables) {
|
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'));
|
await client.query(sql.join('\n'));
|
||||||
|
@ -310,9 +318,7 @@ export const utils = {
|
||||||
if (!existsSync(dirname(path))) {
|
if (!existsSync(dirname(path))) {
|
||||||
mkdirSync(dirname(path), { recursive: true });
|
mkdirSync(dirname(path), { recursive: true });
|
||||||
}
|
}
|
||||||
if (!existsSync(path)) {
|
writeFileSync(path, makeRandomImage());
|
||||||
writeFileSync(path, makeRandomImage());
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
removeImageFile: (path: string) => {
|
removeImageFile: (path: string) => {
|
||||||
|
@ -407,8 +413,14 @@ export const utils = {
|
||||||
},
|
},
|
||||||
]),
|
]),
|
||||||
|
|
||||||
deleteTempFolder: () => {
|
resetTempFolder: () => {
|
||||||
rmSync(`${testAssetDir}/temp`, { recursive: true, force: true });
|
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) => {
|
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/mock-fs": "^4.13.1",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^20.5.7",
|
"@types/node": "^20.5.7",
|
||||||
"@types/supertest": "^6.0.0",
|
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"@typescript-eslint/parser": "^7.0.0",
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
|
@ -98,8 +97,6 @@
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"sql-formatter": "^15.0.0",
|
"sql-formatter": "^15.0.0",
|
||||||
"supertest": "^6.3.3",
|
|
||||||
"testcontainers": "^10.2.1",
|
|
||||||
"ts-jest": "^29.1.1",
|
"ts-jest": "^29.1.1",
|
||||||
"ts-loader": "^9.4.4",
|
"ts-loader": "^9.4.4",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
|
@ -4390,12 +4387,6 @@
|
||||||
"@types/express": "*"
|
"@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": {
|
"node_modules/@types/cookies": {
|
||||||
"version": "0.9.0",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz",
|
||||||
|
@ -4685,12 +4676,6 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"node_modules/@types/mime": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
|
||||||
|
@ -4890,27 +4875,6 @@
|
||||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||||
"dev": true
|
"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": {
|
"node_modules/@types/tedious": {
|
||||||
"version": "4.0.14",
|
"version": "4.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz",
|
||||||
|
@ -5709,12 +5673,6 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/asn1": {
|
||||||
"version": "0.2.6",
|
"version": "0.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz",
|
||||||
"integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="
|
"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": {
|
"node_modules/b4a": {
|
||||||
"version": "1.6.4",
|
"version": "1.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
|
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
|
||||||
|
@ -6587,18 +6539,6 @@
|
||||||
"color-support": "bin.js"
|
"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": {
|
"node_modules/commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
|
@ -6624,12 +6564,6 @@
|
||||||
"node": ">= 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": {
|
"node_modules/compress-commons": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
"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": {
|
"node_modules/core-js-compat": {
|
||||||
"version": "3.35.1",
|
"version": "3.35.1",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
|
"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"
|
"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": {
|
"node_modules/delegates": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||||
|
@ -7093,16 +7012,6 @@
|
||||||
"node": ">=8"
|
"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": {
|
"node_modules/diacritics": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
|
||||||
|
@ -8172,35 +8081,6 @@
|
||||||
"webpack": "^5.11.0"
|
"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": {
|
"node_modules/forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
||||||
|
@ -8711,15 +8591,6 @@
|
||||||
"he": "bin/he"
|
"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": {
|
"node_modules/highlight.js": {
|
||||||
"version": "10.7.3",
|
"version": "10.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||||
|
@ -12922,52 +12793,6 @@
|
||||||
"url": "https://github.com/sponsors/sindresorhus"
|
"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": {
|
"node_modules/supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
@ -17435,12 +17260,6 @@
|
||||||
"@types/express": "*"
|
"@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": {
|
"@types/cookies": {
|
||||||
"version": "0.9.0",
|
"version": "0.9.0",
|
||||||
"resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz",
|
"resolved": "https://registry.npmjs.org/@types/cookies/-/cookies-0.9.0.tgz",
|
||||||
|
@ -17729,12 +17548,6 @@
|
||||||
"@types/node": "*"
|
"@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": {
|
"@types/mime": {
|
||||||
"version": "1.3.3",
|
"version": "1.3.3",
|
||||||
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
|
"resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.3.tgz",
|
||||||
|
@ -17921,27 +17734,6 @@
|
||||||
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
"integrity": "sha512-Hl219/BT5fLAaz6NDkSuhzasy49dwQS/DSdu4MdggFB8zcXv7vflBI3xp7FEmkmdDkBUI2bPUNeMttp2knYdxw==",
|
||||||
"dev": true
|
"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": {
|
"@types/tedious": {
|
||||||
"version": "4.0.14",
|
"version": "4.0.14",
|
||||||
"resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz",
|
"resolved": "https://registry.npmjs.org/@types/tedious/-/tedious-4.0.14.tgz",
|
||||||
|
@ -18536,12 +18328,6 @@
|
||||||
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
"integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==",
|
||||||
"dev": true
|
"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": {
|
"asn1": {
|
||||||
"version": "0.2.6",
|
"version": "0.2.6",
|
||||||
"resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.6.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/async-lock/-/async-lock-1.4.1.tgz",
|
||||||
"integrity": "sha512-Az2ZTpuytrtqENulXwO3GGv1Bztugx6TT37NIo7imr/Qo0gsYiGtSdBa2B6fsXhTpVZDNfu1Qn3pk531e3q+nQ=="
|
"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": {
|
"b4a": {
|
||||||
"version": "1.6.4",
|
"version": "1.6.4",
|
||||||
"resolved": "https://registry.npmjs.org/b4a/-/b4a-1.6.4.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz",
|
||||||
"integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg=="
|
"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": {
|
"commander": {
|
||||||
"version": "4.1.1",
|
"version": "4.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/commander/-/commander-4.1.1.tgz",
|
||||||
|
@ -19210,12 +18981,6 @@
|
||||||
"repeat-string": "^1.6.1"
|
"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": {
|
"compress-commons": {
|
||||||
"version": "6.0.2",
|
"version": "6.0.2",
|
||||||
"resolved": "https://registry.npmjs.org/compress-commons/-/compress-commons-6.0.2.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
|
||||||
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
|
"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": {
|
"core-js-compat": {
|
||||||
"version": "3.35.1",
|
"version": "3.35.1",
|
||||||
"resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.35.1.tgz",
|
"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"
|
"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": {
|
"delegates": {
|
||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
"resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz",
|
||||||
|
@ -19539,16 +19292,6 @@
|
||||||
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
|
"integrity": "sha512-TLz+x/vEXm/Y7P7wn1EJFNLxYpUD4TgMosxY6fAVJUnJMbupHBOncxyWUG9OpTaH9EBD7uFI5LfEgmMOc54DsA==",
|
||||||
"dev": true
|
"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": {
|
"diacritics": {
|
||||||
"version": "1.3.0",
|
"version": "1.3.0",
|
||||||
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
|
"resolved": "https://registry.npmjs.org/diacritics/-/diacritics-1.3.0.tgz",
|
||||||
|
@ -20373,29 +20116,6 @@
|
||||||
"tapable": "^2.2.1"
|
"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": {
|
"forwarded": {
|
||||||
"version": "0.2.0",
|
"version": "0.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz",
|
"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",
|
"resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz",
|
||||||
"integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw=="
|
"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": {
|
"highlight.js": {
|
||||||
"version": "10.7.3",
|
"version": "10.7.3",
|
||||||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||||
|
@ -23956,42 +23670,6 @@
|
||||||
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
|
"integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==",
|
||||||
"dev": true
|
"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": {
|
"supports-color": {
|
||||||
"version": "7.2.0",
|
"version": "7.2.0",
|
||||||
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
"resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz",
|
||||||
|
|
|
@ -105,7 +105,6 @@
|
||||||
"@types/mock-fs": "^4.13.1",
|
"@types/mock-fs": "^4.13.1",
|
||||||
"@types/multer": "^1.4.7",
|
"@types/multer": "^1.4.7",
|
||||||
"@types/node": "^20.5.7",
|
"@types/node": "^20.5.7",
|
||||||
"@types/supertest": "^6.0.0",
|
|
||||||
"@types/ua-parser-js": "^0.7.36",
|
"@types/ua-parser-js": "^0.7.36",
|
||||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||||
"@typescript-eslint/parser": "^7.0.0",
|
"@typescript-eslint/parser": "^7.0.0",
|
||||||
|
@ -122,8 +121,6 @@
|
||||||
"rimraf": "^5.0.1",
|
"rimraf": "^5.0.1",
|
||||||
"source-map-support": "^0.5.21",
|
"source-map-support": "^0.5.21",
|
||||||
"sql-formatter": "^15.0.0",
|
"sql-formatter": "^15.0.0",
|
||||||
"supertest": "^6.3.3",
|
|
||||||
"testcontainers": "^10.2.1",
|
|
||||||
"ts-jest": "^29.1.1",
|
"ts-jest": "^29.1.1",
|
||||||
"ts-loader": "^9.4.4",
|
"ts-loader": "^9.4.4",
|
||||||
"ts-node": "^10.9.1",
|
"ts-node": "^10.9.1",
|
||||||
|
|
|
@ -31,14 +31,6 @@ export interface WatchEvents {
|
||||||
onError(error: Error): void;
|
onError(error: Error): void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export enum StorageEventType {
|
|
||||||
READY = 'ready',
|
|
||||||
ADD = 'add',
|
|
||||||
CHANGE = 'change',
|
|
||||||
UNLINK = 'unlink',
|
|
||||||
ERROR = 'error',
|
|
||||||
}
|
|
||||||
|
|
||||||
export interface IStorageRepository {
|
export interface IStorageRepository {
|
||||||
createZipStream(): ImmichZipStream;
|
createZipStream(): ImmichZipStream;
|
||||||
createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream>;
|
createReadStream(filepath: string, mimeType?: string | null): Promise<ImmichReadStream>;
|
||||||
|
|
|
@ -10,7 +10,6 @@ import {
|
||||||
IStorageRepository,
|
IStorageRepository,
|
||||||
ImmichReadStream,
|
ImmichReadStream,
|
||||||
ImmichZipStream,
|
ImmichZipStream,
|
||||||
StorageEventType,
|
|
||||||
WatchEvents,
|
WatchEvents,
|
||||||
} from 'src/interfaces/storage.interface';
|
} from 'src/interfaces/storage.interface';
|
||||||
import { Instrumentation } from 'src/utils/instrumentation';
|
import { Instrumentation } from 'src/utils/instrumentation';
|
||||||
|
@ -173,11 +172,11 @@ export class StorageRepository implements IStorageRepository {
|
||||||
watch(paths: string[], options: WatchOptions, events: Partial<WatchEvents>) {
|
watch(paths: string[], options: WatchOptions, events: Partial<WatchEvents>) {
|
||||||
const watcher = chokidar.watch(paths, options);
|
const watcher = chokidar.watch(paths, options);
|
||||||
|
|
||||||
watcher.on(StorageEventType.READY, () => events.onReady?.());
|
watcher.on('ready', () => events.onReady?.());
|
||||||
watcher.on(StorageEventType.ADD, (path) => events.onAdd?.(path));
|
watcher.on('add', (path) => events.onAdd?.(path));
|
||||||
watcher.on(StorageEventType.CHANGE, (path) => events.onChange?.(path));
|
watcher.on('change', (path) => events.onChange?.(path));
|
||||||
watcher.on(StorageEventType.UNLINK, (path) => events.onUnlink?.(path));
|
watcher.on('unlink', (path) => events.onUnlink?.(path));
|
||||||
watcher.on(StorageEventType.ERROR, (error) => events.onError?.(error));
|
watcher.on('error', (error) => events.onError?.(error));
|
||||||
|
|
||||||
return () => watcher.close();
|
return () => watcher.close();
|
||||||
}
|
}
|
||||||
|
|
|
@ -13,7 +13,7 @@ import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||||
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
import { IDatabaseRepository } from 'src/interfaces/database.interface';
|
||||||
import { IJobRepository, ILibraryFileJob, ILibraryRefreshJob, JobName, JobStatus } from 'src/interfaces/job.interface';
|
import { IJobRepository, ILibraryFileJob, ILibraryRefreshJob, JobName, JobStatus } from 'src/interfaces/job.interface';
|
||||||
import { ILibraryRepository } from 'src/interfaces/library.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 { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||||
import { LibraryService } from 'src/services/library.service';
|
import { LibraryService } from 'src/services/library.service';
|
||||||
import { assetStub } from 'test/fixtures/asset.stub';
|
import { assetStub } from 'test/fixtures/asset.stub';
|
||||||
|
@ -933,12 +933,6 @@ describe(LibraryService.name, () => {
|
||||||
type: LibraryType.EXTERNAL,
|
type: LibraryType.EXTERNAL,
|
||||||
importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths,
|
importPaths: libraryStub.externalLibraryWithImportPaths1.importPaths,
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(storageMock.watch).toHaveBeenCalledWith(
|
|
||||||
libraryStub.externalLibraryWithImportPaths1.importPaths,
|
|
||||||
expect.anything(),
|
|
||||||
expect.anything(),
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should create with exclusion patterns', async () => {
|
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));
|
await expect(sut.update('library-id', {})).resolves.toEqual(mapLibrary(libraryStub.uploadLibrary1));
|
||||||
expect(libraryMock.update).toHaveBeenCalledWith(expect.objectContaining({ id: 'library-id' }));
|
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', () => {
|
describe('watchAll', () => {
|
||||||
|
@ -1198,9 +1153,7 @@ describe(LibraryService.name, () => {
|
||||||
it('should handle a new file event', async () => {
|
it('should handle a new file event', async () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||||
storageMock.watch.mockImplementation(
|
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] }));
|
||||||
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/foo/photo.jpg' }] }),
|
|
||||||
);
|
|
||||||
|
|
||||||
await sut.watchAll();
|
await sut.watchAll();
|
||||||
|
|
||||||
|
@ -1221,7 +1174,7 @@ describe(LibraryService.name, () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||||
storageMock.watch.mockImplementation(
|
storageMock.watch.mockImplementation(
|
||||||
makeMockWatcher({ items: [{ event: StorageEventType.CHANGE, value: '/foo/photo.jpg' }] }),
|
makeMockWatcher({ items: [{ event: 'change', value: '/foo/photo.jpg' }] }),
|
||||||
);
|
);
|
||||||
|
|
||||||
await sut.watchAll();
|
await sut.watchAll();
|
||||||
|
@ -1244,7 +1197,7 @@ describe(LibraryService.name, () => {
|
||||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||||
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external);
|
assetMock.getByLibraryIdAndOriginalPath.mockResolvedValue(assetStub.external);
|
||||||
storageMock.watch.mockImplementation(
|
storageMock.watch.mockImplementation(
|
||||||
makeMockWatcher({ items: [{ event: StorageEventType.UNLINK, value: '/foo/photo.jpg' }] }),
|
makeMockWatcher({ items: [{ event: 'unlink', value: '/foo/photo.jpg' }] }),
|
||||||
);
|
);
|
||||||
|
|
||||||
await sut.watchAll();
|
await sut.watchAll();
|
||||||
|
@ -1258,19 +1211,17 @@ describe(LibraryService.name, () => {
|
||||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||||
storageMock.watch.mockImplementation(
|
storageMock.watch.mockImplementation(
|
||||||
makeMockWatcher({
|
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 () => {
|
it('should ignore unknown extensions', async () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
libraryMock.get.mockResolvedValue(libraryStub.externalLibraryWithImportPaths1);
|
||||||
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
libraryMock.getAll.mockResolvedValue([libraryStub.externalLibraryWithImportPaths1]);
|
||||||
storageMock.watch.mockImplementation(
|
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/foo/photo.jpg' }] }));
|
||||||
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/foo/photo.jpg' }] }),
|
|
||||||
);
|
|
||||||
|
|
||||||
await sut.watchAll();
|
await sut.watchAll();
|
||||||
|
|
||||||
|
@ -1280,9 +1231,7 @@ describe(LibraryService.name, () => {
|
||||||
it('should ignore excluded paths', async () => {
|
it('should ignore excluded paths', async () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.patternPath);
|
libraryMock.get.mockResolvedValue(libraryStub.patternPath);
|
||||||
libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]);
|
libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]);
|
||||||
storageMock.watch.mockImplementation(
|
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/dir1/photo.txt' }] }));
|
||||||
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/dir1/photo.txt' }] }),
|
|
||||||
);
|
|
||||||
|
|
||||||
await sut.watchAll();
|
await sut.watchAll();
|
||||||
|
|
||||||
|
@ -1292,9 +1241,7 @@ describe(LibraryService.name, () => {
|
||||||
it('should ignore excluded paths without case sensitivity', async () => {
|
it('should ignore excluded paths without case sensitivity', async () => {
|
||||||
libraryMock.get.mockResolvedValue(libraryStub.patternPath);
|
libraryMock.get.mockResolvedValue(libraryStub.patternPath);
|
||||||
libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]);
|
libraryMock.getAll.mockResolvedValue([libraryStub.patternPath]);
|
||||||
storageMock.watch.mockImplementation(
|
storageMock.watch.mockImplementation(makeMockWatcher({ items: [{ event: 'add', value: '/DIR1/photo.txt' }] }));
|
||||||
makeMockWatcher({ items: [{ event: StorageEventType.ADD, value: '/DIR1/photo.txt' }] }),
|
|
||||||
);
|
|
||||||
|
|
||||||
await sut.watchAll();
|
await sut.watchAll();
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,6 @@
|
||||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||||
import { Trie } from 'mnemonist';
|
import { Trie } from 'mnemonist';
|
||||||
import { R_OK } from 'node:constants';
|
import { R_OK } from 'node:constants';
|
||||||
import { EventEmitter } from 'node:events';
|
|
||||||
import { Stats } from 'node:fs';
|
import { Stats } from 'node:fs';
|
||||||
import path, { basename, parse } from 'node:path';
|
import path, { basename, parse } from 'node:path';
|
||||||
import picomatch from 'picomatch';
|
import picomatch from 'picomatch';
|
||||||
|
@ -37,7 +36,7 @@ import {
|
||||||
JobStatus,
|
JobStatus,
|
||||||
} from 'src/interfaces/job.interface';
|
} from 'src/interfaces/job.interface';
|
||||||
import { ILibraryRepository } from 'src/interfaces/library.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 { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||||
import { ImmichLogger } from 'src/utils/logger';
|
import { ImmichLogger } from 'src/utils/logger';
|
||||||
import { mimeTypes } from 'src/utils/mime-types';
|
import { mimeTypes } from 'src/utils/mime-types';
|
||||||
|
@ -48,7 +47,7 @@ import { validateCronExpression } from 'src/validation';
|
||||||
const LIBRARY_SCAN_BATCH_SIZE = 5000;
|
const LIBRARY_SCAN_BATCH_SIZE = 5000;
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class LibraryService extends EventEmitter {
|
export class LibraryService {
|
||||||
readonly logger = new ImmichLogger(LibraryService.name);
|
readonly logger = new ImmichLogger(LibraryService.name);
|
||||||
private configCore: SystemConfigCore;
|
private configCore: SystemConfigCore;
|
||||||
private watchLibraries = false;
|
private watchLibraries = false;
|
||||||
|
@ -64,7 +63,6 @@ export class LibraryService extends EventEmitter {
|
||||||
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
@Inject(IStorageRepository) private storageRepository: IStorageRepository,
|
||||||
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
@Inject(IDatabaseRepository) private databaseRepository: IDatabaseRepository,
|
||||||
) {
|
) {
|
||||||
super();
|
|
||||||
this.configCore = SystemConfigCore.create(configRepository);
|
this.configCore = SystemConfigCore.create(configRepository);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -152,7 +150,6 @@ export class LibraryService extends EventEmitter {
|
||||||
if (matcher(path)) {
|
if (matcher(path)) {
|
||||||
await this.scanAssets(library.id, [path], library.ownerId, false);
|
await this.scanAssets(library.id, [path], library.ownerId, false);
|
||||||
}
|
}
|
||||||
this.emit(StorageEventType.ADD, path);
|
|
||||||
};
|
};
|
||||||
return handlePromiseError(handler(), this.logger);
|
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.
|
// Note: if the changed file was not previously imported, it will be imported now.
|
||||||
await this.scanAssets(library.id, [path], library.ownerId, false);
|
await this.scanAssets(library.id, [path], library.ownerId, false);
|
||||||
}
|
}
|
||||||
this.emit(StorageEventType.CHANGE, path);
|
|
||||||
};
|
};
|
||||||
return handlePromiseError(handler(), this.logger);
|
return handlePromiseError(handler(), this.logger);
|
||||||
},
|
},
|
||||||
|
@ -174,13 +170,11 @@ export class LibraryService extends EventEmitter {
|
||||||
if (asset && matcher(path)) {
|
if (asset && matcher(path)) {
|
||||||
await this.assetRepository.update({ id: asset.id, isOffline: true });
|
await this.assetRepository.update({ id: asset.id, isOffline: true });
|
||||||
}
|
}
|
||||||
this.emit(StorageEventType.UNLINK, path);
|
|
||||||
};
|
};
|
||||||
return handlePromiseError(handler(), this.logger);
|
return handlePromiseError(handler(), this.logger);
|
||||||
},
|
},
|
||||||
onError: (error) => {
|
onError: (error) => {
|
||||||
this.logger.error(`Library watcher for library ${library.id} encountered error: ${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}}`);
|
this.logger.log(`Creating ${dto.type} library for ${dto.ownerId}}`);
|
||||||
|
|
||||||
if (dto.type === LibraryType.EXTERNAL) {
|
|
||||||
await this.watch(library.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
return mapLibrary(library);
|
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);
|
return mapLibrary(library);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { WatchOptions } from 'chokidar';
|
import { WatchOptions } from 'chokidar';
|
||||||
import { StorageCore } from 'src/cores/storage.core';
|
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 {
|
interface MockWatcherOptions {
|
||||||
items?: Array<{ event: 'change' | 'add' | 'unlink' | 'error'; value: string }>;
|
items?: Array<{ event: 'change' | 'add' | 'unlink' | 'error'; value: string }>;
|
||||||
|
@ -13,19 +13,19 @@ export const makeMockWatcher =
|
||||||
events.onReady?.();
|
events.onReady?.();
|
||||||
for (const item of items || []) {
|
for (const item of items || []) {
|
||||||
switch (item.event) {
|
switch (item.event) {
|
||||||
case StorageEventType.ADD: {
|
case 'add': {
|
||||||
events.onAdd?.(item.value);
|
events.onAdd?.(item.value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case StorageEventType.CHANGE: {
|
case 'change': {
|
||||||
events.onChange?.(item.value);
|
events.onChange?.(item.value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case StorageEventType.UNLINK: {
|
case 'unlink': {
|
||||||
events.onUnlink?.(item.value);
|
events.onUnlink?.(item.value);
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
case StorageEventType.ERROR: {
|
case 'error': {
|
||||||
events.onError?.(new Error(item.value));
|
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 a new issue