@webtypen/webframez-core 0.3.47 → 0.3.49

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]`
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`.
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
@@ -54,77 +54,48 @@ export declare function apiFunctionParamToJsonSchema(definition: ApiFunctionPara
54
54
  type: string;
55
55
  pattern?: undefined;
56
56
  items?: undefined;
57
- anyOf?: undefined;
57
+ additionalProperties?: undefined;
58
+ required?: undefined;
59
+ properties?: undefined;
58
60
  "x-webframez-type"?: undefined;
59
61
  enum?: undefined;
60
62
  } | {
61
63
  type: string;
62
64
  pattern: string;
63
65
  items?: undefined;
64
- anyOf?: undefined;
66
+ additionalProperties?: undefined;
67
+ required?: undefined;
68
+ properties?: undefined;
65
69
  "x-webframez-type"?: undefined;
66
70
  enum?: undefined;
67
71
  } | {
68
72
  type: string;
69
73
  items: {};
70
74
  pattern?: undefined;
71
- anyOf?: undefined;
75
+ additionalProperties?: undefined;
76
+ required?: undefined;
77
+ properties?: undefined;
72
78
  "x-webframez-type"?: undefined;
73
79
  enum?: undefined;
74
80
  } | {
75
- anyOf: ({
76
- type: string;
77
- additionalProperties: boolean;
78
- properties: {
79
- download_url: {
80
- type: string;
81
- };
82
- file_id: {
83
- type: string;
84
- };
85
- mime_type: {
86
- type: string;
87
- };
88
- file_name: {
89
- type: string;
90
- };
91
- url: {
92
- type: string;
93
- };
94
- download_link: {
95
- type: string;
96
- };
97
- file_url: {
98
- type: string;
99
- };
100
- content_base64: {
101
- type: string;
102
- };
103
- data_url: {
104
- type: string;
105
- };
106
- id: {
107
- type: string;
108
- };
109
- filename: {
110
- type: string;
111
- };
112
- name: {
113
- type: string;
114
- };
115
- mime: {
116
- type: string;
117
- };
81
+ type: string;
82
+ additionalProperties: boolean;
83
+ required: string[];
84
+ properties: {
85
+ download_url: {
86
+ type: string;
87
+ };
88
+ file_id: {
89
+ type: string;
118
90
  };
119
- description?: undefined;
120
- } | {
121
- type: string;
122
- description: string;
123
- additionalProperties?: undefined;
124
- properties?: undefined;
125
- })[];
91
+ mime_type: {
92
+ type: string;
93
+ };
94
+ file_name: {
95
+ type: string;
96
+ };
97
+ };
126
98
  "x-webframez-type": string;
127
- type?: undefined;
128
99
  pattern?: undefined;
129
100
  items?: undefined;
130
101
  enum?: undefined;
@@ -133,7 +104,9 @@ export declare function apiFunctionParamToJsonSchema(definition: ApiFunctionPara
133
104
  type?: undefined;
134
105
  pattern?: undefined;
135
106
  items?: undefined;
136
- anyOf?: undefined;
107
+ additionalProperties?: undefined;
108
+ required?: undefined;
109
+ properties?: undefined;
137
110
  "x-webframez-type"?: undefined;
138
111
  };
139
112
  export declare function toApiFunctionHttpResult(res: Response, result: any): Response;
@@ -254,10 +254,7 @@ function coerceApiFunctionParam(key, value, definition) {
254
254
  if (isPlainApiObject(value)) {
255
255
  return value;
256
256
  }
257
- if (typeof value === "string") {
258
- return value;
259
- }
260
- throw new ApiFunctionRuntimeError(`Parameter "${key}" must be a file reference string or object`, 400);
257
+ throw new ApiFunctionRuntimeError(`Parameter "${key}" must be an OpenAI file reference object`, 400);
261
258
  }
262
259
  return value;
263
260
  }
@@ -303,31 +300,15 @@ function apiFunctionParamToJsonSchema(definition) {
303
300
  return { type: "object" };
304
301
  if (type === "file") {
305
302
  return {
306
- anyOf: [
307
- {
308
- type: "object",
309
- additionalProperties: true,
310
- properties: {
311
- download_url: { type: "string" },
312
- file_id: { type: "string" },
313
- mime_type: { type: "string" },
314
- file_name: { type: "string" },
315
- url: { type: "string" },
316
- download_link: { type: "string" },
317
- file_url: { type: "string" },
318
- content_base64: { type: "string" },
319
- data_url: { type: "string" },
320
- id: { type: "string" },
321
- filename: { type: "string" },
322
- name: { type: "string" },
323
- mime: { type: "string" },
324
- },
325
- },
326
- {
327
- type: "string",
328
- description: "Connector runtime file reference string. Local paths must be rewritten by the ChatGPT connector before reaching the server.",
329
- },
330
- ],
303
+ type: "object",
304
+ additionalProperties: true,
305
+ required: ["download_url", "file_id"],
306
+ properties: {
307
+ download_url: { type: "string" },
308
+ file_id: { type: "string" },
309
+ mime_type: { type: "string" },
310
+ file_name: { type: "string" },
311
+ },
331
312
  "x-webframez-type": "file",
332
313
  };
333
314
  }
@@ -4,11 +4,14 @@ 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;
12
15
  getConfig(): BackupConfig;
13
16
  listTypes(): {
14
17
  key: string;
@@ -40,6 +40,12 @@ 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
+ }
43
49
  getConfig() {
44
50
  const config = Config_1.Config.get("backup");
45
51
  if (!config || typeof config !== "object") {
@@ -62,7 +68,7 @@ class BackupManager {
62
68
  }
63
69
  const defaults = config.defaults || {};
64
70
  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
71
+ 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
72
  ? backupType.cleanupWorkDir
67
73
  : defaults.cleanupWorkDir !== undefined
68
74
  ? defaults.cleanupWorkDir
@@ -191,28 +197,21 @@ class BackupManager {
191
197
  };
192
198
  });
193
199
  }
194
- buildArtifact(key, backupType, workRunDir, contentDir, date, id) {
200
+ buildArtifact(key, backupType, workRunDir, contentDir, date, id, options) {
195
201
  const filename = (0, BackupPathUtils_1.formatBackupFilename)(backupType.filename, { key, date, id });
196
202
  if (backupType.zip) {
197
203
  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);
204
+ this.log(options, `Creating ZIP artifact '${path_1.default.basename(zipPath)}'`, {
205
+ driver: backupType.zipDriver,
206
+ compressionLevel: backupType.zipCompressionLevel,
207
+ });
208
+ const zipResult = (0, ZipWriter_1.createZipFileFromDirectory)(zipPath, contentDir, {
209
+ driver: backupType.zipDriver,
210
+ compressionLevel: backupType.zipCompressionLevel,
211
+ });
212
+ this.log(options, `ZIP artifact created using ${zipResult.driver} zip`, {
213
+ path: zipPath,
214
+ });
216
215
  return {
217
216
  path: zipPath,
218
217
  filename: path_1.default.basename(zipPath),
@@ -228,10 +227,11 @@ class BackupManager {
228
227
  };
229
228
  }
230
229
  run(key, options) {
231
- var _a, _b;
230
+ var _a, _b, _c;
232
231
  return __awaiter(this, void 0, void 0, function* () {
233
232
  const backupType = this.resolveType(key);
234
233
  this.ensureActive(key, backupType);
234
+ this.log(options, `Starting backup '${key}'`);
235
235
  const now = new Date();
236
236
  const id = (0, BackupPathUtils_1.backupTimestampId)(now);
237
237
  const workRoot = (0, BackupPathUtils_1.resolveProjectPath)(backupType.workDir);
@@ -252,13 +252,17 @@ class BackupManager {
252
252
  files: [],
253
253
  databases: [],
254
254
  };
255
+ this.log(options, "Collecting file sources");
255
256
  const fileEntries = normalizeArray(backupType.files).flatMap((source) => this.collectFilesFromSource(source));
257
+ const fileSize = fileEntries.reduce((sum, entry) => sum + entry.size, 0);
258
+ this.log(options, `Collected ${fileEntries.length} file(s)`, { size: fileSize });
256
259
  result.files = fileEntries.map((entry) => ({
257
260
  from: entry.source,
258
261
  to: entry.target,
259
262
  size: entry.size,
260
263
  }));
261
264
  if (options === null || options === void 0 ? void 0 : options.dryRun) {
265
+ this.log(options, "Dry run enabled; skipping artifact and output writes");
262
266
  result.outputs = outputs.map((output) => ({
263
267
  driver: output.driver,
264
268
  status: "skipped",
@@ -275,28 +279,44 @@ class BackupManager {
275
279
  fs_1.default.rmSync(workRunDir, { recursive: true, force: true });
276
280
  fs_1.default.mkdirSync(contentDir, { recursive: true });
277
281
  try {
282
+ this.log(options, `Copying ${fileEntries.length} file(s) into backup work directory`);
278
283
  this.copyFiles(fileEntries, contentDir);
284
+ this.log(options, "File copy finished");
279
285
  for (const source of normalizeArray(backupType.databases)) {
286
+ this.log(options, `Backing up database '${source.connection || "default"}'`);
280
287
  result.databases.push(yield this.backupDatabase(source, contentDir, { backupKey: key, backupId: id }));
288
+ this.log(options, `Database '${source.connection || "default"}' backup finished`);
281
289
  }
282
- result.artifact = this.buildArtifact(key, backupType, workRunDir, contentDir, now, id);
290
+ result.artifact = this.buildArtifact(key, backupType, workRunDir, contentDir, now, id, options);
283
291
  const manifestPath = path_1.default.join(workRunDir, `${result.artifact.filename}.run-manifest.json`);
284
292
  result.manifestPath = manifestPath;
285
293
  for (const output of outputs) {
286
294
  const retention = mergeRetention((_a = this.getConfig().defaults) === null || _a === void 0 ? void 0 : _a.retention, backupType.retention, output.retention);
287
295
  const driver = BackupOutputDrivers_1.BackupOutputDrivers.get(output.driver);
296
+ this.log(options, `Writing artifact to '${output.driver}' output`, {
297
+ path: output.path,
298
+ folderPath: output.folderPath,
299
+ });
288
300
  const outputResult = yield driver.write(result.artifact, output, {
289
301
  backupKey: key,
290
302
  backupId: id,
291
303
  retention: retention,
292
304
  });
293
305
  result.outputs.push(outputResult);
306
+ this.log(options, `Output '${output.driver}' finished with status '${outputResult.status}'`, {
307
+ path: outputResult.path,
308
+ error: outputResult.error,
309
+ });
294
310
  if (outputResult.status === "success" && (retention === null || retention === void 0 ? void 0 : retention.runAfterBackup)) {
311
+ this.log(options, `Running cleanup for '${output.driver}' output`);
295
312
  result.cleanup.push(yield driver.cleanup(output, { backupKey: key, backupId: id, retention: retention }, { dryRun: false }));
313
+ this.log(options, `Cleanup for '${output.driver}' output finished`, {
314
+ deleted: ((_b = result.cleanup[result.cleanup.length - 1]) === null || _b === void 0 ? void 0 : _b.deleted.length) || 0,
315
+ });
296
316
  }
297
317
  }
298
318
  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;
319
+ const outputManifestPath = (_c = result.outputs.find((output) => output.payload && output.payload.manifestPath)) === null || _c === void 0 ? void 0 : _c.payload.manifestPath;
300
320
  if (outputManifestPath) {
301
321
  result.manifestPath = outputManifestPath;
302
322
  }
@@ -307,10 +327,12 @@ class BackupManager {
307
327
  fs_1.default.writeFileSync(output.payload.manifestPath, manifestJson, "utf-8");
308
328
  }
309
329
  }
330
+ this.log(options, `Backup '${key}' finished`);
310
331
  return result;
311
332
  }
312
333
  finally {
313
334
  if (backupType.cleanupWorkDir) {
335
+ this.log(options, "Cleaning backup work directory");
314
336
  fs_1.default.rmSync(workRunDir, { recursive: true, force: true });
315
337
  }
316
338
  }
@@ -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,8 @@ export type BackupRunResult = {
100
102
  export type BackupRunOptions = {
101
103
  dryRun?: boolean;
102
104
  channels?: string[];
105
+ silent?: boolean;
106
+ log?: (message: string, payload?: any) => void;
103
107
  };
104
108
  export type BackupCleanupOptions = {
105
109
  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,
@@ -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;
@@ -32,7 +32,12 @@ class BackupRunCommand extends ConsoleCommand_1.ConsoleCommand {
32
32
  const result = yield new BackupManager_1.BackupManager().run(key, {
33
33
  dryRun: this.getOption("dry-run") === true,
34
34
  channels: parseChannels(this.getOption("channel")),
35
+ silent: this.getOption("silent") === true,
36
+ log: (message) => this.writeln(` [color=grey]${message}[/color]`),
35
37
  });
38
+ if (this.getOption("silent") === true) {
39
+ return;
40
+ }
36
41
  if (result.dryRun) {
37
42
  this.info(`Dry run for backup '${key}' finished.`);
38
43
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@webtypen/webframez-core",
3
- "version": "0.3.47",
3
+ "version": "0.3.49",
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",