diff --git a/frontend/elements/photo-thumbnail.html b/frontend/elements/photo-thumbnail.html index 97e087a..7cee369 100755 --- a/frontend/elements/photo-thumbnail.html +++ b/frontend/elements/photo-thumbnail.html @@ -81,6 +81,10 @@ Polymer({ is: "photo-thumbnail", properties: { + "unique": { + type: String, + value: "" + }, "disabled": { reflectToAttribute: true }, @@ -89,7 +93,7 @@ }, "thumbpath": { type: String, - computed: "safeItemThumbFilepath(item, base)" + computed: "safeItemThumbFilepath(item, base, unique)" }, "width": { type: Number @@ -130,11 +134,11 @@ this.style.height = width + "px"; }, - safeItemThumbFilepath: function(item, base) { + safeItemThumbFilepath: function(item, base, unique) { if (item === undefined|| base === undefined || item.path === undefined) { return ""; } - return base + item.path + "thumbs/" + item.filename; + return base + item.path + "thumbs/" + item.filename + (this.unique ? ("?" + this.unique) : ""); }, date: function(item) { @@ -154,6 +158,10 @@ event.preventDefault(); }, + reload: function() { + this.unique = parseInt(this.unique || 0) + 1; + }, + attached: function() { var base = document.querySelector("base"); if (base) { diff --git a/frontend/src/ketr-photos/ketr-photos.html b/frontend/src/ketr-photos/ketr-photos.html index 7f4c8f7..bdc99bf 100755 --- a/frontend/src/ketr-photos/ketr-photos.html +++ b/frontend/src/ketr-photos/ketr-photos.html @@ -10,6 +10,7 @@ + @@ -1221,9 +1222,11 @@ let actions = [ "delete" ]; if (this.mode == "duplicates") { actions.unshift("text-format"); - } - if (this.mode == "trash") { + } else if (this.mode == "trash") { actions.unshift("undo"); + } else { + actions.unshift("image:rotate-right"); + actions.unshift("image:rotate-left"); } thumbnail.actions = actions; thumbnail.addEventListener("action", this._imageAction.bind(this)); @@ -1293,49 +1296,65 @@ }.bind(this, thumbnail), 250); }, + undoAction: function(thumbnail) { + var params = {}; + thumbnail.disabled = true; + window.fetch("api/v1/photos/" + thumbnail.item.id + "?a=undelete", + this._removeImageAfterFetch.bind(this, thumbnail), {}, "PUT"); + }, + + deleteAction: function(photo) { + var thumbnail = event.currentTarget, params = {}; + thumbnail.disabled = true; + var query = ""; + if (this.mode == "trash") { + console.log("TODO: Prompt user 'Are you sure?' ?"); + query += "?permanent=1"; + } + window.fetch("api/v1/photos/" + thumbnail.item.id + query, + this._removeImageAfterFetch.bind(this, thumbnail), {}, "DELETE"); + }, + + renameAction: function(thumbnail) { + return; + }, + + rotateAction: function(thumbnail, direction) { + thumbnail.disabled = true; + window.fetch("api/v1/photos/" + thumbnail.item.id + "?a=rotate&direction=" + direction, + function(thumbnail, error, xhr) { + + thumbnail.disabled = false; + + if (error) { + console.log("Unable to take action on photo: " + error); + return; + } + + thumbnail.reload(); + }.bind(this, thumbnail), {}, "PUT"); + }, + _imageAction: function(event) { switch (event.detail) { case "undo": /* Undelete an image */ - var thumbnail = event.currentTarget, params = {}; - thumbnail.disabled = true; - - params.undelete = 1; - - var query = ""; - for (var key in params) { - if (!query) { - query = "?"; - } else { - query += "&"; - } - query += encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); - } - window.fetch("api/v1/photos/" + thumbnail.item.id + query, - this._removeImageAfterFetch.bind(this, thumbnail), {}, "PUT"); + this.undoAction(event.currentTarget); break; case "delete": /* Delete image */ - var thumbnail = event.currentTarget, params = {}; - thumbnail.disabled = true; - if (this.mode == "trash") { - console.log("TODO: Prompt user 'Are you sure?' ?"); - params.permanent = 1; - } + this.deleteAction(event.currentTarget); + break; - var query = ""; - for (var key in params) { - if (!query) { - query = "?"; - } else { - query += "&"; - } - query += encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); - } - window.fetch("api/v1/photos/" + thumbnail.item.id + query, - this._removeImageAfterFetch.bind(this, thumbnail), {}, "DELETE"); + case "image:rotate-left": + this.rotateAction(event.currentTarget, "left"); + break; + + case "image:rotate-right": + this.rotateAction(event.currentTarget, "right"); break; case "text-format": /* Rename this image */ + this.renameAction(event.currentTarget); break; } }, diff --git a/server/routes/photos.js b/server/routes/photos.js index 2373c7a..129a6c6 100755 --- a/server/routes/photos.js +++ b/server/routes/photos.js @@ -32,39 +32,144 @@ const unlink = function (_path) { }); } +const rename = function (_src, _dst) { + if (_src.indexOf(picturesPath.replace(/\/$/, "")) == 0) { + _src = _src.substring(picturesPath.length); + } + if (_dst.indexOf(picturesPath.replace(/\/$/, "")) == 0) { + _dst = _dst.substring(picturesPath.length); + } + + let src = picturesPath + _src, + dst = picturesPath + _dst; + + return new Promise(function (resolve, reject) { + fs.rename(src, dst, function (error, stats) { + if (error) { + return reject(error); + } + return resolve(stats); + }); + }); +} + + +const stat = function (_path) { + if (_path.indexOf(picturesPath.replace(/\/$/, "")) == 0) { + _path = _path.substring(picturesPath.length); + } + + let path = picturesPath + _path; + + return new Promise(function (resolve, reject) { + fs.stat(path, function (error, stats) { + if (error) { + return reject(error); + } + return resolve(stats); + }); + }); +} + +const sharp = require("sharp"), exif = require("exif-reader"); + router.put("/:id", function(req, res/*, next*/) { if (!req.user.maintainer) { - return res.status(401).send("Unauthorized to delete photos."); + return res.status(401).send("Unauthorized to modify photos."); } const replacements = { id: req.params.id }; - let query = ""; - console.log("PUT /" + replacements.id, req.query); - for (let key in req.query) { - switch (key) { - case "undelete": - console.log("Undeleting " + req.params.id); - query = "UPDATE photos SET deleted=0 WHERE id=:id"; - break; - default: - continue; + switch (req.query.a) { + case "undelete": + console.log("Undeleting " + req.params.id); + return photoDB.sequelize.query("UPDATE photos SET deleted=0 WHERE id=:id", { + replacements: replacements + }).then(function() { + return res.status(200).send(req.params.id + " updated."); + }); + + case "rotate": + let direction = req.query.direction || "right"; + if (direction == "right") { + direction = 90; + } else { + direction = -90; } - } - if (!query) { - return res.status(400).send("Invalid request"); + return getPhoto(req.params.id).then(function(asset) { + if (!asset) { + return res.status(404).send(req.params.id + " not found."); + } + + let original = picturesPath + asset.path + asset.filename, + target = picturesPath + asset.path + ".tmp." + asset.filename, + thumb = picturesPath + asset.path + "thumbs/" + asset.filename, + scaled = picturesPath + asset.path + "thumbs/scaled/" + asset.filename; + + let tmp = asset.width; + asset.width = asset.height; + asset.height = tmp; + + asset.image = sharp(original); + return asset.image.rotate(direction).withMetadata().toFile(target).then(function() { + /*...*/ + }).then(function() { + return asset.image.rotate(direction).resize(256, 256).withMetadata().toFile(thumb); + }).then(function() { + return asset.image.resize(Math.min(1024, asset.width)).withMetadata().toFile(scaled); + }).then(function() { + return stat(target).then(function(stats) { + if (!stats) { + throw "Unable to find original file after attempting to rotate!"; + } + asset.size = stats.size; + return photoDB.sequelize.query("UPDATE photos SET " + + "modified=CURRENT_TIMESTAMP,width=:width,height=:height,size=:size,scanned=CURRENT_TIMESTAMP " + + "WHERE id=:id", { + replacements: asset + }).then(function() { + return unlink(original).then(function() { + return rename(target, original); + }); + }); + }); + }).then(function() { + sharp.cache(false); + sharp.cache(true); + return res.status(200).send(asset); + }); + }).catch(function(error) { + console.log(error); + return res.status(500).send(error); + }); } - return photoDB.sequelize.query(query, { - replacements: replacements - }).then(function() { - return res.status(200).send(req.params.id + " updated."); - }); + return res.status(400).send("Invalid request"); }); - + +const getPhoto = function(id) { + return photoDB.sequelize.query("SELECT " + + "photos.*,albums.path AS path,photohashes.hash,(albums.path || photos.filename) AS filepath FROM photos " + + "LEFT JOIN albums ON albums.id=photos.albumId " + + "LEFT JOIN photohashes ON photohashes.photoId=photos.id " + + "WHERE photos.id=:id", { + replacements: { + id: id + }, + type: photoDB.Sequelize.QueryTypes.SELECT, + raw: true + }).then(function(photos) { + if (photos.length == 0) { + return null; + } + + return photos[0]; + }); +} + router.delete("/:id", function(req, res/*, next*/) { if (!req.user.maintainer) { return res.status(401).send("Unauthorized to delete photos."); diff --git a/server/scanner.js b/server/scanner.js index 72a6ef0..6f6124c 100755 --- a/server/scanner.js +++ b/server/scanner.js @@ -333,7 +333,7 @@ function processBlock(items) { return; } - return image.resize(256, 256).toFile(dst).catch(function(error) { + return image.resize(256, 256).withMetadata().toFile(dst).catch(function(error) { setStatus("Error resizing image: " + dst + "\n" + error, "error"); throw error; }); @@ -344,7 +344,7 @@ function processBlock(items) { return; } - return image.resize(Math.min(1024, metadata.width)).toFile(dst).catch(function(error) { + return image.resize(Math.min(1024, metadata.width)).withMetadata().toFile(dst).catch(function(error) { setStatus("Error resizing image: " + dst + "\n" + error, "error"); throw error; });