From 6bbaba786665055c8ae4407a85fd2a8e153d3010 Mon Sep 17 00:00:00 2001
From: Jason Rasmussen <jason@rasm.me>
Date: Fri, 4 Oct 2024 17:09:02 -0400
Subject: [PATCH] refactor(server): resource paths (#13194)

---
 server/src/constants.ts                       | 23 -------------------
 server/src/interfaces/config.interface.ts     | 15 ++++++++++++
 server/src/repositories/config.repository.ts  | 22 ++++++++++++++++++
 server/src/repositories/map.repository.ts     |  9 +++++++-
 .../repositories/server-info.repository.ts    |  3 +--
 server/src/services/api.service.ts            |  6 ++++-
 server/src/workers/api.ts                     |  4 ++--
 .../repositories/config.repository.mock.ts    | 15 ++++++++++++
 8 files changed, 68 insertions(+), 29 deletions(-)

diff --git a/server/src/constants.ts b/server/src/constants.ts
index 26c9ef5a98..5317d5e13c 100644
--- a/server/src/constants.ts
+++ b/server/src/constants.ts
@@ -1,6 +1,5 @@
 import { Duration } from 'luxon';
 import { readFileSync } from 'node:fs';
-import { join } from 'node:path';
 import { SemVer } from 'semver';
 
 export const POSTGRES_VERSION_RANGE = '>=14.0.0';
@@ -26,28 +25,6 @@ export const DEFAULT_EXTERNAL_DOMAIN = 'http://localhost:' + HOST_SERVER_PORT;
 
 export const citiesFile = 'cities500.txt';
 
-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),
-    naturalEarthCountriesPath: join(folders.geodata, 'ne_10m_admin_0_countries.geojson'),
-  },
-  web: {
-    root: folders.web,
-    indexHtml: join(folders.web, 'index.html'),
-  },
-};
-
 export const MOBILE_REDIRECT = 'app.immich:///oauth-callback';
 export const LOGIN_URL = '/auth/login?autoLaunch=0';
 
diff --git a/server/src/interfaces/config.interface.ts b/server/src/interfaces/config.interface.ts
index 23a3803284..d105e40cf9 100644
--- a/server/src/interfaces/config.interface.ts
+++ b/server/src/interfaces/config.interface.ts
@@ -41,6 +41,21 @@ export interface EnvData {
     server: string;
   };
 
+  resourcePaths: {
+    lockFile: string;
+    geodata: {
+      dateFile: string;
+      admin1: string;
+      admin2: string;
+      cities500: string;
+      naturalEarthCountriesPath: string;
+    };
+    web: {
+      root: string;
+      indexHtml: string;
+    };
+  };
+
   storage: {
     ignoreMountCheckErrors: boolean;
   };
diff --git a/server/src/repositories/config.repository.ts b/server/src/repositories/config.repository.ts
index ed9d80a980..a9f9ca0c1d 100644
--- a/server/src/repositories/config.repository.ts
+++ b/server/src/repositories/config.repository.ts
@@ -1,4 +1,6 @@
 import { Injectable } from '@nestjs/common';
+import { join } from 'node:path';
+import { citiesFile } from 'src/constants';
 import { ImmichEnvironment, ImmichWorker, LogLevel } from 'src/enum';
 import { EnvData, IConfigRepository } from 'src/interfaces/config.interface';
 import { DatabaseExtension } from 'src/interfaces/database.interface';
@@ -41,6 +43,11 @@ export class ConfigRepository implements IConfigRepository {
 
     const environment = process.env.IMMICH_ENV as ImmichEnvironment;
     const isProd = environment === ImmichEnvironment.PRODUCTION;
+    const buildFolder = process.env.IMMICH_BUILD_DATA || '/build';
+    const folders = {
+      geodata: join(buildFolder, 'geodata'),
+      web: join(buildFolder, 'www'),
+    };
 
     return {
       port: Number(process.env.IMMICH_PORT) || 3001,
@@ -79,6 +86,21 @@ export class ConfigRepository implements IConfigRepository {
 
       licensePublicKey: isProd ? productionKeys : stagingKeys,
 
+      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),
+          naturalEarthCountriesPath: join(folders.geodata, 'ne_10m_admin_0_countries.geojson'),
+        },
+        web: {
+          root: folders.web,
+          indexHtml: join(folders.web, 'index.html'),
+        },
+      },
+
       storage: {
         ignoreMountCheckErrors: process.env.IMMICH_IGNORE_MOUNT_CHECK_ERRORS === 'true',
       },
