Home Reference Source

cables_dev/cables_electron/src/electron/electron_api.js

import { app, ipcMain, net, shell } from "electron";
import fs from "fs";
import path from "path";
import { marked } from "marked";
import mkdirp from "mkdirp";
import { promisify } from "util";

import jsonfile from "jsonfile";
import sanitizeFileName from "sanitize-filename";
import { utilProvider } from "cables-shared-api";
import { createRequire } from "module";
import cables from "../cables.js";
import logger from "../utils/logger.js";
import doc from "../utils/doc_util.js";
import helper from "../utils/helper_util.js";
import opsUtil from "../utils/ops_util.js";
import subPatchOpUtil from "../utils/subpatchop_util.js";
import settings from "./electron_settings.js";
import projectsUtil from "../utils/projects_util.js";
import electronApp from "./main.js";
import filesUtil from "../utils/files_util.js";
import libsUtil from "../utils/libs_util.js";
import StandaloneZipExport from "../export/export_zip_standalone.js";
import StandaloneExport from "../export/export_patch_standalone.js";

class ElectronApi
{
    constructor()
    {
        this._log = logger;
    }

    init()
    {
        ipcMain.handle("talkerMessage", async (event, cmd, data, topicConfig = {}) =>
        {
            try
            {
                return this.talkerMessage(cmd, data, topicConfig);
            }
            catch (e)
            {
                return this.error(e.message, e);
            }
        });

        ipcMain.on("platformSettings", (event, _cmd, _data) =>
        {
            settings.data.buildInfo = settings.getBuildInfo();
            event.returnValue = settings.data;
        });

        ipcMain.on("cablesConfig", (event, _cmd, _data) =>
        {
            event.returnValue = cables.getConfig();
        });

        ipcMain.on("getStartupLog", (event, _cmd, _data) =>
        {
            event.returnValue = this._log.startUpLog || [];
        });

        ipcMain.on("getOpDir", (event, data) =>
        {
            let opName = opsUtil.getOpNameById(data.opId);
            if (!opName) opName = data.opName;
            event.returnValue = opsUtil.getOpAbsolutePath(opName);
        });

        ipcMain.on("getOpModuleDir", (event, data) =>
        {
            let opName = opsUtil.getOpNameById(data.opId);
            if (!opName) opName = data.opName;
            const opDir = opsUtil.getOpAbsolutePath(opName);
            event.returnValue = path.join(opDir, "node_modules", data.moduleName);
        });

        ipcMain.on("getOpModuleLocation", (event, data) =>
        {
            let opName = opsUtil.getOpNameById(data.opId);
            if (!opName) opName = data.opName;
            const opDir = opsUtil.getOpAbsolutePath(opName);
            const moduleDir = path.join(opDir, "node_modules");
            const moduleRequire = createRequire(moduleDir);
            if (moduleRequire)
            {
                try
                {
                    let location = moduleRequire.resolve(data.moduleName);
                    if (data.asUrl) location = helper.pathToFileURL(location);
                    event.returnValue = location;
                }
                catch (e)
                {
                    this._log.error(e.message);
                    event.returnValue = null;
                }
            }
            else
            {
                event.returnValue = null;
            }
        });

        ipcMain.on("getOpModules", (event, data) =>
        {
            let deps = [];
            if (!data.opName) return [];
            const opName = data.opName;
            const opDocFile = opsUtil.getOpAbsoluteJsonFilename(opName);
            if (fs.existsSync(opDocFile))
            {
                let opDoc = jsonfile.readFileSync(opDocFile);
                if (opDoc)
                {
                    deps = opDoc.dependencies || [];
                }
            }
            event.returnValue = deps.filter((dep) => { return dep.type === "npm"; }).map((dep) => { return dep.src[0]; });
        });
    }

    async talkerMessage(cmd, data, topicConfig = {})
    {
        let response = null;
        if (!cmd) return this.error("UNKNOWN_COMMAND", null, "warn");
        if (typeof this[cmd] === "function")
        {
            if (topicConfig.needsProjectFile)
            {
                const projectFile = settings.getCurrentProjectFile();
                if (!projectFile)
                {
                    const newProjectFile = await electronApp.saveProjectFileDialog(data.name);
                    if (newProjectFile)
                    {
                        let patchData = null;
                        let currentProject = settings.getCurrentProject();
                        if (cmd === "savePatch" && data)
                        {
                            patchData = data;
                        }
                        projectsUtil.writeProjectToFile(newProjectFile, currentProject, patchData);
                        this.loadProject(newProjectFile);
                    }
                    else
                    {
                        return this.error("CANCELLED", null, "info");
                    }
                }
            }
            return this[cmd](data);
        }
        else
        {
            this._log.warn("no method for talkerMessage", cmd);
        }
        return response;
    }

    getOpInfo(data)
    {
        const opName = opsUtil.getOpNameById(data.opName) || data.opName;

        let warns = [];
        try
        {
            const currentProject = settings.getCurrentProject();
            if (currentProject)
            {
                let opDocs = projectsUtil.getOpDocsInProjectDirs(currentProject);
                opDocs = opDocs.filter((opDoc) => { return opDoc.name === opName; });
                opDocs.forEach((opDoc) =>
                {
                    if (opDoc.overrides)
                    {
                        opDoc.overrides.forEach((override) =>
                        {
                            warns.push({
                                "type": "project",
                                "id": "",
                                "text": "<a onclick=\"CABLESUILOADER.talkerAPI.send('openDir', { 'dir': '" + override + "'});\"><span class=\"icon icon-folder\"></span> this op overrides another op</a>"
                            });
                        });
                    }
                });
            }

            warns = warns.concat(opsUtil.getOpCodeWarnings(opName));

            if (opsUtil.isOpNameValid(opName))
            {
                const result = { "warns": warns };
                result.attachmentFiles = opsUtil.getAttachmentFiles(opName);

                const opDocs = doc.getDocForOp(opName);
                let changelogEntries = [];
                if (opDocs && opDocs.changelog)
                {
                    // copy array to not modify reference
                    changelogEntries = changelogEntries.concat(opDocs.changelog);
                    if (data.sort === "asc")
                    {
                        changelogEntries.sort((a, b) => { return a.date - b.date; });
                    }
                    else
                    {
                        changelogEntries.sort((a, b) => { return b.date - a.date; });
                    }
                    const numChangelogEntries = data.cl || 5;
                    result.changelog = changelogEntries.slice(0, numChangelogEntries);
                }
                return this.success("OK", result, true);
            }
            else
            {
                const result = { "warns": [] };
                result.attachmentFiles = [];
                return this.success("OK", result, true);
            }
        }
        catch (e)
        {
            this._log.warn("error when getting opinfo", opName, e.message);
            const result = { "warns": warns };
            result.attachmentFiles = [];
            return this.success("OK", result, true);
        }
    }

    async savePatch(patch)
    {
        const currentProject = settings.getCurrentProject();
        const currentProjectFile = settings.getCurrentProjectFile();

        const re = {
            "msg": "PROJECT_SAVED"
        };
        currentProject.updated = Date.now();
        currentProject.updatedByUser = settings.getCurrentUser().username;
        projectsUtil.writeProjectToFile(currentProjectFile, currentProject, patch);
        this.loadProject(currentProjectFile);
        re.updated = currentProject.updated;
        re.updatedByUser = currentProject.updatedByUser;
        return this.success("OK", re, true);
    }

    async patchCreateBackup()
    {
        const re = {
            "msg": "BACKUP_CREATED"
        };
        const currentProject = settings.getCurrentProject();
        const projectFile = await electronApp.saveProjectFileDialog();
        if (!projectFile)
        {
            logger.info("no backup file chosen");
            return this.error("no backup file chosen", null, "info");
        }

        const backupProject = projectsUtil.getBackup(currentProject);
        fs.writeFileSync(projectFile, JSON.stringify(backupProject));
        return this.success("OK", re, true);
    }

    getPatch()
    {
        const patchPath = settings.getCurrentProjectFile();
        const currentUser = settings.getCurrentUser();
        let currentProject = settings.getCurrentProject();
        if (patchPath && fs.existsSync(patchPath))
        {
            currentProject = fs.readFileSync(patchPath);
            currentProject = JSON.parse(currentProject.toString("utf-8"));
            if (!currentProject.hasOwnProperty("userList")) currentProject.userList = [currentUser];
            if (!currentProject.hasOwnProperty("teams")) currentProject.teams = [];
        }
        else
        {
            if (!currentProject)
            {
                const newProject = projectsUtil.generateNewProject(settings.getCurrentUser());
                this.loadProject(patchPath, newProject);
                currentProject = newProject;
            }
        }
        currentProject.allowEdit = true;
        currentProject.summary = currentProject.summary || {};
        currentProject.summary.title = currentProject.name;
        currentProject.summary.allowEdit = true;
        return this.success("OK", currentProject, true);
    }

    async newPatch()
    {
        electronApp.openPatch();
        return this.success("OK", true, true);
    }

    fileUpload(data)
    {
        const target = cables.getAssetPath();
        if (!data.fileStr) return;
        if (!data.filename)
        {
            return;
        }
        let saveAs = data.filename;
        if (!path.isAbsolute(data.filename)) saveAs = path.join(target, path.join("/", data.filename));
        const buffer = Buffer.from(data.fileStr.split(",")[1], "base64");
        fs.writeFileSync(saveAs, buffer);
        return this.success("OK", { "filename": path.basename(saveAs) }, true);
    }

    async getAllProjectOps()
    {
        const currentUser = settings.getCurrentUser();
        const project = settings.getCurrentProject();

        let opDocs = [];

        if (!project)
        {
            return this.success("OK", opDocs, true);
        }

        let projectOps = [];
        let projectNamespaces = [];
        let usedOpIds = [];
        // add all ops that are used in the toplevel of the project, save them as used
        project.ops.forEach((projectOp) =>
        {
            projectOps.push((opsUtil.getOpNameById(projectOp.opId)));
            usedOpIds.push(projectOp.opId);
        });

        // add all ops in any of the project op directory
        const otherDirsOps = projectsUtil.getOpDocsInProjectDirs(project).map((opDoc) => { return opDoc.name; });
        projectOps = projectOps.concat(otherDirsOps);

        // now we should have all the ops that are used in the project, walk subPatchOps
        // recursively to get their opdocs
        const subPatchOps = subPatchOpUtil.getOpsUsedInSubPatches(project);
        subPatchOps.forEach((subPatchOp) =>
        {
            const opName = opsUtil.getOpNameById(subPatchOp.opId);
            const nsName = opsUtil.getCollectionNamespace(opName);
            projectOps.push(opName);
            if (opsUtil.isCollection(nsName)) projectNamespaces.push(nsName);
            usedOpIds.push(subPatchOp.opId);
        });

        projectOps = helper.uniqueArray(projectOps);
        usedOpIds = helper.uniqueArray(usedOpIds);
        projectNamespaces = helper.uniqueArray(projectNamespaces);
        const coreOpDocs = doc.getOpDocs();
        projectOps.forEach((opName) =>
        {
            let opDoc = doc.getDocForOp(opName, coreOpDocs);
            if (opDoc)
            {
                if (!opDoc.name) opDoc.name = opName;
                opDocs.push(opDoc);
            }
        });

        // get opdocs for all the collected ops
        opDocs = opsUtil.addOpDocsForCollections(projectNamespaces, opDocs);
        opDocs.forEach((opDoc) =>
        {
            if (usedOpIds.includes(opDoc.id)) opDoc.usedInProject = true;
        });

        opsUtil.addPermissionsToOps(opDocs, currentUser, [], project);
        opsUtil.addVersionInfoToOps(opDocs);

        opDocs = doc.makeReadable(opDocs);
        return this.success("OK", opDocs, true);
    }


    async getOpDocsAll()
    {
        const currentUser = settings.getCurrentUser();
        const currentProject = settings.getCurrentProject();
        let opDocs = doc.getOpDocs(true, true);
        opDocs = opDocs.concat(doc.getCollectionOpDocs("Ops.Extension.Standalone", currentUser));
        opDocs = opDocs.concat(projectsUtil.getOpDocsInProjectDirs(currentProject));
        const cleanDocs = doc.makeReadable(opDocs);
        opsUtil.addPermissionsToOps(cleanDocs, null);

        const extensions = doc.getAllExtensionDocs(true, true);
        const libs = projectsUtil.getAvailableLibs(currentProject);
        const coreLibs = projectsUtil.getCoreLibs();

        return this.success("OK", {
            "opDocs": cleanDocs,
            "extensions": extensions,
            "teamNamespaces": [],
            "libs": libs,
            "coreLibs": coreLibs
        }, true);
    }

