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)