Created
December 17, 2011 14:38
-
-
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
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
/** | |
* 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, '&') | |
.replace(/</g, '<') | |
.replace(/>/g, '>') | |
.replace(/"/g, '"') | |
.replace(/'/g, '''); | |
}; | |
/** | |
* 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