    async getOpDocs(data)
    {
        const opName = opsUtil.getOpNameById(data) || data;
        if (!opName)
        {
            return {};
        }
        const result = {};
        result.opDocs = [];

        const opDoc = doc.getDocForOp(opName);
        result.content = "No docs yet...";

        const opDocs = [];
        if (opDoc)
        {
            opDocs.push(opDoc);
            if (opDoc.dependencies)
            {
                const opPackages = opsUtil.getOpNpmPackages(opName);
                const packageDir = opsUtil.getOpAbsolutePath(opName);
                result.dependenciesOutput = await electronApp.installPackages(packageDir, opPackages, opName);
            }
            result.opDocs = doc.makeReadable(opDocs);
            result.opDocs = opsUtil.addPermissionsToOps(result.opDocs, null);
            const c = doc.getOpDocMd(opName);
            if (c) result.content = marked(c || "");
            return this.success("OK", result, true);
        }
        else
        {
            let text = "Could not find op with id " + data + " in:";
            const footer = "Try adding other directories via 'Manage Op Directories' after loading the patch.";
            const reasons = [];

            const errorVars = {
                "text": text,
                "footer": footer,
                "reasons": reasons,
                "hideEnvButton": true,
            };

            const currentProject = settings.getCurrentProject();
            const projectOpDirs = projectsUtil.getProjectOpDirs(currentProject, true);
            projectOpDirs.forEach((projectOpDir) =>
            {
                const link = "<a onclick=\"CABLESUILOADER.talkerAPI.send('openDir', { 'dir': '" + projectOpDir + "'});\"><span class=\"icon icon-folder\"></span> " + projectOpDir + "</a>";
                reasons.push(link);
            });

            if (net.isOnline())
            {
                const getOpEnvironmentDocs = promisify(opsUtil.getOpEnvironmentDocs.bind(opsUtil));
                try
                {
                    const envDocs = await getOpEnvironmentDocs(data);
                    if (envDocs && envDocs.environments && envDocs.environments.length > 0)
                    {
                        const otherEnvName = envDocs.environments[0];
                        errorVars.editorLink = "https://" + otherEnvName + "/op/" + envDocs.name;
                        errorVars.otherEnvButton = "Visit " + otherEnvName;

                        text = "Could not find <a href=\"" + errorVars.editorLink + "\" target=\"_blank\">" + envDocs.name + "</a> in:";

                        envDocs.environments.forEach((envName) =>
                        {
                            const opLink = "https://" + envName + "/op/" + envDocs.name;
                            reasons.push("Found <a href=\"" + opLink + "\" target=\"_blank\">" + envDocs.name + "</a> on " + envName);
                        });
                    }
                    errorVars.text = text;
                    errorVars.reasons = reasons;
                    errorVars.hideEnvButton = false;
                }
                catch (e)
                { // something went wrong, no internet or something, this is informational anyhow}
                }
            }

            return this.error("OP_NOT_FOUND", errorVars, "warn");
        }
    }

    saveOpCode(data)
    {
        const opName = opsUtil.getOpNameById(data.opname);
        const code = data.code;
        let returnedCode = code;

        const format = opsUtil.validateAndFormatOpCode(code);
        if (format.error)
        {
            const {
                line,
                message
            } = format.message;
            this._log.info({
                line,
                message
            });
            return {
                "error": {
                    line,
                    message
                }
            };
        }
        const formatedCode = format.formatedCode;
        if (data.format || opsUtil.isCoreOp(opName))
        {
            returnedCode = formatedCode;
        }
        returnedCode = opsUtil.updateOpCode(opName, settings.getCurrentUser(), returnedCode);
        doc.updateOpDocs(opName);

        return this.success("OK", { "opFullCode": returnedCode }, true);
    }

    getOpCode(data)
    {
        const opName = opsUtil.getOpNameById(data.opId || data.opname);
        if (opsUtil.opExists(opName))
        {
            filesUtil.registerOpChangeListeners([opName]);
            let code = opsUtil.getOpCode(opName);
            return this.success("OK", {
                "name": opName,
                "id": data.opId,
                "code": code
            }, true);
        }
        else
        {
            let code = "//empty file...";
            return this.success("OK", {
                "name": opName,
                "id": null,
                "code": code
            }, true);
        }
    }

    async opAttachmentAdd(data)
    {
        const opName = opsUtil.getOpNameById(data.opname) || data.opname;
        const attName = data.name;
        const p = opsUtil.addAttachment(opName, "att_" + attName, "hello attachment");
        this._log.info("created attachment!", p);
        doc.updateOpDocs(opName);
        this.success("OK");
    }

    async opAttachmentDelete(data)
    {
        const opName = opsUtil.getOpNameById(data.opname) || data.opname;
        const attName = data.name;
        opsUtil.deleteAttachment(opName, attName);
        this.success("OK");
    }

    async opAddCoreLib(data)
    {
        const opName = opsUtil.getOpNameById(data.opname) || data.opname;
        const libName = sanitizeFileName(data.name);
        const opFilename = opsUtil.getOpJsonPath(data.opname);
        const libFilename = cables.getCoreLibsPath() + libName;
        const existsLib = fs.existsSync(libFilename + ".js");
        if (!existsLib)
        {
            this.error("LIB_NOT_FOUND");
            return;
        }

        try
        {
            const obj = jsonfile.readFileSync(opFilename);
            obj.coreLibs = obj.coreLibs || [];

            if (obj.coreLibs.indexOf(libName) === -1) obj.coreLibs.push(libName);

            try
            {
                jsonfile.writeFileSync(opFilename, obj, {
                    "encoding": "utf-8",
                    "spaces": 4
                });
                doc.updateOpDocs(opName);
                this.success("OK", {});
            }
            catch (writeErr)
            {
                this.error("WRITE_ERROR");
            }
        }
        catch (err)
        {
            this.error("UNKNOWN_ERROR");
        }
    }

    async opAddLib(data)
    {
        const opName = opsUtil.getOpNameById(data.opname) || data.opname;
        const libName = sanitizeFileName(data.name);

        const filename = opsUtil.getOpJsonPath(opName);

        const libExists = libsUtil.libExists(libName);
        if (!libExists)
        {
            this.error("LIB_NOT_FOUND", 400);
            return;
        }

        try
        {
            const obj = jsonfile.readFileSync(filename);
            obj.libs = obj.libs || [];

            if (obj.libs.indexOf(libName) === -1) obj.libs.push(libName);

            try
            {
                jsonfile.writeFileSync(filename, obj, {
                    "encoding": "utf-8",
                    "spaces": 4
                });
                doc.updateOpDocs(opName);
                this.success("OK");
            }
            catch (writeErr)
            {
                this.error("WRITE_ERROR", 500);
            }
        }
        catch (err)
        {
            this.error("UNKNOWN_ERROR", 500);
        }
    }

    async opRemoveLib(data)
    {
        const opName = opsUtil.getOpNameById(data.opname) || data.opname;
        const libName = sanitizeFileName(data.name);

        const filename = opsUtil.getOpJsonPath(opName);

        try
        {
            const obj = jsonfile.readFileSync(filename);
            obj.libs = obj.libs || [];

            if (obj.libs.includes(libName)) obj.libs = obj.libs.filter((lib) => { return lib !== libName; });

            try
            {
                jsonfile.writeFileSync(filename, obj, {
                    "encoding": "utf-8",
                    "spaces": 4
                });
                doc.updateOpDocs(opName);
                this.success("OK");
            }
            catch (writeErr)
            {
                this.error("WRITE_ERROR", 500);
            }
        }
        catch (err)
        {
            this.error("UNKNOWN_ERROR", 500);
        }
    }

