Merge remote-tracking branch 'origin/pr/721'

* origin/pr/721:
  Add tests for model.GetIgnores model.SetIgnores
  Expose ignores in the UI
  Add comments directive to ignores
  Expose ignores rest endpoints
  Expose ignores from model
This commit is contained in:
Jakob Borg 2014-09-22 14:59:13 +02:00
commit 737a28050c
9 changed files with 295 additions and 2 deletions

File diff suppressed because one or more lines are too long

View File

@ -84,6 +84,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
getRestMux.HandleFunc("/rest/discovery", restGetDiscovery) getRestMux.HandleFunc("/rest/discovery", restGetDiscovery)
getRestMux.HandleFunc("/rest/errors", restGetErrors) getRestMux.HandleFunc("/rest/errors", restGetErrors)
getRestMux.HandleFunc("/rest/events", restGetEvents) getRestMux.HandleFunc("/rest/events", restGetEvents)
getRestMux.HandleFunc("/rest/ignores", withModel(m, restGetIgnores))
getRestMux.HandleFunc("/rest/lang", restGetLang) getRestMux.HandleFunc("/rest/lang", restGetLang)
getRestMux.HandleFunc("/rest/model", withModel(m, restGetModel)) getRestMux.HandleFunc("/rest/model", withModel(m, restGetModel))
getRestMux.HandleFunc("/rest/model/version", withModel(m, restGetModelVersion)) getRestMux.HandleFunc("/rest/model/version", withModel(m, restGetModelVersion))
@ -105,6 +106,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint) postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint)
postRestMux.HandleFunc("/rest/error", restPostError) postRestMux.HandleFunc("/rest/error", restPostError)
postRestMux.HandleFunc("/rest/error/clear", restClearErrors) postRestMux.HandleFunc("/rest/error/clear", restClearErrors)
postRestMux.HandleFunc("/rest/ignores", withModel(m, restPostIgnores))
postRestMux.HandleFunc("/rest/model/override", withModel(m, restPostOverride)) postRestMux.HandleFunc("/rest/model/override", withModel(m, restPostOverride))
postRestMux.HandleFunc("/rest/reset", restPostReset) postRestMux.HandleFunc("/rest/reset", restPostReset)
postRestMux.HandleFunc("/rest/restart", restPostRestart) postRestMux.HandleFunc("/rest/restart", restPostRestart)
@ -457,6 +459,41 @@ func restGetReport(m *model.Model, w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(reportData(m)) json.NewEncoder(w).Encode(reportData(m))
} }
func restGetIgnores(m *model.Model, w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
w.Header().Set("Content-Type", "application/json; charset=utf-8")
ignores, err := m.GetIgnores(qs.Get("repo"))
if err != nil {
http.Error(w, err.Error(), 500)
return
}
json.NewEncoder(w).Encode(map[string][]string{
"ignore": ignores,
})
}
func restPostIgnores(m *model.Model, w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
var data map[string][]string
err := json.NewDecoder(r.Body).Decode(&data)
r.Body.Close()
if err != nil {
http.Error(w, err.Error(), 500)
return
}
err = m.SetIgnores(qs.Get("repo"), data["ignore"])
if err != nil {
http.Error(w, err.Error(), 500)
return
}
restGetIgnores(m, w, r)
}
func restGetEvents(w http.ResponseWriter, r *http.Request) { func restGetEvents(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query() qs := r.URL.Query()
sinceStr := qs.Get("since") sinceStr := qs.Get("since")

View File

@ -888,6 +888,42 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
$scope.saveConfig(); $scope.saveConfig();
}; };
$scope.editIgnores = function () {
if (!$scope.editingExisting) {
return;
}
$('#editIgnoresButton').attr('disabled', 'disabled');
$http.get(urlbase + '/ignores?repo=' + encodeURIComponent($scope.currentRepo.ID))
.success(function (data) {
$('#editRepo').modal('hide');
var textArea = $('#editIgnores textarea');
textArea.val(data.ignore.join('\n'));
$('#editIgnores').modal()
.on('hidden.bs.modal', function () {
$('#editRepo').modal();
})
.on('shown.bs.modal', function () {
textArea.focus();
});
})
.then(function () {
$('#editIgnoresButton').removeAttr('disabled');
});
};
$scope.saveIgnores = function () {
if (!$scope.editingExisting) {
return;
}
$http.post(urlbase + '/ignores?repo=' + encodeURIComponent($scope.currentRepo.ID), {
ignore: $('#editIgnores textarea').val().split('\n')
});
};
$scope.setAPIKey = function (cfg) { $scope.setAPIKey = function (cfg) {
cfg.APIKey = randomString(30, 32); cfg.APIKey = randomString(30, 32);
}; };

