From 82e5875e521298c98822e885b5ac5e2be6c5a63b Mon Sep 17 00:00:00 2001 From: James Ketrenos Date: Thu, 11 Oct 2018 16:59:38 -0700 Subject: [PATCH] Delete works Signed-off-by: James Ketrenos --- frontend/elements/photo-thumbnail.html | 9 ++ frontend/src/ketr-photos/ketr-photos.html | 134 ++++++++++------ server/app.js | 2 +- server/routes/photos.js | 182 ++++++++++++++++++++++ server/scanner.js | 28 ++-- 5 files changed, 293 insertions(+), 62 deletions(-) diff --git a/frontend/elements/photo-thumbnail.html b/frontend/elements/photo-thumbnail.html index 9b39ada..97e087a 100755 --- a/frontend/elements/photo-thumbnail.html +++ b/frontend/elements/photo-thumbnail.html @@ -30,6 +30,12 @@ @apply --photo-thumbnail; } + :host([disabled]) { + opacity: 0.5; + pointer-events: none; + border: 3px solid red; + } + #info { position: absolute; padding: 0.5em; @@ -75,6 +81,9 @@ Polymer({ is: "photo-thumbnail", properties: { + "disabled": { + reflectToAttribute: true + }, "item": { type: Object }, diff --git a/frontend/src/ketr-photos/ketr-photos.html b/frontend/src/ketr-photos/ketr-photos.html index 0af12c9..7f4c8f7 100755 --- a/frontend/src/ketr-photos/ketr-photos.html +++ b/frontend/src/ketr-photos/ketr-photos.html @@ -850,7 +850,7 @@ } else { index = length >> 1; } - + var pos = this.checkPosition(index), last = -1; while (pos != 0 && last != index) { last = index; /* safety escape in case the DOM changed and nothing matches */ @@ -1222,6 +1222,9 @@ if (this.mode == "duplicates") { actions.unshift("text-format"); } + if (this.mode == "trash") { + actions.unshift("undo"); + } thumbnail.actions = actions; thumbnail.addEventListener("action", this._imageAction.bind(this)); } @@ -1239,56 +1242,99 @@ this.processing = false; }, + _removeImageAfterFetch: function(thumbnail, error, xhr) { + thumbnail.disabled = false; + if (error) { + console.log("Unable to take action on photo: " + error); + return; + } + thumbnail.style.pointerEvents = "none"; + thumbnail.style.opacity = 0; + + this.async(function(thumbnail) { + var parent = thumbnail.parentElement; + + var i = this.thumbnails.indexOf(thumbnail); + if (i != -1) { + if (thumbnail.visible) { + thumbnail.visible = false; + /* This changes the index of all thumbnails, so invalidate the entire + * visibleThumbs list */ + this.visibleThumbs = []; + } + this.thumbnails.splice(i, 1); + this.notifyPath("thumbnails.length"); + } + + Polymer.dom(parent).removeChild(thumbnail); + + if (parent.querySelectorAll("photo-thumbnail").length != 0) { + this.triggerVisibilityChecks(); + return; + } + + /* There are no more thumbnails, so look up the document ancestors + * until we find the element with the date-line class, and then + * remove it from it's parent */ + while (parent && !parent.classList.contains("date-line")) { + parent = parent.parentElement; + } + if (!parent) { + this.triggerVisibilityChecks(); + return; + } + + parent.style.opacity = 0; + + this.async(function(parent) { + Polymer.dom(this.$.thumbnails).removeChild(parent); + this.triggerVisibilityChecks(); + }.bind(this, parent), 250); + }.bind(this, thumbnail), 250); + }, + _imageAction: function(event) { switch (event.detail) { - case "delete": /* Delete image */ - var thumbnail = event.currentTarget; - thumbnail.style.opacity = 0; - thumbnail.style.pointerEvents = "none"; + case "undo": /* Undelete an image */ + var thumbnail = event.currentTarget, params = {}; + thumbnail.disabled = true; - this.async(function(thumbnail) { - var parent = thumbnail.parentElement; + params.undelete = 1; - for (var i = 0; i < this.thumbnails.length; i++) { - if (this.thumbnails[i] == thumbnail) { - console.log("Found thumbnail", this.thumbnails.length); - if (thumbnail.visible) { - console.log("Removing from visible list"); - thumbnail.visible = false; - var j = this.visibleThumbs.indexOf(i); - if (j != -1) { - console.log("Removing visible thumb", this.visibleThumbs.length); - this.visibleThumbs.splice(j, 1); - } - } - this.thumbnails.splice(i, 1); - this.notifyPath("thumbnails.length"); - break; - } - } - - Polymer.dom(parent).removeChild(thumbnail); - - if (parent.querySelectorAll("photo-thumbnail").length == 0) { - /* There are no more thumbnails, so look up the document ancestors - * until we find the element with the date-line class, and then - * remove it from it's parent */ - while (parent && !parent.classList.contains("date-line")) { - parent = parent.parentElement; - } - if (!parent) { - return; - } - parent.style.opacity = 0; - this.async(function(parent) { - Polymer.dom(this.$.thumbnails).removeChild(parent); - this.triggerVisibilityChecks(); - }.bind(this, parent), 250); + var query = ""; + for (var key in params) { + if (!query) { + query = "?"; } else { - this.triggerVisibilityChecks(); + query += "&"; } - }.bind(this, thumbnail), 250); + query += encodeURIComponent(key) + "=" + encodeURIComponent(params[key]); + } + window.fetch("api/v1/photos/" + thumbnail.item.id + query, + this._removeImageAfterFetch.bind(this, thumbnail), {}, "PUT"); 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; + } + + 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"); + break; + case "text-format": /* Rename this image */ break; } diff --git a/server/app.js b/server/app.js index 1e22f51..c4f899a 100755 --- a/server/app.js +++ b/server/app.js @@ -19,7 +19,7 @@ const express = require("express"), require("./console-line.js"); /* Monkey-patch console.log with line numbers */ -const picturesPath = config.get("picturesPath").replace(/\/$/, ""), +const picturesPath = config.get("picturesPath").replace(/\/$/, "") + "/", serverConfig = config.get("server"); let basePath = config.get("basePath"); diff --git a/server/routes/photos.js b/server/routes/photos.js index ef8f5ef..2373c7a 100755 --- a/server/routes/photos.js +++ b/server/routes/photos.js @@ -13,6 +13,188 @@ require("../db/photos").then(function(db) { }); const router = express.Router(); +const picturesPath = config.get("picturesPath").replace(/\/$/, "") + "/"; + +const unlink = function (_path) { + if (_path.indexOf(picturesPath.replace(/\/$/, "")) == 0) { + _path = _path.substring(picturesPath.length); + } + + let path = picturesPath + _path; + + return new Promise(function (resolve, reject) { + fs.unlink(path, function (error, stats) { + if (error) { + return reject(error); + } + return resolve(stats); + }); + }); +} + +router.put("/:id", function(req, res/*, next*/) { + if (!req.user.maintainer) { + return res.status(401).send("Unauthorized to delete 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; + } + } + if (!query) { + return res.status(400).send("Invalid request"); + } + + return photoDB.sequelize.query(query, { + replacements: replacements + }).then(function() { + return res.status(200).send(req.params.id + " updated."); + }); +}); + +router.delete("/:id", function(req, res/*, next*/) { + if (!req.user.maintainer) { + return res.status(401).send("Unauthorized to delete photos."); + } + + const replacements = { + id: req.params.id + }; + + console.log("DELETE /" + replacements.id, req.query); + + 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: replacements, + type: photoDB.Sequelize.QueryTypes.SELECT, + raw: true + }).then(function(photos) { + if (photos.length == 0) { + res.status(404).send("Unable to find photo " + req.params.id); + return true; + } + + const photo = photos[0]; + if (!req.query.permanent) { + return photoDB.sequelize.query("UPDATE photos SET deleted=1 WHERE id=:id", { + replacements: replacements + }).then(function() { + return false; + }); + } + + /* Delete the asset from disk and the DB + * 1. Look if there are duplicates. + * 2. Update all other duplicates to be duplicates of the first image that remains + * 3. If no duplicates, DELETE the entry from photohashes + * 4. If there are duplicates, update the HASH entry to point to the first image that remains + * 5. Delete the entry from photos + * 6. Delete the scaled, thumb, and original from disk + */ + return photoDB.sequelize.transaction(function(transaction) { + return photoDB.sequelize.query("SELECT id FROM photos WHERE duplicate=:id", { + replacements: photo, + type: photoDB.Sequelize.QueryTypes.SELECT, + raw: true + }).then(function(duplicates) { + if (!duplicates.length) { + return null; + } + + let first = duplicates.shift(); + let needsUpdate = []; + duplicates.forEach(function(duplicate) { + needsUpdate.push(duplicate.id); + }); + + if (!needsUpdate.length) { + return first; + } + + // 2. Update all other duplicates to be duplicates of the first image that remains + console.log("Updating " + needsUpdate + " to point to " + first.id); + return photoDB.sequelize.query( + "UPDATE photos SET duplicate=:first WHERE id IN (:needsUpdate)", { + replacements: { + first: first.id, + needsUpdate: needsUpdate + }, + transaction: transaction + }).then(function() { + return first; + }); + }).then(function(first) { + if (!first) { + console.log("Deleting "+ photo.id + " from photohash."); + // 3. If no duplicates, DELETE the entry from photohashes + return photoDB.sequelize.query( + "DELETE FROM photohashes WHERE photoId=:photo", { + replacements: { + photo: photo.id + }, + transaction: transaction + }); + } + console.log("Updating photohash for " + photo.id + " to point to " + first.id); + // 4. If there are duplicates, update the HASH entry to point to the first image that remains + return photoDB.sequelize.query( + "UPDATE phothashes SET photoId=:first WHERE photoId=:photo", { + replacements: { + first: first.id, + photo: photo.id + }, + transaction: transaction + }); + }).then(function() { + console.log("Deleting " + photo.path + photo.filename + " from DB."); + // 5. Delete the entry from photos + return photoDB.sequelize.query("DELETE FROM photos WHERE id=:id", { + replacements: replacements, + transaction: transaction + }); + }).then(function() { + // 6. Delete the scaled, thumb, and original from disk + console.log("Deleting " + photo.path + photo.filename + " from disk."); + return unlink(photo.path + "thumbs/scaled/" + photo.filename).then(function() { + return unlink(photo.path + "thumbs/" + photo.filename).then(function() { + return unlink(photo.path + photo.filename); + }); + }); + }); + }).then(function() { + return false; + }); + }).then(function(sent) { + if (sent) { + return; + } + + return res.status(200).send({ + id: req.params.id, + deleted: true, + permanent: (req.query && req.query.permanent) || false + }); + }).catch(function(error) { + console.log(error); + return res.status(500).send("Unable to delete photo " + req.params.id + ": " + error); + }); +}); /* Each photos has: diff --git a/server/scanner.js b/server/scanner.js index bc34019..72a6ef0 100755 --- a/server/scanner.js +++ b/server/scanner.js @@ -225,7 +225,9 @@ function processBlock(items) { setStatus("Duplicate asset: " + "'" + asset.album.path + asset.filename + "' is a copy of " + "'" + results[0].path + results[0].filename + "'"); - duplicates.push(asset); + if (asset.duplicate != results[0].photoId) { + duplicates.push(asset); + } return null; } @@ -394,22 +396,14 @@ function processBlock(items) { throw error; }).then(function() { setStatus("Completed processing queue. Marking " + duplicates.length + " duplicates."); - let dups = []; - duplicates.forEach(function(asset) { - /* If not already marked as a duplicate, mark it. */ - if (!asset.duplicate) { - dups.push(asset.id); - } - }); - - if (dups.length == 0) { - return; - } - - return photoDB.sequelize.query("UPDATE photos SET duplicate=1,modified=CURRENT_TIMESTAMP,scanned=CURRENT_TIMESTAMP WHERE id IN (:dups)", { - replacements: { - dups: dups - } + return photoDB.sequelize.transaction(function(transaction) { + return Promise.mapSeries(duplicates, function(asset) { + return photoDB.sequelize.query("UPDATE photos " + + "SET duplicate=:duplicate,modified=CURRENT_TIMESTAMP,scanned=CURRENT_TIMESTAMP WHERE id=:id", { + replacements: asset, + transaction: transaction + }); + }); }); }).then(function() { setStatus("Looking for removed assets");