    async opRemoveCoreLib(data)
    {
        const opName = opsUtil.getOpNameById(data.opname) || data.opname;
        const libName = sanitizeFileName(data.name);
        const opFilename = opsUtil.getOpJsonPath(opName);

        try
        {
            const obj = jsonfile.readFileSync(opFilename);
            obj.coreLibs = obj.coreLibs || [];

            if (obj.coreLibs.includes(libName)) obj.coreLibs = obj.coreLibs.filter((lib) => { return lib !== libName; });

            try
            {
                jsonfile.writeFileSync(opFilename, obj, {
                    "encoding": "utf-8",
                    "spaces": 4
                });
                doc.updateOpDocs(opName);
                this.success("OK");
            }
            catch (writeErr)
            {
                this.error("WRITE_ERROR", 500);
            }
        }
        catch (err)
        {
            this.error("UNKNOWN_ERROR", 500);
        }
    }

    async opAttachmentGet(data)
    {
        const opName = opsUtil.getOpNameById(data.opname) || data.opname;
        const attName = data.name;
        const content = opsUtil.getAttachment(opName, attName);
        return this.success("OK", { "content": content }, true);
    }

    async getCollectionOpDocs(data)
    {
        let opDocs = [];
        const collectionName = data.name;
        const currentUser = settings.getCurrentUser();
        if (collectionName)
        {
            const opNames = opsUtil.getCollectionOpNames(collectionName, true);
            opDocs = opsUtil.addOpDocsForCollections(opNames, opDocs);
            opDocs = opsUtil.addVersionInfoToOps(opDocs);
            opDocs = opsUtil.addPermissionsToOps(opDocs, currentUser);
        }
        return this.success("OK", { "opDocs": doc.makeReadable(opDocs) }, true);
    }


    getBuildInfo()
    {
        return this.success("OK", settings.getBuildInfo(), true);
    }

    formatOpCode(data)
    {
        const code = data.code;
        if (code)
        {
            // const format = opsUtil.validateAndFormatOpCode(code);
            // if (format.error)
            // {
            //     const {
            //         line,
            //         message
            //     } = format.message;
            //     return {
            //         "error": {
            //             line,
            //             message
            //         }
            //     };
            // }
            // else
            // {
            //     return {
            //         "opFullCode": format.formatedCode,
            //         "success": true
            //     };
            // }

            return this.success("OK", {
                "opFullCode": code
            }, true);
        }
        else
        {
            return this.success("OK", {
                "opFullCode": ""
            }, true);
        }
    }

    saveUserSettings(data)
    {
        if (data && data.settings)
        {
            settings.setUserSettings(data.settings);
        }
    }

    checkProjectUpdated(data)
    {
        const project = settings.getCurrentProject();
        if (project)
        {
            return this.success("OK", {
                "updated": null,
                "updatedByUser": project.updatedByUser,
                "buildInfo": project.buildInfo,
                "maintenance": false,
                "disallowSave": false
            }, true);
        }
        else
        {
            return this.success("OK", {
                "updated": "",
                "updatedByUser": "",
                "buildInfo": settings.getBuildInfo(),
                "maintenance": false,
                "disallowSave": false
            }, true);
        }
    }

    getChangelog(data)
    {
        const obj = {};
        obj.items = [];
        obj.ts = Date.now();
        return this.success("OK", obj, true);
    }

    opAttachmentSave(data)
    {
        let opName = data.opname;
        if (opsUtil.isOpId(data.opname)) opName = opsUtil.getOpNameById(data.opname);
        const result = opsUtil.updateAttachment(opName, data.name, data.content, false);
        return this.success("OK", result, true);
    }

    setIconSaved()
    {
        let title = electronApp.editorWindow.getTitle();
        const pos = title.lastIndexOf(" *");
        let newTitle = title;
        if (pos !== -1) newTitle = title.substring(0, pos);
        electronApp.setDocumentEdited(false);
        electronApp.editorWindow.setTitle(newTitle);
    }

    setIconUnsaved()
    {
        const title = electronApp.editorWindow.getTitle();
        electronApp.setDocumentEdited(true);
        electronApp.editorWindow.setTitle(title + " *");
    }

    saveScreenshot(data)
    {
        const currentProject = settings.getCurrentProject();
        if (!currentProject || !data || !data.screenshot)
        {
            return this.error("NO_PROJECT");
        }
        currentProject.screenshot = data.screenshot;
        projectsUtil.writeProjectToFile(settings.getCurrentProjectFile(), currentProject);
        return this.success("OK", { "msg": "OK" }, true);
    }

    getFilelist(data)
    {
        let files;
        switch (data.source)
        {
        case "patch":
            files = filesUtil.getPatchFiles();
            break;
        case "lib":
            files = filesUtil.getLibraryFiles();
            break;
        default:
            files = [];
            break;
        }
        return this.success("OK", files, true);
    }

    getFileDetails(data)
    {
        let filePath = helper.fileURLToPath(data.filename);
        const fileDb = filesUtil.getFileDb(filePath, settings.getCurrentProject(), settings.getCurrentUser(), new Date().getTime());
        return this.success("OK", filesUtil.getFileInfo(fileDb), true);
    }

    getLibraryFileInfo(data)
    {
        const fileName = filesUtil.realSanitizeFilename(data.filename);
        const fileCategory = filesUtil.realSanitizeFilename(data.fileCategory);

        const filePath = path.join(fileCategory, fileName);
        const libraryPath = cables.getAssetLibraryPath();
        const finalPath = path.join(libraryPath, filePath);

        if (!fs.existsSync(finalPath))
        {
            return this.success("OK", {}, true);
        }
        else
        {
            const infoFileName = finalPath + ".fileinfo.json";
            let filename = "";

            if (fs.existsSync(infoFileName))filename = infoFileName;

            if (filename === "")
            {
                return this.success("OK", {}, true);
            }
            else
            {
                const fileInfo = JSON.parse(fs.readFileSync(filename));
                return this.success("OK", fileInfo, true);
            }
        }
    }

    checkOpName(data)
    {
        const opDocs = doc.getOpDocs(false, false);
        const newName = data.v;
        const sourceName = data.sourceName || null;
        const currentUser = settings.getCurrentUser();
        const currentProject = settings.getCurrentProject();
        const result = this._getFullRenameResponse(opDocs, newName, sourceName, currentUser, currentProject, true, data.rename, data.opTargetDir);
        result.checkedName = newName;
        return this.success("OK", result, true);
    }