View File

@ -522,6 +522,35 @@
<button type="button" class="btn btn-primary btn-sm" ng-click="saveRepo()" ng-disabled="repoEditor.$invalid"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Save</span></button> <button type="button" class="btn btn-primary btn-sm" ng-click="saveRepo()" ng-disabled="repoEditor.$invalid"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button> <button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
<button ng-if="editingExisting" type="button" class="btn btn-danger pull-left btn-sm" ng-click="deleteRepo()"><span class="glyphicon glyphicon-minus"></span>&emsp;<span translate>Delete</span></button> <button ng-if="editingExisting" type="button" class="btn btn-danger pull-left btn-sm" ng-click="deleteRepo()"><span class="glyphicon glyphicon-minus"></span>&emsp;<span translate>Delete</span></button>
<button id="editIgnoresButton" ng-if="editingExisting" type="button" class="btn btn-default pull-left btn-sm" ng-click="editIgnores()"><span class="glyphicon glyphicon-pencil"></span>&emsp;<span translate>Edit ignored files and directories</span></button>
</div>
</div>
</div>
</div>
<!-- Ignores editor modal -->
<div id="editIgnores" class="modal fade" tabindex="-1">
<div class="modal-dialog modal-lg">
<div class="modal-content">
<div class="modal-header">
<h4 translate class="modal-title">Ignored files and directories</h4>
</div>
<div class="modal-body">
<p translate>Supported patterns:</p>
<ul>
<li><code>*</code> - <span translate>Single-level wildcard (matches anything within a single directory)</span>
<li><code>**</code> - <span translate>Multi-level wildcard (matches anything within all directories at any depth)</span>
<li><code>!</code> - <span translate>Inversion of the given condition, which excludes the given pattern from any previous matches</span>
<li><code>#include</code> - <span translate>Including ignores from another file</span>
<li><code>//</code> - <span translate>Comment</span>
</ul>
<textarea class="form-control" rows="15"></textarea>
</div>
<div class="modal-footer">
<div class="pull-left"><span translate >Ignore file location</span>:<code>{{ currentRepo.Directory }}/.stignore</code></div>
<button type="button" class="btn btn-primary btn-sm" data-dismiss="modal" ng-click="saveIgnores()"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
</div> </div>
</div> </div>
</div> </div>

View File

