diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | .jshintrc | 7 | ||||
-rwxr-xr-x | app.js | 33 | ||||
-rw-r--r-- | files/foobar | 26 | ||||
-rw-r--r-- | package.json | 26 | ||||
-rw-r--r-- | src/files.js | 112 | ||||
-rw-r--r-- | src/multipart.js | 47 | ||||
-rw-r--r-- | views/directory.ejs | 17 | ||||
-rw-r--r-- | views/file.ejs | 15 |
9 files changed, 284 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..c2658d7 --- /dev/null +++ b/.gitignore | |||
@@ -0,0 +1 @@ | |||
node_modules/ | |||
diff --git a/.jshintrc b/.jshintrc new file mode 100644 index 0000000..174ffa3 --- /dev/null +++ b/.jshintrc | |||
@@ -0,0 +1,7 @@ | |||
1 | { | ||
2 | "node": true, | ||
3 | "browser": true, | ||
4 | "unused": true, | ||
5 | "globalstrict": true, | ||
6 | "predef": [ "angular", "$", "describe", "it", "before", "after" ] | ||
7 | } | ||
@@ -0,0 +1,33 @@ | |||
1 | #!/usr/bin/env node | ||
2 | |||
3 | 'use strict'; | ||
4 | |||
5 | var express = require('express'), | ||
6 | morgan = require('morgan'), | ||
7 | compression = require('compression'), | ||
8 | bodyParser = require('body-parser'), | ||
9 | lastMile = require('connect-lastmile'), | ||
10 | multipart = require('./src/multipart'), | ||
11 | files = require('./src/files.js'); | ||
12 | |||
13 | var app = express(); | ||
14 | var router = new express.Router(); | ||
15 | |||
16 | var multipart = multipart({ maxFieldsSize: 2 * 1024, limit: '512mb', timeout: 3 * 60 * 1000 }); | ||
17 | |||
18 | router.get('/api/files/*', files.get); | ||
19 | router.put('/api/files/*', multipart, files.put); | ||
20 | router.delete('/api/files/*', files.del); | ||
21 | |||
22 | app.use(morgan('dev')); | ||
23 | app.use(compression()); | ||
24 | app.use(bodyParser.json()); | ||
25 | app.use(router); | ||
26 | app.use(lastMile()); | ||
27 | |||
28 | var server = app.listen(3000, function () { | ||
29 | var host = server.address().address; | ||
30 | var port = server.address().port; | ||
31 | |||
32 | console.log('Surfer listening at http://%s:%s', host, port); | ||
33 | }); \ No newline at end of file | ||
diff --git a/files/foobar b/files/foobar new file mode 100644 index 0000000..cf08457 --- /dev/null +++ b/files/foobar | |||
@@ -0,0 +1,26 @@ | |||
1 | { | ||
2 | "name": "surfer", | ||
3 | "version": "1.0.0", | ||
4 | "description": "Simple file server", | ||
5 | "main": "app.js", | ||
6 | "scripts": { | ||
7 | "test": "echo \"Error: no test specified\" && exit 1" | ||
8 | }, | ||
9 | "keywords": [ | ||
10 | "file", | ||
11 | "server" | ||
12 | ], | ||
13 | "author": "Johannes Zellner <johannes@nebulon.de>", | ||
14 | "license": "MIT", | ||
15 | "dependencies": { | ||
16 | "body-parser": "^1.13.1", | ||
17 | "compression": "^1.5.0", | ||
18 | "connect-lastmile": "0.0.10", | ||
19 | "connect-timeout": "^1.6.2", | ||
20 | "ejs": "^2.3.1", | ||
21 | "express": "^4.12.4", | ||
22 | "morgan": "^1.6.0", | ||
23 | "multiparty": "^4.1.2", | ||
24 | "rimraf": "^2.4.0" | ||
25 | } | ||
26 | } | ||
diff --git a/package.json b/package.json new file mode 100644 index 0000000..cf08457 --- /dev/null +++ b/package.json | |||
@@ -0,0 +1,26 @@ | |||
1 | { | ||
2 | "name": "surfer", | ||
3 | "version": "1.0.0", | ||
4 | "description": "Simple file server", | ||
5 | "main": "app.js", | ||
6 | "scripts": { | ||
7 | "test": "echo \"Error: no test specified\" && exit 1" | ||
8 | }, | ||
9 | "keywords": [ | ||
10 | "file", | ||
11 | "server" | ||
12 | ], | ||
13 | "author": "Johannes Zellner <johannes@nebulon.de>", | ||
14 | "license": "MIT", | ||
15 | "dependencies": { | ||
16 | "body-parser": "^1.13.1", | ||
17 | "compression": "^1.5.0", | ||
18 | "connect-lastmile": "0.0.10", | ||
19 | "connect-timeout": "^1.6.2", | ||
20 | "ejs": "^2.3.1", | ||
21 | "express": "^4.12.4", | ||
22 | "morgan": "^1.6.0", | ||
23 | "multiparty": "^4.1.2", | ||
24 | "rimraf": "^2.4.0" | ||
25 | } | ||
26 | } | ||
diff --git a/src/files.js b/src/files.js new file mode 100644 index 0000000..fd9c8fd --- /dev/null +++ b/src/files.js | |||
@@ -0,0 +1,112 @@ | |||
1 | 'use strict'; | ||
2 | |||
3 | var fs = require('fs'), | ||
4 | path = require('path'), | ||
5 | ejs = require('ejs'), | ||
6 | rimraf = require('rimraf'), | ||
7 | HttpError = require('connect-lastmile').HttpError, | ||
8 | HttpSuccess = require('connect-lastmile').HttpSuccess; | ||
9 | |||
10 | exports = module.exports = { | ||
11 | get: get, | ||
12 | put: put, | ||
13 | del: del | ||
14 | }; | ||
15 | |||
16 | var FILE_BASE = path.resolve(__dirname, '../files'); | ||
17 | |||
18 | // http://stackoverflow.com/questions/11293857/fastest-way-to-copy-file-in-node-js | ||
19 | function copyFile(source, target, cb) { | ||
20 | var cbCalled = false; | ||
21 | |||
22 | var rd = fs.createReadStream(source); | ||
23 | rd.on("error", function(err) { | ||
24 | done(err); | ||
25 | }); | ||
26 | |||
27 | var wr = fs.createWriteStream(target); | ||
28 | wr.on("error", function(err) { | ||
29 | done(err); | ||
30 | }); | ||
31 | |||
32 | wr.on("close", function(ex) { | ||
33 | done(); | ||
34 | }); | ||
35 | |||
36 | rd.pipe(wr); | ||
37 | |||
38 | function done(err) { | ||
39 | if (!cbCalled) { | ||
40 | cb(err); | ||
41 | cbCalled = true; | ||
42 | } | ||
43 | } | ||
44 | } | ||
45 | |||
46 | function render(view, options) { | ||
47 | return ejs.render(fs.readFileSync(view, 'utf8'), options); | ||
48 | } | ||
49 | |||
50 | function getAbsolutePath(filePath) { | ||
51 | var absoluteFilePath = path.resolve(FILE_BASE, filePath); | ||
52 | |||
53 | if (absoluteFilePath.indexOf(FILE_BASE) !== 0) return null; | ||
54 | return absoluteFilePath; | ||
55 | } | ||
56 | |||
57 | function get(req, res, next) { | ||
58 | var filePath = req.params[0]; | ||
59 | var absoluteFilePath = getAbsolutePath(filePath); | ||
60 | if (!absoluteFilePath) return next(new HttpError(403, 'Path not allowed')); | ||
61 | |||
62 | fs.stat(absoluteFilePath, function (error, result) { | ||
63 | if (error) return next(new HttpError(404, error)); | ||
64 | |||
65 | console.log('get', absoluteFilePath, result); | ||
66 | |||
67 | if (result.isFile()) return res.sendfile(absoluteFilePath); | ||
68 | if (result.isDirectory()) return res.status(200).send({ entries: fs.readdirSync(absoluteFilePath) }); | ||
69 | |||
70 | return next(new HttpError(500, 'unsupported type')); | ||
71 | }); | ||
72 | } | ||
73 | |||
74 | function put(req, res, next) { | ||
75 | var filePath = req.params[0]; | ||
76 | |||
77 | if (!req.files.file) return next(new HttpError(400, 'missing file')); | ||
78 | |||
79 | var absoluteFilePath = getAbsolutePath(filePath); | ||
80 | if (!absoluteFilePath) return next(new HttpError(403, 'Path not allowed')); | ||
81 | |||
82 | fs.stat(absoluteFilePath, function (error, result) { | ||
83 | if (error && error.code !== 'ENOENT') return next(new HttpError(500, error)); | ||
84 | |||
85 | console.log('put', absoluteFilePath, result, req.files.file); | ||
86 | |||
87 | if (result && result.isDirectory()) return next(new HttpError(409, 'cannot put on directories')); | ||
88 | if (!result || result.isFile()) { | ||
89 | return copyFile(req.files.file.path, absoluteFilePath, function (error) { | ||
90 | if (error) return next(new HttpError(500, error)); | ||
91 | next(new HttpSuccess(201, {})); | ||
92 | }); | ||
93 | } | ||
94 | |||
95 | return next(new HttpError(500, 'unsupported type')); | ||
96 | }); | ||
97 | } | ||
98 | |||
99 | function del(req, res, next) { | ||
100 | var filePath = req.params[0]; | ||
101 | var absoluteFilePath = getAbsolutePath(filePath); | ||
102 | if (!absoluteFilePath) return next(new HttpError(403, 'Path not allowed')); | ||
103 | |||
104 | fs.stat(absoluteFilePath, function (error, result) { | ||
105 | if (error) return next(new HttpError(404, error)); | ||
106 | |||
107 | rimraf(absoluteFilePath, function (error) { | ||
108 | if (error) return next(new HttpError(500, 'Unable to remove')); | ||
109 | next(new HttpError(200, {})); | ||
110 | }); | ||
111 | }); | ||
112 | } | ||
diff --git a/src/multipart.js b/src/multipart.js new file mode 100644 index 0000000..7b994cc --- /dev/null +++ b/src/multipart.js | |||
@@ -0,0 +1,47 @@ | |||
1 | /* jshint node:true */ | ||
2 | |||
3 | 'use strict'; | ||
4 | |||
5 | var multiparty = require('multiparty'), | ||
6 | timeout = require('connect-timeout'); | ||
7 | |||
8 | function _mime(req) { | ||
9 | var str = req.headers['content-type'] || ''; | ||
10 | return str.split(';')[0]; | ||
11 | } | ||
12 | |||
13 | module.exports = function multipart(options) { | ||
14 | return function (req, res, next) { | ||
15 | if (_mime(req) !== 'multipart/form-data') return res.status(400).send('Invalid content-type. Expecting multipart'); | ||
16 | |||
17 | var form = new multiparty.Form({ | ||
18 | uploadDir: '/tmp', | ||
19 | keepExtensions: true, | ||
20 | maxFieldsSize: options.maxFieldsSize || (2 * 1024), // only field size, not files | ||
21 | limit: options.limit || '500mb', // file sizes | ||
22 | autoFiles: true | ||
23 | }); | ||
24 | |||
25 | // increase timeout of file uploads by default to 3 mins | ||
26 | if (req.clearTimeout) req.clearTimeout(); // clear any previous installed timeout middleware | ||
27 | |||
28 | timeout(options.timeout || (3 * 60 * 1000))(req, res, function () { | ||
29 | req.fields = { }; | ||
30 | req.files = { }; | ||
31 | |||
32 | form.parse(req, function (err, fields, files) { | ||
33 | if (err) return res.status(400).send('Error parsing request'); | ||
34 | next(null); | ||
35 | }); | ||
36 | |||
37 | form.on('file', function (name, file) { | ||
38 | req.files[name] = file; | ||
39 | }); | ||
40 | |||
41 | form.on('field', function (name, value) { | ||
42 | req.fields[name] = value; // otherwise fields.name is an array | ||
43 | }); | ||
44 | }); | ||
45 | }; | ||
46 | }; | ||
47 | |||
diff --git a/views/directory.ejs b/views/directory.ejs new file mode 100644 index 0000000..7317794 --- /dev/null +++ b/views/directory.ejs | |||
@@ -0,0 +1,17 @@ | |||
1 | <html> | ||
2 | <head> | ||
3 | <title> <%= dirPath %> </title> | ||
4 | </head> | ||
5 | |||
6 | <body> | ||
7 | |||
8 | Directory <%= dirPath %>: | ||
9 | |||
10 | <ul> | ||
11 | <% entries.forEach(function (entry) { %> | ||
12 | <li><a href="/api/files/<%= dirPath %>/<%= entry %>"><%= entry %></a></li> | ||
13 | <% }); %> | ||
14 | </ul> | ||
15 | |||
16 | </body> | ||
17 | </html> \ No newline at end of file | ||
diff --git a/views/file.ejs b/views/file.ejs new file mode 100644 index 0000000..0b62b98 --- /dev/null +++ b/views/file.ejs | |||
@@ -0,0 +1,15 @@ | |||
1 | <html> | ||
2 | <head> | ||
3 | <title> File </title> | ||
4 | </head> | ||
5 | |||
6 | <body> | ||
7 | |||
8 | File <%= filePath %>: | ||
9 | |||
10 | <pre> | ||
11 | <%= fileContent %> | ||
12 | </pre> | ||
13 | |||
14 | </body> | ||
15 | </html> \ No newline at end of file | ||