    getRecentPatches()
    {
        const recents = settings.getRecentProjects();
        const result = [];
        for (let i = 0; i < recents.length; i++)
        {
            const recentProject = recents[i];
            let screenShot = recentProject.screenshot;
            if (!screenShot)
            {
                screenShot = projectsUtil.getScreenShotFileName(recentProject, "png");
                if (!fs.existsSync(screenShot)) screenShot = path.join(cables.getUiDistPath(), "/img/placeholder_dark.png");
            }
            result[i] = recentProject;
            result[i].thumbnail = screenShot;
        }
        return this.success("OK", result.slice(0, 10), true);
    }

    async opCreate(data)
    {
        let opName = data.opname;
        const currentUser = settings.getCurrentUser();
        const opDocDefaults = {
            "layout": data.layout,
            "libs": data.libs,
            "coreLibs": data.coreLibs
        };
        const result = opsUtil.createOp(opName, currentUser, data.code, opDocDefaults, data.attachments, data.opTargetDir);
        filesUtil.registerOpChangeListeners([opName]);
        projectsUtil.invalidateProjectCaches();

        return this.success("OK", result, true);
    }

    opUpdate(data)
    {
        let opName = data.opname;
        if (opsUtil.isOpId(data.opname)) opName = opsUtil.getOpNameById(data.opname);
        const currentUser = settings.getCurrentUser();
        const result = opsUtil.updateOp(currentUser, opName, data.update, { "formatCode": data.formatCode });
        return this.success("OK", { "data": result }, true);
    }

    opSaveLayout(data)
    {
        const layout = data.layout;
        const opName = opsUtil.getOpNameById(data.opname) || layout.name;
        return this.success("OK", opsUtil.saveLayout(opName, layout), true);
    }

    opSetSummary(data)
    {
        const opName = opsUtil.getOpNameById(data.opId) || data.name;
        let summary = data.summary || "";
        if (summary === "No Summary") summary = "";
        const opDocFile = opsUtil.getOpAbsoluteJsonFilename(opName);
        if (fs.existsSync(opDocFile))
        {
            let opDoc = jsonfile.readFileSync(opDocFile);
            if (opDoc)
            {
                opDoc.summary = summary;
                opDoc = doc.cleanOpDocData(opDoc);
                jsonfile.writeFileSync(opDocFile, opDoc, {
                    "encoding": "utf-8",
                    "spaces": 4
                });
                doc.updateOpDocs();
            }
            return this.success("OK", opDoc, true);
        }
        else
        {
            return this.error("UNKNOWN_OP", null, "error");
        }
    }

    opClone(data)
    {
        const newName = data.name;
        const oldName = opsUtil.getOpNameById(data.opname) || data.opname;
        const currentUser = settings.getCurrentUser();
        const cloned = opsUtil.cloneOp(oldName, newName, currentUser, data.opTargetDir);
        projectsUtil.invalidateProjectCaches();
        return this.success("OK", cloned, true);
    }

    opRename(data)
    {
        projectsUtil.invalidateProjectCaches();

        const oldId = data.opname;
        const newName = data.name;
        const oldName = opsUtil.getOpNameById(oldId);

        const currentUser = settings.getCurrentUser();
        const currentProject = settings.getCurrentProject();
        let opNamespace = opsUtil.getNamespace(newName);

        const opDocs = doc.getOpDocs(false, false);
        const renameResults = this._getFullRenameResponse(opDocs, newName, oldName, currentUser, currentProject, opsUtil.isPrivateOp(newName), true);
        if (!oldName)
        {
            renameResults.problems.push("No name for source op given.");
        }

        const result = renameResults;
        result.title = "rename - " + oldName + " - " + newName;
        result.objName = newName;
        result.oldName = oldName;
        result.opId = oldId;
        result.opname = oldName;
        result.opNamespace = opNamespace;
        result.newopname = newName;
        result.shortname = opsUtil.getOpShortName(newName);
        result.oldShortName = opsUtil.getOpShortName(oldName);
        const versions = opsUtil.getOpVersionNumbers(oldName, opDocs);
        result.otherVersions = versions.length > 1 ? versions.filter((v) => { return v.name !== oldName; }) : [];
        result.renamePossible = renameResults.problems.length === 0;

        if (Object.keys(renameResults.problems).length > 0)
        {
            result.problems = Object.values(renameResults.problems);
            return this.success("PROBLEMS", result);
        }

        const start = Date.now();

        result.user = currentUser;
        result.showresult = true;

        let removeOld = true;
        let renameSuccess = false;
        if (opsUtil.isUserOp(newName))
        {
            renameSuccess = opsUtil.renameToUserOp(oldName, newName, currentUser, removeOld);
        }
        else if (opsUtil.isTeamOp(newName))
        {
            renameSuccess = opsUtil.renameToTeamOp(oldName, newName, currentUser, removeOld);
        }
        else if (opsUtil.isExtensionOp(newName))
        {
            renameSuccess = opsUtil.renameToExtensionOp(oldName, newName, currentUser, removeOld);
        }
        else if (opsUtil.isPatchOp(newName))
        {
            renameSuccess = opsUtil.renameToPatchOp(oldName, newName, currentUser, removeOld, false);
        }
        else
        {
            renameSuccess = opsUtil.renameToCoreOp(oldName, newName, currentUser, removeOld);
        }

        projectsUtil.invalidateProjectCaches();

        if (!renameSuccess)
        {
            return this.error("ERROR", 500);
        }
        else
        {
            this._log.verbose("*" + currentUser.username + " finished after " + Math.round((Date.now() - start) / 1000) + " seconds ");
            return this.success("OK", result);
        }
    }

    opDelete(data)
    {
        const opName = opsUtil.getOpNameById(data.opId) || data.opName;
        opsUtil.deleteOp(opName);
        return this.success("OP_DELETED", { "opNames": [opName] });
    }

    async _installOpDependencies(opName)
    {
        const results = [];
        if (opName)
        {
            const targetDir = opsUtil.getOpAbsolutePath(opName);
            const opPackages = opsUtil.getOpNpmPackages(opName);
            if (opPackages.length === 0)
            {
                const nodeModulesDir = path.join(targetDir, "node_modules");
                if (fs.existsSync(nodeModulesDir)) fs.rmSync(nodeModulesDir, { "recursive": true });
                results.push({ "stdout": "nothing to install", "packages": [] });
                return this.success("EMPTY", results, false);
            }
            else
            {
                const npmResults = await electronApp.installPackages(targetDir, opPackages, opName);
                if (npmResults.stderr)
                {
                    return this.error("NPM_ERROR", npmResults, "error");
                }
                else
                {
                    return this.success("OK", npmResults);
                }
            }
        }
        else
        {
            results.push({ "stdout": "nothing to install", "packages": [] });
            return this.success("EMPTY", results, false);
        }
    }

    async installProjectDependencies()
    {
        const currentProject = settings.getCurrentProject();
        if (!currentProject)
        {
            return this.error("UNSAVED_PROJECT", [{ "stdout": "please save your project first", "packages": [] }]);
        }

        const results = [];
        let projectPackages = {};
        currentProject.ops.forEach((op) =>
        {
            const opName = opsUtil.getOpNameById(op.opId);
            if (opName)
            {
                const targetDir = opsUtil.getOpAbsolutePath(opName);
                const opPackages = opsUtil.getOpNpmPackages(opName);
                if (opPackages.length > 0)
                {
                    if (!projectPackages.hasOwnProperty(targetDir)) projectPackages[targetDir] = [];
                    projectPackages[targetDir] = {
                        "opName": opName,
                        "packages": opPackages
                    };
                }
            }
        });
        if (Object.keys(projectPackages).length === 0)
        {
            results.push({ "stdout": "nothing to install", "packages": [] });
            return this.success("EMPTY", results, false);
        }
        else
        {
            const allNpmInstalls = [];
            for (let targetDir in projectPackages)
            {
                const opData = projectPackages[targetDir];
                allNpmInstalls.push(electronApp.installPackages(targetDir, opData.packages, opData.opName));
            }

            const npmResults = await Promise.all(allNpmInstalls);
            if (npmResults.some((result) => { return result.error; }))
            {
                return this.error("NPM_ERROR", npmResults, "error");
            }
            else
            {
                return this.success("OK", npmResults);
            }
        }
    }

    async addOpPackage(data)
    {
        const currentProjectDir = settings.getCurrentProjectDir();
        const targetDir = data.targetDir || currentProjectDir;
        const npmResults = await electronApp.addOpPackage(targetDir, data.package);
        return this.success("OK", npmResults);
    }

    async openDir(options = {})
    {
        await shell.openPath(options.dir || app.getPath("home"));
        return this.success("OK", {}, true);
    }

    async openOpDir(options)
    {
        const opName = opsUtil.getOpNameById(options.opId) || options.opName;
        if (!opName) return;
        const opDir = opsUtil.getOpAbsoluteFileName(opName);
        if (opDir)
        {
            shell.showItemInFolder(opDir);
            return this.success("OK", {}, true);
        }
    }

    async openProjectDir()
    {
        const projectFile = settings.getCurrentProjectFile();
        if (projectFile)
        {
            shell.showItemInFolder(projectFile);
            return this.success("OK", {});
        }
    }

    async openAssetDir(data)
    {
        let assetPath = helper.fileURLToPath(data.url, true);
        if (fs.existsSync(assetPath))
        {
            const stats = fs.statSync(assetPath);
            if (stats.isDirectory())
            {
                shell.openPath(assetPath);
                return this.success("OK", {});
            }
            else
            {
                shell.showItemInFolder(assetPath);
                return this.success("OK", {});
            }
        }
        else
        {
            shell.openPath(cables.getAssetPath());
            return this.success("OK", {});
        }
    }

    async selectFile(data)
    {
        if (data)
        {
            let pickedFileUrl = null;
            if (data.url)
            {
                let assetUrl = helper.fileURLToPath(data.url, true);
                let filter = ["*"];
                if (data.filter)
                {
                    filter = filesUtil.FILETYPES[data.filter] || ["*"];
                }
                pickedFileUrl = await electronApp.pickFileDialog(assetUrl, true, filter);
            }
            else
            {
                let file = data.dir;
                pickedFileUrl = await electronApp.pickFileDialog(file);
            }
            pickedFileUrl = helper.pathToFileURL(pickedFileUrl);
            return this.success("OK", pickedFileUrl, true);
        }
        else
        {
            return this.error("NO_FILE_SELECTED", null, "info");
        }
    }

    async selectDir(data)
    {
        const pickedFileUrl = await electronApp.pickDirDialog(data.dir);
        return this.success("OK", pickedFileUrl, true);
    }


    checkNumAssetPatches()
    {
        return this.success("OK", { "assets": [], "countPatches": 0, "countOps": 0 }, true);
    }

    async saveProjectAs(data)
    {
        const projectFile = await electronApp.saveProjectFileDialog(data.name);
        if (!projectFile)
        {
            return this.error("no project dir chosen", null, "info");
        }

        let collaborators = [];
        let usersReadOnly = [];

        const currentUser = settings.getCurrentUser();
        const origProject = settings.getCurrentProject();
        origProject._id = helper.generateRandomId();
        origProject.name = path.basename(projectFile);
        origProject.summary = origProject.summary || {};
        origProject.summary.title = origProject.name;
        origProject.userId = currentUser._id;
        origProject.cachedUsername = currentUser.username;
        origProject.created = Date.now();
        origProject.cloneOf = origProject._id;
        origProject.updated = Date.now();
        origProject.users = collaborators;
        origProject.usersReadOnly = usersReadOnly;
        origProject.visibility = "private";
        origProject.shortId = helper.generateShortId(origProject._id, Date.now());
        projectsUtil.writeProjectToFile(projectFile, origProject);
        this.loadProject(projectFile);
        electronApp.reload();
        return this.success("OK", origProject, true);
    }

    async gotoPatch(data)
    {
        let project = null;
        let projectFile = null;
        if (data && data.id)
        {
            projectFile = settings.getRecentProjectFile(data.id);
            if (projectFile) project = settings.getProjectFromFile(projectFile);
        }
        if (project && projectFile)
        {
            electronApp.openPatch(projectFile);
            return this.success("OK", true, true);
        }
        else
        {
            const file = await electronApp.pickProjectFileDialog();
            return this.success("OK", { "projectFile": file });
        }
    }

    updateFile(data)
    {
        this._log.info("file edit...");
        if (!data || !data.fileName)
        {
            return this.error("UNKNOWN_FILE");
        }

        const newPath = helper.fileURLToPath(data.fileName, true);
        if (!fs.existsSync(newPath)) mkdirp.sync(newPath);
        try
        {
            if (fs.existsSync(newPath))
            {
                this._log.info("delete old file ", newPath);
                fs.unlinkSync(newPath);
            }
        }
        catch (e) {}

        this._log.info("edit file", newPath);

        fs.writeFileSync(newPath, data.content);
        return this.success("OK", { "filename": newPath }, true);
    }

    getProjectOpDirs()
    {
        const currentProject = settings.getCurrentProject();
        const dirInfos = projectsUtil.getOpDirs(currentProject, false);

        const opDirs = {};
        if (currentProject && currentProject.ops)
        {
            currentProject.ops.forEach((op) =>
            {
                const opName = opsUtil.getOpNameById(op.opId);
                const opPath = opsUtil.getOpAbsolutePath(opName);
                if (opPath)
                {
                    if (!opDirs.hasOwnProperty(opPath)) opDirs[opPath] = 0;
                    opDirs[opPath]++;
                }
            });
        }

        dirInfos.forEach((dirInfo) =>
        {
            if (!dirInfo.hasOwnProperty("numUsedOps")) dirInfo.numUsedOps = 0;
            for (const opDir in opDirs)
            {
                const count = opDirs[opDir];
                if (opDir.startsWith(dirInfo.dir))
                {
                    dirInfo.numUsedOps += count;
                }
            }
        });

        return this.success("OK", dirInfos);
    }

