| @@ -4,4 +4,5 @@ npm-debug.log | |||
| imgs | |||
| !imgs/.placeholder | |||
| .currentRun | |||
| .sessions | |||
| favicon.ico | |||
| @@ -1,14 +1,35 @@ | |||
| var formidable = require("formidable"); | |||
| var crypto = require("crypto"); | |||
| var zlib = require("zlib"); | |||
| var fs = require("fs"); | |||
| var preprocess = require("./preprocess.js"); | |||
| var sessions = {}; | |||
| var sessionTimeouts = {}; | |||
| if (fs.existsSync(".sessions")) { | |||
| sessions = JSON.parse(fs.readFileSync(".sessions", "utf8")); | |||
| //Set appropriate timeouts. | |||
| Object.keys(sessions).forEach(function(i) { | |||
| sessionTimeouts[i] = setTimeout(function() { | |||
| delete sessions[i]; | |||
| }, 1000*60*10); | |||
| }); | |||
| } | |||
| function saveSessions() { | |||
| fs.writeFileSync(".sessions", JSON.stringify(sessions)); | |||
| process.exit(); | |||
| } | |||
| process.on("SIGINT", saveSessions); | |||
| process.on("SIGTERM", saveSessions); | |||
| function templatify(str, args, ctx, env) { | |||
| env.url = ctx.req.url; | |||
| str = preprocess(str, { | |||
| var env = { | |||
| session: function(key) { | |||
| ctx.cachable = false; | |||
| return ctx.session[key]; | |||
| @@ -19,9 +40,11 @@ function templatify(str, args, ctx, env) { | |||
| noescape: args, | |||
| env: env, | |||
| template: function(key) { | |||
| return ctx.template(key); | |||
| return ctx.template(key, args); | |||
| } | |||
| }); | |||
| } | |||
| str = preprocess(str, env); | |||
| return str; | |||
| } | |||
| @@ -63,19 +86,14 @@ module.exports = function(options) { | |||
| sessions[key] = {}; | |||
| this.res.setHeader("Set-Cookie", "session="+key); | |||
| //Delete session after a while | |||
| setTimeout(function() { | |||
| delete sessions[key]; | |||
| }, this.conf.session_timeout); | |||
| } | |||
| this.session = sessions[key]; | |||
| //Reset session delete timer | |||
| if (this.session._timeout) | |||
| clearTimeout(this.session._timeout); | |||
| if (sessionTimeouts[key]) | |||
| clearTimeout(sessionTimeouts[key]); | |||
| this.session._timeout = setTimeout(function() { | |||
| sessionTimeouts[key] = setTimeout(function() { | |||
| delete sessions[key]; | |||
| }, this.conf.session_timeout); | |||
| } | |||
| @@ -22,6 +22,7 @@ | |||
| "pg": "^4.4.0", | |||
| "scrypt": "^4.0.7", | |||
| "uglify-js": "^2.4.24", | |||
| "uglifycss": "0.0.15" | |||
| "uglifycss": "0.0.15", | |||
| "wrench": "^1.5.8" | |||
| } | |||
| } | |||
| @@ -3,6 +3,7 @@ var https = require("https"); | |||
| var fs = require("fs"); | |||
| var domain = require("domain"); | |||
| var zlib = require("zlib"); | |||
| var wrench = require("wrench"); | |||
| var loader = require("./lib/loader.js"); | |||
| var pg = require("pg"); | |||
| var Context = require("./lib/context.js"); | |||
| @@ -49,6 +50,7 @@ var endpoints = { | |||
| "/api/template": "api/template.node.js", | |||
| "/api/image_create": "api/image_create.node.js", | |||
| "/api/collection_create": "api/collection_create.node.js", | |||
| "/api/collection_delete": "api/collection_delete.node.js", | |||
| "/api/account_create": "api/account_create.node.js", | |||
| "/api/account_login": "api/account_login.node.js", | |||
| "/api/account_logout": "api/account_logout.node.js", | |||
| @@ -147,11 +149,19 @@ function purgeCollections() { | |||
| db.query( | |||
| "DELETE FROM collections "+ | |||
| "WHERE user_id IS NULL "+ | |||
| "AND date_created < NOW() - INTERVAL '"+timeout+"'", | |||
| "AND date_created < NOW() - INTERVAL '"+timeout+"' "+ | |||
| "RETURNING id", | |||
| function(err, res) { | |||
| if (err) | |||
| throw err; | |||
| res.rows.forEach(function(row) { | |||
| wrench.rmdirSyncRecursive( | |||
| conf.dir.imgs+"/"+ | |||
| row.id | |||
| ); | |||
| }); | |||
| if (res.rowCount > 0) { | |||
| console.log( | |||
| "Deleted "+res.rowCount+" collections "+ | |||
| @@ -1,5 +1,5 @@ | |||
| <div class="collection"> | |||
| <div class="collection" data-id="{{arg#id}}"> | |||
| <span class="date-created">{{arg#date_created}}</span> | |||
| <a class="name" href="/view?{{arg#id}}">{{arg#name}}</a> | |||
| {{arg#own_profile ? '<button class="delete btn btn-default">X</button>' : ''}} | |||
| {{arg#own_profile ? template#delete-button : ''}} | |||
| </div> | |||
| @@ -0,0 +1 @@ | |||
| <button class="delete btn btn-default" data-id="{{arg#id}}">X</button> | |||
| @@ -1,8 +1,8 @@ | |||
| <div class="image small-width bordered"> | |||
| <div class="title">{{arg#title}}</div> | |||
| <a href="/i?{{arg#id}}.{{arg#extension}}"> | |||
| <img class="img-rounded" src="/i?{{arg#id}}.{{arg#extension}}"> | |||
| <a href="/i?{{arg#collection}}/{{arg#id}}.{{arg#extension}}"> | |||
| <img class="img-rounded" src="/i?{{arg#collection}}/{{arg#id}}.{{arg#extension}}"> | |||
| </a> | |||
| <div class="description">{{arg#description}}</div> | |||
| <input class="url" type="text" value="{{conf#base_url}}/i?{{arg#id}}.{{arg#extension}}" onclick="select()"> | |||
| <input class="url" type="text" value="{{conf#base_url}}/i?{{arg#collection}}/{{arg#id}}.{{arg#extension}}" onclick="select()"> | |||
| </div> | |||
| @@ -1,3 +1,5 @@ | |||
| var fs = require("fs"); | |||
| module.exports = function(ctx) { | |||
| ctx.getPostData(function(err, data) { | |||
| if (err) | |||
| @@ -16,10 +18,17 @@ module.exports = function(ctx) { | |||
| if (err) | |||
| return ctx.fail(err); | |||
| ctx.session.collectionId = res.rows[0].id; | |||
| var id = res.rows[0].id; | |||
| fs.mkdir(ctx.conf.dir.imgs+"/"+id, function(err) { | |||
| if (err) | |||
| return ctx.fail(err); | |||
| ctx.session.lastCollectionId = id; | |||
| ctx.succeed({ | |||
| id: res.rows[0].id | |||
| ctx.succeed({ | |||
| id: id | |||
| }); | |||
| }); | |||
| } | |||
| } | |||
| @@ -0,0 +1,56 @@ | |||
| var wrench = require("wrench"); | |||
| module.exports = function(ctx) { | |||
| ctx.getPostData(function(err, data) { | |||
| var id = parseInt(data.id); | |||
| if (isNaN(id)) | |||
| return ctx.fail("Invalid ID."); | |||
| if (id === ctx.session.lastCollectionId) | |||
| return deleteQuery(); | |||
| if (!ctx.session.loggedIn) | |||
| return ctx.fail("You're not logged in."); | |||
| ctx.db.query( | |||
| "SELECT FROM collections "+ | |||
| "WHERE user_id = $1", | |||
| [ctx.session.userId], | |||
| function(err, res) { | |||
| if (err) | |||
| return ctx.fail(err); | |||
| if (res.rows[0] === undefined) | |||
| return ctx.fail("You don't own that collection."); | |||
| deleteQuery(); | |||
| } | |||
| ); | |||
| }); | |||
| function deleteQuery() { | |||
| ctx.db.query( | |||
| "DELETE FROM collections "+ | |||
| "WHERE id = $1", | |||
| [ctx.postData.data.id], | |||
| queryCallback | |||
| ); | |||
| } | |||
| function queryCallback(err, res) { | |||
| if (err) | |||
| return ctx.fail(err); | |||
| try { | |||
| wrench.rmdirSyncRecursive( | |||
| ctx.conf.dir.imgs+"/"+ | |||
| ctx.postData.data.id | |||
| ); | |||
| } catch (err) { | |||
| return ctx.fail(err); | |||
| } | |||
| ctx.succeed(); | |||
| } | |||
| } | |||
| @@ -10,7 +10,7 @@ module.exports = function(ctx) { | |||
| data.collectionId = parseInt(data.collectionId); | |||
| if (data.collectionId !== ctx.session.collectionId) | |||
| if (data.lastCollectionId !== ctx.session.collectionId) | |||
| return ctx.fail("You don't own that collection."); | |||
| //We want all extensions to be lower case. | |||
| @@ -19,7 +19,7 @@ module.exports = function(ctx) { | |||
| ctx.db.query( | |||
| "INSERT INTO images (name, description, extension, collection_id) "+ | |||
| "VALUES ($1, $2, $3, $4) "+ | |||
| "RETURNING id", | |||
| "RETURNING id, collection_id", | |||
| [data.name, data.description, data.extension, data.collectionId], | |||
| queryCallback | |||
| ); | |||
| @@ -30,10 +30,16 @@ module.exports = function(ctx) { | |||
| return ctx.fail(err); | |||
| var id = res.rows[0].id; | |||
| var collectionId = res.rows[0].collection_id; | |||
| var file = ctx.postData.files.file; | |||
| var readStream = fs.createReadStream(file.path); | |||
| var writeStream = fs.createWriteStream(ctx.conf.dir.imgs+"/"+id); | |||
| var writeStream = fs.createWriteStream( | |||
| ctx.conf.dir.imgs+"/"+ | |||
| collectionId+"/"+ | |||
| id | |||
| ); | |||
| readStream.pipe(writeStream); | |||
| readStream.on("end", function() { | |||
| @@ -41,6 +41,7 @@ form label { | |||
| border: 1px solid #CCC; | |||
| border-radius: 4px; | |||
| padding: 10px !important; | |||
| margin-bottom: 10px; | |||
| } | |||
| .title { | |||
| @@ -1,8 +1,10 @@ | |||
| var fs = require("fs"); | |||
| module.exports = function(ctx) { | |||
| var id = ctx.query.replace(/\..*/, ""); | |||
| if (!id) | |||
| var q = ctx.query.replace(/\..*/, ""); | |||
| var collection = parseInt(q.split("/")[0]); | |||
| var id = parseInt(q.split("/")[1]); | |||
| if (!id || !collection) | |||
| return ctx.err404(); | |||
| ctx.res.setHeader( | |||
| @@ -10,7 +12,11 @@ module.exports = function(ctx) { | |||
| "public, max-age="+ctx.conf.cache_max_age_images | |||
| ); | |||
| var readStream = fs.createReadStream(ctx.conf.dir.imgs+"/"+id); | |||
| var readStream = fs.createReadStream( | |||
| ctx.conf.dir.imgs+"/"+ | |||
| collection+"/"+ | |||
| id | |||
| ); | |||
| readStream.pipe(ctx.res); | |||
| readStream.on("error", function(err){ | |||
| @@ -74,12 +74,21 @@ $(document).on("ready", function() { | |||
| var collectionId = res.id; | |||
| //Go to collection once files are uploaded | |||
| //Go to collection once files are uploaded, or | |||
| //delete the collection if it failed | |||
| var a = util.async(files.length, function(res) { | |||
| if (res.error) | |||
| util.redirect("/", 5000); | |||
| else | |||
| if (res.error) { | |||
| util.api("collection_delete", { | |||
| id: collectionId | |||
| }, function(err, res) { | |||
| if (err) | |||
| return util.error(err); | |||
| util.redirect("/", 5000); | |||
| }); | |||
| } else { | |||
| util.redirect("/view?"+collectionId); | |||
| } | |||
| }); | |||
| //Loop through files, uploading them | |||
| @@ -2,4 +2,27 @@ $(document).on("ready", function() { | |||
| $("#collections .date-created").each(function() { | |||
| this.innerHTML = util.dateToString(new Date(this.innerHTML)); | |||
| }); | |||
| var collections = []; | |||
| $("#collections .collection").each(function() { | |||
| collections[this.getAttribute("data-id")] = $(this); | |||
| }); | |||
| $("#collections .delete").on("click", function(evt) { | |||
| var id = evt.target.getAttribute("data-id"); | |||
| var collection = collections[id]; | |||
| var name = collection.children(".name").html(); | |||
| if (!confirm("Are you sure you want to delete collection "+name+"?")) | |||
| return; | |||
| util.api("collection_delete", { | |||
| id: id | |||
| }, function(err, res) { | |||
| if (err) | |||
| return util.error(err); | |||
| collection.remove(); | |||
| }); | |||
| }); | |||
| }); | |||
| @@ -20,8 +20,8 @@ | |||
| } | |||
| .collection .date-created { | |||
| width: 45%; | |||
| width: 190px; | |||
| } | |||
| .collection .name { | |||
| width: calc(55% - 43px); | |||
| width: calc(100% - 43px - 190px); | |||
| } | |||
| @@ -23,6 +23,7 @@ module.exports = function(ctx) { | |||
| res.rows.forEach(function(row) { | |||
| images += ctx.template("image", { | |||
| title: row.name, | |||
| collection: id, | |||
| id: row.id, | |||
| extension: row.extension, | |||
| description: row.description | |||