@heronlabs/terminal-cli 2.0.0 → 2.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (25) hide show
  1. package/README.md +46 -1
  2. package/bin/src/application/cli/cli-module.js +4 -0
  3. package/bin/src/application/cli/commands/backup/easypanel-backup-command.js +57 -0
  4. package/bin/src/application/cli/commands/backup/mysql-backup-command.js +1 -1
  5. package/bin/src/application/cli/commands/backup/psql-backup-command.js +1 -1
  6. package/bin/src/application/cli/commands/rollup/easypanel-rollup-command.js +58 -0
  7. package/bin/src/application/cli/commands/rollup/mysql-rollup-command.js +1 -1
  8. package/bin/src/application/cli/commands/rollup/psql-rollup-command.js +1 -1
  9. package/bin/src/core/core-module.js +13 -4
  10. package/bin/src/core/services/easy-panel/easypanel-backup-service.js +61 -0
  11. package/bin/src/core/services/easy-panel/easypanel-backup.sh +8 -0
  12. package/bin/src/core/services/easy-panel/easypanel-rollup-service.js +57 -0
  13. package/bin/src/core/services/easy-panel/easypanel-rollup.sh +7 -0
  14. package/bin/src/core/services/{mysql-backup-service.js → mysql/mysql-backup-service.js} +15 -7
  15. package/bin/src/core/services/mysql/mysql-backup.sh +2 -0
  16. package/bin/src/core/services/{mysql-rollup-service.js → mysql/mysql-rollup-service.js} +15 -7
  17. package/bin/src/core/services/mysql/mysql-rollup.sh +2 -0
  18. package/bin/src/core/services/{psql-backup-service.js → psql/psql-backup-service.js} +15 -7
  19. package/bin/src/core/services/psql/psql-backup.sh +2 -0
  20. package/bin/src/core/services/{psql-rollup-service.js → psql/psql-rollup-service.js} +15 -7
  21. package/bin/src/core/services/psql/psql-rollup.sh +2 -0
  22. package/bin/src/core/services/script-loader-service.js +21 -0
  23. package/bin/src/infrastructure/environment/environment-module.js +1 -1
  24. package/bin/src/infrastructure/environment/services/environment-service.js +14 -5
  25. package/package.json +7 -5
package/README.md CHANGED
@@ -95,13 +95,15 @@ pnpm dep:cruise # architecture check
95
95
  | `hcli psql-rollup --filename <file>` | Restore a PostgreSQL database from a backup |
96
96
  | `hcli mysql-backup` | Back up a MySQL database (S3 by default) |
97
97
  | `hcli mysql-rollup --filename <file>` | Restore a MySQL database from a backup |
98
+ | `hcli easypanel-backup` | Snapshot an EasyPanel host — config + Docker state — to a `.tar.gz` (S3 by default). Run as root on the host. |
99
+ | `hcli easypanel-rollup --filename <file>` | Restore an EasyPanel host from a snapshot. Run as root on the host. **Overwrites host files.** |
98
100
  | `hcli version` | Print the current version |
99
101
 
100
102
  ### Flags
101
103
 
102
104
  | Flag | Applies to | Meaning |
103
105
  |---|---|---|
104
- | `-f, --filename <name>` | all | Backup filename. Backup defaults to `<database>-<timestamp>.sql.gz`; rollup requires it. |
106
+ | `-f, --filename <name>` | all | Backup filename. DB backups default to `<database>-<timestamp>.sql.gz` and `easypanel-backup` to `easypanel-<timestamp>.tar.gz`; rollup requires it. |
105
107
  | `--local` | all | Read/write the backup on the local filesystem instead of S3. |
106
108
 
107
109
  Examples:
@@ -118,6 +120,15 @@ hcli psql-rollup --local --filename seed.sql.gz
118
120
 
119
121
  # Restore from S3
120
122
  hcli mysql-rollup --filename mydb-2026-03-05T12-00-00Z.sql.gz
123
+
124
+ # Snapshot an EasyPanel host to S3 (run as root on the host)
125
+ sudo hcli easypanel-backup
126
+
127
+ # Snapshot to a local file with a custom name
128
+ sudo hcli easypanel-backup --local --filename easypanel-snapshot.tar.gz
129
+
130
+ # Restore an EasyPanel host from S3 (OVERWRITES host files)
131
+ sudo hcli easypanel-rollup --filename easypanel-2026-03-05T12-00-00Z.tar.gz
121
132
  ```
122
133
 
123
134
  ## Configuration
@@ -179,6 +190,40 @@ Local stack for manual testing:
179
190
  docker compose up # spins up psql + mysql DBs and CLI containers
180
191
  ```
181
192
 
