/**
* Name: scriptRecovery
* Zweck: Wiederherstellen/Auflisten von Skripten aus verschiedenen Quellen
* Datum: 24.01.2023
* Autor: @fastfoot
* Forum: https://forum.iobroker.net/post/930558
*
* Changelog: 24.01.2023 - verbessertes Error-Handling
* 21.01.2023 - Windows/Mac kompatibel
*/
/* Einstellungen */
// zusätzliche Meldungen, dafür unbedingt auch in der JS-Instanz den Debug-Modus aktivieren!!!
const dbg = false;
// hier wird die JSON Tabelle gespeichert, der DP wird erstellt wenn generateTable true ist
const idBase = "0_userdata.0.scriptRecovery";
const idJson = "jsonTable";
// erzeugt eine JSON Tabelle der gefundenen Skripte für eine evtl. VIS
const generateTable = true;
// erzeugt ein Listing der gefundenen Skripte(__Listingxxx.json)
const generateListing = true;
// Dateien ins Filesystem schreiben, false = brauchbar wenn man nur ein Listing will
const restoreToFilesystem = false;
// Extension für importierte Skripte, zur Unterscheidung von existierenden Skripten.
const scriptSuffix = "_rcvr";
// Skripte werden sofort ins System geladen(Endung: wie in scriptSuffix) ACHTUNG: AUF EIGENE GEFAHR!!!!
// existierende Skripte werden nicht überschrieben
const restoreToSystemDB = false;
// Array Skriptnamen-Filter, wenn vorhanden werden nur diese Skripte behandelt z.B. ['script 1','script 2']
// Nur Teil-Namens sind erlaubt und Groß- Kleinschreibung ist egal
// const scriptFilter = ['^[abc]', '[xyz0-9]$', 'script'];// Skripte beginnen mit a,b oder c oder enden mit x,y,z oder einer Zahl oder haben <<script>> im Namen
const scriptFilter = [''];
// Array Inhaltsfilter z.B. ['0_userdata.0.example_state', 'Wetter'] zum Suchen nach bestimmten Begriffen
// const contentFilter = ['0_user', 'sql', 'sendto'];
const contentFilter = [''];
// Array - Skripte in diesen Ordnern werden nicht berücksichtigt(für rootFolder 'root' oder '/')
// const excludeFolders = ['/','global', 'löschen'];
const excludeFolders = [''];
// Array - Nur Skripte in diesen Ordnern und deren Unterordnern werden berücksichtigt(für NUR rootFolder 'root' oder '/')
// const includeFolders = ['/','tools', 'forum'];
const includeFolders = [''];
// Array Typ Filter um nur bestimmte Typen zu berücksichtigen (Blockly, Javascript, Rules, Typescript)
// const typeFilter = ['Blockly','javas'];// findet Blockly- oder Javascript Skripte
const typeFilter = [''];
// hier liegt die in inputFile definierte Datei, entweder Archiv oder bereits extrahierte Datei
let inputPath = "../../scriptInput";
// hier landen die extrahierten Skripte und die Listing-Datei, wird bei Skriptstart geleert und angelegt wenn nicht vorhanden
const outputPath = "../../scriptOutput";
// Datei mit den Skripten(autoObjects = letzte objects.json[l], autoBackup = letzte backupdatei, autoScripts = letztes Skript Backup)
let inputFile = "";
// Beispiele für mögliche Dateien
// letzte Dateien JS-Controller und BackitUp-Adapter
inputFile = "autoObjects";
// inputFile = "autoBackup";
// inputFile = "autoScripts";
// Backup von JS-Controller
// inputFile = '2023-01-23_13-30_objects.jsonl.gz';
// inputFile = "objects.jsonl";
// inputFile = "2022-12-19_12-18_objects.json.gz";
// inputFile = "objects.json";
// BackitUp-Adapter
// inputFile = "iobroker_2023_01_23-13_34_49_backupiobroker.tar.gz";
// inputFile = "backup.json";
// inputFile = "javascripts_2023_01_23-13_35_04_backupiobroker.tar.gz";
// inputFile = "script.json";
// Konsole: 'iobroker backup'
// inputFile = "2023_01_23-13_36_02_backupiobroker.tar.gz";
// JS-Adapter Export
// inputFile = "2023-01-24-scripts.zip";
// inputFile = "FullBackup-scripts_2023-01-11.zip";
// Einzelskript
// inputFile = 'sqlBlockly.js';
/* **************************************************************************************
* ******************* *******************
* ******************* Ab hier keine Änderungen vornehmen *******************
* ******************* *******************
* **************************************************************************************
*/
// @ts-ignore
const fse = require("fs-extra");
const Path = require("path");
// @ts-ignore
const tar = require('tar');
// @ts-ignore
const JSZip = require('jszip');
const zlib = require('node:zlib');
const { pipeline } = require('node:stream');
// const os = require("os");
// hier werden die aus evtl. Archiven(tar.gz, gz, zip) extrahierten Skripte temporär abgelegt. Wird bei Skriptstart angelegt wenn nicht vorhanden
const tmpRoot = '../../scriptTmp';// os.tmpdir();
const tmpPath = Path.resolve(tmpRoot, scriptName.slice(10));
// Ignoriert Fehler in der JSONL Datenbank
const ignoreJsonlErrors = true;
// komprimiere JSONL Datenbank beim Einlesen
const compressJsonl = false;
// wichtig damit der mirrorPath nicht überschrieben wird und somit alle Skripte gelöscht werden
const mirrorPath = getObject("system.adapter.javascript.0").native.mirrorPath;
start();
async function start() {
if (dbg) console.error('Debugmode aktiv, unbedingt auch in der JS-Instanz den Debug-Modus aktivieren!!!');
fse.ensureDirSync(tmpPath);
fse.emptyDirSync(tmpPath);
fse.ensureDirSync(outputPath);
if (outputPath != mirrorPath) {
fse.emptyDirSync(outputPath);
}
try { await main(); }
catch (e) { return console.error('Ein Fehler ist aufgetreten!') }
stopScript(scriptName);
}
async function main() {
let dataFile = '';
let allScripts = {};
switch (inputFile.toLocaleLowerCase()) {
case 'autoobjects':
inputPath = '../../iobroker-data/backup-objects';
inputFile = await getNewestFile(inputPath, /objects\.jsonl\.gz/);//await getLatestObjects();
break;
case 'autobackup':
inputPath = '../../backups';
inputFile = await getNewestFile(inputPath, /^[2i.+\.tar\.gz]/);//await getLatestBackup();
break;
case 'autoscripts':
inputPath = '../../backups';
inputFile = await getNewestFile(inputPath, /^javascript.+\.tar\.gz/);//await getLatestScripts();
break;
default:
}
if (inputFile === '' || inputFile === undefined) {
return console.error(`[main()] - Keine Datei in ${inputPath} gefunden!`);
}
if (!(await fse.pathExists(Path.resolve(inputPath, inputFile)))) {
return console.error(`[main()] - Die Datei ${inputFile} in ${inputPath} wurde nicht gefunden!`);
}
const fullArchiveName = Path.resolve(inputPath, inputFile);
if (inputFile.indexOf('.tar.gz') > 0) {
dataFile = await tarExtract(fullArchiveName);
if (dataFile.endsWith('backup.json')) {
allScripts = await handleBackup(dataFile);
} else if (dataFile.endsWith('script.json')) {
allScripts = await handleJson(dataFile);
}
} else if (inputFile.endsWith('backup.json')) {
allScripts = await handleBackup(fullArchiveName);
} else if (inputFile.endsWith('script.json')) {
allScripts = await handleJson(fullArchiveName);
} else if (inputFile.indexOf('.jsonl.gz') > 0) {
dataFile = await gzipExtract(fullArchiveName);
allScripts = await handleJsonl(dataFile);
} else if (inputFile.endsWith('objects.jsonl')) {
allScripts = await handleJsonl(fullArchiveName);
} else if (inputFile.indexOf('.json.gz') > 0) {
dataFile = await gzipExtract(fullArchiveName);
allScripts = await handleJson(dataFile);
} else if (inputFile.endsWith('objects.json')) {
allScripts = await handleJson(fullArchiveName);
} else if (inputFile.indexOf('.zip') > 0) {
dataFile = await zipExtract(fullArchiveName);
if (dataFile)
allScripts = await handleExport(dataFile);
else
return console.warn('Keine passenden Dateien gefunden, Filter prüfen!');
} else if (inputFile) {
try {
let b = fse.lstatSync(fullArchiveName).isDirectory()
} catch (e) {
return console.error('[main()] - Fehler: ' + e);
}
allScripts = await handleExport(inputFile, inputPath);
} else {
return console.error('[main()] - Fehler: Variable inputFile falsch belegt')
}
if (dbg) {
console.debug('[main()] - datafile = ' + (dataFile != '' ? dataFile : inputFile))
};
handleScripts(allScripts);
}
async function handleScripts(allScripts) {
let res = {};
let tableData = [];
for (let key in allScripts) {
let fileExtension = "";
let script = allScripts[key];
const keyNew = key + scriptSuffix;
const folder = key.replace('script.js.', '').replace(script.common.name, '').replace(/\.$/, '');
// exclude Filter
if (isExcludedFolder(folder)) continue;
// folder Filter
if (!isIncludedFolder(folder)) continue;
// script Filter
if (!isName(script.common.name)) continue;
// Typ Filter
if (!isType(script.common.engineType)) continue;
// Inhalts Filter
if (!isContent(script.common.source)) continue;
script.common.enabled = false;
script.common.debug = false;
script.common.verbose = false;
script.common.expert = false;
if (!existsObject(keyNew) && restoreToSystemDB) {
if (dbg) console.debug('[handleSripts()] - Key: ' + key);
const oldName = script.common.name;
script.common.name += scriptSuffix;
await createScriptFolder(keyNew.slice(0, keyNew.length - script.common.name.length - 1));
await setObjectAsync(keyNew, { type: "script", common: script.common, native: {} });
script.common.name = oldName;
}
if (restoreToFilesystem) {
let data = script.common.source;
switch (script.common.engineType.toLowerCase()) {
case "blockly":
fileExtension = ".xml";
if (script.common.source.length)
data = handleBlockly(data);
else console.warn("217 Leeres Skript: " + script.common.name);
break;
case "rules":
fileExtension = ".js";
break;
case "javascript/js":
fileExtension = ".js";
break;
case "typescript/ts":
fileExtension = ".ts";
break;
default:
fileExtension = ".js";
}
if (data && data.length) {
if (dbg) console.debug('[handleSripts()] - Key: ' + key);
if (dbg) console.debug('[handleSripts()] - Pfad: ' + Path.resolve(outputPath, key.substring(10)));
let scriptName = key.split('.').pop();
let scriptPfad = key.slice(10, key.length - scriptName.length - 1).replace(/\./g, '/');
scriptPfad = Path.resolve(outputPath, scriptPfad);
fse.ensureDirSync(scriptPfad);
fse.writeFile(
Path.resolve(scriptPfad, scriptName) + fileExtension,
data,
(e) => {
if (e) console.error("[handleSripts()] - Fehler beim Schreiben der Datei:" + e.code);
}
);
} else {
if (dbg) console.debug('[handleSripts()] - No source data: ' + key);
}
}
if (generateTable) {
tableData = generateJsonTable(script, tableData);
}
if (dbg) console.debug('[handleSripts()] - Key: ' + key)
res[key.substring(10)] = script.common.engineType;
}
if (generateTable) {
if (!tableData.length) {
tableData.push({ Warnung: 'Keine Daten vorhanden, Filter prüfen!' });
} else {
tableData.sort((a, b) => a.Name.localeCompare(b.Name));
let no = 0;
tableData.forEach(rec => rec.Nr = ++no);
}
if (await createDatapoints(idBase)) {
setState(`${idBase}.${idJson}`, JSON.stringify(tableData));
}
}
if (generateListing) {
let b = {};
if (Object.keys(res).length) {
// sortieren
let a = Object.keys(res).sort();
for (let i = 0; i < a.length; i++) {
b[i + 1 + " " + a[i]] = res[a[i]];
}
} else {
b.Warnung = 'Keine Daten vorhanden, Filter prüfen!';
}
const fullFileName = Path.resolve(outputPath, '__Listing_' + inputFile + ".json");
fse.writeFile(fullFileName, JSON.stringify(b, null, 3), (e) => {
if (e) console.error("[handleSripts()] - Schreibfehler bei Ergebnisdatei");
});
}
}
/* extrahiere Skripte aus iobroker Datenbank (neueste Version JSONL)*/
async function handleJsonl(dataFile) {
const allScripts = {};
// @ts-ignore
const DB = require("@alcalzone/jsonl-db").JsonlDB;
const dbOptions = {
autoCompress: { onOpen: compressJsonl },
ignoreReadErrors: ignoreJsonlErrors,
};
const db = new DB(dataFile, dbOptions);
try {
await db.open();
} catch (e) {
console.error(`[handleJsonl()] - Fehler beim Öffnen der Datenbank ${dataFile} in ${inputPath}` + e);
}
db.forEach((obj, key) => {
if (obj.type === "script") {
allScripts[key] = obj;
}
});
await db.close();
return allScripts;
}
/* extrahiere Skripte aus iobroker Datenbank (ältere Version JSON)*/
async function handleJson(dataFile) {
let allData = '';
const allScripts = [];
let allObjects = new Object();
try {
allData = fse.readFileSync(dataFile, "utf8");
} catch (e) {
console.error(`[handleJson()] - Fehler beim Lesen von ${inputFile} in ${inputPath}: ` + e);
}
try {
allObjects = JSON.parse(allData);
} catch (e) {
console.error("[handleJson()] - Fehlerhafte Daten: ==> " + e);
}
for (let prop in allObjects) {
const obj = allObjects[prop];
if (obj.type === "script") {
allScripts[prop] = obj;
}
}
return allScripts;
}
/* extrahiere Skripte aus backup.json des Backitup-Adapers */
async function handleBackup(dataFile) {
let allData = '';
const allScripts = {};
let allObjects = [];
try {
allData = fse.readFileSync(dataFile, "utf8");
} catch (e) {
console.error(`[handleBackup()] - Fehler beim Lesen von ${inputFile} in ${inputPath}: ` + e);
}
try {
allObjects = JSON.parse(allData).objects;
} catch (e) {
console.error('[handleBackup()] - Fehlerhafte Daten: ==> ' + e);
}
for (let obj of allObjects) {
if (obj.value.type === 'script') {
allScripts[obj.id] = obj.value;
}
}
return allScripts;
}
/* bearbeite Skripte */
async function handleExport(scriptListx, dir = tmpPath) {
const allScripts = {};
const scriptList = scriptListx.trim().split(' ');
let fileObj = {};
let sourceData = '';
scriptList.forEach((file) => {
const scriptObj = {
"_id": "script.js.",
"common": {
"name": "",
"engineType": "JavaScript/js",
"engine": "system.adapter.javascript.0",
"source": "",
"enabled": false,
"debug": false,
"verbose": false
},
"type": "script",
"native": {},
"ts": 0,
}
const scriptNam = getFileName(file);
if (dbg) console.debug('[handleExport()] - File: ' + file);
if (dbg) console.debug('[handleExport()] - Name: ' + scriptNam);
let scriptData;
scriptData = fse.readFileSync(Path.resolve(dir, file), 'utf8');
const regExport = new RegExp(/\/\* -- do not edit/);
if (regExport.test(scriptData)) {
const regObj = new RegExp(/(\{.+\})(?:\n-- do not edit prev)/s);
sourceData = scriptData.substring(scriptData.indexOf('END --*/') + 9);
if (regObj.test(scriptData)) {
try {
fileObj = JSON.parse(regObj.exec(scriptData)[1]) || '{}';
} catch (e) {
console.error('[handleExport()] - Fehler');
}
}
} else {
sourceData = scriptData;
}
const engineType = getEngineType(sourceData);
if (dbg) console.debug('[handleExport()] - ' + engineType);
if (dbg) console.debug('[handleExport()] - Type: ' + engineType);
if (dbg) console.debug('[handleExport()] - File: ' + file);
if (dbg) console.debug('[handleExport()] - Id: ' + file.replace(/\//g, '.').replace(/.json$/, ''));
scriptObj._id = `script.js.${file.replace(/\//g, '.').replace(/.json$|.js$/, '')}`;
scriptObj.ts = fileObj.ts || Date.now();
scriptObj.common.name = scriptNam;
scriptObj.common.source = sourceData;
scriptObj.common.engineType = fileObj.engineType || engineType;
scriptObj.common.engine = fileObj.engine || 'system.adapter.javascript.0';
scriptObj.common.enabled = fileObj.enabled || false;
scriptObj.common.debug = fileObj.debug || false;
scriptObj.common.verbose = fileObj.verbose || false;
if (fileObj && fileObj.engineType && fileObj.engineType.toLowerCase() === 'typescript/ts') {
scriptObj.common.sourceHash = fileObj.sourceHash;
scriptObj.common.compiled = fileObj.compiled;
}
allScripts[scriptObj._id] = scriptObj;
})
return allScripts;
}
function generateJsonTable(scriptData, tableData) {
const data = scriptData.common.source;
const dt = scriptData.ts && new Date(scriptData.ts) || 0;
let Zweck = '-',
Autor = '-',
Datum = dt && dt.getFullYear() + '-' + dt.getMonth() + 1 + '-' + dt.getDate() || '-',
Instance = scriptData.common.engine.split('.').pop(),
sName = scriptData.common.name
if (/(Zweck|Purpose):\s+(.*)/.test(data)) {
if (dbg) console.debug('[generateJsonTable()] - Zweck: ' + /(Zweck|Purpose):\s+(.*)/.exec(data))
Zweck = /(Zweck|Purpose):\s+(.*)/.exec(data)[2];
}
if (/(Autor|Author):\s+(.*)/.test(data)) {
Autor = /(Autor|Author):\s+(.*)/.exec(data)[2];
}
if (/(Datum|Date):\s+(.*)/.test(data)) {
Datum = /(Datum|Date):\s+(.*)/.exec(data)[2].replace(/(\d+).(\d+).(\d+)/, "$3-$2-$1");
}
let p = scriptData._id.lastIndexOf('.');
let ps = '';
if (p === 9) ps = '/';
else ps = '/' + scriptData._id.slice(10, p);
tableData.push({
Nr: 0,
Name: sName,
Pfad: ps,
Zweck: Zweck,
Autor: Autor,
Datum: Datum,
Instanz: Instance,
Typ: scriptData.common.engineType.split('/')[0]
})
return tableData;
}
function isName(name) {
if (dbg) console.debug('[isName()] - Name: ' + name);
const regExp = new RegExp(scriptFilter.slice(0).join("|"), "i");
return regExp.test(name);
}
function isExcludedFolder(folder) {
if (folder === '') folder = 'root/';
const filter = excludeFolders.slice(0).join("|") || '&&&';
if (dbg) console.debug('[isExcludedFolder()] - Folder: ' + folder);
if (dbg) console.debug('[isExcludedFolder()] - Filter: ' + filter);
const regExp = new RegExp(filter, "i");
return regExp.test(folder);
}
function isIncludedFolder(folder) {
if (folder === '') folder = 'root/';
const filter = includeFolders.slice(0).join("|");
if (dbg) console.debug('[isIncludedFolder()] - Folder: ' + folder);
if (dbg) console.debug('[isIncludedFolder()] - Filter: ' + filter);
const regExp = new RegExp(filter, "i");
return regExp.test(folder);
}
function isType(typ) {
const regExp = new RegExp(typeFilter.slice(0).join("|"), "i");
return regExp.test(typ);
}
function isContent(source) {
const regExp = new RegExp(contentFilter.slice(0).join("|"), "i");
return regExp.test(source);
}
function handleBlockly(source) {
const pos = source.lastIndexOf("\n");
if (pos !== -1) {
source = source.substring(pos + 3);
if (source.indexOf("JTNDeG1sJTIweG1") > -1) {
source = decodeURIComponent(
Buffer.from(source, "base64").toString("utf8")
);
return prettifyXml(source);
}
} else return prettifyXml(source);
//from Stackoverflow
function prettifyXml(xml) {
var reg = /(>)\s*(<)(\/*)/g;
xml = xml.replace(/\r|\n/g, ''); //deleting already existing whitespaces
xml = xml.replace(reg, "$1\r\n$2$3");
return xml;
}
}
const tarList = async file => {
const filenames = []
await tar.list({
file,
onentry: entry => {
if (/script.json|backup.json/.test(entry.path)) {
filenames.push(entry.path)
}
},
})
return filenames
}
const tarExtract = async archive => {
const fileList = await tarList(archive);
const strip = fileList.includes('backup/backup.json') ? 1 : 0;
if (dbg) console.debug(strip + ' ' + fileList);
const opts = {
file: archive,
strip,
cwd: tmpPath
}
await tar.extract(opts, fileList);
return Path.resolve(tmpPath, fileList[0].replace('backup/', ''));
}
const zipExtract = async archive => {
return new Promise(async (resolve, reject) => {
let fileList = [];
const data = fse.readFileSync(archive)
const zip = new JSZip();
const zipObj = await zip.loadAsync(data);
zipObj.filter((a, b) => false)
zipObj.folder().forEach(async (fileName, entry) => {
const fullPath = Path.resolve(tmpPath, fileName);
const pos = fullPath.lastIndexOf(Path.sep) + 1;
const realName = fullPath.substring(pos);
const realPath = fullPath.substring(0, pos - 1);
if (!entry.dir && isName(realName) && realName.indexOf('_dir.json') < 0) {
if (dbg) console.debug(realPath + ' = ' + realName);
fse.ensureDirSync(Path.resolve(tmpPath, realPath));
fse.writeFileSync(fullPath, Buffer.from(entry._data.compressedContent));
fileList.push(fileName);
}
})
resolve(fileList.join(' '));
})
}
const gzipExtract = async archive => {
return new Promise(async (resolve, reject) => {
const objectsFile = archive.split(Path.sep).pop().replace('.gz', '').substring(17);
const gunzip = zlib.createGunzip();
const readStream = fse.createReadStream(archive);
const writeStream = fse.createWriteStream(Path.resolve(tmpPath, objectsFile));
readStream.on('error', e => {
reject(console.error(e));
})
writeStream.on('error', e => {
reject(console.error(e));
})
pipeline(readStream, gunzip, writeStream, e => {
if (e) reject(console.error(e));
resolve(Path.resolve(tmpPath, objectsFile));
})
})
}
function getFileName(fullName) {
return fullName.substring(fullName.lastIndexOf('/') + 1, fullName.lastIndexOf('.'));
}
function isBlockly(sourceData) {
const pos = sourceData.lastIndexOf('\n');
const testData = sourceData.substring(pos + 1);
return /^[\/][\/]JTNDeG1sJTIwe/.test(testData);
}
function isRules(sourceData) {
const pos = sourceData.lastIndexOf('\n');
const testData = sourceData.substring(pos + 1);
return /[/][/]{"triggers":/.test(testData);
}
function isTypescript(sourceData) {
return /(let|const|var)\s*[a-zA-Z0-9]+:*(string|object|number)/.test(sourceData);
}
function getEngineType(sourceData) {
return isBlockly(sourceData) ? 'Blockly'
: isRules(sourceData) ? 'Rules'
: isTypescript(sourceData) ? 'Typescript/ts'
: 'Javascript/js';
}
async function getNewestFile(dir, filter) {
return new Promise((resolve, reject) => {
return fse.readdir(dir, (e, f) => {
if (e) reject(e);
const s = f.map(f => {
let t;
const ff = Path.resolve(dir, f);
try {
t = fse.lstatSync(ff).mtime.getTime();
} catch (e) {
reject(e)
}
return { name: f, time: t }
})
.sort((a, b) => b.time - a.time)
.map(v => v.name)
.filter(v => filter.test(v));
resolve(s[0]);
})
})
}
// create data points if not existing
async function createDatapoints(idBase) {
let dp,
idKey,
firstRun = false;
if (!idBase.startsWith('0_userdata.0')) return firstRun;
firstRun = true;
const stateAttributes = {};
stateAttributes[idJson] = { "name": "Skripte Info", "type": "json", "role": "", "read": true, "write": true, "desc": "enthält Skript Tabelle", "def": "" }
//createScriptFolder(idBase);
for (let key in stateAttributes) {
idKey = idBase + '.' + key;
if (!(await existsStateAsync(idKey))) {
dp = stateAttributes[key];
firstRun = true;
let e = await createStateAsync(idKey, dp);
if (e) console.error('[createDatapoints()] - createState: ' + e);
}
}
return firstRun;
}
async function createScriptFolder(id) {
const arr = id.split('.');
const preId = arr[0] + '.' + arr[1];
if (preId.length === id.length || (preId != 'script.js' /*&& preId != '0_userdata.0'*/)) return;
const idNew = id.replace(/[\s"]/g, '_');
if (!(await existsObjectAsync(idNew))) {
const obj = new Object({
"type": "folder",
"common": {
name: arr[arr.length - 1]
},
"native": {},
})
await setObjectAsync(idNew, obj);
}
arr.pop();
id = arr.join('.');
await createScriptFolder(id);
}
onStop(() => {
fse.removeSync(tmpPath);
if (dbg) console.log('[onStop()] - Skript wurde nach Beendigung automatisch gestoppt!');
})