commit 0c793e04b10ccd4fdd2773cbee9b9f107c56e619 Author: Ambrose Chua Date: Sun Oct 9 13:17:00 2016 +0800 Initial commit diff --git a/assets/login.js b/assets/login.js new file mode 100644 index 0000000..9adb610 --- /dev/null +++ b/assets/login.js @@ -0,0 +1,19 @@ +/* jshint esversion: 6 */ + +$(document).ready(() => { + let $token = $("#login-token"); + let cleanup = () => { + let val = $token.val(); + val = val.replace(/[^0-9 ]/, ""); + val = val.split(""); + if (val.length > 3 && val[3] != " ") { + val.splice(3, 0, " "); + } + val.splice(7); + val = val.join(""); + $token.val(val); + }; + $token.on("keyup", cleanup); + $token.on("keydown", cleanup); + $token.on("change", cleanup); +}); diff --git a/assets/multi.js b/assets/multi.js new file mode 100644 index 0000000..e1dfaa5 --- /dev/null +++ b/assets/multi.js @@ -0,0 +1,24 @@ +/* jshint esversion: 6 */ + +$(document).ready(() => { + let $select = $(".multi-select"); + + let setSelected = (files) => { + $(".multi-files-value").val(JSON.stringify(files)); + $(".multi-files").html( + files.map(f => { + return `
  • ${f}
  • `; + }).join("") + ); + }; + + $select.on("change", () => { + let $selected = $(".multi-select:checked"); + let files = []; + $selected.each((i, ele) => { + files.push($(ele).data("select")); + }); + + setSelected(files); + }); +}); diff --git a/assets/upload.css b/assets/upload.css new file mode 100644 index 0000000..8966bd5 --- /dev/null +++ b/assets/upload.css @@ -0,0 +1,7 @@ +.custom-file-control.file-selected:after { + display: none; +} +.custom-file-control { + white-space: nowrap; + text-overflow: ellipsis; +} diff --git a/assets/upload.js b/assets/upload.js new file mode 100644 index 0000000..083a5a1 --- /dev/null +++ b/assets/upload.js @@ -0,0 +1,24 @@ +/* jshint esversion: 6 */ + +$(document).ready(() => { + let $form = $("form[action='@upload']"); + let $file = $("#upload-file"); + + $(".upload-unhide").fadeOut(); + + $file.on("change", () => { + let file = $file[0].files[0]; + let fnElement = $file.parent().find(".custom-file-control"); + fnElement.addClass("file-selected"); + fnElement.text(file.name); + + $form.find("#upload-file-size").val(filesize(file.size)); + $form.find("[name=saveas]").val(file.name); + $(".upload-unhide").fadeIn(); + }); + + $form.on("submit", () => { + let putresource = $form.find("[name=saveas]").val(); + // TODO: do XHR to PUT at putresource + }); +}); diff --git a/index.js b/index.js new file mode 100644 index 0000000..134a3da --- /dev/null +++ b/index.js @@ -0,0 +1,453 @@ +/* jshint esversion: 6 */ +/* jshint node: true */ +"use strict"; + +const express = require("express"); +const hbs = require("express-handlebars"); +const bodyparser = require("body-parser"); +const session = require("express-session"); +const busboy = require("connect-busboy"); +const flash = require("connect-flash"); + +const archiver = require("archiver"); + +const notp = require("notp"); +const base32 = require("thirty-two"); + +const fs = require("fs"); +const path = require("path"); + +const filesize = require("filesize"); + +let app = express(); + +app.set("views", path.join(__dirname, "views")); +app.engine("handlebars", hbs({ + partialsDir: path.join(__dirname, "views", "partials"), + layoutsDir: path.join(__dirname, "views", "layouts"), + defaultLayout: "main", + helpers: { + eachpath: (path, options) => { + if (typeof path != "string") { + return ""; + } + let out = ""; + path = path.split("/"); + path.splice(path.length - 1, 1); + path.unshift(""); + path.forEach((folder, index) => { + out += options.fn({ + name: folder + "/", + path: "/" + path.slice(1, index + 1).join("/"), + current: index === path.length - 1 + }); + }); + return out; + } + } +})); +app.set("view engine", "handlebars"); + +app.use("/bootstrap", express.static(path.join(__dirname, "node_modules/bootstrap/dist"))); +app.use("/octicons", express.static(path.join(__dirname, "node_modules/octicons/build"))); +app.use("/jquery", express.static(path.join(__dirname, "node_modules/jquery/dist"))); +app.use("/filesize", express.static(path.join(__dirname, "node_modules/filesize/lib"))); +app.use("/assets", express.static(path.join(__dirname, "assets"))); + +app.use(session({ + secret: "meowmeow" +})); +app.use(flash()); +app.use(busboy()); +app.use(bodyparser.urlencoded()); + +// AUTH + +const KEY = process.env.KEY ? base32.decode(process.env.KEY) : null; + +app.get("/@logout", (req, res) => { + if (KEY) { + req.session.login = false; + req.flash("success", "Signed out."); + res.redirect("/@login"); + } + else { + req.flash("error", "You were never logged in..."); + res.redirect("back"); + } +}); + +app.get("/@login", (req, res) => { + res.render("login", flashify(req, {})); +}); +app.post("/@login", (req, res) => { + let pass = notp.totp.verify(req.body.token.replace(" ", ""), KEY); + console.log(pass, req.body.token.replace(" ", "")); + if (pass) { + req.session.login = true; + res.redirect("/"); + } + else { + req.flash("error", "Bad token."); + res.redirect("/@login"); + } +}); + +app.use((req, res, next) => { + if (!KEY) { + return next(); + } + if (req.session.login === true) { + return next(); + } + else { + req.flash("error", "Please sign in."); + res.redirect("/@login"); + } +}); + +function relative(...paths) { + return paths.reduce((a, b) => path.join(a, b), process.cwd()); +} +function flashify(req, obj) { + let error = req.flash("error"); + if (error && error.length > 0) { + if (!obj.errors) { + obj.errors = []; + } + obj.errors.push(error); + } + let success = req.flash("success"); + if (success && success.length > 0) { + if (!obj.successes) { + obj.successes = []; + } + obj.successes.push(success); + } + obj.isloginenabled = !!KEY; + return obj; +} + +app.all("/*", (req, res, next) => { + res.filename = req.params[0]; + + let fileExists = new Promise((resolve, reject) => { + // Check if file exists + fs.stat(relative(res.filename), (err, stats) => { + if (err) { + return reject(err); + } + return resolve(stats); + }); + }); + + fileExists.then((stats) => { + res.stats = stats; + next(); + }).catch((err) => { + res.stats = { error: err }; + next(); + }); +}); + +// Currently unused +app.put("/*", (req, res) => { + if (res.stats.error) { + req.busboy.on("file", (key, file, filename) => { + if (key == "file") { + let save = fs.createWriteStream(relative(res.filename)); + file.pipe(save); + save.on("close", () => { + res.flash("success", "File saved. "); + res.redirect("back"); + }); + save.on("error", (err) => { + res.flash("error", err); + res.redirect("back"); + }); + } + }); + req.busboy.on("field", (key, value) => { + + }); + req.pipe(req.busboy); + } + else { + req.flash("error", "File exists, cannot overwrite. "); + res.redirect("back"); + } +}); + +app.post("/*@upload", (req, res) => { + res.filename = req.params[0]; + + let buff = null; + let saveas = null; + req.busboy.on("file", (key, stream, filename) => { + if (key == "file") { + let buffs = []; + stream.on("data", (d) => { + buffs.push(d); + }); + stream.on("end", () => { + buff = Buffer.concat(buffs); + }); + } + }); + req.busboy.on("field", (key, value) => { + if (key == "saveas") { + saveas = value; + } + }); + req.busboy.on("finish", () => { + if (!buff || !saveas) { + return res.status(400).end(); + } + let fileExists = new Promise((resolve, reject) => { + // Check if file exists + fs.stat(relative(res.filename, saveas), (err, stats) => { + if (err) { + return reject(err); + } + return resolve(stats); + }); + }); + + fileExists.then((stats) => { + req.flash("error", "File exists, cannot overwrite. "); + res.redirect("back"); + }).catch((err) => { + console.log("saving"); + let save = fs.createWriteStream(relative(res.filename, saveas)); + save.on("close", () => { + if (buff.length === 0) { + req.flash("success", "File saved. Warning: empty file."); + } + else { + req.flash("success", "File saved. "); + } + res.redirect("back"); + }); + save.on("error", (err) => { + req.flash("error", err); + res.redirect("back"); + }); + save.write(buff); + save.end(); + }); + }); + req.pipe(req.busboy); +}); + +app.post("/*@mkdir", (req, res) => { + res.filename = req.params[0]; + + let folder = req.body.folder; + if (!folder || folder.length < 1) { + return res.status(400).end(); + } + + let fileExists = new Promise((resolve, reject) => { + // Check if file exists + fs.stat(relative(res.filename, folder), (err, stats) => { + if (err) { + return reject(err); + } + return resolve(stats); + }); + }); + + fileExists.then((stats) => { + req.flash("error", "Folder exists, cannot overwrite. "); + res.redirect("back"); + }).catch((err) => { + fs.mkdir(relative(res.filename, folder), (err) => { + if (err) { + req.flash("error", err); + res.redirect("back"); + } + req.flash("success", "Folder created. "); + res.redirect("back"); + }); + }); +}); + +app.post("/*@delete", (req, res) => { + res.filename = req.params[0]; + + let files = JSON.parse(req.body.files); + if (!files || !files.map) { + req.flash("error", "No files selected."); + res.redirect("back"); + return; // res.status(400).end(); + } + + let promises = files.map(f => { + return new Promise((resolve, reject) => { + fs.stat(relative(res.filename, f), (err, stats) => { + if (err) { + return reject(err); + } + resolve({ + name: f, + isdirectory: stats.isDirectory(), + isfile: stats.isFile() + }); + }); + }); + }); + Promise.all(promises).then((files) => { + let promises = files.map(f => { + return new Promise((resolve, reject) => { + let op = null; + if (f.isdirectory) { + op = fs.rmdir; + } + else if (f.isfile) { + op = fs.unlink; + } + if (op) { + op(relative(res.filename, f.name), (err) => { + if (err) { + return reject(err); + } + resolve(); + }); + } + }); + }); + Promise.all(promises).then(() => { + req.flash("success", "Files deleted. "); + res.redirect("back"); + }).catch((err) => { // TODO: recursive rmdir https://github.com/isaacs/rimraf + req.flash("error", "Unable to delete some files: " + err); + res.redirect("back"); + }); + }).catch((err) => { + req.flash("error", err); + res.redirect("back"); + }); +}); + +app.get("/*@download", (req, res) => { + res.filename = req.params[0]; + + let files = null; + try { + files = JSON.parse(req.query.files); + } catch (e) {} + if (!files || !files.map) { + req.flash("error", "No files selected."); + res.redirect("back"); + return; // res.status(400).end(); + } + + let promises = files.map(f => { + return new Promise((resolve, reject) => { + fs.stat(relative(res.filename, f), (err, stats) => { + if (err) { + return reject(err); + } + resolve({ + name: f, + isdirectory: stats.isDirectory(), + isfile: stats.isFile() + }); + }); + }); + }); + Promise.all(promises).then((files) => { + let zip = archiver.create("zip", {}); + zip.on("error", function(err) { + res.status(500).send({ + error: err.message + }); + }); + + files.filter(f => f.isfile).forEach((f) => { + zip.file(relative(res.filename, f.name), { name: f.name }); + }); + files.filter(f => f.isdirectory).forEach((f) => { + zip.directory(relative(res.filename, f.name), f.name); + }); + + res.attachment("Archive.zip"); + zip.pipe(res); + + zip.finalize(); + }).catch((err) => { + console.log(err); + req.flash("error", err); + res.redirect("back"); + }); +}); + +app.get("/*", (req, res) => { + if (res.stats.error) { + res.render("list", flashify(req, { + path: res.filename, + errors: [ + res.stats.error + ] + })); + } + else if (res.stats.isDirectory()) { + if (!req.url.endsWith("/")) { + return res.redirect(req.url + "/"); + } + + let readDir = new Promise((resolve, reject) => { + fs.readdir(relative(res.filename), (err, filenames) => { + if (err) { + return reject(err); + } + return resolve(filenames); + }); + }); + + readDir.then((filenames) => { + let promises = filenames.map(f => { + return new Promise((resolve, reject) => { + fs.stat(relative(res.filename, f), (err, stats) => { + if (err) { + return reject(err); + } + resolve({ + name: f, + isdirectory: stats.isDirectory(), + size: filesize(stats.size) + }); + }); + }); + }); + + Promise.all(promises).then((files) => { + res.render("list", flashify(req, { + path: res.filename, + files: files, + })); + }).catch((err) => { + res.render("list", flashify(req, { + path: res.filename, + errors: [ + err + ] + })); + }); + }).catch((err) => { + res.render("list", flashify(req, { + path: res.filename, + errors: [ + err + ] + })); + }); + } + else if (res.stats.isFile()) { + res.download(relative(res.filename)); + } +}); + +// startup + +app.listen(process.env.PORT || 8080); diff --git a/package.json b/package.json new file mode 100644 index 0000000..88b7e8d --- /dev/null +++ b/package.json @@ -0,0 +1,19 @@ +{ + "name": "yourdump", + "description": "A simple file manager", + "dependencies": { + "archiver": "^1.1.0", + "body-parser": "^1.15.2", + "bootstrap": "^4.0.0-alpha.4", + "connect-busboy": "0.0.2", + "connect-flash": "^0.1.1", + "express": "^4.14.0", + "express-handlebars": "^3.0.0", + "express-session": "^1.14.1", + "filesize": "^3.3.0", + "jquery": "^3.1.1", + "notp": "^2.0.3", + "octicons": "^4.4.0", + "thirty-two": "^1.0.2" + } +} diff --git a/views/layouts/main.handlebars b/views/layouts/main.handlebars new file mode 100644 index 0000000..ebbce43 --- /dev/null +++ b/views/layouts/main.handlebars @@ -0,0 +1,21 @@ + + + + + + File Manager + + + + + + + + + + + + + {{{body}}} + + diff --git a/views/list.handlebars b/views/list.handlebars new file mode 100644 index 0000000..77a07ed --- /dev/null +++ b/views/list.handlebars @@ -0,0 +1,42 @@ +{{> navbar path=path}} + +
    + {{#each errors as |error|}} + + {{/each}} + {{#each successes as |success|}} + + {{/each}} + +
    + +{{> toolbar}} + +{{> dialogue-upload}} +{{> dialogue-mkdir}} + +{{> dialogue-download}} +{{> dialogue-delete}} diff --git a/views/login.handlebars b/views/login.handlebars new file mode 100644 index 0000000..31fde75 --- /dev/null +++ b/views/login.handlebars @@ -0,0 +1,31 @@ +{{> navbar isloggingin=true}} + +
    +
    +
    + {{#each errors as |error|}} + + {{/each}} + {{#each successes as |success|}} + + {{/each}} +
    +
    +
    + +
    + + + + +
    +
    +
    +
    +
    +
    +
    diff --git a/views/partials/dialogue-delete.handlebars b/views/partials/dialogue-delete.handlebars new file mode 100644 index 0000000..2e0436a --- /dev/null +++ b/views/partials/dialogue-delete.handlebars @@ -0,0 +1,25 @@ +
    + +
    diff --git a/views/partials/dialogue-download.handlebars b/views/partials/dialogue-download.handlebars new file mode 100644 index 0000000..3bf116f --- /dev/null +++ b/views/partials/dialogue-download.handlebars @@ -0,0 +1,25 @@ +
    + +
    diff --git a/views/partials/dialogue-mkdir.handlebars b/views/partials/dialogue-mkdir.handlebars new file mode 100644 index 0000000..d32ba09 --- /dev/null +++ b/views/partials/dialogue-mkdir.handlebars @@ -0,0 +1,24 @@ +
    + +
    diff --git a/views/partials/dialogue-upload.handlebars b/views/partials/dialogue-upload.handlebars new file mode 100644 index 0000000..f587f6b --- /dev/null +++ b/views/partials/dialogue-upload.handlebars @@ -0,0 +1,34 @@ +
    + +
    diff --git a/views/partials/navbar.handlebars b/views/partials/navbar.handlebars new file mode 100644 index 0000000..85b154e --- /dev/null +++ b/views/partials/navbar.handlebars @@ -0,0 +1,25 @@ + diff --git a/views/partials/toolbar.handlebars b/views/partials/toolbar.handlebars new file mode 100644 index 0000000..5818b11 --- /dev/null +++ b/views/partials/toolbar.handlebars @@ -0,0 +1,28 @@ +