1
0
mirror of https://github.com/node-red/node-red.git synced 2023-10-10 13:36:53 +02:00
node-red/packages/node_modules/@node-red/runtime/lib/storage/localfilesystem/projects/index.js
Dave Conway-Jones 5cc791690b
fix flowfile name to flows.json in settings
and warn if not set (as if anyone reads warnings)
Move setting to top of settings.js as it will be edited more often.
Default behaviour will still work
(needs translations)
2021-04-23 14:09:06 +01:00

696 lines
26 KiB
JavaScript

/**
* Copyright JS Foundation and other contributors, http://js.foundation
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
**/
var fs = require('fs-extra');
var fspath = require("path");
var crypto = require('crypto');
var storageSettings = require("../settings");
var util = require("../util");
var gitTools = require("./git");
var sshTools = require("./ssh");
var Projects = require("./Project");
var settings;
var runtime;
var log = require("@node-red/util").log;
const events = require("@node-red/util").events;
var projectsEnabled = false;
var projectLogMessages = [];
var projectsDir;
var activeProject;
var globalGitUser = false;
var usingHostName = false;
function init(_settings, _runtime) {
settings = _settings;
runtime = _runtime;
try {
if (settings.editorTheme.projects.enabled === true) {
projectsEnabled = true;
} else if (settings.editorTheme.projects.enabled === false) {
projectLogMessages.push(log._("storage.localfilesystem.projects.disabled"))
}
} catch(err) {
projectLogMessages.push(log._("storage.localfilesystem.projects.disabledNoFlag"))
projectsEnabled = false;
}
if (settings.flowFile) {
flowsFile = settings.flowFile;
// handle Unix and Windows "C:\" and Windows "\\" for UNC.
if (fspath.isAbsolute(flowsFile)) {
//if (((flowsFile[0] == "\\") && (flowsFile[1] == "\\")) || (flowsFile[0] == "/") || (flowsFile[1] == ":")) {
// Absolute path
flowsFullPath = flowsFile;
} else if (flowsFile.substring(0,2) === "./") {
// Relative to cwd
flowsFullPath = fspath.join(process.cwd(),flowsFile);
} else {
try {
fs.statSync(fspath.join(process.cwd(),flowsFile));
// Found in cwd
flowsFullPath = fspath.join(process.cwd(),flowsFile);
} catch(err) {
// Use userDir
flowsFullPath = fspath.join(settings.userDir,flowsFile);
}
}
} else {
flowsFile = 'flows_'+require('os').hostname()+'.json';
flowsFullPath = fspath.join(settings.userDir,flowsFile);
usingHostName = true;
}
var ffExt = fspath.extname(flowsFullPath);
var ffBase = fspath.basename(flowsFullPath,ffExt);
flowsFileBackup = getBackupFilename(flowsFullPath);
credentialsFile = fspath.join(settings.userDir,ffBase+"_cred"+ffExt);
credentialsFileBackup = getBackupFilename(credentialsFile)
var setupProjectsPromise;
if (projectsEnabled) {
return sshTools.init(settings,runtime).then(function() {
gitTools.init(_settings).then(function(gitConfig) {
if (!gitConfig || /^1\./.test(gitConfig.version)) {
if (!gitConfig) {
projectLogMessages.push(log._("storage.localfilesystem.projects.git-not-found"))
} else {
projectLogMessages.push(log._("storage.localfilesystem.projects.git-version-old",{version:gitConfig.version}))
}
projectsEnabled = false;
try {
// As projects have to be turned on, we know this property
// must exist at this point, so turn it off.
// TODO: when on-by-default, this will need to do more
// work to disable.
settings.editorTheme.projects.enabled = false;
} catch(err) {
}
} else {
// Ensure there's a default workflow mode set
settings.editorTheme.projects.workflow = {
mode: (settings.editorTheme.projects.workflow || {}).mode || "manual"
}
globalGitUser = gitConfig.user;
Projects.init(settings,runtime);
sshTools.init(settings);
projectsDir = fspath.resolve(fspath.join(settings.userDir,"projects"));
if(settings.editorTheme.projects.path) {
projectsDir = settings.editorTheme.projects.path;
}
if (!settings.readOnly) {
return fs.ensureDir(projectsDir)
//TODO: this is accessing settings from storage directly as settings
// has not yet been initialised. That isn't ideal - can this be deferred?
.then(storageSettings.getSettings)
.then(function(globalSettings) {
var saveSettings = false;
if (!globalSettings.projects) {
globalSettings.projects = {
projects: {}
}
saveSettings = true;
} else {
activeProject = globalSettings.projects.activeProject;
}
if (!globalSettings.projects.projects) {
globalSettings.projects.projects = {};
saveSettings = true;
}
if (settings.flowFile) {
// if flowFile is a known project name - use it
if (globalSettings.projects.projects.hasOwnProperty(settings.flowFile)) {
activeProject = settings.flowFile;
globalSettings.projects.activeProject = settings.flowFile;
saveSettings = true;
} else {
// if it resolves to a dir - use it
try {
var stat = fs.statSync(fspath.join(projectsDir,settings.flowFile));
if (stat && stat.isDirectory()) {
activeProject = settings.flowFile;
globalSettings.projects.activeProject = activeProject;
// Now check for a credentialSecret
if (settings.credentialSecret !== undefined) {
globalSettings.projects.projects[settings.flowFile] = {
credentialSecret: settings.credentialSecret
}
saveSettings = true;
}
}
} catch(err) {
// Doesn't exist, handle as a flow file to be created
}
}
}
if (!activeProject) {
projectLogMessages.push(log._("storage.localfilesystem.no-active-project"))
}
if (saveSettings) {
return storageSettings.saveSettings(globalSettings);
}
});
}
}
});
});
}
return Promise.resolve();
}
function listProjects() {
return fs.readdir(projectsDir).then(function(fns) {
var dirs = [];
fns.sort(function(A,B) {
return A.toLowerCase().localeCompare(B.toLowerCase());
}).filter(function(fn) {
var fullPath = fspath.join(projectsDir,fn);
if (fn[0] != ".") {
var stats = fs.lstatSync(fullPath);
if (stats.isDirectory()) {
dirs.push(fn);
}
}
});
return dirs;
});
}
function getUserGitSettings(user) {
var username;
if (!user) {
username = "_";
} else {
username = user.username;
}
var userSettings = settings.getUserSettings(username)||{};
return userSettings.git;
}
function getBackupFilename(filename) {
var ffName = fspath.basename(filename);
var ffDir = fspath.dirname(filename);
return fspath.join(ffDir,"."+ffName+".backup");
}
function loadProject(name) {
let fullPath = fspath.resolve(fspath.join(projectsDir,name));
var projectPath = name;
if (projectPath.indexOf(fspath.sep) === -1) {
projectPath = fullPath;
} else {
// Ensure this project dir is under projectsDir;
let relativePath = fspath.relative(projectsDir,fullPath);
if (/^\.\./.test(relativePath)) {
throw new Error("Invalid project name")
}
}
return Projects.load(projectPath).then(function(project) {
activeProject = project;
flowsFullPath = project.getFlowFile();
flowsFileBackup = project.getFlowFileBackup();
credentialsFile = project.getCredentialsFile();
credentialsFileBackup = project.getCredentialsFileBackup();
return project;
})
}
function getProject(user, name) {
checkActiveProject(name);
return Promise.resolve(activeProject.export());
}
function deleteProject(user, name) {
if (activeProject && activeProject.name === name) {
var e = new Error("NLS: Can't delete the active project");
e.code = "cannot_delete_active_project";
throw e;
}
var projectPath = fspath.join(projectsDir,name);
let relativePath = fspath.relative(projectsDir,projectPath);
if (/^\.\./.test(relativePath)) {
throw new Error("Invalid project name")
}
return Projects.delete(user, projectPath);
}
function checkActiveProject(project) {
if (!activeProject || activeProject.name !== project) {
//TODO: throw better err
throw new Error("Cannot operate on inactive project wanted:"+project+" current:"+(activeProject&&activeProject.name));
}
}
function getFiles(user, project) {
checkActiveProject(project);
return activeProject.getFiles();
}
function stageFile(user, project,file) {
checkActiveProject(project);
return activeProject.stageFile(file);
}
function unstageFile(user, project,file) {
checkActiveProject(project);
return activeProject.unstageFile(file);
}
function commit(user, project,options) {
checkActiveProject(project);
var isMerging = activeProject.isMerging();
return activeProject.commit(user, options).then(function() {
// The project was merging, now it isn't. Lets reload.
if (isMerging && !activeProject.isMerging()) {
return reloadActiveProject("merge-complete");
}
})
}
function getFileDiff(user, project,file,type) {
checkActiveProject(project);
return activeProject.getFileDiff(file,type);
}
function getCommits(user, project,options) {
checkActiveProject(project);
return activeProject.getCommits(options);
}
function getCommit(user, project,sha) {
checkActiveProject(project);
return activeProject.getCommit(sha);
}
function getFile(user, project,filePath,sha) {
checkActiveProject(project);
return activeProject.getFile(filePath,sha);
}
function revertFile(user, project,filePath) {
checkActiveProject(project);
return activeProject.revertFile(filePath).then(function() {
return reloadActiveProject("revert");
})
}
function push(user, project,remoteBranchName,setRemote) {
checkActiveProject(project);
return activeProject.push(user,remoteBranchName,setRemote);
}
function pull(user, project,remoteBranchName,setRemote,allowUnrelatedHistories) {
checkActiveProject(project);
return activeProject.pull(user,remoteBranchName,setRemote,allowUnrelatedHistories).then(function() {
return reloadActiveProject("pull");
});
}
function getStatus(user, project, includeRemote) {
checkActiveProject(project);
return activeProject.status(user, includeRemote);
}
function resolveMerge(user, project,file,resolution) {
checkActiveProject(project);
return activeProject.resolveMerge(file,resolution);
}
function abortMerge(user, project) {
checkActiveProject(project);
return activeProject.abortMerge().then(function() {
return reloadActiveProject("merge-abort")
});
}
function getBranches(user, project,isRemote) {
checkActiveProject(project);
return activeProject.getBranches(user, isRemote);
}
function deleteBranch(user, project, branch, isRemote, force) {
checkActiveProject(project);
return activeProject.deleteBranch(user, branch, isRemote, force);
}
function setBranch(user, project,branchName,isCreate) {
checkActiveProject(project);
return activeProject.setBranch(branchName,isCreate).then(function() {
return reloadActiveProject("change-branch");
});
}
function getBranchStatus(user, project,branchName) {
checkActiveProject(project);
return activeProject.getBranchStatus(branchName);
}
function getRemotes(user, project) {
checkActiveProject(project);
return activeProject.getRemotes(user);
}
function addRemote(user, project, options) {
checkActiveProject(project);
return activeProject.addRemote(user, options.name, options);
}
function removeRemote(user, project, remote) {
checkActiveProject(project);
return activeProject.removeRemote(user, remote);
}
function updateRemote(user, project, remote, body) {
checkActiveProject(project);
return activeProject.updateRemote(user, remote, body);
}
function getActiveProject(user) {
return activeProject;
}
function reloadActiveProject(action) {
return runtime.nodes.stopFlows().then(function() {
return runtime.nodes.loadFlows(true).then(function() {
events.emit("runtime-event",{id:"project-update", payload:{ project: activeProject.name, action:action}});
}).catch(function(err) {
// We're committed to the project change now, so notify editors
// that it has changed.
events.emit("runtime-event",{id:"project-update", payload:{ project: activeProject.name, action:action}});
throw err;
});
});
}
function createProject(user, metadata) {
if (metadata.files && metadata.migrateFiles) {
// We expect there to be no active project in this scenario
if (activeProject) {
throw new Error("Cannot migrate as there is an active project");
}
var currentEncryptionKey = settings.get('credentialSecret');
if (currentEncryptionKey === undefined) {
currentEncryptionKey = settings.get('_credentialSecret');
}
if (!metadata.hasOwnProperty('credentialSecret')) {
metadata.credentialSecret = currentEncryptionKey;
}
if (!metadata.files.flow) {
metadata.files.flow = fspath.basename(flowsFullPath);
}
if (!metadata.files.credentials) {
metadata.files.credentials = fspath.basename(credentialsFile);
}
metadata.files.oldFlow = flowsFullPath;
metadata.files.oldCredentials = credentialsFile;
metadata.files.credentialSecret = currentEncryptionKey;
}
metadata.path = fspath.join(projectsDir,metadata.name);
if (/^\.\./.test(fspath.relative(projectsDir,metadata.path))) {
throw new Error("Invalid project name")
}
return Projects.create(user, metadata).then(function(p) {
return setActiveProject(user, p.name);
}).then(function() {
return getProject(user, metadata.name);
})
}
function setActiveProject(user, projectName) {
return loadProject(projectName).then(function(project) {
var globalProjectSettings = settings.get("projects")||{};
globalProjectSettings.activeProject = project.name;
return settings.set("projects",globalProjectSettings).then(function() {
log.info(log._("storage.localfilesystem.projects.changing-project",{project:(activeProject&&activeProject.name)||"none"}));
log.info(log._("storage.localfilesystem.flows-file",{path:flowsFullPath}));
// console.log("Updated file targets to");
// console.log(flowsFullPath)
// console.log(credentialsFile)
return reloadActiveProject("loaded");
})
});
}
function initialiseProject(user, project, data) {
if (!activeProject || activeProject.name !== project) {
// TODO standardise
throw new Error("Cannot initialise inactive project");
}
return activeProject.initialise(user,data).then(function(result) {
flowsFullPath = activeProject.getFlowFile();
flowsFileBackup = activeProject.getFlowFileBackup();
credentialsFile = activeProject.getCredentialsFile();
credentialsFileBackup = activeProject.getCredentialsFileBackup();
runtime.nodes.setCredentialSecret(activeProject.credentialSecret);
return reloadActiveProject("updated");
});
}
function updateProject(user, project, data) {
if (!activeProject || activeProject.name !== project) {
// TODO standardise
throw new Error("Cannot update inactive project");
}
// In case this triggers a credential secret change
var isReset = data.resetCredentialSecret;
var wasInvalid = activeProject.credentialSecretInvalid;
return activeProject.update(user,data).then(function(result) {
if (result.flowFilesChanged) {
flowsFullPath = activeProject.getFlowFile();
flowsFileBackup = activeProject.getFlowFileBackup();
credentialsFile = activeProject.getCredentialsFile();
credentialsFileBackup = activeProject.getCredentialsFileBackup();
return reloadActiveProject("updated");
} else if (result.credentialSecretChanged) {
if (isReset || !wasInvalid) {
if (isReset) {
runtime.nodes.clearCredentials();
}
runtime.nodes.setCredentialSecret(activeProject.credentialSecret);
return runtime.nodes.exportCredentials()
.then(runtime.storage.saveCredentials)
.then(function() {
if (wasInvalid) {
return reloadActiveProject("updated");
}
});
} else if (wasInvalid) {
return reloadActiveProject("updated");
}
}
});
}
function setCredentialSecret(data) { //existingSecret,secret) {
var isReset = data.resetCredentialSecret;
var wasInvalid = activeProject.credentialSecretInvalid;
return activeProject.update(data).then(function() {
if (isReset || !wasInvalid) {
if (isReset) {
runtime.nodes.clearCredentials();
}
runtime.nodes.setCredentialSecret(activeProject.credentialSecret);
return runtime.nodes.exportCredentials()
.then(runtime.storage.saveCredentials)
.then(function() {
if (wasInvalid) {
return reloadActiveProject("updated");
}
});
} else if (wasInvalid) {
return reloadActiveProject("updated");
}
})
}
var initialFlowLoadComplete = false;
var flowsFile;
var flowsFullPath;
var flowsFileExists = false;
var flowsFileBackup;
var credentialsFile;
var credentialsFileBackup;
async function getFlows() {
if (!initialFlowLoadComplete) {
initialFlowLoadComplete = true;
log.info(log._("storage.localfilesystem.user-dir",{path:settings.userDir}));
if (projectsEnabled) {
log.info(log._("storage.localfilesystem.projects.projects-directory", {projectsDirectory: projectsDir}));
}
if (activeProject) {
// At this point activeProject will be a string, so go load it and
// swap in an instance of Project
return loadProject(activeProject).then(function() {
log.info(log._("storage.localfilesystem.projects.active-project",{project:activeProject.name||"none"}));
log.info(log._("storage.localfilesystem.flows-file",{path:flowsFullPath}));
return getFlows();
});
} else {
if (projectsEnabled) {
log.warn(log._("storage.localfilesystem.projects.no-active-project"))
} else {
projectLogMessages.forEach(log.warn);
}
if (usingHostName) { log.warn(log._("storage.localfilesystem.warn_name")) };
log.info(log._("storage.localfilesystem.flows-file",{path:flowsFullPath}));
}
}
if (activeProject) {
var error;
if (activeProject.isEmpty()) {
log.warn("Project repository is empty");
error = new Error("Project repository is empty");
error.code = "project_empty";
throw error;
}
if (activeProject.missingFiles && activeProject.missingFiles.indexOf('package.json') !== -1) {
log.warn("Project missing package.json");
error = new Error("Project missing package.json");
error.code = "missing_package_file";
throw error;
}
if (!activeProject.getFlowFile()) {
log.warn("Project has no flow file");
error = new Error("Project has no flow file");
error.code = "missing_flow_file";
throw error;
}
if (activeProject.isMerging()) {
log.warn("Project has unmerged changes");
error = new Error("Project has unmerged changes. Cannot load flows");
error.code = "git_merge_conflict";
throw error;
}
}
return util.readFile(flowsFullPath,flowsFileBackup,null,'flow').then(function(result) {
if (result === null) {
flowsFileExists = false;
return [];
}
flowsFileExists = true;
return result;
});
}
async function saveFlows(flows, user) {
if (settings.readOnly) {
return
}
if (activeProject && activeProject.isMerging()) {
var error = new Error("Project has unmerged changes. Cannot deploy new flows");
error.code = "git_merge_conflict";
throw error;
}
flowsFileExists = true;
var flowData;
if (settings.flowFilePretty || (activeProject && settings.flowFilePretty !== false) ) {
// Pretty format if option enabled, or using Projects and not explicitly disabled
flowData = JSON.stringify(flows,null,4);
} else {
flowData = JSON.stringify(flows);
}
return util.writeFile(flowsFullPath, flowData, flowsFileBackup).then(() => {
var gitSettings = getUserGitSettings(user) || {};
if (activeProject) {
var workflowMode = (gitSettings.workflow||{}).mode || settings.editorTheme.projects.workflow.mode;
if (workflowMode === 'auto') {
return activeProject.stageFile([flowsFullPath, credentialsFile]).then(() => {
return activeProject.commit(user,{message:"Update flow files"})
})
}
}
});
}
function getCredentials() {
return util.readFile(credentialsFile,credentialsFileBackup,{},'credentials');
}
async function saveCredentials(credentials) {
if (settings.readOnly) {
return;
}
var credentialData;
if (settings.flowFilePretty || (activeProject && settings.flowFilePretty !== false) ) {
// Pretty format if option enabled, or using Projects and not explicitly disabled
credentialData = JSON.stringify(credentials,null,4);
} else {
credentialData = JSON.stringify(credentials);
}
return util.writeFile(credentialsFile, credentialData, credentialsFileBackup);
}
function getFlowFilename() {
if (flowsFullPath) {
return fspath.basename(flowsFullPath);
}
}
function getCredentialsFilename() {
if (flowsFullPath) {
return fspath.basename(credentialsFile);
}
}
module.exports = {
init: init,
listProjects: listProjects,
getActiveProject: getActiveProject,
setActiveProject: setActiveProject,
getProject: getProject,
deleteProject: deleteProject,
createProject: createProject,
initialiseProject: initialiseProject,
updateProject: updateProject,
getFiles: getFiles,
getFile: getFile,
revertFile: revertFile,
stageFile: stageFile,
unstageFile: unstageFile,
commit: commit,
getFileDiff: getFileDiff,
getCommits: getCommits,
getCommit: getCommit,
push: push,
pull: pull,
getStatus:getStatus,
resolveMerge: resolveMerge,
abortMerge: abortMerge,
getBranches: getBranches,
deleteBranch: deleteBranch,
setBranch: setBranch,
getBranchStatus:getBranchStatus,
getRemotes: getRemotes,
addRemote: addRemote,
removeRemote: removeRemote,
updateRemote: updateRemote,
getFlowFilename: getFlowFilename,
flowFileExists: function() { return flowsFileExists },
getCredentialsFilename: getCredentialsFilename,
getGlobalGitUser: function() { return globalGitUser },
getFlows: getFlows,
saveFlows: saveFlows,
getCredentials: getCredentials,
saveCredentials: saveCredentials,
ssh: sshTools
};