mirror of
https://github.com/immich-app/immich.git
synced 2026-07-10 06:03:14 -07:00
Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9d273d9f4f | |||
| 8bb30e0780 |
Vendored
+19
-19
@@ -29,9 +29,6 @@
|
||||
"editor.formatOnSave": true,
|
||||
"tailwindCSS.lint.suggestCanonicalClasses": "ignore"
|
||||
},
|
||||
"svelte.plugin.svelte.compilerWarnings": {
|
||||
"state_referenced_locally": "ignore"
|
||||
},
|
||||
"[typescript]": {
|
||||
"editor.defaultFormatter": "esbenp.prettier-vscode",
|
||||
"editor.formatOnSave": true
|
||||
@@ -43,37 +40,40 @@
|
||||
"eslint.useFlatConfig": true,
|
||||
"eslint.validate": ["javascript", "typescript", "svelte"],
|
||||
"eslint.workingDirectories": [
|
||||
{ "directory": "cli", "changeProcessCWD": true },
|
||||
{ "directory": "e2e", "changeProcessCWD": true },
|
||||
{ "directory": "server", "changeProcessCWD": true },
|
||||
{ "directory": "web", "changeProcessCWD": true }
|
||||
{ "changeProcessCWD": true, "directory": "cli" },
|
||||
{ "changeProcessCWD": true, "directory": "e2e" },
|
||||
{ "changeProcessCWD": true, "directory": "server" },
|
||||
{ "changeProcessCWD": true, "directory": "web" }
|
||||
],
|
||||
"files.watcherExclude": {
|
||||
"**/.jj/**": true,
|
||||
"**/.git/**": true,
|
||||
"**/node_modules/**": true,
|
||||
"**/build/**": true,
|
||||
"**/dist/**": true,
|
||||
"**/.svelte-kit/**": true
|
||||
},
|
||||
"explorer.fileNesting.enabled": true,
|
||||
"explorer.fileNesting.patterns": {
|
||||
"*.dart": "${capture}.g.dart,${capture}.gr.dart,${capture}.drift.dart",
|
||||
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
|
||||
"*.js": "${capture}.spec.js,${capture}.mock.js",
|
||||
"*.ts": "${capture}.spec.ts,${capture}.mock.ts",
|
||||
"package.json": "package-lock.json, yarn.lock, pnpm-lock.yaml, bun.lockb, bun.lock, pnpm-workspace.yaml, .pnpmfile.cjs"
|
||||
},
|
||||
"files.watcherExclude": {
|
||||
"**/.git/**": true,
|
||||
"**/.jj/**": true,
|
||||
"**/.svelte-kit/**": true,
|
||||
"**/build/**": true,
|
||||
"**/dist/**": true,
|
||||
"**/node_modules/**": true
|
||||
},
|
||||
"js/ts.preferences.importModuleSpecifier": "non-relative",
|
||||
"search.exclude": {
|
||||
"**/node_modules": true,
|
||||
"**/.svelte-kit": true,
|
||||
"**/build": true,
|
||||
"**/dist": true,
|
||||
"**/.svelte-kit": true,
|
||||
"**/node_modules": true,
|
||||
"**/open-api/typescript-sdk/src": true
|
||||
},
|
||||
"svelte.enable-ts-plugin": true,
|
||||
"svelte.plugin.svelte.compilerWarnings": {
|
||||
"state_referenced_locally": "ignore"
|
||||
},
|
||||
"tailwindCSS.experimental.configFile": {
|
||||
"web/src/app.css": "web/src/**"
|
||||
},
|
||||
"js/ts.preferences.importModuleSpecifier": "non-relative",
|
||||
"vitest.maximumConfigs": 10
|
||||
}
|
||||
|
||||
@@ -41,9 +41,17 @@ export default typescriptEslint.config([
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'unicorn/prefer-module': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
'unicorn/consistent-boolean-name': 'off',
|
||||
'unicorn/no-non-function-verb-prefix': 'off',
|
||||
'unicorn/no-unreadable-for-of-expression': 'off',
|
||||
'unicorn/max-nested-calls': 'off',
|
||||
'unicorn/prefer-uint8array-base64': 'off',
|
||||
'unicorn/isolated-functions': 'off',
|
||||
'unicorn/prefer-promise-with-resolvers': 'off',
|
||||
'unicorn/no-declarations-before-early-exit': 'off',
|
||||
curly: 2,
|
||||
'prettier/prettier': 0,
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/name-replacements': 'off',
|
||||
'unicorn/filename-case': 'off',
|
||||
'unicorn/no-null': 'off',
|
||||
'unicorn/prefer-top-level-await': 'off',
|
||||
|
||||
+1
-1
@@ -40,7 +40,7 @@
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^64.0.0",
|
||||
"eslint-plugin-unicorn": "^70.0.0",
|
||||
"exiftool-vendored": "^35.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"luxon": "^3.4.4",
|
||||
|
||||
@@ -118,7 +118,7 @@ describe('/admin/database-backups', () => {
|
||||
|
||||
expect(status).toBe(201);
|
||||
|
||||
cookie = headers['set-cookie'][0].split(';')[0];
|
||||
cookie = headers['set-cookie'][0].split(';', 1)[0];
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
@@ -224,7 +224,7 @@ describe('/admin/database-backups', () => {
|
||||
});
|
||||
|
||||
expect(status).toBe(201);
|
||||
cookie = headers['set-cookie'][0].split(';')[0];
|
||||
cookie = headers['set-cookie'][0].split(';', 1)[0];
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
@@ -295,7 +295,7 @@ describe('/admin/database-backups', () => {
|
||||
});
|
||||
|
||||
expect(status).toBe(201);
|
||||
cookie = headers['set-cookie'][0].split(';')[0];
|
||||
cookie = headers['set-cookie'][0].split(';', 1)[0];
|
||||
|
||||
await expect
|
||||
.poll(
|
||||
|
||||
@@ -85,7 +85,7 @@ describe('/admin/maintenance', () => {
|
||||
|
||||
expect(status).toBe(201);
|
||||
|
||||
cookie = headers['set-cookie'][0].split(';')[0];
|
||||
cookie = headers['set-cookie'][0].split(';', 1)[0];
|
||||
expect(cookie).toEqual(
|
||||
expect.stringMatching(/^immich_maintenance_token=[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*\.[A-Za-z0-9-_]*$/),
|
||||
);
|
||||
@@ -149,7 +149,7 @@ describe('/admin/maintenance', () => {
|
||||
const { status, body } = await request(app)
|
||||
.post('/admin/maintenance/login')
|
||||
.send({
|
||||
token: cookie!.split('=')[1].trim(),
|
||||
token: cookie!.split('=', 2)[1].trim(),
|
||||
});
|
||||
expect(status).toBe(201);
|
||||
expect(body).toEqual(
|
||||
|
||||
@@ -27,7 +27,7 @@ test.describe('Maintenance', () => {
|
||||
test('maintenance shows no options to users until they authenticate', async ({ page }) => {
|
||||
const setCookie = await utils.enterMaintenance(admin.accessToken);
|
||||
const cookie = setCookie
|
||||
?.map((cookie) => cookie.split(';')[0].split('='))
|
||||
?.map((cookie) => cookie.split(';', 1)[0].split('='))
|
||||
?.find(([name]) => name === 'immich_maintenance_token');
|
||||
|
||||
expect(cookie).toBeTruthy();
|
||||
|
||||
@@ -120,6 +120,7 @@ describe('/albums', () => {
|
||||
}),
|
||||
]);
|
||||
|
||||
// eslint-disable-next-line unicorn/no-unreadable-array-destructuring
|
||||
[user2Albums[0]] = await Promise.all([
|
||||
getAlbumInfo({ id: user2Albums[0].id }, { headers: asBearerAuth(user2.accessToken) }),
|
||||
deleteUserAdmin({ id: user3.userId, userAdminDeleteDto: {} }, { headers: asBearerAuth(admin.accessToken) }),
|
||||
|
||||
@@ -781,7 +781,7 @@ describe('/asset', () => {
|
||||
exifImageWidth: 4032,
|
||||
exifImageHeight: 3024,
|
||||
latitude: 41.2203,
|
||||
longitude: -96.071_625,
|
||||
longitude: -96.071625,
|
||||
make: 'Apple',
|
||||
model: 'iPhone 7',
|
||||
lensModel: 'iPhone 7 back camera 3.99mm f/1.8',
|
||||
@@ -973,9 +973,9 @@ describe('/asset', () => {
|
||||
fileSizeInByte: 31_175_472,
|
||||
focalLength: 18.3,
|
||||
iso: 100,
|
||||
latitude: 36.613_24,
|
||||
latitude: 36.61324,
|
||||
lensModel: '18.3mm F2.8',
|
||||
longitude: -121.897_85,
|
||||
longitude: -121.89785,
|
||||
make: 'RICOH IMAGING COMPANY, LTD.',
|
||||
model: 'RICOH GR III',
|
||||
orientation: '1',
|
||||
|
||||
@@ -75,7 +75,7 @@ describe('/map', () => {
|
||||
country: 'United States of America',
|
||||
id: expect.any(String),
|
||||
lat: expect.closeTo(39.115),
|
||||
lon: expect.closeTo(-108.400_968),
|
||||
lon: expect.closeTo(-108.400968),
|
||||
state: 'Colorado',
|
||||
},
|
||||
{
|
||||
@@ -83,7 +83,7 @@ describe('/map', () => {
|
||||
country: 'United States of America',
|
||||
id: expect.any(String),
|
||||
lat: expect.closeTo(41.2203),
|
||||
lon: expect.closeTo(-96.071_625),
|
||||
lon: expect.closeTo(-96.071625),
|
||||
state: 'Nebraska',
|
||||
},
|
||||
]);
|
||||
@@ -123,7 +123,7 @@ describe('/map', () => {
|
||||
country: 'United States of America',
|
||||
id: expect.any(String),
|
||||
lat: expect.closeTo(39.115),
|
||||
lon: expect.closeTo(-108.400_968),
|
||||
lon: expect.closeTo(-108.400968),
|
||||
state: 'Colorado',
|
||||
},
|
||||
{
|
||||
@@ -131,7 +131,7 @@ describe('/map', () => {
|
||||
country: 'United States of America',
|
||||
id: expect.any(String),
|
||||
lat: expect.closeTo(41.2203),
|
||||
lon: expect.closeTo(-96.071_625),
|
||||
lon: expect.closeTo(-96.071625),
|
||||
state: 'Nebraska',
|
||||
},
|
||||
]);
|
||||
@@ -188,20 +188,20 @@ describe('/map', () => {
|
||||
const reverseGeocodeTestCases = [
|
||||
{
|
||||
name: 'Vaucluse',
|
||||
lat: -33.858_977_058_663_13,
|
||||
lon: 151.278_490_730_270_48,
|
||||
lat: -33.85897705866313,
|
||||
lon: 151.27849073027048,
|
||||
results: [{ city: 'Vaucluse', state: 'New South Wales', country: 'Australia' }],
|
||||
},
|
||||
{
|
||||
name: 'Ravenhall',
|
||||
lat: -37.765_732_399_174_75,
|
||||
lon: 144.752_453_164_883_3,
|
||||
lat: -37.76573239917475,
|
||||
lon: 144.7524531648833,
|
||||
results: [{ city: 'Ravenhall', state: 'Victoria', country: 'Australia' }],
|
||||
},
|
||||
{
|
||||
name: 'Scarborough',
|
||||
lat: -31.894_346_156_789_997,
|
||||
lon: 115.757_617_103_904_64,
|
||||
lat: -31.894346156789997,
|
||||
lon: 115.75761710390464,
|
||||
results: [{ city: 'Scarborough', state: 'Western Australia', country: 'Australia' }],
|
||||
},
|
||||
];
|
||||
|
||||
@@ -44,7 +44,7 @@ const loginWithOAuth = async (sub: OAuthUser | string, redirectUri?: string) =>
|
||||
});
|
||||
|
||||
// login
|
||||
const response1 = await redirect(url.replace(authServer.internal, authServer.external));
|
||||
const response1 = await redirect(url.replace(authServer.internal, () => authServer.external));
|
||||
const response2 = await request(authServer.external + response1.location)
|
||||
.post('')
|
||||
.set('Cookie', response1.cookies)
|
||||
|
||||
@@ -86,23 +86,23 @@ describe('/search', () => {
|
||||
|
||||
// note: the coordinates here are not the actual coordinates of the images and are random for most of them
|
||||
const coordinates = [
|
||||
{ latitude: 48.853_41, longitude: 2.3488 }, // paris
|
||||
{ latitude: 35.6895, longitude: 139.691_71 }, // tokyo
|
||||
{ latitude: 52.524_37, longitude: 13.410_53 }, // berlin
|
||||
{ latitude: 1.314_663_1, longitude: 103.845_409_3 }, // singapore
|
||||
{ latitude: 41.013_84, longitude: 28.949_66 }, // istanbul
|
||||
{ latitude: 5.556_02, longitude: -0.1969 }, // accra
|
||||
{ latitude: 37.544_270_6, longitude: -4.727_752_8 }, // andalusia
|
||||
{ latitude: 23.133_02, longitude: -82.383_04 }, // havana
|
||||
{ latitude: 41.694_11, longitude: 44.833_68 }, // tbilisi
|
||||
{ latitude: 31.222_22, longitude: 121.458_06 }, // shanghai
|
||||
{ latitude: 48.85341, longitude: 2.3488 }, // paris
|
||||
{ latitude: 35.6895, longitude: 139.69171 }, // tokyo
|
||||
{ latitude: 52.52437, longitude: 13.41053 }, // berlin
|
||||
{ latitude: 1.3146631, longitude: 103.8454093 }, // singapore
|
||||
{ latitude: 41.01384, longitude: 28.94966 }, // istanbul
|
||||
{ latitude: 5.55602, longitude: -0.1969 }, // accra
|
||||
{ latitude: 37.5442706, longitude: -4.7277528 }, // andalusia
|
||||
{ latitude: 23.13302, longitude: -82.38304 }, // havana
|
||||
{ latitude: 41.69411, longitude: 44.83368 }, // tbilisi
|
||||
{ latitude: 31.22222, longitude: 121.45806 }, // shanghai
|
||||
{ latitude: 38.9711, longitude: -109.7137 }, // thompson springs
|
||||
{ latitude: 40.714_27, longitude: -74.005_97 }, // new york
|
||||
{ latitude: 47.040_57, longitude: 9.068_04 }, // glarus
|
||||
{ latitude: 32.771_52, longitude: -89.116_73 }, // philadelphia
|
||||
{ latitude: 31.634_16, longitude: -7.999_94 }, // marrakesh
|
||||
{ latitude: 38.523_735_4, longitude: -78.488_619_4 }, // tanners ridge
|
||||
{ latitude: 59.938_63, longitude: 30.314_13 }, // st. petersburg
|
||||
{ latitude: 40.71427, longitude: -74.00597 }, // new york
|
||||
{ latitude: 47.04057, longitude: 9.06804 }, // glarus
|
||||
{ latitude: 32.77152, longitude: -89.11673 }, // philadelphia
|
||||
{ latitude: 31.63416, longitude: -7.99994 }, // marrakesh
|
||||
{ latitude: 38.5237354, longitude: -78.4886194 }, // tanners ridge
|
||||
{ latitude: 59.93863, longitude: 30.31413 }, // st. petersburg
|
||||
{ latitude: 0, longitude: 0 }, // null island
|
||||
];
|
||||
|
||||
@@ -111,7 +111,7 @@ describe('/search', () => {
|
||||
);
|
||||
|
||||
await Promise.all(updates);
|
||||
for (const [i] of coordinates.entries()) {
|
||||
for (const i of coordinates.keys()) {
|
||||
await utils.waitForWebsocketEvent({ event: 'assetUpdate', id: assets[i].id });
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,7 @@ describe(`immich login`, () => {
|
||||
it('should login and save auth.yml with 600', async () => {
|
||||
const admin = await utils.adminSetup();
|
||||
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
|
||||
const { stdout, stderr, exitCode } = await immichCli(['login', app, `${key.secret}`]);
|
||||
const { stdout, stderr, exitCode } = await immichCli(['login', app, key.secret]);
|
||||
expect(stdout.split('\n')).toEqual([
|
||||
'Logging in to http://127.0.0.1:2285/api',
|
||||
'Logged in as admin@immich.cloud',
|
||||
@@ -48,7 +48,7 @@ describe(`immich login`, () => {
|
||||
it('should login without /api in the url', async () => {
|
||||
const admin = await utils.adminSetup();
|
||||
const key = await utils.createApiKey(admin.accessToken, [Permission.All]);
|
||||
const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), `${key.secret}`]);
|
||||
const { stdout, stderr, exitCode } = await immichCli(['login', app.replaceAll('/api', ''), key.secret]);
|
||||
expect(stdout.split('\n')).toEqual([
|
||||
'Logging in to http://127.0.0.1:2285',
|
||||
'Discovered API at http://127.0.0.1:2285/api',
|
||||
|
||||
@@ -119,7 +119,9 @@ describe(`immich upload`, () => {
|
||||
const baseDir = `/tmp/upload/`;
|
||||
|
||||
const testPaths = Object.keys(files).map((filePath) => `${baseDir}/${filePath}`);
|
||||
testPaths.map((filePath) => utils.createImageFile(filePath));
|
||||
for (const filePath of testPaths) {
|
||||
utils.createImageFile(filePath);
|
||||
}
|
||||
|
||||
const commandLine = paths.map((argument) => `${baseDir}/${argument}`);
|
||||
|
||||
@@ -135,7 +137,9 @@ describe(`immich upload`, () => {
|
||||
const assets = await getAssetStatistics({}, { headers: asKeyAuth(key) });
|
||||
expect(assets.total).toBe(expectedCount);
|
||||
|
||||
testPaths.map((filePath) => utils.removeImageFile(filePath));
|
||||
for (const filePath of testPaths) {
|
||||
utils.removeImageFile(filePath);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
@@ -21,7 +21,7 @@ export const randomImageFromString = async (
|
||||
let seedNumber = 0;
|
||||
for (let i = 0; i < seed.length; i++) {
|
||||
seedNumber = (seedNumber << 5) - seedNumber + (seed.codePointAt(i) ?? 0);
|
||||
seedNumber = seedNumber & seedNumber; // Convert to 32bit integer
|
||||
seedNumber &= seedNumber; // Convert to 32bit integer
|
||||
}
|
||||
return randomImage(new SeededRandom(Math.abs(seedNumber)), { width, height });
|
||||
};
|
||||
|
||||
@@ -64,7 +64,7 @@ export function generateAsset(
|
||||
const asset: MockTimelineAsset = {
|
||||
id: assetId,
|
||||
ownerId,
|
||||
ratio: Number.parseFloat(ratio.split(':')[0]) / Number.parseFloat(ratio.split(':')[1]),
|
||||
ratio: Number(ratio.split(':', 1)[0]) / Number(ratio.split(':', 2)[1]),
|
||||
thumbhash: generateThumbhash(rng),
|
||||
localDateTime: date.toISOString(),
|
||||
fileCreatedAt: date.toISOString(),
|
||||
@@ -214,7 +214,7 @@ export function generateTimelineData(config: TimelineConfig): MockTimelineData {
|
||||
}
|
||||
|
||||
// Create a mock album from random assets
|
||||
const allAssets = [...buckets.values()].flat();
|
||||
const allAssets = buckets.values().toArray().flat();
|
||||
|
||||
// Select 10-30 random assets for the album (or all assets if less than 10)
|
||||
const albumSize = Math.min(allAssets.length, globalRng.nextInt(10, 31));
|
||||
|
||||
@@ -172,11 +172,7 @@ function shouldIncludeAsset(
|
||||
if (isArchived !== undefined && actuallyArchived !== isArchived) {
|
||||
return false;
|
||||
}
|
||||
if (isFavorite !== undefined && actuallyFavorited !== isFavorite) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
return isFavorite === undefined || actuallyFavorited === isFavorite;
|
||||
}
|
||||
/**
|
||||
* Get summary for all buckets (mimics getTimeBuckets API)
|
||||
@@ -361,7 +357,7 @@ export function getAsset(
|
||||
owner?: UserResponseDto,
|
||||
): AssetResponseDto | undefined {
|
||||
// Search through all buckets for the asset
|
||||
const buckets = [...timelineData.buckets.values()];
|
||||
const buckets = timelineData.buckets.values().toArray();
|
||||
for (const assets of buckets) {
|
||||
const asset = assets.find((a) => a.id === assetId);
|
||||
if (asset) {
|
||||
@@ -395,7 +391,7 @@ export function getAlbum(
|
||||
|
||||
// Get the actual asset objects from the timeline data
|
||||
const albumAssets: AssetResponseDto[] = [];
|
||||
const allAssets = [...timelineData.buckets.values()].flat();
|
||||
const allAssets = timelineData.buckets.values().toArray().flat();
|
||||
|
||||
for (const assetId of album.assetIds) {
|
||||
const assetConfig = allAssets.find((a) => a.id === assetId);
|
||||
|
||||
@@ -143,7 +143,7 @@ export function validateTimelineConfig(config: TimelineConfig): void {
|
||||
}
|
||||
|
||||
// Validate seed if provided
|
||||
if (config.seed !== undefined && (config.seed < 0 || !Number.isInteger(config.seed))) {
|
||||
if (config.seed !== undefined && (config.seed < 0 || !Number.isSafeInteger(config.seed))) {
|
||||
throw new Error('Seed must be a non-negative integer');
|
||||
}
|
||||
|
||||
|
||||
@@ -153,11 +153,8 @@ export function getMockAsset(
|
||||
const isInDifferentPeriod = (date1: DateTime, date2: DateTime): boolean => {
|
||||
if (unit === 'day') {
|
||||
return !date1.startOf('day').equals(date2.startOf('day'));
|
||||
} else if (unit === 'month') {
|
||||
return date1.year !== date2.year || date1.month !== date2.month;
|
||||
} else {
|
||||
return date1.year !== date2.year;
|
||||
}
|
||||
return unit === 'month' ? date1.year !== date2.year || date1.month !== date2.month : date1.year !== date2.year;
|
||||
};
|
||||
|
||||
if (direction === 'next') {
|
||||
|
||||
@@ -40,7 +40,8 @@ export const setupTimelineMockApiRoutes = async (
|
||||
contentType: 'application/json',
|
||||
json: getTimeBuckets(timelineRestData, isTrashed, isArchived, isFavorite, albumId, changes),
|
||||
});
|
||||
} else if (pathname === '/api/timeline/bucket') {
|
||||
}
|
||||
if (pathname === '/api/timeline/bucket') {
|
||||
const timeBucket = url.searchParams.get('timeBucket');
|
||||
if (!timeBucket) {
|
||||
return route.continue();
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable unicorn/no-this-outside-of-class */
|
||||
import type { AssetResponseDto } from '@immich/sdk';
|
||||
import { expect, Page } from '@playwright/test';
|
||||
|
||||
|
||||
@@ -72,6 +72,7 @@ export const thumbnailUtils = {
|
||||
},
|
||||
async queryThumbnailInViewport(page: Page, collector: (assetId: string) => boolean) {
|
||||
const assetIds: string[] = [];
|
||||
// eslint-disable-next-line unicorn/no-this-outside-of-class
|
||||
for (const thumb of await this.locator(page).all()) {
|
||||
const box = await thumb.boundingBox();
|
||||
if (box) {
|
||||
@@ -151,6 +152,7 @@ export const timelineUtils = {
|
||||
page.evaluate(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
|
||||
return document.querySelector('#asset-grid').scrollTop;
|
||||
});
|
||||
await expect.poll(queryTop).toBeGreaterThan(0);
|
||||
@@ -177,6 +179,7 @@ export const assetViewerUtils = {
|
||||
page.evaluate(() => {
|
||||
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
|
||||
// @ts-ignore
|
||||
// eslint-disable-next-line unicorn/no-optional-chaining-on-undeclared-variable
|
||||
return document.activeElement?.dataset?.asset;
|
||||
});
|
||||
await expect(poll(page, activeElement, (result) => result === assetId)).resolves.toBe(assetId);
|
||||
|
||||
+6
-3
@@ -1,3 +1,4 @@
|
||||
/* eslint-disable unicorn/no-top-level-assignment-in-function */
|
||||
import {
|
||||
AssetMediaCreateDto,
|
||||
AssetMediaResponseDto,
|
||||
@@ -177,7 +178,7 @@ export const utils = {
|
||||
resetDatabase: async (tables?: string[]) => {
|
||||
client = await utils.connectDatabase();
|
||||
|
||||
tables = tables || [
|
||||
tables ||= [
|
||||
// TODO e2e test for deleting a stack, since it is quite complex
|
||||
'stack',
|
||||
'library',
|
||||
@@ -304,7 +305,7 @@ export const utils = {
|
||||
},
|
||||
|
||||
adminSetup: async (options?: AdminSetupOptions) => {
|
||||
options = options || { onboarding: true };
|
||||
options ||= { onboarding: true };
|
||||
|
||||
await signUpAdmin({ signUpDto: signupDto.admin });
|
||||
const response = await login({ loginCredentialDto: loginDto.admin });
|
||||
@@ -545,6 +546,7 @@ export const utils = {
|
||||
{
|
||||
headers: asBearerAuth(accessToken),
|
||||
fetch: (...args: Parameters<typeof fetch>) =>
|
||||
// eslint-disable-next-line unicorn/no-invalid-argument-count, unicorn/prefer-await
|
||||
fetch(...args).then((response) => {
|
||||
setCookie = response.headers.getSetCookie();
|
||||
return response;
|
||||
@@ -674,7 +676,7 @@ export const utils = {
|
||||
|
||||
cliLogin: async (accessToken: string) => {
|
||||
const key = await utils.createApiKey(accessToken, [Permission.All]);
|
||||
await immichCli(['login', app, `${key.secret}`]);
|
||||
await immichCli(['login', app, key.secret]);
|
||||
return key.secret;
|
||||
},
|
||||
|
||||
@@ -706,6 +708,7 @@ export const utils = {
|
||||
},
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/no-top-level-side-effects
|
||||
utils.initSdk();
|
||||
|
||||
if (!existsSync(`${testAssetDir}/albums`)) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2023",
|
||||
"lib": ["dom", "ESNext"],
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"incremental": true,
|
||||
|
||||
@@ -40,10 +40,16 @@ export default typescriptEslint.config([
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'unicorn/prefer-module': 'off',
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/name-replacements': 'off',
|
||||
'unicorn/no-unreadable-for-of-expression': 'off',
|
||||
'unicorn/no-declarations-before-early-exit': 'off',
|
||||
'unicorn/no-process-exit': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
'unicorn/consistent-class-member-order': 'off',
|
||||
curly: 2,
|
||||
// prefer the typescript-eslint type-aware version
|
||||
'unicorn/require-array-sort-compare': 'off',
|
||||
'@typescript-eslint/require-array-sort-compare': 'error',
|
||||
'prettier/prettier': 0,
|
||||
'object-shorthand': ['error', 'always'],
|
||||
},
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^64.0.0",
|
||||
"eslint-plugin-unicorn": "^70.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.7.4",
|
||||
|
||||
@@ -39,7 +39,7 @@ describe('uploadFiles', () => {
|
||||
const testDir = fs.mkdtempSync(path.join(os.tmpdir(), 'test-'));
|
||||
const testFilePath = path.join(testDir, 'test.png');
|
||||
const testFileData = 'test';
|
||||
const baseUrl = 'http://example.com';
|
||||
const baseUrl = 'https://example.com';
|
||||
const apiKey = 'key';
|
||||
const retry = 3;
|
||||
|
||||
|
||||
@@ -54,6 +54,7 @@ class UploadFile extends File {
|
||||
super([], basename(filepath));
|
||||
}
|
||||
|
||||
// @ts-expect-error size is already a property on the new File interface
|
||||
get size() {
|
||||
return this._size;
|
||||
}
|
||||
@@ -433,7 +434,7 @@ const uploadFile = async (input: string, stats: Stats): Promise<AssetMediaRespon
|
||||
throw new Error(await response.text());
|
||||
}
|
||||
|
||||
return response.json();
|
||||
return response.json() as Promise<AssetMediaResponseDto>;
|
||||
};
|
||||
|
||||
export const findSidecar = (filepath: string): string | undefined => {
|
||||
@@ -570,7 +571,7 @@ const updateAlbums = async (assets: Asset[], options: UploadOptionsDto) => {
|
||||
albumUpdateProgress.start(assets.length, 0);
|
||||
|
||||
try {
|
||||
for (const [albumId, assets] of albumToAssets.entries()) {
|
||||
for (const [albumId, assets] of albumToAssets) {
|
||||
for (const assetBatch of chunk(assets, Math.min(1000 * concurrency, 65_000))) {
|
||||
await addAssetsToAlbum({ id: albumId, bulkIdsDto: { ids: assetBatch } });
|
||||
albumUpdateProgress.increment(assetBatch.length);
|
||||
|
||||
@@ -52,10 +52,7 @@ export class Queue<T, R> {
|
||||
}
|
||||
|
||||
get tasks(): Task<T, R>[] {
|
||||
const tasks: Task<T, R>[] = [];
|
||||
for (const task of this.store.values()) {
|
||||
tasks.push(task);
|
||||
}
|
||||
const tasks: Task<T, R>[] = this.store.values().toArray();
|
||||
return tasks;
|
||||
}
|
||||
|
||||
|
||||
@@ -39,6 +39,7 @@ export const s = (count: number) => (count === 1 ? '' : 's');
|
||||
let _apiKey: ApiKeyResponseDto;
|
||||
export const requirePermissions = async (permissions: Permission[]) => {
|
||||
if (!_apiKey) {
|
||||
// eslint-disable-next-line unicorn/no-top-level-assignment-in-function
|
||||
_apiKey = await getMyApiKey();
|
||||
}
|
||||
|
||||
@@ -67,8 +68,9 @@ Please make sure your API key has the correct permissions.`,
|
||||
export const connect = async (url: string, key: string) => {
|
||||
const wellKnownUrl = new URL('.well-known/immich', url);
|
||||
try {
|
||||
const wellKnown = await fetch(wellKnownUrl).then((response) => response.json());
|
||||
const endpoint = new URL(wellKnown.api.endpoint, url).toString();
|
||||
// eslint-disable-next-line unicorn/prefer-await
|
||||
const wellKnown = (await fetch(wellKnownUrl).then((response) => response.json())) as { api: { endpoint: string } };
|
||||
const endpoint = new URL(wellKnown.api.endpoint, url).href;
|
||||
if (endpoint !== url) {
|
||||
console.debug(`Discovered API at ${endpoint}`);
|
||||
}
|
||||
@@ -178,7 +180,7 @@ export const crawl = async (options: CrawlOptions): Promise<string[]> => {
|
||||
const searchPatterns = patterns.map((pattern) => {
|
||||
let escapedPattern = pattern.replaceAll("'", "[']").replaceAll('"', '["]').replaceAll('`', '[`]');
|
||||
if (recursive) {
|
||||
escapedPattern = escapedPattern + '/**';
|
||||
escapedPattern += '/**';
|
||||
}
|
||||
return `${escapedPattern}/*.{${extensions.join(',')}}`;
|
||||
});
|
||||
@@ -238,10 +240,12 @@ export class Batcher<T = unknown> {
|
||||
}
|
||||
|
||||
private clearDebounceTimer() {
|
||||
if (this.debounceTimer) {
|
||||
clearTimeout(this.debounceTimer);
|
||||
this.debounceTimer = undefined;
|
||||
if (!this.debounceTimer) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearTimeout(this.debounceTimer);
|
||||
this.debounceTimer = undefined;
|
||||
}
|
||||
|
||||
add(item: T) {
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"allowSyntheticDefaultImports": true,
|
||||
"resolveJsonModule": true,
|
||||
"target": "es2023",
|
||||
"lib": ["ESNext"],
|
||||
"sourceMap": true,
|
||||
"outDir": "./dist",
|
||||
"incremental": true,
|
||||
|
||||
Generated
+25
-172
@@ -34,7 +34,7 @@ importers:
|
||||
dependencies:
|
||||
'@docusaurus/core':
|
||||
specifier: ~3.10.0
|
||||
version: 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(esbuild@0.28.1)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
|
||||
version: 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
|
||||
'@docusaurus/preset-classic':
|
||||
specifier: ~3.10.0
|
||||
version: 3.10.1(@algolia/client-search@5.54.0)(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(@types/react@19.2.17)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(search-insights@2.17.3)(typescript@6.0.3)
|
||||
@@ -58,7 +58,7 @@ importers:
|
||||
version: 10.5.0(postcss@8.5.15)
|
||||
docusaurus-lunr-search:
|
||||
specifier: ^3.3.2
|
||||
version: 3.6.0(@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(esbuild@0.28.1)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
version: 3.6.0(@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
lunr:
|
||||
specifier: ^2.3.9
|
||||
version: 2.3.9
|
||||
@@ -151,8 +151,8 @@ importers:
|
||||
specifier: ^5.1.3
|
||||
version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.5.0(jiti@2.7.0)))(eslint@10.5.0(jiti@2.7.0))(prettier@3.8.4)
|
||||
eslint-plugin-unicorn:
|
||||
specifier: ^64.0.0
|
||||
version: 64.0.0(eslint@10.5.0(jiti@2.7.0))
|
||||
specifier: ^70.0.0
|
||||
version: 70.0.0(eslint@10.5.0(jiti@2.7.0))
|
||||
exiftool-vendored:
|
||||
specifier: ^35.0.0
|
||||
version: 35.21.0
|
||||
@@ -260,8 +260,8 @@ importers:
|
||||
specifier: ^5.1.3
|
||||
version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.5.0(jiti@2.7.0)))(eslint@10.5.0(jiti@2.7.0))(prettier@3.8.4)
|
||||
eslint-plugin-unicorn:
|
||||
specifier: ^64.0.0
|
||||
version: 64.0.0(eslint@10.5.0(jiti@2.7.0))
|
||||
specifier: ^70.0.0
|
||||
version: 70.0.0(eslint@10.5.0(jiti@2.7.0))
|
||||
globals:
|
||||
specifier: ^17.0.0
|
||||
version: 17.6.0
|
||||
@@ -729,8 +729,8 @@ importers:
|
||||
specifier: ^5.1.3
|
||||
version: 5.5.6(@types/eslint@9.6.1)(eslint-config-prettier@10.1.8(eslint@10.5.0(jiti@2.7.0)))(eslint@10.5.0(jiti@2.7.0))(prettier@3.8.4)
|
||||
eslint-plugin-unicorn:
|
||||
specifier: ^64.0.0
|
||||
version: 64.0.0(eslint@10.5.0(jiti@2.7.0))
|
||||
specifier: ^70.0.0
|
||||
version: 70.0.0(eslint@10.5.0(jiti@2.7.0))
|
||||
globals:
|
||||
specifier: ^17.0.0
|
||||
version: 17.6.0
|
||||
@@ -991,8 +991,8 @@ importers:
|
||||
specifier: ^3.12.4
|
||||
version: 3.19.0(eslint@10.5.0(jiti@2.7.0))(svelte@5.56.3(@typescript-eslint/types@8.61.0))
|
||||
eslint-plugin-unicorn:
|
||||
specifier: ^64.0.0
|
||||
version: 64.0.0(eslint@10.5.0(jiti@2.7.0))
|
||||
specifier: ^70.0.0
|
||||
version: 70.0.0(eslint@10.5.0(jiti@2.7.0))
|
||||
factory.ts:
|
||||
specifier: ^1.4.1
|
||||
version: 1.4.2
|
||||
@@ -6509,10 +6509,6 @@ packages:
|
||||
resolution: {integrity: sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==}
|
||||
engines: {node: '>= 10.0'}
|
||||
|
||||
clean-regexp@1.0.0:
|
||||
resolution: {integrity: sha512-GfisEZEJvzKrmGWkvfhgzcz/BllN1USeqD2V6tg14OAOgaCD2Z/PUEuxnAZ/nPvmaHRG7a8y77p1T/IRQ4D1Hw==}
|
||||
engines: {node: '>=4'}
|
||||
|
||||
clean-stack@2.2.0:
|
||||
resolution: {integrity: sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==}
|
||||
engines: {node: '>=6'}
|
||||
@@ -7288,6 +7284,10 @@ packages:
|
||||
detect-europe-js@0.1.2:
|
||||
resolution: {integrity: sha512-lgdERlL3u0aUdHocoouzT10d9I89VVhk0qNRmll7mXdGfJT1/wqZ2ZLA4oJAjeACPY5fT1wsbq2AT+GkuInsow==}
|
||||
|
||||
detect-indent@7.0.2:
|
||||
resolution: {integrity: sha512-y+8xyqdGLL+6sh0tVeHcfP/QDd8gUgbasolJJpY7NgeQGSZ739bDtSiaiDgtoicy+mtYB81dKLxO9xRhCyIB3A==}
|
||||
engines: {node: '>=12.20'}
|
||||
|
||||
detect-libc@2.1.2:
|
||||
resolution: {integrity: sha512-Btj2BOOO83o3WyH59e8MgXsxEQVcarkUOpEYrubB0urwnN10yQ364rsiByU11nZlqWYZm05i/of7io4mzihBtQ==}
|
||||
engines: {node: '>=8'}
|
||||
@@ -7643,11 +7643,11 @@ packages:
|
||||
svelte:
|
||||
optional: true
|
||||
|
||||
eslint-plugin-unicorn@64.0.0:
|
||||
resolution: {integrity: sha512-rNZwalHh8i0UfPlhNwg5BTUO1CMdKNmjqe+TgzOTZnpKoi8VBgsW7u9qCHIdpxEzZ1uwrJrPF0uRb7l//K38gA==}
|
||||
engines: {node: ^20.10.0 || >=21.0.0}
|
||||
eslint-plugin-unicorn@70.0.0:
|
||||
resolution: {integrity: sha512-uAF9xMcVvvhTfvusCgogJ1wh4To3q2KhVMw3i1Apf/ILTbxsCjscvraAZACsEurb7no2fdXblD3whcbVnjw5zg==}
|
||||
engines: {node: '>=22'}
|
||||
peerDependencies:
|
||||
eslint: '>=9.38.0'
|
||||
eslint: '>=10.4'
|
||||
|
||||
eslint-scope@5.1.1:
|
||||
resolution: {integrity: sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==}
|
||||
@@ -11042,10 +11042,6 @@ packages:
|
||||
regenerate@1.4.2:
|
||||
resolution: {integrity: sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==}
|
||||
|
||||
regexp-tree@0.1.27:
|
||||
resolution: {integrity: sha512-iETxpjK6YoRWJG5o6hXLwvjYAoW+FEZn9os0PD/b6AP6xQwsa/Y7lCVgIixBbUPMfhu+i2LtdeAqVTgGlQarfA==}
|
||||
hasBin: true
|
||||
|
||||
regexpu-core@6.4.0:
|
||||
resolution: {integrity: sha512-0ghuzq67LI9bLXpOX/ISfve/Mq33a4aFRzoQYhnnok1JOFpmE/A2TBGkNVenOGEeSBCjIiWcc6MVOG5HEQv0sA==}
|
||||
engines: {node: '>=4'}
|
||||
@@ -14402,49 +14398,6 @@ snapshots:
|
||||
- uglify-js
|
||||
- webpack-cli
|
||||
|
||||
'@docusaurus/bundler@3.10.1(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
'@docusaurus/babel': 3.10.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
'@docusaurus/cssnano-preset': 3.10.1
|
||||
'@docusaurus/logger': 3.10.1
|
||||
'@docusaurus/types': 3.10.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
'@docusaurus/utils': 3.10.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(html-minifier-terser@7.2.0)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
babel-loader: 9.2.1(@babel/core@7.29.7)(webpack@5.107.2(postcss@8.5.15))
|
||||
clean-css: 5.3.3
|
||||
copy-webpack-plugin: 11.0.0(webpack@5.107.2(postcss@8.5.15))
|
||||
css-loader: 6.11.0(webpack@5.107.2(postcss@8.5.15))
|
||||
css-minimizer-webpack-plugin: 5.0.1(clean-css@5.3.3)(esbuild@0.28.1)(webpack@5.107.2(postcss@8.5.15))
|
||||
cssnano: 6.1.2(postcss@8.5.15)
|
||||
file-loader: 6.2.0(webpack@5.107.2(postcss@8.5.15))
|
||||
html-minifier-terser: 7.2.0
|
||||
mini-css-extract-plugin: 2.10.2(webpack@5.107.2(postcss@8.5.15))
|
||||
null-loader: 4.0.1(webpack@5.107.2(postcss@8.5.15))
|
||||
postcss: 8.5.15
|
||||
postcss-loader: 7.3.4(postcss@8.5.15)(typescript@6.0.3)(webpack@5.107.2(postcss@8.5.15))
|
||||
postcss-preset-env: 10.6.1(postcss@8.5.15)
|
||||
terser-webpack-plugin: 5.6.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.15)(webpack@5.107.2(postcss@8.5.15))
|
||||
tslib: 2.8.1
|
||||
url-loader: 4.1.1(file-loader@6.2.0(webpack@5.107.2(postcss@8.5.15)))(webpack@5.107.2(postcss@8.5.15))
|
||||
webpack: 5.107.2(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(html-minifier-terser@7.2.0)(postcss@8.5.15)
|
||||
webpackbar: 7.0.0(webpack@5.107.2(postcss@8.5.15))
|
||||
transitivePeerDependencies:
|
||||
- '@minify-html/node'
|
||||
- '@parcel/css'
|
||||
- '@rspack/core'
|
||||
- '@swc/core'
|
||||
- '@swc/css'
|
||||
- '@swc/html'
|
||||
- csso
|
||||
- esbuild
|
||||
- lightningcss
|
||||
- react
|
||||
- react-dom
|
||||
- supports-color
|
||||
- typescript
|
||||
- uglify-js
|
||||
- webpack-cli
|
||||
|
||||
'@docusaurus/bundler@3.10.1(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@babel/core': 7.29.7
|
||||
@@ -14488,75 +14441,6 @@ snapshots:
|
||||
- uglify-js
|
||||
- webpack-cli
|
||||
|
||||
'@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(esbuild@0.28.1)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@docusaurus/babel': 3.10.1(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
'@docusaurus/bundler': 3.10.1(esbuild@0.28.1)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
|
||||
'@docusaurus/logger': 3.10.1
|
||||
'@docusaurus/mdx-loader': 3.10.1(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
'@docusaurus/utils': 3.10.1(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
'@docusaurus/utils-common': 3.10.1(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
'@docusaurus/utils-validation': 3.10.1(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
'@mdx-js/react': 3.1.1(@types/react@19.2.17)(react@19.2.7)
|
||||
boxen: 6.2.1
|
||||
chalk: 4.1.2
|
||||
chokidar: 3.6.0
|
||||
cli-table3: 0.6.5
|
||||
combine-promises: 1.2.0
|
||||
commander: 5.1.0
|
||||
core-js: 3.49.0
|
||||
detect-port: 1.6.1
|
||||
escape-html: 1.0.3
|
||||
eta: 2.2.0
|
||||
eval: 0.1.8
|
||||
execa: 5.1.1
|
||||
fs-extra: 11.3.5
|
||||
html-tags: 3.3.1
|
||||
html-webpack-plugin: 5.6.7(webpack@5.107.2(postcss@8.5.15))
|
||||
leven: 3.1.0
|
||||
lodash: 4.18.1
|
||||
open: 8.4.2
|
||||
p-map: 4.0.0
|
||||
prompts: 2.4.2
|
||||
react: 19.2.7
|
||||
react-dom: 19.2.7(react@19.2.7)
|
||||
react-helmet-async: '@slorber/react-helmet-async@1.3.0(react-dom@19.2.7(react@19.2.7))(react@19.2.7)'
|
||||
react-loadable: '@docusaurus/react-loadable@6.0.0(react@19.2.7)'
|
||||
react-loadable-ssr-addon-v5-slorber: 1.0.3(@docusaurus/react-loadable@6.0.0(react@19.2.7))(webpack@5.107.2(postcss@8.5.15))
|
||||
react-router: 5.3.4(react@19.2.7)
|
||||
react-router-config: 5.1.1(react-router@5.3.4(react@19.2.7))(react@19.2.7)
|
||||
react-router-dom: 5.3.4(react@19.2.7)
|
||||
semver: 7.8.4
|
||||
serve-handler: 6.1.7
|
||||
tinypool: 1.1.1
|
||||
tslib: 2.8.1
|
||||
update-notifier: 6.0.2
|
||||
webpack: 5.107.2(postcss@8.5.15)
|
||||
webpack-bundle-analyzer: 4.10.2
|
||||
webpack-dev-server: 5.2.5(tslib@2.8.1)(webpack@5.107.2(postcss@8.5.15))
|
||||
webpack-merge: 6.0.1
|
||||
transitivePeerDependencies:
|
||||
- '@minify-html/node'
|
||||
- '@parcel/css'
|
||||
- '@rspack/core'
|
||||
- '@swc/core'
|
||||
- '@swc/css'
|
||||
- '@swc/html'
|
||||
- bufferutil
|
||||
- clean-css
|
||||
- cssnano
|
||||
- csso
|
||||
- debug
|
||||
- esbuild
|
||||
- html-minifier-terser
|
||||
- lightningcss
|
||||
- postcss
|
||||
- supports-color
|
||||
- typescript
|
||||
- uglify-js
|
||||
- utf-8-validate
|
||||
- webpack-cli
|
||||
|
||||
'@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)':
|
||||
dependencies:
|
||||
'@docusaurus/babel': 3.10.1(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)
|
||||
@@ -19613,10 +19497,6 @@ snapshots:
|
||||
dependencies:
|
||||
source-map: 0.6.1
|
||||
|
||||
clean-regexp@1.0.0:
|
||||
dependencies:
|
||||
escape-string-regexp: 1.0.5
|
||||
|
||||
clean-stack@2.2.0: {}
|
||||
|
||||
cli-boxes@3.0.0: {}
|
||||
@@ -19977,19 +19857,6 @@ snapshots:
|
||||
optionalDependencies:
|
||||
webpack: 5.107.2(postcss@8.5.15)
|
||||
|
||||
css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(esbuild@0.28.1)(webpack@5.107.2(postcss@8.5.15)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
cssnano: 6.1.2(postcss@8.5.15)
|
||||
jest-worker: 29.7.0
|
||||
postcss: 8.5.15
|
||||
schema-utils: 4.3.3
|
||||
serialize-javascript: 6.0.2
|
||||
webpack: 5.107.2(postcss@8.5.15)
|
||||
optionalDependencies:
|
||||
clean-css: 5.3.3
|
||||
esbuild: 0.28.1
|
||||
|
||||
css-minimizer-webpack-plugin@5.0.1(clean-css@5.3.3)(webpack@5.107.2(postcss@8.5.15)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
@@ -20399,6 +20266,8 @@ snapshots:
|
||||
|
||||
detect-europe-js@0.1.2: {}
|
||||
|
||||
detect-indent@7.0.2: {}
|
||||
|
||||
detect-libc@2.1.2: {}
|
||||
|
||||
detect-node@2.1.0: {}
|
||||
@@ -20467,9 +20336,9 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- supports-color
|
||||
|
||||
docusaurus-lunr-search@3.6.0(@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(esbuild@0.28.1)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||
docusaurus-lunr-search@3.6.0(@docusaurus/core@3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3))(react-dom@19.2.7(react@19.2.7))(react@19.2.7):
|
||||
dependencies:
|
||||
'@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(esbuild@0.28.1)(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
|
||||
'@docusaurus/core': 3.10.1(@mdx-js/react@3.1.1(@types/react@19.2.17)(react@19.2.7))(postcss@8.5.15)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@6.0.3)
|
||||
autocomplete.js: 0.37.1
|
||||
clsx: 2.1.1
|
||||
gauge: 3.0.2
|
||||
@@ -20890,14 +20759,15 @@ snapshots:
|
||||
transitivePeerDependencies:
|
||||
- ts-node
|
||||
|
||||
eslint-plugin-unicorn@64.0.0(eslint@10.5.0(jiti@2.7.0)):
|
||||
eslint-plugin-unicorn@70.0.0(eslint@10.5.0(jiti@2.7.0)):
|
||||
dependencies:
|
||||
'@babel/helper-validator-identifier': 7.29.7
|
||||
'@eslint-community/eslint-utils': 4.9.1(eslint@10.5.0(jiti@2.7.0))
|
||||
browserslist: 4.28.2
|
||||
change-case: 5.4.4
|
||||
ci-info: 4.4.0
|
||||
clean-regexp: 1.0.0
|
||||
core-js-compat: 3.49.0
|
||||
detect-indent: 7.0.2
|
||||
eslint: 10.5.0(jiti@2.7.0)
|
||||
find-up-simple: 1.0.1
|
||||
globals: 17.6.0
|
||||
@@ -20905,7 +20775,6 @@ snapshots:
|
||||
is-builtin-module: 5.0.0
|
||||
jsesc: 3.1.0
|
||||
pluralize: 8.0.0
|
||||
regexp-tree: 0.1.27
|
||||
regjsparser: 0.13.2
|
||||
semver: 7.8.4
|
||||
strip-indent: 4.1.1
|
||||
@@ -24890,8 +24759,6 @@ snapshots:
|
||||
|
||||
regenerate@1.4.2: {}
|
||||
|
||||
regexp-tree@0.1.27: {}
|
||||
|
||||
regexpu-core@6.4.0:
|
||||
dependencies:
|
||||
regenerate: 1.4.2
|
||||
@@ -26068,20 +25935,6 @@ snapshots:
|
||||
esbuild: 0.28.1
|
||||
lightningcss: 1.32.0
|
||||
|
||||
terser-webpack-plugin@5.6.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(esbuild@0.28.1)(html-minifier-terser@7.2.0)(postcss@8.5.15)(webpack@5.107.2(postcss@8.5.15)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
jest-worker: 27.5.1
|
||||
schema-utils: 4.3.3
|
||||
terser: 5.48.0
|
||||
webpack: 5.107.2(postcss@8.5.15)
|
||||
optionalDependencies:
|
||||
clean-css: 5.3.3
|
||||
cssnano: 6.1.2(postcss@8.5.15)
|
||||
esbuild: 0.28.1
|
||||
html-minifier-terser: 7.2.0
|
||||
postcss: 8.5.15
|
||||
|
||||
terser-webpack-plugin@5.6.1(clean-css@5.3.3)(cssnano@6.1.2(postcss@8.5.15))(html-minifier-terser@7.2.0)(postcss@8.5.15)(webpack@5.107.2(postcss@8.5.15)):
|
||||
dependencies:
|
||||
'@jridgewell/trace-mapping': 0.3.31
|
||||
|
||||
@@ -39,7 +39,7 @@ export default typescriptEslint.config([
|
||||
'@typescript-eslint/explicit-module-boundary-types': 'off',
|
||||
'@typescript-eslint/no-explicit-any': 'off',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'unicorn/prevent-abbreviations': 'off',
|
||||
'unicorn/name-replacements': 'off',
|
||||
'unicorn/filename-case': 'off',
|
||||
'unicorn/no-null': 'off',
|
||||
'unicorn/prefer-top-level-await': 'off',
|
||||
@@ -49,6 +49,24 @@ export default typescriptEslint.config([
|
||||
'unicorn/prefer-structured-clone': 'off',
|
||||
'unicorn/no-for-loop': 'off',
|
||||
'unicorn/no-array-sort': 'off',
|
||||
'unicorn/no-unreadable-for-of-expression': 'off',
|
||||
'unicorn/no-break-in-nested-loop': 'off',
|
||||
'unicorn/no-top-level-assignment-in-function': 'off',
|
||||
'unicorn/prefer-uint8array-base64': 'off',
|
||||
'unicorn/max-nested-calls': 'off',
|
||||
'unicorn/no-declarations-before-early-exit': 'off',
|
||||
'unicorn/no-unreadable-object-destructuring': 'off',
|
||||
// maybe we do want to enable this later. TBD
|
||||
'unicorn/prefer-await': 'off',
|
||||
'unicorn/consistent-class-member-order': 'off',
|
||||
'unicorn/class-reference-in-static-methods': ['error', { preferThis: false, preferSuper: false }],
|
||||
'unicorn/no-unsafe-property-key': 'off',
|
||||
'unicorn/consistent-boolean-name': 'off',
|
||||
'unicorn/no-computed-property-existence-check': 'off',
|
||||
'unicorn/no-non-function-verb-prefix': 'off',
|
||||
// prefer the typescript-eslint type-aware version
|
||||
'unicorn/require-array-sort-compare': 'off',
|
||||
'@typescript-eslint/require-array-sort-compare': 'error',
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
'@typescript-eslint/switch-exhaustiveness-check': ['error', { considerDefaultExhaustiveForUnions: true }],
|
||||
|
||||
+1
-1
@@ -151,7 +151,7 @@
|
||||
"eslint": "^10.0.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^64.0.0",
|
||||
"eslint-plugin-unicorn": "^70.0.0",
|
||||
"globals": "^17.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"pngjs": "^7.0.0",
|
||||
|
||||
@@ -63,6 +63,7 @@ const commonImports = [
|
||||
|
||||
const bullImports = [BullModule.forRoot(bull.config), BullModule.registerQueue(...bull.queues)];
|
||||
|
||||
// eslint-disable-next-line unicorn/no-top-level-side-effects
|
||||
configureUserAgent();
|
||||
|
||||
export class BaseModule implements OnModuleInit, OnModuleDestroy {
|
||||
|
||||
@@ -23,6 +23,7 @@ const handleError = (label: string, error: Error | any) => {
|
||||
console.error(`${label} error: ${error}`);
|
||||
};
|
||||
|
||||
// eslint-disable-next-line unicorn/no-exports-in-scripts
|
||||
export class SqlLogger {
|
||||
queries: string[] = [];
|
||||
errors: Array<{ error: string | Error; query: string }> = [];
|
||||
@@ -109,10 +110,12 @@ class SqlGenerator {
|
||||
const instance = this.app.get<Repository>(Repository);
|
||||
|
||||
// normal repositories
|
||||
data.push(...(await this.runTargets(instance, `${Repository.name}`)));
|
||||
data.push(...(await this.runTargets(instance, Repository.name)));
|
||||
|
||||
// nested repositories
|
||||
if (Repository.name === AccessRepository.name || Repository.name === SyncRepository.name) {
|
||||
// probably a bug that this fails linting?
|
||||
// eslint-disable-next-line unicorn/prefer-object-iterable-methods
|
||||
for (const key of Object.keys(instance)) {
|
||||
const subInstance = (instance as any)[key];
|
||||
data.push(...(await this.runTargets(subInstance, `${Repository.name}.${key}`)));
|
||||
@@ -127,7 +130,7 @@ class SqlGenerator {
|
||||
|
||||
for (const key of this.getPropertyNames(instance)) {
|
||||
const target = instance[key];
|
||||
if (!(typeof target === 'function')) {
|
||||
if (typeof target !== 'function') {
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ export class ChangeMediaLocationCommand extends CommandRunner {
|
||||
{},
|
||||
);
|
||||
|
||||
const success = await this.service.migrateFilePaths({
|
||||
const isSuccess = await this.service.migrateFilePaths({
|
||||
oldValue,
|
||||
newValue,
|
||||
confirm: async ({ sourceFolder, targetFolder }) => {
|
||||
@@ -65,7 +65,7 @@ export class ChangeMediaLocationCommand extends CommandRunner {
|
||||
...
|
||||
)`;
|
||||
|
||||
console.log(`\n ${success ? successMessage : 'No rows were updated'}\n`);
|
||||
console.log(`\n ${isSuccess ? successMessage : 'No rows were updated'}\n`);
|
||||
|
||||
await this.showSamplePaths('after');
|
||||
} catch (error) {
|
||||
|
||||
@@ -131,7 +131,7 @@ export class AssetMediaController {
|
||||
const [_, reqSearch] = req.url.split('?');
|
||||
const redirSearchParams = new URLSearchParams(reqSearch);
|
||||
redirSearchParams.delete('size');
|
||||
return res.redirect('original' + '?' + redirSearchParams.toString());
|
||||
return res.redirect('original?' + redirSearchParams.toString());
|
||||
}
|
||||
|
||||
const viewThumbnailRes = await this.service.viewThumbnail(auth, id, dto);
|
||||
|
||||
@@ -32,7 +32,9 @@ describe(DownloadController.name, () => {
|
||||
it('should be an authenticated route', async () => {
|
||||
const stream = new Readable({
|
||||
read() {
|
||||
// eslint-disable-next-line unicorn/no-this-outside-of-class
|
||||
this.push('test');
|
||||
// eslint-disable-next-line unicorn/no-this-outside-of-class
|
||||
this.push(null);
|
||||
},
|
||||
});
|
||||
|
||||
@@ -65,12 +65,14 @@ export class MaintenanceController {
|
||||
@GetLoginDetails() loginDetails: LoginDetails,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
): Promise<void> {
|
||||
if (dto.action !== MaintenanceAction.End) {
|
||||
const { jwt } = await this.service.startMaintenance(dto, auth.user.name);
|
||||
return respondWithCookie(res, undefined, {
|
||||
isSecure: loginDetails.isSecure,
|
||||
values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }],
|
||||
});
|
||||
if (dto.action === MaintenanceAction.End) {
|
||||
return;
|
||||
}
|
||||
|
||||
const { jwt } = await this.service.startMaintenance(dto, auth.user.name);
|
||||
return respondWithCookie(res, undefined, {
|
||||
isSecure: loginDetails.isSecure,
|
||||
values: [{ key: ImmichCookie.MaintenanceToken, value: jwt }],
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,10 +14,10 @@ function validConfig() {
|
||||
notifications: { smtp: { from: string; transport: { host: string } } };
|
||||
server: { externalDomain: string };
|
||||
};
|
||||
config.oauth.mobileRedirectUri = config.oauth.mobileRedirectUri || 'https://example.com';
|
||||
config.server.externalDomain = config.server.externalDomain || 'https://example.com';
|
||||
config.notifications.smtp.from = config.notifications.smtp.from || 'noreply@example.com';
|
||||
config.notifications.smtp.transport.host = config.notifications.smtp.transport.host || 'localhost';
|
||||
config.oauth.mobileRedirectUri ||= 'https://example.com';
|
||||
config.server.externalDomain ||= 'https://example.com';
|
||||
config.notifications.smtp.from ||= 'noreply@example.com';
|
||||
config.notifications.smtp.transport.host ||= 'localhost';
|
||||
return config;
|
||||
}
|
||||
|
||||
|
||||
@@ -33,7 +33,7 @@ describe(TimelineController.name, () => {
|
||||
expect(service.getTimeBuckets).toHaveBeenCalledWith(
|
||||
undefined,
|
||||
expect.objectContaining({
|
||||
bbox: { west: 11.075_683, south: 49.416_711, east: 11.117_589, north: 49.454_875 },
|
||||
bbox: { west: 11.075683, south: 49.416711, east: 11.117589, north: 49.454875 },
|
||||
}),
|
||||
);
|
||||
});
|
||||
|
||||
@@ -202,20 +202,20 @@ export class StorageCore {
|
||||
let move = await this.moveRepository.getByEntity(entityId, pathType);
|
||||
if (move) {
|
||||
this.logger.log(`Attempting to finish incomplete move: ${move.oldPath} => ${move.newPath}`);
|
||||
const oldPathExists = await this.storageRepository.checkFileExists(move.oldPath);
|
||||
const newPathExists = await this.storageRepository.checkFileExists(move.newPath);
|
||||
const newPathCheck = newPathExists ? move.newPath : null;
|
||||
const actualPath = oldPathExists ? move.oldPath : newPathCheck;
|
||||
const isOldPathExists = await this.storageRepository.checkFileExists(move.oldPath);
|
||||
const isNewPathExists = await this.storageRepository.checkFileExists(move.newPath);
|
||||
const newPathCheck = isNewPathExists ? move.newPath : null;
|
||||
const actualPath = isOldPathExists ? move.oldPath : newPathCheck;
|
||||
if (!actualPath) {
|
||||
this.logger.warn('Unable to complete move. File does not exist at either location.');
|
||||
return;
|
||||
}
|
||||
|
||||
const fileAtNewLocation = actualPath === move.newPath;
|
||||
this.logger.log(`Found file at ${fileAtNewLocation ? 'new' : 'old'} location`);
|
||||
const isFileAtNewLocation = actualPath === move.newPath;
|
||||
this.logger.log(`Found file at ${isFileAtNewLocation ? 'new' : 'old'} location`);
|
||||
|
||||
if (
|
||||
fileAtNewLocation &&
|
||||
isFileAtNewLocation &&
|
||||
!(await this.verifyNewPathContentsMatchesExpected(move.oldPath, move.newPath, assetInfo))
|
||||
) {
|
||||
this.logger.fatal(
|
||||
@@ -349,7 +349,7 @@ export class StorageCore {
|
||||
}
|
||||
|
||||
static getNestedPath(folder: StorageFolder, ownerId: string, filename: string): string {
|
||||
return join(this.getNestedFolder(folder, ownerId, filename), filename);
|
||||
return join(StorageCore.getNestedFolder(folder, ownerId, filename), filename);
|
||||
}
|
||||
|
||||
static getTempPathInDir(dir: string): string {
|
||||
|
||||
@@ -54,9 +54,8 @@ function chunks<T>(collection: Array<T> | Set<T>, size: number): Array<Array<T>>
|
||||
result.push(chunk);
|
||||
}
|
||||
return result;
|
||||
} else {
|
||||
return _.chunk(collection, size);
|
||||
}
|
||||
return _.chunk(collection, size);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,11 +81,13 @@ export function Chunked(
|
||||
(Array.isArray(argument) && argument.length <= chunkSize) ||
|
||||
(argument instanceof Set && argument.size <= chunkSize)
|
||||
) {
|
||||
// eslint-disable-next-line unicorn/no-this-outside-of-class
|
||||
return originalMethod.apply(this, arguments_);
|
||||
}
|
||||
|
||||
return Promise.all(
|
||||
chunks(argument, chunkSize).map((chunk) => {
|
||||
// eslint-disable-next-line unicorn/no-this-outside-of-class
|
||||
return Reflect.apply(originalMethod, this, [
|
||||
...arguments_.slice(0, parameterIndex),
|
||||
chunk,
|
||||
|
||||
@@ -175,7 +175,7 @@ const peopleFromFaces = (faces?: MaybeDehydrated<AssetFace>[]): PersonResponseDt
|
||||
}
|
||||
}
|
||||
|
||||
return [...peopleMap.values()];
|
||||
return peopleMap.values().toArray();
|
||||
};
|
||||
|
||||
const mapStack = (entity: { stack?: Stack | null }) => {
|
||||
|
||||
@@ -92,10 +92,7 @@ export class LibraryResponseDto extends createZodDto(LibraryResponseSchema) {}
|
||||
export class LibraryStatsResponseDto extends createZodDto(LibraryStatsResponseSchema) {}
|
||||
|
||||
export function mapLibrary(entity: Library): LibraryResponseDto {
|
||||
let assetCount = 0;
|
||||
if (entity.assets) {
|
||||
assetCount = entity.assets.length;
|
||||
}
|
||||
const assetCount = entity.assets ? entity.assets.length : 0;
|
||||
return {
|
||||
id: entity.id,
|
||||
ownerId: entity.ownerId,
|
||||
|
||||
+6
-5
@@ -68,9 +68,9 @@ class Workers {
|
||||
const { database } = new ConfigRepository().getEnv();
|
||||
const kysely = new Kysely<DB>(getKyselyConfig(database.config));
|
||||
|
||||
let locked = false;
|
||||
while (!locked) {
|
||||
locked = await kysely.connection().execute(async (conn) => {
|
||||
let isLocked = false;
|
||||
while (!isLocked) {
|
||||
isLocked = await kysely.connection().execute(async (conn) => {
|
||||
const { rows } = await sql<{
|
||||
pg_try_advisory_lock: boolean;
|
||||
}>`SELECT pg_try_advisory_lock(${DatabaseLock.MaintenanceOperation})`.execute(conn);
|
||||
@@ -110,6 +110,7 @@ class Workers {
|
||||
});
|
||||
|
||||
kill = (signal) => void worker.kill(signal);
|
||||
// eslint-disable-next-line unicorn/prefer-hoisting-branch-code
|
||||
anyWorker = worker;
|
||||
} else {
|
||||
const worker = new Worker(workerFile);
|
||||
@@ -151,9 +152,9 @@ class Workers {
|
||||
if (exitCode !== 0) {
|
||||
console.error(`${name} worker exited with code ${exitCode}`);
|
||||
|
||||
if (this.workers[ImmichWorker.Api] && name !== ImmichWorker.Api) {
|
||||
if (Object.hasOwn(this.workers, ImmichWorker.Api) && name !== ImmichWorker.Api) {
|
||||
console.error('Killing api process');
|
||||
void this.workers[ImmichWorker.Api].kill('SIGTERM');
|
||||
void this.workers[ImmichWorker.Api]!.kill('SIGTERM');
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -42,10 +42,12 @@ export class MaintenanceHealthRepository {
|
||||
worker.on('error', (error) => reject(new Error(`Server health check failed, process threw: ${error}`)));
|
||||
|
||||
setTimeout(() => {
|
||||
if (worker.exitCode === null) {
|
||||
reject(new Error('Server health check failed, took too long to start.'));
|
||||
worker.kill('SIGTERM');
|
||||
if (worker.exitCode !== null) {
|
||||
return;
|
||||
}
|
||||
|
||||
reject(new Error('Server health check failed, took too long to start.'));
|
||||
worker.kill('SIGTERM');
|
||||
}, 180_000);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -171,8 +171,8 @@ export class MaintenanceWorkerService {
|
||||
const candidates = ['/data', '/usr/src/app/upload'];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const exists = this.storageRepository.existsSync(candidate);
|
||||
if (exists) {
|
||||
const isExists = this.storageRepository.existsSync(candidate);
|
||||
if (isExists) {
|
||||
targets.push(candidate);
|
||||
}
|
||||
}
|
||||
@@ -295,8 +295,8 @@ export class MaintenanceWorkerService {
|
||||
}
|
||||
|
||||
async runRestoreDatabase(action: SetMaintenanceModeDto) {
|
||||
const lock = await this.databaseRepository.tryLock(DatabaseLock.MaintenanceOperation);
|
||||
if (!lock) {
|
||||
const isLock = await this.databaseRepository.tryLock(DatabaseLock.MaintenanceOperation);
|
||||
if (!isLock) {
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
@@ -130,7 +130,10 @@ class AlbumAccess {
|
||||
.where('shared_link.albumId', 'in', [...albumIds])
|
||||
.execute()
|
||||
.then(
|
||||
(sharedLinks) => new Set(sharedLinks.flatMap((sharedLink) => (sharedLink.albumId ? [sharedLink.albumId] : []))),
|
||||
(sharedLinks) =>
|
||||
new Set(
|
||||
sharedLinks.filter((sharedLink) => sharedLink.albumId).map((sharedLink) => sharedLink.albumId),
|
||||
) as Set<string>,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -314,7 +314,7 @@ export class AlbumRepository {
|
||||
albumUsers: AlbumUserCreateDto[],
|
||||
authUserId: string,
|
||||
) {
|
||||
if (!albumUsers.some((u) => u.role === AlbumUserRole.Owner)) {
|
||||
if (albumUsers.every((u) => u.role !== AlbumUserRole.Owner)) {
|
||||
throw new Error('Album must have an owner');
|
||||
}
|
||||
|
||||
|
||||
@@ -192,9 +192,8 @@ export class DatabaseRepository {
|
||||
) {
|
||||
probes[indexName] = this.targetProbeCount(targetLists);
|
||||
return this.reindexVectors(indexName, { lists: targetLists });
|
||||
} else {
|
||||
probes[indexName] = this.targetProbeCount(lists);
|
||||
}
|
||||
probes[indexName] = this.targetProbeCount(lists);
|
||||
}),
|
||||
);
|
||||
break;
|
||||
@@ -228,7 +227,7 @@ export class DatabaseRepository {
|
||||
if (table === 'smart_search') {
|
||||
await sql`ALTER TABLE ${sql.raw(table)} DROP CONSTRAINT IF EXISTS dim_size_constraint`.execute(tx);
|
||||
}
|
||||
if (!rows.some((row) => row.columnName === 'embedding')) {
|
||||
if (rows.every((row) => row.columnName !== 'embedding')) {
|
||||
this.logger.warn(`Column 'embedding' does not exist in table '${table}', truncating and adding column.`);
|
||||
await sql`TRUNCATE TABLE ${sql.raw(table)}`.execute(tx);
|
||||
await sql`ALTER TABLE ${sql.raw(table)} ADD COLUMN embedding real[] NOT NULL`.execute(tx);
|
||||
@@ -349,11 +348,9 @@ export class DatabaseRepository {
|
||||
private targetListCount(count: number) {
|
||||
if (count < 128_000) {
|
||||
return 1;
|
||||
} else if (count < 2_048_000) {
|
||||
return 1 << (32 - Math.clz32(count / 1000));
|
||||
} else {
|
||||
return 1 << (33 - Math.clz32(Math.sqrt(count)));
|
||||
}
|
||||
// eslint-disable-next-line unicorn/prefer-minimal-ternary
|
||||
return count < 2_048_000 ? 1 << (32 - Math.clz32(count / 1000)) : 1 << (33 - Math.clz32(Math.sqrt(count)));
|
||||
}
|
||||
|
||||
private targetProbeCount(lists: number) {
|
||||
@@ -378,9 +375,7 @@ export class DatabaseRepository {
|
||||
for (const result of results ?? []) {
|
||||
if (result.status === 'Success') {
|
||||
this.logger.log(`Migration "${result.migrationName}" succeeded`);
|
||||
}
|
||||
|
||||
if (result.status === 'Error') {
|
||||
} else if (result.status === 'Error') {
|
||||
this.logger.warn(`Migration "${result.migrationName}" failed`);
|
||||
}
|
||||
}
|
||||
@@ -485,9 +480,7 @@ export class DatabaseRepository {
|
||||
for (const result of results ?? []) {
|
||||
if (result.status === 'Success') {
|
||||
this.logger.log(`Reverted migration "${result.migrationName}"`);
|
||||
}
|
||||
|
||||
if (result.status === 'Error') {
|
||||
} else if (result.status === 'Error') {
|
||||
this.logger.warn(`Failed to revert migration "${result.migrationName}"`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -213,11 +213,11 @@ export class EventRepository {
|
||||
private addHandler<T extends EmitEvent>(item: Item<T>): void {
|
||||
const event = item.event;
|
||||
|
||||
if (!this.emitHandlers[event]) {
|
||||
if (!Object.hasOwn(this.emitHandlers, event)) {
|
||||
this.emitHandlers[event] = [];
|
||||
}
|
||||
|
||||
this.emitHandlers[event].push(item);
|
||||
this.emitHandlers[event]!.push(item);
|
||||
}
|
||||
|
||||
emit<T extends EmitEvent>(event: T, ...args: ArgsOf<T>): Promise<void> {
|
||||
|
||||
@@ -54,11 +54,11 @@ export class JobRepository {
|
||||
const label = `${Service.name}.${handler.name}`;
|
||||
|
||||
// one handler per job
|
||||
if (this.handlers[jobName]) {
|
||||
if (Object.hasOwn(this.handlers, jobName)) {
|
||||
const jobKey = getKeyByValue(JobName, jobName);
|
||||
const errorMessage = `Failed to add job handler for ${label}`;
|
||||
this.logger.error(
|
||||
`${errorMessage}. JobName.${jobKey} is already handled by ${this.handlers[jobName].label}.`,
|
||||
`${errorMessage}. JobName.${jobKey} is already handled by ${this.handlers[jobName]!.label}.`,
|
||||
);
|
||||
throw new ImmichStartupError(errorMessage);
|
||||
}
|
||||
@@ -104,24 +104,26 @@ export class JobRepository {
|
||||
}
|
||||
|
||||
teardown() {
|
||||
if (this.workerWatcher) {
|
||||
clearInterval(this.workerWatcher);
|
||||
this.workerWatcher = undefined;
|
||||
if (!this.workerWatcher) {
|
||||
return;
|
||||
}
|
||||
|
||||
clearInterval(this.workerWatcher);
|
||||
this.workerWatcher = undefined;
|
||||
}
|
||||
|
||||
private async checkWorkers() {
|
||||
let present: boolean;
|
||||
let isPresent: boolean;
|
||||
try {
|
||||
const suffix = `:w:${ImmichWorker.Microservices}`;
|
||||
const workers = await this.getQueue(QueueName.BackgroundTask).getWorkers();
|
||||
present = workers.some((worker) => worker.rawname?.endsWith(suffix));
|
||||
isPresent = workers.some((worker) => worker.rawname?.endsWith(suffix));
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
|
||||
if (this.microservicesPresent !== present) {
|
||||
if (present) {
|
||||
if (this.microservicesPresent !== isPresent) {
|
||||
if (isPresent) {
|
||||
this.logger.log('Microservices worker connected.');
|
||||
} else {
|
||||
this.logger.warn(
|
||||
@@ -129,7 +131,7 @@ export class JobRepository {
|
||||
);
|
||||
}
|
||||
}
|
||||
this.microservicesPresent = present;
|
||||
this.microservicesPresent = isPresent;
|
||||
}
|
||||
|
||||
async run({ name, data }: JobItem) {
|
||||
@@ -212,7 +214,7 @@ export class JobRepository {
|
||||
// need to use add() instead of addBulk() for jobId/deduplication to take effect
|
||||
promises.push(this.getQueue(queueName).add(item.name, item.data, job.options));
|
||||
} else {
|
||||
itemsByQueue[queueName] = itemsByQueue[queueName] || [];
|
||||
itemsByQueue[queueName] ||= [];
|
||||
itemsByQueue[queueName].push(job);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -26,7 +26,7 @@ describe(LoggingRepository.name, () => {
|
||||
|
||||
const logger = new MyConsoleLogger(clsMock, { color: true });
|
||||
|
||||
expect(logger.formatContext('context')).toBe('\u001B[33m[Api:context]\u001B[39m ');
|
||||
expect(logger.formatContext('context')).toBe('\u{1B}[33m[Api:context]\u{1B}[39m ');
|
||||
});
|
||||
|
||||
it('should not use colors when color is false', () => {
|
||||
|
||||
@@ -67,7 +67,7 @@ export class MyConsoleLogger extends ConsoleLogger {
|
||||
};
|
||||
|
||||
private withColor(text: string, color: LogColor) {
|
||||
return this.isColorEnabled ? `\u001B[${color}m${text}\u001B[39m` : text;
|
||||
return this.isColorEnabled ? `\u{1B}[${color}m${text}\u{1B}[39m` : text;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -80,17 +80,17 @@ export class LoggingRepository {
|
||||
@Inject(ClsService) cls: ClsService | undefined,
|
||||
@Inject(ConfigRepository) configRepository: ConfigRepository | undefined,
|
||||
) {
|
||||
let noColor = false;
|
||||
let isNoColor = false;
|
||||
let logFormat = LogFormat.Console;
|
||||
if (configRepository) {
|
||||
const env = configRepository.getEnv();
|
||||
noColor = env.noColor;
|
||||
isNoColor = env.noColor;
|
||||
logFormat = env.logFormat ?? logFormat;
|
||||
}
|
||||
this.logger = new MyConsoleLogger(cls, {
|
||||
context: LoggingRepository.name,
|
||||
json: logFormat === LogFormat.Json,
|
||||
color: !noColor,
|
||||
color: !isNoColor,
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -130,19 +130,19 @@ export class MachineLearningRepository {
|
||||
}
|
||||
|
||||
private async check(url: string) {
|
||||
let healthy = false;
|
||||
let isHealthy = false;
|
||||
try {
|
||||
const response = await fetch(new URL('ping', url), {
|
||||
signal: AbortSignal.timeout(this.config.availabilityChecks.timeout),
|
||||
});
|
||||
if (response.ok) {
|
||||
healthy = true;
|
||||
isHealthy = true;
|
||||
}
|
||||
} catch {
|
||||
// nothing to do here
|
||||
}
|
||||
|
||||
this.setHealthy(url, healthy);
|
||||
this.setHealthy(url, isHealthy);
|
||||
}
|
||||
|
||||
private setHealthy(url: string, healthy: boolean) {
|
||||
|
||||
@@ -291,8 +291,8 @@ export class MapRepository {
|
||||
id: Number.parseInt(lineSplit[0]),
|
||||
name: lineSplit[1],
|
||||
alternateNames: lineSplit[3],
|
||||
latitude: Number.parseFloat(lineSplit[4]),
|
||||
longitude: Number.parseFloat(lineSplit[5]),
|
||||
latitude: Number(lineSplit[4]),
|
||||
longitude: Number(lineSplit[5]),
|
||||
countryCode: lineSplit[8],
|
||||
admin1Code: lineSplit[10],
|
||||
admin2Code: lineSplit[11],
|
||||
@@ -308,6 +308,7 @@ export class MapRepository {
|
||||
.insertInto('geodata_places')
|
||||
.values(bufferGeodata)
|
||||
.execute()
|
||||
|
||||
.then(() => {
|
||||
count += curLength;
|
||||
if (count % 10_000 === 0) {
|
||||
|
||||
@@ -46,9 +46,6 @@ const probe = (input: string, options: string[]): Promise<FfprobeData> =>
|
||||
|
||||
const execFile = promisify(execFileCb);
|
||||
|
||||
sharp.concurrency(0);
|
||||
sharp.cache({ files: 0 });
|
||||
|
||||
const pascalCase = (str: string) => _.upperFirst(_.camelCase(str.toLowerCase()));
|
||||
|
||||
type ProgressEvent = {
|
||||
@@ -69,6 +66,8 @@ export type ExtractResult = {
|
||||
export class MediaRepository {
|
||||
constructor(private logger: LoggingRepository) {
|
||||
this.logger.setContext(MediaRepository.name);
|
||||
sharp.concurrency(0);
|
||||
sharp.cache({ files: 0 });
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -427,6 +426,7 @@ export class MediaRepository {
|
||||
}
|
||||
|
||||
private parseFloat(value: string | number | undefined): number {
|
||||
// eslint-disable-next-line unicorn/prefer-number-coercion
|
||||
return Number.parseFloat(value as string) || 0;
|
||||
}
|
||||
|
||||
|
||||
@@ -108,6 +108,7 @@ export class MetadataRepository {
|
||||
|
||||
readTags(path: string): Promise<ImmichTags> {
|
||||
const options: ReadTaskOptions | undefined = mimeTypes.isVideo(path) ? { readArgs: ['-ee'] } : undefined;
|
||||
|
||||
return this.exiftool.read(path, options).catch((error) => {
|
||||
this.logger.warn(`Error reading exif data (${path}): ${error}\n${error?.stack}`);
|
||||
return {};
|
||||
|
||||
@@ -68,7 +68,7 @@ export class OAuthRepository {
|
||||
params.code_challenge_method = 'S256';
|
||||
}
|
||||
|
||||
const url = buildAuthorizationUrl(client, params).toString();
|
||||
const url = buildAuthorizationUrl(client, params).href;
|
||||
|
||||
return { url, state, codeVerifier };
|
||||
}
|
||||
@@ -173,7 +173,7 @@ export class OAuthRepository {
|
||||
// Validate specific Logout Token claims (RFC 8963):
|
||||
// "events" claim must exist and contain the backchannel-logout event
|
||||
const events = payload.events as Record<string, any> | undefined;
|
||||
if (!events || !events['http://schemas.openid.net/event/backchannel-logout']) {
|
||||
if (!events || !events['https://schemas.openid.net/event/backchannel-logout']) {
|
||||
throw new Error('Missing backchannel-logout event claim');
|
||||
}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ export class ProcessRepository {
|
||||
spawn = spawn;
|
||||
|
||||
spawnDuplexStream(command: string, args?: readonly string[], options?: SpawnOptionsWithoutStdio): Duplex {
|
||||
let stdinClosed = false;
|
||||
let isStdinClosed = false;
|
||||
let drainCallback: undefined | (() => void);
|
||||
|
||||
const process = this.spawn(command, args, options);
|
||||
@@ -15,7 +15,7 @@ export class ProcessRepository {
|
||||
// duplex -> stdin
|
||||
write(chunk, encoding, callback) {
|
||||
// drain the input if process dies
|
||||
if (stdinClosed) {
|
||||
if (isStdinClosed) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
@@ -36,7 +36,7 @@ export class ProcessRepository {
|
||||
},
|
||||
|
||||
final(callback) {
|
||||
if (stdinClosed) {
|
||||
if (isStdinClosed) {
|
||||
callback();
|
||||
} else {
|
||||
process.stdin.end(callback);
|
||||
@@ -55,19 +55,19 @@ export class ProcessRepository {
|
||||
duplex.on('resume', () => process.stdout.resume());
|
||||
|
||||
// end handling
|
||||
let stdoutClosed = false;
|
||||
let isStdoutClosed = false;
|
||||
function close(error?: Error) {
|
||||
stdinClosed = true;
|
||||
isStdinClosed = true;
|
||||
|
||||
if (error) {
|
||||
duplex.destroy(error);
|
||||
} else if (stdoutClosed && typeof process.exitCode === 'number') {
|
||||
} else if (isStdoutClosed && typeof process.exitCode === 'number') {
|
||||
duplex.push(null);
|
||||
}
|
||||
}
|
||||
|
||||
process.stdout.on('close', () => {
|
||||
stdoutClosed = true;
|
||||
isStdoutClosed = true;
|
||||
close();
|
||||
});
|
||||
|
||||
|
||||
@@ -35,7 +35,7 @@ const exec = promisify(execCallback);
|
||||
const maybeFirstLine = async (command: string): Promise<string> => {
|
||||
try {
|
||||
const { stdout } = await exec(command);
|
||||
return stdout.trim().split('\n')[0] || '';
|
||||
return stdout.trim().split('\n', 1)[0] || '';
|
||||
} catch {
|
||||
return '';
|
||||
}
|
||||
@@ -111,6 +111,7 @@ export class ServerInfoRepository {
|
||||
|
||||
const lockfile: BuildLockfile | undefined = await readFile(resourcePaths.lockFile)
|
||||
.then((buffer) => JSON.parse(buffer.toString()))
|
||||
|
||||
.catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`));
|
||||
|
||||
const [nodejsVersion, ffmpegVersion, magickVersion, exiftoolVersion] = await Promise.all([
|
||||
|
||||
@@ -95,10 +95,12 @@ export const bootstrapTelemetry = (port: number) => {
|
||||
};
|
||||
|
||||
export const teardownTelemetry = async () => {
|
||||
if (instance) {
|
||||
await instance.shutdown();
|
||||
instance = undefined;
|
||||
if (!instance) {
|
||||
return;
|
||||
}
|
||||
|
||||
await instance.shutdown();
|
||||
instance = undefined;
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
@@ -153,7 +155,7 @@ export class TelemetryRepository {
|
||||
}
|
||||
|
||||
const method = descriptor.value;
|
||||
const propertyName = snakeCase(String(propName));
|
||||
const propertyName = snakeCase(propName);
|
||||
const metricName = `${snakeCase(className).replaceAll(/_(?=(repository)|(controller)|(provider)|(service)|(module))/g, '.')}.${propertyName}.duration`;
|
||||
|
||||
const histogram = this.metricService.getHistogram(metricName, {
|
||||
@@ -165,6 +167,7 @@ export class TelemetryRepository {
|
||||
|
||||
descriptor.value = function (...args: any[]) {
|
||||
const start = performance.now();
|
||||
// eslint-disable-next-line unicorn/no-this-outside-of-class
|
||||
const result = method.apply(this, args);
|
||||
|
||||
void Promise.resolve(result)
|
||||
|
||||
@@ -46,7 +46,7 @@ export class UserRepository {
|
||||
|
||||
@GenerateSql({ params: [DummyValue.UUID, DummyValue.BOOLEAN] })
|
||||
get(userId: string, options: UserFindOptions) {
|
||||
options = options || {};
|
||||
options ||= {};
|
||||
|
||||
return this.db
|
||||
.selectFrom('user')
|
||||
|
||||
@@ -44,7 +44,7 @@ export class ActivityService extends BaseService {
|
||||
};
|
||||
|
||||
let activity: Activity | undefined;
|
||||
let duplicate = false;
|
||||
let isDuplicate = false;
|
||||
|
||||
if (dto.type === ReactionType.LIKE) {
|
||||
delete dto.comment;
|
||||
@@ -54,7 +54,7 @@ export class ActivityService extends BaseService {
|
||||
assetId: dto.assetId ?? null,
|
||||
isLiked: true,
|
||||
});
|
||||
duplicate = !!activity;
|
||||
isDuplicate = !!activity;
|
||||
}
|
||||
|
||||
if (!activity) {
|
||||
@@ -65,7 +65,7 @@ export class ActivityService extends BaseService {
|
||||
});
|
||||
}
|
||||
|
||||
return { duplicate, value: mapActivity(activity) };
|
||||
return { duplicate: isDuplicate, value: mapActivity(activity) };
|
||||
}
|
||||
|
||||
async delete(auth: AuthDto, id: string): Promise<void> {
|
||||
|
||||
@@ -226,7 +226,7 @@ export class AlbumService extends BaseService {
|
||||
const events: { id: string; recipients: string[] }[] = [];
|
||||
for (const albumId of allowedAlbumIds) {
|
||||
const existingAssetIds = await this.albumRepository.getAssetIds(albumId, [...allowedAssetIds]);
|
||||
const notPresentAssetIds = [...allowedAssetIds].filter((id) => !existingAssetIds.has(id));
|
||||
const notPresentAssetIds = [...allowedAssetIds.difference(existingAssetIds)];
|
||||
if (notPresentAssetIds.length === 0) {
|
||||
continue;
|
||||
}
|
||||
@@ -288,7 +288,7 @@ export class AlbumService extends BaseService {
|
||||
throw new BadRequestException('Cannot add another owner');
|
||||
}
|
||||
|
||||
const exists = album.albumUsers.find(({ user: { id } }) => id === userId);
|
||||
const exists = album.albumUsers.some(({ user: { id } }) => id === userId);
|
||||
if (exists) {
|
||||
continue;
|
||||
}
|
||||
@@ -303,7 +303,7 @@ export class AlbumService extends BaseService {
|
||||
await this.eventRepository.emit('AlbumInvite', { id, userId, senderName: auth.user.name });
|
||||
}
|
||||
|
||||
return this.findOrFail(id, auth.user.id, { withAssets: true }).then(mapAlbum);
|
||||
return mapAlbum(await this.findOrFail(id, auth.user.id, { withAssets: true }));
|
||||
}
|
||||
|
||||
async removeUser(auth: AuthDto, id: string, userId: string | 'me'): Promise<void> {
|
||||
|
||||
@@ -29,7 +29,7 @@ export const render = (index: string, meta: OpenGraphTags) => {
|
||||
|
||||
${imageUrl ? `<meta name="twitter:image" content="${imageUrl}" />` : ''}`;
|
||||
|
||||
return index.replace('<!-- metadata:tags -->', tags);
|
||||
return index.replace('<!-- metadata:tags -->', () => tags);
|
||||
};
|
||||
|
||||
@Injectable()
|
||||
|
||||
@@ -163,8 +163,8 @@ const assetEntity = Object.freeze({
|
||||
duration: null,
|
||||
files: [] as AssetFile[],
|
||||
exifInfo: {
|
||||
latitude: 49.533_547,
|
||||
longitude: 10.703_075,
|
||||
latitude: 49.533547,
|
||||
longitude: 10.703075,
|
||||
},
|
||||
livePhotoVideoId: null,
|
||||
} as MapAsset);
|
||||
|
||||
@@ -280,15 +280,17 @@ export class AssetService extends BaseService {
|
||||
|
||||
let chunk: Array<{ id: string; isOffline: boolean }> = [];
|
||||
const queueChunk = async () => {
|
||||
if (chunk.length > 0) {
|
||||
await this.jobRepository.queueAll(
|
||||
chunk.map(({ id, isOffline }) => ({
|
||||
name: JobName.AssetDelete,
|
||||
data: { id, deleteOnDisk: !isOffline },
|
||||
})),
|
||||
);
|
||||
chunk = [];
|
||||
if (chunk.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.jobRepository.queueAll(
|
||||
chunk.map(({ id, isOffline }) => ({
|
||||
name: JobName.AssetDelete,
|
||||
data: { id, deleteOnDisk: !isOffline },
|
||||
})),
|
||||
);
|
||||
chunk = [];
|
||||
};
|
||||
|
||||
const assets = this.assetJobRepository.streamForDeletedJob(trashedBefore);
|
||||
|
||||
@@ -653,13 +653,13 @@ describe(AuthService.name, () => {
|
||||
|
||||
describe('getMobileRedirect', () => {
|
||||
it('should pass along the query params', () => {
|
||||
expect(sut.getMobileRedirect('http://immich.app?code=123&state=456')).toEqual(
|
||||
expect(sut.getMobileRedirect('https://immich.app?code=123&state=456')).toEqual(
|
||||
'app.immich:///oauth-callback?code=123&state=456',
|
||||
);
|
||||
});
|
||||
|
||||
it('should work if called without query params', () => {
|
||||
expect(sut.getMobileRedirect('http://immich.app')).toEqual('app.immich:///oauth-callback?');
|
||||
expect(sut.getMobileRedirect('https://immich.app')).toEqual('app.immich:///oauth-callback?');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -979,7 +979,7 @@ describe(AuthService.name, () => {
|
||||
});
|
||||
expect(mocks.oauth.getProfilePicture).toHaveBeenCalledWith(profile.picture);
|
||||
expect(mocks.media.generateThumbnail).toHaveBeenCalledWith(
|
||||
Buffer.from(pictureBytes.buffer),
|
||||
Buffer.from(pictureBytes.buffer, pictureBytes.byteOffset, pictureBytes.byteLength),
|
||||
expect.objectContaining({ format: 'webp', processInvalidImages: false }),
|
||||
expect.stringContaining(`/data/profile/${user.id}/${fileId}.webp`),
|
||||
);
|
||||
|
||||
@@ -65,9 +65,9 @@ export class AuthService extends BaseService {
|
||||
const user = await this.userRepository.getByEmail(dto.email, { withPassword: true });
|
||||
// Always run bcrypt so response time is constant regardless of whether the email
|
||||
// is registered, preventing timing-based user enumeration.
|
||||
const authenticated = this.cryptoRepository.compareBcrypt(dto.password, user?.password ?? LOGIN_DUMMY_HASH);
|
||||
const isAuthenticated = this.cryptoRepository.compareBcrypt(dto.password, user?.password ?? LOGIN_DUMMY_HASH);
|
||||
|
||||
if (!user || !user.password || !authenticated) {
|
||||
if (!user || !user.password || !isAuthenticated) {
|
||||
this.logger.warn(`Failed login attempt for user ${dto.email} from ip address ${details.clientIp}`);
|
||||
throw new UnauthorizedException('Incorrect email or password');
|
||||
}
|
||||
@@ -124,8 +124,8 @@ export class AuthService extends BaseService {
|
||||
async changePassword(auth: AuthDto, dto: ChangePasswordDto): Promise<UserAdminResponseDto> {
|
||||
const { password, newPassword } = dto;
|
||||
const user = await this.userRepository.getForChangePassword(auth.user.id);
|
||||
const valid = this.validateSecret(password, user.password);
|
||||
if (!valid) {
|
||||
const isValid = this.validateSecret(password, user.password);
|
||||
if (!isValid) {
|
||||
throw new BadRequestException('Wrong password');
|
||||
}
|
||||
|
||||
@@ -271,7 +271,7 @@ export class AuthService extends BaseService {
|
||||
}
|
||||
|
||||
getMobileRedirect(url: string) {
|
||||
return `${MOBILE_REDIRECT}?${url.split('?')[1] || ''}`;
|
||||
return `${MOBILE_REDIRECT}?${url.split('?', 2)[1] || ''}`;
|
||||
}
|
||||
|
||||
async authorize(dto: OAuthConfigDto) {
|
||||
@@ -625,7 +625,7 @@ export class AuthService extends BaseService {
|
||||
url: string,
|
||||
) {
|
||||
if (mobileOverrideEnabled && mobileRedirectUri) {
|
||||
return url.replace(/app\.immich:\/+oauth-callback/, mobileRedirectUri);
|
||||
return url.replace(/app\.immich:\/+oauth-callback/, () => mobileRedirectUri);
|
||||
}
|
||||
return url;
|
||||
}
|
||||
|
||||
@@ -247,7 +247,7 @@ export class BaseService {
|
||||
ctx.workflowRepository,
|
||||
);
|
||||
|
||||
service.logger.setContext(this.name);
|
||||
service.logger.setContext(BaseService.name);
|
||||
|
||||
return service as T;
|
||||
}
|
||||
|
||||
@@ -45,7 +45,6 @@ export class CliService extends BaseService {
|
||||
|
||||
if (!filesSet.has(name) && rowsSet.has(name)) {
|
||||
migrations.push({ name, status: 'deleted' });
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -87,9 +86,9 @@ export class CliService extends BaseService {
|
||||
}
|
||||
|
||||
async disableMaintenanceMode(): Promise<{ alreadyDisabled: boolean }> {
|
||||
const currentState = await this.systemMetadataRepository
|
||||
.get(SystemMetadataKey.MaintenanceMode)
|
||||
.then((state) => state ?? { isMaintenanceMode: false as const });
|
||||
const currentState = (await this.systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode)) ?? {
|
||||
isMaintenanceMode: false as const,
|
||||
};
|
||||
|
||||
if (!currentState.isMaintenanceMode) {
|
||||
return {
|
||||
@@ -114,9 +113,9 @@ export class CliService extends BaseService {
|
||||
username: 'cli-admin',
|
||||
};
|
||||
|
||||
const state = await this.systemMetadataRepository
|
||||
.get(SystemMetadataKey.MaintenanceMode)
|
||||
.then((state) => state ?? { isMaintenanceMode: false as const });
|
||||
const state = (await this.systemMetadataRepository.get(SystemMetadataKey.MaintenanceMode)) ?? {
|
||||
isMaintenanceMode: false as const,
|
||||
};
|
||||
|
||||
if (state.isMaintenanceMode) {
|
||||
return {
|
||||
@@ -182,11 +181,7 @@ export class CliService extends BaseService {
|
||||
this.userRepository.getFileSamples(),
|
||||
]);
|
||||
|
||||
const paths = [];
|
||||
|
||||
for (const person of people) {
|
||||
paths.push(person.thumbnailPath);
|
||||
}
|
||||
const paths = Array.from(people, (person) => person.thumbnailPath);
|
||||
|
||||
for (const user of users) {
|
||||
paths.push(user.profileImagePath);
|
||||
|
||||
@@ -143,7 +143,7 @@ export class DatabaseBackupService {
|
||||
|
||||
databaseUsername = parsedUrl.username || parsedUrl.searchParams.get('user');
|
||||
|
||||
url = parsedUrl.toString();
|
||||
url = parsedUrl.href;
|
||||
}
|
||||
|
||||
// assume typical values if we can't parse URL or not present
|
||||
@@ -214,6 +214,7 @@ export class DatabaseBackupService {
|
||||
bin: `/usr/lib/postgresql/${databaseMajorVersion}/bin/${bin}`,
|
||||
args,
|
||||
databaseUsername,
|
||||
// eslint-disable-next-line unicorn/prefer-minimal-ternary
|
||||
databasePassword: isUrlConnection ? new URL(databaseConfig.url).password : databaseConfig.password,
|
||||
databaseVersion,
|
||||
databaseMajorVersion,
|
||||
@@ -228,7 +229,7 @@ export class DatabaseBackupService {
|
||||
|
||||
this.logger.log(`Database Backup Starting. Database Version: ${databaseMajorVersion}`);
|
||||
|
||||
const filename = `${filenamePrefix}immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ')[0]}.sql.gz`;
|
||||
const filename = `${filenamePrefix}immich-db-backup-${DateTime.now().toFormat("yyyyLLdd'T'HHmmss")}-v${serverVersion.toString()}-pg${databaseVersion.split(' ', 1)[0]}.sql.gz`;
|
||||
const backupFilePath = path.join(StorageCore.getBaseFolder(StorageFolder.Backups), filename);
|
||||
const temporaryFilePath = `${backupFilePath}.tmp`;
|
||||
|
||||
@@ -249,6 +250,7 @@ export class DatabaseBackupService {
|
||||
this.logger.error(`Database Backup Failure: ${error}`);
|
||||
await this.storageRepository
|
||||
.unlink(temporaryFilePath)
|
||||
|
||||
.catch((error) => this.logger.error(`Failed to delete failed backup file: ${error}`));
|
||||
throw error;
|
||||
}
|
||||
@@ -354,7 +356,7 @@ export class DatabaseBackupService {
|
||||
): Promise<void> {
|
||||
this.logger.debug(`Database Restore Started`);
|
||||
|
||||
let complete = false;
|
||||
let isComplete = false;
|
||||
try {
|
||||
if (!isValidDatabaseBackupName(filename)) {
|
||||
throw new Error('Invalid backup file format!');
|
||||
@@ -399,7 +401,7 @@ export class DatabaseBackupService {
|
||||
});
|
||||
|
||||
const [progressSource, progressSink] = createSqlProgressStreams((progress) => {
|
||||
if (complete) {
|
||||
if (isComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -437,7 +439,7 @@ export class DatabaseBackupService {
|
||||
});
|
||||
|
||||
const [progressSource, progressSink] = createSqlProgressStreams((progress) => {
|
||||
if (complete) {
|
||||
if (isComplete) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -453,7 +455,7 @@ export class DatabaseBackupService {
|
||||
this.logger.error(`Database Restore Failure: ${error}`);
|
||||
throw error;
|
||||
} finally {
|
||||
complete = true;
|
||||
isComplete = true;
|
||||
}
|
||||
|
||||
this.logger.log(`Database Restore Success`);
|
||||
@@ -507,7 +509,7 @@ function createSqlProgressStreams(cb: (progress: number) => void) {
|
||||
const STDIN_START_MARKER = new TextEncoder().encode('FROM stdin');
|
||||
const STDIN_END_MARKER = new TextEncoder().encode(String.raw`\.`);
|
||||
|
||||
let readingStdin = false;
|
||||
let isReadingStdin = false;
|
||||
let sequenceIdx = 0;
|
||||
|
||||
let linesSent = 0;
|
||||
@@ -532,19 +534,19 @@ function createSqlProgressStreams(cb: (progress: number) => void) {
|
||||
const source = new PassThrough({
|
||||
transform(chunk, _encoding, callback) {
|
||||
for (const byte of chunk) {
|
||||
if (!readingStdin && byte === 10 && lastByte !== 10) {
|
||||
if (!isReadingStdin && byte === 10 && lastByte !== 10) {
|
||||
linesSent += 1;
|
||||
}
|
||||
|
||||
lastByte = byte;
|
||||
|
||||
const sequence = readingStdin ? STDIN_END_MARKER : STDIN_START_MARKER;
|
||||
const sequence = isReadingStdin ? STDIN_END_MARKER : STDIN_START_MARKER;
|
||||
if (sequence[sequenceIdx] === byte) {
|
||||
sequenceIdx += 1;
|
||||
|
||||
if (sequence.length === sequenceIdx) {
|
||||
sequenceIdx = 0;
|
||||
readingStdin = !readingStdin;
|
||||
isReadingStdin = !isReadingStdin;
|
||||
}
|
||||
} else {
|
||||
sequenceIdx = 0;
|
||||
@@ -552,6 +554,7 @@ function createSqlProgressStreams(cb: (progress: number) => void) {
|
||||
}
|
||||
|
||||
cbDebounced();
|
||||
// eslint-disable-next-line unicorn/no-this-outside-of-class
|
||||
this.push(chunk);
|
||||
callback();
|
||||
},
|
||||
@@ -633,6 +636,7 @@ function createSqlOwnerTransformStream(databaseUsername: string) {
|
||||
}
|
||||
}
|
||||
|
||||
// eslint-disable-next-line unicorn/no-this-outside-of-class
|
||||
this.push(result);
|
||||
callback();
|
||||
},
|
||||
|
||||
@@ -220,15 +220,15 @@ export class DuplicateService extends BaseService {
|
||||
if (idsToTrash.length > 0) {
|
||||
// TODO: this is duplicated with AssetService.deleteAssets
|
||||
const { trash } = await this.getConfig({ withCache: true });
|
||||
const force = !trash.enabled;
|
||||
const isForce = !trash.enabled;
|
||||
|
||||
await this.assetRepository.updateAll(idsToTrash, {
|
||||
deletedAt: new Date(),
|
||||
status: force ? AssetStatus.Deleted : AssetStatus.Trashed,
|
||||
status: isForce ? AssetStatus.Deleted : AssetStatus.Trashed,
|
||||
duplicateId: null,
|
||||
});
|
||||
|
||||
await this.eventRepository.emit(force ? 'AssetDeleteAll' : 'AssetTrashAll', {
|
||||
await this.eventRepository.emit(isForce ? 'AssetDeleteAll' : 'AssetTrashAll', {
|
||||
assetIds: idsToTrash,
|
||||
userId: auth.user.id,
|
||||
});
|
||||
|
||||
@@ -204,10 +204,12 @@ describe(HlsService.name, () => {
|
||||
mocks.videoStream.getForMainPlaylist.mockResolvedValue(asset);
|
||||
mocks.crypto.randomUUID.mockReturnValue(sessionId);
|
||||
mocks.websocket.serverSend.mockImplementation((event, ...rest) => {
|
||||
if (event === 'HlsSessionRequest') {
|
||||
const { sessionId: id } = rest[0] as { sessionId: string };
|
||||
queueMicrotask(() => sut.onSessionResult({ sessionId: id }));
|
||||
if (event !== 'HlsSessionRequest') {
|
||||
return;
|
||||
}
|
||||
|
||||
const { sessionId: id } = rest[0] as { sessionId: string };
|
||||
queueMicrotask(() => sut.onSessionResult({ sessionId: id }));
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
@@ -312,12 +312,14 @@ export class IntegrityService extends BaseService {
|
||||
this.logger.log(`Processing batch of ${items.length} reports to check if they are out of date.`);
|
||||
|
||||
const results = await Promise.all(
|
||||
items.map(({ reportId, path }) =>
|
||||
this.storageRepository
|
||||
.stat(path)
|
||||
.then(() => void 0)
|
||||
.catch(() => reportId),
|
||||
),
|
||||
items.map(async ({ reportId, path }) => {
|
||||
try {
|
||||
await this.storageRepository.stat(path);
|
||||
return;
|
||||
} catch {
|
||||
return reportId;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const reportIds = results.filter(Boolean) as string[];
|
||||
@@ -383,12 +385,14 @@ export class IntegrityService extends BaseService {
|
||||
this.logger.log(`Processing batch of ${items.length} files to check if they are missing.`);
|
||||
|
||||
const results = await Promise.all(
|
||||
items.map((item) =>
|
||||
this.storageRepository
|
||||
.stat(item.path)
|
||||
.then(() => ({ ...item, exists: true }))
|
||||
.catch(() => ({ ...item, exists: false })),
|
||||
),
|
||||
items.map(async (item) => {
|
||||
try {
|
||||
await this.storageRepository.stat(item.path);
|
||||
return { ...item, exists: true };
|
||||
} catch {
|
||||
return { ...item, exists: false };
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const outdatedReports = results
|
||||
@@ -420,12 +424,14 @@ export class IntegrityService extends BaseService {
|
||||
this.logger.log(`Processing batch of ${paths.length} reports to check if they are out of date.`);
|
||||
|
||||
const results = await Promise.all(
|
||||
paths.map(({ reportId, path }) =>
|
||||
this.storageRepository
|
||||
.stat(path)
|
||||
.then(() => reportId)
|
||||
.catch(() => void 0),
|
||||
),
|
||||
paths.map(async ({ reportId, path }) => {
|
||||
try {
|
||||
await this.storageRepository.stat(path);
|
||||
return reportId;
|
||||
} catch {
|
||||
return;
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const reportIds = results.filter(Boolean) as string[];
|
||||
|
||||
@@ -162,10 +162,12 @@ export class LibraryService extends BaseService {
|
||||
}
|
||||
|
||||
async unwatch(id: string) {
|
||||
if (this.watchers[id]) {
|
||||
await this.watchers[id]();
|
||||
delete this.watchers[id];
|
||||
if (!Object.hasOwn(this.watchers, id)) {
|
||||
return;
|
||||
}
|
||||
|
||||
await this.watchers[id]();
|
||||
delete this.watchers[id];
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'AppShutdown' })
|
||||
@@ -252,18 +254,22 @@ export class LibraryService extends BaseService {
|
||||
if (!library) {
|
||||
this.logger.debug(`Library ${job.libraryId} not found, skipping file import`);
|
||||
return JobStatus.Failed;
|
||||
} else if (library.deletedAt) {
|
||||
}
|
||||
if (library.deletedAt) {
|
||||
this.logger.debug(`Library ${job.libraryId} is deleted, won't import assets into it`);
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
|
||||
const assetImports: Insertable<AssetTable>[] = [];
|
||||
await Promise.all(
|
||||
job.paths.map((path) =>
|
||||
this.processEntity(path, library.ownerId, job.libraryId)
|
||||
.then((asset) => assetImports.push(asset))
|
||||
.catch((error: any) => this.logger.error(`Error processing ${path} for library ${job.libraryId}: ${error}`)),
|
||||
),
|
||||
job.paths.map(async (path) => {
|
||||
try {
|
||||
const asset = await this.processEntity(path, library.ownerId, job.libraryId);
|
||||
assetImports.push(asset);
|
||||
} catch (error) {
|
||||
this.logger.error(`Error processing ${path} for library ${job.libraryId}: ${error}`);
|
||||
}
|
||||
}),
|
||||
);
|
||||
|
||||
const assetIds = await this.assetRepository.createAll(assetImports);
|
||||
@@ -310,9 +316,9 @@ export class LibraryService extends BaseService {
|
||||
return validation;
|
||||
}
|
||||
|
||||
const access = await this.storageRepository.checkFileExists(importPath, R_OK);
|
||||
const isAccess = await this.storageRepository.checkFileExists(importPath, R_OK);
|
||||
|
||||
if (!access) {
|
||||
if (!isAccess) {
|
||||
validation.message = 'Lacking read permission for folder';
|
||||
return validation;
|
||||
}
|
||||
@@ -363,18 +369,20 @@ export class LibraryService extends BaseService {
|
||||
|
||||
await this.assetRepository.updateByLibraryId(libraryId, { deletedAt: new Date() });
|
||||
|
||||
let assetsFound = false;
|
||||
let isAssetsFound = false;
|
||||
let chunk: string[] = [];
|
||||
|
||||
const queueChunk = async () => {
|
||||
if (chunk.length > 0) {
|
||||
assetsFound = true;
|
||||
this.logger.debug(`Queueing deletion of ${chunk.length} asset(s) in library ${libraryId}`);
|
||||
await this.jobRepository.queueAll(
|
||||
chunk.map((id) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: false } })),
|
||||
);
|
||||
chunk = [];
|
||||
if (chunk.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
isAssetsFound = true;
|
||||
this.logger.debug(`Queueing deletion of ${chunk.length} asset(s) in library ${libraryId}`);
|
||||
await this.jobRepository.queueAll(
|
||||
chunk.map((id) => ({ name: JobName.AssetDelete, data: { id, deleteOnDisk: false } })),
|
||||
);
|
||||
chunk = [];
|
||||
};
|
||||
|
||||
this.logger.debug(`Will delete all assets in library ${libraryId}`);
|
||||
@@ -389,7 +397,7 @@ export class LibraryService extends BaseService {
|
||||
|
||||
await queueChunk();
|
||||
|
||||
if (!assetsFound) {
|
||||
if (!isAssetsFound) {
|
||||
this.logger.log(`Deleting library ${libraryId}`);
|
||||
await this.libraryRepository.delete(libraryId);
|
||||
}
|
||||
@@ -514,7 +522,7 @@ export class LibraryService extends BaseService {
|
||||
break;
|
||||
}
|
||||
case AssetSyncResult.CHECK_OFFLINE: {
|
||||
const isInImportPath = job.importPaths.find((path) => asset.originalPath.startsWith(path));
|
||||
const isInImportPath = job.importPaths.some((path) => asset.originalPath.startsWith(path));
|
||||
|
||||
if (!isInImportPath) {
|
||||
this.logger.verbose(
|
||||
@@ -736,28 +744,30 @@ export class LibraryService extends BaseService {
|
||||
let count = 0;
|
||||
|
||||
const queueChunk = async () => {
|
||||
if (chunk.length > 0) {
|
||||
count += chunk.length;
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.LibrarySyncAssets,
|
||||
data: {
|
||||
libraryId: library.id,
|
||||
importPaths: library.importPaths,
|
||||
exclusionPatterns: library.exclusionPatterns,
|
||||
assetIds: chunk.map((id) => id),
|
||||
progressCounter: count,
|
||||
totalAssets: assetCount,
|
||||
},
|
||||
});
|
||||
chunk = [];
|
||||
|
||||
const completePercentage = ((100 * count) / assetCount).toFixed(1);
|
||||
|
||||
this.logger.log(
|
||||
`Queued check of ${count} of ${assetCount} (${completePercentage} %) existing asset(s) so far in library ${library.id}`,
|
||||
);
|
||||
if (chunk.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
count += chunk.length;
|
||||
|
||||
await this.jobRepository.queue({
|
||||
name: JobName.LibrarySyncAssets,
|
||||
data: {
|
||||
libraryId: library.id,
|
||||
importPaths: library.importPaths,
|
||||
exclusionPatterns: library.exclusionPatterns,
|
||||
assetIds: chunk.map((id) => id),
|
||||
progressCounter: count,
|
||||
totalAssets: assetCount,
|
||||
},
|
||||
});
|
||||
chunk = [];
|
||||
|
||||
const completePercentage = ((100 * count) / assetCount).toFixed(1);
|
||||
|
||||
this.logger.log(
|
||||
`Queued check of ${count} of ${assetCount} (${completePercentage} %) existing asset(s) so far in library ${library.id}`,
|
||||
);
|
||||
};
|
||||
|
||||
this.logger.log(`Scanning library ${library.id} for assets missing from disk...`);
|
||||
|
||||
@@ -26,6 +26,7 @@ export class MaintenanceService extends BaseService {
|
||||
getMaintenanceMode(): Promise<MaintenanceModeState> {
|
||||
return this.systemMetadataRepository
|
||||
.get(SystemMetadataKey.MaintenanceMode)
|
||||
|
||||
.then((state) => state ?? { isMaintenanceMode: false });
|
||||
}
|
||||
|
||||
|
||||
@@ -76,8 +76,11 @@ export class MediaService extends BaseService {
|
||||
jobs = [];
|
||||
};
|
||||
|
||||
const fullsizeEnabled = config.image.fullsize.enabled;
|
||||
for await (const asset of this.assetJobRepository.streamForThumbnailJob({ force, fullsizeEnabled })) {
|
||||
const isFullsizeEnabled = config.image.fullsize.enabled;
|
||||
for await (const asset of this.assetJobRepository.streamForThumbnailJob({
|
||||
force,
|
||||
fullsizeEnabled: isFullsizeEnabled,
|
||||
})) {
|
||||
if (force || !asset.isEdited) {
|
||||
jobs.push({ name: JobName.AssetGenerateThumbnails, data: { id: asset.id } });
|
||||
}
|
||||
@@ -272,13 +275,14 @@ export class MediaService extends BaseService {
|
||||
}
|
||||
|
||||
private async extractOriginalImage(asset: ThumbnailAsset, image: SystemConfig['image'], useEdits = false) {
|
||||
const extractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName);
|
||||
const extracted = extractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null;
|
||||
const generateFullsize =
|
||||
const isExtractEmbedded = image.extractEmbedded && mimeTypes.isRaw(asset.originalFileName);
|
||||
const extracted = isExtractEmbedded ? await this.extractImage(asset.originalPath, image.preview.size) : null;
|
||||
const isGenerateFullsize =
|
||||
((image.fullsize.enabled || asset.exifInfo.projectionType === 'EQUIRECTANGULAR') &&
|
||||
!mimeTypes.isWebSupportedImage(asset.originalPath)) ||
|
||||
useEdits;
|
||||
const convertFullsize = generateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`));
|
||||
const isConvertFullsize =
|
||||
isGenerateFullsize && (!extracted || !mimeTypes.isWebSupportedImage(` .${extracted.format}`));
|
||||
|
||||
const thumbSource = extracted ? extracted.buffer : asset.originalPath;
|
||||
const { data, info, colorspace } = await this.decodeImage(
|
||||
@@ -286,7 +290,7 @@ export class MediaService extends BaseService {
|
||||
// only specify orientation to extracted images which don't have EXIF orientation data
|
||||
// or it can double rotate the image
|
||||
extracted ? asset.exifInfo : { ...asset.exifInfo, orientation: null },
|
||||
convertFullsize ? undefined : image.preview.size,
|
||||
isConvertFullsize ? undefined : image.preview.size,
|
||||
);
|
||||
|
||||
let isTransparent = false;
|
||||
@@ -299,8 +303,8 @@ export class MediaService extends BaseService {
|
||||
data,
|
||||
info,
|
||||
colorspace,
|
||||
convertFullsize,
|
||||
generateFullsize,
|
||||
convertFullsize: isConvertFullsize,
|
||||
generateFullsize: isGenerateFullsize,
|
||||
isTransparent,
|
||||
};
|
||||
}
|
||||
@@ -620,20 +624,20 @@ export class MediaService extends BaseService {
|
||||
return JobStatus.Failed;
|
||||
}
|
||||
|
||||
let partialFallbackSuccess = false;
|
||||
let isPartialFallbackSuccess = false;
|
||||
if (ffmpeg.accelDecode) {
|
||||
try {
|
||||
this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()}-accelerated encoding and software decoding`);
|
||||
ffmpeg = { ...ffmpeg, accelDecode: false };
|
||||
const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream);
|
||||
await this.mediaRepository.transcode(input, output, command);
|
||||
partialFallbackSuccess = true;
|
||||
isPartialFallbackSuccess = true;
|
||||
} catch (error: any) {
|
||||
this.logger.error(`Error occurred during transcoding: ${error.message}`);
|
||||
}
|
||||
}
|
||||
|
||||
if (!partialFallbackSuccess) {
|
||||
if (!isPartialFallbackSuccess) {
|
||||
this.logger.error(`Retrying with ${ffmpeg.accel.toUpperCase()} acceleration disabled`);
|
||||
ffmpeg = { ...ffmpeg, accel: TranscodeHardwareAcceleration.Disabled };
|
||||
const command = BaseConfig.create(ffmpeg, this.videoInterfaces).getCommand(target, videoStream, audioStream);
|
||||
@@ -700,9 +704,9 @@ export class MediaService extends BaseService {
|
||||
}
|
||||
|
||||
private isVideoTranscodeRequired(ffmpegConfig: SystemConfigFFmpegDto, stream: VideoStreamInfo): boolean {
|
||||
const scalingEnabled = ffmpegConfig.targetResolution !== 'original';
|
||||
const isScalingEnabled = ffmpegConfig.targetResolution !== 'original';
|
||||
const targetRes = Number.parseInt(ffmpegConfig.targetResolution);
|
||||
const isLargerThanTargetRes = scalingEnabled && Math.min(stream.height, stream.width) > targetRes;
|
||||
const isLargerThanTargetRes = isScalingEnabled && Math.min(stream.height, stream.width) > targetRes;
|
||||
const maxBitrate = this.parseBitrateToBps(ffmpegConfig.maxBitrate);
|
||||
const isLargerThanTargetBitrate = maxBitrate > 0 && stream.bitrate > maxBitrate;
|
||||
|
||||
@@ -757,13 +761,13 @@ export class MediaService extends BaseService {
|
||||
}): boolean {
|
||||
if (colorspace || profileDescription) {
|
||||
return [colorspace, profileDescription].some((s) => s?.toLowerCase().includes('srgb'));
|
||||
} else if (bitsPerSample) {
|
||||
}
|
||||
if (bitsPerSample) {
|
||||
// assume sRGB for 8-bit images with no color profile or colorspace metadata
|
||||
return bitsPerSample === 8;
|
||||
} else {
|
||||
// assume sRGB for images with no relevant metadata
|
||||
return true;
|
||||
}
|
||||
// assume sRGB for images with no relevant metadata
|
||||
return true;
|
||||
}
|
||||
|
||||
private parseBitrateToBps(bitrateString: string) {
|
||||
@@ -776,11 +780,11 @@ export class MediaService extends BaseService {
|
||||
|
||||
if (bitrateString.toLowerCase().endsWith('k')) {
|
||||
return bitrateValue * 1000; // Kilobits per second to bits per second
|
||||
} else if (bitrateString.toLowerCase().endsWith('m')) {
|
||||
return bitrateValue * 1_000_000; // Megabits per second to bits per second
|
||||
} else {
|
||||
return bitrateValue;
|
||||
}
|
||||
if (bitrateString.toLowerCase().endsWith('m')) {
|
||||
return bitrateValue * 1_000_000; // Megabits per second to bits per second
|
||||
}
|
||||
return bitrateValue;
|
||||
}
|
||||
|
||||
private async shouldUseExtractedImage(extractedPathOrBuffer: string | Buffer, targetSize: number) {
|
||||
|
||||
@@ -136,7 +136,7 @@ export class MemoryService extends BaseService {
|
||||
const repos = { access: this.accessRepository, bulk: this.memoryRepository };
|
||||
const results = await addAssets(auth, repos, { parentId: id, assetIds: dto.ids });
|
||||
|
||||
const hasSuccess = results.find(({ success }) => success);
|
||||
const hasSuccess = results.some(({ success }) => success);
|
||||
if (hasSuccess) {
|
||||
await this.memoryRepository.update(id, { updatedAt: new Date() });
|
||||
}
|
||||
@@ -154,7 +154,7 @@ export class MemoryService extends BaseService {
|
||||
canAlwaysRemove: Permission.MemoryDelete,
|
||||
});
|
||||
|
||||
const hasSuccess = results.find(({ success }) => success);
|
||||
const hasSuccess = results.some(({ success }) => success);
|
||||
if (hasSuccess) {
|
||||
await this.memoryRepository.update(id, { id, updatedAt: new Date() });
|
||||
}
|
||||
|
||||
@@ -1396,7 +1396,7 @@ describe(MetadataService.name, () => {
|
||||
expect(mocks.person.refreshFaces).toHaveBeenCalledWith(
|
||||
[
|
||||
expect.objectContaining({
|
||||
boundingBoxX1: Math.floor((0.485_648_148_148_148_2 - 0.2 / 2) * 1000),
|
||||
boundingBoxX1: Math.floor((0.4856481481481482 - 0.2 / 2) * 1000),
|
||||
}),
|
||||
],
|
||||
[],
|
||||
|
||||
@@ -117,9 +117,7 @@ const validateRange = (value: number | undefined, min: number, max: number): Non
|
||||
};
|
||||
|
||||
const getLensModel = (exifTags: ImmichTags): string | null => {
|
||||
const lensModel = String(
|
||||
exifTags.LensID ?? exifTags.LensType ?? exifTags.LensSpec ?? exifTags.LensModel ?? '',
|
||||
).trim();
|
||||
const lensModel = (exifTags.LensID ?? exifTags.LensType ?? exifTags.LensSpec ?? exifTags.LensModel ?? '').trim();
|
||||
if (lensModel === '----') {
|
||||
return null;
|
||||
}
|
||||
@@ -286,7 +284,7 @@ export class MetadataService extends BaseService {
|
||||
exifImageHeight: validate(height),
|
||||
exifImageWidth: validate(width),
|
||||
orientation: validate(exifTags.Orientation)?.toString() ?? null,
|
||||
projectionType: exifTags.ProjectionType ? String(exifTags.ProjectionType).toUpperCase() : null,
|
||||
projectionType: exifTags.ProjectionType ? exifTags.ProjectionType.toUpperCase() : null,
|
||||
bitsPerSample: this.getBitsPerSample(exifTags),
|
||||
colorspace: exifTags.ColorSpace === undefined ? null : String(exifTags.ColorSpace),
|
||||
|
||||
@@ -295,7 +293,7 @@ export class MetadataService extends BaseService {
|
||||
exifTags.Make ?? exifTags.Device?.Manufacturer ?? exifTags.AndroidMake ?? (exifTags.DeviceManufacturer || null),
|
||||
model:
|
||||
exifTags.Model ?? exifTags.Device?.ModelName ?? exifTags.AndroidModel ?? (exifTags.DeviceModelName || null),
|
||||
fps: video?.frameRate ?? validate(Number.parseFloat(exifTags.VideoFrameRate!)),
|
||||
fps: video?.frameRate ?? validate(Number(exifTags.VideoFrameRate!)),
|
||||
iso: validate(exifTags.ISO) as number,
|
||||
exposureTime: exifTags.ExposureTime ?? null,
|
||||
lensModel: getLensModel(exifTags),
|
||||
@@ -326,7 +324,7 @@ export class MetadataService extends BaseService {
|
||||
: undefined;
|
||||
|
||||
const videoData =
|
||||
format?.formatName && format?.formatLongName && video?.codecName && video?.timeBase
|
||||
format?.formatName && format.formatLongName && video?.codecName && video?.timeBase
|
||||
? {
|
||||
assetId: asset.id,
|
||||
bitrate: video.bitrate,
|
||||
@@ -362,8 +360,8 @@ export class MetadataService extends BaseService {
|
||||
: undefined;
|
||||
|
||||
const isSidewards = exifTags.Orientation && this.isOrientationSidewards(exifTags.Orientation);
|
||||
const assetWidth = isSidewards ? validate(height) : validate(width);
|
||||
const assetHeight = isSidewards ? validate(width) : validate(height);
|
||||
const assetWidth = validate(isSidewards ? height : width);
|
||||
const assetHeight = validate(isSidewards ? width : height);
|
||||
|
||||
const tasks = new Tasks();
|
||||
|
||||
@@ -445,8 +443,8 @@ export class MetadataService extends BaseService {
|
||||
|
||||
let sidecarPath = null;
|
||||
for (const candidate of this.getSidecarCandidates(asset)) {
|
||||
const exists = await this.storageRepository.checkFileExists(candidate, constants.R_OK);
|
||||
if (!exists) {
|
||||
const isExists = await this.storageRepository.checkFileExists(candidate, constants.R_OK);
|
||||
if (!isExists) {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -806,8 +804,8 @@ export class MetadataService extends BaseService {
|
||||
}
|
||||
|
||||
// write extracted motion video to disk, especially if the encoded-video folder has been deleted
|
||||
const existsOnDisk = await this.storageRepository.checkFileExists(motionAsset.originalPath);
|
||||
if (!existsOnDisk) {
|
||||
const isExistsOnDisk = await this.storageRepository.checkFileExists(motionAsset.originalPath);
|
||||
if (!isExistsOnDisk) {
|
||||
this.storageCore.ensureFolders(motionAsset.originalPath);
|
||||
await this.storageRepository.createFile(motionAsset.originalPath, video);
|
||||
this.logger.log(`Wrote motion photo video to ${motionAsset.originalPath}`);
|
||||
@@ -1087,6 +1085,7 @@ export class MetadataService extends BaseService {
|
||||
|
||||
private getDuration(tags: ImmichTags): number | null {
|
||||
const duration = tags.Duration;
|
||||
// eslint-disable-next-line unicorn/prefer-number-coercion
|
||||
const seconds = typeof duration === 'number' ? duration : Number.parseFloat(duration as string);
|
||||
return Number.isFinite(seconds) ? Math.round(Duration.fromObject({ seconds }).toMillis()) : null;
|
||||
}
|
||||
|
||||
@@ -120,7 +120,7 @@ export class PersonService extends BaseService {
|
||||
await this.createNewFeaturePhoto([face.person.id]);
|
||||
}
|
||||
|
||||
return await this.findOrFail(personId).then(mapPerson);
|
||||
return mapPerson(await this.findOrFail(personId));
|
||||
}
|
||||
|
||||
async getFacesById(auth: AuthDto, dto: FaceDto): Promise<AssetFaceResponseDto[]> {
|
||||
@@ -152,7 +152,7 @@ export class PersonService extends BaseService {
|
||||
|
||||
async getById(auth: AuthDto, id: string): Promise<PersonResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.PersonRead, ids: [id] });
|
||||
return this.findOrFail(id).then(mapPerson);
|
||||
return mapPerson(await this.findOrFail(id));
|
||||
}
|
||||
|
||||
async getStatistics(auth: AuthDto, id: string): Promise<PersonStatisticsResponseDto> {
|
||||
@@ -192,7 +192,7 @@ export class PersonService extends BaseService {
|
||||
|
||||
const { name, birthDate, isHidden, featureFaceAssetId: assetId, isFavorite, color } = dto;
|
||||
// TODO: set by faceId directly
|
||||
let faceId: string | undefined = undefined;
|
||||
let faceId: string | undefined;
|
||||
if (assetId) {
|
||||
await this.requireAccess({ auth, permission: Permission.AssetRead, ids: [assetId] });
|
||||
const face = await this.personRepository.getForFeatureFaceUpdate({ personId: id, assetId });
|
||||
@@ -655,14 +655,12 @@ export class PersonService extends BaseService {
|
||||
topLeft = { x: topLeft.x * scaleFactor, y: topLeft.y * scaleFactor };
|
||||
bottomRight = { x: bottomRight.x * scaleFactor, y: bottomRight.y * scaleFactor };
|
||||
|
||||
const {
|
||||
points: [invertedTopLeft, invertedBottomRight],
|
||||
} = transformPoints(
|
||||
const [invertedTopLeft, invertedBottomRight] = transformPoints(
|
||||
[topLeft, bottomRight],
|
||||
edits,
|
||||
{ width: asset.width, height: asset.height },
|
||||
{ inverse: true },
|
||||
);
|
||||
).points;
|
||||
|
||||
// make sure topLeft is top-left and bottomRight is bottom-right
|
||||
topLeft = {
|
||||
|
||||
@@ -93,10 +93,7 @@ export class QueueService extends BaseService {
|
||||
private updateConcurrency(config: SystemConfig) {
|
||||
this.logger.debug(`Updating queue concurrency settings`);
|
||||
for (const queueName of Object.values(QueueName)) {
|
||||
let concurrency = 1;
|
||||
if (this.isConcurrentQueue(queueName)) {
|
||||
concurrency = config.job[queueName].concurrency;
|
||||
}
|
||||
const concurrency = this.isConcurrentQueue(queueName) ? config.job[queueName].concurrency : 1;
|
||||
this.logger.debug(`Setting ${queueName} concurrency to ${concurrency}`);
|
||||
this.jobRepository.setConcurrency(queueName, concurrency);
|
||||
}
|
||||
@@ -161,9 +158,7 @@ export class QueueService extends BaseService {
|
||||
throw new BadRequestException(`The BackgroundTask queue cannot be paused`);
|
||||
}
|
||||
await this.jobRepository.pause(name);
|
||||
}
|
||||
|
||||
if (dto.isPaused === false) {
|
||||
} else if (dto.isPaused === false) {
|
||||
await this.jobRepository.resume(name);
|
||||
}
|
||||
|
||||
|
||||
@@ -77,7 +77,7 @@ export class ServerService extends BaseService {
|
||||
serverInfo.diskAvailableRaw = diskInfo.available;
|
||||
serverInfo.diskSizeRaw = diskInfo.total;
|
||||
serverInfo.diskUseRaw = diskInfo.total - diskInfo.free;
|
||||
serverInfo.diskUsagePercentage = Number.parseFloat(usagePercentage);
|
||||
serverInfo.diskUsagePercentage = Number(usagePercentage);
|
||||
return serverInfo;
|
||||
}
|
||||
|
||||
@@ -190,8 +190,12 @@ export class ServerService extends BaseService {
|
||||
throw new BadRequestException('Invalid license key');
|
||||
}
|
||||
const { licensePublicKey } = this.configRepository.getEnv();
|
||||
const licenseValid = this.cryptoRepository.verifySha256(dto.licenseKey, dto.activationKey, licensePublicKey.server);
|
||||
if (!licenseValid) {
|
||||
const isLicenseValid = this.cryptoRepository.verifySha256(
|
||||
dto.licenseKey,
|
||||
dto.activationKey,
|
||||
licensePublicKey.server,
|
||||
);
|
||||
if (!isLicenseValid) {
|
||||
throw new BadRequestException('Invalid license key');
|
||||
}
|
||||
|
||||
|
||||
@@ -20,6 +20,7 @@ export class SharedLinkService extends BaseService {
|
||||
async getAll(auth: AuthDto, { id, albumId }: SharedLinkSearchDto): Promise<SharedLinkResponseDto[]> {
|
||||
return this.sharedLinkRepository
|
||||
.getAll({ userId: auth.user.id, id, albumId })
|
||||
|
||||
.then((links) => links.map((link) => mapSharedLink(link, { stripAssetMetadata: false })));
|
||||
}
|
||||
|
||||
@@ -200,7 +201,7 @@ export class SharedLinkService extends BaseService {
|
||||
|
||||
const results: AssetIdsResponseDto[] = [];
|
||||
for (const assetId of dto.assetIds) {
|
||||
const wasRemoved = removedAssetIds.find((id) => id === assetId);
|
||||
const wasRemoved = removedAssetIds.includes(assetId);
|
||||
if (!wasRemoved) {
|
||||
results.push({ assetId, success: false, error: AssetIdErrorReason.NOT_FOUND });
|
||||
continue;
|
||||
|
||||
@@ -43,12 +43,12 @@ export class SmartInfoService extends BaseService {
|
||||
|
||||
const modelChange =
|
||||
oldConfig && oldConfig.machineLearning.clip.modelName !== newConfig.machineLearning.clip.modelName;
|
||||
const dimSizeChange = dbDimSize !== dimSize;
|
||||
if (!modelChange && !dimSizeChange) {
|
||||
const isDimSizeChange = dbDimSize !== dimSize;
|
||||
if (!modelChange && !isDimSizeChange) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (dimSizeChange) {
|
||||
if (isDimSizeChange) {
|
||||
this.logger.log(
|
||||
`Dimension size of model ${newConfig.machineLearning.clip.modelName} is ${dimSize}, but database expects ${dbDimSize}.`,
|
||||
);
|
||||
|
||||
@@ -36,7 +36,7 @@ export class StackService extends BaseService {
|
||||
async update(auth: AuthDto, id: string, dto: StackUpdateDto): Promise<StackResponseDto> {
|
||||
await this.requireAccess({ auth, permission: Permission.StackUpdate, ids: [id] });
|
||||
const stack = await this.findOrFail(id);
|
||||
if (dto.primaryAssetId && !stack.assets.some(({ id }) => id === dto.primaryAssetId)) {
|
||||
if (dto.primaryAssetId && stack.assets.every(({ id }) => id !== dto.primaryAssetId)) {
|
||||
throw new BadRequestException('Primary asset must be in the stack');
|
||||
}
|
||||
|
||||
|
||||
@@ -141,8 +141,8 @@ export class StorageTemplateService extends BaseService {
|
||||
@OnJob({ name: JobName.StorageTemplateMigrationSingle, queue: QueueName.StorageTemplateMigration })
|
||||
async handleMigrationSingle({ id }: JobOf<JobName.StorageTemplateMigrationSingle>): Promise<JobStatus> {
|
||||
const config = await this.getConfig({ withCache: true });
|
||||
const storageTemplateEnabled = config.storageTemplate.enabled;
|
||||
if (!storageTemplateEnabled) {
|
||||
const isStorageTemplateEnabled = config.storageTemplate.enabled;
|
||||
if (!isStorageTemplateEnabled) {
|
||||
return JobStatus.Skipped;
|
||||
}
|
||||
|
||||
@@ -372,8 +372,8 @@ export class StorageTemplateService extends BaseService {
|
||||
let duplicateCount = 0;
|
||||
|
||||
while (true) {
|
||||
const exists = await this.storageRepository.checkFileExists(destination);
|
||||
if (!exists) {
|
||||
const isExists = await this.storageRepository.checkFileExists(destination);
|
||||
if (!isExists) {
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
@@ -30,8 +30,8 @@ export class StorageService extends BaseService {
|
||||
const candidates = ['/data', '/usr/src/app/upload'];
|
||||
|
||||
for (const candidate of candidates) {
|
||||
const exists = this.storageRepository.existsSync(candidate);
|
||||
if (exists) {
|
||||
const isExists = this.storageRepository.existsSync(candidate);
|
||||
if (isExists) {
|
||||
targets.push(candidate);
|
||||
}
|
||||
}
|
||||
@@ -56,7 +56,7 @@ export class StorageService extends BaseService {
|
||||
flags.mountChecks = {};
|
||||
}
|
||||
|
||||
let updated = false;
|
||||
let isUpdated = false;
|
||||
|
||||
this.logger.log(`Verifying system mount folder checks, current state: ${JSON.stringify(flags)}`);
|
||||
|
||||
@@ -73,11 +73,11 @@ export class StorageService extends BaseService {
|
||||
|
||||
if (!flags.mountChecks[folder]) {
|
||||
flags.mountChecks[folder] = true;
|
||||
updated = true;
|
||||
isUpdated = true;
|
||||
}
|
||||
}
|
||||
|
||||
if (updated) {
|
||||
if (isUpdated) {
|
||||
await this.systemMetadataRepository.set(SystemMetadataKey.SystemFlags, flags);
|
||||
this.logger.log('Successfully enabled system mount folders checks');
|
||||
}
|
||||
@@ -166,7 +166,7 @@ export class StorageService extends BaseService {
|
||||
const { folderPath, internalPath, externalPath } = this.getMountFilePaths(folder);
|
||||
try {
|
||||
this.storageRepository.mkdirSync(folderPath);
|
||||
await this.storageRepository.createFile(internalPath, Buffer.from(`${Date.now()}`));
|
||||
await this.storageRepository.createFile(internalPath, Buffer.from(Date.now().toString()));
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code === 'EEXIST') {
|
||||
this.logger.warn('Found existing mount file, skipping creation');
|
||||
@@ -180,7 +180,7 @@ export class StorageService extends BaseService {
|
||||
private async verifyWriteAccess(folder: StorageFolder) {
|
||||
const { internalPath, externalPath } = this.getMountFilePaths(folder);
|
||||
try {
|
||||
await this.storageRepository.overwriteFile(internalPath, Buffer.from(`${Date.now()}`));
|
||||
await this.storageRepository.overwriteFile(internalPath, Buffer.from(Date.now().toString()));
|
||||
} catch (error) {
|
||||
this.logger.error(`Failed to write ${internalPath}: ${error}`);
|
||||
throw new ImmichStartupError(`Failed to write "${externalPath} - ${docsMessage}"`);
|
||||
|
||||
@@ -192,7 +192,11 @@ export class SyncService extends BaseService {
|
||||
[SyncRequestType.AssetOcrV1]: () => this.syncAssetOcrV1(options, response, checkpointMap, auth),
|
||||
} as const;
|
||||
|
||||
for (const type of SYNC_TYPES_ORDER.filter((type) => dto.types.includes(type))) {
|
||||
for (const type of SYNC_TYPES_ORDER) {
|
||||
if (!dto.types.includes(type)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const handler = handlers[type as keyof typeof handlers];
|
||||
await handler();
|
||||
}
|
||||
@@ -573,16 +577,16 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
|
||||
const creates = this.syncRepository.albumAsset.getCreates({ ...options, ack: createCheckpoint });
|
||||
let first = true;
|
||||
let isFirst = true;
|
||||
for await (const { updateId, ...data } of creates) {
|
||||
if (first) {
|
||||
if (isFirst) {
|
||||
send(response, {
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
data: {},
|
||||
ackType: SyncEntityType.AlbumAssetUpdateV2,
|
||||
ids: [options.nowId],
|
||||
});
|
||||
first = false;
|
||||
isFirst = false;
|
||||
}
|
||||
send(response, { type: createType, ids: [updateId], data: mapSyncAssetV2(data) });
|
||||
}
|
||||
@@ -644,16 +648,16 @@ export class SyncService extends BaseService {
|
||||
}
|
||||
|
||||
const creates = this.syncRepository.albumAssetExif.getCreates({ ...options, ack: createCheckpoint });
|
||||
let first = true;
|
||||
let isFirst = true;
|
||||
for await (const { updateId, ...data } of creates) {
|
||||
if (first) {
|
||||
if (isFirst) {
|
||||
send(response, {
|
||||
type: SyncEntityType.SyncAckV1,
|
||||
data: {},
|
||||
ackType: SyncEntityType.AlbumAssetExifUpdateV1,
|
||||
ids: [options.nowId],
|
||||
});
|
||||
first = false;
|
||||
isFirst = false;
|
||||
}
|
||||
send(response, { type: createType, ids: [updateId], data });
|
||||
}
|
||||
|
||||
@@ -108,10 +108,12 @@ export class TagService extends BaseService {
|
||||
);
|
||||
|
||||
for (const { id: assetId, success } of results) {
|
||||
if (success) {
|
||||
await this.updateTags(assetId);
|
||||
await this.eventRepository.emit('AssetTag', { assetId });
|
||||
if (!success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.updateTags(assetId);
|
||||
await this.eventRepository.emit('AssetTag', { assetId });
|
||||
}
|
||||
|
||||
return results;
|
||||
@@ -127,10 +129,12 @@ export class TagService extends BaseService {
|
||||
);
|
||||
|
||||
for (const { id: assetId, success } of results) {
|
||||
if (success) {
|
||||
await this.updateTags(assetId);
|
||||
await this.eventRepository.emit('AssetUntag', { assetId });
|
||||
if (!success) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await this.updateTags(assetId);
|
||||
await this.eventRepository.emit('AssetUntag', { assetId });
|
||||
}
|
||||
|
||||
return results;
|
||||
|
||||
@@ -34,10 +34,12 @@ export class TelemetryService extends BaseService {
|
||||
|
||||
@OnEvent({ name: 'JobSuccess' })
|
||||
onJobSuccess({ job, response }: ArgOf<'JobSuccess'>) {
|
||||
if (response && Object.values(JobStatus).includes(response as JobStatus)) {
|
||||
const jobMetric = `immich.jobs.${snakeCase(job.name)}.${response}`;
|
||||
this.telemetryRepository.jobs.addToCounter(jobMetric, 1);
|
||||
if (!(response && Object.values(JobStatus).includes(response as JobStatus))) {
|
||||
return;
|
||||
}
|
||||
|
||||
const jobMetric = `immich.jobs.${snakeCase(job.name)}.${response}`;
|
||||
this.telemetryRepository.jobs.addToCounter(jobMetric, 1);
|
||||
}
|
||||
|
||||
@OnEvent({ name: 'JobError' })
|
||||
|
||||
@@ -27,7 +27,7 @@ export class TimelineService extends BaseService {
|
||||
|
||||
private async buildTimeBucketOptions(auth: AuthDto, dto: TimeBucketDto): Promise<TimeBucketOptions> {
|
||||
const { userId, ...options } = dto;
|
||||
let userIds: string[] | undefined = undefined;
|
||||
let userIds: string[] | undefined;
|
||||
|
||||
if (userId) {
|
||||
userIds = [userId];
|
||||
@@ -52,7 +52,7 @@ export class TimelineService extends BaseService {
|
||||
if (dto.albumId) {
|
||||
await this.requireAccess({ auth, permission: Permission.AlbumRead, ids: [dto.albumId] });
|
||||
} else {
|
||||
dto.userId = dto.userId || auth.user.id;
|
||||
dto.userId ||= auth.user.id;
|
||||
}
|
||||
|
||||
if (dto.userId) {
|
||||
@@ -71,12 +71,12 @@ export class TimelineService extends BaseService {
|
||||
}
|
||||
|
||||
if (dto.withPartners) {
|
||||
const requestedLocked = dto.visibility === AssetVisibility.Locked;
|
||||
const requestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined;
|
||||
const requestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
|
||||
const requestedTrash = dto.isTrashed === true;
|
||||
const isRequestedLocked = dto.visibility === AssetVisibility.Locked;
|
||||
const isRequestedArchived = dto.visibility === AssetVisibility.Archive || dto.visibility === undefined;
|
||||
const isRequestedFavorite = dto.isFavorite === true || dto.isFavorite === false;
|
||||
const isRequestedTrash = dto.isTrashed === true;
|
||||
|
||||
if (requestedLocked || requestedArchived || requestedFavorite || requestedTrash) {
|
||||
if (isRequestedLocked || isRequestedArchived || isRequestedFavorite || isRequestedTrash) {
|
||||
throw new BadRequestException(
|
||||
'withPartners is only supported for non-archived, non-trashed, non-favorited, non-locked assets',
|
||||
);
|
||||
|
||||
@@ -490,18 +490,15 @@ describe(TranscodingService.name, () => {
|
||||
|
||||
describe('FFmpeg seek per segment', () => {
|
||||
const eiffelSeeks = [
|
||||
0, 1.987_15, 3.994_372_222_222_222, 6.001_594_444_444_444, 8.008_816_666_666_666, 10.016_038_888_888_888,
|
||||
12.023_261_111_111_111, 14.030_483_333_333_333, 16.037_705_555_555_554, 18.044_927_777_777_776,
|
||||
20.052_149_999_999_997, 22.059_372_222_222_223,
|
||||
0, 1.98715, 3.994372222222222, 6.001594444444444, 8.008816666666666, 10.016038888888888, 12.023261111111111,
|
||||
14.030483333333333, 16.037705555555554, 18.044927777777776, 20.052149999999997, 22.059372222222223,
|
||||
];
|
||||
const waterfallSeeks = [
|
||||
0, 1.994_642_826_321_467, 4.006_047_357_065_803, 6.017_451_887_810_139_5, 8.028_856_418_554_476,
|
||||
10.040_260_949_298_812,
|
||||
0, 1.994642826321467, 4.006047357065803, 6.0174518878101395, 8.028856418554476, 10.040260949298812,
|
||||
];
|
||||
const trainSeeks = [
|
||||
0, 1.991_666_666_666_666_7, 3.991_666_666_666_666_7, 5.991_666_666_666_666, 7.991_666_666_666_666,
|
||||
9.991_666_666_666_667, 11.991_666_666_666_667, 13.991_666_666_666_667, 15.991_666_666_666_667,
|
||||
17.991_666_666_666_667, 19.991_666_666_666_667,
|
||||
0, 1.9916666666666667, 3.9916666666666667, 5.991666666666666, 7.991666666666666, 9.991666666666667,
|
||||
11.991666666666667, 13.991666666666667, 15.991666666666667, 17.991666666666667, 19.991666666666667,
|
||||
];
|
||||
const cases = [
|
||||
...eiffelSeeks.map((expected, segmentIndex) => ({
|
||||
|
||||
@@ -54,7 +54,7 @@ export class TranscodingService extends BaseService {
|
||||
clearInterval(this.cleanupInterval);
|
||||
this.cleanupInterval = null;
|
||||
}
|
||||
return Promise.all([...this.sessions.values()].map(({ id }) => this.onSessionEnd({ sessionId: id })));
|
||||
return Promise.all(this.sessions.values().map(({ id }) => this.onSessionEnd({ sessionId: id })));
|
||||
}
|
||||
|
||||
@OnJob({ name: JobName.HlsSessionCleanup, queue: QueueName.BackgroundTask })
|
||||
@@ -139,9 +139,9 @@ export class TranscodingService extends BaseService {
|
||||
session.variantIndex ??= variantIndex;
|
||||
session.startSegment ??= segmentIndex;
|
||||
const curSegment = session.lastCompletedSegment === null ? session.startSegment : session.lastCompletedSegment + 1;
|
||||
const needsRestart =
|
||||
const isNeedsRestart =
|
||||
session.variantIndex !== variantIndex || segmentIndex < session.startSegment || segmentIndex > curSegment + 1;
|
||||
if (needsRestart) {
|
||||
if (isNeedsRestart) {
|
||||
this.stopTranscode(session);
|
||||
session.variantIndex = variantIndex;
|
||||
session.startSegment = segmentIndex;
|
||||
@@ -372,7 +372,7 @@ export class TranscodingService extends BaseService {
|
||||
|
||||
private removeInactiveSessions() {
|
||||
const cutoff = Date.now() - HLS_INACTIVITY_TIMEOUT_MS;
|
||||
const inactiveSessions = [...this.sessions.values()].filter((s) => s.lastActivityTime.getTime() < cutoff);
|
||||
const inactiveSessions = this.sessions.values().filter((s) => s.lastActivityTime.getTime() < cutoff);
|
||||
return Promise.all(
|
||||
inactiveSessions.map(async (session) => {
|
||||
try {
|
||||
|
||||
@@ -178,19 +178,19 @@ export class UserService extends BaseService {
|
||||
|
||||
const { licensePublicKey } = this.configRepository.getEnv();
|
||||
|
||||
const clientLicenseValid = this.cryptoRepository.verifySha256(
|
||||
const isClientLicenseValid = this.cryptoRepository.verifySha256(
|
||||
license.licenseKey,
|
||||
license.activationKey,
|
||||
licensePublicKey.client,
|
||||
);
|
||||
|
||||
const serverLicenseValid = this.cryptoRepository.verifySha256(
|
||||
const isServerLicenseValid = this.cryptoRepository.verifySha256(
|
||||
license.licenseKey,
|
||||
license.activationKey,
|
||||
licensePublicKey.server,
|
||||
);
|
||||
|
||||
if (!clientLicenseValid && !serverLicenseValid) {
|
||||
if (!isClientLicenseValid && !isServerLicenseValid) {
|
||||
throw new BadRequestException('Invalid license key');
|
||||
}
|
||||
|
||||
@@ -292,12 +292,12 @@ export class UserService extends BaseService {
|
||||
await this.eventRepository.emit('UserDelete', user);
|
||||
}
|
||||
|
||||
private isReadyForDeletion(user: { id: string; deletedAt?: Date | null }, deleteDelay: number): boolean {
|
||||
private isReadyForDeletion(user: { id: string; deletedAt?: Date | null }, delayUntilDeletion: number): boolean {
|
||||
if (!user.deletedAt) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return DateTime.now().minus({ days: deleteDelay }) > DateTime.fromJSDate(user.deletedAt);
|
||||
return DateTime.now().minus({ days: delayUntilDeletion }) > DateTime.fromJSDate(user.deletedAt);
|
||||
}
|
||||
|
||||
private async findOrFail(id: string, options: UserFindOptions) {
|
||||
|
||||
@@ -17,7 +17,7 @@ const asNotification = (
|
||||
): ReleaseEventV1 => {
|
||||
return {
|
||||
// can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483
|
||||
isAvailable: semver.intersects(`>${serverVersion}`, releaseVersion.toString(), {
|
||||
isAvailable: semver.intersects(`>${serverVersion}`, releaseVersion, {
|
||||
includePrerelease: channel === ReleaseChannel.ReleaseCandidate,
|
||||
}),
|
||||
checkedAt,
|
||||
@@ -60,8 +60,8 @@ export class VersionService extends BaseService {
|
||||
this.logger.log(`Adding ${current} to upgrade history`);
|
||||
await this.versionRepository.create({ version: current });
|
||||
|
||||
const needsNewMemories = semver.lt(previousVersion, '1.129.0');
|
||||
if (needsNewMemories) {
|
||||
const isNeedsNewMemories = semver.lt(previousVersion, '1.129.0');
|
||||
if (isNeedsNewMemories) {
|
||||
await this.jobRepository.queue({ name: JobName.MemoryGenerate });
|
||||
}
|
||||
}
|
||||
@@ -115,7 +115,7 @@ export class VersionService extends BaseService {
|
||||
|
||||
// can't use gt because it's broken for release candidates F https://github.com/npm/node-semver/issues/483
|
||||
if (
|
||||
semver.intersects(`>${serverVersion}`, releaseVersion.toString(), {
|
||||
semver.intersects(`>${serverVersion}`, releaseVersion, {
|
||||
includePrerelease: newVersionCheck.channel === ReleaseChannel.ReleaseCandidate,
|
||||
})
|
||||
) {
|
||||
|
||||
@@ -93,6 +93,7 @@ export class WorkflowExecutionService extends BaseService {
|
||||
for (const pattern of context.allowedHosts) {
|
||||
const regex = new RegExp(pattern.replaceAll('.', String.raw`\.`).replaceAll('*', '.*'));
|
||||
if (regex.test(hostname)) {
|
||||
// eslint-disable-next-line unicorn/no-invalid-argument-count
|
||||
const res = await fetch(...args);
|
||||
|
||||
return {
|
||||
@@ -124,8 +125,8 @@ export class WorkflowExecutionService extends BaseService {
|
||||
|
||||
const plugins = await this.pluginRepository.getForLoad();
|
||||
for (const { id, name, version, wasmBytes, methods } of plugins) {
|
||||
const method = methods.some(({ hostFunctions }) => !hostFunctions);
|
||||
if (method) {
|
||||
const isMethod = methods.some(({ hostFunctions }) => !hostFunctions);
|
||||
if (isMethod) {
|
||||
const label = `${name}@${version}`;
|
||||
const key = this.getPluginKey({ id, hostFunctions: false });
|
||||
try {
|
||||
@@ -136,8 +137,8 @@ export class WorkflowExecutionService extends BaseService {
|
||||
}
|
||||
}
|
||||
|
||||
const methodWithFunction = methods.some(({ hostFunctions }) => hostFunctions);
|
||||
if (methodWithFunction) {
|
||||
const isMethodWithFunction = methods.some(({ hostFunctions }) => hostFunctions);
|
||||
if (isMethodWithFunction) {
|
||||
const label = `${name}@${version}/worker`;
|
||||
const key = this.getPluginKey({ id, hostFunctions: true });
|
||||
try {
|
||||
@@ -381,8 +382,8 @@ export class WorkflowExecutionService extends BaseService {
|
||||
// TODO infer from steps
|
||||
let type: T | undefined;
|
||||
for (const targetType of Object.values(WorkflowType)) {
|
||||
const missing = workflow.steps.some((step) => !step.types.includes(targetType));
|
||||
if (!missing) {
|
||||
const isMissing = workflow.steps.some((step) => !step.types.includes(targetType));
|
||||
if (!isMissing) {
|
||||
type = targetType as unknown as T;
|
||||
break;
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user