WebDAV: Improve update and reset of remote connection errors #1781

This commit is contained in:
Michael Mayer 2022-03-27 21:37:11 +02:00
parent 45922f8db0
commit 736b03f87f
22 changed files with 392 additions and 137 deletions

View file

@ -3755,9 +3755,9 @@
}
},
"node_modules/css-declaration-sorter": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.2.1.tgz",
"integrity": "sha512-4qvWVSwnc5f1ZxCe80LccU5aenhZdhuCCyaOSMhr3dDAOTXtJNfAvthW+5x0UV4k1pWzE1EwNk4ztSDCk6WzKw==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.2.2.tgz",
"integrity": "sha512-Ufadglr88ZLsrvS11gjeu/40Lw74D9Am/Jpr3LlYm5Q4ZP5KdlUhG+6u2EjyXeZcxmZ2h1ebCKngDjolpeLHpg==",
"engines": {
"node": "^10 || ^12 || >=14"
},
@ -4321,9 +4321,9 @@
}
},
"node_modules/electron-to-chromium": {
"version": "1.4.95",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.95.tgz",
"integrity": "sha512-h2VAMV/hPtmAeiDkwA8c5sjS+cWt6GlQL4ERdrOUWu7cRIG5IRk9uwR9f0utP+hPJ9ZZsADTq9HpbuT46eBYAg=="
"version": "1.4.96",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.96.tgz",
"integrity": "sha512-DPNjvNGPabv6FcyjzLAN4C0psN/GgD9rSGvMTuv81SeXG/EX3mCz0wiw9N1tUEnfQXYCJi3H8M0oFPRziZh7rw=="
},
"node_modules/emoji-regex": {
"version": "8.0.0",
@ -15331,9 +15331,9 @@
}
},
"css-declaration-sorter": {
"version": "6.2.1",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.2.1.tgz",
"integrity": "sha512-4qvWVSwnc5f1ZxCe80LccU5aenhZdhuCCyaOSMhr3dDAOTXtJNfAvthW+5x0UV4k1pWzE1EwNk4ztSDCk6WzKw==",
"version": "6.2.2",
"resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-6.2.2.tgz",
"integrity": "sha512-Ufadglr88ZLsrvS11gjeu/40Lw74D9Am/Jpr3LlYm5Q4ZP5KdlUhG+6u2EjyXeZcxmZ2h1ebCKngDjolpeLHpg==",
"requires": {}
},
"css-has-pseudo": {
@ -15730,9 +15730,9 @@
}
},
"electron-to-chromium": {
"version": "1.4.95",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.95.tgz",
"integrity": "sha512-h2VAMV/hPtmAeiDkwA8c5sjS+cWt6GlQL4ERdrOUWu7cRIG5IRk9uwR9f0utP+hPJ9ZZsADTq9HpbuT46eBYAg=="
"version": "1.4.96",
"resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.96.tgz",
"integrity": "sha512-DPNjvNGPabv6FcyjzLAN4C0psN/GgD9rSGvMTuv81SeXG/EX3mCz0wiw9N1tUEnfQXYCJi3H8M0oFPRziZh7rw=="
},
"emoji-regex": {
"version": "8.0.0",

View file

@ -67,6 +67,7 @@
</template>
<script>
import Account from "model/account";
import * as options from "options/options";
export default {
name: 'PAccountCreateDialog',
@ -75,6 +76,7 @@ export default {
},
data() {
return {
options: options,
showPassword: false,
loading: false,
search: null,

View file

@ -255,6 +255,30 @@
:items="items.types">
</v-select>
</v-flex>
<v-flex xs12 sm6 class="px-2">
<v-select
v-model="model.AccTimeout"
:label="$gettext('Timeout')"
browser-autocomplete="off"
hide-details
color="secondary-dark"
item-text="text"
item-value="value"
:items="options.Timeouts()">
</v-select>
</v-flex>
<v-flex xs12 sm6 class="px-2">
<v-select
v-model="model.RetryLimit"
:label="$gettext('Retry Limit')"
browser-autocomplete="off"
hide-details
color="secondary-dark"
item-text="text"
item-value="value"
:items="options.RetryLimits()">
</v-select>
</v-flex>
</v-layout>
<v-layout row wrap>
<v-flex xs12 text-xs-right class="pt-3 pb-0">

View file

@ -41,15 +41,15 @@ msgstr ""
msgid "%{n} pictures found"
msgstr ""
#: src/options/options.js:325
#: src/options/options.js:371
msgid "1 hour"
msgstr ""
#: src/options/options.js:327
#: src/options/options.js:373
msgid "12 hours"
msgstr ""
#: src/options/options.js:326
#: src/options/options.js:372
msgid "4 hours"
msgstr ""
@ -71,7 +71,7 @@ msgstr ""
#: src/component/navigation.vue:58
#: src/dialog/share/upload.vue:112
#: src/model/account.js:97
#: src/model/account.js:98
#: src/pages/settings.vue:74
msgid "Account"
msgstr ""
@ -107,7 +107,7 @@ msgid "Add pictures from search results by selecting them."
msgstr ""
#: src/dialog/account/add.vue:6
#: src/pages/settings/sync.vue:46
#: src/pages/settings/sync.vue:47
msgid "Add Server"
msgstr ""
@ -129,23 +129,23 @@ msgstr ""
msgid "Advanced"
msgstr ""
#: src/options/options.js:335
#: src/options/options.js:381
msgid "After 1 day"
msgstr ""
#: src/options/options.js:336
#: src/options/options.js:382
msgid "After 3 days"
msgstr ""
#: src/options/options.js:337
#: src/options/options.js:383
msgid "After 7 days"
msgstr ""
#: src/options/options.js:339
#: src/options/options.js:385
msgid "After one month"
msgstr ""
#: src/options/options.js:341
#: src/options/options.js:387
msgid "After one year"
msgstr ""
@ -154,11 +154,11 @@ msgstr ""
msgid "After selecting pictures from search results, you can add them to an album using the context menu."
msgstr ""
#: src/options/options.js:340
#: src/options/options.js:386
msgid "After two months"
msgstr ""
#: src/options/options.js:338
#: src/options/options.js:384
msgid "After two weeks"
msgstr ""
@ -272,7 +272,7 @@ msgstr ""
msgid "Any private photos and videos remain private and won't be shared."
msgstr ""
#: src/dialog/account/edit.vue:471
#: src/dialog/account/edit.vue:477
msgid "API Key"
msgstr ""
@ -358,19 +358,19 @@ msgstr ""
msgid "Bio"
msgstr ""
#: src/options/options.js:360
#: src/options/options.js:406
msgid "Black"
msgstr ""
#: src/options/options.js:373
#: src/options/options.js:419
msgid "Blackman: Lanczos Modification, Less Ringing Artifacts"
msgstr ""
#: src/options/options.js:356
#: src/options/options.js:402
msgid "Blue"
msgstr ""
#: src/options/options.js:357
#: src/options/options.js:403
msgid "Brown"
msgstr ""
@ -382,7 +382,7 @@ msgstr ""
msgid "Browse indexed files and folders in Library."
msgstr ""
#: src/options/options.js:367
#: src/options/options.js:413
msgid "Bug Report"
msgstr ""
@ -433,8 +433,8 @@ msgstr ""
msgid "Can't select more items"
msgstr ""
#: src/dialog/account/add.vue:15
#: src/dialog/account/edit.vue:94
#: src/dialog/account/add.vue:17
#: src/dialog/account/edit.vue:102
#: src/dialog/account/remove.vue:15
#: src/dialog/album/delete.vue:15
#: src/dialog/album/edit.vue:42
@ -516,12 +516,12 @@ msgstr ""
msgid "Confidence"
msgstr ""
#: src/dialog/account/add.vue:16
#: src/dialog/account/add.vue:18
msgid "Connect"
msgstr ""
#: src/dialog/webdav.vue:4
#: src/pages/settings/sync.vue:42
#: src/pages/settings/sync.vue:43
msgid "Connect via WebDAV"
msgstr ""
@ -580,7 +580,7 @@ msgstr ""
msgid "Creating thumbnails for"
msgstr ""
#: src/options/options.js:375
#: src/options/options.js:421
msgid "Cubic: Moderate Quality, Good Performance"
msgstr ""
@ -588,11 +588,11 @@ msgstr ""
msgid "Current Password"
msgstr ""
#: src/options/options.js:364
#: src/options/options.js:410
msgid "Customer Support"
msgstr ""
#: src/options/options.js:355
#: src/options/options.js:401
msgid "Cyan"
msgstr ""
@ -600,7 +600,7 @@ msgstr ""
msgid "Cyano"
msgstr ""
#: src/options/options.js:328
#: src/options/options.js:374
msgid "Daily"
msgstr ""
@ -613,6 +613,7 @@ msgid "Debug Logs"
msgstr ""
#: src/options/options.js:194
#: src/options/options.js:325
msgid "Default"
msgstr ""
@ -738,7 +739,7 @@ msgstr ""
msgid "Don't use TensorFlow for image classification."
msgstr ""
#: src/options/options.js:368
#: src/options/options.js:414
msgid "Donations"
msgstr ""
@ -768,7 +769,7 @@ msgstr ""
msgid "Download"
msgstr ""
#: src/dialog/account/edit.vue:320
#: src/dialog/account/edit.vue:322
msgid "Download remote files"
msgstr ""
@ -777,7 +778,7 @@ msgid "Download single files and zip archives."
msgstr ""
#: src/component/album/clipboard.vue:86
#: src/component/album/toolbar.vue:106
#: src/component/album/toolbar.vue:105
#: src/component/file/clipboard.vue:46
#: src/component/label/clipboard.vue:59
#: src/component/photo/cards.vue:63
@ -868,7 +869,7 @@ msgstr ""
msgid "Estimates"
msgstr ""
#: src/options/options.js:329
#: src/options/options.js:375
msgid "Every two days"
msgstr ""
@ -945,7 +946,7 @@ msgstr ""
msgid "Favorites"
msgstr ""
#: src/options/options.js:366
#: src/options/options.js:412
msgid "Feature Request"
msgstr ""
@ -1024,7 +1025,7 @@ msgstr ""
msgid "Getting Support"
msgstr ""
#: src/options/options.js:350
#: src/options/options.js:396
msgid "Gold"
msgstr ""
@ -1032,11 +1033,11 @@ msgstr ""
msgid "Grayscale"
msgstr ""
#: src/options/options.js:353
#: src/options/options.js:399
msgid "Green"
msgstr ""
#: src/options/options.js:359
#: src/options/options.js:405
msgid "Grey"
msgstr ""
@ -1072,6 +1073,10 @@ msgstr ""
msgid "Hide photos that have been moved to archive."
msgstr ""
#: src/options/options.js:329
msgid "High"
msgstr ""
#: src/dialog/photo/files.vue:109
#: src/dialog/photo/files.vue:106
msgid "High Dynamic Range (HDR)"
@ -1224,7 +1229,7 @@ msgstr ""
msgid "Labels deleted"
msgstr ""
#: src/options/options.js:374
#: src/options/options.js:420
msgid "Lanczos: Detail Preservation, Minimal Artifacts"
msgstr ""
@ -1233,7 +1238,7 @@ msgid "Language"
msgstr ""
#: src/pages/settings/sync.vue:30
msgid "Last Backup"
msgid "Last Sync"
msgstr ""
#: src/dialog/photo/details.vue:291
@ -1272,7 +1277,7 @@ msgstr ""
msgid "Like"
msgstr ""
#: src/options/options.js:352
#: src/options/options.js:398
msgid "Lime"
msgstr ""
@ -1280,7 +1285,7 @@ msgstr ""
msgid "Limit reached, showing first %{n} files"
msgstr ""
#: src/options/options.js:376
#: src/options/options.js:422
msgid "Linear: Very Smooth, Best Performance"
msgstr ""
@ -1348,7 +1353,11 @@ msgstr ""
msgid "Longitude"
msgstr ""
#: src/options/options.js:346
#: src/options/options.js:333
msgid "Low"
msgstr ""
#: src/options/options.js:392
msgid "Magenta"
msgstr ""
@ -1448,7 +1457,7 @@ msgstr ""
#: src/component/photo/cards.vue:39
#: src/component/photo/list.vue:47
#: src/component/photo/list.vue:235
#: src/dialog/account/edit.vue:396
#: src/dialog/account/edit.vue:402
#: src/dialog/album/edit.vue:106
#: src/dialog/photo/files.vue:71
#: src/dialog/photo/files.vue:68
@ -1481,8 +1490,8 @@ msgstr ""
msgid "Name too long"
msgstr ""
#: src/options/options.js:324
#: src/options/options.js:334
#: src/options/options.js:370
#: src/options/options.js:380
#: src/pages/settings/sync.vue:50
msgid "Never"
msgstr ""
@ -1557,7 +1566,7 @@ msgstr ""
msgid "No recently edited pictures"
msgstr ""
#: src/pages/settings/sync.vue:47
#: src/pages/settings/sync.vue:48
msgid "No servers configured."
msgstr ""
@ -1588,6 +1597,7 @@ msgid "Non-photographic and low-quality images require a review before they appe
msgstr ""
#: src/options/options.js:261
#: src/options/options.js:337
msgid "None"
msgstr ""
@ -1599,7 +1609,7 @@ msgstr ""
msgid "Note you may manually manage your originals folder and importing is optional."
msgstr ""
#: src/pages/settings/sync.vue:34
#: src/pages/settings/sync.vue:35
msgid "Note:"
msgstr ""
@ -1631,7 +1641,7 @@ msgstr ""
msgid "Oldest first"
msgstr ""
#: src/options/options.js:330
#: src/options/options.js:376
msgid "Once a week"
msgstr ""
@ -1680,7 +1690,7 @@ msgstr ""
msgid "or ask in our Community Chat"
msgstr ""
#: src/options/options.js:349
#: src/options/options.js:395
msgid "Orange"
msgstr ""
@ -1706,7 +1716,7 @@ msgstr ""
msgid "Originals"
msgstr ""
#: src/options/options.js:369
#: src/options/options.js:415
msgid "Other"
msgstr ""
@ -1727,7 +1737,7 @@ msgid "Panoramas"
msgstr ""
#: src/dialog/account/add.vue:105
#: src/dialog/account/edit.vue:450
#: src/dialog/account/edit.vue:456
#: src/dialog/share.vue:24
#: src/pages/auth/login.vue:96
#: src/pages/auth/login.vue:98
@ -1777,7 +1787,7 @@ msgstr ""
msgid "Photos"
msgstr ""
#: src/options/options.js:347
#: src/options/options.js:393
msgid "Pink"
msgstr ""
@ -1826,7 +1836,7 @@ msgstr ""
msgid "post your question in GitHub Discussions"
msgstr ""
#: src/dialog/account/edit.vue:337
#: src/dialog/account/edit.vue:340
msgid "Preserve filenames"
msgstr ""
@ -1864,7 +1874,7 @@ msgstr ""
msgid "Private"
msgstr ""
#: src/options/options.js:365
#: src/options/options.js:411
msgid "Product Feedback"
msgstr ""
@ -1873,7 +1883,7 @@ msgstr ""
msgid "Projection"
msgstr ""
#: src/options/options.js:345
#: src/options/options.js:391
msgid "Purple"
msgstr ""
@ -1942,7 +1952,7 @@ msgstr ""
msgid "Recognizes faces so that specific people can be found."
msgstr ""
#: src/options/options.js:348
#: src/options/options.js:394
msgid "Red"
msgstr ""
@ -2002,6 +2012,10 @@ msgstr ""
msgid "Restore"
msgstr ""
#: src/dialog/account/edit.vue:535
msgid "Retry Limit"
msgstr ""
#: src/pages/settings/account.vue:106
msgid "Retype Password"
msgstr ""
@ -2011,7 +2025,7 @@ msgstr ""
msgid "Review"
msgstr ""
#: src/dialog/account/edit.vue:97
#: src/dialog/account/edit.vue:105
#: src/dialog/album/edit.vue:45
#: src/dialog/share.vue:61
msgid "Save"
@ -2088,7 +2102,7 @@ msgid "Server"
msgstr ""
#: src/dialog/account/add.vue:65
#: src/dialog/account/edit.vue:414
#: src/dialog/account/edit.vue:420
#: src/dialog/share.vue:22
msgid "Service URL"
msgstr ""
@ -2291,7 +2305,7 @@ msgstr ""
msgid "Sync"
msgstr ""
#: src/dialog/account/edit.vue:371
#: src/dialog/account/edit.vue:377
msgid "Sync raw and video files"
msgstr ""
@ -2303,7 +2317,7 @@ msgstr ""
msgid "Taken"
msgstr ""
#: src/options/options.js:354
#: src/options/options.js:400
msgid "Teal"
msgstr ""
@ -2338,7 +2352,7 @@ msgid "This currently is a sponsor feature to thank everyone who supports the de
msgstr ""
#: src/dialog/webdav.vue:17
#: src/pages/settings/sync.vue:36
#: src/pages/settings/sync.vue:37
msgid "This mounts the originals folder as a network drive and allows you to open, edit, and delete files from your computer or smartphone as if they were local."
msgstr ""
@ -2354,6 +2368,10 @@ msgstr ""
msgid "Time Zone"
msgstr ""
#: src/dialog/account/edit.vue:515
msgid "Timeout"
msgstr ""
#: src/component/photo/list.vue:43
#: src/dialog/photo/details.vue:97
#: src/dialog/photo/info.vue:45
@ -2407,7 +2425,7 @@ msgstr ""
msgid "Try again using other filters or keywords."
msgstr ""
#: src/dialog/account/edit.vue:489
#: src/dialog/account/edit.vue:495
#: src/dialog/photo/files.vue:89
#: src/dialog/photo/files.vue:86
#: src/dialog/photo/files.vue:37
@ -2504,7 +2522,7 @@ msgstr ""
msgid "Upload failed"
msgstr ""
#: src/dialog/account/edit.vue:354
#: src/dialog/account/edit.vue:359
msgid "Upload local files"
msgstr ""
@ -2543,7 +2561,7 @@ msgid "User Interface"
msgstr ""
#: src/dialog/account/add.vue:84
#: src/dialog/account/edit.vue:432
#: src/dialog/account/edit.vue:438
#: src/dialog/share.vue:23
msgid "Username"
msgstr ""
@ -2595,7 +2613,7 @@ msgstr ""
msgid "WebDAV clients can connect to PhotoPrism using the following URL:"
msgstr ""
#: src/pages/settings/sync.vue:35
#: src/pages/settings/sync.vue:36
msgid "WebDAV clients, like Microsofts Windows Explorer or Apple's Finder, can connect directly to PhotoPrism."
msgstr ""
@ -2604,7 +2622,7 @@ msgstr ""
msgid "WebDAV Upload"
msgstr ""
#: src/options/options.js:358
#: src/options/options.js:404
msgid "White"
msgstr ""
@ -2613,7 +2631,7 @@ msgstr ""
msgid "Year"
msgstr ""
#: src/options/options.js:351
#: src/options/options.js:397
msgid "Yellow"
msgstr ""

View file

@ -39,6 +39,7 @@ export class Account extends RestModel {
AccKey: "",
AccUser: "",
AccPass: "",
AccTimeout: "",
AccError: "",
AccErrors: 0,
AccShare: true,

View file

@ -320,6 +320,52 @@ export const PhotoTypes = () => [
},
];
export const Timeouts = () => [
{
text: $gettext("Default"),
value: "",
},
{
text: $gettext("High"),
value: "high",
},
{
text: $gettext("Low"),
value: "low",
},
{
text: $gettext("None"),
value: "none",
},
];
export const RetryLimits = () => [
{
text: "None",
value: -1,
},
{
text: "1",
value: 1,
},
{
text: "2",
value: 2,
},
{
text: "3",
value: 3,
},
{
text: "4",
value: 4,
},
{
text: "5",
value: 5,
},
];
export const Intervals = () => [
{ value: 0, text: $gettext("Never") },
{ value: 3600, text: $gettext("1 hour") },

View file

@ -28,7 +28,8 @@
<v-btn icon small flat :ripple="false"
class="action-toggle-sync"
@click.stop.prevent="editSync(props.item)">
<v-icon v-if="props.item.AccSync" color="secondary-dark">sync</v-icon>
<v-icon v-if="props.item.AccErrors" color="secondary-dark" :title="props.item.AccError">report_problem</v-icon>
<v-icon v-else-if="props.item.AccSync" color="secondary-dark">sync</v-icon>
<v-icon v-else color="secondary-dark">sync_disabled</v-icon>
</v-btn>
</td>
@ -114,7 +115,7 @@ export default {
{text: this.$gettext('Upload'), value: 'AccShare', sortable: false, align: 'center'},
{text: this.$gettext('Sync'), value: 'AccSync', sortable: false, align: 'center'},
{
text: this.$gettext('Last Backup'),
text: this.$gettext('Last Sync'),
value: 'SyncDate',
sortable: false,
class: 'hidden-sm-and-down',

View file

@ -22,6 +22,14 @@ var StatusCommand = cli.Command{
// statusAction checks if the web server is running.
func statusAction(ctx *cli.Context) error {
conf := config.NewConfig(ctx)
// Create new http.Client instance.
//
// NOTE: Timeout specifies a time limit for requests made by
// this Client. The timeout includes connection time, any
// redirects, and reading the response body. The timer remains
// running after Get, Head, Post, or Do return and will
// interrupt reading of the Response.Body.
client := &http.Client{Timeout: 10 * time.Second}
url := fmt.Sprintf("http://%s:%d/api/v1/status", conf.HttpHost(), conf.HttpPort())

View file

@ -2,15 +2,17 @@ package entity
import (
"database/sql"
"fmt"
"sort"
"time"
"github.com/ulule/deepcopier"
"github.com/photoprism/photoprism/internal/form"
"github.com/photoprism/photoprism/internal/remote"
"github.com/photoprism/photoprism/internal/remote/webdav"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/txt"
"github.com/ulule/deepcopier"
)
const (
@ -32,11 +34,12 @@ type Account struct {
AccKey string `gorm:"type:VARBINARY(255);"`
AccUser string `gorm:"type:VARBINARY(255);"`
AccPass string `gorm:"type:VARBINARY(255);"`
AccTimeout string `gorm:"type:VARBINARY(16);"` // Request timeout: default, high, medium, low, none
AccError string `gorm:"type:VARBINARY(512);"`
AccErrors int
AccShare bool
AccSync bool
RetryLimit int
AccErrors int // Number of general account errors, there are counters for individual files too.
AccShare bool // Manual upload enabled, see SharePath, ShareSize, and ShareExpires.
AccSync bool // Background sync enabled, see SyncDownload and SyncUpload.
RetryLimit int // Number of remote request retry attempts.
SharePath string `gorm:"type:VARBINARY(500);"`
ShareSize string `gorm:"type:VARBINARY(16);"`
ShareExpires int
@ -59,6 +62,7 @@ func CreateAccount(form form.Account) (model *Account, err error) {
ShareSize: "",
ShareExpires: 0,
RetryLimit: 3,
AccTimeout: string(webdav.TimeoutDefault),
SyncStatus: AccountSyncStatusRefresh,
}
@ -67,18 +71,66 @@ func CreateAccount(form form.Account) (model *Account, err error) {
return model, err
}
// LogError updates the account error count and message.
func (m *Account) LogError(err error) error {
if err == nil {
return m.ResetErrors(true, true)
}
// Update error message and increase count.
m.AccError = err.Error()
m.AccErrors++
// Disable sharing when retry limit is reached.
if m.RetryLimit > 0 && m.AccErrors > m.RetryLimit {
m.AccShare = false
}
// Update fields in database.
return m.Updates(Account{AccError: m.AccError, AccErrors: m.AccErrors, AccShare: m.AccShare})
}
// ResetErrors resets the account and related file error messages and counters.
func (m *Account) ResetErrors(share, sync bool) error {
if !share && !sync {
return nil
}
if m.ID == 0 {
return fmt.Errorf("invalid account id")
}
if share {
if err := Db().Model(FileShare{}).Where("account_id = ?", m.ID).Updates(Values{"error": "", "errors": 0}).Error; err != nil {
return err
}
}
if sync {
if err := Db().Model(FileSync{}).Where("account_id = ?", m.ID).Updates(Values{"error": "", "errors": 0}).Error; err != nil {
return err
}
}
m.AccError = ""
m.AccErrors = 0
return m.Updates(Values{"acc_error": m.AccError, "acc_errors": m.AccErrors})
}
// SaveForm saves the entity using form data and stores it in the database.
func (m *Account) SaveForm(form form.Account) error {
db := Db()
// Copy model values from form.
if err := deepcopier.Copy(m).From(form); err != nil {
return err
}
// TODO: Support for other remote services in addition to WebDAV.
if m.AccType != remote.ServiceWebDAV {
m.AccShare = false
m.AccSync = false
m.AccShare = false // Disable manual upload.
m.AccSync = false // Disable background sync.
}
// Prevent two-way sync, see https://github.com/photoprism/photoprism/issues/1785
@ -86,23 +138,39 @@ func (m *Account) SaveForm(form form.Account) error {
m.SyncUpload = false
}
// Set defaults.
// Set default manual upload folder if empty.
if m.SharePath == "" {
m.SharePath = "/"
}
// Set default background sync folder if empty.
if m.SyncPath == "" {
m.SyncPath = "/"
}
// Number of remote request retry attempts.
if m.RetryLimit < -1 {
m.RetryLimit = -1 // Disabled.
} else if m.RetryLimit > 999 {
m.RetryLimit = 999 // 999 retries max.
}
// Refresh after performing changes.
if m.AccSync && m.SyncStatus == AccountSyncStatusSynced {
m.SyncStatus = AccountSyncStatusRefresh
}
// Reset share/sync errors.
if err := m.ResetErrors(m.AccShare, m.AccSync); err != nil {
log.Debugf("account: %s", err)
log.Errorf("account: failed to reset errors")
}
// Ensure account name and owner are not too long.
m.AccName = txt.Clip(m.AccName, txt.ClipName)
m.AccOwner = txt.Clip(m.AccOwner, txt.ClipName)
// Save changes.
return db.Save(m).Error
}
@ -114,12 +182,18 @@ func (m *Account) Delete() error {
// Directories returns a list of directories or albums in an account.
func (m *Account) Directories() (result fs.FileInfos, err error) {
if m.AccType == remote.ServiceWebDAV {
c := webdav.New(m.AccURL, m.AccUser, m.AccPass)
result, err = c.Directories("/", true, webdav.SyncTimeout)
client := webdav.New(m.AccURL, m.AccUser, m.AccPass, webdav.Timeout(m.AccTimeout))
result, err = client.Directories("/", true, 0)
}
// Sort directory list.
sort.Sort(result)
// Update error count and message.
if err := m.LogError(err); err != nil {
log.Warnf("account: %s", err)
}
return result, err
}

View file

@ -49,7 +49,7 @@ func (m *FileSync) Updates(values interface{}) error {
return UnscopedDb().Model(m).UpdateColumns(values).Error
}
// Updates a column in the database.
// Update a column in the database.
func (m *FileSync) Update(attr string, value interface{}) error {
return UnscopedDb().Model(m).UpdateColumn(attr, value).Error
}

View file

@ -15,10 +15,11 @@ type Account struct {
AccKey string `json:"AccKey"`
AccUser string `json:"AccUser"`
AccPass string `json:"AccPass"`
AccTimeout string `json:"AccTimeout"` // Request timeout: default, high, medium, low, none
AccError string `json:"AccError"`
AccShare bool `json:"AccShare"`
AccSync bool `json:"AccSync"`
RetryLimit int `json:"RetryLimit"`
AccShare bool `json:"AccShare"` // Manual upload enabled, see SharePath, ShareSize, and ShareExpires.
AccSync bool `json:"AccSync"` // Background sync enabled, see SyncDownload and SyncUpload.
RetryLimit int `json:"RetryLimit"` // Number of remote request retry attempts.
SharePath string `json:"SharePath"`
ShareSize string `json:"ShareSize"`
ShareExpires int `json:"ShareExpires"`

View file

@ -138,7 +138,15 @@ func (c *Config) Refresh() (err error) {
c.Sanitize()
// Create new http.Client instance.
//
// NOTE: Timeout specifies a time limit for requests made by
// this Client. The timeout includes connection time, any
// redirects, and reading the response body. The timer remains
// running after Get, Head, Post, or Do return and will
// interrupt reading of the Response.Body.
client := &http.Client{Timeout: 60 * time.Second}
url := ServiceURL
method := http.MethodPost

View file

@ -55,7 +55,15 @@ func (c *Config) SendFeedback(f form.Feedback) (err error) {
feedback.UserAgent = f.UserAgent
feedback.ApiKey = c.Key
// Create new http.Client instance.
//
// NOTE: Timeout specifies a time limit for requests made by
// this Client. The timeout includes connection time, any
// redirects, and reading the response body. The timer remains
// running after Get, Head, Post, or Do return and will
// interrupt reading of the Response.Body.
client := &http.Client{Timeout: 60 * time.Second}
url := fmt.Sprintf(FeedbackURL, c.Key)
method := http.MethodPost

View file

@ -36,10 +36,10 @@ var ReverseLookupURL = "https://places.photoprism.app/v1/location/%s"
var Retries = 3
var RetryDelay = 33 * time.Millisecond
var client = &http.Client{Timeout: 60 * time.Second}
// FindLocation retrieves location details from the backend API.
func FindLocation(id string) (result Location, err error) {
// Normalize S2 Cell ID.
id = s2.NormalizeToken(id)
@ -99,6 +99,15 @@ func FindLocation(id string) (result Location, err error) {
var r *http.Response
// Create new http.Client.
//
// NOTE: Timeout specifies a time limit for requests made by
// this Client. The timeout includes connection time, any
// redirects, and reading the response body. The timer remains
// running after Get, Head, Post, or Do return and will
// interrupt reading of the Response.Body.
client := &http.Client{Timeout: 60 * time.Second}
// Perform request.
for i := 0; i < Retries; i++ {
r, err = client.Do(req)

View file

@ -34,8 +34,6 @@ import (
"time"
)
var client = &http.Client{Timeout: 30 * time.Second} // TODO: Change timeout if needed
const (
ServiceWebDAV = "webdav"
ServiceFacebook = "facebook"
@ -57,6 +55,16 @@ func HttpOk(method, rawUrl string) bool {
return false
}
// Create new http.Client instance.
//
// NOTE: Timeout specifies a time limit for requests made by
// this Client. The timeout includes connection time, any
// redirects, and reading the response body. The timer remains
// running after Get, Head, Post, or Do return and will
// interrupt reading of the Response.Body.
client := &http.Client{Timeout: 30 * time.Second}
// Send request to see if it fails.
if resp, err := client.Do(req); err != nil {
return false
} else if resp.StatusCode < 400 {

View file

@ -33,28 +33,57 @@ import (
"runtime/debug"
"time"
"github.com/studio-b12/gowebdav"
"github.com/photoprism/photoprism/internal/event"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/studio-b12/gowebdav"
"github.com/photoprism/photoprism/pkg/sanitize"
)
// Global log instance.
var log = event.Log
const SyncTimeout = time.Second * 45
const AsyncTimeout = time.Minute * 20
type Timeout string
// Request Timeout options.
const (
TimeoutHigh Timeout = "high" // 120 * Second
TimeoutDefault Timeout = "" // 60 * Second
TimeoutMedium Timeout = "medium" // 60 * Second
TimeoutLow Timeout = "low" // 30 * Second
TimeoutNone Timeout = "none" // 0
)
// Second represents a second on which other timeouts are based.
const Second = time.Second
// MaxRequestDuration is the maximum request duration e.g. for recursive retrieval of large remote directory structures.
const MaxRequestDuration = 30 * time.Minute
// Durations maps Timeout options to specific time durations.
var Durations = map[Timeout]time.Duration{
TimeoutHigh: 120 * Second,
TimeoutDefault: 60 * Second,
TimeoutMedium: 60 * Second,
TimeoutLow: 30 * Second,
TimeoutNone: 0,
}
// Client represents a gowebdav.Client wrapper.
type Client struct {
client *gowebdav.Client
client *gowebdav.Client
timeout Timeout
}
// New creates a new WebDAV client.
func New(url, user, pass string) Client {
clt := gowebdav.NewClient(url, user, pass)
clt.SetTimeout(10 * time.Minute) // TODO: Change timeout if needed
func New(url, user, pass string, timeout Timeout) Client {
// Create a new gowebdav.Client instance.
client := gowebdav.NewClient(url, user, pass)
// Create a new gowebdav.Client wrapper.
result := Client{
client: clt,
client: client,
timeout: timeout,
}
return result
@ -95,10 +124,14 @@ func (c Client) Files(dir string) (result fs.FileInfos, err error) {
return result, nil
}
// Directories returns all sub directories in path as string slice.
// Directories returns all subdirectories in a path as string slice.
func (c Client) Directories(root string, recursive bool, timeout time.Duration) (result fs.FileInfos, err error) {
start := time.Now()
if timeout == 0 {
timeout = Durations[c.timeout]
}
result, err = c.fetchDirs(root, recursive, start, timeout)
if time.Now().Sub(start) >= timeout {
@ -129,7 +162,7 @@ func (c Client) fetchDirs(root string, recursive bool, start time.Time, timeout
result = append(result, info)
if recursive && time.Now().Sub(start) < timeout {
if recursive && (timeout < time.Second || time.Now().Sub(start) < timeout) {
subDirs, err := c.fetchDirs(info.Abs, true, start, timeout)
if err != nil {
@ -147,35 +180,41 @@ func (c Client) fetchDirs(root string, recursive bool, start time.Time, timeout
func (c Client) Download(from, to string, force bool) (err error) {
defer func() {
if r := recover(); r != nil {
err = fmt.Errorf("webdav: %s (panic while downloading)\nstack: %s", r, debug.Stack())
log.Errorf("webdav: %s (panic)\nstack: %s", r, sanitize.Log(from))
err = fmt.Errorf("webdav: unexpected error while downloading %s", sanitize.Log(from))
}
}()
// Skip if file already exists.
if _, err := os.Stat(to); err == nil && !force {
return fmt.Errorf("webdav: download skipped, %s already exists", to)
return fmt.Errorf("webdav: download skipped, %s already exists", sanitize.Log(to))
}
dir := path.Dir(to)
dirInfo, err := os.Stat(dir)
if err != nil {
// Create directory
// Create local storage path.
if err := os.MkdirAll(dir, os.ModePerm); err != nil {
return fmt.Errorf("webdav: cannot create %s (%s)", dir, err)
return fmt.Errorf("webdav: cannot create folder %s (%s)", sanitize.Log(dir), err)
}
} else if !dirInfo.IsDir() {
return fmt.Errorf("webdav: %s is not a folder", dir)
return fmt.Errorf("webdav: %s is not a folder", sanitize.Log(dir))
}
var bytes []byte
// Start download.
bytes, err = c.client.Read(from)
// Error?
if err != nil {
return err
log.Errorf("webdav: %s", sanitize.Log(err.Error()))
return fmt.Errorf("webdav: failed downloading %s", sanitize.Log(from))
}
return os.WriteFile(to, bytes, 0644)
// Write data to file and return.
return os.WriteFile(to, bytes, os.ModePerm)
}
// DownloadDir downloads all files from a remote to a local directory.
@ -191,9 +230,9 @@ func (c Client) DownloadDir(from, to string, recursive, force bool) (errs []erro
if _, err = os.Stat(dest); err == nil {
// File already exists.
msg := fmt.Errorf("webdav: %s exists", dest)
errs = append(errs, msg)
msg := fmt.Errorf("webdav: %s already exists", sanitize.Log(dest))
log.Warn(msg)
errs = append(errs, msg)
continue
}
@ -209,7 +248,7 @@ func (c Client) DownloadDir(from, to string, recursive, force bool) (errs []erro
return errs
}
dirs, err := c.Directories(from, false, AsyncTimeout)
dirs, err := c.Directories(from, false, MaxRequestDuration)
for _, dir := range dirs {
errs = append(errs, c.DownloadDir(dir.Abs, to, true, force)...)
@ -245,7 +284,7 @@ func (c Client) Upload(from, to string) (err error) {
_ = file.Close()
}(file)
return c.client.WriteStream(to, file, 0644)
return c.client.WriteStream(to, file, os.ModePerm)
}
// Delete deletes a single file or directory on a remote server.

View file

@ -5,9 +5,10 @@ import (
"path/filepath"
"testing"
"github.com/stretchr/testify/assert"
"github.com/photoprism/photoprism/pkg/fs"
"github.com/photoprism/photoprism/pkg/rnd"
"github.com/stretchr/testify/assert"
)
const (
@ -17,13 +18,13 @@ const (
)
func TestConnect(t *testing.T) {
c := New(testUrl, testUser, testPass)
c := New(testUrl, testUser, testPass, TimeoutLow)
assert.IsType(t, Client{}, c)
}
func TestClient_Files(t *testing.T) {
c := New(testUrl, testUser, testPass)
c := New(testUrl, testUser, testPass, TimeoutLow)
assert.IsType(t, Client{}, c)
@ -39,12 +40,12 @@ func TestClient_Files(t *testing.T) {
}
func TestClient_Directories(t *testing.T) {
c := New(testUrl, testUser, testPass)
c := New(testUrl, testUser, testPass, TimeoutLow)
assert.IsType(t, Client{}, c)
t.Run("non-recursive", func(t *testing.T) {
dirs, err := c.Directories("", false, SyncTimeout)
dirs, err := c.Directories("", false, MaxRequestDuration)
if err != nil {
t.Fatal(err)
@ -62,7 +63,7 @@ func TestClient_Directories(t *testing.T) {
})
t.Run("recursive", func(t *testing.T) {
dirs, err := c.Directories("", true, SyncTimeout)
dirs, err := c.Directories("", true, 0)
if err != nil {
t.Fatal(err)
@ -75,7 +76,7 @@ func TestClient_Directories(t *testing.T) {
}
func TestClient_Download(t *testing.T) {
c := New(testUrl, testUser, testPass)
c := New(testUrl, testUser, testPass, TimeoutDefault)
assert.IsType(t, Client{}, c)
@ -106,7 +107,7 @@ func TestClient_Download(t *testing.T) {
}
func TestClient_DownloadDir(t *testing.T) {
c := New(testUrl, testUser, testPass)
c := New(testUrl, testUser, testPass, TimeoutLow)
assert.IsType(t, Client{}, c)
@ -136,7 +137,7 @@ func TestClient_DownloadDir(t *testing.T) {
}
func TestClient_UploadAndDelete(t *testing.T) {
c := New(testUrl, testUser, testPass)
c := New(testUrl, testUser, testPass, TimeoutLow)
assert.IsType(t, Client{}, c)

View file

@ -78,7 +78,7 @@ func (worker *Share) Start() (err error) {
continue
}
client := webdav.New(a.AccURL, a.AccUser, a.AccPass)
client := webdav.New(a.AccURL, a.AccUser, a.AccPass, webdav.Timeout(a.AccTimeout))
existingDirs := make(map[string]string)
for _, file := range files {
@ -124,7 +124,8 @@ func (worker *Share) Start() (err error) {
file.Status = entity.FileShareShared
}
if a.RetryLimit >= 0 && file.Errors > a.RetryLimit {
// Failed too often?
if a.RetryLimit > 0 && file.Errors > a.RetryLimit {
file.Status = entity.FileShareError
}
@ -158,7 +159,7 @@ func (worker *Share) Start() (err error) {
continue
}
client := webdav.New(a.AccURL, a.AccUser, a.AccPass)
client := webdav.New(a.AccURL, a.AccUser, a.AccPass, webdav.Timeout(a.AccTimeout))
for _, file := range files {
if mutex.ShareWorker.Canceled() {

View file

@ -66,8 +66,8 @@ func (worker *Sync) Start() (err error) {
continue
}
if a.AccErrors > a.RetryLimit {
a.AccErrors = 0
// Failed too often?
if a.RetryLimit > 0 && a.AccErrors > a.RetryLimit {
a.AccSync = false
if err := entity.Db().Save(&a).Error; err != nil {
@ -109,6 +109,7 @@ func (worker *Sync) Start() (err error) {
if complete, err := worker.download(a); err != nil {
accErrors++
accError = err.Error()
syncStatus = entity.AccountSyncStatusRefresh
} else if complete {
if a.SyncUpload {
syncStatus = entity.AccountSyncStatusUpload
@ -123,6 +124,7 @@ func (worker *Sync) Start() (err error) {
if complete, err := worker.upload(a); err != nil {
accErrors++
accError = err.Error()
syncStatus = entity.AccountSyncStatusRefresh
} else if complete {
synced = true
syncStatus = entity.AccountSyncStatusSynced

View file

@ -76,7 +76,7 @@ func (worker *Sync) download(a entity.Account) (complete bool, err error) {
log.Infof("sync: downloading from %s", a.AccName)
client := webdav.New(a.AccURL, a.AccUser, a.AccPass)
client := webdav.New(a.AccURL, a.AccUser, a.AccPass, webdav.Timeout(a.AccTimeout))
var baseDir string
@ -94,7 +94,8 @@ func (worker *Sync) download(a entity.Account) (complete bool, err error) {
return false, nil
}
if file.Errors > a.RetryLimit {
// Failed too often?
if a.RetryLimit > 0 && file.Errors > a.RetryLimit {
log.Debugf("sync: downloading %s failed more than %d times", file.RemoteName, a.RetryLimit)
continue
}
@ -104,14 +105,17 @@ func (worker *Sync) download(a entity.Account) (complete bool, err error) {
if _, err := os.Stat(localName); err == nil {
log.Warnf("sync: download skipped, %s already exists", localName)
file.Status = entity.FileSyncExists
file.Error = ""
file.Errors = 0
} else {
if err := client.Download(file.RemoteName, localName, false); err != nil {
worker.logError(err)
file.Errors++
file.Error = err.Error()
} else {
log.Infof("sync: downloaded %s from %s", file.RemoteName, a.AccName)
file.Status = entity.FileSyncDownloaded
file.Error = ""
file.Errors = 0
}
if mutex.SyncWorker.Canceled() {

View file

@ -14,9 +14,9 @@ func (worker *Sync) refresh(a entity.Account) (complete bool, err error) {
return false, nil
}
client := webdav.New(a.AccURL, a.AccUser, a.AccPass)
client := webdav.New(a.AccURL, a.AccUser, a.AccPass, webdav.Timeout(a.AccTimeout))
subDirs, err := client.Directories(a.SyncPath, true, webdav.AsyncTimeout)
subDirs, err := client.Directories(a.SyncPath, true, webdav.MaxRequestDuration)
if err != nil {
log.Error(err)

View file

@ -31,7 +31,7 @@ func (worker *Sync) upload(a entity.Account) (complete bool, err error) {
return true, nil
}
client := webdav.New(a.AccURL, a.AccUser, a.AccPass)
client := webdav.New(a.AccURL, a.AccUser, a.AccPass, webdav.Timeout(a.AccTimeout))
existingDirs := make(map[string]string)
for _, file := range files {