conf.json | conf.json | ||||
node_modules | node_modules | ||||
npm-debug.log | npm-debug.log | ||||
imgs | |||||
!imgs/.placeholder |
"db": { | "db": { | ||||
"host": "localhost", | "host": "localhost", | ||||
"user": "dbuser", | "user": "dbuser", | ||||
"pass": "dbpass", | |||||
"password": "dbpass", | |||||
"database": "mimg" | "database": "mimg" | ||||
}, | }, | ||||
"use_https": false, | "use_https": false, | ||||
"https": { | "https": { | ||||
"key": "", | "key": "", | ||||
"cert": "" | "cert": "" | ||||
}, | |||||
"web": { | |||||
"title": "Mimg", | |||||
"base_url": "http://example.com" | |||||
}, | |||||
"minify": true, | |||||
"session_timeout": 1800000, | |||||
"dir": { | |||||
"imgs": "imgs" | |||||
} | } | ||||
} | } |
var https = require("https"); | var https = require("https"); | ||||
var fs = require("fs"); | var fs = require("fs"); | ||||
var loader = require("./lib/loader.js"); | var loader = require("./lib/loader.js"); | ||||
var pg = require("pg"); | |||||
var Context = require("./lib/context.js"); | var Context = require("./lib/context.js"); | ||||
var Db = require("./lib/db.js"); | |||||
var conf = JSON.parse(fs.readFileSync("conf.json")); | var conf = JSON.parse(fs.readFileSync("conf.json")); | ||||
"/index/style.css": "index/style.css", | "/index/style.css": "index/style.css", | ||||
//Viewer files | //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 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 loaded = loader.load(endpoints, conf); | ||||
var db = new pg.Client(conf.db); | |||||
//Function to run on each request | //Function to run on each request | ||||
function onRequest(req, res) { | function onRequest(req, res) { | ||||
console.log("Request for "+req.url); | 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 the file doesn't exist, we 404. | ||||
if (!ep) { | if (!ep) { | ||||
res: res, | res: res, | ||||
templates: loaded.templates, | templates: loaded.templates, | ||||
views: loaded.views, | views: loaded.views, | ||||
db: db, | |||||
conf: conf | conf: conf | ||||
})); | })); | ||||
} else { | } else { | ||||
} | } | ||||
//Initiate a postgresql client | //Initiate a postgresql client | ||||
var db = new Db(conf.db, function(err) { | |||||
if (err) throw err; | |||||
db.connect(function() { | |||||
//Create HTTP or HTTPS server | //Create HTTP or HTTPS server | ||||
var server; | var server; | ||||
if (conf.use_https) { | if (conf.use_https) { |
var formidable = require("formidable"); | var formidable = require("formidable"); | ||||
var crypto = require("crypto"); | |||||
var sessions = {}; | |||||
function templatify(str, args) { | function templatify(str, args) { | ||||
if (args == undefined) | if (args == undefined) | ||||
this.res = options.res; | this.res = options.res; | ||||
this.templates = options.templates; | this.templates = options.templates; | ||||
this.views = options.views; | this.views = options.views; | ||||
this.db = options.db; | |||||
this.conf = options.conf; | 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 = { | module.exports.prototype = { | ||||
succeed: function(obj) { | succeed: function(obj) { | ||||
obj = obj || {}; | obj = obj || {}; | ||||
obj.success = true; | obj.success = true; | ||||
this.end(JSON.stringify(obj)); | this.end(JSON.stringify(obj)); | ||||
}, | }, | ||||
fail: function(err) { | fail: function(err) { | ||||
obj = obj || {}; | |||||
obj = {}; | |||||
obj.success = false; | obj.success = false; | ||||
obj.error = error; | |||||
obj.error = err.toString(); | |||||
this.end(JSON.stringify(obj)); | this.end(JSON.stringify(obj)); | ||||
}, | }, | ||||
}, | }, | ||||
getPostData: function(cb) { | getPostData: function(cb) { | ||||
if (this.postData) | |||||
return cb(null, this.postData.data, this.postData.files); | |||||
if (this.req.method.toUpperCase() != "POST") | if (this.req.method.toUpperCase() != "POST") | ||||
return cb(new Error("Expected POST request, got "+this.req.method)); | return cb(new Error("Expected POST request, got "+this.req.method)); | ||||
var form = new formidable.IncomingForm(); | 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)); | |||||
} | } | ||||
} | } |
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); | |||||
} | |||||
} |
var fs = require("fs"); | var fs = require("fs"); | ||||
var minify = require("./minify"); | 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"); | 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); | |||||
} | } |
id SERIAL PRIMARY KEY, | id SERIAL PRIMARY KEY, | ||||
name VARCHAR(64), | 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 ( | CREATE TABLE images ( |
<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> |
<!DOCTYPE html> | <!DOCTYPE html> | ||||
<html> | <html> | ||||
<head> | <head> | ||||
{{head}} | |||||
{{template#head}} | |||||
<link rel="stylesheet" href="/index/style.css"> | <link rel="stylesheet" href="/index/style.css"> | ||||
</head> | </head> | ||||
<body> | <body> | ||||
{{global}} | |||||
{{template#global}} | |||||
<div id="uploader" class="container"> | <div id="uploader" class="container"> | ||||
<input type="file" accept="image/*" id="uploader-input" class="hidden" multiple> | <input type="file" accept="image/*" id="uploader-input" class="hidden" multiple> |
<!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> |
<!DOCTYPE html> | |||||
<html> | |||||
<head> | |||||
{{head}} | |||||
<link rel="stylesheet" href="/viewer/style.css"> | |||||
</head> | |||||
<body> | |||||
{{global}} | |||||
<script src="/viewer/script.js"></script> | |||||
</body> | |||||
</html> |
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 | |||||
}); | |||||
} | |||||
} |
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 | |||||
}); | |||||
}); | |||||
} | |||||
} |
module.exports = function(ctx) { | |||||
ctx.getPostData(function(err, data, files) { | |||||
if (err) return console.log(err); | |||||
ctx.succeed(); | |||||
}); | |||||
} |
}); | }); | ||||
}); | }); | ||||
util.error = function(body) { | |||||
util.notify("An error occurred.", body); | |||||
} | |||||
util.htmlEntities = function(str) { | util.htmlEntities = function(str) { | ||||
return str.replace(/&/g, "&") | return str.replace(/&/g, "&") | ||||
.replace(/</g, "<") | .replace(/</g, "<") | ||||
var fd = new FormData(); | var fd = new FormData(); | ||||
for (var i in data) { | for (var i in data) { | ||||
console.log(i); | |||||
fd.append(i, data[i]); | fd.append(i, data[i]); | ||||
} | } | ||||
return xhr; | return xhr; | ||||
} | } | ||||
}).done(function(res) { | }).done(function(res) { | ||||
console.log("response from "+name+":"); | |||||
console.log(res); | |||||
var obj = JSON.parse(res); | var obj = JSON.parse(res); | ||||
if (obj.success) | if (obj.success) | ||||
cb(null, obj); | cb(null, obj); | ||||
cb(obj.error); | 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; | |||||
} | |||||
} | |||||
})(); | })(); |
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); | |||||
} |
module.exports = function(ctx) { | module.exports = function(ctx) { | ||||
ctx.end(ctx.view("index", { | 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") | |||||
})); | })); | ||||
} | } |
//Enable upload button | //Enable upload button | ||||
$("#uploader-upload").removeAttr("disabled") | $("#uploader-upload").removeAttr("disabled") | ||||
console.log("making uploader button not disabled"); | |||||
var inputFiles = evt.target.files; | var inputFiles = evt.target.files; | ||||
//First, disable all buttons | //First, disable all buttons | ||||
$("#uploader button.btn").prop("disabled", true); | $("#uploader button.btn").prop("disabled", true); | ||||
console.log("making buttons disabled"); | |||||
var elems = []; | var elems = []; | ||||
elems[elem.data("index")] = elem; | 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); | |||||
}); | |||||
}); | }); | ||||
}); | }); | ||||
})(); | })(); |
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 | |||||
})); | |||||
} | |||||
} |
#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; | |||||
} |
module.exports = function(ctx) { | |||||
ctx.end(ctx.view("viewer", { | |||||
head: ctx.template("head"), | |||||
global: ctx.template("global", { | |||||
profile: ctx.template("navbar-profile-login") | |||||
}) | |||||
})); | |||||
} |