Skip to content

Instantly share code, notes, and snippets.

@chjj
Created December 17, 2011 14:38
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 1 You must be signed in to fork a gist
  • Save chjj/1490353 to your computer and use it in GitHub Desktop.
Save chjj/1490353 to your computer and use it in GitHub Desktop.
pocket-sized file manager/sharing so i dont have to start up samba
/**
* pocketfm
* Pocket-sized File Manager
* Uses plain html to be as simple as possible
* Code is messy, needs revision and needs
* native mv, rm, cp implementations
* Copyright (c) 2011, Christopher Jeffrey (MIT License)
*/
process.title = 'pocketfm';
var fs = require('fs')
, path = require('path')
, http = require('http')
, url = require('url');
var lib;
try {
lib = require('vanilla');
} catch(e) {
lib = require('express');
}
var dir = process.argv[2]
|| process.cwd()
|| __dirname;
var app = lib.createServer();
// clean path
app.use(function(req, res, next) {
if (req.pathname) return next();
var uri = url.parse(req.url)
, pathname = uri.pathname || '/';
if (pathname[pathname.length-1] === '/') {
pathname = pathname.slice(0, -1);
}
pathname = unescape(pathname);
try {
pathname = decodeURIComponent(pathname).replace(/\+/g, ' ');
} finally {
pathname = pathname.replace(/\0/g, '');
}
req.pathname = pathname || '/';
next();
});
app.use(function(req, res, next) {
if (~req.pathname.indexOf('..')) {
return next(403);
}
if (~req.pathname.indexOf('\0')) {
return next(404);
}
next();
});
app.use(lib.bodyParser());
app.use(function(req, res, next) {
var action = req.query.a;
if (!action) return next();
if (action === 'upload') {
var pending = 0;
if (req.files.file && req.files.file.name) {
pending++;
var src = req.files.file.path
, dest = path.join(dir, req.pathname, req.files.file.name);
mv(src, dest, function(err) {
if (res.finished) return;
if (err) return next(new Error('Error code: ' + err));
--pending || res.redirect(req.pathname);
});
}
if (req.files.tarball && req.files.tarball.name) {
pending++;
var src = req.files.tarball.path
, dest = path.join(dir, req.pathname);
untar(src, dest, function(err) {
if (res.finished) return;
if (err) return next(new Error('Error code: ' + err));
--pending || res.redirect(req.pathname);
});
}
if (!pending) return res.redirect(req.pathname);
return;
} else if (action === 'delete') {
res.send([
'<!doctype html><title>sure?</title>',
'<style>a { display: block; margin: 10px; }</style>',
'<h1>Delete File: ' + e(req.pathname) + '</h1>',
'<p>',
'Are you sure?',
'<form method="POST" action="'
+ e(req.pathname) + '?a=really_delete">',
'<input type="submit" value="Yes, delete it.">',
'</form>',
'<a href="' + e(path.resolve(req.pathname, '..')) + '">',
'No, go back.</a>',
'</p>'
].join('\n') + '\n');
return;
} else if (action === 'really_delete') {
if (req.method !== 'POST') return next();
return rimraf(path.join(dir, req.pathname), function(err) {
if (err) return next(new Error('Error code: ' + err));
res.redirect(path.resolve(req.pathname, '..'));
});
} else if (action === 'tarball') {
var tar = (path.basename(req.pathname) || 'root') + '.tar.gz';
return tarball(path.join(dir, req.pathname), function(err, file) {
if (err) return next(new Error('Error code: ' + err));
res.setHeader('Content-Disposition',
'attachment; filename="' + tar + '"');
res.sendfile(file);
});
} else if (action === 'mv') {
if (!req.body.dest) return res.redirect(path.resolve(req.pathname, '..'));
var src = path.join(dir, req.pathname);
var dest = path.resolve(req.pathname, '..');
dest = path.resolve(dest, req.body.dest);
dest = path.join(dir, dest);
var op = req.body.hasOwnProperty('mv')
? mv
: cp;
return op(src, dest, function(err) {
if (err) return next(new Error('Error code: ' + err));
res.redirect(path.resolve(req.pathname, '..'));
});
} else if (action === 'mkdir') {
if (!req.body.dir) return next(new Error('Error code: ' + err));
var d = req.body.dir; //path.basename(req.body.dir);
var dest = path.join(dir, req.pathname, d);
return mkdir(dest, function(err) {
if (err) return next(500);
res.redirect(req.pathname);
});
}
next();
});
app.use(function(req, res, next) {
fs.readdir(path.join(dir, req.pathname), function(err, list) {
if (err) return next();
var crumbs = req.pathname + (req.pathname !== '/' ? '/' : '')
, all = '';
crumbs = crumbs.replace(/[^\/]*\//g, function(crumb) {
all += crumb;
return '<a href="' + e(all) + '">' + e(crumb) + '</a>';
});
var out = [
'<!doctype html>',
'<title>' + e(req.pathname) + '</title>',
'<style>',
' td { padding: 0 10px; }',
' table, td, tr { border: none; }',
'',
' table { border-radius: 5px; padding: 5px; }',
' td:hover { background: #ddd; }',
' body { font: 12px/1.5 verdana; }',
' a { text-decoration: none; }',
' a:link { color: #22a; }',
' a:visited, a:hover { color: #a32; }',
' h2 { margin-top: 40px; }',
' label { float: left; width: 60px; }',
' td:first-child:before {',
' content: "•"; float: left; margin-right: 10px;',
' }',
'',
' td [type=text] {',
' display: none;',
' position: absolute; margin-top: 25px; /* 23px */',
' }',
' td [type=text] {',
' border: solid 1px; padding: 3px;',
' box-shadow: #aaa 2px 2px 5px;',
' }',
' td > form:hover > [type=text], ',
' td [type=text]:hover, ',
' td [type=text]:focus {',
' display: block;',
' }',
'</style>',
'<h1>' + crumbs + '</h1>',
'<table>',
'<tr>',
'<td>',
'<a href="' + e(path.resolve(req.pathname, '..')) + '">',
'../',
'</a>',
'</td>',
'</tr>',
];
var i = list.length
, files = []
, dirs = [];
if (!i) return end();
list.forEach(function(file) {
fs.stat(path.join(dir, req.pathname, file), function(err, stat) {
if (err) return --i || end();
var size = stat.size;
if (size >= (1024 * 1024)) {
size = stat.size / 1024 / 1024 >> 0;
size += 'mb';
} else if (size >= 1024) {
size = stat.size / 1024 >> 0;
size += 'kb';
} else {
size += 'b';
}
var obj = stat.isDirectory()
? dirs
: files;
obj.push([
'<tr>',
'<td>',
'<a href="' + e(path.join(req.pathname, file)) + '">',
e(file) + (stat.isDirectory() ? '/' : ''),
'</a>',
'</td>',
'<td>',
e(prettyTime(stat.mtime)),
'</td>',
'<td>',
e(size),
'</td>',
'<td>',
'<form method="POST" action="'
+ e(path.join(req.pathname, file)) + '?a=mv'
+ '">',
' <input type="text" name="dest" placeholder="move/copy to...">',
' <input type="submit" name="mv" value="mv">',
' <input type="submit" name="cp" value="cp">',
'</form>',
'</td>',
'<td>',
'<form method="GET" action="'
+ e(path.join(req.pathname, file))
+ '?a=delete">',
' <input type="hidden" name="a" value="delete">',
' <input type="submit" value="rm">',
'</form>',
'</td>',
'</tr>'
].join('\n'));
--i || end();
});
});
function end() {
dirs = sort(dirs);
files = sort(files);
out.push(dirs.join('\n'));
out.push(files.join('\n'));
out.push('</table>');
out.push([
'<h2>Download Directory</h2>',
'<form action="' + e(req.pathname) + '?a=tarball" method="POST">',
' <input type="submit" value="Download Directory">',
'</form>'
].join('\n'));
out.push([
'<h2>Make Directory</h2>',
'<form method="POST" action="'
+ e(req.pathname) + '?a=mkdir">',
' <input name="dir" type="text" placeholder="./hello/world">',
' <input type="submit" value="mkdir">',
'</form>'
].join('\n'));
out.push([
'<h2>Upload Files</h2>',
'<form action="' + e(req.pathname) + '?a=upload"'
+ ' method="POST" enctype="multipart/form-data">',
' <p><label>File:</label> <input type="file" name="file"></p>',
' <p><label>Tarball:</label> ',
' <input type="file" name="tarball"></p>',
' <input type="submit" value="Upload">',
'</form>',
].join('\n'));
res.send(out.join('\n') + '\n');
}
});
});
/**
* Force mime type of text
* for unknown extensions
*/
var mime = lib.static.mime;
app.use(function(req, res, next) {
var t = mime.lookup(path.basename(req.pathname));
if (t === mime.types.bin && !~req.pathname.indexOf('.bin')) {
res.contentType('.txt');
}
next();
});
app.use(lib.static(dir));
app.use(function(err, req, res, next) {
var body = err.stack || err + ''
, code = err;
if (res.finished || res._header) {
return console.error('res.error failed.');
}
// remove all headers - hack
res._headers = {};
res._headerName = {};
res.statusCode = code = +code || 500;
// 204 and 304 should not have a body
if (code !== 204 && code !== 304 && code > 199) {
var phrase = code + ': ' + http.STATUS_CODES[code];
if (!body) body = 'An error occured.';
body = '<!doctype html>\n'
+ '<title>' + code + '</title>\n'
+ '<h1>' + phrase + '</h1>\n'
+ '<pre>' + body + '</pre>';
res.writeHead(code, {
'Content-Type': 'text/html; charset=utf-8',
'Content-Length': Buffer.byteLength(body)
});
} else {
body = undefined;
}
res.end(body);
});
app.listen(8080);
/**
* Helpers
*/
var sort = function(obj) {
return obj.sort(function(a, b) {
// hack
a = path.basename(/href="([^"]+)"/.exec(a)[1]);
b = path.basename(/href="([^"]+)"/.exec(b)[1]);
a = a.toLowerCase()[0];
b = b.toLowerCase()[0];
return a > b ? 1 : (a < b ? -1 : 0);
});
};
var e = function(html, dbl) {
return (html + '')
.replace(!dbl
? /&(?!#?\w+;)/g
: /&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&apos;');
};
/**
* Tarball
*/
var spawn = require('child_process').spawn;
var tarball = function(file, func) {
var out = '/tmp/up_' + Date.now() + '.tar.gz';
var tar = spawn('tar', ['-czf', out, file]);
tar.on('exit', function(code) {
if (code !== 0) return func(code);
func(null, out);
});
};
var untar = function(src, dest, func) {
var tar = spawn('tar', ['-xzf', src, '-C', dest]);
tar.on('exit', function(code) {
if (code !== 0) return func(code);
func();
});
};
/**
* Filesystem
*/
var cp = function(src, dest, func) {
var cp = spawn('cp', ['-rf', src, dest]);
cp.on('exit', function(code) {
if (code !== 0) return func(code);
func();
});
};
var mv = function(src, dest, func) {
var mv = spawn('mv', ['-f', src, dest]);
mv.on('exit', function(code) {
if (code !== 0) return func(code);
func();
});
};
var mkdir = function(file, func) {
var mkdir = spawn('mkdir', ['-p', file]);
mkdir.on('exit', function(code) {
if (code !== 0) return func(code);
func();
});
};
var rimraf = function(file, func) {
var rm = spawn('rm', ['-rf', file]);
rm.on('exit', function(code) {
if (code !== 0) return func(code);
func();
});
};
/**
* Pretty Time
*/
var prettyTime = function(time) {
var date = time.getUTCFullYear ? time : new Date(time)
, sec = Math.floor(new Date(Date.now() - date) / 1000)
, days = Math.floor(sec / 86400);
if (days === 0) {
if (sec <= 1) {
return '1 second ago';
}
if (sec < 60) {
return sec + ' seconds ago';
}
if (sec < 120) {
return '1 minute ago';
}
if (sec < 3600) {
return Math.floor(sec / 60)
+ ' minutes ago';
}
if (sec < 7200) {
return '1 hour ago';
}
return Math.floor(sec / 3600)
+ ' hours ago';
}
if (days < 31) {
if (days === 1) {
return 'Yesterday';
}
if (days < 14) {
return days + ' days ago';
}
return Math.floor(days / 7)
+ ' weeks ago';
}
if (days >= 31) {
var months = Math.floor(days / 31);
if (months === 1) {
return '1 month ago';
}
if (months >= 12) {
var years = Math.floor(months / 12);
if (years === 1) {
return '1 year ago';
}
return years + ' years ago';
}
return months + ' months ago';
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment