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
+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.