Logs: Improve event log and messages in i18n package

Signed-off-by: Michael Mayer <michael@liquidbytes.net>
This commit is contained in:
Michael Mayer
2020-07-07 10:51:55 +02:00
parent e743ae4480
commit d1db3d04f7
51 changed files with 896 additions and 389 deletions

View File

@@ -37,7 +37,7 @@ class Log {
this.logs = [
/* EXAMPLE LOG MESSAGE
{
"msg": "waiting for events",
"message": "waiting for events",
"level": "debug",
"time": this.created.toISOString(),
},

View File

@@ -33,20 +33,20 @@ import {$gettext} from "./vm";
const Notify = {
info: function (message) {
Event.publish("notify.info", {msg: message});
Event.publish("notify.info", {message});
},
warn: function (message) {
Event.publish("notify.warning", {msg: message});
Event.publish("notify.warning", {message});
},
error: function (message) {
Event.publish("notify.error", {msg: message});
Event.publish("notify.error", {message});
},
success: function (message) {
Event.publish("notify.success", {msg: message});
Event.publish("notify.success", {message});
},
logout: function (message) {
Event.publish("notify.error", {msg: message});
Event.publish("session.logout", {msg: message});
Event.publish("notify.error", {message});
Event.publish("session.logout", {message});
},
ajaxStart: function() {
Event.publish("ajax.start");

View File

@@ -46,7 +46,7 @@
const type = ev.split('.')[1];
// get message from data object
let m = data.msg;
let m = data.message;
if (!m || !m.length) {
console.warn("notify: empty message");
@@ -101,7 +101,7 @@
'color': color,
'textColor': textColor,
'delay': delay,
'msg': message
'message': message
};
this.messages.push(m);
@@ -119,7 +119,7 @@
const message = this.messages.shift();
if (message) {
this.text = message.msg;
this.text = message.message;
this.color = message.color;
this.textColor = message.textColor;
this.visible = true;

View File

@@ -3,10 +3,10 @@
ref="form" autocomplete="off" class="p-photo-toolbar" accept-charset="UTF-8"
@submit.prevent="filterChange">
<v-toolbar flat color="secondary">
<v-text-field class="pt-3 pr-3 p-search-field"
<v-text-field class="pt-3 pr-3 input-search"
browser-autocomplete="off"
single-line
:label="labels.search"
:label="$gettext('Search')"
prepend-inner-icon="search"
clearable
color="secondary-dark"

View File

@@ -4,7 +4,7 @@
<v-form ref="form" class="p-labels-search" lazy-validation @submit.prevent="updateQuery" dense>
<v-toolbar flat color="secondary">
<v-text-field class="pt-3 pr-3 p-search-field"
<v-text-field class="pt-3 pr-3 input-search"
single-line
:label="labels.search"
prepend-inner-icon="search"

View File

@@ -2,12 +2,17 @@
<div class="p-page p-page-errors" v-infinite-scroll="loadMore" :infinite-scroll-disabled="scrollDisabled"
:infinite-scroll-distance="10" :infinite-scroll-listen-for-event="'scrollRefresh'">
<v-toolbar flat color="secondary">
<v-toolbar-title v-if="errors.length > 0">
<translate>Event Log</translate>
</v-toolbar-title>
<v-toolbar-title v-else>
<translate>No warnings or errors</translate>
</v-toolbar-title>
<v-text-field class="pt-3 pr-3 input-search"
browser-autocomplete="off"
single-line
:label="$gettext('Search')"
prepend-inner-icon="search"
clearable
color="secondary-dark"
@click:clear="clearQuery"
v-model="filter.q"
@keyup.enter.native="updateQuery"
></v-text-field>
<v-spacer></v-spacer>
@@ -19,8 +24,10 @@
<v-icon>bug_report</v-icon>
</v-btn>
</v-toolbar>
<v-list dense two-line v-if="errors.length > 0">
<v-container fluid class="pa-4" v-if="loading">
<v-progress-linear color="secondary-dark" :indeterminate="true"></v-progress-linear>
</v-container>
<v-list dense two-line v-else-if="errors.length > 0">
<v-list-tile
v-for="(err, index) in errors" :key="index"
avatar
@@ -37,8 +44,11 @@
</v-list-tile>
</v-list>
<v-card v-else class="errors-empty secondary-light lighten-1 ma-0 pa-2" flat>
<v-card-title primary-title>
<translate>When PhotoPrism found broken files or there are other potential issues, you'll see a short message on this page.</translate>
<v-card-title primary-title v-if="filter.q !== ''">
<translate>No results. Try again with a different search term.</translate>
</v-card-title>
<v-card-title primary-title v-else>
<translate>Related log messages will appear here whenever PhotoPrism comes across broken files or there are other potential issues.</translate>
</v-card-title>
</v-card>
@@ -77,23 +87,55 @@
export default {
name: 'p-page-errors',
watch: {
'$route'() {
const query = this.$route.query;
this.filter.q = query['q'] ? query['q'] : '';
this.reload();
}
},
data() {
const query = this.$route.query;
const q = query["q"] ? query["q"] : "";
return {
errors: [],
dirty: false,
results: [],
loading: false,
scrollDisabled: false,
filter: {q},
pageSize: 100,
offset: 0,
page: 0,
errors: [],
results: [],
details: {
show: false,
err: {"Level": "", "Message": "", "Time": ""},
},
loading: false,
};
},
methods: {
updateQuery() {
const query = {};
Object.assign(query, this.filter);
for (let key in query) {
if (query[key] === undefined || !query[key]) {
delete query[key];
}
}
if (JSON.stringify(this.$route.query) === JSON.stringify(query)) {
return
}
this.$router.replace({query});
},
clearQuery() {
this.filter.q = "";
this.updateQuery();
},
showDetails(err) {
this.details.err = err;
this.details.show = true;
@@ -116,11 +158,9 @@
const count = this.dirty ? (this.page + 2) * this.pageSize : this.pageSize;
const offset = this.dirty ? 0 : this.offset;
const q = this.filter.q;
const params = {
count: count,
offset: offset,
};
const params = {count, offset, q};
Api.get("errors", {params}).then((resp) => {
if (!resp.data) {
@@ -139,7 +179,10 @@
this.offset = offset + count;
this.page++;
}
}).finally(() => this.loading = false)
}).finally(() => {
this.loading = false;
this.dirty = false;
});
},
level(s) {
return s.substr(0, 4).toUpperCase();

View File

@@ -6,7 +6,7 @@
<translate>Nothing to see here yet. Be patient.</translate>
</p>
<p v-for="(log, index) in logs" :key="index.id" class="p-log-message" :class="'p-log-' + log.level">
{{ formatTime(log.time) }} {{ level(log) }} <span>{{ log.msg }}</span>
{{ formatTime(log.time) }} {{ level(log) }} <span>{{ log.message }}</span>
</p>
</v-flex>
</v-layout>
@@ -28,6 +28,10 @@
return log.level.substr(0, 4).toUpperCase();
},
formatTime(s) {
if (!s) {
return this.$gettext("Unknown");
}
return DateTime.fromISO(s).toFormat("yyyy-LL-dd HH:mm:ss");
},
},

View File

@@ -327,7 +327,7 @@
return
}
this.$router.replace({query: query});
this.$router.replace({query});
},
searchParams() {
const params = {

Binary file not shown.

View File

@@ -203,7 +203,7 @@ msgstr "Höhe (m)"
#: src/common/api.js:76
msgid "An error occurred - are you offline?"
msgstr "Server konnte nicht erreicht werden - keine Verbindung?"
msgstr "Server nicht erreichbar - offline?"
#: src/pages/settings/general.vue:773
msgid "Animation"
@@ -348,7 +348,7 @@ msgid "Checked"
msgstr "Geprüft"
#: src/dialog/photo/details.vue:120 src/dialog/share.vue:85
#: src/pages/library/errors.vue:56
#: src/pages/library/errors.vue:54
msgid "Close"
msgstr "Schließen"
@@ -558,10 +558,6 @@ msgstr "Fehler"
msgid "Errors"
msgstr "Fehler"
#: src/pages/library/errors.vue:4
msgid "Event Log"
msgstr "Ereignisprotokoll"
#: src/resources/options.js:159
msgid "Every two days"
msgstr "Jeden zweiten Tag"
@@ -1045,14 +1041,14 @@ msgstr "Keine Bilder gefunden"
msgid "No photos or videos found"
msgstr "Keine Fotos oder Videos gefunden"
#: src/pages/library/errors.vue:30
msgid "No results. Try again with a different search term."
msgstr "Keine Ergebnisse für diesen Suchbegriff."
#: src/pages/settings/sync.vue:17
msgid "No servers configured."
msgstr "Keine Backup-Server eingerichtet."
#: src/pages/library/errors.vue:7
msgid "No warnings or errors"
msgstr "Keine Warnungen oder Fehler vorhanden"
#: src/component/photo/cards.vue:12 src/component/photo/list.vue:101
#: src/component/photo/mosaic.vue:12 src/dialog/upload.vue:50
#: src/pages/settings/general.vue:99 src/share/photo/list.vue:84
@@ -1287,6 +1283,14 @@ msgstr "Zuletzt hinzugefügt"
msgid "Red"
msgstr "Rot"
#: src/pages/library/errors.vue:33
msgid ""
"Related log messages will appear here whenever PhotoPrism comes across "
"broken files or there are other potential issues."
msgstr ""
"Warnungen und Fehler können hier gefunden werden, sobald PhotoPrism "
"beschädigte Dateien findet oder es andere Probleme gibt."
#: src/pages/settings/general.vue:377
msgid "Reloading…"
msgstr "Wird neu geladen…"
@@ -1350,9 +1354,10 @@ msgid "Scans"
msgstr "Scans"
#: src/component/album/toolbar.vue:116 src/component/photo/toolbar.vue:197
#: src/dialog/album/edit.vue:114 src/dialog/photo/details.vue:429
#: src/dialog/photo/labels.vue:114 src/pages/albums.vue:239
#: src/pages/labels.vue:196 src/pages/library/files.vue:176
#: src/component/photo/toolbar.vue:33 src/dialog/album/edit.vue:114
#: src/dialog/photo/details.vue:429 src/dialog/photo/labels.vue:114
#: src/pages/albums.vue:239 src/pages/labels.vue:196
#: src/pages/library/errors.vue:33 src/pages/library/files.vue:176
#: src/pages/places.vue:174 src/routes.js:235 src/share/albums.vue:161
msgid "Search"
msgstr "Suche"
@@ -1432,7 +1437,7 @@ msgstr "Server-Ereignisprotokoll anzeigen, um Fehler zu finden."
#: src/pages/photos.vue:290
msgid "Showing all %{n} results"
msgstr "Es werden alle %{n} Ergebnisse angezeigt"
msgstr "Alle %{n} Ergebnisse werden angezeigt"
#: src/model/file.js:198
msgid "Sidecar"
@@ -1614,9 +1619,10 @@ msgstr "Gruppierung auflösen"
#: src/dialog/photo/details.vue:423 src/dialog/photo/info.vue:221
#: src/model/photo.js:417 src/model/photo.js:431 src/model/photo.js:454
#: src/model/photo.js:466 src/model/photo.js:543 src/model/photo.js:556
#: src/pages/library/errors.vue:149 src/pages/library/errors.vue:156
#: src/resources/options.js:15 src/resources/options.js:29
#: src/resources/options.js:43 src/resources/options.js:57
#: src/pages/library/errors.vue:173 src/pages/library/errors.vue:180
#: src/pages/library/logs.vue:32 src/resources/options.js:15
#: src/resources/options.js:29 src/resources/options.js:43
#: src/resources/options.js:57
msgid "Unknown"
msgstr "Unbekannt"
@@ -1727,14 +1733,6 @@ msgstr "WebDAV Upload"
msgid "Whatever it is, we'd love to hear from you!"
msgstr "Wir freuen uns, von dir zu hören!"
#: src/pages/library/errors.vue:35
msgid ""
"When PhotoPrism found broken files or there are other potential issues, "
"you'll see a short message on this page."
msgstr ""
"Auf dieser Seite erscheint ein Hinweis, falls PhotoPrism beschädigte Dateien "
"findet oder es andere Probleme gibt."
#: src/resources/options.js:188
msgid "White"
msgstr "Weiß"
@@ -1782,6 +1780,19 @@ msgstr ""
"Das Anbieten oder Bewerben kommerzieller Produkte, Waren oder "
"Dienstleistungen ist nur nach vorheriger, schriftlicher Genehmigung erlaubt."
#~ msgid "Event Log"
#~ msgstr "Ereignisprotokoll"
#~ msgid "No warnings or errors"
#~ msgstr "Keine Warnungen oder Fehler vorhanden"
#~ msgid ""
#~ "When PhotoPrism found broken files or there are other potential issues, "
#~ "you'll see a short message on this page."
#~ msgstr ""
#~ "Auf dieser Seite erscheint ein Hinweis, falls PhotoPrism beschädigte "
#~ "Dateien findet oder es andere Probleme gibt."
#~ msgid "Sort By"
#~ msgstr "Sortierung"

File diff suppressed because one or more lines are too long

View File

@@ -372,7 +372,7 @@ msgstr ""
#: src/dialog/photo/details.vue:120
#: src/dialog/share.vue:85
#: src/pages/library/errors.vue:56
#: src/pages/library/errors.vue:54
msgid "Close"
msgstr ""
@@ -604,10 +604,6 @@ msgstr ""
msgid "Errors"
msgstr ""
#: src/pages/library/errors.vue:4
msgid "Event Log"
msgstr ""
#: src/resources/options.js:159
msgid "Every two days"
msgstr ""
@@ -1129,12 +1125,12 @@ msgstr ""
msgid "No photos or videos found"
msgstr ""
#: src/pages/settings/sync.vue:17
msgid "No servers configured."
#: src/pages/library/errors.vue:30
msgid "No results. Try again with a different search term."
msgstr ""
#: src/pages/library/errors.vue:7
msgid "No warnings or errors"
#: src/pages/settings/sync.vue:17
msgid "No servers configured."
msgstr ""
#: src/component/photo/cards.vue:12
@@ -1378,6 +1374,10 @@ msgstr ""
msgid "Red"
msgstr ""
#: src/pages/library/errors.vue:33
msgid "Related log messages will appear here whenever PhotoPrism comes across broken files or there are other potential issues."
msgstr ""
#: src/pages/settings/general.vue:377
msgid "Reloading…"
msgstr ""
@@ -1443,11 +1443,13 @@ msgstr ""
#: src/component/album/toolbar.vue:116
#: src/component/photo/toolbar.vue:197
#: src/component/photo/toolbar.vue:33
#: src/dialog/album/edit.vue:114
#: src/dialog/photo/details.vue:429
#: src/dialog/photo/labels.vue:114
#: src/pages/albums.vue:239
#: src/pages/labels.vue:196
#: src/pages/library/errors.vue:33
#: src/pages/library/files.vue:176
#: src/pages/places.vue:174
#: src/routes.js:235
@@ -1732,8 +1734,9 @@ msgstr ""
#: src/model/photo.js:466
#: src/model/photo.js:543
#: src/model/photo.js:556
#: src/pages/library/errors.vue:149
#: src/pages/library/errors.vue:156
#: src/pages/library/errors.vue:173
#: src/pages/library/errors.vue:180
#: src/pages/library/logs.vue:32
#: src/resources/options.js:15
#: src/resources/options.js:29
#: src/resources/options.js:43
@@ -1853,10 +1856,6 @@ msgstr ""
msgid "Whatever it is, we'd love to hear from you!"
msgstr ""
#: src/pages/library/errors.vue:35
msgid "When PhotoPrism found broken files or there are other potential issues, you'll see a short message on this page."
msgstr ""
#: src/resources/options.js:188
msgid "White"
msgstr ""

View File

@@ -6,7 +6,7 @@ export default class Page {
this.camera = Selector('div.p-camera-select', {timeout: 15000});
this.countries = Selector('div.p-countries-select', {timeout: 15000});
this.time = Selector('div.p-time-select', {timeout: 15000});
this.search1 = Selector('div.p-search-field input', {timeout: 15000});
this.search1 = Selector('div.input-search input', {timeout: 15000});
}
async setFilter(filter, option) {

View File

@@ -18,7 +18,6 @@ import (
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/internal/workers"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
)
// GET /api/v1/accounts
@@ -297,12 +296,12 @@ func DeleteAccount(router *gin.RouterGroup) {
m, err := query.AccountByID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAccountNotFound)
Abort(c, http.StatusNotFound, i18n.ErrAccountNotFound)
return
}
if err := m.Delete(); err != nil {
c.AbortWithStatusJSON(http.StatusInternalServerError, txt.UcFirst(err.Error()))
Error(c, http.StatusInternalServerError, err, i18n.ErrDeleteFailed)
return
}

View File

@@ -193,7 +193,7 @@ func TestDeleteAccount(t *testing.T) {
DeleteAccount(router)
r := PerformRequest(app, "DELETE", "/api/v1/accounts/xxx")
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "Account not found", val.String())
assert.Equal(t, i18n.Msg(i18n.ErrAccountNotFound), val.String())
assert.Equal(t, http.StatusNotFound, r.Code)
})
}

View File

@@ -321,7 +321,7 @@ func CloneAlbums(router *gin.RouterGroup) {
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
}
c.JSON(http.StatusOK, gin.H{"message": i18n.Msg(i18n.MsgAlbumCloned), "album": a, "added": added})
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "message": i18n.Msg(i18n.MsgAlbumCloned), "album": a, "added": added})
})
}
@@ -370,7 +370,7 @@ func AddPhotosToAlbum(router *gin.RouterGroup) {
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
}
c.JSON(http.StatusOK, gin.H{"message": i18n.Msg(i18n.MsgChangesSaved), "album": a, "photos": photos.UIDs(), "added": added})
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "message": i18n.Msg(i18n.MsgChangesSaved), "album": a, "photos": photos.UIDs(), "added": added})
})
}
@@ -415,7 +415,7 @@ func RemovePhotosFromAlbum(router *gin.RouterGroup) {
PublishAlbumEvent(EntityUpdated, a.AlbumUID, c)
}
c.JSON(http.StatusOK, gin.H{"message": i18n.Msg(i18n.MsgChangesSaved), "album": a, "photos": f.Photos, "removed": removed})
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "message": i18n.Msg(i18n.MsgChangesSaved), "album": a, "photos": f.Photos, "removed": removed})
})
}

View File

@@ -38,6 +38,7 @@ import (
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/txt"
)
var log = event.Log
@@ -56,7 +57,20 @@ func UpdateClientConfig() {
func Abort(c *gin.Context, code int, id i18n.Message, params ...interface{}) {
resp := i18n.NewResponse(code, id, params...)
log.Debugf("api: %s", resp.String())
log.Debugf("api: abort %s with code %d (%s)", c.FullPath(), code, resp.String())
c.AbortWithStatusJSON(code, resp)
}
func Error(c *gin.Context, code int, err error, id i18n.Message, params ...interface{}) {
resp := i18n.NewResponse(code, id, params...)
if err != nil {
resp.Details = err.Error()
log.Errorf("api: error %s with code %d in %s (%s)", txt.Quote(err.Error()), code, c.FullPath(), resp.String())
}
c.AbortWithStatusJSON(code, resp)
}
@@ -83,3 +97,7 @@ func AbortBadRequest(c *gin.Context) {
func AbortAlreadyExists(c *gin.Context, s string) {
Abort(c, http.StatusConflict, i18n.ErrAlreadyExists, s)
}
func AbortFeatureDisabled(c *gin.Context) {
Abort(c, http.StatusForbidden, i18n.ErrFeatureDisabled)
}

View File

@@ -1,19 +1,16 @@
package api
import (
"fmt"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/jinzhu/gorm"
"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/internal/i18n"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/gin-gonic/gin"
)
// POST /api/v1/batch/photos/archive
@@ -26,8 +23,6 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
return
}
start := time.Now()
var f form.Selection
if err := c.BindJSON(&f); err != nil {
@@ -36,8 +31,7 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
}
if len(f.Photos) == 0 {
log.Error("no items selected")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst("no items selected")})
Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected)
return
}
@@ -58,13 +52,11 @@ func BatchPhotosArchive(router *gin.RouterGroup) {
log.Errorf("photos: %s", err)
}
elapsed := int(time.Since(start).Seconds())
UpdateClientConfig()
event.EntitiesArchived("photos", f.Photos)
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("photos archived in %d s", elapsed)})
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgSelectionArchived))
})
}
@@ -78,8 +70,6 @@ func BatchPhotosRestore(router *gin.RouterGroup) {
return
}
start := time.Now()
var f form.Selection
if err := c.BindJSON(&f); err != nil {
@@ -88,8 +78,7 @@ func BatchPhotosRestore(router *gin.RouterGroup) {
}
if len(f.Photos) == 0 {
log.Error("no items selected")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst("no items selected")})
Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected)
return
}
@@ -107,13 +96,11 @@ func BatchPhotosRestore(router *gin.RouterGroup) {
log.Errorf("photos: %s", err)
}
elapsed := int(time.Since(start).Seconds())
UpdateClientConfig()
event.EntitiesRestored("photos", f.Photos)
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("photos restored in %d s", elapsed)})
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgSelectionRestored))
})
}
@@ -135,8 +122,7 @@ func BatchAlbumsDelete(router *gin.RouterGroup) {
}
if len(f.Albums) == 0 {
log.Error("no albums selected")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst("no albums selected")})
Abort(c, http.StatusBadRequest, i18n.ErrNoAlbumsSelected)
return
}
@@ -149,7 +135,7 @@ func BatchAlbumsDelete(router *gin.RouterGroup) {
event.EntitiesDeleted("albums", f.Albums)
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("albums deleted")})
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgAlbumsDeleted))
})
}
@@ -163,8 +149,6 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
return
}
start := time.Now()
var f form.Selection
if err := c.BindJSON(&f); err != nil {
@@ -173,8 +157,7 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
}
if len(f.Photos) == 0 {
log.Error("no items selected")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst("no items selected")})
Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected)
return
}
@@ -198,9 +181,7 @@ func BatchPhotosPrivate(router *gin.RouterGroup) {
UpdateClientConfig()
elapsed := time.Since(start)
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("photos marked as private in %s", elapsed)})
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgSelectionProtected))
})
}
@@ -223,7 +204,7 @@ func BatchLabelsDelete(router *gin.RouterGroup) {
if len(f.Labels) == 0 {
log.Error("no labels selected")
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"error": txt.UcFirst("no labels selected")})
Abort(c, http.StatusBadRequest, i18n.ErrNoLabelsSelected)
return
}
@@ -232,8 +213,7 @@ func BatchLabelsDelete(router *gin.RouterGroup) {
var labels entity.Labels
if err := entity.Db().Where("label_uid IN (?)", f.Labels).Find(&labels).Error; err != nil {
logError("labels", err)
c.AbortWithStatusJSON(http.StatusInternalServerError, ErrDeleteFailed)
Error(c, http.StatusInternalServerError, err, i18n.ErrDeleteFailed)
return
}
@@ -245,6 +225,6 @@ func BatchLabelsDelete(router *gin.RouterGroup) {
event.EntitiesDeleted("labels", f.Labels)
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("labels deleted")})
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgLabelsDeleted))
})
}

View File

@@ -1,7 +1,9 @@
package api
import (
"encoding/json"
"fmt"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
"net/http"
@@ -20,7 +22,7 @@ func TestBatchPhotosArchive(t *testing.T) {
BatchPhotosArchive(router)
r2 := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/archive", `{"photos": ["pt9jtdre2lvl0yh7", "pt9jtdre2lvl0ycc"]}`)
val2 := gjson.Get(r2.Body.String(), "message")
assert.Contains(t, val2.String(), "photos archived")
assert.Contains(t, val2.String(), "Selection archived")
assert.Equal(t, http.StatusOK, r2.Code)
r3 := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh7")
@@ -33,7 +35,7 @@ func TestBatchPhotosArchive(t *testing.T) {
BatchPhotosArchive(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/archive", `{"photos": []}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "No items selected", val.String())
assert.Equal(t, i18n.Msg(i18n.ErrNoItemsSelected), val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
@@ -51,7 +53,7 @@ func TestBatchPhotosRestore(t *testing.T) {
BatchPhotosArchive(router)
r2 := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/archive", `{"photos": ["pt9jtdre2lvl0yh8", "pt9jtdre2lvl0ycc"]}`)
val2 := gjson.Get(r2.Body.String(), "message")
assert.Contains(t, val2.String(), "photos archived")
assert.Contains(t, val2.String(), "Selection archived")
assert.Equal(t, http.StatusOK, r2.Code)
GetPhoto(router)
@@ -63,7 +65,7 @@ func TestBatchPhotosRestore(t *testing.T) {
BatchPhotosRestore(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/restore", `{"photos": ["pt9jtdre2lvl0yh8", "pt9jtdre2lvl0ycc"]}`)
val := gjson.Get(r.Body.String(), "message")
assert.Contains(t, val.String(), "photos restored")
assert.Contains(t, val.String(), "Selection restored")
assert.Equal(t, http.StatusOK, r.Code)
r4 := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh8")
@@ -76,7 +78,7 @@ func TestBatchPhotosRestore(t *testing.T) {
BatchPhotosRestore(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/restore", `{"photos": []}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "No items selected", val.String())
assert.Equal(t, i18n.Msg(i18n.ErrNoItemsSelected), val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
@@ -105,12 +107,12 @@ func TestBatchAlbumsDelete(t *testing.T) {
BatchAlbumsDelete(router)
r2 := PerformRequestWithBody(app, "POST", "/api/v1/batch/albums/delete", fmt.Sprintf(`{"albums": ["%s", "pt9jtdre2lvl0ycc"]}`, uid))
val2 := gjson.Get(r2.Body.String(), "message")
assert.Contains(t, val2.String(), "albums deleted")
assert.Contains(t, val2.String(), i18n.Msg(i18n.MsgAlbumsDeleted))
assert.Equal(t, http.StatusOK, r2.Code)
r3 := PerformRequest(app, "GET", "/api/v1/albums/"+uid)
val3 := gjson.Get(r3.Body.String(), "error")
assert.Equal(t, "Album not found", val3.String())
assert.Equal(t, i18n.Msg(i18n.ErrAlbumNotFound), val3.String())
assert.Equal(t, http.StatusNotFound, r3.Code)
})
t.Run("no albums selected", func(t *testing.T) {
@@ -118,7 +120,7 @@ func TestBatchAlbumsDelete(t *testing.T) {
BatchAlbumsDelete(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/albums/delete", `{"albums": []}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "No albums selected", val.String())
assert.Equal(t, i18n.Msg(i18n.ErrNoAlbumsSelected), val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
@@ -141,7 +143,7 @@ func TestBatchPhotosPrivate(t *testing.T) {
BatchPhotosPrivate(router)
r2 := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/private", `{"photos": ["pt9jtdre2lvl0yh8", "pt9jtdre2lvl0ycc"]}`)
val2 := gjson.Get(r2.Body.String(), "message")
assert.Contains(t, val2.String(), "photos marked as private")
assert.Contains(t, val2.String(), "Selection marked as private")
assert.Equal(t, http.StatusOK, r2.Code)
r3 := PerformRequest(app, "GET", "/api/v1/photos/pt9jtdre2lvl0yh8")
@@ -154,7 +156,7 @@ func TestBatchPhotosPrivate(t *testing.T) {
BatchPhotosPrivate(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/photos/private", `{"photos": []}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "No items selected", val.String())
assert.Equal(t, i18n.Msg(i18n.ErrNoItemsSelected), val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("invalid request", func(t *testing.T) {
@@ -175,9 +177,18 @@ func TestBatchLabelsDelete(t *testing.T) {
BatchLabelsDelete(router)
r2 := PerformRequestWithBody(app, "POST", "/api/v1/batch/labels/delete", fmt.Sprintf(`{"labels": ["lt9k3pw1wowuy3c6", "pt9jtdre2lvl0ycc"]}`))
val2 := gjson.Get(r2.Body.String(), "message")
assert.Contains(t, val2.String(), "labels deleted")
var resp i18n.Response
if err := json.Unmarshal(r2.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
assert.True(t, resp.Success())
assert.Equal(t, i18n.Msg(i18n.MsgLabelsDeleted), resp.Msg)
assert.Equal(t, i18n.Msg(i18n.MsgLabelsDeleted), resp.String())
assert.Equal(t, http.StatusOK, r2.Code)
assert.Equal(t, http.StatusOK, resp.Code)
r3 := PerformRequest(app, "GET", "/api/v1/labels?count=15")
val3 := gjson.Get(r3.Body.String(), `#(Name=="BatchDelete").Slug`)
@@ -188,7 +199,7 @@ func TestBatchLabelsDelete(t *testing.T) {
BatchLabelsDelete(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/batch/labels/delete", `{"labels": []}`)
val := gjson.Get(r.Body.String(), "error")
assert.Equal(t, "No labels selected", val.String())
assert.Equal(t, i18n.Msg(i18n.ErrNoLabelsSelected), val.String())
assert.Equal(t, http.StatusBadRequest, r.Code)
})
t.Run("invalid request", func(t *testing.T) {

View File

@@ -6,32 +6,10 @@ import (
"github.com/gin-gonic/gin"
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/config"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt"
)
var (
ErrUnauthorized = gin.H{"code": http.StatusUnauthorized, "error": txt.UcFirst(config.ErrUnauthorized.Error())}
ErrReadOnly = gin.H{"code": http.StatusForbidden, "error": txt.UcFirst(config.ErrReadOnly.Error())}
ErrUploadNSFW = gin.H{"code": http.StatusForbidden, "error": txt.UcFirst(config.ErrUploadNSFW.Error())}
ErrPublic = gin.H{"code": http.StatusForbidden, "error": "Not available in public mode"}
ErrAccountNotFound = gin.H{"code": http.StatusNotFound, "error": "Account not found"}
ErrConnectionFailed = gin.H{"code": http.StatusConflict, "error": "Failed to connect"}
ErrAlbumNotFound = gin.H{"code": http.StatusNotFound, "error": "Album not found"}
ErrPhotoNotFound = gin.H{"code": http.StatusNotFound, "error": "Photo not found"}
ErrLabelNotFound = gin.H{"code": http.StatusNotFound, "error": "Label not found"}
ErrFileNotFound = gin.H{"code": http.StatusNotFound, "error": "File not found"}
ErrSessionNotFound = gin.H{"code": http.StatusNotFound, "error": "Session not found"}
ErrUnexpectedError = gin.H{"code": http.StatusInternalServerError, "error": "Unexpected error"}
ErrSaveFailed = gin.H{"code": http.StatusInternalServerError, "error": "Changes could not be saved"}
ErrDeleteFailed = gin.H{"code": http.StatusInternalServerError, "error": "Changes could not be saved"}
ErrFormInvalid = gin.H{"code": http.StatusBadRequest, "error": "Changes could not be saved"}
ErrFeatureDisabled = gin.H{"code": http.StatusForbidden, "error": "Feature disabled"}
ErrNotFound = gin.H{"code": http.StatusNotFound, "error": "Not found"}
ErrInvalidPassword = gin.H{"code": http.StatusBadRequest, "error": "Invalid password, please try again"}
)
func GetErrors(router *gin.RouterGroup) {
router.GET("/errors", func(c *gin.Context) {
s := Auth(SessionID(c), acl.ResourceLogs, acl.ActionSearch)
@@ -44,7 +22,7 @@ func GetErrors(router *gin.RouterGroup) {
limit := txt.Int(c.Query("count"))
offset := txt.Int(c.Query("offset"))
if resp, err := query.Errors(limit, offset); err != nil {
if resp, err := query.Errors(limit, offset, c.Query("q")); err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst(err.Error())})
return
} else {

View File

@@ -1,7 +1,6 @@
package api
import (
"fmt"
"net/http"
"os"
"path/filepath"
@@ -12,6 +11,7 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/fs"
@@ -31,7 +31,7 @@ func StartImport(router *gin.RouterGroup) {
conf := service.Config()
if conf.ReadOnly() || !conf.Settings().Features.Import {
c.AbortWithStatusJSON(http.StatusForbidden, ErrFeatureDisabled)
AbortFeatureDisabled(c)
return
}
@@ -62,10 +62,10 @@ func StartImport(router *gin.RouterGroup) {
var opt photoprism.ImportOptions
if f.Move {
event.Info(fmt.Sprintf("moving files from %s", txt.Quote(filepath.Base(path))))
event.InfoMsg(i18n.MsgMovingFilesFrom, txt.Quote(filepath.Base(path)))
opt = photoprism.ImportOptionsMove(path)
} else {
event.Info(fmt.Sprintf("copying files from %s", txt.Quote(filepath.Base(path))))
event.InfoMsg(i18n.MsgCopyingFilesFrom, txt.Quote(filepath.Base(path)))
opt = photoprism.ImportOptionsCopy(path)
}
@@ -92,7 +92,9 @@ func StartImport(router *gin.RouterGroup) {
elapsed := int(time.Since(start).Seconds())
event.Success(fmt.Sprintf("import completed in %d s", elapsed))
msg := i18n.Msg(i18n.MsgImportCompletedIn, elapsed)
event.Success(msg)
event.Publish("import.completed", event.Data{"path": path, "seconds": elapsed})
event.Publish("index.completed", event.Data{"path": path, "seconds": elapsed})
@@ -102,7 +104,7 @@ func StartImport(router *gin.RouterGroup) {
UpdateClientConfig()
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("import completed in %d s", elapsed)})
c.JSON(http.StatusOK, i18n.Response{Code: http.StatusOK, Msg: msg})
})
}
@@ -119,7 +121,7 @@ func CancelImport(router *gin.RouterGroup) {
conf := service.Config()
if conf.ReadOnly() || !conf.Settings().Features.Import {
c.AbortWithStatusJSON(http.StatusForbidden, ErrFeatureDisabled)
AbortFeatureDisabled(c)
return
}
@@ -127,6 +129,6 @@ func CancelImport(router *gin.RouterGroup) {
imp.Cancel()
c.JSON(http.StatusOK, gin.H{"message": "import canceled"})
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgImportCanceled))
})
}

View File

@@ -1,11 +1,12 @@
package api
import (
"encoding/json"
"net/http"
"testing"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
)
func TestCancelImport(t *testing.T) {
@@ -13,8 +14,17 @@ func TestCancelImport(t *testing.T) {
app, router, _ := NewApiTest()
CancelImport(router)
r := PerformRequest(app, "DELETE", "/api/v1/import")
val := gjson.Get(r.Body.String(), "message")
assert.Equal(t, "import canceled", val.String())
var resp i18n.Response
if err := json.Unmarshal(r.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
assert.True(t, resp.Success())
assert.Equal(t, i18n.Msg(i18n.MsgImportCanceled), resp.Msg)
assert.Equal(t, i18n.Msg(i18n.MsgImportCanceled), resp.String())
assert.Equal(t, http.StatusOK, r.Code)
assert.Equal(t, http.StatusOK, resp.Code)
})
}

View File

@@ -1,7 +1,6 @@
package api
import (
"fmt"
"net/http"
"path/filepath"
"time"
@@ -10,6 +9,7 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/txt"
@@ -28,7 +28,7 @@ func StartIndexing(router *gin.RouterGroup) {
conf := service.Config()
if !conf.Settings().Features.Library {
c.AbortWithStatusJSON(http.StatusForbidden, ErrFeatureDisabled)
AbortFeatureDisabled(c)
return
}
@@ -52,9 +52,9 @@ func StartIndexing(router *gin.RouterGroup) {
}
if len(indOpt.Path) > 1 {
event.Info(fmt.Sprintf("indexing files in %s", txt.Quote(indOpt.Path)))
event.InfoMsg(i18n.MsgIndexingFiles, txt.Quote(indOpt.Path))
} else {
event.Info("indexing originals...")
event.InfoMsg(i18n.MsgIndexingOriginals)
}
indexed := ind.Start(indOpt)
@@ -70,7 +70,7 @@ func StartIndexing(router *gin.RouterGroup) {
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
return
} else if len(files) > 0 || len(photos) > 0 {
event.Info(fmt.Sprintf("removed %d files and %d photos", len(files), len(photos)))
event.InfoMsg(i18n.MsgRemovedFilesAndPhotos, len(files), len(photos))
}
moments := service.Moments()
@@ -81,12 +81,14 @@ func StartIndexing(router *gin.RouterGroup) {
elapsed := int(time.Since(start).Seconds())
event.Success(fmt.Sprintf("indexing completed in %d s", elapsed))
msg := i18n.Msg(i18n.MsgIndexingCompletedIn, elapsed)
event.Success(msg)
event.Publish("index.completed", event.Data{"path": path, "seconds": elapsed})
UpdateClientConfig()
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("indexing completed in %d s", elapsed)})
c.JSON(http.StatusOK, i18n.Response{Code: http.StatusOK, Msg: msg})
})
}
@@ -103,7 +105,7 @@ func CancelIndexing(router *gin.RouterGroup) {
conf := service.Config()
if !conf.Settings().Features.Library {
c.AbortWithStatusJSON(http.StatusForbidden, ErrFeatureDisabled)
AbortFeatureDisabled(c)
return
}
@@ -111,6 +113,6 @@ func CancelIndexing(router *gin.RouterGroup) {
ind.Cancel()
c.JSON(http.StatusOK, gin.H{"message": "indexing canceled"})
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgIndexingCanceled))
})
}

View File

@@ -1,10 +1,12 @@
package api
import (
"github.com/stretchr/testify/assert"
"github.com/tidwall/gjson"
"encoding/json"
"net/http"
"testing"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/stretchr/testify/assert"
)
func TestCancelIndex(t *testing.T) {
@@ -12,8 +14,17 @@ func TestCancelIndex(t *testing.T) {
app, router, _ := NewApiTest()
CancelIndexing(router)
r := PerformRequest(app, "DELETE", "/api/v1/index")
val := gjson.Get(r.Body.String(), "message")
assert.Equal(t, "indexing canceled", val.String())
var resp i18n.Response
if err := json.Unmarshal(r.Body.Bytes(), &resp); err != nil {
t.Fatal(err)
}
assert.True(t, resp.Success())
assert.Equal(t, i18n.Msg(i18n.MsgIndexingCanceled), resp.Msg)
assert.Equal(t, i18n.Msg(i18n.MsgIndexingCanceled), resp.String())
assert.Equal(t, http.StatusOK, r.Code)
assert.Equal(t, http.StatusOK, resp.Code)
})
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
@@ -77,14 +78,14 @@ func UpdateLabel(router *gin.RouterGroup) {
m, err := query.LabelByUID(id)
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound)
Abort(c, http.StatusNotFound, i18n.ErrLabelNotFound)
return
}
m.SetName(f.LabelName)
entity.Db().Save(&m)
event.Success("label saved")
event.SuccessMsg(i18n.MsgLabelSaved)
PublishLabelEvent(EntityUpdated, id, c)

View File

@@ -9,6 +9,7 @@ import (
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/pkg/txt"
)
@@ -120,7 +121,7 @@ func CreateLink(c *gin.Context) {
func CreateAlbumLink(router *gin.RouterGroup) {
router.POST("/albums/:uid/links", func(c *gin.Context) {
if _, err := query.AlbumByUID(c.Param("uid")); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound)
return
}
@@ -148,7 +149,7 @@ func GetAlbumLinks(router *gin.RouterGroup) {
m, err := query.AlbumByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound)
return
}
@@ -188,7 +189,7 @@ func GetPhotoLinks(router *gin.RouterGroup) {
m, err := query.PhotoByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound)
return
}
@@ -200,7 +201,7 @@ func GetPhotoLinks(router *gin.RouterGroup) {
func CreateLabelLink(router *gin.RouterGroup) {
router.POST("/labels/:uid/links", func(c *gin.Context) {
if _, err := query.LabelByUID(c.Param("uid")); err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrLabelNotFound)
Abort(c, http.StatusNotFound, i18n.ErrLabelNotFound)
return
}
@@ -228,7 +229,7 @@ func GetLabelLinks(router *gin.RouterGroup) {
m, err := query.LabelByUID(c.Param("uid"))
if err != nil {
c.AbortWithStatusJSON(http.StatusNotFound, ErrAlbumNotFound)
Abort(c, http.StatusNotFound, i18n.ErrAlbumNotFound)
return
}

View File

@@ -22,7 +22,7 @@ func GetSettings(router *gin.RouterGroup) {
if settings := service.Config().Settings(); settings != nil {
c.JSON(http.StatusOK, settings)
} else {
c.AbortWithStatusJSON(http.StatusNotFound, ErrNotFound)
Abort(c, http.StatusNotFound, i18n.ErrNotFound)
}
})
}

View File

@@ -1,7 +1,6 @@
package api
import (
"fmt"
"net/http"
"os"
"path"
@@ -10,6 +9,7 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/service"
"github.com/photoprism/photoprism/pkg/txt"
@@ -21,7 +21,7 @@ func Upload(router *gin.RouterGroup) {
router.POST("/upload/:path", func(c *gin.Context) {
conf := service.Config()
if conf.ReadOnly() || !conf.Settings().Features.Upload {
c.AbortWithStatusJSON(http.StatusForbidden, ErrReadOnly)
Abort(c, http.StatusForbidden, i18n.ErrReadOnly)
return
}
@@ -97,15 +97,17 @@ func Upload(router *gin.RouterGroup) {
}
}
c.AbortWithStatusJSON(http.StatusForbidden, ErrUploadNSFW)
Abort(c, http.StatusForbidden, i18n.ErrOffensiveUpload)
return
}
}
elapsed := time.Since(start)
elapsed := int(time.Since(start).Seconds())
log.Infof("%d files uploaded in %s", uploaded, elapsed)
msg := i18n.Msg(i18n.MsgFilesUploadedIn, uploaded, elapsed)
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("%d files uploaded in %s", uploaded, elapsed)})
log.Info(msg)
c.JSON(http.StatusOK, i18n.Response{Code: http.StatusOK, Msg: msg})
})
}

View File

@@ -7,6 +7,7 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/entity"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/service"
)
@@ -16,7 +17,7 @@ func ChangePassword(router *gin.RouterGroup) {
conf := service.Config()
if conf.Public() {
c.AbortWithStatusJSON(http.StatusForbidden, ErrPublic)
Abort(c, http.StatusForbidden, i18n.ErrPublic)
return
}
@@ -31,31 +32,27 @@ func ChangePassword(router *gin.RouterGroup) {
m := entity.FindPersonByUID(uid)
if m == nil {
log.Errorf("change password: user not found")
c.AbortWithStatusJSON(http.StatusNotFound, ErrInvalidPassword)
Abort(c, http.StatusNotFound, i18n.ErrUserNotFound)
return
}
f := form.ChangePassword{}
if err := c.BindJSON(&f); err != nil {
log.Errorf("change password: %s", err)
c.AbortWithStatusJSON(http.StatusBadRequest, ErrInvalidPassword)
Error(c, http.StatusBadRequest, err, i18n.ErrInvalidPassword)
return
}
if m.InvalidPassword(f.OldPassword) {
log.Errorf("change password: invalid password")
c.AbortWithStatusJSON(http.StatusBadRequest, ErrInvalidPassword)
Abort(c, http.StatusBadRequest, i18n.ErrInvalidPassword)
return
}
if err := m.SetPassword(f.NewPassword); err != nil {
log.Errorf("change password: %s", err)
c.AbortWithStatusJSON(http.StatusBadRequest, gin.H{"code": http.StatusBadRequest, "error": err.Error()})
Error(c, http.StatusBadRequest, err, i18n.ErrInvalidPassword)
return
}
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "message": "password changed"})
c.JSON(http.StatusOK, i18n.NewResponse(http.StatusOK, i18n.MsgPasswordChanged))
})
}

View File

@@ -12,6 +12,7 @@ import (
"github.com/photoprism/photoprism/internal/acl"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/i18n"
"github.com/photoprism/photoprism/internal/photoprism"
"github.com/photoprism/photoprism/internal/query"
"github.com/photoprism/photoprism/internal/service"
@@ -35,7 +36,7 @@ func CreateZip(router *gin.RouterGroup) {
conf := service.Config()
if !conf.Settings().Features.Download {
c.AbortWithStatusJSON(http.StatusForbidden, ErrFeatureDisabled)
AbortFeatureDisabled(c)
return
}
@@ -48,17 +49,17 @@ func CreateZip(router *gin.RouterGroup) {
}
if f.Empty() {
c.AbortWithStatusJSON(400, gin.H{"error": txt.UcFirst("no items selected")})
Abort(c, http.StatusBadRequest, i18n.ErrNoItemsSelected)
return
}
files, err := query.FileSelection(f)
if err != nil {
c.AbortWithStatusJSON(400, gin.H{"error": err.Error()})
Error(c, http.StatusBadRequest, err, i18n.ErrZipFailed)
return
} else if len(files) == 0 {
c.AbortWithStatusJSON(404, gin.H{"error": txt.UcFirst("no files available for download")})
Abort(c, http.StatusNotFound, i18n.ErrNoFilesForDownload)
return
}
@@ -69,16 +70,14 @@ func CreateZip(router *gin.RouterGroup) {
zipFileName := path.Join(zipPath, zipBaseName)
if err := os.MkdirAll(zipPath, 0700); err != nil {
log.Error(err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst("failed to create zip folder")})
Error(c, http.StatusInternalServerError, err, i18n.ErrZipFailed)
return
}
newZipFile, err := os.Create(zipFileName)
if err != nil {
log.Error(err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst(err.Error())})
Error(c, http.StatusInternalServerError, err, i18n.ErrZipFailed)
return
}
@@ -93,8 +92,7 @@ func CreateZip(router *gin.RouterGroup) {
if fs.FileExists(fileName) {
if err := addFileToZip(zipWriter, fileName, fileAlias); err != nil {
log.Error(err)
c.AbortWithStatusJSON(http.StatusInternalServerError, gin.H{"error": txt.UcFirst("failed to create zip file")})
Error(c, http.StatusInternalServerError, err, i18n.ErrZipFailed)
return
}
log.Infof("zip: added %s as %s", txt.Quote(f.FileName), txt.Quote(fileAlias))
@@ -108,7 +106,7 @@ func CreateZip(router *gin.RouterGroup) {
log.Infof("zip: archive %s created in %s", txt.Quote(zipBaseName), time.Since(start))
c.JSON(http.StatusOK, gin.H{"message": fmt.Sprintf("zip created in %d s", elapsed), "filename": zipBaseName})
c.JSON(http.StatusOK, gin.H{"code": http.StatusOK, "message": i18n.Msg(i18n.MsgZipCreatedIn, elapsed), "filename": zipBaseName})
})
}

View File

@@ -13,7 +13,7 @@ func TestCreateZip(t *testing.T) {
CreateZip(router)
r := PerformRequestWithBody(app, "POST", "/api/v1/zip", `{"photos": ["pt9jtdre2lvl0y12", "pt9jtdre2lvl0y11"]}`)
val := gjson.Get(r.Body.String(), "message")
assert.Contains(t, val.String(), "zip created")
assert.Contains(t, val.String(), "Zip created")
assert.Equal(t, http.StatusOK, r.Code)
})
t.Run("no items selected", func(t *testing.T) {

View File

@@ -1,6 +1,9 @@
package entity
import "time"
import (
"fmt"
"time"
)
// Details stores additional metadata fields for each photo to improve search performance.
type Details struct {
@@ -22,6 +25,10 @@ func NewDetails(photo Photo) Details {
// Create inserts a new row to the database.
func (m *Details) Create() error {
if m.PhotoID == 0 {
return fmt.Errorf("details: photo id must not be empty (create)")
}
return Db().Create(m).Error
}

View File

@@ -40,7 +40,7 @@ func SaveErrorMessages() {
newError := Error{ErrorLevel: logLevel.String()}
if val, ok := msg.Fields["msg"]; ok {
if val, ok := msg.Fields["message"]; ok {
newError.ErrorMessage = val.(string)
}

View File

@@ -147,19 +147,29 @@ func (m *File) AllFilesMissing() bool {
// Create inserts a new row to the database.
func (m *File) Create() error {
if m.PhotoID == 0 {
return fmt.Errorf("file: photo id is empty (create)")
return fmt.Errorf("file: photo id must not be empty (create)")
}
return UnscopedDb().Create(m).Error
if err := UnscopedDb().Create(m).Error; err != nil {
log.Errorf("file: %s (create)", err)
return err
}
return nil
}
// Saves the file in the database.
func (m *File) Save() error {
if m.PhotoID == 0 {
return fmt.Errorf("file: photo id is empty (%s)", m.FileUID)
return fmt.Errorf("file: photo id must not be empty (save %s)", m.FileUID)
}
return UnscopedDb().Save(m).Error
if err := UnscopedDb().Save(m).Error; err != nil {
log.Errorf("file: %s (save %s)", err, m.FileUID)
return err
}
return nil
}
// UpdateVideoInfos updates related video infos based on this file.

View File

@@ -110,6 +110,6 @@ func TestFile_Save(t *testing.T) {
t.Fatalf("file id should be 0: %d", file.ID)
}
assert.Equal(t, "file: photo id is empty (123)", err.Error())
assert.Equal(t, "file: photo id must not be empty (save 123)", err.Error())
})
}

View File

@@ -188,7 +188,7 @@ func (m *Photo) Create() error {
}
if err := UnscopedDb().Save(m.GetDetails()).Error; err != nil {
log.Errorf("photo: %s (save details after create)", err)
log.Errorf("photo: %s (save details for %s)", err, m.PhotoUID)
return err
}
@@ -198,12 +198,12 @@ func (m *Photo) Create() error {
// Save updates the existing or inserts a new row.
func (m *Photo) Save() error {
if err := UnscopedDb().Save(m).Error; err != nil {
log.Errorf("photo: %s (save)", err)
log.Errorf("photo: %s (save %s)", err, m.PhotoUID)
return err
}
if err := UnscopedDb().Save(m.GetDetails()).Error; err != nil {
log.Errorf("photo: %s (save details)", err)
log.Errorf("photo: %s (save details for %s)", err, m.PhotoUID)
return err
}

View File

@@ -22,22 +22,22 @@ func SharedHub() *Hub {
func Error(msg string) {
Log.Error(msg)
Publish("notify.error", Data{"msg": msg})
Publish("notify.error", Data{"message": msg})
}
func Success(msg string) {
Log.Info(msg)
Publish("notify.success", Data{"msg": msg})
Publish("notify.success", Data{"message": msg})
}
func Info(msg string) {
Log.Info(msg)
Publish("notify.info", Data{"msg": msg})
Publish("notify.info", Data{"message": msg})
}
func Warning(msg string) {
Log.Warn(msg)
Publish("notify.warning", Data{"msg": msg})
Publish("notify.warning", Data{"message": msg})
}
func ErrorMsg(id i18n.Message, params ...interface{}) {

View File

@@ -39,7 +39,7 @@ func TestError(t *testing.T) {
msg := <-s.Receiver
assert.Equal(t, "notify.error", msg.Name)
assert.Equal(t, Data{"msg": "error message"}, msg.Fields)
assert.Equal(t, Data{"message": "error message"}, msg.Fields)
Unsubscribe(s)
}
@@ -53,7 +53,7 @@ func TestSuccess(t *testing.T) {
msg := <-s.Receiver
assert.Equal(t, "notify.success", msg.Name)
assert.Equal(t, Data{"msg": "success message"}, msg.Fields)
assert.Equal(t, Data{"message": "success message"}, msg.Fields)
Unsubscribe(s)
}
@@ -67,7 +67,7 @@ func TestInfo(t *testing.T) {
msg := <-s.Receiver
assert.Equal(t, "notify.info", msg.Name)
assert.Equal(t, Data{"msg": "info message"}, msg.Fields)
assert.Equal(t, Data{"message": "info message"}, msg.Fields)
Unsubscribe(s)
}
@@ -81,7 +81,7 @@ func TestWarning(t *testing.T) {
msg := <-s.Receiver
assert.Equal(t, "notify.warning", msg.Name)
assert.Equal(t, Data{"msg": "warning message"}, msg.Fields)
assert.Equal(t, Data{"message": "warning message"}, msg.Fields)
Unsubscribe(s)
}

View File

@@ -21,9 +21,9 @@ func (h *Hook) Fire(entry *logrus.Entry) error {
h.hub.Publish(Message{
Name: "log." + entry.Level.String(),
Fields: Data{
"time": entry.Time,
"level": entry.Level.String(),
"msg": entry.Message,
"time": entry.Time,
"level": entry.Level.String(),
"message": entry.Message,
},
})

View File

@@ -1,33 +1,67 @@
package i18n
var MsgGerman = MessageMap{
ErrUnexpected: "Unerwarteter Fehler, bitte erneut versuchen",
ErrSaveFailed: "Daten gekonnten nicht gespeichert werden",
ErrBadRequest: "Ungültige Anfrage",
ErrAlreadyExists: "%s existiert bereits",
ErrEntityNotFound: "Eintrag nicht gefunden",
ErrAccountNotFound: "Unbekannter Account",
ErrAlbumNotFound: "Album nicht gefunden - gelöscht?",
ErrReadOnly: "Funktion im 'read-only' Modus nicht verfügbar",
ErrUnauthorized: "Anmeldung erforderlich",
ErrUploadNSFW: "Inhalt könnte anstößig sein und wurde abgelehnt",
ErrNoItemsSelected: "Auswahl ist leer, bitte erneut versuchen",
ErrCreateFile: "Datei konnte nicht angelegt werden",
ErrCreateFolder: "Verzeichnis konnte nicht angelegt werden",
ErrConnectionFailed: "Could not connect, please try again",
// Error messages:
ErrUnexpected: "Unerwarteter Fehler, bitte erneut versuchen",
ErrSaveFailed: "Fehler beim Speichern der Daten",
ErrBadRequest: "Ungültige Anfrage",
ErrAlreadyExists: "%s existiert bereits",
ErrEntityNotFound: "Nicht auf Server gefunden, gelöscht?",
ErrAccountNotFound: "Unbekannter Account",
ErrAlbumNotFound: "Album nicht gefunden - gelöscht?",
ErrReadOnly: "Funktion im 'read-only' Modus nicht verfügbar",
ErrUnauthorized: "Anmeldung erforderlich",
ErrOffensiveUpload: "Inhalt könnte anstößig sein und wurde abgelehnt",
ErrNoItemsSelected: "Auswahl ist leer, bitte erneut versuchen",
ErrCreateFile: "Datei konnte nicht angelegt werden",
ErrCreateFolder: "Verzeichnis konnte nicht angelegt werden",
ErrConnectionFailed: "Verbindung fehlgeschlagen",
ErrDeleteFailed: "Konnte nicht gelöscht werden",
ErrNotFound: "Nicht auf Server gefunden, gelöscht?",
ErrFileNotFound: "Datei konnte nicht gefunden werden",
ErrSelectionNotFound: "Nicht auf Server gefunden, gelöscht?",
ErrUserNotFound: "Nutzer nicht gefunden",
ErrLabelNotFound: "Kategorie nicht gefunden",
ErrPublic: "Im öffentlichen Modus nicht verfügbar",
ErrInvalidPassword: "Ungültiges Passwort",
ErrFeatureDisabled: "Funktion deaktiviert",
ErrNoLabelsSelected: "Keine Kategorien ausgewählt",
ErrNoAlbumsSelected: "Keine Alben ausgewählt",
ErrNoFilesForDownload: "Nicht zum Download verfügbar",
ErrZipFailed: "Zip-Datei konnte nicht erstellt werden",
MsgChangesSaved: "Änderungen erfolgreich gespeichert",
MsgAlbumCreated: "Album erstellt",
MsgAlbumSaved: "Album gespeichert",
MsgAlbumDeleted: "Album %s gelöscht",
MsgAlbumCloned: "Album-Einträge kopiert",
MsgFileUngrouped: "Datei-Gruppierung aufgehoben",
MsgSelectionAddedTo: "Auswahl zu %s hinzugefügt",
MsgEntryAddedTo: "Ein Eintrag zu %s hinzugefügt",
MsgEntriesAddedTo: "%d Einträge zu %s hinzugefügt",
MsgEntryRemovedFrom: "Ein Eintrag aus %s entfernt",
MsgEntriesRemovedFrom: "%d Einträge aus %s entfernt",
MsgAccountCreated: "Server-Konfiguration angelegt",
MsgAccountSaved: "Server-Konfiguration gespeichert",
MsgAccountDeleted: "Server-Konfiguration gelöscht",
// Info and confirmation messages:
MsgChangesSaved: "Änderungen erfolgreich gespeichert",
MsgAlbumCreated: "Album erstellt",
MsgAlbumSaved: "Album gespeichert",
MsgAlbumDeleted: "Album %s gelöscht",
MsgAlbumCloned: "Album-Einträge kopiert",
MsgFileUngrouped: "Datei-Gruppierung aufgehoben",
MsgSelectionAddedTo: "Auswahl zu %s hinzugefügt",
MsgEntryAddedTo: "Ein Eintrag zu %s hinzugefügt",
MsgEntriesAddedTo: "%d Einträge zu %s hinzugefügt",
MsgEntryRemovedFrom: "Ein Eintrag aus %s entfernt",
MsgEntriesRemovedFrom: "%d Einträge aus %s entfernt",
MsgAccountCreated: "Server-Konfiguration angelegt",
MsgAccountSaved: "Server-Konfiguration gespeichert",
MsgAccountDeleted: "Server-Konfiguration gelöscht",
MsgSettingsSaved: "Einstellungen gespeichert",
MsgPasswordChanged: "Passwort geändert",
MsgImportCompletedIn: "Import in %d s abgeschlossen",
MsgImportCanceled: "Import abgebrochen",
MsgIndexingCompletedIn: "Indizierung in %d s abgeschlossen",
MsgIndexingOriginals: "Indiziere Dateien...",
MsgIndexingFiles: "Indiziere Dateien in %s",
MsgIndexingCanceled: "Indizierung abgebrochen",
MsgRemovedFilesAndPhotos: "%d Dateien und %d Fotos wurden entfernt",
MsgMovingFilesFrom: "Verschiebe Dateien von %s",
MsgCopyingFilesFrom: "Kopiere Dateien von %s",
MsgLabelsDeleted: "Kategorien gelöscht",
MsgLabelSaved: "Kategorie gespeichert",
MsgFilesUploadedIn: "%d Dateien hochgeladen in %d s",
MsgSelectionArchived: "Auswahl archiviert",
MsgSelectionRestored: "Auswahl wiederhergestellt",
MsgSelectionProtected: "Auswahl als privat markiert",
MsgAlbumsDeleted: "Alben gelöscht",
MsgZipCreatedIn: "Zip-Datei erstellt in %d s",
}

View File

@@ -4,17 +4,30 @@ const (
ErrUnexpected Message = iota + 1
ErrBadRequest
ErrSaveFailed
ErrDeleteFailed
ErrAlreadyExists
ErrNotFound
ErrFileNotFound
ErrSelectionNotFound
ErrEntityNotFound
ErrAccountNotFound
ErrUserNotFound
ErrLabelNotFound
ErrAlbumNotFound
ErrPublic
ErrReadOnly
ErrUnauthorized
ErrUploadNSFW
ErrOffensiveUpload
ErrNoItemsSelected
ErrCreateFile
ErrCreateFolder
ErrConnectionFailed
ErrInvalidPassword
ErrFeatureDisabled
ErrNoLabelsSelected
ErrNoAlbumsSelected
ErrNoFilesForDownload
ErrZipFailed
MsgChangesSaved
MsgAlbumCreated
@@ -31,37 +44,88 @@ const (
MsgAccountSaved
MsgAccountDeleted
MsgSettingsSaved
MsgPasswordChanged
MsgImportCompletedIn
MsgImportCanceled
MsgIndexingCompletedIn
MsgIndexingOriginals
MsgIndexingFiles
MsgIndexingCanceled
MsgRemovedFilesAndPhotos
MsgMovingFilesFrom
MsgCopyingFilesFrom
MsgLabelsDeleted
MsgLabelSaved
MsgFilesUploadedIn
MsgSelectionArchived
MsgSelectionRestored
MsgSelectionProtected
MsgAlbumsDeleted
MsgZipCreatedIn
)
var MsgEnglish = MessageMap{
ErrUnexpected: "Unexpected error, please try again",
ErrBadRequest: "Invalid request",
ErrSaveFailed: "Changes could not be saved",
ErrAlreadyExists: "%s already exists",
ErrEntityNotFound: "Unknown entity",
ErrAccountNotFound: "Unknown account",
ErrAlbumNotFound: "Album not found",
ErrReadOnly: "not available in read-only mode",
ErrUnauthorized: "Please log in and try again",
ErrUploadNSFW: "Upload might be offensive",
ErrNoItemsSelected: "No items selected",
ErrCreateFile: "Failed creating file, please check permissions",
ErrCreateFolder: "Failed creating folder, please check permissions",
ErrConnectionFailed: "Could not connect, please try again",
// Error messages:
ErrUnexpected: "Unexpected error, please try again",
ErrBadRequest: "Invalid request",
ErrSaveFailed: "Changes could not be saved",
ErrDeleteFailed: "Could not be deleted",
ErrAlreadyExists: "%s already exists",
ErrNotFound: "Not found on server, deleted?",
ErrFileNotFound: "File not found",
ErrSelectionNotFound: "Selection not found",
ErrEntityNotFound: "Not found on server, deleted?",
ErrAccountNotFound: "Account not found",
ErrUserNotFound: "User not found",
ErrLabelNotFound: "Label not found",
ErrAlbumNotFound: "Album not found",
ErrPublic: "Not available in public mode",
ErrReadOnly: "not available in read-only mode",
ErrUnauthorized: "Please log in and try again",
ErrOffensiveUpload: "Upload might be offensive",
ErrNoItemsSelected: "No items selected",
ErrCreateFile: "Failed creating file, please check permissions",
ErrCreateFolder: "Failed creating folder, please check permissions",
ErrConnectionFailed: "Could not connect, please try again",
ErrInvalidPassword: "Invalid password, please try again",
ErrFeatureDisabled: "Feature disabled",
ErrNoLabelsSelected: "No labels selected",
ErrNoAlbumsSelected: "No albums selected",
ErrNoFilesForDownload: "No files available for download",
ErrZipFailed: "Failed to create zip file",
MsgChangesSaved: "Changes successfully saved",
MsgAlbumCreated: "Album created",
MsgAlbumSaved: "Album saved",
MsgAlbumDeleted: "Album %s deleted",
MsgAlbumCloned: "Album contents cloned",
MsgFileUngrouped: "File successfully ungrouped",
MsgSelectionAddedTo: "Selection added to %s",
MsgEntryAddedTo: "One entry added to %s",
MsgEntriesAddedTo: "%d entries added to %s",
MsgEntryRemovedFrom: "One entry removed from %s",
MsgEntriesRemovedFrom: "%d entries removed from %s",
MsgAccountCreated: "Account created",
MsgAccountSaved: "Account saved",
MsgAccountDeleted: "Account deleted",
MsgSettingsSaved: "Settings saved",
// Info and confirmation messages:
MsgChangesSaved: "Changes successfully saved",
MsgAlbumCreated: "Album created",
MsgAlbumSaved: "Album saved",
MsgAlbumDeleted: "Album %s deleted",
MsgAlbumCloned: "Album contents cloned",
MsgFileUngrouped: "File successfully ungrouped",
MsgSelectionAddedTo: "Selection added to %s",
MsgEntryAddedTo: "One entry added to %s",
MsgEntriesAddedTo: "%d entries added to %s",
MsgEntryRemovedFrom: "One entry removed from %s",
MsgEntriesRemovedFrom: "%d entries removed from %s",
MsgAccountCreated: "Account created",
MsgAccountSaved: "Account saved",
MsgAccountDeleted: "Account deleted",
MsgSettingsSaved: "Settings saved",
MsgPasswordChanged: "Password changed",
MsgImportCompletedIn: "Import completed in %d s",
MsgImportCanceled: "Import canceled",
MsgIndexingCompletedIn: "Indexing completed in %d s",
MsgIndexingOriginals: "Indexing originals...",
MsgIndexingFiles: "Indexing files in %s",
MsgIndexingCanceled: "Indexing canceled",
MsgRemovedFilesAndPhotos: "Removed %d files and %d photos",
MsgMovingFilesFrom: "Moving files from %s",
MsgCopyingFilesFrom: "Copying files from %s",
MsgLabelsDeleted: "Labels deleted",
MsgLabelSaved: "Label saved",
MsgFilesUploadedIn: "%d files uploaded in %d s",
MsgSelectionArchived: "Selection archived",
MsgSelectionRestored: "Selection restored",
MsgSelectionProtected: "Selection marked as private",
MsgAlbumsDeleted: "Albums deleted",
MsgZipCreatedIn: "Zip created in %d s",
}

67
internal/i18n/lang-es.go Normal file
View File

@@ -0,0 +1,67 @@
package i18n
var MsgSpanish = MessageMap{
// Error messages:
ErrUnexpected: "Unexpected error, please try again",
ErrBadRequest: "Invalid request",
ErrSaveFailed: "Changes could not be saved",
ErrDeleteFailed: "Could not be deleted",
ErrAlreadyExists: "%s already exists",
ErrNotFound: "Not found on server, deleted?",
ErrFileNotFound: "File not found",
ErrSelectionNotFound: "Selection not found",
ErrEntityNotFound: "Not found on server, deleted?",
ErrAccountNotFound: "Account not found",
ErrUserNotFound: "User not found",
ErrLabelNotFound: "Label not found",
ErrAlbumNotFound: "Album not found",
ErrPublic: "Not available in public mode",
ErrReadOnly: "not available in read-only mode",
ErrUnauthorized: "Please log in and try again",
ErrOffensiveUpload: "Upload might be offensive",
ErrNoItemsSelected: "No items selected",
ErrCreateFile: "Failed creating file, please check permissions",
ErrCreateFolder: "Failed creating folder, please check permissions",
ErrConnectionFailed: "Could not connect, please try again",
ErrInvalidPassword: "Invalid password, please try again",
ErrFeatureDisabled: "Feature disabled",
ErrNoLabelsSelected: "No labels selected",
ErrNoAlbumsSelected: "No albums selected",
ErrNoFilesForDownload: "No files available for download",
ErrZipFailed: "Failed to create zip file",
// Info and confirmation messages:
MsgChangesSaved: "Changes successfully saved",
MsgAlbumCreated: "Album created",
MsgAlbumSaved: "Album saved",
MsgAlbumDeleted: "Album %s deleted",
MsgAlbumCloned: "Album contents cloned",
MsgFileUngrouped: "File successfully ungrouped",
MsgSelectionAddedTo: "Selection added to %s",
MsgEntryAddedTo: "One entry added to %s",
MsgEntriesAddedTo: "%d entries added to %s",
MsgEntryRemovedFrom: "One entry removed from %s",
MsgEntriesRemovedFrom: "%d entries removed from %s",
MsgAccountCreated: "Account created",
MsgAccountSaved: "Account saved",
MsgAccountDeleted: "Account deleted",
MsgSettingsSaved: "Settings saved",
MsgPasswordChanged: "Password changed",
MsgImportCompletedIn: "Import completed in %d s",
MsgImportCanceled: "Import canceled",
MsgIndexingCompletedIn: "Indexing completed in %d s",
MsgIndexingOriginals: "Indexing originals...",
MsgIndexingFiles: "Indexing files in %s",
MsgIndexingCanceled: "Indexing canceled",
MsgRemovedFilesAndPhotos: "Removed %d files and %d photos",
MsgMovingFilesFrom: "Moving files from %s",
MsgCopyingFilesFrom: "Copying files from %s",
MsgLabelsDeleted: "Labels deleted",
MsgLabelSaved: "Label saved",
MsgFilesUploadedIn: "%d files uploaded in %d s",
MsgSelectionArchived: "Selection archived",
MsgSelectionRestored: "Selection restored",
MsgSelectionProtected: "Selection marked as private",
MsgAlbumsDeleted: "Albums deleted",
MsgZipCreatedIn: "Zip created in %d s",
}

View File

@@ -1,32 +1,67 @@
package i18n
var MsgFrench = MessageMap{
ErrUnexpected: "Unexpected error, please try again",
ErrBadRequest: "Invalid request, please try again",
ErrSaveFailed: "Changes could not be saved",
ErrAlreadyExists: "%s already exists",
ErrEntityNotFound: "Unknown entity",
ErrAccountNotFound: "Unknown account",
ErrAlbumNotFound: "Album not found",
ErrReadOnly: "not available in read-only mode",
ErrUnauthorized: "please log in and try again",
ErrUploadNSFW: "Upload might be offensive",
ErrNoItemsSelected: "No items selected",
ErrCreateFile: "Failed creating file, please check permissions",
ErrCreateFolder: "Failed creating folder, please check permissions",
// Error messages:
ErrUnexpected: "Unexpected error, please try again",
ErrBadRequest: "Invalid request",
ErrSaveFailed: "Changes could not be saved",
ErrDeleteFailed: "Could not be deleted",
ErrAlreadyExists: "%s already exists",
ErrNotFound: "Not found on server, deleted?",
ErrFileNotFound: "File not found",
ErrSelectionNotFound: "Selection not found",
ErrEntityNotFound: "Not found on server, deleted?",
ErrAccountNotFound: "Account not found",
ErrUserNotFound: "User not found",
ErrLabelNotFound: "Label not found",
ErrAlbumNotFound: "Album not found",
ErrPublic: "Not available in public mode",
ErrReadOnly: "not available in read-only mode",
ErrUnauthorized: "Please log in and try again",
ErrOffensiveUpload: "Upload might be offensive",
ErrNoItemsSelected: "No items selected",
ErrCreateFile: "Failed creating file, please check permissions",
ErrCreateFolder: "Failed creating folder, please check permissions",
ErrConnectionFailed: "Could not connect, please try again",
ErrInvalidPassword: "Invalid password, please try again",
ErrFeatureDisabled: "Feature disabled",
ErrNoLabelsSelected: "No labels selected",
ErrNoAlbumsSelected: "No albums selected",
ErrNoFilesForDownload: "No files available for download",
ErrZipFailed: "Failed to create zip file",
MsgChangesSaved: "Changes successfully saved",
MsgAlbumCreated: "Album created",
MsgAlbumSaved: "Album saved",
MsgAlbumDeleted: "Album %s deleted",
MsgAlbumCloned: "Album contents cloned",
MsgFileUngrouped: "File successfully ungrouped",
MsgSelectionAddedTo: "Selection added to %s",
MsgEntryAddedTo: "One entry added to %s",
MsgEntriesAddedTo: "%d entries added to %s",
MsgEntryRemovedFrom: "One entry removed from %s",
MsgEntriesRemovedFrom: "%d entries removed from %s",
MsgAccountCreated: "Account created",
MsgAccountSaved: "Account saved",
MsgAccountDeleted: "Account deleted",
// Info and confirmation messages:
MsgChangesSaved: "Changes successfully saved",
MsgAlbumCreated: "Album created",
MsgAlbumSaved: "Album saved",
MsgAlbumDeleted: "Album %s deleted",
MsgAlbumCloned: "Album contents cloned",
MsgFileUngrouped: "File successfully ungrouped",
MsgSelectionAddedTo: "Selection added to %s",
MsgEntryAddedTo: "One entry added to %s",
MsgEntriesAddedTo: "%d entries added to %s",
MsgEntryRemovedFrom: "One entry removed from %s",
MsgEntriesRemovedFrom: "%d entries removed from %s",
MsgAccountCreated: "Account created",
MsgAccountSaved: "Account saved",
MsgAccountDeleted: "Account deleted",
MsgSettingsSaved: "Settings saved",
MsgPasswordChanged: "Password changed",
MsgImportCompletedIn: "Import completed in %d s",
MsgImportCanceled: "Import canceled",
MsgIndexingCompletedIn: "Indexing completed in %d s",
MsgIndexingOriginals: "Indexing originals...",
MsgIndexingFiles: "Indexing files in %s",
MsgIndexingCanceled: "Indexing canceled",
MsgRemovedFilesAndPhotos: "Removed %d files and %d photos",
MsgMovingFilesFrom: "Moving files from %s",
MsgCopyingFilesFrom: "Copying files from %s",
MsgLabelsDeleted: "Labels deleted",
MsgLabelSaved: "Label saved",
MsgFilesUploadedIn: "%d files uploaded in %d s",
MsgSelectionArchived: "Selection archived",
MsgSelectionRestored: "Selection restored",
MsgSelectionProtected: "Selection marked as private",
MsgAlbumsDeleted: "Albums deleted",
MsgZipCreatedIn: "Zip created in %d s",
}

View File

@@ -1,32 +1,67 @@
package i18n
var MsgDutch = MessageMap{
ErrUnexpected: "Unexpected error, please try again",
ErrBadRequest: "Invalid request, please try again",
ErrSaveFailed: "Changes could not be saved",
ErrAlreadyExists: "%s already exists",
ErrEntityNotFound: "Unknown entity",
ErrAccountNotFound: "Unknown account",
ErrAlbumNotFound: "Album not found",
ErrReadOnly: "not available in read-only mode",
ErrUnauthorized: "please log in and try again",
ErrUploadNSFW: "Upload might be offensive",
ErrNoItemsSelected: "No items selected",
ErrCreateFile: "Failed creating file, please check permissions",
ErrCreateFolder: "Failed creating folder, please check permissions",
// Error messages:
ErrUnexpected: "Unexpected error, please try again",
ErrBadRequest: "Invalid request",
ErrSaveFailed: "Changes could not be saved",
ErrDeleteFailed: "Could not be deleted",
ErrAlreadyExists: "%s already exists",
ErrNotFound: "Not found on server, deleted?",
ErrFileNotFound: "File not found",
ErrSelectionNotFound: "Selection not found",
ErrEntityNotFound: "Not found on server, deleted?",
ErrAccountNotFound: "Account not found",
ErrUserNotFound: "User not found",
ErrLabelNotFound: "Label not found",
ErrAlbumNotFound: "Album not found",
ErrPublic: "Not available in public mode",
ErrReadOnly: "not available in read-only mode",
ErrUnauthorized: "Please log in and try again",
ErrOffensiveUpload: "Upload might be offensive",
ErrNoItemsSelected: "No items selected",
ErrCreateFile: "Failed creating file, please check permissions",
ErrCreateFolder: "Failed creating folder, please check permissions",
ErrConnectionFailed: "Could not connect, please try again",
ErrInvalidPassword: "Invalid password, please try again",
ErrFeatureDisabled: "Feature disabled",
ErrNoLabelsSelected: "No labels selected",
ErrNoAlbumsSelected: "No albums selected",
ErrNoFilesForDownload: "No files available for download",
ErrZipFailed: "Failed to create zip file",
MsgChangesSaved: "Changes successfully saved",
MsgAlbumCreated: "Album created",
MsgAlbumSaved: "Album saved",
MsgAlbumDeleted: "Album %s deleted",
MsgAlbumCloned: "Album contents cloned",
MsgFileUngrouped: "File successfully ungrouped",
MsgSelectionAddedTo: "Selection added to %s",
MsgEntryAddedTo: "One entry added to %s",
MsgEntriesAddedTo: "%d entries added to %s",
MsgEntryRemovedFrom: "One entry removed from %s",
MsgEntriesRemovedFrom: "%d entries removed from %s",
MsgAccountCreated: "Account created",
MsgAccountSaved: "Account saved",
MsgAccountDeleted: "Account deleted",
// Info and confirmation messages:
MsgChangesSaved: "Changes successfully saved",
MsgAlbumCreated: "Album created",
MsgAlbumSaved: "Album saved",
MsgAlbumDeleted: "Album %s deleted",
MsgAlbumCloned: "Album contents cloned",
MsgFileUngrouped: "File successfully ungrouped",
MsgSelectionAddedTo: "Selection added to %s",
MsgEntryAddedTo: "One entry added to %s",
MsgEntriesAddedTo: "%d entries added to %s",
MsgEntryRemovedFrom: "One entry removed from %s",
MsgEntriesRemovedFrom: "%d entries removed from %s",
MsgAccountCreated: "Account created",
MsgAccountSaved: "Account saved",
MsgAccountDeleted: "Account deleted",
MsgSettingsSaved: "Settings saved",
MsgPasswordChanged: "Password changed",
MsgImportCompletedIn: "Import completed in %d s",
MsgImportCanceled: "Import canceled",
MsgIndexingCompletedIn: "Indexing completed in %d s",
MsgIndexingOriginals: "Indexing originals...",
MsgIndexingFiles: "Indexing files in %s",
MsgIndexingCanceled: "Indexing canceled",
MsgRemovedFilesAndPhotos: "Removed %d files and %d photos",
MsgMovingFilesFrom: "Moving files from %s",
MsgCopyingFilesFrom: "Copying files from %s",
MsgLabelsDeleted: "Labels deleted",
MsgLabelSaved: "Label saved",
MsgFilesUploadedIn: "%d files uploaded in %d s",
MsgSelectionArchived: "Selection archived",
MsgSelectionRestored: "Selection restored",
MsgSelectionProtected: "Selection marked as private",
MsgAlbumsDeleted: "Albums deleted",
MsgZipCreatedIn: "Zip created in %d s",
}

67
internal/i18n/lang-pt.go Normal file
View File

@@ -0,0 +1,67 @@
package i18n
var MsgPortuguese = MessageMap{
// Error messages:
ErrUnexpected: "Unexpected error, please try again",
ErrBadRequest: "Invalid request",
ErrSaveFailed: "Changes could not be saved",
ErrDeleteFailed: "Could not be deleted",
ErrAlreadyExists: "%s already exists",
ErrNotFound: "Not found on server, deleted?",
ErrFileNotFound: "File not found",
ErrSelectionNotFound: "Selection not found",
ErrEntityNotFound: "Not found on server, deleted?",
ErrAccountNotFound: "Account not found",
ErrUserNotFound: "User not found",
ErrLabelNotFound: "Label not found",
ErrAlbumNotFound: "Album not found",
ErrPublic: "Not available in public mode",
ErrReadOnly: "not available in read-only mode",
ErrUnauthorized: "Please log in and try again",
ErrOffensiveUpload: "Upload might be offensive",
ErrNoItemsSelected: "No items selected",
ErrCreateFile: "Failed creating file, please check permissions",
ErrCreateFolder: "Failed creating folder, please check permissions",
ErrConnectionFailed: "Could not connect, please try again",
ErrInvalidPassword: "Invalid password, please try again",
ErrFeatureDisabled: "Feature disabled",
ErrNoLabelsSelected: "No labels selected",
ErrNoAlbumsSelected: "No albums selected",
ErrNoFilesForDownload: "No files available for download",
ErrZipFailed: "Failed to create zip file",
// Info and confirmation messages:
MsgChangesSaved: "Changes successfully saved",
MsgAlbumCreated: "Album created",
MsgAlbumSaved: "Album saved",
MsgAlbumDeleted: "Album %s deleted",
MsgAlbumCloned: "Album contents cloned",
MsgFileUngrouped: "File successfully ungrouped",
MsgSelectionAddedTo: "Selection added to %s",
MsgEntryAddedTo: "One entry added to %s",
MsgEntriesAddedTo: "%d entries added to %s",
MsgEntryRemovedFrom: "One entry removed from %s",
MsgEntriesRemovedFrom: "%d entries removed from %s",
MsgAccountCreated: "Account created",
MsgAccountSaved: "Account saved",
MsgAccountDeleted: "Account deleted",
MsgSettingsSaved: "Settings saved",
MsgPasswordChanged: "Password changed",
MsgImportCompletedIn: "Import completed in %d s",
MsgImportCanceled: "Import canceled",
MsgIndexingCompletedIn: "Indexing completed in %d s",
MsgIndexingOriginals: "Indexing originals...",
MsgIndexingFiles: "Indexing files in %s",
MsgIndexingCanceled: "Indexing canceled",
MsgRemovedFilesAndPhotos: "Removed %d files and %d photos",
MsgMovingFilesFrom: "Moving files from %s",
MsgCopyingFilesFrom: "Copying files from %s",
MsgLabelsDeleted: "Labels deleted",
MsgLabelSaved: "Label saved",
MsgFilesUploadedIn: "%d files uploaded in %d s",
MsgSelectionArchived: "Selection archived",
MsgSelectionRestored: "Selection restored",
MsgSelectionProtected: "Selection marked as private",
MsgAlbumsDeleted: "Albums deleted",
MsgZipCreatedIn: "Zip created in %d s",
}

View File

@@ -1,32 +1,67 @@
package i18n
var MsgRussian = MessageMap{
ErrUnexpected: "Unexpected error, please try again",
ErrBadRequest: "Invalid request, please try again",
ErrSaveFailed: "Changes could not be saved",
ErrAlreadyExists: "%s already exists",
ErrEntityNotFound: "Unknown entity",
ErrAccountNotFound: "Unknown account",
ErrAlbumNotFound: "Album not found",
ErrReadOnly: "not available in read-only mode",
ErrUnauthorized: "please log in and try again",
ErrUploadNSFW: "Upload might be offensive",
ErrNoItemsSelected: "No items selected",
ErrCreateFile: "Failed creating file, please check permissions",
ErrCreateFolder: "Failed creating folder, please check permissions",
// Error messages:
ErrUnexpected: "Unexpected error, please try again",
ErrBadRequest: "Invalid request",
ErrSaveFailed: "Changes could not be saved",
ErrDeleteFailed: "Could not be deleted",
ErrAlreadyExists: "%s already exists",
ErrNotFound: "Not found on server, deleted?",
ErrFileNotFound: "File not found",
ErrSelectionNotFound: "Selection not found",
ErrEntityNotFound: "Not found on server, deleted?",
ErrAccountNotFound: "Account not found",
ErrUserNotFound: "User not found",
ErrLabelNotFound: "Label not found",
ErrAlbumNotFound: "Album not found",
ErrPublic: "Not available in public mode",
ErrReadOnly: "not available in read-only mode",
ErrUnauthorized: "Please log in and try again",
ErrOffensiveUpload: "Upload might be offensive",
ErrNoItemsSelected: "No items selected",
ErrCreateFile: "Failed creating file, please check permissions",
ErrCreateFolder: "Failed creating folder, please check permissions",
ErrConnectionFailed: "Could not connect, please try again",
ErrInvalidPassword: "Invalid password, please try again",
ErrFeatureDisabled: "Feature disabled",
ErrNoLabelsSelected: "No labels selected",
ErrNoAlbumsSelected: "No albums selected",
ErrNoFilesForDownload: "No files available for download",
ErrZipFailed: "Failed to create zip file",
MsgChangesSaved: "Changes successfully saved",
MsgAlbumCreated: "Album created",
MsgAlbumSaved: "Album saved",
MsgAlbumDeleted: "Album %s deleted",
MsgAlbumCloned: "Album contents cloned",
MsgFileUngrouped: "File successfully ungrouped",
MsgSelectionAddedTo: "Selection added to %s",
MsgEntryAddedTo: "One entry added to %s",
MsgEntriesAddedTo: "%d entries added to %s",
MsgEntryRemovedFrom: "One entry removed from %s",
MsgEntriesRemovedFrom: "%d entries removed from %s",
MsgAccountCreated: "Account created",
MsgAccountSaved: "Account saved",
MsgAccountDeleted: "Account deleted",
// Info and confirmation messages:
MsgChangesSaved: "Changes successfully saved",
MsgAlbumCreated: "Album created",
MsgAlbumSaved: "Album saved",
MsgAlbumDeleted: "Album %s deleted",
MsgAlbumCloned: "Album contents cloned",
MsgFileUngrouped: "File successfully ungrouped",
MsgSelectionAddedTo: "Selection added to %s",
MsgEntryAddedTo: "One entry added to %s",
MsgEntriesAddedTo: "%d entries added to %s",
MsgEntryRemovedFrom: "One entry removed from %s",
MsgEntriesRemovedFrom: "%d entries removed from %s",
MsgAccountCreated: "Account created",
MsgAccountSaved: "Account saved",
MsgAccountDeleted: "Account deleted",
MsgSettingsSaved: "Settings saved",
MsgPasswordChanged: "Password changed",
MsgImportCompletedIn: "Import completed in %d s",
MsgImportCanceled: "Import canceled",
MsgIndexingCompletedIn: "Indexing completed in %d s",
MsgIndexingOriginals: "Indexing originals...",
MsgIndexingFiles: "Indexing files in %s",
MsgIndexingCanceled: "Indexing canceled",
MsgRemovedFilesAndPhotos: "Removed %d files and %d photos",
MsgMovingFilesFrom: "Moving files from %s",
MsgCopyingFilesFrom: "Copying files from %s",
MsgLabelsDeleted: "Labels deleted",
MsgLabelSaved: "Label saved",
MsgFilesUploadedIn: "%d files uploaded in %d s",
MsgSelectionArchived: "Selection archived",
MsgSelectionRestored: "Selection restored",
MsgSelectionProtected: "Selection marked as private",
MsgAlbumsDeleted: "Albums deleted",
MsgZipCreatedIn: "Zip created in %d s",
}

67
internal/i18n/lang-zh.go Normal file
View File

@@ -0,0 +1,67 @@
package i18n
var MsgChinese = MessageMap{
// Error messages:
ErrUnexpected: "Unexpected error, please try again",
ErrBadRequest: "Invalid request",
ErrSaveFailed: "Changes could not be saved",
ErrDeleteFailed: "Could not be deleted",
ErrAlreadyExists: "%s already exists",
ErrNotFound: "Not found on server, deleted?",
ErrFileNotFound: "File not found",
ErrSelectionNotFound: "Selection not found",
ErrEntityNotFound: "Not found on server, deleted?",
ErrAccountNotFound: "Account not found",
ErrUserNotFound: "User not found",
ErrLabelNotFound: "Label not found",
ErrAlbumNotFound: "Album not found",
ErrPublic: "Not available in public mode",
ErrReadOnly: "not available in read-only mode",
ErrUnauthorized: "Please log in and try again",
ErrOffensiveUpload: "Upload might be offensive",
ErrNoItemsSelected: "No items selected",
ErrCreateFile: "Failed creating file, please check permissions",
ErrCreateFolder: "Failed creating folder, please check permissions",
ErrConnectionFailed: "Could not connect, please try again",
ErrInvalidPassword: "Invalid password, please try again",
ErrFeatureDisabled: "Feature disabled",
ErrNoLabelsSelected: "No labels selected",
ErrNoAlbumsSelected: "No albums selected",
ErrNoFilesForDownload: "No files available for download",
ErrZipFailed: "Failed to create zip file",
// Info and confirmation messages:
MsgChangesSaved: "Changes successfully saved",
MsgAlbumCreated: "Album created",
MsgAlbumSaved: "Album saved",
MsgAlbumDeleted: "Album %s deleted",
MsgAlbumCloned: "Album contents cloned",
MsgFileUngrouped: "File successfully ungrouped",
MsgSelectionAddedTo: "Selection added to %s",
MsgEntryAddedTo: "One entry added to %s",
MsgEntriesAddedTo: "%d entries added to %s",
MsgEntryRemovedFrom: "One entry removed from %s",
MsgEntriesRemovedFrom: "%d entries removed from %s",
MsgAccountCreated: "Account created",
MsgAccountSaved: "Account saved",
MsgAccountDeleted: "Account deleted",
MsgSettingsSaved: "Settings saved",
MsgPasswordChanged: "Password changed",
MsgImportCompletedIn: "Import completed in %d s",
MsgImportCanceled: "Import canceled",
MsgIndexingCompletedIn: "Indexing completed in %d s",
MsgIndexingOriginals: "Indexing originals...",
MsgIndexingFiles: "Indexing files in %s",
MsgIndexingCanceled: "Indexing canceled",
MsgRemovedFilesAndPhotos: "Removed %d files and %d photos",
MsgMovingFilesFrom: "Moving files from %s",
MsgCopyingFilesFrom: "Copying files from %s",
MsgLabelsDeleted: "Labels deleted",
MsgLabelSaved: "Label saved",
MsgFilesUploadedIn: "%d files uploaded in %d s",
MsgSelectionArchived: "Selection archived",
MsgSelectionRestored: "Selection restored",
MsgSelectionProtected: "Selection marked as private",
MsgAlbumsDeleted: "Albums deleted",
MsgZipCreatedIn: "Zip created in %d s",
}

View File

@@ -7,20 +7,26 @@ type Language string
type LanguageMap map[Language]MessageMap
const (
English Language = "en"
Dutch Language = "nl"
French Language = "fr"
German Language = "de"
Russian Language = "ru"
Default = English
German Language = "de"
English Language = "en"
Spanish Language = "es"
French Language = "fr"
Dutch Language = "nl"
Portuguese Language = "pt"
Russian Language = "ru"
Chinese Language = "zh"
Default = English
)
var Languages = LanguageMap{
English: MsgEnglish,
Dutch: MsgDutch,
French: MsgFrench,
German: MsgGerman,
Russian: MsgRussian,
German: MsgGerman,
English: MsgEnglish,
Spanish: MsgSpanish,
French: MsgFrench,
Dutch: MsgDutch,
Portuguese: MsgPortuguese,
Russian: MsgRussian,
Chinese: MsgChinese,
}
var Lang = Default

View File

@@ -3,9 +3,10 @@ package i18n
import "strings"
type Response struct {
Code int `json:"code"`
Err string `json:"error,omitempty"`
Msg string `json:"success,omitempty"`
Code int `json:"code"`
Err string `json:"error,omitempty"`
Msg string `json:"message,omitempty"`
Details string `json:"details,omitempty"`
}
func (r Response) String() string {
@@ -24,6 +25,10 @@ func (r Response) Error() string {
return r.Err
}
func (r Response) Success() bool {
return r.Err == "" && r.Code < 400
}
func NewResponse(code int, id Message, params ...interface{}) Response {
if code < 400 {
return Response{Code: code, Msg: Msg(id, params...)}

View File

@@ -32,7 +32,7 @@ func TestNewResponse(t *testing.T) {
if s, err := json.Marshal(resp); err != nil {
t.Fatal(err)
} else {
assert.Equal(t, `{"code":200,"success":"Changes successfully saved"}`, string(s))
assert.Equal(t, `{"code":200,"message":"Changes successfully saved"}`, string(s))
}
})
}

View File

@@ -1,13 +1,21 @@
package query
import (
"strings"
"github.com/photoprism/photoprism/internal/entity"
)
// Errors returns the error log.
func Errors(limit, offset int) (results entity.Errors, err error) {
// Errors returns the error log filtered with an optional search string.
func Errors(limit, offset int, s string) (results entity.Errors, err error) {
stmt := Db()
s = strings.TrimSpace(s)
if len(s) >= 3 {
stmt = stmt.Where("error_message LIKE ?", "%"+s+"%")
}
err = stmt.Order("error_time DESC").Limit(limit).Offset(offset).Find(&results).Error
return results, err