Browse Source

image uploading and viewing now works.

master
mort 8 years ago
parent
commit
220a598be5

+ 2
- 0
.gitignore View File

@@ -1,3 +1,5 @@
conf.json
node_modules
npm-debug.log
imgs
!imgs/.placeholder

+ 10
- 1
conf.json.example View File

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

web/viewer/script.js → imgs/.placeholder View File


+ 14
- 9
index.js View File

@@ -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) {

+ 46
- 3
lib/context.js View File

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

+ 0
- 23
lib/db.js View File

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

+ 24
- 6
lib/includeHtml.js View File

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

+ 1
- 1
sql/setup.sql View File

@@ -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 (

+ 6
- 0
templates/image.html View File

@@ -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>

+ 2
- 2
views/index.html View File

@@ -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>

+ 16
- 0
views/view.html View File

@@ -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>

+ 0
- 12
views/viewer.html View File

@@ -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>

+ 23
- 0
web/api/collection_create.node.js View File

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

+ 45
- 0
web/api/image_create.node.js View File

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

+ 0
- 7
web/api/upload.node.js View File

@@ -1,7 +0,0 @@
module.exports = function(ctx) {
ctx.getPostData(function(err, data, files) {
if (err) return console.log(err);

ctx.succeed();
});
}

+ 26
- 1
web/global.js View File

@@ -24,6 +24,10 @@
});
});

util.error = function(body) {
util.notify("An error occurred.", body);
}

util.htmlEntities = function(str) {
return str.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
@@ -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;
}
}
})();

+ 9
- 0
web/i/index.node.js View File

@@ -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
- 4
web/index/index.node.js View File

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

+ 47
- 22
web/index/script.js View File

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

+ 31
- 0
web/view/index.node.js View File

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

+ 18
- 0
web/view/style.css View File

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

+ 0
- 8
web/viewer/index.node.js View File

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

+ 0
- 0
web/viewer/style.css View File


Loading…
Cancel
Save