diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000000..622bd4d698 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +dev.ts \ No newline at end of file diff --git a/dev.ts b/dev.ts new file mode 100755 index 0000000000..427dc6bda6 --- /dev/null +++ b/dev.ts @@ -0,0 +1,228 @@ +#!/bin/sh +':' //; exec node --disable-warning=ExperimentalWarning --experimental-strip-types "$0" "$@" +':' /* +@echo off +node "%~dpnx0" %* +exit /b %errorlevel% +*/ + +import { execSync, type ExecSyncOptions, spawn } from 'node:child_process'; +import { Dir, Dirent, existsSync, mkdirSync, opendirSync, readFileSync, rmSync } from 'node:fs'; +import { platform } from 'node:os'; +import { join, resolve } from 'node:path'; +import { parseArgs } from 'node:util'; + +// Utilities +const tryRun = (fn: () => T, onSuccess?: (result: T) => void, onError?: (e: unknown) => void, onFinally?: (result: T | undefined) => void): T | void => { + let result: T | undefined= undefined; + try { + result = fn(); + onSuccess?.(result); + return result; + } catch (e: unknown) { + onError?.(e); + } finally { + onFinally?.(result); + } +}; + + +const FALSE = () => false; +const exit0 = () => process.exit(0); +const exit1 = () => process.exit(1); +const log = (msg: string) => { console.log(msg); return msg; }; +const err = (msg: string, e?: unknown) => { console.log(msg, e); return undefined; }; +const errExit = (msg: string, e?: unknown) => ()=>{ console.log(msg, e); exit1(); }; + + +const exec = (cmd: string, opts: ExecSyncOptions = { stdio: 'inherit' }) => execSync(cmd, opts); + +const isWSL = () => platform() === 'linux' && + tryRun(() => readFileSync('/proc/version', 'utf-8').toLowerCase().includes('microsoft'), undefined, FALSE); + +const isWindows = () => platform() === 'win32'; +const supportsChown = () => !isWindows() || isWSL(); + +const onExit = (handler: () => void) => { + ['SIGINT', 'SIGTERM'].forEach(sig => process.on(sig, () => { handler(); exit0(); })); + if (isWindows()) process.on('SIGBREAK', () => { handler(); exit0(); }); +}; + +// Directory operations +const mkdirs = (dirs: string[]) => dirs.forEach(dir => + tryRun( + () => mkdirSync(dir, { recursive: true }), + () => log(`Created directory: ${dir}`), + e => err(`Error creating directory ${dir}:`, e) + )); + +const chown = (dirs: string[], uid: string, gid: string) => { + if (!supportsChown()) { + log('Skipping ownership changes on Windows (not supported outside WSL)'); + return; + } + for (const dir of dirs) { + tryRun( + () => exec(`chown -R ${uid}:${gid} "${dir}"`), + undefined, + errExit(`Permission denied when changing owner of volumes. Try running 'sudo ./dev.ts prepare-volumes' first.`) + ); + } +}; + +const findAndRemove = (path: string, target: string) => { + if (!existsSync(path)) return; + + const removeLoop = (dir: Dir) => { + let dirent: Dirent | null; + while ((dirent = dir.readSync()) !== null) { + if (!dirent.isDirectory()) continue; + + const itemPath = join(path, dirent.name); + if (dirent.name === target) { + log(` Removing: ${itemPath}`); + rmSync(itemPath, { recursive: true, force: true }); + } else { + findAndRemove(itemPath, target); + } + } + } + + tryRun(() => opendirSync(path), removeLoop, errExit( `Error opening directory ${path}`), (dir) => dir?.closeSync()); +}; + +// Docker DSL +const docker = { + compose: (file: string) => ({ + up: (opts?: string[]) => spawn('docker', ['compose', '-f', file, 'up', ...(opts || [])], { + stdio: 'inherit', + env: { ...process.env, COMPOSE_BAKE: 'true' }, + shell: true + }), + down: () => tryRun(() => exec(`docker compose -f ${file} down --remove-orphans`)) + }), + + isAvailable: () => !!tryRun(() => exec('docker --version', { stdio: 'ignore' }), undefined, FALSE) +}; + +// Environment configuration +const envConfig = { + volumeDirs: [ + './.pnpm-store', './web/.svelte-kit', './web/node_modules', './web/coverage', + './e2e/node_modules', './docs/node_modules', './server/node_modules', + './open-api/typescript-sdk/node_modules', './.github/node_modules', + './node_modules', './cli/node_modules' + ], + + cleanDirs: ['node_modules', 'dist', 'build', '.svelte-kit', 'coverage', '.pnpm-store'], + + composeFiles: { + dev: './docker/docker-compose.dev.yml', + e2e: './e2e/docker-compose.yml', + prod: './docker/docker-compose.prod.yml' + }, + + getEnv: () => ({ + uid: process.env.UID || '1000', + gid: process.env.GID || '1000' + }) +}; + +// Commands +const commands = { + 'prepare-volumes': () => { + log('Preparing volumes...'); + const { uid, gid } = envConfig.getEnv(); + + mkdirs(envConfig.volumeDirs); + chown(envConfig.volumeDirs, uid, gid); + + // Handle UPLOAD_LOCATION + const uploadLocation = tryRun(() => { + const content = readFileSync('./docker/.env', 'utf-8'); + const match = content.match(/^UPLOAD_LOCATION=(.+)$/m); + return match?.[1]?.trim(); + }); + + if (uploadLocation) { + const targetPath = resolve('docker', uploadLocation); + mkdirs([targetPath]); + + if (supportsChown()) { + tryRun( + () => { + // First chown the uploadLocation directory itself + exec(`chown ${uid}:${gid} "${targetPath}"`); + // Then chown all contents except postgres folder (using -prune to skip it entirely) + exec(`find "${targetPath}" -mindepth 1 -name postgres -prune -o -exec chown ${uid}:${gid} {} +`); + }, + undefined, + errExit(`Permission denied when changing owner of volumes. Try running 'sudo ./dev.ts prepare-volumes' first.`) + ); + } else { + log('Skipping ownership changes on Windows (not supported outside WSL)'); + } + } + + log('Volume preparation completed.'); + }, + + clean: () => { + log('Starting clean process...'); + + envConfig.cleanDirs.forEach(dir => { + log(`Removing ${dir} directories...`); + findAndRemove('.', dir); + }); + + docker.isAvailable() && + log('Stopping and removing Docker containers...') && + docker.compose(envConfig.composeFiles.dev).down(); + + log('Clean process completed.'); + }, + + down: (opts: { e2e?: boolean; prod?: boolean }) => { + const type = opts.prod ? 'prod' : opts.e2e ? 'e2e' : 'dev'; + const file = envConfig.composeFiles[type]; + + log(`\nStopping ${type} environment...`); + docker.compose(file).down(); + }, + + up: (opts: { e2e?: boolean; prod?: boolean }) => { + commands['prepare-volumes'](); + + const type = opts.prod ? 'prod' : opts.e2e ? 'e2e' : 'dev'; + const file = envConfig.composeFiles[type]; + const args = opts.prod ? ['--build', '-V', '--remove-orphans'] : ['--remove-orphans']; + + onExit(() => commands.down(opts)); + + log(`Starting ${type} environment...`); + + const proc = docker.compose(file).up(args); + proc.on('error',errExit('Failed to start docker compose:' )); + proc.on('exit', (code: number) => { commands.down(opts); code ? exit1() : exit0(); }); + } +}; + +// Main +const { positionals, values } = parseArgs({ + args: process.argv.slice(2), + allowPositionals: true, + options: { + e2e: { type: 'boolean', default: false }, + prod: { type: 'boolean', default: false } + } +}); + +const command = positionals[0]; +const handler = commands[command as keyof typeof commands]; + +if (!handler) { + log('Usage: ./dev.ts [clean|prepare-volumes|up [--e2e] [--prod]|down [--e2e] [--prod]]'); + exit1(); +} + +handler(values); \ No newline at end of file diff --git a/docker/docker-compose.dev.yml b/docker/docker-compose.dev.yml index 372352d12a..ff6e7fe11f 100644 --- a/docker/docker-compose.dev.yml +++ b/docker/docker-compose.dev.yml @@ -18,6 +18,7 @@ services: container_name: immich_server command: ['immich-dev'] image: immich-server-dev:latest + pull_policy: never # extends: # file: hwaccel.transcoding.yml # service: cpu # set to one of [nvenc, quicksync, rkmpp, vaapi, vaapi-wsl] for accelerated transcoding @@ -80,6 +81,7 @@ services: immich-web: container_name: immich_web image: immich-web-dev:latest + pull_policy: never # Needed for rootless docker setup, see https://github.com/moby/moby/issues/45919 # user: 0:0 user: '${UID:-1000}:${GID:-1000}' @@ -120,6 +122,7 @@ services: immich-machine-learning: container_name: immich_machine_learning image: immich-machine-learning-dev:latest + pull_policy: never # extends: # file: hwaccel.ml.yml # service: cpu # set to one of [armnn, cuda, rocm, openvino, openvino-wsl, rknn] for accelerated inference diff --git a/package.json b/package.json index a718d7ccd0..e83b641736 100644 --- a/package.json +++ b/package.json @@ -6,5 +6,12 @@ "packageManager": "pnpm@10.14.0+sha512.ad27a79641b49c3e481a16a805baa71817a04bbe06a38d17e60e2eaee83f6a146c6a688125f5792e48dd5ba30e7da52a5cda4c3992b9ccf333f9ce223af84748", "engines": { "pnpm": ">=10.0.0" + }, + "type": "module", + "scripts": { + "dev": "node --experimental-strip-types dev.ts" + }, + "devDependencies": { + "@types/node": "22.18.0" } } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index fa24e5b31f..99c4d168c8 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,7 +15,11 @@ pnpmfileChecksum: sha256-AG/qwrPNpmy9q60PZwCpecoYVptglTHgH+N6RKQHOM0= importers: - .: {} + .: + devDependencies: + '@types/node': + specifier: 22.18.0 + version: 22.18.0 .github: devDependencies: @@ -4658,6 +4662,9 @@ packages: '@types/node@22.17.2': resolution: {integrity: sha512-gL6z5N9Jm9mhY+U2KXZpteb+09zyffliRkZyZOHODGATyC5B1Jt/7TzuuiLkFsSUMLbS1OLmlj/E+/3KF4Q/4w==} + '@types/node@22.18.0': + resolution: {integrity: sha512-m5ObIqwsUp6BZzyiy4RdZpzWGub9bqLJMvZDD0QMXhxjqMHMENlj+SqF5QxoUwaQNFe+8kz8XM8ZQhqkQPTgMQ==} + '@types/node@24.3.0': resolution: {integrity: sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow==} @@ -14530,7 +14537,7 @@ snapshots: '@jest/schemas': 29.6.3 '@types/istanbul-lib-coverage': 2.0.6 '@types/istanbul-reports': 3.0.4 - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/yargs': 17.0.33 chalk: 4.1.2 @@ -16349,7 +16356,7 @@ snapshots: '@types/accepts@1.3.7': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/archiver@6.0.3': dependencies: @@ -16363,22 +16370,22 @@ snapshots: '@types/bcrypt@6.0.0': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/body-parser@1.19.6': dependencies: '@types/connect': 3.4.38 - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/bonjour@3.5.13': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/braces@3.0.5': {} '@types/bunyan@1.8.11': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/byte-size@8.1.2': {} @@ -16397,21 +16404,21 @@ snapshots: '@types/cli-progress@3.11.6': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/compression@1.8.1': dependencies: '@types/express': 5.0.3 - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/connect-history-api-fallback@1.5.4': dependencies: '@types/express-serve-static-core': 5.0.6 - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/connect@3.4.38': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/content-disposition@0.5.9': {} @@ -16428,11 +16435,11 @@ snapshots: '@types/connect': 3.4.38 '@types/express': 5.0.3 '@types/keygrip': 1.0.6 - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/cors@2.8.19': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/debug@4.1.12': dependencies: @@ -16442,13 +16449,13 @@ snapshots: '@types/docker-modem@3.0.6': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/ssh2': 1.15.5 '@types/dockerode@3.3.42': dependencies: '@types/docker-modem': 3.0.6 - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/ssh2': 1.15.5 '@types/dom-to-image@2.6.7': {} @@ -16471,14 +16478,14 @@ snapshots: '@types/express-serve-static-core@4.19.6': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 0.17.5 '@types/express-serve-static-core@5.0.6': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/qs': 6.14.0 '@types/range-parser': 1.2.7 '@types/send': 0.17.5 @@ -16504,7 +16511,7 @@ snapshots: '@types/fluent-ffmpeg@2.1.27': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/geojson-vt@3.2.5': dependencies: @@ -16541,7 +16548,7 @@ snapshots: '@types/http-proxy@1.17.16': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/inquirer@8.2.11': dependencies: @@ -16579,7 +16586,7 @@ snapshots: '@types/http-errors': 2.0.5 '@types/keygrip': 1.0.6 '@types/koa-compose': 3.2.8 - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/leaflet@1.9.19': dependencies: @@ -16605,7 +16612,7 @@ snapshots: '@types/memcached@2.2.10': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/methods@1.1.4': {} @@ -16617,7 +16624,7 @@ snapshots: '@types/mock-fs@4.13.4': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/ms@2.1.0': {} @@ -16627,16 +16634,16 @@ snapshots: '@types/mysql@2.15.27': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/node-fetch@2.6.12': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 form-data: 4.0.3 '@types/node-forge@1.3.11': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/node@17.0.45': {} @@ -16656,6 +16663,10 @@ snapshots: dependencies: undici-types: 6.21.0 + '@types/node@22.18.0': + dependencies: + undici-types: 6.21.0 + '@types/node@24.3.0': dependencies: undici-types: 7.10.0 @@ -16663,17 +16674,17 @@ snapshots: '@types/nodemailer@6.4.17': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/oidc-provider@9.1.2': dependencies: '@types/keygrip': 1.0.6 '@types/koa': 3.0.0 - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/oracledb@6.5.2': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/parse5@5.0.3': {} @@ -16683,13 +16694,13 @@ snapshots: '@types/pg@8.15.4': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 pg-protocol: 1.10.3 pg-types: 2.2.0 '@types/pg@8.15.5': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 pg-protocol: 1.10.3 pg-types: 2.2.0 @@ -16697,13 +16708,13 @@ snapshots: '@types/pngjs@6.0.5': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/prismjs@1.26.5': {} '@types/qrcode@1.5.5': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/qs@6.14.0': {} @@ -16743,7 +16754,7 @@ snapshots: '@types/readdir-glob@1.1.5': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/retry@0.12.0': {} @@ -16753,14 +16764,14 @@ snapshots: '@types/sax@1.2.7': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/semver@7.7.0': {} '@types/send@0.17.5': dependencies: '@types/mime': 1.3.5 - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/serve-index@1.9.4': dependencies: @@ -16769,20 +16780,20 @@ snapshots: '@types/serve-static@1.15.8': dependencies: '@types/http-errors': 2.0.5 - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/send': 0.17.5 '@types/sockjs@0.3.36': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/ssh2-streams@0.1.12': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/ssh2@0.5.52': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/ssh2-streams': 0.1.12 '@types/ssh2@1.15.5': @@ -16793,7 +16804,7 @@ snapshots: dependencies: '@types/cookiejar': 2.1.5 '@types/methods': 1.1.4 - '@types/node': 22.17.2 + '@types/node': 22.18.0 form-data: 4.0.3 '@types/supercluster@7.1.3': @@ -16807,11 +16818,11 @@ snapshots: '@types/tedious@4.0.14': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/through@0.0.33': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/ua-parser-js@0.7.39': {} @@ -16825,7 +16836,7 @@ snapshots: '@types/ws@8.18.1': dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 '@types/yargs-parser@21.0.3': {} @@ -18915,7 +18926,7 @@ snapshots: engine.io@6.6.4: dependencies: '@types/cors': 2.8.19 - '@types/node': 22.17.2 + '@types/node': 22.18.0 accepts: 1.3.8 base64id: 2.0.0 cookie: 0.7.2 @@ -19339,7 +19350,7 @@ snapshots: eval@0.1.8: dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 require-like: 0.1.2 event-emitter@0.3.5: @@ -20591,7 +20602,7 @@ snapshots: jest-util@29.7.0: dependencies: '@jest/types': 29.6.3 - '@types/node': 22.17.2 + '@types/node': 22.18.0 chalk: 4.1.2 ci-info: 3.9.0 graceful-fs: 4.2.11 @@ -20599,13 +20610,13 @@ snapshots: jest-worker@27.5.1: dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 merge-stream: 2.0.0 supports-color: 8.1.1 jest-worker@29.7.0: dependencies: - '@types/node': 22.17.2 + '@types/node': 22.18.0 jest-util: 29.7.0 merge-stream: 2.0.0 supports-color: 8.1.1 @@ -23098,7 +23109,7 @@ snapshots: '@protobufjs/path': 1.1.2 '@protobufjs/pool': 1.1.0 '@protobufjs/utf8': 1.1.0 - '@types/node': 22.17.2 + '@types/node': 22.18.0 long: 5.3.2 protocol-buffers-schema@3.6.0: {}