refactor(server): take the storage roots from DataPathsService

The data-directory change said Nest classes inject DataPathsService, but
only AdminService did. StorageRegistryService still read the built-in
uploads and backups roots and the scratch dir from module constants
frozen at import, and StorageJobsService read the scratch dir the same
way.

The registry now takes DataPathsService through its constructor (a
hand-built one resolves the same layout), and the jobs service asks the
registry for its temp dir. The plain helper modules keep calling
resolveDataPaths() because they run without a container or are shared
with code that does: config.ts, the database path, index.ts, the logger,
the backup functions and the plugin trees, which the out-of-process
supervisor reads too. The app-config README now lists both sides, and
the nest README, server/CLAUDE.md and the provider's own comment say the
same.
This commit is contained in:
Maurice
2026-10-08 16:18:39 +02:00
parent d1b461a37f
commit fa0e6a3eae
7 changed files with 73 additions and 18 deletions
+21 -4
View File
@@ -60,10 +60,27 @@ tokens, `RuntimeEnvService`) and consumes the SAME derive functions.
`uploads/` (on `SERVER_ROOT`, the package directory, never on `process.cwd()`)
and names what lives in them: backups, the scratch dir, the logs, the JWT
secret and encryption key files, and the database file (TREK_DB_FILE when set,
else `data/travel.db`). Code that runs before the container exists imports it
directly; Nest classes inject `DataPathsService` (`src/nest/app-config/`),
which resolves the same layout once per built app. Never derive one of these
paths from `__dirname` in a new file: a moved file would quietly move the data.
else `data/travel.db`). Never derive one of these paths from `__dirname` in a
new file: a moved file would quietly move the data.
Two ways in, by where the code runs:
- **Nest providers inject `DataPathsService`** (`src/nest/app-config/`), which
resolves the same layout once per built app: `AdminService` (the JWT secret
file), `StorageRegistryService` (the built-in uploads and backups roots and
the scratch dir) and, through `StorageRegistryService.tempDir()`,
`StorageJobsService`. A new provider that needs a data path takes it the
same way.
- **Plain modules call `resolveDataPaths()`** because they run without a
container or are shared with code that does: `config.ts` key resolution,
`db/db-path.ts`, `index.ts`, the file logger, `demo/demo-reset.ts`, the
backup restore and archive functions (`nest/backup/backup.impl.ts`,
`auto-backup.settings.ts`, file I/O with no container state), the plugin
trees (`nest/plugins/paths.ts`, also read by the out-of-process supervisor),
and `nest/storage/storage-paths.ts`, which keeps the seed-config test seam
that `StorageAdminService` and the registry share. `files.constants.ts`
still exports `filesDir` for the tests that pin the layout; no production
code reads it.
## app-url.ts — instance base-URL resolution (moved 2026-07-28)
+4 -1
View File
@@ -378,7 +378,10 @@ sharing).
or read runtime-toggled values live through `RuntimeEnvService` / `readEnv()`
from `src/app-config`. Every variable has exactly one owner, a token or
`readEnv()`, and `config-ownership.test.ts` fails on a variable read through
both. Data paths come from `DataPathsService`. The classification and
both. A provider that reads a data path takes `DataPathsService`
(`AdminService`, `StorageRegistryService`, and through it the storage
jobs); plain helper modules call `resolveDataPaths()` (see
`src/app-config/README.md` for which and why). The classification and
invariants live in `src/app-config/README.md`.
## Parity gotchas worth remembering
@@ -5,11 +5,14 @@ import { resolveDataPaths, type DataPaths } from '../../app-config/data-paths';
* The data layout for Nest classes: the data directory, uploads, backups,
* scratch space, logs, the key files and the database file.
*
* Code that runs before the container exists (config.ts key resolution, the
* database connection, index.ts, the file logger) calls `resolveDataPaths()`
* directly; this provider hands the same answer to everything that can take a
* constructor argument, so neither side counts `..` hops from its own file.
* Resolved once per built app: none of these move while a process runs.
* Providers that read a data path take this one (AdminService,
* StorageRegistryService). Code that runs before the container exists or is
* shared with code outside it (config.ts key resolution, the database
* connection, index.ts, the file logger, the backup functions, the plugin
* trees) calls `resolveDataPaths()` directly; src/app-config/README.md keeps
* the list. Both get the same answer, and neither counts `..` hops from its
* own file. Resolved once per built app: none of these move while a process
* runs.
*/
@Injectable()
export class DataPathsService implements DataPaths {
@@ -11,7 +11,6 @@ import {
} from '@trek/shared';
import { contentTypeFor } from './content-type';
import { MirrorDriver } from './drivers/mirror.driver';
import { GLOBAL_TEMP_DIR } from './storage-paths';
import { StorageRegistryService } from './storage-registry.service';
import type { StorageDriver } from './storage.types';
@@ -252,7 +251,7 @@ export class StorageJobsService {
sourceKey: string,
destKey: string,
): Promise<boolean> {
const file = path.join(GLOBAL_TEMP_DIR, randomUUID());
const file = path.join(this.registry.tempDir(), randomUUID());
try {
const { stream } = await source.getStream(sourceKey);
await pipeline(stream, fs.createWriteStream(file)); // source errors RETHROW (abort → failed)
@@ -7,6 +7,7 @@ import { STORAGE_BACKEND_TYPES, storageConfigSchema } from '@trek/shared';
import { UnitOfWork } from '../database/unit-of-work';
import { withRequestContext } from '../database/request-context';
import { storageConfig } from '../app-config/tokens';
import { DataPathsService } from '../app-config/data-paths.service';
import { decrypt_api_key } from '../common/crypto/apiKeyCrypto';
import { AppSettings } from '../../db/entities/AppSettings.entity';
import type { AppSettingsRepository } from '../../db/repositories/AppSettings.repository';
@@ -14,7 +15,7 @@ import { LocalDriver } from './drivers/local.driver';
import { MirrorDriver, type ReplicaFailure } from './drivers/mirror.driver';
import { S3Driver } from './drivers/s3.driver';
import { StorageEventsService } from './storage-events.service';
import { DEFAULT_BACKUPS_ROOT, DEFAULT_UPLOADS_ROOT, GLOBAL_TEMP_DIR, getSeedConfigPath } from './storage-paths';
import { getSeedConfigPath } from './storage-paths';
import { assertNoMaskSentinels, encryptStorageSecrets } from './storage-secrets';
import {
SERVED_CATEGORIES,
@@ -141,6 +142,9 @@ export class StorageRegistryService implements OnModuleInit {
private readonly events: StorageEventsService,
private readonly uow: UnitOfWork,
private readonly orm: MikroORM,
// The built-in roots and the scratch dir. A hand-built registry (the unit
// suites) resolves the same layout the container's provider does.
private readonly dataPaths: DataPathsService = new DataPathsService(),
) {}
/**
@@ -199,7 +203,7 @@ export class StorageRegistryService implements OnModuleInit {
/** Driver-agnostic global scratch space (data/tmp). */
tempDir(): string {
return GLOBAL_TEMP_DIR;
return this.dataPaths.tmpDir;
}
/**
@@ -409,8 +413,8 @@ export class StorageRegistryService implements OnModuleInit {
// uploads-local's root is the computed default; relocation is a settings
// override row bearing the built-in's name.
const backends = new Map<string, BackendConfig>();
backends.set('uploads-local', { name: 'uploads-local', type: 'local', options: { root: DEFAULT_UPLOADS_ROOT } });
backends.set('backups-local', { name: 'backups-local', type: 'local', options: { root: DEFAULT_BACKUPS_ROOT } });
backends.set('uploads-local', { name: 'uploads-local', type: 'local', options: { root: this.dataPaths.uploadsDir } });
backends.set('backups-local', { name: 'backups-local', type: 'local', options: { root: this.dataPaths.backupsDir } });
const backendSources = new Map<string, BackendSource>([
['uploads-local', 'built-in'],
['backups-local', 'built-in'],
@@ -482,7 +486,7 @@ export class StorageRegistryService implements OnModuleInit {
}),
);
}
fs.mkdirSync(GLOBAL_TEMP_DIR, { recursive: true });
fs.mkdirSync(this.dataPaths.tmpDir, { recursive: true });
// 6. The effective world with provenance — assembled from the same merged
// maps, so it always matches what drivers/categories actually resolve to.