1
0
Fork 0
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:
Jonathan Jogenfors 2024-02-02 04:18:00 +01:00 committed by GitHub
parent e4d0560d49
commit f44fa45aa0
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
218 changed files with 2471 additions and 1244 deletions

View file

@ -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
View file

@ -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",

View file

@ -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": {

View file

@ -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);
}
}

View file

@ -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')

View file

@ -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();
}
}

View file

@ -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}`);
}
}

View file

@ -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);

View file

@ -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');
}
});

View file

@ -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`,

View file

@ -17,6 +17,6 @@ export default defineConfig({
minForks: 1,
},
},
testTimeout: 10000,
testTimeout: 10_000,
},
});

View file

@ -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"]
}

View file

@ -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
View file

@ -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",

View file

@ -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": {

View file

@ -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();
}
}
}
}

View file

@ -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> {

View file

@ -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 {

View file

@ -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,
};
}),

View file

@ -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',

View file

@ -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);

View file

@ -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;
}
}
}

View file

@ -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;

View file

@ -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,

View file

@ -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 };

View file

@ -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 {

View file

@ -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({

View file

@ -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;
});
}

View file

@ -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]);
});
}
});

View file

@ -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)],
};

View file

@ -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;
}
}

View file

@ -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);

View file

@ -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
>;

View file

@ -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[] = [

View file

@ -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;
}
}
});

View file

@ -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]);

View file

@ -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;
}

View file

@ -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}`);
}
}
}

View file

@ -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 });

View file

@ -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,

View file

@ -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,

View file

@ -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 },

View file

@ -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) {

View file

@ -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>;

View file

@ -1,5 +1,5 @@
import { VideoCodec } from '@app/infra/entities';
import { Writable } from 'stream';
import { Writable } from 'node:stream';
export const IMediaRepository = 'IMediaRepository';

View file

@ -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 {

View file

@ -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 {

View file

@ -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,

View file

@ -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;
}

View file

@ -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);
}

View file

@ -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 {

View file

@ -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 }) => {

View file

@ -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;

View file

@ -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 {

View file

@ -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;

View file

@ -136,7 +136,7 @@ const updatedConfig = Object.freeze<SystemConfig>({
watch: {
enabled: false,
usePolling: false,
interval: 10000,
interval: 10_000,
},
},
});

View file

@ -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)'}`);
}

View file

@ -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 });
}
}

View file

@ -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;
};

View file

@ -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({

View file

@ -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);

View file

@ -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;
}

View file

@ -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;
}
}

View file

@ -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,

View file

@ -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;
};

View file

@ -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;
}
}
}

View file

@ -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;
}

View file

@ -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) {

View file

@ -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' });
}
};

View file

@ -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]);
}
}

View file

@ -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,
};
}

View file

@ -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;
}

View file

@ -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;
}
}
}
}

View file

@ -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,
};

View file

@ -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,

View file

@ -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;

View file

@ -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));
};

View file

@ -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(

View file

@ -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

View file

@ -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));

View file

@ -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 [];
}

View file

@ -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, {

View file

@ -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;
}
}
}

View file

@ -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()));
});

View file

@ -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;
}
}
}

View file

@ -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 });
}
}

View file

@ -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';

View file

@ -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> {

View file

@ -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) => {

View file

@ -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;

View file

@ -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> {

View file

@ -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[]> {

View file

@ -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> {

View file

@ -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');
}

View file

@ -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 }> = [];

View file

@ -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;

View file

@ -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();

View file

@ -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);
});
});

View file

@ -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', () => {

View file

@ -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