This commit is contained in:
gongwenxin
2025-08-28 16:20:24 +08:00
parent 61e533cddf
commit 73e887ebf4
1539 changed files with 360926 additions and 0 deletions
@@ -0,0 +1,232 @@
/**
* 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 Path = require('path');
var crypto = require('crypto');
var log = require("@node-red/util").log;
var runtime;
var storageModule;
var settingsAvailable;
var sessionsAvailable;
var Mutex = require('async-mutex').Mutex;
const settingsSaveMutex = new Mutex();
var libraryFlowsCachedResult = null;
function moduleSelector(aSettings) {
var toReturn;
if (aSettings.storageModule) {
if (typeof aSettings.storageModule === "string") {
// TODO: allow storage modules to be specified by absolute path
toReturn = require("./"+aSettings.storageModule);
} else {
toReturn = aSettings.storageModule;
}
} else {
toReturn = require("./localfilesystem");
}
return toReturn;
}
function is_malicious(path) {
return path.indexOf('../') != -1 || path.indexOf('..\\') != -1;
}
var storageModuleInterface = {
init: async function(_runtime) {
runtime = _runtime;
// Any errors thrown by the module will get passed up to the called
// as a rejected promise
storageModule = moduleSelector(runtime.settings);
settingsAvailable = storageModule.hasOwnProperty("getSettings") && storageModule.hasOwnProperty("saveSettings");
sessionsAvailable = storageModule.hasOwnProperty("getSessions") && storageModule.hasOwnProperty("saveSessions");
if (!!storageModule.projects) {
var projectsEnabled = false;
if (runtime.settings.hasOwnProperty("editorTheme") && runtime.settings.editorTheme.hasOwnProperty("projects")) {
projectsEnabled = runtime.settings.editorTheme.projects.enabled === true;
}
if (projectsEnabled) {
storageModuleInterface.projects = storageModule.projects;
}
}
if (storageModule.sshkeys) {
storageModuleInterface.sshkeys = storageModule.sshkeys;
}
return storageModule.init(runtime.settings,runtime);
},
getFlows: async function() {
return storageModule.getFlows().then(function(flows) {
return storageModule.getCredentials().then(function(creds) {
var result = {
flows: flows,
credentials: creds
};
result.rev = crypto.createHash('sha256').update(JSON.stringify(result.flows)).digest("hex");
return result;
})
});
},
saveFlows: async function(config, user) {
var flows = config.flows;
var credentials = config.credentials;
var credentialSavePromise;
if (config.credentialsDirty) {
credentialSavePromise = storageModule.saveCredentials(credentials);
} else {
credentialSavePromise = Promise.resolve();
}
delete config.credentialsDirty;
return credentialSavePromise.then(function() {
return storageModule.saveFlows(flows, user).then(function() {
return crypto.createHash('sha256').update(JSON.stringify(config.flows)).digest("hex");
})
});
},
// getCredentials: function() {
// return storageModule.getCredentials();
// },
saveCredentials: async function(credentials) {
return storageModule.saveCredentials(credentials);
},
getSettings: async function() {
if (settingsAvailable) {
return storageModule.getSettings();
} else {
return null
}
},
saveSettings: async function(settings) {
if (settingsAvailable) {
return settingsSaveMutex.runExclusive(() => storageModule.saveSettings(settings))
}
},
getSessions: async function() {
if (sessionsAvailable) {
return storageModule.getSessions();
} else {
return null
}
},
saveSessions: async function(sessions) {
if (sessionsAvailable) {
return storageModule.saveSessions(sessions);
}
},
/* Library Functions */
getLibraryEntry: async function(type, path) {
if (is_malicious(path)) {
var err = new Error();
err.code = "forbidden";
throw err;
}
return storageModule.getLibraryEntry(type, path);
},
saveLibraryEntry: async function(type, path, meta, body) {
if (is_malicious(path)) {
var err = new Error();
err.code = "forbidden";
throw err;
}
return storageModule.saveLibraryEntry(type, path, meta, body);
},
/* Deprecated functions */
getAllFlows: async function() {
if (storageModule.hasOwnProperty("getAllFlows")) {
return storageModule.getAllFlows();
} else {
if (libraryFlowsCachedResult) {
return libraryFlowsCachedResult;
} else {
return listFlows("/").then(function(result) {
libraryFlowsCachedResult = result;
return result;
});
}
}
},
getFlow: function(fn) {
if (is_malicious(fn)) {
var err = new Error();
err.code = "forbidden";
throw err;
}
if (storageModule.hasOwnProperty("getFlow")) {
return storageModule.getFlow(fn);
} else {
return storageModule.getLibraryEntry("flows",fn);
}
},
saveFlow: function(fn, data) {
if (is_malicious(fn)) {
var err = new Error();
err.code = "forbidden";
throw err;
}
libraryFlowsCachedResult = null;
if (storageModule.hasOwnProperty("saveFlow")) {
return storageModule.saveFlow(fn, data);
} else {
return storageModule.saveLibraryEntry("flows",fn,{},data);
}
}
/* End deprecated functions */
}
function listFlows(path) {
return storageModule.getLibraryEntry("flows",path).then(function(res) {
const promises = [];
res.forEach(function(r) {
if (typeof r === "string") {
promises.push(listFlows(Path.join(path,r)));
} else {
promises.push(Promise.resolve(r));
}
});
return Promise.all(promises).then(res2 => {
let i = 0;
const result = {};
res2.forEach(function(r) {
// TODO: name||fn
if (r.fn) {
var name = r.name;
if (!name) {
name = r.fn.replace(/\.json$/, "");
}
result.f = result.f || [];
result.f.push(name);
} else {
result.d = result.d || {};
result.d[res[i]] = r;
//console.log(">",r.value);
}
i++;
});
return result;
});
});
}
module.exports = storageModuleInterface;
@@ -0,0 +1,89 @@
/**
* 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 log = require("@node-red/util").log; // TODO: separate module
var util = require("./util");
var library = require("./library");
var sessions = require("./sessions");
var runtimeSettings = require("./settings");
var projects = require("./projects");
var initialFlowLoadComplete = false;
var settings;
function checkForConfigFile(dir) {
return fs.existsSync(fspath.join(dir,".config.json")) ||
fs.existsSync(fspath.join(dir,".config.nodes.json"))
}
var localfilesystem = {
init: async function(_settings, runtime) {
settings = _settings;
if (!settings.userDir) {
if (checkForConfigFile(process.env.NODE_RED_HOME)) {
settings.userDir = process.env.NODE_RED_HOME
} else if (process.env.HOMEPATH && checkForConfigFile(fspath.join(process.env.HOMEPATH,".node-red"))) {
settings.userDir = fspath.join(process.env.HOMEPATH,".node-red");
} else {
settings.userDir = fspath.join(process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH || process.env.NODE_RED_HOME,".node-red");
}
}
if (!settings.readOnly) {
await fs.ensureDir(fspath.join(settings.userDir,"node_modules"));
}
sessions.init(settings);
await runtimeSettings.init(settings);
await library.init(settings);
await projects.init(settings, runtime);
var packageFile = fspath.join(settings.userDir,"package.json");
if (!settings.readOnly) {
try {
fs.statSync(packageFile);
} catch(err) {
var defaultPackage = {
"name": "node-red-project",
"description": "A Node-RED Project",
"version": "0.0.1",
"private": true
};
return util.writeFile(packageFile,JSON.stringify(defaultPackage,"",4));
}
}
},
getFlows: projects.getFlows,
saveFlows: projects.saveFlows,
getCredentials: projects.getCredentials,
saveCredentials: projects.saveCredentials,
getSettings: runtimeSettings.getSettings,
saveSettings: runtimeSettings.saveSettings,
getSessions: sessions.getSessions,
saveSessions: sessions.saveSessions,
getLibraryEntry: library.getLibraryEntry,
saveLibraryEntry: library.saveLibraryEntry,
projects: projects
};
module.exports = localfilesystem;
@@ -0,0 +1,175 @@
/**
* 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 util = require("./util");
var settings;
var libDir;
var libFlowsDir;
function toSingleLine(text) {
var result = text.replace(/\\/g, "\\\\").replace(/\n/g, "\\n");
return result;
}
function fromSingleLine(text) {
var result = text.replace(/\\[\\n]/g, function(s) {
return ((s === "\\\\") ? "\\" : "\n");
});
return result;
}
function getFileMeta(root, path) {
var fn = fspath.join(root, path);
var fd = fs.openSync(fn, 'r');
var size = fs.fstatSync(fd).size;
var meta = {};
var read = 0;
var length = 10;
var remaining = Buffer.alloc(0);
var buffer = Buffer.alloc(length);
while (read < size) {
read += fs.readSync(fd, buffer, 0, length);
var data = Buffer.concat([remaining, buffer]);
var index = data.lastIndexOf(0x0a);
if (index !== -1) {
var parts = data.slice(0, index).toString().split('\n');
for (var i = 0; i < parts.length; i++) {
var match = /^\/\/ (\w+): (.*)/.exec(parts[i]);
if (match) {
meta[match[1]] = fromSingleLine(match[2]);
} else {
read = size;
break;
}
}
remaining = data.slice(index + 1);
} else {
remaining = data;
}
}
fs.closeSync(fd);
return meta;
}
function getFileBody(root, path) {
var body = '';
var fn = fspath.join(root, path);
var data = fs.readFileSync(fn, 'utf8');
var parts = data.split('\n');
var scanning = true;
for (var i = 0; i < parts.length; i++) {
if (! /^\/\/ \w+: /.test(parts[i]) || !scanning) {
body += (body.length > 0 ? '\n' : '') + parts[i];
scanning = false;
}
}
return body;
}
function getLibraryEntry(type,path) {
var root = fspath.join(libDir,type);
var rootPath = fspath.join(libDir,type,path);
// don't create the folder if it does not exist - we are only reading....
return fs.lstat(rootPath).then(function(stats) {
if (stats.isFile()) {
return getFileBody(root,path);
}
if (path.substr(-1) == '/') {
path = path.substr(0,path.length-1);
}
return fs.readdir(rootPath).then(function(fns) {
var dirs = [];
var files = [];
fns.sort().filter(function(fn) {
var fullPath = fspath.join(path,fn);
// we use fs.realpathSync to also resolve Symbolic Link
var absoluteFullPath = fs.realpathSync(fspath.join(root,fullPath));
if (fn[0] != ".") {
var stats = fs.lstatSync(absoluteFullPath);
if (stats.isDirectory()) {
dirs.push(fn);
} else {
var meta = getFileMeta(root,fullPath);
meta.fn = fn;
files.push(meta);
}
}
});
return dirs.concat(files);
});
}).catch(function(err) {
// if path is empty, then assume it was a folder, return empty
if (path === ""){
return [];
}
// if path ends with slash, it was a folder
// so return empty
if (path.substr(-1) == '/') {
return [];
}
// else path was specified, but did not exist,
// check for path.json as an alternative if flows
if (type === "flows" && !/\.json$/.test(path)) {
return getLibraryEntry(type,path+".json")
.catch(function(e) {
throw err;
});
} else {
throw new Error(`Library Entry not found ${path}`, { cause: err});
}
});
}
module.exports = {
init: async function(_settings) {
settings = _settings;
libDir = fspath.join(settings.userDir,"lib");
libFlowsDir = fspath.join(libDir,"flows");
if (!settings.readOnly) {
return fs.ensureDir(libFlowsDir);
}
},
getLibraryEntry: getLibraryEntry,
saveLibraryEntry: async function(type,path,meta,body) {
if (settings.readOnly) {
return;
}
if (type === "flows" && !path.endsWith(".json")) {
path += ".json";
}
var fn = fspath.join(libDir, type, path);
var headers = "";
for (var i in meta) {
if (meta.hasOwnProperty(i)) {
headers += "// "+i+": "+toSingleLine(meta[i])+"\n";
}
}
if (type === "flows" && settings.flowFilePretty) {
body = JSON.stringify(JSON.parse(body),null,4);
}
return fs.ensureDir(fspath.dirname(fn)).then(function () {
util.writeFile(fn,headers+body);
});
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,48 @@
/**
* 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 i18n = require("@node-red/util").i18n;
module.exports = {
"package.json": function(project) {
var packageDetails = {
"name": project.name,
"description": project.summary||i18n._("storage.localfilesystem.projects.summary"),
"version": "0.0.1",
"dependencies": {},
"node-red": {
"settings": {
}
}
};
if (project.files) {
if (project.files.flow) {
packageDetails['node-red'].settings.flowFile = project.files.flow;
packageDetails['node-red'].settings.credentialsFile = project.files.credentials;
}
}
return JSON.stringify(packageDetails,"",4);
},
"README.md": function(project) {
var content = project.name+"\n"+("=".repeat(project.name.length))+"\n\n";
if (project.summary) {
content += project.summary+"\n\n";
}
content += i18n._("storage.localfilesystem.projects.readme");
return content;
},
".gitignore": function() { return "*.backup" ;}
}
@@ -0,0 +1,46 @@
/**
* 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 authCache = {}
module.exports = {
init: function() {
authCache = {};
},
clear: function(project,remote, user) {
if (user && remote && authCache[project] && authCache[project][remote]) {
delete authCache[project][remote][user];
} else if (remote && authCache.hasOwnProperty(project)) {
delete authCache[project][remote];
} else {
delete authCache[project];
}
},
set: function(project,remote,user,auth) {
// console.log("AuthCache.set",remote,user,auth);
authCache[project] = authCache[project]||{};
authCache[project][remote] = authCache[project][remote]||{};
authCache[project][remote][user] = auth;
// console.log(JSON.stringify(authCache,'',4));
},
get: function(project,remote,user) {
// console.log("AuthCache.get",remote,user,authCache[project]&&authCache[project][remote]&&authCache[project][remote][user]);
if (authCache[project] && authCache[project][remote]) {
return authCache[project][remote][user];
}
return
}
}
@@ -0,0 +1,133 @@
/**
* 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 net = require("net");
var fs = require("fs-extra");
var path = require("path");
var os = require("os");
const crypto = require("crypto");
function getListenPath() {
var seed = crypto.randomBytes(8).toString('hex');
var fn = 'node-red-git-askpass-'+seed+'-sock';
var listenPath;
if (process.platform === 'win32') {
listenPath = '\\\\.\\pipe\\'+fn;
} else {
listenPath = path.join(process.env['XDG_RUNTIME_DIR'] || os.tmpdir(), fn);
}
// console.log(listenPath);
return listenPath;
}
var ResponseServer = function(auth) {
return new Promise(function(resolve, reject) {
var server = net.createServer(function(connection) {
connection.setEncoding('utf8');
var parts = [];
connection.on('data', function(data) {
var m = data.indexOf("\n");
if (m !== -1) {
parts.push(data.substring(0, m));
data = data.substring(m);
var line = parts.join("");
// console.log("LINE:",line);
parts = [];
if (line==='Username') {
connection.end(auth.username);
} else if (line === 'Password') {
connection.end(auth.password);
server.close();
} else {
}
}
if (data.length > 0) {
parts.push(data);
}
});
});
var listenPath = getListenPath();
server.listen(listenPath, function(ready) {
resolve({path:listenPath,close:function() { server.close(); }});
});
server.on('close', function() {
// console.log("Closing response server");
fs.removeSync(listenPath);
});
server.on('error',function(err) {
console.log("ResponseServer unexpectedError:",err.toString());
server.close();
reject(err);
})
});
}
var ResponseSSHServer = function(auth) {
return new Promise(function(resolve, reject) {
var server = net.createServer(function(connection) {
connection.setEncoding('utf8');
var parts = [];
connection.on('data', function(data) {
var m = data.indexOf("\n");
if (m !== -1) {
parts.push(data.substring(0, m));
data = data.substring(m);
var line = parts.join("");
parts = [];
if (line==='The') {
// TODO: document these exchanges!
connection.end('yes');
// server.close();
} else if (line === 'Enter') {
connection.end(auth.passphrase);
// server.close();
} else {
}
}
if (data.length > 0) {
parts.push(data);
}
});
});
var listenPath = getListenPath();
server.listen(listenPath, function(ready) {
resolve({path:listenPath,close:function() { server.close(); }});
});
server.on('close', function() {
// console.log("Closing response server");
fs.removeSync(listenPath);
});
server.on('error',function(err) {
console.log("ResponseServer unexpectedError:",err.toString());
server.close();
reject(err);
})
});
}
module.exports = {
ResponseServer: ResponseServer,
ResponseSSHServer: ResponseSSHServer
}
@@ -0,0 +1,24 @@
/**
* 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 net = require("net");
var socket = net.connect(process.argv[2], function() {
socket.on('data', function(data) { console.log(data);});
socket.on('end', function() {
});
socket.write((process.argv[3]||"")+"\n", 'utf8');
});
socket.setEncoding('utf8');
@@ -0,0 +1,662 @@
/**
* 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 authResponseServer = require('./authServer').ResponseServer;
var sshResponseServer = require('./authServer').ResponseSSHServer;
var clone = require('clone');
var path = require("path");
var gitCommand = "git";
var gitVersion;
const {log,exec} = require("@node-red/util");
function runGitCommand(args,cwd,env,emit) {
log.trace(gitCommand + JSON.stringify(args));
args.unshift("credential.helper=")
args.unshift("-c");
return exec.run(gitCommand, args, {cwd:cwd, detached:true, env:env}, emit).then(result => {
return result.stdout;
}).catch(result => {
var stdout = result.stdout;
var stderr = result.stderr;
var err = new Error(stderr);
err.stdout = stdout;
err.stderr = stderr;
if (/Connection refused/i.test(stderr)) {
err.code = "git_connection_failed";
} else if (/Connection timed out/i.test(stderr)) {
err.code = "git_connection_failed";
} else if(/Host key verification failed/i.test(stderr)) {
// TODO: handle host key verification errors separately
err.code = "git_host_key_verification_failed";
} else if (/fatal: could not read/i.test(stderr)) {
// Username/Password
err.code = "git_auth_failed";
} else if(/HTTP Basic: Access denied/i.test(stderr)) {
err.code = "git_auth_failed";
} else if(/Permission denied \(publickey\)/i.test(stderr)) {
err.code = "git_auth_failed";
} else if(/Authentication failed/i.test(stderr)) {
err.code = "git_auth_failed";
} else if (/The requested URL returned error: 403/i.test(stderr)) {
err.code = "git_auth_failed";
} else if (/commit your changes or stash/i.test(stderr)) {
err.code = "git_local_overwrite";
} else if (/CONFLICT/.test(err.stdout)) {
err.code = "git_pull_merge_conflict";
} else if (/not fully merged/i.test(stderr)) {
err.code = "git_delete_branch_unmerged";
} else if (/remote .* already exists/i.test(stderr)) {
err.code = "git_remote_already_exists";
} else if (/does not appear to be a git repository/i.test(stderr)) {
err.code = "git_not_a_repository";
} else if (/Repository not found/i.test(stderr)) {
err.code = "git_repository_not_found";
} else if (/repository '.*' does not exist/i.test(stderr)) {
err.code = "git_repository_not_found";
} else if (/refusing to merge unrelated histories/i.test(stderr)) {
err.code = "git_pull_unrelated_history"
} else if (/Please tell me who you are/i.test(stderr)) {
err.code = "git_missing_user";
} else if (/name consists only of disallowed characters/i.test(stderr)) {
err.code = "git_missing_user";
} else if (/nothing (add )?to commit/i.test(stdout)) {
return stdout;
}
throw err;
})
}
function runGitCommandWithAuth(args,cwd,auth,emit) {
log.trace("runGitCommandWithAuth "+JSON.stringify(auth).replace(/("pass.*?"\s*:\s*").+?"/g,'$1[hidden]"'));
return authResponseServer(auth).then(function(rs) {
var commandEnv = clone(process.env);
commandEnv.GIT_ASKPASS = path.join(__dirname,"node-red-ask-pass.sh");
commandEnv.NODE_RED_GIT_NODE_PATH = process.execPath;
commandEnv.NODE_RED_GIT_SOCK_PATH = rs.path;
commandEnv.NODE_RED_GIT_ASKPASS_PATH = path.join(__dirname,"authWriter.js");
return runGitCommand(args,cwd,commandEnv,emit).then( result => {
rs.close();
return result;
}).catch(err => {
rs.close();
throw err;
});
})
}
function runGitCommandWithSSHCommand(args,cwd,auth,emit) {
log.trace("runGitCommandWithSSHCommand "+JSON.stringify(auth).replace(/("pass.*?"\s*:\s*").+?"/g,'$1[hidden]"'));
return sshResponseServer(auth).then(function(rs) {
var commandEnv = clone(process.env);
commandEnv.SSH_ASKPASS = path.join(__dirname,"node-red-ask-pass.sh");
commandEnv.DISPLAY = "dummy:0";
commandEnv.NODE_RED_GIT_NODE_PATH = process.execPath;
commandEnv.NODE_RED_GIT_SOCK_PATH = rs.path;
commandEnv.NODE_RED_GIT_ASKPASS_PATH = path.join(__dirname,"authWriter.js");
// For git < 2.3.0
commandEnv.GIT_SSH = path.join(__dirname,"node-red-ssh.sh");
commandEnv.NODE_RED_KEY_FILE=auth.key_path;
// GIT_SSH_COMMAND - added in git 2.3.0
commandEnv.GIT_SSH_COMMAND = "ssh -i \"" + auth.key_path + "\" -F /dev/null";
// console.log('commandEnv:', commandEnv);
return runGitCommand(args,cwd,commandEnv,emit).then( result => {
rs.close();
return result;
}).catch(err => {
rs.close();
throw err;
});
})
}
function cleanFilename(name) {
if (name[0] !== '"') {
return name;
}
return name.substring(1,name.length-1);
}
function parseFilenames(name) {
var re = /([^ "]+|(".*?"))($| -> ([^ ]+|(".*"))$)/;
var m = re.exec(name);
var result = [];
if (m) {
result.push(cleanFilename(m[1]));
if (m[4]) {
result.push(cleanFilename(m[4]));
}
}
return result;
}
// function getBranchInfo(localRepo) {
// return runGitCommand(["status","--porcelain","-b"],localRepo).then(function(output) {
// var lines = output.split("\n");
// var unknownDirs = [];
// var branchLineRE = /^## (No commits yet on )?(.+?)($|\.\.\.(.+?)($| \[(ahead (\d+))?.*?(behind (\d+))?\]))/m;
// console.log(output);
// console.log(lines);
// var m = branchLineRE.exec(output);
// console.log(m);
// var result = {}; //commits:{}};
// if (m) {
// if (m[1]) {
// result.empty = true;
// }
// result.local = m[2];
// if (m[4]) {
// result.remote = m[4];
// }
// }
// return result;
// });
// }
function getStatus(localRepo) {
// parseFilename('"test with space"');
// parseFilename('"test with space" -> knownFile.txt');
// parseFilename('"test with space" -> "un -> knownFile.txt"');
var result = {
files: {},
commits: {},
branches: {}
}
return runGitCommand(['rev-list', 'HEAD', '--count'],localRepo).then(function(count) {
result.commits.total = parseInt(count);
}).catch(function(err) {
if (/ambiguous argument/i.test(err.message)) {
result.commits.total = 0;
} else {
throw err;
}
}).then(function() {
return runGitCommand(["ls-files","--cached","--others","--exclude-standard"],localRepo).then(function(output) {
var lines = output.split("\n");
lines.forEach(function(l) {
if (l==="") {
return;
}
var fullName = cleanFilename(l);
// parseFilename(l);
var parts = fullName.split("/");
var p = result.files;
var name;
for (var i = 0;i<parts.length-1;i++) {
var name = parts.slice(0,i+1).join("/")+"/";
if (!p.hasOwnProperty(name)) {
p[name] = {
type:"d"
}
}
}
result.files[fullName] = {
type: /\/$/.test(fullName)?"d":"f"
}
})
return runGitCommand(["status","--porcelain","-b"],localRepo).then(function(output) {
var lines = output.split("\n");
var unknownDirs = [];
var branchLineRE = /^## (?:(?:No commits yet on )|(?:Initial commit on))?(.+?)(?:$|\.\.\.(.+?)(?:$| \[(?:(?:ahead (\d+)(?:,\s*)?)?(?:behind (\d+))?|(gone))\]))/;
lines.forEach(function(line) {
if (line==="") {
return;
}
if (line[0] === "#") {
var m = branchLineRE.exec(line);
if (m) {
result.branches.local = m[1];
if (m[2]) {
result.branches.remote = m[2];
result.commits.ahead = 0;
result.commits.behind = 0;
}
if (m[3] !== undefined) {
result.commits.ahead = parseInt(m[3]);
}
if (m[4] !== undefined) {
result.commits.behind = parseInt(m[4]);
}
if (m[5] !== undefined) {
result.commits.ahead = result.commits.total;
result.branches.remoteError = {
code: "git_remote_gone"
}
}
}
return;
}
var status = line.substring(0,2);
var fileName;
var names;
if (status !== '??') {
names = parseFilenames(line.substring(3));
} else {
names = [cleanFilename(line.substring(3))];
}
fileName = names[0];
if (names.length > 1) {
fileName = names[1];
}
// parseFilename(fileName);
if (fileName.charCodeAt(0) === 34) {
fileName = fileName.substring(1,fileName.length-1);
}
if (result.files.hasOwnProperty(fileName)) {
result.files[fileName].status = status;
} else {
result.files[fileName] = {
type: "f",
status: status
};
}
if (names.length > 1) {
result.files[fileName].oldName = names[0];
}
if (status === "??" && fileName[fileName.length-1] === '/') {
unknownDirs.push(fileName);
}
})
var allFilenames = Object.keys(result.files);
allFilenames.forEach(function(f) {
var entry = result.files[f];
if (!entry.hasOwnProperty('status')) {
unknownDirs.forEach(function(uf) {
if (f.startsWith(uf)) {
entry.status = "??"
}
});
}
})
// console.log(files);
return result;
})
})
})
}
function parseLog(log) {
var lines = log.split("\n");
var currentCommit = {};
var commits = [];
lines.forEach(function(l) {
if (l === "-----") {
commits.push(currentCommit);
currentCommit = {}
return;
}
var m = /^(.*?): (.*)$/.exec(l);
if (m) {
// git 2.1.4 (Debian Stable) doesn't support %D for refs - so filter out
if (m[1] === 'refs' && m[2]) {
if (m[2] !== '%D') {
currentCommit[m[1]] = m[2].split(",").map(function(v) { return v.trim() });
} else {
currentCommit[m[1]] = [];
}
} else {
if (m[1] === 'parents') {
currentCommit[m[1]] = m[2].split(" ");
} else {
currentCommit[m[1]] = m[2];
}
}
}
});
return commits;
}
function getRemotes(cwd) {
return runGitCommand(['remote','-v'],cwd).then(function(output) {
var result;
if (output.length > 0) {
result = {};
var remoteRE = /^(.+)\t(.+) \((.+)\)$/gm;
var m;
while ((m = remoteRE.exec(output)) !== null) {
result[m[1]] = result[m[1]]||{};
result[m[1]][m[3]] = m[2];
}
}
return result;
})
}
function getBranches(cwd, remote) {
var args = ['branch','-vv','--no-color'];
if (remote) {
args.push('-r');
}
var branchRE = /^([ \*] )(\S+) +(\S+)(?: \[(\S+?)(?:: (?:ahead (\d+)(?:, )?)?(?:behind (\d+))?)?\])? (.*)$/;
return runGitCommand(args,cwd).then(function(output) {
var branches = [];
var lines = output.split("\n");
branches = lines.map(function(l) {
var m = branchRE.exec(l);
var branch = null;
if (m) {
branch = {
name: m[2],
remote: m[4],
status: {
ahead: m[5]||0,
behind: m[6]||0,
},
commit: {
sha: m[3],
subject: m[7]
}
}
if (m[1] === '* ') {
branch.current = true;
}
}
return branch;
}).filter(function(v) { return !!v && v.commit.sha !== '->' });
return {branches:branches};
})
}
function getBranchStatus(cwd,remoteBranch) {
var commands = [
// #commits master ahead
runGitCommand(['rev-list', 'HEAD','^'+remoteBranch, '--count'],cwd),
// #commits master behind
runGitCommand(['rev-list', '^HEAD',remoteBranch, '--count'],cwd)
];
return Promise.all(commands).then(function(results) {
return {
commits: {
ahead: parseInt(results[0]),
behind: parseInt(results[1])
}
}
})
}
function addRemote(cwd,name,options) {
var args = ["remote","add",name,options.url]
return runGitCommand(args,cwd);
}
function removeRemote(cwd,name) {
var args = ["remote","remove",name];
return runGitCommand(args,cwd);
}
module.exports = {
init: function(_settings) {
return new Promise(function(resolve,reject) {
Promise.all([
runGitCommand(["--version"]),
runGitCommand(["config","--global","user.name"]).catch(err=>""),
runGitCommand(["config","--global","user.email"]).catch(err=>"")
]).then(function(output) {
var m = / (\d\S+)/.exec(output[0]);
gitVersion = m[1];
var globalUserName = output[1].trim();
var globalUserEmail = output[2].trim();
var result = {
version: gitVersion
};
if (globalUserName && globalUserEmail) {
result.user = {
name: globalUserName,
email: globalUserEmail
}
}
log.trace("git init: "+JSON.stringify(result));
resolve(result);
}).catch(function(err) {
log.trace("git init: git not found");
resolve(null);
});
});
},
initRepo: function(cwd) {
var args = ["init", "--initial-branch", "main"];
return runGitCommand(args, cwd).catch(function () {
return runGitCommand(["init"], cwd);
});
},
setUpstream: function(cwd,remoteBranch) {
var args = ["branch","--set-upstream-to",remoteBranch];
return runGitCommand(args,cwd);
},
pull: function(cwd,remote,branch,allowUnrelatedHistories,auth,gitUser) {
var args = ["pull", "--no-rebase"];
if (remote && branch) {
args.push(remote);
args.push(branch);
}
if (gitUser && gitUser['name'] && gitUser['email']) {
args.unshift('user.name="'+gitUser['name']+'"');
args.unshift('-c');
args.unshift('user.email="'+gitUser['email']+'"');
args.unshift('-c');
}
if (allowUnrelatedHistories) {
args.push("--allow-unrelated-histories");
}
var promise;
if (auth) {
if ( auth.key_path ) {
promise = runGitCommandWithSSHCommand(args,cwd,auth,true);
}
else {
promise = runGitCommandWithAuth(args,cwd,auth,true);
}
} else {
promise = runGitCommand(args,cwd,undefined,true)
}
return promise;
// .catch(function(err) {
// if (/CONFLICT/.test(err.stdout)) {
// var e = new Error("pull failed - merge conflict");
// e.code = "git_pull_merge_conflict";
// throw e;
// } else if (/Please commit your changes or stash/i.test(err.message)) {
// var e = new Error("Pull failed - local changes would be overwritten");
// e.code = "git_pull_overwrite";
// throw e;
// }
// throw err;
// });
},
push: function(cwd,remote,branch,setUpstream, auth) {
var args = ["push"];
if (branch) {
if (setUpstream) {
args.push("-u");
}
args.push(remote);
args.push("HEAD:"+branch);
} else {
args.push(remote);
}
args.push("--porcelain");
var promise;
if (auth) {
if ( auth.key_path ) {
promise = runGitCommandWithSSHCommand(args,cwd,auth,true);
}
else {
promise = runGitCommandWithAuth(args,cwd,auth,true);
}
} else {
promise = runGitCommand(args,cwd,undefined,true)
}
return promise.catch(function(err) {
if (err.code === 'git_error') {
if (/^!.*non-fast-forward/m.test(err.stdout)) {
err.code = 'git_push_failed';
}
throw err;
} else {
throw err;
}
});
},
clone: function(remote, auth, cwd) {
var args = ["clone",remote.url];
if (remote.name) {
args.push("-o");
args.push(remote.name);
}
if (remote.branch) {
args.push("-b");
args.push(remote.branch);
}
args.push(".");
if (auth) {
if ( auth.key_path ) {
return runGitCommandWithSSHCommand(args,cwd,auth,true);
}
else {
return runGitCommandWithAuth(args,cwd,auth,true);
}
} else {
return runGitCommand(args,cwd,undefined,true);
}
},
getStatus: getStatus,
getFile: function(cwd, filePath, treeish) {
var args = ["show",treeish+":"+filePath];
return runGitCommand(args,cwd);
},
getFiles: function(cwd) {
return getStatus(cwd).then(function(status) {
return status.files;
})
},
revertFile: function(cwd, filePath) {
var args = ["checkout",filePath];
return runGitCommand(args,cwd);
},
stageFile: function(cwd,file) {
var args = ["add"];
if (Array.isArray(file)) {
args = args.concat(file);
} else {
args.push(file);
}
return runGitCommand(args,cwd);
},
unstageFile: function(cwd, file) {
var args = ["reset","--"];
if (file) {
args.push(file);
}
return runGitCommand(args,cwd);
},
commit: function(cwd, message, gitUser) {
var args = ["commit","-m",message];
var env;
if (gitUser && gitUser['name'] && gitUser['email']) {
args.unshift('user.name="'+gitUser['name']+'"');
args.unshift('-c');
args.unshift('user.email="'+gitUser['email']+'"');
args.unshift('-c');
}
return runGitCommand(args,cwd,env);
},
getFileDiff(cwd,file,type) {
var args = ["diff","-w"];
if (type === "tree") {
// nothing else to do
} else if (type === "index") {
args.push("--cached");
}
args.push(file);
return runGitCommand(args,cwd);
},
fetch: function(cwd,remote,auth) {
var args = ["fetch",remote];
if (auth) {
if ( auth.key_path ) {
return runGitCommandWithSSHCommand(args,cwd,auth);
}
else {
return runGitCommandWithAuth(args,cwd,auth);
}
} else {
return runGitCommand(args,cwd);
}
},
getCommits: function(cwd,options) {
var args = ["log", "--format=sha: %H%nparents: %p%nrefs: %D%nauthor: %an%ndate: %ct%nsubject: %s%n-----"];
var limit = parseInt(options.limit) || 20;
args.push("-n "+limit);
var before = options.before;
if (before) {
args.push(before);
}
var commands = [
runGitCommand(['rev-list', 'HEAD', '--count'],cwd),
runGitCommand(args,cwd).then(parseLog)
];
return Promise.all(commands).then(function(results) {
var result = results[0];
result.count = results[1].length;
result.before = before;
result.commits = results[1];
return {
count: results[1].length,
commits: results[1],
before: before,
total: parseInt(results[0])
};
})
},
getCommit: function(cwd,sha) {
var args = ["show",sha];
return runGitCommand(args,cwd);
},
abortMerge: function(cwd) {
return runGitCommand(['merge','--abort'],cwd);
},
getRemotes: getRemotes,
getRemoteBranch: function(cwd) {
return runGitCommand(['rev-parse','--abbrev-ref','--symbolic-full-name','@{u}'],cwd).catch(function(err) {
if (/no upstream configured for branch/i.test(err.message)) {
return null;
}
throw err;
})
},
getBranches: getBranches,
// getBranchInfo: getBranchInfo,
checkoutBranch: function(cwd, branchName, isCreate) {
var args = ['checkout'];
if (isCreate) {
args.push('-b');
}
args.push(branchName);
return runGitCommand(args,cwd);
},
deleteBranch: function(cwd, branchName, isRemote, force) {
if (isRemote) {
throw new Error("Deleting remote branches not supported");
}
var args = ['branch'];
if (force) {
args.push('-D');
} else {
args.push('-d');
}
args.push(branchName);
return runGitCommand(args, cwd);
},
getBranchStatus: getBranchStatus,
addRemote: addRemote,
removeRemote: removeRemote
}
@@ -0,0 +1,2 @@
#!/bin/sh
"$NODE_RED_GIT_NODE_PATH" "$NODE_RED_GIT_ASKPASS_PATH" "$NODE_RED_GIT_SOCK_PATH" $@
@@ -0,0 +1,2 @@
#!/bin/sh
ssh -i "$NODE_RED_KEY_FILE" -F /dev/null $@
@@ -0,0 +1,716 @@
/**
* 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 loadProject(name).then(function () {
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, clearContext) {
// Stop the current flows
return runtime.nodes.stopFlows().then(function() {
if (clearContext) {
// Reset context to remove any old values
return runtime.nodes.clearContext()
} else {
return Promise.resolve()
}
}).then(function() {
// Load the new project flows and start them
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;
});
}).catch(function(err) {
console.log(err.stack);
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, clearContext) {
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", clearContext);
})
});
}
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.status(user, false).then((result) => {
const items = Object.values(result.files || {});
// check if saved flow make modification to repository
if (items.findIndex((item) => (item.status === "M ")) < 0) {
return Promise.resolve();
}
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
};
@@ -0,0 +1,212 @@
/**
* 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 keygen = require("./keygen");
var settings;
var log = require("@node-red/util").log;
var sshkeyDir;
var userSSHKeyDir;
function init(_settings) {
settings = _settings;
sshkeyDir = fspath.resolve(fspath.join(settings.userDir, "projects", ".sshkeys"));
userSSHKeyDir = fspath.join(process.env.HOME || process.env.USERPROFILE || process.env.HOMEPATH, ".ssh");
// console.log('sshkeys.init()');
return fs.ensureDir(sshkeyDir);
}
function listSSHKeys(username) {
return listSSHKeysInDir(sshkeyDir,username + '_').then(function(customKeys) {
return listSSHKeysInDir(userSSHKeyDir).then(function(existingKeys) {
existingKeys.forEach(function(k){
k.system = true;
customKeys.push(k);
})
return customKeys;
});
});
}
function listSSHKeysInDir(dir,startStr) {
startStr = startStr || "";
return fs.readdir(dir).then(function(fns) {
var ret = fns.sort()
.filter(function(fn) {
var fullPath = fspath.join(dir,fn);
if (fn.length > 2 || fn[0] != ".") {
var stats = fs.lstatSync(fullPath);
if (stats.isFile()) {
return fn.startsWith(startStr);
}
}
return false;
})
.map(function(filename) {
return filename.substr(startStr.length);
})
.reduce(function(prev, current) {
var parsePath = fspath.parse(current);
if ( parsePath ) {
if ( parsePath.ext !== '.pub' ) {
// Private Keys
prev.keyFiles.push(parsePath.base);
}
else if ( parsePath.ext === '.pub' && (prev.keyFiles.some(function(elem){ return elem === parsePath.name; }))) {
prev.privateKeyFiles.push(parsePath.name);
}
}
return prev;
}, { keyFiles: [], privateKeyFiles: [] });
return ret.privateKeyFiles.map(function(filename) {
return {
name: filename
};
});
}).then(function(result) {
return result;
}).catch(function() {
return []
});
}
function getSSHKey(username, name) {
return checkSSHKeyFileAndGetPublicKeyFileName(username, name)
.then(function(publicSSHKeyPath) {
return fs.readFile(publicSSHKeyPath, 'utf-8');
}).catch(function() {
var privateKeyPath = fspath.join(userSSHKeyDir,name);
var publicKeyPath = privateKeyPath+".pub";
return checkFilePairExist(privateKeyPath,publicKeyPath).then(function() {
return fs.readFile(publicKeyPath, 'utf-8');
}).catch(function() {
return null
});
});
}
function generateSSHKey(username, options) {
options = options || {};
var name = options.name || "";
if (!/^[a-zA-Z0-9\-_]+$/.test(options.name)) {
var err = new Error("Invalid SSH Key name");
e.code = "invalid_key_name";
return Promise.reject(err);
}
return checkExistSSHKeyFiles(username, name)
.then(function(result) {
if ( result ) {
var e = new Error("SSH Key name exists");
e.code = "key_exists";
throw e;
} else {
var comment = options.comment || "";
var password = options.password || "";
var size = options.size || 2048;
var sshKeyFileBasename = username + '_' + name;
var privateKeyFilePath = fspath.normalize(fspath.join(sshkeyDir, sshKeyFileBasename));
return generateSSHKeyPair(name, privateKeyFilePath, comment, password, size)
}
})
}
function deleteSSHKey(username, name) {
return checkSSHKeyFileAndGetPublicKeyFileName(username, name)
.then(function() {
return deleteSSHKeyFiles(username, name);
});
}
function checkExistSSHKeyFiles(username, name) {
var sshKeyFileBasename = username + '_' + name;
var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename);
var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub');
return checkFilePairExist(privateKeyFilePath,publicKeyFilePath)
.then(function() {
return true;
})
.catch(function() {
return false;
});
}
function checkSSHKeyFileAndGetPublicKeyFileName(username, name) {
var sshKeyFileBasename = username + '_' + name;
var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename);
var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub');
return checkFilePairExist(privateKeyFilePath,publicKeyFilePath).then(function() {
return publicKeyFilePath;
});
}
function checkFilePairExist(privateKeyFilePath,publicKeyFilePath) {
return Promise.all([
fs.access(privateKeyFilePath, (fs.constants || fs).R_OK),
fs.access(publicKeyFilePath , (fs.constants || fs).R_OK)
])
}
function deleteSSHKeyFiles(username, name) {
var sshKeyFileBasename = username + '_' + name;
var privateKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename);
var publicKeyFilePath = fspath.join(sshkeyDir, sshKeyFileBasename + '.pub');
return Promise.all([
fs.remove(privateKeyFilePath),
fs.remove(publicKeyFilePath)
])
}
function generateSSHKeyPair(name, privateKeyPath, comment, password, size) {
log.trace("ssh-keygen["+[name,privateKeyPath,comment,size,"hasPassword?"+!!password].join(",")+"]");
return keygen.generateKey({location: privateKeyPath, comment: comment, password: password, size: size})
.then(function(stdout) {
return name;
})
.catch(function(err) {
log.log('[SSHKey generation] error:', err);
throw err;
});
}
function getPrivateKeyPath(username, name) {
var sshKeyFileBasename = username + '_' + name;
var privateKeyFilePath = fspath.normalize(fspath.join(sshkeyDir, sshKeyFileBasename));
try {
fs.accessSync(privateKeyFilePath, (fs.constants || fs).R_OK);
} catch(err) {
privateKeyFilePath = fspath.join(userSSHKeyDir,name);
try {
fs.accessSync(privateKeyFilePath, (fs.constants || fs).R_OK);
} catch(err2) {
return null;
}
}
if (fspath.sep === '\\') {
privateKeyFilePath = privateKeyFilePath.replace(/\\/g,'\\\\');
}
return privateKeyFilePath;
}
module.exports = {
init: init,
listSSHKeys: listSSHKeys,
getSSHKey: getSSHKey,
getPrivateKeyPath: getPrivateKeyPath,
generateSSHKey: generateSSHKey,
deleteSSHKey: deleteSSHKey
};
@@ -0,0 +1,96 @@
/**
* 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 child_process = require('child_process');
var sshkeygenCommand = "ssh-keygen";
var log = require("@node-red/util").log;
function runSshKeygenCommand(args,cwd,env) {
return new Promise(function(resolve, reject) {
var child = child_process.spawn(sshkeygenCommand, args, {cwd: cwd, detached: true, env: env});
var stdout = "";
var stderr = "";
child.stdout.on('data', function(data) {
stdout += data;
});
child.stderr.on('data', function(data) {
stderr += data;
});
child.on('close', function(code, signal) {
// console.log(code);
// console.log(stdout);
// console.log(stderr);
if (code !== 0) {
var err = new Error(stderr);
err.stdout = stdout;
err.stderr = stderr;
if (/short/.test(stderr)) {
err.code = "key_passphrase_too_short";
} else if(/Key must at least be 1024 bits/.test(stderr)) {
err.code = "key_length_too_short";
}
reject(err);
}
else {
resolve(stdout);
}
});
child.on('error', function(err) {
if (/ENOENT/.test(err.toString())) {
err.code = "command_not_found";
}
reject(err);
});
});
}
function generateKey(options) {
var args = ['-q', '-t', 'rsa'];
var err;
if (options.size) {
if (options.size < 1024) {
err = new Error("key_length_too_short");
err.code = "key_length_too_short";
throw err;
}
args.push('-b', options.size);
}
if (options.location) {
args.push('-f', options.location);
}
if (options.comment) {
args.push('-C', options.comment);
}
if (options.password) {
if (options.password.length < 5) {
err = new Error("key_passphrase_too_short");
err.code = "key_passphrase_too_short";
throw err;
}
args.push('-N', options.password||'');
} else {
args.push('-N', '');
}
return runSshKeygenCommand(args,__dirname);
}
module.exports = {
generateKey: generateKey
};
@@ -0,0 +1,52 @@
/**
* 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 log = require("@node-red/util").log; // TODO: separate module
var util = require("./util");
var sessionsFile;
var settings;
module.exports = {
init: function(_settings) {
settings = _settings;
sessionsFile = fspath.join(settings.userDir,".sessions.json");
},
getSessions: async function() {
return new Promise(function(resolve,reject) {
fs.readFile(sessionsFile,'utf8',function(err,data){
if (!err) {
try {
return resolve(util.parseJSON(data));
} catch(err2) {
log.trace("Corrupted sessions file - resetting");
}
}
resolve({});
})
});
},
saveSessions: async function(sessions) {
if (settings.readOnly) {
return;
}
return util.writeFile(sessionsFile,JSON.stringify(sessions));
}
}
@@ -0,0 +1,132 @@
/**
* 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.
**/
const fs = require('fs-extra');
const fspath = require("path");
const log = require("@node-red/util").log;
const util = require("./util");
const configSections = ['nodes','users','projects','modules'];
const settingsCache = {};
var globalSettingsFile;
var globalSettingsBackup;
var settings;
async function migrateToMultipleConfigFiles() {
const nodesFilename = getSettingsFilename("nodes");
if (fs.existsSync(nodesFilename)) {
// We have both .config.json and .config.nodes.json
// Use the more recently modified. This handles users going back to pre1.2
// and up again.
// We can remove this logic in 1.3+ and remove the old .config.json file entirely
//
const fsStatNodes = await fs.stat(nodesFilename);
const fsStatGlobal = await fs.stat(globalSettingsFile);
if (fsStatNodes.mtimeMs > fsStatGlobal.mtimeMs) {
// .config.nodes.json is newer than .config.json - no migration needed
return;
}
}
const data = await util.readFile(globalSettingsFile,globalSettingsBackup,{});
// In a later release we should remove the old settings file. But don't do
// that *yet* otherwise users won't be able to downgrade easily.
return writeSettings(data) // .then( () => fs.remove(globalSettingsFile) );
}
/**
* Takes the single settings object and splits it into separate files. This makes
* it easier to backup selected parts of the settings and also helps reduce the blast
* radius if a file is lost.
*
* The settings are written to four files:
* - .config.nodes.json - the node registry
* - .config.users.json - user specific settings (eg editor settings)
* - .config.projects.json - project settings, including the active project
* - .config.modules.json - external modules installed by the runtime
* - .config.runtime.json - everything else - most notable _credentialSecret
*/
function writeSettings(data) {
const configKeys = Object.keys(data);
const writePromises = [];
configSections.forEach(key => {
const sectionData = data[key] || {};
delete data[key];
const sectionFilename = getSettingsFilename(key);
const sectionContent = JSON.stringify(sectionData,null,4);
if (sectionContent !== settingsCache[key]) {
settingsCache[key] = sectionContent;
writePromises.push(util.writeFile(sectionFilename,sectionContent,sectionFilename+".backup"))
}
})
// Having extracted nodes/users/projects, write whatever is left to the runtime config
const sectionFilename = getSettingsFilename("runtime");
const sectionContent = JSON.stringify(data,null,4);
if (sectionContent !== settingsCache["runtime"]) {
settingsCache["runtime"] = sectionContent;
writePromises.push(util.writeFile(sectionFilename,sectionContent,sectionFilename+".backup"));
}
return Promise.all(writePromises);
}
async function readSettings() {
// Read the 'runtime' settings file first
const runtimeFilename = getSettingsFilename("runtime");
const result = await util.readFile(runtimeFilename,runtimeFilename+".backup",{});
settingsCache["runtime"] = JSON.stringify(result, null ,4);
const readPromises = [];
// Read the other settings files and add them into the runtime settings
configSections.forEach(key => {
const sectionFilename = getSettingsFilename(key);
readPromises.push(util.readFile(sectionFilename,sectionFilename+".backup",{}).then(sectionData => {
settingsCache[key] = JSON.stringify(sectionData, null ,4);
if (Object.keys(sectionData).length > 0) {
result[key] = sectionData;
}
}))
});
return Promise.all(readPromises).then(() => result);
}
function getSettingsFilename(section) {
return fspath.join(settings.userDir,`.config.${section}.json`);
}
module.exports = {
init: function(_settings) {
settings = _settings;
globalSettingsFile = fspath.join(settings.userDir,".config.json");
globalSettingsBackup = fspath.join(settings.userDir,".config.json.backup");
if (fs.existsSync(globalSettingsFile) && !settings.readOnly) {
return migrateToMultipleConfigFiles();
} else {
return Promise.resolve();
}
},
getSettings: function() {
return readSettings()
},
saveSettings: function(newSettings) {
if (settings.readOnly) {
return Promise.resolve();
}
return writeSettings(newSettings);
}
}
@@ -0,0 +1,134 @@
/**
* 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.
**/
const fs = require('fs-extra');
const fspath = require('path');
const log = require("@node-red/util").log;
function parseJSON(data) {
if (data.charCodeAt(0) === 0xFEFF) {
data = data.slice(1)
}
return JSON.parse(data);
}
function readFile(path,backupPath,emptyResponse,type) {
return new Promise(function(resolve) {
fs.readFile(path,'utf8',function(err,data) {
if (!err) {
if (data.length === 0) {
log.warn(log._("storage.localfilesystem.empty",{type:type}));
try {
var backupStat = fs.statSync(backupPath);
if (backupStat.size === 0) {
// Empty flows, empty backup - return empty flow
return resolve(emptyResponse);
}
// Empty flows, restore backup
log.warn(log._("storage.localfilesystem.restore",{path:backupPath,type:type}));
fs.copy(backupPath,path,function(backupCopyErr) {
if (backupCopyErr) {
// Restore backup failed
log.warn(log._("storage.localfilesystem.restore-fail",{message:backupCopyErr.toString(),type:type}));
resolve([]);
} else {
// Loop back in to load the restored backup
resolve(readFile(path,backupPath,emptyResponse,type));
}
});
return;
} catch(backupStatErr) {
// Empty flow file, no back-up file
return resolve(emptyResponse);
}
}
try {
return resolve(parseJSON(data));
} catch(parseErr) {
log.warn(log._("storage.localfilesystem.invalid",{type:type}));
return resolve(emptyResponse);
}
} else {
if (type === 'flow') {
log.info(log._("storage.localfilesystem.create",{type:type}));
}
resolve(emptyResponse);
}
});
});
}
const writeFileLocks = {}
module.exports = {
/**
* Write content to a file using UTF8 encoding.
* This forces a fsync before completing to ensure
* the write hits disk.
*/
writeFile: async function(path,content,backupPath) {
if (!writeFileLocks[path]) {
writeFileLocks[path] = Promise.resolve()
}
const result = writeFileLocks[path].then(async () => {
var backupPromise;
if (backupPath && fs.existsSync(path)) {
await fs.copy(path,backupPath);
log.trace(`utils.writeFile - copied ${path} TO ${backupPath}`)
}
const dirname = fspath.dirname(path);
const tempFile = `${path}.$$$`;
await fs.ensureDir(dirname)
await new Promise(function(resolve,reject) {
var stream = fs.createWriteStream(tempFile);
stream.on('open',function(fd) {
stream.write(content,'utf8',function() {
fs.fsync(fd,function(err) {
if (err) {
log.warn(log._("storage.localfilesystem.fsync-fail",{path: tempFile, message: err.toString()}));
}
stream.end(resolve);
});
});
});
stream.on('error',function(err) {
log.warn(log._("storage.localfilesystem.fsync-fail",{path: tempFile, message: err.toString()}));
reject(err);
});
})
log.trace(`utils.writeFile - written content to ${tempFile}`)
await new Promise(function(resolve,reject) {
fs.rename(tempFile,path,err => {
if (err) {
log.warn(log._("storage.localfilesystem.fsync-fail",{path: path, message: err.toString()}));
return reject(err);
}
log.trace(`utils.writeFile - renamed ${tempFile} to ${path}`)
resolve();
})
})
})
writeFileLocks[path] = result.catch(() => {})
return result
},
readFile: readFile,
parseJSON: parseJSON
}