Archived
Showing detailed sync progress (fixes #476)
based on commit by Audrius Butkevicius <audrius.butkevicius@gmail.com>
This commit is contained in:
1 parent
b48d9a3a82
commit
90a31589bb
14 files changed
+570
-188
No files matched your search
@@ -23,6 +23,7 @@ Marcin Dziadus <dziadus.marcin@gmail.com>
|
||||
Michael Tilli <pyfisch@gmail.com>
|
||||
Philippe Schommers <philippe@schommers.be>
|
||||
Phill Luby <phill.luby@newredo.com>
|
||||
Piotr Bejda <piotrb10@gmail.com>
|
||||
Ryan Sullivan <kayoticsully@gmail.com>
|
||||
Tully Robinson <tully@tojr.org>
|
||||
Veeti Paananen <veeti.paananen@rojekti.fi>
|
||||
|
||||
+67
-6
@@ -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;
|
||||
});
|
||||
};
|
||||
|
||||
|
||||
+23
-2
@@ -765,9 +765,29 @@
|
||||
<tr ng-repeat="f in needed" ng-init="a = needAction(f)">
|
||||
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
|
||||
<td title="{{f.Name}}">{{f.Name | basename}}</td>
|
||||
<td class="text-right small-data"><span ng-if="f.Size > 0">{{f.Size | binary}}B</span></td>
|
||||
<td>
|
||||
<span ng-if="a == 'sync' && progress[neededFolder] && progress[neededFolder][f.Name]">
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" style="width: {{progress[neededFolder][f.Name].Reused}}%"></div>
|
||||
<div class="progress-bar" style="width: {{progress[neededFolder][f.Name].CopiedFromOrigin}}%"></div>
|
||||
<div class="progress-bar progress-bar-info" style="width: {{progress[neededFolder][f.Name].CopiedFromElsewhere}}%"></div>
|
||||
<div class="progress-bar progress-bar-warning" style="width: {{progress[neededFolder][f.Name].Pulled}}%"></div>
|
||||
<div class="progress-bar progress-bar-danger" style="width: {{progress[neededFolder][f.Name].Pulling}}%"></div>
|
||||
<span class="show frontal">{{progress[neededFolder][f.Name].BytesDone | binary}}B / {{progress[neededFolder][f.Name].BytesTotal | binary}}B</span>
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<span translate>Legend:</span>
|
||||
<div class="progress progress-striped active">
|
||||
<div class="progress-bar progress-bar-success" style="width: 20%"><span translate class="show">Reused</span></div>
|
||||
<div class="progress-bar" style="width: 20%"><span translate class="show">Copied from original</span></div>
|
||||
<div class="progress-bar progress-bar-info" style="width: 20%"><span translate class="show">Copied from elsewhere</span></div>
|
||||
<div class="progress-bar progress-bar-warning" style="width: 20%"><span translate class="show">Downloaded</span></div>
|
||||
<div class="progress-bar progress-bar-danger" style="width: 20%"><span translate class="show">Downloading</span></div>
|
||||
</div>
|
||||
|
||||
</modal>
|
||||
|
||||
<!-- About modal -->
|
||||
@@ -793,11 +813,11 @@
|
||||
<li>Emil Hessman</li>
|
||||
<li>Felix Ableitner</li>
|
||||
<li>Felix Unterpaintner</li>
|
||||
<li>Gilli Sigurdsson</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<ul>
|
||||
<li>Gilli Sigurdsson</li>
|
||||
<li>James Patterson</li>
|
||||
<li>Jens Diemer</li>
|
||||
<li>Jochen Voss</li>
|
||||
@@ -806,6 +826,7 @@
|
||||
<li>Michael Tilli</li>
|
||||
<li>Philippe Schommers</li>
|
||||
<li>Phill Luby</li>
|
||||
<li>Piotr Bejda</li>
|
||||
<li>Ryan Sullivan</li>
|
||||
<li>Tully Robinson</li>
|
||||
<li>Veeti Paananen</li>
|
||||
|
||||
@@ -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%;
|
||||
}
|
||||
@@ -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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 truncated
|
||||
bs, _ = base64.StdEncoding.DecodeString("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 truncated
|
||||
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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 truncated
|
||||
bs, _ = base64.StdEncoding.DecodeString("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 truncated
|
||||
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
|
||||
|
||||
+19
-18
@@ -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:"-"`
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -19,5 +19,6 @@
|
||||
<autoUpgradeIntervalH>24</autoUpgradeIntervalH>
|
||||
<keepTemporariesH>48</keepTemporariesH>
|
||||
<cacheIgnoredFiles>false</cacheIgnoredFiles>
|
||||
<progressUpdateIntervalS>10</progressUpdateIntervalS>
|
||||
</options>
|
||||
</configuration>
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
+17
-12
@@ -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)
|
||||
}
|
||||
|
||||
Executable
+133
@@ -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
|
||||
}
|
||||
@@ -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 <http://www.gnu.org/licenses/>.
|
||||
|
||||
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)
|
||||
|
||||
}
|
||||
+120
-107
@@ -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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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),
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user