193
+ ## EasyPanel host backup
194
+
195
+ `easypanel-backup` / `easypanel-rollup` snapshot a whole [EasyPanel](https://easypanel.io/)
196
+ host — `/etc/easypanel`, `/var/lib/docker/volumes`, and `/var/lib/docker/buildkit` —
197
+ into a single `.tar.gz`. To keep the snapshot consistent the command stops the
198
+ Docker daemon while the archive is taken and restarts it afterwards (even if the
199
+ backup fails).
200
+
201
+ Because it runs `systemctl stop docker` and reads `/var/lib/docker`, it must run
202
+ **natively on the host as root**, not inside a container managed by that daemon.
203
+ If not run as root the command refuses to start (it will not stop Docker and
204
+ then fail), so schedule it as root.
205
+
206
+ > ⚠️ **`easypanel-rollup` extracts the archive to `/` and overwrites host files**
207
+ > (`/etc/easypanel` and `/var/lib/docker`). It does not prompt — run it
208
+ > deliberately, against a host you intend to roll back.
209
+
210
+ ### Scheduling (every 12h)
211
+
212
+ Install the CLI on the host (Ubuntu 22.04) and match the DB cron containers'
213
+ cadence with a host crontab line:
214
+
215
+ ```bash
216
+ npm i -g @heronlabs/terminal-cli
217
+ ```
218
+
219
+ ```cron
220
+ # /etc/crontab — run as root, source the env first
221
+ 0 */12 * * * root . /etc/easypanel-backup.env; hcli easypanel-backup
222
+ ```
223
+
224
+ Put the `AWS_*` variables (the `easypanel-*` commands need only `AWS_*`, no DB
225
+ vars) in `/etc/easypanel-backup.env`.
226
+
182
227
  ## Testing
183
228
 
184
229
  | Detail | Value |
@@ -10,8 +10,10 @@ exports.CliModule = exports.cliModule = void 0;
10
10
  const common_1 = require("@nestjs/common");
11
11
  const core_module_1 = require("../../core/core-module");
12
12
  const log_module_1 = require("../../infrastructure/log/log-module");
13
+ const easypanel_backup_command_1 = require("./commands/backup/easypanel-backup-command");
13
14
  const mysql_backup_command_1 = require("./commands/backup/mysql-backup-command");
14
15
  const psql_backup_command_1 = require("./commands/backup/psql-backup-command");
16
+ const easypanel_rollup_command_1 = require("./commands/rollup/easypanel-rollup-command");
15
17
  const mysql_rollup_command_1 = require("./commands/rollup/mysql-rollup-command");
16
18
  const psql_rollup_command_1 = require("./commands/rollup/psql-rollup-command");
17
19
  const version_command_1 = require("./commands/version/version-command");
@@ -24,6 +26,8 @@ exports.cliModule = {
24
26
  psql_rollup_command_1.PsqlRollupCommand,
25
27
  mysql_backup_command_1.MysqlBackupCommand,
26
28
  mysql_rollup_command_1.MysqlRollupCommand,
29
+ easypanel_backup_command_1.EasypanelBackupCommand,
30
+ easypanel_rollup_command_1.EasypanelRollupCommand,
27
31
  ],
28
32
  };
