Started adding maintainer modes for managing duplicates, deletions, trash, etc.

Signed-off-by: James Ketrenos <james_git@ketrenos.com>
This commit is contained in:
James Ketr 2018-10-06 16:00:02 -07:00
parent a930d27860
commit 064e228226
7 changed files with 302 additions and 155 deletions

View File

@ -46,7 +46,7 @@
</style>
<div class="layout vertical start">
<div>[[item.name]]</div>
<div>[[item.name]] ([[item.id]])</div>
<div>[[item.taken]]</div>
<div on-tap="_pathTap">[[item.path]]</div>
</div>

View File

@ -310,6 +310,8 @@
<!--paper-tab tab="time"><paper-icon-button icon="date-range"></paper-icon-button></paper-tab-->
<paper-tab tab="memories"><paper-icon-button icon="today"></paper-icon-button></paper-tab>
<paper-tab tab="albums"><paper-icon-button icon="folder"></paper-icon-button></paper-tab>
<paper-tab hidden$="[[!user.maintainer]]" tab="duplicates"><paper-icon-button icon="compare-arrows"></paper-icon-button></paper-tab>
<paper-tab hidden$="[[!user.maintainer]]" tab="trash"><paper-icon-button icon="delete"></paper-icon-button></paper-tab>
</paper-tabs>
<iron-pages id="pages" attr-for-selected="id" selected="[[mode]]" fallback-selection="loading">
<div id="loading"></div>
@ -336,6 +338,21 @@
<div>On <b>[[memoryDate]]</b>, there have been <b>[[add(thumbnails.length,pendingPhotos.length)]]</b> photos taken over <b>[[years.length]]</b> year(s).</div>
</div>
</div>
<div hidden$="[[!user.maintainer]]" id="trash" class="flex layout vertical">
<div><b>Trash</b></div>
<div>There are <b>[[add(thumbnails.length,pendingPhotos.length)]]</b> photos in the trash.</div>
<div>Do you want to purge the trash?</div>
</div>
<div hidden$="[[!user.maintainer]]" id="duplicates" class="flex layout vertical">
<div><b>Duplicate names</b></div>
<div>There are <b>[[add(thumbnails.length,pendingPhotos.length)]]</b> photos which may be duplicates
based on either their name.</div>
<div>Look for duplicates in each file-name pair. If they are not the same,
tap <iron-icon icon="text-format"></iron-icon> on the photo and that one will be renamed
on the server by adding four letters from the image's signature to the name.</div>
<div>If they are duplicates, you can tap <iron-icon icon="delete"></iron-icon> to move the
photo to the trash.</div>
</div>
<div id="albums" class="flex layout vertical">
<template is="dom-repeat" items="[[breadcrumb(path)]]">
<div tabindex="0" on-tap="loadPath">[[item.name]] /</div>
@ -468,10 +485,6 @@
type: Object,
value: null
},
order: {
type: String,
value: "by-date"
},
loading: Boolean,
pendingPhotos: {
type: Array,
@ -486,10 +499,6 @@
value: true,
reflectToAttribute: true
},
showAlbums: {
type: Boolean,
computed: "shouldShowAlbums(order)"
},
path: {
type: String,
value: ""
@ -575,10 +584,6 @@
this.date = "2016-" + window.moment(Math.ceil(Math.random() * 365), "DDD").format("MM-DD");
},
shouldShowAlbums: function(order) {
return order == "by-album";
},
login: function(event) {
if (this.loading) {
return;
@ -787,7 +792,7 @@
}
}
if (top) {
if (top && (this.mode == "memories" || this.mode == "albums")) {
var photo = top.item;
this.$.pager.style.opacity = 1;
var date = window.moment(new Date((photo.taken || photo.modified || photo.added).replace(/T.*/, " GMT")));
@ -1091,58 +1096,117 @@
throw error;
}
var dateBlock = this.root.querySelector("#date-" + datetime), thumbnails;
if (!dateBlock) {
dateBlock = document.createElement("div");
dateBlock.id = "date-" + datetime;
dateBlock.classList.add("date-line");
dateBlock.classList.add("layout");
dateBlock.classList.add("vertical");
var header = document.createElement("div");
header.classList.add("header");
header.classList.add("layout");
header.classList.add("center");
header.classList.add("horizontal");
var div = document.createElement("div");
div.classList.add("date");
if (this.mode == "memories") {
var ago = window.moment(datetime, "YYYY-MM-DD").fromNow();
ago = ago.charAt(0).toUpperCase() + ago.substr(1);
div.innerHTML = "<b>" + ago + "</b><br><span style='font-size: 0.8em;font-weight: normal'>" + window.moment(datetime, "YYYY-MM-DD").calendar(null, { sameElse: "MMM DD, YYYY" }).replace(/ at.*/, "") + "</span>";
if (this.mode == "duplicates") {
var name = photo.filename.replace(/\./g, "_"),
nameBlock = this.root.querySelector("#name-" + name), thumbnails;
if (!nameBlock) {
nameBlock = document.createElement("div");
nameBlock.id = "name-" + name;
nameBlock.classList.add("date-line");
nameBlock.classList.add("layout");
nameBlock.classList.add("vertical");
var header = document.createElement("div");
header.classList.add("header");
header.classList.add("layout");
header.classList.add("center");
header.classList.add("horizontal");
var div = document.createElement("div");
div.classList.add("date");
div.textContent = photo.filename;
Polymer.dom(nameBlock).appendChild(header);
Polymer.dom(header).appendChild(div);
thumbnails = document.createElement("div");
thumbnails.classList.add("thumbnails");
thumbnails.classList.add("layout");
thumbnails.classList.add("horizontal");
thumbnails.classList.add("wrap");
Polymer.dom(nameBlock).appendChild(thumbnails);
Polymer.dom(this.$.thumbnails).appendChild(nameBlock);
} else {
div.textContent = window.moment(datetime, "YYYY-MM-DD").calendar(null, { sameElse: "MMM DD, YYYY" }).replace(/ at.*/, "");
thumbnails = Polymer.dom(nameBlock).querySelector(".thumbnails");
}
} else if (this.mode == "trash") {
var trashBlock = this.root.querySelector("#trash-images"), thumbnails;
if (!trashBlock) {
trashBlock = document.createElement("div");
trashBlock.id = "trash-images";
trashBlock.classList.add("date-line");
trashBlock.classList.add("layout");
trashBlock.classList.add("vertical");
var header = document.createElement("div");
header.classList.add("header");
header.classList.add("layout");
header.classList.add("center");
header.classList.add("horizontal");
var div = document.createElement("div");
div.classList.add("date");
div.textContent = "Trash";
Polymer.dom(trashBlock).appendChild(header);
Polymer.dom(header).appendChild(div);
thumbnails = document.createElement("div");
thumbnails.classList.add("thumbnails");
thumbnails.classList.add("layout");
thumbnails.classList.add("horizontal");
thumbnails.classList.add("wrap");
Polymer.dom(trashBlock).appendChild(thumbnails);
Polymer.dom(this.$.thumbnails).appendChild(trashBlock);
} else {
thumbnails = Polymer.dom(trashBlock).querySelector(".thumbnails");
}
Polymer.dom(dateBlock).appendChild(header);
Polymer.dom(header).appendChild(div);
thumbnails = document.createElement("div");
thumbnails.classList.add("thumbnails");
thumbnails.classList.add("layout");
thumbnails.classList.add("horizontal");
thumbnails.classList.add("wrap");
Polymer.dom(dateBlock).appendChild(thumbnails);
Polymer.dom(this.$.thumbnails).appendChild(dateBlock);
} else {
thumbnails = Polymer.dom(dateBlock).querySelector(".thumbnails");
}
var dateBlock = this.root.querySelector("#date-" + datetime), thumbnails;
if (!dateBlock) {
dateBlock = document.createElement("div");
dateBlock.id = "date-" + datetime;
dateBlock.classList.add("date-line");
dateBlock.classList.add("layout");
dateBlock.classList.add("vertical");
var header = document.createElement("div");
header.classList.add("header");
header.classList.add("layout");
header.classList.add("center");
header.classList.add("horizontal");
var div = document.createElement("div");
div.classList.add("date");
if (this.mode == "memories") {
var ago = window.moment(datetime, "YYYY-MM-DD").fromNow();
ago = ago.charAt(0).toUpperCase() + ago.substr(1);
div.innerHTML = "<b>" + ago + "</b><br><span style='font-size: 0.8em;font-weight: normal'>" + window.moment(datetime, "YYYY-MM-DD").calendar(null, { sameElse: "MMM DD, YYYY" }).replace(/ at.*/, "") + "</span>";
} else {
div.textContent = window.moment(datetime, "YYYY-MM-DD").calendar(null, { sameElse: "MMM DD, YYYY" }).replace(/ at.*/, "");
}
Polymer.dom(dateBlock).appendChild(header);
Polymer.dom(header).appendChild(div);
thumbnails = document.createElement("div");
thumbnails.classList.add("thumbnails");
thumbnails.classList.add("layout");
thumbnails.classList.add("horizontal");
thumbnails.classList.add("wrap");
Polymer.dom(dateBlock).appendChild(thumbnails);
Polymer.dom(this.$.thumbnails).appendChild(dateBlock);
} else {
thumbnails = Polymer.dom(dateBlock).querySelector(".thumbnails");
}
if (this.mode == "albums") {
if (lastPath != photo.path) {
lastPath = photo.path;
var albumBlock = document.createElement("div");
albumBlock.classList.add("album-line");
albumBlock.classList.add("layout");
albumBlock.classList.add("horizontal");
var trail = this.breadcrumb(lastPath);
trail.forEach(function(crumb) {
var div = document.createElement("div");
div.path = crumb.path;
div.textContent = crumb.name + " /";
div.addEventListener("tap", this.pathTapped.bind(this));
albumBlock.appendChild(div);
}.bind(this));
if (this.order == "by-album") {
if (lastPath != photo.path) {
lastPath = photo.path;
var albumBlock = document.createElement("div");
albumBlock.classList.add("album-line");
albumBlock.classList.add("layout");
albumBlock.classList.add("horizontal");
var trail = this.breadcrumb(lastPath);
trail.forEach(function(crumb) {
var div = document.createElement("div");
div.path = crumb.path;
div.textContent = crumb.name + " /";
div.addEventListener("tap", this.pathTapped.bind(this));
albumBlock.appendChild(div);
}.bind(this));
var header = dateBlock.querySelector(".header");
Polymer.dom(header).appendChild(albumBlock);
var header = dateBlock.querySelector(".header");
Polymer.dom(header).appendChild(albumBlock);
}
}
}
@ -1188,9 +1252,6 @@
if (start) {
params.next = start;
}
if (this.sortOrder) {
params.sort = this.sortOrder;
}
for (var key in params) {
if (query == "") {
query = "?";
@ -1205,7 +1266,7 @@
path = mode;
if (mode == "time") {
path = "";
} else {
} else if (mode == "memories") {
path = "memories/" + (this.date.replace(/2016-/, "") || "");
}
}
@ -1323,6 +1384,7 @@
},
userChanged: function(user) {
console.log("User: ", user);
if (!this.firstRequest) {
this.mode = "loading";
return;
@ -1394,6 +1456,7 @@
this.$.toast.setAttribute("error", true);
this.$.toast.updateStyles();
this.$.toast.show();
console.log(xhr.responseText);
return;
}

View File

@ -28,7 +28,7 @@ config.get("smtp.sender");
let basePath = config.get("basePath");
let photoDB = null, userDB = null
let photoDB = null, userDB = null;
basePath = "/" + basePath.replace(/^\/+/, "").replace(/\/+$/, "") + "/";
if (basePath == "//") {
@ -53,6 +53,26 @@ app.use(bodyParser.urlencoded({
extended: false
}));
/* ******************************************************************************* */
/* Logging */
/* This runs before after cookie parsing, but before routes. If we set
* immediate: true on the morgan options, it happens before cookie parsing */
morgan.token('remote-user', function (req) {
return req.user ? req.user.username : "N/A";
});
const logSkipPaths = new RegExp("^" + basePath + "(" + [
"bower_components",
].join(")|(") + ")");
console.log(logSkipPaths);
app.use(morgan('common', {
skip: function (req) {
return logSkipPaths.exec(req.originalUrl);
}
}));
/* Logging */
/* ******************************************************************************* */
/* body-parser does not support text/*, so add support for that here */
app.use(function(req, res, next){
if (!req.is('text/*')) {
@ -110,7 +130,8 @@ const templates = {
].join("\n")
};
/* Allow loading of the app w/out being logged in */
/* Look for action-token URLs and process; this does not require a user to be logged
* in */
app.use(basePath, function(req, res, next) {
let match = req.url.match(/^\/([0-9a-f]+)$/);
if (!match) {
@ -197,9 +218,12 @@ app.use(basePath, function(req, res, next) {
});
});
/* Allow loading of the app w/out being logged in */
app.use(basePath, index);
app.use(basePath + "api/v1/users", require("./routes/users"));
/* Allow access to the 'users' API w/out being logged in */
const users = require("./routes/users");
app.use(basePath + "api/v1/users", users.router);
app.use(function(err, req, res, next) {
res.status(err.status || 500).json({
@ -208,68 +232,19 @@ app.use(function(err, req, res, next) {
});
});
/* Everything below here requires a successful authentication */
/* Check authentication */
app.use(basePath, function(req, res, next) {
if (!req.session || !req.session.userId) {
return res.status(401).send("Unauthorized");
}
if (req.session.userId == "LDAP") {
if (req.session.ldapUser) {
req.user = req.session.ldapUser;
return next();
}
req.session.userId = null;
req.session.ldapUser = null;
return res.status(401).send("Invalid LDAP session");
}
let query = "SELECT uid AS username,displayName,mailVerified,authenticated,memberSince AS name,mail " +
"FROM users WHERE id=:id";
return userDB.sequelize.query(query, {
replacements: {
id: req.session.userId
},
type: userDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(results) {
if (results.length != 1) {
return res.status(401).send("Invalid account");
}
req.user = results[0];
if (!req.user.authenticated) {
return res.status(401).send("Accout not authenticated.");
}
if (!req.user.mailVerified) {
return res.status(401).send("Account mail not verified.");
}
if (!config.has("restrictions")) {
return next();
}
let allowed = config.get("restrictions");
if (!Array.isArray(allowed)) {
allowed = [ allowed ];
}
for (let i = 0; i < allowed.length; i++) {
if (allowed[i] == req.user.username) {
return next();
}
}
console.log("Unauthorized (logged in) access by user: " + req.user.username);
return res.status(401).send("Unauthorized");
return users.getSessionUser(req).then(function(user) {
req.user = user;
return next();
}).catch(function(error) {
return res.status(401).send(error);
});
});
});
/* Everything below here requires a successful authentication */
app.use(basePath, express.static(picturesPath, { index: false }));
app.use(morgan("common"));
app.use(basePath + "api/v1/photos", require("./routes/photos"));
app.use(basePath + "api/v1/days", require("./routes/days"));
app.use(basePath + "api/v1/albums", require("./routes/albums"));

View File

@ -60,6 +60,7 @@ function init() {
taken: Sequelize.DATE,
width: Sequelize.INTEGER,
height: Sequelize.INTEGER,
size: Sequelize.INTEGER,
duplicate: {
type: Sequelize.BOOLEAN,
defaultValue: 0

View File

@ -23,7 +23,6 @@ const router = express.Router();
* photo info
*/
router.get("/memories/:date?", function(req, res/*, next*/) {
let limit = parseInt(req.query.limit) || 50,
id, cursor, index;
@ -96,6 +95,54 @@ router.get("/memories/:date?", function(req, res/*, next*/) {
});
});
router.get("/duplicates", function(req, res/*, next*/) {
let replacements = {};
return photoDB.sequelize.query(
"SELECT filename,COUNT(*) AS count FROM photos WHERE photos.duplicate!=1 AND photos.deleted!=1 GROUP BY filename HAVING count > 1", {
replacements: replacements,
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(duplicates) {
let filenames = [];
duplicates.forEach(function(duplicate) {
filenames.push(duplicate.filename);
});
replacements.filenames = filenames;
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 filename IN (:filenames) ORDER BY photos.filename", {
replacements: replacements,
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(photos) {
return res.status(200).json({
items: photos
});
});
}).catch(function(error) {
return Promise.reject(error);
});
});
router.get("/trash", function(req, res/*, next*/) {
return photoDB.sequelize.query(
"SELECT photos.*,albums.path AS path,(albums.path || photos.filename) AS filepath FROM photos " +
"LEFT JOIN albums ON albums.id=photos.albumId " +
"WHERE deleted=1 ORDER BY photos.filename", {
type: photoDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(photos) {
return res.status(200).json({
items: photos
});
});
});
router.get("/*", function(req, res/*, next*/) {
let limit = parseInt(req.query.limit) || 50,
id, cursor, index;
@ -167,4 +214,5 @@ router.get("/*", function(req, res/*, next*/) {
});
});
module.exports = router;

View File

@ -31,8 +31,10 @@ require("../db/users").then(function(db) {
router.get("/", function(req, res/*, next*/) {
console.log("/users/");
return getSessionUser(req).then(function(user) {
req.user = user;
return res.status(200).send(req.user);
return res.status(200).send(user);
}).catch(function(error) {
console.log("User not logged in: " + error);
return res.status(200).send({});
});
});
@ -185,33 +187,87 @@ router.post("/create", function(req, res) {
return res.status(200).send(user);
});
});
}).catch(function(error) {
console.log("Error creating account: ", error);
return res.status(401).send(error);
});
});
const getSessionUser = function(req) {
if (!req.session.userId) {
return Promise.resolve({});
}
if (req.session.userId == "LDAP") {
return Promise.resolve(req.session.ldapUser);
}
let query = "SELECT " +
"uid AS username,displayName,mailVerified,authenticated,memberSince AS name,mail " +
"FROM users WHERE id=:id";
return userDB.sequelize.query(query, {
replacements: {
id: req.session.userId
},
type: userDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(results) {
if (results.length != 1) {
return {};
return Promise.resolve().then(function() {
if (!req.session || !req.session.userId) {
throw "Unauthorized. You must be logged in.";
}
return results[0];
if (req.session.userId == "LDAP") {
if (req.session.ldapUser) {
return req.session.ldapUser;
}
req.session.userId = null;
req.session.ldapUser = null;
throw "Invalid LDAP session";
}
let query = "SELECT " +
"uid AS username,displayName,mailVerified,authenticated,memberSince AS name,mail " +
"FROM users WHERE id=:id";
return userDB.sequelize.query(query, {
replacements: {
id: req.session.userId
},
type: userDB.Sequelize.QueryTypes.SELECT,
raw: true
}).then(function(results) {
if (results.length != 1) {
throw "Invalid account.";
}
req.user = results[0];
if (!req.user.authenticated) {
throw "Accout not authenticated.";
}
if (!req.user.mailVerified) {
throw "Account mail not verified.";
}
});
}).then(function(user) {
if (!config.has("restrictions")) {
return user;
}
let allowed = config.get("restrictions");
if (!Array.isArray(allowed)) {
allowed = [ allowed ];
}
for (let i = 0; i < allowed.length; i++) {
if (allowed[i] == user.username) {
return user;
}
}
console.log("Unauthorized (logged in) access by user: " + req.user.username);
throw "Unauthorized access attempt to restricted album.";
}).then(function(user) {
if (config.has("maintainers")) {
let maintainers = config.get("maintainers");
if (maintainers.indexOf(user.username) != -1) {
user.maintainer = true;
}
}
return user;
}).then(function(user) {
/* Strip out any fields that shouldn't be there. The allowed fields are: */
let allowed = [
"maintainer", "username", "displayName", "mailVerified", "authenticated", "name", "mail"
];
for (let field in user) {
if (allowed.indexOf(field) == -1) {
delete user[field];
}
}
return user;
});
}
@ -293,4 +349,7 @@ router.get("/logout", function(req, res) {
res.status(200).send({});
});
module.exports = router;
module.exports = {
router,
getSessionUser
};

View File

@ -198,7 +198,7 @@ function processBlock(items) {
/* Sort to newest files to be processed first */
processing.sort(function(a, b) {
return a.stats.mtime - b.stats.mtime;
return b.stats.mtime - a.stats.mtime;
});
let toProcess = processing.length, lastMessage = moment();
@ -349,7 +349,7 @@ function processBlock(items) {
});
}).then(function() {
return photoDB.sequelize.query("UPDATE photos SET " +
"added=:added,modified=:modified,taken=:taken,width=:width,height=:height,scanned=CURRENT_TIMESTAMP " +
"added=:added,modified=:modified,taken=:taken,width=:width,height=:height,size=:size,scanned=CURRENT_TIMESTAMP " +
"WHERE id=:id", {
replacements: asset,
});
@ -520,6 +520,7 @@ function scanDir(parent, path) {
mtime: stats.mtime,
ctime: stats.ctime
},
size: stats.size,
album: album
});
});
@ -588,8 +589,8 @@ function findOrUpdateDBAsset(transaction, asset) {
}).then(function(results) {
if (results.length == 0) {
return photoDB.sequelize.query("INSERT INTO photos " +
"(albumId,filename,name) " +
"VALUES(:albumId,:filename,:name)", {
"(albumId,filename,name,size) " +
"VALUES(:albumId,:filename,:name,:size)", {
replacements: asset,
transaction: transaction
}).spread(function(results, metadata) {