From 5f25e2ce824b9a7a82561a78005d36ccd2f412fa Mon Sep 17 00:00:00 2001
From: Jason Rasmussen <jrasm91@gmail.com>
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`<sup>\*1</sup>   | 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<void> {
     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,