initial commit
|
@ -0,0 +1,41 @@
|
||||||
|
Copyright 2018 AJ ONeal
|
||||||
|
|
||||||
|
This is open source software; you can redistribute it and/or modify it under the
|
||||||
|
terms of either:
|
||||||
|
|
||||||
|
a) the "MIT License"
|
||||||
|
b) the "Apache-2.0 License"
|
||||||
|
|
||||||
|
MIT License
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||||
|
of this software and associated documentation files (the "Software"), to deal
|
||||||
|
in the Software without restriction, including without limitation the rights
|
||||||
|
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||||
|
copies of the Software, and to permit persons to whom the Software is
|
||||||
|
furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all
|
||||||
|
copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||||
|
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||||
|
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||||
|
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||||
|
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||||
|
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||||
|
SOFTWARE.
|
||||||
|
|
||||||
|
Apache-2.0 License Summary
|
||||||
|
|
||||||
|
Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
you may not use this file except in compliance with the License.
|
||||||
|
You may obtain a copy of the License at
|
||||||
|
|
||||||
|
http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
|
||||||
|
Unless required by applicable law or agreed to in writing, software
|
||||||
|
distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
See the License for the specific language governing permissions and
|
||||||
|
limitations under the License.
|
|
@ -0,0 +1,46 @@
|
||||||
|
serve-tpl-download
|
||||||
|
==================
|
||||||
|
|
||||||
|
A fork of the original `serve-index` template that, in combination with `serve-static`,
|
||||||
|
provides support for direct file downloads.
|
||||||
|
|
||||||
|
```js
|
||||||
|
var express = require('express');
|
||||||
|
var app = express();
|
||||||
|
|
||||||
|
var serveIndex = require('serve-index');
|
||||||
|
var serveTpl = require('serve-tpl-download');
|
||||||
|
var serveDirs = serveIndex({ template: serveTpl() });
|
||||||
|
|
||||||
|
app.use('/', function (req, res, next) {
|
||||||
|
|
||||||
|
// enable direct downloads for express.static()
|
||||||
|
if (req.query.download) {
|
||||||
|
res.setHeader('Content-Type', 'application/octet-stream');
|
||||||
|
res.setHeader('Content-Disposition', 'attachment; filename="'+path.basename(req.url)+'"');
|
||||||
|
}
|
||||||
|
express.static('./public')(req, res, function () {
|
||||||
|
serveDirs(req, res, next);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
```
|
||||||
|
|
||||||
|
Additional Options
|
||||||
|
==================
|
||||||
|
|
||||||
|
### privatefiles
|
||||||
|
|
||||||
|
As an additional security precaution you can ignore files which are not world-readable.
|
||||||
|
|
||||||
|
For example, this would prevent files in a `~/.ssh` from being read even when `dotfiles` are allowed.
|
||||||
|
|
||||||
|
`{ privatefiles: 'ignore' }`
|
||||||
|
|
||||||
|
```js
|
||||||
|
var serveTpl = require('serve-tpl-download');
|
||||||
|
|
||||||
|
var serveTemplate = serveTpl({ privatefiles: 'ignore' })
|
||||||
|
```
|
||||||
|
|
||||||
|
This is most effective on Unix-based systems (macOS, Linux, Android).
|
||||||
|
Windows may rely on ACLs instead of user-group-other style permissions.
|
|
@ -0,0 +1,456 @@
|
||||||
|
/*!
|
||||||
|
* telebit/serve-index
|
||||||
|
* Copyright(c) 2018 AJ ONeal
|
||||||
|
*
|
||||||
|
* Derivative work of github.com/expressjs/serve-index
|
||||||
|
* Copyright(c) 2011 Sencha Inc.
|
||||||
|
* Copyright(c) 2011 TJ Holowaychuk
|
||||||
|
* Copyright(c) 2014-2015 Douglas Christopher Wilson
|
||||||
|
* MIT Licensed
|
||||||
|
*/
|
||||||
|
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module dependencies.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
var escapeHtml = require('escape-html');
|
||||||
|
var fs = require('fs')
|
||||||
|
, path = require('path')
|
||||||
|
, normalize = path.normalize
|
||||||
|
, sep = path.sep
|
||||||
|
, extname = path.extname
|
||||||
|
, join = path.join;
|
||||||
|
var Batch = require('batch');
|
||||||
|
var mime = require('mime-types');
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Module exports.
|
||||||
|
* @public
|
||||||
|
*/
|
||||||
|
|
||||||
|
var defaultPagepath = path.join(__dirname, 'public/directory.html');
|
||||||
|
var defaultStylepath = path.join(__dirname, 'public/style.css');
|
||||||
|
var defaultList = '<ul id="files" class="view-{view}">{head}{files}</ul>';
|
||||||
|
var defaultHead = '<li class="header">'
|
||||||
|
+ '<span class="name">Name</span>'
|
||||||
|
+ '<span class="size">Size</span>'
|
||||||
|
+ '<span class="date">Modified</span>'
|
||||||
|
+ '</li>'
|
||||||
|
;
|
||||||
|
var defaultFile = '<li><div>'
|
||||||
|
+ '<a href="{path}?download=true" class="download" title="Download {file.name}">'
|
||||||
|
+ '<span class="download">⬇️</span>'
|
||||||
|
+ '</a>'
|
||||||
|
+ '<a href="{path}" class="{classes}" title="{file.name}">'
|
||||||
|
+ '<span class="name">{file.name}</span>'
|
||||||
|
+ '<span class="size">{file.size}</span>'
|
||||||
|
+ '<span class="date">{file.date}</span>'
|
||||||
|
+ '</a>'
|
||||||
|
+ '</div></li>'
|
||||||
|
;
|
||||||
|
module.exports = function (opts) {
|
||||||
|
if (!opts) { opts = {}; }
|
||||||
|
return createHtmlRender({
|
||||||
|
pagepath: opts.pagepath || defaultPagepath
|
||||||
|
, stylepath: opts.stylepath || defaultStylepath
|
||||||
|
, list: opts.list || defaultList
|
||||||
|
, head: opts.head || defaultHead
|
||||||
|
, file: opts.file || defaultFile
|
||||||
|
, privatefiles: opts.privatefiles
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
/*!
|
||||||
|
* Icon cache.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var cache = {};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map html `files`, returning an html unordered list.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function createHtmlFileList(opts, files, dir, useIcons, view) {
|
||||||
|
var ftpls = files.map(function (file) {
|
||||||
|
var classes = [];
|
||||||
|
var isDir = file.stat && file.stat.isDirectory();
|
||||||
|
var path = dir.split('/').map(function (c) { return encodeURIComponent(c); });
|
||||||
|
|
||||||
|
if (useIcons) {
|
||||||
|
classes.push('icon');
|
||||||
|
|
||||||
|
if (isDir) {
|
||||||
|
classes.push('icon-directory');
|
||||||
|
} else {
|
||||||
|
var ext = extname(file.name);
|
||||||
|
var icon = iconLookup(file.name);
|
||||||
|
|
||||||
|
classes.push('icon');
|
||||||
|
classes.push('icon-' + ext.substring(1));
|
||||||
|
|
||||||
|
if (classes.indexOf(icon.className) === -1) {
|
||||||
|
classes.push(icon.className);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
path.push(encodeURIComponent(file.name));
|
||||||
|
|
||||||
|
var date = file.stat && file.name !== '..'
|
||||||
|
? file.stat.mtime.toLocaleDateString() + ' ' + file.stat.mtime.toLocaleTimeString()
|
||||||
|
: '';
|
||||||
|
var size = file.stat && !isDir
|
||||||
|
? file.stat.size
|
||||||
|
: '';
|
||||||
|
var OCTAL = 8;
|
||||||
|
var WORLD_READ = parseInt(4, OCTAL); // R(4)W(2)X(1)
|
||||||
|
var hasWorldRead = file.mode | WORLD_READ;
|
||||||
|
|
||||||
|
if (!hasWorldRead && 'ignore' === opts.privatefiles) {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
return opts.file.replace(/{path}/g, escapeHtml(normalizeSlashes(normalize(path.join('/')))))
|
||||||
|
.replace(/{classes}/g, escapeHtml(classes.join(' ')))
|
||||||
|
.replace(/{file.name}/g, escapeHtml(file.name))
|
||||||
|
.replace(/{file.size}/g, escapeHtml(size))
|
||||||
|
.replace(/{file.date}/g, escapeHtml(date))
|
||||||
|
;
|
||||||
|
}).filter(Boolean).join('\n');
|
||||||
|
|
||||||
|
var html = opts.list
|
||||||
|
.replace(/{view}/g, escapeHtml(view))
|
||||||
|
.replace(/{head}/g, view === 'details' ? opts.head : '')
|
||||||
|
.replace(/{files}/g, ftpls)
|
||||||
|
;
|
||||||
|
|
||||||
|
return html;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create function to render html.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function createHtmlRender(opts) {
|
||||||
|
return function render(locals, callback) {
|
||||||
|
// read template
|
||||||
|
fs.readFile(opts.pagepath, 'utf8', function (err, pageStr) {
|
||||||
|
fs.readFile(opts.stylepath, 'utf8', function (err, styleStr) {
|
||||||
|
if (err) return callback(err);
|
||||||
|
|
||||||
|
var body = pageStr
|
||||||
|
.replace(/\{style\}/g, styleStr.concat(iconStyle(locals.fileList, locals.displayIcons)))
|
||||||
|
.replace(/\{files\}/g, createHtmlFileList(opts, locals.fileList, locals.directory, locals.displayIcons, locals.viewName))
|
||||||
|
.replace(/\{directory\}/g, escapeHtml(locals.directory))
|
||||||
|
.replace(/\{linked-path\}/g, htmlPath(locals.directory))
|
||||||
|
;
|
||||||
|
|
||||||
|
callback(null, body);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map html `dir`, returning a linked path.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function htmlPath(dir) {
|
||||||
|
var parts = dir.split('/');
|
||||||
|
var crumb = new Array(parts.length);
|
||||||
|
|
||||||
|
for (var i = 0; i < parts.length; i++) {
|
||||||
|
var part = parts[i];
|
||||||
|
|
||||||
|
if (part) {
|
||||||
|
parts[i] = encodeURIComponent(part);
|
||||||
|
crumb[i] = '<a href="' + escapeHtml(parts.slice(0, i + 1).join('/')) + '">' + escapeHtml(part) + '</a>';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return crumb.join(' / ');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the icon data for the file name.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function iconLookup(filename) {
|
||||||
|
var ext = extname(filename);
|
||||||
|
|
||||||
|
// try by extension
|
||||||
|
if (icons[ext]) {
|
||||||
|
return {
|
||||||
|
className: 'icon-' + ext.substring(1),
|
||||||
|
fileName: icons[ext]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var mimetype = mime.lookup(ext);
|
||||||
|
|
||||||
|
// default if no mime type
|
||||||
|
if (mimetype === false) {
|
||||||
|
return {
|
||||||
|
className: 'icon-default',
|
||||||
|
fileName: icons.default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// try by mime type
|
||||||
|
if (icons[mimetype]) {
|
||||||
|
return {
|
||||||
|
className: 'icon-' + mimetype.replace('/', '-'),
|
||||||
|
fileName: icons[mimetype]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var suffix = mimetype.split('+')[1];
|
||||||
|
|
||||||
|
if (suffix && icons['+' + suffix]) {
|
||||||
|
return {
|
||||||
|
className: 'icon-' + suffix,
|
||||||
|
fileName: icons['+' + suffix]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
var type = mimetype.split('/')[0];
|
||||||
|
|
||||||
|
// try by type only
|
||||||
|
if (icons[type]) {
|
||||||
|
return {
|
||||||
|
className: 'icon-' + type,
|
||||||
|
fileName: icons[type]
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
className: 'icon-default',
|
||||||
|
fileName: icons.default
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load icon images, return css string.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function iconStyle(files, useIcons) {
|
||||||
|
if (!useIcons) return '';
|
||||||
|
var i;
|
||||||
|
var list = [];
|
||||||
|
var rules = {};
|
||||||
|
var selector;
|
||||||
|
var selectors = {};
|
||||||
|
var style = '';
|
||||||
|
|
||||||
|
for (i = 0; i < files.length; i++) {
|
||||||
|
var file = files[i];
|
||||||
|
|
||||||
|
var isDir = file.stat && file.stat.isDirectory();
|
||||||
|
var icon = isDir
|
||||||
|
? { className: 'icon-directory', fileName: icons.folder }
|
||||||
|
: iconLookup(file.name);
|
||||||
|
var iconName = icon.fileName;
|
||||||
|
|
||||||
|
selector = '#files .' + icon.className + ' .name';
|
||||||
|
|
||||||
|
if (!rules[iconName]) {
|
||||||
|
rules[iconName] = 'background-image: url(data:image/png;base64,' + load(iconName) + ');'
|
||||||
|
selectors[iconName] = [];
|
||||||
|
list.push(iconName);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (selectors[iconName].indexOf(selector) === -1) {
|
||||||
|
selectors[iconName].push(selector);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (i = 0; i < list.length; i++) {
|
||||||
|
iconName = list[i];
|
||||||
|
style += selectors[iconName].join(',\n') + ' {\n ' + rules[iconName] + '\n}\n';
|
||||||
|
}
|
||||||
|
|
||||||
|
return style;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load and cache the given `icon`.
|
||||||
|
*
|
||||||
|
* @param {String} icon
|
||||||
|
* @return {String}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function load(icon) {
|
||||||
|
if (cache[icon]) return cache[icon];
|
||||||
|
return cache[icon] = fs.readFileSync(__dirname + '/public/icons/' + icon, 'base64');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Normalizes the path separator from system separator
|
||||||
|
* to URL separator, aka `/`.
|
||||||
|
*
|
||||||
|
* @param {String} path
|
||||||
|
* @return {String}
|
||||||
|
* @api private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function normalizeSlashes(path) {
|
||||||
|
return path.split(sep).join('/');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Send a response.
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
|
||||||
|
function send (res, type, body) {
|
||||||
|
// security header for content sniffing
|
||||||
|
res.setHeader('X-Content-Type-Options', 'nosniff');
|
||||||
|
|
||||||
|
// standard headers
|
||||||
|
res.setHeader('Content-Type', type + '; charset=utf-8');
|
||||||
|
res.setHeader('Content-Length', Buffer.byteLength(body, 'utf8'));
|
||||||
|
|
||||||
|
// body
|
||||||
|
res.end(body, 'utf8');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stat all files and return array of stat
|
||||||
|
* in same order.
|
||||||
|
*/
|
||||||
|
|
||||||
|
function stat(dir, files, cb) {
|
||||||
|
var batch = new Batch();
|
||||||
|
|
||||||
|
batch.concurrency(10);
|
||||||
|
|
||||||
|
files.forEach(function(file){
|
||||||
|
batch.push(function(done){
|
||||||
|
fs.stat(join(dir, file), function(err, stat){
|
||||||
|
if (err && err.code !== 'ENOENT') return done(err);
|
||||||
|
|
||||||
|
// pass ENOENT as null stat, not error
|
||||||
|
done(null, stat || null);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
batch.end(cb);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Icon map.
|
||||||
|
*/
|
||||||
|
|
||||||
|
var icons = {
|
||||||
|
// base icons
|
||||||
|
'default': 'page_white.png',
|
||||||
|
'folder': 'folder.png',
|
||||||
|
|
||||||
|
// generic mime type icons
|
||||||
|
'font': 'font.png',
|
||||||
|
'image': 'image.png',
|
||||||
|
'text': 'page_white_text.png',
|
||||||
|
'video': 'film.png',
|
||||||
|
|
||||||
|
// generic mime suffix icons
|
||||||
|
'+json': 'page_white_code.png',
|
||||||
|
'+xml': 'page_white_code.png',
|
||||||
|
'+zip': 'box.png',
|
||||||
|
|
||||||
|
// specific mime type icons
|
||||||
|
'application/javascript': 'page_white_code_red.png',
|
||||||
|
'application/json': 'page_white_code.png',
|
||||||
|
'application/msword': 'page_white_word.png',
|
||||||
|
'application/pdf': 'page_white_acrobat.png',
|
||||||
|
'application/postscript': 'page_white_vector.png',
|
||||||
|
'application/rtf': 'page_white_word.png',
|
||||||
|
'application/vnd.ms-excel': 'page_white_excel.png',
|
||||||
|
'application/vnd.ms-powerpoint': 'page_white_powerpoint.png',
|
||||||
|
'application/vnd.oasis.opendocument.presentation': 'page_white_powerpoint.png',
|
||||||
|
'application/vnd.oasis.opendocument.spreadsheet': 'page_white_excel.png',
|
||||||
|
'application/vnd.oasis.opendocument.text': 'page_white_word.png',
|
||||||
|
'application/x-7z-compressed': 'box.png',
|
||||||
|
'application/x-sh': 'application_xp_terminal.png',
|
||||||
|
'application/x-msaccess': 'page_white_database.png',
|
||||||
|
'application/x-shockwave-flash': 'page_white_flash.png',
|
||||||
|
'application/x-sql': 'page_white_database.png',
|
||||||
|
'application/x-tar': 'box.png',
|
||||||
|
'application/x-xz': 'box.png',
|
||||||
|
'application/xml': 'page_white_code.png',
|
||||||
|
'application/zip': 'box.png',
|
||||||
|
'image/svg+xml': 'page_white_vector.png',
|
||||||
|
'text/css': 'page_white_code.png',
|
||||||
|
'text/html': 'page_white_code.png',
|
||||||
|
'text/less': 'page_white_code.png',
|
||||||
|
|
||||||
|
// other, extension-specific icons
|
||||||
|
'.accdb': 'page_white_database.png',
|
||||||
|
'.apk': 'box.png',
|
||||||
|
'.app': 'application_xp.png',
|
||||||
|
'.as': 'page_white_actionscript.png',
|
||||||
|
'.asp': 'page_white_code.png',
|
||||||
|
'.aspx': 'page_white_code.png',
|
||||||
|
'.bat': 'application_xp_terminal.png',
|
||||||
|
'.bz2': 'box.png',
|
||||||
|
'.c': 'page_white_c.png',
|
||||||
|
'.cab': 'box.png',
|
||||||
|
'.cfm': 'page_white_coldfusion.png',
|
||||||
|
'.clj': 'page_white_code.png',
|
||||||
|
'.cc': 'page_white_cplusplus.png',
|
||||||
|
'.cgi': 'application_xp_terminal.png',
|
||||||
|
'.cpp': 'page_white_cplusplus.png',
|
||||||
|
'.cs': 'page_white_csharp.png',
|
||||||
|
'.db': 'page_white_database.png',
|
||||||
|
'.dbf': 'page_white_database.png',
|
||||||
|
'.deb': 'box.png',
|
||||||
|
'.dll': 'page_white_gear.png',
|
||||||
|
'.dmg': 'drive.png',
|
||||||
|
'.docx': 'page_white_word.png',
|
||||||
|
'.erb': 'page_white_ruby.png',
|
||||||
|
'.exe': 'application_xp.png',
|
||||||
|
'.fnt': 'font.png',
|
||||||
|
'.gam': 'controller.png',
|
||||||
|
'.gz': 'box.png',
|
||||||
|
'.h': 'page_white_h.png',
|
||||||
|
'.ini': 'page_white_gear.png',
|
||||||
|
'.iso': 'cd.png',
|
||||||
|
'.jar': 'box.png',
|
||||||
|
'.java': 'page_white_cup.png',
|
||||||
|
'.jsp': 'page_white_cup.png',
|
||||||
|
'.lua': 'page_white_code.png',
|
||||||
|
'.lz': 'box.png',
|
||||||
|
'.lzma': 'box.png',
|
||||||
|
'.m': 'page_white_code.png',
|
||||||
|
'.map': 'map.png',
|
||||||
|
'.msi': 'box.png',
|
||||||
|
'.mv4': 'film.png',
|
||||||
|
'.pdb': 'page_white_database.png',
|
||||||
|
'.php': 'page_white_php.png',
|
||||||
|
'.pl': 'page_white_code.png',
|
||||||
|
'.pkg': 'box.png',
|
||||||
|
'.pptx': 'page_white_powerpoint.png',
|
||||||
|
'.psd': 'page_white_picture.png',
|
||||||
|
'.py': 'page_white_code.png',
|
||||||
|
'.rar': 'box.png',
|
||||||
|
'.rb': 'page_white_ruby.png',
|
||||||
|
'.rm': 'film.png',
|
||||||
|
'.rom': 'controller.png',
|
||||||
|
'.rpm': 'box.png',
|
||||||
|
'.sass': 'page_white_code.png',
|
||||||
|
'.sav': 'controller.png',
|
||||||
|
'.scss': 'page_white_code.png',
|
||||||
|
'.srt': 'page_white_text.png',
|
||||||
|
'.tbz2': 'box.png',
|
||||||
|
'.tgz': 'box.png',
|
||||||
|
'.tlz': 'box.png',
|
||||||
|
'.vb': 'page_white_code.png',
|
||||||
|
'.vbs': 'page_white_code.png',
|
||||||
|
'.xcf': 'page_white_picture.png',
|
||||||
|
'.xlsx': 'page_white_excel.png',
|
||||||
|
'.yaws': 'page_white_code.png'
|
||||||
|
};
|
|
@ -0,0 +1,20 @@
|
||||||
|
{
|
||||||
|
"name": "serve-tpl-download",
|
||||||
|
"version": "1.0.0",
|
||||||
|
"description": "A template for serve-static with a direct download option (requires serve-index)",
|
||||||
|
"homepage": "https://git.coolaj86.com/coolaj86/serve-tpl-download.js",
|
||||||
|
"main": "index.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1"
|
||||||
|
},
|
||||||
|
"repository": {
|
||||||
|
"type": "git",
|
||||||
|
"url": "https://git.coolaj86.com/coolaj86/serve-tpl-download.js.git"
|
||||||
|
},
|
||||||
|
"keywords": [
|
||||||
|
"serve-static",
|
||||||
|
"serve-index"
|
||||||
|
],
|
||||||
|
"author": "AJ ONeal <coolaj86@gmail.com> (https://coolaj86.com/)",
|
||||||
|
"license": "(MIT OR Apache-2.0)"
|
||||||
|
}
|
|
@ -0,0 +1,97 @@
|
||||||
|
<!DOCTYPE html>
|
||||||
|
<html>
|
||||||
|
<head>
|
||||||
|
<meta charset='utf-8'>
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no" />
|
||||||
|
<title>listing directory {directory}</title>
|
||||||
|
<style>{style}</style>
|
||||||
|
<script>
|
||||||
|
function $(id){
|
||||||
|
var el = 'string' == typeof id
|
||||||
|
? document.getElementById(id)
|
||||||
|
: id;
|
||||||
|
|
||||||
|
el.on = function(event, fn){
|
||||||
|
if ('content loaded' == event) {
|
||||||
|
event = window.attachEvent ? "load" : "DOMContentLoaded";
|
||||||
|
}
|
||||||
|
el.addEventListener
|
||||||
|
? el.addEventListener(event, fn, false)
|
||||||
|
: el.attachEvent("on" + event, fn);
|
||||||
|
};
|
||||||
|
|
||||||
|
el.all = function(selector){
|
||||||
|
return $(el.querySelectorAll(selector));
|
||||||
|
};
|
||||||
|
|
||||||
|
el.each = function(fn){
|
||||||
|
for (var i = 0, len = el.length; i < len; ++i) {
|
||||||
|
fn($(el[i]), i);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
el.getClasses = function(){
|
||||||
|
return this.getAttribute('class').split(/\s+/);
|
||||||
|
};
|
||||||
|
|
||||||
|
el.addClass = function(name){
|
||||||
|
var classes = this.getAttribute('class');
|
||||||
|
el.setAttribute('class', classes
|
||||||
|
? classes + ' ' + name
|
||||||
|
: name);
|
||||||
|
};
|
||||||
|
|
||||||
|
el.removeClass = function(name){
|
||||||
|
var classes = this.getClasses().filter(function(curr){
|
||||||
|
return curr != name;
|
||||||
|
});
|
||||||
|
this.setAttribute('class', classes.join(' '));
|
||||||
|
};
|
||||||
|
|
||||||
|
return el;
|
||||||
|
}
|
||||||
|
|
||||||
|
function search() {
|
||||||
|
var str = $('search').value.toLowerCase();
|
||||||
|
var links = $('files').all('a');
|
||||||
|
|
||||||
|
links.each(function(link){
|
||||||
|
var text = link.textContent.toLowerCase();
|
||||||
|
|
||||||
|
if ('..' == text) return;
|
||||||
|
if (str.length && ~text.indexOf(str)) {
|
||||||
|
link.addClass('highlight');
|
||||||
|
} else {
|
||||||
|
link.removeClass('highlight');
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
$(window).on('content loaded', function(){
|
||||||
|
$('search').on('keyup', search);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</head>
|
||||||
|
<body class="directory">
|
||||||
|
<input id="search" type="text" placeholder="Search" autocomplete="off" />
|
||||||
|
<div id="wrapper">
|
||||||
|
<h1><a href="/">~</a>{linked-path}</h1>
|
||||||
|
{files}
|
||||||
|
</div>
|
||||||
|
<script>
|
||||||
|
'use strict';
|
||||||
|
|
||||||
|
window.fetch(document.location, {
|
||||||
|
method: 'GET'
|
||||||
|
, cache: 'no-store'
|
||||||
|
, headers: {
|
||||||
|
'Accept': 'application/json' //; charset=utf-8
|
||||||
|
}
|
||||||
|
}).then(function (resp) {
|
||||||
|
return resp.json().then(function (files) {
|
||||||
|
console.log(files);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
</body>
|
||||||
|
</html>
|
After Width: | Height: | Size: 426 B |
After Width: | Height: | Size: 507 B |
After Width: | Height: | Size: 555 B |
After Width: | Height: | Size: 673 B |
After Width: | Height: | Size: 666 B |
After Width: | Height: | Size: 346 B |
After Width: | Height: | Size: 653 B |
After Width: | Height: | Size: 634 B |
After Width: | Height: | Size: 567 B |
After Width: | Height: | Size: 516 B |
After Width: | Height: | Size: 804 B |
After Width: | Height: | Size: 635 B |
After Width: | Height: | Size: 739 B |
After Width: | Height: | Size: 794 B |
After Width: | Height: | Size: 818 B |
After Width: | Height: | Size: 663 B |
After Width: | Height: | Size: 740 B |
After Width: | Height: | Size: 807 B |
After Width: | Height: | Size: 793 B |
After Width: | Height: | Size: 817 B |
After Width: | Height: | Size: 879 B |
After Width: | Height: | Size: 833 B |
After Width: | Height: | Size: 779 B |
After Width: | Height: | Size: 621 B |
After Width: | Height: | Size: 801 B |
After Width: | Height: | Size: 839 B |
After Width: | Height: | Size: 830 B |
After Width: | Height: | Size: 813 B |
After Width: | Height: | Size: 703 B |
After Width: | Height: | Size: 641 B |
After Width: | Height: | Size: 858 B |
After Width: | Height: | Size: 774 B |
After Width: | Height: | Size: 294 B |
After Width: | Height: | Size: 591 B |
After Width: | Height: | Size: 664 B |
After Width: | Height: | Size: 512 B |
After Width: | Height: | Size: 587 B |
After Width: | Height: | Size: 656 B |
After Width: | Height: | Size: 666 B |
After Width: | Height: | Size: 603 B |
After Width: | Height: | Size: 587 B |
After Width: | Height: | Size: 592 B |
After Width: | Height: | Size: 724 B |
After Width: | Height: | Size: 309 B |
After Width: | Height: | Size: 621 B |
After Width: | Height: | Size: 700 B |
After Width: | Height: | Size: 639 B |
After Width: | Height: | Size: 579 B |
After Width: | Height: | Size: 536 B |
After Width: | Height: | Size: 638 B |
After Width: | Height: | Size: 618 B |
After Width: | Height: | Size: 623 B |
After Width: | Height: | Size: 663 B |
After Width: | Height: | Size: 676 B |
After Width: | Height: | Size: 582 B |
After Width: | Height: | Size: 639 B |
After Width: | Height: | Size: 402 B |
After Width: | Height: | Size: 516 B |
After Width: | Height: | Size: 612 B |
After Width: | Height: | Size: 603 B |
After Width: | Height: | Size: 296 B |
After Width: | Height: | Size: 616 B |
After Width: | Height: | Size: 669 B |
After Width: | Height: | Size: 614 B |
After Width: | Height: | Size: 554 B |
After Width: | Height: | Size: 706 B |
After Width: | Height: | Size: 779 B |
After Width: | Height: | Size: 688 B |
After Width: | Height: | Size: 618 B |
After Width: | Height: | Size: 620 B |
After Width: | Height: | Size: 538 B |
After Width: | Height: | Size: 650 B |
After Width: | Height: | Size: 588 B |
After Width: | Height: | Size: 523 B |
After Width: | Height: | Size: 626 B |
After Width: | Height: | Size: 317 B |
After Width: | Height: | Size: 565 B |
After Width: | Height: | Size: 634 B |
After Width: | Height: | Size: 342 B |
After Width: | Height: | Size: 315 B |
After Width: | Height: | Size: 668 B |
After Width: | Height: | Size: 644 B |
After Width: | Height: | Size: 702 B |
After Width: | Height: | Size: 309 B |
After Width: | Height: | Size: 651 B |
After Width: | Height: | Size: 734 B |
After Width: | Height: | Size: 613 B |
After Width: | Height: | Size: 386 B |
After Width: | Height: | Size: 777 B |
After Width: | Height: | Size: 903 B |
|
@ -0,0 +1,266 @@
|
||||||
|
* {
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
outline: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
body {
|
||||||
|
padding: 80px 100px;
|
||||||
|
font: 13px "Helvetica Neue", "Lucida Grande", "Arial";
|
||||||
|
background: #ECE9E9 -webkit-gradient(linear, 0% 0%, 0% 100%, from(#fff), to(#ECE9E9));
|
||||||
|
background: #ECE9E9 -moz-linear-gradient(top, #fff, #ECE9E9);
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
color: #555;
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
}
|
||||||
|
h1, h2, h3 {
|
||||||
|
font-size: 22px;
|
||||||
|
color: #343434;
|
||||||
|
}
|
||||||
|
h1 em, h2 em {
|
||||||
|
padding: 0 5px;
|
||||||
|
font-weight: normal;
|
||||||
|
}
|
||||||
|
h1 {
|
||||||
|
font-size: 60px;
|
||||||
|
}
|
||||||
|
h2 {
|
||||||
|
margin-top: 10px;
|
||||||
|
}
|
||||||
|
h3 {
|
||||||
|
margin: 5px 0 10px 0;
|
||||||
|
padding-bottom: 5px;
|
||||||
|
border-bottom: 1px solid #eee;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
ul li {
|
||||||
|
list-style: none;
|
||||||
|
}
|
||||||
|
ul li:hover {
|
||||||
|
cursor: pointer;
|
||||||
|
color: #2e2e2e;
|
||||||
|
}
|
||||||
|
ul li .path {
|
||||||
|
padding-left: 5px;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
ul li .line {
|
||||||
|
padding-right: 5px;
|
||||||
|
font-style: italic;
|
||||||
|
}
|
||||||
|
ul li:first-child .path {
|
||||||
|
padding-left: 0;
|
||||||
|
}
|
||||||
|
p {
|
||||||
|
line-height: 1.5;
|
||||||
|
}
|
||||||
|
a {
|
||||||
|
color: #555;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
a:hover {
|
||||||
|
color: #303030;
|
||||||
|
}
|
||||||
|
#stacktrace {
|
||||||
|
margin-top: 15px;
|
||||||
|
}
|
||||||
|
.directory h1 {
|
||||||
|
margin-bottom: 15px;
|
||||||
|
font-size: 18px;
|
||||||
|
}
|
||||||
|
ul#files {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
ul#files li {
|
||||||
|
float: left;
|
||||||
|
width: 30%;
|
||||||
|
line-height: 25px;
|
||||||
|
margin: 1px;
|
||||||
|
}
|
||||||
|
ul#files li div {
|
||||||
|
display: block;
|
||||||
|
height: 25px;
|
||||||
|
border: 1px solid transparent;
|
||||||
|
-webkit-border-radius: 5px;
|
||||||
|
-moz-border-radius: 5px;
|
||||||
|
border-radius: 5px;
|
||||||
|
overflow: hidden;
|
||||||
|
white-space: nowrap;
|
||||||
|
}
|
||||||
|
ul#files li div:focus,
|
||||||
|
ul#files li div:hover {
|
||||||
|
background: rgba(255,255,255,0.65);
|
||||||
|
border: 1px solid #ececec;
|
||||||
|
}
|
||||||
|
ul#files li div.highlight {
|
||||||
|
-webkit-transition: background .4s ease-in-out;
|
||||||
|
background: #ffff4f;
|
||||||
|
border-color: #E9DC51;
|
||||||
|
}
|
||||||
|
#search {
|
||||||
|
display: block;
|
||||||
|
position: fixed;
|
||||||
|
top: 20px;
|
||||||
|
right: 20px;
|
||||||
|
width: 90px;
|
||||||
|
-webkit-transition: width ease 0.2s, opacity ease 0.4s;
|
||||||
|
-moz-transition: width ease 0.2s, opacity ease 0.4s;
|
||||||
|
-webkit-border-radius: 32px;
|
||||||
|
-moz-border-radius: 32px;
|
||||||
|
-webkit-box-shadow: inset 0px 0px 3px rgba(0, 0, 0, 0.25), inset 0px 1px 3px rgba(0, 0, 0, 0.7), 0px 1px 0px rgba(255, 255, 255, 0.03);
|
||||||
|
-moz-box-shadow: inset 0px 0px 3px rgba(0, 0, 0, 0.25), inset 0px 1px 3px rgba(0, 0, 0, 0.7), 0px 1px 0px rgba(255, 255, 255, 0.03);
|
||||||
|
-webkit-font-smoothing: antialiased;
|
||||||
|
text-align: left;
|
||||||
|
font: 13px "Helvetica Neue", Arial, sans-serif;
|
||||||
|
padding: 4px 10px;
|
||||||
|
border: none;
|
||||||
|
background: transparent;
|
||||||
|
margin-bottom: 0;
|
||||||
|
outline: none;
|
||||||
|
opacity: 0.7;
|
||||||
|
color: #888;
|
||||||
|
}
|
||||||
|
#search:focus {
|
||||||
|
width: 120px;
|
||||||
|
opacity: 1.0;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*views*/
|
||||||
|
#files span {
|
||||||
|
display: inline-block;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
text-indent: 10px;
|
||||||
|
}
|
||||||
|
#files .name {
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
}
|
||||||
|
#files .icon .name {
|
||||||
|
text-indent: 28px;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*tiles*/
|
||||||
|
.view-tiles .download {
|
||||||
|
background-position: 8px 5px;
|
||||||
|
}
|
||||||
|
.view-tiles .name {
|
||||||
|
width: 100%;
|
||||||
|
background-position: 8px 5px;
|
||||||
|
}
|
||||||
|
#files span.size,
|
||||||
|
#files span.date,
|
||||||
|
.view-tiles .size,
|
||||||
|
.view-tiles .date {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*details*/
|
||||||
|
ul#files.view-details li {
|
||||||
|
float: none;
|
||||||
|
display: block;
|
||||||
|
width: 90%;
|
||||||
|
}
|
||||||
|
ul#files.view-details li.header {
|
||||||
|
height: 25px;
|
||||||
|
background: #000;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: bold;
|
||||||
|
}
|
||||||
|
.view-details .header {
|
||||||
|
border-radius: 5px;
|
||||||
|
}
|
||||||
|
.view-details .name {
|
||||||
|
width: 60%;
|
||||||
|
background-position: 8px 5px;
|
||||||
|
}
|
||||||
|
.view-details .size {
|
||||||
|
width: 10%;
|
||||||
|
}
|
||||||
|
.view-details .date {
|
||||||
|
width: 30%;
|
||||||
|
}
|
||||||
|
.view-details .size,
|
||||||
|
.view-details .date {
|
||||||
|
text-align: right;
|
||||||
|
direction: rtl;
|
||||||
|
}
|
||||||
|
|
||||||
|
ul#files li a.download {
|
||||||
|
display: inline;
|
||||||
|
}
|
||||||
|
|
||||||
|
/*mobile*/
|
||||||
|
@media (max-width: 768px) {
|
||||||
|
body {
|
||||||
|
font-size: 13px;
|
||||||
|
line-height: 16px;
|
||||||
|
padding: 0;
|
||||||
|
}
|
||||||
|
#search {
|
||||||
|
position: static;
|
||||||
|
width: 100%;
|
||||||
|
font-size: 2em;
|
||||||
|
line-height: 1.8em;
|
||||||
|
text-indent: 10px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 10px 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
#search:focus {
|
||||||
|
width: 100%;
|
||||||
|
border: 0;
|
||||||
|
opacity: 1;
|
||||||
|
}
|
||||||
|
.directory h1 {
|
||||||
|
font-size: 2em;
|
||||||
|
line-height: 1.5em;
|
||||||
|
color: #fff;
|
||||||
|
background: #000;
|
||||||
|
padding: 15px 10px;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
ul#files {
|
||||||
|
border-top: 1px solid #cacaca;
|
||||||
|
}
|
||||||
|
ul#files li {
|
||||||
|
float: none;
|
||||||
|
width: auto !important;
|
||||||
|
display: block;
|
||||||
|
border-bottom: 1px solid #cacaca;
|
||||||
|
font-size: 2em;
|
||||||
|
line-height: 1.2em;
|
||||||
|
text-indent: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
ul#files li:nth-child(odd) {
|
||||||
|
background: #e0e0e0;
|
||||||
|
}
|
||||||
|
ul#files li div {
|
||||||
|
height: auto;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 0;
|
||||||
|
padding: 15px 10px;
|
||||||
|
}
|
||||||
|
ul#files li a:focus,
|
||||||
|
ul#files li a:hover {
|
||||||
|
border: 0;
|
||||||
|
}
|
||||||
|
#files .header,
|
||||||
|
#files .size,
|
||||||
|
#files .date {
|
||||||
|
display: none !important;
|
||||||
|
}
|
||||||
|
#files .name {
|
||||||
|
float: none;
|
||||||
|
display: inline-block;
|
||||||
|
width: 100%;
|
||||||
|
text-indent: 0;
|
||||||
|
background-position: 0 50%;
|
||||||
|
}
|
||||||
|
#files .icon .name {
|
||||||
|
text-indent: 41px;
|
||||||
|
}
|
||||||
|
}
|