Browse Source

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
Jakob Borg 11 years ago
parent
commit
737a28050c
9 changed files with 293 additions and 0 deletions
  1. 0 0
      auto/gui.files.go
  2. 37 0
      cmd/syncthing/gui.go
  3. 36 0
      gui/app.js
  4. 29 0
      gui/index.html
  5. 10 0
      gui/lang/lang-en.json
  6. 2 0
      ignore/ignore.go
  7. 18 0
      ignore/ignore_test.go
  8. 75 0
      model/model.go
  9. 86 0
      model/model_test.go

File diff suppressed because it is too large
+ 0 - 0
auto/gui.files.go


+ 37 - 0
cmd/syncthing/gui.go

@@ -84,6 +84,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
 	getRestMux.HandleFunc("/rest/discovery", restGetDiscovery)
 	getRestMux.HandleFunc("/rest/errors", restGetErrors)
 	getRestMux.HandleFunc("/rest/events", restGetEvents)
+	getRestMux.HandleFunc("/rest/ignores", withModel(m, restGetIgnores))
 	getRestMux.HandleFunc("/rest/lang", restGetLang)
 	getRestMux.HandleFunc("/rest/model", withModel(m, restGetModel))
 	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/error", restPostError)
 	postRestMux.HandleFunc("/rest/error/clear", restClearErrors)
+	postRestMux.HandleFunc("/rest/ignores", withModel(m, restPostIgnores))
 	postRestMux.HandleFunc("/rest/model/override", withModel(m, restPostOverride))
 	postRestMux.HandleFunc("/rest/reset", restPostReset)
 	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))
 }
 
+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) {
 	qs := r.URL.Query()
 	sinceStr := qs.Get("since")

+ 36 - 0
gui/app.js

@@ -888,6 +888,42 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
         $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) {
         cfg.APIKey = randomString(30, 32);
     };

+ 29 - 0
gui/index.html

@@ -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-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 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>

+ 10 - 0
gui/lang/lang-en.json

@@ -11,6 +11,7 @@
    "Bugs": "Bugs",
    "CPU Utilization": "CPU Utilization",
    "Close": "Close",
+   "Comment": "Comment",
    "Connection Error": "Connection Error",
    "Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:",
    "Delete": "Delete",
@@ -20,6 +21,7 @@
    "Edit": "Edit",
    "Edit Node": "Edit Node",
    "Edit Repository": "Edit Repository",
+   "Edit ignored files and directories": "Edit ignored files and directories",
    "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.",
    "Error": "Error",
@@ -37,7 +39,11 @@
    "Global Repository": "Global Repository",
    "Idle": "Idle",
    "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)",
+   "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",
    "Last seen": "Last seen",
    "Latest Release": "Latest Release",
@@ -48,6 +54,7 @@
    "Max File Change Rate (KiB/s)": "Max File Change Rate (KiB/s)",
    "Max Outstanding Requests": "Max Outstanding Requests",
    "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",
    "No": "No",
    "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.",
    "Shutdown": "Shutdown",
    "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",
    "Staggered File Versioning": "Staggered File Versioning",
    "Start Browser": "Start Browser",
    "Stopped": "Stopped",
    "Support / Forum": "Support / Forum",
+   "Supported patterns:": "Supported patterns:",
    "Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses",
    "Synchronization": "Synchronization",
    "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 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 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 unique.": "The repository ID must be unique.",
    "The repository path cannot be blank.": "The repository path cannot be blank.",

+ 2 - 0
ignore/ignore.go

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

+ 18 - 0
ignore/ignore_test.go

@@ -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")
+	}
+}

+ 75 - 0
model/model.go

@@ -5,10 +5,12 @@
 package model
 
 import (
+	"bufio"
 	"crypto/tls"
 	"errors"
 	"fmt"
 	"io"
+	"io/ioutil"
 	"net"
 	"os"
 	"path/filepath"
@@ -22,6 +24,7 @@ import (
 	"github.com/syncthing/syncthing/files"
 	"github.com/syncthing/syncthing/ignore"
 	"github.com/syncthing/syncthing/lamport"
+	"github.com/syncthing/syncthing/osutil"
 	"github.com/syncthing/syncthing/protocol"
 	"github.com/syncthing/syncthing/scanner"
 	"github.com/syncthing/syncthing/stats"
@@ -579,6 +582,78 @@ func (m *Model) ConnectedTo(nodeID protocol.NodeID) bool {
 	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
 // be sent to the connected peer, thereafter index updates whenever the local
 // repository changes.

+ 86 - 0
model/model_test.go

@@ -369,3 +369,89 @@ func TestClusterConfig(t *testing.T) {
 		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)
+	}
+}

Some files were not shown because too many files changed in this diff