diff --git a/server/src/repositories/map.repository.ts b/server/src/repositories/map.repository.ts
index 3508de720b..8ba9b4cab8 100644
--- a/server/src/repositories/map.repository.ts
+++ b/server/src/repositories/map.repository.ts
@@ -4,11 +4,12 @@ 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, resourcePaths } from 'src/constants';
+import { citiesFile } from 'src/constants';
 import { AssetEntity } from 'src/entities/asset.entity';
 import { GeodataPlacesEntity } from 'src/entities/geodata-places.entity';
 import { NaturalEarthCountriesEntity } from 'src/entities/natural-earth-countries.entity';
 import { SystemMetadataKey } from 'src/enum';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import {
   GeoPoint,
@@ -32,6 +33,7 @@ export class MapRepository implements IMapRepository {
     @InjectRepository(NaturalEarthCountriesEntity)
     private naturalEarthCountriesRepository: Repository<NaturalEarthCountriesEntity>,
     @InjectDataSource() private dataSource: DataSource,
+    @Inject(IConfigRepository) private configRepository: IConfigRepository,
     @Inject(ISystemMetadataRepository) private metadataRepository: ISystemMetadataRepository,
     @Inject(ILoggerRepository) private logger: ILoggerRepository,
   ) {
@@ -40,6 +42,7 @@ export class MapRepository implements IMapRepository {
 
   async init(): Promise<void> {
     this.logger.log('Initializing metadata repository');
+    const { resourcePaths } = this.configRepository.getEnv();
     const geodataDate = await readFile(resourcePaths.geodata.dateFile, 'utf8');
 
     // TODO move to service init
@@ -181,6 +184,8 @@ export class MapRepository implements IMapRepository {
     const queryRunner = this.dataSource.createQueryRunner();
     await queryRunner.connect();
 
+    const { resourcePaths } = this.configRepository.getEnv();
+
     try {
       await queryRunner.startTransaction();
       await queryRunner.manager.clear(NaturalEarthCountriesEntity);
@@ -225,6 +230,7 @@ export class MapRepository implements IMapRepository {
     const queryRunner = this.dataSource.createQueryRunner();
     await queryRunner.connect();
 
+    const { resourcePaths } = this.configRepository.getEnv();
     const admin1 = await this.loadAdmin(resourcePaths.geodata.admin1);
     const admin2 = await this.loadAdmin(resourcePaths.geodata.admin2);
 
@@ -280,6 +286,7 @@ export class MapRepository implements IMapRepository {
     admin1Map: Map<string, string>,
     admin2Map: Map<string, string>,
   ) {
+    const { resourcePaths } = this.configRepository.getEnv();
     await this.loadGeodataToTableFromFile(
       queryRunner,
       (lineSplit: string[]) =>
diff --git a/server/src/repositories/server-info.repository.ts b/server/src/repositories/server-info.repository.ts
index ae04f600c0..1936ecdb61 100644
--- a/server/src/repositories/server-info.repository.ts
+++ b/server/src/repositories/server-info.repository.ts
@@ -4,7 +4,6 @@ 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 { IConfigRepository } from 'src/interfaces/config.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { GitHubRelease, IServerInfoRepository, ServerBuildVersions } from 'src/interfaces/server-info.interface';
@@ -60,7 +59,7 @@ export class ServerInfoRepository implements IServerInfoRepository {
   }
 
   async getBuildVersions(): Promise<ServerBuildVersions> {
-    const { nodeVersion } = this.configRepository.getEnv();
+    const { nodeVersion, resourcePaths } = this.configRepository.getEnv();
 
     const [nodejsOutput, ffmpegOutput, magickOutput] = await Promise.all([
       maybeFirstLine('node --version'),
diff --git a/server/src/services/api.service.ts b/server/src/services/api.service.ts
index 039dcb9aae..66f8061d3c 100644
--- a/server/src/services/api.service.ts
+++ b/server/src/services/api.service.ts
@@ -2,7 +2,8 @@ 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 { ONE_HOUR, resourcePaths } from 'src/constants';
+import { ONE_HOUR } from 'src/constants';
+import { IConfigRepository } from 'src/interfaces/config.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
 import { AuthService } from 'src/services/auth.service';
 import { JobService } from 'src/services/job.service';
@@ -37,6 +38,7 @@ export class ApiService {
     private jobService: JobService,
     private sharedLinkService: SharedLinkService,
     private versionService: VersionService,
+    @Inject(IConfigRepository) private configRepository: IConfigRepository,
     @Inject(ILoggerRepository) private logger: ILoggerRepository,
   ) {
     this.logger.setContext(ApiService.name);
@@ -53,6 +55,8 @@ export class ApiService {
   }
 
   ssr(excludePaths: string[]) {
+    const { resourcePaths } = this.configRepository.getEnv();
+
     let index = '';
     try {
       index = readFileSync(resourcePaths.web.indexHtml).toString();
diff --git a/server/src/workers/api.ts b/server/src/workers/api.ts
index e5ce37dbab..7535a902b8 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 { excludePaths, resourcePaths, serverVersion } from 'src/constants';
+import { excludePaths, serverVersion } from 'src/constants';
 import { ImmichEnvironment } from 'src/enum';
 import { IConfigRepository } from 'src/interfaces/config.interface';
 import { ILoggerRepository } from 'src/interfaces/logger.interface';
@@ -36,7 +36,7 @@ async function bootstrap() {
   const logger = await app.resolve<ILoggerRepository>(ILoggerRepository);
   const configRepository = app.get<IConfigRepository>(IConfigRepository);
 
-  const { environment, port } = configRepository.getEnv();
+  const { environment, port, resourcePaths } = configRepository.getEnv();
   const isDev = environment === ImmichEnvironment.DEVELOPMENT;
 
   logger.setContext('Bootstrap');
diff --git a/server/test/repositories/config.repository.mock.ts b/server/test/repositories/config.repository.mock.ts
index 960a7c1e83..d44d50524a 100644
--- a/server/test/repositories/config.repository.mock.ts
+++ b/server/test/repositories/config.repository.mock.ts
@@ -25,6 +25,21 @@ const envData: EnvData = {
     server: 'server-public-key',
   },
 
+  resourcePaths: {
+    lockFile: 'build-lock.json',
+    geodata: {
+      dateFile: '/build/geodata/geodata-date.txt',
+      admin1: '/build/geodata/admin1CodesASCII.txt',
+      admin2: '/build/geodata/admin2Codes.txt',
+      cities500: '/build/geodata/cities500.txt',
+      naturalEarthCountriesPath: 'build/ne_10m_admin_0_countries.geojson',
+    },
+    web: {
+      root: '/build/www',
+      indexHtml: '/build/www/index.html',
+    },
+  },
+
   storage: {
     ignoreMountCheckErrors: false,
   },