    async addProjectOpDir()
    {
        let currentProject = settings.getCurrentProject();
        if (!currentProject) return this.error("Please save your project before adding op directories", null, "warn");
        const opDir = await electronApp.pickOpDirDialog();
        if (opDir)
        {
            currentProject = projectsUtil.addOpDir(currentProject, opDir, true);
            projectsUtil.writeProjectToFile(settings.getCurrentProjectFile(), currentProject);
        }
        return this.success("OK", projectsUtil.getProjectOpDirs(currentProject, true));
    }

    async removeProjectOpDir(dirName)
    {
        let currentProject = settings.getCurrentProject();
        if (!currentProject || !dirName) return this.success("OK", projectsUtil.getProjectOpDirs(currentProject, true));
        dirName = path.resolve(dirName);
        currentProject = projectsUtil.removeOpDir(currentProject, dirName);

        projectsUtil.writeProjectToFile(settings.getCurrentProjectFile(), currentProject);
        return this.success("OK", projectsUtil.getProjectOpDirs(currentProject, true));
    }

    saveProjectOpDirOrder(order)
    {
        let currentProject = settings.getCurrentProject();
        if (!currentProject || !order) return this.error("NO_PROJECT", null, "warn");
        currentProject = projectsUtil.reorderOpDirs(currentProject, order);
        return this.success("OK", projectsUtil.getProjectOpDirs(currentProject, true));
    }

    setProjectName(options)
    {
        const oldFile = settings.getCurrentProjectFile();
        let project = settings.getCurrentProject();
        project.name = options.name;
        const newFile = path.join(settings.getCurrentProjectDir(), projectsUtil.getProjectFileName(project));
        project.name = path.basename(newFile);
        project.summary = project.summary || {};
        project.summary.title = project.name;
        fs.renameSync(oldFile, newFile);
        settings.replaceInRecentProjects(oldFile, newFile);
        projectsUtil.writeProjectToFile(newFile, project);
        this.loadProject(newFile);
        const summary = projectsUtil.getSummary(settings.getCurrentProject());
        electronApp.updateTitle();
        return this.success("OK", { "name": project.name, "summary": summary });
    }

    cycleFullscreen()
    {
        electronApp.cycleFullscreen();
    }

    collectAssets()
    {
        const currentProject = settings.getCurrentProject();
        const assetPorts = projectsUtil.getProjectAssetPorts(currentProject, true);

        const oldNew = {};
        let projectAssetPath = cables.getAssetPath();
        projectAssetPath = path.join(projectAssetPath, "assets");
        if (!fs.existsSync(projectAssetPath)) mkdirp.sync(projectAssetPath);
        assetPorts.forEach((assetPort) =>
        {
            const portValue = assetPort.value;
            let oldFile = helper.fileURLToPath(portValue, true);
            if (!helper.isLocalAssetPath(oldFile) && !oldNew.hasOwnProperty(portValue) && fs.existsSync(oldFile))
            {
                const baseName = path.basename(oldFile);
                const newName = this._findNewAssetFilename(projectAssetPath, baseName);
                const newLocation = path.join(projectAssetPath, newName);
                fs.copyFileSync(oldFile, newLocation);
                // cant use path.join here since we need to keep the ./
                oldNew[assetPort.value] = projectsUtil.getAssetPathUrl(currentProject) + newName;
            }
        });
        return this.success("OK", oldNew);
    }

    collectOps()
    {
        const currentProject = settings.getCurrentProject();
        const movedOps = {};
        const allOpNames = [];
        if (currentProject && currentProject.ops)
        {
            currentProject.ops.forEach((op) =>
            {
                const opName = opsUtil.getOpNameById(op.opId);
                allOpNames.push(opName);
                if (!movedOps.hasOwnProperty(opName))
                {
                    const opPath = opsUtil.getOpAbsolutePath(opName);
                    if (!opPath.startsWith(cables.getOpsPath()))
                    {
                        const targetPath = opsUtil.getOpTargetDir(opName, true);
                        const newOpLocation = path.join(cables.getProjectOpsPath(true), targetPath);
                        if (opPath !== newOpLocation)
                        {
                            fs.cpSync(opPath, newOpLocation, { "recursive": true });
                            movedOps[opName] = newOpLocation;
                        }
                    }
                }
            });
        }
        filesUtil.registerOpChangeListeners(allOpNames, true);
        return this.success("OK", movedOps);
    }

    loadProject(projectFile, newProject = null, rebuildCache = true)
    {
        let project = newProject;
        if (projectFile)
        {
            project = settings.getProjectFromFile(projectFile);
            if (project)
            {
                settings.setProject(projectFile, project);
                if (rebuildCache) projectsUtil.invalidateProjectCaches();
                // add ops in project dirs to lookup
                projectsUtil.getOpDocsInProjectDirs(project, true);
                filesUtil.registerAssetChangeListeners(project, true);
                if (project.ops)
                {
                    const opNames = [];
                    project.ops.forEach((op) =>
                    {
                        const opName = opsUtil.getOpNameById(op.opId);
                        if (opName)
                        {
                            opNames.push(opName);
                        }
                    });
                    filesUtil.registerOpChangeListeners(opNames);
                }
            }
        }
        else
        {
            settings.setProject(null, null);
            projectsUtil.getOpDocsInProjectDirs(project);
        }
        electronApp.updateTitle();
    }

    async addOpDependency(options)
    {
        if (!options.opName || !options.name || !options.type) return this.error("INVALID_DATA");
        let version = "";
        if (options.type === "npm")
        {
            const parts = options.name.split("@");
            if (options.name.startsWith("@"))
            {
                version = parts[2] || "";
                options.name = "@" + parts[1];
            }
            else
            {
                version = parts[1] || "";
            }
        }
        const opName = options.opName;
        const dep = {
            "name": options.name,
            "type": options.type,
            "src": [options.name],
            "version": version
        };
        const opDocFile = opsUtil.getOpAbsoluteJsonFilename(opName);
        if (fs.existsSync(opDocFile))
        {
            let opDoc = jsonfile.readFileSync(opDocFile);
            if (opDoc)
            {
                const deps = opDoc.dependencies || [];
                if (!deps.some((d) => { return d.name === dep.name && d.name === dep.name; }))
                {
                    deps.push(dep);
                }
                opDoc.dependencies = deps;
                opDoc = doc.cleanOpDocData(opDoc);
                jsonfile.writeFileSync(opDocFile, opDoc, { "encoding": "utf-8", "spaces": 4 });
                doc.updateOpDocs();
                return await this._installOpDependencies(opName);
            }
            else
            {
                return this.error("OP_NOT_FOUND");
            }
        }
        else
        {
            return this.error("OP_NOT_FOUND");
        }
    }