29
33
  let CliModule = class CliModule {
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.EasypanelBackupCommand = void 0;
13
+ const nest_commander_1 = require("nest-commander");
14
+ const easypanel_backup_service_1 = require("../../../../core/services/easy-panel/easypanel-backup-service");
15
+ const backup_options_1 = require("./types/backup-options");
16
+ let EasypanelBackupCommand = class EasypanelBackupCommand extends nest_commander_1.CommandRunner {
17
+ easypanelBackupService;
18
+ parseFilename(val) {
19
+ return val;
20
+ }
21
+ parseLocal() {
22
+ return true;
23
+ }
24
+ async run(_args, options) {
25
+ await this.easypanelBackupService.run(options?.[backup_options_1.BackupOptionsKeys.LOCAL] ?? false, options?.[backup_options_1.BackupOptionsKeys.FILENAME]);
26
+ }
27
+ constructor(easypanelBackupService) {
28
+ super();
29
+ this.easypanelBackupService = easypanelBackupService;
30
+ }
31
+ };
32
+ exports.EasypanelBackupCommand = EasypanelBackupCommand;
33
+ __decorate([
34
+ (0, nest_commander_1.Option)({
35
+ flags: `-f, --${backup_options_1.BackupOptionsKeys.FILENAME} <filename>`,
36
+ description: 'Output filename (e.g. snapshot.tar.gz). Defaults to "easypanel-<timestamp>.tar.gz"',
37
+ }),
38
+ __metadata("design:type", Function),
39
+ __metadata("design:paramtypes", [String]),
40
+ __metadata("design:returntype", String)
41
+ ], EasypanelBackupCommand.prototype, "parseFilename", null);
42
+ __decorate([
43
+ (0, nest_commander_1.Option)({
44
+ flags: `--${backup_options_1.BackupOptionsKeys.LOCAL}`,
45
+ description: 'Save backup to the local filesystem instead of S3 (default: S3)',
46
+ }),
47
+ __metadata("design:type", Function),
48
+ __metadata("design:paramtypes", []),
49
+ __metadata("design:returntype", Boolean)
50
+ ], EasypanelBackupCommand.prototype, "parseLocal", null);
51
+ exports.EasypanelBackupCommand = EasypanelBackupCommand = __decorate([
52
+ (0, nest_commander_1.Command)({
53
+ name: 'easypanel-backup',
54
+ description: 'Backup EasyPanel host (config + Docker state)',
55
+ }),
56
+ __metadata("design:paramtypes", [easypanel_backup_service_1.EasypanelBackupService])
57
+ ], EasypanelBackupCommand);
@@ -11,7 +11,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.MysqlBackupCommand = void 0;
13
13
  const nest_commander_1 = require("nest-commander");
14
- const mysql_backup_service_1 = require("../../../../core/services/mysql-backup-service");
14
+ const mysql_backup_service_1 = require("../../../../core/services/mysql/mysql-backup-service");
15
15
  const backup_options_1 = require("./types/backup-options");
16
16
  let MysqlBackupCommand = class MysqlBackupCommand extends nest_commander_1.CommandRunner {
17
17
  mysqlBackupService;
@@ -11,7 +11,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.PsqlBackupCommand = void 0;
13
13
  const nest_commander_1 = require("nest-commander");
14
- const psql_backup_service_1 = require("../../../../core/services/psql-backup-service");
14
+ const psql_backup_service_1 = require("../../../../core/services/psql/psql-backup-service");
15
15
  const backup_options_1 = require("./types/backup-options");
16
16
  let PsqlBackupCommand = class PsqlBackupCommand extends nest_commander_1.CommandRunner {
17
17
  psqlBackupService;
@@ -0,0 +1,58 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.EasypanelRollupCommand = void 0;
13
+ const nest_commander_1 = require("nest-commander");
14
+ const easypanel_rollup_service_1 = require("../../../../core/services/easy-panel/easypanel-rollup-service");
15
+ const rollup_options_1 = require("./types/rollup-options");
16
+ let EasypanelRollupCommand = class EasypanelRollupCommand extends nest_commander_1.CommandRunner {
17
+ easypanelRollupService;
18
+ parseFilename(val) {
19
+ return val;
20
+ }
21
+ parseLocal() {
22
+ return true;
23
+ }
24
+ async run(_args, options) {
25
+ await this.easypanelRollupService.run(options[rollup_options_1.RollupOptionsKeys.FILENAME], options[rollup_options_1.RollupOptionsKeys.LOCAL] ?? false);
26
+ }
27
+ constructor(easypanelRollupService) {
28
+ super();
29
+ this.easypanelRollupService = easypanelRollupService;
30
+ }
31
+ };
32
+ exports.EasypanelRollupCommand = EasypanelRollupCommand;
33
+ __decorate([
34
+ (0, nest_commander_1.Option)({
35
+ flags: `-f, --${rollup_options_1.RollupOptionsKeys.FILENAME} <filename>`,
36
+ description: 'Backup filename to restore (e.g. easypanel-2026-03-05T12-00-00Z.tar.gz)',
37
+ required: true,
38
+ }),
39
+ __metadata("design:type", Function),
40
+ __metadata("design:paramtypes", [String]),
41
+ __metadata("design:returntype", String)
42
+ ], EasypanelRollupCommand.prototype, "parseFilename", null);
43
+ __decorate([
44
+ (0, nest_commander_1.Option)({
45
+ flags: `--${rollup_options_1.RollupOptionsKeys.LOCAL}`,
46
+ description: 'Read backup from the local filesystem instead of S3 (default: S3)',
47
+ }),
48
+ __metadata("design:type", Function),
49
+ __metadata("design:paramtypes", []),
50
+ __metadata("design:returntype", Boolean)
51
+ ], EasypanelRollupCommand.prototype, "parseLocal", null);
52
+ exports.EasypanelRollupCommand = EasypanelRollupCommand = __decorate([
53
+ (0, nest_commander_1.Command)({
54
+ name: 'easypanel-rollup',
55
+ description: 'Restore an EasyPanel host from a backup file (S3 by default, or local filesystem with --local). Overwrites host files.',
56
+ }),
57
+ __metadata("design:paramtypes", [easypanel_rollup_service_1.EasypanelRollupService])
58
+ ], EasypanelRollupCommand);
@@ -11,7 +11,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.MysqlRollupCommand = void 0;
13
13
  const nest_commander_1 = require("nest-commander");
14
- const mysql_rollup_service_1 = require("../../../../core/services/mysql-rollup-service");
14
+ const mysql_rollup_service_1 = require("../../../../core/services/mysql/mysql-rollup-service");
15
15
  const rollup_options_1 = require("./types/rollup-options");
16
16
  let MysqlRollupCommand = class MysqlRollupCommand extends nest_commander_1.CommandRunner {
17
17
  mysqlRollupService;
@@ -11,7 +11,7 @@ var __metadata = (this && this.__metadata) || function (k, v) {
11
11
  Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.PsqlRollupCommand = void 0;
13
13
  const nest_commander_1 = require("nest-commander");
14
- const psql_rollup_service_1 = require("../../../../core/services/psql-rollup-service");
14
+ const psql_rollup_service_1 = require("../../../../core/services/psql/psql-rollup-service");
15
15
  const rollup_options_1 = require("./types/rollup-options");
16
16
  let PsqlRollupCommand = class PsqlRollupCommand extends nest_commander_1.CommandRunner {
17
17
  psqlRollupService;
@@ -10,20 +10,29 @@ exports.CoreModule = void 0;
10
10
  const common_1 = require("@nestjs/common");
11
11
  const environment_module_1 = require("../infrastructure/environment/environment-module");
12
12
  const storage_module_1 = require("../infrastructure/storage/storage-module");
13
- const mysql_backup_service_1 = require("./services/mysql-backup-service");
14
- const mysql_rollup_service_1 = require("./services/mysql-rollup-service");
15
- const psql_backup_service_1 = require("./services/psql-backup-service");
16
- const psql_rollup_service_1 = require("./services/psql-rollup-service");
13
+ const easypanel_backup_service_1 = require("./services/easy-panel/easypanel-backup-service");
14
+ const easypanel_rollup_service_1 = require("./services/easy-panel/easypanel-rollup-service");
15
+ const mysql_backup_service_1 = require("./services/mysql/mysql-backup-service");
16
+ const mysql_rollup_service_1 = require("./services/mysql/mysql-rollup-service");
17
+ const psql_backup_service_1 = require("./services/psql/psql-backup-service");
18
+ const psql_rollup_service_1 = require("./services/psql/psql-rollup-service");
19
+ const script_loader_service_1 = require("./services/script-loader-service");
17
20
  const coreModule = {
18
21
  imports: [environment_module_1.EnvironmentModule, storage_module_1.StorageModule],
19
22
  providers: [
20
23
  common_1.Logger,
24
+ script_loader_service_1.ScriptLoaderService,
25
+ easypanel_backup_service_1.EasypanelBackupService,
26
+ easypanel_rollup_service_1.EasypanelRollupService,
21
27
  mysql_backup_service_1.MysqlBackupService,
22
28
  mysql_rollup_service_1.MysqlRollupService,
23
29
  psql_backup_service_1.PsqlBackupService,
24
30
  psql_rollup_service_1.PsqlRollupService,
25
31
  ],
26
32
  exports: [
33
+ script_loader_service_1.ScriptLoaderService,
34
+ easypanel_backup_service_1.EasypanelBackupService,
35
+ easypanel_rollup_service_1.EasypanelRollupService,
27
36
  mysql_backup_service_1.MysqlBackupService,
28
37
  mysql_rollup_service_1.MysqlRollupService,
29
38
  psql_backup_service_1.PsqlBackupService,
@@ -0,0 +1,61 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.EasypanelBackupService = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const child_process_1 = require("child_process");
15
+ const luxon_1 = require("luxon");
16
+ const s3_storage_service_1 = require("../../../infrastructure/storage/services/s3-storage-service");
17
+ const backup_service_1 = require("../../interfaces/backup-service");
18
+ const script_loader_service_1 = require("../script-loader-service");
19
+ let EasypanelBackupService = class EasypanelBackupService extends backup_service_1.BackupService {
20
+ logger;
21
+ s3StorageService;
22
+ scriptLoader;
23
+ async dump(filename) {
24
+ if (process.getuid?.() !== 0) {
25
+ return {
26
+ ok: false,
27
+ error: new Error('easypanel-backup must run as root (needs systemctl + /var/lib/docker access)'),
28
+ };
29
+ }
30
+ const timestamp = luxon_1.DateTime.utc().toFormat("yyyy-MM-dd'T'HH-mm-ss'Z'");
31
+ const backupFileName = filename ?? `easypanel-${timestamp}.tar.gz`;
32
+ try {
33
+ (0, child_process_1.execSync)(this.scriptLoader.load('easy-panel', 'easypanel-backup'), {
34
+ env: {
35
+ ...process.env,
36
+ ARCHIVE: backupFileName,
37
+ },
38
+ stdio: ['inherit', 'pipe', 'inherit'],
39
+ shell: '/bin/bash',
40
+ });
41
+ this.logger.log(`Backup EasyPanel host successfully! Filename: ${backupFileName}`);
42
+ return { ok: true, data: { backupFileName } };
43
+ }
44
+ catch {
45
+ return { ok: false, error: new Error('easypanel backup failed') };
46
+ }
47
+ }
48
+ constructor(logger, s3StorageService, scriptLoader) {
49
+ super(logger, s3StorageService);
50
+ this.logger = logger;
51
+ this.s3StorageService = s3StorageService;
52
+ this.scriptLoader = scriptLoader;
53
+ }
54
+ };
55
+ exports.EasypanelBackupService = EasypanelBackupService;
56
+ exports.EasypanelBackupService = EasypanelBackupService = __decorate([
57
+ (0, common_1.Injectable)(),
58
+ __metadata("design:paramtypes", [common_1.Logger,
59
+ s3_storage_service_1.S3StorageService,
60
+ script_loader_service_1.ScriptLoaderService])
61
+ ], EasypanelBackupService);
@@ -0,0 +1,8 @@
1
+ set -e
2
+ trap 'systemctl start docker' EXIT
3
+ systemctl stop docker.socket || true
4
+ systemctl stop docker
5
+ set -- /etc/easypanel /var/lib/docker/volumes
6
+ [ -d /var/lib/docker/buildkit ] && set -- "$@" /var/lib/docker/buildkit
7
+ tar czf "$ARCHIVE" --warning=no-file-changed "$@" 2>/dev/null || { rc=$?; [ "$rc" -le 1 ] || exit "$rc"; }
8
+ test -s "$ARCHIVE"
@@ -0,0 +1,57 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ var __metadata = (this && this.__metadata) || function (k, v) {
9
+ if (typeof Reflect === "object" && typeof Reflect.metadata === "function") return Reflect.metadata(k, v);
10
+ };
11
+ Object.defineProperty(exports, "__esModule", { value: true });
12
+ exports.EasypanelRollupService = void 0;
13
+ const common_1 = require("@nestjs/common");
14
+ const child_process_1 = require("child_process");
15
+ const s3_storage_service_1 = require("../../../infrastructure/storage/services/s3-storage-service");
16
+ const rollup_service_1 = require("../../interfaces/rollup-service");
17
+ const script_loader_service_1 = require("../script-loader-service");
18
+ let EasypanelRollupService = class EasypanelRollupService extends rollup_service_1.RollupService {
19
+ logger;
20
+ s3StorageService;
21
+ scriptLoader;
22
+ async restore(backupFileName) {
23
+ if (process.getuid?.() !== 0) {
24
+ return {
25
+ ok: false,
26
+ error: new Error('easypanel-rollup must run as root (needs systemctl + /var/lib/docker access)'),
27
+ };
28
+ }
29
+ try {
30
+ (0, child_process_1.execSync)(this.scriptLoader.load('easy-panel', 'easypanel-rollup'), {
31
+ env: {
32
+ ...process.env,
33
+ ARCHIVE: backupFileName,
34
+ },
35
+ stdio: ['inherit', 'pipe', 'inherit'],
36
+ shell: '/bin/bash',
37
+ });
38
+ return { ok: true, data: { backupFileName } };
39
+ }
40
+ catch {
41
+ return { ok: false, error: new Error('easypanel restore failed') };
42
+ }
43
+ }
44
+ constructor(logger, s3StorageService, scriptLoader) {
45
+ super(logger, s3StorageService);
46
+ this.logger = logger;
47
+ this.s3StorageService = s3StorageService;
48
+ this.scriptLoader = scriptLoader;
49
+ }
50
+ };
51
+ exports.EasypanelRollupService = EasypanelRollupService;
52
+ exports.EasypanelRollupService = EasypanelRollupService = __decorate([
53
+ (0, common_1.Injectable)(),
54
+ __metadata("design:paramtypes", [common_1.Logger,
55
+ s3_storage_service_1.S3StorageService,
56
+ script_loader_service_1.ScriptLoaderService])
57
+ ], EasypanelRollupService);
@@ -0,0 +1,7 @@
1
+ set -e
2
+ test -s "$ARCHIVE"
3
+ gzip -t "$ARCHIVE"
4
+ trap 'systemctl start docker' EXIT
5
+ systemctl stop docker.socket || true
6
+ systemctl stop docker
7
+ tar xzf "$ARCHIVE" -C /
@@ -13,19 +13,25 @@ exports.MysqlBackupService = void 0;
13
13
  const common_1 = require("@nestjs/common");
14
14
  const child_process_1 = require("child_process");
15
15
  const luxon_1 = require("luxon");
16
- const environment_service_1 = require("../../infrastructure/environment/services/environment-service");
17
- const s3_storage_service_1 = require("../../infrastructure/storage/services/s3-storage-service");
18
- const backup_service_1 = require("../interfaces/backup-service");
16
+ const environment_service_1 = require("../../../infrastructure/environment/services/environment-service");
17
+ const s3_storage_service_1 = require("../../../infrastructure/storage/services/s3-storage-service");
18
+ const backup_service_1 = require("../../interfaces/backup-service");
19
+ const script_loader_service_1 = require("../script-loader-service");
19
20
  let MysqlBackupService = class MysqlBackupService extends backup_service_1.BackupService {
20
21
  logger;
21
22
  environmentService;
22
23
  s3StorageService;
24
+ scriptLoader;
23
25
  async dump(filename) {
24
- const { host, port, name, user, password } = await this.environmentService.database();
26
+ const db = await this.environmentService.database();
27
+ if (!db.ok) {
28
+ return { ok: false, error: db.error };
29
+ }
30
+ const { host, port, name, user, password } = db.connection;
25
31
  const timestamp = luxon_1.DateTime.utc().toFormat("yyyy-MM-dd'T'HH-mm-ss'Z'");
26
32
  const backupFileName = filename ?? `${name}-${timestamp}.sql.gz`;
27
33
  try {
28
- (0, child_process_1.execSync)('set -o pipefail; mariadb-dump -u "$DB_USER" -h "$DB_HOST" -P "$DB_PORT" "$DB_NAME" | gzip > "$BACKUP_FILE"', {
34
+ (0, child_process_1.execSync)(this.scriptLoader.load('mysql', 'mysql-backup'), {
29
35
  env: {
30
36
  ...process.env,
31
37
  DB_USER: user,
@@ -45,11 +51,12 @@ let MysqlBackupService = class MysqlBackupService extends backup_service_1.Backu
45
51
  return { ok: false, error: new Error('mariadb-dump failed') };
46
52
  }
47
53
  }
48
- constructor(logger, environmentService, s3StorageService) {
54
+ constructor(logger, environmentService, s3StorageService, scriptLoader) {
49
55
  super(logger, s3StorageService);
50
56
  this.logger = logger;
51
57
  this.environmentService = environmentService;
52
58
  this.s3StorageService = s3StorageService;
59
+ this.scriptLoader = scriptLoader;
53
60
  }
54
61
  };
55
62
  exports.MysqlBackupService = MysqlBackupService;
@@ -57,5 +64,6 @@ exports.MysqlBackupService = MysqlBackupService = __decorate([
57
64
  (0, common_1.Injectable)(),
58
65
  __metadata("design:paramtypes", [common_1.Logger,
59
66
  environment_service_1.EnvironmentService,
60
- s3_storage_service_1.S3StorageService])
67
+ s3_storage_service_1.S3StorageService,
68
+ script_loader_service_1.ScriptLoaderService])
61
69
  ], MysqlBackupService);
@@ -0,0 +1,2 @@
1
+ set -o pipefail
2
+ mariadb-dump -u "$DB_USER" -h "$DB_HOST" -P "$DB_PORT" "$DB_NAME" | gzip > "$BACKUP_FILE"
@@ -12,17 +12,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.MysqlRollupService = void 0;
13
13
  const common_1 = require("@nestjs/common");
14
14
  const child_process_1 = require("child_process");
15
- const environment_service_1 = require("../../infrastructure/environment/services/environment-service");
16
- const s3_storage_service_1 = require("../../infrastructure/storage/services/s3-storage-service");
17
- const rollup_service_1 = require("../interfaces/rollup-service");
15
+ const environment_service_1 = require("../../../infrastructure/environment/services/environment-service");
16
+ const s3_storage_service_1 = require("../../../infrastructure/storage/services/s3-storage-service");
17
+ const rollup_service_1 = require("../../interfaces/rollup-service");
18
+ const script_loader_service_1 = require("../script-loader-service");
18
19
  let MysqlRollupService = class MysqlRollupService extends rollup_service_1.RollupService {
19
20
  logger;
20
21
  environmentService;
21
22
  s3StorageService;
23
+ scriptLoader;
22
24
  async restore(backupFileName) {
23
- const { host, port, name, user, password } = await this.environmentService.database();
25
+ const db = await this.environmentService.database();
26
+ if (!db.ok) {
27
+ return { ok: false, error: db.error };
28
+ }
29
+ const { host, port, name, user, password } = db.connection;
24
30
  try {
25
- (0, child_process_1.execSync)('set -o pipefail; gunzip -c "$BACKUP_FILE" | mariadb -u "$DB_USER" -h "$DB_HOST" -P "$DB_PORT" "$DB_NAME"', {
31
+ (0, child_process_1.execSync)(this.scriptLoader.load('mysql', 'mysql-rollup'), {
26
32
  env: {
27
33
  ...process.env,
28
34
  DB_USER: user,
@@ -41,11 +47,12 @@ let MysqlRollupService = class MysqlRollupService extends rollup_service_1.Rollu
41
47
  return { ok: false, error: new Error('mariadb restore failed') };
42
48
  }
43
49
  }
44
- constructor(logger, environmentService, s3StorageService) {
50
+ constructor(logger, environmentService, s3StorageService, scriptLoader) {
45
51
  super(logger, s3StorageService);
46
52
  this.logger = logger;
47
53
  this.environmentService = environmentService;
48
54
  this.s3StorageService = s3StorageService;
55
+ this.scriptLoader = scriptLoader;
49
56
  }
50
57
  };
51
58
  exports.MysqlRollupService = MysqlRollupService;
@@ -53,5 +60,6 @@ exports.MysqlRollupService = MysqlRollupService = __decorate([
53
60
  (0, common_1.Injectable)(),
54
61
  __metadata("design:paramtypes", [common_1.Logger,
55
62
  environment_service_1.EnvironmentService,
56
- s3_storage_service_1.S3StorageService])
63
+ s3_storage_service_1.S3StorageService,
64
+ script_loader_service_1.ScriptLoaderService])
57
65
  ], MysqlRollupService);
@@ -0,0 +1,2 @@
1
+ set -o pipefail
2
+ gunzip -c "$BACKUP_FILE" | mariadb -u "$DB_USER" -h "$DB_HOST" -P "$DB_PORT" "$DB_NAME"
@@ -13,19 +13,25 @@ exports.PsqlBackupService = void 0;
13
13
  const common_1 = require("@nestjs/common");
14
14
  const child_process_1 = require("child_process");
15
15
  const luxon_1 = require("luxon");
16
- const environment_service_1 = require("../../infrastructure/environment/services/environment-service");
17
- const s3_storage_service_1 = require("../../infrastructure/storage/services/s3-storage-service");
18
- const backup_service_1 = require("../interfaces/backup-service");
16
+ const environment_service_1 = require("../../../infrastructure/environment/services/environment-service");
17
+ const s3_storage_service_1 = require("../../../infrastructure/storage/services/s3-storage-service");
18
+ const backup_service_1 = require("../../interfaces/backup-service");
19
+ const script_loader_service_1 = require("../script-loader-service");
19
20
  let PsqlBackupService = class PsqlBackupService extends backup_service_1.BackupService {
20
21
  logger;
21
22
  environmentService;
22
23
  s3StorageService;
24
+ scriptLoader;
23
25
  async dump(filename) {
24
- const { host, port, name, user, password } = await this.environmentService.database();
26
+ const db = await this.environmentService.database();
27
+ if (!db.ok) {
28
+ return { ok: false, error: db.error };
29
+ }
30
+ const { host, port, name, user, password } = db.connection;
25
31
  const timestamp = luxon_1.DateTime.utc().toFormat("yyyy-MM-dd'T'HH-mm-ss'Z'");
26
32
  const backupFileName = filename ?? `${name}-${timestamp}.sql.gz`;
27
33
  try {
28
- (0, child_process_1.execSync)('set -o pipefail; pg_dump | gzip > "$BACKUP_FILE"', {
34
+ (0, child_process_1.execSync)(this.scriptLoader.load('psql', 'psql-backup'), {
29
35
  env: {
30
36
  ...process.env,
31
37
  PGHOST: host,
@@ -45,11 +51,12 @@ let PsqlBackupService = class PsqlBackupService extends backup_service_1.BackupS
45
51
  return { ok: false, error: new Error('pg_dump failed') };
46
52
  }
47
53
  }
48
- constructor(logger, environmentService, s3StorageService) {
54
+ constructor(logger, environmentService, s3StorageService, scriptLoader) {
49
55
  super(logger, s3StorageService);
50
56
  this.logger = logger;
51
57
  this.environmentService = environmentService;
52
58
  this.s3StorageService = s3StorageService;
59
+ this.scriptLoader = scriptLoader;
53
60
  }
54
61
  };
55
62
  exports.PsqlBackupService = PsqlBackupService;
@@ -57,5 +64,6 @@ exports.PsqlBackupService = PsqlBackupService = __decorate([
57
64
  (0, common_1.Injectable)(),
58
65
  __metadata("design:paramtypes", [common_1.Logger,
59
66
  environment_service_1.EnvironmentService,
60
- s3_storage_service_1.S3StorageService])
67
+ s3_storage_service_1.S3StorageService,
68
+ script_loader_service_1.ScriptLoaderService])
61
69
  ], PsqlBackupService);
@@ -0,0 +1,2 @@
1
+ set -o pipefail
2
+ pg_dump | gzip > "$BACKUP_FILE"
@@ -12,17 +12,23 @@ Object.defineProperty(exports, "__esModule", { value: true });
12
12
  exports.PsqlRollupService = void 0;
13
13
  const common_1 = require("@nestjs/common");
14
14
  const child_process_1 = require("child_process");
15
- const environment_service_1 = require("../../infrastructure/environment/services/environment-service");
16
- const s3_storage_service_1 = require("../../infrastructure/storage/services/s3-storage-service");
17
- const rollup_service_1 = require("../interfaces/rollup-service");
15
+ const environment_service_1 = require("../../../infrastructure/environment/services/environment-service");
16
+ const s3_storage_service_1 = require("../../../infrastructure/storage/services/s3-storage-service");
17
+ const rollup_service_1 = require("../../interfaces/rollup-service");
18
+ const script_loader_service_1 = require("../script-loader-service");
18
19
  let PsqlRollupService = class PsqlRollupService extends rollup_service_1.RollupService {
19
20
  logger;
20
21
  environmentService;
21
22
  s3StorageService;
23
+ scriptLoader;
22
24
  async restore(backupFileName) {
23
- const { host, port, name, user, password } = await this.environmentService.database();
25
+ const db = await this.environmentService.database();
26
+ if (!db.ok) {
27
+ return { ok: false, error: db.error };
28
+ }
29
+ const { host, port, name, user, password } = db.connection;
24
30
  try {
25
- (0, child_process_1.execSync)('set -o pipefail; gunzip -c "$BACKUP_FILE" | psql', {
31
+ (0, child_process_1.execSync)(this.scriptLoader.load('psql', 'psql-rollup'), {
26
32
  env: {
27
33
  ...process.env,
28
34
  PGHOST: host,
@@ -41,11 +47,12 @@ let PsqlRollupService = class PsqlRollupService extends rollup_service_1.RollupS
41
47
  return { ok: false, error: new Error('psql restore failed') };
42
48
  }
43
49
  }
44
- constructor(logger, environmentService, s3StorageService) {
50
+ constructor(logger, environmentService, s3StorageService, scriptLoader) {
45
51
  super(logger, s3StorageService);
46
52
  this.logger = logger;
47
53
  this.environmentService = environmentService;
48
54
  this.s3StorageService = s3StorageService;
55
+ this.scriptLoader = scriptLoader;
49
56
  }
50
57
  };
51
58
  exports.PsqlRollupService = PsqlRollupService;
@@ -53,5 +60,6 @@ exports.PsqlRollupService = PsqlRollupService = __decorate([
53
60
  (0, common_1.Injectable)(),
54
61
  __metadata("design:paramtypes", [common_1.Logger,
55
62
  environment_service_1.EnvironmentService,
56
- s3_storage_service_1.S3StorageService])
63
+ s3_storage_service_1.S3StorageService,
64
+ script_loader_service_1.ScriptLoaderService])
57
65
  ], PsqlRollupService);
@@ -0,0 +1,2 @@
1
+ set -o pipefail
2
+ gunzip -c "$BACKUP_FILE" | psql
@@ -0,0 +1,21 @@
1
+ "use strict";
2
+ var __decorate = (this && this.__decorate) || function (decorators, target, key, desc) {
3
+ var c = arguments.length, r = c < 3 ? target : desc === null ? desc = Object.getOwnPropertyDescriptor(target, key) : desc, d;
4
+ if (typeof Reflect === "object" && typeof Reflect.decorate === "function") r = Reflect.decorate(decorators, target, key, desc);
5
+ else for (var i = decorators.length - 1; i >= 0; i--) if (d = decorators[i]) r = (c < 3 ? d(r) : c > 3 ? d(target, key, r) : d(target, key)) || r;
6
+ return c > 3 && r && Object.defineProperty(target, key, r), r;
7
+ };
8
+ Object.defineProperty(exports, "__esModule", { value: true });
9
+ exports.ScriptLoaderService = void 0;
10
+ const common_1 = require("@nestjs/common");
11
+ const fs_1 = require("fs");
12
+ const path_1 = require("path");
13
+ let ScriptLoaderService = class ScriptLoaderService {
14
+ load(dir, name) {
15
+ return (0, fs_1.readFileSync)((0, path_1.join)(__dirname, dir, `${name}.sh`), 'utf8');
16
+ }
17
+ };
18
+ exports.ScriptLoaderService = ScriptLoaderService;
19
+ exports.ScriptLoaderService = ScriptLoaderService = __decorate([
20
+ (0, common_1.Injectable)()
21
+ ], ScriptLoaderService);
@@ -15,7 +15,7 @@ exports.environmentModule = {
15
15
  providers: [
16
16
  environment_service_1.EnvironmentService,
17
17
  {
18
- provide: env_ssm_1.SsmConfigService,
18
+ provide: env_ssm_1.ConfigService,
19
19
  useValue: env_ssm_1.SsmConfigFactory.make(),
20
20
  },
21
21
  ],
@@ -17,13 +17,19 @@ let EnvironmentService = class EnvironmentService {
17
17
  configService;
18
18
  ssmConfigService;
19
19
  async database() {
20
- const databaseUrl = await this.ssmConfigService.getOrThrow('DB_URL');
20
+ let databaseUrl;
21
+ try {
22
+ databaseUrl = await this.ssmConfigService.getOrThrow('DB_URL');
23
+ }
24
+ catch (error) {
25
+ return { ok: false, error: error };
26
+ }
21
27
  let url;
22
28
  try {
23
29
  url = new URL(databaseUrl);
24
30
  }
25
31
  catch {
26
- throw new Error('Invalid DB_URL');
32
+ return { ok: false, error: new Error('Invalid DB_URL') };
27
33
  }
28
34
  const connection = {
29
35
  host: url.hostname,
@@ -34,9 +40,12 @@ let EnvironmentService = class EnvironmentService {
34
40
  };
35
41
  const missing = ['host', 'name', 'user'].filter(field => connection[field] === '');
36
42
  if (missing.length > 0) {
37
- throw new Error(`Invalid DB_URL: missing ${missing.join(', ')}`);
43
+ return {
44
+ ok: false,
45
+ error: new Error(`Invalid DB_URL: missing ${missing.join(', ')}`),
46
+ };
38
47
  }
39
- return connection;
48
+ return { ok: true, connection };
40
49
  }
41
50
  get storage() {
42
51
  return {
@@ -52,5 +61,5 @@ exports.EnvironmentService = EnvironmentService;
52
61
  exports.EnvironmentService = EnvironmentService = __decorate([
53
62
  (0, common_1.Injectable)(),
54
63
  __metadata("design:paramtypes", [config_1.ConfigService,
55
- env_ssm_1.SsmConfigService])
64
+ env_ssm_1.ConfigService])
56
65
  ], EnvironmentService);
package/package.json CHANGED
@@ -8,7 +8,7 @@
8
8
  },
9
9
  "dependencies": {
10
10
  "@aws-sdk/client-s3": "^3.1045.0",
11
- "@heronlabs/env-ssm": "^2.0.1",
11
+ "@heronlabs/env-ssm": "^3.0.1",
12
12
  "@nestjs/common": "^11.1.18",
13
13
  "@nestjs/config": "^4.0.4",
14
14
  "@nestjs/core": "^11.1.18",
@@ -40,6 +40,7 @@
40
40
  "jiti": "^2.7.0",
41
41
  "moq.ts": "^10.0.8",
42
42
  "nest-commander-testing": "^3.5.1",
43
+ "shellcheck": "^4.1.0",
43
44
  "typescript": "~5.8.3",
44
45
  "typescript-eslint": "^8.60.0",
45
46
  "vitest": "^4.1.6"
@@ -47,7 +48,7 @@
47
48
  "engineStrict": true,
48
49
  "engines": {
49
50
  "node": ">=22",
50
- "pnpm": ">=10.29.3"
51
+ "pnpm": ">=10.33.2"
51
52
  },
52
53
  "files": [
53
54
  "bin"
@@ -87,12 +88,13 @@
87
88
  "scripts": {
88
89
  "build": "nest build --path tsconfig.bin.json",
89
90
  "dep:cruise": "depcruise src --config .dependency-cruiser.json",
90
- "lint:check": "gts lint && eslint \"**/*.json\" \"**/*.yml\" \"**/*.yaml\" --no-error-on-unmatched-pattern",
91
- "lint:fix": "gts fix && eslint \"**/*.json\" \"**/*.yml\" \"**/*.yaml\" --fix --no-error-on-unmatched-pattern",
91
+ "lint:check": "gts lint && eslint \"**/*.json\" \"**/*.yml\" \"**/*.yaml\" --no-error-on-unmatched-pattern && pnpm lint:shell",
92
+ "lint:fix": "gts fix && eslint \"**/*.json\" \"**/*.yml\" \"**/*.yaml\" --fix --no-error-on-unmatched-pattern && pnpm lint:shell",
93
+ "lint:shell": "find src -name '*.sh' -exec shellcheck {} +",
92
94
  "prepublishOnly": "pnpm build",
93
95
  "start": "dotenv -- node bin/src/main.js",
94
96
  "test:mutation": "stryker run stryker.conf.json",
95
97
  "test:unit": "vitest run"
96
98
  },
97
- "version": "2.0.0"
99
+ "version": "2.0.1"
98
100
  }