Home Reference Source

cables_dev/cables_electron/src/utils/projects_util.js

import { SharedProjectsUtil, utilProvider } from "cables-shared-api";
import path from "path";
import sanitizeFileName from "sanitize-filename";
import { app } from "electron";
import pako from "pako";
import crypto from "crypto";
import jsonfile from "jsonfile";
import fs from "fs";
import settings from "../electron/electron_settings.js";
import helper from "./helper_util.js";
import cables from "../cables.js";
import filesUtil from "./files_util.js";
import opsUtil from "./ops_util.js";

class ProjectsUtil extends SharedProjectsUtil
{
    constructor(provider)
    {
        super(provider);
        this.CABLES_PROJECT_FILE_EXTENSION = "cables";

        this._dirInfos = null;
        this._projectOpDocs = null;
    }

    getAssetPath(projectId)
    {
        return cables.getAssetPath();
    }

    getAssetPathUrl(projectId)
    {
        return "./assets/";
    }

    getScreenShotPath(pId)
    {
        return path.join(app.getPath("userData"), "screenshots/");
    }

    getScreenShotFileName(proj, ext)
    {
        const screenShotPath = this.getScreenShotPath(proj.id);
        return path.join(screenShotPath, "/", filesUtil.realSanitizeFilename(proj.name) + "." + ext);
    }

    generateNewProject(owner)
    {
        if (!owner) owner = settings.getCurrentUser();
        const now = Date.now();
        const projectId = helper.generateRandomId();
        const shortId = helper.generateShortId(projectId, now);
        const randomize = settings.getUserSetting("randomizePatchName", false);
        const newProjectName = this.getNewProjectName(randomize);

        return {
            "_id": projectId,
            "shortId": shortId,
            "name": newProjectName,
            "description": "",
            "userId": owner._id,
            "cachedUsername": owner.username,
            "created": now,
            "updated": now,
            "visibility": "private",
            "ops": [],
            "settings": {
                "licence": "none"
            },
            "userList": [owner],
            "teams": [],
            "log": []
        };
    }

    getNewProjectName(randomize = false)
    {
        return "untitled";
    }

    getProjectOpDirs(project, includeOsDir = true, reverse = false, addLocalCoreIfPackaged = true)
    {
        let opsDirs = [];

        const projectDir = settings.getCurrentProjectDir();
        if (projectDir)
        {
            const currentDir = path.join(projectDir, "ops/");
            opsDirs.push(currentDir);
        }

        if (project && project.dirs && project.dirs.ops)
        {
            project.dirs.ops.forEach((dir) =>
            {
                if (projectDir && !path.isAbsolute(dir)) dir = path.join(projectDir, dir);
                opsDirs.push(dir);
            });
        }
        if (includeOsDir)
        {
            const osOpsDir = cables.getOsOpsDir();
            if (osOpsDir) opsDirs.push(osOpsDir);
        }
        if (addLocalCoreIfPackaged && !cables.isPackaged())
        {
            opsDirs.push(cables.getExtensionOpsPath());
            opsDirs.push(cables.getCoreOpsPath());
        }
        opsDirs = helper.uniqueArray(opsDirs);
        if (reverse) return opsDirs.reverse();
        return opsDirs;
    }

    isFixedPositionOpDir(dir)
    {
        const projectDir = settings.getCurrentProjectDir();
        if (projectDir) if (dir === path.join(projectDir, "ops/")) return true;
        if (dir === "./ops") return true;
        if (dir === cables.getOsOpsDir()) return true;
        if (cables.isPackaged()) return false;
        if (dir === cables.getExtensionOpsPath()) return true;
        return dir === cables.getCoreOpsPath();
    }

    getProjectFileName(project)
    {
        return sanitizeFileName(project.name).replace(/ /g, "_") + "." + this.CABLES_PROJECT_FILE_EXTENSION;
    }

    writeProjectToFile(projectFile, project = null, patch = null)
    {
        if (!project) project = this.generateNewProject();
        if (!project.ops) project.ops = [];
        if (patch && (patch.data || patch.dataB64))
        {
            try
            {
                let buf = patch.data;
                if (patch.dataB64) buf = Buffer.from(patch.dataB64, "base64");

                const qData = JSON.parse(pako.inflate(buf, { "to": "string" }));
                if (qData.ops) project.ops = qData.ops;
                if (qData.ui) project.ui = qData.ui;
            }
            catch (e)
            {
                this._log.error("patch save error/invalid data", e);
                return;
            }
        }

        // filter imported ops, so we do not save these to the database
        project.ops = project.ops.filter((op) =>
        {
            return !(op.storage && op.storage.blueprint);
        });

        project.name = path.basename(projectFile, "." + this.CABLES_PROJECT_FILE_EXTENSION);
        project.summary = project.summary || {};
        project.summary.title = project.name;

        project.opsHash = crypto
            .createHash("sha1")
            .update(JSON.stringify(project.ops))
            .digest("hex");
        project.buildInfo = settings.getBuildInfo();
        jsonfile.writeFileSync(projectFile, project, { "encoding": "utf-8", "spaces": 4 });
        settings.addToRecentProjects(projectFile, project);
    }

