Skip to content

Commit

Permalink
Add ?dl and ?download logic for downloading vs. viewing files
Browse files Browse the repository at this point in the history
  • Loading branch information
humphd committed Jan 6, 2019
1 parent dced878 commit 0c003af
Show file tree
Hide file tree
Showing 6 changed files with 97 additions and 48 deletions.
10 changes: 10 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,10 @@ This uses [Filer](https://github.com/filerjs/filer) to run a node'js style
POSIX filesystem inside a Service Worker, and handle requests for static files and
directories.

The most likely use case for it would be an app that uses Filer to run a filesystem
in the window, and then using nohost to provide a way to interact with the filesystem
in the browser like you would with Apache or another web server hosting static files.

To run it:

```
Expand All @@ -15,6 +19,12 @@ Open `http://localhost:1234/`, which will install the Service Worker. You can
then browse into the filesystem via `http://localhost:1234/fs/*`, where `/*` is
a path into the filesystem.

To get metadata about files/directories vs. contents, add `?json` to the URL.
For example: `http://localhost:1234/fs/dir?json`

To download instead of view files in the browser, add `?download` or `?dl` to the URL.
For example: `http://localhost:1234/fs/path/to/file.png?dl`

NOTE: I don't currently have a demo up, so the filesystem is empty. My plan
is to rework this into a module you can include along with Filer to allow
self-hosting of static files in the browser.
10 changes: 5 additions & 5 deletions src/content-type.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,9 @@
const mime = require('mime-types');

function getMimeType(path) {
return mime.lookup(path) || 'application/octet-stream';
}

// https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types#Audio_and_video_types
function isMedia(path) {
let mimeType = mime.lookup(path);
Expand All @@ -14,7 +18,7 @@ function isMedia(path) {
return true;
}

// Any thing else with `audio/*` or `video/*` is "media"
// Anything else with `audio/*` or `video/*` is "media"
return mimeType.startsWith('audio/') || mimeType.startsWith('video/');
}

Expand All @@ -28,10 +32,6 @@ function isImage(path) {
return mimeType.toLowerCase().startsWith('image/');
}

function getMimeType(path) {
return mime.lookup(path) || 'application/octet-stream';
}

module.exports = {
isMedia,
isImage,
Expand Down
36 changes: 24 additions & 12 deletions src/html-formatter.js
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ const formatRow = (
<td align='right'>${size}</td><td>&nbsp;</td></tr>`;
};

const footerClose = '<address>nohost (Web)</address></body></html>';
const footerClose = '<address>nohost (Web Browser Server)</address></body></html>';

/**
* Send an Apache-style 404
Expand All @@ -62,8 +62,11 @@ function format404(url) {

return {
body,
type: 'text/html',
status: 404
config: {
status: 404,
statusText: 'Not Found',
headers: { 'Content-Type': 'text/html' }
}
};
}

Expand All @@ -83,16 +86,19 @@ function format500(path, err) {

return {
body,
type: 'text/html',
status: 500
config: {
status: 500,
statusText: 'Internal Error',
headers: { 'Content-Type': 'text/html' }
}
};
}

/**
* Send an Apache-style directory listing
*/
function formatDir(route, dirPath, entries) {
const parent = path.dirname(dirPath);
const parent = path.dirname(dirPath) || '/';
const header = `
<!DOCTYPE html>
<html><head><title>Index of ${dirPath}</title></head>
Expand All @@ -102,7 +108,7 @@ function formatDir(route, dirPath, entries) {
<th><b>Size</b></th><th><b>Description</b></th></tr>
<tr><th colspan='5'><hr></th></tr>
<tr><td valign='top'><img src='${iconBack}' alt='[DIR]'></td>
<td><a href='/www${parent}'>Parent Directory</a></td><td>&nbsp;</td>
<td><a href='/${route}${parent}'>Parent Directory</a></td><td>&nbsp;</td>
<td align='right'> - </td><td>&nbsp;</td></tr>`;
const footer = `<tr><th colspan='5'><hr></th></tr></table>${footerClose}`;

Expand Down Expand Up @@ -132,17 +138,23 @@ function formatDir(route, dirPath, entries) {
}).join('\n');

return {
type: 'text/html',
status: 200,
body: header + rows + footer
body: header + rows + footer,
config: {
status: 200,
statusText: 'OK',
headers: { 'Content-Type': 'text/html' }
}
};
}

function formatFile(path, content) {
return {
body: content,
type: getMimeType(path),
status: 200
config: {
status: 200,
statusText: 'OK',
headers: { 'Content-Type': getMimeType(path) }
}
};
}

Expand Down
28 changes: 20 additions & 8 deletions src/json-formatter.js
Original file line number Diff line number Diff line change
@@ -1,32 +1,44 @@
function format404(url) {
return {
body: `The requested URL ${url} was not found on this server.`,
type: 'application/json',
status: 404
config: {
status: 404,
statusText: 'Not Found',
headers: { 'Content-Type': 'application/json' }
}
};
}

function format500(path, err) {
return {
body: `Internal Server Error accessing ${path}: ${err.message}`,
type: 'application/json',
status: 500
config: {
status: 500,
statusText: 'Not Found',
headers: { 'Content-Type': 'application/json' }
}
};
}

function formatDir(route, path, entries) {
return {
body: JSON.stringify(entries),
type: 'application/json',
status: 200
config: {
status: 200,
statusText: 'OK',
headers: { 'Content-Type': 'application/json' }
}
};
}

function formatFile(path, contents, stats) {
return {
type: 'application/json',
body: JSON.stringify(stats),
status: 200
config: {
status: 200,
statusText: 'OK',
headers: { 'Content-Type': 'application/json' }
}
};
}

Expand Down
21 changes: 8 additions & 13 deletions src/nohost-sw.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ workbox.skipWaiting();
workbox.clientsClaim();

function install(route) {
const webServer = new WebServer();
const webServer = new WebServer(route);
const wwwRegex = new RegExp(`/${route}(/.*)`);

workbox.routing.registerRoute(
Expand All @@ -19,23 +19,18 @@ function install(route) {
// Pull the filesystem path off the url
const path = url.pathname.match(wwwRegex)[1];

// Allow passing ?json on URL to get back JSON vs. raw response
// Allow passing `?json` on URL to get back JSON vs. raw response
const formatter =
url.searchParams.get('json') !== null
? jsonFormatter
: htmlFormatter;

return new Promise((resolve, reject) => {
webServer.serve(path, formatter)
.then((res) => {
resolve(new Response(res.body, {
status: res.status,
statusText: 'OK',
headers: { 'Content-Type': res.type },
}));
})
.catch(reject);
});
// Allow passing `?download` or `dl` to have the file downloaded vs. displayed
const download =
url.searchParams.get('download') !== null ||
url.searchParams.get('dl') !== null;

return webServer.serve(path, formatter, download);
},
'GET'
);
Expand Down
40 changes: 30 additions & 10 deletions src/webserver.js
Original file line number Diff line number Diff line change
@@ -1,53 +1,73 @@
'use strict';

const { fs } = require('filer');
const { fs, Path } = require('filer');
const sh = new fs.Shell();

// https://tools.ietf.org/html/rfc2183
function formatContentDisposition(path, stats) {
const filename = Path.basename(path);
const modified = stats.mtime.toUTCString();
return `attachment; filename="${filename}"; modification-date="${modified}"; size=${stats.size};`;
}

function WebServer(route) {
this.route = route;
}
WebServer.prototype.serve = (path, formatter) => {
WebServer.prototype.serve = function(path, formatter, download) {
const route = this.route;

return new Promise((resolve) => {
function buildResponse(responseData) {
return new Response(responseData.body, responseData.config);
}

function serveError(path, err) {
if(err.code === 'ENOENT') {
return resolve(formatter.format404(path, err));
return resolve(buildResponse(formatter.format404(path, err)));
}
resolve(formatter.format500(path, err));
resolve(buildResponse(formatter.format500(path, err)));
}

function serveFile(path, stats) {
fs.readFile(path, (err, contents) => {
if(err) {
return resolve(serveError(path, err));
return serveError(path, err);
}

const responseData = formatter.formatFile(path, contents, stats);

// If we are supposed to serve this file or download, add headers
if(responseData.config.status === 200 && download) {
responseData.config.headers['Content-Disposition'] =
formatContentDisposition(path, stats);
}

resolve(formatter.formatFile(path, contents, stats));
resolve(new Response(responseData.body, responseData.config));
});
}

function serveDir(path) {
sh.ls(path, (err, entries) => {
if(err) {
return resolve(serveError(path, err));
return serveError(path, err);
}

resolve(formatter.formatDir(route, path, entries));
const responseData = formatter.formatDir(route, path, entries);
resolve(new Response(responseData.body, responseData.config));
});
}

fs.stat(path, (err, stats) => {
if(err) {
return resolve(serveError(path, err));
return serveError(path, err);
}

if(stats.isDirectory()) {
serveDir(path);
} else {
serveFile(path, stats);
}
});
});
});
};

Expand Down

0 comments on commit 0c003af

Please sign in to comment.