From 5f25e2ce824b9a7a82561a78005d36ccd2f412fa Mon Sep 17 00:00:00 2001 From: Jason Rasmussen Date: Mon, 8 Jul 2024 14:53:18 -0400 Subject: [PATCH] refactor(server): build resources (#10958) --- docs/docs/install/environment-variables.md | 2 -- server/Dockerfile | 6 ++--- server/src/constants.ts | 27 ++++++++++++++----- server/src/repositories/map.repository.ts | 10 +++---- .../repositories/server-info.repository.ts | 5 ++-- server/src/services/api.service.ts | 7 +++-- server/src/workers/api.ts | 6 ++--- 7 files changed, 38 insertions(+), 25 deletions(-) diff --git a/docs/docs/install/environment-variables.md b/docs/docs/install/environment-variables.md index bd694e26d9..64a3078fc8 100644 --- a/docs/docs/install/environment-variables.md +++ b/docs/docs/install/environment-variables.md @@ -45,8 +45,6 @@ Regardless of filesystem, it is not recommended to use a network share for your | `IMMICH_LOG_LEVEL` | Log Level (verbose, debug, log, warn, error) | `log` | server, machine learning | api, microservices | | `IMMICH_MEDIA_LOCATION` | Media Location | `./upload`\*1 | server | api, microservices | | `IMMICH_CONFIG_FILE` | Path to config file | | server | api, microservices | -| `IMMICH_WEB_ROOT` | Path of root index.html | `/usr/src/app/www` | server | api | -| `IMMICH_REVERSE_GEOCODING_ROOT` | Path of reverse geocoding dump directory | `/usr/src/resources` | server | microservices | | `NO_COLOR` | Set to `true` to disable color-coded log output | `false` | server, machine learning | | | `CPU_CORES` | Amount of cores available to the immich server | auto-detected cpu core count | server | | | `IMMICH_API_METRICS_PORT` | Port for the OTEL metrics | `8081` | server | api | diff --git a/server/Dockerfile b/server/Dockerfile index eaae1cc640..94f784525f 100644 --- a/server/Dockerfile +++ b/server/Dockerfile @@ -1,5 +1,5 @@ # dev build -FROM ghcr.io/immich-app/base-server-dev:20240702@sha256:5d675b67826ac643ee64ecf2ef78adac1e491eef9a845f30818a1c0d1338ecc8 as dev +FROM ghcr.io/immich-app/base-server-dev:20240708@sha256:2a9e3231c34493cb861299d475c84c031e7f04519dbc895bbebb5017d479a3cb as dev RUN apt-get install --no-install-recommends -yqq tini WORKDIR /usr/src/app @@ -41,7 +41,7 @@ RUN npm run build # prod build -FROM ghcr.io/immich-app/base-server-prod:20240702@sha256:419a873052cf2f012ed1977e4a771a38e68ce64ea1c66047cc06232b1a79bafe +FROM ghcr.io/immich-app/base-server-prod:20240708@sha256:0af3a5bb036c9a4b6a5a51becaa6e94fe182f6bc97480d57e8f2e6f994bfa453 WORKDIR /usr/src/app ENV NODE_ENV=production \ @@ -50,7 +50,7 @@ ENV NODE_ENV=production \ COPY --from=prod /usr/src/app/node_modules ./node_modules COPY --from=prod /usr/src/app/dist ./dist COPY --from=prod /usr/src/app/bin ./bin -COPY --from=web /usr/src/app/build ./www +COPY --from=web /usr/src/app/build /build/www COPY server/resources resources COPY server/package.json server/package-lock.json ./ COPY server/start*.sh ./ diff --git a/server/src/constants.ts b/server/src/constants.ts index 4e2aca7184..cd418e9234 100644 --- a/server/src/constants.ts +++ b/server/src/constants.ts @@ -27,13 +27,28 @@ export const WEB_ROOT = process.env.IMMICH_WEB_ROOT || '/usr/src/app/www'; const HOST_SERVER_PORT = process.env.IMMICH_PORT || '2283'; export const DEFAULT_EXTERNAL_DOMAIN = 'http://localhost:' + HOST_SERVER_PORT; -const GEODATA_ROOT_PATH = process.env.IMMICH_REVERSE_GEOCODING_ROOT || '/usr/src/resources'; - export const citiesFile = 'cities500.txt'; -export const geodataDatePath = join(GEODATA_ROOT_PATH, 'geodata-date.txt'); -export const geodataAdmin1Path = join(GEODATA_ROOT_PATH, 'admin1CodesASCII.txt'); -export const geodataAdmin2Path = join(GEODATA_ROOT_PATH, 'admin2Codes.txt'); -export const geodataCities500Path = join(GEODATA_ROOT_PATH, citiesFile); + +const buildFolder = process.env.IMMICH_BUILD_DATA || '/build'; + +const folders = { + geodata: join(buildFolder, 'geodata'), + web: join(buildFolder, 'www'), +}; + +export const resourcePaths = { + lockFile: join(buildFolder, 'build-lock.json'), + geodata: { + dateFile: join(folders.geodata, 'geodata-date.txt'), + admin1: join(folders.geodata, 'admin1CodesASCII.txt'), + admin2: join(folders.geodata, 'admin2Codes.txt'), + cities500: join(folders.geodata, citiesFile), + }, + web: { + root: folders.web, + indexHtml: join(folders.web, 'index.html'), + }, +}; export const MOBILE_REDIRECT = 'app.immich:/'; export const LOGIN_URL = '/auth/login?autoLaunch=0'; diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts index 75ea8121fa..fbba3b6c53 100644 --- a/server/src/repositories/map.repository.ts +++ b/server/src/repositories/map.repository.ts @@ -4,7 +4,7 @@ import { getName } from 'i18n-iso-countries'; import { createReadStream, existsSync } from 'node:fs'; import { readFile } from 'node:fs/promises'; import readLine from 'node:readline'; -import { citiesFile, geodataAdmin1Path, geodataAdmin2Path, geodataCities500Path, geodataDatePath } from 'src/constants'; +import { citiesFile, resourcePaths } from 'src/constants'; import { AssetEntity } from 'src/entities/asset.entity'; import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity'; import { SystemMetadataKey } from 'src/entities/system-metadata.entity'; @@ -37,7 +37,7 @@ export class MapRepository implements IMapRepository { async init(): Promise { this.logger.log('Initializing metadata repository'); - const geodataDate = await readFile(geodataDatePath, 'utf8'); + const geodataDate = await readFile(resourcePaths.geodata.dateFile, 'utf8'); // TODO move to service init const geocodingMetadata = await this.metadataRepository.get(SystemMetadataKey.REVERSE_GEOCODING_STATE); @@ -150,8 +150,8 @@ export class MapRepository implements IMapRepository { const queryRunner = this.dataSource.createQueryRunner(); await queryRunner.connect(); - const admin1 = await this.loadAdmin(geodataAdmin1Path); - const admin2 = await this.loadAdmin(geodataAdmin2Path); + const admin1 = await this.loadAdmin(resourcePaths.geodata.admin1); + const admin2 = await this.loadAdmin(resourcePaths.geodata.admin2); try { await queryRunner.startTransaction(); @@ -221,7 +221,7 @@ export class MapRepository implements IMapRepository { admin1Name: admin1Map.get(`${lineSplit[8]}.${lineSplit[10]}`), admin2Name: admin2Map.get(`${lineSplit[8]}.${lineSplit[10]}.${lineSplit[11]}`), }), - geodataCities500Path, + resourcePaths.geodata.cities500, { entityFilter: (lineSplit) => lineSplit[7] != 'PPLX' }, ); } diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts index c4b1e664af..f74eb7dd0d 100644 --- a/server/src/repositories/server-info.repository.ts +++ b/server/src/repositories/server-info.repository.ts @@ -4,6 +4,7 @@ import { exec as execCallback } from 'node:child_process'; import { readFile } from 'node:fs/promises'; import { promisify } from 'node:util'; import sharp from 'sharp'; +import { resourcePaths } from 'src/constants'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface'; import { Instrumentation } from 'src/utils/instrumentation'; @@ -61,9 +62,9 @@ export class ServerInfoRepository implements IServerInfoRepository { maybeFirstLine('convert --version'), ]); - const lockfile = await readFile('build-lock.json') + const lockfile = await readFile(resourcePaths.lockFile) .then((buffer) => JSON.parse(buffer.toString())) - .catch(() => this.logger.warn('Failed to read build-lock.json')); + .catch(() => this.logger.warn(`Failed to read ${resourcePaths.lockFile}`)); return { nodejs: nodejsOutput || process.env.NODE_VERSION || '', diff --git a/server/src/services/api.service.ts b/server/src/services/api.service.ts index 06a6c41bc9..039dcb9aae 100644 --- a/server/src/services/api.service.ts +++ b/server/src/services/api.service.ts @@ -2,8 +2,7 @@ import { Inject, Injectable } from '@nestjs/common'; import { Cron, CronExpression, Interval } from '@nestjs/schedule'; import { NextFunction, Request, Response } from 'express'; import { readFileSync } from 'node:fs'; -import { join } from 'node:path'; -import { ONE_HOUR, WEB_ROOT } from 'src/constants'; +import { ONE_HOUR, resourcePaths } from 'src/constants'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { AuthService } from 'src/services/auth.service'; import { JobService } from 'src/services/job.service'; @@ -56,9 +55,9 @@ export class ApiService { ssr(excludePaths: string[]) { let index = ''; try { - index = readFileSync(join(WEB_ROOT, 'index.html')).toString(); + index = readFileSync(resourcePaths.web.indexHtml).toString(); } catch { - this.logger.warn('Unable to open `www/index.html, skipping SSR.'); + this.logger.warn(`Unable to open ${resourcePaths.web.indexHtml}, skipping SSR.`); } return async (request: Request, res: Response, next: NextFunction) => { diff --git a/server/src/workers/api.ts b/server/src/workers/api.ts index 30d440240f..8166665515 100644 --- a/server/src/workers/api.ts +++ b/server/src/workers/api.ts @@ -5,7 +5,7 @@ import cookieParser from 'cookie-parser'; import { existsSync } from 'node:fs'; import sirv from 'sirv'; import { ApiModule } from 'src/app.module'; -import { envName, excludePaths, isDev, serverVersion, WEB_ROOT } from 'src/constants'; +import { envName, excludePaths, isDev, resourcePaths, serverVersion } from 'src/constants'; import { ILoggerRepository } from 'src/interfaces/logger.interface'; import { WebSocketAdapter } from 'src/middleware/websocket.adapter'; import { ApiService } from 'src/services/api.service'; @@ -38,11 +38,11 @@ async function bootstrap() { useSwagger(app); app.setGlobalPrefix('api', { exclude: excludePaths }); - if (existsSync(WEB_ROOT)) { + if (existsSync(resourcePaths.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, { + sirv(resourcePaths.web.root, { etag: true, gzip: true, brotli: true,