mirror of
https://github.com/immich-app/immich.git
synced 2025-01-16 00:36:47 +01:00
chore(server,cli,web): housekeeping and stricter code style (#6751)
* add unicorn to eslint * fix lint errors for cli * fix merge * fix album name extraction * Update cli/src/commands/upload.command.ts Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> * es2k23 * use lowercase os * return undefined album name * fix bug in asset response dto * auto fix issues * fix server code style * es2022 and formatting * fix compilation error * fix test * fix config load * fix last lint errors * set string type * bump ts * start work on web * web formatting * Fix UUIDParamDto as UUIDParamDto * fix library service lint * fix web errors * fix errors * formatting * wip * lints fixed * web can now start * alphabetical package json * rename error * chore: clean up --------- Co-authored-by: Ben McCann <322311+benmccann@users.noreply.github.com> Co-authored-by: Jason Rasmussen <jrasm91@gmail.com>
This commit is contained in:
parent
e4d0560d49
commit
f44fa45aa0
218 changed files with 2471 additions and 1244 deletions
|
@ -6,7 +6,7 @@ module.exports = {
|
|||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
|
||||
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
|
@ -19,6 +19,14 @@ module.exports = {
|
|||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'unicorn/prefer-module': 'off',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
'unicorn/prevent-abbreviations': [
|
||||
'error',
|
||||
{
|
||||
ignore: ['\\.e2e-spec$', /^ignore/i],
|
||||
},
|
||||
],
|
||||
},
|
||||
};
|
||||
|
|
30
cli/package-lock.json
generated
30
cli/package-lock.json
generated
|
@ -30,15 +30,15 @@
|
|||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@vitest/coverage-v8": "^1.2.2",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-unicorn": "^50.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^50.0.1",
|
||||
"immich": "file:../server",
|
||||
"mock-fs": "^5.2.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.5.3",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.2.1"
|
||||
}
|
||||
},
|
||||
|
@ -73,6 +73,7 @@
|
|||
"@nestjs/typeorm": "^10.0.0",
|
||||
"@nestjs/websockets": "^10.2.2",
|
||||
"@socket.io/postgres-adapter": "^0.3.1",
|
||||
"@types/picomatch": "^2.3.3",
|
||||
"archiver": "^6.0.0",
|
||||
"async-lock": "^1.4.0",
|
||||
"axios": "^1.5.0",
|
||||
|
@ -96,6 +97,7 @@
|
|||
"node-addon-api": "^7.0.0",
|
||||
"openid-client": "^5.4.3",
|
||||
"pg": "^8.11.3",
|
||||
"picomatch": "^3.0.1",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
|
@ -127,10 +129,12 @@
|
|||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||
"@typescript-eslint/parser": "^6.4.1",
|
||||
"chokidar": "^3.5.3",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^50.0.1",
|
||||
"jest": "^29.6.4",
|
||||
"jest-when": "^3.6.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
|
@ -8768,6 +8772,7 @@
|
|||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/multer": "^1.4.7",
|
||||
"@types/node": "^20.5.7",
|
||||
"@types/picomatch": "^2.3.3",
|
||||
"@types/sharp": "^0.31.1",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
|
@ -8778,13 +8783,15 @@
|
|||
"axios": "^1.5.0",
|
||||
"bcrypt": "^5.1.1",
|
||||
"bullmq": "^4.8.0",
|
||||
"chokidar": "^3.5.3",
|
||||
"class-transformer": "^0.5.1",
|
||||
"class-validator": "^0.14.0",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^50.0.1",
|
||||
"exiftool-vendored": "~24.4.0",
|
||||
"exiftool-vendored.pl": "12.73",
|
||||
"fluent-ffmpeg": "^2.1.2",
|
||||
|
@ -8803,6 +8810,7 @@
|
|||
"node-addon-api": "^7.0.0",
|
||||
"openid-client": "^5.4.3",
|
||||
"pg": "^8.11.3",
|
||||
"picomatch": "^3.0.1",
|
||||
"prettier": "^3.0.2",
|
||||
"prettier-plugin-organize-imports": "^3.2.3",
|
||||
"reflect-metadata": "^0.1.13",
|
||||
|
|
|
@ -28,18 +28,18 @@
|
|||
"@types/cli-progress": "^3.11.0",
|
||||
"@types/mock-fs": "^4.13.1",
|
||||
"@types/node": "^20.3.1",
|
||||
"@typescript-eslint/eslint-plugin": "^6.0.0",
|
||||
"@typescript-eslint/parser": "^6.0.0",
|
||||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||
"@typescript-eslint/parser": "^6.4.1",
|
||||
"@vitest/coverage-v8": "^1.2.2",
|
||||
"eslint": "^8.43.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint-plugin-unicorn": "^50.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^50.0.1",
|
||||
"immich": "file:../server",
|
||||
"mock-fs": "^5.2.0",
|
||||
"ts-node": "^10.9.1",
|
||||
"tslib": "^2.5.3",
|
||||
"typescript": "^5.0.0",
|
||||
"typescript": "^5.3.3",
|
||||
"vitest": "^1.2.1"
|
||||
},
|
||||
"scripts": {
|
||||
|
|
|
@ -8,7 +8,7 @@ import { BaseCommand } from './base-command';
|
|||
import { basename } from 'node:path';
|
||||
import { access, constants, stat, unlink } from 'node:fs/promises';
|
||||
import { createHash } from 'node:crypto';
|
||||
import Os from 'os';
|
||||
import os from 'node:os';
|
||||
|
||||
class Asset {
|
||||
readonly path: string;
|
||||
|
@ -27,7 +27,7 @@ class Asset {
|
|||
|
||||
async prepare() {
|
||||
const stats = await stat(this.path);
|
||||
this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replace(/\s+/g, '');
|
||||
this.deviceAssetId = `${basename(this.path)}-${stats.size}`.replaceAll(/\s+/g, '');
|
||||
this.fileCreatedAt = stats.mtime.toISOString();
|
||||
this.fileModifiedAt = stats.mtime.toISOString();
|
||||
this.fileSize = stats.size;
|
||||
|
@ -35,9 +35,15 @@ class Asset {
|
|||
}
|
||||
|
||||
async getUploadFormData(): Promise<FormData> {
|
||||
if (!this.deviceAssetId) throw new Error('Device asset id not set');
|
||||
if (!this.fileCreatedAt) throw new Error('File created at not set');
|
||||
if (!this.fileModifiedAt) throw new Error('File modified at not set');
|
||||
if (!this.deviceAssetId) {
|
||||
throw new Error('Device asset id not set');
|
||||
}
|
||||
if (!this.fileCreatedAt) {
|
||||
throw new Error('File created at not set');
|
||||
}
|
||||
if (!this.fileModifiedAt) {
|
||||
throw new Error('File modified at not set');
|
||||
}
|
||||
|
||||
// TODO: doesn't xmp replace the file extension? Will need investigation
|
||||
const sideCarPath = `${this.path}.xmp`;
|
||||
|
@ -45,7 +51,7 @@ class Asset {
|
|||
try {
|
||||
await access(sideCarPath, constants.R_OK);
|
||||
sidecarData = createReadStream(sideCarPath);
|
||||
} catch (error) {}
|
||||
} catch {}
|
||||
|
||||
const data: any = {
|
||||
assetData: createReadStream(this.path),
|
||||
|
@ -57,8 +63,8 @@ class Asset {
|
|||
};
|
||||
const formData = new FormData();
|
||||
|
||||
for (const prop in data) {
|
||||
formData.append(prop, data[prop]);
|
||||
for (const property in data) {
|
||||
formData.append(property, data[property]);
|
||||
}
|
||||
|
||||
if (sidecarData) {
|
||||
|
@ -86,12 +92,8 @@ class Asset {
|
|||
return await sha1(this.path);
|
||||
}
|
||||
|
||||
private extractAlbumName(): string {
|
||||
if (Os.platform() === 'win32') {
|
||||
return this.path.split('\\').slice(-2)[0];
|
||||
} else {
|
||||
return this.path.split('/').slice(-2)[0];
|
||||
}
|
||||
private extractAlbumName(): string | undefined {
|
||||
return os.platform() === 'win32' ? this.path.split('\\').at(-2) : this.path.split('/').at(-2);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -162,7 +164,7 @@ export class UploadCommand extends BaseCommand {
|
|||
}
|
||||
}
|
||||
|
||||
const existingAlbums = (await this.immichApi.albumApi.getAllAlbums()).data;
|
||||
const { data: existingAlbums } = await this.immichApi.albumApi.getAllAlbums();
|
||||
|
||||
uploadProgress.start(totalSize, 0);
|
||||
uploadProgress.update({ value_formatted: 0, total_formatted: byteSize(totalSize) });
|
||||
|
@ -195,32 +197,30 @@ export class UploadCommand extends BaseCommand {
|
|||
skipAsset = skipUpload && !isDuplicate;
|
||||
}
|
||||
|
||||
if (!skipAsset) {
|
||||
if (!options.dryRun) {
|
||||
if (!skipUpload) {
|
||||
const formData = await asset.getUploadFormData();
|
||||
const res = await this.uploadAsset(formData);
|
||||
existingAssetId = res.data.id;
|
||||
uploadCounter++;
|
||||
totalSizeUploaded += asset.fileSize;
|
||||
if (!skipAsset && !options.dryRun) {
|
||||
if (!skipUpload) {
|
||||
const formData = await asset.getUploadFormData();
|
||||
const { data } = await this.uploadAsset(formData);
|
||||
existingAssetId = data.id;
|
||||
uploadCounter++;
|
||||
totalSizeUploaded += asset.fileSize;
|
||||
}
|
||||
|
||||
if ((options.album || options.albumName) && asset.albumName !== undefined) {
|
||||
let album = existingAlbums.find((album) => album.albumName === asset.albumName);
|
||||
if (!album) {
|
||||
const { data } = await this.immichApi.albumApi.createAlbum({
|
||||
createAlbumDto: { albumName: asset.albumName },
|
||||
});
|
||||
album = data;
|
||||
existingAlbums.push(album);
|
||||
}
|
||||
|
||||
if ((options.album || options.albumName) && asset.albumName !== undefined) {
|
||||
let album = existingAlbums.find((album) => album.albumName === asset.albumName);
|
||||
if (!album) {
|
||||
const res = await this.immichApi.albumApi.createAlbum({
|
||||
createAlbumDto: { albumName: asset.albumName },
|
||||
});
|
||||
album = res.data;
|
||||
existingAlbums.push(album);
|
||||
}
|
||||
|
||||
if (existingAssetId) {
|
||||
await this.immichApi.albumApi.addAssetsToAlbum({
|
||||
id: album.id,
|
||||
bulkIdsDto: { ids: [existingAssetId] },
|
||||
});
|
||||
}
|
||||
if (existingAssetId) {
|
||||
await this.immichApi.albumApi.addAssetsToAlbum({
|
||||
id: album.id,
|
||||
bulkIdsDto: { ids: [existingAssetId] },
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -233,12 +233,7 @@ export class UploadCommand extends BaseCommand {
|
|||
uploadProgress.stop();
|
||||
}
|
||||
|
||||
let messageStart;
|
||||
if (options.dryRun) {
|
||||
messageStart = 'Would have';
|
||||
} else {
|
||||
messageStart = 'Successfully';
|
||||
}
|
||||
const messageStart = options.dryRun ? 'Would have' : 'Successfully';
|
||||
|
||||
if (uploadCounter === 0) {
|
||||
console.log('All assets were already uploaded, nothing to do.');
|
||||
|
@ -276,12 +271,11 @@ export class UploadCommand extends BaseCommand {
|
|||
'x-api-key': this.immichApi.apiKey,
|
||||
...data.getHeaders(),
|
||||
},
|
||||
maxContentLength: Infinity,
|
||||
maxBodyLength: Infinity,
|
||||
maxContentLength: Number.POSITIVE_INFINITY,
|
||||
maxBodyLength: Number.POSITIVE_INFINITY,
|
||||
data,
|
||||
};
|
||||
|
||||
const res = await axios(config);
|
||||
return res;
|
||||
return axios(config);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,21 +1,21 @@
|
|||
#! /usr/bin/env node
|
||||
import { Command, Option } from 'commander';
|
||||
import path from 'node:path';
|
||||
import os from 'os';
|
||||
import os from 'node:os';
|
||||
import { version } from '../package.json';
|
||||
import { LoginCommand } from './commands/login';
|
||||
import { LogoutCommand } from './commands/logout.command';
|
||||
import { ServerInfoCommand } from './commands/server-info.command';
|
||||
import { UploadCommand } from './commands/upload.command';
|
||||
|
||||
const userHomeDir = os.homedir();
|
||||
const configDir = path.join(userHomeDir, '.config/immich/');
|
||||
const homeDirectory = os.homedir();
|
||||
const configDirectory = path.join(homeDirectory, '.config/immich/');
|
||||
|
||||
const program = new Command()
|
||||
.name('immich')
|
||||
.version(version)
|
||||
.description('Command line interface for Immich')
|
||||
.addOption(new Option('-d, --config', 'Configuration directory').env('IMMICH_CONFIG_DIR').default(configDir));
|
||||
.addOption(new Option('-d, --config', 'Configuration directory').env('IMMICH_CONFIG_DIR').default(configDirectory));
|
||||
|
||||
program
|
||||
.command('upload')
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { glob } from 'glob';
|
||||
import * as fs from 'fs';
|
||||
import * as fs from 'node:fs';
|
||||
|
||||
export class CrawlOptions {
|
||||
pathsToCrawl!: string[];
|
||||
|
@ -12,14 +12,14 @@ export class CrawlService {
|
|||
private readonly extensions!: string[];
|
||||
|
||||
constructor(image: string[], video: string[]) {
|
||||
this.extensions = image.concat(video).map((extension) => extension.replace('.', ''));
|
||||
this.extensions = [...image, ...video].map((extension) => extension.replace('.', ''));
|
||||
}
|
||||
|
||||
async crawl(options: CrawlOptions): Promise<string[]> {
|
||||
const { recursive, pathsToCrawl, exclusionPatterns, includeHidden } = options;
|
||||
|
||||
if (!pathsToCrawl) {
|
||||
return Promise.resolve([]);
|
||||
return [];
|
||||
}
|
||||
|
||||
const patterns: string[] = [];
|
||||
|
@ -65,8 +65,6 @@ export class CrawlService {
|
|||
ignore: exclusionPatterns,
|
||||
});
|
||||
|
||||
const returnedFiles = crawledFiles.concat(globbedFiles);
|
||||
returnedFiles.sort();
|
||||
return returnedFiles;
|
||||
return [...crawledFiles, ...globbedFiles].sort();
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import { existsSync } from 'fs';
|
||||
import { existsSync } from 'node:fs';
|
||||
import { access, constants, mkdir, readFile, unlink, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
import yaml from 'yaml';
|
||||
|
@ -15,12 +15,12 @@ class LoginError extends Error {
|
|||
}
|
||||
|
||||
export class SessionService {
|
||||
readonly configDir!: string;
|
||||
readonly configDirectory!: string;
|
||||
readonly authPath!: string;
|
||||
|
||||
constructor(configDir: string) {
|
||||
this.configDir = configDir;
|
||||
this.authPath = path.join(configDir, '/auth.yml');
|
||||
constructor(configDirectory: string) {
|
||||
this.configDirectory = configDirectory;
|
||||
this.authPath = path.join(configDirectory, '/auth.yml');
|
||||
}
|
||||
|
||||
async connect(): Promise<ImmichApi> {
|
||||
|
@ -74,11 +74,11 @@ export class SessionService {
|
|||
|
||||
console.log(`Logged in as ${userInfo.email}`);
|
||||
|
||||
if (!existsSync(this.configDir)) {
|
||||
if (!existsSync(this.configDirectory)) {
|
||||
// Create config folder if it doesn't exist
|
||||
const created = await mkdir(this.configDir, { recursive: true });
|
||||
const created = await mkdir(this.configDirectory, { recursive: true });
|
||||
if (!created) {
|
||||
throw new Error(`Failed to create config folder ${this.configDir}`);
|
||||
throw new Error(`Failed to create config folder ${this.configDirectory}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
import pkg from '../package.json';
|
||||
import { version } from '../package.json';
|
||||
|
||||
export interface ICliVersion {
|
||||
major: number;
|
||||
|
@ -23,7 +23,7 @@ export class CliVersion implements ICliVersion {
|
|||
}
|
||||
|
||||
static fromString(version: string): CliVersion {
|
||||
const regex = /(?:v)?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i;
|
||||
const regex = /v?(?<major>\d+)\.(?<minor>\d+)\.(?<patch>\d+)/i;
|
||||
const matchResult = version.match(regex);
|
||||
if (matchResult) {
|
||||
const [, major, minor, patch] = matchResult.map(Number);
|
||||
|
@ -34,4 +34,4 @@ export class CliVersion implements ICliVersion {
|
|||
}
|
||||
}
|
||||
|
||||
export const cliVersion = CliVersion.fromString(pkg.version);
|
||||
export const cliVersion = CliVersion.fromString(version);
|
||||
|
|
|
@ -10,10 +10,10 @@ describe(`login-key (e2e)`, () => {
|
|||
|
||||
beforeAll(async () => {
|
||||
await testApp.create();
|
||||
if (!process.env.IMMICH_INSTANCE_URL) {
|
||||
throw new Error('IMMICH_INSTANCE_URL environment variable not set');
|
||||
} else {
|
||||
if (process.env.IMMICH_INSTANCE_URL) {
|
||||
instanceUrl = process.env.IMMICH_INSTANCE_URL;
|
||||
} else {
|
||||
throw new Error('IMMICH_INSTANCE_URL environment variable not set');
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,6 +1,11 @@
|
|||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import { PostgreSqlContainer } from '@testcontainers/postgresql';
|
||||
import { access } from 'fs/promises';
|
||||
import { access } from 'node:fs/promises';
|
||||
|
||||
export const directoryExists = (directory: string) =>
|
||||
access(directory)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
|
||||
export default async () => {
|
||||
let IMMICH_TEST_ASSET_PATH: string = '';
|
||||
|
@ -12,11 +17,6 @@ export default async () => {
|
|||
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`,
|
||||
|
|
|
@ -17,6 +17,6 @@ export default defineConfig({
|
|||
minForks: 1,
|
||||
},
|
||||
},
|
||||
testTimeout: 10000,
|
||||
testTimeout: 10_000,
|
||||
},
|
||||
});
|
||||
|
|
|
@ -9,7 +9,7 @@
|
|||
"experimentalDecorators": true,
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2021",
|
||||
"target": "es2022",
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"incremental": true,
|
||||
|
@ -30,5 +30,5 @@
|
|||
},
|
||||
"types": ["vitest/globals"]
|
||||
},
|
||||
"exclude": ["dist", "node_modules", "upload"]
|
||||
"exclude": ["dist", "node_modules"]
|
||||
}
|
||||
|
|
|
@ -6,7 +6,7 @@ module.exports = {
|
|||
tsconfigRootDir: __dirname,
|
||||
},
|
||||
plugins: ['@typescript-eslint/eslint-plugin'],
|
||||
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended'],
|
||||
extends: ['plugin:@typescript-eslint/recommended', 'plugin:prettier/recommended', 'plugin:unicorn/recommended'],
|
||||
root: true,
|
||||
env: {
|
||||
node: true,
|
||||
|
@ -19,6 +19,12 @@ module.exports = {
|
|||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/filename-case': 'off',
|
||||
'unicorn/no-null': 'off',
|
||||
'unicorn/prefer-top-level-await': 'off',
|
||||
'unicorn/prefer-event-target': 'off',
|
||||
'unicorn/no-thenable': 'off',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
},
|
||||
|
|
700
server/package-lock.json
generated
700
server/package-lock.json
generated
|
@ -80,9 +80,10 @@
|
|||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||
"@typescript-eslint/parser": "^6.4.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^50.0.1",
|
||||
"jest": "^29.6.4",
|
||||
"jest-when": "^3.6.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
|
@ -97,7 +98,7 @@
|
|||
"ts-loader": "^9.4.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.2.2",
|
||||
"typescript": "^5.3.3",
|
||||
"utimes": "^5.2.1"
|
||||
}
|
||||
},
|
||||
|
@ -3233,6 +3234,12 @@
|
|||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/normalize-package-data": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
|
||||
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/@types/pg": {
|
||||
"version": "8.10.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz",
|
||||
|
@ -4439,9 +4446,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/browserslist": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
|
||||
"integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
|
||||
"version": "4.22.3",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz",
|
||||
"integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -4458,9 +4465,9 @@
|
|||
}
|
||||
],
|
||||
"dependencies": {
|
||||
"caniuse-lite": "^1.0.30001541",
|
||||
"electron-to-chromium": "^1.4.535",
|
||||
"node-releases": "^2.0.13",
|
||||
"caniuse-lite": "^1.0.30001580",
|
||||
"electron-to-chromium": "^1.4.648",
|
||||
"node-releases": "^2.0.14",
|
||||
"update-browserslist-db": "^1.0.13"
|
||||
},
|
||||
"bin": {
|
||||
|
@ -4545,6 +4552,18 @@
|
|||
"node": ">=10.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/builtin-modules": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
|
||||
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/bullmq": {
|
||||
"version": "4.17.0",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.17.0.tgz",
|
||||
|
@ -4664,9 +4683,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/caniuse-lite": {
|
||||
"version": "1.0.30001542",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001542.tgz",
|
||||
"integrity": "sha512-UrtAXVcj1mvPBFQ4sKd38daP8dEcXXr5sQe6QNNinaPd0iA/cxg9/l3VrSdL73jgw5sKyuQ6jNgiKO12W3SsVA==",
|
||||
"version": "1.0.30001581",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz",
|
||||
"integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
|
@ -4791,6 +4810,27 @@
|
|||
"validator": "^13.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/clean-regexp": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
|
||||
"integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"escape-string-regexp": "^1.0.5"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/clean-regexp/node_modules/escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=0.8.0"
|
||||
}
|
||||
},
|
||||
"node_modules/cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
|
@ -5111,6 +5151,19 @@
|
|||
"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",
|
||||
"integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"browserslist": "^4.22.2"
|
||||
},
|
||||
"funding": {
|
||||
"type": "opencollective",
|
||||
"url": "https://opencollective.com/core-js"
|
||||
}
|
||||
},
|
||||
"node_modules/core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
|
@ -5535,9 +5588,9 @@
|
|||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"node_modules/electron-to-chromium": {
|
||||
"version": "1.4.538",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.538.tgz",
|
||||
"integrity": "sha512-1a2m63NEookb1beNFTGDihgF3CKL7ksZ7PSA0VloON5DpTEhnOVgaDes8xkrDhkXRxlcN8JymQDGnv+Nn+uvhg==",
|
||||
"version": "1.4.650",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.650.tgz",
|
||||
"integrity": "sha512-sYSQhJCJa4aGA1wYol5cMQgekDBlbVfTRavlGZVr3WZpDdOPcp6a6xUnFfrt8TqZhsBYYbDxJZCjGfHuGupCRQ==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/emittery": {
|
||||
|
@ -5751,6 +5804,66 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-unicorn": {
|
||||
"version": "50.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz",
|
||||
"integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@eslint/eslintrc": "^2.1.4",
|
||||
"ci-info": "^4.0.0",
|
||||
"clean-regexp": "^1.0.0",
|
||||
"core-js-compat": "^3.34.0",
|
||||
"esquery": "^1.5.0",
|
||||
"indent-string": "^4.0.0",
|
||||
"is-builtin-module": "^3.2.1",
|
||||
"jsesc": "^3.0.2",
|
||||
"pluralize": "^8.0.0",
|
||||
"read-pkg-up": "^7.0.1",
|
||||
"regexp-tree": "^0.1.27",
|
||||
"regjsparser": "^0.10.0",
|
||||
"semver": "^7.5.4",
|
||||
"strip-indent": "^3.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sindresorhus/eslint-plugin-unicorn?sponsor=1"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"eslint": ">=8.56.0"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-unicorn/node_modules/ci-info": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz",
|
||||
"integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==",
|
||||
"dev": true,
|
||||
"funding": [
|
||||
{
|
||||
"type": "github",
|
||||
"url": "https://github.com/sponsors/sibiraj-s"
|
||||
}
|
||||
],
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-plugin-unicorn/node_modules/jsesc": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
|
||||
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/eslint-scope": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||
|
@ -6884,6 +6997,12 @@
|
|||
"node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/hosted-git-info": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
|
@ -7019,6 +7138,15 @@
|
|||
"node": ">=0.8.19"
|
||||
}
|
||||
},
|
||||
"node_modules/indent-string": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
|
@ -7114,6 +7242,21 @@
|
|||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/is-builtin-module": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
|
||||
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"builtin-modules": "^3.3.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/is-core-module": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
|
||||
|
@ -8475,6 +8618,15 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/min-indent": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=4"
|
||||
}
|
||||
},
|
||||
"node_modules/minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
|
@ -8818,9 +8970,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node_modules/node-releases": {
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
|
||||
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
||||
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/nopt": {
|
||||
|
@ -8837,6 +8989,27 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-package-data": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"hosted-git-info": "^2.1.4",
|
||||
"resolve": "^1.10.0",
|
||||
"semver": "2 || 3 || 4 || 5",
|
||||
"validate-npm-package-license": "^3.0.1"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-package-data/node_modules/semver": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"semver": "bin/semver"
|
||||
}
|
||||
},
|
||||
"node_modules/normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
|
@ -9735,6 +9908,108 @@
|
|||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/read-pkg": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
|
||||
"integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"@types/normalize-package-data": "^2.4.0",
|
||||
"normalize-package-data": "^2.5.0",
|
||||
"parse-json": "^5.0.0",
|
||||
"type-fest": "^0.6.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/read-pkg-up": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
|
||||
"integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"find-up": "^4.1.0",
|
||||
"read-pkg": "^5.2.0",
|
||||
"type-fest": "^0.8.1"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/read-pkg-up/node_modules/find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/read-pkg-up/node_modules/locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-locate": "^4.1.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/read-pkg-up/node_modules/p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-try": "^2.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=6"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/sponsors/sindresorhus"
|
||||
}
|
||||
},
|
||||
"node_modules/read-pkg-up/node_modules/p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"p-limit": "^2.2.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/read-pkg-up/node_modules/type-fest": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
|
||||
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/read-pkg/node_modules/type-fest": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
|
||||
"integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
|
||||
"dev": true,
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
|
@ -9838,6 +10113,36 @@
|
|||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
|
||||
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
|
||||
},
|
||||
"node_modules/regexp-tree": {
|
||||
"version": "0.1.27",
|
||||
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
|
||||
"integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"regexp-tree": "bin/regexp-tree"
|
||||
}
|
||||
},
|
||||
"node_modules/regjsparser": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz",
|
||||
"integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"jsesc": "~0.5.0"
|
||||
},
|
||||
"bin": {
|
||||
"regjsparser": "bin/parser"
|
||||
}
|
||||
},
|
||||
"node_modules/regjsparser/node_modules/jsesc": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
|
||||
"integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==",
|
||||
"dev": true,
|
||||
"bin": {
|
||||
"jsesc": "bin/jsesc"
|
||||
}
|
||||
},
|
||||
"node_modules/repeat-string": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
|
||||
|
@ -10465,6 +10770,38 @@
|
|||
"node": ">=0.10.0"
|
||||
}
|
||||
},
|
||||
"node_modules/spdx-correct": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
|
||||
"integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"spdx-expression-parse": "^3.0.0",
|
||||
"spdx-license-ids": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/spdx-exceptions": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz",
|
||||
"integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/spdx-expression-parse": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
|
||||
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"spdx-exceptions": "^2.1.0",
|
||||
"spdx-license-ids": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/spdx-license-ids": {
|
||||
"version": "3.0.16",
|
||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
|
||||
"integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/split-ca": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
|
||||
|
@ -10672,6 +11009,18 @@
|
|||
"node": ">=6"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-indent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"min-indent": "^1.0.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=8"
|
||||
}
|
||||
},
|
||||
"node_modules/strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
|
@ -11901,6 +12250,16 @@
|
|||
"integrity": "sha512-ASFBup0Mz1uyiIjANan1jzLQami9z1PoYSZCiiYW2FczPbenXc45FZdBZLzOT+r6+iciuEModtmCti+hjaAk0A==",
|
||||
"dev": true
|
||||
},
|
||||
"node_modules/validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
|
||||
"dev": true,
|
||||
"dependencies": {
|
||||
"spdx-correct": "^3.0.0",
|
||||
"spdx-expression-parse": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/validator": {
|
||||
"version": "13.11.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
|
||||
|
@ -14439,6 +14798,12 @@
|
|||
"undici-types": "~5.26.4"
|
||||
}
|
||||
},
|
||||
"@types/normalize-package-data": {
|
||||
"version": "2.4.4",
|
||||
"resolved": "https://registry.npmjs.org/@types/normalize-package-data/-/normalize-package-data-2.4.4.tgz",
|
||||
"integrity": "sha512-37i+OaWTh9qeK4LSHPsyRC7NahnGotNuZvjLSgcPzblpHB3rrCJxAOgI5gCdKm7coonsaX1Of0ILiTcnZjbfxA==",
|
||||
"dev": true
|
||||
},
|
||||
"@types/pg": {
|
||||
"version": "8.10.9",
|
||||
"resolved": "https://registry.npmjs.org/@types/pg/-/pg-8.10.9.tgz",
|
||||
|
@ -15402,14 +15767,14 @@
|
|||
}
|
||||
},
|
||||
"browserslist": {
|
||||
"version": "4.22.1",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.1.tgz",
|
||||
"integrity": "sha512-FEVc202+2iuClEhZhrWy6ZiAcRLvNMyYcxZ8raemul1DYVOVdFsbqckWLdsixQZCpJlwe77Z3UTalE7jsjnKfQ==",
|
||||
"version": "4.22.3",
|
||||
"resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.22.3.tgz",
|
||||
"integrity": "sha512-UAp55yfwNv0klWNapjs/ktHoguxuQNGnOzxYmfnXIS+8AsRDZkSDxg7R1AX3GKzn078SBI5dzwzj/Yx0Or0e3A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"caniuse-lite": "^1.0.30001541",
|
||||
"electron-to-chromium": "^1.4.535",
|
||||
"node-releases": "^2.0.13",
|
||||
"caniuse-lite": "^1.0.30001580",
|
||||
"electron-to-chromium": "^1.4.648",
|
||||
"node-releases": "^2.0.14",
|
||||
"update-browserslist-db": "^1.0.13"
|
||||
}
|
||||
},
|
||||
|
@ -15462,6 +15827,12 @@
|
|||
"dev": true,
|
||||
"optional": true
|
||||
},
|
||||
"builtin-modules": {
|
||||
"version": "3.3.0",
|
||||
"resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz",
|
||||
"integrity": "sha512-zhaCDicdLuWN5UbN5IMnFqNMhNfo919sH85y2/ea+5Yg9TsTkeZxpL+JLbp6cgYFS4sRLp3YV4S6yDuqVWHYOw==",
|
||||
"dev": true
|
||||
},
|
||||
"bullmq": {
|
||||
"version": "4.17.0",
|
||||
"resolved": "https://registry.npmjs.org/bullmq/-/bullmq-4.17.0.tgz",
|
||||
|
@ -15553,9 +15924,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"caniuse-lite": {
|
||||
"version": "1.0.30001542",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001542.tgz",
|
||||
"integrity": "sha512-UrtAXVcj1mvPBFQ4sKd38daP8dEcXXr5sQe6QNNinaPd0iA/cxg9/l3VrSdL73jgw5sKyuQ6jNgiKO12W3SsVA==",
|
||||
"version": "1.0.30001581",
|
||||
"resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001581.tgz",
|
||||
"integrity": "sha512-whlTkwhqV2tUmP3oYhtNfaWGYHDdS3JYFQBKXxcUR9qqPWsRhFHhoISO2Xnl/g0xyKzht9mI1LZpiNWfMzHixQ==",
|
||||
"dev": true
|
||||
},
|
||||
"chalk": {
|
||||
|
@ -15631,6 +16002,23 @@
|
|||
"validator": "^13.9.0"
|
||||
}
|
||||
},
|
||||
"clean-regexp": {
|
||||
"version": "1.0.0",
|
||||
"resolved": "https://registry.npmjs.org/clean-regexp/-/clean-regexp-1.0.0.tgz",
|
||||
"integrity": "sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"escape-string-regexp": "^1.0.5"
|
||||
},
|
||||
"dependencies": {
|
||||
"escape-string-regexp": {
|
||||
"version": "1.0.5",
|
||||
"resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz",
|
||||
"integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"cli-cursor": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-3.1.0.tgz",
|
||||
|
@ -15874,6 +16262,15 @@
|
|||
"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",
|
||||
"integrity": "sha512-sftHa5qUJY3rs9Zht1WEnmkvXputCyDBczPnr7QDgL8n3qrF3CMXY4VPSYtOLLiOUJcah2WNXREd48iOl6mQIw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"browserslist": "^4.22.2"
|
||||
}
|
||||
},
|
||||
"core-util-is": {
|
||||
"version": "1.0.3",
|
||||
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
|
||||
|
@ -16185,9 +16582,9 @@
|
|||
"integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow=="
|
||||
},
|
||||
"electron-to-chromium": {
|
||||
"version": "1.4.538",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.538.tgz",
|
||||
"integrity": "sha512-1a2m63NEookb1beNFTGDihgF3CKL7ksZ7PSA0VloON5DpTEhnOVgaDes8xkrDhkXRxlcN8JymQDGnv+Nn+uvhg==",
|
||||
"version": "1.4.650",
|
||||
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.650.tgz",
|
||||
"integrity": "sha512-sYSQhJCJa4aGA1wYol5cMQgekDBlbVfTRavlGZVr3WZpDdOPcp6a6xUnFfrt8TqZhsBYYbDxJZCjGfHuGupCRQ==",
|
||||
"dev": true
|
||||
},
|
||||
"emittery": {
|
||||
|
@ -16369,6 +16766,44 @@
|
|||
"synckit": "^0.8.6"
|
||||
}
|
||||
},
|
||||
"eslint-plugin-unicorn": {
|
||||
"version": "50.0.1",
|
||||
"resolved": "https://registry.npmjs.org/eslint-plugin-unicorn/-/eslint-plugin-unicorn-50.0.1.tgz",
|
||||
"integrity": "sha512-KxenCZxqSYW0GWHH18okDlOQcpezcitm5aOSz6EnobyJ6BIByiPDviQRjJIUAjG/tMN11958MxaQ+qCoU6lfDA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@babel/helper-validator-identifier": "^7.22.20",
|
||||
"@eslint-community/eslint-utils": "^4.4.0",
|
||||
"@eslint/eslintrc": "^2.1.4",
|
||||
"ci-info": "^4.0.0",
|
||||
"clean-regexp": "^1.0.0",
|
||||
"core-js-compat": "^3.34.0",
|
||||
"esquery": "^1.5.0",
|
||||
"indent-string": "^4.0.0",
|
||||
"is-builtin-module": "^3.2.1",
|
||||
"jsesc": "^3.0.2",
|
||||
"pluralize": "^8.0.0",
|
||||
"read-pkg-up": "^7.0.1",
|
||||
"regexp-tree": "^0.1.27",
|
||||
"regjsparser": "^0.10.0",
|
||||
"semver": "^7.5.4",
|
||||
"strip-indent": "^3.0.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"ci-info": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/ci-info/-/ci-info-4.0.0.tgz",
|
||||
"integrity": "sha512-TdHqgGf9odd8SXNuxtUBVx8Nv+qZOejE6qyqiy5NtbYYQOeFa6zmHkxlPzmaLxWWHsU6nJmB7AETdVPi+2NBUg==",
|
||||
"dev": true
|
||||
},
|
||||
"jsesc": {
|
||||
"version": "3.0.2",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz",
|
||||
"integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"eslint-scope": {
|
||||
"version": "7.2.2",
|
||||
"resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-7.2.2.tgz",
|
||||
|
@ -17192,6 +17627,12 @@
|
|||
"resolved": "https://registry.npmjs.org/highlight.js/-/highlight.js-10.7.3.tgz",
|
||||
"integrity": "sha512-tzcUFauisWKNHaRkN4Wjl/ZA07gENAjFl3J/c480dprkGTg5EQstgaNFqBfUqCq54kZRIEcreTsAgF/m2quD7A=="
|
||||
},
|
||||
"hosted-git-info": {
|
||||
"version": "2.8.9",
|
||||
"resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz",
|
||||
"integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==",
|
||||
"dev": true
|
||||
},
|
||||
"html-escaper": {
|
||||
"version": "2.0.2",
|
||||
"resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz",
|
||||
|
@ -17277,6 +17718,12 @@
|
|||
"integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==",
|
||||
"dev": true
|
||||
},
|
||||
"indent-string": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz",
|
||||
"integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==",
|
||||
"dev": true
|
||||
},
|
||||
"inflight": {
|
||||
"version": "1.0.6",
|
||||
"resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz",
|
||||
|
@ -17353,6 +17800,15 @@
|
|||
"binary-extensions": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"is-builtin-module": {
|
||||
"version": "3.2.1",
|
||||
"resolved": "https://registry.npmjs.org/is-builtin-module/-/is-builtin-module-3.2.1.tgz",
|
||||
"integrity": "sha512-BSLE3HnV2syZ0FK0iMA/yUGplUeMmNz4AW5fnTunbCIqZi4vG3WjJT9FHMy5D69xmAYBHXQhJdALdpwVxV501A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"builtin-modules": "^3.3.0"
|
||||
}
|
||||
},
|
||||
"is-core-module": {
|
||||
"version": "2.13.0",
|
||||
"resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.13.0.tgz",
|
||||
|
@ -18401,6 +18857,12 @@
|
|||
"resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz",
|
||||
"integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg=="
|
||||
},
|
||||
"min-indent": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/min-indent/-/min-indent-1.0.1.tgz",
|
||||
"integrity": "sha512-I9jwMn07Sy/IwOj3zVkVik2JTvgpaykDZEigL6Rx6N9LbMywwUSMtxET+7lVoDLLd3O3IXwJwvuuns8UB/HeAg==",
|
||||
"dev": true
|
||||
},
|
||||
"minimatch": {
|
||||
"version": "3.1.2",
|
||||
"resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz",
|
||||
|
@ -18676,9 +19138,9 @@
|
|||
"dev": true
|
||||
},
|
||||
"node-releases": {
|
||||
"version": "2.0.13",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.13.tgz",
|
||||
"integrity": "sha512-uYr7J37ae/ORWdZeQ1xxMJe3NtdmqMC/JZK+geofDrkLUApKRHPd18/TxtBOJ4A0/+uUIliorNrfYV6s1b02eQ==",
|
||||
"version": "2.0.14",
|
||||
"resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz",
|
||||
"integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==",
|
||||
"dev": true
|
||||
},
|
||||
"nopt": {
|
||||
|
@ -18689,6 +19151,26 @@
|
|||
"abbrev": "1"
|
||||
}
|
||||
},
|
||||
"normalize-package-data": {
|
||||
"version": "2.5.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-package-data/-/normalize-package-data-2.5.0.tgz",
|
||||
"integrity": "sha512-/5CMN3T0R4XTj4DcGaexo+roZSdSFW/0AOOTROrjxzCG1wrWXEsGbRKevjlIL+ZDE4sZlJr5ED4YW0yqmkK+eA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"hosted-git-info": "^2.1.4",
|
||||
"resolve": "^1.10.0",
|
||||
"semver": "2 || 3 || 4 || 5",
|
||||
"validate-npm-package-license": "^3.0.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"semver": {
|
||||
"version": "5.7.2",
|
||||
"resolved": "https://registry.npmjs.org/semver/-/semver-5.7.2.tgz",
|
||||
"integrity": "sha512-cBznnQ9KjJqU67B52RMC65CMarK2600WFnbkcaiwWq3xy/5haFJlshgnpjovMVJ+Hff49d8GEn0b87C5pDQ10g==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"normalize-path": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz",
|
||||
|
@ -19338,6 +19820,82 @@
|
|||
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
|
||||
"dev": true
|
||||
},
|
||||
"read-pkg": {
|
||||
"version": "5.2.0",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg/-/read-pkg-5.2.0.tgz",
|
||||
"integrity": "sha512-Ug69mNOpfvKDAc2Q8DRpMjjzdtrnv9HcSMX+4VsZxD1aZ6ZzrIE7rlzXBtWTyhULSMKg076AW6WR5iZpD0JiOg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"@types/normalize-package-data": "^2.4.0",
|
||||
"normalize-package-data": "^2.5.0",
|
||||
"parse-json": "^5.0.0",
|
||||
"type-fest": "^0.6.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"type-fest": {
|
||||
"version": "0.6.0",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.6.0.tgz",
|
||||
"integrity": "sha512-q+MB8nYR1KDLrgr4G5yemftpMC7/QLqVndBmEEdqzmNj5dcFOO4Oo8qlwZE3ULT3+Zim1F8Kq4cBnikNhlCMlg==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"read-pkg-up": {
|
||||
"version": "7.0.1",
|
||||
"resolved": "https://registry.npmjs.org/read-pkg-up/-/read-pkg-up-7.0.1.tgz",
|
||||
"integrity": "sha512-zK0TB7Xd6JpCLmlLmufqykGE+/TlOePD6qKClNW7hHDKFh/J7/7gCWGR7joEQEW1bKq3a3yUZSObOoWLFQ4ohg==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"find-up": "^4.1.0",
|
||||
"read-pkg": "^5.2.0",
|
||||
"type-fest": "^0.8.1"
|
||||
},
|
||||
"dependencies": {
|
||||
"find-up": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz",
|
||||
"integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"locate-path": "^5.0.0",
|
||||
"path-exists": "^4.0.0"
|
||||
}
|
||||
},
|
||||
"locate-path": {
|
||||
"version": "5.0.0",
|
||||
"resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz",
|
||||
"integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-locate": "^4.1.0"
|
||||
}
|
||||
},
|
||||
"p-limit": {
|
||||
"version": "2.3.0",
|
||||
"resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz",
|
||||
"integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-try": "^2.0.0"
|
||||
}
|
||||
},
|
||||
"p-locate": {
|
||||
"version": "4.1.0",
|
||||
"resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz",
|
||||
"integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"p-limit": "^2.2.0"
|
||||
}
|
||||
},
|
||||
"type-fest": {
|
||||
"version": "0.8.1",
|
||||
"resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.8.1.tgz",
|
||||
"integrity": "sha512-4dbzIzqvjtgiM5rw1k5rEHtBANKmdudhGyBEajN01fEyhaAIhsoKNy6y7+IN93IfpFtwY9iqi7kD+xwKhQsNJA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"readable-stream": {
|
||||
"version": "3.6.2",
|
||||
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz",
|
||||
|
@ -19421,6 +19979,29 @@
|
|||
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.0.tgz",
|
||||
"integrity": "sha512-srw17NI0TUWHuGa5CFGGmhfNIeja30WMBfbslPNhf6JrqQlLN5gcrvig1oqPxiVaXb0oW0XRKtH6Nngs5lKCIA=="
|
||||
},
|
||||
"regexp-tree": {
|
||||
"version": "0.1.27",
|
||||
"resolved": "https://registry.npmjs.org/regexp-tree/-/regexp-tree-0.1.27.tgz",
|
||||
"integrity": "sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==",
|
||||
"dev": true
|
||||
},
|
||||
"regjsparser": {
|
||||
"version": "0.10.0",
|
||||
"resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.10.0.tgz",
|
||||
"integrity": "sha512-qx+xQGZVsy55CH0a1hiVwHmqjLryfh7wQyF5HO07XJ9f7dQMY/gPQHhlyDkIzJKC+x2fUCpCcUODUUUFrm7SHA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"jsesc": "~0.5.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"jsesc": {
|
||||
"version": "0.5.0",
|
||||
"resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz",
|
||||
"integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==",
|
||||
"dev": true
|
||||
}
|
||||
}
|
||||
},
|
||||
"repeat-string": {
|
||||
"version": "1.6.1",
|
||||
"resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz",
|
||||
|
@ -19901,6 +20482,38 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"spdx-correct": {
|
||||
"version": "3.2.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-correct/-/spdx-correct-3.2.0.tgz",
|
||||
"integrity": "sha512-kN9dJbvnySHULIluDHy32WHRUu3Og7B9sbY7tsFLctQkIqnMh3hErYgdMjTYuqmcXX+lK5T1lnUt3G7zNswmZA==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"spdx-expression-parse": "^3.0.0",
|
||||
"spdx-license-ids": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"spdx-exceptions": {
|
||||
"version": "2.4.0",
|
||||
"resolved": "https://registry.npmjs.org/spdx-exceptions/-/spdx-exceptions-2.4.0.tgz",
|
||||
"integrity": "sha512-hcjppoJ68fhxA/cjbN4T8N6uCUejN8yFw69ttpqtBeCbF3u13n7mb31NB9jKwGTTWWnt9IbRA/mf1FprYS8wfw==",
|
||||
"dev": true
|
||||
},
|
||||
"spdx-expression-parse": {
|
||||
"version": "3.0.1",
|
||||
"resolved": "https://registry.npmjs.org/spdx-expression-parse/-/spdx-expression-parse-3.0.1.tgz",
|
||||
"integrity": "sha512-cbqHunsQWnJNE6KhVSMsMeH5H/L9EpymbzqTQ3uLwNCLZ1Q481oWaofqH7nO6V07xlXwY6PhQdQ2IedWx/ZK4Q==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"spdx-exceptions": "^2.1.0",
|
||||
"spdx-license-ids": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"spdx-license-ids": {
|
||||
"version": "3.0.16",
|
||||
"resolved": "https://registry.npmjs.org/spdx-license-ids/-/spdx-license-ids-3.0.16.tgz",
|
||||
"integrity": "sha512-eWN+LnM3GR6gPu35WxNgbGl8rmY1AEmoMDvL/QD6zYmPWgywxWqJWNdLGT+ke8dKNWrcYgYjPpG5gbTfghP8rw==",
|
||||
"dev": true
|
||||
},
|
||||
"split-ca": {
|
||||
"version": "1.0.1",
|
||||
"resolved": "https://registry.npmjs.org/split-ca/-/split-ca-1.0.1.tgz",
|
||||
|
@ -20063,6 +20676,15 @@
|
|||
"integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==",
|
||||
"dev": true
|
||||
},
|
||||
"strip-indent": {
|
||||
"version": "3.0.0",
|
||||
"resolved": "https://registry.npmjs.org/strip-indent/-/strip-indent-3.0.0.tgz",
|
||||
"integrity": "sha512-laJTa3Jb+VQpaC6DseHhF7dXVqHTfJPCRDaEbid/drOhgitgYku/letMUqOXFoWV0zIIUbjpdH2t+tYj4bQMRQ==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"min-indent": "^1.0.0"
|
||||
}
|
||||
},
|
||||
"strip-json-comments": {
|
||||
"version": "3.1.1",
|
||||
"resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz",
|
||||
|
@ -20892,6 +21514,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
"validate-npm-package-license": {
|
||||
"version": "3.0.4",
|
||||
"resolved": "https://registry.npmjs.org/validate-npm-package-license/-/validate-npm-package-license-3.0.4.tgz",
|
||||
"integrity": "sha512-DpKm2Ui/xN7/HQKCtpZxoRWBhZ9Z0kqtygG8XCgNQ8ZlDnxuQmWhj566j8fN4Cu3/JmbhsDo7fcAJq4s9h27Ew==",
|
||||
"dev": true,
|
||||
"requires": {
|
||||
"spdx-correct": "^3.0.0",
|
||||
"spdx-expression-parse": "^3.0.0"
|
||||
}
|
||||
},
|
||||
"validator": {
|
||||
"version": "13.11.0",
|
||||
"resolved": "https://registry.npmjs.org/validator/-/validator-13.11.0.tgz",
|
||||
|
|
|
@ -105,9 +105,10 @@
|
|||
"@typescript-eslint/eslint-plugin": "^6.4.1",
|
||||
"@typescript-eslint/parser": "^6.4.1",
|
||||
"dotenv": "^16.3.1",
|
||||
"eslint": "^8.48.0",
|
||||
"eslint-config-prettier": "^9.0.0",
|
||||
"eslint-plugin-prettier": "^5.0.0",
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^50.0.1",
|
||||
"jest": "^29.6.4",
|
||||
"jest-when": "^3.6.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
|
@ -122,7 +123,7 @@
|
|||
"ts-loader": "^9.4.4",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"typescript": "^5.2.2",
|
||||
"typescript": "^5.3.3",
|
||||
"utimes": "^5.2.1"
|
||||
},
|
||||
"jest": {
|
||||
|
|
|
@ -107,42 +107,51 @@ export class AccessCore {
|
|||
const sharedLinkId = sharedLink.id;
|
||||
|
||||
switch (permission) {
|
||||
case Permission.ASSET_READ:
|
||||
case Permission.ASSET_READ: {
|
||||
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
|
||||
}
|
||||
|
||||
case Permission.ASSET_VIEW:
|
||||
case Permission.ASSET_VIEW: {
|
||||
return await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids);
|
||||
}
|
||||
|
||||
case Permission.ASSET_DOWNLOAD:
|
||||
return !!sharedLink.allowDownload
|
||||
case Permission.ASSET_DOWNLOAD: {
|
||||
return sharedLink.allowDownload
|
||||
? await this.repository.asset.checkSharedLinkAccess(sharedLinkId, ids)
|
||||
: new Set();
|
||||
}
|
||||
|
||||
case Permission.ASSET_UPLOAD:
|
||||
case Permission.ASSET_UPLOAD: {
|
||||
return sharedLink.allowUpload ? ids : new Set();
|
||||
}
|
||||
|
||||
case Permission.ASSET_SHARE:
|
||||
case Permission.ASSET_SHARE: {
|
||||
// TODO: fix this to not use sharedLink.userId for access control
|
||||
return await this.repository.asset.checkOwnerAccess(sharedLink.userId, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_READ:
|
||||
case Permission.ALBUM_READ: {
|
||||
return await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_DOWNLOAD:
|
||||
return !!sharedLink.allowDownload
|
||||
case Permission.ALBUM_DOWNLOAD: {
|
||||
return sharedLink.allowDownload
|
||||
? await this.repository.album.checkSharedLinkAccess(sharedLinkId, ids)
|
||||
: new Set();
|
||||
}
|
||||
|
||||
default:
|
||||
default: {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async checkAccessOther(auth: AuthDto, permission: Permission, ids: Set<string>) {
|
||||
switch (permission) {
|
||||
// uses album id
|
||||
case Permission.ACTIVITY_CREATE:
|
||||
case Permission.ACTIVITY_CREATE: {
|
||||
return await this.repository.activity.checkCreateAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
// uses activity id
|
||||
case Permission.ACTIVITY_DELETE: {
|
||||
|
@ -190,14 +199,17 @@ export class AccessCore {
|
|||
return setUnion(isOwner, isAlbum, isPartner);
|
||||
}
|
||||
|
||||
case Permission.ASSET_UPDATE:
|
||||
case Permission.ASSET_UPDATE: {
|
||||
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ASSET_DELETE:
|
||||
case Permission.ASSET_DELETE: {
|
||||
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ASSET_RESTORE:
|
||||
case Permission.ASSET_RESTORE: {
|
||||
return await this.repository.asset.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_READ: {
|
||||
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
||||
|
@ -205,14 +217,17 @@ export class AccessCore {
|
|||
return setUnion(isOwner, isShared);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_UPDATE:
|
||||
case Permission.ALBUM_UPDATE: {
|
||||
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_DELETE:
|
||||
case Permission.ALBUM_DELETE: {
|
||||
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_SHARE:
|
||||
case Permission.ALBUM_SHARE: {
|
||||
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_DOWNLOAD: {
|
||||
const isOwner = await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
||||
|
@ -220,17 +235,21 @@ export class AccessCore {
|
|||
return setUnion(isOwner, isShared);
|
||||
}
|
||||
|
||||
case Permission.ALBUM_REMOVE_ASSET:
|
||||
case Permission.ALBUM_REMOVE_ASSET: {
|
||||
return await this.repository.album.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ASSET_UPLOAD:
|
||||
case Permission.ASSET_UPLOAD: {
|
||||
return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.ARCHIVE_READ:
|
||||
case Permission.ARCHIVE_READ: {
|
||||
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
|
||||
}
|
||||
|
||||
case Permission.AUTH_DEVICE_DELETE:
|
||||
case Permission.AUTH_DEVICE_DELETE: {
|
||||
return await this.repository.authDevice.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.TIMELINE_READ: {
|
||||
const isOwner = ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
|
||||
|
@ -238,8 +257,9 @@ export class AccessCore {
|
|||
return setUnion(isOwner, isPartner);
|
||||
}
|
||||
|
||||
case Permission.TIMELINE_DOWNLOAD:
|
||||
case Permission.TIMELINE_DOWNLOAD: {
|
||||
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set();
|
||||
}
|
||||
|
||||
case Permission.LIBRARY_READ: {
|
||||
const isOwner = await this.repository.library.checkOwnerAccess(auth.user.id, ids);
|
||||
|
@ -247,32 +267,41 @@ export class AccessCore {
|
|||
return setUnion(isOwner, isPartner);
|
||||
}
|
||||
|
||||
case Permission.LIBRARY_UPDATE:
|
||||
case Permission.LIBRARY_UPDATE: {
|
||||
return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.LIBRARY_DELETE:
|
||||
case Permission.LIBRARY_DELETE: {
|
||||
return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_READ:
|
||||
case Permission.PERSON_READ: {
|
||||
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_WRITE:
|
||||
case Permission.PERSON_WRITE: {
|
||||
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_MERGE:
|
||||
case Permission.PERSON_MERGE: {
|
||||
return await this.repository.person.checkOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_CREATE:
|
||||
case Permission.PERSON_CREATE: {
|
||||
return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PERSON_REASSIGN:
|
||||
case Permission.PERSON_REASSIGN: {
|
||||
return this.repository.person.checkFaceOwnerAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
case Permission.PARTNER_UPDATE:
|
||||
case Permission.PARTNER_UPDATE: {
|
||||
return await this.repository.partner.checkUpdateAccess(auth.user.id, ids);
|
||||
}
|
||||
|
||||
default:
|
||||
default: {
|
||||
return new Set();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -35,7 +35,7 @@ export class ActivityService {
|
|||
isLiked: dto.type && dto.type === ReactionType.LIKE,
|
||||
});
|
||||
|
||||
return activities.map(mapActivity);
|
||||
return activities.map((activity) => mapActivity(activity));
|
||||
}
|
||||
|
||||
async getStatistics(auth: AuthDto, dto: ActivityDto): Promise<ActivityStatisticsResponseDto> {
|
||||
|
|
|
@ -27,10 +27,11 @@ export class AlbumResponseDto {
|
|||
export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumResponseDto => {
|
||||
const sharedUsers: UserResponseDto[] = [];
|
||||
|
||||
entity.sharedUsers?.forEach((user) => {
|
||||
const userDto = mapUser(user);
|
||||
sharedUsers.push(userDto);
|
||||
});
|
||||
if (entity.sharedUsers) {
|
||||
for (const user of entity.sharedUsers) {
|
||||
sharedUsers.push(mapUser(user));
|
||||
}
|
||||
}
|
||||
|
||||
const assets = entity.assets || [];
|
||||
|
||||
|
@ -41,9 +42,7 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean): AlbumRespons
|
|||
let endDate = assets.at(-1)?.fileCreatedAt || undefined;
|
||||
// Swap dates if start date is greater than end date.
|
||||
if (startDate && endDate && startDate > endDate) {
|
||||
const temp = startDate;
|
||||
startDate = endDate;
|
||||
endDate = temp;
|
||||
[startDate, endDate] = [endDate, startDate];
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -69,19 +69,17 @@ export class AlbumService {
|
|||
|
||||
// Get asset count for each album. Then map the result to an object:
|
||||
// { [albumId]: assetCount }
|
||||
const albumMetadataForIds = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id));
|
||||
const albumMetadataForIdsObj: Record<string, AlbumAssetCount> = albumMetadataForIds.reduce(
|
||||
(obj: Record<string, AlbumAssetCount>, { albumId, assetCount, startDate, endDate }) => {
|
||||
obj[albumId] = {
|
||||
albumId,
|
||||
assetCount,
|
||||
startDate,
|
||||
endDate,
|
||||
};
|
||||
return obj;
|
||||
},
|
||||
{},
|
||||
);
|
||||
const results = await this.albumRepository.getMetadataForIds(albums.map((album) => album.id));
|
||||
const albumMetadata: Record<string, AlbumAssetCount> = {};
|
||||
for (const metadata of results) {
|
||||
const { albumId, assetCount, startDate, endDate } = metadata;
|
||||
albumMetadata[albumId] = {
|
||||
albumId,
|
||||
assetCount,
|
||||
startDate,
|
||||
endDate,
|
||||
};
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
albums.map(async (album) => {
|
||||
|
@ -89,9 +87,9 @@ export class AlbumService {
|
|||
return {
|
||||
...mapAlbumWithoutAssets(album),
|
||||
sharedLinks: undefined,
|
||||
startDate: albumMetadataForIdsObj[album.id].startDate,
|
||||
endDate: albumMetadataForIdsObj[album.id].endDate,
|
||||
assetCount: albumMetadataForIdsObj[album.id].assetCount,
|
||||
startDate: albumMetadata[album.id].startDate,
|
||||
endDate: albumMetadata[album.id].endDate,
|
||||
assetCount: albumMetadata[album.id].assetCount,
|
||||
lastModifiedAssetTimestamp: lastModifiedAsset?.fileModifiedAt,
|
||||
};
|
||||
}),
|
||||
|
|
|
@ -12,7 +12,7 @@ export class APIKeyService {
|
|||
) {}
|
||||
|
||||
async create(auth: AuthDto, dto: APIKeyCreateDto): Promise<APIKeyCreateResponseDto> {
|
||||
const secret = this.crypto.randomBytes(32).toString('base64').replace(/\W/g, '');
|
||||
const secret = this.crypto.randomBytes(32).toString('base64').replaceAll(/\W/g, '');
|
||||
const entity = await this.repository.create({
|
||||
key: this.crypto.hashSha256(secret),
|
||||
name: dto.name || 'API Key',
|
||||
|
|
|
@ -1009,9 +1009,7 @@ describe(AssetService.name, () => {
|
|||
it('get assets by device id', async () => {
|
||||
const assets = [assetStub.image, assetStub.image1];
|
||||
|
||||
assetMock.getAllByDeviceId.mockImplementation(() =>
|
||||
Promise.resolve<string[]>(Array.from(assets.map((asset) => asset.deviceAssetId))),
|
||||
);
|
||||
assetMock.getAllByDeviceId.mockResolvedValue(assets.map((asset) => asset.deviceAssetId));
|
||||
|
||||
const deviceId = 'device-id';
|
||||
const result = await sut.getUserAssetsByDeviceId(authStub.user1, deviceId);
|
||||
|
|
|
@ -3,7 +3,7 @@ import { ImmichLogger } from '@app/infra/logger';
|
|||
import { BadRequestException, Inject } from '@nestjs/common';
|
||||
import _ from 'lodash';
|
||||
import { DateTime, Duration } from 'luxon';
|
||||
import { extname } from 'path';
|
||||
import { extname } from 'node:path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { AccessCore, Permission } from '../access';
|
||||
import { AuthDto } from '../auth';
|
||||
|
@ -93,7 +93,7 @@ export class AssetService {
|
|||
}
|
||||
|
||||
search(auth: AuthDto, dto: AssetSearchDto) {
|
||||
let checksum: Buffer | undefined = undefined;
|
||||
let checksum: Buffer | undefined;
|
||||
|
||||
if (dto.checksum) {
|
||||
const encoding = dto.checksum.length === 28 ? 'base64' : 'hex';
|
||||
|
@ -126,29 +126,33 @@ export class AssetService {
|
|||
const filename = file.originalName;
|
||||
|
||||
switch (fieldName) {
|
||||
case UploadFieldName.ASSET_DATA:
|
||||
case UploadFieldName.ASSET_DATA: {
|
||||
if (mimeTypes.isAsset(filename)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case UploadFieldName.LIVE_PHOTO_DATA:
|
||||
case UploadFieldName.LIVE_PHOTO_DATA: {
|
||||
if (mimeTypes.isVideo(filename)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case UploadFieldName.SIDECAR_DATA:
|
||||
case UploadFieldName.SIDECAR_DATA: {
|
||||
if (mimeTypes.isSidecar(filename)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case UploadFieldName.PROFILE_DATA:
|
||||
case UploadFieldName.PROFILE_DATA: {
|
||||
if (mimeTypes.isProfile(filename)) {
|
||||
return true;
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
this.logger.error(`Unsupported file type ${filename}`);
|
||||
|
@ -158,13 +162,13 @@ export class AssetService {
|
|||
getUploadFilename({ auth, fieldName, file }: UploadRequest): string {
|
||||
this.access.requireUploadAccess(auth);
|
||||
|
||||
const originalExt = extname(file.originalName);
|
||||
const originalExtension = extname(file.originalName);
|
||||
|
||||
const lookup = {
|
||||
[UploadFieldName.ASSET_DATA]: originalExt,
|
||||
[UploadFieldName.ASSET_DATA]: originalExtension,
|
||||
[UploadFieldName.LIVE_PHOTO_DATA]: '.mov',
|
||||
[UploadFieldName.SIDECAR_DATA]: '.xmp',
|
||||
[UploadFieldName.PROFILE_DATA]: originalExt,
|
||||
[UploadFieldName.PROFILE_DATA]: originalExtension,
|
||||
};
|
||||
|
||||
return sanitize(`${file.uuid}${lookup[fieldName]}`);
|
||||
|
@ -247,11 +251,9 @@ export class AssetService {
|
|||
await this.timeBucketChecks(auth, dto);
|
||||
const timeBucketOptions = await this.buildTimeBucketOptions(auth, dto);
|
||||
const assets = await this.assetRepository.getTimeBucket(dto.timeBucket, timeBucketOptions);
|
||||
if (!auth.sharedLink || auth.sharedLink?.showExif) {
|
||||
return assets.map((asset) => mapAsset(asset, { withStack: true }));
|
||||
} else {
|
||||
return assets.map((asset) => mapAsset(asset, { stripMetadata: true }));
|
||||
}
|
||||
return !auth.sharedLink || auth.sharedLink?.showExif
|
||||
? assets.map((asset) => mapAsset(asset, { withStack: true }))
|
||||
: assets.map((asset) => mapAsset(asset, { stripMetadata: true }));
|
||||
}
|
||||
|
||||
async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
|
||||
|
@ -371,14 +373,14 @@ export class AssetService {
|
|||
const assetsWithChildren = assets.filter((a) => a.stack && a.stack.assets.length > 0);
|
||||
ids.push(...assetsWithChildren.flatMap((child) => child.stack!.assets.map((gChild) => gChild.id)));
|
||||
|
||||
if (!stack) {
|
||||
stack = await this.assetStackRepository.create({
|
||||
if (stack) {
|
||||
await this.assetStackRepository.update({
|
||||
id: stack.id,
|
||||
primaryAssetId: primaryAsset.id,
|
||||
assets: ids.map((id) => ({ id }) as AssetEntity),
|
||||
});
|
||||
} else {
|
||||
await this.assetStackRepository.update({
|
||||
id: stack.id,
|
||||
stack = await this.assetStackRepository.create({
|
||||
primaryAssetId: primaryAsset.id,
|
||||
assets: ids.map((id) => ({ id }) as AssetEntity),
|
||||
});
|
||||
|
@ -394,9 +396,10 @@ export class AssetService {
|
|||
}
|
||||
|
||||
await this.assetRepository.updateAll(ids, options);
|
||||
const stacksToDelete = (
|
||||
await Promise.all(stackIdsToCheckForDelete.map((id) => this.assetStackRepository.getById(id)))
|
||||
)
|
||||
const stackIdsToDelete = await Promise.all(
|
||||
stackIdsToCheckForDelete.map((id) => this.assetStackRepository.getById(id)),
|
||||
);
|
||||
const stacksToDelete = stackIdsToDelete
|
||||
.flatMap((stack) => (stack ? [stack] : []))
|
||||
.filter((stack) => stack.assets.length < 2);
|
||||
await Promise.all(stacksToDelete.map((as) => this.assetStackRepository.delete(as.id)));
|
||||
|
@ -510,9 +513,8 @@ export class AssetService {
|
|||
throw new Error('Asset not found or not in a stack');
|
||||
}
|
||||
if (oldParent != null) {
|
||||
childIds.push(oldParent.id);
|
||||
// Get all children of old parent
|
||||
childIds.push(...(oldParent.stack?.assets.map((a) => a.id) ?? []));
|
||||
childIds.push(oldParent.id, ...(oldParent.stack?.assets.map((a) => a.id) ?? []));
|
||||
}
|
||||
await this.assetStackRepository.update({
|
||||
id: oldParent.stackId,
|
||||
|
@ -530,17 +532,20 @@ export class AssetService {
|
|||
|
||||
for (const id of dto.assetIds) {
|
||||
switch (dto.name) {
|
||||
case AssetJobName.REFRESH_METADATA:
|
||||
case AssetJobName.REFRESH_METADATA: {
|
||||
jobs.push({ name: JobName.METADATA_EXTRACTION, data: { id } });
|
||||
break;
|
||||
}
|
||||
|
||||
case AssetJobName.REGENERATE_THUMBNAIL:
|
||||
case AssetJobName.REGENERATE_THUMBNAIL: {
|
||||
jobs.push({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: { id } });
|
||||
break;
|
||||
}
|
||||
|
||||
case AssetJobName.TRANSCODE_VIDEO:
|
||||
case AssetJobName.TRANSCODE_VIDEO: {
|
||||
jobs.push({ name: JobName.VIDEO_CONVERSION, data: { id } });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -26,7 +26,6 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
|||
libraryId!: string;
|
||||
originalPath!: string;
|
||||
originalFileName!: string;
|
||||
resized!: boolean;
|
||||
fileCreatedAt!: Date;
|
||||
fileModifiedAt!: Date;
|
||||
updatedAt!: Date;
|
||||
|
@ -56,7 +55,7 @@ export type AssetMapOptions = {
|
|||
const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[] => {
|
||||
const result: PersonWithFacesResponseDto[] = [];
|
||||
if (faces) {
|
||||
faces.forEach((face) => {
|
||||
for (const face of faces) {
|
||||
if (face.person) {
|
||||
const existingPersonEntry = result.find((item) => item.id === face.person!.id);
|
||||
if (existingPersonEntry) {
|
||||
|
@ -65,7 +64,7 @@ const peopleWithFaces = (faces: AssetFaceEntity[]): PersonWithFacesResponseDto[]
|
|||
result.push({ ...mapPerson(face.person!), faces: [mapFacesWithoutPerson(face)] });
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return result;
|
||||
|
|
|
@ -33,7 +33,7 @@ export function mapExif(entity: ExifEntity): ExifResponseDto {
|
|||
model: entity.model,
|
||||
exifImageWidth: entity.exifImageWidth,
|
||||
exifImageHeight: entity.exifImageHeight,
|
||||
fileSizeInByte: entity.fileSizeInByte ? parseInt(entity.fileSizeInByte.toString()) : null,
|
||||
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
|
||||
orientation: entity.orientation,
|
||||
dateTimeOriginal: entity.dateTimeOriginal,
|
||||
modifyDate: entity.modifyDate,
|
||||
|
@ -55,7 +55,7 @@ export function mapExif(entity: ExifEntity): ExifResponseDto {
|
|||
|
||||
export function mapSanitizedExif(entity: ExifEntity): ExifResponseDto {
|
||||
return {
|
||||
fileSizeInByte: entity.fileSizeInByte ? parseInt(entity.fileSizeInByte.toString()) : null,
|
||||
fileSizeInByte: entity.fileSizeInByte ? Number.parseInt(entity.fileSizeInByte.toString()) : null,
|
||||
orientation: entity.orientation,
|
||||
dateTimeOriginal: entity.dateTimeOriginal,
|
||||
timeZone: entity.timeZone,
|
||||
|
|
|
@ -91,40 +91,50 @@ export class AuditService {
|
|||
}
|
||||
|
||||
switch (pathType) {
|
||||
case AssetPathType.ENCODED_VIDEO:
|
||||
case AssetPathType.ENCODED_VIDEO: {
|
||||
await this.assetRepository.save({ id, encodedVideoPath: pathValue });
|
||||
break;
|
||||
}
|
||||
|
||||
case AssetPathType.JPEG_THUMBNAIL:
|
||||
case AssetPathType.JPEG_THUMBNAIL: {
|
||||
await this.assetRepository.save({ id, resizePath: pathValue });
|
||||
break;
|
||||
}
|
||||
|
||||
case AssetPathType.WEBP_THUMBNAIL:
|
||||
case AssetPathType.WEBP_THUMBNAIL: {
|
||||
await this.assetRepository.save({ id, webpPath: pathValue });
|
||||
break;
|
||||
}
|
||||
|
||||
case AssetPathType.ORIGINAL:
|
||||
case AssetPathType.ORIGINAL: {
|
||||
await this.assetRepository.save({ id, originalPath: pathValue });
|
||||
break;
|
||||
}
|
||||
|
||||
case AssetPathType.SIDECAR:
|
||||
case AssetPathType.SIDECAR: {
|
||||
await this.assetRepository.save({ id, sidecarPath: pathValue });
|
||||
break;
|
||||
}
|
||||
|
||||
case PersonPathType.FACE:
|
||||
case PersonPathType.FACE: {
|
||||
await this.personRepository.update({ id, thumbnailPath: pathValue });
|
||||
break;
|
||||
}
|
||||
|
||||
case UserPathType.PROFILE:
|
||||
case UserPathType.PROFILE: {
|
||||
await this.userRepository.update(id, { profileImagePath: pathValue });
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fullPath(filename: string) {
|
||||
return resolve(filename);
|
||||
}
|
||||
|
||||
async getFileReport() {
|
||||
const fullPath = (filename: string) => resolve(filename);
|
||||
const hasFile = (items: Set<string>, filename: string) => items.has(filename) || items.has(fullPath(filename));
|
||||
const hasFile = (items: Set<string>, filename: string) => items.has(filename) || items.has(this.fullPath(filename));
|
||||
const crawl = async (folder: StorageFolder) =>
|
||||
new Set(
|
||||
await this.storageRepository.crawl({
|
||||
|
@ -150,7 +160,7 @@ export class AuditService {
|
|||
return;
|
||||
}
|
||||
allFiles.delete(filename);
|
||||
allFiles.delete(fullPath(filename));
|
||||
allFiles.delete(this.fullPath(filename));
|
||||
};
|
||||
|
||||
this.logger.log(
|
||||
|
@ -226,7 +236,7 @@ export class AuditService {
|
|||
|
||||
// send as absolute paths
|
||||
for (const orphan of orphans) {
|
||||
orphan.pathValue = fullPath(orphan.pathValue);
|
||||
orphan.pathValue = this.fullPath(orphan.pathValue);
|
||||
}
|
||||
|
||||
return { orphans, extras };
|
||||
|
|
|
@ -18,7 +18,7 @@ import {
|
|||
userStub,
|
||||
userTokenStub,
|
||||
} from '@test';
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
import { IncomingHttpHeaders } from 'node:http';
|
||||
import { Issuer, generators } from 'openid-client';
|
||||
import { Socket } from 'socket.io';
|
||||
import {
|
||||
|
|
|
@ -8,8 +8,8 @@ import {
|
|||
UnauthorizedException,
|
||||
} from '@nestjs/common';
|
||||
import cookieParser from 'cookie';
|
||||
import { IncomingHttpHeaders } from 'http';
|
||||
import { DateTime } from 'luxon';
|
||||
import { IncomingHttpHeaders } from 'node:http';
|
||||
import { ClientMetadata, Issuer, UserinfoResponse, custom, generators } from 'openid-client';
|
||||
import { AccessCore, Permission } from '../access';
|
||||
import {
|
||||
|
@ -85,7 +85,7 @@ export class AuthService {
|
|||
this.configCore = SystemConfigCore.create(configRepository);
|
||||
this.userCore = UserCore.create(cryptoRepository, libraryRepository, userRepository);
|
||||
|
||||
custom.setHttpOptionsDefaults({ timeout: 30000 });
|
||||
custom.setHttpOptionsDefaults({ timeout: 30_000 });
|
||||
}
|
||||
|
||||
async login(dto: LoginCredentialDto, details: LoginDetails): Promise<LoginResponse> {
|
||||
|
@ -213,7 +213,8 @@ export class AuthService {
|
|||
}
|
||||
|
||||
const { scope, buttonText, autoLaunch } = config.oauth;
|
||||
const url = (await this.getOAuthClient(config)).authorizationUrl({
|
||||
const oauthClient = await this.getOAuthClient(config);
|
||||
const url = oauthClient.authorizationUrl({
|
||||
redirect_uri: this.normalize(config, dto.redirectUri),
|
||||
scope,
|
||||
state: generators.state(),
|
||||
|
@ -376,12 +377,10 @@ export class AuthService {
|
|||
|
||||
const bytes = Buffer.from(key, key.length === 100 ? 'hex' : 'base64url');
|
||||
const sharedLink = await this.sharedLinkRepository.getByKey(bytes);
|
||||
if (sharedLink) {
|
||||
if (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date()) {
|
||||
const user = sharedLink.user;
|
||||
if (user) {
|
||||
return { user, sharedLink };
|
||||
}
|
||||
if (sharedLink && (!sharedLink.expiresAt || new Date(sharedLink.expiresAt) > new Date())) {
|
||||
const user = sharedLink.user;
|
||||
if (user) {
|
||||
return { user, sharedLink };
|
||||
}
|
||||
}
|
||||
throw new UnauthorizedException('Invalid share key');
|
||||
|
@ -423,7 +422,7 @@ export class AuthService {
|
|||
}
|
||||
|
||||
private async createLoginResponse(user: UserEntity, authType: AuthType, loginDetails: LoginDetails) {
|
||||
const key = this.cryptoRepository.randomBytes(32).toString('base64').replace(/\W/g, '');
|
||||
const key = this.cryptoRepository.randomBytes(32).toString('base64').replaceAll(/\W/g, '');
|
||||
const token = this.cryptoRepository.hashSha256(key);
|
||||
|
||||
await this.userTokenRepository.create({
|
||||
|
|
|
@ -50,14 +50,14 @@ export class DatabaseService {
|
|||
}
|
||||
|
||||
private async createVectors() {
|
||||
await this.databaseRepository.createExtension(DatabaseExtension.VECTORS).catch(async (err: QueryFailedError) => {
|
||||
await this.databaseRepository.createExtension(DatabaseExtension.VECTORS).catch(async (error: QueryFailedError) => {
|
||||
const image = await this.getVectorsImage();
|
||||
this.logger.fatal(`
|
||||
Failed to create pgvecto.rs extension.
|
||||
If you have not updated your Postgres instance to a docker image that supports pgvecto.rs (such as '${image}'), please do so.
|
||||
See the v1.91.0 release notes for more info: https://github.com/immich-app/immich/releases/tag/v1.91.0'
|
||||
`);
|
||||
throw err;
|
||||
throw error;
|
||||
});
|
||||
}
|
||||
|
||||
|
|
|
@ -108,9 +108,9 @@ describe('mimeTypes', () => {
|
|||
expect(keys).toEqual([...keys].sort());
|
||||
});
|
||||
|
||||
for (const [ext, v] of Object.entries(mimeTypes.profile)) {
|
||||
it(`should lookup ${ext}`, () => {
|
||||
expect(mimeTypes.lookup(`test.${ext}`)).toEqual(v[0]);
|
||||
for (const [extension, v] of Object.entries(mimeTypes.profile)) {
|
||||
it(`should lookup ${extension}`, () => {
|
||||
expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -135,9 +135,9 @@ describe('mimeTypes', () => {
|
|||
expect(values).toEqual(values.filter((mimeType) => mimeType.startsWith('image/')));
|
||||
});
|
||||
|
||||
for (const [ext, v] of Object.entries(mimeTypes.image)) {
|
||||
it(`should lookup ${ext}`, () => {
|
||||
expect(mimeTypes.lookup(`test.${ext}`)).toEqual(v[0]);
|
||||
for (const [extension, v] of Object.entries(mimeTypes.image)) {
|
||||
it(`should lookup ${extension}`, () => {
|
||||
expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -162,9 +162,9 @@ describe('mimeTypes', () => {
|
|||
expect(values).toEqual(values.filter((mimeType) => mimeType.startsWith('video/')));
|
||||
});
|
||||
|
||||
for (const [ext, v] of Object.entries(mimeTypes.video)) {
|
||||
it(`should lookup ${ext}`, () => {
|
||||
expect(mimeTypes.lookup(`test.${ext}`)).toEqual(v[0]);
|
||||
for (const [extension, v] of Object.entries(mimeTypes.video)) {
|
||||
it(`should lookup ${extension}`, () => {
|
||||
expect(mimeTypes.lookup(`test.${extension}`)).toEqual(v[0]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
@ -188,9 +188,9 @@ describe('mimeTypes', () => {
|
|||
expect(Object.values(mimeTypes.sidecar).flat()).toEqual(['application/xml', 'text/xml']);
|
||||
});
|
||||
|
||||
for (const [ext, v] of Object.entries(mimeTypes.sidecar)) {
|
||||
it(`should lookup ${ext}`, () => {
|
||||
expect(mimeTypes.lookup(`it.${ext}`)).toEqual(v[0]);
|
||||
for (const [extension, v] of Object.entries(mimeTypes.sidecar)) {
|
||||
it(`should lookup ${extension}`, () => {
|
||||
expect(mimeTypes.lookup(`it.${extension}`)).toEqual(v[0]);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
|
@ -3,8 +3,6 @@ import { Duration } from 'luxon';
|
|||
import { readFileSync } from 'node:fs';
|
||||
import { extname, join } from 'node:path';
|
||||
|
||||
const pkg = JSON.parse(readFileSync('./package.json', 'utf-8'));
|
||||
|
||||
export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
|
||||
export const ONE_HOUR = Duration.fromObject({ hours: 1 });
|
||||
|
||||
|
@ -31,7 +29,7 @@ export class Version implements IVersion {
|
|||
}
|
||||
|
||||
static fromString(version: string): Version {
|
||||
const regex = /(?:v)?(?<major>\d+)(?:\.(?<minor>\d+))?(?:[\.-](?<patch>\d+))?/i;
|
||||
const regex = /v?(?<major>\d+)(?:\.(?<minor>\d+))?(?:[.-](?<patch>\d+))?/i;
|
||||
const matchResult = version.match(regex);
|
||||
if (matchResult) {
|
||||
const { major, minor = '0', patch = '0' } = matchResult.groups as { [K in keyof IVersion]: string };
|
||||
|
@ -68,7 +66,8 @@ export class Version implements IVersion {
|
|||
export const envName = (process.env.NODE_ENV || 'development').toUpperCase();
|
||||
export const isDev = process.env.NODE_ENV === 'development';
|
||||
|
||||
export const serverVersion = Version.fromString(pkg.version);
|
||||
const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
export const serverVersion = Version.fromString(version);
|
||||
|
||||
export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
|
||||
|
||||
|
@ -129,9 +128,9 @@ const image: Record<string, string[]> = {
|
|||
'.x3f': ['image/x3f', 'image/x-sigma-x3f'],
|
||||
};
|
||||
|
||||
const profileExtensions = ['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp'];
|
||||
const profileExtensions = new Set(['.avif', '.dng', '.heic', '.heif', '.jpeg', '.jpg', '.png', '.webp']);
|
||||
const profile: Record<string, string[]> = Object.fromEntries(
|
||||
Object.entries(image).filter(([key]) => profileExtensions.includes(key)),
|
||||
Object.entries(image).filter(([key]) => profileExtensions.has(key)),
|
||||
);
|
||||
|
||||
const video: Record<string, string[]> = {
|
||||
|
@ -180,5 +179,5 @@ export const mimeTypes = {
|
|||
}
|
||||
return AssetType.OTHER;
|
||||
},
|
||||
getSupportedFileExtensions: () => Object.keys(image).concat(Object.keys(video)),
|
||||
getSupportedFileExtensions: () => [...Object.keys(image), ...Object.keys(video)],
|
||||
};
|
||||
|
|
|
@ -46,7 +46,8 @@ export type Options = {
|
|||
|
||||
export const isConnectionAborted = (error: Error | any) => error.code === 'ECONNABORTED';
|
||||
|
||||
export function ValidateUUID({ optional, each }: Options = { optional: false, each: false }) {
|
||||
export function ValidateUUID(options?: Options) {
|
||||
const { optional, each } = { optional: false, each: false, ...options };
|
||||
return applyDecorators(
|
||||
IsUUID('4', { each }),
|
||||
ApiProperty({ format: 'uuid' }),
|
||||
|
@ -58,7 +59,7 @@ export function ValidateUUID({ optional, each }: Options = { optional: false, ea
|
|||
export function validateCronExpression(expression: string) {
|
||||
try {
|
||||
new CronJob(expression, () => {});
|
||||
} catch (error) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -96,7 +97,7 @@ export const toBoolean = ({ value }: IValue) => {
|
|||
|
||||
export const toEmail = ({ value }: IValue) => value?.toLowerCase();
|
||||
|
||||
export const toSanitized = ({ value }: IValue) => sanitize((value || '').replace(/\./g, ''));
|
||||
export const toSanitized = ({ value }: IValue) => sanitize((value || '').replaceAll('.', ''));
|
||||
|
||||
export function getFileNameWithoutExtension(path: string): string {
|
||||
return basename(path, extname(path));
|
||||
|
@ -173,7 +174,7 @@ export function Optional({ nullable, ...validationOptions }: OptionalOptions = {
|
|||
return IsOptional(validationOptions);
|
||||
}
|
||||
|
||||
return ValidateIf((obj: any, v: any) => v !== undefined, validationOptions);
|
||||
return ValidateIf((object: any, v: any) => v !== undefined, validationOptions);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -186,8 +187,8 @@ export function chunks<T>(collection: Array<T> | Set<T>, size: number): T[][] {
|
|||
if (collection instanceof Set) {
|
||||
const result = [];
|
||||
let chunk = [];
|
||||
for (const elem of collection) {
|
||||
chunk.push(elem);
|
||||
for (const element of collection) {
|
||||
chunk.push(element);
|
||||
if (chunk.length === size) {
|
||||
result.push(chunk);
|
||||
chunk = [];
|
||||
|
@ -209,8 +210,8 @@ export function chunks<T>(collection: Array<T> | Set<T>, size: number): T[][] {
|
|||
export const setUnion = <T>(...sets: Set<T>[]): Set<T> => {
|
||||
const union = new Set(sets[0]);
|
||||
for (const set of sets.slice(1)) {
|
||||
for (const elem of set) {
|
||||
union.add(elem);
|
||||
for (const element of set) {
|
||||
union.add(element);
|
||||
}
|
||||
}
|
||||
return union;
|
||||
|
@ -219,16 +220,16 @@ export const setUnion = <T>(...sets: Set<T>[]): Set<T> => {
|
|||
export const setDifference = <T>(setA: Set<T>, ...sets: Set<T>[]): Set<T> => {
|
||||
const difference = new Set(setA);
|
||||
for (const set of sets) {
|
||||
for (const elem of set) {
|
||||
difference.delete(elem);
|
||||
for (const element of set) {
|
||||
difference.delete(element);
|
||||
}
|
||||
}
|
||||
return difference;
|
||||
};
|
||||
|
||||
export const setIsSuperset = <T>(set: Set<T>, subset: Set<T>): boolean => {
|
||||
for (const elem of subset) {
|
||||
if (!set.has(elem)) {
|
||||
for (const element of subset) {
|
||||
if (!set.has(element)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { AssetEntity } from '@app/infra/entities';
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { extname } from 'path';
|
||||
import { extname } from 'node:path';
|
||||
import { AccessCore, Permission } from '../access';
|
||||
import { AssetIdsDto } from '../asset';
|
||||
import { AuthDto } from '../auth';
|
||||
|
@ -68,10 +68,12 @@ export class DownloadService {
|
|||
}
|
||||
}
|
||||
|
||||
return {
|
||||
totalSize: archives.reduce((total, item) => (total += item.size), 0),
|
||||
archives,
|
||||
};
|
||||
let totalSize = 0;
|
||||
for (const archive of archives) {
|
||||
totalSize += archive.size;
|
||||
}
|
||||
|
||||
return { totalSize, archives };
|
||||
}
|
||||
|
||||
async downloadArchive(auth: AuthDto, dto: AssetIdsDto): Promise<ImmichReadStream> {
|
||||
|
@ -82,12 +84,12 @@ export class DownloadService {
|
|||
const paths: Record<string, number> = {};
|
||||
|
||||
for (const { originalPath, originalFileName } of assets) {
|
||||
const ext = extname(originalPath);
|
||||
let filename = `${originalFileName}${ext}`;
|
||||
const extension = extname(originalPath);
|
||||
let filename = `${originalFileName}${extension}`;
|
||||
const count = paths[filename] || 0;
|
||||
paths[filename] = count + 1;
|
||||
if (count !== 0) {
|
||||
filename = `${originalFileName}+${count}${ext}`;
|
||||
filename = `${originalFileName}+${count}${extension}`;
|
||||
}
|
||||
|
||||
zip.addFile(originalPath, filename);
|
||||
|
|
|
@ -23,7 +23,7 @@ import { JobService } from './job.service';
|
|||
|
||||
const makeMockHandlers = (success: boolean) => {
|
||||
const mock = jest.fn().mockResolvedValue(success);
|
||||
return Object.values(JobName).reduce((map, jobName) => ({ ...map, [jobName]: mock }), {}) as Record<
|
||||
return Object.fromEntries(Object.values(JobName).map((jobName) => [jobName, mock])) as unknown as Record<
|
||||
JobName,
|
||||
JobHandler
|
||||
>;
|
||||
|
|
|
@ -36,26 +36,31 @@ export class JobService {
|
|||
this.logger.debug(`Handling command: queue=${queueName},force=${dto.force}`);
|
||||
|
||||
switch (dto.command) {
|
||||
case JobCommand.START:
|
||||
case JobCommand.START: {
|
||||
await this.start(queueName, dto);
|
||||
break;
|
||||
}
|
||||
|
||||
case JobCommand.PAUSE:
|
||||
case JobCommand.PAUSE: {
|
||||
await this.jobRepository.pause(queueName);
|
||||
break;
|
||||
}
|
||||
|
||||
case JobCommand.RESUME:
|
||||
case JobCommand.RESUME: {
|
||||
await this.jobRepository.resume(queueName);
|
||||
break;
|
||||
}
|
||||
|
||||
case JobCommand.EMPTY:
|
||||
case JobCommand.EMPTY: {
|
||||
await this.jobRepository.empty(queueName);
|
||||
break;
|
||||
}
|
||||
|
||||
case JobCommand.CLEAR_FAILED:
|
||||
case JobCommand.CLEAR_FAILED: {
|
||||
const failedJobs = await this.jobRepository.clear(queueName, QueueCleanType.FAILED);
|
||||
this.logger.debug(`Cleared failed jobs: ${failedJobs}`);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return this.getJobStatus(queueName);
|
||||
|
@ -85,42 +90,53 @@ export class JobService {
|
|||
}
|
||||
|
||||
switch (name) {
|
||||
case QueueName.VIDEO_CONVERSION:
|
||||
case QueueName.VIDEO_CONVERSION: {
|
||||
return this.jobRepository.queue({ name: JobName.QUEUE_VIDEO_CONVERSION, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.STORAGE_TEMPLATE_MIGRATION:
|
||||
case QueueName.STORAGE_TEMPLATE_MIGRATION: {
|
||||
return this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION });
|
||||
}
|
||||
|
||||
case QueueName.MIGRATION:
|
||||
case QueueName.MIGRATION: {
|
||||
return this.jobRepository.queue({ name: JobName.QUEUE_MIGRATION });
|
||||
}
|
||||
|
||||
case QueueName.SMART_SEARCH:
|
||||
case QueueName.SMART_SEARCH: {
|
||||
await this.configCore.requireFeature(FeatureFlag.SMART_SEARCH);
|
||||
return this.jobRepository.queue({ name: JobName.QUEUE_SMART_SEARCH, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.METADATA_EXTRACTION:
|
||||
case QueueName.METADATA_EXTRACTION: {
|
||||
return this.jobRepository.queue({ name: JobName.QUEUE_METADATA_EXTRACTION, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.SIDECAR:
|
||||
case QueueName.SIDECAR: {
|
||||
await this.configCore.requireFeature(FeatureFlag.SIDECAR);
|
||||
return this.jobRepository.queue({ name: JobName.QUEUE_SIDECAR, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.THUMBNAIL_GENERATION:
|
||||
case QueueName.THUMBNAIL_GENERATION: {
|
||||
return this.jobRepository.queue({ name: JobName.QUEUE_GENERATE_THUMBNAILS, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.FACE_DETECTION:
|
||||
case QueueName.FACE_DETECTION: {
|
||||
await this.configCore.requireFeature(FeatureFlag.FACIAL_RECOGNITION);
|
||||
return this.jobRepository.queue({ name: JobName.QUEUE_FACE_DETECTION, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.FACIAL_RECOGNITION:
|
||||
case QueueName.FACIAL_RECOGNITION: {
|
||||
await this.configCore.requireFeature(FeatureFlag.FACIAL_RECOGNITION);
|
||||
return this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force } });
|
||||
}
|
||||
|
||||
case QueueName.LIBRARY:
|
||||
case QueueName.LIBRARY: {
|
||||
return this.jobRepository.queue({ name: JobName.LIBRARY_QUEUE_SCAN_ALL, data: { force } });
|
||||
}
|
||||
|
||||
default:
|
||||
default: {
|
||||
throw new BadRequestException(`Invalid job name: ${name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -184,17 +200,19 @@ export class JobService {
|
|||
private async onDone(item: JobItem) {
|
||||
switch (item.name) {
|
||||
case JobName.SIDECAR_SYNC:
|
||||
case JobName.SIDECAR_DISCOVERY:
|
||||
case JobName.SIDECAR_DISCOVERY: {
|
||||
await this.jobRepository.queue({ name: JobName.METADATA_EXTRACTION, data: item.data });
|
||||
break;
|
||||
}
|
||||
|
||||
case JobName.SIDECAR_WRITE:
|
||||
case JobName.SIDECAR_WRITE: {
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.METADATA_EXTRACTION,
|
||||
data: { id: item.data.id, source: 'sidecar-write' },
|
||||
});
|
||||
}
|
||||
|
||||
case JobName.METADATA_EXTRACTION:
|
||||
case JobName.METADATA_EXTRACTION: {
|
||||
if (item.data.source === 'sidecar-write') {
|
||||
const [asset] = await this.assetRepository.getByIds([item.data.id]);
|
||||
if (asset) {
|
||||
|
@ -203,24 +221,28 @@ export class JobService {
|
|||
}
|
||||
await this.jobRepository.queue({ name: JobName.LINK_LIVE_PHOTOS, data: item.data });
|
||||
break;
|
||||
}
|
||||
|
||||
case JobName.LINK_LIVE_PHOTOS:
|
||||
case JobName.LINK_LIVE_PHOTOS: {
|
||||
await this.jobRepository.queue({ name: JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE, data: item.data });
|
||||
break;
|
||||
}
|
||||
|
||||
case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE:
|
||||
case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: {
|
||||
if (item.data.source === 'upload') {
|
||||
await this.jobRepository.queue({ name: JobName.GENERATE_JPEG_THUMBNAIL, data: item.data });
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case JobName.GENERATE_PERSON_THUMBNAIL:
|
||||
case JobName.GENERATE_PERSON_THUMBNAIL: {
|
||||
const { id } = item.data;
|
||||
const person = await this.personRepository.getById(id);
|
||||
if (person) {
|
||||
this.communicationRepository.send(ClientEvent.PERSON_THUMBNAIL, person.ownerId, person.id);
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
case JobName.GENERATE_JPEG_THUMBNAIL: {
|
||||
const jobs: JobItem[] = [
|
||||
|
|
|
@ -19,7 +19,7 @@ import {
|
|||
} from '@test';
|
||||
|
||||
import { newFSWatcherMock } from '@test/mocks';
|
||||
import { Stats } from 'fs';
|
||||
import { Stats } from 'node:fs';
|
||||
import { ILibraryFileJob, ILibraryRefreshJob, JobName } from '../job';
|
||||
import {
|
||||
IAssetRepository,
|
||||
|
@ -116,12 +116,15 @@ describe(LibraryService.name, () => {
|
|||
|
||||
libraryMock.get.mockImplementation(async (id) => {
|
||||
switch (id) {
|
||||
case libraryStub.externalLibraryWithImportPaths1.id:
|
||||
case libraryStub.externalLibraryWithImportPaths1.id: {
|
||||
return libraryStub.externalLibraryWithImportPaths1;
|
||||
case libraryStub.externalLibraryWithImportPaths2.id:
|
||||
}
|
||||
case libraryStub.externalLibraryWithImportPaths2.id: {
|
||||
return libraryStub.externalLibraryWithImportPaths2;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
@ -532,7 +535,7 @@ describe(LibraryService.name, () => {
|
|||
});
|
||||
|
||||
it('should set a missing asset to offline', async () => {
|
||||
storageMock.stat.mockRejectedValue(new Error());
|
||||
storageMock.stat.mockRejectedValue(new Error('Path not found'));
|
||||
|
||||
const mockLibraryJob: ILibraryFileJob = {
|
||||
id: assetStub.image.id,
|
||||
|
@ -1430,12 +1433,15 @@ describe(LibraryService.name, () => {
|
|||
|
||||
libraryMock.get.mockImplementation(async (id) => {
|
||||
switch (id) {
|
||||
case libraryStub.externalLibraryWithImportPaths1.id:
|
||||
case libraryStub.externalLibraryWithImportPaths1.id: {
|
||||
return libraryStub.externalLibraryWithImportPaths1;
|
||||
case libraryStub.externalLibraryWithImportPaths2.id:
|
||||
}
|
||||
case libraryStub.externalLibraryWithImportPaths2.id: {
|
||||
return libraryStub.externalLibraryWithImportPaths2;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
|
|
|
@ -1,17 +1,17 @@
|
|||
import { AssetType, LibraryType } from '@app/infra/entities';
|
||||
import { ImmichLogger } from '@app/infra/logger';
|
||||
import { BadRequestException, Inject, Injectable } from '@nestjs/common';
|
||||
import { EventEmitter } from 'events';
|
||||
import { R_OK } from 'node:constants';
|
||||
import { EventEmitter } from 'node:events';
|
||||
import { Stats } from 'node:fs';
|
||||
import path from 'node:path';
|
||||
import { basename, parse } from 'path';
|
||||
import path, { basename, parse } from 'node:path';
|
||||
import picomatch from 'picomatch';
|
||||
import { AccessCore, Permission } from '../access';
|
||||
import { AuthDto } from '../auth';
|
||||
import { mimeTypes } from '../domain.constant';
|
||||
import { usePagination, validateCronExpression } from '../domain.util';
|
||||
import { IBaseJob, IEntityJob, ILibraryFileJob, ILibraryRefreshJob, JOBS_ASSET_PAGINATION_SIZE, JobName } from '../job';
|
||||
|
||||
import {
|
||||
IAccessRepository,
|
||||
IAssetRepository,
|
||||
|
@ -84,11 +84,7 @@ export class LibraryService extends EventEmitter {
|
|||
|
||||
if (library.watch.enabled !== this.watchLibraries) {
|
||||
this.watchLibraries = library.watch.enabled;
|
||||
if (this.watchLibraries) {
|
||||
await this.watchAll();
|
||||
} else {
|
||||
await this.unwatchAll();
|
||||
}
|
||||
await (this.watchLibraries ? this.watchAll() : this.unwatchAll());
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -227,12 +223,13 @@ export class LibraryService extends EventEmitter {
|
|||
|
||||
async create(auth: AuthDto, dto: CreateLibraryDto): Promise<LibraryResponseDto> {
|
||||
switch (dto.type) {
|
||||
case LibraryType.EXTERNAL:
|
||||
case LibraryType.EXTERNAL: {
|
||||
if (!dto.name) {
|
||||
dto.name = 'New External Library';
|
||||
}
|
||||
break;
|
||||
case LibraryType.UPLOAD:
|
||||
}
|
||||
case LibraryType.UPLOAD: {
|
||||
if (!dto.name) {
|
||||
dto.name = 'New Upload Library';
|
||||
}
|
||||
|
@ -246,6 +243,7 @@ export class LibraryService extends EventEmitter {
|
|||
throw new BadRequestException('Upload libraries cannot be watched');
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const library = await this.repository.create({
|
||||
|
@ -401,7 +399,7 @@ export class LibraryService extends EventEmitter {
|
|||
sidecarPath = `${assetPath}.xmp`;
|
||||
}
|
||||
|
||||
const deviceAssetId = `${basename(assetPath)}`.replace(/\s+/g, '');
|
||||
const deviceAssetId = `${basename(assetPath)}`.replaceAll(/\s+/g, '');
|
||||
|
||||
let assetId;
|
||||
if (doImport) {
|
||||
|
@ -533,17 +531,17 @@ export class LibraryService extends EventEmitter {
|
|||
}
|
||||
|
||||
this.logger.verbose(`Refreshing library: ${job.id}`);
|
||||
const crawledAssetPaths = (
|
||||
await this.storageRepository.crawl({
|
||||
pathsToCrawl: library.importPaths,
|
||||
exclusionPatterns: library.exclusionPatterns,
|
||||
})
|
||||
)
|
||||
.map(path.normalize)
|
||||
const rawPaths = await this.storageRepository.crawl({
|
||||
pathsToCrawl: library.importPaths,
|
||||
exclusionPatterns: library.exclusionPatterns,
|
||||
});
|
||||
|
||||
const crawledAssetPaths = rawPaths
|
||||
.map((filePath) => path.normalize(filePath))
|
||||
.filter((assetPath) =>
|
||||
// Filter out paths that are not within the user's external path
|
||||
assetPath.match(new RegExp(`^${user.externalPath}`)),
|
||||
);
|
||||
) as string[];
|
||||
|
||||
this.logger.debug(`Found ${crawledAssetPaths.length} asset(s) when crawling import paths ${library.importPaths}`);
|
||||
const assetsInLibrary = await this.assetRepository.getByLibraryId([job.id]);
|
||||
|
|
|
@ -181,13 +181,14 @@ export class MediaService {
|
|||
this.storageCore.ensureFolders(path);
|
||||
|
||||
switch (asset.type) {
|
||||
case AssetType.IMAGE:
|
||||
case AssetType.IMAGE: {
|
||||
const colorspace = this.isSRGB(asset) ? Colorspace.SRGB : thumbnail.colorspace;
|
||||
const thumbnailOptions = { format, size, colorspace, quality: thumbnail.quality };
|
||||
await this.mediaRepository.resize(asset.originalPath, path, thumbnailOptions);
|
||||
break;
|
||||
}
|
||||
|
||||
case AssetType.VIDEO:
|
||||
case AssetType.VIDEO: {
|
||||
const { audioStreams, videoStreams } = await this.mediaRepository.probe(asset.originalPath);
|
||||
const mainVideoStream = this.getMainStream(videoStreams);
|
||||
if (!mainVideoStream) {
|
||||
|
@ -199,9 +200,11 @@ export class MediaService {
|
|||
const options = new ThumbnailConfig(config).getOptions(mainVideoStream, mainAudioStream);
|
||||
await this.mediaRepository.transcode(asset.originalPath, path, options);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
default: {
|
||||
throw new UnsupportedMediaTypeException(`Unsupported asset type for thumbnail generation: ${asset.type}`);
|
||||
}
|
||||
}
|
||||
this.logger.log(
|
||||
`Successfully generated ${format.toUpperCase()} ${asset.type.toLowerCase()} thumbnail for asset ${asset.id}`,
|
||||
|
@ -297,16 +300,16 @@ export class MediaService {
|
|||
let transcodeOptions;
|
||||
try {
|
||||
transcodeOptions = await this.getCodecConfig(config).then((c) => c.getOptions(mainVideoStream, mainAudioStream));
|
||||
} catch (err) {
|
||||
this.logger.error(`An error occurred while configuring transcoding options: ${err}`);
|
||||
} catch (error) {
|
||||
this.logger.error(`An error occurred while configuring transcoding options: ${error}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
this.logger.log(`Start encoding video ${asset.id} ${JSON.stringify(transcodeOptions)}`);
|
||||
try {
|
||||
await this.mediaRepository.transcode(input, output, transcodeOptions);
|
||||
} catch (err) {
|
||||
this.logger.error(err);
|
||||
} catch (error) {
|
||||
this.logger.error(error);
|
||||
if (config.accel !== TranscodeHWAccel.DISABLED) {
|
||||
this.logger.error(
|
||||
`Error occurred during transcoding. Retrying with ${config.accel.toUpperCase()} acceleration disabled.`,
|
||||
|
@ -354,23 +357,29 @@ export class MediaService {
|
|||
const isLargerThanTargetBitrate = bitrate > this.parseBitrateToBps(ffmpegConfig.maxBitrate);
|
||||
|
||||
switch (ffmpegConfig.transcode) {
|
||||
case TranscodePolicy.DISABLED:
|
||||
case TranscodePolicy.DISABLED: {
|
||||
return false;
|
||||
}
|
||||
|
||||
case TranscodePolicy.ALL:
|
||||
case TranscodePolicy.ALL: {
|
||||
return true;
|
||||
}
|
||||
|
||||
case TranscodePolicy.REQUIRED:
|
||||
case TranscodePolicy.REQUIRED: {
|
||||
return !allTargetsMatching || videoStream.isHDR;
|
||||
}
|
||||
|
||||
case TranscodePolicy.OPTIMAL:
|
||||
case TranscodePolicy.OPTIMAL: {
|
||||
return !allTargetsMatching || isLargerThanTargetRes || videoStream.isHDR;
|
||||
}
|
||||
|
||||
case TranscodePolicy.BITRATE:
|
||||
case TranscodePolicy.BITRATE: {
|
||||
return !allTargetsMatching || isLargerThanTargetBitrate || videoStream.isHDR;
|
||||
}
|
||||
|
||||
default:
|
||||
default: {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -383,14 +392,18 @@ export class MediaService {
|
|||
|
||||
private getSWCodecConfig(config: SystemConfigFFmpegDto) {
|
||||
switch (config.targetVideoCodec) {
|
||||
case VideoCodec.H264:
|
||||
case VideoCodec.H264: {
|
||||
return new H264Config(config);
|
||||
case VideoCodec.HEVC:
|
||||
}
|
||||
case VideoCodec.HEVC: {
|
||||
return new HEVCConfig(config);
|
||||
case VideoCodec.VP9:
|
||||
}
|
||||
case VideoCodec.VP9: {
|
||||
return new VP9Config(config);
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
throw new UnsupportedMediaTypeException(`Codec '${config.targetVideoCodec}' is unsupported`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -398,23 +411,28 @@ export class MediaService {
|
|||
let handler: VideoCodecHWConfig;
|
||||
let devices: string[];
|
||||
switch (config.accel) {
|
||||
case TranscodeHWAccel.NVENC:
|
||||
case TranscodeHWAccel.NVENC: {
|
||||
handler = new NVENCConfig(config);
|
||||
break;
|
||||
case TranscodeHWAccel.QSV:
|
||||
}
|
||||
case TranscodeHWAccel.QSV: {
|
||||
devices = await this.storageRepository.readdir('/dev/dri');
|
||||
handler = new QSVConfig(config, devices);
|
||||
break;
|
||||
case TranscodeHWAccel.VAAPI:
|
||||
}
|
||||
case TranscodeHWAccel.VAAPI: {
|
||||
devices = await this.storageRepository.readdir('/dev/dri');
|
||||
handler = new VAAPIConfig(config, devices);
|
||||
break;
|
||||
case TranscodeHWAccel.RKMPP:
|
||||
}
|
||||
case TranscodeHWAccel.RKMPP: {
|
||||
devices = await this.storageRepository.readdir('/dev/dri');
|
||||
handler = new RKMPPConfig(config, devices);
|
||||
break;
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
throw new UnsupportedMediaTypeException(`${config.accel.toUpperCase()} acceleration is unsupported`);
|
||||
}
|
||||
}
|
||||
if (!handler.getSupportedCodecs().includes(config.targetVideoCodec)) {
|
||||
throw new UnsupportedMediaTypeException(
|
||||
|
@ -441,14 +459,14 @@ export class MediaService {
|
|||
parseBitrateToBps(bitrateString: string) {
|
||||
const bitrateValue = Number.parseInt(bitrateString);
|
||||
|
||||
if (isNaN(bitrateValue)) {
|
||||
if (Number.isNaN(bitrateValue)) {
|
||||
return 0;
|
||||
}
|
||||
|
||||
if (bitrateString.toLowerCase().endsWith('k')) {
|
||||
return bitrateValue * 1000; // Kilobits per second to bits per second
|
||||
} else if (bitrateString.toLowerCase().endsWith('m')) {
|
||||
return bitrateValue * 1000000; // Megabits per second to bits per second
|
||||
return bitrateValue * 1_000_000; // Megabits per second to bits per second
|
||||
} else {
|
||||
return bitrateValue;
|
||||
}
|
||||
|
|
|
@ -15,16 +15,14 @@ class BaseConfig implements VideoCodecSWConfig {
|
|||
getOptions(videoStream: VideoStreamInfo, audioStream?: AudioStreamInfo) {
|
||||
const options = {
|
||||
inputOptions: this.getBaseInputOptions(),
|
||||
outputOptions: this.getBaseOutputOptions(videoStream, audioStream).concat('-v verbose'),
|
||||
outputOptions: [...this.getBaseOutputOptions(videoStream, audioStream), '-v verbose'],
|
||||
twoPass: this.eligibleForTwoPass(),
|
||||
} as TranscodeOptions;
|
||||
const filters = this.getFilterOptions(videoStream);
|
||||
if (filters.length > 0) {
|
||||
options.outputOptions.push(`-vf ${filters.join(',')}`);
|
||||
}
|
||||
options.outputOptions.push(...this.getPresetOptions());
|
||||
options.outputOptions.push(...this.getThreadOptions());
|
||||
options.outputOptions.push(...this.getBitrateOptions());
|
||||
options.outputOptions.push(...this.getPresetOptions(), ...this.getThreadOptions(), ...this.getBitrateOptions());
|
||||
|
||||
return options;
|
||||
}
|
||||
|
@ -129,11 +127,10 @@ class BaseConfig implements VideoCodecSWConfig {
|
|||
|
||||
getTargetResolution(videoStream: VideoStreamInfo) {
|
||||
let target;
|
||||
if (this.config.targetResolution === 'original') {
|
||||
target = Math.min(videoStream.height, videoStream.width);
|
||||
} else {
|
||||
target = Number.parseInt(this.config.targetResolution);
|
||||
}
|
||||
target =
|
||||
this.config.targetResolution === 'original'
|
||||
? Math.min(videoStream.height, videoStream.width)
|
||||
: Number.parseInt(this.config.targetResolution);
|
||||
|
||||
if (target % 2 !== 0) {
|
||||
target -= 1;
|
||||
|
@ -182,7 +179,7 @@ class BaseConfig implements VideoCodecSWConfig {
|
|||
|
||||
getBitrateUnit() {
|
||||
const maxBitrate = this.getMaxBitrateValue();
|
||||
return this.config.maxBitrate.trim().substring(maxBitrate.toString().length); // use inputted unit if provided
|
||||
return this.config.maxBitrate.trim().slice(maxBitrate.toString().length); // use inputted unit if provided
|
||||
}
|
||||
|
||||
getMaxBitrateValue() {
|
||||
|
@ -411,8 +408,7 @@ export class NVENCConfig extends BaseHWConfig {
|
|||
...super.getBaseOutputOptions(videoStream, audioStream),
|
||||
];
|
||||
if (this.getBFrames() > 0) {
|
||||
options.push('-b_ref_mode middle');
|
||||
options.push('-b_qfactor 1.1');
|
||||
options.push('-b_ref_mode middle', '-b_qfactor 1.1');
|
||||
}
|
||||
if (this.config.temporalAQ) {
|
||||
options.push('-temporal-aq 1');
|
||||
|
@ -474,8 +470,8 @@ export class NVENCConfig extends BaseHWConfig {
|
|||
|
||||
export class QSVConfig extends BaseHWConfig {
|
||||
getBaseInputOptions() {
|
||||
if (!this.devices.length) {
|
||||
throw Error('No QSV device found');
|
||||
if (this.devices.length === 0) {
|
||||
throw new Error('No QSV device found');
|
||||
}
|
||||
|
||||
let qsvString = '';
|
||||
|
@ -519,8 +515,7 @@ export class QSVConfig extends BaseHWConfig {
|
|||
options.push(`-${this.useCQP() ? 'q:v' : 'global_quality'} ${this.config.crf}`);
|
||||
const bitrates = this.getBitrateDistribution();
|
||||
if (bitrates.max > 0) {
|
||||
options.push(`-maxrate ${bitrates.max}${bitrates.unit}`);
|
||||
options.push(`-bufsize ${bitrates.max * 2}${bitrates.unit}`);
|
||||
options.push(`-maxrate ${bitrates.max}${bitrates.unit}`, `-bufsize ${bitrates.max * 2}${bitrates.unit}`);
|
||||
}
|
||||
return options;
|
||||
}
|
||||
|
@ -623,7 +618,7 @@ export class RKMPPConfig extends BaseHWConfig {
|
|||
|
||||
getBaseInputOptions() {
|
||||
if (this.devices.length === 0) {
|
||||
throw Error('No RKMPP device found');
|
||||
throw new Error('No RKMPP device found');
|
||||
}
|
||||
return [];
|
||||
}
|
||||
|
@ -642,14 +637,17 @@ export class RKMPPConfig extends BaseHWConfig {
|
|||
|
||||
getPresetOptions() {
|
||||
switch (this.config.targetVideoCodec) {
|
||||
case VideoCodec.H264:
|
||||
case VideoCodec.H264: {
|
||||
// from ffmpeg_mpp help, commonly referred to as H264 level 5.1
|
||||
return ['-level 51'];
|
||||
case VideoCodec.HEVC:
|
||||
}
|
||||
case VideoCodec.HEVC: {
|
||||
// from ffmpeg_mpp help, commonly referred to as HEVC level 5.1
|
||||
return ['-level 153'];
|
||||
default:
|
||||
throw Error(`Incompatible video codec for RKMPP: ${this.config.targetVideoCodec}`);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Incompatible video codec for RKMPP: ${this.config.targetVideoCodec}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -16,11 +16,11 @@ import {
|
|||
newSystemConfigRepositoryMock,
|
||||
probeStub,
|
||||
} from '@test';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { BinaryField } from 'exiftool-vendored';
|
||||
import { Stats } from 'fs';
|
||||
import { constants } from 'fs/promises';
|
||||
import { when } from 'jest-when';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { Stats } from 'node:fs';
|
||||
import { constants } from 'node:fs/promises';
|
||||
import { JobName } from '../job';
|
||||
import {
|
||||
ClientEvent,
|
||||
|
@ -234,7 +234,7 @@ describe(MetadataService.name, () => {
|
|||
|
||||
describe('handleMetadataExtraction', () => {
|
||||
beforeEach(() => {
|
||||
storageMock.stat.mockResolvedValue({ size: 123456 } as Stats);
|
||||
storageMock.stat.mockResolvedValue({ size: 123_456 } as Stats);
|
||||
});
|
||||
|
||||
it('should handle an asset that could not be found', async () => {
|
||||
|
@ -507,7 +507,7 @@ describe(MetadataService.name, () => {
|
|||
exifImageWidth: null,
|
||||
exposureTime: tags.ExposureTime,
|
||||
fNumber: null,
|
||||
fileSizeInByte: 123456,
|
||||
fileSizeInByte: 123_456,
|
||||
focalLength: tags.FocalLength,
|
||||
fps: null,
|
||||
iso: tags.ISO,
|
||||
|
@ -565,7 +565,7 @@ describe(MetadataService.name, () => {
|
|||
|
||||
it('should handle duration with scale', async () => {
|
||||
assetMock.getByIds.mockResolvedValue([assetStub.image]);
|
||||
metadataMock.readTags.mockResolvedValue({ Duration: { Scale: 1.11111111111111e-5, Value: 558720 } });
|
||||
metadataMock.readTags.mockResolvedValue({ Duration: { Scale: 1.111_111_111_111_11e-5, Value: 558_720 } });
|
||||
|
||||
await sut.handleMetadataExtraction({ id: assetStub.image.id });
|
||||
|
||||
|
|
|
@ -3,9 +3,9 @@ import { ImmichLogger } from '@app/infra/logger';
|
|||
import { Inject, Injectable } from '@nestjs/common';
|
||||
import { ExifDateTime, Tags } from 'exiftool-vendored';
|
||||
import { firstDateTime } from 'exiftool-vendored/dist/FirstDateTime';
|
||||
import { constants } from 'fs/promises';
|
||||
import _ from 'lodash';
|
||||
import { Duration } from 'luxon';
|
||||
import { constants } from 'node:fs/promises';
|
||||
import { Subscription } from 'rxjs';
|
||||
import { usePagination } from '../domain.util';
|
||||
import { IBaseJob, IEntityJob, ISidecarWriteJob, JOBS_ASSET_PAGINATION_SIZE, JobName, QueueName } from '../job';
|
||||
|
@ -85,7 +85,7 @@ const validate = <T>(value: T): NonNullable<T> | null => {
|
|||
return null;
|
||||
}
|
||||
|
||||
if (typeof value === 'number' && (isNaN(value) || !isFinite(value))) {
|
||||
if (typeof value === 'number' && (Number.isNaN(value) || !Number.isFinite(value))) {
|
||||
return null;
|
||||
}
|
||||
|
||||
|
@ -217,18 +217,22 @@ export class MetadataService {
|
|||
|
||||
if (videoStreams[0]) {
|
||||
switch (videoStreams[0].rotation) {
|
||||
case -90:
|
||||
case -90: {
|
||||
exifData.orientation = Orientation.Rotate90CW;
|
||||
break;
|
||||
case 0:
|
||||
}
|
||||
case 0: {
|
||||
exifData.orientation = Orientation.Horizontal;
|
||||
break;
|
||||
case 90:
|
||||
}
|
||||
case 90: {
|
||||
exifData.orientation = Orientation.Rotate270CW;
|
||||
break;
|
||||
case 180:
|
||||
}
|
||||
case 180: {
|
||||
exifData.orientation = Orientation.Rotate180;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -243,7 +247,7 @@ export class MetadataService {
|
|||
const timeZoneOffset = tzOffset(firstDateTime(tags as Tags)) ?? 0;
|
||||
|
||||
if (dateTimeOriginal && timeZoneOffset) {
|
||||
localDateTime = new Date(dateTimeOriginal.getTime() + timeZoneOffset * 60000);
|
||||
localDateTime = new Date(dateTimeOriginal.getTime() + timeZoneOffset * 60_000);
|
||||
}
|
||||
await this.assetRepository.save({
|
||||
id: asset.id,
|
||||
|
@ -413,7 +417,13 @@ export class MetadataService {
|
|||
const checksum = this.cryptoRepository.hashSha1(video);
|
||||
|
||||
let motionAsset = await this.assetRepository.getByChecksum(asset.ownerId, checksum);
|
||||
if (!motionAsset) {
|
||||
if (motionAsset) {
|
||||
this.logger.debug(
|
||||
`Asset ${asset.id}'s motion photo video with checksum ${checksum.toString(
|
||||
'base64',
|
||||
)} already exists in the repository`,
|
||||
);
|
||||
} else {
|
||||
// We create a UUID in advance so that each extracted video can have a unique filename
|
||||
// (allowing us to delete old ones if necessary)
|
||||
const motionAssetId = this.cryptoRepository.randomUUID();
|
||||
|
@ -448,12 +458,6 @@ export class MetadataService {
|
|||
await this.jobRepository.queue({ name: JobName.ASSET_DELETION, data: { id: asset.livePhotoVideoId } });
|
||||
this.logger.log(`Removed old motion photo video asset (${asset.livePhotoVideoId})`);
|
||||
}
|
||||
} else {
|
||||
this.logger.debug(
|
||||
`Asset ${asset.id}'s motion photo video with checksum ${checksum.toString(
|
||||
'base64',
|
||||
)} already exists in the repository`,
|
||||
);
|
||||
}
|
||||
|
||||
this.logger.debug(`Finished motion photo video extraction (${asset.id})`);
|
||||
|
@ -494,7 +498,7 @@ export class MetadataService {
|
|||
fileSizeInByte: stats.size,
|
||||
fNumber: validate(tags.FNumber),
|
||||
focalLength: validate(tags.FocalLength),
|
||||
fps: validate(parseFloat(tags.VideoFrameRate!)),
|
||||
fps: validate(Number.parseFloat(tags.VideoFrameRate!)),
|
||||
iso: validate(tags.ISO),
|
||||
latitude: validate(tags.GPSLatitude),
|
||||
lensModel: tags.LensModel ?? null,
|
||||
|
|
|
@ -24,7 +24,7 @@ export class PartnerService {
|
|||
}
|
||||
|
||||
const partner = await this.repository.create(partnerId);
|
||||
return this.map(partner, PartnerDirection.SharedBy);
|
||||
return this.mapToPartnerEntity(partner, PartnerDirection.SharedBy);
|
||||
}
|
||||
|
||||
async remove(auth: AuthDto, sharedWithId: string): Promise<void> {
|
||||
|
@ -43,7 +43,7 @@ export class PartnerService {
|
|||
return partners
|
||||
.filter((partner) => partner.sharedBy && partner.sharedWith) // Filter out soft deleted users
|
||||
.filter((partner) => partner[key] === auth.user.id)
|
||||
.map((partner) => this.map(partner, direction));
|
||||
.map((partner) => this.mapToPartnerEntity(partner, direction));
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, sharedById: string, dto: UpdatePartnerDto): Promise<PartnerResponseDto> {
|
||||
|
@ -51,10 +51,10 @@ export class PartnerService {
|
|||
const partnerId: PartnerIds = { sharedById, sharedWithId: auth.user.id };
|
||||
|
||||
const entity = await this.repository.update({ ...partnerId, inTimeline: dto.inTimeline });
|
||||
return this.map(entity, PartnerDirection.SharedWith);
|
||||
return this.mapToPartnerEntity(entity, PartnerDirection.SharedWith);
|
||||
}
|
||||
|
||||
private map(partner: PartnerEntity, direction: PartnerDirection): PartnerResponseDto {
|
||||
private mapToPartnerEntity(partner: PartnerEntity, direction: PartnerDirection): PartnerResponseDto {
|
||||
// this is opposite to return the non-me user of the "partner"
|
||||
const user = mapUser(
|
||||
direction === PartnerDirection.SharedBy ? partner.sharedWith : partner.sharedBy,
|
||||
|
|
|
@ -814,7 +814,7 @@ describe(PersonService.name, () => {
|
|||
}
|
||||
|
||||
const faces = [
|
||||
{ face: faceStub.noPerson1, distance: 0.0 },
|
||||
{ face: faceStub.noPerson1, distance: 0 },
|
||||
{ face: faceStub.primaryFace1, distance: 0.2 },
|
||||
{ face: faceStub.noPerson2, distance: 0.3 },
|
||||
{ face: faceStub.face1, distance: 0.4 },
|
||||
|
@ -843,7 +843,7 @@ describe(PersonService.name, () => {
|
|||
|
||||
it('should create a new person if the face is a core point with no person', async () => {
|
||||
const faces = [
|
||||
{ face: faceStub.noPerson1, distance: 0.0 },
|
||||
{ face: faceStub.noPerson1, distance: 0 },
|
||||
{ face: faceStub.noPerson2, distance: 0.3 },
|
||||
] as FaceSearchResult[];
|
||||
|
||||
|
@ -867,7 +867,7 @@ describe(PersonService.name, () => {
|
|||
});
|
||||
|
||||
it('should defer non-core faces to end of queue', async () => {
|
||||
const faces = [{ face: faceStub.noPerson1, distance: 0.0 }] as FaceSearchResult[];
|
||||
const faces = [{ face: faceStub.noPerson1, distance: 0 }] as FaceSearchResult[];
|
||||
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 2 },
|
||||
|
@ -888,7 +888,7 @@ describe(PersonService.name, () => {
|
|||
});
|
||||
|
||||
it('should not assign person to non-core face with no matching person', async () => {
|
||||
const faces = [{ face: faceStub.noPerson1, distance: 0.0 }] as FaceSearchResult[];
|
||||
const faces = [{ face: faceStub.noPerson1, distance: 0 }] as FaceSearchResult[];
|
||||
|
||||
configMock.load.mockResolvedValue([
|
||||
{ key: SystemConfigKey.MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES, value: 2 },
|
||||
|
|
|
@ -122,7 +122,7 @@ export class PersonService {
|
|||
}
|
||||
if (changeFeaturePhoto.length > 0) {
|
||||
// Remove duplicates
|
||||
await this.createNewFeaturePhoto(Array.from(new Set(changeFeaturePhoto)));
|
||||
await this.createNewFeaturePhoto([...new Set(changeFeaturePhoto)]);
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
@ -332,7 +332,7 @@ export class PersonService {
|
|||
this.logger.debug(`${faces.length} faces detected in ${asset.resizePath}`);
|
||||
this.logger.verbose(faces.map((face) => ({ ...face, embedding: `vector(${face.embedding.length})` })));
|
||||
|
||||
if (faces.length) {
|
||||
if (faces.length > 0) {
|
||||
await this.jobRepository.queue({ name: JobName.QUEUE_FACIAL_RECOGNITION, data: { force: false } });
|
||||
|
||||
const mappedFaces = faces.map((face) => ({
|
||||
|
@ -417,7 +417,7 @@ export class PersonService {
|
|||
numResults: machineLearning.facialRecognition.minFaces,
|
||||
});
|
||||
|
||||
this.logger.debug(`Face ${id} has ${matches.length} match${matches.length != 1 ? 'es' : ''}`);
|
||||
this.logger.debug(`Face ${id} has ${matches.length} match${matches.length == 1 ? '' : 'es'}`);
|
||||
|
||||
const isCore = matches.length >= machineLearning.facialRecognition.minFaces;
|
||||
if (!isCore && !deferred) {
|
||||
|
|
|
@ -15,7 +15,7 @@ export enum DatabaseLock {
|
|||
export const IDatabaseRepository = 'IDatabaseRepository';
|
||||
|
||||
export interface IDatabaseRepository {
|
||||
getExtensionVersion(extName: string): Promise<Version | null>;
|
||||
getExtensionVersion(extensionName: string): Promise<Version | null>;
|
||||
getPostgresVersion(): Promise<Version>;
|
||||
createExtension(extension: DatabaseExtension): Promise<void>;
|
||||
runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise<void>;
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import { VideoCodec } from '@app/infra/entities';
|
||||
import { Writable } from 'stream';
|
||||
import { Writable } from 'node:stream';
|
||||
|
||||
export const IMediaRepository = 'IMediaRepository';
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { FSWatcher, WatchOptions } from 'chokidar';
|
||||
import { Stats } from 'fs';
|
||||
import { FileReadOptions } from 'fs/promises';
|
||||
import { Readable } from 'stream';
|
||||
import { Stats } from 'node:fs';
|
||||
import { FileReadOptions } from 'node:fs/promises';
|
||||
import { Readable } from 'node:stream';
|
||||
import { CrawlOptionsDto } from '../library';
|
||||
|
||||
export interface ImmichReadStream {
|
||||
|
|
|
@ -46,7 +46,7 @@ export class SearchService {
|
|||
this.assetRepository.getAssetIdByTag(auth.user.id, options),
|
||||
]);
|
||||
const assetIds = new Set<string>(results.flatMap((field) => field.items.map((item) => item.data)));
|
||||
const assets = await this.assetRepository.getByIds(Array.from(assetIds));
|
||||
const assets = await this.assetRepository.getByIds([...assetIds]);
|
||||
const assetMap = new Map<string, AssetResponseDto>(assets.map((asset) => [asset.id, mapAsset(asset)]));
|
||||
|
||||
return results.map(({ fieldName, items }) => ({
|
||||
|
@ -75,7 +75,7 @@ export class SearchService {
|
|||
let assets: AssetEntity[] = [];
|
||||
|
||||
switch (strategy) {
|
||||
case SearchStrategy.SMART:
|
||||
case SearchStrategy.SMART: {
|
||||
const embedding = await this.machineLearning.encodeText(
|
||||
machineLearning.url,
|
||||
{ text: query },
|
||||
|
@ -88,10 +88,13 @@ export class SearchService {
|
|||
withArchived,
|
||||
});
|
||||
break;
|
||||
case SearchStrategy.TEXT:
|
||||
}
|
||||
case SearchStrategy.TEXT: {
|
||||
assets = await this.assetRepository.searchMetadata(query, userIds, { numResults: 250 });
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
|
|
|
@ -71,12 +71,12 @@ describe(ServerInfoService.name, () => {
|
|||
|
||||
await expect(sut.getInfo()).resolves.toEqual({
|
||||
diskAvailable: '293.0 KiB',
|
||||
diskAvailableRaw: 300000,
|
||||
diskAvailableRaw: 300_000,
|
||||
diskSize: '488.3 KiB',
|
||||
diskSizeRaw: 500000,
|
||||
diskSizeRaw: 500_000,
|
||||
diskUsagePercentage: 60,
|
||||
diskUse: '293.0 KiB',
|
||||
diskUseRaw: 300000,
|
||||
diskUseRaw: 300_000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
|
@ -87,12 +87,12 @@ describe(ServerInfoService.name, () => {
|
|||
|
||||
await expect(sut.getInfo()).resolves.toEqual({
|
||||
diskAvailable: '286.1 MiB',
|
||||
diskAvailableRaw: 300000000,
|
||||
diskAvailableRaw: 300_000_000,
|
||||
diskSize: '476.8 MiB',
|
||||
diskSizeRaw: 500000000,
|
||||
diskSizeRaw: 500_000_000,
|
||||
diskUsagePercentage: 60,
|
||||
diskUse: '286.1 MiB',
|
||||
diskUseRaw: 300000000,
|
||||
diskUseRaw: 300_000_000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
|
@ -107,12 +107,12 @@ describe(ServerInfoService.name, () => {
|
|||
|
||||
await expect(sut.getInfo()).resolves.toEqual({
|
||||
diskAvailable: '279.4 GiB',
|
||||
diskAvailableRaw: 300000000000,
|
||||
diskAvailableRaw: 300_000_000_000,
|
||||
diskSize: '465.7 GiB',
|
||||
diskSizeRaw: 500000000000,
|
||||
diskSizeRaw: 500_000_000_000,
|
||||
diskUsagePercentage: 60,
|
||||
diskUse: '279.4 GiB',
|
||||
diskUseRaw: 300000000000,
|
||||
diskUseRaw: 300_000_000_000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
|
@ -127,12 +127,12 @@ describe(ServerInfoService.name, () => {
|
|||
|
||||
await expect(sut.getInfo()).resolves.toEqual({
|
||||
diskAvailable: '272.8 TiB',
|
||||
diskAvailableRaw: 300000000000000,
|
||||
diskAvailableRaw: 300_000_000_000_000,
|
||||
diskSize: '454.7 TiB',
|
||||
diskSizeRaw: 500000000000000,
|
||||
diskSizeRaw: 500_000_000_000_000,
|
||||
diskUsagePercentage: 60,
|
||||
diskUse: '272.8 TiB',
|
||||
diskUseRaw: 300000000000000,
|
||||
diskUseRaw: 300_000_000_000_000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
|
@ -147,12 +147,12 @@ describe(ServerInfoService.name, () => {
|
|||
|
||||
await expect(sut.getInfo()).resolves.toEqual({
|
||||
diskAvailable: '266.5 PiB',
|
||||
diskAvailableRaw: 300000000000000000,
|
||||
diskAvailableRaw: 300_000_000_000_000_000,
|
||||
diskSize: '444.1 PiB',
|
||||
diskSizeRaw: 500000000000000000,
|
||||
diskSizeRaw: 500_000_000_000_000_000,
|
||||
diskUsagePercentage: 60,
|
||||
diskUse: '266.5 PiB',
|
||||
diskUseRaw: 300000000000000000,
|
||||
diskUseRaw: 300_000_000_000_000_000,
|
||||
});
|
||||
|
||||
expect(storageMock.checkDiskUsage).toHaveBeenCalledWith('upload/library');
|
||||
|
@ -219,7 +219,7 @@ describe(ServerInfoService.name, () => {
|
|||
userName: '1 User',
|
||||
photos: 10,
|
||||
videos: 11,
|
||||
usage: 12345,
|
||||
usage: 12_345,
|
||||
quotaSizeInBytes: 0,
|
||||
},
|
||||
{
|
||||
|
@ -227,7 +227,7 @@ describe(ServerInfoService.name, () => {
|
|||
userName: '2 User',
|
||||
photos: 10,
|
||||
videos: 20,
|
||||
usage: 123456,
|
||||
usage: 123_456,
|
||||
quotaSizeInBytes: 0,
|
||||
},
|
||||
{
|
||||
|
@ -235,7 +235,7 @@ describe(ServerInfoService.name, () => {
|
|||
userName: '3 User',
|
||||
photos: 100,
|
||||
videos: 0,
|
||||
usage: 987654,
|
||||
usage: 987_654,
|
||||
quotaSizeInBytes: 0,
|
||||
},
|
||||
]);
|
||||
|
@ -243,12 +243,12 @@ describe(ServerInfoService.name, () => {
|
|||
await expect(sut.getStatistics()).resolves.toEqual({
|
||||
photos: 120,
|
||||
videos: 31,
|
||||
usage: 1123455,
|
||||
usage: 1_123_455,
|
||||
usageByUser: [
|
||||
{
|
||||
photos: 10,
|
||||
quotaSizeInBytes: 0,
|
||||
usage: 12345,
|
||||
usage: 12_345,
|
||||
userName: '1 User',
|
||||
userId: 'user1',
|
||||
videos: 11,
|
||||
|
@ -256,7 +256,7 @@ describe(ServerInfoService.name, () => {
|
|||
{
|
||||
photos: 10,
|
||||
quotaSizeInBytes: 0,
|
||||
usage: 123456,
|
||||
usage: 123_456,
|
||||
userName: '2 User',
|
||||
userId: 'user2',
|
||||
videos: 20,
|
||||
|
@ -264,7 +264,7 @@ describe(ServerInfoService.name, () => {
|
|||
{
|
||||
photos: 100,
|
||||
quotaSizeInBytes: 0,
|
||||
usage: 987654,
|
||||
usage: 987_654,
|
||||
userName: '3 User',
|
||||
userId: 'user3',
|
||||
videos: 0,
|
||||
|
|
|
@ -67,7 +67,7 @@ export class ServerInfoService {
|
|||
serverInfo.diskAvailableRaw = diskInfo.available;
|
||||
serverInfo.diskSizeRaw = diskInfo.total;
|
||||
serverInfo.diskUseRaw = diskInfo.total - diskInfo.free;
|
||||
serverInfo.diskUsagePercentage = parseFloat(usagePercentage);
|
||||
serverInfo.diskUsagePercentage = Number.parseFloat(usagePercentage);
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
|
|
|
@ -21,7 +21,7 @@ export class SharedLinkService {
|
|||
}
|
||||
|
||||
getAll(auth: AuthDto): Promise<SharedLinkResponseDto[]> {
|
||||
return this.repository.getAll(auth.user.id).then((links) => links.map(mapSharedLink));
|
||||
return this.repository.getAll(auth.user.id).then((links) => links.map((link) => mapSharedLink(link)));
|
||||
}
|
||||
|
||||
async getMine(auth: AuthDto, dto: SharedLinkPasswordDto): Promise<SharedLinkResponseDto> {
|
||||
|
@ -30,7 +30,7 @@ export class SharedLinkService {
|
|||
}
|
||||
|
||||
const sharedLink = await this.findOrFail(auth.user.id, auth.sharedLink.id);
|
||||
const response = this.map(sharedLink, { withExif: sharedLink.showExif });
|
||||
const response = this.mapToSharedLink(sharedLink, { withExif: sharedLink.showExif });
|
||||
if (sharedLink.password) {
|
||||
response.token = this.validateAndRefreshToken(sharedLink, dto);
|
||||
}
|
||||
|
@ -40,19 +40,20 @@ export class SharedLinkService {
|
|||
|
||||
async get(auth: AuthDto, id: string): Promise<SharedLinkResponseDto> {
|
||||
const sharedLink = await this.findOrFail(auth.user.id, id);
|
||||
return this.map(sharedLink, { withExif: true });
|
||||
return this.mapToSharedLink(sharedLink, { withExif: true });
|
||||
}
|
||||
|
||||
async create(auth: AuthDto, dto: SharedLinkCreateDto): Promise<SharedLinkResponseDto> {
|
||||
switch (dto.type) {
|
||||
case SharedLinkType.ALBUM:
|
||||
case SharedLinkType.ALBUM: {
|
||||
if (!dto.albumId) {
|
||||
throw new BadRequestException('Invalid albumId');
|
||||
}
|
||||
await this.access.requirePermission(auth, Permission.ALBUM_SHARE, dto.albumId);
|
||||
break;
|
||||
}
|
||||
|
||||
case SharedLinkType.INDIVIDUAL:
|
||||
case SharedLinkType.INDIVIDUAL: {
|
||||
if (!dto.assetIds || dto.assetIds.length === 0) {
|
||||
throw new BadRequestException('Invalid assetIds');
|
||||
}
|
||||
|
@ -60,6 +61,7 @@ export class SharedLinkService {
|
|||
await this.access.requirePermission(auth, Permission.ASSET_SHARE, dto.assetIds);
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const sharedLink = await this.repository.create({
|
||||
|
@ -76,7 +78,7 @@ export class SharedLinkService {
|
|||
showExif: dto.showMetadata ?? true,
|
||||
});
|
||||
|
||||
return this.map(sharedLink, { withExif: true });
|
||||
return this.mapToSharedLink(sharedLink, { withExif: true });
|
||||
}
|
||||
|
||||
async update(auth: AuthDto, id: string, dto: SharedLinkEditDto) {
|
||||
|
@ -91,7 +93,7 @@ export class SharedLinkService {
|
|||
allowDownload: dto.allowDownload,
|
||||
showExif: dto.showMetadata,
|
||||
});
|
||||
return this.map(sharedLink, { withExif: true });
|
||||
return this.mapToSharedLink(sharedLink, { withExif: true });
|
||||
}
|
||||
|
||||
async remove(auth: AuthDto, id: string): Promise<void> {
|
||||
|
@ -173,7 +175,7 @@ export class SharedLinkService {
|
|||
|
||||
const sharedLink = await this.findOrFail(auth.sharedLink.userId, auth.sharedLink.id);
|
||||
const assetId = sharedLink.album?.albumThumbnailAssetId || sharedLink.assets[0]?.id;
|
||||
const assetCount = sharedLink.assets.length || sharedLink.album?.assets.length || 0;
|
||||
const assetCount = sharedLink.assets.length ?? sharedLink.album?.assets.length ?? 0;
|
||||
|
||||
return {
|
||||
title: sharedLink.album ? sharedLink.album.albumName : 'Public Share',
|
||||
|
@ -184,7 +186,7 @@ export class SharedLinkService {
|
|||
};
|
||||
}
|
||||
|
||||
private map(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) {
|
||||
private mapToSharedLink(sharedLink: SharedLinkEntity, { withExif }: { withExif: boolean }) {
|
||||
return withExif ? mapSharedLink(sharedLink) : mapSharedLinkWithoutMetadata(sharedLink);
|
||||
}
|
||||
|
||||
|
|
|
@ -111,8 +111,12 @@ export const CLIP_MODEL_INFO: Record<string, ModelInfo> = {
|
|||
};
|
||||
|
||||
export function cleanModelName(modelName: string): string {
|
||||
const tokens = modelName.split('/');
|
||||
return tokens[tokens.length - 1].replace(/:/g, '_');
|
||||
const token = modelName.split('/').at(-1);
|
||||
if (!token) {
|
||||
throw new Error(`Invalid model name: ${modelName}`);
|
||||
}
|
||||
|
||||
return token.replaceAll(':', '_');
|
||||
}
|
||||
|
||||
export function getCLIPModelInfo(modelName: string): ModelInfo {
|
||||
|
|
|
@ -269,7 +269,7 @@ describe(StorageTemplateService.name, () => {
|
|||
when(storageMock.stat)
|
||||
.calledWith(newPath)
|
||||
.mockResolvedValue({ size: 5000 } as Stats);
|
||||
when(cryptoMock.hashFile).calledWith(newPath).mockResolvedValue(Buffer.from('different-hash', 'utf-8'));
|
||||
when(cryptoMock.hashFile).calledWith(newPath).mockResolvedValue(Buffer.from('different-hash', 'utf8'));
|
||||
|
||||
when(assetMock.save)
|
||||
.calledWith({ id: assetStub.image.id, originalPath: newPath })
|
||||
|
@ -311,9 +311,9 @@ describe(StorageTemplateService.name, () => {
|
|||
});
|
||||
|
||||
it.each`
|
||||
failedPathChecksum | failedPathSize | reason
|
||||
${assetStub.image.checksum} | ${500} | ${'file size'}
|
||||
${Buffer.from('bad checksum', 'utf-8')} | ${assetStub.image.exifInfo?.fileSizeInByte} | ${'checksum'}
|
||||
failedPathChecksum | failedPathSize | reason
|
||||
${assetStub.image.checksum} | ${500} | ${'file size'}
|
||||
${Buffer.from('bad checksum', 'utf8')} | ${assetStub.image.exifInfo?.fileSizeInByte} | ${'checksum'}
|
||||
`(
|
||||
'should fail to migrate previously failed move from previous new path when old path no longer exists if $reason validation fails',
|
||||
async ({ failedPathChecksum, failedPathSize }) => {
|
||||
|
|
|
@ -86,7 +86,8 @@ export class StorageTemplateService {
|
|||
}
|
||||
|
||||
async handleMigrationSingle({ id }: IEntityJob) {
|
||||
const storageTemplateEnabled = (await this.configCore.getConfig()).storageTemplate.enabled;
|
||||
const config = await this.configCore.getConfig();
|
||||
const storageTemplateEnabled = config.storageTemplate.enabled;
|
||||
if (!storageTemplateEnabled) {
|
||||
return true;
|
||||
}
|
||||
|
@ -109,8 +110,9 @@ export class StorageTemplateService {
|
|||
|
||||
async handleMigration() {
|
||||
this.logger.log('Starting storage template migration');
|
||||
const storageTemplateEnabled = (await this.configCore.getConfig()).storageTemplate.enabled;
|
||||
if (!storageTemplateEnabled) {
|
||||
const { storageTemplate } = await this.configCore.getConfig();
|
||||
const { enabled } = storageTemplate;
|
||||
if (!enabled) {
|
||||
this.logger.log('Storage template migration disabled, skipping');
|
||||
return true;
|
||||
}
|
||||
|
@ -145,7 +147,7 @@ export class StorageTemplateService {
|
|||
}
|
||||
|
||||
return this.databaseRepository.withLock(DatabaseLock.StorageTemplateMigration, async () => {
|
||||
const { id, sidecarPath, originalPath, exifInfo } = asset;
|
||||
const { id, sidecarPath, originalPath, exifInfo, checksum } = asset;
|
||||
const oldPath = originalPath;
|
||||
const newPath = await this.getTemplatePath(asset, metadata);
|
||||
|
||||
|
@ -160,7 +162,7 @@ export class StorageTemplateService {
|
|||
pathType: AssetPathType.ORIGINAL,
|
||||
oldPath,
|
||||
newPath,
|
||||
assetInfo: { sizeInBytes: exifInfo.fileSizeInByte, checksum: asset.checksum },
|
||||
assetInfo: { sizeInBytes: exifInfo.fileSizeInByte, checksum },
|
||||
});
|
||||
if (sidecarPath) {
|
||||
await this.storageCore.moveFile({
|
||||
|
@ -171,7 +173,7 @@ export class StorageTemplateService {
|
|||
});
|
||||
}
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Problem applying storage template`, error?.stack, { id: asset.id, oldPath, newPath });
|
||||
this.logger.error(`Problem applying storage template`, error?.stack, { id, oldPath, newPath });
|
||||
}
|
||||
});
|
||||
}
|
||||
|
@ -181,8 +183,8 @@ export class StorageTemplateService {
|
|||
|
||||
try {
|
||||
const source = asset.originalPath;
|
||||
const ext = path.extname(source).split('.').pop() as string;
|
||||
const sanitized = sanitize(path.basename(filename, `.${ext}`));
|
||||
const extension = path.extname(source).split('.').pop() as string;
|
||||
const sanitized = sanitize(path.basename(filename, `.${extension}`));
|
||||
const rootPath = StorageCore.getLibraryFolder({ id: asset.ownerId, storageLabel });
|
||||
|
||||
let albumName = null;
|
||||
|
@ -194,11 +196,11 @@ export class StorageTemplateService {
|
|||
const storagePath = this.render(this.template.compiled, {
|
||||
asset,
|
||||
filename: sanitized,
|
||||
extension: ext,
|
||||
extension: extension,
|
||||
albumName,
|
||||
});
|
||||
const fullPath = path.normalize(path.join(rootPath, storagePath));
|
||||
let destination = `${fullPath}.${ext}`;
|
||||
let destination = `${fullPath}.${extension}`;
|
||||
|
||||
if (!fullPath.startsWith(rootPath)) {
|
||||
this.logger.warn(`Skipped attempt to access an invalid path: ${fullPath}. Path should start with ${rootPath}`);
|
||||
|
@ -223,8 +225,8 @@ export class StorageTemplateService {
|
|||
* The lines below will be used to check if the differences between the source and destination is only the
|
||||
* +7 suffix, and if so, it will be considered as already migrated.
|
||||
*/
|
||||
if (source.startsWith(fullPath) && source.endsWith(`.${ext}`)) {
|
||||
const diff = source.replace(fullPath, '').replace(`.${ext}`, '');
|
||||
if (source.startsWith(fullPath) && source.endsWith(`.${extension}`)) {
|
||||
const diff = source.replace(fullPath, '').replace(`.${extension}`, '');
|
||||
const hasDuplicationAnnotation = /^\+\d+$/.test(diff);
|
||||
if (hasDuplicationAnnotation) {
|
||||
return source;
|
||||
|
@ -240,7 +242,7 @@ export class StorageTemplateService {
|
|||
}
|
||||
|
||||
duplicateCount++;
|
||||
destination = `${fullPath}+${duplicateCount}.${ext}`;
|
||||
destination = `${fullPath}+${duplicateCount}.${extension}`;
|
||||
}
|
||||
|
||||
return destination;
|
||||
|
@ -264,9 +266,9 @@ export class StorageTemplateService {
|
|||
extension: 'jpg',
|
||||
albumName: 'album',
|
||||
});
|
||||
} catch (e) {
|
||||
this.logger.warn(`Storage template validation failed: ${JSON.stringify(e)}`);
|
||||
throw new Error(`Invalid storage template: ${e}`);
|
||||
} catch (error) {
|
||||
this.logger.warn(`Storage template validation failed: ${JSON.stringify(error)}`);
|
||||
throw new Error(`Invalid storage template: ${error}`);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -282,7 +284,7 @@ export class StorageTemplateService {
|
|||
return {
|
||||
raw: template,
|
||||
compiled: handlebar.compile(template, { knownHelpers: undefined, strict: true }),
|
||||
needsAlbum: template.indexOf('{{album}}') !== -1,
|
||||
needsAlbum: template.includes('{{album}}'),
|
||||
};
|
||||
}
|
||||
|
||||
|
@ -295,7 +297,7 @@ export class StorageTemplateService {
|
|||
filetypefull: asset.type == AssetType.IMAGE ? 'IMAGE' : 'VIDEO',
|
||||
assetId: asset.id,
|
||||
//just throw into the root if it doesn't belong to an album
|
||||
album: (albumName && sanitize(albumName.replace(/\.+/g, ''))) || '.',
|
||||
album: (albumName && sanitize(albumName.replaceAll(/\.+/g, ''))) || '.',
|
||||
};
|
||||
|
||||
const systemTimeZone = Intl.DateTimeFormat().resolvedOptions().timeZone;
|
||||
|
|
|
@ -118,40 +118,44 @@ export class StorageCore {
|
|||
async moveAssetFile(asset: AssetEntity, pathType: GeneratedAssetPath) {
|
||||
const { id: entityId, resizePath, webpPath, encodedVideoPath } = asset;
|
||||
switch (pathType) {
|
||||
case AssetPathType.JPEG_THUMBNAIL:
|
||||
case AssetPathType.JPEG_THUMBNAIL: {
|
||||
return this.moveFile({
|
||||
entityId,
|
||||
pathType,
|
||||
oldPath: resizePath,
|
||||
newPath: StorageCore.getLargeThumbnailPath(asset),
|
||||
});
|
||||
case AssetPathType.WEBP_THUMBNAIL:
|
||||
}
|
||||
case AssetPathType.WEBP_THUMBNAIL: {
|
||||
return this.moveFile({
|
||||
entityId,
|
||||
pathType,
|
||||
oldPath: webpPath,
|
||||
newPath: StorageCore.getSmallThumbnailPath(asset),
|
||||
});
|
||||
case AssetPathType.ENCODED_VIDEO:
|
||||
}
|
||||
case AssetPathType.ENCODED_VIDEO: {
|
||||
return this.moveFile({
|
||||
entityId,
|
||||
pathType,
|
||||
oldPath: encodedVideoPath,
|
||||
newPath: StorageCore.getEncodedVideoPath(asset),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
async movePersonFile(person: PersonEntity, pathType: PersonPathType) {
|
||||
const { id: entityId, thumbnailPath } = person;
|
||||
switch (pathType) {
|
||||
case PersonPathType.FACE:
|
||||
case PersonPathType.FACE: {
|
||||
await this.moveFile({
|
||||
entityId,
|
||||
pathType,
|
||||
oldPath: thumbnailPath,
|
||||
newPath: StorageCore.getPersonThumbnailPath(person),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -168,7 +172,8 @@ export class StorageCore {
|
|||
this.logger.log(`Attempting to finish incomplete move: ${move.oldPath} => ${move.newPath}`);
|
||||
const oldPathExists = await this.repository.checkFileExists(move.oldPath);
|
||||
const newPathExists = await this.repository.checkFileExists(move.newPath);
|
||||
const actualPath = oldPathExists ? move.oldPath : newPathExists ? move.newPath : null;
|
||||
const newPathCheck = newPathExists ? move.newPath : null;
|
||||
const actualPath = oldPathExists ? move.oldPath : newPathCheck;
|
||||
if (!actualPath) {
|
||||
this.logger.warn('Unable to complete move. File does not exist at either location.');
|
||||
return;
|
||||
|
@ -177,13 +182,14 @@ export class StorageCore {
|
|||
const fileAtNewLocation = actualPath === move.newPath;
|
||||
this.logger.log(`Found file at ${fileAtNewLocation ? 'new' : 'old'} location`);
|
||||
|
||||
if (fileAtNewLocation) {
|
||||
if (!(await this.verifyNewPathContentsMatchesExpected(move.oldPath, move.newPath, assetInfo))) {
|
||||
this.logger.fatal(
|
||||
`Skipping move as file verification failed, old file is missing and new file is different to what was expected`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (
|
||||
fileAtNewLocation &&
|
||||
!(await this.verifyNewPathContentsMatchesExpected(move.oldPath, move.newPath, assetInfo))
|
||||
) {
|
||||
this.logger.fatal(
|
||||
`Skipping move as file verification failed, old file is missing and new file is different to what was expected`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
move = await this.moveRepository.update({ id: move.id, oldPath: actualPath, newPath });
|
||||
|
@ -200,10 +206,10 @@ export class StorageCore {
|
|||
try {
|
||||
this.logger.debug(`Attempting to rename file: ${move.oldPath} => ${newPath}`);
|
||||
await this.repository.rename(move.oldPath, newPath);
|
||||
} catch (err: any) {
|
||||
if (err.code !== 'EXDEV') {
|
||||
} catch (error: any) {
|
||||
if (error.code !== 'EXDEV') {
|
||||
this.logger.warn(
|
||||
`Unable to complete move. Error renaming file with code ${err.code} and message: ${err.message}`,
|
||||
`Unable to complete move. Error renaming file with code ${error.code} and message: ${error.message}`,
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
@ -218,8 +224,8 @@ export class StorageCore {
|
|||
|
||||
try {
|
||||
await this.repository.unlink(move.oldPath);
|
||||
} catch (err: any) {
|
||||
this.logger.warn(`Unable to delete old file, it will now no longer be tracked by Immich: ${err.message}`);
|
||||
} catch (error: any) {
|
||||
this.logger.warn(`Unable to delete old file, it will now no longer be tracked by Immich: ${error.message}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -233,14 +239,17 @@ export class StorageCore {
|
|||
newPath: string,
|
||||
assetInfo?: { sizeInBytes: number; checksum: Buffer },
|
||||
) {
|
||||
const oldPathSize = assetInfo ? assetInfo.sizeInBytes : (await this.repository.stat(oldPath)).size;
|
||||
const newPathSize = (await this.repository.stat(newPath)).size;
|
||||
const oldStat = await this.repository.stat(oldPath);
|
||||
const newStat = await this.repository.stat(newPath);
|
||||
const oldPathSize = assetInfo ? assetInfo.sizeInBytes : oldStat.size;
|
||||
const newPathSize = newStat.size;
|
||||
this.logger.debug(`File size check: ${newPathSize} === ${oldPathSize}`);
|
||||
if (newPathSize !== oldPathSize) {
|
||||
this.logger.warn(`Unable to complete move. File size mismatch: ${newPathSize} !== ${oldPathSize}`);
|
||||
return false;
|
||||
}
|
||||
if (assetInfo && (await this.configCore.getConfig()).storageTemplate.hashVerificationEnabled) {
|
||||
const config = await this.configCore.getConfig();
|
||||
if (assetInfo && config.storageTemplate.hashVerificationEnabled) {
|
||||
const { checksum } = assetInfo;
|
||||
const newChecksum = await this.cryptoRepository.hashFile(newPath);
|
||||
if (!newChecksum.equals(checksum)) {
|
||||
|
@ -266,23 +275,29 @@ export class StorageCore {
|
|||
|
||||
private savePath(pathType: PathType, id: string, newPath: string) {
|
||||
switch (pathType) {
|
||||
case AssetPathType.ORIGINAL:
|
||||
case AssetPathType.ORIGINAL: {
|
||||
return this.assetRepository.save({ id, originalPath: newPath });
|
||||
case AssetPathType.JPEG_THUMBNAIL:
|
||||
}
|
||||
case AssetPathType.JPEG_THUMBNAIL: {
|
||||
return this.assetRepository.save({ id, resizePath: newPath });
|
||||
case AssetPathType.WEBP_THUMBNAIL:
|
||||
}
|
||||
case AssetPathType.WEBP_THUMBNAIL: {
|
||||
return this.assetRepository.save({ id, webpPath: newPath });
|
||||
case AssetPathType.ENCODED_VIDEO:
|
||||
}
|
||||
case AssetPathType.ENCODED_VIDEO: {
|
||||
return this.assetRepository.save({ id, encodedVideoPath: newPath });
|
||||
case AssetPathType.SIDECAR:
|
||||
}
|
||||
case AssetPathType.SIDECAR: {
|
||||
return this.assetRepository.save({ id, sidecarPath: newPath });
|
||||
case PersonPathType.FACE:
|
||||
}
|
||||
case PersonPathType.FACE: {
|
||||
return this.personRepository.update({ id, thumbnailPath: newPath });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
static getNestedFolder(folder: StorageFolder, ownerId: string, filename: string): string {
|
||||
return join(StorageCore.getFolderLocation(folder, ownerId), filename.substring(0, 2), filename.substring(2, 4));
|
||||
return join(StorageCore.getFolderLocation(folder, ownerId), filename.slice(0, 2), filename.slice(2, 4));
|
||||
}
|
||||
|
||||
static getNestedPath(folder: StorageFolder, ownerId: string, filename: string): string {
|
||||
|
|
|
@ -132,7 +132,7 @@ export const defaults = Object.freeze<SystemConfig>({
|
|||
watch: {
|
||||
enabled: false,
|
||||
usePolling: false,
|
||||
interval: 10000,
|
||||
interval: 10_000,
|
||||
},
|
||||
},
|
||||
server: {
|
||||
|
@ -184,22 +184,30 @@ export class SystemConfigCore {
|
|||
const hasFeature = await this.hasFeature(feature);
|
||||
if (!hasFeature) {
|
||||
switch (feature) {
|
||||
case FeatureFlag.SMART_SEARCH:
|
||||
case FeatureFlag.SMART_SEARCH: {
|
||||
throw new BadRequestException('Smart search is not enabled');
|
||||
case FeatureFlag.FACIAL_RECOGNITION:
|
||||
}
|
||||
case FeatureFlag.FACIAL_RECOGNITION: {
|
||||
throw new BadRequestException('Facial recognition is not enabled');
|
||||
case FeatureFlag.SIDECAR:
|
||||
}
|
||||
case FeatureFlag.SIDECAR: {
|
||||
throw new BadRequestException('Sidecar is not enabled');
|
||||
case FeatureFlag.SEARCH:
|
||||
}
|
||||
case FeatureFlag.SEARCH: {
|
||||
throw new BadRequestException('Search is not enabled');
|
||||
case FeatureFlag.OAUTH:
|
||||
}
|
||||
case FeatureFlag.OAUTH: {
|
||||
throw new BadRequestException('OAuth is not enabled');
|
||||
case FeatureFlag.PASSWORD_LOGIN:
|
||||
}
|
||||
case FeatureFlag.PASSWORD_LOGIN: {
|
||||
throw new BadRequestException('Password login is not enabled');
|
||||
case FeatureFlag.CONFIG_FILE:
|
||||
}
|
||||
case FeatureFlag.CONFIG_FILE: {
|
||||
throw new BadRequestException('Config file is not set');
|
||||
default:
|
||||
}
|
||||
default: {
|
||||
throw new ForbiddenException(`Missing required feature: ${feature}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -278,9 +286,9 @@ export class SystemConfigCore {
|
|||
for (const validator of this.validators) {
|
||||
await validator(newConfig, oldConfig);
|
||||
}
|
||||
} catch (e) {
|
||||
this.logger.warn(`Unable to save system config due to a validation error: ${e}`);
|
||||
throw new BadRequestException(e instanceof Error ? e.message : e);
|
||||
} catch (error) {
|
||||
this.logger.warn(`Unable to save system config due to a validation error: ${error}`);
|
||||
throw new BadRequestException(error instanceof Error ? error.message : error);
|
||||
}
|
||||
|
||||
const updates: SystemConfigEntity[] = [];
|
||||
|
@ -330,19 +338,20 @@ export class SystemConfigCore {
|
|||
private async loadFromFile(filepath: string, force = false) {
|
||||
if (force || !this.configCache) {
|
||||
try {
|
||||
const file = JSON.parse((await this.repository.readFile(filepath)).toString());
|
||||
const file = await this.repository.readFile(filepath);
|
||||
const json = JSON.parse(file.toString());
|
||||
const overrides: SystemConfigEntity<SystemConfigValue>[] = [];
|
||||
|
||||
for (const key of Object.values(SystemConfigKey)) {
|
||||
const value = _.get(file, key);
|
||||
this.unsetDeep(file, key);
|
||||
const value = _.get(json, key);
|
||||
this.unsetDeep(json, key);
|
||||
if (value !== undefined) {
|
||||
overrides.push({ key, value });
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(file)) {
|
||||
this.logger.warn(`Unknown keys found: ${JSON.stringify(file, null, 2)}`);
|
||||
if (!_.isEmpty(json)) {
|
||||
this.logger.warn(`Unknown keys found: ${JSON.stringify(json, null, 2)}`);
|
||||
}
|
||||
|
||||
this.configCache = overrides;
|
||||
|
|
|
@ -136,7 +136,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
|
|||
watch: {
|
||||
enabled: false,
|
||||
usePolling: false,
|
||||
interval: 10000,
|
||||
interval: 10_000,
|
||||
},
|
||||
},
|
||||
});
|
||||
|
|
|
@ -121,7 +121,7 @@ export class SystemConfigService {
|
|||
private async setLogLevel({ logging }: SystemConfig) {
|
||||
const envLevel = this.getEnvLogLevel();
|
||||
const configLevel = logging.enabled ? logging.level : false;
|
||||
const level = envLevel ? envLevel : configLevel;
|
||||
const level = envLevel ?? configLevel;
|
||||
ImmichLogger.setLogLevel(level);
|
||||
this.logger.log(`LogLevel=${level} ${envLevel ? '(set via LOG_LEVEL)' : '(set via system config)'}`);
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ export class TagService {
|
|||
constructor(@Inject(ITagRepository) private repository: ITagRepository) {}
|
||||
|
||||
getAll(auth: AuthDto) {
|
||||
return this.repository.getAll(auth.user.id).then((tags) => tags.map(mapTag));
|
||||
return this.repository.getAll(auth.user.id).then((tags) => tags.map((tag) => mapTag(tag)));
|
||||
}
|
||||
|
||||
async getById(auth: AuthDto, id: string): Promise<TagResponseDto> {
|
||||
|
@ -78,10 +78,10 @@ export class TagService {
|
|||
const results: AssetIdsResponseDto[] = [];
|
||||
for (const assetId of dto.assetIds) {
|
||||
const hasAsset = await this.repository.hasAsset(auth.user.id, id, assetId);
|
||||
if (!hasAsset) {
|
||||
results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND });
|
||||
} else {
|
||||
if (hasAsset) {
|
||||
results.push({ assetId, success: true });
|
||||
} else {
|
||||
results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND });
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -5,10 +5,7 @@ import { IsEnum } from 'class-validator';
|
|||
export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => {
|
||||
const values = Object.values(UserAvatarColor);
|
||||
const randomIndex = Math.floor(
|
||||
user.email
|
||||
.split('')
|
||||
.map((letter) => letter.charCodeAt(0))
|
||||
.reduce((a, b) => a + b, 0) % values.length,
|
||||
[...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
|
||||
);
|
||||
return values[randomIndex] as UserAvatarColor;
|
||||
};
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import { LibraryType, UserEntity } from '@app/infra/entities';
|
||||
import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { ICryptoRepository, ILibraryRepository, IUserRepository } from '../repositories';
|
||||
import { UserResponseDto } from './response-dto';
|
||||
|
@ -97,7 +97,7 @@ export class UserCore {
|
|||
payload.password = await this.cryptoRepository.hashBcrypt(payload.password, SALT_ROUNDS);
|
||||
}
|
||||
if (payload.storageLabel) {
|
||||
payload.storageLabel = sanitize(payload.storageLabel.replace(/\./g, ''));
|
||||
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
|
||||
}
|
||||
const userEntity = await this.userRepository.create(payload);
|
||||
await this.libraryRepository.create({
|
||||
|
|
|
@ -418,7 +418,7 @@ describe(UserService.name, () => {
|
|||
|
||||
it('should default to a random password', async () => {
|
||||
userMock.getAdmin.mockResolvedValue(userStub.admin);
|
||||
const ask = jest.fn().mockResolvedValue(undefined);
|
||||
const ask = jest.fn().mockImplementation(() => {});
|
||||
|
||||
const response = await sut.resetAdminPassword(ask);
|
||||
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { UserEntity } from '@app/infra/entities';
|
||||
import { ImmichLogger } from '@app/infra/logger';
|
||||
import { BadRequestException, ForbiddenException, Inject, Injectable, NotFoundException } from '@nestjs/common';
|
||||
import { randomBytes } from 'crypto';
|
||||
import { randomBytes } from 'node:crypto';
|
||||
import { AuthDto } from '../auth';
|
||||
import { CacheControl, ImmichFileResponse } from '../domain.util';
|
||||
import { IEntityJob, JobName } from '../job';
|
||||
|
@ -39,7 +39,7 @@ export class UserService {
|
|||
|
||||
async getAll(auth: AuthDto, isAll: boolean): Promise<UserResponseDto[]> {
|
||||
const users = await this.userRepository.getList({ withDeleted: !isAll });
|
||||
return users.map(mapUser);
|
||||
return users.map((user) => mapUser(user));
|
||||
}
|
||||
|
||||
async get(userId: string): Promise<UserResponseDto> {
|
||||
|
@ -125,7 +125,7 @@ export class UserService {
|
|||
}
|
||||
|
||||
const providedPassword = await ask(mapUser(admin));
|
||||
const password = providedPassword || randomBytes(24).toString('base64').replace(/\W/g, '');
|
||||
const password = providedPassword || randomBytes(24).toString('base64').replaceAll(/\W/g, '');
|
||||
|
||||
await this.userCore.updateUser(admin, admin.id, { password });
|
||||
|
||||
|
@ -188,9 +188,10 @@ export class UserService {
|
|||
return false;
|
||||
}
|
||||
|
||||
const msInDay = 86400000;
|
||||
// TODO use luxon for date calculation
|
||||
const msInDay = 86_400_000;
|
||||
const msDeleteWait = msInDay * 7;
|
||||
const msSinceDelete = new Date().getTime() - (Date.parse(user.deletedAt.toString()) || 0);
|
||||
const msSinceDelete = Date.now() - (Date.parse(user.deletedAt.toString()) || 0);
|
||||
|
||||
return msSinceDelete >= msDeleteWait;
|
||||
}
|
||||
|
|
|
@ -13,20 +13,20 @@ export class ResetAdminPasswordCommand extends CommandRunner {
|
|||
super();
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const ask = (admin: UserResponseDto) => {
|
||||
const { id, oauthId, email, name } = admin;
|
||||
console.log(`Found Admin:
|
||||
ask = (admin: UserResponseDto) => {
|
||||
const { id, oauthId, email, name } = admin;
|
||||
console.log(`Found Admin:
|
||||
- ID=${id}
|
||||
- OAuth ID=${oauthId}
|
||||
- Email=${email}
|
||||
- Name=${name}`);
|
||||
|
||||
return this.inquirer.ask<{ password: string }>('prompt-password', undefined).then(({ password }) => password);
|
||||
};
|
||||
return this.inquirer.ask<{ password: string }>('prompt-password', {}).then(({ password }) => password);
|
||||
};
|
||||
|
||||
async run(): Promise<void> {
|
||||
try {
|
||||
const { password, provided } = await this.userService.resetAdminPassword(ask);
|
||||
const { password, provided } = await this.userService.resetAdminPassword(this.ask);
|
||||
|
||||
if (provided) {
|
||||
console.log(`The admin password has been updated.`);
|
||||
|
@ -46,7 +46,7 @@ export class PromptPasswordQuestions {
|
|||
message: 'Please choose a new password (optional)',
|
||||
name: 'password',
|
||||
})
|
||||
parsePassword(val: string) {
|
||||
return val;
|
||||
parsePassword(value: string) {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -38,7 +38,7 @@ export class AssetCore {
|
|||
isArchived: dto.isArchived ?? false,
|
||||
duration: dto.duration || null,
|
||||
isVisible: dto.isVisible ?? true,
|
||||
livePhotoVideo: livePhotoAssetId != null ? ({ id: livePhotoAssetId } as AssetEntity) : null,
|
||||
livePhotoVideo: livePhotoAssetId === null ? null : ({ id: livePhotoAssetId } as AssetEntity),
|
||||
resizePath: null,
|
||||
webpPath: null,
|
||||
thumbhash: null,
|
||||
|
|
|
@ -51,8 +51,8 @@ const _getAsset_1 = () => {
|
|||
asset_1.encodedVideoPath = '';
|
||||
asset_1.duration = '0:00:00.000000';
|
||||
asset_1.exifInfo = new ExifEntity();
|
||||
asset_1.exifInfo.latitude = 49.533547;
|
||||
asset_1.exifInfo.longitude = 10.703075;
|
||||
asset_1.exifInfo.latitude = 49.533_547;
|
||||
asset_1.exifInfo.longitude = 10.703_075;
|
||||
return asset_1;
|
||||
};
|
||||
|
||||
|
|
|
@ -27,7 +27,6 @@ import { AssetSearchDto } from './dto/asset-search.dto';
|
|||
import { CheckExistingAssetsDto } from './dto/check-existing-assets.dto';
|
||||
import { CreateAssetDto } from './dto/create-asset.dto';
|
||||
import { GetAssetThumbnailDto, GetAssetThumbnailFormatEnum } from './dto/get-asset-thumbnail.dto';
|
||||
import { SearchPropertiesDto } from './dto/search-properties.dto';
|
||||
import { ServeFileDto } from './dto/serve-file.dto';
|
||||
import {
|
||||
AssetBulkUploadCheckResponseDto,
|
||||
|
@ -163,7 +162,8 @@ export class AssetService {
|
|||
const possibleSearchTerm = new Set<string>();
|
||||
|
||||
const rows = await this.assetRepositoryV1.getSearchPropertiesByUserId(auth.user.id);
|
||||
rows.forEach((row: SearchPropertiesDto) => {
|
||||
|
||||
for (const row of rows) {
|
||||
// tags
|
||||
row.tags?.map((tag: string) => possibleSearchTerm.add(tag?.toLowerCase()));
|
||||
|
||||
|
@ -187,9 +187,9 @@ export class AssetService {
|
|||
possibleSearchTerm.add(row.city?.toLowerCase() || '');
|
||||
possibleSearchTerm.add(row.state?.toLowerCase() || '');
|
||||
possibleSearchTerm.add(row.country?.toLowerCase() || '');
|
||||
});
|
||||
}
|
||||
|
||||
return Array.from(possibleSearchTerm).filter((x) => x != null && x != '');
|
||||
return [...possibleSearchTerm].filter((x) => x != null && x != '');
|
||||
}
|
||||
|
||||
async getCuratedLocation(auth: AuthDto): Promise<CuratedLocationsResponseDto[]> {
|
||||
|
@ -249,18 +249,18 @@ export class AssetService {
|
|||
|
||||
private getThumbnailPath(asset: AssetEntity, format: GetAssetThumbnailFormatEnum) {
|
||||
switch (format) {
|
||||
case GetAssetThumbnailFormatEnum.WEBP:
|
||||
case GetAssetThumbnailFormatEnum.WEBP: {
|
||||
if (asset.webpPath) {
|
||||
return asset.webpPath;
|
||||
}
|
||||
this.logger.warn(`WebP thumbnail requested but not found for asset ${asset.id}, falling back to JPEG`);
|
||||
|
||||
case GetAssetThumbnailFormatEnum.JPEG:
|
||||
default:
|
||||
}
|
||||
case GetAssetThumbnailFormatEnum.JPEG: {
|
||||
if (!asset.resizePath) {
|
||||
throw new NotFoundException(`No thumbnail found for asset ${asset.id}`);
|
||||
}
|
||||
return asset.resizePath;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -50,8 +50,8 @@ export const SharedLinkRoute = () =>
|
|||
applyDecorators(SetMetadata(Metadata.SHARED_ROUTE, true), ApiQuery({ name: 'key', type: String, required: false }));
|
||||
export const AdminRoute = (value = true) => SetMetadata(Metadata.ADMIN_ROUTE, value);
|
||||
|
||||
export const Auth = createParamDecorator((data, ctx: ExecutionContext): AuthDto => {
|
||||
return ctx.switchToHttp().getRequest<{ user: AuthDto }>().user;
|
||||
export const Auth = createParamDecorator((data, context: ExecutionContext): AuthDto => {
|
||||
return context.switchToHttp().getRequest<{ user: AuthDto }>().user;
|
||||
});
|
||||
|
||||
export const FileResponse = () =>
|
||||
|
@ -59,15 +59,15 @@ export const FileResponse = () =>
|
|||
content: { 'application/octet-stream': { schema: { type: 'string', format: 'binary' } } },
|
||||
});
|
||||
|
||||
export const GetLoginDetails = createParamDecorator((data, ctx: ExecutionContext): LoginDetails => {
|
||||
const req = ctx.switchToHttp().getRequest<Request>();
|
||||
const userAgent = UAParser(req.headers['user-agent']);
|
||||
export const GetLoginDetails = createParamDecorator((data, context: ExecutionContext): LoginDetails => {
|
||||
const request = context.switchToHttp().getRequest<Request>();
|
||||
const userAgent = UAParser(request.headers['user-agent']);
|
||||
|
||||
return {
|
||||
clientIp: req.ip,
|
||||
isSecure: req.secure,
|
||||
deviceType: userAgent.browser.name || userAgent.device.type || (req.headers.devicemodel as string) || '',
|
||||
deviceOS: userAgent.os.name || (req.headers.devicetype as string) || '',
|
||||
clientIp: request.ip,
|
||||
isSecure: request.secure,
|
||||
deviceType: userAgent.browser.name || userAgent.device.type || (request.headers.devicemodel as string) || '',
|
||||
deviceOS: userAgent.os.name || (request.headers.devicetype as string) || '',
|
||||
};
|
||||
});
|
||||
|
||||
|
@ -95,20 +95,20 @@ export class AppGuard implements CanActivate {
|
|||
return true;
|
||||
}
|
||||
|
||||
const req = context.switchToHttp().getRequest<AuthRequest>();
|
||||
const request = context.switchToHttp().getRequest<AuthRequest>();
|
||||
|
||||
const authDto = await this.authService.validate(req.headers, req.query as Record<string, string>);
|
||||
const authDto = await this.authService.validate(request.headers, request.query as Record<string, string>);
|
||||
if (authDto.sharedLink && !isSharedRoute) {
|
||||
this.logger.warn(`Denied access to non-shared route: ${req.path}`);
|
||||
this.logger.warn(`Denied access to non-shared route: ${request.path}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
if (isAdminRoute && !authDto.user.isAdmin) {
|
||||
this.logger.warn(`Denied access to admin only route: ${req.path}`);
|
||||
this.logger.warn(`Denied access to admin only route: ${request.path}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
req.user = authDto;
|
||||
request.user = authDto;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import { ImmichLogger } from '@app/infra/logger';
|
|||
import { Injectable } from '@nestjs/common';
|
||||
import { Cron, CronExpression, Interval } from '@nestjs/schedule';
|
||||
import { NextFunction, Request, Response } from 'express';
|
||||
import { readFileSync } from 'fs';
|
||||
import { readFileSync } from 'node:fs';
|
||||
|
||||
const render = (index: string, meta: OpenGraphTags) => {
|
||||
const tags = `
|
||||
|
@ -79,15 +79,15 @@ export class AppService {
|
|||
let index = '';
|
||||
try {
|
||||
index = readFileSync(WEB_ROOT_PATH).toString();
|
||||
} catch (error: Error | any) {
|
||||
} catch {
|
||||
this.logger.warn('Unable to open `www/index.html, skipping SSR.');
|
||||
}
|
||||
|
||||
return async (req: Request, res: Response, next: NextFunction) => {
|
||||
return async (request: Request, res: Response, next: NextFunction) => {
|
||||
if (
|
||||
req.url.startsWith('/api') ||
|
||||
req.method.toLowerCase() !== 'get' ||
|
||||
excludePaths.find((item) => req.url.startsWith(item))
|
||||
request.url.startsWith('/api') ||
|
||||
request.method.toLowerCase() !== 'get' ||
|
||||
excludePaths.some((item) => request.url.startsWith(item))
|
||||
) {
|
||||
return next();
|
||||
}
|
||||
|
@ -107,7 +107,7 @@ export class AppService {
|
|||
|
||||
try {
|
||||
for (const { regex, onMatch } of targets) {
|
||||
const matches = req.url.match(regex);
|
||||
const matches = request.url.match(regex);
|
||||
if (matches) {
|
||||
const meta = await onMatch(matches);
|
||||
if (meta) {
|
||||
|
|
|
@ -18,11 +18,11 @@ import {
|
|||
SwaggerModule,
|
||||
} from '@nestjs/swagger';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { writeFileSync } from 'fs';
|
||||
import { access, constants } from 'fs/promises';
|
||||
import _ from 'lodash';
|
||||
import path, { isAbsolute } from 'path';
|
||||
import { promisify } from 'util';
|
||||
import { writeFileSync } from 'node:fs';
|
||||
import { access, constants } from 'node:fs/promises';
|
||||
import path, { isAbsolute } from 'node:path';
|
||||
import { promisify } from 'node:util';
|
||||
|
||||
import { applyDecorators, UsePipes, ValidationPipe } from '@nestjs/common';
|
||||
import { SchemaObject } from '@nestjs/swagger/dist/interfaces/open-api-spec.interface';
|
||||
|
@ -55,13 +55,15 @@ export const sendFile = async (
|
|||
try {
|
||||
const file = await handler();
|
||||
switch (file.cacheControl) {
|
||||
case CacheControl.PRIVATE_WITH_CACHE:
|
||||
case CacheControl.PRIVATE_WITH_CACHE: {
|
||||
res.set('Cache-Control', 'private, max-age=86400, no-transform');
|
||||
break;
|
||||
}
|
||||
|
||||
case CacheControl.PRIVATE_WITHOUT_CACHE:
|
||||
case CacheControl.PRIVATE_WITHOUT_CACHE: {
|
||||
res.set('Cache-Control', 'private, no-cache, no-transform');
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
res.header('Content-Type', file.contentType);
|
||||
|
@ -94,21 +96,21 @@ export const asStreamableFile = ({ stream, type, length }: ImmichReadStream) =>
|
|||
return new StreamableFile(stream, { type, length });
|
||||
};
|
||||
|
||||
function sortKeys<T>(obj: T): T {
|
||||
if (!obj || typeof obj !== 'object' || Array.isArray(obj)) {
|
||||
return obj;
|
||||
function sortKeys<T>(target: T): T {
|
||||
if (!target || typeof target !== 'object' || Array.isArray(target)) {
|
||||
return target;
|
||||
}
|
||||
|
||||
const result: Partial<T> = {};
|
||||
const keys = Object.keys(obj).sort() as Array<keyof T>;
|
||||
const keys = Object.keys(target).sort() as Array<keyof T>;
|
||||
for (const key of keys) {
|
||||
result[key] = sortKeys(obj[key]);
|
||||
result[key] = sortKeys(target[key]);
|
||||
}
|
||||
return result as T;
|
||||
}
|
||||
|
||||
export const routeToErrorMessage = (methodName: string) =>
|
||||
'Failed to ' + methodName.replace(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`);
|
||||
'Failed to ' + methodName.replaceAll(/[A-Z]+/g, (letter) => ` ${letter.toLowerCase()}`);
|
||||
|
||||
const patchOpenAPI = (document: OpenAPIObject) => {
|
||||
document.paths = sortKeys(document.paths);
|
||||
|
@ -152,7 +154,7 @@ const patchOpenAPI = (document: OpenAPIObject) => {
|
|||
continue;
|
||||
}
|
||||
|
||||
if ((operation.security || []).find((item) => !!item[Metadata.PUBLIC_SECURITY])) {
|
||||
if ((operation.security || []).some((item) => !!item[Metadata.PUBLIC_SECURITY])) {
|
||||
delete operation.security;
|
||||
}
|
||||
|
||||
|
@ -177,7 +179,7 @@ const patchOpenAPI = (document: OpenAPIObject) => {
|
|||
return document;
|
||||
};
|
||||
|
||||
export const useSwagger = (app: INestApplication, isDev: boolean) => {
|
||||
export const useSwagger = (app: INestApplication, isDevelopment: boolean) => {
|
||||
const config = new DocumentBuilder()
|
||||
.setTitle('Immich')
|
||||
.setDescription('Immich API')
|
||||
|
@ -203,7 +205,7 @@ export const useSwagger = (app: INestApplication, isDev: boolean) => {
|
|||
operationIdFactory: (controllerKey: string, methodKey: string) => methodKey,
|
||||
};
|
||||
|
||||
const doc = SwaggerModule.createDocument(app, config, options);
|
||||
const specification = SwaggerModule.createDocument(app, config, options);
|
||||
|
||||
const customOptions: SwaggerCustomOptions = {
|
||||
swaggerOptions: {
|
||||
|
@ -212,11 +214,11 @@ export const useSwagger = (app: INestApplication, isDev: boolean) => {
|
|||
customSiteTitle: 'Immich API Documentation',
|
||||
};
|
||||
|
||||
SwaggerModule.setup('doc', app, doc, customOptions);
|
||||
SwaggerModule.setup('doc', app, specification, customOptions);
|
||||
|
||||
if (isDev) {
|
||||
if (isDevelopment) {
|
||||
// Generate API Documentation only in development mode
|
||||
const outputPath = path.resolve(process.cwd(), '../open-api/immich-openapi-specs.json');
|
||||
writeFileSync(outputPath, JSON.stringify(patchOpenAPI(doc), null, 2), { encoding: 'utf8' });
|
||||
writeFileSync(outputPath, JSON.stringify(patchOpenAPI(specification), null, 2), { encoding: 'utf8' });
|
||||
}
|
||||
};
|
||||
|
|
|
@ -78,13 +78,13 @@ export class AuthController {
|
|||
@Post('logout')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
logout(
|
||||
@Req() req: Request,
|
||||
@Req() request: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
@Auth() auth: AuthDto,
|
||||
): Promise<LogoutResponseDto> {
|
||||
res.clearCookie(IMMICH_ACCESS_COOKIE);
|
||||
res.clearCookie(IMMICH_AUTH_TYPE_COOKIE);
|
||||
|
||||
return this.service.logout(auth, (req.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]);
|
||||
return this.service.logout(auth, (request.cookies || {})[IMMICH_AUTH_TYPE_COOKIE]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -25,9 +25,9 @@ export class OAuthController {
|
|||
@PublicRoute()
|
||||
@Get('mobile-redirect')
|
||||
@Redirect()
|
||||
redirectOAuthToMobile(@Req() req: Request) {
|
||||
redirectOAuthToMobile(@Req() request: Request) {
|
||||
return {
|
||||
url: this.service.getMobileRedirect(req.url),
|
||||
url: this.service.getMobileRedirect(request.url),
|
||||
statusCode: HttpStatus.TEMPORARY_REDIRECT,
|
||||
};
|
||||
}
|
||||
|
|
|
@ -33,10 +33,10 @@ export class SharedLinkController {
|
|||
async getMySharedLink(
|
||||
@Auth() auth: AuthDto,
|
||||
@Query() dto: SharedLinkPasswordDto,
|
||||
@Req() req: Request,
|
||||
@Req() request: Request,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
): Promise<SharedLinkResponseDto> {
|
||||
const sharedLinkToken = req.cookies?.[IMMICH_SHARED_LINK_ACCESS_COOKIE];
|
||||
const sharedLinkToken = request.cookies?.[IMMICH_SHARED_LINK_ACCESS_COOKIE];
|
||||
if (sharedLinkToken) {
|
||||
dto.token = sharedLinkToken;
|
||||
}
|
||||
|
|
|
@ -4,9 +4,9 @@ import { CallHandler, ExecutionContext, Injectable, NestInterceptor } from '@nes
|
|||
import { PATH_METADATA } from '@nestjs/common/constants';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { transformException } from '@nestjs/platform-express/multer/multer/multer.utils';
|
||||
import { createHash, randomUUID } from 'crypto';
|
||||
import { NextFunction, RequestHandler } from 'express';
|
||||
import multer, { StorageEngine, diskStorage } from 'multer';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { Observable } from 'rxjs';
|
||||
import { AuthRequest } from '../app.guard';
|
||||
|
||||
|
@ -40,17 +40,17 @@ interface Callback<T> {
|
|||
(error: null, result: T): void;
|
||||
}
|
||||
|
||||
const callbackify = async <T>(fn: (...args: any[]) => T, callback: Callback<T>) => {
|
||||
const callbackify = async <T>(target: (...arguments_: any[]) => T, callback: Callback<T>) => {
|
||||
try {
|
||||
return callback(null, await fn());
|
||||
return callback(null, await target());
|
||||
} catch (error: Error | any) {
|
||||
return callback(error);
|
||||
}
|
||||
};
|
||||
|
||||
const asRequest = (req: AuthRequest, file: Express.Multer.File) => {
|
||||
const asRequest = (request: AuthRequest, file: Express.Multer.File) => {
|
||||
return {
|
||||
auth: req.user || null,
|
||||
auth: request.user || null,
|
||||
fieldName: file.fieldname as UploadFieldName,
|
||||
file: mapToUploadFile(file as ImmichFile),
|
||||
};
|
||||
|
@ -94,14 +94,14 @@ export class FileUploadInterceptor implements NestInterceptor {
|
|||
}
|
||||
|
||||
async intercept(context: ExecutionContext, next: CallHandler<any>): Promise<Observable<any>> {
|
||||
const ctx = context.switchToHttp();
|
||||
const context_ = context.switchToHttp();
|
||||
const route = this.reflect.get<string>(PATH_METADATA, context.getClass());
|
||||
|
||||
const handler: RequestHandler | null = this.getHandler(route as Route);
|
||||
if (handler) {
|
||||
await new Promise<void>((resolve, reject) => {
|
||||
const next: NextFunction = (error) => (error ? reject(transformException(error)) : resolve());
|
||||
handler(ctx.getRequest(), ctx.getResponse(), next);
|
||||
handler(context_.getRequest(), context_.getResponse(), next);
|
||||
});
|
||||
} else {
|
||||
this.logger.warn(`Skipping invalid file upload route: ${route}`);
|
||||
|
@ -110,28 +110,31 @@ export class FileUploadInterceptor implements NestInterceptor {
|
|||
return next.handle();
|
||||
}
|
||||
|
||||
private fileFilter(req: AuthRequest, file: Express.Multer.File, callback: multer.FileFilterCallback) {
|
||||
return callbackify(() => this.assetService.canUploadFile(asRequest(req, file)), callback);
|
||||
private fileFilter(request: AuthRequest, file: Express.Multer.File, callback: multer.FileFilterCallback) {
|
||||
return callbackify(() => this.assetService.canUploadFile(asRequest(request, file)), callback);
|
||||
}
|
||||
|
||||
private filename(req: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
|
||||
return callbackify(() => this.assetService.getUploadFilename(asRequest(req, file)), callback as Callback<string>);
|
||||
private filename(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
|
||||
return callbackify(
|
||||
() => this.assetService.getUploadFilename(asRequest(request, file)),
|
||||
callback as Callback<string>,
|
||||
);
|
||||
}
|
||||
|
||||
private destination(req: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
|
||||
return callbackify(() => this.assetService.getUploadFolder(asRequest(req, file)), callback as Callback<string>);
|
||||
private destination(request: AuthRequest, file: Express.Multer.File, callback: DiskStorageCallback) {
|
||||
return callbackify(() => this.assetService.getUploadFolder(asRequest(request, file)), callback as Callback<string>);
|
||||
}
|
||||
|
||||
private handleFile(req: AuthRequest, file: Express.Multer.File, callback: Callback<Partial<ImmichFile>>) {
|
||||
private handleFile(request: AuthRequest, file: Express.Multer.File, callback: Callback<Partial<ImmichFile>>) {
|
||||
(file as ImmichMulterFile).uuid = randomUUID();
|
||||
if (!this.isAssetUploadFile(file)) {
|
||||
this.defaultStorage._handleFile(req, file, callback);
|
||||
this.defaultStorage._handleFile(request, file, callback);
|
||||
return;
|
||||
}
|
||||
|
||||
const hash = createHash('sha1');
|
||||
file.stream.on('data', (chunk) => hash.update(chunk));
|
||||
this.defaultStorage._handleFile(req, file, (error, info) => {
|
||||
this.defaultStorage._handleFile(request, file, (error, info) => {
|
||||
if (error) {
|
||||
hash.destroy();
|
||||
callback(error);
|
||||
|
@ -141,15 +144,16 @@ export class FileUploadInterceptor implements NestInterceptor {
|
|||
});
|
||||
}
|
||||
|
||||
private removeFile(req: AuthRequest, file: Express.Multer.File, callback: (error: Error | null) => void) {
|
||||
this.defaultStorage._removeFile(req, file, callback);
|
||||
private removeFile(request: AuthRequest, file: Express.Multer.File, callback: (error: Error | null) => void) {
|
||||
this.defaultStorage._removeFile(request, file, callback);
|
||||
}
|
||||
|
||||
private isAssetUploadFile(file: Express.Multer.File) {
|
||||
switch (file.fieldname as UploadFieldName) {
|
||||
case UploadFieldName.ASSET_DATA:
|
||||
case UploadFieldName.LIVE_PHOTO_DATA:
|
||||
case UploadFieldName.LIVE_PHOTO_DATA: {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
|
@ -157,14 +161,17 @@ export class FileUploadInterceptor implements NestInterceptor {
|
|||
|
||||
private getHandler(route: Route) {
|
||||
switch (route) {
|
||||
case Route.ASSET:
|
||||
case Route.ASSET: {
|
||||
return this.handlers.assetUpload;
|
||||
}
|
||||
|
||||
case Route.USER:
|
||||
case Route.USER: {
|
||||
return this.handlers.userProfile;
|
||||
}
|
||||
|
||||
default:
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -6,12 +6,13 @@ const urlOrParts = url
|
|||
? { url }
|
||||
: {
|
||||
host: process.env.DB_HOSTNAME || 'localhost',
|
||||
port: parseInt(process.env.DB_PORT || '5432'),
|
||||
port: Number.parseInt(process.env.DB_PORT || '5432'),
|
||||
username: process.env.DB_USERNAME || 'postgres',
|
||||
password: process.env.DB_PASSWORD || 'postgres',
|
||||
database: process.env.DB_DATABASE_NAME || 'immich',
|
||||
};
|
||||
|
||||
/* eslint unicorn/prefer-module: "off" -- We can fix this when migrating to ESM*/
|
||||
export const databaseConfig: PostgresConnectionOptions = {
|
||||
type: 'postgres',
|
||||
entities: [__dirname + '/entities/*.entity.{js,ts}'],
|
||||
|
@ -19,7 +20,7 @@ export const databaseConfig: PostgresConnectionOptions = {
|
|||
migrations: [__dirname + '/migrations/*.{js,ts}'],
|
||||
subscribers: [__dirname + '/subscribers/*.{js,ts}'],
|
||||
migrationsRun: false,
|
||||
connectTimeoutMS: 10000, // 10 seconds
|
||||
connectTimeoutMS: 10_000, // 10 seconds
|
||||
parseInt8: true,
|
||||
...urlOrParts,
|
||||
};
|
||||
|
|
|
@ -15,8 +15,8 @@ function parseRedisConfig(): RedisOptions {
|
|||
}
|
||||
return {
|
||||
host: process.env.REDIS_HOSTNAME || 'immich_redis',
|
||||
port: parseInt(process.env.REDIS_PORT || '6379'),
|
||||
db: parseInt(process.env.REDIS_DBINDEX || '0'),
|
||||
port: Number.parseInt(process.env.REDIS_PORT || '6379'),
|
||||
db: Number.parseInt(process.env.REDIS_DBINDEX || '0'),
|
||||
username: process.env.REDIS_USERNAME || undefined,
|
||||
password: process.env.REDIS_PASSWORD || undefined,
|
||||
path: process.env.REDIS_SOCKET || undefined,
|
||||
|
|
|
@ -27,4 +27,4 @@ export const DummyValue = {
|
|||
// maximum number of parameters is 65535. Any query that tries to bind more than that (e.g. searching
|
||||
// by a list of IDs) requires splitting the query into multiple chunks.
|
||||
// We are rounding down this limit, as queries commonly include other filters and parameters.
|
||||
export const DATABASE_PARAMETER_CHUNK_SIZE = 65500;
|
||||
export const DATABASE_PARAMETER_CHUNK_SIZE = 65_500;
|
||||
|
|
|
@ -59,21 +59,25 @@ export const isValidInteger = (value: number, options: { min?: number; max?: num
|
|||
export function Chunked(options: { paramIndex?: number; mergeFn?: (results: any) => any } = {}): MethodDecorator {
|
||||
return (target: any, propertyKey: string | symbol, descriptor: PropertyDescriptor) => {
|
||||
const originalMethod = descriptor.value;
|
||||
const paramIndex = options.paramIndex ?? 0;
|
||||
descriptor.value = async function (...args: any[]) {
|
||||
const arg = args[paramIndex];
|
||||
const parameterIndex = options.paramIndex ?? 0;
|
||||
descriptor.value = async function (...arguments_: any[]) {
|
||||
const argument = arguments_[parameterIndex];
|
||||
|
||||
// Early return if argument length is less than or equal to the chunk size.
|
||||
if (
|
||||
(arg instanceof Array && arg.length <= DATABASE_PARAMETER_CHUNK_SIZE) ||
|
||||
(arg instanceof Set && arg.size <= DATABASE_PARAMETER_CHUNK_SIZE)
|
||||
(Array.isArray(argument) && argument.length <= DATABASE_PARAMETER_CHUNK_SIZE) ||
|
||||
(argument instanceof Set && argument.size <= DATABASE_PARAMETER_CHUNK_SIZE)
|
||||
) {
|
||||
return await originalMethod.apply(this, args);
|
||||
return await originalMethod.apply(this, arguments_);
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
chunks(arg, DATABASE_PARAMETER_CHUNK_SIZE).map(async (chunk) => {
|
||||
await originalMethod.apply(this, [...args.slice(0, paramIndex), chunk, ...args.slice(paramIndex + 1)]);
|
||||
chunks(argument, DATABASE_PARAMETER_CHUNK_SIZE).map(async (chunk) => {
|
||||
await Reflect.apply(originalMethod, this, [
|
||||
...arguments_.slice(0, parameterIndex),
|
||||
chunk,
|
||||
...arguments_.slice(parameterIndex + 1),
|
||||
]);
|
||||
}),
|
||||
).then((results) => (options.mergeFn ? options.mergeFn(results) : results));
|
||||
};
|
||||
|
|
|
@ -24,7 +24,8 @@ export class AddLibraries1688392120838 implements MigrationInterface {
|
|||
);
|
||||
|
||||
// Create default library for each user and assign all assets to it
|
||||
const userIds: string[] = (await queryRunner.query(`SELECT id FROM "users"`)).map((user: any) => user.id);
|
||||
const users = await queryRunner.query(`SELECT id FROM "users"`);
|
||||
const userIds: string[] = users.map((user: any) => user.id);
|
||||
|
||||
for (const userId of userIds) {
|
||||
await queryRunner.query(
|
||||
|
|
|
@ -14,7 +14,7 @@ export class UsePgVectors1700713871511 implements MigrationInterface {
|
|||
|
||||
const clipModelNameQuery = await queryRunner.query(`SELECT value FROM system_config WHERE key = 'machineLearning.clip.modelName'`);
|
||||
const clipModelName: string = clipModelNameQuery?.[0]?.['value'] ?? 'ViT-B-32__openai';
|
||||
const clipDimSize = getCLIPModelInfo(clipModelName.replace(/"/g, '')).dimSize;
|
||||
const clipDimSize = getCLIPModelInfo(clipModelName.replaceAll('"', '')).dimSize;
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE asset_faces
|
||||
|
|
|
@ -167,7 +167,7 @@ class AlbumAccess implements IAlbumAccess {
|
|||
})
|
||||
.then(
|
||||
(sharedLinks) =>
|
||||
new Set(sharedLinks.flatMap((sharedLink) => (!!sharedLink.albumId ? [sharedLink.albumId] : []))),
|
||||
new Set(sharedLinks.flatMap((sharedLink) => (sharedLink.albumId ? [sharedLink.albumId] : []))),
|
||||
),
|
||||
),
|
||||
).then((results) => setUnion(...results));
|
||||
|
|
|
@ -71,7 +71,7 @@ export class AlbumRepository implements IAlbumRepository {
|
|||
@ChunkedArray()
|
||||
async getMetadataForIds(ids: string[]): Promise<AlbumAssetCount[]> {
|
||||
// Guard against running invalid query when ids list is empty.
|
||||
if (!ids.length) {
|
||||
if (ids.length === 0) {
|
||||
return [];
|
||||
}
|
||||
|
||||
|
|
|
@ -24,7 +24,7 @@ import { Injectable } from '@nestjs/common';
|
|||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import _ from 'lodash';
|
||||
import { DateTime } from 'luxon';
|
||||
import path from 'path';
|
||||
import path from 'node:path';
|
||||
import {
|
||||
And,
|
||||
Brackets,
|
||||
|
@ -471,7 +471,7 @@ export class AssetRepository implements IAssetRepository {
|
|||
let where: FindOptionsWhere<AssetEntity> | FindOptionsWhere<AssetEntity>[] = {};
|
||||
|
||||
switch (property) {
|
||||
case WithoutProperty.THUMBNAIL:
|
||||
case WithoutProperty.THUMBNAIL: {
|
||||
where = [
|
||||
{ resizePath: IsNull(), isVisible: true },
|
||||
{ resizePath: '', isVisible: true },
|
||||
|
@ -480,15 +480,17 @@ export class AssetRepository implements IAssetRepository {
|
|||
{ thumbhash: IsNull(), isVisible: true },
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
case WithoutProperty.ENCODED_VIDEO:
|
||||
case WithoutProperty.ENCODED_VIDEO: {
|
||||
where = [
|
||||
{ type: AssetType.VIDEO, encodedVideoPath: IsNull() },
|
||||
{ type: AssetType.VIDEO, encodedVideoPath: '' },
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
case WithoutProperty.EXIF:
|
||||
case WithoutProperty.EXIF: {
|
||||
relations = {
|
||||
exifInfo: true,
|
||||
jobStatus: true,
|
||||
|
@ -500,8 +502,9 @@ export class AssetRepository implements IAssetRepository {
|
|||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case WithoutProperty.SMART_SEARCH:
|
||||
case WithoutProperty.SMART_SEARCH: {
|
||||
relations = {
|
||||
smartSearch: true,
|
||||
};
|
||||
|
@ -513,8 +516,9 @@ export class AssetRepository implements IAssetRepository {
|
|||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case WithoutProperty.OBJECT_TAGS:
|
||||
case WithoutProperty.OBJECT_TAGS: {
|
||||
relations = {
|
||||
smartInfo: true,
|
||||
};
|
||||
|
@ -526,8 +530,9 @@ export class AssetRepository implements IAssetRepository {
|
|||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case WithoutProperty.FACES:
|
||||
case WithoutProperty.FACES: {
|
||||
relations = {
|
||||
faces: true,
|
||||
jobStatus: true,
|
||||
|
@ -544,8 +549,9 @@ export class AssetRepository implements IAssetRepository {
|
|||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case WithoutProperty.PERSON:
|
||||
case WithoutProperty.PERSON: {
|
||||
relations = {
|
||||
faces: true,
|
||||
};
|
||||
|
@ -558,16 +564,19 @@ export class AssetRepository implements IAssetRepository {
|
|||
},
|
||||
};
|
||||
break;
|
||||
}
|
||||
|
||||
case WithoutProperty.SIDECAR:
|
||||
case WithoutProperty.SIDECAR: {
|
||||
where = [
|
||||
{ sidecarPath: IsNull(), isVisible: true },
|
||||
{ sidecarPath: '', isVisible: true },
|
||||
];
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
default: {
|
||||
throw new Error(`Invalid getWithout property: ${property}`);
|
||||
}
|
||||
}
|
||||
|
||||
return paginate(this.repository, pagination, {
|
||||
|
@ -584,18 +593,21 @@ export class AssetRepository implements IAssetRepository {
|
|||
let where: FindOptionsWhere<AssetEntity> | FindOptionsWhere<AssetEntity>[] = {};
|
||||
|
||||
switch (property) {
|
||||
case WithProperty.SIDECAR:
|
||||
case WithProperty.SIDECAR: {
|
||||
where = [{ sidecarPath: Not(IsNull()), isVisible: true }];
|
||||
break;
|
||||
case WithProperty.IS_OFFLINE:
|
||||
}
|
||||
case WithProperty.IS_OFFLINE: {
|
||||
if (!libraryId) {
|
||||
throw new Error('Library id is required when finding offline assets');
|
||||
}
|
||||
where = [{ isOffline: true, libraryId: libraryId }];
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
default: {
|
||||
throw new Error(`Invalid getWith property: ${property}`);
|
||||
}
|
||||
}
|
||||
|
||||
return paginate(this.repository, pagination, {
|
||||
|
|
|
@ -51,13 +51,15 @@ export class CommunicationRepository
|
|||
|
||||
on(event: 'connect' | ServerEvent, callback: OnConnectCallback | OnServerEventCallback) {
|
||||
switch (event) {
|
||||
case 'connect':
|
||||
case 'connect': {
|
||||
this.onConnectCallbacks.push(callback);
|
||||
break;
|
||||
}
|
||||
|
||||
default:
|
||||
default: {
|
||||
this.onServerEventCallbacks[event].push(callback as OnServerEventCallback);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { ICryptoRepository } from '@app/domain';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { compareSync, hash } from 'bcrypt';
|
||||
import { createHash, randomBytes, randomUUID } from 'crypto';
|
||||
import { createReadStream } from 'fs';
|
||||
import { createHash, randomBytes, randomUUID } from 'node:crypto';
|
||||
import { createReadStream } from 'node:fs';
|
||||
|
||||
@Injectable()
|
||||
export class CryptoRepository implements ICryptoRepository {
|
||||
|
@ -24,7 +24,7 @@ export class CryptoRepository implements ICryptoRepository {
|
|||
return new Promise<Buffer>((resolve, reject) => {
|
||||
const hash = createHash('sha1');
|
||||
const stream = createReadStream(filepath);
|
||||
stream.on('error', (err) => reject(err));
|
||||
stream.on('error', (error) => reject(error));
|
||||
stream.on('data', (chunk) => hash.update(chunk));
|
||||
stream.on('end', () => resolve(hash.digest()));
|
||||
});
|
||||
|
|
|
@ -10,10 +10,10 @@ import {
|
|||
import { ImmichLogger } from '@app/infra/logger';
|
||||
import archiver from 'archiver';
|
||||
import chokidar, { WatchOptions } from 'chokidar';
|
||||
import { constants, createReadStream, existsSync, mkdirSync } from 'fs';
|
||||
import fs, { copyFile, readdir, rename, writeFile } from 'fs/promises';
|
||||
import { glob } from 'glob';
|
||||
import path from 'path';
|
||||
import { constants, createReadStream, existsSync, mkdirSync } from 'node:fs';
|
||||
import fs, { copyFile, readdir, rename, writeFile } from 'node:fs/promises';
|
||||
import path from 'node:path';
|
||||
|
||||
export class FilesystemProvider implements IStorageRepository {
|
||||
private logger = new ImmichLogger(FilesystemProvider.name);
|
||||
|
@ -60,7 +60,7 @@ export class FilesystemProvider implements IStorageRepository {
|
|||
try {
|
||||
await fs.access(filepath, mode);
|
||||
return true;
|
||||
} catch (_) {
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
@ -68,11 +68,11 @@ export class FilesystemProvider implements IStorageRepository {
|
|||
async unlink(file: string) {
|
||||
try {
|
||||
await fs.unlink(file);
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException)?.code === 'ENOENT') {
|
||||
this.logger.warn(`File ${file} does not exist.`);
|
||||
} else {
|
||||
throw err;
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -15,7 +15,7 @@ import { ModuleRef } from '@nestjs/core';
|
|||
import { SchedulerRegistry } from '@nestjs/schedule';
|
||||
import { Job, JobsOptions, Processor, Queue, Worker, WorkerOptions } from 'bullmq';
|
||||
import { CronJob, CronTime } from 'cron';
|
||||
import { setTimeout } from 'timers/promises';
|
||||
import { setTimeout } from 'node:timers/promises';
|
||||
import { bullConfig } from '../infra.config';
|
||||
|
||||
@Injectable()
|
||||
|
@ -24,7 +24,7 @@ export class JobRepository implements IJobRepository {
|
|||
private logger = new ImmichLogger(JobRepository.name);
|
||||
|
||||
constructor(
|
||||
private moduleRef: ModuleRef,
|
||||
private moduleReference: ModuleRef,
|
||||
private schedulerReqistry: SchedulerRegistry,
|
||||
) {}
|
||||
|
||||
|
@ -118,7 +118,7 @@ export class JobRepository implements IJobRepository {
|
|||
}
|
||||
|
||||
async queueAll(items: JobItem[]): Promise<void> {
|
||||
if (!items.length) {
|
||||
if (items.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -167,19 +167,23 @@ export class JobRepository implements IJobRepository {
|
|||
|
||||
private getJobOptions(item: JobItem): JobsOptions | null {
|
||||
switch (item.name) {
|
||||
case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE:
|
||||
case JobName.STORAGE_TEMPLATE_MIGRATION_SINGLE: {
|
||||
return { jobId: item.data.id };
|
||||
case JobName.GENERATE_PERSON_THUMBNAIL:
|
||||
}
|
||||
case JobName.GENERATE_PERSON_THUMBNAIL: {
|
||||
return { priority: 1 };
|
||||
case JobName.QUEUE_FACIAL_RECOGNITION:
|
||||
}
|
||||
case JobName.QUEUE_FACIAL_RECOGNITION: {
|
||||
return { jobId: JobName.QUEUE_FACIAL_RECOGNITION };
|
||||
}
|
||||
|
||||
default:
|
||||
default: {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private getQueue(queue: QueueName): Queue {
|
||||
return this.moduleRef.get<Queue>(getQueueToken(queue), { strict: false });
|
||||
return this.moduleReference.get<Queue>(getQueueToken(queue), { strict: false });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -10,7 +10,7 @@ import {
|
|||
VisionModelInput,
|
||||
} from '@app/domain';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
|
||||
const errorPrefix = 'Machine learning request';
|
||||
|
||||
|
|
|
@ -2,10 +2,10 @@ import { CropOptions, IMediaRepository, ResizeOptions, TranscodeOptions, VideoIn
|
|||
import { Colorspace } from '@app/infra/entities';
|
||||
import { ImmichLogger } from '@app/infra/logger';
|
||||
import ffmpeg, { FfprobeData } from 'fluent-ffmpeg';
|
||||
import fs from 'fs/promises';
|
||||
import fs from 'node:fs/promises';
|
||||
import { Writable } from 'node:stream';
|
||||
import { promisify } from 'node:util';
|
||||
import sharp from 'sharp';
|
||||
import { Writable } from 'stream';
|
||||
import { promisify } from 'util';
|
||||
|
||||
const probe = promisify<string, FfprobeData>(ffmpeg.ffprobe);
|
||||
sharp.concurrency(0);
|
||||
|
@ -91,7 +91,7 @@ export class MediaRepository implements IMediaRepository {
|
|||
}
|
||||
|
||||
if (typeof output !== 'string') {
|
||||
throw new Error('Two-pass transcoding does not support writing to a stream');
|
||||
throw new TypeError('Two-pass transcoding does not support writing to a stream');
|
||||
}
|
||||
|
||||
// two-pass allows for precise control of bitrate at the cost of running twice
|
||||
|
@ -124,12 +124,12 @@ export class MediaRepository implements IMediaRepository {
|
|||
.inputOptions(options.inputOptions)
|
||||
.outputOptions(options.outputOptions)
|
||||
.output(output)
|
||||
.on('error', (err, stdout, stderr) => this.logger.error(stderr || err));
|
||||
.on('error', (error, stdout, stderr) => this.logger.error(stderr || error));
|
||||
}
|
||||
|
||||
chainPath(existing: string, path: string) {
|
||||
const sep = existing.endsWith(':') ? '' : ':';
|
||||
return `${existing}${sep}${path}`;
|
||||
const separator = existing.endsWith(':') ? '' : ':';
|
||||
return `${existing}${separator}${path}`;
|
||||
}
|
||||
|
||||
async generateThumbhash(imagePath: string): Promise<Buffer> {
|
||||
|
|
|
@ -15,11 +15,11 @@ import { ImmichLogger } from '@app/infra/logger';
|
|||
import { Inject } from '@nestjs/common';
|
||||
import { InjectDataSource, InjectRepository } from '@nestjs/typeorm';
|
||||
import { DefaultReadTaskOptions, exiftool, Tags } from 'exiftool-vendored';
|
||||
import { createReadStream, existsSync } from 'fs';
|
||||
import { readFile } from 'fs/promises';
|
||||
import * as geotz from 'geo-tz';
|
||||
import { getName } from 'i18n-iso-countries';
|
||||
import * as readLine from 'readline';
|
||||
import { createReadStream, existsSync } from 'node:fs';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import * as readLine from 'node:readline';
|
||||
import { DataSource, DeepPartial, QueryRunner, Repository } from 'typeorm';
|
||||
|
||||
type GeoEntity = GeodataPlacesEntity | GeodataAdmin1Entity | GeodataAdmin2Entity;
|
||||
|
@ -69,10 +69,10 @@ export class MetadataRepository implements IMetadataRepository {
|
|||
await this.loadAdmin2(queryRunner);
|
||||
|
||||
await queryRunner.commitTransaction();
|
||||
} catch (e) {
|
||||
this.logger.fatal('Error importing geodata', e);
|
||||
} catch (error) {
|
||||
this.logger.fatal('Error importing geodata', error);
|
||||
await queryRunner.rollbackTransaction();
|
||||
throw e;
|
||||
throw error;
|
||||
} finally {
|
||||
await queryRunner.release();
|
||||
}
|
||||
|
@ -110,10 +110,10 @@ export class MetadataRepository implements IMetadataRepository {
|
|||
queryRunner,
|
||||
(lineSplit: string[]) =>
|
||||
this.geodataPlacesRepository.create({
|
||||
id: parseInt(lineSplit[0]),
|
||||
id: Number.parseInt(lineSplit[0]),
|
||||
name: lineSplit[1],
|
||||
latitude: parseFloat(lineSplit[4]),
|
||||
longitude: parseFloat(lineSplit[5]),
|
||||
latitude: Number.parseFloat(lineSplit[4]),
|
||||
longitude: Number.parseFloat(lineSplit[5]),
|
||||
countryCode: lineSplit[8],
|
||||
admin1Code: lineSplit[10],
|
||||
admin2Code: lineSplit[11],
|
||||
|
@ -192,7 +192,8 @@ export class MetadataRepository implements IMetadataRepository {
|
|||
backfillTimezones: true,
|
||||
inferTimezoneFromDatestamps: true,
|
||||
useMWG: true,
|
||||
numericTags: DefaultReadTaskOptions.numericTags.concat(['FocalLength']),
|
||||
numericTags: [...DefaultReadTaskOptions.numericTags, 'FocalLength'],
|
||||
/* eslint unicorn/no-array-callback-reference: off, unicorn/no-array-method-this-argument: off */
|
||||
geoTz: (lat, lon) => geotz.find(lat, lon)[0],
|
||||
})
|
||||
.catch((error) => {
|
||||
|
|
|
@ -28,12 +28,7 @@ export class PersonRepository implements IPersonRepository {
|
|||
.createQueryBuilder()
|
||||
.update()
|
||||
.set({ personId: newPersonId })
|
||||
.where(
|
||||
_.omitBy(
|
||||
{ personId: oldPersonId ? oldPersonId : undefined, id: faceIds ? In(faceIds) : undefined },
|
||||
_.isUndefined,
|
||||
),
|
||||
)
|
||||
.where(_.omitBy({ personId: oldPersonId ?? undefined, id: faceIds ? In(faceIds) : undefined }, _.isUndefined))
|
||||
.execute();
|
||||
|
||||
return result.affected ?? 0;
|
||||
|
|
|
@ -31,11 +31,11 @@ export class SmartInfoRepository implements ISmartInfoRepository {
|
|||
throw new Error(`Invalid CLIP model name: ${modelName}`);
|
||||
}
|
||||
|
||||
const curDimSize = await this.getDimSize();
|
||||
this.logger.verbose(`Current database CLIP dimension size is ${curDimSize}`);
|
||||
const currentDimSize = await this.getDimSize();
|
||||
this.logger.verbose(`Current database CLIP dimension size is ${currentDimSize}`);
|
||||
|
||||
if (dimSize != curDimSize) {
|
||||
this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${curDimSize}.`);
|
||||
if (dimSize != currentDimSize) {
|
||||
this.logger.log(`Dimension size of model ${modelName} is ${dimSize}, but database expects ${currentDimSize}.`);
|
||||
await this.updateDimSize(dimSize);
|
||||
}
|
||||
}
|
||||
|
@ -119,7 +119,9 @@ export class SmartInfoRepository implements ISmartInfoRepository {
|
|||
cte = cte.andWhere('faces."personId" IS NOT NULL');
|
||||
}
|
||||
|
||||
this.faceColumns.forEach((col) => cte.addSelect(`faces.${col}`, col));
|
||||
for (const col of this.faceColumns) {
|
||||
cte.addSelect(`faces.${col}`, col);
|
||||
}
|
||||
|
||||
results = await manager
|
||||
.createQueryBuilder()
|
||||
|
@ -157,8 +159,8 @@ export class SmartInfoRepository implements ISmartInfoRepository {
|
|||
throw new Error(`Invalid CLIP dimension size: ${dimSize}`);
|
||||
}
|
||||
|
||||
const curDimSize = await this.getDimSize();
|
||||
if (curDimSize === dimSize) {
|
||||
const currentDimSize = await this.getDimSize();
|
||||
if (currentDimSize === dimSize) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
@ -181,7 +183,7 @@ export class SmartInfoRepository implements ISmartInfoRepository {
|
|||
$$)`);
|
||||
});
|
||||
|
||||
this.logger.log(`Successfully updated database CLIP dimension size from ${curDimSize} to ${dimSize}.`);
|
||||
this.logger.log(`Successfully updated database CLIP dimension size from ${currentDimSize} to ${dimSize}.`);
|
||||
}
|
||||
|
||||
private async getDimSize(): Promise<number> {
|
||||
|
|
|
@ -1,7 +1,7 @@
|
|||
import { ISystemConfigRepository } from '@app/domain';
|
||||
import { InjectRepository } from '@nestjs/typeorm';
|
||||
import axios from 'axios';
|
||||
import { readFile } from 'fs/promises';
|
||||
import { readFile } from 'node:fs/promises';
|
||||
import { In, Repository } from 'typeorm';
|
||||
import { SystemConfigEntity } from '../entities';
|
||||
import { DummyValue, GenerateSql } from '../infra.util';
|
||||
|
@ -22,7 +22,7 @@ export class SystemConfigRepository implements ISystemConfigRepository {
|
|||
}
|
||||
|
||||
readFile(filename: string): Promise<string> {
|
||||
return readFile(filename, { encoding: 'utf-8' });
|
||||
return readFile(filename, { encoding: 'utf8' });
|
||||
}
|
||||
|
||||
saveAll(items: SystemConfigEntity[]): Promise<SystemConfigEntity[]> {
|
||||
|
|
|
@ -74,11 +74,7 @@ export class UserRepository implements IUserRepository {
|
|||
}
|
||||
|
||||
async delete(user: UserEntity, hard?: boolean): Promise<UserEntity> {
|
||||
if (hard) {
|
||||
return this.userRepository.remove(user);
|
||||
} else {
|
||||
return this.userRepository.softRemove(user);
|
||||
}
|
||||
return hard ? this.userRepository.remove(user) : this.userRepository.softRemove(user);
|
||||
}
|
||||
|
||||
async restore(user: UserEntity): Promise<UserEntity> {
|
||||
|
|
|
@ -1,10 +1,11 @@
|
|||
#!/usr/bin/env node
|
||||
import { ISystemConfigRepository } from '@app/domain';
|
||||
import { INestApplication } from '@nestjs/common';
|
||||
import { Reflector } from '@nestjs/core';
|
||||
import { Test } from '@nestjs/testing';
|
||||
import { TypeOrmModule } from '@nestjs/typeorm';
|
||||
import { mkdir, rm, writeFile } from 'fs/promises';
|
||||
import { join } from 'path';
|
||||
import { mkdir, rm, writeFile } from 'node:fs/promises';
|
||||
import { join } from 'node:path';
|
||||
import { databaseConfig } from '../database.config';
|
||||
import { databaseEntities } from '../entities';
|
||||
import { GENERATE_SQL_KEY, GenerateSqlQueries } from '../infra.util';
|
||||
|
@ -157,7 +158,7 @@ class SqlGenerator {
|
|||
|
||||
private async write() {
|
||||
for (const [repoName, data] of Object.entries(this.results)) {
|
||||
const filename = repoName.replace(/[A-Z]/g, (letter) => `.${letter.toLowerCase()}`).replace('.', '');
|
||||
const filename = repoName.replaceAll(/[A-Z]/g, (letter) => `.${letter.toLowerCase()}`).replace('.', '');
|
||||
const file = join(this.options.targetDir, `${filename}.sql`);
|
||||
await writeFile(file, data.join('\n\n') + '\n');
|
||||
}
|
||||
|
|
|
@ -1,8 +1,6 @@
|
|||
import { format } from 'sql-formatter';
|
||||
import { Logger } from 'typeorm';
|
||||
|
||||
// eslint-disable-next-line @typescript-eslint/no-var-requires
|
||||
const { format } = require('sql-formatter');
|
||||
|
||||
export class SqlLogger implements Logger {
|
||||
queries: string[] = [];
|
||||
errors: Array<{ error: string | Error; query: string }> = [];
|
||||
|
|
|
@ -16,21 +16,23 @@ export class AuditSubscriber implements EntitySubscriberInterface<AssetEntity |
|
|||
|
||||
private getAudit(entityName: string, entity: any): Partial<AuditEntity> | null {
|
||||
switch (entityName) {
|
||||
case AssetEntity.name:
|
||||
case AssetEntity.name: {
|
||||
const asset = entity as AssetEntity;
|
||||
return {
|
||||
entityType: EntityType.ASSET,
|
||||
entityId: asset.id,
|
||||
ownerId: asset.ownerId,
|
||||
};
|
||||
}
|
||||
|
||||
case AlbumEntity.name:
|
||||
case AlbumEntity.name: {
|
||||
const album = entity as AlbumEntity;
|
||||
return {
|
||||
entityType: EntityType.ALBUM,
|
||||
entityId: album.id,
|
||||
ownerId: album.ownerId,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return null;
|
||||
|
|
|
@ -10,18 +10,21 @@ if (process.argv[2] === immichApp) {
|
|||
|
||||
function bootstrap() {
|
||||
switch (immichApp) {
|
||||
case 'immich':
|
||||
case 'immich': {
|
||||
process.title = 'immich_server';
|
||||
return server();
|
||||
case 'microservices':
|
||||
}
|
||||
case 'microservices': {
|
||||
process.title = 'immich_microservices';
|
||||
return microservices();
|
||||
case 'immich-admin':
|
||||
}
|
||||
case 'immich-admin': {
|
||||
process.title = 'immich_admin_cli';
|
||||
return admin();
|
||||
default:
|
||||
console.log(`Invalid app name: ${immichApp}. Expected one of immich|microservices|cli`);
|
||||
process.exit(1);
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Invalid app name: ${immichApp}. Expected one of immich|microservices|cli`);
|
||||
}
|
||||
}
|
||||
}
|
||||
void bootstrap();
|
||||
|
|
|
@ -7,7 +7,7 @@ describe('parsing latitude from string input', () => {
|
|||
expect(parseLatitude('Infinity')).toBeNull();
|
||||
expect(parseLatitude('-Infinity')).toBeNull();
|
||||
expect(parseLatitude('90.001')).toBeNull();
|
||||
expect(parseLatitude(-90.000001)).toBeNull();
|
||||
expect(parseLatitude(-90.000_001)).toBeNull();
|
||||
expect(parseLatitude('1000')).toBeNull();
|
||||
expect(parseLatitude(-1000)).toBeNull();
|
||||
});
|
||||
|
@ -15,10 +15,10 @@ describe('parsing latitude from string input', () => {
|
|||
it('returns the numeric coordinate for valid inputs', () => {
|
||||
expect(parseLatitude('90')).toBeCloseTo(90);
|
||||
expect(parseLatitude('-90')).toBeCloseTo(-90);
|
||||
expect(parseLatitude(89.999999)).toBeCloseTo(89.999999);
|
||||
expect(parseLatitude(89.999_999)).toBeCloseTo(89.999_999);
|
||||
expect(parseLatitude('-89.9')).toBeCloseTo(-89.9);
|
||||
expect(parseLatitude(0)).toBeCloseTo(0);
|
||||
expect(parseLatitude('-0.0')).toBeCloseTo(-0.0);
|
||||
expect(parseLatitude('-0.0')).toBeCloseTo(-0);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -32,7 +32,7 @@ describe('parsing longitude from string input', () => {
|
|||
it('returns null for invalid inputs', () => {
|
||||
expect(parseLongitude('')).toBeNull();
|
||||
expect(parseLongitude('NaN')).toBeNull();
|
||||
expect(parseLongitude(Infinity)).toBeNull();
|
||||
expect(parseLongitude(Number.POSITIVE_INFINITY)).toBeNull();
|
||||
expect(parseLongitude('-Infinity')).toBeNull();
|
||||
expect(parseLongitude('180.001')).toBeNull();
|
||||
expect(parseLongitude('-180.000001')).toBeNull();
|
||||
|
@ -43,10 +43,10 @@ describe('parsing longitude from string input', () => {
|
|||
it('returns the numeric coordinate for valid inputs', () => {
|
||||
expect(parseLongitude(180)).toBeCloseTo(180);
|
||||
expect(parseLongitude('-180')).toBeCloseTo(-180);
|
||||
expect(parseLongitude('179.999999')).toBeCloseTo(179.999999);
|
||||
expect(parseLongitude('179.999999')).toBeCloseTo(179.999_999);
|
||||
expect(parseLongitude(-179.9)).toBeCloseTo(-179.9);
|
||||
expect(parseLongitude('0')).toBeCloseTo(0);
|
||||
expect(parseLongitude('-0.0')).toBeCloseTo(-0.0);
|
||||
expect(parseLongitude('-0.0')).toBeCloseTo(-0);
|
||||
});
|
||||
});
|
||||
|
||||
|
|
|
@ -2,15 +2,15 @@ import { isDecimalNumber, isNumberInRange, toNumberOrNull } from './numbers';
|
|||
|
||||
describe('checks if a number is a decimal number', () => {
|
||||
it('returns false for non-decimal numbers', () => {
|
||||
expect(isDecimalNumber(NaN)).toBe(false);
|
||||
expect(isDecimalNumber(Infinity)).toBe(false);
|
||||
expect(isDecimalNumber(-Infinity)).toBe(false);
|
||||
expect(isDecimalNumber(Number.NaN)).toBe(false);
|
||||
expect(isDecimalNumber(Number.POSITIVE_INFINITY)).toBe(false);
|
||||
expect(isDecimalNumber(Number.NEGATIVE_INFINITY)).toBe(false);
|
||||
});
|
||||
|
||||
it('returns true for decimal numbers', () => {
|
||||
expect(isDecimalNumber(0)).toBe(true);
|
||||
expect(isDecimalNumber(-0)).toBe(true);
|
||||
expect(isDecimalNumber(10.12345)).toBe(true);
|
||||
expect(isDecimalNumber(10.123_45)).toBe(true);
|
||||
expect(isDecimalNumber(Number.MAX_VALUE)).toBe(true);
|
||||
expect(isDecimalNumber(Number.MIN_VALUE)).toBe(true);
|
||||
});
|
||||
|
@ -26,16 +26,17 @@ describe('checks if a number is within a range', () => {
|
|||
it('returns true for numbers inside the range', () => {
|
||||
expect(isNumberInRange(0, 0, 50)).toBe(true);
|
||||
expect(isNumberInRange(50, 0, 50)).toBe(true);
|
||||
expect(isNumberInRange(-50.12345, -50.12345, 0)).toBe(true);
|
||||
expect(isNumberInRange(-50.123_45, -50.123_45, 0)).toBe(true);
|
||||
});
|
||||
});
|
||||
|
||||
describe('converts input to a number or null', () => {
|
||||
it('returns null for invalid inputs', () => {
|
||||
expect(toNumberOrNull(null)).toBeNull();
|
||||
// eslint-disable-next-line unicorn/no-useless-undefined
|
||||
expect(toNumberOrNull(undefined)).toBeNull();
|
||||
expect(toNumberOrNull('')).toBeNull();
|
||||
expect(toNumberOrNull(NaN)).toBeNull();
|
||||
expect(toNumberOrNull(Number.NaN)).toBeNull();
|
||||
});
|
||||
|
||||
it('returns a number for valid inputs', () => {
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
export function isDecimalNumber(num: number): boolean {
|
||||
return !Number.isNaN(num) && Number.isFinite(num);
|
||||
export function isDecimalNumber(number_: number): boolean {
|
||||
return !Number.isNaN(number_) && Number.isFinite(number_);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if `num` is a valid number and is between `start` and `end` (inclusive)
|
||||
*/
|
||||
export function isNumberInRange(num: number, start: number, end: number): boolean {
|
||||
return isDecimalNumber(num) && num >= start && num <= end;
|
||||
export function isNumberInRange(number_: number, start: number, end: number): boolean {
|
||||
return isDecimalNumber(number_) && number_ >= start && number_ <= end;
|
||||
}
|
||||
|
||||
export function toNumberOrNull(input: number | string | null | undefined): number | null {
|
||||
|
@ -14,6 +14,6 @@ export function toNumberOrNull(input: number | string | null | undefined): numbe
|
|||
return null;
|
||||
}
|
||||
|
||||
const num = typeof input === 'string' ? Number.parseFloat(input) : input;
|
||||
return isDecimalNumber(num) ? num : null;
|
||||
const number_ = typeof input === 'string' ? Number.parseFloat(input) : input;
|
||||
return isDecimalNumber(number_) ? number_ : null;
|
||||
}
|
||||
|
|
Some files were not shown because too many files have changed in this diff Show more
Loading…
Reference in a new issue