diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index 232b33c5c..cbff1132b 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -111,6 +111,7 @@ type modelIntf interface { RemoteSequence(folder string) (int64, bool) State(folder string) (string, time.Time, error) UsageReportingStats(version int, preview bool) map[string]interface{} + PullErrors(folder string) ([]model.FileError, error) } type configIntf interface { @@ -263,6 +264,7 @@ func (s *apiService) Serve() { getRestMux.HandleFunc("/rest/db/status", s.getDBStatus) // folder getRestMux.HandleFunc("/rest/db/browse", s.getDBBrowse) // folder [prefix] [dirsonly] [levels] getRestMux.HandleFunc("/rest/folder/versions", s.getFolderVersions) // folder + getRestMux.HandleFunc("/rest/folder/pullerrors", s.getPullErrors) // folder getRestMux.HandleFunc("/rest/events", s.getIndexEvents) // [since] [limit] [timeout] [events] getRestMux.HandleFunc("/rest/events/disk", s.getDiskEvents) // [since] [limit] [timeout] getRestMux.HandleFunc("/rest/stats/device", s.getDeviceStats) // - @@ -681,12 +683,23 @@ func jsonCompletion(comp model.FolderCompletion) map[string]interface{} { func (s *apiService) getDBStatus(w http.ResponseWriter, r *http.Request) { qs := r.URL.Query() folder := qs.Get("folder") - sendJSON(w, folderSummary(s.cfg, s.model, folder)) + if sum, err := folderSummary(s.cfg, s.model, folder); err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + } else { + sendJSON(w, sum) + } } -func folderSummary(cfg configIntf, m modelIntf, folder string) map[string]interface{} { +func folderSummary(cfg configIntf, m modelIntf, folder string) (map[string]interface{}, error) { var res = make(map[string]interface{}) + pullErrors, err := m.PullErrors(folder) + if err != nil && err != model.ErrFolderPaused { + // Stats from the db can still be obtained if the folder is just paused + return nil, err + } + res["pullErrors"] = len(pullErrors) + res["invalid"] = "" // Deprecated, retains external API for now global := m.GlobalSize(folder) @@ -700,7 +713,6 @@ func folderSummary(cfg configIntf, m modelIntf, folder string) map[string]interf res["inSyncFiles"], res["inSyncBytes"] = global.Files-need.Files, global.Bytes-need.Bytes - var err error res["state"], res["stateChanged"], err = m.State(folder) if err != nil { res["error"] = err.Error() @@ -721,7 +733,7 @@ func folderSummary(cfg configIntf, m modelIntf, folder string) map[string]interf } } - return res + return res, nil } func (s *apiService) postDBOverride(w http.ResponseWriter, r *http.Request) { @@ -1352,6 +1364,36 @@ func (s *apiService) postFolderVersionsRestore(w http.ResponseWriter, r *http.Re sendJSON(w, ferr) } +func (s *apiService) getPullErrors(w http.ResponseWriter, r *http.Request) { + qs := r.URL.Query() + folder := qs.Get("folder") + page, perpage := getPagingParams(qs) + + errors, err := s.model.PullErrors(folder) + + if err != nil { + http.Error(w, err.Error(), http.StatusNotFound) + return + } + + start := (page - 1) * perpage + if start >= len(errors) { + errors = nil + } else { + errors = errors[start:] + if perpage < len(errors) { + errors = errors[:perpage] + } + } + + sendJSON(w, map[string]interface{}{ + "folder": folder, + "errors": errors, + "page": page, + "perpage": perpage, + }) +} + func (s *apiService) getSystemBrowse(w http.ResponseWriter, r *http.Request) { qs := r.URL.Query() current := qs.Get("current") diff --git a/cmd/syncthing/mocked_model_test.go b/cmd/syncthing/mocked_model_test.go index e6c6949d5..b9c507a17 100644 --- a/cmd/syncthing/mocked_model_test.go +++ b/cmd/syncthing/mocked_model_test.go @@ -132,3 +132,7 @@ func (m *mockedModel) State(folder string) (string, time.Time, error) { func (m *mockedModel) UsageReportingStats(version int, preview bool) map[string]interface{} { return nil } + +func (m *mockedModel) PullErrors(folder string) ([]model.FileError, error) { + return nil, nil +} diff --git a/cmd/syncthing/summaryservice.go b/cmd/syncthing/summaryservice.go index 0a8417205..0c23f5f11 100644 --- a/cmd/syncthing/summaryservice.go +++ b/cmd/syncthing/summaryservice.go @@ -187,7 +187,10 @@ func (c *folderSummaryService) foldersToHandle() []string { func (c *folderSummaryService) sendSummary(folder string) { // The folder summary contains how many bytes, files etc // are in the folder and how in sync we are. - data := folderSummary(c.cfg, c.model, folder) + data, err := folderSummary(c.cfg, c.model, folder) + if err != nil { + return + } events.Default.Log(events.FolderSummary, map[string]interface{}{ "folder": folder, "summary": data, diff --git a/gui/default/index.html b/gui/default/index.html index 1a74a4547..86ad9d870 100644 --- a/gui/default/index.html +++ b/gui/default/index.html @@ -355,7 +355,7 @@  Failed Items - {{failed[folder.id].length | alwaysNumber}} items + {{model[folder.id].pullErrors | alwaysNumber}} items diff --git a/gui/default/syncthing/core/syncthingController.js b/gui/default/syncthing/core/syncthingController.js index 47f7e54e2..b4c06e1d6 100755 --- a/gui/default/syncthing/core/syncthingController.js +++ b/gui/default/syncthing/core/syncthingController.js @@ -48,8 +48,6 @@ angular.module('syncthing.core') $scope.neededCurrentPage = 1; $scope.neededPageSize = 10; $scope.failed = {}; - $scope.failedCurrentPage = 1; - $scope.failedPageSize = 10; $scope.scanProgress = {}; $scope.themes = []; $scope.globalChangeEvents = {}; @@ -198,13 +196,6 @@ angular.module('syncthing.core') $scope.model[data.folder].state = data.to; $scope.model[data.folder].error = data.error; - // If a folder has started syncing, then any old list of - // errors is obsolete. We may get a new list of errors very - // shortly though. - if (data.to === 'syncing') { - $scope.failed[data.folder] = []; - } - // If a folder has started scanning, then any scan progress is // also obsolete. if (data.to === 'scanning') { @@ -344,8 +335,7 @@ angular.module('syncthing.core') }); $scope.$on(Events.FOLDER_ERRORS, function (event, arg) { - var data = arg.data; - $scope.failed[data.folder] = data.errors; + $scope.model[arg.data.folder].pullErrors = arg.data.errors.length; }); $scope.$on(Events.FOLDER_SCAN_PROGRESS, function (event, arg) { @@ -657,12 +647,12 @@ angular.module('syncthing.core') refreshNeed($scope.neededFolder); }; - $scope.failedPageChanged = function (page) { - $scope.failedCurrentPage = page; - }; - - $scope.failedChangePageSize = function (perpage) { - $scope.failedPageSize = perpage; + $scope.refreshFailed = function (page, perpage) { + var url = urlbase + '/folder/pullerrors?folder=' + encodeURIComponent($scope.failed.folder); + url += "&page=" + page + "&perpage=" + perpage; + $http.get(url).success(function (data) { + $scope.failed = data; + }).error($scope.emitHTTPError); }; $scope.refreshRemoteNeed = function (folder, page, perpage) { @@ -1018,14 +1008,6 @@ angular.module('syncthing.core') return '?'; }; - $scope.deviceCompletion = function (deviceCfg) { - var conn = $scope.connections[deviceCfg.deviceID]; - if (conn) { - return conn.completion + '%'; - } - return ''; - }; - $scope.friendlyNameFromShort = function (shortID) { var matches = $scope.devices.filter(function (n) { return n.deviceID.substr(0, 7) === shortID; @@ -2067,24 +2049,18 @@ angular.module('syncthing.core') }; $scope.showFailed = function (folder) { - $scope.failedCurrent = $scope.failed[folder]; - $scope.failedFolderPath = $scope.folders[folder].path; - if ($scope.failedFolderPath[$scope.failedFolderPath.length - 1] !== $scope.system.pathSeparator) { - $scope.failedFolderPath += $scope.system.pathSeparator; - } + $scope.failed.folder = folder; + $scope.failed = $scope.refreshFailed(1, 10); $('#failed').modal().on('hidden.bs.modal', function () { - $scope.failedCurrent = undefined; + $scope.failed = {}; }); }; $scope.hasFailedFiles = function (folder) { - if (!$scope.failed[folder]) { + if (!$scope.model[folder]) { return false; } - if ($scope.failed[folder].length === 0) { - return false; - } - return true; + return $scope.model[folder].pullErrors !== 0; }; $scope.override = function (folder) { diff --git a/gui/default/syncthing/transfer/failedFilesModalView.html b/gui/default/syncthing/transfer/failedFilesModalView.html index 2d465c6ca..c1355cd7e 100644 --- a/gui/default/syncthing/transfer/failedFilesModalView.html +++ b/gui/default/syncthing/transfer/failedFilesModalView.html @@ -5,15 +5,15 @@ They are retried automatically and will be synced when the error is resolved.

- - + +
{{failedFolderPath}}{{e.path}}
{{e.path}} {{e.error | lastErrorComponent}}
- +
diff --git a/lib/model/model.go b/lib/model/model.go index 6b3026b4e..976c8cb5f 100644 --- a/lib/model/model.go +++ b/lib/model/model.go @@ -64,6 +64,7 @@ type service interface { Serve() Stop() CheckHealth() error + PullErrors() []FileError getState() (folderState, time.Time, error) setState(state folderState) @@ -119,7 +120,7 @@ var ( errDeviceUnknown = errors.New("unknown device") errDevicePaused = errors.New("device is paused") errDeviceIgnored = errors.New("device is ignored") - errFolderPaused = errors.New("folder is paused") + ErrFolderPaused = errors.New("folder is paused") errFolderNotRunning = errors.New("folder is not running") errFolderMissing = errors.New("no such folder") errNetworkNotAllowed = errors.New("network not allowed") @@ -2226,6 +2227,15 @@ func (m *Model) State(folder string) (string, time.Time, error) { return state.String(), changed, err } +func (m *Model) PullErrors(folder string) ([]FileError, error) { + m.fmut.RLock() + defer m.fmut.RUnlock() + if err := m.checkFolderRunningLocked(folder); err != nil { + return nil, err + } + return m.folderRunners[folder].PullErrors(), nil +} + func (m *Model) Override(folder string) { m.fmut.RLock() fs, ok := m.folderFiles[folder] @@ -2657,7 +2667,7 @@ func (m *Model) checkFolderRunningLocked(folder string) error { if cfg, ok := m.cfg.Folder(folder); !ok { return errFolderMissing } else if cfg.Paused { - return errFolderPaused + return ErrFolderPaused } return errFolderNotRunning diff --git a/lib/model/model_test.go b/lib/model/model_test.go index c1225146d..ed4d93c0f 100644 --- a/lib/model/model_test.go +++ b/lib/model/model_test.go @@ -3229,7 +3229,7 @@ func TestPausedFolders(t *testing.T) { } w.Wait() - if err := m.ScanFolder("default"); err != errFolderPaused { + if err := m.ScanFolder("default"); err != ErrFolderPaused { t.Errorf("Expected folder paused error, received: %v", err) } diff --git a/lib/model/rofolder.go b/lib/model/rofolder.go index b7a567102..ef75f515b 100644 --- a/lib/model/rofolder.go +++ b/lib/model/rofolder.go @@ -66,3 +66,7 @@ func (f *sendOnlyFolder) Serve() { func (f *sendOnlyFolder) String() string { return fmt.Sprintf("sendOnlyFolder/%s@%p", f.folderID, f) } + +func (f *sendOnlyFolder) PullErrors() []FileError { + return nil +} diff --git a/lib/model/rwfolder.go b/lib/model/rwfolder.go index cac9f13f1..eb1b42bb2 100644 --- a/lib/model/rwfolder.go +++ b/lib/model/rwfolder.go @@ -286,7 +286,7 @@ func (f *sendReceiveFolder) pull(prevIgnoreHash string) (curIgnoreHash string, s // we're not making it. Probably there are write // errors preventing us. Flag this with a warning and // wait a bit longer before retrying. - if folderErrors := f.currentErrors(); len(folderErrors) > 0 { + if folderErrors := f.PullErrors(); len(folderErrors) > 0 { events.Default.Log(events.FolderErrors, map[string]interface{}{ "folder": f.folderID, "errors": folderErrors, @@ -1797,11 +1797,11 @@ func (f *sendReceiveFolder) clearErrors() { f.errorsMut.Unlock() } -func (f *sendReceiveFolder) currentErrors() []fileError { +func (f *sendReceiveFolder) PullErrors() []FileError { f.errorsMut.Lock() - errors := make([]fileError, 0, len(f.errors)) + errors := make([]FileError, 0, len(f.errors)) for path, err := range f.errors { - errors = append(errors, fileError{path, err}) + errors = append(errors, FileError{path, err}) } sort.Sort(fileErrorList(errors)) f.errorsMut.Unlock() @@ -1880,13 +1880,13 @@ func (f *sendReceiveFolder) deleteDir(dir string, ignores *ignore.Matcher, scanC return err } -// A []fileError is sent as part of an event and will be JSON serialized. -type fileError struct { +// A []FileError is sent as part of an event and will be JSON serialized. +type FileError struct { Path string `json:"path"` Err string `json:"error"` } -type fileErrorList []fileError +type fileErrorList []FileError func (l fileErrorList) Len() int { return len(l)