@ -11,6 +11,7 @@
"Bugs": "Bugs", "Bugs": "Bugs",
"CPU Utilization": "CPU Utilization", "CPU Utilization": "CPU Utilization",
"Close": "Close", "Close": "Close",
"Comment": "Comment",
"Connection Error": "Connection Error", "Connection Error": "Connection Error",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:", "Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:",
"Delete": "Delete", "Delete": "Delete",
@ -20,6 +21,7 @@
"Edit": "Edit", "Edit": "Edit",
"Edit Node": "Edit Node", "Edit Node": "Edit Node",
"Edit Repository": "Edit Repository", "Edit Repository": "Edit Repository",
"Edit ignored files and directories": "Edit ignored files and directories",
"Enable UPnP": "Enable UPnP", "Enable UPnP": "Enable UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.", "Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.",
"Error": "Error", "Error": "Error",
@ -37,7 +39,11 @@
"Global Repository": "Global Repository", "Global Repository": "Global Repository",
"Idle": "Idle", "Idle": "Idle",
"Ignore Permissions": "Ignore Permissions", "Ignore Permissions": "Ignore Permissions",
"Ignore file location": "Ignore file location",
"Ignored files and directories": "Ignored files and directories",
"Including ignores from another file": "Including ignores from another file",
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)", "Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
"Inversion of the given condition, which excludes the given pattern from any previous matches": "Inversion of the given condition, which excludes the given pattern from any previous matches",
"Keep Versions": "Keep Versions", "Keep Versions": "Keep Versions",
"Last seen": "Last seen", "Last seen": "Last seen",
"Latest Release": "Latest Release", "Latest Release": "Latest Release",
@ -48,6 +54,7 @@
"Max File Change Rate (KiB/s)": "Max File Change Rate (KiB/s)", "Max File Change Rate (KiB/s)": "Max File Change Rate (KiB/s)",
"Max Outstanding Requests": "Max Outstanding Requests", "Max Outstanding Requests": "Max Outstanding Requests",
"Maximum Age": "Maximum Age", "Maximum Age": "Maximum Age",
"Multi-level wildcard (matches anything within all directories at any depth)": "Multi-level wildcard (matches anything within all directories at any depth)",
"Never": "Never", "Never": "Never",
"No": "No", "No": "No",
"No File Versioning": "No File Versioning", "No File Versioning": "No File Versioning",
@ -90,11 +97,13 @@
"Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.", "Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.": "Shown instead of Node ID in the cluster status. Will be updated to the name the node advertises if left empty.",
"Shutdown": "Shutdown", "Shutdown": "Shutdown",
"Simple File Versioning": "Simple File Versioning", "Simple File Versioning": "Simple File Versioning",
"Single-level wildcard (matches anything within a single directory)": "Single-level wildcard (matches anything within a single directory)",
"Source Code": "Source Code", "Source Code": "Source Code",
"Staggered File Versioning": "Staggered File Versioning", "Staggered File Versioning": "Staggered File Versioning",
"Start Browser": "Start Browser", "Start Browser": "Start Browser",
"Stopped": "Stopped", "Stopped": "Stopped",
"Support / Forum": "Support / Forum", "Support / Forum": "Support / Forum",
"Supported patterns:": "Supported patterns:",
"Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses", "Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses",
"Synchronization": "Synchronization", "Synchronization": "Synchronization",
"Syncing": "Syncing", "Syncing": "Syncing",
@ -116,6 +125,7 @@
"The number of old versions to keep, per file.": "The number of old versions to keep, per file.", "The number of old versions to keep, per file.": "The number of old versions to keep, per file.",
"The number of versions must be a number and cannot be blank.": "The number of versions must be a number and cannot be blank.", "The number of versions must be a number and cannot be blank.": "The number of versions must be a number and cannot be blank.",
"The repository ID cannot be blank.": "The repository ID cannot be blank.", "The repository ID cannot be blank.": "The repository ID cannot be blank.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.", "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be unique.": "The repository ID must be unique.", "The repository ID must be unique.": "The repository ID must be unique.",
"The repository path cannot be blank.": "The repository path cannot be blank.", "The repository path cannot be blank.": "The repository path cannot be blank.",

View File

@ -122,6 +122,8 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) (Pa
switch { switch {
case line == "": case line == "":
continue continue
case strings.HasPrefix(line, "//"):
continue
case strings.HasPrefix(line, "#"): case strings.HasPrefix(line, "#"):
err = addPattern(line) err = addPattern(line)
case strings.HasSuffix(line, "/**"): case strings.HasSuffix(line, "/**"):

View File

@ -133,3 +133,21 @@ func TestCaseSensitivity(t *testing.T) {
} }
} }
} }
func TestCommentsAndBlankLines(t *testing.T) {
stignore := `
// foo
//bar
//!baz
//#dex
// ips
`
pats, _ := ignore.Parse(bytes.NewBufferString(stignore), ".stignore")
if len(pats) > 0 {
t.Errorf("Expected no patterns")
}
}

View File

