@webtypen/webframez-core 0.3.48 → 0.3.50

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.
package/README.md CHANGED
@@ -617,6 +617,8 @@ export default {
617
617
  outputDir: "storage/backups",
618
618
  filename: "{key}_{date}_{time}",
619
619
  zip: true,
620
+ zipDriver: "auto",
621
+ zipCompressionLevel: 6,
620
622
  cleanupWorkDir: true,
621
623
  retention: {
622
624
  keepLast: 10,
@@ -665,10 +667,12 @@ export default {
665
667
 
666
668
  Built-in backup commands:
667
669
  - `backup:list`
668
- - `backup:run <key> [--dry-run] [--channel=local]`
670
+ - `backup:run <key> [--dry-run] [--channel=local] [--silent] [--log-interval=1000]`
669
671
  - `backup:queue <key> [--worker=worker_1] [--priority=10]`
670
672
  - `backup:cleanup <key> [--channel=local] [--dry-run]`
671
673
 
674
+ ZIP artifacts use the system `zip` binary by default when available and fall back to the built-in Node.js ZIP writer. Set `zipDriver: "system"` to require system zip, `zipDriver: "node"` to force the built-in writer, or `zipDriver: "auto"` for fallback behavior. `zipCompressionLevel` accepts `0` through `9`. Backup runs log file-scan and copy progress every 1000 files by default; change this with `--log-interval=250` or suppress progress with `--silent`.
675
+
672
676
  Remote outputs are optional packages and must be registered by the application before the backup config is used:
673
677
 
674
678
  ```ts
@@ -4,11 +4,15 @@ type ResolvedBackupTypeConfig = BackupTypeConfig & {
4
4
  outputDir: string;
5
5
  filename: string;
6
6
  zip: boolean;
7
+ zipDriver: "auto" | "system" | "node";
8
+ zipCompressionLevel?: number;
7
9
  cleanupWorkDir: boolean;
8
10
  retention?: BackupRetentionConfig;
9
11
  outputs: BackupOutputConfig[];
10
12
  };
11
13
  export declare class BackupManager {
14
+ private log;
15
+ private logInterval;
12
16
  getConfig(): BackupConfig;
13
17
  listTypes(): {
14
18
  key: string;
@@ -40,6 +40,15 @@ function normalizeArray(value) {
40
40
  return value && Array.isArray(value) ? value : [];
41
41
  }
42
42
  class BackupManager {
43
+ log(options, message, payload) {
44
+ if ((options === null || options === void 0 ? void 0 : options.silent) || !(options === null || options === void 0 ? void 0 : options.log)) {
45
+ return;
46
+ }
47
+ options.log(message, payload);
48
+ }
49
+ logInterval(options) {
50
+ return (options === null || options === void 0 ? void 0 : options.logInterval) && options.logInterval > 0 ? options.logInterval : 1000;
51
+ }
43
52
  getConfig() {
44
53
  const config = Config_1.Config.get("backup");
45
54
  if (!config || typeof config !== "object") {
@@ -62,7 +71,7 @@ class BackupManager {
62
71
  }
63
72
  const defaults = config.defaults || {};
64
73
  const outputDir = backupType.outputDir || defaults.outputDir || "storage/backups";
65
- return Object.assign(Object.assign(Object.assign({}, defaults), backupType), { workDir: backupType.workDir || defaults.workDir || "storage/backups/.work", outputDir: outputDir, filename: backupType.filename || defaults.filename || "{key}_{date}_{time}", zip: backupType.zip !== undefined ? backupType.zip : defaults.zip !== undefined ? defaults.zip : true, cleanupWorkDir: backupType.cleanupWorkDir !== undefined
74
+ return Object.assign(Object.assign(Object.assign({}, defaults), backupType), { workDir: backupType.workDir || defaults.workDir || "storage/backups/.work", outputDir: outputDir, filename: backupType.filename || defaults.filename || "{key}_{date}_{time}", zip: backupType.zip !== undefined ? backupType.zip : defaults.zip !== undefined ? defaults.zip : true, zipDriver: backupType.zipDriver || defaults.zipDriver || "auto", zipCompressionLevel: backupType.zipCompressionLevel !== undefined ? backupType.zipCompressionLevel : defaults.zipCompressionLevel, cleanupWorkDir: backupType.cleanupWorkDir !== undefined
66
75
  ? backupType.cleanupWorkDir
67
76
  : defaults.cleanupWorkDir !== undefined
68
77
  ? defaults.cleanupWorkDir
@@ -116,7 +125,7 @@ class BackupManager {
116
125
  return channels.includes(key) || channels.includes(output.driver);
117
126
  });
118
127
  }
119
- collectFilesFromSource(source) {
128
+ collectFilesFromSource(source, options) {
120
129
  const sourcePath = (0, BackupPathUtils_1.resolveProjectPath)(source.from);
121
130
  if (!fs_1.default.existsSync(sourcePath)) {
122
131
  if (source.optional) {
@@ -128,17 +137,28 @@ class BackupManager {
128
137
  const files = [];
129
138
  const sourceBase = sourceStats.isDirectory() ? sourcePath : path_1.default.dirname(sourcePath);
130
139
  const toBase = (0, BackupPathUtils_1.normalizeBackupPath)(source.to !== undefined ? source.to : sourceStats.isDirectory() ? path_1.default.basename(source.from) : "");
140
+ const interval = this.logInterval(options);
141
+ let visited = 0;
142
+ let included = 0;
131
143
  const addFile = (filepath) => {
144
+ visited += 1;
132
145
  const relative = (0, BackupPathUtils_1.normalizeBackupPath)(path_1.default.relative(sourceBase, filepath));
133
146
  if (!(0, GlobMatcher_1.matchesBackupGlobs)(relative, source.include, source.exclude)) {
147
+ if (visited % interval === 0) {
148
+ this.log(options, `Scanned ${visited} file(s), included ${included}`);
149
+ }
134
150
  return;
135
151
  }
152
+ included += 1;
136
153
  files.push({
137
154
  source: filepath,
138
155
  relative: relative,
139
156
  target: (0, BackupPathUtils_1.normalizeBackupPath)(path_1.default.join(toBase, relative)),
140
157
  size: fs_1.default.statSync(filepath).size,
141
158
  });
159
+ if (visited % interval === 0) {
160
+ this.log(options, `Scanned ${visited} file(s), included ${included}`);
161
+ }
142
162
  };
143
163
  const walk = (dir) => {
144
164
  for (const entry of fs_1.default.readdirSync(dir)) {
@@ -160,11 +180,17 @@ class BackupManager {
160
180
  }
161
181
  return files;
162
182
  }
163
- copyFiles(files, contentDir) {
164
- for (const file of files) {
183
+ copyFiles(files, contentDir, options) {
184
+ const interval = this.logInterval(options);
185
+ for (let index = 0; index < files.length; index += 1) {
186
+ const file = files[index];
165
187
  const targetPath = path_1.default.join(contentDir, file.target);
166
188
  fs_1.default.mkdirSync(path_1.default.dirname(targetPath), { recursive: true });
167
189
  fs_1.default.copyFileSync(file.source, targetPath);
190
+ const count = index + 1;
191
+ if (count % interval === 0 || count === files.length) {
192
+ this.log(options, `Copied ${count}/${files.length} file(s)`);
193
+ }
168
194
  }
169
195
  }
170
196
  backupDatabase(source, contentDir, context) {
@@ -191,28 +217,21 @@ class BackupManager {
191
217
  };
192
218
  });
193
219
  }
194
- buildArtifact(key, backupType, workRunDir, contentDir, date, id) {
220
+ buildArtifact(key, backupType, workRunDir, contentDir, date, id, options) {
195
221
  const filename = (0, BackupPathUtils_1.formatBackupFilename)(backupType.filename, { key, date, id });
196
222
  if (backupType.zip) {
197
223
  const zipPath = path_1.default.join(workRunDir, `${filename}.zip`);
198
- const files = [];
199
- const walk = (dir) => {
200
- for (const entry of fs_1.default.readdirSync(dir)) {
201
- const filepath = path_1.default.join(dir, entry);
202
- const stats = fs_1.default.statSync(filepath);
203
- if (stats.isDirectory()) {
204
- walk(filepath);
205
- }
206
- else if (stats.isFile()) {
207
- files.push({
208
- source: filepath,
209
- name: (0, BackupPathUtils_1.normalizeBackupPath)(path_1.default.relative(contentDir, filepath)),
210
- });
211
- }
212
- }
213
- };
214
- walk(contentDir);
215
- (0, ZipWriter_1.createZipFile)(zipPath, files);
224
+ this.log(options, `Creating ZIP artifact '${path_1.default.basename(zipPath)}'`, {
225
+ driver: backupType.zipDriver,
226
+ compressionLevel: backupType.zipCompressionLevel,
227
+ });
228
+ const zipResult = (0, ZipWriter_1.createZipFileFromDirectory)(zipPath, contentDir, {
229
+ driver: backupType.zipDriver,
230
+ compressionLevel: backupType.zipCompressionLevel,
231
+ });
232
+ this.log(options, `ZIP artifact created using ${zipResult.driver} zip`, {
233
+ path: zipPath,
234
+ });
216
235
  return {
217
236
  path: zipPath,
218
237
  filename: path_1.default.basename(zipPath),
@@ -228,10 +247,11 @@ class BackupManager {
228
247
  };
229
248
  }
230
249
  run(key, options) {
231
- var _a, _b;
250
+ var _a, _b, _c;
232
251
  return __awaiter(this, void 0, void 0, function* () {
233
252
  const backupType = this.resolveType(key);
234
253
  this.ensureActive(key, backupType);
254
+ this.log(options, `Starting backup '${key}'`);
235
255
  const now = new Date();
236
256
  const id = (0, BackupPathUtils_1.backupTimestampId)(now);
237
257
  const workRoot = (0, BackupPathUtils_1.resolveProjectPath)(backupType.workDir);
@@ -252,13 +272,17 @@ class BackupManager {
252
272
  files: [],
253
273
  databases: [],
254
274
  };
255
- const fileEntries = normalizeArray(backupType.files).flatMap((source) => this.collectFilesFromSource(source));
275
+ this.log(options, "Collecting file sources");
276
+ const fileEntries = normalizeArray(backupType.files).flatMap((source) => this.collectFilesFromSource(source, options));
277
+ const fileSize = fileEntries.reduce((sum, entry) => sum + entry.size, 0);
278
+ this.log(options, `Collected ${fileEntries.length} file(s)`, { size: fileSize });
256
279
  result.files = fileEntries.map((entry) => ({
257
280
  from: entry.source,
258
281
  to: entry.target,
259
282
  size: entry.size,
260
283
  }));
261
284
  if (options === null || options === void 0 ? void 0 : options.dryRun) {
285
+ this.log(options, "Dry run enabled; skipping artifact and output writes");
262
286
  result.outputs = outputs.map((output) => ({
263
287
  driver: output.driver,
264
288
  status: "skipped",
@@ -275,28 +299,50 @@ class BackupManager {
275
299
  fs_1.default.rmSync(workRunDir, { recursive: true, force: true });
276
300
  fs_1.default.mkdirSync(contentDir, { recursive: true });
277
301
  try {
278
- this.copyFiles(fileEntries, contentDir);
302
+ this.log(options, `Copying ${fileEntries.length} file(s) into backup work directory`);
303
+ this.copyFiles(fileEntries, contentDir, options);
304
+ this.log(options, "File copy finished");
279
305
  for (const source of normalizeArray(backupType.databases)) {
306
+ this.log(options, `Backing up database '${source.connection || "default"}'`);
280
307
  result.databases.push(yield this.backupDatabase(source, contentDir, { backupKey: key, backupId: id }));
308
+ this.log(options, `Database '${source.connection || "default"}' backup finished`);
281
309
  }
282
- result.artifact = this.buildArtifact(key, backupType, workRunDir, contentDir, now, id);
310
+ result.artifact = this.buildArtifact(key, backupType, workRunDir, contentDir, now, id, options);
283
311
  const manifestPath = path_1.default.join(workRunDir, `${result.artifact.filename}.run-manifest.json`);
284
312
  result.manifestPath = manifestPath;
285
313
  for (const output of outputs) {
286
314
  const retention = mergeRetention((_a = this.getConfig().defaults) === null || _a === void 0 ? void 0 : _a.retention, backupType.retention, output.retention);
287
315
  const driver = BackupOutputDrivers_1.BackupOutputDrivers.get(output.driver);
316
+ this.log(options, `Writing artifact to '${output.driver}' output`, {
317
+ path: output.path,
318
+ folderPath: output.folderPath,
319
+ });
288
320
  const outputResult = yield driver.write(result.artifact, output, {
289
321
  backupKey: key,
290
322
  backupId: id,
291
323
  retention: retention,
324
+ log: (message, payload) => this.log(options, message, payload),
292
325
  });
293
326
  result.outputs.push(outputResult);
327
+ this.log(options, `Output '${output.driver}' finished with status '${outputResult.status}'`, {
328
+ path: outputResult.path,
329
+ error: outputResult.error,
330
+ });
294
331
  if (outputResult.status === "success" && (retention === null || retention === void 0 ? void 0 : retention.runAfterBackup)) {
295
- result.cleanup.push(yield driver.cleanup(output, { backupKey: key, backupId: id, retention: retention }, { dryRun: false }));
332
+ this.log(options, `Running cleanup for '${output.driver}' output`);
333
+ result.cleanup.push(yield driver.cleanup(output, {
334
+ backupKey: key,
335
+ backupId: id,
336
+ retention: retention,
337
+ log: (message, payload) => this.log(options, message, payload),
338
+ }, { dryRun: false }));
339
+ this.log(options, `Cleanup for '${output.driver}' output finished`, {
340
+ deleted: ((_b = result.cleanup[result.cleanup.length - 1]) === null || _b === void 0 ? void 0 : _b.deleted.length) || 0,
341
+ });
296
342
  }
297
343
  }
298
344
  result.endedAt = new Date().toISOString();
299
- const outputManifestPath = (_b = result.outputs.find((output) => output.payload && output.payload.manifestPath)) === null || _b === void 0 ? void 0 : _b.payload.manifestPath;
345
+ const outputManifestPath = (_c = result.outputs.find((output) => output.payload && output.payload.manifestPath)) === null || _c === void 0 ? void 0 : _c.payload.manifestPath;
300
346
  if (outputManifestPath) {
301
347
  result.manifestPath = outputManifestPath;
302
348
  }
@@ -307,10 +353,12 @@ class BackupManager {
307
353
  fs_1.default.writeFileSync(output.payload.manifestPath, manifestJson, "utf-8");
308
354
  }
309
355
  }
356
+ this.log(options, `Backup '${key}' finished`);
310
357
  return result;
311
358
  }
312
359
  finally {
313
360
  if (backupType.cleanupWorkDir) {
361
+ this.log(options, "Cleaning backup work directory");
314
362
  fs_1.default.rmSync(workRunDir, { recursive: true, force: true });
315
363
  }
316
364
  }
@@ -332,6 +380,7 @@ class BackupManager {
332
380
  backupKey: key,
333
381
  backupId: (0, BackupPathUtils_1.backupTimestampId)(),
334
382
  retention: retention,
383
+ log: () => null,
335
384
  }, options));
336
385
  }
337
386
  return results;
@@ -32,6 +32,8 @@ export type BackupDefaultsConfig = {
32
32
  outputDir?: string;
33
33
  filename?: string;
34
34
  zip?: boolean;
35
+ zipDriver?: "auto" | "system" | "node";
36
+ zipCompressionLevel?: number;
35
37
  cleanupWorkDir?: boolean;
36
38
  retention?: BackupRetentionConfig;
37
39
  };
@@ -100,6 +102,9 @@ export type BackupRunResult = {
100
102
  export type BackupRunOptions = {
101
103
  dryRun?: boolean;
102
104
  channels?: string[];
105
+ silent?: boolean;
106
+ logInterval?: number;
107
+ log?: (message: string, payload?: any) => void;
103
108
  };
104
109
  export type BackupCleanupOptions = {
105
110
  dryRun?: boolean;
@@ -22,6 +22,7 @@ class BackupRunJob extends BaseQueueJob_1.BaseQueueJob {
22
22
  this.log(`Starting backup '${backupKey}'`);
23
23
  const result = yield new BackupManager_1.BackupManager().run(backupKey, {
24
24
  channels: job.payload.channels,
25
+ log: (message, payload) => this.log(message, payload),
25
26
  });
26
27
  this.log(`Backup '${backupKey}' finished`, {
27
28
  id: result.id,
@@ -3,6 +3,7 @@ export type BackupOutputDriverContext = {
3
3
  backupKey: string;
4
4
  backupId: string;
5
5
  retention?: BackupRetentionConfig;
6
+ log?: (message: string, payload?: any) => void;
6
7
  };
7
8
  export interface BaseBackupOutputDriver {
8
9
  write(artifact: BackupArtifact, output: BackupOutputConfig, context: BackupOutputDriverContext): Promise<BackupOutputResult>;
@@ -2,3 +2,13 @@ export declare function createZipFile(zipPath: string, files: Array<{
2
2
  source: string;
3
3
  name: string;
4
4
  }>): string;
5
+ export declare function createZipFileFromDirectory(zipPath: string, sourceDir: string, options?: {
6
+ driver?: "auto" | "system" | "node";
7
+ compressionLevel?: number;
8
+ }): {
9
+ path: string;
10
+ driver: "system";
11
+ } | {
12
+ path: string;
13
+ driver: "node";
14
+ };
@@ -3,10 +3,11 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- exports.createZipFile = void 0;
6
+ exports.createZipFileFromDirectory = exports.createZipFile = void 0;
7
7
  const fs_1 = __importDefault(require("fs"));
8
8
  const path_1 = __importDefault(require("path"));
9
9
  const zlib_1 = __importDefault(require("zlib"));
10
+ const child_process_1 = require("child_process");
10
11
  const BackupPathUtils_1 = require("./BackupPathUtils");
11
12
  const crcTable = (() => {
12
13
  const table = [];
@@ -118,3 +119,51 @@ function createZipFile(zipPath, files) {
118
119
  return zipPath;
119
120
  }
120
121
  exports.createZipFile = createZipFile;
122
+ function collectZipFiles(sourceDir) {
123
+ const files = [];
124
+ const walk = (dir) => {
125
+ for (const entry of fs_1.default.readdirSync(dir)) {
126
+ const filepath = path_1.default.join(dir, entry);
127
+ const stats = fs_1.default.statSync(filepath);
128
+ if (stats.isDirectory()) {
129
+ walk(filepath);
130
+ }
131
+ else if (stats.isFile()) {
132
+ files.push({
133
+ source: filepath,
134
+ name: (0, BackupPathUtils_1.normalizeBackupPath)(path_1.default.relative(sourceDir, filepath)),
135
+ });
136
+ }
137
+ }
138
+ };
139
+ walk(sourceDir);
140
+ return files;
141
+ }
142
+ function compressionArg(level) {
143
+ if (level === undefined || level === null || Number.isNaN(level)) {
144
+ return "-6";
145
+ }
146
+ const normalized = Math.max(0, Math.min(9, Math.round(level)));
147
+ return `-${normalized}`;
148
+ }
149
+ function createZipFileFromDirectory(zipPath, sourceDir, options) {
150
+ var _a;
151
+ fs_1.default.mkdirSync(path_1.default.dirname(zipPath), { recursive: true });
152
+ if ((options === null || options === void 0 ? void 0 : options.driver) !== "node") {
153
+ const args = ["-q", compressionArg(options === null || options === void 0 ? void 0 : options.compressionLevel), "-r", zipPath, "."];
154
+ const result = (0, child_process_1.spawnSync)("zip", args, {
155
+ cwd: sourceDir,
156
+ encoding: "utf-8",
157
+ });
158
+ if (result.status === 0 && fs_1.default.existsSync(zipPath)) {
159
+ return { path: zipPath, driver: "system" };
160
+ }
161
+ if ((options === null || options === void 0 ? void 0 : options.driver) === "system") {
162
+ const detail = result.stderr || result.stdout || ((_a = result.error) === null || _a === void 0 ? void 0 : _a.message) || "unknown error";
163
+ throw new Error(`System zip failed: ${detail}`);
164
+ }
165
+ }
166
+ createZipFile(zipPath, collectZipFiles(sourceDir));
167
+ return { path: zipPath, driver: "node" };
168
+ }
169
+ exports.createZipFileFromDirectory = createZipFileFromDirectory;
@@ -21,6 +21,13 @@ function parseChannels(value) {
21
21
  .map((entry) => entry.trim())
22
22
  .filter((entry) => entry !== "");
23
23
  }
24
+ function parsePositiveNumber(value) {
25
+ if (!value || typeof value !== "string") {
26
+ return undefined;
27
+ }
28
+ const parsed = parseInt(value, 10);
29
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : undefined;
30
+ }
24
31
  class BackupRunCommand extends ConsoleCommand_1.ConsoleCommand {
25
32
  handle() {
26
33
  return __awaiter(this, void 0, void 0, function* () {
@@ -32,7 +39,13 @@ class BackupRunCommand extends ConsoleCommand_1.ConsoleCommand {
32
39
  const result = yield new BackupManager_1.BackupManager().run(key, {
33
40
  dryRun: this.getOption("dry-run") === true,
34
41
  channels: parseChannels(this.getOption("channel")),
42
+ silent: this.getOption("silent") === true,
43
+ logInterval: parsePositiveNumber(this.getOption("log-interval")),
44
+ log: (message) => this.writeln(` [color=grey]${message}[/color]`),
35
45
  });
46
+ if (this.getOption("silent") === true) {
47
+ return;
48
+ }
36
49
  if (result.dryRun) {
37
50
  this.info(`Dry run for backup '${key}' finished.`);
38
51
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtypen/webframez-core",
3
- "version": "0.3.48",
3
+ "version": "0.3.50",
4
4
  "description": "The core package of the typescript based webframez (web-framework) by webtypen.de",
5
5
  "homepage": "https://webtypen.de/",
6
6
  "license": "MIT",