iobroker.script-restore 0.0.4 → 0.0.5

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
@@ -30,7 +30,7 @@ The archive is parsed entirely in the browser — no files are written to disk d
30
30
  - Supported formats: `.tar.gz`, `.tar`, `.json`, `.jsonl`
31
31
  - Tree view of all scripts organized by folder
32
32
  - Search across all script names
33
- - View source code with syntax highlighting (JS/TS/Blockly/Rules)
33
+ - View source code (JS/TS/Blockly/Rules)
34
34
  - Copy source code to clipboard or download as file
35
35
  - Fully browser-based parsing — no server roundtrip for uploads
36
36
 
@@ -75,8 +75,16 @@ The archive is parsed entirely in the browser — no files are written to disk d
75
75
  Placeholder for the next version (at the beginning of the line):
76
76
  ### **WORK IN PROGRESS**
77
77
  -->
78
- ### **WORK IN PROGRESS**
79
- * (ipod86) update README, fix SVG icon dimensions, add full language translations
78
+ ### 0.0.5 (2026-04-08)
79
+ * (ipod86) add FTP and SMB as optional backup sources with connection test button
80
+ * (ipod86) make local backup source optional (enable/disable in settings)
81
+ * (ipod86) add SMB version info (SMB2 only) in settings
82
+
83
+ ### 0.0.4 (2026-04-06)
84
+ * (ipod86) improve dark theme detection: live switching via MutationObserver and storage events
85
+
86
+ ### 0.0.3 (2026-04-06)
87
+ * (ipod86) add dark theme support for admin tab UI
80
88
 
81
89
  ### 0.0.1 (2026-04-06)
82
90
  * (ipod86) initial release
@@ -18,17 +18,27 @@
18
18
  <script type="text/javascript" src="words.js"></script>
19
19
 
20
20
  <script type="text/javascript">
21
+ // Checkboxes that are true by default when not yet configured
22
+ var defaultTrue = ['localEnabled'];
23
+
21
24
  function load(settings, onChange) {
22
25
  if (!settings) return;
23
26
  $('.value').each(function () {
24
27
  var $key = $(this);
25
28
  var id = $key.attr('id');
26
29
  if ($key.attr('type') === 'checkbox') {
27
- $key.prop('checked', settings[id]).on('change', () => onChange());
30
+ var val = defaultTrue.indexOf(id) !== -1
31
+ ? settings[id] !== false
32
+ : (settings[id] || false);
33
+ $key.prop('checked', val).on('change', function () {
34
+ onChange();
35
+ updateSections();
36
+ });
28
37
  } else {
29
- $key.val(settings[id]).on('change', () => onChange()).on('keyup', () => onChange());
38
+ $key.val(settings[id] || '').on('change', () => onChange()).on('keyup', () => onChange());
30
39
  }
31
40
  });
41
+ updateSections();
32
42
  onChange(false);
33
43
  if (M) M.updateTextFields();
34
44
  }
@@ -39,12 +49,75 @@
39
49
  var $this = $(this);
40
50
  if ($this.attr('type') === 'checkbox') {
41
51
  obj[$this.attr('id')] = $this.prop('checked');
52
+ } else if ($this.attr('type') === 'number') {
53
+ obj[$this.attr('id')] = parseInt($this.val(), 10) || 0;
42
54
  } else {
43
55
  obj[$this.attr('id')] = $this.val();
44
56
  }
45
57
  });
46
58
  callback(obj);
47
59
  }
60
+
61
+ function updateSections() {
62
+ var localEnabled = $('#localEnabled').prop('checked');
63
+ var ftpEnabled = $('#ftpEnabled').prop('checked');
64
+ var smbEnabled = $('#smbEnabled').prop('checked');
65
+ $('#localDetails').toggle(localEnabled);
66
+ $('#ftpDetails').toggle(ftpEnabled);
67
+ $('#smbDetails').toggle(smbEnabled);
68
+ if (M) M.updateTextFields();
69
+ }
70
+
71
+ function getAdapterInstance() {
72
+ var params = new URLSearchParams(window.location.search);
73
+ var id = params.get('id') || '';
74
+ var m = id.match(/\.(\d+)$/);
75
+ return m ? m[1] : '0';
76
+ }
77
+
78
+ function testConnection(type) {
79
+ var resultEl = document.getElementById(type + 'TestResult');
80
+ var btn = document.getElementById(type + 'TestBtn');
81
+ resultEl.style.display = 'none';
82
+ btn.disabled = true;
83
+ btn.textContent = '⏳ ...';
84
+
85
+ var data = {};
86
+ if (type === 'ftp') {
87
+ data = {
88
+ host: $('#ftpHost').val(),
89
+ port: parseInt($('#ftpPort').val(), 10) || 21,
90
+ user: $('#ftpUser').val(),
91
+ password: $('#ftpPassword').val(),
92
+ path: $('#ftpPath').val() || '/',
93
+ secure: $('#ftpSecure').prop('checked'),
94
+ };
95
+ } else {
96
+ data = {
97
+ host: $('#smbHost').val(),
98
+ share: $('#smbShare').val(),
99
+ path: $('#smbPath').val(),
100
+ user: $('#smbUser').val(),
101
+ password: $('#smbPassword').val(),
102
+ domain: $('#smbDomain').val(),
103
+ };
104
+ }
105
+
106
+ var inst = 'script-restore.' + getAdapterInstance();
107
+ var cmd = type === 'ftp' ? 'testFtp' : 'testSmb';
108
+ socket.emit('sendTo', inst, cmd, data, function (result) {
109
+ btn.disabled = false;
110
+ btn.textContent = _('testConnection');
111
+ resultEl.style.display = 'inline';
112
+ if (result && result.success) {
113
+ resultEl.style.color = '#388e3c';
114
+ resultEl.textContent = '✓ ' + result.message;
115
+ } else {
116
+ resultEl.style.color = '#d32f2f';
117
+ resultEl.textContent = '✗ ' + (result && result.message ? result.message : 'Fehler');
118
+ }
119
+ });
120
+ }
48
121
  </script>
49
122
 
50
123
  </head>
@@ -59,11 +132,132 @@
59
132
  </div>
60
133
  </div>
61
134
 
135
+ <!-- Local backup path -->
136
+ <div class="row" style="margin-top: 8px;">
137
+ <div class="col s12">
138
+ <h6 class="translate" style="font-weight:600; border-bottom: 1px solid #ccc; padding-bottom: 4px;">localSection</h6>
139
+ </div>
140
+ </div>
141
+ <div class="row">
142
+ <div class="col s12">
143
+ <input class="value filled-in" id="localEnabled" type="checkbox">
144
+ <label for="localEnabled" class="translate">localEnabled</label>
145
+ </div>
146
+ </div>
147
+ <div id="localDetails">
148
+ <div class="row">
149
+ <div class="input-field col s12 m8 l6">
150
+ <input class="value" id="backupPath" type="text" placeholder="/opt/iobroker/backups">
151
+ <label for="backupPath" class="translate">backupPath</label>
152
+ <span class="translate helper-text">backupPathHint</span>
153
+ </div>
154
+ </div>
155
+ </div>
156
+
157
+ <!-- FTP Section -->
158
+ <div class="row" style="margin-top: 24px;">
159
+ <div class="col s12">
160
+ <h6 class="translate" style="font-weight:600; border-bottom: 1px solid #ccc; padding-bottom: 4px;">ftpSection</h6>
161
+ </div>
162
+ </div>
62
163
  <div class="row">
63
- <div class="input-field col s12 m8 l6">
64
- <input class="value" id="backupPath" type="text" placeholder="/opt/iobroker/backups">
65
- <label for="backupPath" class="translate">backupPath</label>
66
- <span class="translate helper-text">backupPathHint</span>
164
+ <div class="col s12">
165
+ <input class="value filled-in" id="ftpEnabled" type="checkbox">
166
+ <label for="ftpEnabled" class="translate">ftpEnabled</label>
167
+ </div>
168
+ </div>
169
+ <div id="ftpDetails" style="display:none;">
170
+ <div class="row">
171
+ <div class="input-field col s12 m5 l4">
172
+ <input class="value" id="ftpHost" type="text" placeholder="192.168.1.100">
173
+ <label for="ftpHost" class="translate">ftpHost</label>
174
+ </div>
175
+ <div class="input-field col s6 m2 l1">
176
+ <input class="value" id="ftpPort" type="number" placeholder="21" min="1" max="65535">
177
+ <label for="ftpPort" class="translate">ftpPort</label>
178
+ </div>
179
+ <div class="col s6 m3 l2" style="padding-top: 20px;">
180
+ <input class="value filled-in" id="ftpSecure" type="checkbox">
181
+ <label for="ftpSecure" class="translate">ftpSecure</label>
182
+ </div>
183
+ </div>
184
+ <div class="row">
185
+ <div class="input-field col s12 m4 l3">
186
+ <input class="value" id="ftpUser" type="text" placeholder="anonymous">
187
+ <label for="ftpUser" class="translate">ftpUser</label>
188
+ </div>
189
+ <div class="input-field col s12 m4 l3">
190
+ <input class="value" id="ftpPassword" type="password" placeholder="">
191
+ <label for="ftpPassword" class="translate">ftpPassword</label>
192
+ </div>
193
+ <div class="input-field col s12 m4 l3">
194
+ <input class="value" id="ftpPath" type="text" placeholder="/backups">
195
+ <label for="ftpPath" class="translate">ftpPath</label>
196
+ </div>
197
+ </div>
198
+ <div class="row" style="margin-top: 4px;">
199
+ <div class="col s12">
200
+ <button id="ftpTestBtn" class="btn btn-small waves-effect waves-light" onclick="testConnection('ftp')" type="button">
201
+ <span class="translate">testConnection</span>
202
+ </button>
203
+ <span id="ftpTestResult" style="display:none; margin-left:12px; font-size:0.9rem; font-weight:500;"></span>
204
+ </div>
205
+ </div>
206
+ </div>
207
+
208
+ <!-- SMB Section -->
209
+ <div class="row" style="margin-top: 24px;">
210
+ <div class="col s12">
211
+ <h6 class="translate" style="font-weight:600; border-bottom: 1px solid #ccc; padding-bottom: 4px;">smbSection</h6>
212
+ </div>
213
+ </div>
214
+ <div class="row">
215
+ <div class="col s12">
216
+ <input class="value filled-in" id="smbEnabled" type="checkbox">
217
+ <label for="smbEnabled" class="translate">smbEnabled</label>
218
+ </div>
219
+ </div>
220
+ <div id="smbDetails" style="display:none;">
221
+ <div class="row">
222
+ <div class="col s12">
223
+ <span class="translate helper-text" style="color:#e65100; font-size:0.82rem;">smbVersionNote</span>
224
+ </div>
225
+ </div>
226
+ <div class="row" style="margin-top: 8px;">
227
+ <div class="input-field col s12 m5 l4">
228
+ <input class="value" id="smbHost" type="text" placeholder="192.168.1.100">
229
+ <label for="smbHost" class="translate">smbHost</label>
230
+ </div>
231
+ <div class="input-field col s12 m4 l3">
232
+ <input class="value" id="smbShare" type="text" placeholder="Backups">
233
+ <label for="smbShare" class="translate">smbShare</label>
234
+ </div>
235
+ <div class="input-field col s12 m3 l2">
236
+ <input class="value" id="smbPath" type="text" placeholder="">
237
+ <label for="smbPath" class="translate">smbPath</label>
238
+ </div>
239
+ </div>
240
+ <div class="row">
241
+ <div class="input-field col s12 m4 l3">
242
+ <input class="value" id="smbUser" type="text" placeholder="">
243
+ <label for="smbUser" class="translate">smbUser</label>
244
+ </div>
245
+ <div class="input-field col s12 m4 l3">
246
+ <input class="value" id="smbPassword" type="password" placeholder="">
247
+ <label for="smbPassword" class="translate">smbPassword</label>
248
+ </div>
249
+ <div class="input-field col s12 m4 l3">
250
+ <input class="value" id="smbDomain" type="text" placeholder="">
251
+ <label for="smbDomain" class="translate">smbDomain</label>
252
+ </div>
253
+ </div>
254
+ <div class="row" style="margin-top: 4px;">
255
+ <div class="col s12">
256
+ <button id="smbTestBtn" class="btn btn-small waves-effect waves-light" onclick="testConnection('smb')" type="button">
257
+ <span class="translate">testConnection</span>
258
+ </button>
259
+ <span id="smbTestResult" style="display:none; margin-left:12px; font-size:0.9rem; font-weight:500;"></span>
260
+ </div>
67
261
  </div>
68
262
  </div>
69
263
 
package/admin/tab_m.html CHANGED
@@ -225,7 +225,7 @@
225
225
  @keyframes spin { 100% { transform: rotate(360deg); } }
226
226
  #loaderText { color: #495057; font-size: 0.95rem; }
227
227
 
