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