mirror of
https://github.com/unraid/api.git
synced 2026-02-04 23:19:04 -06:00
refactor: add & use ConfigFilePersister primitive (#1534)
Add `ConfigFilePersister<T>` to provide automatic JSON file persistence
for configs. It bridges the gap between in-memory configuration (via
`ConfigService`) and persistent file storage, with minimal developer
effort.
## Key Features
- **Reactive Persistence**: Automatically saves config changes to disk
when `ConfigService` updates
- **NestJS Integration**: Implements lifecycle hooks for proper
initialization and cleanup
- **Standalone Operations**: Provides direct file access via
`getFileHandler()` for non-reactive use cases
- **Change Detection**: Only writes to disk when configuration actually
changes (performance optimization)
- **Error Handling**: Includes logging and graceful error handling
throughout
## How to Implement
Extend the class and implement these required methods:
```typescript
@Injectable()
class MyConfigPersister extends ConfigFilePersister<MyConfigType> {
constructor(configService: ConfigService) {
super(configService);
}
// Required: JSON filename in config directory
fileName(): string {
return "my-config.json";
}
// Required: ConfigService key for reactive updates
configKey(): string {
return "myConfig";
}
// Required: Default values for new installations
defaultConfig(): MyConfigType {
return { enabled: false, timeout: 5000 };
}
// optionally, override validate() and/or migrateConfig()
}
```
## Lifecycle Behavior
- **Initialization** (`onModuleInit`): Loads config from disk → sets in
ConfigService → starts reactive subscription
- **Runtime**: Automatically persists to disk when ConfigService changes
(buffered every 25ms)
- **Shutdown** (`onModuleDestroy`): Final persistence and cleanup
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
## Summary by CodeRabbit
* **New Features**
* Introduced a unified, robust configuration file management system with
automatic migration, validation, and efficient persistence for plugins
and services.
* **Refactor**
* Centralized configuration persistence logic into a shared base class,
simplifying and standardizing config handling.
* Refactored multiple config persisters to extend the new base class,
removing redundant manual file and lifecycle management.
* Removed legacy config state management, persistence helpers, and
related modules, streamlining the codebase.
* Simplified test suites to focus on core functionality and error
handling.
* **Chores**
* Updated dependencies to support the new configuration management
system.
* Integrated the new API config module into plugin modules for
consistent configuration handling.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
@@ -1,81 +1,25 @@
|
||||
import { Logger, Injectable, OnModuleInit } from "@nestjs/common";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
import { existsSync, readFileSync } from "fs";
|
||||
import { writeFile } from "fs/promises";
|
||||
import path from "path";
|
||||
import { bufferTime } from "rxjs/operators";
|
||||
import { Injectable } from "@nestjs/common";
|
||||
import { ConfigFilePersister } from "@unraid/shared/services/config-file.js"; // npm install @unraid/shared
|
||||
import { PluginNameConfig } from "./config.entity.js";
|
||||
import { ConfigService } from "@nestjs/config";
|
||||
|
||||
@Injectable()
|
||||
export class PluginNameConfigPersister implements OnModuleInit {
|
||||
constructor(private readonly configService: ConfigService) {}
|
||||
|
||||
private logger = new Logger(PluginNameConfigPersister.name);
|
||||
|
||||
/** the file path to the config file for this plugin */
|
||||
get configPath() {
|
||||
return path.join(
|
||||
this.configService.get("PATHS_CONFIG_MODULES")!,
|
||||
"plugin-name.json" // Use kebab-case for the filename
|
||||
);
|
||||
export class PluginNameConfigPersister extends ConfigFilePersister<PluginNameConfig> {
|
||||
constructor(configService: ConfigService) {
|
||||
super(configService);
|
||||
}
|
||||
|
||||
onModuleInit() {
|
||||
this.logger.debug(`Config path: ${this.configPath}`);
|
||||
// Load the config from the file if it exists, otherwise initialize it with defaults.
|
||||
if (existsSync(this.configPath)) {
|
||||
try {
|
||||
const configFromFile = JSON.parse(
|
||||
readFileSync(this.configPath, "utf8")
|
||||
);
|
||||
this.configService.set("plugin-name", configFromFile);
|
||||
this.logger.verbose(`Config loaded from ${this.configPath}`);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error reading or parsing config file at ${this.configPath}. Using defaults.`,
|
||||
error
|
||||
);
|
||||
// If loading fails, ensure default config is set and persisted
|
||||
this.persist();
|
||||
}
|
||||
} else {
|
||||
this.logger.log(
|
||||
`Config file ${this.configPath} does not exist. Writing default config...`
|
||||
);
|
||||
// Persist the default configuration provided by configFeature
|
||||
this.persist();
|
||||
}
|
||||
|
||||
// Automatically persist changes to the config file after a short delay.
|
||||
this.configService.changes$.pipe(bufferTime(25)).subscribe({
|
||||
next: async (changes) => {
|
||||
const pluginNameConfigChanged = changes.some(({ path }) =>
|
||||
path.startsWith("plugin-name.")
|
||||
);
|
||||
if (pluginNameConfigChanged) {
|
||||
this.logger.verbose("Plugin config changed");
|
||||
await this.persist();
|
||||
}
|
||||
},
|
||||
error: (err) => {
|
||||
this.logger.error("Error receiving config changes:", err);
|
||||
},
|
||||
});
|
||||
fileName(): string {
|
||||
return "plugin-name.json"; // Use kebab-case for the filename
|
||||
}
|
||||
|
||||
async persist(
|
||||
config = this.configService.get<PluginNameConfig>("plugin-name")
|
||||
) {
|
||||
const data = JSON.stringify(config, null, 2);
|
||||
this.logger.verbose(`Persisting config to ${this.configPath}: ${data}`);
|
||||
try {
|
||||
await writeFile(this.configPath, data);
|
||||
this.logger.verbose(`Config change persisted to ${this.configPath}`);
|
||||
} catch (error) {
|
||||
this.logger.error(
|
||||
`Error persisting config to '${this.configPath}':`,
|
||||
error
|
||||
);
|
||||
}
|
||||
configKey(): string {
|
||||
return "plugin-name";
|
||||
}
|
||||
|
||||
defaultConfig(): PluginNameConfig {
|
||||
// Return the default configuration for your plugin
|
||||
// This should match the structure defined in your config.entity.ts
|
||||
return {} as PluginNameConfig;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user