@ -5,10 +5,12 @@
package model package model
import ( import (
"bufio"
"crypto/tls" "crypto/tls"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"io/ioutil"
"net" "net"
"os" "os"
"path/filepath" "path/filepath"
@ -22,6 +24,7 @@ import (
"github.com/syncthing/syncthing/files" "github.com/syncthing/syncthing/files"
"github.com/syncthing/syncthing/ignore" "github.com/syncthing/syncthing/ignore"
"github.com/syncthing/syncthing/lamport" "github.com/syncthing/syncthing/lamport"
"github.com/syncthing/syncthing/osutil"
"github.com/syncthing/syncthing/protocol" "github.com/syncthing/syncthing/protocol"
"github.com/syncthing/syncthing/scanner" "github.com/syncthing/syncthing/scanner"
"github.com/syncthing/syncthing/stats" "github.com/syncthing/syncthing/stats"
@ -579,6 +582,78 @@ func (m *Model) ConnectedTo(nodeID protocol.NodeID) bool {
return ok return ok
} }
func (m *Model) GetIgnores(repo string) ([]string, error) {
var lines []string
cfg, ok := m.repoCfgs[repo]
if !ok {
return lines, fmt.Errorf("Repo %s does not exist", repo)
}
m.rmut.Lock()
defer m.rmut.Unlock()
fd, err := os.Open(filepath.Join(cfg.Directory, ".stignore"))
if err != nil {
if os.IsNotExist(err) {
return lines, nil
}
l.Warnln("Loading .stignore:", err)
return lines, err
}
defer fd.Close()
scanner := bufio.NewScanner(fd)
for scanner.Scan() {
lines = append(lines, strings.TrimSpace(scanner.Text()))
}
return lines, nil
}
func (m *Model) SetIgnores(repo string, content []string) error {
cfg, ok := m.repoCfgs[repo]
if !ok {
return fmt.Errorf("Repo %s does not exist", repo)
}
fd, err := ioutil.TempFile("", "stignore-"+repo)
if err != nil {
l.Warnln("Saving .stignore:", err)
return err
}
writer := bufio.NewWriter(fd)
for _, line := range content {
fmt.Fprintln(writer, line)
}
err = writer.Flush()
if err != nil {
l.Warnln("Saving .stignore:", err)
fd.Close()
return err
}
err = fd.Close()
if err != nil {
l.Warnln("Saving .stignore:", err)
return err
}
file := filepath.Join(cfg.Directory, ".stignore")
m.rmut.Lock()
os.Remove(file)
err = osutil.Rename(fd.Name(), file)
m.rmut.Unlock()
if err != nil {
l.Warnln("Saving .stignore:", err)
return err
}
return m.ScanRepo(repo)
}
// AddConnection adds a new peer connection to the model. An initial index will // AddConnection adds a new peer connection to the model. An initial index will
// be sent to the connected peer, thereafter index updates whenever the local // be sent to the connected peer, thereafter index updates whenever the local
// repository changes. // repository changes.

View File

@ -369,3 +369,89 @@ func TestClusterConfig(t *testing.T) {
t.Errorf("Incorrect node ID %x != %x", id, node2) t.Errorf("Incorrect node ID %x != %x", id, node2)
} }
} }
func TestIgnores(t *testing.T) {
arrEqual := func(a, b []string) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i] != b[i] {
return false
}
}
return true
}
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
m := NewModel("/tmp", nil, "node", "syncthing", "dev", db)
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
expected := []string{
".*",
"quux",
}
ignores, err := m.GetIgnores("default")
if err != nil {
t.Error(err)
}
if !arrEqual(ignores, expected) {
t.Errorf("Incorrect ignores: %v != %v", ignores, expected)
}
ignores = append(ignores, "pox")
err = m.SetIgnores("default", ignores)
if err != nil {
t.Error(err)
}
ignores2, err := m.GetIgnores("default")
if err != nil {
t.Error(err)
}
if arrEqual(expected, ignores2) {
t.Errorf("Incorrect ignores: %v == %v", ignores2, expected)
}
if !arrEqual(ignores, ignores2) {
t.Errorf("Incorrect ignores: %v != %v", ignores2, ignores)
}
err = m.SetIgnores("default", expected)
if err != nil {
t.Error(err)
}
ignores, err = m.GetIgnores("default")
if err != nil {
t.Error(err)
}
if !arrEqual(ignores, expected) {
t.Errorf("Incorrect ignores: %v != %v", ignores, expected)
}
ignores, err = m.GetIgnores("doesnotexist")
if err == nil {
t.Error("No error")
}
err = m.SetIgnores("doesnotexist", expected)
if err == nil {
t.Error("No error")
}
m.AddRepo(config.RepositoryConfiguration{ID: "fresh", Directory: "XXX"})
ignores, err = m.GetIgnores("fresh")
if err != nil {
t.Error(err)
}
if len(ignores) > 0 {
t.Errorf("Expected no ignores, got: %v", ignores)
}
}