Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Delete ignored existing items #9139

Open
wants to merge 3 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 7 additions & 0 deletions gui/default/index.html
Original file line number Diff line number Diff line change
Expand Up @@ -510,6 +510,12 @@ <h4 class="panel-title">
<a href="" ng-click="showLocalChanged(folder.id, folder.type)">{{model[folder.id].receiveOnlyTotalItems | alwaysNumber | localeNumber}} <span translate>items</span>, ~{{model[folder.id].receiveOnlyChangedBytes | binary}}B</a>
</td>
</tr>
<tr ng-if="hasLocalIgnored(folder)">
<th><span class="fas fa-fw fa-exclamation-circle"></span>&nbsp;<span translate>Ignored items</span></th>
<td class="text-right">
<a href="" ng-click="showLocalIgnored(folder.id, folder.type)">{{model[folder.id].localIgnoredTotalItems | alwaysNumber | localeNumber}} <span translate>items</span>, ~{{model[folder.id].localIgnoredBytes | binary}}B</a>
</td>
</tr>
<tr>
<th><span class="fas fa-fw fa-folder"></span>&nbsp;<span translate>Folder Type</span></th>
<td class="text-right">
Expand Down Expand Up @@ -1040,6 +1046,7 @@ <h4 class="panel-title">
<ng-include src="'syncthing/transfer/failedFilesModalView.html'"></ng-include>
<ng-include src="'syncthing/transfer/remoteNeededFilesModalView.html'"></ng-include>
<ng-include src="'syncthing/transfer/localChangedFilesModalView.html'"></ng-include>
<ng-include src="'syncthing/transfer/localIgnoredFilesModalView.html'"></ng-include>
<ng-include src="'syncthing/core/upgradeModalView.html'"></ng-include>
<ng-include src="'syncthing/core/majorUpgradeModalView.html'"></ng-include>
<ng-include src="'syncthing/core/aboutModalView.html'"></ng-include>
Expand Down
48 changes: 47 additions & 1 deletion gui/default/syncthing/core/syncthingController.js
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,7 @@ angular.module('syncthing.core')
$scope.neededFolder = '';
$scope.failed = {};
$scope.localChanged = {};
$scope.localIgnored = {};
$scope.scanProgress = {};
$scope.themes = [];
$scope.globalChangeEvents = {};
Expand Down Expand Up @@ -906,6 +907,19 @@ angular.module('syncthing.core')
}).error($scope.emitHTTPError);
};

