diff --git a/AUTHORS b/AUTHORS index 1d3b94ff5..a1a1a14db 100644 --- a/AUTHORS +++ b/AUTHORS @@ -23,6 +23,7 @@ Marcin Dziadus Michael Tilli Philippe Schommers Phill Luby +Piotr Bejda Ryan Sullivan Tully Robinson Veeti Paananen diff --git a/gui/app.js b/gui/app.js index b953fa88d..ccf29b10b 100644 --- a/gui/app.js +++ b/gui/app.js @@ -116,6 +116,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca $scope.seenError = ''; $scope.upgradeInfo = null; $scope.stats = {}; + $scope.progress = {}; $http.get(urlbase + "/lang").success(function (langs) { // Find the first language in the list provided by the user's browser @@ -270,6 +271,55 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca }); }); + $scope.$on('DownloadProgress', function (event, arg) { + var stats = arg.data; + var progress = {}; + for(var folder in stats){ + refreshFolder(folder); + progress[folder] = {}; + for(var file in stats[folder]){ + var s = stats[folder][file]; + var reused = Math.floor(100 * s.Reused / s.Total); + var copiedFromOrigin = Math.floor(100 * s.CopiedFromOrigin / s.Total); + var copiedFromElsewhere = Math.floor(100 * s.CopiedFromElsewhere / s.Total); + var pulled = Math.floor(100 * s.Pulled / s.Total); + var pulling = Math.floor(100 * s.Pulling / s.Total); + // We can do the following, because if s.Pulling > 0, than reused + copied + pulled < 100 because off rounding them down. + // We do this to show which files are currently being pulled + if (s.Pulling && pulling == 0) { + pulling = 1; + } + progress[folder][file] = { + Reused: reused, + CopiedFromOrigin: copiedFromOrigin, + CopiedFromElsewhere: copiedFromElsewhere, + Pulled: pulled, + Pulling: pulling, + BytesTotal: s.BytesTotal, + BytesDone: s.BytesDone, + }; + } + } + for(var folder in $scope.progress){ + var refresh = false; + if (!(folder in progress)) { + refresh = true; + refreshFolder(folder); + } else { + for(file in $scope.progress[folder]){ + if (!(file in progress[folder])) { + refresh = true; + } + } + } + if (refresh) { + refreshNeed(folder); + } + } + $scope.progress = progress; + console.log("DownloadProgress", $scope.progress); + }); + var debouncedFuncs = {}; function refreshFolder(folder) { @@ -394,6 +444,17 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca }); } + function refreshNeed (folder) { + if ($scope.neededFolder == folder) { + $http.get(urlbase + "/need?folder=" + encodeURIComponent(folder)).success(function (data) { + if ($scope.neededFolder == folder) { + console.log("refreshNeed", folder, data); + $scope.needed = data; + } + }); + } + } + var refreshDeviceStats = debounce(function () { $http.get(urlbase + "/stats/device").success(function (data) { $scope.stats = data; @@ -475,7 +536,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca } var pct = 100 * $scope.model[folder].inSyncBytes / $scope.model[folder].globalBytes; - return Math.floor(pct); + return Math.min(Math.floor(pct), 100); }; $scope.deviceIcon = function (deviceCfg) { @@ -974,11 +1035,11 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca }; $scope.showNeed = function (folder) { - $scope.neededLoaded = false; - $('#needed').modal(); - $http.get(urlbase + "/need?folder=" + encodeURIComponent(folder)).success(function (data) { - $scope.needed = data; - $scope.neededLoaded = true; + $scope.neededFolder = folder; + refreshNeed(folder); + $('#needed').modal().result.finally(function(){ + $scope.neededFolder = undefined; + $scope.needed = undefined; }); }; diff --git a/gui/index.html b/gui/index.html index 112f48fc3..a9dc47897 100644 --- a/gui/index.html +++ b/gui/index.html @@ -765,9 +765,29 @@ {{needActions[a]}} {{f.Name | basename}} - {{f.Size | binary}}B + + +
+
+
+
+
+
+ {{progress[neededFolder][f.Name].BytesDone | binary}}B / {{progress[neededFolder][f.Name].BytesTotal | binary}}B +
+
+ + Legend: +
+
Reused
+
Copied from original
+
Copied from elsewhere
+
Downloaded
+
Downloading
+
+ @@ -793,11 +813,11 @@
  • Emil Hessman
  • Felix Ableitner
  • Felix Unterpaintner
  • +
  • Gilli Sigurdsson
    • -
    • Gilli Sigurdsson
    • James Patterson
    • Jens Diemer
    • Jochen Voss
    • @@ -806,6 +826,7 @@
    • Michael Tilli
    • Philippe Schommers
    • Phill Luby
    • +
    • Piotr Bejda
    • Ryan Sullivan
    • Tully Robinson
    • Veeti Paananen
    • diff --git a/gui/overrides.css b/gui/overrides.css index e641078c4..b02f3ca91 100644 --- a/gui/overrides.css +++ b/gui/overrides.css @@ -149,3 +149,19 @@ table.table-condensed td { .dl-horizontal.dl-narrow dd { margin-left: 60px; } + +/** + * Progress bars with centered text + */ + +.progress { + margin-bottom: 0px; + position: relative; +} + +.progress span.frontal { + text-align: center; + position: absolute; + display: block; + width: 100%; +} diff --git a/internal/auto/gui.files.go b/internal/auto/gui.files.go index cbabc7881..b49485f53 100644 --- a/internal/auto/gui.files.go +++ b/internal/auto/gui.files.go @@ -27,7 +27,7 @@ func Assets() map[string][]byte { bs, _ = ioutil.ReadAll(gr) assets["angular/angular.min.js"] = bs - bs, _ = base64.StdEncoding.DecodeString("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") + bs, _ = base64.StdEncoding.DecodeString("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") gr, _ = gzip.NewReader(bytes.NewBuffer(bs)) bs, _ = ioutil.ReadAll(gr) assets["app.js"] = bs @@ -92,7 +92,7 @@ func Assets() map[string][]byte { bs, _ = ioutil.ReadAll(gr) assets["img/logo-text-64.png"] = bs - bs, _ = base64.StdEncoding.DecodeString("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") + bs, _ = base64.StdEncoding.DecodeString("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") gr, _ = gzip.NewReader(bytes.NewBuffer(bs)) bs, _ = ioutil.ReadAll(gr) assets["index.html"] = bs @@ -207,7 +207,7 @@ func Assets() map[string][]byte { bs, _ = ioutil.ReadAll(gr) assets["modal.html"] = bs - bs, _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/5xW3W7jNhO991PMl+8mSS3b+dukdruoG2x2A+xmA8fBIih6QUmURYQiBZKyoy723Xuov9hOnBY14MSiOIdnZs7McHjYGw7pUuelEYvU0f7lAR2Pjk5pnnK6K1XkUqEWNC1cqo0dYLPfP0+FpdzohWEZ4WdiOCerE7diho+p1AVFTJHhsbDOiLBwnIQjpuKhNpTpWCQlFjxUoWJuyOE0x01mSSfVw8ebe/rIFTdM0m0RShHRZxFxZTkxHO1XbMpjCstq+xUIeLS7hgNdaQAzJ7TqExfYYmjJjcUznbRnNIB9Aqd95jxtQzr3RgcejKmSJHPPprvcf/YyJqEq7FTn8CgFKvxeCSkp5FRYnhSyT9hJ367nn77ezz3c9OaBvk1ns+nN/GGCzQg1NvAlr6FElksBZPhlmHKlp//lw+zyE/ZPf7/+fD1/gAce6Op6fvPh7o6uvs5oSrfT2fz68v7zdEa397Pbr3cfBnTH+T+FN6mxMo0oxtwxIbu0PyCvFuRkTClbcuQ34mIJaowiKOjf5E5qtfBQ3ktsfo7jgK4TUtr1yYLjL6lz+Xg4XK1Wg4UqBtoshrLGsMP3g97hsNcLdVzS9x7hk7M4hkyDUDunszGdj/KnSfUm0coFCcuELMe094nLJXciYnTDC77Xp26hT1MjGHJjmbKB5UYkk96PXi896lN6jO8Jvqf4njVHbgLPmOQrVgLxP5xRyJ862IyZhVCB0/mYjgZnPKt2iJgrIEC8fhfklkuGY4WSQvEglDp69O7m2gqv3jEyA+EiNX51JWKXAs1jEaXc13n3WAFsrWnIPZF6NaalsCKUvOIw2CTR4Z50sW5h6pVNE0jFNXY6Z5FwoD8aXJw1WUKBjOn/FxcXtV3OFJegxXxW6cXJXQA6z9d9Z6HVErVYr1eRDE5bjpInYBj83DLcPOn7FtR6GNfjkooYlCqEPyLJrD38dS8QKtF7f9KW0/21HbaIIm7t25tyIyCC8uWmVnh1rJKkFs/A8ScXZFppi7jy19T5hSup+/RFKxbh/6VWCBCz0OolOp5Af7vhKwi1A2mAGVIf4PTYl1383vkovXcGP/r08q0vx91vE63d7rcdcvwm8qtvO+S48T3UBiOlLiGlVeON1Au9UWPQ32S9eXTPtWLOWsW8poYKEE0/UDoICyl5m5xq0boSFF2Z85pAsx2kZZG90PB2EYMfGkBTXOd1RXZ9wWwUKnxl0ePC+Fnn0bUByUW4f3zyrk/HpyP/5+hgsh4VA6EXFjXaudd6f5Q/UVcmbYiOsXhc18qaBxCJGmC6ss2mVdfWqO5aW44cjdrFSq9MioWPqHdn8lY8fOjQ2COp2eN25Hxs6X8Yj9o4DMY11WKytE0qFY4Hlaq9wcqwfG0wrJqOdToabVjHW3Ol9uwYfW3XefHAZkzKYC0qrx4Nk8piW8eNTb0K73ADGKPUn3jsbV436Wi+bExdoJ9fcSlFboWdvEXutwxXNkb7GXtqMnf+7jx/Olint5uH/wwP/Q0CvSQUiEueIkcWJYXJDxbVuAkNZ4/WX5T8ZQBVhRsAbCTkObQpLm9xdTvoEA+plp0dEMZ+u7rlgEH0a89+1Jphy5CZul4admBW8YDy6utH1ftbzDbVbYWdbU6I6rKxI2mN/NvLRyOlGGNFG/EXdMakf1K4vyEEsducn6fdtNxpEb9WZ+8au78BAAD//wEAAP//Y3cv+sILAAA=") + bs, _ = base64.StdEncoding.DecodeString("H4sIAAAJbogA/5xWf08jORL9P5+ijtNJwKWTEGDgktvRZtEwgzTDoBA0Qqv9w93tTlu47ZbtTsiO5rvvc/8iCQmz2kiBtNv1/KrqVZX7x51+n650vjJinjo6vDqi4eDkjGYpp/uVilwq1JwmhUu1sT1s9vtnqbCUGz03LCP8TAznZHXilszwEa10QRFTZHgsrDMiLBwn4YipuK8NZToWyQoLHqpQMTfkcJrjJrOkk/Lh4+0DfeSKGybprgiliOiziLiynBiO9is25TGFq3L7NQh4tPuaA11rADMntOoSF9hiaMGNxTOdNmfUgF0Cp0PmPG1DOvdGRx6MqRVJ5l5M97n/4mVMQpXYqc7hUQpU+L0UUlLIqbA8KWSXsJO+3cw+fX2YebjJ7SN9m0ynk9vZ4xibEWps4AteQYkslwLI8Msw5Vae/pcP06tP2D/57ebzzewRHnig65vZ7Yf7e7r+OqUJ3U2ms5urh8+TKd09TO++3n/o0T3nPwtvUmFlGlGMuWNCtml/RF4tyMmYUrbgyG/ExQLUGEVQ0N/JndRq7qG8l9j8Esce3SSktOuSBcf/p87lo35/uVz25qroaTPvywrD9t/3Osf9TifU8Yq+dwifnMUxZBqE2jmdjehikD+PyzeJVi5IWCbkakQHn7hccCciRre84Addahe6NDGCITeWKRtYbkQy7vzodNKTLqVDfE/xPcP3vD5yE3jKJF+yFRD/wRmF/G8LmzEzFypwOh/RSe+cZ+UOEXMFBIjX74LccslwrFBSKB6EUkdP3t1cW+HVO0JmIFykxq8uRexSoHksopT7Om8fS4CtNQ25J1IvR7QQVoSSlxx6myRa3NM21g1MtbJpAqm42k7nLBIO9Ae9y/M6SyiQEf378vKyssuZ4hK0mM8qvTq5DUDr+brvLLRaohar9TKSwVnDUfIEDIP/NQw3T/q+BbUexvW4pCIGpRLh90gya49/OQiESvTBH7TldHdthy2iiFv79qbcCIhg9XpTI7wqVklSiafn+LMLMq20RVz5LnV+4UrqLn3RikX4f6UVAsQstHqFjifQ3275EkJtQWpghtQHOD32ZRe/dz5K753Bjy69fuvLcf/bRGu3/22LHL+JvPNtixzXvofaYKRUJaS0qr2Req43agz6G683j/a5Usx5o5hdaigB0fQDpYOwkJI3ySkXrVuBolvlvCJQbwdpWWSvNLxdxOCHBlAX10VVkW1fMBuFCl9Z9DQ3ftZ5dG1Ach4eDk/fdWl4NvB/To7G61ExEHphUaOte433J/kztWXShGiIxWFVK2seQCSqh+nKNptWVVuDqmttOXIyaBZLvTIp5j6i3p3xW/HwoUNjj6RmT9uR87Glf2E8auMwGNdUi8nSNKlUOB6UqvYGS8PytcGwrDvW2WCwYR1vzZXKsyH62r7z4p7NmJTBWlR2Hg2T0mJbx7VNtQrvcAMYodSfeextdpu0NF83pjbQL6+4lCK3wo7fIvdrhisbo8OMPdeZu3h3kT8frdPbz8N/+sf+BoFeEgrEJU+RI4uSwuQHi3LchIazJ+svSv4ygKrCDQA2EvLs2xSXt7i8HbSIx1TJzvYIY79Z3XLAIPqVZz8qzbBFyExVLzU7MCt5QHnV9aPs/Q1mk+qmws43J0R52diTtFr+zeWjllKMsaKN+BM6Y9I/KdzfEILYbc7Ps3Za7rWId9XZu8auf3zcQZDu/D0K0wU9wdjKQdyXcHv1GYIUOt5TuNNs2839Z12vNS97QGJKro061iq7Ovrt0bxrjLftYvAff+BfAAAA//8BAAD//zw+5S6fDAAA") gr, _ = gzip.NewReader(bytes.NewBuffer(bs)) bs, _ = ioutil.ReadAll(gr) assets["overrides.css"] = bs diff --git a/internal/config/config.go b/internal/config/config.go index 6bc66b6a3..5fce4b14b 100644 --- a/internal/config/config.go +++ b/internal/config/config.go @@ -159,24 +159,25 @@ type FolderDeviceConfiguration struct { } type OptionsConfiguration struct { - ListenAddress []string `xml:"listenAddress" default:"0.0.0.0:22000"` - GlobalAnnServer string `xml:"globalAnnounceServer" default:"announce.syncthing.net:22026"` - GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" default:"true"` - LocalAnnEnabled bool `xml:"localAnnounceEnabled" default:"true"` - LocalAnnPort int `xml:"localAnnouncePort" default:"21025"` - LocalAnnMCAddr string `xml:"localAnnounceMCAddr" default:"[ff32::5222]:21026"` - MaxSendKbps int `xml:"maxSendKbps"` - MaxRecvKbps int `xml:"maxRecvKbps"` - ReconnectIntervalS int `xml:"reconnectionIntervalS" default:"60"` - StartBrowser bool `xml:"startBrowser" default:"true"` - UPnPEnabled bool `xml:"upnpEnabled" default:"true"` - UPnPLease int `xml:"upnpLeaseMinutes" default:"0"` - UPnPRenewal int `xml:"upnpRenewalMinutes" default:"30"` - URAccepted int `xml:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently) - RestartOnWakeup bool `xml:"restartOnWakeup" default:"true"` - AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" default:"12"` // 0 for off - KeepTemporariesH int `xml:"keepTemporariesH" default:"24"` // 0 for off - CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" default:"true"` + ListenAddress []string `xml:"listenAddress" default:"0.0.0.0:22000"` + GlobalAnnServer string `xml:"globalAnnounceServer" default:"announce.syncthing.net:22026"` + GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" default:"true"` + LocalAnnEnabled bool `xml:"localAnnounceEnabled" default:"true"` + LocalAnnPort int `xml:"localAnnouncePort" default:"21025"` + LocalAnnMCAddr string `xml:"localAnnounceMCAddr" default:"[ff32::5222]:21026"` + MaxSendKbps int `xml:"maxSendKbps"` + MaxRecvKbps int `xml:"maxRecvKbps"` + ReconnectIntervalS int `xml:"reconnectionIntervalS" default:"60"` + StartBrowser bool `xml:"startBrowser" default:"true"` + UPnPEnabled bool `xml:"upnpEnabled" default:"true"` + UPnPLease int `xml:"upnpLeaseMinutes" default:"0"` + UPnPRenewal int `xml:"upnpRenewalMinutes" default:"30"` + URAccepted int `xml:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently) + RestartOnWakeup bool `xml:"restartOnWakeup" default:"true"` + AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" default:"12"` // 0 for off + KeepTemporariesH int `xml:"keepTemporariesH" default:"24"` // 0 for off + CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" default:"true"` + ProgressUpdateIntervalS int `xml:"progressUpdateIntervalS" default:"5"` Deprecated_RescanIntervalS int `xml:"rescanIntervalS,omitempty" json:"-"` Deprecated_UREnabled bool `xml:"urEnabled,omitempty" json:"-"` diff --git a/internal/config/config_test.go b/internal/config/config_test.go index ac779aafa..426aacfe2 100644 --- a/internal/config/config_test.go +++ b/internal/config/config_test.go @@ -35,23 +35,24 @@ func init() { func TestDefaultValues(t *testing.T) { expected := OptionsConfiguration{ - ListenAddress: []string{"0.0.0.0:22000"}, - GlobalAnnServer: "announce.syncthing.net:22026", - GlobalAnnEnabled: true, - LocalAnnEnabled: true, - LocalAnnPort: 21025, - LocalAnnMCAddr: "[ff32::5222]:21026", - MaxSendKbps: 0, - MaxRecvKbps: 0, - ReconnectIntervalS: 60, - StartBrowser: true, - UPnPEnabled: true, - UPnPLease: 0, - UPnPRenewal: 30, - RestartOnWakeup: true, - AutoUpgradeIntervalH: 12, - KeepTemporariesH: 24, - CacheIgnoredFiles: true, + ListenAddress: []string{"0.0.0.0:22000"}, + GlobalAnnServer: "announce.syncthing.net:22026", + GlobalAnnEnabled: true, + LocalAnnEnabled: true, + LocalAnnPort: 21025, + LocalAnnMCAddr: "[ff32::5222]:21026", + MaxSendKbps: 0, + MaxRecvKbps: 0, + ReconnectIntervalS: 60, + StartBrowser: true, + UPnPEnabled: true, + UPnPLease: 0, + UPnPRenewal: 30, + RestartOnWakeup: true, + AutoUpgradeIntervalH: 12, + KeepTemporariesH: 24, + CacheIgnoredFiles: true, + ProgressUpdateIntervalS: 5, } cfg := New(device1) @@ -136,23 +137,24 @@ func TestNoListenAddress(t *testing.T) { func TestOverriddenValues(t *testing.T) { expected := OptionsConfiguration{ - ListenAddress: []string{":23000"}, - GlobalAnnServer: "syncthing.nym.se:22026", - GlobalAnnEnabled: false, - LocalAnnEnabled: false, - LocalAnnPort: 42123, - LocalAnnMCAddr: "quux:3232", - MaxSendKbps: 1234, - MaxRecvKbps: 2341, - ReconnectIntervalS: 6000, - StartBrowser: false, - UPnPEnabled: false, - UPnPLease: 60, - UPnPRenewal: 15, - RestartOnWakeup: false, - AutoUpgradeIntervalH: 24, - KeepTemporariesH: 48, - CacheIgnoredFiles: false, + ListenAddress: []string{":23000"}, + GlobalAnnServer: "syncthing.nym.se:22026", + GlobalAnnEnabled: false, + LocalAnnEnabled: false, + LocalAnnPort: 42123, + LocalAnnMCAddr: "quux:3232", + MaxSendKbps: 1234, + MaxRecvKbps: 2341, + ReconnectIntervalS: 6000, + StartBrowser: false, + UPnPEnabled: false, + UPnPLease: 60, + UPnPRenewal: 15, + RestartOnWakeup: false, + AutoUpgradeIntervalH: 24, + KeepTemporariesH: 48, + CacheIgnoredFiles: false, + ProgressUpdateIntervalS: 10, } cfg, err := Load("testdata/overridenvalues.xml", device1) diff --git a/internal/config/testdata/overridenvalues.xml b/internal/config/testdata/overridenvalues.xml index bf8cb2fa8..eca1d74c8 100755 --- a/internal/config/testdata/overridenvalues.xml +++ b/internal/config/testdata/overridenvalues.xml @@ -19,5 +19,6 @@ 24 48 false + 10 diff --git a/internal/events/events.go b/internal/events/events.go index 0d26074a1..8dd845d38 100644 --- a/internal/events/events.go +++ b/internal/events/events.go @@ -38,6 +38,7 @@ const ( StateChanged FolderRejected ConfigSaved + DownloadProgress AllEvents = (1 << iota) - 1 ) @@ -70,6 +71,8 @@ func (t EventType) String() string { return "FolderRejected" case ConfigSaved: return "ConfigSaved" + case DownloadProgress: + return "DownloadProgress" default: return "Unknown" } diff --git a/internal/model/model.go b/internal/model/model.go index 8df196dec..46abf97d8 100644 --- a/internal/model/model.go +++ b/internal/model/model.go @@ -82,9 +82,10 @@ type service interface { } type Model struct { - cfg *config.ConfigWrapper - db *leveldb.DB - finder *files.BlockFinder + cfg *config.ConfigWrapper + db *leveldb.DB + finder *files.BlockFinder + progressEmitter *ProgressEmitter deviceName string clientName string @@ -142,7 +143,9 @@ func NewModel(cfg *config.ConfigWrapper, deviceName, clientName, clientVersion s rawConn: make(map[protocol.DeviceID]io.Closer), deviceVer: make(map[protocol.DeviceID]string), finder: files.NewBlockFinder(db, cfg), + progressEmitter: NewProgressEmitter(cfg), } + go m.progressEmitter.Serve() var timeout = 20 * 60 // seconds if t := os.Getenv("STDEADLOCKTIMEOUT"); len(t) > 0 { @@ -172,15 +175,16 @@ func (m *Model) StartFolderRW(folder string) { panic("cannot start already running folder " + folder) } p := &Puller{ - folder: folder, - dir: cfg.Path, - scanIntv: time.Duration(cfg.RescanIntervalS) * time.Second, - model: m, - ignorePerms: cfg.IgnorePerms, - lenientMtimes: cfg.LenientMtimes, - copiers: cfg.Copiers, - pullers: cfg.Pullers, - finishers: cfg.Finishers, + folder: folder, + dir: cfg.Path, + scanIntv: time.Duration(cfg.RescanIntervalS) * time.Second, + model: m, + ignorePerms: cfg.IgnorePerms, + lenientMtimes: cfg.LenientMtimes, + progressEmitter: m.progressEmitter, + copiers: cfg.Copiers, + pullers: cfg.Pullers, + finishers: cfg.Finishers, } m.folderRunners[folder] = p m.fmut.Unlock() @@ -392,6 +396,7 @@ func (m *Model) NeedSize(folder string) (files int, bytes int64) { return true }) } + bytes -= m.progressEmitter.BytesCompleted(folder) if debug { l.Debugf("%v NeedSize(%q): %d %d", m, folder, files, bytes) } diff --git a/internal/model/progressemitter.go b/internal/model/progressemitter.go new file mode 100755 index 000000000..e7bc05323 --- /dev/null +++ b/internal/model/progressemitter.go @@ -0,0 +1,133 @@ +package model + +import ( + "path/filepath" + "reflect" + "sync" + "time" + + "github.com/syncthing/syncthing/internal/config" + "github.com/syncthing/syncthing/internal/events" +) + +type ProgressEmitter struct { + registry map[string]*sharedPullerState + interval time.Duration + last map[string]map[string]*pullerProgress + mut sync.Mutex + + timer *time.Timer + + stop chan struct{} +} + +// Creates a new progress emitter which emits DownloadProgress events every +// interval. +func NewProgressEmitter(cfg *config.ConfigWrapper) *ProgressEmitter { + t := &ProgressEmitter{ + stop: make(chan struct{}), + registry: make(map[string]*sharedPullerState), + last: make(map[string]map[string]*pullerProgress), + timer: time.NewTimer(time.Millisecond), + } + t.Changed(cfg.Raw()) + cfg.Subscribe(t) + return t +} + +// Starts progress emitter which starts emitting DownloadProgress events as +// the progress happens. +func (t *ProgressEmitter) Serve() { + for { + select { + case <-t.stop: + if debug { + l.Debugln("progress emitter: stopping") + } + return + case <-t.timer.C: + if debug { + l.Debugln("progress emitter: timer - looking after", len(t.registry)) + } + output := make(map[string]map[string]*pullerProgress) + t.mut.Lock() + for _, puller := range t.registry { + if output[puller.folder] == nil { + output[puller.folder] = make(map[string]*pullerProgress) + } + output[puller.folder][puller.file.Name] = puller.Progress() + } + if !reflect.DeepEqual(t.last, output) { + events.Default.Log(events.DownloadProgress, output) + t.last = output + if debug { + l.Debugf("progress emitter: emitting %#v", output) + } + } else if debug { + l.Debugln("progress emitter: nothing new") + } + if len(t.registry) != 0 { + t.timer.Reset(t.interval) + } + t.mut.Unlock() + } + } +} + +// Interface method to handle configuration changes +func (t *ProgressEmitter) Changed(cfg config.Configuration) error { + t.mut.Lock() + defer t.mut.Unlock() + + t.interval = time.Duration(cfg.Options.ProgressUpdateIntervalS) * time.Second + if debug { + l.Debugln("progress emitter: updated interval", t.interval) + } + return nil +} + +// Stops the emitter. +func (t *ProgressEmitter) Stop() { + t.stop <- struct{}{} +} + +// Register a puller with the emitter which will start broadcasting pullers +// progress. +func (t *ProgressEmitter) Register(s *sharedPullerState) { + t.mut.Lock() + defer t.mut.Unlock() + if debug { + l.Debugln("progress emitter: registering", s.folder, s.file.Name) + } + if len(t.registry) == 0 { + t.timer.Reset(t.interval) + } + t.registry[filepath.Join(s.folder, s.file.Name)] = s +} + +// Deregister a puller which will stop boardcasting pullers state. +func (t *ProgressEmitter) Deregister(s *sharedPullerState) { + t.mut.Lock() + defer t.mut.Unlock() + if debug { + l.Debugln("progress emitter: deregistering", s.folder, s.file.Name) + } + delete(t.registry, filepath.Join(s.folder, s.file.Name)) +} + +// Returns number of bytes completed in the given folder. +func (t *ProgressEmitter) BytesCompleted(folder string) (bytes int64) { + t.mut.Lock() + defer t.mut.Unlock() + + files, ok := t.last[folder] + if ok { + for _, s := range files { + bytes += s.BytesDone + } + } + if debug { + l.Debugf("progress emitter: bytes completed for %s: %d", folder, bytes) + } + return +} diff --git a/internal/model/progressemitter_test.go b/internal/model/progressemitter_test.go new file mode 100644 index 000000000..df56dad41 --- /dev/null +++ b/internal/model/progressemitter_test.go @@ -0,0 +1,95 @@ +// Copyright (C) 2014 The Syncthing Authors. +// +// This program is free software: you can redistribute it and/or modify it +// under the terms of the GNU General Public License as published by the Free +// Software Foundation, either version 3 of the License, or (at your option) +// any later version. +// +// This program is distributed in the hope that it will be useful, but WITHOUT +// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or +// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for +// more details. +// +// You should have received a copy of the GNU General Public License along +// with this program. If not, see . + +package model + +import ( + "testing" + "time" + + "github.com/syncthing/syncthing/internal/config" + "github.com/syncthing/syncthing/internal/events" +) + +var timeout = 10 * time.Millisecond + +func expectEvent(w *events.Subscription, t *testing.T, size int) { + event, err := w.Poll(timeout) + if err != nil { + t.Fatal("Unexpected error:", err) + } + if event.Type != events.DownloadProgress { + t.Fatal("Unexpected event:", event) + } + data := event.Data.(map[string]map[string]*pullerProgress) + if len(data) != size { + t.Fatal("Unexpected event data size:", data) + } +} + +func expectTimeout(w *events.Subscription, t *testing.T){ + _, err := w.Poll(timeout) + if err != events.ErrTimeout { + t.Fatal("Unexpected non-Timeout error:", err) + } +} + +func TestProgressEmitter(t *testing.T) { + l.Debugln("test progress emitter") + + w := events.Default.Subscribe(events.DownloadProgress) + + c := config.Wrap("/tmp/test", config.Configuration{}) + c.SetOptions(config.OptionsConfiguration{ + ProgressUpdateIntervalS: 0, + }) + + p := NewProgressEmitter(c) + go p.Serve() + + expectTimeout(w, t) + + s := sharedPullerState{} + p.Register(&s) + + expectEvent(w, t, 1) + expectTimeout(w, t) + + s.copyDone() + + expectEvent(w, t, 1) + expectTimeout(w, t) + + s.copiedFromOrigin() + + expectEvent(w, t, 1) + expectTimeout(w, t) + + s.pullStarted() + + expectEvent(w, t, 1) + expectTimeout(w, t) + + s.pullDone() + + expectEvent(w, t, 1) + expectTimeout(w, t) + + p.Deregister(&s) + + expectEvent(w, t, 0) + expectTimeout(w, t) + +} diff --git a/internal/model/puller.go b/internal/model/puller.go index 807cef2aa..d390c4880 100644 --- a/internal/model/puller.go +++ b/internal/model/puller.go @@ -65,17 +65,18 @@ var ( ) type Puller struct { - folder string - dir string - scanIntv time.Duration - model *Model - stop chan struct{} - versioner versioner.Versioner - ignorePerms bool - lenientMtimes bool - copiers int - pullers int - finishers int + folder string + dir string + scanIntv time.Duration + model *Model + stop chan struct{} + versioner versioner.Versioner + ignorePerms bool + lenientMtimes bool + progressEmitter *ProgressEmitter + copiers int + pullers int + finishers int } // Serve will run scans and pulls. It will return when Stop()ed or on a @@ -527,9 +528,9 @@ func (p *Puller) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksSt folder: p.folder, tempName: tempName, realName: realName, - copyTotal: len(blocks), - copyNeeded: len(blocks), - reused: reused, + copyTotal: uint32(len(blocks)), + copyNeeded: uint32(len(blocks)), + reused: uint32(reused), } if debug { @@ -598,6 +599,10 @@ nextFile: continue nextFile } + if p.progressEmitter != nil { + p.progressEmitter.Register(state.sharedPullerState) + } + evictionChan := make(chan lfu.Eviction) fdCache := lfu.New() @@ -737,101 +742,109 @@ nextBlock: } } +func (p *Puller) performFinish(state *sharedPullerState) { + if closed, err := state.finalClose(); closed { + if debug { + l.Debugln(p, "closing", state.file.Name) + } + if err != nil { + l.Warnln("puller: final:", err) + return + } + + // Verify the file against expected hashes + fd, err := os.Open(state.tempName) + if err != nil { + l.Warnln("puller: final:", err) + return + } + err = scanner.Verify(fd, protocol.BlockSize, state.file.Blocks) + fd.Close() + if err != nil { + l.Infoln("puller:", state.file.Name, err, "(file changed during pull?)") + return + } + + // Set the correct permission bits on the new file + if !p.ignorePerms { + err = os.Chmod(state.tempName, os.FileMode(state.file.Flags&0777)) + if err != nil { + l.Warnln("puller: final:", err) + return + } + } + + // Set the correct timestamp on the new file + t := time.Unix(state.file.Modified, 0) + err = os.Chtimes(state.tempName, t, t) + if err != nil { + if p.lenientMtimes { + // We accept the failure with a warning here and allow the sync to + // continue. We'll sync the new mtime back to the other devices later. + // If they have the same problem & setting, we might never get in + // sync. + l.Infof("Puller (folder %q, file %q): final: %v (continuing anyway as requested)", p.folder, state.file.Name, err) + } else { + l.Warnln("puller: final:", err) + return + } + } + + // If we should use versioning, let the versioner archive the old + // file before we replace it. Archiving a non-existent file is not + // an error. + if p.versioner != nil { + err = p.versioner.Archive(state.realName) + if err != nil { + l.Warnln("puller: final:", err) + return + } + } + + // If the target path is a symlink or a directory, we cannot copy + // over it, hence remove it before proceeding. + stat, err := os.Lstat(state.realName) + isLink, _ := symlinks.IsSymlink(state.realName) + if isLink || (err == nil && stat.IsDir()) { + osutil.InWritableDir(os.Remove, state.realName) + } + // Replace the original content with the new one + err = osutil.Rename(state.tempName, state.realName) + if err != nil { + l.Warnln("puller: final:", err) + return + } + + // If it's a symlink, the target of the symlink is inside the file. + if state.file.IsSymlink() { + content, err := ioutil.ReadFile(state.realName) + if err != nil { + l.Warnln("puller: final: reading symlink:", err) + return + } + + // Remove the file, and replace it with a symlink. + err = osutil.InWritableDir(func(path string) error { + os.Remove(path) + return symlinks.Create(path, string(content), state.file.Flags) + }, state.realName) + if err != nil { + l.Warnln("puller: final: creating symlink:", err) + return + } + } + + // Record the updated file in the index + p.model.updateLocal(p.folder, state.file) + + } +} + func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) { for state := range in { - if closed, err := state.finalClose(); closed { - if debug { - l.Debugln(p, "closing", state.file.Name) - } - if err != nil { - l.Warnln("puller: final:", err) - continue - } - - // Verify the file against expected hashes - fd, err := os.Open(state.tempName) - if err != nil { - l.Warnln("puller: final:", err) - continue - } - err = scanner.Verify(fd, protocol.BlockSize, state.file.Blocks) - fd.Close() - if err != nil { - l.Infoln("puller:", state.file.Name, err, "(file changed during pull?)") - continue - } - - // Set the correct permission bits on the new file - if !p.ignorePerms { - err = os.Chmod(state.tempName, os.FileMode(state.file.Flags&0777)) - if err != nil { - l.Warnln("puller: final:", err) - continue - } - } - - // Set the correct timestamp on the new file - t := time.Unix(state.file.Modified, 0) - err = os.Chtimes(state.tempName, t, t) - if err != nil { - if p.lenientMtimes { - // We accept the failure with a warning here and allow the sync to - // continue. We'll sync the new mtime back to the other devices later. - // If they have the same problem & setting, we might never get in - // sync. - l.Infof("Puller (folder %q, file %q): final: %v (continuing anyway as requested)", p.folder, state.file.Name, err) - } else { - l.Warnln("puller: final:", err) - continue - } - } - - // If we should use versioning, let the versioner archive the old - // file before we replace it. Archiving a non-existent file is not - // an error. - if p.versioner != nil { - err = p.versioner.Archive(state.realName) - if err != nil { - l.Warnln("puller: final:", err) - continue - } - } - - // If the target path is a symlink or a directory, we cannot copy - // over it, hence remove it before proceeding. - stat, err := os.Lstat(state.realName) - isLink, _ := symlinks.IsSymlink(state.realName) - if isLink || (err == nil && stat.IsDir()) { - osutil.InWritableDir(os.Remove, state.realName) - } - // Replace the original content with the new one - err = osutil.Rename(state.tempName, state.realName) - if err != nil { - l.Warnln("puller: final:", err) - continue - } - - // If it's a symlink, the target of the symlink is inside the file. - if state.file.IsSymlink() { - content, err := ioutil.ReadFile(state.realName) - if err != nil { - l.Warnln("puller: final: reading symlink:", err) - continue - } - - // Remove the file, and replace it with a symlink. - err = osutil.InWritableDir(func(path string) error { - os.Remove(path) - return symlinks.Create(path, string(content), state.file.Flags) - }, state.realName) - if err != nil { - l.Warnln("puller: final: creating symlink:", err) - continue - } - } - - // Record the updated file in the index - p.model.updateLocal(p.folder, state.file) + p.performFinish(state) + if state.closed && p.progressEmitter != nil { + p.progressEmitter.Deregister(state) } } } diff --git a/internal/model/sharedpullerstate.go b/internal/model/sharedpullerstate.go index 3eab3773f..5d10d2d9f 100644 --- a/internal/model/sharedpullerstate.go +++ b/internal/model/sharedpullerstate.go @@ -31,20 +31,32 @@ type sharedPullerState struct { folder string tempName string realName string - reused int // Number of blocks reused from temporary file + reused uint32 // Number of blocks reused from temporary file // Mutable, must be locked for access err error // The first error we hit fd *os.File // The fd of the temp file - copyTotal int // Total number of copy actions for the whole job - pullTotal int // Total number of pull actions for the whole job - copyNeeded int // Number of copy actions still pending - pullNeeded int // Number of block pulls still pending - copyOrigin int // Number of blocks copied from the original file + copyTotal uint32 // Total number of copy actions for the whole job + pullTotal uint32 // Total number of pull actions for the whole job + copyOrigin uint32 // Number of blocks copied from the original file + copyNeeded uint32 // Number of copy actions still pending + pullNeeded uint32 // Number of block pulls still pending closed bool // Set when the file has been closed mut sync.Mutex // Protects the above } +// A momentary state representing the progress of the puller +type pullerProgress struct { + Total uint32 + Reused uint32 + CopiedFromOrigin uint32 + CopiedFromElsewhere uint32 + Pulled uint32 + Pulling uint32 + BytesDone int64 + BytesTotal int64 +} + // tempFile returns the fd for the temporary file, reusing an open fd // or creating the file as necessary. func (s *sharedPullerState) tempFile() (*os.File, error) { @@ -208,3 +220,21 @@ func (s *sharedPullerState) finalClose() (bool, error) { } return true, nil } + +// Returns the momentarily progress for the puller +func (s *sharedPullerState) Progress() *pullerProgress { + s.mut.Lock() + defer s.mut.Unlock() + total := s.reused + s.copyTotal + s.pullTotal + done := total - s.copyNeeded - s.pullNeeded + return &pullerProgress{ + Total: total, + Reused: s.reused, + CopiedFromOrigin: s.copyOrigin, + CopiedFromElsewhere: s.copyTotal - s.copyNeeded - s.copyOrigin, + Pulled: s.pullTotal - s.pullNeeded, + Pulling: s.pullNeeded, + BytesTotal: protocol.BlocksToSize(total), + BytesDone: protocol.BlocksToSize(done), + } +}