@@ -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") | |||
}) | |||
})); | |||
} |