Browse Source

Allow prioritization of downloads based on name (fixes #174)

Arthur Axel 'fREW' Schmidt 11 years ago
parent
commit
82cfd37263
5 changed files with 188 additions and 8 deletions
  1. 1 0
      CONTRIBUTORS
  2. 52 7
      config/config.go
  3. 96 0
      config/config_test.go
  4. 34 0
      files/sort.go
  5. 5 1
      model/model.go

+ 1 - 0
CONTRIBUTORS

@@ -1,5 +1,6 @@
 Aaron Bieber <[email protected]>
 Andrew Dunham <[email protected]>
+Arthur Axel fREW Schmidt <[email protected]>
 Brandon Philips <[email protected]>
 James Patterson <[email protected]>
 Jens Diemer <[email protected]>

+ 52 - 7
config/config.go

@@ -11,10 +11,12 @@ import (
 	"io"
 	"os"
 	"reflect"
+	"regexp"
 	"sort"
 	"strconv"
 	"strings"
 
+	"github.com/calmh/syncthing/scanner"
 	"code.google.com/p/go.crypto/bcrypt"
 	"github.com/calmh/syncthing/logger"
 )
@@ -30,14 +32,42 @@ type Configuration struct {
 	XMLName      xml.Name                  `xml:"configuration" json:"-"`
 }
 
+// SyncOrderPattern allows a user to prioritize file downloading based on a
+// regular expression.  If a file matches the Pattern the Priority will be
+// assigned to the file.  If a file matches more than one Pattern the
+// Priorities are summed.  This allows a user to, for example, prioritize files
+// in a directory, as well as prioritize based on file type.  The higher the
+// priority the "sooner" a file will be downloaded.  Files can be deprioritized
+// by giving them a negative priority.  While Priority is represented as an
+// integer, the expected range is something like -1000 to 1000.
+type SyncOrderPattern struct {
+	Pattern         string `xml:"pattern,attr"`
+	Priority        int    `xml:"priority,attr"`
+	compiledPattern *regexp.Regexp
+}
+
+func (s *SyncOrderPattern) CompiledPattern() *regexp.Regexp {
+	if s.compiledPattern == nil {
+		re, err := regexp.Compile(s.Pattern)
+		if err != nil {
+			l.Warnln("Could not compile regexp (" + s.Pattern + "): " + err.Error())
+			s.compiledPattern = regexp.MustCompile("^\\0$")
+		} else {
+			s.compiledPattern = re
+		}
+	}
+	return s.compiledPattern
+}
+
 type RepositoryConfiguration struct {
-	ID          string                  `xml:"id,attr"`
-	Directory   string                  `xml:"directory,attr"`
-	Nodes       []NodeConfiguration     `xml:"node"`
-	ReadOnly    bool                    `xml:"ro,attr"`
-	IgnorePerms bool                    `xml:"ignorePerms,attr"`
-	Invalid     string                  `xml:"-"` // Set at runtime when there is an error, not saved
-	Versioning  VersioningConfiguration `xml:"versioning"`
+	ID                string                  `xml:"id,attr"`
+	Directory         string                  `xml:"directory,attr"`
+	Nodes             []NodeConfiguration     `xml:"node"`
+	ReadOnly          bool                    `xml:"ro,attr"`
+	IgnorePerms       bool                    `xml:"ignorePerms,attr"`
+	Invalid           string                  `xml:"-"` // Set at runtime when there is an error, not saved
+	Versioning        VersioningConfiguration `xml:"versioning"`
+	SyncOrderPatterns []SyncOrderPattern      `xml:"syncorder>pattern"`
 
 	nodeIDs []string
 }
@@ -92,6 +122,21 @@ func (r *RepositoryConfiguration) NodeIDs() []string {
 	return r.nodeIDs
 }
 
+func (r RepositoryConfiguration) FileRanker() func(scanner.File) int {
+	if len(r.SyncOrderPatterns) <= 0 {
+		return nil
+	}
+	return func(f scanner.File) int {
+		ret := 0
+		for _, v := range r.SyncOrderPatterns {
+			if v.CompiledPattern().MatchString(f.Name) {
+				ret += v.Priority
+			}
+		}
+		return ret
+	}
+}
+
 type NodeConfiguration struct {
 	NodeID    string   `xml:"id,attr"`
 	Name      string   `xml:"name,attr,omitempty"`

+ 96 - 0
config/config_test.go

@@ -10,6 +10,9 @@ import (
 	"os"
 	"reflect"
 	"testing"
+
+	"github.com/calmh/syncthing/files"
+	"github.com/calmh/syncthing/scanner"
 )
 
 func TestDefaultValues(t *testing.T) {
@@ -281,3 +284,96 @@ func TestStripNodeIs(t *testing.T) {
 		}
 	}
 }
+
+func TestSyncOrders(t *testing.T) {
+	data := []byte(`
+<configuration version="2">
+    <node id="AAAA-BBBB-CCCC">
+        <address>dynamic</address>
+    </node>
+    <repository directory="~/Sync">
+        <syncorder>
+            <pattern pattern="\.jpg$" priority="1" />
+        </syncorder>
+        <node id="AAAA-BBBB-CCCC" name=""></node>
+    </repository>
+</configuration>
+`)
+
+	expected := []SyncOrderPattern{
+		{
+			Pattern: "\\.jpg$",
+			Priority:  1,
+		},
+	}
+
+	cfg, err := Load(bytes.NewReader(data), "n4")
+	if err != nil {
+		t.Error(err)
+	}
+
+	for i := range expected {
+		if !reflect.DeepEqual(cfg.Repositories[0].SyncOrderPatterns[i], expected[i]) {
+			t.Errorf("Nodes[%d] differ;\n  E: %#v\n  A: %#v", i, expected[i], cfg.Repositories[0].SyncOrderPatterns[i])
+		}
+	}
+}
+
+func TestFileSorter(t *testing.T) {
+	rcfg := RepositoryConfiguration{
+		SyncOrderPatterns: []SyncOrderPattern{
+			{"\\.jpg$", 10, nil},
+			{"\\.mov$", 5, nil},
+			{"^camera-uploads", 100, nil},
+		},
+	}
+
+	f := []scanner.File{
+		{Name: "bar.mov"},
+		{Name: "baz.txt"},
+		{Name: "foo.jpg"},
+		{Name: "frew/foo.jpg"},
+		{Name: "frew/lol.go"},
+		{Name: "frew/rofl.copter"},
+		{Name: "frew/bar.mov"},
+		{Name: "camera-uploads/foo.jpg"},
+		{Name: "camera-uploads/hurr.pl"},
+		{Name: "camera-uploads/herp.mov"},
+		{Name: "camera-uploads/wee.txt"},
+	}
+
+	files.SortBy(rcfg.FileRanker()).Sort(f)
+
+	expected := []scanner.File{
+		{Name: "camera-uploads/foo.jpg"},
+		{Name: "camera-uploads/herp.mov"},
+		{Name: "camera-uploads/hurr.pl"},
+		{Name: "camera-uploads/wee.txt"},
+		{Name: "foo.jpg"},
+		{Name: "frew/foo.jpg"},
+		{Name: "bar.mov"},
+		{Name: "frew/bar.mov"},
+		{Name: "frew/lol.go"},
+		{Name: "baz.txt"},
+		{Name: "frew/rofl.copter"},
+	}
+
+	if !reflect.DeepEqual(f, expected) {
+		t.Errorf(
+			"\n\nexpected:\n" +
+			formatFiles(expected) + "\n" +
+			"got:\n" +
+			formatFiles(f) + "\n\n",
+		)
+	}
+}
+
+func formatFiles(f []scanner.File) string {
+	ret := ""
+
+	for _, v := range f {
+		ret += "   " + v.Name + "\n"
+	}
+
+	return ret
+}

+ 34 - 0
files/sort.go

@@ -0,0 +1,34 @@
+package files
+
+import (
+	"sort"
+
+	"github.com/calmh/syncthing/scanner"
+)
+
+type SortBy func(p scanner.File) int
+
+func (by SortBy) Sort(files []scanner.File) {
+	ps := &fileSorter{
+		files: files,
+		by:    by,
+	}
+	sort.Sort(ps)
+}
+
+type fileSorter struct {
+	files []scanner.File
+	by    func(p1 scanner.File) int
+}
+
+func (s *fileSorter) Len() int {
+	return len(s.files)
+}
+
+func (s *fileSorter) Swap(i, j int) {
+	s.files[i], s.files[j] = s.files[j], s.files[i]
+}
+
+func (s *fileSorter) Less(i, j int) bool {
+	return s.by(s.files[i]) > s.by(s.files[j])
+}

+ 5 - 1
model/model.go

@@ -248,7 +248,11 @@ func (m *Model) NeedFilesRepo(repo string) []scanner.File {
 	m.rmut.RLock()
 	defer m.rmut.RUnlock()
 	if rf, ok := m.repoFiles[repo]; ok {
-		return rf.Need(cid.LocalID)
+		f := rf.Need(cid.LocalID)
+		if r := m.repoCfgs[repo].FileRanker(); r != nil {
+			files.SortBy(r).Sort(f)
+		}
+		return f
 	}
 	return nil
 }