merge main
This commit is contained in:
@@ -25,6 +25,7 @@ module.exports = {
|
||||
'unicorn/prefer-event-target': 'off',
|
||||
'unicorn/no-thenable': 'off',
|
||||
'unicorn/import-style': 'off',
|
||||
'unicorn/prefer-structured-clone': 'off',
|
||||
'@typescript-eslint/await-thenable': 'error',
|
||||
'@typescript-eslint/no-floating-promises': 'error',
|
||||
'@typescript-eslint/no-misused-promises': 'error',
|
||||
|
||||
+1
-1
@@ -1 +1 @@
|
||||
v20.12
|
||||
20.13
|
||||
|
||||
+7
-4
@@ -1,5 +1,5 @@
|
||||
# dev build
|
||||
FROM ghcr.io/immich-app/base-server-dev:20240507@sha256:ca3b8f7ca4ef72cb191b97d2715bfa0e3decdf39e666c5020536e41cf14cee1e as dev
|
||||
FROM ghcr.io/immich-app/base-server-dev:20240521@sha256:1558bd68efc2c3a4bb4968428abd66b992a41a76afc1dbb601d2a7ea2a1f7c9a as dev
|
||||
|
||||
RUN apt-get install --no-install-recommends -yqq tini
|
||||
WORKDIR /usr/src/app
|
||||
@@ -11,7 +11,7 @@ RUN npm ci && \
|
||||
rm -rf node_modules/@img/sharp-linuxmusl-x64
|
||||
COPY server .
|
||||
ENV PATH="${PATH}:/usr/src/app/bin" \
|
||||
NODE_ENV=development \
|
||||
IMMICH_ENV=development \
|
||||
NVIDIA_DRIVER_CAPABILITIES=all \
|
||||
NVIDIA_VISIBLE_DEVICES=all
|
||||
ENTRYPOINT ["tini", "--", "/bin/sh"]
|
||||
@@ -25,7 +25,7 @@ COPY --from=dev /usr/src/app/node_modules/@img ./node_modules/@img
|
||||
COPY --from=dev /usr/src/app/node_modules/exiftool-vendored.pl ./node_modules/exiftool-vendored.pl
|
||||
|
||||
# web build
|
||||
FROM node:iron-alpine3.18@sha256:fe31b16ddfb4ba4ae1a42ea540e9e44b916d754e67c64642b090839a9b2ed0ee as web
|
||||
FROM node:iron-alpine3.18@sha256:53108f67824964a573ea435fed258f6cee4d88343e9859a99d356883e71b490c as web
|
||||
|
||||
WORKDIR /usr/src/open-api/typescript-sdk
|
||||
COPY open-api/typescript-sdk/package*.json open-api/typescript-sdk/tsconfig*.json ./
|
||||
@@ -41,7 +41,7 @@ RUN npm run build
|
||||
|
||||
|
||||
# prod build
|
||||
FROM ghcr.io/immich-app/base-server-prod:20240507@sha256:1394878615cc665fd6b04f07b78d6586bb5d888423cdf8987cea072d1d72fd1f
|
||||
FROM ghcr.io/immich-app/base-server-prod:20240521@sha256:894e6dff5617062c03e65d44d946169a60df51e232b63b6f38bac1c0d168d989
|
||||
|
||||
WORKDIR /usr/src/app
|
||||
ENV NODE_ENV=production \
|
||||
@@ -61,3 +61,6 @@ ENV PATH="${PATH}:/usr/src/app/bin"
|
||||
VOLUME /usr/src/app/upload
|
||||
EXPOSE 3001
|
||||
ENTRYPOINT ["tini", "--", "/bin/bash"]
|
||||
CMD ["start.sh"]
|
||||
|
||||
HEALTHCHECK CMD npm run healthcheck
|
||||
|
||||
Generated
+1135
-1243
File diff suppressed because it is too large
Load Diff
+9
-5
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "immich",
|
||||
"version": "1.103.1",
|
||||
"version": "1.105.1",
|
||||
"description": "",
|
||||
"author": "",
|
||||
"private": true,
|
||||
@@ -18,6 +18,7 @@
|
||||
"check": "tsc --noEmit",
|
||||
"check:code": "npm run format && npm run lint && npm run check",
|
||||
"check:all": "npm run check:code && npm run test:cov",
|
||||
"healthcheck": "node ./dist/utils/healthcheck.js",
|
||||
"test": "vitest",
|
||||
"test:watch": "vitest --watch",
|
||||
"test:cov": "vitest --coverage",
|
||||
@@ -29,7 +30,8 @@
|
||||
"typeorm:migrations:revert": "typeorm migration:revert -d ./dist/database.config.js",
|
||||
"typeorm:schema:drop": "typeorm query -d ./dist/database.config.js 'DROP schema public cascade; CREATE schema public;'",
|
||||
"typeorm:schema:reset": "npm run typeorm:schema:drop && npm run typeorm:migrations:run",
|
||||
"sql:generate": "node ./dist/utils/sql.js",
|
||||
"sync:open-api": "node ./dist/bin/sync-open-api.js",
|
||||
"sync:sql": "node ./dist/bin/sync-sql.js",
|
||||
"email:dev": "email dev -p 3050 --dir src/emails"
|
||||
},
|
||||
"dependencies": {
|
||||
@@ -72,7 +74,7 @@
|
||||
"mnemonist": "^0.39.8",
|
||||
"nest-commander": "^3.11.1",
|
||||
"nestjs-cls": "^4.3.0",
|
||||
"nestjs-otel": "^5.1.5",
|
||||
"nestjs-otel": "^6.0.0",
|
||||
"nodemailer": "^6.9.13",
|
||||
"openid-client": "^5.4.3",
|
||||
"pg": "^8.11.3",
|
||||
@@ -81,6 +83,7 @@
|
||||
"reflect-metadata": "^0.2.0",
|
||||
"rxjs": "^7.8.1",
|
||||
"sanitize-filename": "^1.6.3",
|
||||
"semver": "^7.6.2",
|
||||
"sharp": "^0.33.0",
|
||||
"sirv": "^2.0.4",
|
||||
"thumbhash": "^0.1.1",
|
||||
@@ -107,6 +110,7 @@
|
||||
"@types/node": "^20.5.7",
|
||||
"@types/nodemailer": "^6.4.14",
|
||||
"@types/picomatch": "^2.3.3",
|
||||
"@types/semver": "^7.5.8",
|
||||
"@types/supertest": "^6.0.0",
|
||||
"@types/ua-parser-js": "^0.7.36",
|
||||
"@typescript-eslint/eslint-plugin": "^7.0.0",
|
||||
@@ -115,7 +119,7 @@
|
||||
"eslint": "^8.56.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"eslint-plugin-prettier": "^5.1.3",
|
||||
"eslint-plugin-unicorn": "^52.0.0",
|
||||
"eslint-plugin-unicorn": "^53.0.0",
|
||||
"mock-fs": "^5.2.0",
|
||||
"prettier": "^3.0.2",
|
||||
"prettier-plugin-organize-imports": "^3.2.3",
|
||||
@@ -129,6 +133,6 @@
|
||||
"vitest": "^1.5.0"
|
||||
},
|
||||
"volta": {
|
||||
"node": "20.12.2"
|
||||
"node": "20.13.1"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,23 @@
|
||||
#!/usr/bin/env node
|
||||
process.env.DB_URL = 'postgres://postgres:postgres@localhost:5432/immich';
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { ApiModule } from 'src/app.module';
|
||||
import { useSwagger } from 'src/utils/misc';
|
||||
|
||||
const sync = async () => {
|
||||
const app = await NestFactory.create<NestExpressApplication>(ApiModule, { preview: true });
|
||||
useSwagger(app, true);
|
||||
await app.close();
|
||||
};
|
||||
|
||||
sync()
|
||||
.then(() => {
|
||||
console.log('Done');
|
||||
process.exit(0);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error(error);
|
||||
console.log('Something went wrong');
|
||||
process.exit(1);
|
||||
});
|
||||
@@ -1,18 +1,18 @@
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
import { UserService } from 'src/services/user.service';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
|
||||
@Command({
|
||||
name: 'list-users',
|
||||
description: 'List Immich users',
|
||||
})
|
||||
export class ListUsersCommand extends CommandRunner {
|
||||
constructor(private userService: UserService) {
|
||||
constructor(private service: CliService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
try {
|
||||
const users = await this.userService.listUsers();
|
||||
const users = await this.service.listUsers();
|
||||
console.dir(users);
|
||||
} catch (error) {
|
||||
console.error(error);
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
import { SystemConfigService } from 'src/services/system-config.service';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
|
||||
@Command({
|
||||
name: 'enable-oauth-login',
|
||||
description: 'Enable OAuth login',
|
||||
})
|
||||
export class EnableOAuthLogin extends CommandRunner {
|
||||
constructor(private configService: SystemConfigService) {
|
||||
constructor(private service: CliService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const config = await this.configService.getConfig();
|
||||
config.oauth.enabled = true;
|
||||
await this.configService.updateConfig(config);
|
||||
await this.service.enableOAuthLogin();
|
||||
console.log('OAuth login has been enabled.');
|
||||
}
|
||||
}
|
||||
@@ -23,14 +21,12 @@ export class EnableOAuthLogin extends CommandRunner {
|
||||
description: 'Disable OAuth login',
|
||||
})
|
||||
export class DisableOAuthLogin extends CommandRunner {
|
||||
constructor(private configService: SystemConfigService) {
|
||||
constructor(private service: CliService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const config = await this.configService.getConfig();
|
||||
config.oauth.enabled = false;
|
||||
await this.configService.updateConfig(config);
|
||||
await this.service.disableOAuthLogin();
|
||||
console.log('OAuth login has been disabled.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
import { Command, CommandRunner } from 'nest-commander';
|
||||
import { SystemConfigService } from 'src/services/system-config.service';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
|
||||
@Command({
|
||||
name: 'enable-password-login',
|
||||
description: 'Enable password login',
|
||||
})
|
||||
export class EnablePasswordLoginCommand extends CommandRunner {
|
||||
constructor(private configService: SystemConfigService) {
|
||||
constructor(private service: CliService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const config = await this.configService.getConfig();
|
||||
config.passwordLogin.enabled = true;
|
||||
await this.configService.updateConfig(config);
|
||||
await this.service.enablePasswordLogin();
|
||||
console.log('Password login has been enabled.');
|
||||
}
|
||||
}
|
||||
@@ -23,14 +21,12 @@ export class EnablePasswordLoginCommand extends CommandRunner {
|
||||
description: 'Disable password login',
|
||||
})
|
||||
export class DisablePasswordLoginCommand extends CommandRunner {
|
||||
constructor(private configService: SystemConfigService) {
|
||||
constructor(private service: CliService) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
const config = await this.configService.getConfig();
|
||||
config.passwordLogin.enabled = false;
|
||||
await this.configService.updateConfig(config);
|
||||
await this.service.disablePasswordLogin();
|
||||
console.log('Password login has been disabled.');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,20 +1,9 @@
|
||||
import { Command, CommandRunner, InquirerService, Question, QuestionSet } from 'nest-commander';
|
||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { UserService } from 'src/services/user.service';
|
||||
import { CliService } from 'src/services/cli.service';
|
||||
|
||||
@Command({
|
||||
name: 'reset-admin-password',
|
||||
description: 'Reset the admin password',
|
||||
})
|
||||
export class ResetAdminPasswordCommand extends CommandRunner {
|
||||
constructor(
|
||||
private userService: UserService,
|
||||
private inquirer: InquirerService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
ask = (admin: UserResponseDto) => {
|
||||
const prompt = (inquirer: InquirerService) => {
|
||||
return function ask(admin: UserResponseDto) {
|
||||
const { id, oauthId, email, name } = admin;
|
||||
console.log(`Found Admin:
|
||||
- ID=${id}
|
||||
@@ -22,12 +11,25 @@ export class ResetAdminPasswordCommand extends CommandRunner {
|
||||
- Email=${email}
|
||||
- Name=${name}`);
|
||||
|
||||
return this.inquirer.ask<{ password: string }>('prompt-password', {}).then(({ password }) => password);
|
||||
return inquirer.ask<{ password: string }>('prompt-password', {}).then(({ password }) => password);
|
||||
};
|
||||
};
|
||||
|
||||
@Command({
|
||||
name: 'reset-admin-password',
|
||||
description: 'Reset the admin password',
|
||||
})
|
||||
export class ResetAdminPasswordCommand extends CommandRunner {
|
||||
constructor(
|
||||
private service: CliService,
|
||||
private inquirer: InquirerService,
|
||||
) {
|
||||
super();
|
||||
}
|
||||
|
||||
async run(): Promise<void> {
|
||||
try {
|
||||
const { password, provided } = await this.userService.resetAdminPassword(this.ask);
|
||||
const { password, provided } = await this.service.resetAdminPassword(prompt(this.inquirer));
|
||||
|
||||
if (provided) {
|
||||
console.log(`The admin password has been updated.`);
|
||||
|
||||
+347
-6
@@ -1,12 +1,354 @@
|
||||
import { RegisterQueueOptions } from '@nestjs/bullmq';
|
||||
import { ConfigModuleOptions } from '@nestjs/config';
|
||||
import { CronExpression } from '@nestjs/schedule';
|
||||
import { QueueOptions } from 'bullmq';
|
||||
import { Request, Response } from 'express';
|
||||
import { RedisOptions } from 'ioredis';
|
||||
import Joi from 'joi';
|
||||
import { CLS_ID, ClsModuleOptions } from 'nestjs-cls';
|
||||
import { LogLevel } from 'src/entities/system-config.entity';
|
||||
import { QueueName } from 'src/interfaces/job.interface';
|
||||
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
||||
|
||||
export enum TranscodePolicy {
|
||||
ALL = 'all',
|
||||
OPTIMAL = 'optimal',
|
||||
BITRATE = 'bitrate',
|
||||
REQUIRED = 'required',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum TranscodeTarget {
|
||||
NONE,
|
||||
AUDIO,
|
||||
VIDEO,
|
||||
ALL,
|
||||
}
|
||||
|
||||
export enum VideoCodec {
|
||||
H264 = 'h264',
|
||||
HEVC = 'hevc',
|
||||
VP9 = 'vp9',
|
||||
AV1 = 'av1',
|
||||
}
|
||||
|
||||
export enum AudioCodec {
|
||||
MP3 = 'mp3',
|
||||
AAC = 'aac',
|
||||
LIBOPUS = 'libopus',
|
||||
}
|
||||
|
||||
export enum TranscodeHWAccel {
|
||||
NVENC = 'nvenc',
|
||||
QSV = 'qsv',
|
||||
VAAPI = 'vaapi',
|
||||
RKMPP = 'rkmpp',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum ToneMapping {
|
||||
HABLE = 'hable',
|
||||
MOBIUS = 'mobius',
|
||||
REINHARD = 'reinhard',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum CQMode {
|
||||
AUTO = 'auto',
|
||||
CQP = 'cqp',
|
||||
ICQ = 'icq',
|
||||
}
|
||||
|
||||
export enum Colorspace {
|
||||
SRGB = 'srgb',
|
||||
P3 = 'p3',
|
||||
}
|
||||
|
||||
export enum ImageFormat {
|
||||
JPEG = 'jpeg',
|
||||
WEBP = 'webp',
|
||||
}
|
||||
|
||||
export enum LogLevel {
|
||||
VERBOSE = 'verbose',
|
||||
DEBUG = 'debug',
|
||||
LOG = 'log',
|
||||
WARN = 'warn',
|
||||
ERROR = 'error',
|
||||
FATAL = 'fatal',
|
||||
}
|
||||
|
||||
export interface SystemConfig {
|
||||
ffmpeg: {
|
||||
crf: number;
|
||||
threads: number;
|
||||
preset: string;
|
||||
targetVideoCodec: VideoCodec;
|
||||
acceptedVideoCodecs: VideoCodec[];
|
||||
targetAudioCodec: AudioCodec;
|
||||
acceptedAudioCodecs: AudioCodec[];
|
||||
targetResolution: string;
|
||||
maxBitrate: string;
|
||||
bframes: number;
|
||||
refs: number;
|
||||
gopSize: number;
|
||||
npl: number;
|
||||
temporalAQ: boolean;
|
||||
cqMode: CQMode;
|
||||
twoPass: boolean;
|
||||
preferredHwDevice: string;
|
||||
transcode: TranscodePolicy;
|
||||
accel: TranscodeHWAccel;
|
||||
accelDecode: boolean;
|
||||
tonemap: ToneMapping;
|
||||
};
|
||||
job: Record<ConcurrentQueueName, { concurrency: number }>;
|
||||
logging: {
|
||||
enabled: boolean;
|
||||
level: LogLevel;
|
||||
};
|
||||
machineLearning: {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
clip: {
|
||||
enabled: boolean;
|
||||
modelName: string;
|
||||
};
|
||||
duplicateDetection: {
|
||||
enabled: boolean;
|
||||
maxDistance: number;
|
||||
};
|
||||
facialRecognition: {
|
||||
enabled: boolean;
|
||||
modelName: string;
|
||||
minScore: number;
|
||||
minFaces: number;
|
||||
maxDistance: number;
|
||||
};
|
||||
};
|
||||
map: {
|
||||
enabled: boolean;
|
||||
lightStyle: string;
|
||||
darkStyle: string;
|
||||
};
|
||||
reverseGeocoding: {
|
||||
enabled: boolean;
|
||||
};
|
||||
oauth: {
|
||||
autoLaunch: boolean;
|
||||
autoRegister: boolean;
|
||||
buttonText: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
defaultStorageQuota: number;
|
||||
enabled: boolean;
|
||||
issuerUrl: string;
|
||||
mobileOverrideEnabled: boolean;
|
||||
mobileRedirectUri: string;
|
||||
scope: string;
|
||||
signingAlgorithm: string;
|
||||
storageLabelClaim: string;
|
||||
storageQuotaClaim: string;
|
||||
};
|
||||
passwordLogin: {
|
||||
enabled: boolean;
|
||||
};
|
||||
storageTemplate: {
|
||||
enabled: boolean;
|
||||
hashVerificationEnabled: boolean;
|
||||
template: string;
|
||||
};
|
||||
image: {
|
||||
thumbnailFormat: ImageFormat;
|
||||
thumbnailSize: number;
|
||||
previewFormat: ImageFormat;
|
||||
previewSize: number;
|
||||
quality: number;
|
||||
colorspace: Colorspace;
|
||||
extractEmbedded: boolean;
|
||||
};
|
||||
newVersionCheck: {
|
||||
enabled: boolean;
|
||||
};
|
||||
trash: {
|
||||
enabled: boolean;
|
||||
days: number;
|
||||
};
|
||||
theme: {
|
||||
customCss: string;
|
||||
};
|
||||
library: {
|
||||
scan: {
|
||||
enabled: boolean;
|
||||
cronExpression: string;
|
||||
};
|
||||
watch: {
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
notifications: {
|
||||
smtp: {
|
||||
enabled: boolean;
|
||||
from: string;
|
||||
replyTo: string;
|
||||
transport: {
|
||||
ignoreCert: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
server: {
|
||||
externalDomain: string;
|
||||
loginPageMessage: string;
|
||||
};
|
||||
user: {
|
||||
deleteDelay: number;
|
||||
};
|
||||
}
|
||||
|
||||
export const defaults = Object.freeze<SystemConfig>({
|
||||
ffmpeg: {
|
||||
crf: 23,
|
||||
threads: 0,
|
||||
preset: 'ultrafast',
|
||||
targetVideoCodec: VideoCodec.H264,
|
||||
acceptedVideoCodecs: [VideoCodec.H264],
|
||||
targetAudioCodec: AudioCodec.AAC,
|
||||
acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS],
|
||||
targetResolution: '720',
|
||||
maxBitrate: '0',
|
||||
bframes: -1,
|
||||
refs: 0,
|
||||
gopSize: 0,
|
||||
npl: 0,
|
||||
temporalAQ: false,
|
||||
cqMode: CQMode.AUTO,
|
||||
twoPass: false,
|
||||
preferredHwDevice: 'auto',
|
||||
transcode: TranscodePolicy.REQUIRED,
|
||||
tonemap: ToneMapping.HABLE,
|
||||
accel: TranscodeHWAccel.DISABLED,
|
||||
accelDecode: false,
|
||||
},
|
||||
job: {
|
||||
[QueueName.BACKGROUND_TASK]: { concurrency: 5 },
|
||||
[QueueName.SMART_SEARCH]: { concurrency: 2 },
|
||||
[QueueName.METADATA_EXTRACTION]: { concurrency: 5 },
|
||||
[QueueName.FACE_DETECTION]: { concurrency: 2 },
|
||||
[QueueName.SEARCH]: { concurrency: 5 },
|
||||
[QueueName.SIDECAR]: { concurrency: 5 },
|
||||
[QueueName.LIBRARY]: { concurrency: 5 },
|
||||
[QueueName.MIGRATION]: { concurrency: 5 },
|
||||
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
|
||||
[QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
|
||||
[QueueName.NOTIFICATION]: { concurrency: 5 },
|
||||
},
|
||||
logging: {
|
||||
enabled: true,
|
||||
level: LogLevel.LOG,
|
||||
},
|
||||
machineLearning: {
|
||||
enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
|
||||
url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003',
|
||||
clip: {
|
||||
enabled: true,
|
||||
modelName: 'ViT-B-32__openai',
|
||||
},
|
||||
duplicateDetection: {
|
||||
enabled: false,
|
||||
maxDistance: 0.03,
|
||||
},
|
||||
facialRecognition: {
|
||||
enabled: true,
|
||||
modelName: 'buffalo_l',
|
||||
minScore: 0.7,
|
||||
maxDistance: 0.5,
|
||||
minFaces: 3,
|
||||
},
|
||||
},
|
||||
map: {
|
||||
enabled: true,
|
||||
lightStyle: '',
|
||||
darkStyle: '',
|
||||
},
|
||||
reverseGeocoding: {
|
||||
enabled: true,
|
||||
},
|
||||
oauth: {
|
||||
autoLaunch: false,
|
||||
autoRegister: true,
|
||||
buttonText: 'Login with OAuth',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
defaultStorageQuota: 0,
|
||||
enabled: false,
|
||||
issuerUrl: '',
|
||||
mobileOverrideEnabled: false,
|
||||
mobileRedirectUri: '',
|
||||
scope: 'openid email profile',
|
||||
signingAlgorithm: 'RS256',
|
||||
storageLabelClaim: 'preferred_username',
|
||||
storageQuotaClaim: 'immich_quota',
|
||||
},
|
||||
passwordLogin: {
|
||||
enabled: true,
|
||||
},
|
||||
storageTemplate: {
|
||||
enabled: false,
|
||||
hashVerificationEnabled: true,
|
||||
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||
},
|
||||
image: {
|
||||
thumbnailFormat: ImageFormat.WEBP,
|
||||
thumbnailSize: 250,
|
||||
previewFormat: ImageFormat.JPEG,
|
||||
previewSize: 1440,
|
||||
quality: 80,
|
||||
colorspace: Colorspace.P3,
|
||||
extractEmbedded: false,
|
||||
},
|
||||
newVersionCheck: {
|
||||
enabled: true,
|
||||
},
|
||||
trash: {
|
||||
enabled: true,
|
||||
days: 30,
|
||||
},
|
||||
theme: {
|
||||
customCss: '',
|
||||
},
|
||||
library: {
|
||||
scan: {
|
||||
enabled: true,
|
||||
cronExpression: CronExpression.EVERY_DAY_AT_MIDNIGHT,
|
||||
},
|
||||
watch: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
server: {
|
||||
externalDomain: '',
|
||||
loginPageMessage: '',
|
||||
},
|
||||
notifications: {
|
||||
smtp: {
|
||||
enabled: false,
|
||||
from: '',
|
||||
replyTo: '',
|
||||
transport: {
|
||||
ignoreCert: false,
|
||||
host: '',
|
||||
port: 587,
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
deleteDelay: 7,
|
||||
},
|
||||
});
|
||||
|
||||
const WHEN_DB_URL_SET = Joi.when('DB_URL', {
|
||||
is: Joi.exist(),
|
||||
@@ -18,8 +360,8 @@ export const immichAppConfig: ConfigModuleOptions = {
|
||||
envFilePath: '.env',
|
||||
isGlobal: true,
|
||||
validationSchema: Joi.object({
|
||||
NODE_ENV: Joi.string().optional().valid('development', 'production', 'staging').default('development'),
|
||||
LOG_LEVEL: Joi.string()
|
||||
IMMICH_ENV: Joi.string().optional().valid('development', 'production').default('production'),
|
||||
IMMICH_LOG_LEVEL: Joi.string()
|
||||
.optional()
|
||||
.valid(...Object.values(LogLevel)),
|
||||
|
||||
@@ -30,8 +372,7 @@ export const immichAppConfig: ConfigModuleOptions = {
|
||||
DB_VECTOR_EXTENSION: Joi.string().optional().valid('pgvector', 'pgvecto.rs').default('pgvecto.rs'),
|
||||
DB_SKIP_MIGRATIONS: Joi.boolean().optional().default(false),
|
||||
|
||||
MACHINE_LEARNING_PORT: Joi.number().optional(),
|
||||
MICROSERVICES_PORT: Joi.number().optional(),
|
||||
IMMICH_PORT: Joi.number().optional(),
|
||||
IMMICH_METRICS_PORT: Joi.number().optional(),
|
||||
|
||||
IMMICH_METRICS: Joi.boolean().optional().default(false),
|
||||
|
||||
@@ -1,7 +1,11 @@
|
||||
import { Duration } from 'luxon';
|
||||
import { readFileSync } from 'node:fs';
|
||||
import { join } from 'node:path';
|
||||
import { Version } from 'src/utils/version';
|
||||
import { SemVer } from 'semver';
|
||||
|
||||
export const POSTGRES_VERSION_RANGE = '>=14.0.0';
|
||||
export const VECTORS_VERSION_RANGE = '0.2.x';
|
||||
export const VECTOR_VERSION_RANGE = '>=0.5 <1';
|
||||
|
||||
export const NEXT_RELEASE = 'NEXT_RELEASE';
|
||||
export const LIFECYCLE_EXTENSION = 'x-immich-lifecycle';
|
||||
@@ -11,13 +15,13 @@ export const ADDED_IN_PREFIX = 'This property was added in ';
|
||||
export const SALT_ROUNDS = 10;
|
||||
|
||||
const { version } = JSON.parse(readFileSync('./package.json', 'utf8'));
|
||||
export const serverVersion = Version.fromString(version);
|
||||
export const serverVersion = new SemVer(version);
|
||||
|
||||
export const AUDIT_LOG_MAX_DURATION = Duration.fromObject({ days: 100 });
|
||||
export const ONE_HOUR = Duration.fromObject({ hours: 1 });
|
||||
|
||||
export const envName = (process.env.NODE_ENV || 'development').toUpperCase();
|
||||
export const isDev = process.env.NODE_ENV === 'development';
|
||||
export const envName = (process.env.IMMICH_ENV || 'production').toUpperCase();
|
||||
export const isDev = () => process.env.IMMICH_ENV === 'development';
|
||||
export const APP_MEDIA_LOCATION = process.env.IMMICH_MEDIA_LOCATION || './upload';
|
||||
export const WEB_ROOT = process.env.IMMICH_WEB_ROOT || '/usr/src/app/www';
|
||||
|
||||
|
||||
@@ -14,7 +14,7 @@ import { ActivityService } from 'src/services/activity.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Activity')
|
||||
@Controller('activity')
|
||||
@Controller('activities')
|
||||
export class ActivityController {
|
||||
constructor(private service: ActivityService) {}
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ import { AlbumService } from 'src/services/album.service';
|
||||
import { ParseMeUUIDPipe, UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Album')
|
||||
@Controller('album')
|
||||
@Controller('albums')
|
||||
export class AlbumController {
|
||||
constructor(private service: AlbumService) {}
|
||||
|
||||
|
||||
@@ -7,7 +7,7 @@ import { APIKeyService } from 'src/services/api-key.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('API Key')
|
||||
@Controller('api-key')
|
||||
@Controller('api-keys')
|
||||
export class APIKeyController {
|
||||
constructor(private service: APIKeyService) {}
|
||||
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
import {
|
||||
Body,
|
||||
Controller,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Param,
|
||||
ParseFilePipe,
|
||||
Post,
|
||||
Put,
|
||||
Res,
|
||||
UploadedFiles,
|
||||
UseInterceptors,
|
||||
} from '@nestjs/common';
|
||||
import { ApiConsumes, ApiTags } from '@nestjs/swagger';
|
||||
import { Response } from 'express';
|
||||
import { EndpointLifecycle } from 'src/decorators';
|
||||
import {
|
||||
AssetBulkUploadCheckResponseDto,
|
||||
AssetMediaResponseDto,
|
||||
AssetMediaStatusEnum,
|
||||
CheckExistingAssetsResponseDto,
|
||||
} from 'src/dtos/asset-media-response.dto';
|
||||
import {
|
||||
AssetBulkUploadCheckDto,
|
||||
AssetMediaReplaceDto,
|
||||
CheckExistingAssetsDto,
|
||||
UploadFieldName,
|
||||
} from 'src/dtos/asset-media.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { FileUploadInterceptor, Route, UploadFiles, getFiles } from 'src/middleware/file-upload.interceptor';
|
||||
import { AssetMediaService } from 'src/services/asset-media.service';
|
||||
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Asset')
|
||||
@Controller(Route.ASSET)
|
||||
export class AssetMediaController {
|
||||
constructor(
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
private service: AssetMediaService,
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Replace the asset with new file, without changing its id
|
||||
*/
|
||||
@Put(':id/file')
|
||||
@UseInterceptors(FileUploadInterceptor)
|
||||
@ApiConsumes('multipart/form-data')
|
||||
@Authenticated({ sharedLink: true })
|
||||
@EndpointLifecycle({ addedAt: 'v1.106.0' })
|
||||
async replaceAsset(
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@UploadedFiles(new ParseFilePipe({ validators: [new FileNotEmptyValidator([UploadFieldName.ASSET_DATA])] }))
|
||||
files: UploadFiles,
|
||||
@Body() dto: AssetMediaReplaceDto,
|
||||
@Res({ passthrough: true }) res: Response,
|
||||
): Promise<AssetMediaResponseDto> {
|
||||
const { file } = getFiles(files);
|
||||
const responseDto = await this.service.replaceAsset(auth, id, dto, file);
|
||||
if (responseDto.status === AssetMediaStatusEnum.DUPLICATE) {
|
||||
res.status(HttpStatus.OK);
|
||||
}
|
||||
return responseDto;
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||
*/
|
||||
@Post('exist')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
checkExistingAssets(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: CheckExistingAssetsDto,
|
||||
): Promise<CheckExistingAssetsResponseDto> {
|
||||
return this.service.checkExistingAssets(auth, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if assets exist by checksums
|
||||
*/
|
||||
@Post('bulk-upload-check')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
checkBulkUpload(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: AssetBulkUploadCheckDto,
|
||||
): Promise<AssetBulkUploadCheckResponseDto> {
|
||||
return this.service.bulkUploadCheck(auth, dto);
|
||||
}
|
||||
}
|
||||
@@ -2,8 +2,8 @@ import {
|
||||
Body,
|
||||
Controller,
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Next,
|
||||
Param,
|
||||
ParseFilePipe,
|
||||
@@ -15,38 +15,24 @@ import {
|
||||
} from '@nestjs/common';
|
||||
import { ApiBody, ApiConsumes, ApiHeader, ApiTags } from '@nestjs/swagger';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import {
|
||||
AssetBulkUploadCheckResponseDto,
|
||||
AssetFileUploadResponseDto,
|
||||
CheckExistingAssetsResponseDto,
|
||||
} from 'src/dtos/asset-v1-response.dto';
|
||||
import {
|
||||
AssetBulkUploadCheckDto,
|
||||
AssetSearchDto,
|
||||
CheckExistingAssetsDto,
|
||||
CreateAssetDto,
|
||||
GetAssetThumbnailDto,
|
||||
ServeFileDto,
|
||||
} from 'src/dtos/asset-v1.dto';
|
||||
import { AssetFileUploadResponseDto } from 'src/dtos/asset-v1-response.dto';
|
||||
import { CreateAssetDto, GetAssetThumbnailDto, ServeFileDto } from 'src/dtos/asset-v1.dto';
|
||||
import { AuthDto, ImmichHeader } from 'src/dtos/auth.dto';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { AssetUploadInterceptor } from 'src/middleware/asset-upload.interceptor';
|
||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||
import { FileUploadInterceptor, ImmichFile, Route, mapToUploadFile } from 'src/middleware/file-upload.interceptor';
|
||||
import { FileUploadInterceptor, Route, UploadFiles, mapToUploadFile } from 'src/middleware/file-upload.interceptor';
|
||||
import { AssetServiceV1 } from 'src/services/asset-v1.service';
|
||||
import { sendFile } from 'src/utils/file';
|
||||
import { FileNotEmptyValidator, UUIDParamDto } from 'src/validation';
|
||||
|
||||
interface UploadFiles {
|
||||
assetData: ImmichFile[];
|
||||
livePhotoData?: ImmichFile[];
|
||||
sidecarData: ImmichFile[];
|
||||
}
|
||||
|
||||
@ApiTags('Asset')
|
||||
@Controller(Route.ASSET)
|
||||
export class AssetControllerV1 {
|
||||
constructor(private service: AssetServiceV1) {}
|
||||
constructor(
|
||||
private service: AssetServiceV1,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {}
|
||||
|
||||
@Post('upload')
|
||||
@UseInterceptors(AssetUploadInterceptor, FileUploadInterceptor)
|
||||
@@ -95,7 +81,7 @@ export class AssetControllerV1 {
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Query() dto: ServeFileDto,
|
||||
) {
|
||||
await sendFile(res, next, () => this.service.serveFile(auth, id, dto));
|
||||
await sendFile(res, next, () => this.service.serveFile(auth, id, dto), this.logger);
|
||||
}
|
||||
|
||||
@Get('/thumbnail/:id')
|
||||
@@ -108,47 +94,6 @@ export class AssetControllerV1 {
|
||||
@Param() { id }: UUIDParamDto,
|
||||
@Query() dto: GetAssetThumbnailDto,
|
||||
) {
|
||||
await sendFile(res, next, () => this.service.serveThumbnail(auth, id, dto));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all AssetEntity belong to the user
|
||||
*/
|
||||
@Get('/')
|
||||
@ApiHeader({
|
||||
name: 'if-none-match',
|
||||
description: 'ETag of data already cached on the client',
|
||||
required: false,
|
||||
schema: { type: 'string' },
|
||||
})
|
||||
@Authenticated()
|
||||
getAllAssets(@Auth() auth: AuthDto, @Query() dto: AssetSearchDto): Promise<AssetResponseDto[]> {
|
||||
return this.service.getAllAssets(auth, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if multiple assets exist on the server and returns all existing - used by background backup
|
||||
*/
|
||||
@Post('/exist')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
checkExistingAssets(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: CheckExistingAssetsDto,
|
||||
): Promise<CheckExistingAssetsResponseDto> {
|
||||
return this.service.checkExistingAssets(auth, dto);
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if assets exist by checksums
|
||||
*/
|
||||
@Post('/bulk-upload-check')
|
||||
@HttpCode(HttpStatus.OK)
|
||||
@Authenticated()
|
||||
checkBulkUpload(
|
||||
@Auth() auth: AuthDto,
|
||||
@Body() dto: AssetBulkUploadCheckDto,
|
||||
): Promise<AssetBulkUploadCheckResponseDto> {
|
||||
return this.service.bulkUploadCheck(auth, dto);
|
||||
await sendFile(res, next, () => this.service.serveThumbnail(auth, id, dto), this.logger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
import { Body, Controller, HttpCode, HttpStatus, Next, Param, Post, Res, StreamableFile } from '@nestjs/common';
|
||||
import { Body, Controller, HttpCode, HttpStatus, Inject, Next, Param, Post, Res, StreamableFile } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { AssetIdsDto } from 'src/dtos/asset.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { DownloadInfoDto, DownloadResponseDto } from 'src/dtos/download.dto';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||
import { DownloadService } from 'src/services/download.service';
|
||||
import { asStreamableFile, sendFile } from 'src/utils/file';
|
||||
@@ -12,7 +13,10 @@ import { UUIDParamDto } from 'src/validation';
|
||||
@ApiTags('Download')
|
||||
@Controller('download')
|
||||
export class DownloadController {
|
||||
constructor(private service: DownloadService) {}
|
||||
constructor(
|
||||
private service: DownloadService,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {}
|
||||
|
||||
@Post('info')
|
||||
@Authenticated({ sharedLink: true })
|
||||
@@ -38,6 +42,6 @@ export class DownloadController {
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
) {
|
||||
await sendFile(res, next, () => this.service.downloadFile(auth, id));
|
||||
await sendFile(res, next, () => this.service.downloadFile(auth, id), this.logger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,18 @@
|
||||
import { Controller, Get } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { DuplicateResponseDto } from 'src/dtos/duplicate.dto';
|
||||
import { Auth, Authenticated } from 'src/middleware/auth.guard';
|
||||
import { DuplicateService } from 'src/services/duplicate.service';
|
||||
|
||||
@ApiTags('Duplicate')
|
||||
@Controller('duplicates')
|
||||
export class DuplicateController {
|
||||
constructor(private service: DuplicateService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
getAssetDuplicates(@Auth() auth: AuthDto): Promise<DuplicateResponseDto[]> {
|
||||
return this.service.getDuplicates(auth);
|
||||
}
|
||||
}
|
||||
@@ -7,7 +7,7 @@ import { PersonService } from 'src/services/person.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Face')
|
||||
@Controller('face')
|
||||
@Controller('faces')
|
||||
export class FaceController {
|
||||
constructor(private service: PersonService) {}
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import { Authenticated } from 'src/middleware/auth.guard';
|
||||
import { AuditService } from 'src/services/audit.service';
|
||||
|
||||
@ApiTags('File Report')
|
||||
@Controller('report')
|
||||
@Controller('reports')
|
||||
export class ReportController {
|
||||
constructor(private service: AuditService) {}
|
||||
|
||||
|
||||
@@ -2,11 +2,13 @@ import { ActivityController } from 'src/controllers/activity.controller';
|
||||
import { AlbumController } from 'src/controllers/album.controller';
|
||||
import { APIKeyController } from 'src/controllers/api-key.controller';
|
||||
import { AppController } from 'src/controllers/app.controller';
|
||||
import { AssetMediaController } from 'src/controllers/asset-media.controller';
|
||||
import { AssetControllerV1 } from 'src/controllers/asset-v1.controller';
|
||||
import { AssetController } from 'src/controllers/asset.controller';
|
||||
import { AuditController } from 'src/controllers/audit.controller';
|
||||
import { AuthController } from 'src/controllers/auth.controller';
|
||||
import { DownloadController } from 'src/controllers/download.controller';
|
||||
import { DuplicateController } from 'src/controllers/duplicate.controller';
|
||||
import { FaceController } from 'src/controllers/face.controller';
|
||||
import { ReportController } from 'src/controllers/file-report.controller';
|
||||
import { JobController } from 'src/controllers/job.controller';
|
||||
@@ -34,9 +36,11 @@ export const controllers = [
|
||||
AppController,
|
||||
AssetController,
|
||||
AssetControllerV1,
|
||||
AssetMediaController,
|
||||
AuditController,
|
||||
AuthController,
|
||||
DownloadController,
|
||||
DuplicateController,
|
||||
FaceController,
|
||||
JobController,
|
||||
LibraryController,
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put, Query } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, HttpCode, HttpStatus, Param, Post, Put } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
CreateLibraryDto,
|
||||
LibraryResponseDto,
|
||||
LibraryStatsResponseDto,
|
||||
ScanLibraryDto,
|
||||
SearchLibraryDto,
|
||||
UpdateLibraryDto,
|
||||
ValidateLibraryDto,
|
||||
ValidateLibraryResponseDto,
|
||||
@@ -15,14 +14,14 @@ import { LibraryService } from 'src/services/library.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Library')
|
||||
@Controller('library')
|
||||
@Controller('libraries')
|
||||
export class LibraryController {
|
||||
constructor(private service: LibraryService) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated({ admin: true })
|
||||
getAllLibraries(@Query() dto: SearchLibraryDto): Promise<LibraryResponseDto[]> {
|
||||
return this.service.getAll(dto);
|
||||
getAllLibraries(): Promise<LibraryResponseDto[]> {
|
||||
return this.service.getAll();
|
||||
}
|
||||
|
||||
@Post()
|
||||
|
||||
@@ -8,7 +8,7 @@ import { PartnerService } from 'src/services/partner.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Partner')
|
||||
@Controller('partner')
|
||||
@Controller('partners')
|
||||
export class PartnerController {
|
||||
constructor(private service: PartnerService) {}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { Body, Controller, Delete, Get, Next, Param, Post, Put, Query, Res } from '@nestjs/common';
|
||||
import { Body, Controller, Delete, Get, Inject, Next, Param, Post, Put, Query, Res } from '@nestjs/common';
|
||||
import { ApiTags } from '@nestjs/swagger';
|
||||
import { NextFunction, Response } from 'express';
|
||||
import { BulkIdResponseDto } from 'src/dtos/asset-ids.response.dto';
|
||||
@@ -15,15 +15,19 @@ import {
|
||||
PersonStatisticsResponseDto,
|
||||
PersonUpdateDto,
|
||||
} from 'src/dtos/person.dto';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||
import { PersonService } from 'src/services/person.service';
|
||||
import { sendFile } from 'src/utils/file';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Person')
|
||||
@Controller('person')
|
||||
@Controller('people')
|
||||
export class PersonController {
|
||||
constructor(private service: PersonService) {}
|
||||
constructor(
|
||||
private service: PersonService,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
@@ -74,7 +78,7 @@ export class PersonController {
|
||||
@Auth() auth: AuthDto,
|
||||
@Param() { id }: UUIDParamDto,
|
||||
) {
|
||||
await sendFile(res, next, () => this.service.getThumbnail(auth, id));
|
||||
await sendFile(res, next, () => this.service.getThumbnail(auth, id), this.logger);
|
||||
}
|
||||
|
||||
@Get(':id/assets')
|
||||
|
||||
@@ -3,25 +3,29 @@ import { ApiTags } from '@nestjs/swagger';
|
||||
import {
|
||||
ServerConfigDto,
|
||||
ServerFeaturesDto,
|
||||
ServerInfoResponseDto,
|
||||
ServerMediaTypesResponseDto,
|
||||
ServerPingResponse,
|
||||
ServerStatsResponseDto,
|
||||
ServerStorageResponseDto,
|
||||
ServerThemeDto,
|
||||
ServerVersionResponseDto,
|
||||
} from 'src/dtos/server-info.dto';
|
||||
import { Authenticated } from 'src/middleware/auth.guard';
|
||||
import { ServerInfoService } from 'src/services/server-info.service';
|
||||
import { VersionService } from 'src/services/version.service';
|
||||
|
||||
@ApiTags('Server Info')
|
||||
@Controller('server-info')
|
||||
export class ServerInfoController {
|
||||
constructor(private service: ServerInfoService) {}
|
||||
constructor(
|
||||
private service: ServerInfoService,
|
||||
private versionService: VersionService,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Get('storage')
|
||||
@Authenticated()
|
||||
getServerInfo(): Promise<ServerInfoResponseDto> {
|
||||
return this.service.getInfo();
|
||||
getStorage(): Promise<ServerStorageResponseDto> {
|
||||
return this.service.getStorage();
|
||||
}
|
||||
|
||||
@Get('ping')
|
||||
@@ -31,7 +35,7 @@ export class ServerInfoController {
|
||||
|
||||
@Get('version')
|
||||
getServerVersion(): ServerVersionResponseDto {
|
||||
return this.service.getVersion();
|
||||
return this.versionService.getVersion();
|
||||
}
|
||||
|
||||
@Get('features')
|
||||
|
||||
@@ -17,7 +17,7 @@ import { respondWithCookie } from 'src/utils/response';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Shared Link')
|
||||
@Controller('shared-link')
|
||||
@Controller('shared-links')
|
||||
export class SharedLinkController {
|
||||
constructor(private service: SharedLinkService) {}
|
||||
|
||||
|
||||
@@ -10,7 +10,7 @@ import { TagService } from 'src/services/tag.service';
|
||||
import { UUIDParamDto } from 'src/validation';
|
||||
|
||||
@ApiTags('Tag')
|
||||
@Controller('tag')
|
||||
@Controller('tags')
|
||||
export class TagController {
|
||||
constructor(private service: TagService) {}
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import {
|
||||
Get,
|
||||
HttpCode,
|
||||
HttpStatus,
|
||||
Inject,
|
||||
Next,
|
||||
Param,
|
||||
Post,
|
||||
@@ -19,6 +20,7 @@ import { NextFunction, Response } from 'express';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { CreateProfileImageDto, CreateProfileImageResponseDto } from 'src/dtos/user-profile.dto';
|
||||
import { CreateUserDto, DeleteUserDto, UpdateUserDto, UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { Auth, Authenticated, FileResponse } from 'src/middleware/auth.guard';
|
||||
import { FileUploadInterceptor, Route } from 'src/middleware/file-upload.interceptor';
|
||||
import { UserService } from 'src/services/user.service';
|
||||
@@ -28,7 +30,10 @@ import { UUIDParamDto } from 'src/validation';
|
||||
@ApiTags('User')
|
||||
@Controller(Route.USER)
|
||||
export class UserController {
|
||||
constructor(private service: UserService) {}
|
||||
constructor(
|
||||
private service: UserService,
|
||||
@Inject(ILoggerRepository) private logger: ILoggerRepository,
|
||||
) {}
|
||||
|
||||
@Get()
|
||||
@Authenticated()
|
||||
@@ -36,10 +41,10 @@ export class UserController {
|
||||
return this.service.getAll(auth, isAll);
|
||||
}
|
||||
|
||||
@Get('info/:id')
|
||||
@Authenticated()
|
||||
getUserById(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
|
||||
return this.service.get(id);
|
||||
@Post()
|
||||
@Authenticated({ admin: true })
|
||||
createUser(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
|
||||
return this.service.create(createUserDto);
|
||||
}
|
||||
|
||||
@Get('me')
|
||||
@@ -48,10 +53,10 @@ export class UserController {
|
||||
return this.service.getMe(auth);
|
||||
}
|
||||
|
||||
@Post()
|
||||
@Authenticated({ admin: true })
|
||||
createUser(@Body() createUserDto: CreateUserDto): Promise<UserResponseDto> {
|
||||
return this.service.create(createUserDto);
|
||||
@Get(':id')
|
||||
@Authenticated()
|
||||
getUserById(@Param() { id }: UUIDParamDto): Promise<UserResponseDto> {
|
||||
return this.service.get(id);
|
||||
}
|
||||
|
||||
@Delete('profile-image')
|
||||
@@ -96,10 +101,10 @@ export class UserController {
|
||||
return this.service.createProfileImage(auth, fileInfo);
|
||||
}
|
||||
|
||||
@Get('profile-image/:id')
|
||||
@Get(':id/profile-image')
|
||||
@FileResponse()
|
||||
@Authenticated()
|
||||
async getProfileImage(@Res() res: Response, @Next() next: NextFunction, @Param() { id }: UUIDParamDto) {
|
||||
await sendFile(res, next, () => this.service.getProfileImage(id));
|
||||
await sendFile(res, next, () => this.service.getProfileImage(id), this.logger);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -274,7 +274,7 @@ export class AccessCore {
|
||||
}
|
||||
|
||||
case Permission.ASSET_UPLOAD: {
|
||||
return await this.repository.library.checkOwnerAccess(auth.user.id, ids);
|
||||
return ids.has(auth.user.id) ? new Set([auth.user.id]) : new Set<string>();
|
||||
}
|
||||
|
||||
case Permission.ARCHIVE_READ: {
|
||||
|
||||
@@ -1,18 +1,18 @@
|
||||
import { randomUUID } from 'node:crypto';
|
||||
import { dirname, join, resolve } from 'node:path';
|
||||
import { ImageFormat } from 'src/config';
|
||||
import { APP_MEDIA_LOCATION } from 'src/constants';
|
||||
import { SystemConfigCore } from 'src/cores/system-config.core';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { AssetPathType, PathType, PersonPathType } from 'src/entities/move.entity';
|
||||
import { PersonEntity } from 'src/entities/person.entity';
|
||||
import { ImageFormat } from 'src/entities/system-config.entity';
|
||||
import { IAssetRepository } from 'src/interfaces/asset.interface';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { IMoveRepository } from 'src/interfaces/move.interface';
|
||||
import { IPersonRepository } from 'src/interfaces/person.interface';
|
||||
import { IStorageRepository } from 'src/interfaces/storage.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
|
||||
export enum StorageFolder {
|
||||
ENCODED_VIDEO = 'encoded-video',
|
||||
@@ -49,10 +49,10 @@ export class StorageCore {
|
||||
private moveRepository: IMoveRepository,
|
||||
private personRepository: IPersonRepository,
|
||||
private storageRepository: IStorageRepository,
|
||||
systemConfigRepository: ISystemConfigRepository,
|
||||
systemMetadataRepository: ISystemMetadataRepository,
|
||||
private logger: ILoggerRepository,
|
||||
) {
|
||||
this.configCore = SystemConfigCore.create(systemConfigRepository, this.logger);
|
||||
this.configCore = SystemConfigCore.create(systemMetadataRepository, this.logger);
|
||||
}
|
||||
|
||||
static create(
|
||||
@@ -61,7 +61,7 @@ export class StorageCore {
|
||||
moveRepository: IMoveRepository,
|
||||
personRepository: IPersonRepository,
|
||||
storageRepository: IStorageRepository,
|
||||
systemConfigRepository: ISystemConfigRepository,
|
||||
systemMetadataRepository: ISystemMetadataRepository,
|
||||
logger: ILoggerRepository,
|
||||
) {
|
||||
if (!instance) {
|
||||
@@ -71,7 +71,7 @@ export class StorageCore {
|
||||
moveRepository,
|
||||
personRepository,
|
||||
storageRepository,
|
||||
systemConfigRepository,
|
||||
systemMetadataRepository,
|
||||
logger,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,201 +1,37 @@
|
||||
import { BadRequestException, ForbiddenException, Injectable } from '@nestjs/common';
|
||||
import { CronExpression } from '@nestjs/schedule';
|
||||
import { Injectable } from '@nestjs/common';
|
||||
import AsyncLock from 'async-lock';
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { load as loadYaml } from 'js-yaml';
|
||||
import * as _ from 'lodash';
|
||||
import { Subject } from 'rxjs';
|
||||
import { SystemConfig, defaults } from 'src/config';
|
||||
import { SystemConfigDto } from 'src/dtos/system-config.dto';
|
||||
import {
|
||||
AudioCodec,
|
||||
CQMode,
|
||||
Colorspace,
|
||||
ImageFormat,
|
||||
LogLevel,
|
||||
SystemConfig,
|
||||
SystemConfigEntity,
|
||||
SystemConfigKey,
|
||||
SystemConfigValue,
|
||||
ToneMapping,
|
||||
TranscodeHWAccel,
|
||||
TranscodePolicy,
|
||||
VideoCodec,
|
||||
} from 'src/entities/system-config.entity';
|
||||
import { QueueName } from 'src/interfaces/job.interface';
|
||||
import { SystemMetadataKey } from 'src/entities/system-metadata.entity';
|
||||
import { DatabaseLock } from 'src/interfaces/database.interface';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { ISystemConfigRepository } from 'src/interfaces/system-config.interface';
|
||||
import { ISystemMetadataRepository } from 'src/interfaces/system-metadata.interface';
|
||||
import { getKeysDeep, unsetDeep } from 'src/utils/misc';
|
||||
import { DeepPartial } from 'typeorm';
|
||||
|
||||
export type SystemConfigValidator = (config: SystemConfig, newConfig: SystemConfig) => void | Promise<void>;
|
||||
|
||||
export const defaults = Object.freeze<SystemConfig>({
|
||||
ffmpeg: {
|
||||
crf: 23,
|
||||
threads: 0,
|
||||
preset: 'ultrafast',
|
||||
targetVideoCodec: VideoCodec.H264,
|
||||
acceptedVideoCodecs: [VideoCodec.H264],
|
||||
targetAudioCodec: AudioCodec.AAC,
|
||||
acceptedAudioCodecs: [AudioCodec.AAC, AudioCodec.MP3, AudioCodec.LIBOPUS],
|
||||
targetResolution: '720',
|
||||
maxBitrate: '0',
|
||||
bframes: -1,
|
||||
refs: 0,
|
||||
gopSize: 0,
|
||||
npl: 0,
|
||||
temporalAQ: false,
|
||||
cqMode: CQMode.AUTO,
|
||||
twoPass: false,
|
||||
preferredHwDevice: 'auto',
|
||||
transcode: TranscodePolicy.REQUIRED,
|
||||
tonemap: ToneMapping.HABLE,
|
||||
accel: TranscodeHWAccel.DISABLED,
|
||||
},
|
||||
job: {
|
||||
[QueueName.BACKGROUND_TASK]: { concurrency: 5 },
|
||||
[QueueName.SMART_SEARCH]: { concurrency: 2 },
|
||||
[QueueName.METADATA_EXTRACTION]: { concurrency: 5 },
|
||||
[QueueName.FACE_DETECTION]: { concurrency: 2 },
|
||||
[QueueName.SEARCH]: { concurrency: 5 },
|
||||
[QueueName.SIDECAR]: { concurrency: 5 },
|
||||
[QueueName.LIBRARY]: { concurrency: 5 },
|
||||
[QueueName.MIGRATION]: { concurrency: 5 },
|
||||
[QueueName.THUMBNAIL_GENERATION]: { concurrency: 5 },
|
||||
[QueueName.VIDEO_CONVERSION]: { concurrency: 1 },
|
||||
[QueueName.NOTIFICATION]: { concurrency: 5 },
|
||||
},
|
||||
logging: {
|
||||
enabled: true,
|
||||
level: LogLevel.LOG,
|
||||
},
|
||||
machineLearning: {
|
||||
enabled: process.env.IMMICH_MACHINE_LEARNING_ENABLED !== 'false',
|
||||
url: process.env.IMMICH_MACHINE_LEARNING_URL || 'http://immich-machine-learning:3003',
|
||||
clip: {
|
||||
enabled: true,
|
||||
modelName: 'ViT-B-32__openai',
|
||||
},
|
||||
facialRecognition: {
|
||||
enabled: true,
|
||||
modelName: 'buffalo_l',
|
||||
minScore: 0.7,
|
||||
maxDistance: 0.5,
|
||||
minFaces: 3,
|
||||
},
|
||||
},
|
||||
map: {
|
||||
enabled: true,
|
||||
lightStyle: '',
|
||||
darkStyle: '',
|
||||
},
|
||||
reverseGeocoding: {
|
||||
enabled: true,
|
||||
},
|
||||
oauth: {
|
||||
autoLaunch: false,
|
||||
autoRegister: true,
|
||||
buttonText: 'Login with OAuth',
|
||||
clientId: '',
|
||||
clientSecret: '',
|
||||
defaultStorageQuota: 0,
|
||||
enabled: false,
|
||||
issuerUrl: '',
|
||||
mobileOverrideEnabled: false,
|
||||
mobileRedirectUri: '',
|
||||
scope: 'openid email profile',
|
||||
signingAlgorithm: 'RS256',
|
||||
storageLabelClaim: 'preferred_username',
|
||||
storageQuotaClaim: 'immich_quota',
|
||||
},
|
||||
passwordLogin: {
|
||||
enabled: true,
|
||||
},
|
||||
storageTemplate: {
|
||||
enabled: false,
|
||||
hashVerificationEnabled: true,
|
||||
template: '{{y}}/{{y}}-{{MM}}-{{dd}}/{{filename}}',
|
||||
},
|
||||
image: {
|
||||
thumbnailFormat: ImageFormat.WEBP,
|
||||
thumbnailSize: 250,
|
||||
previewFormat: ImageFormat.JPEG,
|
||||
previewSize: 1440,
|
||||
quality: 80,
|
||||
colorspace: Colorspace.P3,
|
||||
extractEmbedded: false,
|
||||
},
|
||||
newVersionCheck: {
|
||||
enabled: true,
|
||||
},
|
||||
trash: {
|
||||
enabled: true,
|
||||
days: 30,
|
||||
},
|
||||
theme: {
|
||||
customCss: '',
|
||||
},
|
||||
library: {
|
||||
scan: {
|
||||
enabled: true,
|
||||
cronExpression: CronExpression.EVERY_DAY_AT_MIDNIGHT,
|
||||
},
|
||||
watch: {
|
||||
enabled: false,
|
||||
},
|
||||
},
|
||||
server: {
|
||||
externalDomain: '',
|
||||
loginPageMessage: '',
|
||||
},
|
||||
notifications: {
|
||||
smtp: {
|
||||
enabled: false,
|
||||
from: '',
|
||||
replyTo: '',
|
||||
transport: {
|
||||
ignoreCert: false,
|
||||
host: '',
|
||||
port: 587,
|
||||
username: '',
|
||||
password: '',
|
||||
},
|
||||
},
|
||||
},
|
||||
user: {
|
||||
deleteDelay: 7,
|
||||
},
|
||||
});
|
||||
|
||||
export enum FeatureFlag {
|
||||
SMART_SEARCH = 'smartSearch',
|
||||
FACIAL_RECOGNITION = 'facialRecognition',
|
||||
MAP = 'map',
|
||||
REVERSE_GEOCODING = 'reverseGeocoding',
|
||||
SIDECAR = 'sidecar',
|
||||
SEARCH = 'search',
|
||||
OAUTH = 'oauth',
|
||||
OAUTH_AUTO_LAUNCH = 'oauthAutoLaunch',
|
||||
PASSWORD_LOGIN = 'passwordLogin',
|
||||
CONFIG_FILE = 'configFile',
|
||||
TRASH = 'trash',
|
||||
EMAIL = 'email',
|
||||
}
|
||||
|
||||
export type FeatureFlags = Record<FeatureFlag, boolean>;
|
||||
|
||||
let instance: SystemConfigCore | null;
|
||||
|
||||
@Injectable()
|
||||
export class SystemConfigCore {
|
||||
private configCache: SystemConfigEntity<SystemConfigValue>[] | null = null;
|
||||
private readonly asyncLock = new AsyncLock();
|
||||
private config: SystemConfig | null = null;
|
||||
private lastUpdated: number | null = null;
|
||||
|
||||
public config$ = new Subject<SystemConfig>();
|
||||
config$ = new Subject<SystemConfig>();
|
||||
|
||||
private constructor(
|
||||
private repository: ISystemConfigRepository,
|
||||
private repository: ISystemMetadataRepository,
|
||||
private logger: ILoggerRepository,
|
||||
) {}
|
||||
|
||||
static create(repository: ISystemConfigRepository, logger: ILoggerRepository) {
|
||||
static create(repository: ISystemMetadataRepository, logger: ILoggerRepository) {
|
||||
if (!instance) {
|
||||
instance = new SystemConfigCore(repository, logger);
|
||||
}
|
||||
@@ -206,182 +42,103 @@ export class SystemConfigCore {
|
||||
instance = null;
|
||||
}
|
||||
|
||||
async requireFeature(feature: FeatureFlag) {
|
||||
const hasFeature = await this.hasFeature(feature);
|
||||
if (!hasFeature) {
|
||||
switch (feature) {
|
||||
case FeatureFlag.SMART_SEARCH: {
|
||||
throw new BadRequestException('Smart search is not enabled');
|
||||
}
|
||||
case FeatureFlag.FACIAL_RECOGNITION: {
|
||||
throw new BadRequestException('Facial recognition is not enabled');
|
||||
}
|
||||
case FeatureFlag.SIDECAR: {
|
||||
throw new BadRequestException('Sidecar is not enabled');
|
||||
}
|
||||
case FeatureFlag.SEARCH: {
|
||||
throw new BadRequestException('Search is not enabled');
|
||||
}
|
||||
case FeatureFlag.OAUTH: {
|
||||
throw new BadRequestException('OAuth is not enabled');
|
||||
}
|
||||
case FeatureFlag.PASSWORD_LOGIN: {
|
||||
throw new BadRequestException('Password login is not enabled');
|
||||
}
|
||||
case FeatureFlag.CONFIG_FILE: {
|
||||
throw new BadRequestException('Config file is not set');
|
||||
}
|
||||
default: {
|
||||
throw new ForbiddenException(`Missing required feature: ${feature}`);
|
||||
async getConfig(force = false): Promise<SystemConfig> {
|
||||
if (force || !this.config) {
|
||||
const lastUpdated = this.lastUpdated;
|
||||
await this.asyncLock.acquire(DatabaseLock[DatabaseLock.GetSystemConfig], async () => {
|
||||
if (lastUpdated === this.lastUpdated) {
|
||||
this.config = await this.buildConfig();
|
||||
this.lastUpdated = Date.now();
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
return this.config!;
|
||||
}
|
||||
|
||||
async updateConfig(newConfig: SystemConfig): Promise<SystemConfig> {
|
||||
// get the difference between the new config and the default config
|
||||
const partialConfig: DeepPartial<SystemConfig> = {};
|
||||
for (const property of getKeysDeep(defaults)) {
|
||||
const newValue = _.get(newConfig, property);
|
||||
const isEmpty = newValue === undefined || newValue === null || newValue === '';
|
||||
const defaultValue = _.get(defaults, property);
|
||||
const isEqual = newValue === defaultValue || _.isEqual(newValue, defaultValue);
|
||||
|
||||
if (isEmpty || isEqual) {
|
||||
continue;
|
||||
}
|
||||
|
||||
_.set(partialConfig, property, newValue);
|
||||
}
|
||||
|
||||
await this.repository.set(SystemMetadataKey.SYSTEM_CONFIG, partialConfig);
|
||||
|
||||
const config = await this.getConfig(true);
|
||||
this.config$.next(config);
|
||||
return config;
|
||||
}
|
||||
|
||||
async hasFeature(feature: FeatureFlag) {
|
||||
const features = await this.getFeatures();
|
||||
return features[feature] ?? false;
|
||||
async refreshConfig() {
|
||||
const newConfig = await this.getConfig(true);
|
||||
this.config$.next(newConfig);
|
||||
}
|
||||
|
||||
async getFeatures(): Promise<FeatureFlags> {
|
||||
const config = await this.getConfig();
|
||||
const mlEnabled = config.machineLearning.enabled;
|
||||
|
||||
return {
|
||||
[FeatureFlag.SMART_SEARCH]: mlEnabled && config.machineLearning.clip.enabled,
|
||||
[FeatureFlag.FACIAL_RECOGNITION]: mlEnabled && config.machineLearning.facialRecognition.enabled,
|
||||
[FeatureFlag.MAP]: config.map.enabled,
|
||||
[FeatureFlag.REVERSE_GEOCODING]: config.reverseGeocoding.enabled,
|
||||
[FeatureFlag.SIDECAR]: true,
|
||||
[FeatureFlag.SEARCH]: true,
|
||||
[FeatureFlag.TRASH]: config.trash.enabled,
|
||||
[FeatureFlag.OAUTH]: config.oauth.enabled,
|
||||
[FeatureFlag.OAUTH_AUTO_LAUNCH]: config.oauth.autoLaunch,
|
||||
[FeatureFlag.PASSWORD_LOGIN]: config.passwordLogin.enabled,
|
||||
[FeatureFlag.CONFIG_FILE]: !!process.env.IMMICH_CONFIG_FILE,
|
||||
[FeatureFlag.EMAIL]: config.notifications.smtp.enabled,
|
||||
};
|
||||
isUsingConfigFile() {
|
||||
return !!process.env.IMMICH_CONFIG_FILE;
|
||||
}
|
||||
|
||||
public getDefaults(): SystemConfig {
|
||||
return defaults;
|
||||
}
|
||||
private async buildConfig() {
|
||||
// load partial
|
||||
const partial = this.isUsingConfigFile()
|
||||
? await this.loadFromFile(process.env.IMMICH_CONFIG_FILE as string)
|
||||
: await this.repository.get(SystemMetadataKey.SYSTEM_CONFIG);
|
||||
|
||||
public async getConfig(force = false): Promise<SystemConfig> {
|
||||
const configFilePath = process.env.IMMICH_CONFIG_FILE;
|
||||
// merge with defaults
|
||||
const config = _.cloneDeep(defaults);
|
||||
const overrides = configFilePath ? await this.loadFromFile(configFilePath, force) : await this.repository.load();
|
||||
|
||||
for (const { key, value } of overrides) {
|
||||
// set via dot notation
|
||||
_.set(config, key, value);
|
||||
for (const property of getKeysDeep(partial)) {
|
||||
_.set(config, property, _.get(partial, property));
|
||||
}
|
||||
|
||||
// check for extra properties
|
||||
const unknownKeys = _.cloneDeep(config);
|
||||
for (const property of getKeysDeep(defaults)) {
|
||||
unsetDeep(unknownKeys, property);
|
||||
}
|
||||
|
||||
if (!_.isEmpty(unknownKeys)) {
|
||||
this.logger.warn(`Unknown keys found: ${JSON.stringify(unknownKeys, null, 2)}`);
|
||||
}
|
||||
|
||||
// validate full config
|
||||
const errors = await validate(plainToInstance(SystemConfigDto, config));
|
||||
if (errors.length > 0) {
|
||||
this.logger.error('Validation error', errors);
|
||||
if (configFilePath) {
|
||||
if (this.isUsingConfigFile()) {
|
||||
throw new Error(`Invalid value(s) in file: ${errors}`);
|
||||
} else {
|
||||
this.logger.error('Validation error', errors);
|
||||
}
|
||||
}
|
||||
|
||||
if (!config.ffmpeg.acceptedVideoCodecs.includes(config.ffmpeg.targetVideoCodec)) {
|
||||
config.ffmpeg.acceptedVideoCodecs.unshift(config.ffmpeg.targetVideoCodec);
|
||||
config.ffmpeg.acceptedVideoCodecs.push(config.ffmpeg.targetVideoCodec);
|
||||
}
|
||||
|
||||
if (!config.ffmpeg.acceptedAudioCodecs.includes(config.ffmpeg.targetAudioCodec)) {
|
||||
config.ffmpeg.acceptedAudioCodecs.unshift(config.ffmpeg.targetAudioCodec);
|
||||
config.ffmpeg.acceptedAudioCodecs.push(config.ffmpeg.targetAudioCodec);
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public async updateConfig(newConfig: SystemConfig): Promise<SystemConfig> {
|
||||
if (await this.hasFeature(FeatureFlag.CONFIG_FILE)) {
|
||||
throw new BadRequestException('Cannot update configuration while IMMICH_CONFIG_FILE is in use');
|
||||
}
|
||||
|
||||
const updates: SystemConfigEntity[] = [];
|
||||
const deletes: SystemConfigEntity[] = [];
|
||||
|
||||
for (const key of Object.values(SystemConfigKey)) {
|
||||
// get via dot notation
|
||||
const item = { key, value: _.get(newConfig, key) as SystemConfigValue };
|
||||
const defaultValue = _.get(defaults, key);
|
||||
const isMissing = !_.has(newConfig, key);
|
||||
|
||||
if (
|
||||
isMissing ||
|
||||
item.value === null ||
|
||||
item.value === '' ||
|
||||
item.value === defaultValue ||
|
||||
_.isEqual(item.value, defaultValue)
|
||||
) {
|
||||
deletes.push(item);
|
||||
continue;
|
||||
}
|
||||
|
||||
updates.push(item);
|
||||
}
|
||||
|
||||
if (updates.length > 0) {
|
||||
await this.repository.saveAll(updates);
|
||||
}
|
||||
|
||||
if (deletes.length > 0) {
|
||||
await this.repository.deleteKeys(deletes.map((item) => item.key));
|
||||
}
|
||||
|
||||
const config = await this.getConfig();
|
||||
|
||||
this.config$.next(config);
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
public async refreshConfig() {
|
||||
const newConfig = await this.getConfig(true);
|
||||
|
||||
this.config$.next(newConfig);
|
||||
}
|
||||
|
||||
private async loadFromFile(filepath: string, force = false) {
|
||||
if (force || !this.configCache) {
|
||||
try {
|
||||
const file = await this.repository.readFile(filepath);
|
||||
const config = loadYaml(file.toString()) as any;
|
||||
const overrides: SystemConfigEntity<SystemConfigValue>[] = [];
|
||||
|
||||
for (const key of Object.values(SystemConfigKey)) {
|
||||
const value = _.get(config, key);
|
||||
this.unsetDeep(config, key);
|
||||
if (value !== undefined) {
|
||||
overrides.push({ key, value });
|
||||
}
|
||||
}
|
||||
|
||||
if (!_.isEmpty(config)) {
|
||||
this.logger.warn(`Unknown keys found: ${JSON.stringify(config, null, 2)}`);
|
||||
}
|
||||
|
||||
this.configCache = overrides;
|
||||
} catch (error: Error | any) {
|
||||
this.logger.error(`Unable to load configuration file: ${filepath}`);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
return this.configCache;
|
||||
}
|
||||
|
||||
private unsetDeep(object: object, key: string) {
|
||||
_.unset(object, key);
|
||||
const path = key.split('.');
|
||||
while (path.pop()) {
|
||||
if (!_.isEmpty(_.get(object, path))) {
|
||||
return;
|
||||
}
|
||||
_.unset(object, path);
|
||||
private async loadFromFile(filepath: string) {
|
||||
try {
|
||||
const file = await this.repository.readFile(filepath);
|
||||
return loadYaml(file.toString()) as unknown;
|
||||
} catch (error: Error | any) {
|
||||
this.logger.error(`Unable to load configuration file: ${filepath}`);
|
||||
this.logger.error(error);
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,10 +2,8 @@ import { BadRequestException, ForbiddenException } from '@nestjs/common';
|
||||
import sanitize from 'sanitize-filename';
|
||||
import { SALT_ROUNDS } from 'src/constants';
|
||||
import { UserResponseDto } from 'src/dtos/user.dto';
|
||||
import { LibraryType } from 'src/entities/library.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { ICryptoRepository } from 'src/interfaces/crypto.interface';
|
||||
import { ILibraryRepository } from 'src/interfaces/library.interface';
|
||||
import { IUserRepository } from 'src/interfaces/user.interface';
|
||||
|
||||
let instance: UserCore | null;
|
||||
@@ -13,17 +11,12 @@ let instance: UserCore | null;
|
||||
export class UserCore {
|
||||
private constructor(
|
||||
private cryptoRepository: ICryptoRepository,
|
||||
private libraryRepository: ILibraryRepository,
|
||||
private userRepository: IUserRepository,
|
||||
) {}
|
||||
|
||||
static create(
|
||||
cryptoRepository: ICryptoRepository,
|
||||
libraryRepository: ILibraryRepository,
|
||||
userRepository: IUserRepository,
|
||||
) {
|
||||
static create(cryptoRepository: ICryptoRepository, userRepository: IUserRepository) {
|
||||
if (!instance) {
|
||||
instance = new UserCore(cryptoRepository, libraryRepository, userRepository);
|
||||
instance = new UserCore(cryptoRepository, userRepository);
|
||||
}
|
||||
|
||||
return instance;
|
||||
@@ -70,7 +63,7 @@ export class UserCore {
|
||||
dto.storageLabel = null;
|
||||
}
|
||||
|
||||
return this.userRepository.update(id, dto);
|
||||
return this.userRepository.update(id, { ...dto, updatedAt: new Date() });
|
||||
}
|
||||
|
||||
async createUser(dto: Partial<UserEntity> & { email: string }): Promise<UserEntity> {
|
||||
@@ -93,17 +86,7 @@ export class UserCore {
|
||||
if (payload.storageLabel) {
|
||||
payload.storageLabel = sanitize(payload.storageLabel.replaceAll('.', ''));
|
||||
}
|
||||
const userEntity = await this.userRepository.create(payload);
|
||||
await this.libraryRepository.create({
|
||||
owner: { id: userEntity.id } as UserEntity,
|
||||
name: 'Default Library',
|
||||
assets: [],
|
||||
type: LibraryType.UPLOAD,
|
||||
importPaths: [],
|
||||
exclusionPatterns: [],
|
||||
isVisible: true,
|
||||
});
|
||||
|
||||
return userEntity;
|
||||
return this.userRepository.create(payload);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -33,5 +33,5 @@ export const databaseConfig: PostgresConnectionOptions = {
|
||||
*/
|
||||
export const dataSource = new DataSource({ ...databaseConfig, host: 'localhost' });
|
||||
|
||||
export const vectorExt =
|
||||
export const getVectorExtension = () =>
|
||||
process.env.DB_VECTOR_EXTENSION === 'pgvector' ? DatabaseExtension.VECTOR : DatabaseExtension.VECTORS;
|
||||
|
||||
@@ -2,7 +2,6 @@ import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ArrayNotEmpty, IsArray, IsEnum, IsString, ValidateNested } from 'class-validator';
|
||||
import _ from 'lodash';
|
||||
import { PropertyLifecycle } from 'src/decorators';
|
||||
import { AssetResponseDto, mapAsset } from 'src/dtos/asset-response.dto';
|
||||
import { AuthDto } from 'src/dtos/auth.dto';
|
||||
import { UserResponseDto, mapUser } from 'src/dtos/user.dto';
|
||||
@@ -25,10 +24,6 @@ export class AlbumUserAddDto {
|
||||
}
|
||||
|
||||
export class AddUsersDto {
|
||||
@ValidateUUID({ each: true, optional: true })
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.102.0' })
|
||||
sharedUserIds?: string[];
|
||||
|
||||
@ArrayNotEmpty()
|
||||
albumUsers!: AlbumUserAddDto[];
|
||||
}
|
||||
@@ -55,13 +50,8 @@ export class CreateAlbumDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AlbumUserCreateDto)
|
||||
@PropertyLifecycle({ addedAt: 'v1.104.0' })
|
||||
albumUsers?: AlbumUserCreateDto[];
|
||||
|
||||
@ValidateUUID({ optional: true, each: true })
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.104.0' })
|
||||
sharedWithUserIds?: string[];
|
||||
|
||||
@ValidateUUID({ optional: true, each: true })
|
||||
assetIds?: string[];
|
||||
}
|
||||
@@ -137,8 +127,6 @@ export class AlbumResponseDto {
|
||||
updatedAt!: Date;
|
||||
albumThumbnailAssetId!: string | null;
|
||||
shared!: boolean;
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.102.0' })
|
||||
sharedUsers!: UserResponseDto[];
|
||||
albumUsers!: AlbumUserResponseDto[];
|
||||
hasSharedLink!: boolean;
|
||||
assets!: AssetResponseDto[];
|
||||
@@ -192,7 +180,6 @@ export const mapAlbum = (entity: AlbumEntity, withAssets: boolean, auth?: AuthDt
|
||||
id: entity.id,
|
||||
ownerId: entity.ownerId,
|
||||
owner: mapUser(entity.owner),
|
||||
sharedUsers,
|
||||
albumUsers: albumUsersSorted,
|
||||
shared: hasSharedUser || hasSharedLink,
|
||||
hasSharedLink,
|
||||
|
||||
@@ -0,0 +1,36 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
|
||||
export enum AssetMediaStatusEnum {
|
||||
REPLACED = 'replaced',
|
||||
DUPLICATE = 'duplicate',
|
||||
}
|
||||
export class AssetMediaResponseDto {
|
||||
@ApiProperty({ enum: AssetMediaStatusEnum, enumName: 'AssetMediaStatus' })
|
||||
status!: AssetMediaStatusEnum;
|
||||
id!: string;
|
||||
}
|
||||
|
||||
export enum AssetUploadAction {
|
||||
ACCEPT = 'accept',
|
||||
REJECT = 'reject',
|
||||
}
|
||||
|
||||
export enum AssetRejectReason {
|
||||
DUPLICATE = 'duplicate',
|
||||
UNSUPPORTED_FORMAT = 'unsupported-format',
|
||||
}
|
||||
|
||||
export class AssetBulkUploadCheckResult {
|
||||
id!: string;
|
||||
action!: AssetUploadAction;
|
||||
reason?: AssetRejectReason;
|
||||
assetId?: string;
|
||||
}
|
||||
|
||||
export class AssetBulkUploadCheckResponseDto {
|
||||
results!: AssetBulkUploadCheckResult[];
|
||||
}
|
||||
|
||||
export class CheckExistingAssetsResponseDto {
|
||||
existingIds!: string[];
|
||||
}
|
||||
@@ -0,0 +1,64 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ArrayNotEmpty, IsArray, IsNotEmpty, IsString, ValidateNested } from 'class-validator';
|
||||
import { Optional, ValidateDate } from 'src/validation';
|
||||
|
||||
export enum UploadFieldName {
|
||||
ASSET_DATA = 'assetData',
|
||||
LIVE_PHOTO_DATA = 'livePhotoData',
|
||||
SIDECAR_DATA = 'sidecarData',
|
||||
PROFILE_DATA = 'file',
|
||||
}
|
||||
|
||||
export class AssetMediaReplaceDto {
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
deviceAssetId!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
deviceId!: string;
|
||||
|
||||
@ValidateDate()
|
||||
fileCreatedAt!: Date;
|
||||
|
||||
@ValidateDate()
|
||||
fileModifiedAt!: Date;
|
||||
|
||||
@Optional()
|
||||
@IsString()
|
||||
duration?: string;
|
||||
|
||||
// The properties below are added to correctly generate the API docs
|
||||
// and client SDKs. Validation should be handled in the controller.
|
||||
@ApiProperty({ type: 'string', format: 'binary' })
|
||||
[UploadFieldName.ASSET_DATA]!: any;
|
||||
}
|
||||
|
||||
export class AssetBulkUploadCheckItem {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
id!: string;
|
||||
|
||||
/** base64 or hex encoded sha1 hash */
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
checksum!: string;
|
||||
}
|
||||
|
||||
export class AssetBulkUploadCheckDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AssetBulkUploadCheckItem)
|
||||
assets!: AssetBulkUploadCheckItem[];
|
||||
}
|
||||
|
||||
export class CheckExistingAssetsDto {
|
||||
@ArrayNotEmpty()
|
||||
@IsString({ each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
deviceAssetIds!: string[];
|
||||
|
||||
@IsNotEmpty()
|
||||
deviceId!: string;
|
||||
}
|
||||
@@ -31,7 +31,8 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
||||
deviceId!: string;
|
||||
ownerId!: string;
|
||||
owner?: UserResponseDto;
|
||||
libraryId!: string;
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.106.0' })
|
||||
libraryId?: string | null;
|
||||
originalPath!: string;
|
||||
originalFileName!: string;
|
||||
fileCreatedAt!: Date;
|
||||
@@ -41,10 +42,6 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
||||
isArchived!: boolean;
|
||||
isTrashed!: boolean;
|
||||
isOffline!: boolean;
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.104.0' })
|
||||
isExternal?: boolean;
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.104.0' })
|
||||
isReadOnly?: boolean;
|
||||
exifInfo?: ExifResponseDto;
|
||||
smartInfo?: SmartInfoResponseDto;
|
||||
tags?: TagResponseDto[];
|
||||
@@ -55,6 +52,7 @@ export class AssetResponseDto extends SanitizedAssetResponseDto {
|
||||
stack?: AssetResponseDto[];
|
||||
@ApiProperty({ type: 'integer' })
|
||||
stackCount!: number | null;
|
||||
duplicateId?: string | null;
|
||||
}
|
||||
|
||||
export type AssetMapOptions = {
|
||||
@@ -132,16 +130,12 @@ export function mapAsset(entity: AssetEntity, options: AssetMapOptions = {}): As
|
||||
: undefined,
|
||||
stackCount: entity.stack?.assets?.length ?? null,
|
||||
isOffline: entity.isOffline,
|
||||
isExternal: false,
|
||||
isReadOnly: false,
|
||||
hasMetadata: true,
|
||||
duplicateId: entity.duplicateId,
|
||||
};
|
||||
}
|
||||
|
||||
export class MemoryLaneResponseDto {
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.100.0' })
|
||||
title!: string;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
yearsAgo!: number;
|
||||
|
||||
|
||||
@@ -1,29 +1,4 @@
|
||||
export class AssetBulkUploadCheckResult {
|
||||
id!: string;
|
||||
action!: AssetUploadAction;
|
||||
reason?: AssetRejectReason;
|
||||
assetId?: string;
|
||||
}
|
||||
|
||||
export class AssetBulkUploadCheckResponseDto {
|
||||
results!: AssetBulkUploadCheckResult[];
|
||||
}
|
||||
|
||||
export enum AssetUploadAction {
|
||||
ACCEPT = 'accept',
|
||||
REJECT = 'reject',
|
||||
}
|
||||
|
||||
export enum AssetRejectReason {
|
||||
DUPLICATE = 'duplicate',
|
||||
UNSUPPORTED_FORMAT = 'unsupported-format',
|
||||
}
|
||||
|
||||
export class AssetFileUploadResponseDto {
|
||||
id!: string;
|
||||
duplicate!: boolean;
|
||||
}
|
||||
|
||||
export class CheckExistingAssetsResponseDto {
|
||||
existingIds!: string[];
|
||||
}
|
||||
|
||||
@@ -1,72 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { ArrayNotEmpty, IsArray, IsEnum, IsInt, IsNotEmpty, IsString, IsUUID, ValidateNested } from 'class-validator';
|
||||
import { IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { UploadFieldName } from 'src/dtos/asset.dto';
|
||||
import { Optional, ValidateBoolean, ValidateDate, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class AssetBulkUploadCheckItem {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
id!: string;
|
||||
|
||||
/** base64 or hex encoded sha1 hash */
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
checksum!: string;
|
||||
}
|
||||
|
||||
export class AssetBulkUploadCheckDto {
|
||||
@IsArray()
|
||||
@ValidateNested({ each: true })
|
||||
@Type(() => AssetBulkUploadCheckItem)
|
||||
assets!: AssetBulkUploadCheckItem[];
|
||||
}
|
||||
|
||||
export class AssetSearchDto {
|
||||
@ValidateBoolean({ optional: true })
|
||||
isFavorite?: boolean;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
isArchived?: boolean;
|
||||
|
||||
@Optional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
@ApiProperty({ type: 'integer' })
|
||||
skip?: number;
|
||||
|
||||
@Optional()
|
||||
@IsInt()
|
||||
@Type(() => Number)
|
||||
@ApiProperty({ type: 'integer' })
|
||||
take?: number;
|
||||
|
||||
@Optional()
|
||||
@IsUUID('4')
|
||||
@ApiProperty({ format: 'uuid' })
|
||||
userId?: string;
|
||||
|
||||
@ValidateDate({ optional: true })
|
||||
updatedAfter?: Date;
|
||||
|
||||
@ValidateDate({ optional: true })
|
||||
updatedBefore?: Date;
|
||||
}
|
||||
|
||||
export class CheckExistingAssetsDto {
|
||||
@ArrayNotEmpty()
|
||||
@IsString({ each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
deviceAssetIds!: string[];
|
||||
|
||||
@IsNotEmpty()
|
||||
deviceId!: string;
|
||||
}
|
||||
import { Optional, ValidateBoolean, ValidateDate } from 'src/validation';
|
||||
|
||||
export class CreateAssetDto {
|
||||
@ValidateUUID({ optional: true })
|
||||
libraryId?: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
@IsString()
|
||||
deviceAssetId!: string;
|
||||
|
||||
@@ -57,6 +57,9 @@ export class AssetBulkUpdateDto extends UpdateAssetBase {
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
removeParent?: boolean;
|
||||
|
||||
@Optional()
|
||||
duplicateId?: string | null;
|
||||
}
|
||||
|
||||
export class UpdateAssetDto extends UpdateAssetBase {
|
||||
|
||||
@@ -0,0 +1,27 @@
|
||||
import { IsNotEmpty } from 'class-validator';
|
||||
import { groupBy } from 'lodash';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { ValidateUUID } from 'src/validation';
|
||||
|
||||
export class DuplicateResponseDto {
|
||||
duplicateId!: string;
|
||||
assets!: AssetResponseDto[];
|
||||
}
|
||||
|
||||
export class ResolveDuplicatesDto {
|
||||
@IsNotEmpty()
|
||||
@ValidateUUID({ each: true })
|
||||
assetIds!: string[];
|
||||
}
|
||||
|
||||
export function mapDuplicateResponse(assets: AssetResponseDto[]): DuplicateResponseDto[] {
|
||||
const result = [];
|
||||
|
||||
const grouped = groupBy(assets, (a) => a.duplicateId);
|
||||
|
||||
for (const [duplicateId, assets] of Object.entries(grouped)) {
|
||||
result.push({ duplicateId, assets });
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
@@ -73,6 +73,9 @@ export class AllJobStatusResponseDto implements Record<QueueName, JobStatusDto>
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.SEARCH]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.DUPLICATE_DETECTION]!: JobStatusDto;
|
||||
|
||||
@ApiProperty({ type: JobStatusDto })
|
||||
[QueueName.FACE_DETECTION]!: JobStatusDto;
|
||||
|
||||
|
||||
@@ -1,13 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { ArrayMaxSize, ArrayUnique, IsEnum, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { LibraryEntity, LibraryType } from 'src/entities/library.entity';
|
||||
import { ArrayMaxSize, ArrayUnique, IsNotEmpty, IsString } from 'class-validator';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
import { Optional, ValidateBoolean, ValidateUUID } from 'src/validation';
|
||||
|
||||
export class CreateLibraryDto {
|
||||
@IsEnum(LibraryType)
|
||||
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
|
||||
type!: LibraryType;
|
||||
|
||||
@ValidateUUID()
|
||||
ownerId!: string;
|
||||
|
||||
@@ -16,9 +12,6 @@ export class CreateLibraryDto {
|
||||
@IsNotEmpty()
|
||||
name?: string;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
isVisible?: boolean;
|
||||
|
||||
@Optional()
|
||||
@IsString({ each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
@@ -40,9 +33,6 @@ export class UpdateLibraryDto {
|
||||
@IsNotEmpty()
|
||||
name?: string;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
isVisible?: boolean;
|
||||
|
||||
@Optional()
|
||||
@IsString({ each: true })
|
||||
@IsNotEmpty({ each: true })
|
||||
@@ -103,21 +93,11 @@ export class ScanLibraryDto {
|
||||
refreshAllFiles?: boolean;
|
||||
}
|
||||
|
||||
export class SearchLibraryDto {
|
||||
@IsEnum(LibraryType)
|
||||
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
|
||||
@Optional()
|
||||
type?: LibraryType;
|
||||
}
|
||||
|
||||
export class LibraryResponseDto {
|
||||
id!: string;
|
||||
ownerId!: string;
|
||||
name!: string;
|
||||
|
||||
@ApiProperty({ enumName: 'LibraryType', enum: LibraryType })
|
||||
type!: LibraryType;
|
||||
|
||||
@ApiProperty({ type: 'integer' })
|
||||
assetCount!: number;
|
||||
|
||||
@@ -152,7 +132,6 @@ export function mapLibrary(entity: LibraryEntity): LibraryResponseDto {
|
||||
return {
|
||||
id: entity.id,
|
||||
ownerId: entity.ownerId,
|
||||
type: entity.type,
|
||||
name: entity.name,
|
||||
createdAt: entity.createdAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
|
||||
@@ -4,10 +4,12 @@ import { IsEnum, IsNotEmpty, IsNumber, IsString, Max, Min } from 'class-validato
|
||||
import { CLIPMode, ModelType } from 'src/interfaces/machine-learning.interface';
|
||||
import { Optional, ValidateBoolean } from 'src/validation';
|
||||
|
||||
export class ModelConfig {
|
||||
export class TaskConfig {
|
||||
@ValidateBoolean()
|
||||
enabled!: boolean;
|
||||
}
|
||||
|
||||
export class ModelConfig extends TaskConfig {
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
modelName!: string;
|
||||
@@ -25,6 +27,15 @@ export class CLIPConfig extends ModelConfig {
|
||||
mode?: CLIPMode;
|
||||
}
|
||||
|
||||
export class DuplicateDetectionConfig extends TaskConfig {
|
||||
@IsNumber()
|
||||
@Min(0.001)
|
||||
@Max(0.1)
|
||||
@Type(() => Number)
|
||||
@ApiProperty({ type: 'number', format: 'float' })
|
||||
maxDistance!: number;
|
||||
}
|
||||
|
||||
export class RecognitionConfig extends ModelConfig {
|
||||
@IsNumber()
|
||||
@Min(0)
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Type } from 'class-transformer';
|
||||
import { IsEnum, IsInt, IsNotEmpty, IsString, Max, Min } from 'class-validator';
|
||||
import { PropertyLifecycle } from 'src/decorators';
|
||||
import { AlbumResponseDto } from 'src/dtos/album.dto';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { AssetOrder } from 'src/entities/album.entity';
|
||||
@@ -155,18 +154,6 @@ export class MetadataSearchDto extends BaseSearchDto {
|
||||
@Optional()
|
||||
originalPath?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.100.0' })
|
||||
resizePath?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
@PropertyLifecycle({ deprecatedAt: 'v1.100.0' })
|
||||
webpPath?: string;
|
||||
|
||||
@IsString()
|
||||
@IsNotEmpty()
|
||||
@Optional()
|
||||
@@ -317,6 +304,9 @@ export class MapMarkerDto {
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
withPartners?: boolean;
|
||||
|
||||
@ValidateBoolean({ optional: true })
|
||||
withSharedAlbums?: boolean;
|
||||
}
|
||||
|
||||
export class MemoryLaneDto {
|
||||
|
||||
@@ -1,15 +1,13 @@
|
||||
import { ApiProperty, ApiResponseProperty } from '@nestjs/swagger';
|
||||
import type { DateTime } from 'luxon';
|
||||
import { FeatureFlags } from 'src/cores/system-config.core';
|
||||
import { SemVer } from 'semver';
|
||||
import { SystemConfigThemeDto } from 'src/dtos/system-config.dto';
|
||||
import { IVersion, VersionType } from 'src/utils/version';
|
||||
|
||||
export class ServerPingResponse {
|
||||
@ApiResponseProperty({ type: String, example: 'pong' })
|
||||
res!: string;
|
||||
}
|
||||
|
||||
export class ServerInfoResponseDto {
|
||||
export class ServerStorageResponseDto {
|
||||
diskSize!: string;
|
||||
diskUse!: string;
|
||||
diskAvailable!: string;
|
||||
@@ -27,13 +25,17 @@ export class ServerInfoResponseDto {
|
||||
diskUsagePercentage!: number;
|
||||
}
|
||||
|
||||
export class ServerVersionResponseDto implements IVersion {
|
||||
export class ServerVersionResponseDto {
|
||||
@ApiProperty({ type: 'integer' })
|
||||
major!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
minor!: number;
|
||||
@ApiProperty({ type: 'integer' })
|
||||
patch!: number;
|
||||
|
||||
static fromSemVer(value: SemVer) {
|
||||
return { major: value.major, minor: value.minor, patch: value.patch };
|
||||
}
|
||||
}
|
||||
|
||||
export class UsageByUserDto {
|
||||
@@ -96,8 +98,9 @@ export class ServerConfigDto {
|
||||
externalDomain!: string;
|
||||
}
|
||||
|
||||
export class ServerFeaturesDto implements FeatureFlags {
|
||||
export class ServerFeaturesDto {
|
||||
smartSearch!: boolean;
|
||||
duplicateDetection!: boolean;
|
||||
configFile!: boolean;
|
||||
facialRecognition!: boolean;
|
||||
map!: boolean;
|
||||
@@ -112,8 +115,9 @@ export class ServerFeaturesDto implements FeatureFlags {
|
||||
}
|
||||
|
||||
export interface ReleaseNotification {
|
||||
isAvailable: VersionType;
|
||||
checkedAt: DateTime<boolean> | null;
|
||||
isAvailable: boolean;
|
||||
/** ISO8601 */
|
||||
checkedAt: string;
|
||||
serverVersion: ServerVersionResponseDto;
|
||||
releaseVersion: ServerVersionResponseDto;
|
||||
}
|
||||
|
||||
@@ -18,7 +18,6 @@ import {
|
||||
ValidatorConstraint,
|
||||
ValidatorConstraintInterface,
|
||||
} from 'class-validator';
|
||||
import { CLIPConfig, RecognitionConfig } from 'src/dtos/model-config.dto';
|
||||
import {
|
||||
AudioCodec,
|
||||
CQMode,
|
||||
@@ -30,7 +29,8 @@ import {
|
||||
TranscodeHWAccel,
|
||||
TranscodePolicy,
|
||||
VideoCodec,
|
||||
} from 'src/entities/system-config.entity';
|
||||
} from 'src/config';
|
||||
import { CLIPConfig, DuplicateDetectionConfig, RecognitionConfig } from 'src/dtos/model-config.dto';
|
||||
import { ConcurrentQueueName, QueueName } from 'src/interfaces/job.interface';
|
||||
import { ValidateBoolean, validateCronExpression } from 'src/validation';
|
||||
|
||||
@@ -132,6 +132,9 @@ export class SystemConfigFFmpegDto {
|
||||
@ApiProperty({ enumName: 'TranscodeHWAccel', enum: TranscodeHWAccel })
|
||||
accel!: TranscodeHWAccel;
|
||||
|
||||
@ValidateBoolean()
|
||||
accelDecode!: boolean;
|
||||
|
||||
@IsEnum(ToneMapping)
|
||||
@ApiProperty({ enumName: 'ToneMapping', enum: ToneMapping })
|
||||
tonemap!: ToneMapping;
|
||||
@@ -262,6 +265,11 @@ class SystemConfigMachineLearningDto {
|
||||
@IsObject()
|
||||
clip!: CLIPConfig;
|
||||
|
||||
@Type(() => DuplicateDetectionConfig)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
duplicateDetection!: DuplicateDetectionConfig;
|
||||
|
||||
@Type(() => RecognitionConfig)
|
||||
@ValidateNested()
|
||||
@IsObject()
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { UploadFieldName } from 'src/dtos/asset.dto';
|
||||
import { UserAvatarColor, UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export class CreateProfileImageDto {
|
||||
@ApiProperty({ type: 'string', format: 'binary' })
|
||||
@@ -18,11 +17,3 @@ export function mapCreateProfileImageResponse(userId: string, profileImagePath:
|
||||
profileImagePath: profileImagePath,
|
||||
};
|
||||
}
|
||||
|
||||
export const getRandomAvatarColor = (user: UserEntity): UserAvatarColor => {
|
||||
const values = Object.values(UserAvatarColor);
|
||||
const randomIndex = Math.floor(
|
||||
[...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
|
||||
);
|
||||
return values[randomIndex] as UserAvatarColor;
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validate } from 'class-validator';
|
||||
import { CreateAdminDto, CreateUserDto, CreateUserOAuthDto, UpdateUserDto } from 'src/dtos/user.dto';
|
||||
import { CreateUserDto, CreateUserOAuthDto, UpdateUserDto } from 'src/dtos/user.dto';
|
||||
|
||||
describe('update user DTO', () => {
|
||||
it('should allow emails without a tld', async () => {
|
||||
@@ -52,22 +52,6 @@ describe('create user DTO', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('create admin DTO', () => {
|
||||
it('should allow emails without a tld', async () => {
|
||||
const someEmail = 'test@test';
|
||||
|
||||
const dto = plainToInstance(CreateAdminDto, {
|
||||
isAdmin: true,
|
||||
email: someEmail,
|
||||
password: 'some password',
|
||||
name: 'some name',
|
||||
});
|
||||
const errors = await validate(dto);
|
||||
expect(errors).toHaveLength(0);
|
||||
expect(dto.email).toEqual(someEmail);
|
||||
});
|
||||
});
|
||||
|
||||
describe('create user oauth DTO', () => {
|
||||
it('should allow emails without a tld', async () => {
|
||||
const someEmail = 'test@test';
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { ApiProperty } from '@nestjs/swagger';
|
||||
import { Transform } from 'class-transformer';
|
||||
import { IsBoolean, IsEmail, IsEnum, IsNotEmpty, IsNumber, IsPositive, IsString, IsUUID } from 'class-validator';
|
||||
import { getRandomAvatarColor } from 'src/dtos/user-profile.dto';
|
||||
import { UserAvatarColor, UserEntity, UserStatus } from 'src/entities/user.entity';
|
||||
import { UserAvatarColor } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity, UserStatus } from 'src/entities/user.entity';
|
||||
import { getPreferences } from 'src/utils/preferences';
|
||||
import { Optional, ValidateBoolean, toEmail, toSanitized } from 'src/validation';
|
||||
|
||||
export class CreateUserDto {
|
||||
@@ -40,21 +41,6 @@ export class CreateUserDto {
|
||||
notify?: boolean;
|
||||
}
|
||||
|
||||
export class CreateAdminDto {
|
||||
@IsNotEmpty()
|
||||
isAdmin!: true;
|
||||
|
||||
@IsEmail({ require_tld: false })
|
||||
@Transform(({ value }) => value?.toLowerCase())
|
||||
email!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
password!: string;
|
||||
|
||||
@IsNotEmpty()
|
||||
name!: string;
|
||||
}
|
||||
|
||||
export class CreateUserOAuthDto {
|
||||
@IsEmail({ require_tld: false })
|
||||
@Transform(({ value }) => value?.toLowerCase())
|
||||
@@ -151,7 +137,7 @@ export const mapSimpleUser = (entity: UserEntity): UserDto => {
|
||||
email: entity.email,
|
||||
name: entity.name,
|
||||
profileImagePath: entity.profileImagePath,
|
||||
avatarColor: entity.avatarColor ?? getRandomAvatarColor(entity),
|
||||
avatarColor: getPreferences(entity).avatar.color,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -165,7 +151,7 @@ export function mapUser(entity: UserEntity): UserResponseDto {
|
||||
deletedAt: entity.deletedAt,
|
||||
updatedAt: entity.updatedAt,
|
||||
oauthId: entity.oauthId,
|
||||
memoriesEnabled: entity.memoriesEnabled,
|
||||
memoriesEnabled: getPreferences(entity).memories.enabled,
|
||||
quotaSizeInBytes: entity.quotaSizeInBytes,
|
||||
quotaUsageInBytes: entity.quotaUsageInBytes,
|
||||
status: entity.status,
|
||||
|
||||
@@ -105,8 +105,7 @@ export const WelcomeEmail = ({ baseUrl, displayName, username, password }: Welco
|
||||
</Link>
|
||||
<Link href="https://apps.apple.com/sg/app/immich/id1613945652">
|
||||
<Img
|
||||
// TODO get this as a png
|
||||
src={`https://immich.app/img/ios-app-store-badge.svg`}
|
||||
src={`https://immich.app/img/ios-app-store-badge.png`}
|
||||
alt="Immich"
|
||||
style={{ height: '68px', padding: '14px' }}
|
||||
/>
|
||||
@@ -133,7 +132,7 @@ export const WelcomeEmail = ({ baseUrl, displayName, username, password }: Welco
|
||||
WelcomeEmail.PreviewProps = {
|
||||
baseUrl: 'https://demo.immich.app/auth/login',
|
||||
displayName: 'Alan Turing',
|
||||
username: 'alanturing',
|
||||
username: 'alanturing@immich.app',
|
||||
password: 'mysuperpassword',
|
||||
} as WelcomeEmailProps;
|
||||
|
||||
|
||||
@@ -15,4 +15,7 @@ export class AssetJobStatusEntity {
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
metadataExtractedAt!: Date | null;
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
duplicatesDetectedAt!: Date | null;
|
||||
}
|
||||
|
||||
@@ -25,12 +25,17 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_library_checksum';
|
||||
export const ASSET_CHECKSUM_CONSTRAINT = 'UQ_assets_owner_checksum';
|
||||
|
||||
@Entity('assets')
|
||||
// Checksums must be unique per user and library
|
||||
@Index(ASSET_CHECKSUM_CONSTRAINT, ['owner', 'library', 'checksum'], {
|
||||
@Index(ASSET_CHECKSUM_CONSTRAINT, ['owner', 'checksum'], {
|
||||
unique: true,
|
||||
where: '"libraryId" IS NULL',
|
||||
})
|
||||
@Index('UQ_assets_owner_library_checksum' + '', ['owner', 'library', 'checksum'], {
|
||||
unique: true,
|
||||
where: '"libraryId" IS NOT NULL',
|
||||
})
|
||||
@Index('IDX_day_of_month', { synchronize: false })
|
||||
@Index('IDX_month', { synchronize: false })
|
||||
@@ -51,11 +56,11 @@ export class AssetEntity {
|
||||
@Column()
|
||||
ownerId!: string;
|
||||
|
||||
@ManyToOne(() => LibraryEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE', nullable: false })
|
||||
library!: LibraryEntity;
|
||||
@ManyToOne(() => LibraryEntity, { onDelete: 'CASCADE', onUpdate: 'CASCADE' })
|
||||
library?: LibraryEntity | null;
|
||||
|
||||
@Column()
|
||||
libraryId!: string;
|
||||
@Column({ nullable: true })
|
||||
libraryId?: string | null;
|
||||
|
||||
@Column()
|
||||
deviceId!: string;
|
||||
@@ -165,6 +170,10 @@ export class AssetEntity {
|
||||
|
||||
@OneToOne(() => AssetJobStatusEntity, (jobStatus) => jobStatus.asset, { nullable: true })
|
||||
jobStatus?: AssetJobStatusEntity;
|
||||
|
||||
@Index('IDX_assets_duplicateId')
|
||||
@Column({ type: 'uuid', nullable: true })
|
||||
duplicateId!: string | null;
|
||||
}
|
||||
|
||||
export enum AssetType {
|
||||
|
||||
@@ -98,20 +98,4 @@ export class ExifEntity {
|
||||
/* Video info */
|
||||
@Column({ type: 'float8', nullable: true })
|
||||
fps?: number | null;
|
||||
|
||||
@Index('exif_text_searchable', { synchronize: false })
|
||||
@Column({
|
||||
type: 'tsvector',
|
||||
generatedType: 'STORED',
|
||||
select: false,
|
||||
asExpression: `TO_TSVECTOR('english',
|
||||
COALESCE(make, '') || ' ' ||
|
||||
COALESCE(model, '') || ' ' ||
|
||||
COALESCE(orientation, '') || ' ' ||
|
||||
COALESCE("lensModel", '') || ' ' ||
|
||||
COALESCE("city", '') || ' ' ||
|
||||
COALESCE("state", '') || ' ' ||
|
||||
COALESCE("country", ''))`,
|
||||
})
|
||||
exifTextSearchableColumn!: string;
|
||||
}
|
||||
|
||||
@@ -18,9 +18,9 @@ import { SessionEntity } from 'src/entities/session.entity';
|
||||
import { SharedLinkEntity } from 'src/entities/shared-link.entity';
|
||||
import { SmartInfoEntity } from 'src/entities/smart-info.entity';
|
||||
import { SmartSearchEntity } from 'src/entities/smart-search.entity';
|
||||
import { SystemConfigEntity } from 'src/entities/system-config.entity';
|
||||
import { SystemMetadataEntity } from 'src/entities/system-metadata.entity';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export const entities = [
|
||||
@@ -42,10 +42,10 @@ export const entities = [
|
||||
SharedLinkEntity,
|
||||
SmartInfoEntity,
|
||||
SmartSearchEntity,
|
||||
SystemConfigEntity,
|
||||
SystemMetadataEntity,
|
||||
TagEntity,
|
||||
UserEntity,
|
||||
UserMetadataEntity,
|
||||
SessionEntity,
|
||||
LibraryEntity,
|
||||
];
|
||||
|
||||
@@ -30,9 +30,6 @@ export class LibraryEntity {
|
||||
@Column()
|
||||
ownerId!: string;
|
||||
|
||||
@Column()
|
||||
type!: LibraryType;
|
||||
|
||||
@Column('text', { array: true })
|
||||
importPaths!: string[];
|
||||
|
||||
@@ -50,12 +47,4 @@ export class LibraryEntity {
|
||||
|
||||
@Column({ type: 'timestamptz', nullable: true })
|
||||
refreshedAt!: Date | null;
|
||||
|
||||
@Column({ type: 'boolean', default: true })
|
||||
isVisible!: boolean;
|
||||
}
|
||||
|
||||
export enum LibraryType {
|
||||
UPLOAD = 'UPLOAD',
|
||||
EXTERNAL = 'EXTERNAL',
|
||||
}
|
||||
|
||||
@@ -11,10 +11,6 @@ export class SmartSearchEntity {
|
||||
assetId!: string;
|
||||
|
||||
@Index('clip_index', { synchronize: false })
|
||||
@Column({
|
||||
type: 'float4',
|
||||
array: true,
|
||||
select: false,
|
||||
})
|
||||
@Column({ type: 'float4', array: true, transformer: { from: (v) => JSON.parse(v), to: (v) => v } })
|
||||
embedding!: number[];
|
||||
}
|
||||
|
||||
@@ -1,339 +0,0 @@
|
||||
import { ConcurrentQueueName } from 'src/interfaces/job.interface';
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
export type SystemConfigValue = string | string[] | number | boolean;
|
||||
|
||||
// https://stackoverflow.com/a/47058976
|
||||
// https://stackoverflow.com/a/70692231
|
||||
type PathsToStringProps<T> = T extends SystemConfigValue
|
||||
? []
|
||||
: {
|
||||
[K in keyof T]: [K, ...PathsToStringProps<T[K]>];
|
||||
}[keyof T];
|
||||
|
||||
type Join<T extends string[], D extends string> = T extends []
|
||||
? never
|
||||
: T extends [infer F]
|
||||
? F
|
||||
: T extends [infer F, ...infer R]
|
||||
? F extends string
|
||||
? `${F}${D}${Join<Extract<R, string[]>, D>}`
|
||||
: never
|
||||
: string;
|
||||
|
||||
// dot notation matches path in `SystemConfig`
|
||||
// TODO: migrate to key value per section
|
||||
export const SystemConfigKey = {
|
||||
FFMPEG_CRF: 'ffmpeg.crf',
|
||||
FFMPEG_THREADS: 'ffmpeg.threads',
|
||||
FFMPEG_PRESET: 'ffmpeg.preset',
|
||||
FFMPEG_TARGET_VIDEO_CODEC: 'ffmpeg.targetVideoCodec',
|
||||
FFMPEG_ACCEPTED_VIDEO_CODECS: 'ffmpeg.acceptedVideoCodecs',
|
||||
FFMPEG_TARGET_AUDIO_CODEC: 'ffmpeg.targetAudioCodec',
|
||||
FFMPEG_ACCEPTED_AUDIO_CODECS: 'ffmpeg.acceptedAudioCodecs',
|
||||
FFMPEG_TARGET_RESOLUTION: 'ffmpeg.targetResolution',
|
||||
FFMPEG_MAX_BITRATE: 'ffmpeg.maxBitrate',
|
||||
FFMPEG_BFRAMES: 'ffmpeg.bframes',
|
||||
FFMPEG_REFS: 'ffmpeg.refs',
|
||||
FFMPEG_GOP_SIZE: 'ffmpeg.gopSize',
|
||||
FFMPEG_NPL: 'ffmpeg.npl',
|
||||
FFMPEG_TEMPORAL_AQ: 'ffmpeg.temporalAQ',
|
||||
FFMPEG_CQ_MODE: 'ffmpeg.cqMode',
|
||||
FFMPEG_TWO_PASS: 'ffmpeg.twoPass',
|
||||
FFMPEG_PREFERRED_HW_DEVICE: 'ffmpeg.preferredHwDevice',
|
||||
FFMPEG_TRANSCODE: 'ffmpeg.transcode',
|
||||
FFMPEG_ACCEL: 'ffmpeg.accel',
|
||||
FFMPEG_TONEMAP: 'ffmpeg.tonemap',
|
||||
|
||||
JOB_THUMBNAIL_GENERATION_CONCURRENCY: 'job.thumbnailGeneration.concurrency',
|
||||
JOB_METADATA_EXTRACTION_CONCURRENCY: 'job.metadataExtraction.concurrency',
|
||||
JOB_VIDEO_CONVERSION_CONCURRENCY: 'job.videoConversion.concurrency',
|
||||
JOB_FACE_DETECTION_CONCURRENCY: 'job.faceDetection.concurrency',
|
||||
JOB_CLIP_ENCODING_CONCURRENCY: 'job.smartSearch.concurrency',
|
||||
JOB_BACKGROUND_TASK_CONCURRENCY: 'job.backgroundTask.concurrency',
|
||||
JOB_SEARCH_CONCURRENCY: 'job.search.concurrency',
|
||||
JOB_SIDECAR_CONCURRENCY: 'job.sidecar.concurrency',
|
||||
JOB_LIBRARY_CONCURRENCY: 'job.library.concurrency',
|
||||
JOB_MIGRATION_CONCURRENCY: 'job.migration.concurrency',
|
||||
|
||||
LIBRARY_SCAN_ENABLED: 'library.scan.enabled',
|
||||
LIBRARY_SCAN_CRON_EXPRESSION: 'library.scan.cronExpression',
|
||||
|
||||
LIBRARY_WATCH_ENABLED: 'library.watch.enabled',
|
||||
|
||||
LOGGING_ENABLED: 'logging.enabled',
|
||||
LOGGING_LEVEL: 'logging.level',
|
||||
|
||||
MACHINE_LEARNING_ENABLED: 'machineLearning.enabled',
|
||||
MACHINE_LEARNING_URL: 'machineLearning.url',
|
||||
|
||||
MACHINE_LEARNING_CLIP_ENABLED: 'machineLearning.clip.enabled',
|
||||
MACHINE_LEARNING_CLIP_MODEL_NAME: 'machineLearning.clip.modelName',
|
||||
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_ENABLED: 'machineLearning.facialRecognition.enabled',
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MODEL_NAME: 'machineLearning.facialRecognition.modelName',
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_SCORE: 'machineLearning.facialRecognition.minScore',
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MAX_DISTANCE: 'machineLearning.facialRecognition.maxDistance',
|
||||
MACHINE_LEARNING_FACIAL_RECOGNITION_MIN_FACES: 'machineLearning.facialRecognition.minFaces',
|
||||
|
||||
MAP_ENABLED: 'map.enabled',
|
||||
MAP_LIGHT_STYLE: 'map.lightStyle',
|
||||
MAP_DARK_STYLE: 'map.darkStyle',
|
||||
|
||||
NOTIFICATIONS_SMTP_ENABLED: 'notifications.smtp.enabled',
|
||||
NOTIFICATIONS_SMTP_FROM: 'notifications.smtp.from',
|
||||
NOTIFICATIONS_SMTP_REPLY_TO: 'notifications.smtp.replyTo',
|
||||
NOTIFICATIONS_SMTP_TRANSPORT_IGNORE_CERT: 'notifications.smtp.transport.ignoreCert',
|
||||
NOTIFICATIONS_SMTP_TRANSPORT_HOST: 'notifications.smtp.transport.host',
|
||||
NOTIFICATIONS_SMTP_TRANSPORT_PORT: 'notifications.smtp.transport.port',
|
||||
NOTIFICATIONS_SMTP_TRANSPORT_USERNAME: 'notifications.smtp.transport.username',
|
||||
NOTIFICATIONS_SMTP_TRANSPORT_PASSWORD: 'notifications.smtp.transport.password',
|
||||
|
||||
REVERSE_GEOCODING_ENABLED: 'reverseGeocoding.enabled',
|
||||
|
||||
NEW_VERSION_CHECK_ENABLED: 'newVersionCheck.enabled',
|
||||
|
||||
OAUTH_AUTO_LAUNCH: 'oauth.autoLaunch',
|
||||
OAUTH_AUTO_REGISTER: 'oauth.autoRegister',
|
||||
OAUTH_BUTTON_TEXT: 'oauth.buttonText',
|
||||
OAUTH_CLIENT_ID: 'oauth.clientId',
|
||||
OAUTH_CLIENT_SECRET: 'oauth.clientSecret',
|
||||
OAUTH_DEFAULT_STORAGE_QUOTA: 'oauth.defaultStorageQuota',
|
||||
OAUTH_ENABLED: 'oauth.enabled',
|
||||
OAUTH_ISSUER_URL: 'oauth.issuerUrl',
|
||||
OAUTH_MOBILE_OVERRIDE_ENABLED: 'oauth.mobileOverrideEnabled',
|
||||
OAUTH_MOBILE_REDIRECT_URI: 'oauth.mobileRedirectUri',
|
||||
OAUTH_SCOPE: 'oauth.scope',
|
||||
OAUTH_SIGNING_ALGORITHM: 'oauth.signingAlgorithm',
|
||||
OAUTH_STORAGE_LABEL_CLAIM: 'oauth.storageLabelClaim',
|
||||
OAUTH_STORAGE_QUOTA_CLAIM: 'oauth.storageQuotaClaim',
|
||||
|
||||
PASSWORD_LOGIN_ENABLED: 'passwordLogin.enabled',
|
||||
|
||||
SERVER_EXTERNAL_DOMAIN: 'server.externalDomain',
|
||||
SERVER_LOGIN_PAGE_MESSAGE: 'server.loginPageMessage',
|
||||
|
||||
STORAGE_TEMPLATE_ENABLED: 'storageTemplate.enabled',
|
||||
STORAGE_TEMPLATE_HASH_VERIFICATION_ENABLED: 'storageTemplate.hashVerificationEnabled',
|
||||
STORAGE_TEMPLATE: 'storageTemplate.template',
|
||||
|
||||
IMAGE_THUMBNAIL_FORMAT: 'image.thumbnailFormat',
|
||||
IMAGE_THUMBNAIL_SIZE: 'image.thumbnailSize',
|
||||
IMAGE_PREVIEW_FORMAT: 'image.previewFormat',
|
||||
IMAGE_PREVIEW_SIZE: 'image.previewSize',
|
||||
IMAGE_QUALITY: 'image.quality',
|
||||
IMAGE_COLORSPACE: 'image.colorspace',
|
||||
IMAGE_EXTRACT_EMBEDDED: 'image.extractEmbedded',
|
||||
|
||||
TRASH_ENABLED: 'trash.enabled',
|
||||
TRASH_DAYS: 'trash.days',
|
||||
|
||||
THEME_CUSTOM_CSS: 'theme.customCss',
|
||||
|
||||
USER_DELETE_DELAY: 'user.deleteDelay',
|
||||
} as const satisfies Record<string, Join<PathsToStringProps<SystemConfig>, '.'>>;
|
||||
|
||||
export type SystemConfigKeyPaths = (typeof SystemConfigKey)[keyof typeof SystemConfigKey];
|
||||
|
||||
@Entity('system_config')
|
||||
export class SystemConfigEntity<T = SystemConfigValue> {
|
||||
@PrimaryColumn({ type: 'varchar' })
|
||||
key!: SystemConfigKeyPaths;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true, transformer: { to: JSON.stringify, from: JSON.parse } })
|
||||
value!: T;
|
||||
}
|
||||
|
||||
export enum TranscodePolicy {
|
||||
ALL = 'all',
|
||||
OPTIMAL = 'optimal',
|
||||
BITRATE = 'bitrate',
|
||||
REQUIRED = 'required',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum TranscodeTarget {
|
||||
NONE,
|
||||
AUDIO,
|
||||
VIDEO,
|
||||
ALL,
|
||||
}
|
||||
|
||||
export enum VideoCodec {
|
||||
H264 = 'h264',
|
||||
HEVC = 'hevc',
|
||||
VP9 = 'vp9',
|
||||
AV1 = 'av1',
|
||||
}
|
||||
|
||||
export enum AudioCodec {
|
||||
MP3 = 'mp3',
|
||||
AAC = 'aac',
|
||||
LIBOPUS = 'libopus',
|
||||
}
|
||||
|
||||
export enum TranscodeHWAccel {
|
||||
NVENC = 'nvenc',
|
||||
QSV = 'qsv',
|
||||
VAAPI = 'vaapi',
|
||||
RKMPP = 'rkmpp',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum ToneMapping {
|
||||
HABLE = 'hable',
|
||||
MOBIUS = 'mobius',
|
||||
REINHARD = 'reinhard',
|
||||
DISABLED = 'disabled',
|
||||
}
|
||||
|
||||
export enum CQMode {
|
||||
AUTO = 'auto',
|
||||
CQP = 'cqp',
|
||||
ICQ = 'icq',
|
||||
}
|
||||
|
||||
export enum Colorspace {
|
||||
SRGB = 'srgb',
|
||||
P3 = 'p3',
|
||||
}
|
||||
|
||||
export enum ImageFormat {
|
||||
JPEG = 'jpeg',
|
||||
WEBP = 'webp',
|
||||
}
|
||||
|
||||
export enum LogLevel {
|
||||
VERBOSE = 'verbose',
|
||||
DEBUG = 'debug',
|
||||
LOG = 'log',
|
||||
WARN = 'warn',
|
||||
ERROR = 'error',
|
||||
FATAL = 'fatal',
|
||||
}
|
||||
|
||||
export interface SystemConfig {
|
||||
ffmpeg: {
|
||||
crf: number;
|
||||
threads: number;
|
||||
preset: string;
|
||||
targetVideoCodec: VideoCodec;
|
||||
acceptedVideoCodecs: VideoCodec[];
|
||||
targetAudioCodec: AudioCodec;
|
||||
acceptedAudioCodecs: AudioCodec[];
|
||||
targetResolution: string;
|
||||
maxBitrate: string;
|
||||
bframes: number;
|
||||
refs: number;
|
||||
gopSize: number;
|
||||
npl: number;
|
||||
temporalAQ: boolean;
|
||||
cqMode: CQMode;
|
||||
twoPass: boolean;
|
||||
preferredHwDevice: string;
|
||||
transcode: TranscodePolicy;
|
||||
accel: TranscodeHWAccel;
|
||||
tonemap: ToneMapping;
|
||||
};
|
||||
job: Record<ConcurrentQueueName, { concurrency: number }>;
|
||||
logging: {
|
||||
enabled: boolean;
|
||||
level: LogLevel;
|
||||
};
|
||||
machineLearning: {
|
||||
enabled: boolean;
|
||||
url: string;
|
||||
clip: {
|
||||
enabled: boolean;
|
||||
modelName: string;
|
||||
};
|
||||
facialRecognition: {
|
||||
enabled: boolean;
|
||||
modelName: string;
|
||||
minScore: number;
|
||||
minFaces: number;
|
||||
maxDistance: number;
|
||||
};
|
||||
};
|
||||
map: {
|
||||
enabled: boolean;
|
||||
lightStyle: string;
|
||||
darkStyle: string;
|
||||
};
|
||||
reverseGeocoding: {
|
||||
enabled: boolean;
|
||||
};
|
||||
oauth: {
|
||||
autoLaunch: boolean;
|
||||
autoRegister: boolean;
|
||||
buttonText: string;
|
||||
clientId: string;
|
||||
clientSecret: string;
|
||||
defaultStorageQuota: number;
|
||||
enabled: boolean;
|
||||
issuerUrl: string;
|
||||
mobileOverrideEnabled: boolean;
|
||||
mobileRedirectUri: string;
|
||||
scope: string;
|
||||
signingAlgorithm: string;
|
||||
storageLabelClaim: string;
|
||||
storageQuotaClaim: string;
|
||||
};
|
||||
passwordLogin: {
|
||||
enabled: boolean;
|
||||
};
|
||||
storageTemplate: {
|
||||
enabled: boolean;
|
||||
hashVerificationEnabled: boolean;
|
||||
template: string;
|
||||
};
|
||||
image: {
|
||||
thumbnailFormat: ImageFormat;
|
||||
thumbnailSize: number;
|
||||
previewFormat: ImageFormat;
|
||||
previewSize: number;
|
||||
quality: number;
|
||||
colorspace: Colorspace;
|
||||
extractEmbedded: boolean;
|
||||
};
|
||||
newVersionCheck: {
|
||||
enabled: boolean;
|
||||
};
|
||||
trash: {
|
||||
enabled: boolean;
|
||||
days: number;
|
||||
};
|
||||
theme: {
|
||||
customCss: string;
|
||||
};
|
||||
library: {
|
||||
scan: {
|
||||
enabled: boolean;
|
||||
cronExpression: string;
|
||||
};
|
||||
watch: {
|
||||
enabled: boolean;
|
||||
};
|
||||
};
|
||||
notifications: {
|
||||
smtp: {
|
||||
enabled: boolean;
|
||||
from: string;
|
||||
replyTo: string;
|
||||
transport: {
|
||||
ignoreCert: boolean;
|
||||
host: string;
|
||||
port: number;
|
||||
username: string;
|
||||
password: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
server: {
|
||||
externalDomain: string;
|
||||
loginPageMessage: string;
|
||||
};
|
||||
user: {
|
||||
deleteDelay: number;
|
||||
};
|
||||
}
|
||||
@@ -1,20 +1,27 @@
|
||||
import { Column, Entity, PrimaryColumn } from 'typeorm';
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { Column, DeepPartial, Entity, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('system_metadata')
|
||||
export class SystemMetadataEntity {
|
||||
@PrimaryColumn()
|
||||
key!: string;
|
||||
export class SystemMetadataEntity<T extends keyof SystemMetadata = SystemMetadataKey> {
|
||||
@PrimaryColumn({ type: 'varchar' })
|
||||
key!: T;
|
||||
|
||||
@Column({ type: 'jsonb', default: '{}', transformer: { to: JSON.stringify, from: JSON.parse } })
|
||||
value!: { [key: string]: unknown };
|
||||
@Column({ type: 'jsonb' })
|
||||
value!: SystemMetadata[T];
|
||||
}
|
||||
|
||||
export enum SystemMetadataKey {
|
||||
REVERSE_GEOCODING_STATE = 'reverse-geocoding-state',
|
||||
ADMIN_ONBOARDING = 'admin-onboarding',
|
||||
SYSTEM_CONFIG = 'system-config',
|
||||
VERSION_CHECK_STATE = 'version-check-state',
|
||||
}
|
||||
|
||||
export interface SystemMetadata extends Record<SystemMetadataKey, { [key: string]: unknown }> {
|
||||
export type VersionCheckMetadata = { checkedAt: string; releaseVersion: string };
|
||||
|
||||
export interface SystemMetadata extends Record<SystemMetadataKey, Record<string, any>> {
|
||||
[SystemMetadataKey.REVERSE_GEOCODING_STATE]: { lastUpdate?: string; lastImportFileName?: string };
|
||||
[SystemMetadataKey.ADMIN_ONBOARDING]: { isOnboarded: boolean };
|
||||
[SystemMetadataKey.SYSTEM_CONFIG]: DeepPartial<SystemConfig>;
|
||||
[SystemMetadataKey.VERSION_CHECK_STATE]: VersionCheckMetadata;
|
||||
}
|
||||
|
||||
@@ -0,0 +1,63 @@
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
import { Column, DeepPartial, Entity, ManyToOne, PrimaryColumn } from 'typeorm';
|
||||
|
||||
@Entity('user_metadata')
|
||||
export class UserMetadataEntity<T extends keyof UserMetadata = UserMetadataKey> {
|
||||
@PrimaryColumn({ type: 'uuid' })
|
||||
userId!: string;
|
||||
|
||||
@ManyToOne(() => UserEntity, (user) => user.metadata, { onUpdate: 'CASCADE', onDelete: 'CASCADE' })
|
||||
user!: UserEntity;
|
||||
|
||||
@PrimaryColumn({ type: 'varchar' })
|
||||
key!: T;
|
||||
|
||||
@Column({ type: 'jsonb' })
|
||||
value!: UserMetadata[T];
|
||||
}
|
||||
|
||||
export enum UserAvatarColor {
|
||||
PRIMARY = 'primary',
|
||||
PINK = 'pink',
|
||||
RED = 'red',
|
||||
YELLOW = 'yellow',
|
||||
BLUE = 'blue',
|
||||
GREEN = 'green',
|
||||
PURPLE = 'purple',
|
||||
ORANGE = 'orange',
|
||||
GRAY = 'gray',
|
||||
AMBER = 'amber',
|
||||
}
|
||||
|
||||
export interface UserPreferences {
|
||||
memories: {
|
||||
enabled: boolean;
|
||||
};
|
||||
avatar: {
|
||||
color: UserAvatarColor;
|
||||
};
|
||||
}
|
||||
|
||||
export const getDefaultPreferences = (user: { email: string }): UserPreferences => {
|
||||
const values = Object.values(UserAvatarColor);
|
||||
const randomIndex = Math.floor(
|
||||
[...user.email].map((letter) => letter.codePointAt(0) ?? 0).reduce((a, b) => a + b, 0) % values.length,
|
||||
);
|
||||
|
||||
return {
|
||||
memories: {
|
||||
enabled: true,
|
||||
},
|
||||
avatar: {
|
||||
color: values[randomIndex],
|
||||
},
|
||||
};
|
||||
};
|
||||
|
||||
export enum UserMetadataKey {
|
||||
PREFERENCES = 'preferences',
|
||||
}
|
||||
|
||||
export interface UserMetadata extends Record<UserMetadataKey, Record<string, any>> {
|
||||
[UserMetadataKey.PREFERENCES]: DeepPartial<UserPreferences>;
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
import { TagEntity } from 'src/entities/tag.entity';
|
||||
import { UserMetadataEntity } from 'src/entities/user-metadata.entity';
|
||||
import {
|
||||
Column,
|
||||
CreateDateColumn,
|
||||
@@ -10,19 +11,6 @@ import {
|
||||
UpdateDateColumn,
|
||||
} from 'typeorm';
|
||||
|
||||
export enum UserAvatarColor {
|
||||
PRIMARY = 'primary',
|
||||
PINK = 'pink',
|
||||
RED = 'red',
|
||||
YELLOW = 'yellow',
|
||||
BLUE = 'blue',
|
||||
GREEN = 'green',
|
||||
PURPLE = 'purple',
|
||||
ORANGE = 'orange',
|
||||
GRAY = 'gray',
|
||||
AMBER = 'amber',
|
||||
}
|
||||
|
||||
export enum UserStatus {
|
||||
ACTIVE = 'active',
|
||||
REMOVING = 'removing',
|
||||
@@ -37,9 +25,6 @@ export class UserEntity {
|
||||
@Column({ default: '' })
|
||||
name!: string;
|
||||
|
||||
@Column({ type: 'varchar', nullable: true })
|
||||
avatarColor!: UserAvatarColor | null;
|
||||
|
||||
@Column({ default: false })
|
||||
isAdmin!: boolean;
|
||||
|
||||
@@ -73,9 +58,6 @@ export class UserEntity {
|
||||
@UpdateDateColumn({ type: 'timestamptz' })
|
||||
updatedAt!: Date;
|
||||
|
||||
@Column({ default: true })
|
||||
memoriesEnabled!: boolean;
|
||||
|
||||
@OneToMany(() => TagEntity, (tag) => tag.user)
|
||||
tags!: TagEntity[];
|
||||
|
||||
@@ -87,4 +69,7 @@ export class UserEntity {
|
||||
|
||||
@Column({ type: 'bigint', default: 0 })
|
||||
quotaUsageInBytes!: number;
|
||||
|
||||
@OneToMany(() => UserMetadataEntity, (metadata) => metadata.user)
|
||||
metadata!: UserMetadataEntity[];
|
||||
}
|
||||
|
||||
@@ -26,10 +26,6 @@ export interface IAccessRepository {
|
||||
checkSharedLinkAccess(sharedLinkId: string, albumIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
library: {
|
||||
checkOwnerAccess(userId: string, libraryIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
timeline: {
|
||||
checkPartnerAccess(userId: string, partnerIds: Set<string>): Promise<Set<string>>;
|
||||
};
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
import { AssetSearchDto, CheckExistingAssetsDto } from 'src/dtos/asset-v1.dto';
|
||||
import { AssetEntity } from 'src/entities/asset.entity';
|
||||
|
||||
export interface AssetCheck {
|
||||
@@ -12,10 +11,7 @@ export interface AssetOwnerCheck extends AssetCheck {
|
||||
|
||||
export interface IAssetRepositoryV1 {
|
||||
get(id: string): Promise<AssetEntity | null>;
|
||||
getAllByUserId(userId: string, dto: AssetSearchDto): Promise<AssetEntity[]>;
|
||||
getAssetsByChecksums(userId: string, checksums: Buffer[]): Promise<AssetCheck[]>;
|
||||
getExistingAssets(userId: string, checkDuplicateAssetDto: CheckExistingAssetsDto): Promise<string[]>;
|
||||
getByOriginalPath(originalPath: string): Promise<AssetOwnerCheck | null>;
|
||||
}
|
||||
|
||||
export const IAssetRepositoryV1 = 'IAssetRepositoryV1';
|
||||
|
||||
@@ -40,6 +40,7 @@ export enum WithoutProperty {
|
||||
ENCODED_VIDEO = 'encoded-video',
|
||||
EXIF = 'exif',
|
||||
SMART_SEARCH = 'smart-search',
|
||||
DUPLICATE = 'duplicate',
|
||||
OBJECT_TAGS = 'object-tags',
|
||||
FACES = 'faces',
|
||||
PERSON = 'person',
|
||||
@@ -60,6 +61,7 @@ export interface AssetBuilderOptions {
|
||||
isArchived?: boolean;
|
||||
isFavorite?: boolean;
|
||||
isTrashed?: boolean;
|
||||
isDuplicate?: boolean;
|
||||
albumId?: string;
|
||||
personId?: string;
|
||||
userIds?: string[];
|
||||
@@ -109,7 +111,10 @@ export type AssetWithoutRelations = Omit<
|
||||
| 'tags'
|
||||
>;
|
||||
|
||||
export type AssetUpdateOptions = Pick<AssetWithoutRelations, 'id'> & Partial<AssetWithoutRelations>;
|
||||
type AssetUpdateWithoutRelations = Pick<AssetWithoutRelations, 'id'> & Partial<AssetWithoutRelations>;
|
||||
type AssetUpdateWithLivePhotoRelation = Pick<AssetWithoutRelations, 'id'> & Pick<AssetEntity, 'livePhotoVideo'>;
|
||||
|
||||
export type AssetUpdateOptions = AssetUpdateWithoutRelations | AssetUpdateWithLivePhotoRelation;
|
||||
|
||||
export type AssetUpdateAllOptions = Omit<Partial<AssetWithoutRelations>, 'id'>;
|
||||
|
||||
@@ -134,8 +139,6 @@ export interface AssetFullSyncOptions {
|
||||
lastCreationDate?: Date;
|
||||
lastId?: string;
|
||||
updatedUntil: Date;
|
||||
isArchived?: false;
|
||||
withStacked?: true;
|
||||
limit: number;
|
||||
}
|
||||
|
||||
@@ -145,6 +148,12 @@ export interface AssetDeltaSyncOptions {
|
||||
limit: number;
|
||||
}
|
||||
|
||||
export interface AssetUpdateDuplicateOptions {
|
||||
targetDuplicateId: string | null;
|
||||
assetIds: string[];
|
||||
duplicateIds: string[];
|
||||
}
|
||||
|
||||
export type AssetPathEntity = Pick<AssetEntity, 'id' | 'originalPath' | 'isOffline'>;
|
||||
|
||||
export const IAssetRepository = 'IAssetRepository';
|
||||
@@ -158,9 +167,11 @@ export interface IAssetRepository {
|
||||
): Promise<AssetEntity[]>;
|
||||
getByIdsWithAllRelations(ids: string[]): Promise<AssetEntity[]>;
|
||||
getByDayOfYear(ownerIds: string[], monthDay: MonthDay): Promise<AssetEntity[]>;
|
||||
getByChecksum(libraryId: string, checksum: Buffer): Promise<AssetEntity | null>;
|
||||
getByChecksum(libraryId: string | null, checksum: Buffer): Promise<AssetEntity | null>;
|
||||
getByChecksums(userId: string, checksums: Buffer[]): Promise<AssetEntity[]>;
|
||||
getUploadAssetIdByChecksum(ownerId: string, checksum: Buffer): Promise<string | undefined>;
|
||||
getByAlbumId(pagination: PaginationOptions, albumId: string): Paginated<AssetEntity>;
|
||||
getByDeviceIds(ownerId: string, deviceId: string, deviceAssetIds: string[]): Promise<AssetEntity[]>;
|
||||
getByUserId(pagination: PaginationOptions, userId: string, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||
getById(
|
||||
id: string,
|
||||
@@ -178,19 +189,21 @@ export interface IAssetRepository {
|
||||
getAll(pagination: PaginationOptions, options?: AssetSearchOptions): Paginated<AssetEntity>;
|
||||
getAllByDeviceId(userId: string, deviceId: string): Promise<string[]>;
|
||||
updateAll(ids: string[], options: Partial<AssetUpdateAllOptions>): Promise<void>;
|
||||
updateDuplicates(options: AssetUpdateDuplicateOptions): Promise<void>;
|
||||
update(asset: AssetUpdateOptions): Promise<void>;
|
||||
remove(asset: AssetEntity): Promise<void>;
|
||||
softDeleteAll(ids: string[]): Promise<void>;
|
||||
restoreAll(ids: string[]): Promise<void>;
|
||||
findLivePhotoMatch(options: LivePhotoSearchOptions): Promise<AssetEntity | null>;
|
||||
getMapMarkers(ownerIds: string[], options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
||||
getMapMarkers(ownerIds: string[], albumIds: string[], options?: MapMarkerSearchOptions): Promise<MapMarker[]>;
|
||||
getStatistics(ownerId: string, options: AssetStatsOptions): Promise<AssetStats>;
|
||||
getTimeBuckets(options: TimeBucketOptions): Promise<TimeBucketItem[]>;
|
||||
getTimeBucket(timeBucket: string, options: TimeBucketOptions): Promise<AssetEntity[]>;
|
||||
upsertExif(exif: Partial<ExifEntity>): Promise<void>;
|
||||
upsertJobStatus(jobStatus: Partial<AssetJobStatusEntity>): Promise<void>;
|
||||
upsertJobStatus(...jobStatus: Partial<AssetJobStatusEntity>[]): Promise<void>;
|
||||
getAssetIdByCity(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
|
||||
getAssetIdByTag(userId: string, options: AssetExploreFieldOptions): Promise<SearchExploreItem<string>>;
|
||||
getDuplicates(options: AssetBuilderOptions): Promise<AssetEntity[]>;
|
||||
getAllForUserFullSync(options: AssetFullSyncOptions): Promise<AssetEntity[]>;
|
||||
getChangedDeltaSync(options: AssetDeltaSyncOptions): Promise<AssetEntity[]>;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
import { Version } from 'src/utils/version';
|
||||
|
||||
export enum DatabaseExtension {
|
||||
CUBE = 'cube',
|
||||
EARTH_DISTANCE = 'earthdistance',
|
||||
@@ -20,9 +18,10 @@ export enum DatabaseLock {
|
||||
StorageTemplateMigration = 420,
|
||||
CLIPDimSize = 512,
|
||||
LibraryWatch = 1337,
|
||||
GetSystemConfig = 69,
|
||||
}
|
||||
|
||||
export const extName: Record<DatabaseExtension, string> = {
|
||||
export const EXTENSION_NAMES: Record<DatabaseExtension, string> = {
|
||||
cube: 'cube',
|
||||
earthdistance: 'earthdistance',
|
||||
vector: 'pgvector',
|
||||
@@ -36,13 +35,12 @@ export interface VectorUpdateResult {
|
||||
export const IDatabaseRepository = 'IDatabaseRepository';
|
||||
|
||||
export interface IDatabaseRepository {
|
||||
getExtensionVersion(extensionName: string): Promise<Version | null>;
|
||||
getAvailableExtensionVersion(extension: DatabaseExtension): Promise<Version | null>;
|
||||
getPreferredVectorExtension(): VectorExtension;
|
||||
getPostgresVersion(): Promise<Version>;
|
||||
getExtensionVersion(extensionName: string): Promise<string | undefined>;
|
||||
getAvailableExtensionVersion(extension: DatabaseExtension): Promise<string | undefined>;
|
||||
getPostgresVersion(): Promise<string>;
|
||||
createExtension(extension: DatabaseExtension): Promise<void>;
|
||||
updateExtension(extension: DatabaseExtension, version?: Version): Promise<void>;
|
||||
updateVectorExtension(extension: VectorExtension, version?: Version): Promise<VectorUpdateResult>;
|
||||
updateExtension(extension: DatabaseExtension, version?: string): Promise<void>;
|
||||
updateVectorExtension(extension: VectorExtension, version?: string): Promise<VectorUpdateResult>;
|
||||
reindex(index: VectorIndex): Promise<void>;
|
||||
shouldReindex(name: VectorIndex): Promise<boolean>;
|
||||
runMigrations(options?: { transaction?: 'all' | 'none' | 'each' }): Promise<void>;
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { SystemConfig } from 'src/config';
|
||||
import { AssetResponseDto } from 'src/dtos/asset-response.dto';
|
||||
import { ReleaseNotification, ServerVersionResponseDto } from 'src/dtos/server-info.dto';
|
||||
import { SystemConfig } from 'src/entities/system-config.entity';
|
||||
|
||||
export const IEventRepository = 'IEventRepository';
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ export enum QueueName {
|
||||
FACE_DETECTION = 'faceDetection',
|
||||
FACIAL_RECOGNITION = 'facialRecognition',
|
||||
SMART_SEARCH = 'smartSearch',
|
||||
DUPLICATE_DETECTION = 'duplicateDetection',
|
||||
BACKGROUND_TASK = 'backgroundTask',
|
||||
STORAGE_TEMPLATE_MIGRATION = 'storageTemplateMigration',
|
||||
MIGRATION = 'migration',
|
||||
@@ -16,7 +17,7 @@ export enum QueueName {
|
||||
|
||||
export type ConcurrentQueueName = Exclude<
|
||||
QueueName,
|
||||
QueueName.STORAGE_TEMPLATE_MIGRATION | QueueName.FACIAL_RECOGNITION
|
||||
QueueName.STORAGE_TEMPLATE_MIGRATION | QueueName.FACIAL_RECOGNITION | QueueName.DUPLICATE_DETECTION
|
||||
>;
|
||||
|
||||
export enum JobCommand {
|
||||
@@ -86,6 +87,10 @@ export enum JobName {
|
||||
QUEUE_SMART_SEARCH = 'queue-smart-search',
|
||||
SMART_SEARCH = 'smart-search',
|
||||
|
||||
// duplicate detection
|
||||
QUEUE_DUPLICATE_DETECTION = 'queue-duplicate-detection',
|
||||
DUPLICATE_DETECTION = 'duplicate-detection',
|
||||
|
||||
// XMP sidecars
|
||||
QUEUE_SIDECAR = 'queue-sidecar',
|
||||
SIDECAR_DISCOVERY = 'sidecar-discovery',
|
||||
@@ -95,6 +100,9 @@ export enum JobName {
|
||||
// Notification
|
||||
NOTIFY_SIGNUP = 'notify-signup',
|
||||
SEND_EMAIL = 'notification-send-email',
|
||||
|
||||
// Version check
|
||||
VERSION_CHECK = 'version-check',
|
||||
}
|
||||
|
||||
export const JOBS_ASSET_PAGINATION_SIZE = 1000;
|
||||
@@ -105,7 +113,7 @@ export interface IBaseJob {
|
||||
|
||||
export interface IEntityJob extends IBaseJob {
|
||||
id: string;
|
||||
source?: 'upload' | 'sidecar-write';
|
||||
source?: 'upload' | 'sidecar-write' | 'copy';
|
||||
}
|
||||
|
||||
export interface ILibraryFileJob extends IEntityJob {
|
||||
@@ -212,6 +220,10 @@ export type JobItem =
|
||||
| { name: JobName.QUEUE_SMART_SEARCH; data: IBaseJob }
|
||||
| { name: JobName.SMART_SEARCH; data: IEntityJob }
|
||||
|
||||
// Duplicate Detection
|
||||
| { name: JobName.QUEUE_DUPLICATE_DETECTION; data: IBaseJob }
|
||||
| { name: JobName.DUPLICATE_DETECTION; data: IEntityJob }
|
||||
|
||||
// Filesystem
|
||||
| { name: JobName.DELETE_FILES; data: IDeleteFilesJob }
|
||||
|
||||
@@ -234,7 +246,10 @@ export type JobItem =
|
||||
|
||||
// Notification
|
||||
| { name: JobName.SEND_EMAIL; data: IEmailJob }
|
||||
| { name: JobName.NOTIFY_SIGNUP; data: INotifySignupJob };
|
||||
| { name: JobName.NOTIFY_SIGNUP; data: INotifySignupJob }
|
||||
|
||||
// Version check
|
||||
| { name: JobName.VERSION_CHECK; data: IBaseJob };
|
||||
|
||||
export enum JobStatus {
|
||||
SUCCESS = 'success',
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
import { LibraryStatsResponseDto } from 'src/dtos/library.dto';
|
||||
import { LibraryEntity, LibraryType } from 'src/entities/library.entity';
|
||||
import { LibraryEntity } from 'src/entities/library.entity';
|
||||
|
||||
export const ILibraryRepository = 'ILibraryRepository';
|
||||
|
||||
export interface ILibraryRepository {
|
||||
getCountForUser(ownerId: string): Promise<number>;
|
||||
getAll(withDeleted?: boolean, type?: LibraryType): Promise<LibraryEntity[]>;
|
||||
getAll(withDeleted?: boolean): Promise<LibraryEntity[]>;
|
||||
getAllDeleted(): Promise<LibraryEntity[]>;
|
||||
get(id: string, withDeleted?: boolean): Promise<LibraryEntity | null>;
|
||||
create(library: Partial<LibraryEntity>): Promise<LibraryEntity>;
|
||||
delete(id: string): Promise<void>;
|
||||
softDelete(id: string): Promise<void>;
|
||||
getDefaultUploadLibrary(ownerId: string): Promise<LibraryEntity | null>;
|
||||
getUploadLibraryCount(ownerId: string): Promise<number>;
|
||||
update(library: Partial<LibraryEntity>): Promise<LibraryEntity>;
|
||||
getStatistics(id: string): Promise<LibraryStatsResponseDto>;
|
||||
getStatistics(id: string): Promise<LibraryStatsResponseDto | undefined>;
|
||||
getAssetIds(id: string, withDeleted?: boolean): Promise<string[]>;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import { LogLevel } from 'src/entities/system-config.entity';
|
||||
import { LogLevel } from 'src/config';
|
||||
|
||||
export const ILoggerRepository = 'ILoggerRepository';
|
||||
|
||||
export interface ILoggerRepository {
|
||||
setAppName(name: string): void;
|
||||
setContext(message: string): void;
|
||||
setLogLevel(level: LogLevel): void;
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { Writable } from 'node:stream';
|
||||
import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/entities/system-config.entity';
|
||||
import { ImageFormat, TranscodeTarget, VideoCodec } from 'src/config';
|
||||
|
||||
export const IMediaRepository = 'IMediaRepository';
|
||||
|
||||
|
||||
@@ -152,15 +152,29 @@ export interface FaceEmbeddingSearch extends SearchEmbeddingOptions {
|
||||
maxDistance?: number;
|
||||
}
|
||||
|
||||
export interface AssetDuplicateSearch {
|
||||
assetId: string;
|
||||
embedding: Embedding;
|
||||
userIds: string[];
|
||||
maxDistance?: number;
|
||||
}
|
||||
|
||||
export interface FaceSearchResult {
|
||||
distance: number;
|
||||
face: AssetFaceEntity;
|
||||
}
|
||||
|
||||
export interface AssetDuplicateResult {
|
||||
assetId: string;
|
||||
duplicateId: string | null;
|
||||
distance: number;
|
||||
}
|
||||
|
||||
export interface ISearchRepository {
|
||||
init(modelName: string): Promise<void>;
|
||||
searchMetadata(pagination: SearchPaginationOptions, options: AssetSearchOptions): Paginated<AssetEntity>;
|
||||
searchSmart(pagination: SearchPaginationOptions, options: SmartSearchOptions): Paginated<AssetEntity>;
|
||||
searchDuplicates(options: AssetDuplicateSearch): Promise<AssetDuplicateResult[]>;
|
||||
searchFaces(search: FaceEmbeddingSearch): Promise<FaceSearchResult[]>;
|
||||
upsert(assetId: string, embedding: number[]): Promise<void>;
|
||||
searchPlaces(placeName: string): Promise<GeodataPlacesEntity[]>;
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
import { SystemConfigEntity } from 'src/entities/system-config.entity';
|
||||
|
||||
export const ISystemConfigRepository = 'ISystemConfigRepository';
|
||||
|
||||
export interface ISystemConfigRepository {
|
||||
fetchStyle(url: string): Promise<any>;
|
||||
load(): Promise<SystemConfigEntity[]>;
|
||||
readFile(filename: string): Promise<string>;
|
||||
saveAll(items: SystemConfigEntity[]): Promise<SystemConfigEntity[]>;
|
||||
deleteKeys(keys: string[]): Promise<void>;
|
||||
}
|
||||
@@ -5,4 +5,6 @@ export const ISystemMetadataRepository = 'ISystemMetadataRepository';
|
||||
export interface ISystemMetadataRepository {
|
||||
get<T extends keyof SystemMetadata>(key: T): Promise<SystemMetadata[T] | null>;
|
||||
set<T extends keyof SystemMetadata>(key: T, value: SystemMetadata[T]): Promise<void>;
|
||||
fetchStyle(url: string): Promise<any>;
|
||||
readFile(filename: string): Promise<string>;
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { UserMetadata } from 'src/entities/user-metadata.entity';
|
||||
import { UserEntity } from 'src/entities/user.entity';
|
||||
|
||||
export interface UserListFilter {
|
||||
@@ -31,6 +32,7 @@ export interface IUserRepository {
|
||||
getUserStats(): Promise<UserStatsQueryResponse[]>;
|
||||
create(user: Partial<UserEntity>): Promise<UserEntity>;
|
||||
update(id: string, user: Partial<UserEntity>): Promise<UserEntity>;
|
||||
upsertMetadata<T extends keyof UserMetadata>(id: string, item: { key: T; value: UserMetadata[T] }): Promise<void>;
|
||||
delete(user: UserEntity, hard?: boolean): Promise<UserEntity>;
|
||||
updateUsage(id: string, delta: number): Promise<void>;
|
||||
syncUsage(id?: string): Promise<void>;
|
||||
|
||||
+31
-87
@@ -1,80 +1,8 @@
|
||||
import { NestFactory } from '@nestjs/core';
|
||||
import { NestExpressApplication } from '@nestjs/platform-express';
|
||||
import { json } from 'body-parser';
|
||||
import cookieParser from 'cookie-parser';
|
||||
import { CommandFactory } from 'nest-commander';
|
||||
import { existsSync } from 'node:fs';
|
||||
import sirv from 'sirv';
|
||||
import { ApiModule, ImmichAdminModule, MicroservicesModule } from 'src/app.module';
|
||||
import { WEB_ROOT, envName, excludePaths, isDev, serverVersion } from 'src/constants';
|
||||
import { LogLevel } from 'src/entities/system-config.entity';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { WebSocketAdapter } from 'src/middleware/websocket.adapter';
|
||||
import { ApiService } from 'src/services/api.service';
|
||||
import { otelSDK } from 'src/utils/instrumentation';
|
||||
import { useSwagger } from 'src/utils/misc';
|
||||
|
||||
const host = process.env.HOST;
|
||||
|
||||
async function bootstrapMicroservices() {
|
||||
otelSDK.start();
|
||||
|
||||
const port = Number(process.env.MICROSERVICES_PORT) || 3002;
|
||||
const app = await NestFactory.create(MicroservicesModule, { bufferLogs: true });
|
||||
const logger = await app.resolve(ILoggerRepository);
|
||||
logger.setContext('ImmichMicroservice');
|
||||
app.useLogger(logger);
|
||||
app.useWebSocketAdapter(new WebSocketAdapter(app));
|
||||
|
||||
await (host ? app.listen(port, host) : app.listen(port));
|
||||
|
||||
logger.log(`Immich Microservices is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `);
|
||||
}
|
||||
|
||||
async function bootstrapApi() {
|
||||
otelSDK.start();
|
||||
|
||||
const port = Number(process.env.SERVER_PORT) || 3001;
|
||||
const app = await NestFactory.create<NestExpressApplication>(ApiModule, { bufferLogs: true });
|
||||
const logger = await app.resolve(ILoggerRepository);
|
||||
|
||||
logger.setContext('ImmichServer');
|
||||
app.useLogger(logger);
|
||||
app.set('trust proxy', ['loopback', 'linklocal', 'uniquelocal']);
|
||||
app.set('etag', 'strong');
|
||||
app.use(cookieParser());
|
||||
app.use(json({ limit: '10mb' }));
|
||||
if (isDev) {
|
||||
app.enableCors();
|
||||
}
|
||||
app.useWebSocketAdapter(new WebSocketAdapter(app));
|
||||
useSwagger(app, isDev);
|
||||
|
||||
app.setGlobalPrefix('api', { exclude: excludePaths });
|
||||
if (existsSync(WEB_ROOT)) {
|
||||
// copied from https://github.com/sveltejs/kit/blob/679b5989fe62e3964b9a73b712d7b41831aa1f07/packages/adapter-node/src/handler.js#L46
|
||||
// provides serving of precompressed assets and caching of immutable assets
|
||||
app.use(
|
||||
sirv(WEB_ROOT, {
|
||||
etag: true,
|
||||
gzip: true,
|
||||
brotli: true,
|
||||
setHeaders: (res, pathname) => {
|
||||
if (pathname.startsWith(`/_app/immutable`) && res.statusCode === 200) {
|
||||
res.setHeader('cache-control', 'public,max-age=31536000,immutable');
|
||||
}
|
||||
},
|
||||
}),
|
||||
);
|
||||
}
|
||||
app.use(app.get(ApiService).ssr(excludePaths));
|
||||
|
||||
const server = await (host ? app.listen(port, host) : app.listen(port));
|
||||
server.requestTimeout = 30 * 60 * 1000;
|
||||
|
||||
logger.log(`Immich Server is listening on ${await app.getUrl()} [v${serverVersion}] [${envName}] `);
|
||||
}
|
||||
|
||||
import { Worker } from 'node:worker_threads';
|
||||
import { ImmichAdminModule } from 'src/app.module';
|
||||
import { LogLevel } from 'src/config';
|
||||
import { getWorkers } from 'src/utils/workers';
|
||||
const immichApp = process.argv[2] || process.env.IMMICH_APP;
|
||||
|
||||
if (process.argv[2] === immichApp) {
|
||||
@@ -82,27 +10,43 @@ if (process.argv[2] === immichApp) {
|
||||
}
|
||||
|
||||
async function bootstrapImmichAdmin() {
|
||||
process.env.LOG_LEVEL = LogLevel.WARN;
|
||||
process.env.IMMICH_LOG_LEVEL = LogLevel.WARN;
|
||||
await CommandFactory.run(ImmichAdminModule);
|
||||
}
|
||||
|
||||
function bootstrapWorker(name: string) {
|
||||
console.log(`Starting ${name} worker`);
|
||||
const worker = new Worker(`./dist/workers/${name}.js`);
|
||||
worker.on('exit', (exitCode) => {
|
||||
if (exitCode !== 0) {
|
||||
console.error(`${name} worker exited with code ${exitCode}`);
|
||||
process.exit(exitCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bootstrap() {
|
||||
switch (immichApp) {
|
||||
case 'immich': {
|
||||
process.title = 'immich_server';
|
||||
return bootstrapApi();
|
||||
}
|
||||
case 'microservices': {
|
||||
process.title = 'immich_microservices';
|
||||
return bootstrapMicroservices();
|
||||
}
|
||||
case 'immich-admin': {
|
||||
process.title = 'immich_admin_cli';
|
||||
return bootstrapImmichAdmin();
|
||||
}
|
||||
default: {
|
||||
throw new Error(`Invalid app name: ${immichApp}. Expected one of immich|microservices|immich-admin`);
|
||||
case 'immich': {
|
||||
if (!process.env.IMMICH_WORKERS_INCLUDE) {
|
||||
process.env.IMMICH_WORKERS_INCLUDE = 'api';
|
||||
}
|
||||
break;
|
||||
}
|
||||
case 'microservices': {
|
||||
if (!process.env.IMMICH_WORKERS_INCLUDE) {
|
||||
process.env.IMMICH_WORKERS_INCLUDE = 'microservices';
|
||||
}
|
||||
break;
|
||||
}
|
||||
}
|
||||
process.title = 'immich';
|
||||
for (const worker of getWorkers()) {
|
||||
bootstrapWorker(worker);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -6,14 +6,34 @@ import { NextFunction, RequestHandler } from 'express';
|
||||
import multer, { StorageEngine, diskStorage } from 'multer';
|
||||
import { createHash, randomUUID } from 'node:crypto';
|
||||
import { Observable } from 'rxjs';
|
||||
import { UploadFieldName } from 'src/dtos/asset.dto';
|
||||
import { UploadFieldName } from 'src/dtos/asset-media.dto';
|
||||
import { ILoggerRepository } from 'src/interfaces/logger.interface';
|
||||
import { AuthRequest } from 'src/middleware/auth.guard';
|
||||
import { AssetService, UploadFile } from 'src/services/asset.service';
|
||||
import { UploadFile } from 'src/services/asset-media.service';
|
||||
import { AssetService } from 'src/services/asset.service';
|
||||
|
||||
export interface UploadFiles {
|
||||
assetData: ImmichFile[];
|
||||
livePhotoData?: ImmichFile[];
|
||||
sidecarData: ImmichFile[];
|
||||
}
|
||||
|
||||
export function getFile(files: UploadFiles, property: 'assetData' | 'livePhotoData' | 'sidecarData') {
|
||||
const file = files[property]?.[0];
|
||||
return file ? mapToUploadFile(file) : file;
|
||||
}
|
||||
|
||||
export function getFiles(files: UploadFiles) {
|
||||
return {
|
||||
file: getFile(files, 'assetData') as UploadFile,
|
||||
livePhotoFile: getFile(files, 'livePhotoData'),
|
||||
sidecarFile: getFile(files, 'sidecarData'),
|
||||
};
|
||||
}
|
||||
|
||||
export enum Route {
|
||||
ASSET = 'asset',
|
||||
USER = 'user',
|
||||
USER = 'users',
|
||||
}
|
||||
|
||||
export interface ImmichFile extends Express.Multer.File {
|
||||
|
||||
@@ -11,21 +11,6 @@ export class MatchMigrationsWithTypeORMEntities1656889061566 implements Migratio
|
||||
COALESCE("state", '') || ' ' ||
|
||||
COALESCE("country", ''))) STORED`);
|
||||
await queryRunner.query(`ALTER TABLE "exif" ALTER COLUMN "exifTextSearchableColumn" SET NOT NULL`);
|
||||
await queryRunner.query(
|
||||
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
|
||||
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'postgres', 'public', 'exif'],
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[
|
||||
'postgres',
|
||||
'public',
|
||||
'exif',
|
||||
'GENERATED_COLUMN',
|
||||
'exifTextSearchableColumn',
|
||||
"TO_TSVECTOR('english',\n COALESCE(make, '') || ' ' ||\n COALESCE(model, '') || ' ' ||\n COALESCE(orientation, '') || ' ' ||\n COALESCE(\"lensModel\", '') || ' ' ||\n COALESCE(\"city\", '') || ' ' ||\n COALESCE(\"state\", '') || ' ' ||\n COALESCE(\"country\", ''))",
|
||||
],
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" SET NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" SET NOT NULL`);
|
||||
@@ -51,10 +36,6 @@ export class MatchMigrationsWithTypeORMEntities1656889061566 implements Migratio
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "isAdmin" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "lastName" DROP NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ALTER COLUMN "firstName" DROP NOT NULL`);
|
||||
await queryRunner.query(
|
||||
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
|
||||
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_7ae4e03729895bf87e056d7b598"`);
|
||||
await queryRunner.query(`ALTER TABLE "asset_album" DROP CONSTRAINT "FK_256a30a03a4a0aff0394051397d"`);
|
||||
|
||||
@@ -5,10 +5,6 @@ export class AddExifImageNameAsSearchableText1658860470248 implements MigrationI
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
|
||||
await queryRunner.query(
|
||||
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
|
||||
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
|
||||
COALESCE(make, '') || ' ' ||
|
||||
COALESCE(model, '') || ' ' ||
|
||||
@@ -18,33 +14,9 @@ export class AddExifImageNameAsSearchableText1658860470248 implements MigrationI
|
||||
COALESCE("city", '') || ' ' ||
|
||||
COALESCE("state", '') || ' ' ||
|
||||
COALESCE("country", ''))) STORED`);
|
||||
await queryRunner.query(
|
||||
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
|
||||
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
|
||||
);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[
|
||||
'immich',
|
||||
'public',
|
||||
'exif',
|
||||
'GENERATED_COLUMN',
|
||||
'exifTextSearchableColumn',
|
||||
"TO_TSVECTOR('english',\n COALESCE(make, '') || ' ' ||\n COALESCE(model, '') || ' ' ||\n COALESCE(orientation, '') || ' ' ||\n COALESCE(\"lensModel\", '') || ' ' ||\n COALESCE(\"imageName\", '') || ' ' ||\n COALESCE(\"city\", '') || ' ' ||\n COALESCE(\"state\", '') || ' ' ||\n COALESCE(\"country\", ''))",
|
||||
],
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
|
||||
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
['immich', 'public', 'exif', 'GENERATED_COLUMN', 'exifTextSearchableColumn', ''],
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector NOT NULL`);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,10 +5,6 @@ export class RemoveImageNameFromEXIFTable1681159594469 implements MigrationInter
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN IF EXISTS "exifTextSearchableColumn"`);
|
||||
await queryRunner.query(
|
||||
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
|
||||
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
|
||||
COALESCE(make, '') || ' ' ||
|
||||
COALESCE(model, '') || ' ' ||
|
||||
@@ -17,37 +13,11 @@ export class RemoveImageNameFromEXIFTable1681159594469 implements MigrationInter
|
||||
COALESCE("city", '') || ' ' ||
|
||||
COALESCE("state", '') || ' ' ||
|
||||
COALESCE("country", ''))) STORED NOT NULL`);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[
|
||||
'immich',
|
||||
'public',
|
||||
'exif',
|
||||
'GENERATED_COLUMN',
|
||||
'exifTextSearchableColumn',
|
||||
"TO_TSVECTOR('english',\n COALESCE(make, '') || ' ' ||\n COALESCE(model, '') || ' ' ||\n COALESCE(orientation, '') || ' ' ||\n COALESCE(\"lensModel\", '') || ' ' ||\n COALESCE(\"city\", '') || ' ' ||\n COALESCE(\"state\", '') || ' ' ||\n COALESCE(\"country\", ''))",
|
||||
],
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "imageName"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`,
|
||||
['GENERATED_COLUMN', 'exifTextSearchableColumn', 'immich', 'public', 'exif'],
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[
|
||||
'immich',
|
||||
'public',
|
||||
'exif',
|
||||
'GENERATED_COLUMN',
|
||||
'exifTextSearchableColumn',
|
||||
"TO_TSVECTOR('english',\n COALESCE(make, '') || ' ' ||\n COALESCE(model, '') || ' ' ||\n COALESCE(orientation, '') || ' ' ||\n COALESCE(\"lensModel\", '') || ' ' ||\n COALESCE(\"imageName\", '') || ' ' ||\n COALESCE(\"city\", '') || ' ' ||\n COALESCE(\"state\", '') || ' ' ||\n COALESCE(\"country\", ''))",
|
||||
],
|
||||
);
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
|
||||
COALESCE(make, '') || ' ' ||
|
||||
COALESCE(model, '') || ' ' ||
|
||||
|
||||
@@ -8,8 +8,6 @@ export class Geodata1700362016675 implements MigrationInterface {
|
||||
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS earthdistance`)
|
||||
await queryRunner.query(`CREATE TABLE "geodata_admin2" ("key" character varying NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_1e3886455dbb684d6f6b4756726" PRIMARY KEY ("key"))`);
|
||||
await queryRunner.query(`CREATE TABLE "geodata_admin1" ("key" character varying NOT NULL, "name" character varying NOT NULL, CONSTRAINT "PK_3fe3a89c5aac789d365871cb172" PRIMARY KEY ("key"))`);
|
||||
await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`, ["immich","public","geodata_places","GENERATED_COLUMN","admin1Key","\"countryCode\" || '.' || \"admin1Code\""]);
|
||||
await queryRunner.query(`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value") VALUES ($1, $2, $3, $4, $5, $6)`, ["immich","public","geodata_places","GENERATED_COLUMN","admin2Key","\"countryCode\" || '.' || \"admin1Code\" || '.' || \"admin2Code\""]);
|
||||
await queryRunner.query(`CREATE TABLE "geodata_places" ("id" integer NOT NULL, "name" character varying(200) NOT NULL, "longitude" double precision NOT NULL, "latitude" double precision NOT NULL, "countryCode" character(2) NOT NULL, "admin1Code" character varying(20), "admin2Code" character varying(80), "admin1Key" character varying GENERATED ALWAYS AS ("countryCode" || '.' || "admin1Code") STORED, "admin2Key" character varying GENERATED ALWAYS AS ("countryCode" || '.' || "admin1Code" || '.' || "admin2Code") STORED, "modificationDate" date NOT NULL, CONSTRAINT "PK_c29918988912ef4036f3d7fbff4" PRIMARY KEY ("id"))`);
|
||||
await queryRunner.query(`ALTER TABLE "geodata_places" ADD "earthCoord" earth GENERATED ALWAYS AS (ll_to_earth(latitude, longitude)) STORED`)
|
||||
await queryRunner.query(`CREATE INDEX "IDX_geodata_gist_earthcoord" ON "geodata_places" USING gist ("earthCoord");`)
|
||||
@@ -18,8 +16,6 @@ export class Geodata1700362016675 implements MigrationInterface {
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`DROP INDEX "IDX_geodata_gist_earthcoord"`);
|
||||
await queryRunner.query(`DROP TABLE "geodata_places"`);
|
||||
await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`, ["GENERATED_COLUMN","admin2Key","immich","public","geodata_places"]);
|
||||
await queryRunner.query(`DELETE FROM "typeorm_metadata" WHERE "type" = $1 AND "name" = $2 AND "database" = $3 AND "schema" = $4 AND "table" = $5`, ["GENERATED_COLUMN","admin1Key","immich","public","geodata_places"]);
|
||||
await queryRunner.query(`DROP TABLE "geodata_admin1"`);
|
||||
await queryRunner.query(`DROP TABLE "geodata_admin2"`);
|
||||
await queryRunner.query(`DROP EXTENSION cube`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { vectorExt } from 'src/database.config';
|
||||
import { getVectorExtension } from 'src/database.config';
|
||||
import { getCLIPModelInfo } from 'src/utils/misc';
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
@@ -7,7 +7,7 @@ export class UsePgVectors1700713871511 implements MigrationInterface {
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`SET search_path TO "$user", public, vectors`);
|
||||
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS ${vectorExt}`);
|
||||
await queryRunner.query(`CREATE EXTENSION IF NOT EXISTS ${getVectorExtension()}`);
|
||||
const faceDimQuery = await queryRunner.query(`
|
||||
SELECT CARDINALITY(embedding::real[]) as dimsize
|
||||
FROM asset_faces
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { vectorExt } from 'src/database.config';
|
||||
import { getVectorExtension } from 'src/database.config';
|
||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
@@ -6,7 +6,7 @@ export class AddCLIPEmbeddingIndex1700713994428 implements MigrationInterface {
|
||||
name = 'AddCLIPEmbeddingIndex1700713994428';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
if (vectorExt === DatabaseExtension.VECTORS) {
|
||||
if (getVectorExtension() === DatabaseExtension.VECTORS) {
|
||||
await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
|
||||
}
|
||||
await queryRunner.query(`SET search_path TO "$user", public, vectors`);
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { vectorExt } from 'src/database.config';
|
||||
import { getVectorExtension } from 'src/database.config';
|
||||
import { DatabaseExtension } from 'src/interfaces/database.interface';
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
@@ -6,7 +6,7 @@ export class AddFaceEmbeddingIndex1700714033632 implements MigrationInterface {
|
||||
name = 'AddFaceEmbeddingIndex1700714033632';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
if (vectorExt === DatabaseExtension.VECTORS) {
|
||||
if (getVectorExtension() === DatabaseExtension.VECTORS) {
|
||||
await queryRunner.query(`SET vectors.pgvector_compatibility=on`);
|
||||
}
|
||||
await queryRunner.query(`SET search_path TO "$user", public, vectors`);
|
||||
|
||||
@@ -6,7 +6,7 @@ export class DefaultOnboardingForExistingInstallations1704571051932 implements M
|
||||
if (adminCount[0].count > 0) {
|
||||
await queryRunner.query(`INSERT INTO system_metadata (key, value) VALUES ($1, $2)`, [
|
||||
'admin-onboarding',
|
||||
'"{\\"isOnboarded\\":true}"',
|
||||
String.raw`"{\"isOnboarded\":true}"`,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -49,30 +49,6 @@ export class GeodataLocationSearch1708059341865 implements MigrationInterface {
|
||||
CREATE INDEX idx_geodata_places_admin2_name
|
||||
ON geodata_places
|
||||
USING gin (f_unaccent("admin2Name") gin_trgm_ops)`);
|
||||
|
||||
await queryRunner.query(
|
||||
`
|
||||
DELETE FROM "typeorm_metadata"
|
||||
WHERE
|
||||
"type" = $1 AND
|
||||
"name" = $2 AND
|
||||
"database" = $3 AND
|
||||
"schema" = $4 AND
|
||||
"table" = $5`,
|
||||
['GENERATED_COLUMN', 'admin1Key', 'immich', 'public', 'geodata_places'],
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`
|
||||
DELETE FROM "typeorm_metadata"
|
||||
WHERE
|
||||
"type" = $1 AND
|
||||
"name" = $2 AND
|
||||
"database" = $3 AND
|
||||
"schema" = $4 AND
|
||||
"table" = $5`,
|
||||
['GENERATED_COLUMN', 'admin2Key', 'immich', 'public', 'geodata_places'],
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
@@ -91,7 +67,7 @@ export class GeodataLocationSearch1708059341865 implements MigrationInterface {
|
||||
)`);
|
||||
|
||||
await queryRunner.query(`
|
||||
ALTER TABLE geodata_places
|
||||
ALTER TABLE geodata_places
|
||||
ADD COLUMN "admin1Key" character varying
|
||||
GENERATED ALWAYS AS ("countryCode" || '.' || "admin1Code") STORED,
|
||||
ADD COLUMN "admin2Key" character varying
|
||||
@@ -128,25 +104,5 @@ export class GeodataLocationSearch1708059341865 implements MigrationInterface {
|
||||
SET "admin2Name" = admin2.name
|
||||
FROM geodata_admin2 admin2
|
||||
WHERE admin2.key = "admin2Key";`);
|
||||
|
||||
await queryRunner.query(
|
||||
`
|
||||
INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value")
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
['immich', 'public', 'geodata_places', 'GENERATED_COLUMN', 'admin1Key', '"countryCode" || \'.\' || "admin1Code"'],
|
||||
);
|
||||
|
||||
await queryRunner.query(
|
||||
`INSERT INTO "typeorm_metadata"("database", "schema", "table", "type", "name", "value")
|
||||
VALUES ($1, $2, $3, $4, $5, $6)`,
|
||||
[
|
||||
'immich',
|
||||
'public',
|
||||
'geodata_places',
|
||||
'GENERATED_COLUMN',
|
||||
'admin2Key',
|
||||
'"countryCode" || \'.\' || "admin1Code" || \'.\' || "admin2Code"',
|
||||
],
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class CreateAssetDuplicateColumns1711989989911 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE assets ADD COLUMN "duplicateId" uuid`);
|
||||
await queryRunner.query(`ALTER TABLE asset_job_status ADD COLUMN "duplicatesDetectedAt" timestamptz`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_assets_duplicateId" ON assets ("duplicateId")`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE assets DROP COLUMN "duplicateId"`);
|
||||
await queryRunner.query(`ALTER TABLE asset_job_status DROP COLUMN "duplicatesDetectedAt"`);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class MotionAssetExtensionMP41715435221124 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`UPDATE "assets" SET "originalFileName" = regexp_replace("originalFileName", '\\.[a-zA-Z0-9]+$', '.mp4') WHERE "originalPath" LIKE '%.mp4' AND "isVisible" = false`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class RemoveTextSearchColumn1715623169039 implements MigrationInterface {
|
||||
name = 'RemoveTextSearchColumn1715623169039'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" DROP COLUMN "exifTextSearchableColumn"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "exif" ADD "exifTextSearchableColumn" tsvector GENERATED ALWAYS AS (TO_TSVECTOR('english',
|
||||
COALESCE(make, '') || ' ' ||
|
||||
COALESCE(model, '') || ' ' ||
|
||||
COALESCE(orientation, '') || ' ' ||
|
||||
COALESCE("lensModel", '') || ' ' ||
|
||||
COALESCE("city", '') || ' ' ||
|
||||
COALESCE("state", '') || ' ' ||
|
||||
COALESCE("country", ''))) STORED NOT NULL`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
import _ from 'lodash';
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RemoveSystemConfigTable1715787369686 implements MigrationInterface {
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
const overrides = await queryRunner.query('SELECT "key", "value" FROM "system_config"');
|
||||
if (overrides.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
const config = {};
|
||||
for (const { key, value } of overrides) {
|
||||
_.set(config, key, JSON.parse(value));
|
||||
}
|
||||
|
||||
await queryRunner.query(`INSERT INTO "system_metadata" ("key", "value") VALUES ($1, $2)`, [
|
||||
'system-config',
|
||||
// yup, we're double-stringifying it
|
||||
JSON.stringify(JSON.stringify(config)),
|
||||
]);
|
||||
|
||||
await queryRunner.query(`DROP TABLE "system_config"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
// no data restore, you just get the table back
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "system_config" ("key" character varying NOT NULL, "value" character varying, CONSTRAINT "PK_aab69295b445016f56731f4d535" PRIMARY KEY ("key"))`,
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,14 @@
|
||||
import { MigrationInterface, QueryRunner } from "typeorm";
|
||||
|
||||
export class RemoveLibraryIsVisible1715798702876 implements MigrationInterface {
|
||||
name = 'RemoveLibraryIsVisible1715798702876'
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "libraries" DROP COLUMN "isVisible"`);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "libraries" ADD "isVisible" boolean NOT NULL DEFAULT true`);
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class RemoveLibraryType1715804005643 implements MigrationInterface {
|
||||
name = 'RemoveLibraryType1715804005643';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "assets" DROP CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."UQ_assets_owner_library_checksum"`);
|
||||
await queryRunner.query(`DROP INDEX "public"."IDX_originalPath_libraryId"`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ALTER COLUMN "libraryId" DROP NOT NULL`);
|
||||
await queryRunner.query(`
|
||||
UPDATE "assets"
|
||||
SET "libraryId" = NULL
|
||||
FROM "libraries"
|
||||
WHERE "assets"."libraryId" = "libraries"."id"
|
||||
AND "libraries"."type" = 'UPLOAD'
|
||||
`);
|
||||
await queryRunner.query(`DELETE FROM "libraries" WHERE "type" = 'UPLOAD'`);
|
||||
await queryRunner.query(`ALTER TABLE "libraries" DROP COLUMN "type"`);
|
||||
await queryRunner.query(`CREATE INDEX "IDX_originalPath_libraryId" ON "assets" ("originalPath", "libraryId")`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "UQ_assets_owner_checksum" ON "assets" ("ownerId", "checksum") WHERE "libraryId" IS NULL`);
|
||||
await queryRunner.query(`CREATE UNIQUE INDEX "UQ_assets_owner_library_checksum" ON "assets" ("ownerId", "libraryId", "checksum") WHERE "libraryId" IS NOT NULL`);
|
||||
await queryRunner.query(`ALTER TABLE "assets" ADD CONSTRAINT "FK_9977c3c1de01c3d848039a6b90c" FOREIGN KEY ("libraryId") REFERENCES "libraries"("id") ON DELETE CASCADE ON UPDATE CASCADE`);
|
||||
}
|
||||
|
||||
public async down(): Promise<void> {
|
||||
// not implemented
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,24 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class FixJsonB1715890481637 implements MigrationInterface {
|
||||
name = 'FixJsonB1715890481637';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "system_metadata" ALTER COLUMN "value" DROP DEFAULT`);
|
||||
const records = await queryRunner.query('SELECT "key", "value" FROM "system_metadata"');
|
||||
for (const { key, value } of records) {
|
||||
await queryRunner.query(`UPDATE "system_metadata" SET "value" = $1 WHERE "key" = $2`, [value, key]);
|
||||
}
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "system_metadata" ALTER COLUMN "value" SET DEFAULT '{}'`);
|
||||
const records = await queryRunner.query('SELECT "key", "value" FROM "system_metadata"');
|
||||
for (const { key, value } of records) {
|
||||
await queryRunner.query(`UPDATE "system_metadata" SET "value" = $1 WHERE "key" = $2`, [
|
||||
JSON.stringify(JSON.stringify(value)),
|
||||
key,
|
||||
]);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,60 @@
|
||||
import { MigrationInterface, QueryRunner } from 'typeorm';
|
||||
|
||||
export class UserMetadata1716312279245 implements MigrationInterface {
|
||||
name = 'UserMetadata1716312279245';
|
||||
|
||||
public async up(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(
|
||||
`CREATE TABLE "user_metadata" ("userId" uuid NOT NULL, "key" character varying NOT NULL, "value" jsonb NOT NULL, CONSTRAINT "PK_5931462150b3438cbc83277fe5a" PRIMARY KEY ("userId", "key"))`,
|
||||
);
|
||||
const users = await queryRunner.query('SELECT "id", "memoriesEnabled", "avatarColor" FROM "users"');
|
||||
for (const { id, memoriesEnabled, avatarColor } of users) {
|
||||
const preferences: any = {};
|
||||
if (!memoriesEnabled) {
|
||||
preferences.memories = { enabled: false };
|
||||
}
|
||||
|
||||
if (avatarColor) {
|
||||
preferences.avatar = { color: avatarColor };
|
||||
}
|
||||
|
||||
if (Object.keys(preferences).length === 0) {
|
||||
continue;
|
||||
}
|
||||
|
||||
await queryRunner.query('INSERT INTO "user_metadata" ("userId", "key", "value") VALUES ($1, $2, $3)', [
|
||||
id,
|
||||
'preferences',
|
||||
preferences,
|
||||
]);
|
||||
}
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "memoriesEnabled"`);
|
||||
await queryRunner.query(`ALTER TABLE "users" DROP COLUMN "avatarColor"`);
|
||||
await queryRunner.query(
|
||||
`ALTER TABLE "user_metadata" ADD CONSTRAINT "FK_6afb43681a21cf7815932bc38ac" FOREIGN KEY ("userId") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE`,
|
||||
);
|
||||
}
|
||||
|
||||
public async down(queryRunner: QueryRunner): Promise<void> {
|
||||
await queryRunner.query(`ALTER TABLE "user_metadata" DROP CONSTRAINT "FK_6afb43681a21cf7815932bc38ac"`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "avatarColor" character varying`);
|
||||
await queryRunner.query(`ALTER TABLE "users" ADD "memoriesEnabled" boolean NOT NULL DEFAULT true`);
|
||||
const items = await queryRunner.query(
|
||||
`SELECT "userId" as "id", "value" FROM "user_metadata" WHERE "key"='preferences'`,
|
||||
);
|
||||
for (const { id, value } of items) {
|
||||
if (!value) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (value.avatar?.color) {
|
||||
await queryRunner.query(`UPDATE "users" SET "avatarColor" = $1 WHERE "id" = $2`, [value.avatar.color, id]);
|
||||
}
|
||||
|
||||
if (value.memories?.enabled === false) {
|
||||
await queryRunner.query(`UPDATE "users" SET "memoriesEnabled" = false WHERE "id" = $1`, [id]);
|
||||
}
|
||||
}
|
||||
await queryRunner.query(`DROP TABLE "user_metadata"`);
|
||||
}
|
||||
}
|
||||
@@ -153,6 +153,7 @@ FROM
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
"partner"."sharedWithId" = $1
|
||||
AND "asset"."isArchived" = false
|
||||
AND "asset"."id" IN ($2)
|
||||
|
||||
-- AccessRepository.asset.checkSharedLinkAccess
|
||||
@@ -191,20 +192,6 @@ WHERE
|
||||
AND ("SessionEntity"."id" IN ($2))
|
||||
)
|
||||
|
||||
-- AccessRepository.library.checkOwnerAccess
|
||||
SELECT
|
||||
"LibraryEntity"."id" AS "LibraryEntity_id"
|
||||
FROM
|
||||
"libraries" "LibraryEntity"
|
||||
WHERE
|
||||
(
|
||||
(
|
||||
("LibraryEntity"."id" IN ($1))
|
||||
AND ("LibraryEntity"."ownerId" = $2)
|
||||
)
|
||||
)
|
||||
AND ("LibraryEntity"."deletedAt" IS NULL)
|
||||
|
||||
-- AccessRepository.memory.checkOwnerAccess
|
||||
SELECT
|
||||
"MemoryEntity"."id" AS "MemoryEntity_id"
|
||||
|
||||
@@ -12,7 +12,6 @@ SELECT
|
||||
"ActivityEntity"."isLiked" AS "ActivityEntity_isLiked",
|
||||
"ActivityEntity__ActivityEntity_user"."id" AS "ActivityEntity__ActivityEntity_user_id",
|
||||
"ActivityEntity__ActivityEntity_user"."name" AS "ActivityEntity__ActivityEntity_user_name",
|
||||
"ActivityEntity__ActivityEntity_user"."avatarColor" AS "ActivityEntity__ActivityEntity_user_avatarColor",
|
||||
"ActivityEntity__ActivityEntity_user"."isAdmin" AS "ActivityEntity__ActivityEntity_user_isAdmin",
|
||||
"ActivityEntity__ActivityEntity_user"."email" AS "ActivityEntity__ActivityEntity_user_email",
|
||||
"ActivityEntity__ActivityEntity_user"."storageLabel" AS "ActivityEntity__ActivityEntity_user_storageLabel",
|
||||
@@ -23,7 +22,6 @@ SELECT
|
||||
"ActivityEntity__ActivityEntity_user"."deletedAt" AS "ActivityEntity__ActivityEntity_user_deletedAt",
|
||||
"ActivityEntity__ActivityEntity_user"."status" AS "ActivityEntity__ActivityEntity_user_status",
|
||||
"ActivityEntity__ActivityEntity_user"."updatedAt" AS "ActivityEntity__ActivityEntity_user_updatedAt",
|
||||
"ActivityEntity__ActivityEntity_user"."memoriesEnabled" AS "ActivityEntity__ActivityEntity_user_memoriesEnabled",
|
||||
"ActivityEntity__ActivityEntity_user"."quotaSizeInBytes" AS "ActivityEntity__ActivityEntity_user_quotaSizeInBytes",
|
||||
"ActivityEntity__ActivityEntity_user"."quotaUsageInBytes" AS "ActivityEntity__ActivityEntity_user_quotaUsageInBytes"
|
||||
FROM
|
||||
|
||||
@@ -18,7 +18,6 @@ FROM
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
@@ -29,7 +28,6 @@ FROM
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
@@ -37,7 +35,6 @@ FROM
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
|
||||
@@ -48,7 +45,6 @@ FROM
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
|
||||
@@ -98,7 +94,6 @@ SELECT
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
@@ -109,7 +104,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
@@ -117,7 +111,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
|
||||
@@ -128,7 +121,6 @@ SELECT
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes"
|
||||
FROM
|
||||
@@ -160,7 +152,6 @@ SELECT
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
@@ -171,7 +162,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."albumsId" AS "AlbumEntity__AlbumEntity_albumUsers_albumsId",
|
||||
@@ -179,7 +169,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
|
||||
@@ -190,7 +179,6 @@ SELECT
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes"
|
||||
FROM
|
||||
@@ -299,7 +287,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
|
||||
@@ -310,7 +297,6 @@ SELECT
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
|
||||
@@ -327,7 +313,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
@@ -338,7 +323,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
|
||||
FROM
|
||||
@@ -376,7 +360,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_albumUsers"."role" AS "AlbumEntity__AlbumEntity_albumUsers_role",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."id" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_id",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."name" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_name",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."avatarColor" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_avatarColor",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."isAdmin" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_isAdmin",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."email" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_email",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."storageLabel" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_storageLabel",
|
||||
@@ -387,7 +370,6 @@ SELECT
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."deletedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_deletedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."status" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_status",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."updatedAt" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_updatedAt",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."memoriesEnabled" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_memoriesEnabled",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaSizeInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaSizeInBytes",
|
||||
"a641d58cf46d4a391ba060ac4dc337665c69ffea"."quotaUsageInBytes" AS "a641d58cf46d4a391ba060ac4dc337665c69ffea_quotaUsageInBytes",
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."id" AS "AlbumEntity__AlbumEntity_sharedLinks_id",
|
||||
@@ -404,7 +386,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
@@ -415,7 +396,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
|
||||
FROM
|
||||
@@ -504,7 +484,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_sharedLinks"."albumId" AS "AlbumEntity__AlbumEntity_sharedLinks_albumId",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
@@ -515,7 +494,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
|
||||
FROM
|
||||
@@ -564,7 +542,6 @@ SELECT
|
||||
"AlbumEntity"."order" AS "AlbumEntity_order",
|
||||
"AlbumEntity__AlbumEntity_owner"."id" AS "AlbumEntity__AlbumEntity_owner_id",
|
||||
"AlbumEntity__AlbumEntity_owner"."name" AS "AlbumEntity__AlbumEntity_owner_name",
|
||||
"AlbumEntity__AlbumEntity_owner"."avatarColor" AS "AlbumEntity__AlbumEntity_owner_avatarColor",
|
||||
"AlbumEntity__AlbumEntity_owner"."isAdmin" AS "AlbumEntity__AlbumEntity_owner_isAdmin",
|
||||
"AlbumEntity__AlbumEntity_owner"."email" AS "AlbumEntity__AlbumEntity_owner_email",
|
||||
"AlbumEntity__AlbumEntity_owner"."storageLabel" AS "AlbumEntity__AlbumEntity_owner_storageLabel",
|
||||
@@ -575,7 +552,6 @@ SELECT
|
||||
"AlbumEntity__AlbumEntity_owner"."deletedAt" AS "AlbumEntity__AlbumEntity_owner_deletedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."status" AS "AlbumEntity__AlbumEntity_owner_status",
|
||||
"AlbumEntity__AlbumEntity_owner"."updatedAt" AS "AlbumEntity__AlbumEntity_owner_updatedAt",
|
||||
"AlbumEntity__AlbumEntity_owner"."memoriesEnabled" AS "AlbumEntity__AlbumEntity_owner_memoriesEnabled",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaSizeInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaSizeInBytes",
|
||||
"AlbumEntity__AlbumEntity_owner"."quotaUsageInBytes" AS "AlbumEntity__AlbumEntity_owner_quotaUsageInBytes"
|
||||
FROM
|
||||
|
||||
@@ -11,7 +11,6 @@ FROM
|
||||
"APIKeyEntity"."userId" AS "APIKeyEntity_userId",
|
||||
"APIKeyEntity__APIKeyEntity_user"."id" AS "APIKeyEntity__APIKeyEntity_user_id",
|
||||
"APIKeyEntity__APIKeyEntity_user"."name" AS "APIKeyEntity__APIKeyEntity_user_name",
|
||||
"APIKeyEntity__APIKeyEntity_user"."avatarColor" AS "APIKeyEntity__APIKeyEntity_user_avatarColor",
|
||||
"APIKeyEntity__APIKeyEntity_user"."isAdmin" AS "APIKeyEntity__APIKeyEntity_user_isAdmin",
|
||||
"APIKeyEntity__APIKeyEntity_user"."email" AS "APIKeyEntity__APIKeyEntity_user_email",
|
||||
"APIKeyEntity__APIKeyEntity_user"."storageLabel" AS "APIKeyEntity__APIKeyEntity_user_storageLabel",
|
||||
@@ -22,7 +21,6 @@ FROM
|
||||
"APIKeyEntity__APIKeyEntity_user"."deletedAt" AS "APIKeyEntity__APIKeyEntity_user_deletedAt",
|
||||
"APIKeyEntity__APIKeyEntity_user"."status" AS "APIKeyEntity__APIKeyEntity_user_status",
|
||||
"APIKeyEntity__APIKeyEntity_user"."updatedAt" AS "APIKeyEntity__APIKeyEntity_user_updatedAt",
|
||||
"APIKeyEntity__APIKeyEntity_user"."memoriesEnabled" AS "APIKeyEntity__APIKeyEntity_user_memoriesEnabled",
|
||||
"APIKeyEntity__APIKeyEntity_user"."quotaSizeInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaSizeInBytes",
|
||||
"APIKeyEntity__APIKeyEntity_user"."quotaUsageInBytes" AS "APIKeyEntity__APIKeyEntity_user_quotaUsageInBytes"
|
||||
FROM
|
||||
|
||||
@@ -30,6 +30,7 @@ SELECT
|
||||
"entity"."originalFileName" AS "entity_originalFileName",
|
||||
"entity"."sidecarPath" AS "entity_sidecarPath",
|
||||
"entity"."stackId" AS "entity_stackId",
|
||||
"entity"."duplicateId" AS "entity_duplicateId",
|
||||
"exifInfo"."assetId" AS "exifInfo_assetId",
|
||||
"exifInfo"."description" AS "exifInfo_description",
|
||||
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
|
||||
@@ -111,7 +112,8 @@ SELECT
|
||||
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
|
||||
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
|
||||
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
|
||||
"AssetEntity"."stackId" AS "AssetEntity_stackId"
|
||||
"AssetEntity"."stackId" AS "AssetEntity_stackId",
|
||||
"AssetEntity"."duplicateId" AS "AssetEntity_duplicateId"
|
||||
FROM
|
||||
"assets" "AssetEntity"
|
||||
WHERE
|
||||
@@ -147,6 +149,7 @@ SELECT
|
||||
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
|
||||
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
|
||||
"AssetEntity"."stackId" AS "AssetEntity_stackId",
|
||||
"AssetEntity"."duplicateId" AS "AssetEntity_duplicateId",
|
||||
"AssetEntity__AssetEntity_exifInfo"."assetId" AS "AssetEntity__AssetEntity_exifInfo_assetId",
|
||||
"AssetEntity__AssetEntity_exifInfo"."description" AS "AssetEntity__AssetEntity_exifInfo_description",
|
||||
"AssetEntity__AssetEntity_exifInfo"."exifImageWidth" AS "AssetEntity__AssetEntity_exifInfo_exifImageWidth",
|
||||
@@ -231,7 +234,8 @@ SELECT
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."livePhotoVideoId" AS "bd93d5747511a4dad4923546c51365bf1a803774_livePhotoVideoId",
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."originalFileName" AS "bd93d5747511a4dad4923546c51365bf1a803774_originalFileName",
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."sidecarPath" AS "bd93d5747511a4dad4923546c51365bf1a803774_sidecarPath",
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."stackId" AS "bd93d5747511a4dad4923546c51365bf1a803774_stackId"
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."stackId" AS "bd93d5747511a4dad4923546c51365bf1a803774_stackId",
|
||||
"bd93d5747511a4dad4923546c51365bf1a803774"."duplicateId" AS "bd93d5747511a4dad4923546c51365bf1a803774_duplicateId"
|
||||
FROM
|
||||
"assets" "AssetEntity"
|
||||
LEFT JOIN "exif" "AssetEntity__AssetEntity_exifInfo" ON "AssetEntity__AssetEntity_exifInfo"."assetId" = "AssetEntity"."id"
|
||||
@@ -312,7 +316,8 @@ FROM
|
||||
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
|
||||
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
|
||||
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
|
||||
"AssetEntity"."stackId" AS "AssetEntity_stackId"
|
||||
"AssetEntity"."stackId" AS "AssetEntity_stackId",
|
||||
"AssetEntity"."duplicateId" AS "AssetEntity_duplicateId"
|
||||
FROM
|
||||
"assets" "AssetEntity"
|
||||
LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId"
|
||||
@@ -408,7 +413,8 @@ SELECT
|
||||
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
|
||||
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
|
||||
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
|
||||
"AssetEntity"."stackId" AS "AssetEntity_stackId"
|
||||
"AssetEntity"."stackId" AS "AssetEntity_stackId",
|
||||
"AssetEntity"."duplicateId" AS "AssetEntity_duplicateId"
|
||||
FROM
|
||||
"assets" "AssetEntity"
|
||||
WHERE
|
||||
@@ -424,6 +430,15 @@ SET
|
||||
WHERE
|
||||
"id" IN ($2)
|
||||
|
||||
-- AssetRepository.updateDuplicates
|
||||
UPDATE "assets"
|
||||
SET
|
||||
"duplicateId" = $1,
|
||||
"updatedAt" = CURRENT_TIMESTAMP
|
||||
WHERE
|
||||
"duplicateId" IN ($2)
|
||||
OR "id" IN ($3)
|
||||
|
||||
-- AssetRepository.getByChecksum
|
||||
SELECT
|
||||
"AssetEntity"."id" AS "AssetEntity_id",
|
||||
@@ -453,7 +468,8 @@ SELECT
|
||||
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
|
||||
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
|
||||
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
|
||||
"AssetEntity"."stackId" AS "AssetEntity_stackId"
|
||||
"AssetEntity"."stackId" AS "AssetEntity_stackId",
|
||||
"AssetEntity"."duplicateId" AS "AssetEntity_duplicateId"
|
||||
FROM
|
||||
"assets" "AssetEntity"
|
||||
WHERE
|
||||
@@ -467,27 +483,31 @@ WHERE
|
||||
LIMIT
|
||||
1
|
||||
|
||||
-- AssetRepository.getUploadAssetIdByChecksum
|
||||
SELECT DISTINCT
|
||||
"distinctAlias"."AssetEntity_id" AS "ids_AssetEntity_id"
|
||||
-- AssetRepository.getByChecksums
|
||||
SELECT
|
||||
"AssetEntity"."id" AS "AssetEntity_id",
|
||||
"AssetEntity"."checksum" AS "AssetEntity_checksum"
|
||||
FROM
|
||||
"assets" "AssetEntity"
|
||||
WHERE
|
||||
(
|
||||
SELECT
|
||||
"AssetEntity"."id" AS "AssetEntity_id"
|
||||
FROM
|
||||
"assets" "AssetEntity"
|
||||
LEFT JOIN "libraries" "AssetEntity__AssetEntity_library" ON "AssetEntity__AssetEntity_library"."id" = "AssetEntity"."libraryId"
|
||||
WHERE
|
||||
(
|
||||
("AssetEntity"."ownerId" = $1)
|
||||
AND ("AssetEntity"."checksum" = $2)
|
||||
AND (
|
||||
(("AssetEntity__AssetEntity_library"."type" = $3))
|
||||
)
|
||||
)
|
||||
) "distinctAlias"
|
||||
ORDER BY
|
||||
"AssetEntity_id" ASC
|
||||
("AssetEntity"."ownerId" = $1)
|
||||
AND (
|
||||
"AssetEntity"."checksum" IN ($2, $3, $4, $5, $6, $7, $8, $9, $10)
|
||||
)
|
||||
)
|
||||
|
||||
-- AssetRepository.getUploadAssetIdByChecksum
|
||||
SELECT
|
||||
"AssetEntity"."id" AS "AssetEntity_id"
|
||||
FROM
|
||||
"assets" "AssetEntity"
|
||||
WHERE
|
||||
(
|
||||
("AssetEntity"."ownerId" = $1)
|
||||
AND ("AssetEntity"."checksum" = $2)
|
||||
AND ("AssetEntity"."libraryId" IS NULL)
|
||||
)
|
||||
LIMIT
|
||||
1
|
||||
|
||||
@@ -520,7 +540,8 @@ SELECT
|
||||
"AssetEntity"."livePhotoVideoId" AS "AssetEntity_livePhotoVideoId",
|
||||
"AssetEntity"."originalFileName" AS "AssetEntity_originalFileName",
|
||||
"AssetEntity"."sidecarPath" AS "AssetEntity_sidecarPath",
|
||||
"AssetEntity"."stackId" AS "AssetEntity_stackId"
|
||||
"AssetEntity"."stackId" AS "AssetEntity_stackId",
|
||||
"AssetEntity"."duplicateId" AS "AssetEntity_duplicateId"
|
||||
FROM
|
||||
"assets" "AssetEntity"
|
||||
WHERE
|
||||
@@ -576,6 +597,7 @@ SELECT
|
||||
"asset"."originalFileName" AS "asset_originalFileName",
|
||||
"asset"."sidecarPath" AS "asset_sidecarPath",
|
||||
"asset"."stackId" AS "asset_stackId",
|
||||
"asset"."duplicateId" AS "asset_duplicateId",
|
||||
"exifInfo"."assetId" AS "exifInfo_assetId",
|
||||
"exifInfo"."description" AS "exifInfo_description",
|
||||
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
|
||||
@@ -633,7 +655,8 @@ SELECT
|
||||
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
|
||||
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
|
||||
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
|
||||
"stackedAssets"."stackId" AS "stackedAssets_stackId"
|
||||
"stackedAssets"."stackId" AS "stackedAssets_stackId",
|
||||
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
|
||||
FROM
|
||||
"assets" "asset"
|
||||
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
|
||||
@@ -714,6 +737,7 @@ SELECT
|
||||
"asset"."originalFileName" AS "asset_originalFileName",
|
||||
"asset"."sidecarPath" AS "asset_sidecarPath",
|
||||
"asset"."stackId" AS "asset_stackId",
|
||||
"asset"."duplicateId" AS "asset_duplicateId",
|
||||
"exifInfo"."assetId" AS "exifInfo_assetId",
|
||||
"exifInfo"."description" AS "exifInfo_description",
|
||||
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
|
||||
@@ -771,7 +795,8 @@ SELECT
|
||||
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
|
||||
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
|
||||
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
|
||||
"stackedAssets"."stackId" AS "stackedAssets_stackId"
|
||||
"stackedAssets"."stackId" AS "stackedAssets_stackId",
|
||||
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
|
||||
FROM
|
||||
"assets" "asset"
|
||||
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
|
||||
@@ -798,6 +823,112 @@ ORDER BY
|
||||
)::timestamptz DESC,
|
||||
"asset"."fileCreatedAt" DESC
|
||||
|
||||
-- AssetRepository.getDuplicates
|
||||
SELECT
|
||||
"asset"."id" AS "asset_id",
|
||||
"asset"."deviceAssetId" AS "asset_deviceAssetId",
|
||||
"asset"."ownerId" AS "asset_ownerId",
|
||||
"asset"."libraryId" AS "asset_libraryId",
|
||||
"asset"."deviceId" AS "asset_deviceId",
|
||||
"asset"."type" AS "asset_type",
|
||||
"asset"."originalPath" AS "asset_originalPath",
|
||||
"asset"."previewPath" AS "asset_previewPath",
|
||||
"asset"."thumbnailPath" AS "asset_thumbnailPath",
|
||||
"asset"."thumbhash" AS "asset_thumbhash",
|
||||
"asset"."encodedVideoPath" AS "asset_encodedVideoPath",
|
||||
"asset"."createdAt" AS "asset_createdAt",
|
||||
"asset"."updatedAt" AS "asset_updatedAt",
|
||||
"asset"."deletedAt" AS "asset_deletedAt",
|
||||
"asset"."fileCreatedAt" AS "asset_fileCreatedAt",
|
||||
"asset"."localDateTime" AS "asset_localDateTime",
|
||||
"asset"."fileModifiedAt" AS "asset_fileModifiedAt",
|
||||
"asset"."isFavorite" AS "asset_isFavorite",
|
||||
"asset"."isArchived" AS "asset_isArchived",
|
||||
"asset"."isExternal" AS "asset_isExternal",
|
||||
"asset"."isOffline" AS "asset_isOffline",
|
||||
"asset"."checksum" AS "asset_checksum",
|
||||
"asset"."duration" AS "asset_duration",
|
||||
"asset"."isVisible" AS "asset_isVisible",
|
||||
"asset"."livePhotoVideoId" AS "asset_livePhotoVideoId",
|
||||
"asset"."originalFileName" AS "asset_originalFileName",
|
||||
"asset"."sidecarPath" AS "asset_sidecarPath",
|
||||
"asset"."stackId" AS "asset_stackId",
|
||||
"asset"."duplicateId" AS "asset_duplicateId",
|
||||
"exifInfo"."assetId" AS "exifInfo_assetId",
|
||||
"exifInfo"."description" AS "exifInfo_description",
|
||||
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
|
||||
"exifInfo"."exifImageHeight" AS "exifInfo_exifImageHeight",
|
||||
"exifInfo"."fileSizeInByte" AS "exifInfo_fileSizeInByte",
|
||||
"exifInfo"."orientation" AS "exifInfo_orientation",
|
||||
"exifInfo"."dateTimeOriginal" AS "exifInfo_dateTimeOriginal",
|
||||
"exifInfo"."modifyDate" AS "exifInfo_modifyDate",
|
||||
"exifInfo"."timeZone" AS "exifInfo_timeZone",
|
||||
"exifInfo"."latitude" AS "exifInfo_latitude",
|
||||
"exifInfo"."longitude" AS "exifInfo_longitude",
|
||||
"exifInfo"."projectionType" AS "exifInfo_projectionType",
|
||||
"exifInfo"."city" AS "exifInfo_city",
|
||||
"exifInfo"."livePhotoCID" AS "exifInfo_livePhotoCID",
|
||||
"exifInfo"."autoStackId" AS "exifInfo_autoStackId",
|
||||
"exifInfo"."state" AS "exifInfo_state",
|
||||
"exifInfo"."country" AS "exifInfo_country",
|
||||
"exifInfo"."make" AS "exifInfo_make",
|
||||
"exifInfo"."model" AS "exifInfo_model",
|
||||
"exifInfo"."lensModel" AS "exifInfo_lensModel",
|
||||
"exifInfo"."fNumber" AS "exifInfo_fNumber",
|
||||
"exifInfo"."focalLength" AS "exifInfo_focalLength",
|
||||
"exifInfo"."iso" AS "exifInfo_iso",
|
||||
"exifInfo"."exposureTime" AS "exifInfo_exposureTime",
|
||||
"exifInfo"."profileDescription" AS "exifInfo_profileDescription",
|
||||
"exifInfo"."colorspace" AS "exifInfo_colorspace",
|
||||
"exifInfo"."bitsPerSample" AS "exifInfo_bitsPerSample",
|
||||
"exifInfo"."fps" AS "exifInfo_fps",
|
||||
"stack"."id" AS "stack_id",
|
||||
"stack"."primaryAssetId" AS "stack_primaryAssetId",
|
||||
"stackedAssets"."id" AS "stackedAssets_id",
|
||||
"stackedAssets"."deviceAssetId" AS "stackedAssets_deviceAssetId",
|
||||
"stackedAssets"."ownerId" AS "stackedAssets_ownerId",
|
||||
"stackedAssets"."libraryId" AS "stackedAssets_libraryId",
|
||||
"stackedAssets"."deviceId" AS "stackedAssets_deviceId",
|
||||
"stackedAssets"."type" AS "stackedAssets_type",
|
||||
"stackedAssets"."originalPath" AS "stackedAssets_originalPath",
|
||||
"stackedAssets"."previewPath" AS "stackedAssets_previewPath",
|
||||
"stackedAssets"."thumbnailPath" AS "stackedAssets_thumbnailPath",
|
||||
"stackedAssets"."thumbhash" AS "stackedAssets_thumbhash",
|
||||
"stackedAssets"."encodedVideoPath" AS "stackedAssets_encodedVideoPath",
|
||||
"stackedAssets"."createdAt" AS "stackedAssets_createdAt",
|
||||
"stackedAssets"."updatedAt" AS "stackedAssets_updatedAt",
|
||||
"stackedAssets"."deletedAt" AS "stackedAssets_deletedAt",
|
||||
"stackedAssets"."fileCreatedAt" AS "stackedAssets_fileCreatedAt",
|
||||
"stackedAssets"."localDateTime" AS "stackedAssets_localDateTime",
|
||||
"stackedAssets"."fileModifiedAt" AS "stackedAssets_fileModifiedAt",
|
||||
"stackedAssets"."isFavorite" AS "stackedAssets_isFavorite",
|
||||
"stackedAssets"."isArchived" AS "stackedAssets_isArchived",
|
||||
"stackedAssets"."isExternal" AS "stackedAssets_isExternal",
|
||||
"stackedAssets"."isOffline" AS "stackedAssets_isOffline",
|
||||
"stackedAssets"."checksum" AS "stackedAssets_checksum",
|
||||
"stackedAssets"."duration" AS "stackedAssets_duration",
|
||||
"stackedAssets"."isVisible" AS "stackedAssets_isVisible",
|
||||
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
|
||||
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
|
||||
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
|
||||
"stackedAssets"."stackId" AS "stackedAssets_stackId",
|
||||
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
|
||||
FROM
|
||||
"assets" "asset"
|
||||
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
|
||||
LEFT JOIN "asset_stack" "stack" ON "stack"."id" = "asset"."stackId"
|
||||
LEFT JOIN "assets" "stackedAssets" ON "stackedAssets"."stackId" = "stack"."id"
|
||||
AND ("stackedAssets"."deletedAt" IS NULL)
|
||||
WHERE
|
||||
(
|
||||
"asset"."isVisible" = true
|
||||
AND "asset"."ownerId" IN ($1, $2)
|
||||
AND "asset"."duplicateId" IS NOT NULL
|
||||
)
|
||||
AND ("asset"."deletedAt" IS NULL)
|
||||
ORDER BY
|
||||
"asset"."duplicateId" ASC
|
||||
|
||||
-- AssetRepository.getAssetIdByCity
|
||||
WITH
|
||||
"cities" AS (
|
||||
@@ -888,6 +1019,7 @@ SELECT
|
||||
"asset"."originalFileName" AS "asset_originalFileName",
|
||||
"asset"."sidecarPath" AS "asset_sidecarPath",
|
||||
"asset"."stackId" AS "asset_stackId",
|
||||
"asset"."duplicateId" AS "asset_duplicateId",
|
||||
"exifInfo"."assetId" AS "exifInfo_assetId",
|
||||
"exifInfo"."description" AS "exifInfo_description",
|
||||
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
|
||||
@@ -945,7 +1077,8 @@ SELECT
|
||||
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
|
||||
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
|
||||
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
|
||||
"stackedAssets"."stackId" AS "stackedAssets_stackId"
|
||||
"stackedAssets"."stackId" AS "stackedAssets_stackId",
|
||||
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
|
||||
FROM
|
||||
"assets" "asset"
|
||||
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
|
||||
@@ -993,6 +1126,7 @@ SELECT
|
||||
"asset"."originalFileName" AS "asset_originalFileName",
|
||||
"asset"."sidecarPath" AS "asset_sidecarPath",
|
||||
"asset"."stackId" AS "asset_stackId",
|
||||
"asset"."duplicateId" AS "asset_duplicateId",
|
||||
"exifInfo"."assetId" AS "exifInfo_assetId",
|
||||
"exifInfo"."description" AS "exifInfo_description",
|
||||
"exifInfo"."exifImageWidth" AS "exifInfo_exifImageWidth",
|
||||
@@ -1050,7 +1184,8 @@ SELECT
|
||||
"stackedAssets"."livePhotoVideoId" AS "stackedAssets_livePhotoVideoId",
|
||||
"stackedAssets"."originalFileName" AS "stackedAssets_originalFileName",
|
||||
"stackedAssets"."sidecarPath" AS "stackedAssets_sidecarPath",
|
||||
"stackedAssets"."stackId" AS "stackedAssets_stackId"
|
||||
"stackedAssets"."stackId" AS "stackedAssets_stackId",
|
||||
"stackedAssets"."duplicateId" AS "stackedAssets_duplicateId"
|
||||
FROM
|
||||
"assets" "asset"
|
||||
LEFT JOIN "exif" "exifInfo" ON "exifInfo"."assetId" = "asset"."id"
|
||||
@@ -1060,8 +1195,4 @@ FROM
|
||||
WHERE
|
||||
"asset"."isVisible" = true
|
||||
AND "asset"."ownerId" IN ($1)
|
||||
AND (
|
||||
"stack"."primaryAssetId" = "asset"."id"
|
||||
OR "asset"."stackId" IS NULL
|
||||
)
|
||||
AND "asset"."updatedAt" > $2
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user