    getUsedAssetFilenames(project, includeLibraryAssets = false)
    {
        const fileNames = [];
        if (!project || !project.ops) return [];
        const assetPorts = this.getProjectAssetPorts(project, includeLibraryAssets);
        let urls = assetPorts.map((assetPort) => { return helper.pathToFileURL(assetPort.value, true); });
        urls.forEach((url) =>
        {
            let fullPath = helper.fileURLToPath(url, true);
            if (fullPath && fs.existsSync(fullPath))
            {
                fileNames.push(fullPath);
            }
        });
        return helper.uniqueArray(fileNames);
    }

    addOpDir(project, opDir, atTop = false)
    {
        if (!project.dirs) project.dirs = {};
        if (!project.dirs.ops) project.dirs.ops = [];
        if (atTop)
        {
            project.dirs.ops.unshift(opDir);
        }
        else
        {
            project.dirs.ops.push(opDir);
        }
        project.dirs.ops = helper.uniqueArray(project.dirs.ops);
        this.invalidateProjectCaches(opDir, atTop);
        return project;
    }

    removeOpDir(project, opDir)
    {
        if (!project.dirs) project.dirs = {};
        if (!project.dirs.ops) project.dirs.ops = [];
        project.dirs.ops = project.dirs.ops.filter((dirName) =>
        {
            return dirName !== opDir;
        });
        project.dirs.ops = helper.uniqueArray(project.dirs.ops);
        this.invalidateProjectCaches(opDir);
        return project;
    }

    getSummary(project)
    {
        if (!project) return {};
        return {
            "allowEdit": true,
            "title": project.name,
            "owner": settings.getCurrentUser(),
            "description": project.description,
            "licence": {
                "name": "No licence chosen"
            }
        };
    }

    getOpDirs(currentProject)
    {
        const dirs = this.getProjectOpDirs(currentProject, true);
        const dirInfos = [];

        dirs.forEach((dir) =>
        {
            const opJsons = helper.getFileNamesRecursive(dir, ".json");
            const opLocations = {};
            opJsons.forEach((jsonLocation) =>
            {
                const jsonName = path.basename(jsonLocation, ".json");
                if (opsUtil.isOpNameValid(jsonName))
                {
                    opLocations[jsonName] = path.dirname(path.join(dir, jsonLocation));
                }
            });
            const opNames = Object.keys(opLocations);

            dirInfos.push({
                "dir": dir,
                "opLocations": opLocations,
                "numOps": opNames.length,
                "fixedPlace": this.isFixedPositionOpDir(dir)
            });
        });
        return dirInfos;
    }

    reorderOpDirs(currentProject, order)
    {
        const currentProjectFile = settings.getCurrentProjectFile();
        const newOrder = [];
        order.forEach((opDir) =>
        {
            if (fs.existsSync(opDir)) newOrder.push(opDir);
        });
        if (!currentProject.dirs) currentProject.dirs = {};
        if (!currentProject.dirs.ops) currentProject.dirs.ops = [];
        currentProject.dirs.ops = newOrder.filter((dir) => { return !this.isFixedPositionOpDir(dir); });
        currentProject.dirs.ops = helper.uniqueArray(currentProject.dirs.ops);
        this.writeProjectToFile(currentProjectFile, currentProject);
        this.invalidateProjectCaches();
        return currentProject;
    }

    getAbsoluteOpDirFromHierarchy(opName)
    {
        const currentProject = settings.getCurrentProject();
        if (!this._dirInfos)
        {
            this._log.debug("rebuilding opdir-cache, changed by:", opName);
            this._dirInfos = this.getOpDirs(currentProject);
        }
        if (!this._dirInfos) return this._opsUtil.getOpSourceNoHierarchy(opName);

        for (let i = 0; i < this._dirInfos.length; i++)
        {
            const dirInfo = this._dirInfos[i];
            const opNames = dirInfo.opLocations ? Object.keys(dirInfo.opLocations) : [];
            if (opNames.includes(opName))
            {
                return dirInfo.opLocations[opName];
            }
        }
        return this._opsUtil.getOpSourceNoHierarchy(opName);
    }

    invalidateProjectCaches()
    {
        this._dirInfos = null;
        this._projectOpDocs = null;
    }

    getOpDocsInProjectDirs(project, rebuildCache = false)
    {
        if (this._projectOpDocs && !rebuildCache) return this._projectOpDocs;

        const opDocs = {};
        const opDirs = this.getProjectOpDirs(project, true, false, false);

        opDirs.forEach((opDir) =>
        {
            if (fs.existsSync(opDir))
            {
                const opJsons = helper.getFilesRecursive(opDir, ".json");
                for (let jsonPath in opJsons)
                {
                    const opName = path.basename(jsonPath, ".json");
                    if (opsUtil.isOpNameValid(opName))
                    {
                        if (opDocs.hasOwnProperty(opName))
                        {
                            if (!opDocs[opName].hasOwnProperty("overrides")) opDocs[opName].overrides = [];
                            opDocs[opName].overrides.push(path.join(opDir, path.dirname(jsonPath)));
                        }
                        else
                        {
                            try
                            {
                                const opDoc = jsonfile.readFileSync(path.join(opDir, jsonPath));
                                opDoc.name = opName;
                                opDocs[opName] = opDoc;
                            }
                            catch (e)
                            {
                                this._log.warn("failed to parse opDoc for", opName, "from", jsonPath);
                            }
                        }
                    }
                }
            }
        });
        this._projectOpDocs = Object.values(opDocs);
        this._docsUtil.addOpsToLookup(this._projectOpDocs);
        return this._projectOpDocs;
    }
}
export default new ProjectsUtil(utilProvider);