node-red
This commit is contained in:
@@ -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;
|
||||
+175
@@ -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);
|
||||
});
|
||||
}
|
||||
}
|
||||
+1080
File diff suppressed because it is too large
Load Diff
Vendored
+48
@@ -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" ;}
|
||||
}
|
||||
Vendored
+46
@@ -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
|
||||
}
|
||||
}
|
||||
Vendored
+133
@@ -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
|
||||
}
|
||||
Vendored
+24
@@ -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');
|
||||
+662
@@ -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
|
||||
}
|
||||
Vendored
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
"$NODE_RED_GIT_NODE_PATH" "$NODE_RED_GIT_ASKPASS_PATH" "$NODE_RED_GIT_SOCK_PATH" $@
|
||||
Vendored
Executable
+2
@@ -0,0 +1,2 @@
|
||||
#!/bin/sh
|
||||
ssh -i "$NODE_RED_KEY_FILE" -F /dev/null $@
|
||||
+716
@@ -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
|
||||
|
||||
};
|
||||
+212
@@ -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
|
||||
};
|
||||
+96
@@ -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
|
||||
};
|
||||
+52
@@ -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));
|
||||
}
|
||||
}
|
||||
+132
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user