From 4c516cac3859f2873fb8d159653cc02286c76085 Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Sun, 2 Oct 2022 22:09:02 +0200 Subject: [PATCH] Auth: Rename database tables and delete temporary tables #98 #782 Signed-off-by: Michael Mayer --- frontend/package-lock.json | 100 +++++++------- frontend/src/app/routes.js | 17 ++- frontend/src/component/navigation.vue | 2 +- frontend/src/component/photo/toolbar.vue | 1 + frontend/src/dialog/share/upload.vue | 2 +- frontend/src/dialog/upload.vue | 2 +- frontend/src/model/link.js | 8 +- frontend/src/pages/album/photos.vue | 16 +-- frontend/src/pages/photos.vue | 6 +- frontend/src/pages/settings/sync.vue | 2 +- internal/config/client_config.go | 15 +++ internal/config/config_customize.go | 1 - internal/entity/album.go | 7 + .../entity/{album_auth.go => album_user.go} | 32 ++--- internal/entity/auth_session.go | 8 +- internal/entity/auth_share.go | 2 +- internal/entity/auth_user.go | 2 +- internal/entity/auth_user_details.go | 2 +- internal/entity/auth_user_settings.go | 2 +- internal/entity/deprecated.go | 10 ++ internal/entity/entity_const.go | 1 + internal/entity/entity_tables.go | 4 +- internal/entity/photo.go | 123 +++++++++--------- internal/entity/photo_fixtures.go | 2 + .../entity/{photo_auth.go => photo_user.go} | 30 ++--- internal/entity/reaction.go | 24 ++-- internal/entity/reaction_fixtures.go | 18 +-- internal/entity/reaction_test.go | 2 +- internal/migrate/dialect_mysql.go | 2 +- internal/migrate/dialect_sqlite3.go | 2 +- internal/migrate/mysql/20220927-000200.sql | 2 +- internal/migrate/sqlite3/20220927-000200.sql | 2 +- internal/photoprism/index_mediafile.go | 4 + internal/search/albums.go | 19 +-- internal/search/photos.go | 28 ++-- internal/search/photos_geo.go | 26 ++-- internal/search/photos_results.go | 1 + internal/search/photos_test.go | 15 +++ 38 files changed, 302 insertions(+), 240 deletions(-) rename internal/entity/{album_auth.go => album_user.go} (54%) rename internal/entity/{photo_auth.go => photo_user.go} (58%) diff --git a/frontend/package-lock.json b/frontend/package-lock.json index 3e375967d..1aadefc0c 100644 --- a/frontend/package-lock.json +++ b/frontend/package-lock.json @@ -2472,9 +2472,9 @@ "integrity": "sha512-0PLQ6RUtZM0vO3teRfzGi4ltLUO5aO+kLgwh4Um3THSR03rpQWLTuRCkuO5A41ITzwdWeKdPHtSARuPkoo5pCQ==" }, "node_modules/@vvo/tzdb": { - "version": "6.68.0", - "resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.68.0.tgz", - "integrity": "sha512-gTYX0c/zfvdeywLFZdHJzxczXjZf4oZHRnkTemziyn4p0R+qoqdrRK5PqY2DFnH64YkFcpreNS1JbbnfWiMQgQ==" + "version": "6.69.0", + "resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.69.0.tgz", + "integrity": "sha512-L0BcVtrFG0a43mmiWo34yyQarsoAYbfD16q4yyO/A/r+fgcg3glAzFfqUxXGm7PLxt2SK9/f+jVtPZWNn2oSrg==" }, "node_modules/@webassemblyjs/ast": { "version": "1.11.1", @@ -4661,9 +4661,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.4.268", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.268.tgz", - "integrity": "sha512-PO90Bv++vEzdln+eA9qLg1IRnh0rKETus6QkTzcFm5P3Wg3EQBZud5dcnzkpYXuIKWBjKe5CO8zjz02cicvn1g==" + "version": "1.4.270", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.270.tgz", + "integrity": "sha512-KNhIzgLiJmDDC444dj9vEOpZEgsV96ult9Iff98Vanumn+ShJHd5se8aX6KeVxdc0YQeqdrezBZv89rleDbvSg==" }, "node_modules/emoji-regex": { "version": "8.0.0", @@ -6410,19 +6410,19 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" }, "node_modules/flow-parser": { - "version": "0.188.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.188.0.tgz", - "integrity": "sha512-yu/Tc3UOhE0lXZk02MO/69N8RLdZsBtz3R6pBS+GaHiaopAxRcfUHPkqOZnI2BtotaylYTLrZGkIqFC8KqVxXQ==", + "version": "0.188.1", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.188.1.tgz", + "integrity": "sha512-6PA9S1Dphgj5j61As1VkCw8xE0sBXd/ql/WYRQRT8kgORqclp5Eh/blAKvye2p/oIrCpWYwENs1khKqTZvd2UA==", "engines": { "node": ">=0.4.0" } }, "node_modules/flow-remove-types": { - "version": "2.188.0", - "resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.188.0.tgz", - "integrity": "sha512-bvNLQtjIFWJ4bkzZmLpIr4iZ83TsKv+S5Er9x/5OQMlNXvqGV0q/d/NIs/oZ7v9ZVnj0CObP2XgcfdduzKW4Dw==", + "version": "2.188.1", + "resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.188.1.tgz", + "integrity": "sha512-54QoA21/OJ7j4tmdvWJf1ujPRo/6YvHSxvnjsBi+PATwEZDETlCrDxpXvxSnwzeR1PQ71zy8BNbbvFRDE1FRMw==", "dependencies": { - "flow-parser": "^0.188.0", + "flow-parser": "^0.188.1", "pirates": "^3.0.2", "vlq": "^0.2.1" }, @@ -6982,9 +6982,9 @@ } }, "node_modules/hls.js": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.2.3.tgz", - "integrity": "sha512-CC/vHi82ldiiydIhliNI3whlcepRXxI2jdpd/KKb6lyEv+74e7lXs4cGk5PHfTLxZMKOj6+m5LX9VAbvV/r7AQ==" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.2.4.tgz", + "integrity": "sha512-yC3K79Kzq1W+OgjT12JxKMDXv9DbfvulppxmPBl7D04SaTyd2IwWk5eNASQV1mUaPlKbjr16yI9292qpSGo0ig==" }, "node_modules/hpkp": { "version": "2.0.0", @@ -8231,15 +8231,15 @@ } }, "node_modules/log4js": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.6.1.tgz", - "integrity": "sha512-J8VYFH2UQq/xucdNu71io4Fo+purYYudyErgBbswWKO0MC6QVOERRomt5su/z6d3RJSmLyTGmXl3Q/XjKCf+/A==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.7.0.tgz", + "integrity": "sha512-KA0W9ffgNBLDj6fZCq/lRbgR6ABAodRIDHrZnS48vOtfKa4PzWImb0Md1lmGCdO3n3sbCm/n1/WmrNlZ8kCI3Q==", "dependencies": { - "date-format": "^4.0.13", + "date-format": "^4.0.14", "debug": "^4.3.4", - "flatted": "^3.2.6", + "flatted": "^3.2.7", "rfdc": "^1.3.0", - "streamroller": "^3.1.2" + "streamroller": "^3.1.3" }, "engines": { "node": ">=8.0" @@ -9287,9 +9287,9 @@ "integrity": "sha512-sk96pUvpNwDV6PLrnhr68Uu1S5NohsxqLKz0GuracgrDo40BdF/r1RhHnjakUk6Q4Z0OKIybOQ7GevLKGN1iYw==" }, "node_modules/postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.4.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.17.tgz", + "integrity": "sha512-UNxNOLQydcOFi41yHNMcKRZ39NeXlr8AxGuZJsdub8vIb12fHzcq37DTU/QtbI6WLxNg2gF9Z+8qtRwTj1UI1Q==", "funding": [ { "type": "opencollective", @@ -15054,9 +15054,9 @@ "integrity": "sha512-0PLQ6RUtZM0vO3teRfzGi4ltLUO5aO+kLgwh4Um3THSR03rpQWLTuRCkuO5A41ITzwdWeKdPHtSARuPkoo5pCQ==" }, "@vvo/tzdb": { - "version": "6.68.0", - "resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.68.0.tgz", - "integrity": "sha512-gTYX0c/zfvdeywLFZdHJzxczXjZf4oZHRnkTemziyn4p0R+qoqdrRK5PqY2DFnH64YkFcpreNS1JbbnfWiMQgQ==" + "version": "6.69.0", + "resolved": "https://registry.npmjs.org/@vvo/tzdb/-/tzdb-6.69.0.tgz", + "integrity": "sha512-L0BcVtrFG0a43mmiWo34yyQarsoAYbfD16q4yyO/A/r+fgcg3glAzFfqUxXGm7PLxt2SK9/f+jVtPZWNn2oSrg==" }, "@webassemblyjs/ast": { "version": "1.11.1", @@ -16662,9 +16662,9 @@ } }, "electron-to-chromium": { - "version": "1.4.268", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.268.tgz", - "integrity": "sha512-PO90Bv++vEzdln+eA9qLg1IRnh0rKETus6QkTzcFm5P3Wg3EQBZud5dcnzkpYXuIKWBjKe5CO8zjz02cicvn1g==" + "version": "1.4.270", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.270.tgz", + "integrity": "sha512-KNhIzgLiJmDDC444dj9vEOpZEgsV96ult9Iff98Vanumn+ShJHd5se8aX6KeVxdc0YQeqdrezBZv89rleDbvSg==" }, "emoji-regex": { "version": "8.0.0", @@ -17931,16 +17931,16 @@ "integrity": "sha512-5nqDSxl8nn5BSNxyR3n4I6eDmbolI6WT+QqR547RwxQapgjQBmtktdP+HTBb/a/zLsbzERTONyUB5pefh5TtjQ==" }, "flow-parser": { - "version": "0.188.0", - "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.188.0.tgz", - "integrity": "sha512-yu/Tc3UOhE0lXZk02MO/69N8RLdZsBtz3R6pBS+GaHiaopAxRcfUHPkqOZnI2BtotaylYTLrZGkIqFC8KqVxXQ==" + "version": "0.188.1", + "resolved": "https://registry.npmjs.org/flow-parser/-/flow-parser-0.188.1.tgz", + "integrity": "sha512-6PA9S1Dphgj5j61As1VkCw8xE0sBXd/ql/WYRQRT8kgORqclp5Eh/blAKvye2p/oIrCpWYwENs1khKqTZvd2UA==" }, "flow-remove-types": { - "version": "2.188.0", - "resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.188.0.tgz", - "integrity": "sha512-bvNLQtjIFWJ4bkzZmLpIr4iZ83TsKv+S5Er9x/5OQMlNXvqGV0q/d/NIs/oZ7v9ZVnj0CObP2XgcfdduzKW4Dw==", + "version": "2.188.1", + "resolved": "https://registry.npmjs.org/flow-remove-types/-/flow-remove-types-2.188.1.tgz", + "integrity": "sha512-54QoA21/OJ7j4tmdvWJf1ujPRo/6YvHSxvnjsBi+PATwEZDETlCrDxpXvxSnwzeR1PQ71zy8BNbbvFRDE1FRMw==", "requires": { - "flow-parser": "^0.188.0", + "flow-parser": "^0.188.1", "pirates": "^3.0.2", "vlq": "^0.2.1" }, @@ -18329,9 +18329,9 @@ "integrity": "sha512-Io1zA2yOA1YJslkr+AJlWSf2yWFkKjvkcL9Ni1XSUqnGLr/qRQe2UI3Cn/J9MsJht7yEVCe0SscY1HgVMujbgg==" }, "hls.js": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.2.3.tgz", - "integrity": "sha512-CC/vHi82ldiiydIhliNI3whlcepRXxI2jdpd/KKb6lyEv+74e7lXs4cGk5PHfTLxZMKOj6+m5LX9VAbvV/r7AQ==" + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/hls.js/-/hls.js-1.2.4.tgz", + "integrity": "sha512-yC3K79Kzq1W+OgjT12JxKMDXv9DbfvulppxmPBl7D04SaTyd2IwWk5eNASQV1mUaPlKbjr16yI9292qpSGo0ig==" }, "hpkp": { "version": "2.0.0", @@ -19224,15 +19224,15 @@ } }, "log4js": { - "version": "6.6.1", - "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.6.1.tgz", - "integrity": "sha512-J8VYFH2UQq/xucdNu71io4Fo+purYYudyErgBbswWKO0MC6QVOERRomt5su/z6d3RJSmLyTGmXl3Q/XjKCf+/A==", + "version": "6.7.0", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.7.0.tgz", + "integrity": "sha512-KA0W9ffgNBLDj6fZCq/lRbgR6ABAodRIDHrZnS48vOtfKa4PzWImb0Md1lmGCdO3n3sbCm/n1/WmrNlZ8kCI3Q==", "requires": { - "date-format": "^4.0.13", + "date-format": "^4.0.14", "debug": "^4.3.4", - "flatted": "^3.2.6", + "flatted": "^3.2.7", "rfdc": "^1.3.0", - "streamroller": "^3.1.2" + "streamroller": "^3.1.3" } }, "loupe": { @@ -20010,9 +20010,9 @@ "integrity": "sha512-sk96pUvpNwDV6PLrnhr68Uu1S5NohsxqLKz0GuracgrDo40BdF/r1RhHnjakUk6Q4Z0OKIybOQ7GevLKGN1iYw==" }, "postcss": { - "version": "8.4.16", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.16.tgz", - "integrity": "sha512-ipHE1XBvKzm5xI7hiHCZJCSugxvsdq2mPnsq5+UF+VHCjiBvtDrlxJfMBToWaP9D5XlgNmcFGqoHmUn0EYEaRQ==", + "version": "8.4.17", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.4.17.tgz", + "integrity": "sha512-UNxNOLQydcOFi41yHNMcKRZ39NeXlr8AxGuZJsdub8vIb12fHzcq37DTU/QtbI6WLxNg2gF9Z+8qtRwTj1UI1Q==", "requires": { "nanoid": "^3.3.4", "picocolors": "^1.0.0", diff --git a/frontend/src/app/routes.js b/frontend/src/app/routes.js index 76f9a5c4c..164660a1c 100644 --- a/frontend/src/app/routes.js +++ b/frontend/src/app/routes.js @@ -82,10 +82,12 @@ export default [ component: Login, meta: { title: siteTitle, auth: false, hideNav: true }, beforeEnter: (to, from, next) => { - if (session.isUser()) { - next({ name: session.getHome() }); - } else { + if (!session.isUser()) { next(); + } else if (config.deny("photos", "search")) { + next({ name: "albums" }); + } else { + next({ name: "browse" }); } }, }, @@ -105,6 +107,15 @@ export default [ path: "/browse", component: Photos, meta: { title: siteTitle, icon: true, auth: true }, + beforeEnter: (to, from, next) => { + if (!session.isUser()) { + next({ name: "login" }); + } else if (config.deny("photos", "search")) { + next({ name: "albums" }); + } else { + next(); + } + }, }, { name: "all", diff --git a/frontend/src/component/navigation.vue b/frontend/src/component/navigation.vue index e9ecf7408..013c07c0a 100644 --- a/frontend/src/component/navigation.vue +++ b/frontend/src/component/navigation.vue @@ -332,7 +332,7 @@ - map + near_me diff --git a/frontend/src/component/photo/toolbar.vue b/frontend/src/component/photo/toolbar.vue index b8466865c..9dd457c1a 100644 --- a/frontend/src/component/photo/toolbar.vue +++ b/frontend/src/component/photo/toolbar.vue @@ -209,6 +209,7 @@ export default { {value: 'name', text: this.$gettext('Sort by file name')}, {value: 'similar', text: this.$gettext('Group by similarity')}, {value: 'relevance', text: this.$gettext('Most relevant')}, + {value: 'duration', text: this.$gettext('Duration')}, ], }, }; diff --git a/frontend/src/dialog/share/upload.vue b/frontend/src/dialog/share/upload.vue index 0c5746589..33a99afe1 100644 --- a/frontend/src/dialog/share/upload.vue +++ b/frontend/src/dialog/share/upload.vue @@ -184,7 +184,7 @@ export default { const params = { share: true, - count: 1000, + count: 2000, offset: 0, }; diff --git a/frontend/src/dialog/upload.vue b/frontend/src/dialog/upload.vue index c636080fa..0504bbeaf 100644 --- a/frontend/src/dialog/upload.vue +++ b/frontend/src/dialog/upload.vue @@ -134,7 +134,7 @@ export default { const params = { q: q, - count: 1000, + count: 2000, offset: 0, type: "album" }; diff --git a/frontend/src/model/link.js b/frontend/src/model/link.js index c7f70a334..b6a02cefc 100644 --- a/frontend/src/model/link.js +++ b/frontend/src/model/link.js @@ -33,7 +33,7 @@ export default class Link extends Model { getDefaults() { return { UID: "", - Share: "", + ShareUID: "", Slug: "", Token: "", Expires: 0, @@ -41,8 +41,8 @@ export default class Link extends Model { MaxViews: 0, Password: "", HasPassword: false, - CanComment: false, - CanEdit: false, + Comment: "", + Perm: 0, CreatedAt: "", ModifiedAt: "", }; @@ -74,7 +74,7 @@ export default class Link extends Model { return `${siteUrl}s/${token}/${this.Slug}`; } - return `${siteUrl}s/${token}/${this.Share}`; + return `${siteUrl}s/${token}/${this.ShareUID}`; } caption() { diff --git a/frontend/src/pages/album/photos.vue b/frontend/src/pages/album/photos.vue index 2a9963f9a..a0d3e6e91 100644 --- a/frontend/src/pages/album/photos.vue +++ b/frontend/src/pages/album/photos.vue @@ -172,14 +172,14 @@ export default { const photo = this.results[index]; - if (photo && photo.CellID && photo.CellID !== "zz") { - if (this.canSearchPlaces) { - this.$router.push({name: "places_query", params: {q: photo.CellID}}); - } else { - this.$router.push({name: "places_scope", params: {s: this.uid, q: photo.CellID}}); - } - } else { - this.$router.push({name: "places_scope", params: {s: this.uid, q: ""}}); + if (!photo) { + return; + } + + if (this.canSearchPlaces && photo.CellID && photo.CellID !== "zz") { + this.$router.push({name: "places_query", params: {q: photo.CellID}}); + } else if (this.uid) { + this.$router.push({name: "places_scope", params: {s: this.uid, q: photo.CellID}}); } }, editPhoto(index) { diff --git a/frontend/src/pages/photos.vue b/frontend/src/pages/photos.vue index 1a6ba861c..c000a2f32 100644 --- a/frontend/src/pages/photos.vue +++ b/frontend/src/pages/photos.vue @@ -234,12 +234,16 @@ export default { return "newest"; }, openLocation(index) { - if (!this.canSearchPlaces) { + if (!this.hasPlaces || !this.canSearchPlaces) { return; } const photo = this.results[index]; + if (!photo) { + return; + } + if (photo.CellID && photo.CellID !== "zz") { this.$router.push({name: "places_query", params: {q: photo.CellID}}); } else if (photo.Country && photo.Country !== "zz") { diff --git a/frontend/src/pages/settings/sync.vue b/frontend/src/pages/settings/sync.vue index 4248dcc15..8daf0d013 100644 --- a/frontend/src/pages/settings/sync.vue +++ b/frontend/src/pages/settings/sync.vue @@ -150,7 +150,7 @@ export default { return DateTime.fromISO(time).toLocaleString(DateTime.DATE_FULL); }, load() { - Account.search({count: 1000}).then(r => this.results = r.models); + Account.search({count: 2000}).then(r => this.results = r.models); }, remove(model) { this.model = model.clone(); diff --git a/internal/config/client_config.go b/internal/config/client_config.go index 0a556185e..7f4b0a583 100644 --- a/internal/config/client_config.go +++ b/internal/config/client_config.go @@ -257,6 +257,11 @@ func (c *Config) ClientPublic() ClientConfig { ReadOnly: c.ReadOnly(), Public: c.Public(), Experimental: c.Experimental(), + Albums: entity.Albums{}, + Cameras: entity.Cameras{}, + Lenses: entity.Lenses{}, + Countries: entity.Countries{}, + People: entity.People{}, Status: "", MapKey: "", Thumbs: Thumbs, @@ -328,6 +333,11 @@ func (c *Config) ClientShare() ClientConfig { UploadNSFW: c.UploadNSFW(), Public: c.Public(), Experimental: c.Experimental(), + Albums: entity.Albums{}, + Cameras: entity.Cameras{}, + Lenses: entity.Lenses{}, + Countries: entity.Countries{}, + People: entity.People{}, Colors: colors.All.List(), Thumbs: Thumbs, Status: c.Hub().Status, @@ -404,6 +414,11 @@ func (c *Config) ClientUser(withSettings bool) ClientConfig { UploadNSFW: c.UploadNSFW(), Public: c.Public(), Experimental: c.Experimental(), + Albums: entity.Albums{}, + Cameras: entity.Cameras{}, + Lenses: entity.Lenses{}, + Countries: entity.Countries{}, + People: entity.People{}, Colors: colors.All.List(), Thumbs: Thumbs, Status: c.Hub().Status, diff --git a/internal/config/config_customize.go b/internal/config/config_customize.go index 58da606f0..5788edb1c 100644 --- a/internal/config/config_customize.go +++ b/internal/config/config_customize.go @@ -6,7 +6,6 @@ import ( "strings" "github.com/photoprism/photoprism/internal/i18n" - "github.com/photoprism/photoprism/pkg/clean" "github.com/photoprism/photoprism/pkg/fs" "github.com/photoprism/photoprism/pkg/txt" diff --git a/internal/entity/album.go b/internal/entity/album.go index 10a4eb292..1a2259ef9 100644 --- a/internal/entity/album.go +++ b/internal/entity/album.go @@ -347,12 +347,19 @@ func FindAlbum(find Album) *Album { // Create search condition. stmt := UnscopedDb().Where("album_type = ?", find.AlbumType) + + // Search by slug and filter or title. if find.AlbumType != AlbumDefault && find.AlbumFilter != "" { stmt = stmt.Where("album_slug = ? OR album_filter = ?", find.AlbumSlug, find.AlbumFilter) } else { stmt = stmt.Where("album_slug = ? OR album_title LIKE ?", find.AlbumSlug, find.AlbumTitle) } + // Filter by creator if the album has not been published yet. + if find.CreatedBy != "" { + stmt = stmt.Where("published_at > ? OR created_by = ?", TimeStamp(), find.CreatedBy) + } + // Find first matching record. if stmt.First(&m).RecordNotFound() { return nil diff --git a/internal/entity/album_auth.go b/internal/entity/album_user.go similarity index 54% rename from internal/entity/album_auth.go rename to internal/entity/album_user.go index 006604468..a66bd78b2 100644 --- a/internal/entity/album_auth.go +++ b/internal/entity/album_user.go @@ -2,53 +2,49 @@ package entity import "github.com/photoprism/photoprism/internal/event" -// AlbumAuth represents the ownership of an Album and the corresponding permissions. -type AlbumAuth struct { +// AlbumUser represents the user and group ownership of an Album and the corresponding permissions. +type AlbumUser struct { UID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false" json:"UID" yaml:"UID"` - UserUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;index" json:"UserUID" yaml:"UserUID"` - TeamUID string `gorm:"type:VARBINARY(42);index" json:"TeamUID" yaml:"TeamUID"` + UserUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;index" json:"UserUID,omitempty" yaml:"UserUID,omitempty"` + TeamUID string `gorm:"type:VARBINARY(42);index" json:"TeamUID,omitempty" yaml:"TeamUID,omitempty"` Perm uint `json:"Perm,omitempty" yaml:"Perm,omitempty"` - Changed int64 `json:"-" yaml:"-"` } // TableName returns the database table name. -func (AlbumAuth) TableName() string { - return "albums_auth_dev" +func (AlbumUser) TableName() string { + return "albums_users" } -// NewAlbumAuth creates a new entity model. -func NewAlbumAuth(uid, userUid, teamUid string, perm uint) *AlbumAuth { - result := &AlbumAuth{ +// NewAlbumUser creates a new entity model. +func NewAlbumUser(uid, userUid, teamUid string, perm uint) *AlbumUser { + result := &AlbumUser{ UID: uid, UserUID: userUid, TeamUID: teamUid, Perm: perm, - Changed: TimeStamp().Unix(), } return result } // Create inserts a new record into the database. -func (m *AlbumAuth) Create() error { - m.Changed = TimeStamp().Unix() +func (m *AlbumUser) Create() error { return Db().Create(m).Error } // Save updates the record in the database or inserts a new record if it does not already exist. -func (m *AlbumAuth) Save() error { - m.Changed = TimeStamp().Unix() +func (m *AlbumUser) Save() error { return Db().Save(m).Error } // FirstOrCreateAlbumUser returns the existing record or inserts a new record if it does not already exist. -func FirstOrCreateAlbumUser(m *AlbumAuth) *AlbumAuth { - found := AlbumAuth{} +func FirstOrCreateAlbumUser(m *AlbumUser) *AlbumUser { + found := AlbumUser{} if err := Db().Where("uid = ?", m.UID).First(&found).Error; err == nil { return &found } else if err = m.Create(); err != nil { - event.AuditErr([]string{"photo %s", "failed to set owner and permissions", "%s"}, m.UID, err) + event.AuditErr([]string{"album %s", "failed to set owner and permissions", "%s"}, m.UID, err) return nil } diff --git a/internal/entity/auth_session.go b/internal/entity/auth_session.go index 6145d96f8..846f65b0a 100644 --- a/internal/entity/auth_session.go +++ b/internal/entity/auth_session.go @@ -29,9 +29,9 @@ type Sessions []Session type Session struct { ID string `gorm:"type:VARBINARY(2048);primary_key;auto_increment:false;" json:"ID" yaml:"ID"` Status int `json:"Status,omitempty" yaml:"-"` - AuthDomain string `gorm:"type:VARBINARY(253);default:'';" json:"-" yaml:"AuthDomain,omitempty"` AuthMethod string `gorm:"type:VARBINARY(128);default:'';" json:"-" yaml:"AuthMethod,omitempty"` AuthProvider string `gorm:"type:VARBINARY(128);default:'';" json:"-" yaml:"AuthProvider,omitempty"` + AuthDomain string `gorm:"type:VARBINARY(255);default:'';" json:"-" yaml:"AuthDomain,omitempty"` AuthScope string `gorm:"size:1024;default:'';" json:"-" yaml:"AuthScope,omitempty"` AuthID string `gorm:"type:VARBINARY(128);index;default:'';" json:"-" yaml:"AuthID,omitempty"` UserUID string `gorm:"type:VARBINARY(42);index;default:'';" json:"UserUID,omitempty" yaml:"UserUID,omitempty"` @@ -46,18 +46,18 @@ type Session struct { ClientIP string `gorm:"size:64;column:client_ip;" json:"-" yaml:"ClientIP,omitempty"` LoginIP string `gorm:"size:64;column:login_ip" json:"-" yaml:"-"` LoginAt time.Time `json:"-" yaml:"-"` - MaxAge time.Duration `json:"MaxAge,omitempty" yaml:"MaxAge,omitempty"` - Timeout time.Duration `json:"Timeout,omitempty" yaml:"Timeout,omitempty"` DataJSON json.RawMessage `gorm:"type:VARBINARY(4096);" json:"Data,omitempty" yaml:"Data,omitempty"` data *SessionData `gorm:"-"` RefID string `gorm:"type:VARBINARY(16);default:'';" json:"-" yaml:"-"` CreatedAt time.Time `json:"CreatedAt" yaml:"CreatedAt"` + MaxAge time.Duration `json:"MaxAge,omitempty" yaml:"MaxAge,omitempty"` UpdatedAt time.Time `json:"UpdatedAt" yaml:"UpdatedAt"` + Timeout time.Duration `json:"Timeout,omitempty" yaml:"Timeout,omitempty"` } // TableName returns the entity table name. func (Session) TableName() string { - return "auth_sessions_dev" + return "auth_sessions" } // NewSession creates a new session and returns it. diff --git a/internal/entity/auth_share.go b/internal/entity/auth_share.go index df9c7297a..ef85b558f 100644 --- a/internal/entity/auth_share.go +++ b/internal/entity/auth_share.go @@ -76,7 +76,7 @@ type Share struct { // TableName returns the entity table name. func (Share) TableName() string { - return "auth_shares_dev" + return "auth_users_shares" } // NewShare creates a new entity model. diff --git a/internal/entity/auth_user.go b/internal/entity/auth_user.go index cb735351b..a3686ca2e 100644 --- a/internal/entity/auth_user.go +++ b/internal/entity/auth_user.go @@ -76,7 +76,7 @@ type User struct { // TableName returns the entity table name. func (User) TableName() string { - return "auth_users_dev" + return "auth_users" } // NewUser creates a new user and returns it. diff --git a/internal/entity/auth_user_details.go b/internal/entity/auth_user_details.go index 7ab37cf76..5f3437f9f 100644 --- a/internal/entity/auth_user_details.go +++ b/internal/entity/auth_user_details.go @@ -50,7 +50,7 @@ type UserDetails struct { // TableName returns the entity table name. func (UserDetails) TableName() string { - return "auth_users_details_dev" + return "auth_users_details" } // NewUserDetails creates new user details. diff --git a/internal/entity/auth_user_settings.go b/internal/entity/auth_user_settings.go index a1464d99d..07ebe8d05 100644 --- a/internal/entity/auth_user_settings.go +++ b/internal/entity/auth_user_settings.go @@ -28,7 +28,7 @@ type UserSettings struct { // TableName returns the entity table name. func (UserSettings) TableName() string { - return "auth_users_settings_dev" + return "auth_users_settings" } // NewUserSettings creates new user preferences. diff --git a/internal/entity/deprecated.go b/internal/entity/deprecated.go index 9ff1ab67c..f067db9be 100644 --- a/internal/entity/deprecated.go +++ b/internal/entity/deprecated.go @@ -46,4 +46,14 @@ var DeprecatedTables = Deprecated{ "faces_dev8", "faces_dev9", "faces_dev10", + "reactions_dev", + "albums_users_dev", + "photos_users_dev", + "auth_sessions_dev", + "auth_shares_dev", + "auth_users_details_dev", + "auth_users_dev", + "auth_users_logins_dev", + "auth_users_settings_dev", + "auth_tokens_dev", } diff --git a/internal/entity/entity_const.go b/internal/entity/entity_const.go index 2dee282eb..66fb981aa 100644 --- a/internal/entity/entity_const.go +++ b/internal/entity/entity_const.go @@ -56,6 +56,7 @@ const ( const ( SortOrderDefault = "" SortOrderRelevance = "relevance" + SortOrderDuration = "duration" SortOrderCount = "count" SortOrderAdded = "added" SortOrderImported = "imported" diff --git a/internal/entity/entity_tables.go b/internal/entity/entity_tables.go index 84f28aa01..58c781592 100644 --- a/internal/entity/entity_tables.go +++ b/internal/entity/entity_tables.go @@ -28,7 +28,7 @@ var Entities = Tables{ FileShare{}.TableName(): &FileShare{}, FileSync{}.TableName(): &FileSync{}, Photo{}.TableName(): &Photo{}, - PhotoAuth{}.TableName(): &PhotoAuth{}, + PhotoUser{}.TableName(): &PhotoUser{}, Details{}.TableName(): &Details{}, Place{}.TableName(): &Place{}, Cell{}.TableName(): &Cell{}, @@ -36,7 +36,7 @@ var Entities = Tables{ Lens{}.TableName(): &Lens{}, Country{}.TableName(): &Country{}, Album{}.TableName(): &Album{}, - AlbumAuth{}.TableName(): &AlbumAuth{}, + AlbumUser{}.TableName(): &AlbumUser{}, PhotoAlbum{}.TableName(): &PhotoAlbum{}, Label{}.TableName(): &Label{}, Category{}.TableName(): &Category{}, diff --git a/internal/entity/photo.go b/internal/entity/photo.go index e2ff0584c..43256af8a 100644 --- a/internal/entity/photo.go +++ b/internal/entity/photo.go @@ -50,67 +50,68 @@ func MapKey(takenAt time.Time, cellId string) string { // Photo represents a photo, all its properties, and link to all its images and sidecar files. type Photo struct { - ID uint `gorm:"primary_key" yaml:"-"` - UUID string `gorm:"type:VARBINARY(64);index;" json:"DocumentID,omitempty" yaml:"DocumentID,omitempty"` - TakenAt time.Time `gorm:"type:DATETIME;index:idx_photos_taken_uid;" json:"TakenAt" yaml:"TakenAt"` - TakenAtLocal time.Time `gorm:"type:DATETIME;" yaml:"-"` - TakenSrc string `gorm:"type:VARBINARY(8);" json:"TakenSrc" yaml:"TakenSrc,omitempty"` - PhotoUID string `gorm:"type:VARBINARY(42);unique_index;index:idx_photos_taken_uid;" json:"UID" yaml:"UID"` - PhotoType string `gorm:"type:VARBINARY(8);default:'image';" json:"Type" yaml:"Type"` - TypeSrc string `gorm:"type:VARBINARY(8);" json:"TypeSrc" yaml:"TypeSrc,omitempty"` - PhotoTitle string `gorm:"type:VARCHAR(200);" json:"Title" yaml:"Title"` - TitleSrc string `gorm:"type:VARBINARY(8);" json:"TitleSrc" yaml:"TitleSrc,omitempty"` - PhotoDescription string `gorm:"type:VARCHAR(4096);" json:"Description" yaml:"Description,omitempty"` - DescriptionSrc string `gorm:"type:VARBINARY(8);" json:"DescriptionSrc" yaml:"DescriptionSrc,omitempty"` - PhotoPath string `gorm:"type:VARBINARY(1024);index:idx_photos_path_name;" json:"Path" yaml:"-"` - PhotoName string `gorm:"type:VARBINARY(255);index:idx_photos_path_name;" json:"Name" yaml:"-"` - OriginalName string `gorm:"type:VARBINARY(755);" json:"OriginalName" yaml:"OriginalName,omitempty"` - PhotoStack int8 `json:"Stack" yaml:"Stack,omitempty"` - PhotoFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"` - PhotoPrivate bool `json:"Private" yaml:"Private,omitempty"` - PhotoScan bool `json:"Scan" yaml:"Scan,omitempty"` - PhotoPanorama bool `json:"Panorama" yaml:"Panorama,omitempty"` - TimeZone string `gorm:"type:VARBINARY(64);" json:"TimeZone" yaml:"TimeZone,omitempty"` - PlaceID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"PlaceID" yaml:"-"` - PlaceSrc string `gorm:"type:VARBINARY(8);" json:"PlaceSrc" yaml:"PlaceSrc,omitempty"` - CellID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"CellID" yaml:"-"` - CellAccuracy int `json:"CellAccuracy" yaml:"CellAccuracy,omitempty"` - PhotoAltitude int `json:"Altitude" yaml:"Altitude,omitempty"` - PhotoLat float32 `gorm:"type:FLOAT;index;" json:"Lat" yaml:"Lat,omitempty"` - PhotoLng float32 `gorm:"type:FLOAT;index;" json:"Lng" yaml:"Lng,omitempty"` - PhotoCountry string `gorm:"type:VARBINARY(2);index:idx_photos_country_year_month;default:'zz'" json:"Country" yaml:"-"` - PhotoYear int `gorm:"index:idx_photos_ymd;index:idx_photos_country_year_month;" json:"Year" yaml:"Year"` - PhotoMonth int `gorm:"index:idx_photos_ymd;index:idx_photos_country_year_month;" json:"Month" yaml:"Month"` - PhotoDay int `gorm:"index:idx_photos_ymd" json:"Day" yaml:"Day"` - PhotoIso int `json:"Iso" yaml:"ISO,omitempty"` - PhotoExposure string `gorm:"type:VARBINARY(64);" json:"Exposure" yaml:"Exposure,omitempty"` - PhotoFNumber float32 `gorm:"type:FLOAT;" json:"FNumber" yaml:"FNumber,omitempty"` - PhotoFocalLength int `json:"FocalLength" yaml:"FocalLength,omitempty"` - PhotoQuality int `gorm:"type:SMALLINT" json:"Quality" yaml:"Quality,omitempty"` - PhotoFaces int `json:"Faces,omitempty" yaml:"Faces,omitempty"` - PhotoResolution int `gorm:"type:SMALLINT" json:"Resolution" yaml:"-"` - PhotoColor int16 `json:"Color" yaml:"-"` - CameraID uint `gorm:"index:idx_photos_camera_lens;default:1" json:"CameraID" yaml:"-"` - CameraSerial string `gorm:"type:VARBINARY(160);" json:"CameraSerial" yaml:"CameraSerial,omitempty"` - CameraSrc string `gorm:"type:VARBINARY(8);" json:"CameraSrc" yaml:"-"` - LensID uint `gorm:"index:idx_photos_camera_lens;default:1" json:"LensID" yaml:"-"` - Details *Details `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Details" yaml:"Details"` - Camera *Camera `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Camera" yaml:"-"` - Lens *Lens `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Lens" yaml:"-"` - Cell *Cell `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Cell" yaml:"-"` - Place *Place `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Place" yaml:"-"` - Keywords []Keyword `json:"-" yaml:"-"` - Albums []Album `json:"-" yaml:"-"` - Files []File `yaml:"-"` - Labels []PhotoLabel `yaml:"-"` - CreatedBy string `gorm:"type:VARBINARY(42);index" json:"CreatedBy,omitempty" yaml:"CreatedBy,omitempty"` - CreatedAt time.Time `yaml:"CreatedAt,omitempty"` - UpdatedAt time.Time `yaml:"UpdatedAt,omitempty"` - EditedAt *time.Time `yaml:"EditedAt,omitempty"` - PublishedAt *time.Time `sql:"index" json:"PublishedAt,omitempty" yaml:"PublishedAt,omitempty"` - CheckedAt *time.Time `sql:"index" yaml:"-"` - EstimatedAt *time.Time `json:"EstimatedAt,omitempty" yaml:"-"` - DeletedAt *time.Time `sql:"index" yaml:"DeletedAt,omitempty"` + ID uint `gorm:"primary_key" yaml:"-"` + UUID string `gorm:"type:VARBINARY(64);index;" json:"DocumentID,omitempty" yaml:"DocumentID,omitempty"` + TakenAt time.Time `gorm:"type:DATETIME;index:idx_photos_taken_uid;" json:"TakenAt" yaml:"TakenAt"` + TakenAtLocal time.Time `gorm:"type:DATETIME;" yaml:"-"` + TakenSrc string `gorm:"type:VARBINARY(8);" json:"TakenSrc" yaml:"TakenSrc,omitempty"` + PhotoUID string `gorm:"type:VARBINARY(42);unique_index;index:idx_photos_taken_uid;" json:"UID" yaml:"UID"` + PhotoType string `gorm:"type:VARBINARY(8);default:'image';" json:"Type" yaml:"Type"` + TypeSrc string `gorm:"type:VARBINARY(8);" json:"TypeSrc" yaml:"TypeSrc,omitempty"` + PhotoTitle string `gorm:"type:VARCHAR(200);" json:"Title" yaml:"Title"` + TitleSrc string `gorm:"type:VARBINARY(8);" json:"TitleSrc" yaml:"TitleSrc,omitempty"` + PhotoDescription string `gorm:"type:VARCHAR(4096);" json:"Description" yaml:"Description,omitempty"` + DescriptionSrc string `gorm:"type:VARBINARY(8);" json:"DescriptionSrc" yaml:"DescriptionSrc,omitempty"` + PhotoPath string `gorm:"type:VARBINARY(1024);index:idx_photos_path_name;" json:"Path" yaml:"-"` + PhotoName string `gorm:"type:VARBINARY(255);index:idx_photos_path_name;" json:"Name" yaml:"-"` + OriginalName string `gorm:"type:VARBINARY(755);" json:"OriginalName" yaml:"OriginalName,omitempty"` + PhotoStack int8 `json:"Stack" yaml:"Stack,omitempty"` + PhotoFavorite bool `json:"Favorite" yaml:"Favorite,omitempty"` + PhotoPrivate bool `json:"Private" yaml:"Private,omitempty"` + PhotoScan bool `json:"Scan" yaml:"Scan,omitempty"` + PhotoPanorama bool `json:"Panorama" yaml:"Panorama,omitempty"` + TimeZone string `gorm:"type:VARBINARY(64);" json:"TimeZone" yaml:"TimeZone,omitempty"` + PlaceID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"PlaceID" yaml:"-"` + PlaceSrc string `gorm:"type:VARBINARY(8);" json:"PlaceSrc" yaml:"PlaceSrc,omitempty"` + CellID string `gorm:"type:VARBINARY(42);index;default:'zz'" json:"CellID" yaml:"-"` + CellAccuracy int `json:"CellAccuracy" yaml:"CellAccuracy,omitempty"` + PhotoAltitude int `json:"Altitude" yaml:"Altitude,omitempty"` + PhotoLat float32 `gorm:"type:FLOAT;index;" json:"Lat" yaml:"Lat,omitempty"` + PhotoLng float32 `gorm:"type:FLOAT;index;" json:"Lng" yaml:"Lng,omitempty"` + PhotoCountry string `gorm:"type:VARBINARY(2);index:idx_photos_country_year_month;default:'zz'" json:"Country" yaml:"-"` + PhotoYear int `gorm:"index:idx_photos_ymd;index:idx_photos_country_year_month;" json:"Year" yaml:"Year"` + PhotoMonth int `gorm:"index:idx_photos_ymd;index:idx_photos_country_year_month;" json:"Month" yaml:"Month"` + PhotoDay int `gorm:"index:idx_photos_ymd" json:"Day" yaml:"Day"` + PhotoIso int `json:"Iso" yaml:"ISO,omitempty"` + PhotoExposure string `gorm:"type:VARBINARY(64);" json:"Exposure" yaml:"Exposure,omitempty"` + PhotoFNumber float32 `gorm:"type:FLOAT;" json:"FNumber" yaml:"FNumber,omitempty"` + PhotoFocalLength int `json:"FocalLength" yaml:"FocalLength,omitempty"` + PhotoQuality int `gorm:"type:SMALLINT" json:"Quality" yaml:"Quality,omitempty"` + PhotoFaces int `json:"Faces,omitempty" yaml:"Faces,omitempty"` + PhotoResolution int `gorm:"type:SMALLINT" json:"Resolution" yaml:"-"` + PhotoDuration time.Duration `json:"Duration,omitempty" yaml:"Duration,omitempty"` + PhotoColor int16 `json:"Color" yaml:"-"` + CameraID uint `gorm:"index:idx_photos_camera_lens;default:1" json:"CameraID" yaml:"-"` + CameraSerial string `gorm:"type:VARBINARY(160);" json:"CameraSerial" yaml:"CameraSerial,omitempty"` + CameraSrc string `gorm:"type:VARBINARY(8);" json:"CameraSrc" yaml:"-"` + LensID uint `gorm:"index:idx_photos_camera_lens;default:1" json:"LensID" yaml:"-"` + Details *Details `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Details" yaml:"Details"` + Camera *Camera `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Camera" yaml:"-"` + Lens *Lens `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Lens" yaml:"-"` + Cell *Cell `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Cell" yaml:"-"` + Place *Place `gorm:"association_autoupdate:false;association_autocreate:false;association_save_reference:false" json:"Place" yaml:"-"` + Keywords []Keyword `json:"-" yaml:"-"` + Albums []Album `json:"-" yaml:"-"` + Files []File `yaml:"-"` + Labels []PhotoLabel `yaml:"-"` + CreatedBy string `gorm:"type:VARBINARY(42);index" json:"CreatedBy,omitempty" yaml:"CreatedBy,omitempty"` + CreatedAt time.Time `yaml:"CreatedAt,omitempty"` + UpdatedAt time.Time `yaml:"UpdatedAt,omitempty"` + EditedAt *time.Time `yaml:"EditedAt,omitempty"` + PublishedAt *time.Time `sql:"index" json:"PublishedAt,omitempty" yaml:"PublishedAt,omitempty"` + CheckedAt *time.Time `sql:"index" yaml:"-"` + EstimatedAt *time.Time `json:"EstimatedAt,omitempty" yaml:"-"` + DeletedAt *time.Time `sql:"index" yaml:"DeletedAt,omitempty"` } // TableName returns the entity table name. diff --git a/internal/entity/photo_fixtures.go b/internal/entity/photo_fixtures.go index 11207a669..63f2f68ce 100644 --- a/internal/entity/photo_fixtures.go +++ b/internal/entity/photo_fixtures.go @@ -222,6 +222,7 @@ var PhotoFixtures = PhotoMap{ TakenAtLocal: time.Date(1990, 4, 18, 1, 0, 0, 0, time.UTC), TakenSrc: "meta", PhotoType: "video", + PhotoDuration: time.Hour * 2, TypeSrc: "", PhotoTitle: "", TitleSrc: "", @@ -644,6 +645,7 @@ var PhotoFixtures = PhotoMap{ TakenAtLocal: time.Date(2016, 11, 11, 11, 7, 18, 0, time.UTC), TakenSrc: "manual", PhotoType: "video", + PhotoDuration: time.Minute * 2, TypeSrc: "", PhotoTitle: "Title", TitleSrc: "", diff --git a/internal/entity/photo_auth.go b/internal/entity/photo_user.go similarity index 58% rename from internal/entity/photo_auth.go rename to internal/entity/photo_user.go index 5ffc4135f..49326ef78 100644 --- a/internal/entity/photo_auth.go +++ b/internal/entity/photo_user.go @@ -2,48 +2,44 @@ package entity import "github.com/photoprism/photoprism/internal/event" -// PhotoAuth represents the ownership of a Photo and the corresponding permissions. -type PhotoAuth struct { +// PhotoUser represents the user and group ownership of a Photo and the corresponding permissions. +type PhotoUser struct { UID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false" json:"UID" yaml:"UID"` - UserUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;index" json:"UserUID" yaml:"UserUID"` - TeamUID string `gorm:"type:VARBINARY(42);index" json:"TeamUID" yaml:"TeamUID"` + UserUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false;index" json:"UserUID,omitempty" yaml:"UserUID,omitempty"` + TeamUID string `gorm:"type:VARBINARY(42);index" json:"TeamUID,omitempty" yaml:"TeamUID,omitempty"` Perm uint `json:"Perm,omitempty" yaml:"Perm,omitempty"` - Changed int64 `json:"-" yaml:"-"` } // TableName returns the database table name. -func (PhotoAuth) TableName() string { - return "photos_auth_dev" +func (PhotoUser) TableName() string { + return "photos_users" } -// NewPhotoAuth creates a new entity model. -func NewPhotoAuth(uid, userUid, teamUid string, perm uint) *PhotoAuth { - result := &PhotoAuth{ +// NewPhotoUser creates a new entity model. +func NewPhotoUser(uid, userUid, teamUid string, perm uint) *PhotoUser { + result := &PhotoUser{ UID: uid, UserUID: userUid, TeamUID: teamUid, Perm: perm, - Changed: TimeStamp().Unix(), } return result } // Create inserts a new record into the database. -func (m *PhotoAuth) Create() error { - m.Changed = TimeStamp().Unix() +func (m *PhotoUser) Create() error { return Db().Create(m).Error } // Save updates the record in the database or inserts a new record if it does not already exist. -func (m *PhotoAuth) Save() error { - m.Changed = TimeStamp().Unix() +func (m *PhotoUser) Save() error { return Db().Save(m).Error } // FirstOrCreatePhotoUser returns the existing record or inserts a new record if it does not already exist. -func FirstOrCreatePhotoUser(m *PhotoAuth) *PhotoAuth { - found := PhotoAuth{} +func FirstOrCreatePhotoUser(m *PhotoUser) *PhotoUser { + found := PhotoUser{} if err := Db().Where("uid = ?", m.UID).First(&found).Error; err == nil { return &found diff --git a/internal/entity/reaction.go b/internal/entity/reaction.go index 9296c2a98..9d08fed4c 100644 --- a/internal/entity/reaction.go +++ b/internal/entity/reaction.go @@ -11,7 +11,7 @@ import ( // Reaction represents a human response to content such as photos and albums. type Reaction struct { - EntityUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false" json:"EntityUID,omitempty" yaml:"EntityUID,omitempty"` + UID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false" json:"UID,omitempty" yaml:"UID,omitempty"` UserUID string `gorm:"type:VARBINARY(42);primary_key;auto_increment:false" json:"UserUID,omitempty" yaml:"UserUID,omitempty"` Reaction string `gorm:"type:VARBINARY(64);primary_key;auto_increment:false" json:"Reaction,omitempty" yaml:"Reaction,omitempty"` Reacted int `json:"Reacted,omitempty" yaml:"Reacted,omitempty"` @@ -20,26 +20,26 @@ type Reaction struct { // TableName returns the entity table name. func (Reaction) TableName() string { - return "reactions_dev" + return "reactions" } // NewReaction creates a new Reaction struct. -func NewReaction(entityUid, userUid string) *Reaction { +func NewReaction(uid, userUid string) *Reaction { return &Reaction{ - EntityUID: entityUid, - UserUID: userUid, + UID: uid, + UserUID: userUid, } } // FindReaction returns the matching Reaction record or nil if it was not found. -func FindReaction(entityUid, userUid string) (m *Reaction) { - if entityUid == "" || userUid == "" { +func FindReaction(uid, userUid string) (m *Reaction) { + if uid == "" || userUid == "" { return nil } m = &Reaction{} - if Db().First(m, "entity_uid = ? AND user_uid = ?", entityUid, userUid).RecordNotFound() { + if Db().First(m, "uid = ? AND user_uid = ?", uid, userUid).RecordNotFound() { return nil } @@ -65,7 +65,7 @@ func (m *Reaction) String() string { // InvalidUID checks if the entity or user uid are missing or incorrect. func (m *Reaction) InvalidUID() bool { - return m.EntityUID == "" || m.UserUID == "" + return m.UID == "" || m.UserUID == "" } // Unknown checks if the reaction data is missing or incorrect. @@ -92,7 +92,7 @@ func (m *Reaction) Save() (err error) { values := Values{"reaction": m.Reaction, "reacted": gorm.Expr("reacted + 1"), "reacted_at": reactedAt} if err = Db().Model(Reaction{}). - Where("entity_uid = ? AND user_uid = ?", m.EntityUID, m.UserUID). + Where("uid = ? AND user_uid = ?", m.UID, m.UserUID). UpdateColumns(values).Error; err == nil { m.Reacted += 1 m.ReactedAt = reactedAt @@ -107,7 +107,7 @@ func (m *Reaction) Create() (err error) { return fmt.Errorf("reaction invalid") } - r := &Reaction{EntityUID: m.EntityUID, UserUID: m.UserUID, Reaction: m.Reaction, Reacted: m.Reacted, ReactedAt: TimePointer()} + r := &Reaction{UID: m.UID, UserUID: m.UserUID, Reaction: m.Reaction, Reacted: m.Reacted, ReactedAt: TimePointer()} if err = Db().Create(r).Error; err == nil { m.ReactedAt = r.ReactedAt @@ -123,7 +123,7 @@ func (m *Reaction) Delete() error { } // Delete record. - err := Db().Delete(m, "entity_uid = ? AND user_uid = ?", m.EntityUID, m.UserUID).Error + err := Db().Delete(m, "uid = ? AND user_uid = ?", m.UID, m.UserUID).Error // Ok? if err == nil { diff --git a/internal/entity/reaction_fixtures.go b/internal/entity/reaction_fixtures.go index 94e96f65e..14ef27ba2 100644 --- a/internal/entity/reaction_fixtures.go +++ b/internal/entity/reaction_fixtures.go @@ -22,19 +22,19 @@ func (m ReactionMap) Pointer(name string) *Reaction { var ReactionFixtures = ReactionMap{ "SubjectJohnLike": Reaction{ - EntityUID: SubjectFixtures.Get("john-doe").SubjUID, - UserUID: UserFixtures.Get("alice").UserUID, - Reaction: react.Like.String(), + UID: SubjectFixtures.Get("john-doe").SubjUID, + UserUID: UserFixtures.Get("alice").UserUID, + Reaction: react.Like.String(), }, "PhotoAliceLove": Reaction{ - EntityUID: PhotoFixtures.Get("Photo01").PhotoUID, - UserUID: UserFixtures.Pointer("alice").UserUID, - Reaction: react.Love.String(), + UID: PhotoFixtures.Get("Photo01").PhotoUID, + UserUID: UserFixtures.Pointer("alice").UserUID, + Reaction: react.Love.String(), }, "PhotoBobLove": Reaction{ - EntityUID: PhotoFixtures.Get("Photo01").PhotoUID, - UserUID: UserFixtures.Pointer("bob").UserUID, - Reaction: react.Love.String(), + UID: PhotoFixtures.Get("Photo01").PhotoUID, + UserUID: UserFixtures.Pointer("bob").UserUID, + Reaction: react.Love.String(), }, } diff --git a/internal/entity/reaction_test.go b/internal/entity/reaction_test.go index ae31a628e..48688bafc 100644 --- a/internal/entity/reaction_test.go +++ b/internal/entity/reaction_test.go @@ -12,7 +12,7 @@ func TestNewReaction(t *testing.T) { t.Run("Rainbow", func(t *testing.T) { m := NewReaction(FileFixtures.Pointer("bridge.jpg").FileUID, UserFixtures.Pointer("bob").UserUID).React("🌈") - assert.Equal(t, FileFixtures.Pointer("bridge.jpg").FileUID, m.EntityUID) + assert.Equal(t, FileFixtures.Pointer("bridge.jpg").FileUID, m.UID) assert.Equal(t, UserFixtures.Pointer("bob").UserUID, m.UserUID) assert.Equal(t, "🌈", m.Reaction) assert.Equal(t, react.Rainbow, m.Emoji()) diff --git a/internal/migrate/dialect_mysql.go b/internal/migrate/dialect_mysql.go index 88582d776..cd4fe592d 100644 --- a/internal/migrate/dialect_mysql.go +++ b/internal/migrate/dialect_mysql.go @@ -106,7 +106,7 @@ var DialectMySQL = Migrations{ { ID: "20220927-000200", Dialect: "mysql", - Statements: []string{"REPLACE INTO auth_users_dev (id, user_uid, super_admin, can_login, can_sync, user_role, display_name, user_name, user_email, login_at, created_at, updated_at) SELECT id, user_uid, role_admin, 1, 1, 'admin', full_name, user_name, primary_email, login_at, created_at, updated_at FROM users WHERE role_admin = 1 AND user_name NOT IN (SELECT user_name FROM auth_users) AND user_name <> '' AND user_name IS NOT NULL;"}, + Statements: []string{"REPLACE INTO auth_users (id, user_uid, super_admin, can_login, can_sync, user_role, display_name, user_name, user_email, login_at, created_at, updated_at) SELECT id, user_uid, role_admin, 1, 1, 'admin', full_name, user_name, primary_email, login_at, created_at, updated_at FROM users WHERE role_admin = 1 AND user_name NOT IN (SELECT user_name FROM auth_users) AND user_name <> '' AND user_name IS NOT NULL;"}, }, { ID: "20221002-000100", diff --git a/internal/migrate/dialect_sqlite3.go b/internal/migrate/dialect_sqlite3.go index aafa2b737..d39531666 100644 --- a/internal/migrate/dialect_sqlite3.go +++ b/internal/migrate/dialect_sqlite3.go @@ -61,6 +61,6 @@ var DialectSQLite3 = Migrations{ { ID: "20220927-000200", Dialect: "sqlite3", - Statements: []string{"REPLACE INTO auth_users_dev (id, user_uid, super_admin, can_login, can_sync, user_role, display_name, user_name, user_email, login_at, created_at, updated_at) SELECT id, user_uid, 1, 1, 1 'admin', full_name, user_name, primary_email, login_at, created_at, updated_at FROM users WHERE user_name <> '' AND user_name IS NOT NULL AND user_uid <> '' AND user_uid IS NOT NULL AND role_admin = 1 AND user_disabled = 0;"}, + Statements: []string{"REPLACE INTO auth_users (id, user_uid, super_admin, can_login, can_sync, user_role, display_name, user_name, user_email, login_at, created_at, updated_at) SELECT id, user_uid, 1, 1, 1 'admin', full_name, user_name, primary_email, login_at, created_at, updated_at FROM users WHERE user_name <> '' AND user_name IS NOT NULL AND user_uid <> '' AND user_uid IS NOT NULL AND role_admin = 1 AND user_disabled = 0;"}, }, } diff --git a/internal/migrate/mysql/20220927-000200.sql b/internal/migrate/mysql/20220927-000200.sql index e09b120b3..2d8f930d0 100644 --- a/internal/migrate/mysql/20220927-000200.sql +++ b/internal/migrate/mysql/20220927-000200.sql @@ -1 +1 @@ -REPLACE INTO auth_users_dev (id, user_uid, super_admin, can_login, can_sync, user_role, display_name, user_name, user_email, login_at, created_at, updated_at) SELECT id, user_uid, role_admin, 1, 1, 'admin', full_name, user_name, primary_email, login_at, created_at, updated_at FROM users WHERE role_admin = 1 AND user_name NOT IN (SELECT user_name FROM auth_users) AND user_name <> '' AND user_name IS NOT NULL; +REPLACE INTO auth_users (id, user_uid, super_admin, can_login, can_sync, user_role, display_name, user_name, user_email, login_at, created_at, updated_at) SELECT id, user_uid, role_admin, 1, 1, 'admin', full_name, user_name, primary_email, login_at, created_at, updated_at FROM users WHERE role_admin = 1 AND user_name NOT IN (SELECT user_name FROM auth_users) AND user_name <> '' AND user_name IS NOT NULL; diff --git a/internal/migrate/sqlite3/20220927-000200.sql b/internal/migrate/sqlite3/20220927-000200.sql index 5e9e89b4f..ff9a687b2 100644 --- a/internal/migrate/sqlite3/20220927-000200.sql +++ b/internal/migrate/sqlite3/20220927-000200.sql @@ -1 +1 @@ -REPLACE INTO auth_users_dev (id, user_uid, super_admin, can_login, can_sync, user_role, display_name, user_name, user_email, login_at, created_at, updated_at) SELECT id, user_uid, 1, 1, 1 'admin', full_name, user_name, primary_email, login_at, created_at, updated_at FROM users WHERE user_name <> '' AND user_name IS NOT NULL AND user_uid <> '' AND user_uid IS NOT NULL AND role_admin = 1 AND user_disabled = 0; \ No newline at end of file +REPLACE INTO auth_users (id, user_uid, super_admin, can_login, can_sync, user_role, display_name, user_name, user_email, login_at, created_at, updated_at) SELECT id, user_uid, 1, 1, 1 'admin', full_name, user_name, primary_email, login_at, created_at, updated_at FROM users WHERE user_name <> '' AND user_name IS NOT NULL AND user_uid <> '' AND user_uid IS NOT NULL AND role_admin = 1 AND user_disabled = 0; \ No newline at end of file diff --git a/internal/photoprism/index_mediafile.go b/internal/photoprism/index_mediafile.go index 0640ec39a..f3d359b18 100644 --- a/internal/photoprism/index_mediafile.go +++ b/internal/photoprism/index_mediafile.go @@ -555,6 +555,10 @@ func (ind *Index) UserMediaFile(m *MediaFile, o IndexOptions, originalName, phot photo.PhotoResolution = res } + if file.FileDuration > photo.PhotoDuration { + photo.PhotoDuration = file.FileDuration + } + photo.SetCamera(entity.FirstOrCreateCamera(entity.NewCamera(m.CameraModel(), m.CameraMake())), entity.SrcMeta) photo.SetLens(entity.FirstOrCreateLens(entity.NewLens(m.LensModel(), m.LensMake())), entity.SrcMeta) photo.SetExposure(m.FocalLength(), m.FNumber(), m.Iso(), m.Exposure(), entity.SrcMeta) diff --git a/internal/search/albums.go b/internal/search/albums.go index a28aeb786..cac0ba1d4 100644 --- a/internal/search/albums.go +++ b/internal/search/albums.go @@ -9,7 +9,6 @@ import ( "github.com/photoprism/photoprism/internal/acl" "github.com/photoprism/photoprism/internal/entity" - "github.com/photoprism/photoprism/internal/event" "github.com/photoprism/photoprism/internal/form" "github.com/photoprism/photoprism/pkg/txt" ) @@ -55,26 +54,20 @@ func UserAlbums(f form.SearchAlbums, sess *entity.Session) (results AlbumResults aclResource = acl.ResourcePlaces } - // Check user rights. + // Check user permissions. if acl.Resources.DenyAll(aclResource, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary, acl.AccessShared, acl.AccessOwn}) { return AlbumResults{}, ErrForbidden } - // Visitors and other restricted users can only access shared content. - if sess.IsVisitor() && sess.NoShares() { - event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", "denied"}, sess.RefID, acl.AccessShared.String(), string(aclResource), aclRole) - return AlbumResults{}, ErrForbidden - } - // Limit results by UID, owner and path. - if sess.IsVisitor() { - s = s.Where("albums.album_uid IN (?)", sess.SharedUIDs()) + if sess.IsVisitor() || sess.NotRegistered() { + s = s.Where("albums.album_uid IN (?) OR albums.published_at > ?", sess.SharedUIDs(), entity.TimeStamp()) } else if acl.Resources.DenyAll(aclResource, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary}) { if user.BasePath == "" { - s = s.Where("albums.album_uid IN (?) OR albums.created_by = ?", sess.SharedUIDs(), user.UserUID) + s = s.Where("albums.album_uid IN (?) OR albums.created_by = ? OR albums.published_at > ?", sess.SharedUIDs(), user.UserUID, entity.TimeStamp()) } else { - s = s.Where("albums.album_uid IN (?) OR albums.created_by = ? OR albums.album_type = ? AND (albums.album_path = ? OR albums.album_path LIKE ?)", - sess.SharedUIDs(), user.UserUID, entity.AlbumFolder, user.BasePath, user.BasePath+"/%") + s = s.Where("albums.album_uid IN (?) OR albums.created_by = ? OR albums.published_at > ? OR albums.album_type = ? AND (albums.album_path = ? OR albums.album_path LIKE ?)", + sess.SharedUIDs(), user.UserUID, entity.TimeStamp(), entity.AlbumFolder, user.BasePath, user.BasePath+"/%") } } diff --git a/internal/search/photos.go b/internal/search/photos.go index 32a2ffd6f..3ef1e537a 100644 --- a/internal/search/photos.go +++ b/internal/search/photos.go @@ -96,13 +96,6 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string) user := sess.User() aclRole := user.AclRole() - // Visitors and other restricted users can only access shared content. - if !sess.HasShare(f.Scope) && (sess.IsVisitor() || sess.NotRegistered()) || - f.Scope == "" && acl.Resources.Deny(acl.ResourcePhotos, aclRole, acl.ActionSearch) { - event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", "denied"}, sess.RefID, acl.ActionSearch.String(), string(acl.ResourcePhotos), aclRole) - return PhotoResults{}, 0, ErrForbidden - } - // Exclude private content? if acl.Resources.Deny(acl.ResourcePhotos, aclRole, acl.AccessPrivate) { f.Public = true @@ -120,13 +113,22 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string) f.Hidden = false } - // Limit results by owner and path? + // Visitors and other restricted users can only access shared content. + if f.Scope != "" && !sess.HasShare(f.Scope) && (sess.IsVisitor() || sess.NotRegistered()) || + f.Scope == "" && acl.Resources.Deny(acl.ResourcePhotos, aclRole, acl.ActionSearch) { + event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", "denied"}, sess.RefID, acl.ActionSearch.String(), string(acl.ResourcePhotos), aclRole) + return PhotoResults{}, 0, ErrForbidden + } + + // Limit results for external users. if f.Scope == "" && acl.Resources.DenyAll(acl.ResourcePhotos, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary}) { - if user.BasePath == "" { - s = s.Where("photos.created_by = ?", user.UserUID) + if sess.IsVisitor() || sess.NotRegistered() { + s = s.Where("photos.published_at > ?", entity.TimeStamp()) + } else if user.BasePath == "" { + s = s.Where("photos.created_by = ? OR photos.published_at > ?", user.UserUID, entity.TimeStamp()) } else { - s = s.Where("photos.created_by = ? OR photos.photo_path = ? OR photos.photo_path LIKE ?", - user.UserUID, user.BasePath, user.BasePath+"/%") + s = s.Where("photos.created_by = ? OR photos.published_at > ? OR photos.photo_path = ? OR photos.photo_path LIKE ?", + user.UserUID, entity.TimeStamp(), user.BasePath, user.BasePath+"/%") } } } @@ -141,6 +143,8 @@ func searchPhotos(f form.SearchPhotos, sess *entity.Session, resultCols string) } else { s = s.Order("photos.photo_quality DESC, files.time_index") } + case entity.SortOrderDuration: + s = s.Order("photos.photo_duration DESC, files.time_index") case entity.SortOrderNewest: s = s.Order("files.time_index") case entity.SortOrderOldest: diff --git a/internal/search/photos_geo.go b/internal/search/photos_geo.go index bf97df11d..d04d4c847 100644 --- a/internal/search/photos_geo.go +++ b/internal/search/photos_geo.go @@ -100,13 +100,6 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes user := sess.User() aclRole := user.AclRole() - // Visitors and other restricted users can only access shared content. - if !sess.HasShare(f.Scope) && (sess.IsVisitor() || sess.NotRegistered()) || - f.Scope == "" && acl.Resources.Deny(acl.ResourcePlaces, aclRole, acl.ActionSearch) { - event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", "denied"}, sess.RefID, acl.ActionSearch.String(), string(acl.ResourcePlaces), aclRole) - return GeoResults{}, ErrForbidden - } - // Exclude private content? if acl.Resources.Deny(acl.ResourcePlaces, aclRole, acl.AccessPrivate) { f.Public = true @@ -119,13 +112,22 @@ func UserPhotosGeo(f form.SearchPhotosGeo, sess *entity.Session) (results GeoRes f.Review = false } - // Limit results by owner and path? + // Visitors and other restricted users can only access shared content. + if f.Scope != "" && !sess.HasShare(f.Scope) && (sess.IsVisitor() || sess.NotRegistered()) || + f.Scope == "" && acl.Resources.Deny(acl.ResourcePlaces, aclRole, acl.ActionSearch) { + event.AuditErr([]string{sess.IP(), "session %s", "%s %s as %s", "denied"}, sess.RefID, acl.ActionSearch.String(), string(acl.ResourcePlaces), aclRole) + return GeoResults{}, ErrForbidden + } + + // Limit results for external users. if f.Scope == "" && acl.Resources.DenyAll(acl.ResourcePlaces, aclRole, acl.Permissions{acl.AccessAll, acl.AccessLibrary}) { - if user.BasePath == "" { - s = s.Where("photos.created_by = ?", user.UserUID) + if sess.IsVisitor() || sess.NotRegistered() { + s = s.Where("photos.published_at > ?", entity.TimeStamp()) + } else if user.BasePath == "" { + s = s.Where("photos.created_by = ? OR photos.published_at > ?", user.UserUID, entity.TimeStamp()) } else { - s = s.Where("photos.created_by = ? OR photos.photo_path = ? OR photos.photo_path LIKE ?", - user.UserUID, user.BasePath, user.BasePath+"/%") + s = s.Where("photos.created_by = ? OR photos.published_at > ? OR photos.photo_path = ? OR photos.photo_path LIKE ?", + user.UserUID, entity.TimeStamp(), user.BasePath, user.BasePath+"/%") } } } diff --git a/internal/search/photos_results.go b/internal/search/photos_results.go index f320cd8b8..880080dca 100644 --- a/internal/search/photos_results.go +++ b/internal/search/photos_results.go @@ -42,6 +42,7 @@ type Photo struct { PhotoFaces int `json:"Faces,omitempty" select:"photos.photo_faces"` PhotoQuality int `json:"Quality" select:"photos.photo_quality"` PhotoResolution int `json:"Resolution" select:"photos.photo_resolution"` + PhotoDuration time.Duration `json:"Duration,omitempty" yaml:"photos.photo_duration"` PhotoColor int16 `json:"Color" select:"photos.photo_color"` PhotoScan bool `json:"Scan" select:"photos.photo_scan"` PhotoPanorama bool `json:"Panorama" select:"photos.photo_panorama"` diff --git a/internal/search/photos_test.go b/internal/search/photos_test.go index f18264de0..c1f34799c 100644 --- a/internal/search/photos_test.go +++ b/internal/search/photos_test.go @@ -11,6 +11,21 @@ import ( ) func TestPhotos(t *testing.T) { + t.Run("OrderDuration", func(t *testing.T) { + var frm form.SearchPhotos + + frm.Query = "" + frm.Count = 10 + frm.Offset = 0 + frm.Order = "duration" + + photos, _, err := Photos(frm) + if err != nil { + t.Fatal(err) + } + + assert.LessOrEqual(t, 2, len(photos)) + }) t.Run("Chinese", func(t *testing.T) { var frm form.SearchPhotos