$scope.refreshLocalIgnored = function (page, perpage) {
console.log("refreshLocalIgnored")
if (!$scope.localIgnoredFolder) {
return;
}
var url = urlbase + '/db/localignored?folder=';
url += encodeURIComponent($scope.localIgnoredFolder);
url += "&page=" + page + "&perpage=" + perpage;
$http.get(url).success(function (data) {
$scope.localIgnored = data;
console.log("refreshLocalIgnored", data)
}).error($scope.emitHTTPError);
};
var refreshDeviceStats = debounce(function () {
$http.get(urlbase + "/stats/device").success(function (data) {
$scope.deviceStats = data;
Expand Down Expand Up @@ -2970,13 +2984,40 @@ angular.module('syncthing.core')
};

$scope.hasReceiveOnlyChanged = function (folderCfg) {
if (!folderCfg || ["receiveonly", "receiveencrypted"].indexOf(folderCfg.type) === -1) {
if (!folderCfg || ["receiveonly", "receiveencrypted"].indexOf(folderCfg.type) === -1) {
return false;
}
var counts = $scope.model[folderCfg.id];
return counts && counts.receiveOnlyTotalItems > 0;
};

$scope.hasDeleteIgnoredItems = function (folderCfg) {
if (!folderCfg || ["receiveonly"].indexOf(folderCfg.type) === -1) {
return false;
}
var counts = $scope.model[folderCfg.id];
return counts && counts.localIgnoredTotalItems > 0;
};

$scope.showLocalIgnored = function (folder, folderType) {
$scope.localIgnoredFolder = folder;
$scope.localIgnoredType = folderType;
$scope.localIgnored = $scope.refreshLocalIgnored(1, 10);
$('#localIgnored').modal().one('hidden.bs.modal', function () {
$scope.localIgnored = {};
$scope.localIgnoredFolder = undefined;
$scope.localIgnoredType = undefined;
});
};

$scope.hasLocalIgnored = function (folderCfg) {
if (!folderCfg || ["sendreceive", "sendonly", "receiveonly"].indexOf(folderCfg.type) === -1) {
return false;
}
var counts = $scope.model[folderCfg.id];
return counts && counts.localIgnoredFiles > 0;
};

$scope.revertOverride = function () {
$http.post(
urlbase + "/db/" + $scope.revertOverrideParams.operation +"?folder="
Expand Down Expand Up @@ -3004,6 +3045,11 @@ angular.module('syncthing.core')
params.icon = "fas fa-minus-circle"
params.operation = "revert";
break;
case "deleteIgn":
params.heading = $translate.instant("Delete Ignored Items");
params.icon = "fas fa-minus-circle"
params.operation = "deleteIgnored";
break;
}
$scope.revertOverrideParams = params;
showModal('#revert-override-confirmation');
Expand Down
34 changes: 34 additions & 0 deletions gui/default/syncthing/transfer/localIgnoredFilesModalView.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
<modal id="localIgnored" status="info" icon="fas fa-exclamation-circle" heading="{{localIgnoredHeading(localIgnoredType)}}" large="yes" closeable="yes">
<div class="modal-body" ng-switch="localIgnoredType">
<p translate>
The following items were ignored locally.
</p>
<table class="table table-striped">
<thead>
<tr>
<th translate>Path</th>
<th translate>Size</th>
</tr>
</thead>
<tr dir-paginate="file in localIgnored.files | itemsPerPage: localIgnored.perpage" current-page="localIgnored.page" total-items="model[localIgnoredFolder].localIgnoredTotalItems" pagination-id="localIgnored">
<td class="word-break-all">{{file.name}}</td>
<td><span ng-hide="file.type == 'DIRECTORY'">{{file.size | binary}}B</span></td>
</tr>
</table>
<dir-pagination-controls on-page-change="refreshLocalIgnored(newPageNumber, localIgnored.perpage)" pagination-id="localIgnored"></dir-pagination-controls>
<ul class="pagination pull-right">
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: localIgnored.perpage == option }">
<a href="#" ng-click="refreshLocalIgnored(localIgnored.page, option)">{{option}}</a>
</li>
</ul>
<div class="clearfix"></div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-sm btn-danger pull-left" data-dismiss="modal" ng-click="revertOverrideConfirmationModal('deleteIgn', localIgnoredFolder)" ng-if="model[localIgnoredFolder].localIgnoredTotalItems > 0">
<span class="fa fa-arrow-circle-down"></span>&nbsp;<span translate>Delete ignored files</span>
</button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fas fa-times"></span>&nbsp;<span translate>Close</span>
</button>
</div>
</modal>
28 changes: 28 additions & 0 deletions lib/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -253,6 +253,7 @@ func (s *service) Serve(ctx context.Context) error {
restMux.HandlerFunc(http.MethodGet, "/rest/db/need", s.getDBNeed) // folder [perpage] [page]
restMux.HandlerFunc(http.MethodGet, "/rest/db/remoteneed", s.getDBRemoteNeed) // device folder [perpage] [page]
restMux.HandlerFunc(http.MethodGet, "/rest/db/localchanged", s.getDBLocalChanged) // folder [perpage] [page]
restMux.HandlerFunc(http.MethodGet, "/rest/db/localignored", s.getDBLocalIgnored) // folder [perpage] [page]
restMux.HandlerFunc(http.MethodGet, "/rest/db/status", s.getDBStatus) // folder
restMux.HandlerFunc(http.MethodGet, "/rest/db/browse", s.getDBBrowse) // folder [prefix] [dirsonly] [levels]
restMux.HandlerFunc(http.MethodGet, "/rest/folder/versions", s.getFolderVersions) // folder
Expand Down Expand Up @@ -285,6 +286,7 @@ func (s *service) Serve(ctx context.Context) error {
restMux.HandlerFunc(http.MethodPost, "/rest/db/ignores", s.postDBIgnores) // folder
restMux.HandlerFunc(http.MethodPost, "/rest/db/override", s.postDBOverride) // folder
restMux.HandlerFunc(http.MethodPost, "/rest/db/revert", s.postDBRevert) // folder
restMux.HandlerFunc(http.MethodPost, "/rest/db/deleteignored", s.postDBDeleteIgnored) // folder
restMux.HandlerFunc(http.MethodPost, "/rest/db/scan", s.postDBScan) // folder [sub...] [delay]
restMux.HandlerFunc(http.MethodPost, "/rest/folder/versions", s.postFolderVersionsRestore) // folder <body>
restMux.HandlerFunc(http.MethodPost, "/rest/system/error", s.postSystemError) // <body>
Expand Down Expand Up @@ -844,6 +846,12 @@ func (s *service) postDBRevert(_ http.ResponseWriter, r *http.Request) {
go s.model.Revert(folder)
}

func (s *service) postDBDeleteIgnored(_ http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
folder := qs.Get("folder")
go s.model.DeleteIgnored(folder)
}

func getPagingParams(qs url.Values) (int, int) {
page, err := strconv.Atoi(qs.Get("page"))
if err != nil || page < 1 {
Expand Down Expand Up @@ -925,6 +933,26 @@ func (s *service) getDBLocalChanged(w http.ResponseWriter, r *http.Request) {
})
}

func (s *service) getDBLocalIgnored(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()

folder := qs.Get("folder")

page, perpage := getPagingParams(qs)

files, err := s.model.LocalIgnoredFolderFiles(folder, page, perpage)
if err != nil {
http.Error(w, err.Error(), http.StatusNotFound)
return
}

sendJSON(w, map[string]interface{}{
"files": toJsonFileInfoSlice(files),
"page": page,
"perpage": perpage,
})
}

func (s *service) getSystemConnections(w http.ResponseWriter, _ *http.Request) {
sendJSON(w, s.model.ConnectionStats())
}
Expand Down
4 changes: 4 additions & 0 deletions lib/db/set.go
Original file line number Diff line number Diff line change
Expand Up @@ -360,6 +360,10 @@ func (s *Snapshot) ReceiveOnlyChangedSize() Counts {
return s.meta.Counts(protocol.LocalDeviceID, protocol.FlagLocalReceiveOnly)
}

func (s *Snapshot) ReceiveRemoveIgnoredSize() Counts {
return s.meta.Counts(protocol.LocalDeviceID, protocol.FlagRemoveIgnored)
}

func (s *Snapshot) GlobalSize() Counts {
return s.meta.Counts(protocol.GlobalDeviceID, 0)
}
Expand Down
29 changes: 27 additions & 2 deletions lib/db/set_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -1278,14 +1278,20 @@ func TestReceiveOnlyAccounting(t *testing.T) {
if n := receiveOnlyChangedSize(t, s).Bytes; n != 0 {
t.Fatal("expected 0 receive only changed bytes initially, not", n)
}
if n := receiveRemoveIgnoredSize(t, s).Files; n != 0 {
t.Fatal("expected 0 receive only remove ignored files initially, not", n)
}
if n := receiveRemoveIgnoredSize(t, s).Bytes; n != 0 {
t.Fatal("expected 0 receive only remove ignored bytes initially, not", n)
}

// Detected a local change in a receive only folder

changed := files[0]
changed.Version = changed.Version.Update(local.Short())
changed.Size = 100
changed.ModifiedBy = local.Short()
changed.LocalFlags = protocol.FlagLocalReceiveOnly
changed.LocalFlags = protocol.FlagLocalReceiveOnly | protocol.FlagRemoveIgnored
s.Update(protocol.LocalDeviceID, []protocol.FileInfo{changed})

// Check that we see the files
Expand All @@ -1308,13 +1314,20 @@ func TestReceiveOnlyAccounting(t *testing.T) {
if n := receiveOnlyChangedSize(t, s).Bytes; n != 100 {
t.Fatal("expected 100 receive only changed bytes after local change, not", n)
}
if n := receiveRemoveIgnoredSize(t, s).Files; n != 1 {
t.Fatal("expected 1 receive only remove ignored file after local change, not", n)
}
if n := receiveRemoveIgnoredSize(t, s).Bytes; n != 100 {
t.Fatal("expected 100 receive only remove ignored bytes after local change, not", n)
}

// Fake a revert. That's a two step process, first converting our
// changed file into a less preferred variant, then pulling down the old
// version.

changed.Version = protocol.Vector{}
changed.LocalFlags &^= protocol.FlagLocalReceiveOnly
changed.LocalFlags &^= protocol.FlagRemoveIgnored
s.Update(protocol.LocalDeviceID, []protocol.FileInfo{changed})

s.Update(protocol.LocalDeviceID, []protocol.FileInfo{files[0]})
Expand All @@ -1339,6 +1352,12 @@ func TestReceiveOnlyAccounting(t *testing.T) {
if n := receiveOnlyChangedSize(t, s).Bytes; n != 0 {
t.Fatal("expected 0 receive only changed bytes after revert, not", n)
}
if n := receiveRemoveIgnoredSize(t, s).Files; n != 0 {
t.Fatal("expected 0 receive only remove ignored files after revert, not", n)
}
if n := receiveRemoveIgnoredSize(t, s).Bytes; n != 0 {
t.Fatal("expected 0 receive only remove ignored bytes after revert, not", n)
}
}

func TestNeedAfterUnignore(t *testing.T) {
Expand All @@ -1353,7 +1372,7 @@ func TestNeedAfterUnignore(t *testing.T) {

// Initial state: Devices in sync, locally ignored
local := protocol.FileInfo{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: remID, Value: 1}, {ID: myID, Value: 1}}}, ModifiedS: 10}
local.SetIgnored()
local.SetIgnored(false)
remote := protocol.FileInfo{Name: file, Version: protocol.Vector{Counters: []protocol.Counter{{ID: remID, Value: 1}, {ID: myID, Value: 1}}}, ModifiedS: 10}
s.Update(protocol.LocalDeviceID, fileList{local})
s.Update(remoteDevice0, fileList{remote})
Expand Down Expand Up @@ -1846,6 +1865,12 @@ func receiveOnlyChangedSize(t testing.TB, fs *db.FileSet) db.Counts {
return snap.ReceiveOnlyChangedSize()
}

func receiveRemoveIgnoredSize(t testing.TB, fs *db.FileSet) db.Counts {
snap := snapshot(t, fs)
defer snap.Release()
return snap.ReceiveRemoveIgnoredSize()
}

func filesToCounts(files []protocol.FileInfo) db.Counts {
cp := db.Counts{}
for _, f := range files {
Expand Down
15 changes: 13 additions & 2 deletions lib/db/structs.go
Original file line number Diff line number Diff line change
Expand Up @@ -47,6 +47,10 @@ func (f FileInfoTruncated) IsIgnored() bool {
return f.LocalFlags&protocol.FlagLocalIgnored != 0
}

func (f FileInfoTruncated) IsRemoveIgnored() bool {
return f.LocalFlags&protocol.FlagRemoveIgnored != 0
}

func (f FileInfoTruncated) MustRescan() bool {
return f.LocalFlags&protocol.FlagLocalMustRescan != 0
}
Expand Down Expand Up @@ -137,15 +141,22 @@ func (f FileInfoTruncated) FileBlocksHash() []byte {
return f.BlocksHash
}

func (f FileInfoTruncated) ConvertToIgnoredFileInfo() protocol.FileInfo {
func (f FileInfoTruncated) ConvertToIgnoredFileInfo(removeIgnored bool) protocol.FileInfo {
file := f.copyToFileInfo()
file.SetIgnored(removeIgnored)
return file
}

func (f FileInfoTruncated) ConvertToRemoveIgnoredFileInfo(removeIgnored bool) protocol.FileInfo {
file := f.copyToFileInfo()
file.SetIgnored()
file.SetRemoveIgnored(removeIgnored)
return file
}

func (f FileInfoTruncated) ConvertToDeletedFileInfo(by protocol.ShortID) protocol.FileInfo {
file := f.copyToFileInfo()
file.SetDeleted(by)
file.LocalFlags &^= protocol.FlagRemoveIgnored
return file
}

Expand Down
25 changes: 20 additions & 5 deletions lib/ignore/ignore.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,10 +26,11 @@ import (
)

const (
resultNotMatched Result = 0
resultInclude Result = 1 << iota
resultDeletable = 1 << iota
resultFoldCase = 1 << iota
resultNotMatched Result = 0
resultInclude Result = 1 << iota
resultDeletable = 1 << iota
resultFoldCase = 1 << iota
resultRemoveIgnored = 1 << iota
)

var defaultResult Result = resultInclude
Expand Down Expand Up @@ -84,10 +85,16 @@ func (p Pattern) String() string {
if p.result&resultDeletable == resultDeletable {
ret = "(?d)" + ret
}
if p.result&resultRemoveIgnored == resultRemoveIgnored {
ret = "(?r)" + ret
}
return ret
}

func (p Pattern) allowsSkippingIgnoredDirs() bool {
// if p.result.IsRemoveLocally() {
// return false
// }
if p.result.IsIgnored() {
return true
}
Expand All @@ -111,6 +118,10 @@ func (r Result) IsDeletable() bool {
return r.IsIgnored() && r&resultDeletable == resultDeletable
}

func (r Result) IsRemoveLocally() bool {
return r.IsIgnored() && r&resultRemoveIgnored == resultRemoveIgnored
}

func (r Result) IsCaseFolded() bool {
return r&resultFoldCase == resultFoldCase
}
Expand Down Expand Up @@ -435,7 +446,7 @@ func parseLine(line string) ([]Pattern, error) {
}

// Allow prefixes to be specified in any order, but only once.
var seenPrefix [3]bool
var seenPrefix [4]bool

for {
if strings.HasPrefix(line, "!") && !seenPrefix[0] {
Expand All @@ -450,6 +461,10 @@ func parseLine(line string) ([]Pattern, error) {
seenPrefix[2] = true
pattern.result |= resultDeletable
line = line[4:]
} else if strings.HasPrefix(line, "(?r)") && !seenPrefix[3] {
seenPrefix[3] = true
pattern.result |= resultRemoveIgnored
line = line[4:]
} else {
break
}
Expand Down
Loading
Loading