228
- /* Local files dropdown */
228
+ /* Files dropdown */
229
229
  .dropdown-wrapper { position: relative; }
230
230
  .dropdown-menu {
231
231
  display: none; position: absolute; top: 100%; left: 0; z-index: 1000;
@@ -322,12 +322,24 @@
322
322
  <input type="file" id="fileInput" accept=".tar,.gz,.tar.gz,.json,.jsonl">
323
323
  </label>
324
324
  <div class="dropdown-wrapper" id="localDropdown">
325
- <button class="btn btn-outline" onclick="toggleLocalFiles()">
325
+ <button class="btn btn-outline" onclick="toggleDropdown('local')">
326
326
  🗂️ Lokale Backups ▾
327
327
  </button>
328
328
  <div class="dropdown-menu" id="localMenu"></div>
329
329
  </div>
330
- <span class="status-msg" id="statusMsg">Backup laden oder lokale Datei wählen</span>
330
+ <div class="dropdown-wrapper" id="ftpDropdown" style="display:none;">
331
+ <button class="btn btn-outline" onclick="toggleDropdown('ftp')">
332
+ 🌐 FTP Backups ▾
333
+ </button>
334
+ <div class="dropdown-menu" id="ftpMenu"></div>
335
+ </div>
336
+ <div class="dropdown-wrapper" id="smbDropdown" style="display:none;">
337
+ <button class="btn btn-outline" onclick="toggleDropdown('smb')">
338
+ 🗄️ SMB Backups ▾
339
+ </button>
340
+ <div class="dropdown-menu" id="smbMenu"></div>
341
+ </div>
342
+ <span class="status-msg" id="statusMsg">Backup laden oder Quelle wählen</span>
331
343
  </div>
332
344
  </div>
333
345
 
@@ -712,53 +724,87 @@
712
724
  this.value = '';
713
725
  });
714
726
 
