mirror of
https://github.com/mickael-kerjean/filestash.git
synced 2025-11-02 11:57:04 +08:00
315 lines
11 KiB
JavaScript
315 lines
11 KiB
JavaScript
const gitclient = require("nodegit"),
|
|
toString = require('stream-to-string'),
|
|
fs = require('fs'),
|
|
Readable = require('stream').Readable,
|
|
Path = require('path'),
|
|
crypto = require("crypto"),
|
|
BASE_PATH = "/tmp/";
|
|
|
|
let repos = {};
|
|
setInterval(() => autoVacuum, 1000*60*60*10);
|
|
|
|
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.open(params)
|
|
.then(() => Promise.resolve(params));
|
|
},
|
|
cat: function(path, params){
|
|
return git.open(params)
|
|
.then((repo) => git.refresh(repo, params))
|
|
.then(() => file.cat(calculate_path(params, path)));
|
|
},
|
|
ls: function(path, params){
|
|
return git.open(params)
|
|
.then((repo) => git.refresh(repo, params))
|
|
.then(() => file.ls(calculate_path(params, path)))
|
|
.then((files) => files.filter((file) => (file.name === '.git' && file.type === 'directory') ? false: true))
|
|
},
|
|
write: function(path, content, params){
|
|
return git.open(params)
|
|
.then(() => file.write(calculate_path(params, path), content))
|
|
.then(() => git.save(params, path, "write"));
|
|
},
|
|
rm: function(path, params){
|
|
return git.open(params)
|
|
.then(() => file.rm(calculate_path(params, path)))
|
|
.then(() => git.save(params, path, "delete"));
|
|
},
|
|
mv: function(from, to, params){
|
|
return git.open(params)
|
|
.then(() => file.mv(calculate_path(params, from), calculate_path(params, to)))
|
|
.then(() => git.save(params, to, 'move'));
|
|
},
|
|
mkdir: function(path, params){
|
|
return git.open(params)
|
|
.then(() => file.mkdir(calculate_path(params, path)))
|
|
.then(() => git.save(params, path, "create"))
|
|
},
|
|
touch: function(path, params){
|
|
var stream = new Readable(); stream.push(''); stream.push(null);
|
|
return git.open(params)
|
|
.then(() => file.write(calculate_path(params, path), stream))
|
|
.then(() => git.save(params, path, 'create'));
|
|
}
|
|
};
|
|
|
|
function autovacuum(){
|
|
const MAXIMUM_DATE_BEFORE_CLEAN = new Date().getTime() - 1000*60*60*24;
|
|
for(let repo_path in repos){
|
|
if(repos[repo_path] > MAXIMUM_DATE_BEFORE_CLEAN){
|
|
fs.unlink(repo_path);
|
|
delete repos[repo_path];
|
|
}
|
|
}
|
|
}
|
|
|
|
function calculate_path(params, path){
|
|
const repo = path_repo(params);
|
|
const full_path = Path.posix.join(repo, path);
|
|
if(full_path.indexOf(BASE_PATH) !== 0 || full_path === BASE_PATH){
|
|
return BASE_PATH+"error";
|
|
}
|
|
return full_path;
|
|
}
|
|
|
|
function path_repo(obj){
|
|
let hash = crypto.createHash('md5');
|
|
for(let key in obj){
|
|
if(typeof obj[key] === 'string'){
|
|
hash.update(obj[key]);
|
|
}
|
|
}
|
|
const path = BASE_PATH+"git_"+obj.uid+"_"+obj.repo.replace(/[^a-zA-Z]/g, "")+"_"+hash.digest('hex');
|
|
repos[path] = new Date().getTime();
|
|
return path;
|
|
}
|
|
|
|
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+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+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.open = function(params){
|
|
count = 0;
|
|
return gitclient.Repository.open(path_repo(params))
|
|
.catch((err) => {
|
|
return gitclient.Clone(params.repo, path_repo(params), {fetchOpts: { callbacks: { credentials: git_creds.bind(null, params) }}})
|
|
.then((repo) => {
|
|
const branch = params.branch;
|
|
return repo.getBranchCommit("origin/"+branch)
|
|
.catch(() => repo.getHeadCommit("origin"))
|
|
.then((commit) => {
|
|
return repo.createBranch(branch, commit)
|
|
.then(() => repo.checkoutBranch(branch))
|
|
.then(() => Promise.resolve(repo));
|
|
})
|
|
.catch(() => Promise.resolve(repo));
|
|
});
|
|
});
|
|
};
|
|
|
|
git.refresh = function(repo, params){
|
|
count = 0;
|
|
return repo.fetchAll({callbacks: { credentials: git_creds.bind(null, params) }})
|
|
.then(() => repo.mergeBranches(params.branch, "origin/"+params.branch, gitclient.Signature.default(repo), 2))
|
|
.catch(err => {
|
|
if(err.errno === -13){
|
|
return git.save(params, '', 'merge')
|
|
.then(() => git.refresh(repo, params))
|
|
.then(() => Promise.resolve(repo));
|
|
}
|
|
return Promise.resolve(repo);
|
|
});
|
|
};
|
|
|
|
git.save = function(params, path = '', type = ''){
|
|
count = 0;
|
|
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.posix.dirname(path))
|
|
.replace("{filename}", Path.posix.basename(path))
|
|
.replace("{path}", path || '');
|
|
|
|
return git.open(params)
|
|
.then((repo) => Promise.all([
|
|
Promise.resolve(repo),
|
|
getParent(repo, params),
|
|
refresh(repo, params)
|
|
]))
|
|
.then((data) => {
|
|
const [repo, commit, oid] = data;
|
|
const parents = commit ? [commit] : [];
|
|
return repo.createCommit("HEAD", author, committer, message, oid, parents)
|
|
.then(() => Promise.resolve(repo));
|
|
})
|
|
.then((repo) => {
|
|
return repo.getRemote("origin")
|
|
.then((remote) => {
|
|
return remote.push(
|
|
["refs/heads/"+params.branch+":refs/heads/"+params.branch],
|
|
{ callbacks: { credentials: git_creds.bind(null, params, true) }}
|
|
);
|
|
})
|
|
.catch((err) => Promise.reject({status: 403, message: "Not authorized to push"}));
|
|
});
|
|
|
|
function getParent(repo, params){
|
|
return repo.getBranchCommit(params.branch)
|
|
.catch(() => {
|
|
return repo.getHeadCommit()
|
|
.catch(() => Promise.resolve(null));
|
|
});
|
|
}
|
|
function refresh(repo, params){
|
|
return repo.refreshIndex()
|
|
.then((index) => {
|
|
return index.addAll()
|
|
.then(() => index.write())
|
|
.then(() => index.writeTree());
|
|
});
|
|
}
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// the count thinghy is used to see if the request succeeded or not
|
|
// when something fail, nodegit would just run the callback again and again.
|
|
// The only way to make it throw an error is to return the defaultNew thinghy
|
|
let count = 0;
|
|
function git_creds(params, fn, _count){
|
|
count += 1;
|
|
|
|
if(count > 1 && _count !== undefined){
|
|
return new gitclient.Cred.defaultNew();
|
|
}else if(/http[s]?\:\/\//.test(params.repo)){
|
|
return new gitclient.Cred.userpassPlaintextNew(params.username, params.password);
|
|
}else{
|
|
return new gitclient.Cred.sshKeyMemoryNew(params.username, "", params.password, params.passphrase || "")
|
|
}
|
|
}
|