    async removeOpDependency(options)
    {
        if (!options.opName || !options.name || !options.type) return this.error("INVALID_DATA");
        const opName = options.opName;
        const opDocFile = opsUtil.getOpAbsoluteJsonFilename(opName);
        if (fs.existsSync(opDocFile))
        {
            let opDoc = jsonfile.readFileSync(opDocFile);
            if (opDoc)
            {
                const newDeps = [];
                const deps = opDoc.dependencies || [];
                deps.forEach((dep) =>
                {
                    if (!(dep.name === options.name && dep.type === options.type)) newDeps.push(dep);
                });
                opDoc.dependencies = newDeps;
                if (opDoc.dependencies) jsonfile.writeFileSync(opDocFile, opDoc, { "encoding": "utf-8", "spaces": 4 });
                doc.updateOpDocs();
                this._installOpDependencies(opName);
                return this.success("OK");
            }
            else
            {
                return this.error("OP_NOT_FOUND");
            }
        }
        else
        {
            return this.error("OP_NOT_FOUND");
        }
    }

    async createFile(data)
    {
        let file = data.name;
        let pickedFileUrl = await electronApp.saveFileDialog(file);
        if (pickedFileUrl)
        {
            fs.writeFileSync(pickedFileUrl, "");
            return this.success("OK", pickedFileUrl, true);
        }
        else
        {
            return this.success("NO_DIR_CHOSEN", null, true);
        }
    }

    async exportPatch()
    {
        const service = new StandaloneZipExport(utilProvider);

        const exportPromise = promisify(service.doExport.bind(service));

        try
        {
            const result = await exportPromise(null);
            return this.success("OK", result);
        }
        catch (e)
        {
            return this.error("ERROR", e);
        }
    }

    async exportPatchBundle()
    {
        const service = new StandaloneExport(utilProvider);

        const exportPromise = promisify(service.doExport.bind(service));

        try
        {
            const result = await exportPromise(null);
            return this.success("OK", result);
        }
        catch (e)
        {
            return this.error("ERROR", e);
        }
    }

    success(msg, data, raw = false)
    {
        if (raw)
        {
            if (data && typeof data === "object") data.success = true;
            return data;
        }
        else
        {
            return { "success": true, "msg": msg, "data": data };
        }
    }

    error(msg, data = null, level = "warn")
    {
        const error = { "error": true, "msg": msg, "level": level };
        if (data) error.data = data;
        return error;
    }

    _getFullRenameResponse(opDocs, newName, oldName, currentUser, project = null, ignoreVersionGap = false, fromRename = false, targetDir = false)
    {
        let opNamespace = opsUtil.getNamespace(newName, true);
        let availableNamespaces = [];

        if (project)
        {
            const projectOpDocs = projectsUtil.getOpDocsInProjectDirs(project);
            availableNamespaces = projectOpDocs.map((opDoc) => { return opsUtil.getNamespace(opDoc.name, true); });
        }

        availableNamespaces = availableNamespaces.map((availableNamespace) => { return availableNamespace.endsWith(".") ? availableNamespace : availableNamespace + "."; });
        availableNamespaces = helper.uniqueArray(availableNamespaces);
        availableNamespaces = availableNamespaces.sort((a, b) => { return a.localeCompare(b); });

        if (project)
        {
            availableNamespaces.unshift(opsUtil.getPatchOpsNamespaceForProject(project));
        }

        if (opNamespace && !availableNamespaces.includes(opNamespace)) availableNamespaces.unshift(opNamespace);

        availableNamespaces = availableNamespaces.filter((availableNamespace) => { return availableNamespace.startsWith(opsUtil.PREFIX_OPS); });

        let removeOld = newName && !(opsUtil.isExtensionOp(newName) && opsUtil.isCoreOp(newName));
        const result = {
            "namespaces": availableNamespaces,
            "problems": [],
            "consequences": [],
            "action": removeOld ? "Rename" : "Copy"
        };

        if (!newName)
        {
            result.problems.push("No name for new op given.");
            return result;
        }

        if (fromRename) targetDir = opsUtil.getOpSourceDir(oldName);
        const problems = opsUtil.getOpRenameProblems(newName, oldName, currentUser, [], null, null, [], true, targetDir);
        const hints = {};
        const consequences = opsUtil.getOpRenameConsequences(newName, oldName, targetDir);

        let newOpDocs = opDocs;
        if (!opsUtil.isCoreOp(newName)) newOpDocs = doc.getCollectionOpDocs(newName, currentUser);

        const nextOpName = opsUtil.getNextVersionOpName(newName, newOpDocs);
        const nextShort = opsUtil.getOpShortName(nextOpName);
        let nextVersion = null;
        let suggestVersion = false;

        if (problems.target_exists)
        {
            suggestVersion = true;
        }

        if (!ignoreVersionGap)
        {
            const wantedVersion = opsUtil.getVersionFromOpName(newName);
            const currentHighest = opsUtil.getHighestVersionNumber(newName, newOpDocs);

            const versionTolerance = currentHighest ? 1 : 2;
            if ((wantedVersion - versionTolerance) > currentHighest)
            {
                hints.version_gap = "Gap in version numbers!";
                suggestVersion = true;
            }
        }

        if (problems.illegal_ops || problems.illegal_references)
        {
            suggestVersion = false;
        }

        if (!fromRename && oldName)
        {
            const hierarchyProblem = opsUtil.getNamespaceHierarchyProblem(oldName, newName);
            if (hierarchyProblem)
            {
                problems.bad_op_hierarchy = hierarchyProblem;
                suggestVersion = false;
            }
        }

        if (suggestVersion)
        {
            const text = "Try creating a new version <a class='button-small versionSuggestion' data-short-name='" + nextShort + "' data-next-name='" + nextOpName + "'>" + nextOpName + "</a>";
            nextVersion = {
                "fullName": nextOpName,
                "namespace": opsUtil.getNamespace(nextOpName),
                "shortName": nextShort
            };
            if (problems.target_exists)
            {
                problems.version_suggestion = text;
            }
            else
            {
                hints.version_suggestion = text;
            }
        }

        result.problems = Object.values(problems);
        result.hints = Object.values(hints);
        result.consequences = Object.values(consequences);
        if (nextVersion) result.nextVersion = nextVersion;
        return result;
    }

    _findNewAssetFilename(targetDir, fileName)
    {
        let fileInfo = path.parse(fileName);
        let newName = fileName;
        let counter = 1;
        while (fs.existsSync(path.join(targetDir, newName)))
        {
            newName = path.format({ "name": fileInfo.name + "_" + counter, "ext": fileInfo.ext });
            counter++;
        }
        return newName;
    }
}

export default new ElectronApi();