715
- // === Local Files ===
716
- let localMenuOpen = false;
727
+ // === Source Config ===
728
+ function loadSourceConfig(attempt) {
729
+ attempt = attempt || 0;
730
+ sendTo('getSourceConfig', {}, function(result) {
731
+ if (!result || result.error) {
732
+ if (attempt < 10) setTimeout(function() { loadSourceConfig(attempt + 1); }, 500);
733
+ return;
734
+ }
735
+ if (result.localEnabled === false) document.getElementById('localDropdown').style.display = 'none';
736
+ if (result.ftpEnabled) document.getElementById('ftpDropdown').style.display = '';
737
+ if (result.smbEnabled) document.getElementById('smbDropdown').style.display = '';
738
+ });
739
+ }
740
+ setTimeout(function() { loadSourceConfig(0); }, 300);
741
+
742
+ // === Dropdowns (local / ftp / smb) ===
743
+ const dropdownState = { local: false, ftp: false, smb: false };
744
+ const dropdownConfig = {
745
+ local: { listCmd: 'listLocalFiles', parseCmd: 'parseLocalFile', menuId: 'localMenu', wrapperId: 'localDropdown' },
746
+ ftp: { listCmd: 'listFtpFiles', parseCmd: 'parseFtpFile', menuId: 'ftpMenu', wrapperId: 'ftpDropdown' },
747
+ smb: { listCmd: 'listSmbFiles', parseCmd: 'parseSmbFile', menuId: 'smbMenu', wrapperId: 'smbDropdown' },
748
+ };
749
+
750
+ function toggleDropdown(src) {
751
+ const cfg = dropdownConfig[src];
752
+ const menu = document.getElementById(cfg.menuId);
753
+ const isOpen = dropdownState[src];
754
+
755
+ // Close all other open dropdowns first
756
+ Object.keys(dropdownState).forEach(k => {
757
+ if (k !== src && dropdownState[k]) {
758
+ dropdownState[k] = false;
759
+ document.getElementById(dropdownConfig[k].menuId).classList.remove('open');
760
+ }
761
+ });
717
762
 
718
- function toggleLocalFiles() {
719
- const menu = document.getElementById('localMenu');
720
- localMenuOpen = !localMenuOpen;
721
- if (localMenuOpen) {
763
+ dropdownState[src] = !isOpen;
764
+ if (dropdownState[src]) {
722
765
  menu.classList.add('open');
723
766
  menu.innerHTML = '<div class="dropdown-loading">⏳ Lade Dateiliste...</div>';
724
- sendTo('listLocalFiles', {}, function(result) {
767
+ sendTo(cfg.listCmd, {}, function(result) {
725
768
  if (result && result.error) {
726
769
  menu.innerHTML = '<div class="dropdown-empty">⚠️ ' + escapeHTML(result.error) + '</div>';
727
770
  } else if (result && result.files && result.files.length > 0) {
728
771
  menu.innerHTML = result.files.map(f =>
729
- '<div class="dropdown-item" data-file="' + escapeHTML(f) + '">' +
730
- escapeHTML(f) + '</div>'
772
+ '<div class="dropdown-item" data-file="' + escapeHTML(f) + '">' + escapeHTML(f) + '</div>'
731
773
  ).join('');
732
774
  menu.querySelectorAll('.dropdown-item').forEach(el => {
733
- el.addEventListener('click', function() { loadLocalFile(this.dataset.file); });
775
+ el.addEventListener('click', function() { loadRemoteFile(src, this.dataset.file); });
734
776
  });
735
777
  } else {
736
778
  menu.innerHTML = '<div class="dropdown-empty">Keine Dateien gefunden in:<br>' + escapeHTML((result && result.path) || '') + '</div>';
737
779
  }
738
780
  });
739
- // Close when clicking outside
740
- setTimeout(() => document.addEventListener('click', closeLocalMenuOutside), 0);
781
+ setTimeout(() => document.addEventListener('click', closeDropdownOutside), 0);
741
782
  } else {
742
783
  menu.classList.remove('open');
743
- document.removeEventListener('click', closeLocalMenuOutside);
744
784
  }
745
785
  }
746
786
 
747
- function closeLocalMenuOutside(e) {
748
- const wrapper = document.getElementById('localDropdown');
749
- if (!wrapper.contains(e.target)) {
750
- document.getElementById('localMenu').classList.remove('open');
751
- localMenuOpen = false;
752
- document.removeEventListener('click', closeLocalMenuOutside);
753
- }
787
+ function closeDropdownOutside(e) {
788
+ let anyOpen = false;
789
+ Object.keys(dropdownState).forEach(src => {
790
+ if (!dropdownState[src]) return;
791
+ const wrapper = document.getElementById(dropdownConfig[src].wrapperId);
792
+ if (!wrapper.contains(e.target)) {
793
+ dropdownState[src] = false;
794
+ document.getElementById(dropdownConfig[src].menuId).classList.remove('open');
795
+ } else {
796
+ anyOpen = true;
797
+ }
798
+ });
799
+ if (!anyOpen) document.removeEventListener('click', closeDropdownOutside);
754
800
  }
755
801
 
756
- function loadLocalFile(filename) {
757
- document.getElementById('localMenu').classList.remove('open');
758
- localMenuOpen = false;
759
- document.removeEventListener('click', closeLocalMenuOutside);
802
+ function loadRemoteFile(src, filename) {
803
+ const cfg = dropdownConfig[src];
804
+ document.getElementById(cfg.menuId).classList.remove('open');
805
+ dropdownState[src] = false;
760
806
  showLoaderSpinner('Lade und verarbeite ' + filename + '...');
761
- sendTo('parseLocalFile', { filename: filename }, function(result) {
807
+ sendTo(cfg.parseCmd, { filename: filename }, function(result) {
762
808
  hideLoader();
763
809
  if (result && result.error) {
764
810
  setStatus('Fehler: ' + result.error, 'error');
package/admin/words.js CHANGED
@@ -43,4 +43,264 @@ systemDictionary = {
43
43
  uk: "Каталог, де зберігаються файли резервних копій ioBroker",
44
44
  "zh-cn": "存储 ioBroker 备份文件的目录",
45
45
  },
46
+ "localEnabled": {
47
+ en: "Enable local source",
48
+ de: "Lokale Quelle aktivieren",
49
+ ru: "Включить локальный источник",
50
+ pt: "Ativar fonte local",
51
+ nl: "Lokale bron inschakelen",
52
+ fr: "Activer la source locale",
53
+ it: "Abilita sorgente locale",
54
+ es: "Activar fuente local",
55
+ pl: "Włącz źródło lokalne",
56
+ uk: "Увімкнути локальне джерело",
57
+ "zh-cn": "启用本地来源",
58
+ },
59
+ "localSection": {
60
+ en: "Local backup path",
61
+ de: "Lokaler Backup-Pfad",
62
+ ru: "Локальный путь к резервным копиям",
63
+ pt: "Caminho de backup local",
64
+ nl: "Lokaal back-uppad",
65
+ fr: "Chemin de sauvegarde local",
66
+ it: "Percorso di backup locale",
67
+ es: "Ruta de copia de seguridad local",
68
+ pl: "Lokalna ścieżka kopii zapasowej",
69
+ uk: "Локальний шлях до резервних копій",
70
+ "zh-cn": "本地备份路径",
71
+ },
72
+ "testConnection": {
73
+ en: "Test connection",
74
+ de: "Verbindung testen",
75
+ ru: "Проверить подключение",
76
+ pt: "Testar ligação",
77
+ nl: "Verbinding testen",
78
+ fr: "Tester la connexion",
79
+ it: "Testa connessione",
80
+ es: "Probar conexión",
81
+ pl: "Testuj połączenie",
82
+ uk: "Перевірити з'єднання",
83
+ "zh-cn": "测试连接",
84
+ },
85
+ "smbVersionNote": {
86
+ en: "Note: only SMB2 is supported (not SMB1 or SMB3). Enable SMB2 on your NAS/server if needed.",
87
+ de: "Hinweis: Nur SMB2 wird unterstützt (nicht SMB1 oder SMB3). SMB2 am NAS/Server ggf. aktivieren.",
88
+ ru: "Примечание: поддерживается только SMB2 (не SMB1 и не SMB3).",
89
+ pt: "Nota: apenas SMB2 é suportado (não SMB1 nem SMB3).",
90
+ nl: "Let op: alleen SMB2 wordt ondersteund (niet SMB1 of SMB3).",
91
+ fr: "Remarque : seul SMB2 est pris en charge (pas SMB1 ni SMB3).",
92
+ it: "Nota: è supportato solo SMB2 (non SMB1 o SMB3).",
93
+ es: "Nota: solo se admite SMB2 (no SMB1 ni SMB3).",
94
+ pl: "Uwaga: obsługiwany jest tylko SMB2 (nie SMB1 ani SMB3).",
95
+ uk: "Примітка: підтримується лише SMB2 (не SMB1 і не SMB3).",
96
+ "zh-cn": "注意:仅支持 SMB2(不支持 SMB1 或 SMB3)。",
97
+ },
98
+ "ftpSection": {
99
+ en: "FTP / FTPS",
100
+ de: "FTP / FTPS",
101
+ ru: "FTP / FTPS",
102
+ pt: "FTP / FTPS",
103
+ nl: "FTP / FTPS",
104
+ fr: "FTP / FTPS",
105
+ it: "FTP / FTPS",
106
+ es: "FTP / FTPS",
107
+ pl: "FTP / FTPS",
108
+ uk: "FTP / FTPS",
109
+ "zh-cn": "FTP / FTPS",
110
+ },
111
+ "ftpEnabled": {
112
+ en: "Enable FTP source",
113
+ de: "FTP-Quelle aktivieren",
114
+ ru: "Включить источник FTP",
115
+ pt: "Ativar fonte FTP",
116
+ nl: "FTP-bron inschakelen",
117
+ fr: "Activer la source FTP",
118
+ it: "Abilita sorgente FTP",
119
+ es: "Activar fuente FTP",
120
+ pl: "Włącz źródło FTP",
121
+ uk: "Увімкнути джерело FTP",
122
+ "zh-cn": "启用 FTP 来源",
123
+ },
124
+ "ftpHost": {
125
+ en: "Host",
126
+ de: "Host",
127
+ ru: "Хост",
128
+ pt: "Host",
129
+ nl: "Host",
130
+ fr: "Hôte",
131
+ it: "Host",
132
+ es: "Host",
133
+ pl: "Host",
134
+ uk: "Хост",
135
+ "zh-cn": "主机",
136
+ },
137
+ "ftpPort": {
138
+ en: "Port",
139
+ de: "Port",
140
+ ru: "Порт",
141
+ pt: "Porta",
142
+ nl: "Poort",
143
+ fr: "Port",
144
+ it: "Porta",
145
+ es: "Puerto",
146
+ pl: "Port",
147
+ uk: "Порт",
148
+ "zh-cn": "端口",
149
+ },
150
+ "ftpUser": {
151
+ en: "Username",
152
+ de: "Benutzername",
153
+ ru: "Имя пользователя",
154
+ pt: "Utilizador",
155
+ nl: "Gebruikersnaam",
156
+ fr: "Nom d'utilisateur",
157
+ it: "Nome utente",
158
+ es: "Usuario",
159
+ pl: "Nazwa użytkownika",
160
+ uk: "Ім'я користувача",
161
+ "zh-cn": "用户名",
162
+ },
163
+ "ftpPassword": {
164
+ en: "Password",
165
+ de: "Passwort",
166
+ ru: "Пароль",
167
+ pt: "Senha",
168
+ nl: "Wachtwoord",
169
+ fr: "Mot de passe",
170
+ it: "Password",
171
+ es: "Contraseña",
172
+ pl: "Hasło",
173
+ uk: "Пароль",
174
+ "zh-cn": "密码",
175
+ },
176
+ "ftpPath": {
177
+ en: "Remote path",
178
+ de: "Remote-Pfad",
179
+ ru: "Удалённый путь",
180
+ pt: "Caminho remoto",
181
+ nl: "Extern pad",
182
+ fr: "Chemin distant",
183
+ it: "Percorso remoto",
184
+ es: "Ruta remota",
185
+ pl: "Ścieżka zdalna",
186
+ uk: "Віддалений шлях",
187
+ "zh-cn": "远程路径",
188
+ },
189
+ "ftpSecure": {
190
+ en: "Use FTPS (SSL/TLS)",
191
+ de: "FTPS verwenden (SSL/TLS)",
192
+ ru: "Использовать FTPS (SSL/TLS)",
193
+ pt: "Usar FTPS (SSL/TLS)",
194
+ nl: "FTPS gebruiken (SSL/TLS)",
195
+ fr: "Utiliser FTPS (SSL/TLS)",
196
+ it: "Usa FTPS (SSL/TLS)",
197
+ es: "Usar FTPS (SSL/TLS)",
198
+ pl: "Użyj FTPS (SSL/TLS)",
199
+ uk: "Використовувати FTPS (SSL/TLS)",
200
+ "zh-cn": "使用 FTPS (SSL/TLS)",
201
+ },
202
+ "smbSection": {
203
+ en: "SMB (Windows Share / NAS)",
204
+ de: "SMB (Windows-Freigabe / NAS)",
205
+ ru: "SMB (общий ресурс Windows / NAS)",
206
+ pt: "SMB (partilha Windows / NAS)",
207
+ nl: "SMB (Windows-share / NAS)",
208
+ fr: "SMB (partage Windows / NAS)",
209
+ it: "SMB (condivisione Windows / NAS)",
210
+ es: "SMB (recurso compartido Windows / NAS)",
211
+ pl: "SMB (udział Windows / NAS)",
212
+ uk: "SMB (мережева папка Windows / NAS)",
213
+ "zh-cn": "SMB(Windows 共享 / NAS)",
214
+ },
215
+ "smbEnabled": {
216
+ en: "Enable SMB source",
217
+ de: "SMB-Quelle aktivieren",
218
+ ru: "Включить источник SMB",
219
+ pt: "Ativar fonte SMB",
220
+ nl: "SMB-bron inschakelen",
221
+ fr: "Activer la source SMB",
222
+ it: "Abilita sorgente SMB",
223
+ es: "Activar fuente SMB",
224
+ pl: "Włącz źródło SMB",
225
+ uk: "Увімкнути джерело SMB",
226
+ "zh-cn": "启用 SMB 来源",
227
+ },
228
+ "smbHost": {
229
+ en: "Host / IP",
230
+ de: "Host / IP",
231
+ ru: "Хост / IP",
232
+ pt: "Host / IP",
233
+ nl: "Host / IP",
234
+ fr: "Hôte / IP",
235
+ it: "Host / IP",
236
+ es: "Host / IP",
237
+ pl: "Host / IP",
238
+ uk: "Хост / IP",
239
+ "zh-cn": "主机 / IP",
240
+ },
241
+ "smbShare": {
242
+ en: "Share name",
243
+ de: "Freigabename",
244
+ ru: "Имя ресурса",
245
+ pt: "Nome de partilha",
246
+ nl: "Sharenaam",
247
+ fr: "Nom du partage",
248
+ it: "Nome condivisione",
249
+ es: "Nombre del recurso",
250
+ pl: "Nazwa udziału",
251
+ uk: "Ім'я ресурсу",
252
+ "zh-cn": "共享名称",
253
+ },
254
+ "smbPath": {
255
+ en: "Path within share (optional)",
256
+ de: "Pfad innerhalb der Freigabe (optional)",
257
+ ru: "Путь внутри ресурса (необязательно)",
258
+ pt: "Caminho na partilha (opcional)",
259
+ nl: "Pad binnen share (optioneel)",
260
+ fr: "Chemin dans le partage (optionnel)",
261
+ it: "Percorso nella condivisione (opzionale)",
262
+ es: "Ruta dentro del recurso (opcional)",
263
+ pl: "Ścieżka w udziale (opcjonalnie)",
264
+ uk: "Шлях всередині ресурсу (необов'язково)",
265
+ "zh-cn": "共享内路径(可选)",
266
+ },
267
+ "smbUser": {
268
+ en: "Username",
269
+ de: "Benutzername",
270
+ ru: "Имя пользователя",
271
+ pt: "Utilizador",
272
+ nl: "Gebruikersnaam",
273
+ fr: "Nom d'utilisateur",
274
+ it: "Nome utente",
275
+ es: "Usuario",
276
+ pl: "Nazwa użytkownika",
277
+ uk: "Ім'я користувача",
278
+ "zh-cn": "用户名",
279
+ },
280
+ "smbPassword": {
281
+ en: "Password",
282
+ de: "Passwort",
283
+ ru: "Пароль",
284
+ pt: "Senha",
285
+ nl: "Wachtwoord",
286
+ fr: "Mot de passe",
287
+ it: "Password",
288
+ es: "Contraseña",
289
+ pl: "Hasło",
290
+ uk: "Пароль",
291
+ "zh-cn": "密码",
292
+ },
293
+ "smbDomain": {
294
+ en: "Domain (optional)",
295
+ de: "Domäne (optional)",
296
+ ru: "Домен (необязательно)",
297
+ pt: "Domínio (opcional)",
298
+ nl: "Domein (optioneel)",
299
+ fr: "Domaine (optionnel)",
300
+ it: "Dominio (opzionale)",
301
+ es: "Dominio (opcional)",
302
+ pl: "Domena (opcjonalnie)",
303
+ uk: "Домен (необов'язково)",
304
+ "zh-cn": "域(可选)",
305
+ },
46
306
  };
package/build/main.js CHANGED
@@ -27,6 +27,9 @@ 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
+ const SMB2 = require("@marsaud/smb2");
30
33
  const execAsync = (0, import_node_util.promisify)(import_node_child_process.exec);
31
34
  class ScriptRestore extends utils.Adapter {
32
35
  constructor(options = {}) {
@@ -39,8 +42,7 @@ class ScriptRestore extends utils.Adapter {
39
42
  this.on("unload", this.onUnload.bind(this));
40
43
  }
41
44
  onReady() {
42
- const cfg = this.config;
43
- this.log.info(`Script Restore ready. Backup path: ${cfg.backupPath || "/opt/iobroker/backups"}`);
45
+ this.log.info(`Script Restore ready. Backup path: ${this.config.backupPath || "/opt/iobroker/backups"}`);
44
46
  }
45
47
  onUnload(callback) {
46
48
  callback();
@@ -60,6 +62,36 @@ class ScriptRestore extends utils.Adapter {
60
62
  case "parseUploadedFile":
61
63
  await this.handleParseUploadedFile(obj);
62
64
  break;
65
+ case "getSourceConfig":
66
+ this.sendTo(
67
+ obj.from,
68
+ obj.command,
69
+ {
70
+ localEnabled: this.config.localEnabled !== false,
71
+ ftpEnabled: !!this.config.ftpEnabled,
72
+ smbEnabled: !!this.config.smbEnabled
73
+ },
74
+ obj.callback
75
+ );
76
+ break;
77
+ case "testFtp":
78
+ await this.handleTestFtp(obj);
79
+ break;
80
+ case "testSmb":
81
+ await this.handleTestSmb(obj);
82
+ break;
83
+ case "listFtpFiles":
84
+ await this.handleListFtpFiles(obj);
85
+ break;
86
+ case "parseFtpFile":
87
+ await this.handleParseFtpFile(obj);
88
+ break;
89
+ case "listSmbFiles":
90
+ await this.handleListSmbFiles(obj);
91
+ break;
92
+ case "parseSmbFile":
93
+ await this.handleParseSmbFile(obj);
94
+ break;
63
95
  default:
64
96
  this.sendTo(obj.from, obj.command, { error: "Unknown command" }, obj.callback);
65
97
  }
@@ -68,9 +100,13 @@ class ScriptRestore extends utils.Adapter {
68
100
  this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
69
101
  }
70
102
  }
103
+ // ─── Local ───────────────────────────────────────────────────────────────
71
104
  async handleListLocalFiles(obj) {
72
- const cfg = this.config;
73
- const backupPath = cfg.backupPath || "/opt/iobroker/backups";
105
+ if (this.config.localEnabled === false) {
106
+ this.sendTo(obj.from, obj.command, { error: "Local source not enabled" }, obj.callback);
107
+ return;
108
+ }
109
+ const backupPath = this.config.backupPath || "/opt/iobroker/backups";
74
110
  try {
75
111
  const rawEntries = await fs.readdir(backupPath, { withFileTypes: true, encoding: "utf8" });
76
112
  const entries = rawEntries;
@@ -89,8 +125,11 @@ class ScriptRestore extends utils.Adapter {
89
125
  }
90
126
  }
91
127
  async handleParseLocalFile(obj) {
92
- const cfg = this.config;
93
- const backupPath = cfg.backupPath || "/opt/iobroker/backups";
128
+ if (this.config.localEnabled === false) {
129
+ this.sendTo(obj.from, obj.command, { error: "Local source not enabled" }, obj.callback);
130
+ return;
131
+ }
132
+ const backupPath = this.config.backupPath || "/opt/iobroker/backups";
94
133
  const msg = obj.message;
95
134
  const filename = path.basename(msg.filename);
96
135
  const filepath = path.join(backupPath, filename);
@@ -112,6 +151,193 @@ class ScriptRestore extends utils.Adapter {
112
151
  this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
113
152
  }
114
153
  }
154
+ // ─── Tests ───────────────────────────────────────────────────────────────
155
+ async handleTestFtp(obj) {
156
+ const msg = obj.message;
157
+ const client = new ftp.Client();
158
+ client.ftp.verbose = false;
159
+ try {
160
+ await client.access({
161
+ host: msg.host,
162
+ port: msg.port || 21,
163
+ user: msg.user || "anonymous",
164
+ password: msg.password || "",
165
+ secure: msg.secure || false
166
+ });
167
+ const list = await client.list(msg.path || "/");
168
+ const count = list.filter((i) => i.type === ftp.FileType.File).length;
169
+ this.sendTo(
170
+ obj.from,
171
+ obj.command,
172
+ { success: true, message: `Verbunden! ${count} Datei(en) gefunden in: ${msg.path || "/"}` },
173
+ obj.callback
174
+ );
175
+ } catch (e) {
176
+ this.sendTo(obj.from, obj.command, { success: false, message: e.message }, obj.callback);
177
+ } finally {
178
+ client.close();
179
+ }
180
+ }
181
+ async handleTestSmb(obj) {
182
+ const msg = obj.message;
183
+ const smb = new SMB2({
184
+ share: `\\\\${msg.host}\\${msg.share}`,
185
+ username: msg.user || "",
186
+ password: msg.password || "",
187
+ domain: msg.domain || ""
188
+ });
189
+ try {
190
+ const files = await this.smbReaddir(smb, msg.path || "");
191
+ this.sendTo(
192
+ obj.from,
193
+ obj.command,
194
+ {
195
+ success: true,
196
+ message: `Verbunden! ${files.length} Eintr\xE4ge in: \\\\${msg.host}\\${msg.share}${msg.path ? `\\${msg.path}` : ""}`
197
+ },
198
+ obj.callback
199
+ );
200
+ } catch (e) {
201
+ this.sendTo(obj.from, obj.command, { success: false, message: e.message }, obj.callback);
202
+ } finally {
203
+ smb.disconnect();
204
+ }
205
+ }
206
+ // ─── FTP ─────────────────────────────────────────────────────────────────
207
+ createFtpClient() {
208
+ const client = new ftp.Client();
209
+ client.ftp.verbose = false;
210
+ return client;
211
+ }
212
+ async ftpConnect(client) {
213
+ await client.access({
214
+ host: this.config.ftpHost,
215
+ port: this.config.ftpPort || 21,
216
+ user: this.config.ftpUser || "anonymous",
217
+ password: this.config.ftpPassword || "",
218
+ secure: this.config.ftpSecure || false
219
+ });
220
+ }
221
+ async handleListFtpFiles(obj) {
222
+ if (!this.config.ftpEnabled) {
223
+ this.sendTo(obj.from, obj.command, { error: "FTP not enabled" }, obj.callback);
224
+ return;
225
+ }
226
+ const client = this.createFtpClient();
227
+ try {
228
+ await this.ftpConnect(client);
229
+ const remotePath = this.config.ftpPath || "/";
230
+ const list = await client.list(remotePath);
231
+ const files = list.filter((item) => {
232
+ const n = item.name;
233
+ 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"));
234
+ }).map((item) => item.name).sort().reverse();
235
+ this.sendTo(obj.from, obj.command, { files, path: remotePath }, obj.callback);
236
+ } catch (e) {
237
+ this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
238
+ } finally {
239
+ client.close();
240
+ }
241
+ }
242
+ async handleParseFtpFile(obj) {
243
+ if (!this.config.ftpEnabled) {
244
+ this.sendTo(obj.from, obj.command, { error: "FTP not enabled" }, obj.callback);
245
+ return;
246
+ }
247
+ const msg = obj.message;
248
+ const filename = path.basename(msg.filename);
249
+ const remotePath = path.posix.join(this.config.ftpPath || "/", filename);
250
+ const client = this.createFtpClient();
251
+ try {
252
+ await this.ftpConnect(client);
253
+ const chunks = [];
254
+ const writable = new import_node_stream.Writable({
255
+ write(chunk, _enc, cb) {
256
+ chunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
257
+ cb();
258
+ }
259
+ });
260
+ await client.downloadTo(writable, remotePath);
261
+ const buf = Buffer.concat(chunks);
262
+ const scripts = await this.parseBuffer(buf, filename);
263
+ this.sendTo(obj.from, obj.command, { scripts }, obj.callback);
264
+ } catch (e) {
265
+ this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
266
+ } finally {
267
+ client.close();
268
+ }
269
+ }
270
+ // ─── SMB ─────────────────────────────────────────────────────────────────
271
+ createSmbClient() {
272
+ return new SMB2({
273
+ share: `\\\\${this.config.smbHost}\\${this.config.smbShare}`,
274
+ username: this.config.smbUser || "",
275
+ password: this.config.smbPassword || "",
276
+ domain: this.config.smbDomain || ""
277
+ });
278
+ }
279
+ smbReaddir(smb, dirPath) {
280
+ return new Promise((resolve, reject) => {
281
+ smb.readdir(dirPath, (err, files) => {
282
+ if (err) {
283
+ reject(err);
284
+ } else {
285
+ resolve(files);
286
+ }
287
+ });
288
+ });
289
+ }
290
+ smbReadFile(smb, filePath) {
291
+ return new Promise((resolve, reject) => {
292
+ smb.readFile(filePath, (err, data) => {
293
+ if (err) {
294
+ reject(err);
295
+ } else {
296
+ resolve(data);
297
+ }
298
+ });
299
+ });
300
+ }
301
+ async handleListSmbFiles(obj) {
302
+ if (!this.config.smbEnabled) {
303
+ this.sendTo(obj.from, obj.command, { error: "SMB not enabled" }, obj.callback);
304
+ return;
305
+ }
306
+ const smb = this.createSmbClient();
307
+ try {
308
+ const smbPath = this.config.smbPath || "";
309
+ const entries = await this.smbReaddir(smb, smbPath);
310
+ const files = entries.filter((n) => {
311
+ return (n.startsWith("iobroker") || n.startsWith("javascript")) && (n.endsWith(".tar.gz") || n.endsWith(".tar") || n.endsWith(".json") || n.endsWith(".jsonl"));
312
+ }).sort().reverse();
313
+ this.sendTo(obj.from, obj.command, { files, path: smbPath }, obj.callback);
314
+ } catch (e) {
315
+ this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
316
+ } finally {
317
+ smb.disconnect();
318
+ }
319
+ }
320
+ async handleParseSmbFile(obj) {
321
+ if (!this.config.smbEnabled) {
322
+ this.sendTo(obj.from, obj.command, { error: "SMB not enabled" }, obj.callback);
323
+ return;
324
+ }
325
+ const msg = obj.message;
326
+ const filename = path.basename(msg.filename);
327
+ const smbPath = this.config.smbPath || "";
328
+ const filePath = smbPath ? `${smbPath}\\${filename}` : filename;
329
+ const smb = this.createSmbClient();
330
+ try {
331
+ const buf = await this.smbReadFile(smb, filePath);
332
+ const scripts = await this.parseBuffer(buf, filename);
333
+ this.sendTo(obj.from, obj.command, { scripts }, obj.callback);
334
+ } catch (e) {
335
+ this.sendTo(obj.from, obj.command, { error: e.message }, obj.callback);
336
+ } finally {
337
+ smb.disconnect();
338
+ }
339
+ }
340
+ // ─── Parsing ─────────────────────────────────────────────────────────────
115
341
  async parseBuffer(buf, filename) {
116
342
  const name = filename.toLowerCase();
117
343
  if (name.endsWith(".tar.gz") || name.endsWith(".tgz") || name.endsWith(".tar")) {
@@ -151,10 +377,7 @@ class ScriptRestore extends utils.Adapter {
151
377
  const walk = async (d) => {
152
378
  let entries;
153
379
  try {
154
- entries = await fs.readdir(d, {
155
- withFileTypes: true,
156
- encoding: "utf8"
157
- });
380
+ entries = await fs.readdir(d, { withFileTypes: true, encoding: "utf8" });
158
381
  } catch {
159
382
  return null;
160
383
  }
package/build/main.js.map CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
3
3
  "sources": ["../src/main.ts"],
4
- "sourcesContent": ["/*\n * ioBroker Script Restore Adapter\n * Restore ioBroker scripts from backup archives\n * Copyright (c) 2024 ipod86 <david@graef.email>\n * MIT License\n */\n\nimport type { Dirent } from \"node:fs\";\nimport * as utils from \"@iobroker/adapter-core\";\nimport * as fs from \"fs/promises\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { exec } from \"node:child_process\";\nimport { promisify } from \"node:util\";\n\nconst execAsync = promisify(exec);\n\ninterface ScriptEntry {\n\tname: string;\n\tpath: string;\n\ttype: string;\n\tsource: string;\n}\n\ninterface AdapterConfig {\n\tbackupPath: string;\n}\n\nclass ScriptRestore extends utils.Adapter {\n\tpublic constructor(options: Partial<utils.AdapterOptions> = {}) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\tname: \"script-restore\",\n\t\t});\n\t\tthis.on(\"ready\", this.onReady.bind(this));\n\t\tthis.on(\"message\", this.onMessage.bind(this));\n\t\tthis.on(\"unload\", this.onUnload.bind(this));\n\t}\n\n\tprivate onReady(): void {\n\t\tconst cfg = this.config as unknown as AdapterConfig;\n\t\tthis.log.info(`Script Restore ready. Backup path: ${cfg.backupPath || \"/opt/iobroker/backups\"}`);\n\t}\n\n\tprivate onUnload(callback: () => void): void {\n\t\tcallback();\n\t}\n\n\tprivate async onMessage(obj: ioBroker.Message): Promise<void> {\n\t\tif (!obj.callback) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tswitch (obj.command) {\n\t\t\t\tcase \"listLocalFiles\":\n\t\t\t\t\tawait this.handleListLocalFiles(obj);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"parseLocalFile\":\n\t\t\t\t\tawait this.handleParseLocalFile(obj);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"parseUploadedFile\":\n\t\t\t\t\tawait this.handleParseUploadedFile(obj);\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tthis.sendTo(obj.from, obj.command, { error: \"Unknown command\" }, obj.callback);\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.log.error(`Error handling ${obj.command}: ${(e as Error).message}`);\n\t\t\tthis.sendTo(obj.from, obj.command, { error: (e as Error).message }, obj.callback);\n\t\t}\n\t}\n\n\tprivate async handleListLocalFiles(obj: ioBroker.Message): Promise<void> {\n\t\tconst cfg = this.config as unknown as AdapterConfig;\n\t\tconst backupPath = cfg.backupPath || \"/opt/iobroker/backups\";\n\t\ttry {\n\t\t\tconst rawEntries = await fs.readdir(backupPath, { withFileTypes: true, encoding: \"utf8\" });\n\t\t\tconst entries = rawEntries as unknown as Dirent[];\n\t\t\tconst files = entries\n\t\t\t\t.filter(e => {\n\t\t\t\t\tconst n = String(e.name);\n\t\t\t\t\treturn (\n\t\t\t\t\t\te.isFile() &&\n\t\t\t\t\t\t(n.startsWith(\"iobroker\") || n.startsWith(\"javascript\")) &&\n\t\t\t\t\t\t(n.endsWith(\".tar.gz\") || n.endsWith(\".tar\") || n.endsWith(\".json\") || n.endsWith(\".jsonl\"))\n\t\t\t\t\t);\n\t\t\t\t})\n\t\t\t\t.map(e => String(e.name))\n\t\t\t\t.sort()\n\t\t\t\t.reverse();\n\t\t\tthis.sendTo(obj.from, obj.command, { files, path: backupPath }, obj.callback);\n\t\t} catch (e) {\n\t\t\tthis.sendTo(\n\t\t\t\tobj.from,\n\t\t\t\tobj.command,\n\t\t\t\t{ error: `Verzeichnis nicht lesbar: ${(e as Error).message}` },\n\t\t\t\tobj.callback,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async handleParseLocalFile(obj: ioBroker.Message): Promise<void> {\n\t\tconst cfg = this.config as unknown as AdapterConfig;\n\t\tconst backupPath = cfg.backupPath || \"/opt/iobroker/backups\";\n\t\tconst msg = obj.message as { filename: string };\n\t\t// Security: only allow simple filenames, no path traversal\n\t\tconst filename = path.basename(msg.filename);\n\t\tconst filepath = path.join(backupPath, filename);\n\t\ttry {\n\t\t\tconst buf = await fs.readFile(filepath);\n\t\t\tconst scripts = await this.parseBuffer(buf, filename);\n\t\t\tthis.sendTo(obj.from, obj.command, { scripts }, obj.callback);\n\t\t} catch (e) {\n\t\t\tthis.sendTo(obj.from, obj.command, { error: (e as Error).message }, obj.callback);\n\t\t}\n\t}\n\n\tprivate async handleParseUploadedFile(obj: ioBroker.Message): Promise<void> {\n\t\tconst msg = obj.message as { name: string; data: string };\n\t\ttry {\n\t\t\tconst buf = Buffer.from(msg.data, \"base64\");\n\t\t\tconst scripts = await this.parseBuffer(buf, msg.name);\n\t\t\tthis.sendTo(obj.from, obj.command, { scripts }, obj.callback);\n\t\t} catch (e) {\n\t\t\tthis.sendTo(obj.from, obj.command, { error: (e as Error).message }, obj.callback);\n\t\t}\n\t}\n\n\tprivate async parseBuffer(buf: Buffer, filename: string): Promise<ScriptEntry[]> {\n\t\tconst name = filename.toLowerCase();\n\t\tif (name.endsWith(\".tar.gz\") || name.endsWith(\".tgz\") || name.endsWith(\".tar\")) {\n\t\t\treturn this.parseTarArchive(buf, name.endsWith(\".tar\") && !name.endsWith(\".tar.gz\"));\n\t\t}\n\t\treturn this.parseJsonContent(buf.toString(\"utf8\"), filename);\n\t}\n\n\tprivate async parseTarArchive(buf: Buffer, isPlainTar: boolean): Promise<ScriptEntry[]> {\n\t\tconst tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), \"script-restore-\"));\n\t\tconst tmpFile = path.join(tmpDir, `archive.tar${isPlainTar ? \"\" : \".gz\"}`);\n\t\ttry {\n\t\t\tawait fs.writeFile(tmpFile, buf);\n\n\t\t\tconst extractFlag = isPlainTar ? \"-xf\" : \"-xzf\";\n\t\t\t// Extract only matching files to avoid extracting entire large archives\n\t\t\ttry {\n\t\t\t\tawait execAsync(\n\t\t\t\t\t`tar ${extractFlag} \"${tmpFile}\" -C \"${tmpDir}\" --wildcards` +\n\t\t\t\t\t\t` \"*/objects.jsonl\" \"*/objects.json\" \"*/scripts.json\" \"*/script.json\"` +\n\t\t\t\t\t\t` 2>/dev/null`,\n\t\t\t\t);\n\t\t\t} catch {\n\t\t\t\t// Try without wildcards filter (some tar versions behave differently)\n\t\t\t\tawait execAsync(`tar ${extractFlag} \"${tmpFile}\" -C \"${tmpDir}\" 2>/dev/null`).catch(() => {});\n\t\t\t}\n\n\t\t\tconst targets = [\"objects.jsonl\", \"objects.json\", \"scripts.json\", \"script.json\"];\n\t\t\tconst found = await this.findFile(tmpDir, targets);\n\t\t\tif (!found) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"Keine passende Datei im Archiv gefunden (objects.json, objects.jsonl, scripts.json, script.json)\",\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst content = await fs.readFile(found, \"utf8\");\n\t\t\treturn this.parseJsonContent(content, path.basename(found));\n\t\t} finally {\n\t\t\tawait fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});\n\t\t}\n\t}\n\n\tprivate async findFile(dir: string, names: string[]): Promise<string | null> {\n\t\tconst walk = async (d: string): Promise<string | null> => {\n\t\t\tlet entries: Dirent[];\n\t\t\ttry {\n\t\t\t\tentries = (await fs.readdir(d, {\n\t\t\t\t\twithFileTypes: true,\n\t\t\t\t\tencoding: \"utf8\",\n\t\t\t\t})) as unknown as Dirent[];\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tfor (const e of entries) {\n\t\t\t\tconst p = path.join(d, String(e.name));\n\t\t\t\tif (e.isDirectory()) {\n\t\t\t\t\tconst found = await walk(p);\n\t\t\t\t\tif (found) {\n\t\t\t\t\t\treturn found;\n\t\t\t\t\t}\n\t\t\t\t} else if (names.includes(String(e.name))) {\n\t\t\t\t\treturn p;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t};\n\t\treturn walk(dir);\n\t}\n\n\tprivate parseJsonContent(content: string, filename: string): ScriptEntry[] {\n\t\tconst scripts: ScriptEntry[] = [];\n\t\tconst trimmed = content.trimStart();\n\n\t\t// Detect JSONL format: file ends with .jsonl or starts with single JSON object per line\n\t\tconst isJsonl =\n\t\t\tfilename.endsWith(\".jsonl\") ||\n\t\t\t(trimmed.startsWith(\"{\") && !trimmed.startsWith('{\\n \"') && trimmed.includes(\"\\n{\"));\n\n\t\tif (isJsonl) {\n\t\t\tfor (const line of content.split(\"\\n\")) {\n\t\t\t\tconst l = line.trim();\n\t\t\t\tif (!l) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst item = JSON.parse(l) as Record<string, unknown>;\n\t\t\t\t\tthis.processItem(\n\t\t\t\t\t\t(item._id || item.id) as string,\n\t\t\t\t\t\t(item.value || item.doc || item) as Record<string, unknown>,\n\t\t\t\t\t\tscripts,\n\t\t\t\t\t);\n\t\t\t\t} catch {\n\t\t\t\t\t// skip invalid lines\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tconst data = JSON.parse(content) as Record<string, unknown>;\n\t\t\tfor (const [k, v] of Object.entries(data)) {\n\t\t\t\tthis.processItem(k, v as Record<string, unknown>, scripts);\n\t\t\t}\n\t\t}\n\n\t\treturn scripts.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));\n\t}\n\n\tprivate processItem(key: string, val: unknown, scripts: ScriptEntry[]): void {\n\t\tif (!key || typeof val !== \"object\" || val === null) {\n\t\t\treturn;\n\t\t}\n\t\tconst v = val as Record<string, unknown>;\n\n\t\tif ([\"channel\", \"device\", \"folder\", \"meta\"].includes(v.type as string)) {\n\t\t\treturn;\n\t\t}\n\t\tif (v.type !== \"script\" && !key.startsWith(\"script.js.\")) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst c = v.common as Record<string, unknown> | undefined;\n\t\tif (!c || (c.engineType === undefined && c.source === undefined)) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst rawEngineType = typeof c.engineType === \"string\" ? c.engineType : \"JS\";\n\t\tconst engineType = rawEngineType.toLowerCase();\n\t\tlet stype: string;\n\t\tif (engineType.includes(\"ts\") || engineType.includes(\"typescript\")) {\n\t\t\tstype = \"TypeScript\";\n\t\t} else if (engineType.includes(\"blockly\")) {\n\t\t\tstype = \"Blockly\";\n\t\t} else if (engineType.includes(\"rules\")) {\n\t\t\tstype = \"Rules\";\n\t\t} else {\n\t\t\tstype = \"JS\";\n\t\t}\n\n\t\tlet name: string;\n\t\tconst nameObj = c.name;\n\t\tif (typeof nameObj === \"object\" && nameObj !== null) {\n\t\t\tconst n = nameObj as Record<string, string>;\n\t\t\tname = n.de || n.en || Object.values(n)[0] || key.split(\".\").pop() || key;\n\t\t} else {\n\t\t\tname = typeof nameObj === \"string\" && nameObj ? nameObj : (key.split(\".\").pop() ?? key);\n\t\t}\n\n\t\tconst scriptPath = key.startsWith(\"script.js.\") ? key.slice(10) : key;\n\n\t\tscripts.push({\n\t\t\tname,\n\t\t\tpath: scriptPath,\n\t\t\ttype: stype,\n\t\t\tsource: typeof c.source === \"string\" ? c.source : \"\",\n\t\t});\n\t}\n}\n\nif (require.main !== module) {\n\tmodule.exports = (options: Partial<utils.AdapterOptions> | undefined) => new ScriptRestore(options);\n} else {\n\t(() => new ScriptRestore())();\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;AAQA,YAAuB;AACvB,SAAoB;AACpB,WAAsB;AACtB,SAAoB;AACpB,gCAAqB;AACrB,uBAA0B;AAE1B,MAAM,gBAAY,4BAAU,8BAAI;AAahC,MAAM,sBAAsB,MAAM,QAAQ;AAAA,EAClC,YAAY,UAAyC,CAAC,GAAG;AAC/D,UAAM;AAAA,MACL,GAAG;AAAA,MACH,MAAM;AAAA,IACP,CAAC;AACD,SAAK,GAAG,SAAS,KAAK,QAAQ,KAAK,IAAI,CAAC;AACxC,SAAK,GAAG,WAAW,KAAK,UAAU,KAAK,IAAI,CAAC;AAC5C,SAAK,GAAG,UAAU,KAAK,SAAS,KAAK,IAAI,CAAC;AAAA,EAC3C;AAAA,EAEQ,UAAgB;AACvB,UAAM,MAAM,KAAK;AACjB,SAAK,IAAI,KAAK,sCAAsC,IAAI,cAAc,uBAAuB,EAAE;AAAA,EAChG;AAAA,EAEQ,SAAS,UAA4B;AAC5C,aAAS;AAAA,EACV;AAAA,EAEA,MAAc,UAAU,KAAsC;AAC7D,QAAI,CAAC,IAAI,UAAU;AAClB;AAAA,IACD;AAEA,QAAI;AACH,cAAQ,IAAI,SAAS;AAAA,QACpB,KAAK;AACJ,gBAAM,KAAK,qBAAqB,GAAG;AACnC;AAAA,QACD,KAAK;AACJ,gBAAM,KAAK,qBAAqB,GAAG;AACnC;AAAA,QACD,KAAK;AACJ,gBAAM,KAAK,wBAAwB,GAAG;AACtC;AAAA,QACD;AACC,eAAK,OAAO,IAAI,MAAM,IAAI,SAAS,EAAE,OAAO,kBAAkB,GAAG,IAAI,QAAQ;AAAA,MAC/E;AAAA,IACD,SAAS,GAAG;AACX,WAAK,IAAI,MAAM,kBAAkB,IAAI,OAAO,KAAM,EAAY,OAAO,EAAE;AACvE,WAAK,OAAO,IAAI,MAAM,IAAI,SAAS,EAAE,OAAQ,EAAY,QAAQ,GAAG,IAAI,QAAQ;AAAA,IACjF;AAAA,EACD;AAAA,EAEA,MAAc,qBAAqB,KAAsC;AACxE,UAAM,MAAM,KAAK;AACjB,UAAM,aAAa,IAAI,cAAc;AACrC,QAAI;AACH,YAAM,aAAa,MAAM,GAAG,QAAQ,YAAY,EAAE,eAAe,MAAM,UAAU,OAAO,CAAC;AACzF,YAAM,UAAU;AAChB,YAAM,QAAQ,QACZ,OAAO,OAAK;AACZ,cAAM,IAAI,OAAO,EAAE,IAAI;AACvB,eACC,EAAE,OAAO,MACR,EAAE,WAAW,UAAU,KAAK,EAAE,WAAW,YAAY,OACrD,EAAE,SAAS,SAAS,KAAK,EAAE,SAAS,MAAM,KAAK,EAAE,SAAS,OAAO,KAAK,EAAE,SAAS,QAAQ;AAAA,MAE5F,CAAC,EACA,IAAI,OAAK,OAAO,EAAE,IAAI,CAAC,EACvB,KAAK,EACL,QAAQ;AACV,WAAK,OAAO,IAAI,MAAM,IAAI,SAAS,EAAE,OAAO,MAAM,WAAW,GAAG,IAAI,QAAQ;AAAA,IAC7E,SAAS,GAAG;AACX,WAAK;AAAA,QACJ,IAAI;AAAA,QACJ,IAAI;AAAA,QACJ,EAAE,OAAO,6BAA8B,EAAY,OAAO,GAAG;AAAA,QAC7D,IAAI;AAAA,MACL;AAAA,IACD;AAAA,EACD;AAAA,EAEA,MAAc,qBAAqB,KAAsC;AACxE,UAAM,MAAM,KAAK;AACjB,UAAM,aAAa,IAAI,cAAc;AACrC,UAAM,MAAM,IAAI;AAEhB,UAAM,WAAW,KAAK,SAAS,IAAI,QAAQ;AAC3C,UAAM,WAAW,KAAK,KAAK,YAAY,QAAQ;AAC/C,QAAI;AACH,YAAM,MAAM,MAAM,GAAG,SAAS,QAAQ;AACtC,YAAM,UAAU,MAAM,KAAK,YAAY,KAAK,QAAQ;AACpD,WAAK,OAAO,IAAI,MAAM,IAAI,SAAS,EAAE,QAAQ,GAAG,IAAI,QAAQ;AAAA,IAC7D,SAAS,GAAG;AACX,WAAK,OAAO,IAAI,MAAM,IAAI,SAAS,EAAE,OAAQ,EAAY,QAAQ,GAAG,IAAI,QAAQ;AAAA,IACjF;AAAA,EACD;AAAA,EAEA,MAAc,wBAAwB,KAAsC;AAC3E,UAAM,MAAM,IAAI;AAChB,QAAI;AACH,YAAM,MAAM,OAAO,KAAK,IAAI,MAAM,QAAQ;AAC1C,YAAM,UAAU,MAAM,KAAK,YAAY,KAAK,IAAI,IAAI;AACpD,WAAK,OAAO,IAAI,MAAM,IAAI,SAAS,EAAE,QAAQ,GAAG,IAAI,QAAQ;AAAA,IAC7D,SAAS,GAAG;AACX,WAAK,OAAO,IAAI,MAAM,IAAI,SAAS,EAAE,OAAQ,EAAY,QAAQ,GAAG,IAAI,QAAQ;AAAA,IACjF;AAAA,EACD;AAAA,EAEA,MAAc,YAAY,KAAa,UAA0C;AAChF,UAAM,OAAO,SAAS,YAAY;AAClC,QAAI,KAAK,SAAS,SAAS,KAAK,KAAK,SAAS,MAAM,KAAK,KAAK,SAAS,MAAM,GAAG;AAC/E,aAAO,KAAK,gBAAgB,KAAK,KAAK,SAAS,MAAM,KAAK,CAAC,KAAK,SAAS,SAAS,CAAC;AAAA,IACpF;AACA,WAAO,KAAK,iBAAiB,IAAI,SAAS,MAAM,GAAG,QAAQ;AAAA,EAC5D;AAAA,EAEA,MAAc,gBAAgB,KAAa,YAA6C;AACvF,UAAM,SAAS,MAAM,GAAG,QAAQ,KAAK,KAAK,GAAG,OAAO,GAAG,iBAAiB,CAAC;AACzE,UAAM,UAAU,KAAK,KAAK,QAAQ,cAAc,aAAa,KAAK,KAAK,EAAE;AACzE,QAAI;AACH,YAAM,GAAG,UAAU,SAAS,GAAG;AAE/B,YAAM,cAAc,aAAa,QAAQ;AAEzC,UAAI;AACH,cAAM;AAAA,UACL,OAAO,WAAW,KAAK,OAAO,SAAS,MAAM;AAAA,QAG9C;AAAA,MACD,QAAQ;AAEP,cAAM,UAAU,OAAO,WAAW,KAAK,OAAO,SAAS,MAAM,eAAe,EAAE,MAAM,MAAM;AAAA,QAAC,CAAC;AAAA,MAC7F;AAEA,YAAM,UAAU,CAAC,iBAAiB,gBAAgB,gBAAgB,aAAa;AAC/E,YAAM,QAAQ,MAAM,KAAK,SAAS,QAAQ,OAAO;AACjD,UAAI,CAAC,OAAO;AACX,cAAM,IAAI;AAAA,UACT;AAAA,QACD;AAAA,MACD;AAEA,YAAM,UAAU,MAAM,GAAG,SAAS,OAAO,MAAM;AAC/C,aAAO,KAAK,iBAAiB,SAAS,KAAK,SAAS,KAAK,CAAC;AAAA,IAC3D,UAAE;AACD,YAAM,GAAG,GAAG,QAAQ,EAAE,WAAW,MAAM,OAAO,KAAK,CAAC,EAAE,MAAM,MAAM;AAAA,MAAC,CAAC;AAAA,IACrE;AAAA,EACD;AAAA,EAEA,MAAc,SAAS,KAAa,OAAyC;AAC5E,UAAM,OAAO,OAAO,MAAsC;AACzD,UAAI;AACJ,UAAI;AACH,kBAAW,MAAM,GAAG,QAAQ,GAAG;AAAA,UAC9B,eAAe;AAAA,UACf,UAAU;AAAA,QACX,CAAC;AAAA,MACF,QAAQ;AACP,eAAO;AAAA,MACR;AACA,iBAAW,KAAK,SAAS;AACxB,cAAM,IAAI,KAAK,KAAK,GAAG,OAAO,EAAE,IAAI,CAAC;AACrC,YAAI,EAAE,YAAY,GAAG;AACpB,gBAAM,QAAQ,MAAM,KAAK,CAAC;AAC1B,cAAI,OAAO;AACV,mBAAO;AAAA,UACR;AAAA,QACD,WAAW,MAAM,SAAS,OAAO,EAAE,IAAI,CAAC,GAAG;AAC1C,iBAAO;AAAA,QACR;AAAA,MACD;AACA,aAAO;AAAA,IACR;AACA,WAAO,KAAK,GAAG;AAAA,EAChB;AAAA,EAEQ,iBAAiB,SAAiB,UAAiC;AAC1E,UAAM,UAAyB,CAAC;AAChC,UAAM,UAAU,QAAQ,UAAU;AAGlC,UAAM,UACL,SAAS,SAAS,QAAQ,KACzB,QAAQ,WAAW,GAAG,KAAK,CAAC,QAAQ,WAAW,QAAQ,KAAK,QAAQ,SAAS,KAAK;AAEpF,QAAI,SAAS;AACZ,iBAAW,QAAQ,QAAQ,MAAM,IAAI,GAAG;AACvC,cAAM,IAAI,KAAK,KAAK;AACpB,YAAI,CAAC,GAAG;AACP;AAAA,QACD;AACA,YAAI;AACH,gBAAM,OAAO,KAAK,MAAM,CAAC;AACzB,eAAK;AAAA,YACH,KAAK,OAAO,KAAK;AAAA,YACjB,KAAK,SAAS,KAAK,OAAO;AAAA,YAC3B;AAAA,UACD;AAAA,QACD,QAAQ;AAAA,QAER;AAAA,MACD;AAAA,IACD,OAAO;AACN,YAAM,OAAO,KAAK,MAAM,OAAO;AAC/B,iBAAW,CAAC,GAAG,CAAC,KAAK,OAAO,QAAQ,IAAI,GAAG;AAC1C,aAAK,YAAY,GAAG,GAA8B,OAAO;AAAA,MAC1D;AAAA,IACD;AAEA,WAAO,QAAQ,KAAK,CAAC,GAAG,MAAM,EAAE,KAAK,YAAY,EAAE,cAAc,EAAE,KAAK,YAAY,CAAC,CAAC;AAAA,EACvF;AAAA,EAEQ,YAAY,KAAa,KAAc,SAA8B;AA1O9E;AA2OE,QAAI,CAAC,OAAO,OAAO,QAAQ,YAAY,QAAQ,MAAM;AACpD;AAAA,IACD;AACA,UAAM,IAAI;AAEV,QAAI,CAAC,WAAW,UAAU,UAAU,MAAM,EAAE,SAAS,EAAE,IAAc,GAAG;AACvE;AAAA,IACD;AACA,QAAI,EAAE,SAAS,YAAY,CAAC,IAAI,WAAW,YAAY,GAAG;AACzD;AAAA,IACD;AAEA,UAAM,IAAI,EAAE;AACZ,QAAI,CAAC,KAAM,EAAE,eAAe,UAAa,EAAE,WAAW,QAAY;AACjE;AAAA,IACD;AAEA,UAAM,gBAAgB,OAAO,EAAE,eAAe,WAAW,EAAE,aAAa;AACxE,UAAM,aAAa,cAAc,YAAY;AAC7C,QAAI;AACJ,QAAI,WAAW,SAAS,IAAI,KAAK,WAAW,SAAS,YAAY,GAAG;AACnE,cAAQ;AAAA,IACT,WAAW,WAAW,SAAS,SAAS,GAAG;AAC1C,cAAQ;AAAA,IACT,WAAW,WAAW,SAAS,OAAO,GAAG;AACxC,cAAQ;AAAA,IACT,OAAO;AACN,cAAQ;AAAA,IACT;AAEA,QAAI;AACJ,UAAM,UAAU,EAAE;AAClB,QAAI,OAAO,YAAY,YAAY,YAAY,MAAM;AACpD,YAAM,IAAI;AACV,aAAO,EAAE,MAAM,EAAE,MAAM,OAAO,OAAO,CAAC,EAAE,CAAC,KAAK,IAAI,MAAM,GAAG,EAAE,IAAI,KAAK;AAAA,IACvE,OAAO;AACN,aAAO,OAAO,YAAY,YAAY,UAAU,WAAW,SAAI,MAAM,GAAG,EAAE,IAAI,MAAnB,YAAwB;AAAA,IACpF;AAEA,UAAM,aAAa,IAAI,WAAW,YAAY,IAAI,IAAI,MAAM,EAAE,IAAI;AAElE,YAAQ,KAAK;AAAA,MACZ;AAAA,MACA,MAAM;AAAA,MACN,MAAM;AAAA,MACN,QAAQ,OAAO,EAAE,WAAW,WAAW,EAAE,SAAS;AAAA,IACnD,CAAC;AAAA,EACF;AACD;AAEA,IAAI,QAAQ,SAAS,QAAQ;AAC5B,SAAO,UAAU,CAAC,YAAuD,IAAI,cAAc,OAAO;AACnG,OAAO;AACN,GAAC,MAAM,IAAI,cAAc,GAAG;AAC7B;",
4
+ "sourcesContent": ["/*\n * ioBroker Script Restore Adapter\n * Restore ioBroker scripts from backup archives\n * Copyright (c) 2024 ipod86 <david@graef.email>\n * MIT License\n */\n\nimport type { Dirent } from \"node:fs\";\nimport * as utils from \"@iobroker/adapter-core\";\nimport * as fs from \"fs/promises\";\nimport * as path from \"node:path\";\nimport * as os from \"node:os\";\nimport { exec } from \"node:child_process\";\nimport { promisify } from \"node:util\";\nimport * as ftp from \"basic-ftp\";\nimport { Writable } from \"node:stream\";\n\n// eslint-disable-next-line @typescript-eslint/no-require-imports\nconst SMB2 = require(\"@marsaud/smb2\");\n\nconst execAsync = promisify(exec);\n\ninterface ScriptEntry {\n\tname: string;\n\tpath: string;\n\ttype: string;\n\tsource: string;\n}\n\nclass ScriptRestore extends utils.Adapter {\n\tpublic constructor(options: Partial<utils.AdapterOptions> = {}) {\n\t\tsuper({\n\t\t\t...options,\n\t\t\tname: \"script-restore\",\n\t\t});\n\t\tthis.on(\"ready\", this.onReady.bind(this));\n\t\tthis.on(\"message\", this.onMessage.bind(this));\n\t\tthis.on(\"unload\", this.onUnload.bind(this));\n\t}\n\n\tprivate onReady(): void {\n\t\tthis.log.info(`Script Restore ready. Backup path: ${this.config.backupPath || \"/opt/iobroker/backups\"}`);\n\t}\n\n\tprivate onUnload(callback: () => void): void {\n\t\tcallback();\n\t}\n\n\tprivate async onMessage(obj: ioBroker.Message): Promise<void> {\n\t\tif (!obj.callback) {\n\t\t\treturn;\n\t\t}\n\n\t\ttry {\n\t\t\tswitch (obj.command) {\n\t\t\t\tcase \"listLocalFiles\":\n\t\t\t\t\tawait this.handleListLocalFiles(obj);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"parseLocalFile\":\n\t\t\t\t\tawait this.handleParseLocalFile(obj);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"parseUploadedFile\":\n\t\t\t\t\tawait this.handleParseUploadedFile(obj);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"getSourceConfig\":\n\t\t\t\t\tthis.sendTo(\n\t\t\t\t\t\tobj.from,\n\t\t\t\t\t\tobj.command,\n\t\t\t\t\t\t{\n\t\t\t\t\t\t\tlocalEnabled: this.config.localEnabled !== false,\n\t\t\t\t\t\t\tftpEnabled: !!this.config.ftpEnabled,\n\t\t\t\t\t\t\tsmbEnabled: !!this.config.smbEnabled,\n\t\t\t\t\t\t},\n\t\t\t\t\t\tobj.callback,\n\t\t\t\t\t);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"testFtp\":\n\t\t\t\t\tawait this.handleTestFtp(obj);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"testSmb\":\n\t\t\t\t\tawait this.handleTestSmb(obj);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"listFtpFiles\":\n\t\t\t\t\tawait this.handleListFtpFiles(obj);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"parseFtpFile\":\n\t\t\t\t\tawait this.handleParseFtpFile(obj);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"listSmbFiles\":\n\t\t\t\t\tawait this.handleListSmbFiles(obj);\n\t\t\t\t\tbreak;\n\t\t\t\tcase \"parseSmbFile\":\n\t\t\t\t\tawait this.handleParseSmbFile(obj);\n\t\t\t\t\tbreak;\n\t\t\t\tdefault:\n\t\t\t\t\tthis.sendTo(obj.from, obj.command, { error: \"Unknown command\" }, obj.callback);\n\t\t\t}\n\t\t} catch (e) {\n\t\t\tthis.log.error(`Error handling ${obj.command}: ${(e as Error).message}`);\n\t\t\tthis.sendTo(obj.from, obj.command, { error: (e as Error).message }, obj.callback);\n\t\t}\n\t}\n\n\t// \u2500\u2500\u2500 Local \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\tprivate async handleListLocalFiles(obj: ioBroker.Message): Promise<void> {\n\t\tif (this.config.localEnabled === false) {\n\t\t\tthis.sendTo(obj.from, obj.command, { error: \"Local source not enabled\" }, obj.callback);\n\t\t\treturn;\n\t\t}\n\t\tconst backupPath = this.config.backupPath || \"/opt/iobroker/backups\";\n\t\ttry {\n\t\t\tconst rawEntries = await fs.readdir(backupPath, { withFileTypes: true, encoding: \"utf8\" });\n\t\t\tconst entries = rawEntries as unknown as Dirent[];\n\t\t\tconst files = entries\n\t\t\t\t.filter(e => {\n\t\t\t\t\tconst n = String(e.name);\n\t\t\t\t\treturn (\n\t\t\t\t\t\te.isFile() &&\n\t\t\t\t\t\t(n.startsWith(\"iobroker\") || n.startsWith(\"javascript\")) &&\n\t\t\t\t\t\t(n.endsWith(\".tar.gz\") || n.endsWith(\".tar\") || n.endsWith(\".json\") || n.endsWith(\".jsonl\"))\n\t\t\t\t\t);\n\t\t\t\t})\n\t\t\t\t.map(e => String(e.name))\n\t\t\t\t.sort()\n\t\t\t\t.reverse();\n\t\t\tthis.sendTo(obj.from, obj.command, { files, path: backupPath }, obj.callback);\n\t\t} catch (e) {\n\t\t\tthis.sendTo(\n\t\t\t\tobj.from,\n\t\t\t\tobj.command,\n\t\t\t\t{ error: `Verzeichnis nicht lesbar: ${(e as Error).message}` },\n\t\t\t\tobj.callback,\n\t\t\t);\n\t\t}\n\t}\n\n\tprivate async handleParseLocalFile(obj: ioBroker.Message): Promise<void> {\n\t\tif (this.config.localEnabled === false) {\n\t\t\tthis.sendTo(obj.from, obj.command, { error: \"Local source not enabled\" }, obj.callback);\n\t\t\treturn;\n\t\t}\n\t\tconst backupPath = this.config.backupPath || \"/opt/iobroker/backups\";\n\t\tconst msg = obj.message as { filename: string };\n\t\tconst filename = path.basename(msg.filename);\n\t\tconst filepath = path.join(backupPath, filename);\n\t\ttry {\n\t\t\tconst buf = await fs.readFile(filepath);\n\t\t\tconst scripts = await this.parseBuffer(buf, filename);\n\t\t\tthis.sendTo(obj.from, obj.command, { scripts }, obj.callback);\n\t\t} catch (e) {\n\t\t\tthis.sendTo(obj.from, obj.command, { error: (e as Error).message }, obj.callback);\n\t\t}\n\t}\n\n\tprivate async handleParseUploadedFile(obj: ioBroker.Message): Promise<void> {\n\t\tconst msg = obj.message as { name: string; data: string };\n\t\ttry {\n\t\t\tconst buf = Buffer.from(msg.data, \"base64\");\n\t\t\tconst scripts = await this.parseBuffer(buf, msg.name);\n\t\t\tthis.sendTo(obj.from, obj.command, { scripts }, obj.callback);\n\t\t} catch (e) {\n\t\t\tthis.sendTo(obj.from, obj.command, { error: (e as Error).message }, obj.callback);\n\t\t}\n\t}\n\n\t// \u2500\u2500\u2500 Tests \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\tprivate async handleTestFtp(obj: ioBroker.Message): Promise<void> {\n\t\tconst msg = obj.message as {\n\t\t\thost: string;\n\t\t\tport: number;\n\t\t\tuser: string;\n\t\t\tpassword: string;\n\t\t\tpath: string;\n\t\t\tsecure: boolean;\n\t\t};\n\t\tconst client = new ftp.Client();\n\t\tclient.ftp.verbose = false;\n\t\ttry {\n\t\t\tawait client.access({\n\t\t\t\thost: msg.host,\n\t\t\t\tport: msg.port || 21,\n\t\t\t\tuser: msg.user || \"anonymous\",\n\t\t\t\tpassword: msg.password || \"\",\n\t\t\t\tsecure: msg.secure || false,\n\t\t\t});\n\t\t\tconst list = await client.list(msg.path || \"/\");\n\t\t\tconst count = list.filter(i => i.type === ftp.FileType.File).length;\n\t\t\tthis.sendTo(\n\t\t\t\tobj.from,\n\t\t\t\tobj.command,\n\t\t\t\t{ success: true, message: `Verbunden! ${count} Datei(en) gefunden in: ${msg.path || \"/\"}` },\n\t\t\t\tobj.callback,\n\t\t\t);\n\t\t} catch (e) {\n\t\t\tthis.sendTo(obj.from, obj.command, { success: false, message: (e as Error).message }, obj.callback);\n\t\t} finally {\n\t\t\tclient.close();\n\t\t}\n\t}\n\n\tprivate async handleTestSmb(obj: ioBroker.Message): Promise<void> {\n\t\tconst msg = obj.message as {\n\t\t\thost: string;\n\t\t\tshare: string;\n\t\t\tpath: string;\n\t\t\tuser: string;\n\t\t\tpassword: string;\n\t\t\tdomain: string;\n\t\t};\n\t\tconst smb = new SMB2({\n\t\t\tshare: `\\\\\\\\${msg.host}\\\\${msg.share}`,\n\t\t\tusername: msg.user || \"\",\n\t\t\tpassword: msg.password || \"\",\n\t\t\tdomain: msg.domain || \"\",\n\t\t});\n\t\ttry {\n\t\t\tconst files = await this.smbReaddir(smb, msg.path || \"\");\n\t\t\tthis.sendTo(\n\t\t\t\tobj.from,\n\t\t\t\tobj.command,\n\t\t\t\t{\n\t\t\t\t\tsuccess: true,\n\t\t\t\t\tmessage: `Verbunden! ${files.length} Eintr\u00E4ge in: \\\\\\\\${msg.host}\\\\${msg.share}${msg.path ? `\\\\${msg.path}` : \"\"}`,\n\t\t\t\t},\n\t\t\t\tobj.callback,\n\t\t\t);\n\t\t} catch (e) {\n\t\t\tthis.sendTo(obj.from, obj.command, { success: false, message: (e as Error).message }, obj.callback);\n\t\t} finally {\n\t\t\tsmb.disconnect();\n\t\t}\n\t}\n\n\t// \u2500\u2500\u2500 FTP \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\tprivate createFtpClient(): ftp.Client {\n\t\tconst client = new ftp.Client();\n\t\tclient.ftp.verbose = false;\n\t\treturn client;\n\t}\n\n\tprivate async ftpConnect(client: ftp.Client): Promise<void> {\n\t\tawait client.access({\n\t\t\thost: this.config.ftpHost,\n\t\t\tport: this.config.ftpPort || 21,\n\t\t\tuser: this.config.ftpUser || \"anonymous\",\n\t\t\tpassword: this.config.ftpPassword || \"\",\n\t\t\tsecure: this.config.ftpSecure || false,\n\t\t});\n\t}\n\n\tprivate async handleListFtpFiles(obj: ioBroker.Message): Promise<void> {\n\t\tif (!this.config.ftpEnabled) {\n\t\t\tthis.sendTo(obj.from, obj.command, { error: \"FTP not enabled\" }, obj.callback);\n\t\t\treturn;\n\t\t}\n\t\tconst client = this.createFtpClient();\n\t\ttry {\n\t\t\tawait this.ftpConnect(client);\n\t\t\tconst remotePath = this.config.ftpPath || \"/\";\n\t\t\tconst list = await client.list(remotePath);\n\t\t\tconst files = list\n\t\t\t\t.filter(item => {\n\t\t\t\t\tconst n = item.name;\n\t\t\t\t\treturn (\n\t\t\t\t\t\titem.type === ftp.FileType.File &&\n\t\t\t\t\t\t(n.startsWith(\"iobroker\") || n.startsWith(\"javascript\")) &&\n\t\t\t\t\t\t(n.endsWith(\".tar.gz\") || n.endsWith(\".tar\") || n.endsWith(\".json\") || n.endsWith(\".jsonl\"))\n\t\t\t\t\t);\n\t\t\t\t})\n\t\t\t\t.map(item => item.name)\n\t\t\t\t.sort()\n\t\t\t\t.reverse();\n\t\t\tthis.sendTo(obj.from, obj.command, { files, path: remotePath }, obj.callback);\n\t\t} catch (e) {\n\t\t\tthis.sendTo(obj.from, obj.command, { error: (e as Error).message }, obj.callback);\n\t\t} finally {\n\t\t\tclient.close();\n\t\t}\n\t}\n\n\tprivate async handleParseFtpFile(obj: ioBroker.Message): Promise<void> {\n\t\tif (!this.config.ftpEnabled) {\n\t\t\tthis.sendTo(obj.from, obj.command, { error: \"FTP not enabled\" }, obj.callback);\n\t\t\treturn;\n\t\t}\n\t\tconst msg = obj.message as { filename: string };\n\t\tconst filename = path.basename(msg.filename);\n\t\tconst remotePath = path.posix.join(this.config.ftpPath || \"/\", filename);\n\t\tconst client = this.createFtpClient();\n\t\ttry {\n\t\t\tawait this.ftpConnect(client);\n\t\t\tconst chunks: Buffer[] = [];\n\t\t\tconst writable = new Writable({\n\t\t\t\twrite(chunk, _enc, cb) {\n\t\t\t\t\tchunks.push(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk as string));\n\t\t\t\t\tcb();\n\t\t\t\t},\n\t\t\t});\n\t\t\tawait client.downloadTo(writable, remotePath);\n\t\t\tconst buf = Buffer.concat(chunks);\n\t\t\tconst scripts = await this.parseBuffer(buf, filename);\n\t\t\tthis.sendTo(obj.from, obj.command, { scripts }, obj.callback);\n\t\t} catch (e) {\n\t\t\tthis.sendTo(obj.from, obj.command, { error: (e as Error).message }, obj.callback);\n\t\t} finally {\n\t\t\tclient.close();\n\t\t}\n\t}\n\n\t// \u2500\u2500\u2500 SMB \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\tprivate createSmbClient(): typeof SMB2 {\n\t\treturn new SMB2({\n\t\t\tshare: `\\\\\\\\${this.config.smbHost}\\\\${this.config.smbShare}`,\n\t\t\tusername: this.config.smbUser || \"\",\n\t\t\tpassword: this.config.smbPassword || \"\",\n\t\t\tdomain: this.config.smbDomain || \"\",\n\t\t});\n\t}\n\n\tprivate smbReaddir(smb: typeof SMB2, dirPath: string): Promise<string[]> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tsmb.readdir(dirPath, (err: Error | null, files: string[]) => {\n\t\t\t\tif (err) {\n\t\t\t\t\treject(err);\n\t\t\t\t} else {\n\t\t\t\t\tresolve(files);\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate smbReadFile(smb: typeof SMB2, filePath: string): Promise<Buffer> {\n\t\treturn new Promise((resolve, reject) => {\n\t\t\tsmb.readFile(filePath, (err: Error | null, data: Buffer) => {\n\t\t\t\tif (err) {\n\t\t\t\t\treject(err);\n\t\t\t\t} else {\n\t\t\t\t\tresolve(data);\n\t\t\t\t}\n\t\t\t});\n\t\t});\n\t}\n\n\tprivate async handleListSmbFiles(obj: ioBroker.Message): Promise<void> {\n\t\tif (!this.config.smbEnabled) {\n\t\t\tthis.sendTo(obj.from, obj.command, { error: \"SMB not enabled\" }, obj.callback);\n\t\t\treturn;\n\t\t}\n\t\tconst smb = this.createSmbClient();\n\t\ttry {\n\t\t\tconst smbPath = this.config.smbPath || \"\";\n\t\t\tconst entries = await this.smbReaddir(smb, smbPath);\n\t\t\tconst files = entries\n\t\t\t\t.filter(n => {\n\t\t\t\t\treturn (\n\t\t\t\t\t\t(n.startsWith(\"iobroker\") || n.startsWith(\"javascript\")) &&\n\t\t\t\t\t\t(n.endsWith(\".tar.gz\") || n.endsWith(\".tar\") || n.endsWith(\".json\") || n.endsWith(\".jsonl\"))\n\t\t\t\t\t);\n\t\t\t\t})\n\t\t\t\t.sort()\n\t\t\t\t.reverse();\n\t\t\tthis.sendTo(obj.from, obj.command, { files, path: smbPath }, obj.callback);\n\t\t} catch (e) {\n\t\t\tthis.sendTo(obj.from, obj.command, { error: (e as Error).message }, obj.callback);\n\t\t} finally {\n\t\t\tsmb.disconnect();\n\t\t}\n\t}\n\n\tprivate async handleParseSmbFile(obj: ioBroker.Message): Promise<void> {\n\t\tif (!this.config.smbEnabled) {\n\t\t\tthis.sendTo(obj.from, obj.command, { error: \"SMB not enabled\" }, obj.callback);\n\t\t\treturn;\n\t\t}\n\t\tconst msg = obj.message as { filename: string };\n\t\tconst filename = path.basename(msg.filename);\n\t\tconst smbPath = this.config.smbPath || \"\";\n\t\tconst filePath = smbPath ? `${smbPath}\\\\${filename}` : filename;\n\t\tconst smb = this.createSmbClient();\n\t\ttry {\n\t\t\tconst buf = await this.smbReadFile(smb, filePath);\n\t\t\tconst scripts = await this.parseBuffer(buf, filename);\n\t\t\tthis.sendTo(obj.from, obj.command, { scripts }, obj.callback);\n\t\t} catch (e) {\n\t\t\tthis.sendTo(obj.from, obj.command, { error: (e as Error).message }, obj.callback);\n\t\t} finally {\n\t\t\tsmb.disconnect();\n\t\t}\n\t}\n\n\t// \u2500\u2500\u2500 Parsing \u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\u2500\n\n\tprivate async parseBuffer(buf: Buffer, filename: string): Promise<ScriptEntry[]> {\n\t\tconst name = filename.toLowerCase();\n\t\tif (name.endsWith(\".tar.gz\") || name.endsWith(\".tgz\") || name.endsWith(\".tar\")) {\n\t\t\treturn this.parseTarArchive(buf, name.endsWith(\".tar\") && !name.endsWith(\".tar.gz\"));\n\t\t}\n\t\treturn this.parseJsonContent(buf.toString(\"utf8\"), filename);\n\t}\n\n\tprivate async parseTarArchive(buf: Buffer, isPlainTar: boolean): Promise<ScriptEntry[]> {\n\t\tconst tmpDir = await fs.mkdtemp(path.join(os.tmpdir(), \"script-restore-\"));\n\t\tconst tmpFile = path.join(tmpDir, `archive.tar${isPlainTar ? \"\" : \".gz\"}`);\n\t\ttry {\n\t\t\tawait fs.writeFile(tmpFile, buf);\n\n\t\t\tconst extractFlag = isPlainTar ? \"-xf\" : \"-xzf\";\n\t\t\ttry {\n\t\t\t\tawait execAsync(\n\t\t\t\t\t`tar ${extractFlag} \"${tmpFile}\" -C \"${tmpDir}\" --wildcards` +\n\t\t\t\t\t\t` \"*/objects.jsonl\" \"*/objects.json\" \"*/scripts.json\" \"*/script.json\"` +\n\t\t\t\t\t\t` 2>/dev/null`,\n\t\t\t\t);\n\t\t\t} catch {\n\t\t\t\tawait execAsync(`tar ${extractFlag} \"${tmpFile}\" -C \"${tmpDir}\" 2>/dev/null`).catch(() => {});\n\t\t\t}\n\n\t\t\tconst targets = [\"objects.jsonl\", \"objects.json\", \"scripts.json\", \"script.json\"];\n\t\t\tconst found = await this.findFile(tmpDir, targets);\n\t\t\tif (!found) {\n\t\t\t\tthrow new Error(\n\t\t\t\t\t\"Keine passende Datei im Archiv gefunden (objects.json, objects.jsonl, scripts.json, script.json)\",\n\t\t\t\t);\n\t\t\t}\n\n\t\t\tconst content = await fs.readFile(found, \"utf8\");\n\t\t\treturn this.parseJsonContent(content, path.basename(found));\n\t\t} finally {\n\t\t\tawait fs.rm(tmpDir, { recursive: true, force: true }).catch(() => {});\n\t\t}\n\t}\n\n\tprivate async findFile(dir: string, names: string[]): Promise<string | null> {\n\t\tconst walk = async (d: string): Promise<string | null> => {\n\t\t\tlet entries: Dirent[];\n\t\t\ttry {\n\t\t\t\tentries = (await fs.readdir(d, { withFileTypes: true, encoding: \"utf8\" })) as unknown as Dirent[];\n\t\t\t} catch {\n\t\t\t\treturn null;\n\t\t\t}\n\t\t\tfor (const e of entries) {\n\t\t\t\tconst p = path.join(d, String(e.name));\n\t\t\t\tif (e.isDirectory()) {\n\t\t\t\t\tconst found = await walk(p);\n\t\t\t\t\tif (found) {\n\t\t\t\t\t\treturn found;\n\t\t\t\t\t}\n\t\t\t\t} else if (names.includes(String(e.name))) {\n\t\t\t\t\treturn p;\n\t\t\t\t}\n\t\t\t}\n\t\t\treturn null;\n\t\t};\n\t\treturn walk(dir);\n\t}\n\n\tprivate parseJsonContent(content: string, filename: string): ScriptEntry[] {\n\t\tconst scripts: ScriptEntry[] = [];\n\t\tconst trimmed = content.trimStart();\n\n\t\tconst isJsonl =\n\t\t\tfilename.endsWith(\".jsonl\") ||\n\t\t\t(trimmed.startsWith(\"{\") && !trimmed.startsWith('{\\n \"') && trimmed.includes(\"\\n{\"));\n\n\t\tif (isJsonl) {\n\t\t\tfor (const line of content.split(\"\\n\")) {\n\t\t\t\tconst l = line.trim();\n\t\t\t\tif (!l) {\n\t\t\t\t\tcontinue;\n\t\t\t\t}\n\t\t\t\ttry {\n\t\t\t\t\tconst item = JSON.parse(l) as Record<string, unknown>;\n\t\t\t\t\tthis.processItem(\n\t\t\t\t\t\t(item._id || item.id) as string,\n\t\t\t\t\t\t(item.value || item.doc || item) as Record<string, unknown>,\n\t\t\t\t\t\tscripts,\n\t\t\t\t\t);\n\t\t\t\t} catch {\n\t\t\t\t\t// skip invalid lines\n\t\t\t\t}\n\t\t\t}\n\t\t} else {\n\t\t\tconst data = JSON.parse(content) as Record<string, unknown>;\n\t\t\tfor (const [k, v] of Object.entries(data)) {\n\t\t\t\tthis.processItem(k, v as Record<string, unknown>, scripts);\n\t\t\t}\n\t\t}\n\n\t\treturn scripts.sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));\n\t}\n\n\tprivate processItem(key: string, val: unknown, scripts: ScriptEntry[]): void {\n\t\tif (!key || typeof val !== \"object\" || val === null) {\n\t\t\treturn;\n\t\t}\n\t\tconst v = val as Record<string, unknown>;\n\n\t\tif ([\"channel\", \"device\", \"folder\", \"meta\"].includes(v.type as string)) {\n\t\t\treturn;\n\t\t}\n\t\tif (v.type !== \"script\" && !key.startsWith(\"script.js.\")) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst c = v.common as Record<string, unknown> | undefined;\n\t\tif (!c || (c.engineType === undefined && c.source === undefined)) {\n\t\t\treturn;\n\t\t}\n\n\t\tconst rawEngineType = typeof c.engineType === \"string\" ? c.engineType : \"JS\";\n\t\tconst engineType = rawEngineType.toLowerCase();\n\t\tlet stype: string;\n\t\tif (engineType.includes(\"ts\") || engineType.includes(\"typescript\")) {\n\t\t\tstype = \"TypeScript\";\n\t\t} else if (engineType.includes(\"blockly\")) {\n\t\t\tstype = \"Blockly\";\n\t\t} else if (engineType.includes(\"rules\")) {\n\t\t\tstype = \"Rules\";\n\t\t} else {\n\t\t\tstype = \"JS\";\n\t\t}\n\n\t\tlet name: string;\n\t\tconst nameObj = c.name;\n\t\tif (typeof nameObj === \"object\" && nameObj !== null) {\n\t\t\tconst n = nameObj as Record<string, string>;\n\t\t\tname = n.de || n.en || Object.values(n)[0] || key.split(\".\").pop() || key;\n\t\t} else {\n\t\t\tname = typeof nameObj === \"string\" && nameObj ? nameObj : (key.split(\".\").pop() ?? key);\n\t\t}\n\n\t\tconst scriptPath = key.startsWith(\"script.js.\") ? key.slice(10) : key;\n\n\t\tscripts.push({\n\t\t\tname,\n\t\t\tpath: scriptPath,\n\t\t\ttype: stype,\n\t\t\tsource: typeof c.source === \"string\" ? c.source : \"\",\n\t\t});\n\t}\n}\n\nif (require.main !== module) {\n\tmodule.exports = (options: Partial<utils.AdapterOptions> | undefined) => new ScriptRestore(options);\n} else {\n\t(() => new ScriptRestore())();\n}\n"],
5
+ "mappings": "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",
6
6
  "names": []
7
7
  }
package/io-package.json CHANGED
@@ -1,20 +1,46 @@
1
1
  {
2
2
  "common": {
3
3
  "name": "script-restore",
4
- "version": "0.0.4",
4
+ "version": "0.0.5",
5
5
  "news": {
6
- "0.0.2": {
7
- "en": "update README, fix SVG icon dimensions, add full language translations",
8
- "de": "rEADME aktualisieren, SVG-Icondimensionen beheben, vollständige Übersetzungen hinzufügen",
9
- "ru": "обновление README, исправление размеров значков SVG, добавление полных языковых переводов",
10
- "pt": "atualizar README, corrigir dimensões do ícone SVG, adicionar traduções completas do idioma",
11
- "nl": "update README, fix SVG pictogram afmetingen, voeg volledige taal vertalingen",
12
- "fr": "mettre à jour README, corriger les dimensions des icônes SVG, ajouter des traductions en langue complète",
13
- "it": "aggiornare README, correggere le dimensioni dell'icona SVG, aggiungere traduzioni in lingua completa",
14
- "es": "actualizar README, fijar dimensiones de icono SVG, añadir traducciones completas del idioma",
15
- "pl": "aktualizacja README, poprawianie wymiarów ikon SVG, dodawanie pełnych tłumaczeń językowych",
16
- "uk": "оновлення README, зафіксувати розміри значків SVG, додати повномовні переклади",
17
- "zh-cn": "更新 README, 修正 SVG 图标尺寸, 添加完整的语言翻译"
6
+ "0.0.5": {
7
+ "en": "Add FTP/SMB as optional backup sources with test button; make local source optional; configurable Beaufort threshold",
8
+ "de": "FTP/SMB als optionale Backup-Quellen mit Testbutton; lokale Quelle optional schaltbar; SMB-Versionshinweis",
9
+ "ru": "Добавлены FTP/SMB как дополнительные источники резервных копий с кнопкой тестирования; локальный источник теперь опционален",
10
+ "pt": "Adicionar FTP/SMB como fontes de backup opcionais com botão de teste; fonte local opcional",
11
+ "nl": "FTP/SMB toegevoegd als optionele back-upbronnen met testknop; lokale bron optioneel",
12
+ "fr": "Ajout de FTP/SMB comme sources de sauvegarde optionnelles avec bouton de test; source locale optionnelle",
13
+ "it": "Aggiunta di FTP/SMB come sorgenti di backup opzionali con pulsante di test; sorgente locale opzionale",
14
+ "es": "Añadir FTP/SMB como fuentes de copia de seguridad opcionales con botón de prueba; fuente local opcional",
15
+ "pl": "Dodano FTP/SMB jako opcjonalne źródła kopii zapasowych z przyciskiem testowym; lokalne źródło opcjonalne",
16
+ "uk": "Додано FTP/SMB як опціональні джерела резервних копій з кнопкою тестування; локальне джерело опціональне",
17
+ "zh-cn": "添加 FTP/SMB 作为可选备份来源(含测试按钮);本地来源可选"
18
+ },
19
+ "0.0.4": {
20
+ "en": "Improve dark theme detection: live switching via MutationObserver and storage events",
21
+ "de": "Dark-Theme-Erkennung verbessert: Live-Wechsel per MutationObserver und Storage-Events",
22
+ "ru": "Улучшено определение тёмной темы: переключение в реальном времени через MutationObserver и storage-события",
23
+ "pt": "Melhoria na detecção do tema escuro: troca em tempo real via MutationObserver e eventos de armazenamento",
24
+ "nl": "Verbeterde detectie van donker thema: live wisseling via MutationObserver en opslag-events",
25
+ "fr": "Amélioration de la détection du thème sombre : changement en direct via MutationObserver et événements de stockage",
26
+ "it": "Migliorata la rilevazione del tema scuro: cambio in tempo reale tramite MutationObserver ed eventi di archiviazione",
27
+ "es": "Mejora en la detección del tema oscuro: cambio en vivo mediante MutationObserver y eventos de almacenamiento",
28
+ "pl": "Ulepszone wykrywanie ciemnego motywu: przełączanie na żywo przez MutationObserver i zdarzenia storage",
29
+ "uk": "Покращено визначення темної теми: перемикання в реальному часі через MutationObserver та події сховища",
30
+ "zh-cn": "改进暗色主题检测:通过 MutationObserver 和存储事件实现实时切换"
31
+ },
32
+ "0.0.3": {
33
+ "en": "Add dark theme support for admin tab UI",
34
+ "de": "Dark-Theme-Unterstützung für die Admin-Tab-Oberfläche hinzugefügt",
35
+ "ru": "Добавлена поддержка тёмной темы для вкладки администратора",
36
+ "pt": "Adicionado suporte ao tema escuro para a interface da aba admin",
37
+ "nl": "Ondersteuning voor donker thema toegevoegd aan de admin-tab UI",
38
+ "fr": "Ajout du support du thème sombre pour l'interface de l'onglet admin",
39
+ "it": "Aggiunto supporto al tema scuro per l'interfaccia della scheda admin",
40
+ "es": "Añadido soporte de tema oscuro para la interfaz de la pestaña admin",
41
+ "pl": "Dodano obsługę ciemnego motywu dla interfejsu karty administratora",
42
+ "uk": "Додано підтримку темної теми для інтерфейсу вкладки адміністратора",
43
+ "zh-cn": "为管理员选项卡界面添加暗色主题支持"
18
44
  },
19
45
  "0.0.1": {
20
46
  "en": "Initial release",
@@ -114,7 +140,22 @@
114
140
  ]
115
141
  },
116
142
  "native": {
117
- "backupPath": "/opt/iobroker/backups"
143
+ "localEnabled": true,
144
+ "backupPath": "/opt/iobroker/backups",
145
+ "ftpEnabled": false,
146
+ "ftpHost": "",
147
+ "ftpPort": 21,
148
+ "ftpUser": "",
149
+ "ftpPassword": "",
150
+ "ftpPath": "/",
151
+ "ftpSecure": false,
152
+ "smbEnabled": false,
153
+ "smbHost": "",
154
+ "smbShare": "",
155
+ "smbPath": "",
156
+ "smbUser": "",
157
+ "smbPassword": "",
158
+ "smbDomain": ""
118
159
  },
119
160
  "objects": [],
120
161
  "instanceObjects": []
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "iobroker.script-restore",
3
- "version": "0.0.4",
3
+ "version": "0.0.5",
4
4
  "description": "Restore ioBroker scripts from backup archives",
5
5
  "author": {
6
6
  "name": "ipod86",
@@ -25,7 +25,9 @@
25
25
  "node": ">= 20"
26
26
  },
27
27
  "dependencies": {
28
- "@iobroker/adapter-core": "^3.3.2"
28
+ "@iobroker/adapter-core": "^3.3.2",
29
+ "@marsaud/smb2": "^0.18.0",
30
+ "basic-ftp": "^5.2.0"
29
31
  },
30
32
  "devDependencies": {
31
33
  "@alcalzone/release-script": "^5.1.1",