| @@ -1,3 +1,5 @@ | |||
| conf.json | |||
| node_modules | |||
| npm-debug.log | |||
| imgs | |||
| !imgs/.placeholder | |||
| @@ -4,12 +4,21 @@ | |||
| "db": { | |||
| "host": "localhost", | |||
| "user": "dbuser", | |||
| "pass": "dbpass", | |||
| "password": "dbpass", | |||
| "database": "mimg" | |||
| }, | |||
| "use_https": false, | |||
| "https": { | |||
| "key": "", | |||
| "cert": "" | |||
| }, | |||
| "web": { | |||
| "title": "Mimg", | |||
| "base_url": "http://example.com" | |||
| }, | |||
| "minify": true, | |||
| "session_timeout": 1800000, | |||
| "dir": { | |||
| "imgs": "imgs" | |||
| } | |||
| } | |||
| @@ -2,8 +2,8 @@ var http = require("http"); | |||
| var https = require("https"); | |||
| var fs = require("fs"); | |||
| var loader = require("./lib/loader.js"); | |||
| var pg = require("pg"); | |||
| var Context = require("./lib/context.js"); | |||
| var Db = require("./lib/db.js"); | |||
| var conf = JSON.parse(fs.readFileSync("conf.json")); | |||
| @@ -21,21 +21,26 @@ var endpoints = { | |||
| "/index/style.css": "index/style.css", | |||
| //Viewer files | |||
| "/viewer": "viewer/index.node.js", | |||
| "/viewer/script.js": "viewer/script.js", | |||
| "/viewer/style.css": "viewer/style.css", | |||
| "/view": "view/index.node.js", | |||
| "/view/style.css": "view/style.css", | |||
| //Plain image files | |||
| "/i": "i/index.node.js", | |||
| //API files | |||
| "/api/upload": "api/upload.node.js" | |||
| "/api/image_create": "api/image_create.node.js", | |||
| "/api/collection_create": "api/collection_create.node.js" | |||
| } | |||
| var loaded = loader.load(endpoints, conf); | |||
| var db = new pg.Client(conf.db); | |||
| //Function to run on each request | |||
| function onRequest(req, res) { | |||
| console.log("Request for "+req.url); | |||
| var ep = loaded.endpoints[req.url]; | |||
| var ep = loaded.endpoints[req.url.split("?")[0]]; | |||
| //If the file doesn't exist, we 404. | |||
| if (!ep) { | |||
| @@ -50,6 +55,7 @@ function onRequest(req, res) { | |||
| res: res, | |||
| templates: loaded.templates, | |||
| views: loaded.views, | |||
| db: db, | |||
| conf: conf | |||
| })); | |||
| } else { | |||
| @@ -58,9 +64,8 @@ function onRequest(req, res) { | |||
| } | |||
| //Initiate a postgresql client | |||
| var db = new Db(conf.db, function(err) { | |||
| if (err) throw err; | |||
| db.connect(function() { | |||
| //Create HTTP or HTTPS server | |||
| var server; | |||
| if (conf.use_https) { | |||
| @@ -1,4 +1,7 @@ | |||
| var formidable = require("formidable"); | |||
| var crypto = require("crypto"); | |||
| var sessions = {}; | |||
| function templatify(str, args) { | |||
| if (args == undefined) | |||
| @@ -16,7 +19,34 @@ module.exports = function(options) { | |||
| this.res = options.res; | |||
| this.templates = options.templates; | |||
| this.views = options.views; | |||
| this.db = options.db; | |||
| this.conf = options.conf; | |||
| //Handle cookies | |||
| this.cookies = {}; | |||
| this.req.headers.cookie = this.req.headers.cookie || ""; | |||
| this.req.headers.cookie.split(/;\s*/).forEach(function(elem) { | |||
| var pair = elem.split("="); | |||
| this.cookies[pair[0]] = decodeURIComponent(pair[1]); | |||
| }.bind(this)); | |||
| //Handle sessions | |||
| if (sessions[this.cookies.session]) { | |||
| this.session = sessions[this.cookies.session]; | |||
| } else { | |||
| var key; | |||
| do { | |||
| key = crypto.randomBytes(64).toString("hex"); | |||
| } while (sessions[key]); | |||
| sessions[key] = {}; | |||
| this.res.setHeader("Set-Cookie", "session="+key); | |||
| //Delete session after a while | |||
| setTimeout(function() { | |||
| delete sessions[key]; | |||
| }, this.conf.session_timeout); | |||
| } | |||
| } | |||
| module.exports.prototype = { | |||
| @@ -27,13 +57,14 @@ module.exports.prototype = { | |||
| succeed: function(obj) { | |||
| obj = obj || {}; | |||
| obj.success = true; | |||
| this.end(JSON.stringify(obj)); | |||
| }, | |||
| fail: function(err) { | |||
| obj = obj || {}; | |||
| obj = {}; | |||
| obj.success = false; | |||
| obj.error = error; | |||
| obj.error = err.toString(); | |||
| this.end(JSON.stringify(obj)); | |||
| }, | |||
| @@ -54,10 +85,22 @@ module.exports.prototype = { | |||
| }, | |||
| getPostData: function(cb) { | |||
| if (this.postData) | |||
| return cb(null, this.postData.data, this.postData.files); | |||
| if (this.req.method.toUpperCase() != "POST") | |||
| return cb(new Error("Expected POST request, got "+this.req.method)); | |||
| var form = new formidable.IncomingForm(); | |||
| form.parse(this.req, cb); | |||
| form.parse(this.req, function(err, data, files) { | |||
| if (err) return cb(err); | |||
| this.postData = { | |||
| data: data, | |||
| files: files | |||
| } | |||
| cb(null, data, files); | |||
| }.bind(this)); | |||
| } | |||
| } | |||
| @@ -1,23 +0,0 @@ | |||
| var pg = require("pg"); | |||
| module.exports = function(conf, cb) { | |||
| var conStr = | |||
| "postgres://"+ | |||
| conf.user+":"+ | |||
| conf.pass+"@"+ | |||
| conf.host+"/"+ | |||
| conf.database; | |||
| pg.connect(conStr, function(err, client) { | |||
| if (err) return cb(err); | |||
| this.client = client; | |||
| cb(); | |||
| }.bind(this)); | |||
| } | |||
| module.exports.prototype = { | |||
| query: function(str) { | |||
| this.client.query(str); | |||
| } | |||
| } | |||
| @@ -1,14 +1,32 @@ | |||
| var fs = require("fs"); | |||
| var minify = require("./minify"); | |||
| module.exports = function(path, conf) { | |||
| var globalRegex = /{{([^}]+)#([^}]+)}}/g; | |||
| var localRegex = /{{([^}]+)#([^}]+)}}/; | |||
| module.exports = function load(path, conf) { | |||
| var html = fs.readFileSync(path, "utf8"); | |||
| for (var i in conf) { | |||
| html = html.split("{{conf#"+i+"}}").join(conf[i]); | |||
| } | |||
| var placeholders = html.match(globalRegex); | |||
| if (!placeholders) | |||
| return minify.html(html); | |||
| placeholders.forEach(function(p) { | |||
| var parts = html.match(localRegex); | |||
| var s = parts[0]; | |||
| var ns = parts[1]; | |||
| var key = parts[2]; | |||
| html = minify.html(html); | |||
| switch (ns) { | |||
| case "conf": | |||
| html = html.replace(s, conf[key]); | |||
| break; | |||
| case "template": | |||
| html = html.replace(s, load("templates/"+key+".html", conf)); | |||
| break; | |||
| } | |||
| }); | |||
| return html; | |||
| return minify.html(html); | |||
| } | |||
| @@ -9,7 +9,7 @@ CREATE TABLE collections ( | |||
| id SERIAL PRIMARY KEY, | |||
| name VARCHAR(64), | |||
| user_id INTEGER NOT NULL REFERENCES users(id) ON DELETE CASCADE | |||
| user_id INTEGER REFERENCES users(id) ON DELETE CASCADE | |||
| ); | |||
| CREATE TABLE images ( | |||
| @@ -0,0 +1,6 @@ | |||
| <div class="image"> | |||
| <div class="title">{{title}}</div> | |||
| <img class="img-rounded" src="/i?{{id}}.{{extension}}"> | |||
| <div class="description">{{description}}</div> | |||
| <input class="url" type="text" value="{{conf#base_url}}/i?{{id}}.{{extension}}" onclick="select()"> | |||
| </div> | |||
| @@ -1,11 +1,11 @@ | |||
| <!DOCTYPE html> | |||
| <html> | |||
| <head> | |||
| {{head}} | |||
| {{template#head}} | |||
| <link rel="stylesheet" href="/index/style.css"> | |||
| </head> | |||
| <body> | |||
| {{global}} | |||
| {{template#global}} | |||
| <div id="uploader" class="container"> | |||
| <input type="file" accept="image/*" id="uploader-input" class="hidden" multiple> | |||
| @@ -0,0 +1,16 @@ | |||
| <!DOCTYPE html> | |||
| <html> | |||
| <head> | |||
| {{template#head}} | |||
| <link rel="stylesheet" href="/view/style.css"> | |||
| </head> | |||
| <body> | |||
| {{template#global}} | |||
| <div id="viewer" class="container"> | |||
| {{images}} | |||
| </div> | |||
| <script src="/index/script.js"></script> | |||
| </body> | |||
| </html> | |||
| @@ -1,12 +0,0 @@ | |||
| <!DOCTYPE html> | |||
| <html> | |||
| <head> | |||
| {{head}} | |||
| <link rel="stylesheet" href="/viewer/style.css"> | |||
| </head> | |||
| <body> | |||
| {{global}} | |||
| <script src="/viewer/script.js"></script> | |||
| </body> | |||
| </html> | |||
| @@ -0,0 +1,23 @@ | |||
| module.exports = function(ctx) { | |||
| ctx.getPostData(function(err, data) { | |||
| if (err) return ctx.fail(err); | |||
| ctx.db.query( | |||
| "INSERT INTO collections (name) "+ | |||
| "VALUES ($1) "+ | |||
| "RETURNING id", | |||
| [data.name], | |||
| queryCallback | |||
| ); | |||
| }); | |||
| function queryCallback(err, res) { | |||
| if (err) return ctx.fail(err); | |||
| ctx.session.collectionId = res.rows[0].id; | |||
| ctx.succeed({ | |||
| id: res.rows[0].id | |||
| }); | |||
| } | |||
| } | |||
| @@ -0,0 +1,45 @@ | |||
| var fs = require("fs"); | |||
| module.exports = function(ctx) { | |||
| ctx.getPostData(function(err, data, files) { | |||
| if (err) | |||
| return ctx.fail(err); | |||
| if (!files.file) | |||
| return ctx.fail("No file supplied."); | |||
| data.collectionId = parseInt(data.collectionId); | |||
| if (data.collectionId !== ctx.session.collectionId) | |||
| return ctx.fail("You don't own that collection."); | |||
| //We want all extensions to be lower case. | |||
| data.extension = data.extension.toLowerCase(); | |||
| ctx.db.query( | |||
| "INSERT INTO images (name, description, extension, collection_id) "+ | |||
| "VALUES ($1, $2, $3, $4) "+ | |||
| "RETURNING id", | |||
| [data.name, data.description, data.extension, data.collectionId], | |||
| queryCallback | |||
| ); | |||
| }); | |||
| function queryCallback(err, res) { | |||
| if (err) | |||
| return ctx.fail(err); | |||
| var id = res.rows[0].id; | |||
| var file = ctx.postData.files.file; | |||
| var readStream = fs.createReadStream(file.path); | |||
| var writeStream = fs.createWriteStream(ctx.conf.dir.imgs+"/"+id); | |||
| readStream.pipe(writeStream); | |||
| readStream.on("end", function() { | |||
| ctx.succeed({ | |||
| id: id | |||
| }); | |||
| }); | |||
| } | |||
| } | |||
| @@ -1,7 +0,0 @@ | |||
| module.exports = function(ctx) { | |||
| ctx.getPostData(function(err, data, files) { | |||
| if (err) return console.log(err); | |||
| ctx.succeed(); | |||
| }); | |||
| } | |||
| @@ -24,6 +24,10 @@ | |||
| }); | |||
| }); | |||
| util.error = function(body) { | |||
| util.notify("An error occurred.", body); | |||
| } | |||
| util.htmlEntities = function(str) { | |||
| return str.replace(/&/g, "&") | |||
| .replace(/</g, "<") | |||
| @@ -35,7 +39,6 @@ | |||
| var fd = new FormData(); | |||
| for (var i in data) { | |||
| console.log(i); | |||
| fd.append(i, data[i]); | |||
| } | |||
| @@ -54,6 +57,8 @@ | |||
| return xhr; | |||
| } | |||
| }).done(function(res) { | |||
| console.log("response from "+name+":"); | |||
| console.log(res); | |||
| var obj = JSON.parse(res); | |||
| if (obj.success) | |||
| cb(null, obj); | |||
| @@ -61,4 +66,24 @@ | |||
| cb(obj.error); | |||
| }); | |||
| } | |||
| util.async = function(n, cb) { | |||
| if (typeof n !== "number") | |||
| throw new Error("Expected number, got "+typeof n); | |||
| if (n < 1) | |||
| return cb(); | |||
| var res = {}; | |||
| return function(key, val) { | |||
| if (key !== undefined) | |||
| res[key] = val; | |||
| if (n === 1) | |||
| cb(res); | |||
| else | |||
| n -= 1; | |||
| } | |||
| } | |||
| })(); | |||
| @@ -0,0 +1,9 @@ | |||
| var fs = require("fs"); | |||
| module.exports = function(ctx) { | |||
| var id = ctx.req.url.split("?")[1] | |||
| .replace(/\..*/, ""); | |||
| var readStream = fs.createReadStream(ctx.conf.dir.imgs+"/"+id); | |||
| readStream.pipe(ctx.res); | |||
| } | |||
| @@ -1,8 +1,5 @@ | |||
| module.exports = function(ctx) { | |||
| ctx.end(ctx.view("index", { | |||
| head: ctx.template("head"), | |||
| global: ctx.template("global", { | |||
| profile: ctx.template("navbar-profile-login") | |||
| }) | |||
| profile: ctx.template("navbar-profile-login") | |||
| })); | |||
| } | |||
| @@ -24,7 +24,6 @@ | |||
| //Enable upload button | |||
| $("#uploader-upload").removeAttr("disabled") | |||
| console.log("making uploader button not disabled"); | |||
| var inputFiles = evt.target.files; | |||
| @@ -57,7 +56,6 @@ | |||
| //First, disable all buttons | |||
| $("#uploader button.btn").prop("disabled", true); | |||
| console.log("making buttons disabled"); | |||
| var elems = []; | |||
| @@ -66,26 +64,53 @@ | |||
| elems[elem.data("index")] = elem; | |||
| }); | |||
| files.forEach(function(f, i) { | |||
| var progressBar = elems[i].children(".progress-bar"); | |||
| function getXhr(xhr) { | |||
| xhr.upload.addEventListener("progress", function(evt) { | |||
| if (!evt.lengthComputable) | |||
| return; | |||
| var percent = (evt.loaded / evt.total) * 100; | |||
| progressBar.css({width: percent+"%"}); | |||
| }, false); | |||
| } | |||
| var ajax = util.api("upload", { | |||
| name: f.name, | |||
| data: f | |||
| }, function(err, res) { | |||
| console.log(res); | |||
| }, getXhr); | |||
| //First, create a collection | |||
| util.api("collection_create", { | |||
| name: "New Collection" | |||
| }, function(err, res) { | |||
| if (err) return util.error(err); | |||
| var collectionId = res.id; | |||
| //Go to collection once files are uploaded | |||
| var a = util.async(files.length, function() { | |||
| setTimeout(function() { | |||
| location.href = "/view?"+collectionId; | |||
| }, 1000); | |||
| }); | |||
| //Loop through files, uploading them | |||
| files.forEach(function(f, i) { | |||
| var progressBar = elems[i].children(".progress-bar"); | |||
| //Handle progress bars | |||
| function getXhr(xhr) { | |||
| xhr.upload.addEventListener("progress", function(evt) { | |||
| if (!evt.lengthComputable) | |||
| return; | |||
| var percent = (evt.loaded / evt.total) * 100; | |||
| progressBar.css({width: percent+"%"}); | |||
| }, false); | |||
| } | |||
| //Get file extension | |||
| var ext = f.name.split("."); | |||
| ext = ext[ext.length - 1]; | |||
| util.api("image_create", { | |||
| name: f.name, | |||
| description: "An image.", | |||
| extension: ext, | |||
| collectionId: collectionId, | |||
| file: f | |||
| }, function(err, res) { | |||
| if (err) return util.error(err); | |||
| a(); | |||
| }, getXhr); | |||
| }); | |||
| }); | |||
| }); | |||
| })(); | |||
| @@ -0,0 +1,31 @@ | |||
| module.exports = function(ctx) { | |||
| var id = parseInt(ctx.req.url.split("?")[1]); | |||
| ctx.db.query( | |||
| "SELECT id, name, description, extension "+ | |||
| "FROM images "+ | |||
| "WHERE collection_id = $1", | |||
| [id], | |||
| queryCallback | |||
| ); | |||
| function queryCallback(err, res) { | |||
| if (err) | |||
| return ctx.fail(err); | |||
| var images = ""; | |||
| res.rows.forEach(function(row) { | |||
| images += ctx.template("image", { | |||
| title: row.name, | |||
| id: row.id, | |||
| extension: row.extension, | |||
| description: row.description | |||
| }); | |||
| }); | |||
| ctx.end(ctx.view("view", { | |||
| profile: ctx.template("navbar-profile-login"), | |||
| images: images | |||
| })); | |||
| } | |||
| } | |||
| @@ -0,0 +1,18 @@ | |||
| #viewer { | |||
| max-width: 400px; | |||
| } | |||
| #viewer .image img { | |||
| width: 100%; | |||
| } | |||
| #viewer .image .url { | |||
| width: 100%; | |||
| } | |||
| #viewer .image { | |||
| margin-bottom: 20px; | |||
| border: 1px solid #CCC; | |||
| border-radius: 4px; | |||
| padding: 6px; | |||
| } | |||
| @@ -1,8 +0,0 @@ | |||
| module.exports = function(ctx) { | |||
| ctx.end(ctx.view("viewer", { | |||
| head: ctx.template("head"), | |||
| global: ctx.template("global", { | |||
| profile: ctx.template("navbar-profile-login") | |||
| }) | |||
| })); | |||
| } | |||