iobroker.script-restore 0.0.4 → 0.0.6

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/build/main.js CHANGED
@@ -27,6 +27,12 @@ var path = __toESM(require("node:path"));
27
27
  var os = __toESM(require("node:os"));
28
28
  var import_node_child_process = require("node:child_process");
29
29
  var import_node_util = require("node:util");
30
+ var ftp = __toESM(require("basic-ftp"));
31
+ var import_node_stream = require("node:stream");
32
+ var https = __toESM(require("node:https"));
33
+ var http = __toESM(require("node:http"));
34
+ var import_ssh2_sftp_client = __toESM(require("ssh2-sftp-client"));
35
+ const SMB2 = require("@marsaud/smb2");
30
36
  const execAsync = (0, import_node_util.promisify)(import_node_child_process.exec);
31
37
  class ScriptRestore extends utils.Adapter {
32
38
  constructor(options = {}) {
@@ -39,8 +45,7 @@ class ScriptRestore extends utils.Adapter {
39
45
  this.on("unload", this.onUnload.bind(this));
40
46
  }
41
47
  onReady() {
42
- const cfg = this.config;
43
- this.log.info(`Script Restore ready. Backup path: ${cfg.backupPath || "/opt/iobroker/backups"}`);
48
+ this.log.info(`Script Restore ready. Backup path: ${this.config.backupPath || "/opt/iobroker/backups"}`);
44
49
  }
45
50
  onUnload(callback) {
46
51
  callback();
@@ -60,6 +65,63 @@ class ScriptRestore extends utils.Adapter {
60
65
  case "parseUploadedFile":
61
66
  await this.handleParseUploadedFile(obj);
62
67
  break;
68
+ case "getSourceConfig":
69
+ this.sendTo(
70
+ obj.from,
71
+ obj.command,
72
+ {
73
+ localEnabled: this.config.localEnabled !== false,
74
+ ftpEnabled: !!this.config.ftpEnabled,
75
+ smbEnabled: !!this.config.smbEnabled,
76
+ httpEnabled: !!this.config.httpEnabled,
77
+ sftpEnabled: !!this.config.sftpEnabled,
78
+ webdavEnabled: !!this.config.webdavEnabled
79
+ },
80
+ obj.callback
81
+ );
82
+ break;
83
+ case "suggestBackupPath":
84
+ await this.handleSuggestBackupPath(obj);
85
+ break;
86
+ case "parseHttpUrl":
87
+ await this.handleParseHttpUrl(obj);
88
+ break;
89
+ case "testSftp":
90
+ await this.handleTestSftp(obj);
91
+ break;
92
+ case "listSftpFiles":
93
+ await this.handleListSftpFiles(obj);
94
+ break;
95
+ case "parseSftpFile":
96
+ await this.handleParseSftpFile(obj);
97
+ break;
98
+ case "testWebdav":
99
+ await this.handleTestWebdav(obj);
100
+ break;
101
+ case "listWebdavFiles":
102
+ await this.handleListWebdavFiles(obj);
103
+ break;
104
+ case "parseWebdavFile":
105
+ await this.handleParseWebdavFile(obj);
106
+ break;
107
+ case "testFtp":
108
+ await this.handleTestFtp(obj);
109
+ break;
110
+ case "testSmb":
111
+ await this.handleTestSmb(obj);
112
+ break;
113
+ case "listFtpFiles":
114
+ await this.handleListFtpFiles(obj);
115
+ break;
116
+ case "parseFtpFile":
117
+ await this.handleParseFtpFile(obj);
118
+ break;
119
+ case "listSmbFiles":
120
+ await this.handleListSmbFiles(obj);
121
+ break;
122
+ case "parseSmbFile":
123
+ await this.handleParseSmbFile(obj);
124
+ break;
63
125
  default:
64
126
  this.sendTo(obj.from, obj.command, { error: "Unknown command" }, obj.callback);
65
127
  }
@@ -68,9 +130,13 @@ class ScriptRestore extends utils.Adapter {
68
130
  this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
69
131
  }
70
132
  }
133
+ // ─── Local ───────────────────────────────────────────────────────────────
71
134
  async handleListLocalFiles(obj) {
72
- const cfg = this.config;
73
- const backupPath = cfg.backupPath || "/opt/iobroker/backups";
135
+ if (this.config.localEnabled === false) {
136
+ this.sendTo(obj.from, obj.command, { error: "Local source not enabled" }, obj.callback);
137
+ return;
138
+ }
139
+ const backupPath = this.config.backupPath || "/opt/iobroker/backups";
74
140
  try {
75
141
  const rawEntries = await fs.readdir(backupPath, { withFileTypes: true, encoding: "utf8" });
76
142
  const entries = rawEntries;
@@ -89,8 +155,11 @@ class ScriptRestore extends utils.Adapter {
89
155
  }
90
156
  }
91
157
  async handleParseLocalFile(obj) {
92
- const cfg = this.config;
93
- const backupPath = cfg.backupPath || "/opt/iobroker/backups";
158
+ if (this.config.localEnabled === false) {
159
+ this.sendTo(obj.from, obj.command, { error: "Local source not enabled" }, obj.callback);
160
+ return;
161
+ }
162
+ const backupPath = this.config.backupPath || "/opt/iobroker/backups";
94
163
  const msg = obj.message;
95
164
  const filename = path.basename(msg.filename);
96
165
  const filepath = path.join(backupPath, filename);
@@ -112,6 +181,193 @@ class ScriptRestore extends utils.Adapter {
112
181
  this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
113
182
  }
114
183
  }
184
+ // ─── Tests ───────────────────────────────────────────────────────────────
185
+ async handleTestFtp(obj) {
186
+ const msg = obj.message;
187
+ const client = new ftp.Client();
188
+ client.ftp.verbose = false;
189
+ try {
190
+ await client.access({
191
+ host: msg.host,
192
+ port: msg.port || 21,
193
+ user: msg.user || "anonymous",
194
+ password: msg.password || "",
195
+ secure: msg.secure || false
196
+ });
197
+ const list = await client.list(msg.path || "/");
198
+ const count = list.filter((i) => i.type === ftp.FileType.File).length;
199
+ this.sendTo(
200
+ obj.from,
201
+ obj.command,
202
+ { success: true, message: `Verbunden! ${count} Datei(en) gefunden in: ${msg.path || "/"}` },
203
+ obj.callback
204
+ );
205
+ } catch (e) {
206
+ this.sendTo(obj.from, obj.command, { success: false, message: e.message }, obj.callback);
207
+ } finally {
208
+ client.close();
209
+ }
210
+ }
211
+ async handleTestSmb(obj) {
212
+ const msg = obj.message;
213
+ const smb = new SMB2({
214
+ share: `\\\\${msg.host}\\${msg.share}`,
215
+ username: msg.user || "",
216
+ password: msg.password || "",
217
+ domain: msg.domain || ""
218
+ });
219
+ try {
220
+ const files = await this.smbReaddir(smb, msg.path || "");
221
+ this.sendTo(
222
+ obj.from,
223
+ obj.command,
224
+ {
225
+ success: true,
226
+ message: `Verbunden! ${files.length} Eintr\xE4ge in: \\\\${msg.host}\\${msg.share}${msg.path ? `\\${msg.path}` : ""}`
227
+ },
228
+ obj.callback
229
+ );
230
+ } catch (e) {
231
+ this.sendTo(obj.from, obj.command, { success: false, message: e.message }, obj.callback);
232
+ } finally {
233
+ smb.disconnect();
234
+ }
235
+ }
236
+ // ─── FTP ─────────────────────────────────────────────────────────────────
237
+ createFtpClient() {
238
+ const client = new ftp.Client();
239
+ client.ftp.verbose = false;
240
+ return client;
241
+ }
242
+ async ftpConnect(client) {
243
+ await client.access({
244
+ host: this.config.ftpHost,
245
+ port: this.config.ftpPort || 21,
246
+ user: this.config.ftpUser || "anonymous",
247
+ password: this.config.ftpPassword || "",
248
+ secure: this.config.ftpSecure || false
249
+ });
250
+ }
251
+ async handleListFtpFiles(obj) {
252
+ if (!this.config.ftpEnabled) {
253
+ this.sendTo(obj.from, obj.command, { error: "FTP not enabled" }, obj.callback);
254
+ return;
255
+ }
256
+ const client = this.createFtpClient();
257
+ try {
258
+ await this.ftpConnect(client);
259
+ const remotePath = this.config.ftpPath || "/";
260
+ const list = await client.list(remotePath);
261
+ const files = list.filter((item) => {
262
+ const n = item.name;
263
+ return item.type === ftp.FileType.File && (n.startsWith("iobroker") || n.startsWith("javascript")) && (n.endsWith(".tar.gz") || n.endsWith(".tar") || n.endsWith(".json") || n.endsWith(".jsonl"));
264
+ }).map((item) => item.name).sort().reverse();
265
+ this.sendTo(obj.from, obj.command, { files, path: remotePath }, obj.callback);
266
+ } catch (e) {
267
+ this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
268
+ } finally {
269
+ client.close();
270
+ }
271
+ }
272
+ async handleParseFtpFile(obj) {
273
+ if (!this.config.ftpEnabled) {
274
+ this.sendTo(obj.from, obj.command, { error: "FTP not enabled" }, obj.callback);
275
+ return;
276
+ }
277
+ const msg = obj.message;
278
+ const filename = path.basename(msg.filename);
279
+ const remotePath = path.posix.join(this.config.ftpPath || "/", filename);
280
+ const client = this.createFtpClient();
281
+ try {
282
+ await this.ftpConnect(client);
283
+ const chunks = [];
284
+ const writable = new import_node_stream.Writable({
285
+ write(chunk, _enc, cb) {
286
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
287
+ cb();
288
+ }
289
+ });
290
+ await client.downloadTo(writable, remotePath);
291
+ const buf = Buffer.concat(chunks);
292
+ const scripts = await this.parseBuffer(buf, filename);
293
+ this.sendTo(obj.from, obj.command, { scripts }, obj.callback);
294
+ } catch (e) {
295
+ this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
296
+ } finally {
297
+ client.close();
298
+ }
299
+ }
300
+ // ─── SMB ─────────────────────────────────────────────────────────────────
301
+ createSmbClient() {
302
+ return new SMB2({
303
+ share: `\\\\${this.config.smbHost}\\${this.config.smbShare}`,
304
+ username: this.config.smbUser || "",
305
+ password: this.config.smbPassword || "",
306
+ domain: this.config.smbDomain || ""
307
+ });
308
+ }
309
+ smbReaddir(smb, dirPath) {
310
+ return new Promise((resolve, reject) => {
311
+ smb.readdir(dirPath, (err, files) => {
312
+ if (err) {
313
+ reject(err);
314
+ } else {
315
+ resolve(files);
316
+ }
317
+ });
318
+ });
319
+ }
320
+ smbReadFile(smb, filePath) {
321
+ return new Promise((resolve, reject) => {
322
+ smb.readFile(filePath, (err, data) => {
323
+ if (err) {
324
+ reject(err);
325
+ } else {
326
+ resolve(data);
327
+ }
328
+ });
329
+ });
330
+ }
331
+ async handleListSmbFiles(obj) {
332
+ if (!this.config.smbEnabled) {
333
+ this.sendTo(obj.from, obj.command, { error: "SMB not enabled" }, obj.callback);
334
+ return;
335
+ }
336
+ const smb = this.createSmbClient();
337
+ try {
338
+ const smbPath = this.config.smbPath || "";
339
+ const entries = await this.smbReaddir(smb, smbPath);
340
+ const files = entries.filter((n) => {
341
+ return (n.startsWith("iobroker") || n.startsWith("javascript")) && (n.endsWith(".tar.gz") || n.endsWith(".tar") || n.endsWith(".json") || n.endsWith(".jsonl"));
342
+ }).sort().reverse();
343
+ this.sendTo(obj.from, obj.command, { files, path: smbPath }, obj.callback);
344
+ } catch (e) {
345
+ this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
346
+ } finally {
347
+ smb.disconnect();
348
+ }
349
+ }
350
+ async handleParseSmbFile(obj) {
351
+ if (!this.config.smbEnabled) {
352
+ this.sendTo(obj.from, obj.command, { error: "SMB not enabled" }, obj.callback);
353
+ return;
354
+ }
355
+ const msg = obj.message;
356
+ const filename = path.basename(msg.filename);
357
+ const smbPath = this.config.smbPath || "";
358
+ const filePath = smbPath ? `${smbPath}\\${filename}` : filename;
359
+ const smb = this.createSmbClient();
360
+ try {
361
+ const buf = await this.smbReadFile(smb, filePath);
362
+ const scripts = await this.parseBuffer(buf, filename);
363
+ this.sendTo(obj.from, obj.command, { scripts }, obj.callback);
364
+ } catch (e) {
365
+ this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
366
+ } finally {
367
+ smb.disconnect();
368
+ }
369
+ }
370
+ // ─── Parsing ─────────────────────────────────────────────────────────────
115
371
  async parseBuffer(buf, filename) {
116
372
  const name = filename.toLowerCase();
117
373
  if (name.endsWith(".tar.gz") || name.endsWith(".tgz") || name.endsWith(".tar")) {
@@ -151,10 +407,7 @@ class ScriptRestore extends utils.Adapter {
151
407
  const walk = async (d) => {
152
408
  let entries;
153
409
  try {
154
- entries = await fs.readdir(d, {
155
- withFileTypes: true,
156
- encoding: "utf8"
157
- });
410
+ entries = await fs.readdir(d, { withFileTypes: true, encoding: "utf8" });
158
411
  } catch {
159
412
  return null;
160
413
  }
@@ -245,6 +498,191 @@ class ScriptRestore extends utils.Adapter {
245
498
  source: typeof c.source === "string" ? c.source : ""
246
499
  });
247
500
  }
501
+ // ─── Suggest backup path ─────────────────────────────────────────────────
502
+ async handleSuggestBackupPath(obj) {
503
+ var _a;
504
+ const candidates = ["/opt/iobroker/backups", "/root/backups"];
505
+ try {
506
+ const backupObj = await this.getForeignObjectAsync("system.adapter.backitup.0");
507
+ if ((_a = backupObj == null ? void 0 : backupObj.native) == null ? void 0 : _a.defaultFolder) {
508
+ candidates.unshift(backupObj.native.defaultFolder);
509
+ }
510
+ } catch {
511
+ }
512
+ for (const p of candidates) {
513
+ try {
514
+ await fs.access(p);
515
+ this.sendTo(obj.from, obj.command, { path: p }, obj.callback);
516
+ return;
517
+ } catch {
518
+ }
519
+ }
520
+ this.sendTo(obj.from, obj.command, { path: null }, obj.callback);
521
+ }
522
+ // ─── HTTP ────────────────────────────────────────────────────────────────
523
+ downloadUrl(url) {
524
+ return new Promise((resolve, reject) => {
525
+ const mod = url.startsWith("https") ? https : http;
526
+ mod.get(url, (res) => {
527
+ if (res.statusCode !== 200) {
528
+ reject(new Error(`HTTP ${res.statusCode}`));
529
+ return;
530
+ }
531
+ const chunks = [];
532
+ res.on("data", (c) => chunks.push(c));
533
+ res.on("end", () => resolve(Buffer.concat(chunks)));
534
+ res.on("error", reject);
535
+ }).on("error", reject);
536
+ });
537
+ }
538
+ async handleParseHttpUrl(obj) {
539
+ if (!this.config.httpEnabled) {
540
+ this.sendTo(obj.from, obj.command, { error: "HTTP not enabled" }, obj.callback);
541
+ return;
542
+ }
543
+ const msg = obj.message;
544
+ const filename = msg.url.split("/").pop() || "backup";
545
+ try {
546
+ const buf = await this.downloadUrl(msg.url);
547
+ const scripts = await this.parseBuffer(buf, filename);
548
+ this.sendTo(obj.from, obj.command, { scripts }, obj.callback);
549
+ } catch (e) {
550
+ this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
551
+ }
552
+ }
553
+ // ─── SFTP ────────────────────────────────────────────────────────────────
554
+ async handleTestSftp(obj) {
555
+ const msg = obj.message;
556
+ const sftp = new import_ssh2_sftp_client.default();
557
+ try {
558
+ await sftp.connect({ host: msg.host, port: msg.port || 22, username: msg.user, password: msg.password });
559
+ const list = await sftp.list(msg.path || "/");
560
+ const count = list.filter((i) => i.type === "-").length;
561
+ this.sendTo(
562
+ obj.from,
563
+ obj.command,
564
+ { success: true, message: `Verbunden! ${count} Datei(en) in: ${msg.path || "/"}` },
565
+ obj.callback
566
+ );
567
+ } catch (e) {
568
+ this.sendTo(obj.from, obj.command, { success: false, message: e.message }, obj.callback);
569
+ } finally {
570
+ await sftp.end();
571
+ }
572
+ }
573
+ async handleListSftpFiles(obj) {
574
+ if (!this.config.sftpEnabled) {
575
+ this.sendTo(obj.from, obj.command, { error: "SFTP not enabled" }, obj.callback);
576
+ return;
577
+ }
578
+ const sftp = new import_ssh2_sftp_client.default();
579
+ try {
580
+ await sftp.connect({
581
+ host: this.config.sftpHost,
582
+ port: this.config.sftpPort || 22,
583
+ username: this.config.sftpUser,
584
+ password: this.config.sftpPassword
585
+ });
586
+ const remotePath = this.config.sftpPath || "/";
587
+ const list = await sftp.list(remotePath);
588
+ const files = list.filter((i) => {
589
+ const n = i.name;
590
+ return i.type === "-" && (n.startsWith("iobroker") || n.startsWith("javascript")) && (n.endsWith(".tar.gz") || n.endsWith(".tar") || n.endsWith(".json") || n.endsWith(".jsonl"));
591
+ }).map((i) => i.name).sort().reverse();
592
+ this.sendTo(obj.from, obj.command, { files, path: remotePath }, obj.callback);
593
+ } catch (e) {
594
+ this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
595
+ } finally {
596
+ await sftp.end();
597
+ }
598
+ }
599
+ async handleParseSftpFile(obj) {
600
+ if (!this.config.sftpEnabled) {
601
+ this.sendTo(obj.from, obj.command, { error: "SFTP not enabled" }, obj.callback);
602
+ return;
603
+ }
604
+ const msg = obj.message;
605
+ const filename = path.basename(msg.filename);
606
+ const remotePath = path.posix.join(this.config.sftpPath || "/", filename);
607
+ const sftp = new import_ssh2_sftp_client.default();
608
+ try {
609
+ await sftp.connect({
610
+ host: this.config.sftpHost,
611
+ port: this.config.sftpPort || 22,
612
+ username: this.config.sftpUser,
613
+ password: this.config.sftpPassword
614
+ });
615
+ const buf = await sftp.get(remotePath);
616
+ const scripts = await this.parseBuffer(buf, filename);
617
+ this.sendTo(obj.from, obj.command, { scripts }, obj.callback);
618
+ } catch (e) {
619
+ this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
620
+ } finally {
621
+ await sftp.end();
622
+ }
623
+ }
624
+ // ─── WebDAV ──────────────────────────────────────────────────────────────
625
+ async handleTestWebdav(obj) {
626
+ const msg = obj.message;
627
+ try {
628
+ const { createClient: createWebdavClient } = await Promise.resolve().then(() => __toESM(require("webdav")));
629
+ const client = createWebdavClient(msg.url, { username: msg.user, password: msg.password });
630
+ const list = await client.getDirectoryContents(msg.path || "/");
631
+ const arr = Array.isArray(list) ? list : list.data;
632
+ this.sendTo(
633
+ obj.from,
634
+ obj.command,
635
+ { success: true, message: `Verbunden! ${arr.length} Eintr\xE4ge in: ${msg.path || "/"}` },
636
+ obj.callback
637
+ );
638
+ } catch (e) {
639
+ this.sendTo(obj.from, obj.command, { success: false, message: e.message }, obj.callback);
640
+ }
641
+ }
642
+ async handleListWebdavFiles(obj) {
643
+ if (!this.config.webdavEnabled) {
644
+ this.sendTo(obj.from, obj.command, { error: "WebDAV not enabled" }, obj.callback);
645
+ return;
646
+ }
647
+ try {
648
+ const { createClient: createWebdavClient } = await Promise.resolve().then(() => __toESM(require("webdav")));
649
+ const client = createWebdavClient(this.config.webdavUrl, {
650
+ username: this.config.webdavUser,
651
+ password: this.config.webdavPassword
652
+ });
653
+ const remotePath = this.config.webdavPath || "/";
654
+ const list = await client.getDirectoryContents(remotePath);
655
+ const arr = Array.isArray(list) ? list : list.data;
656
+ const files = arr.filter((i) => {
657
+ const n = i.basename;
658
+ return i.type === "file" && (n.startsWith("iobroker") || n.startsWith("javascript")) && (n.endsWith(".tar.gz") || n.endsWith(".tar") || n.endsWith(".json") || n.endsWith(".jsonl"));
659
+ }).map((i) => i.basename).sort().reverse();
660
+ this.sendTo(obj.from, obj.command, { files, path: remotePath }, obj.callback);
661
+ } catch (e) {
662
+ this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
663
+ }
664
+ }
665
+ async handleParseWebdavFile(obj) {
666
+ if (!this.config.webdavEnabled) {
667
+ this.sendTo(obj.from, obj.command, { error: "WebDAV not enabled" }, obj.callback);
668
+ return;
669
+ }
670
+ const msg = obj.message;
671
+ const filename = path.basename(msg.filename);
672
+ try {
673
+ const { createClient: createWebdavClient } = await Promise.resolve().then(() => __toESM(require("webdav")));
674
+ const client = createWebdavClient(this.config.webdavUrl, {
675
+ username: this.config.webdavUser,
676
+ password: this.config.webdavPassword
677
+ });
678
+ const remotePath = (this.config.webdavPath ? `${this.config.webdavPath}/` : "/") + filename;
679
+ const buf = Buffer.from(await client.getFileContents(remotePath));
680
+ const scripts = await this.parseBuffer(buf, filename);
681
+ this.sendTo(obj.from, obj.command, { scripts }, obj.callback);
682
+ } catch (e) {
683
+ this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
684
+ }
685
+ }
248
686
  }
249
687
  if (require.main !== module) {
250
688
  module.exports = (options) => new ScriptRestore(options);