From e110e14a8b6a3bd96e1b26a5e8e44a87c2471ec8 Mon Sep 17 00:00:00 2001 From: Mickael KERJEAN Date: Mon, 26 Jun 2017 23:07:36 +1000 Subject: [PATCH] feature (git): add git support --- server/ctrl/files.js | 2 +- server/ctrl/session.js | 16 ++- server/model/backend/git.js | 263 ++++++++++++++++++++++++++++++++++++ server/model/files.js | 3 +- server/model/session.js | 3 +- src/pages/connectpage.js | 28 +++- 6 files changed, 304 insertions(+), 11 deletions(-) create mode 100644 server/model/backend/git.js diff --git a/server/ctrl/files.js b/server/ctrl/files.js index f30430ac..63144539 100644 --- a/server/ctrl/files.js +++ b/server/ctrl/files.js @@ -34,7 +34,7 @@ app.get('/cat', function(req, res){ }) .catch(function(err){ res.send({status: 'error', message: err.message || 'couldn\t read the file', trace: err}) - }) + }); }else{ res.send({status: 'error', message: 'unknown path'}) } diff --git a/server/ctrl/session.js b/server/ctrl/session.js index 098be6df..4badddb7 100644 --- a/server/ctrl/session.js +++ b/server/ctrl/session.js @@ -20,16 +20,20 @@ app.post('/', function(req, res){ type: req.body.type, payload: state }; - res.cookie('auth', crypto.encrypt(persist), { maxAge: 365*24*60*60*1000, httpOnly: true }); - res.send({status: 'ok', result: 'pong'}); + const cookie = crypto.encrypt(persist); + if(Buffer.byteLength(cookie, 'utf-8') > 4096){ + res.send({status: 'error', message: 'we can\'t authenticate you', }) + }else{ + res.cookie('auth', crypto.encrypt(persist), { maxAge: 365*24*60*60*1000, httpOnly: true }); + res.send({status: 'ok', result: 'pong'}); + } }) .catch((err) => { - console.log(err) let message = function(err){ - let t = 'could not establish a connection' + let t = err && err.message || 'could not establish a connection'; if(err.code){ - t += ' ('+err.code+')' - } + t += ' ('+err.code+')'; + } return t; } res.send({status: 'error', message: message(err), code: err.code}); diff --git a/server/model/backend/git.js b/server/model/backend/git.js new file mode 100644 index 00000000..b1883bab --- /dev/null +++ b/server/model/backend/git.js @@ -0,0 +1,263 @@ +const gitclient = require("nodegit"), + toString = require('stream-to-string'), + crypto = require('crypto'), + fs = require('fs'), + Readable = require('stream').Readable, + Path = require('path'); + +module.exports = { + test: function(params){ + if(!params || !params.repo){ return Promise.reject({message: 'invalid authentication', code: 'INVALID_PARAMS'}) }; + if(!params.commit) params.commit = "{action}({filename}): {path}" + if(!params.branch) params.branch = 'master'; + if(!params.author_name) params.author_name = "Nuage"; + if(!params.author_email) params.author_email = "https://nuage.kerjean.me"; + if(!params.committer_name) params.committer_name = "Nuage"; + if(!params.committer_email) params.committer_email = "https://nuage.kerjean.me"; + + if(params.password && params.password.length > 2700){ + return Promise.reject({message: "Your password couldn\'t fit in a cookie :/", code: "COOKIE_ERROR"}) + } + return git.clone(params); + }, + cat: function(path, params){ + return file.cat(Path.join(path_repo(params), path)); + }, + ls: function(path, params){ + return file.ls(Path.join(path_repo(params), path)) + .then((files) => files.filter((file) => (file.name === '.git' && file.type === 'directory') ? false: true)) + }, + write: function(path, content, params){ + return file.write(Path.join(path_repo(params), path), content) + .then(() => git.save(params, path, "write")); + }, + rm: function(path, params){ + return file.rm(Path.join(path_repo(params), path)) + .then(() => git.save(params, path, "delete")); + }, + mv: function(from, to, params){ + return file.mv(Path.join(path_repo(params), from), Path.join(path_repo(params), to)) + .then(() => git.save(params, to, 'move')); + }, + mkdir: function(path, params){ + return file.mkdir(Path.join(path_repo(params), path)); + }, + touch: function(path, params){ + var stream = new Readable(); stream.push(''); stream.push(null); + return file.write(Path.join(path_repo(params), path), stream) + .then(() => git.save(params, path, 'create')); + } +} + + +function path_repo(obj){ + let hash = crypto.createHash('md5').update('git_'); + for(let key in obj){ + if(typeof obj[key] === 'string'){ + hash.update(obj[key]); + } + } + return "/tmp/"+hash.digest('hex'); +} + +const file = {}; +file.write = function (path, stream){ + return new Promise((done, err) => { + let writer = fs.createWriteStream(path, { flags : 'w' }); + stream.pipe(writer); + writer.on('close', function(){ + done('ok'); + }); + writer.on('error', function(error){ + err(error); + }); + }); +}; +file.mkdir = function(path){ + return new Promise((done, err) => { + fs.mkdir(path, function(error){ + if(error){ return err(error); } + return done("ok"); + }); + }); +} +file.mv = function(from, to){ + return new Promise((done, err) => { + fs.rename(from, to, function(error){ + if(error){ return err(error); } + return done("ok"); + }); + }); +} +file.ls = function(path){ + return new Promise((done, err) => { + fs.readdir(path, (error, files) => { + if(error){ return err(error); } + Promise.all(files.map((file) => { + return stats(Path.join(path, file)).then((stat) => { + stat.name = file; + return Promise.resolve(stat); + }); + })).then((files) => { + done(files.map((file) => { + return { + size: file.size, + time: new Date(file.mtime).getTime(), + name: file.name, + type: file.isFile()? 'file' : 'directory' + }; + })); + }).catch((error) => err(error)); + }); + }); + + function stats(path){ + return new Promise((done, err) => { + fs.stat(path, function(error, res){ + if(error) return err(error); + return done(res); + }); + }); + } +} +file.rm = function(path){ + return rm(path); + + function rm(path){ + return stat(path).then((_stat) => { + if(_stat.isDirectory()){ + return ls(path) + .then((files) => Promise.all(files.map(file => rm(Path.join(path, file))))) + .then(() => removeEmptyFolder(path)); + }else{ + return removeFileOrLink(path); + } + }); + } + + function removeEmptyFolder(path){ + return new Promise((done, err) => { + fs.rmdir(path, function(error){ + if(error){ return err(error); } + return done("ok"); + }); + }); + } + function removeFileOrLink(path){ + return new Promise((done, err) => { + fs.unlink(path, function(error){ + if(error){ return err(error); } + return done("ok"); + }); + }); + } + function ls(path){ + return new Promise((done, err) => { + fs.readdir(path, function (error, files) { + if(error) return err(error) + return done(files) + }); + }); + } + function stat(path){ + return new Promise((done, err) => { + fs.stat(path, function (error, _stat) { + if(error){ return err(error); } + return done(_stat); + }); + }); + } +} + +file.cat = function(path){ + return Promise.resolve(fs.createReadStream(path)); +} + + +const git = {}; +git.clone = function(params, alreadyExist = false){ + return new Promise((done, err) => { + gitclient.Clone(params.repo, path_repo(params), {fetchOpts: { callbacks: { credentials: git_creds.bind(null, params) }}}) + .then((repo) => pull(repo, params.branch)) + .then(() => done(params)) + .catch((error) => { + if(error.errno === -4){ + return gitclient.Repository.open(path_repo(params)) + .then((repo) => { + return pull(repo, params.branch) + .then(() => _refresh(repo, params.branch, params)) + }) + .then(() => done(params)) + .catch((error) => { + err({code: error && error.errno? "GIT_ERR"+error.errno : "GIT_ERR" , message: error && error.message || "can\'t clone the repo" }); + }); + } + return err({code: error && error.errno? "GIT_ERR"+error.errno : "GIT_ERR" , message: error && error.message || "can\'t clone the repo" }); + }); + }) + + function pull(repo, branch){ + return repo.getBranchCommit("origin/"+params.branch) + .then((commit) => { + return repo.createBranch(params.branch, commit) + .catch(() => Promise.resolve()) + }) + .then(() => repo.checkoutBranch(params.branch)); + } +} + +function _refresh(repo, branch, params){ + return repo.fetchAll({callbacks: { credentials: git_creds.bind(null, params) }}) + .then(() => repo.mergeBranches(branch, "origin/"+branch, gitclient.Signature.default(repo), 2)) + .catch(err => { + if(err.errno === -13){ + return git.save(params, '', 'merge') + .then(() => _refresh(repo, branch, params)) + } + return Promise.reject(err); + }) +} +git.save = function(params, path = '', type = ''){ + let data = {repo: null, commit: null, index: null, oid: null} + const author = gitclient.Signature.now(params.author_name, params.author_email); + const committer = gitclient.Signature.now(params.committer_name, params.committer_email); + const message = params.commit + .replace("{action}", type) + .replace("{dirname}", Path.dirname(path)) + .replace("{filename}", Path.basename(path)) + .replace("{path}", path || ''); + + return new Promise((done, err) => { + gitclient.Repository.open(path_repo(params)) + .then((repo) => { + data.repo = repo; + return repo.getBranchCommit(params.branch) + }) + .then((commit) => { + data.commit = commit; + return commit.repo.refreshIndex(); + }) + .then((index) => { + data.index = index; + return index.addAll(); + }) + .then(() => data.index.write()) + .then(() => data.index.writeTree()) + .then((oid) => data.repo.createCommit("HEAD", author, committer, message, oid, [data.commit])) + .then((commit) => data.repo.getRemote("origin")) + .then((remote) => remote.push(["refs/heads/"+params.branch+":refs/heads/"+params.branch], { callbacks: { credentials: git_creds.bind(null, params) }})) + .then((ok) => done(ok)) + .catch((error) => { + err(error) + }); + }); +} +function git_creds(params, url, username){ + const user = username? username : params.username; + if(/http[s]?\:\/\//.test(url)){ + return gitclient.Cred.userpassPlaintextNew(username, params.password); + }else{ + return gitclient.Cred.sshKeyMemoryNew(username, "", params.password, params.passphrase || "") + } + +} diff --git a/server/model/files.js b/server/model/files.js index f08b8184..3c711e75 100644 --- a/server/model/files.js +++ b/server/model/files.js @@ -4,7 +4,8 @@ var backend = { webdav: require('./backend/webdav'), dropbox: require('./backend/dropbox'), gdrive: require('./backend/gdrive'), - s3: require('./backend/s3') + s3: require('./backend/s3'), + git: require('./backend/git') }; exports.cat = function(path, params, res){ diff --git a/server/model/session.js b/server/model/session.js index ec84fca6..bce3145f 100644 --- a/server/model/session.js +++ b/server/model/session.js @@ -4,7 +4,8 @@ var backend = { webdav: require('./backend/webdav'), dropbox: require('./backend/dropbox'), gdrive: require('./backend/gdrive'), - s3: require('./backend/s3') + s3: require('./backend/s3'), + git: require('./backend/git') }; exports.test = function(params){ diff --git a/src/pages/connectpage.js b/src/pages/connectpage.js index fab44610..ced06987 100644 --- a/src/pages/connectpage.js +++ b/src/pages/connectpage.js @@ -14,6 +14,7 @@ export class ConnectPage extends React.Component { advanced_sftp: false, // state of checkbox in the UI advanced_webdav: false, advanced_s3: false, + advanced_git: false, credentials: {}, password: password.get() || null, marginTop: this._marginTop() @@ -79,6 +80,9 @@ export class ConnectPage extends React.Component { if(this.state.credentials['s3'] && this.state.credentials['s3']['path']){ this.setState({advanced_s3: true}) } + if(this.state.credentials['git'] && (this.state.credentials['git']['username'] || this.state.credentials['git']['commit'] || this.state.credentials['git']['branch'] || this.state.credentials['git']['passphrase'] || this.state.credentials['git']['author_name'] || this.state.credentials['git']['author_email'] || this.state.credentials['git']['committer_name'] || this.state.credentials['git']['committer_email'])){ + this.setState({advanced_git: true}) + } } } } @@ -189,16 +193,17 @@ export class ConnectPage extends React.Component { return (
- + -
+
+ @@ -247,6 +252,25 @@ export class ConnectPage extends React.Component { + + +