diff --git a/frontend/elements/photo-thumbnail.html b/frontend/elements/photo-thumbnail.html
index a31a023..4a3b8d3 100755
--- a/frontend/elements/photo-thumbnail.html
+++ b/frontend/elements/photo-thumbnail.html
@@ -46,7 +46,7 @@
@@ -336,6 +338,21 @@
On [[memoryDate]], there have been [[add(thumbnails.length,pendingPhotos.length)]] photos taken over [[years.length]] year(s).
+
+
Trash
+
There are [[add(thumbnails.length,pendingPhotos.length)]] photos in the trash.
+
Do you want to purge the trash?
+
+
+
Duplicate names
+
There are [[add(thumbnails.length,pendingPhotos.length)]] photos which may be duplicates
+ based on either their name.
+
Look for duplicates in each file-name pair. If they are not the same,
+ tap on the photo and that one will be renamed
+ on the server by adding four letters from the image's signature to the name.
+
If they are duplicates, you can tap to move the
+ photo to the trash.
+
[[item.name]] /
@@ -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 = "" + ago + "
" + window.moment(datetime, "YYYY-MM-DD").calendar(null, { sameElse: "MMM DD, YYYY" }).replace(/ at.*/, "") + "";
+ 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 = "" + ago + "
" + window.moment(datetime, "YYYY-MM-DD").calendar(null, { sameElse: "MMM DD, YYYY" }).replace(/ at.*/, "") + "";
+ } 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;
}
diff --git a/server/app.js b/server/app.js
index a631adb..acb3d0e 100755
--- a/server/app.js
+++ b/server/app.js
@@ -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"));
diff --git a/server/db/photos.js b/server/db/photos.js
index 4239f74..7f49779 100755
--- a/server/db/photos.js
+++ b/server/db/photos.js
@@ -60,6 +60,7 @@ function init() {
taken: Sequelize.DATE,
width: Sequelize.INTEGER,
height: Sequelize.INTEGER,
+ size: Sequelize.INTEGER,
duplicate: {
type: Sequelize.BOOLEAN,
defaultValue: 0
diff --git a/server/routes/photos.js b/server/routes/photos.js
index d0656ea..e44a581 100755
--- a/server/routes/photos.js
+++ b/server/routes/photos.js
@@ -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;
diff --git a/server/routes/users.js b/server/routes/users.js
index 070fd7c..5b2c8d0 100755
--- a/server/routes/users.js
+++ b/server/routes/users.js
@@ -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
+};
diff --git a/server/scanner.js b/server/scanner.js
index d75bde4..4938a55 100755
--- a/server/scanner.js
+++ b/server/scanner.js
@@ -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) {