Compare commits

...

18 Commits

Author SHA1 Message Date
Jakob Borg
456d9e870d Integration test, API key 2014-06-08 19:17:42 +02:00
Jakob Borg
a1533696a5 Travis badge 2014-06-08 07:40:57 +02:00
Jakob Borg
92499af323 Revert "Build for Solaris"
This reverts commit 5a2328d9a5.
2014-06-08 07:37:51 +02:00
Arthur Axel 'fREW' Schmidt
b2988cdd35 test against travis-ci 2014-06-08 07:37:42 +02:00
Arthur Axel 'fREW' Schmidt
82cfd37263 Allow prioritization of downloads based on name (fixes #174) 2014-06-08 07:16:25 +02:00
Jakob Borg
df381fd03f Let server side decide if restart is needed on config change 2014-06-07 04:00:46 +02:00
Jakob Borg
5a2328d9a5 Build for Solaris 2014-06-07 03:56:13 +02:00
Jakob Borg
b2f66cfb60 Reject index for existing repo from unshared node (fixes #342) 2014-06-06 21:48:29 +02:00
Jakob Borg
6d24e4f122 Test case for #342 2014-06-06 21:40:04 +02:00
Jakob Borg
2e2185165c Improve test suite, fix bug in Set.Global() 2014-06-05 15:32:11 +02:00
Jakob Borg
f0612e57c2 Integration tests with API key 2014-06-05 11:48:22 +02:00
Jakob Borg
e5d16ed08a Remove extra whitespace around node ID (fixes #335) 2014-06-05 11:29:05 +02:00
Jakob Borg
1cff9ccc63 API key change should take effect on restart only 2014-06-05 09:16:12 +02:00
Jakob Borg
20a018db2e Implement API keys 2014-06-04 22:00:55 +02:00
Jakob Borg
80c2b32b92 Implement CSRF protection for REST interface (fixes #287) 2014-06-04 21:20:07 +02:00
Jakob Borg
028e9bc17a Tweak Shared With wording 2014-06-04 15:05:23 +02:00
Jakob Borg
afc2d6fda4 Clarify repo mismatch message (fixes #331) 2014-06-04 14:17:48 +02:00
Jakob Borg
bec5c76631 Use unique name and O_EXCL for temporary indexes (fixes #332) 2014-06-04 13:43:59 +02:00
23 changed files with 699 additions and 176 deletions

12
.travis.yml Normal file
View File

@@ -0,0 +1,12 @@
language: go
go:
- 1.2
- tip
install:
- export PATH=$PATH:$HOME/gopath/bin
- ./build.sh setup
script:
- ./build.sh test

View File

@@ -1,5 +1,6 @@
Aaron Bieber <qbit@deftly.net>
Andrew Dunham <andrew@du.nham.ca>
Arthur Axel fREW Schmidt <frew@afoolishmanifesto.com>
Brandon Philips <brandon@ifup.org>
James Patterson <jamespatterson@operamail.com>
Jens Diemer <github.com@jensdiemer.de>

View File

@@ -1,4 +1,4 @@
syncthing
syncthing [![Build Status](https://travis-ci.org/calmh/syncthing.svg?branch=master)](https://travis-ci.org/calmh/syncthing)
=========
This is the `syncthing` project. The following are the project goals:

View File

File diff suppressed because one or more lines are too long

View File

@@ -16,6 +16,7 @@ import (
"net"
"net/http"
"path/filepath"
"reflect"
"runtime"
"sync"
"time"
@@ -40,6 +41,7 @@ var (
guiErrors = []guiError{}
guiErrorsMut sync.Mutex
static func(http.ResponseWriter, *http.Request, *log.Logger)
apiKey string
)
const (
@@ -105,6 +107,7 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
router.Post("/rest/discovery/hint", restPostDiscoveryHint)
mr := martini.New()
mr.Use(csrfMiddleware)
if len(cfg.User) > 0 && len(cfg.Password) > 0 {
mr.Use(basic(cfg.User, cfg.Password))
}
@@ -114,6 +117,9 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
mr.Action(router.Handle)
mr.Map(m)
apiKey = cfg.APIKey
loadCsrfTokens()
go http.Serve(listener, mr)
return nil
@@ -205,9 +211,43 @@ func restPostConfig(req *http.Request) {
newCfg.GUI.Password = string(hash)
}
}
// Figure out if any changes require a restart
if len(cfg.Repositories) != len(newCfg.Repositories) {
configInSync = false
} else {
om := cfg.RepoMap()
nm := newCfg.RepoMap()
for id := range om {
if !reflect.DeepEqual(om[id], nm[id]) {
configInSync = false
break
}
}
}
if len(cfg.Nodes) != len(newCfg.Nodes) {
configInSync = false
} else {
om := cfg.NodeMap()
nm := newCfg.NodeMap()
for k := range om {
if _, ok := nm[k]; !ok {
configInSync = false
break
}
}
}
if !reflect.DeepEqual(cfg.Options, newCfg.Options) {
configInSync = false
}
// Activate and save
cfg = newCfg
saveConfig()
configInSync = false
}
}
@@ -318,6 +358,10 @@ func getQR(w http.ResponseWriter, params martini.Params) {
func basic(username string, passhash string) http.HandlerFunc {
return func(res http.ResponseWriter, req *http.Request) {
if validAPIKey(req.Header.Get("X-API-Key")) {
return
}
error := func() {
time.Sleep(time.Duration(rand.Intn(100)+100) * time.Millisecond)
res.Header().Set("WWW-Authenticate", "Basic realm=\"Authorization Required\"")
@@ -355,6 +399,10 @@ func basic(username string, passhash string) http.HandlerFunc {
}
}
func validAPIKey(k string) bool {
return len(apiKey) > 0 && k == apiKey
}
func embeddedStatic() func(http.ResponseWriter, *http.Request, *log.Logger) {
var modt = time.Now().UTC().Format(http.TimeFormat)

115
cmd/syncthing/gui_csrf.go Normal file
View File

@@ -0,0 +1,115 @@
package main
import (
"bufio"
"crypto/rand"
"encoding/base64"
"fmt"
"net/http"
"os"
"path/filepath"
"strings"
"sync"
"time"
"github.com/calmh/syncthing/osutil"
)
var csrfTokens []string
var csrfMut sync.Mutex
// Check for CSRF token on /rest/ URLs. If a correct one is not given, reject
// the request with 403. For / and /index.html, set a new CSRF cookie if none
// is currently set.
func csrfMiddleware(w http.ResponseWriter, r *http.Request) {
if validAPIKey(r.Header.Get("X-API-Key")) {
return
}
if strings.HasPrefix(r.URL.Path, "/rest/") {
token := r.Header.Get("X-CSRF-Token")
if !validCsrfToken(token) {
http.Error(w, "CSRF Error", 403)
}
} else if r.URL.Path == "/" || r.URL.Path == "/index.html" {
cookie, err := r.Cookie("CSRF-Token")
if err != nil || !validCsrfToken(cookie.Value) {
cookie = &http.Cookie{
Name: "CSRF-Token",
Value: newCsrfToken(),
}
http.SetCookie(w, cookie)
}
}
}
func validCsrfToken(token string) bool {
csrfMut.Lock()
defer csrfMut.Unlock()
for _, t := range csrfTokens {
if t == token {
return true
}
}
return false
}
func newCsrfToken() string {
bs := make([]byte, 30)
_, err := rand.Reader.Read(bs)
if err != nil {
l.Fatalln(err)
}
token := base64.StdEncoding.EncodeToString(bs)
csrfMut.Lock()
csrfTokens = append(csrfTokens, token)
if len(csrfTokens) > 10 {
csrfTokens = csrfTokens[len(csrfTokens)-10:]
}
defer csrfMut.Unlock()
saveCsrfTokens()
return token
}
func saveCsrfTokens() {
name := filepath.Join(confDir, "csrftokens.txt")
tmp := fmt.Sprintf("%s.tmp.%d", name, time.Now().UnixNano())
f, err := os.OpenFile(tmp, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
if err != nil {
return
}
defer os.Remove(tmp)
for _, t := range csrfTokens {
_, err := fmt.Fprintln(f, t)
if err != nil {
return
}
}
err = f.Close()
if err != nil {
return
}
osutil.Rename(tmp, name)
}
func loadCsrfTokens() {
name := filepath.Join(confDir, "csrftokens.txt")
f, err := os.Open(name)
if err != nil {
return
}
defer f.Close()
s := bufio.NewScanner(f)
for s.Scan() {
csrfTokens = append(csrfTokens, s.Text())
}
}

View File

@@ -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"`
@@ -123,6 +168,23 @@ type GUIConfiguration struct {
User string `xml:"user,omitempty"`
Password string `xml:"password,omitempty"`
UseTLS bool `xml:"tls,attr"`
APIKey string `xml:"apikey,omitempty"`
}
func (cfg *Configuration) NodeMap() map[string]NodeConfiguration {
m := make(map[string]NodeConfiguration, len(cfg.Nodes))
for _, n := range cfg.Nodes {
m[n.NodeID] = n
}
return m
}
func (cfg *Configuration) RepoMap() map[string]RepositoryConfiguration {
m := make(map[string]RepositoryConfiguration, len(cfg.Repositories))
for _, r := range cfg.Repositories {
m[r.ID] = r
}
return m
}
func setDefaults(data interface{}) error {

View File

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

View File

@@ -297,6 +297,9 @@ func (m *Set) replace(cid uint, fs []scanner.File) {
if na != 0 {
// Someone had the file
f := m.files[nk]
f.Global = true
m.files[nk] = f
m.globalKey[n] = nk
m.globalAvailability[n] = na
} else {

View File

@@ -2,7 +2,7 @@
// Use of this source code is governed by an MIT-style license that can be
// found in the LICENSE file.
package files
package files_test
import (
"fmt"
@@ -11,6 +11,7 @@ import (
"testing"
"github.com/calmh/syncthing/cid"
"github.com/calmh/syncthing/files"
"github.com/calmh/syncthing/lamport"
"github.com/calmh/syncthing/protocol"
"github.com/calmh/syncthing/scanner"
@@ -31,7 +32,7 @@ func (l fileList) Swap(a, b int) {
}
func TestGlobalSet(t *testing.T) {
m := NewSet()
m := files.NewSet()
local := []scanner.File{
scanner.File{Name: "a", Version: 1000},
@@ -40,7 +41,15 @@ func TestGlobalSet(t *testing.T) {
scanner.File{Name: "d", Version: 1000},
}
remote := []scanner.File{
remote0 := []scanner.File{
scanner.File{Name: "a", Version: 1000},
scanner.File{Name: "c", Version: 1002},
}
remote1 := []scanner.File{
scanner.File{Name: "b", Version: 1001},
scanner.File{Name: "e", Version: 1000},
}
remoteTot := []scanner.File{
scanner.File{Name: "a", Version: 1000},
scanner.File{Name: "b", Version: 1001},
scanner.File{Name: "c", Version: 1002},
@@ -55,25 +64,86 @@ func TestGlobalSet(t *testing.T) {
scanner.File{Name: "e", Version: 1000},
}
expectedLocalNeed := []scanner.File{
scanner.File{Name: "b", Version: 1001},
scanner.File{Name: "c", Version: 1002},
scanner.File{Name: "e", Version: 1000},
}
expectedRemoteNeed := []scanner.File{
scanner.File{Name: "d", Version: 1000},
}
m.ReplaceWithDelete(cid.LocalID, local)
m.Replace(1, remote)
m.Replace(1, remote0)
m.Update(1, remote1)
g := m.Global()
sort.Sort(fileList(g))
sort.Sort(fileList(expectedGlobal))
if !reflect.DeepEqual(g, expectedGlobal) {
t.Errorf("Global incorrect;\n A: %v !=\n E: %v", g, expectedGlobal)
}
if lb := len(m.files); lb != 7 {
t.Errorf("Num files incorrect %d != 7\n%v", lb, m.files)
h := m.Have(cid.LocalID)
sort.Sort(fileList(h))
if !reflect.DeepEqual(h, local) {
t.Errorf("Have incorrect;\n A: %v !=\n E: %v", h, local)
}
h = m.Have(1)
sort.Sort(fileList(h))
if !reflect.DeepEqual(h, remoteTot) {
t.Errorf("Have incorrect;\n A: %v !=\n E: %v", h, remoteTot)
}
n := m.Need(cid.LocalID)
sort.Sort(fileList(n))
if !reflect.DeepEqual(n, expectedLocalNeed) {
t.Errorf("Need incorrect;\n A: %v !=\n E: %v", n, expectedLocalNeed)
}
n = m.Need(1)
sort.Sort(fileList(n))
if !reflect.DeepEqual(n, expectedRemoteNeed) {
t.Errorf("Need incorrect;\n A: %v !=\n E: %v", n, expectedRemoteNeed)
}
f := m.Get(cid.LocalID, "b")
if !reflect.DeepEqual(f, local[1]) {
t.Errorf("Get incorrect;\n A: %v !=\n E: %v", f, local[1])
}
f = m.Get(1, "b")
if !reflect.DeepEqual(f, remote1[0]) {
t.Errorf("Get incorrect;\n A: %v !=\n E: %v", f, remote1[0])
}
f = m.GetGlobal("b")
if !reflect.DeepEqual(f, remote1[0]) {
t.Errorf("Get incorrect;\n A: %v !=\n E: %v", f, remote1[0])
}
a := int(m.Availability("a"))
if av := 1<<0 + 1<<1; a != av {
t.Errorf("Availability incorrect;\n A: %v !=\n E: %v", a, av)
}
a = int(m.Availability("b"))
if av := 1 << 1; a != av {
t.Errorf("Availability incorrect;\n A: %v !=\n E: %v", a, av)
}
a = int(m.Availability("d"))
if av := 1 << 0; a != av {
t.Errorf("Availability incorrect;\n A: %v !=\n E: %v", a, av)
}
}
func TestLocalDeleted(t *testing.T) {
m := NewSet()
m := files.NewSet()
lamport.Default = lamport.Clock{}
local1 := []scanner.File{
@@ -151,7 +221,7 @@ func Benchmark10kReplace(b *testing.B) {
b.ResetTimer()
for i := 0; i < b.N; i++ {
m := NewSet()
m := files.NewSet()
m.ReplaceWithDelete(cid.LocalID, local)
}
}
@@ -162,7 +232,7 @@ func Benchmark10kUpdateChg(b *testing.B) {
remote = append(remote, scanner.File{Name: fmt.Sprintf("file%d", i), Version: 1000})
}
m := NewSet()
m := files.NewSet()
m.Replace(1, remote)
var local []scanner.File
@@ -189,7 +259,7 @@ func Benchmark10kUpdateSme(b *testing.B) {
remote = append(remote, scanner.File{Name: fmt.Sprintf("file%d", i), Version: 1000})
}
m := NewSet()
m := files.NewSet()
m.Replace(1, remote)
var local []scanner.File
@@ -211,7 +281,7 @@ func Benchmark10kNeed2k(b *testing.B) {
remote = append(remote, scanner.File{Name: fmt.Sprintf("file%d", i), Version: 1000})
}
m := NewSet()
m := files.NewSet()
m.Replace(cid.LocalID+1, remote)
var local []scanner.File
@@ -239,7 +309,7 @@ func Benchmark10kHave(b *testing.B) {
remote = append(remote, scanner.File{Name: fmt.Sprintf("file%d", i), Version: 1000})
}
m := NewSet()
m := files.NewSet()
m.Replace(cid.LocalID+1, remote)
var local []scanner.File
@@ -267,7 +337,7 @@ func Benchmark10kGlobal(b *testing.B) {
remote = append(remote, scanner.File{Name: fmt.Sprintf("file%d", i), Version: 1000})
}
m := NewSet()
m := files.NewSet()
m.Replace(cid.LocalID+1, remote)
var local []scanner.File
@@ -290,7 +360,7 @@ func Benchmark10kGlobal(b *testing.B) {
}
func TestGlobalReset(t *testing.T) {
m := NewSet()
m := files.NewSet()
local := []scanner.File{
scanner.File{Name: "a", Version: 1000},
@@ -306,28 +376,27 @@ func TestGlobalReset(t *testing.T) {
scanner.File{Name: "e", Version: 1000},
}
expectedGlobalKey := map[string]key{
"a": keyFor(local[0]),
"b": keyFor(local[1]),
"c": keyFor(local[2]),
"d": keyFor(local[3]),
m.ReplaceWithDelete(cid.LocalID, local)
g := m.Global()
sort.Sort(fileList(g))
if !reflect.DeepEqual(g, local) {
t.Errorf("Global incorrect;\n%v !=\n%v", g, local)
}
m.ReplaceWithDelete(cid.LocalID, local)
m.Replace(1, remote)
m.Replace(1, nil)
if !reflect.DeepEqual(m.globalKey, expectedGlobalKey) {
t.Errorf("Global incorrect;\n%v !=\n%v", m.globalKey, expectedGlobalKey)
}
g = m.Global()
sort.Sort(fileList(g))
if lb := len(m.files); lb != 4 {
t.Errorf("Num files incorrect %d != 4\n%v", lb, m.files)
if !reflect.DeepEqual(g, local) {
t.Errorf("Global incorrect;\n%v !=\n%v", g, local)
}
}
func TestNeed(t *testing.T) {
m := NewSet()
m := files.NewSet()
local := []scanner.File{
scanner.File{Name: "a", Version: 1000},
@@ -363,7 +432,7 @@ func TestNeed(t *testing.T) {
}
func TestChanges(t *testing.T) {
m := NewSet()
m := files.NewSet()
local1 := []scanner.File{
scanner.File{Name: "a", Version: 1000},

34
files/sort.go Normal file
View File

@@ -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])
}

View File

@@ -10,6 +10,11 @@
var syncthing = angular.module('syncthing', []);
var urlbase = 'rest';
syncthing.config(function ($httpProvider) {
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token';
});
syncthing.controller('SyncthingCtrl', function ($scope, $http) {
var prevDate = 0;
var getOK = true;
@@ -47,6 +52,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
{id: 'User', descr: 'GUI Authentication User', type: 'text', restart: true},
{id: 'Password', descr: 'GUI Authentication Password', type: 'password', restart: true},
{id: 'UseTLS', descr: 'Use HTTPS for GUI', type: 'bool', restart: true},
{id: 'APIKey', descr: 'API Key', type: 'apikey'},
];
function getSucceeded() {
@@ -262,6 +268,16 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
$('#settings').modal({backdrop: 'static', keyboard: true});
};
$scope.saveConfig = function() {
var cfg = JSON.stringify($scope.config);
var opts = {headers: {'Content-Type': 'application/json'}};
$http.post(urlbase + '/config', cfg, opts).success(function () {
$http.get(urlbase + '/config/sync').success(function (data) {
$scope.configInSync = data.configInSync;
});
});
};
$scope.saveSettings = function () {
// Make sure something changed
var changed = ! angular.equals($scope.config.Options, $scope.config.workingOptions) ||
@@ -275,10 +291,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
// Apply new settings locally
$scope.config.Options = angular.copy($scope.config.workingOptions);
$scope.config.GUI = angular.copy($scope.config.workingGUI);
$scope.configInSync = false;
$scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) { return x.trim(); });
$http.post(urlbase + '/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
$scope.saveConfig();
}
$('#settings').modal("hide");
@@ -352,14 +367,12 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
});
}
$scope.configInSync = false;
$http.post(urlbase + '/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
$scope.saveConfig();
};
$scope.saveNode = function () {
var nodeCfg, done, i;
$scope.configInSync = false;
$('#editNode').modal('hide');
nodeCfg = $scope.currentNode;
nodeCfg.NodeID = nodeCfg.NodeID.replace(/ /g, '').replace(/-/g, '').toUpperCase().trim();
@@ -381,7 +394,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
$scope.nodes.sort(nodeCompare);
$scope.config.Nodes = $scope.nodes;
$http.post(urlbase + '/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
$scope.saveConfig();
};
$scope.otherNodes = function () {
@@ -456,7 +469,6 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
$scope.saveRepo = function () {
var repoCfg, done, i;
$scope.configInSync = false;
$('#editRepo').modal('hide');
repoCfg = $scope.currentRepo;
repoCfg.Nodes = [];
@@ -484,7 +496,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
$scope.repos[repoCfg.ID] = repoCfg;
$scope.config.Repositories = repoList($scope.repos);
$http.post(urlbase + '/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
$scope.saveConfig();
};
$scope.sharesRepo = function(repoCfg) {
@@ -505,8 +517,11 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
delete $scope.repos[$scope.currentRepo.ID];
$scope.config.Repositories = repoList($scope.repos);
$scope.configInSync = false;
$http.post(urlbase + '/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
$scope.saveConfig();
};
$scope.setAPIKey = function (cfg) {
cfg.APIKey = randomString(30, 32);
};
$scope.init = function() {
@@ -588,6 +603,18 @@ function decimals(val, num) {
return decs;
}
function randomString(len, bits)
{
bits = bits || 36;
var outStr = "", newStr;
while (outStr.length < len)
{
newStr = Math.random().toString(bits).slice(2);
outStr += newStr.slice(0, Math.min(newStr.length, (len - outStr.length)));
}
return outStr.toUpperCase();
}
syncthing.filter('natural', function () {
return function (input, valid) {
return input.toFixed(decimals(input, valid));

View File

@@ -408,9 +408,7 @@ found in the LICENSE file.
</h4>
</div>
<div class="modal-body">
<div class="well well-sm text-monospace text-center">
{{myID | chunkID}}
</div>
<div class="well well-sm text-monospace text-center">{{myID | chunkID}}</div>
<img ng-if="myID" class="center-block img-thumbnail" src="qr/{{myID | chunkID}}"/>
</div>
<div class="modal-footer">
@@ -516,7 +514,7 @@ found in the LICENSE file.
<p class="help-block">File permission bits are ignored when looking for changes. Use on FAT filesystems.</p>
</div>
<div class="form-group">
<label for="nodes">Nodes</label>
<label for="nodes">Share With Nodes</label>
<div class="checkbox" ng-repeat="node in otherNodes()">
<label>
<input type="checkbox" ng-model="currentRepo.selectedNodes[node.NodeID]"> {{nodeName(node)}}
@@ -595,6 +593,11 @@ found in the LICENSE file.
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.workingGUI[setting.id]"></input>
</label>
</div>
<div ng-if="setting.type == 'apikey'">
<label>{{setting.descr}} (<a href="http://discourse.syncthing.net/t/v0-8-14-api-keys/335">Usage</a>)</label>
<div class="well well-sm text-monospace">{{config.workingGUI[setting.id] || "-"}}</div>
<button type="button" class="btn btn-sm btn-default" ng-click="setAPIKey(config.workingGUI)">Generate</button>
</div>
</div>
</div>
</div>

View File

@@ -12,3 +12,5 @@ json
*.idx.gz
dirs-*
*.out
csrftokens.txt
s4d

View File

@@ -1,27 +1,32 @@
<configuration version="1">
<repository directory="s1" ro="true">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="f1">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="f2">
<address>127.0.0.1:22002</address>
</node>
<configuration version="2">
<repository id="default" directory="s1" ro="true" ignorePerms="false">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
<versioning></versioning>
<syncorder></syncorder>
</repository>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="f1">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="f2">
<address>127.0.0.1:22002</address>
</node>
<gui enabled="true" tls="false">
<address>127.0.0.1:8081</address>
<apikey>abc123</apikey>
</gui>
<options>
<listenAddress>127.0.0.1:22001</listenAddress>
<readOnly>true</readOnly>
<allowDelete>true</allowDelete>
<followSymlinks>true</followSymlinks>
<guiEnabled>true</guiEnabled>
<guiAddress>127.0.0.1:8081</guiAddress>
<globalAnnounceServer>announce.syncthing.net:22025</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
<parallelRequests>16</parallelRequests>
<maxSendKbps>0</maxSendKbps>
<rescanIntervalS>10</rescanIntervalS>
<reconnectionIntervalS>5</reconnectionIntervalS>
<maxChangeKbps>10000</maxChangeKbps>
<startBrowser>false</startBrowser>
<upnpEnabled>true</upnpEnabled>
</options>
</configuration>

View File

@@ -1,27 +1,32 @@
<configuration version="1">
<repository directory="s2">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="f1">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="f2">
<address>127.0.0.1:22002</address>
</node>
<configuration version="2">
<repository id="default" directory="s2" ro="false" ignorePerms="false">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
<versioning></versioning>
<syncorder></syncorder>
</repository>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="f1">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="f2">
<address>127.0.0.1:22002</address>
</node>
<gui enabled="true" tls="false">
<address>127.0.0.1:8082</address>
<apikey>abc123</apikey>
</gui>
<options>
<listenAddress>127.0.0.1:22002</listenAddress>
<readOnly>false</readOnly>
<allowDelete>true</allowDelete>
<followSymlinks>true</followSymlinks>
<guiEnabled>true</guiEnabled>
<guiAddress>127.0.0.1:8082</guiAddress>
<globalAnnounceServer>announce.syncthing.net:22025</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
<parallelRequests>16</parallelRequests>
<maxSendKbps>0</maxSendKbps>
<rescanIntervalS>15</rescanIntervalS>
<reconnectionIntervalS>5</reconnectionIntervalS>
<maxChangeKbps>10000</maxChangeKbps>
<startBrowser>false</startBrowser>
<upnpEnabled>true</upnpEnabled>
</options>
</configuration>

View File

@@ -20,7 +20,7 @@ start() {
stop() {
echo "Stopping..."
for i in 1 2 ; do
curl -X POST "http://localhost:808$i/rest/shutdown"
curl -HX-API-Key:abc123 -X POST "http://localhost:808$i/rest/shutdown"
done
}
@@ -39,8 +39,8 @@ setup() {
testConvergence() {
while true ; do
sleep 5
s1comp=$(curl -s "http://localhost:8082/rest/connections" | ./json "$id1/Completion")
s2comp=$(curl -s "http://localhost:8081/rest/connections" | ./json "$id2/Completion")
s1comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8082/rest/connections" | ./json "$id1/Completion")
s2comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8081/rest/connections" | ./json "$id2/Completion")
s1comp=${s1comp:-0}
s2comp=${s2comp:-0}
tot=$(($s1comp + $s2comp))

View File

@@ -1,41 +1,43 @@
<configuration version="1">
<repository directory="s1">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2">
<address>127.0.0.1:22002</address>
</node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3">
<address>127.0.0.1:22003</address>
</node>
<node id="EJHMPAQOGCVORISB4IS3SYYVJXTKJGLTU66DIQPGJ5D2GXGQ3OWQ" name="s4">
<address>127.0.0.1:22004</address>
</node>
<configuration version="2">
<repository id="default" directory="s1" ro="false" ignorePerms="false">
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA"></node>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
<versioning></versioning>
</repository>
<repository id="s12" directory="s12-1">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2">
<address>127.0.0.1:22002</address>
</node>
<repository id="s12" directory="s12-1" ro="false" ignorePerms="false">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
<versioning></versioning>
</repository>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2">
<address>127.0.0.1:22002</address>
</node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3">
<address>127.0.0.1:22003</address>
</node>
<node id="EJHMPAQOGCVORISB4IS3SYYVJXTKJGLTU66DIQPGJ5D2GXGQ3OWQ" name="s4">
<address>127.0.0.1:22004</address>
</node>
<gui enabled="true" tls="false">
<address>127.0.0.1:8081</address>
<apikey>abc123</apikey>
</gui>
<options>
<listenAddress>127.0.0.1:22001</listenAddress>
<readOnly>false</readOnly>
<allowDelete>true</allowDelete>
<followSymlinks>true</followSymlinks>
<guiEnabled>true</guiEnabled>
<guiAddress>127.0.0.1:8081</guiAddress>
<globalAnnounceServer>announce.syncthing.net:22025</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
<parallelRequests>16</parallelRequests>
<maxSendKbps>0</maxSendKbps>
<rescanIntervalS>10</rescanIntervalS>
<reconnectionIntervalS>5</reconnectionIntervalS>
<maxChangeKbps>10000</maxChangeKbps>
<startBrowser>false</startBrowser>
<upnpEnabled>true</upnpEnabled>
</options>
</configuration>

View File

@@ -1,46 +1,45 @@
<configuration version="1">
<repository directory="s2">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2">
<address>127.0.0.1:22002</address>
</node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3">
<address>127.0.0.1:22003</address>
</node>
<configuration version="2">
<repository id="default" directory="s2" ro="false" ignorePerms="false">
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA"></node>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
<versioning></versioning>
</repository>
<repository id="s12" directory="s12-2">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2">
<address>127.0.0.1:22002</address>
</node>
<repository id="s12" directory="s12-2" ro="false" ignorePerms="false">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
<versioning></versioning>
</repository>
<repository id="s23" directory="s23-2">
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2">
<address>127.0.0.1:22002</address>
</node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3">
<address>127.0.0.1:22003</address>
</node>
<repository id="s23" directory="s23-2" ro="false" ignorePerms="false">
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
<versioning></versioning>
</repository>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2">
<address>127.0.0.1:22002</address>
</node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3">
<address>127.0.0.1:22003</address>
</node>
<gui enabled="true" tls="false">
<address>127.0.0.1:8082</address>
<apikey>abc123</apikey>
</gui>
<options>
<listenAddress>127.0.0.1:22002</listenAddress>
<readOnly>false</readOnly>
<allowDelete>true</allowDelete>
<followSymlinks>true</followSymlinks>
<guiEnabled>true</guiEnabled>
<guiAddress>127.0.0.1:8082</guiAddress>
<globalAnnounceServer>announce.syncthing.net:22025</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
<parallelRequests>16</parallelRequests>
<maxSendKbps>0</maxSendKbps>
<rescanIntervalS>15</rescanIntervalS>
<reconnectionIntervalS>5</reconnectionIntervalS>
<maxChangeKbps>10000</maxChangeKbps>
<startBrowser>false</startBrowser>
<upnpEnabled>true</upnpEnabled>
</options>
</configuration>

View File

@@ -1,38 +1,40 @@
<configuration version="1">
<repository directory="s3">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2">
<address>127.0.0.1:22002</address>
</node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3">
<address>127.0.0.1:22003</address>
</node>
<configuration version="2">
<repository id="default" directory="s3" ro="false" ignorePerms="false">
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA"></node>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
<versioning></versioning>
</repository>
<repository id="s23" directory="s23-3">
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2">
<address>127.0.0.1:22002</address>
</node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3">
<address>127.0.0.1:22003</address>
</node>
<repository id="s23" directory="s23-3" ro="false" ignorePerms="false">
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
<versioning></versioning>
</repository>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2">
<address>127.0.0.1:22002</address>
</node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3">
<address>127.0.0.1:22003</address>
</node>
<gui enabled="true" tls="false">
<address>127.0.0.1:8083</address>
<apikey>abc123</apikey>
</gui>
<options>
<listenAddress>127.0.0.1:22003</listenAddress>
<readOnly>false</readOnly>
<allowDelete>true</allowDelete>
<followSymlinks>true</followSymlinks>
<guiEnabled>true</guiEnabled>
<guiAddress>127.0.0.1:8083</guiAddress>
<globalAnnounceServer>announce.syncthing.net:22025</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
<parallelRequests>16</parallelRequests>
<maxSendKbps>0</maxSendKbps>
<rescanIntervalS>20</rescanIntervalS>
<reconnectionIntervalS>5</reconnectionIntervalS>
<maxChangeKbps>10000</maxChangeKbps>
<startBrowser>false</startBrowser>
<upnpEnabled>true</upnpEnabled>
</options>
</configuration>

View File

@@ -5,6 +5,9 @@
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3"></node>
<node id="EJHMPAQOGCVORISB4IS3SYYVJXTKJGLTU66DIQPGJ5D2GXGQ3OWQ" name="s4"></node>
</repository>
<repository id="default" directory="s4d" ro="false">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1"></node>
</repository>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1">
<address>127.0.0.1:22001</address>
</node>
@@ -19,6 +22,7 @@
</node>
<gui enabled="true">
<address>127.0.0.1:8084</address>
<apikey>abc123</apikey>
</gui>
<options>
<listenAddress>:22004</listenAddress>

View File

@@ -24,9 +24,9 @@ start() {
testConvergence() {
while true ; do
sleep 5
s1comp=$(curl -s "http://localhost:8082/rest/connections" | ./json "$id1/Completion")
s2comp=$(curl -s "http://localhost:8083/rest/connections" | ./json "$id2/Completion")
s3comp=$(curl -s "http://localhost:8081/rest/connections" | ./json "$id3/Completion")
s1comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8082/rest/connections" | ./json "$id1/Completion")
s2comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8083/rest/connections" | ./json "$id2/Completion")
s3comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8081/rest/connections" | ./json "$id3/Completion")
s1comp=${s1comp:-0}
s2comp=${s2comp:-0}
s3comp=${s3comp:-0}
@@ -105,10 +105,11 @@ alterFiles() {
pkill -CONT syncthing
}
rm -f h?/*.idx.gz
rm -rf s? s??-? s4d
echo "Setting up files..."
for i in 1 2 3 12-1 12-2 23-2 23-3; do
rm -f h$i/*.idx.gz
rm -rf "s$i"
mkdir "s$i"
pushd "s$i" >/dev/null
echo " $i: random nonoverlapping"
@@ -117,9 +118,17 @@ for i in 1 2 3 12-1 12-2 23-2 23-3; do
touch "empty-$i"
echo " $i: large file"
dd if=/dev/urandom of=large-$i bs=1024k count=55 2>/dev/null
echo " $i: weird encodings"
echo somedata > "$(echo -e utf8-nfc-\\xc3\\xad)-$i"
echo somedata > "$(echo -e utf8-nfd-i\\xcc\\x81)-$i"
echo somedata > "$(echo -e cp850-\\xa1)-$i"
touch "empty-$i"
popd >/dev/null
done
mkdir s4d
echo somerandomdata > s4d/extrafile
echo "MD5-summing..."
for i in 1 2 3 12-1 12-2 23-2 23-3 ; do
pushd "s$i" >/dev/null
@@ -140,5 +149,5 @@ for ((t = 1; t <= $iterations; t++)) ; do
done
for i in 1 2 3 4 ; do
curl -X POST "http://localhost:808$i/rest/shutdown"
curl -HX-API-Key:abc123 -X POST "http://localhost:808$i/rest/shutdown"
done

View File

@@ -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
}
@@ -260,6 +264,11 @@ func (m *Model) Index(nodeID string, repo string, fs []protocol.FileInfo) {
l.Debugf("IDX(in): %s %q: %d files", nodeID, repo, len(fs))
}
if !m.repoSharedWith(repo, nodeID) {
l.Warnf("Unexpected repository ID %q sent from node %q; ensure that the repository exists and that this node is selected under \"Share With\" in the repository configuration.", repo, nodeID)
return
}
var files = make([]scanner.File, len(fs))
for i := range fs {
f := fs[i]
@@ -279,8 +288,7 @@ func (m *Model) Index(nodeID string, repo string, fs []protocol.FileInfo) {
if r, ok := m.repoFiles[repo]; ok {
r.Replace(id, files)
} else {
l.Warnf("Index from %s for unexpected repo %q; verify configuration", nodeID, repo)
l.Fatalf("Index for nonexistant repo %q", repo)
}
m.rmut.RUnlock()
}
@@ -292,6 +300,11 @@ func (m *Model) IndexUpdate(nodeID string, repo string, fs []protocol.FileInfo)
l.Debugf("IDXUP(in): %s / %q: %d files", nodeID, repo, len(fs))
}
if !m.repoSharedWith(repo, nodeID) {
l.Warnf("Unexpected repository ID %q sent from node %q; ensure that the repository exists and that this node is selected under \"Share With\" in the repository configuration.", repo, nodeID)
return
}
var files = make([]scanner.File, len(fs))
for i := range fs {
f := fs[i]
@@ -311,11 +324,22 @@ func (m *Model) IndexUpdate(nodeID string, repo string, fs []protocol.FileInfo)
if r, ok := m.repoFiles[repo]; ok {
r.Update(id, files)
} else {
l.Warnf("Index update from %s for nonexistant repo %q; dropping", nodeID, repo)
l.Fatalf("IndexUpdate for nonexistant repo %q", repo)
}
m.rmut.RUnlock()
}
func (m *Model) repoSharedWith(repo, nodeID string) bool {
m.rmut.RLock()
defer m.rmut.RUnlock()
for _, nrepo := range m.nodeRepos[nodeID] {
if nrepo == repo {
return true
}
}
return false
}
func (m *Model) ClusterConfig(nodeID string, config protocol.ClusterConfigMessage) {
compErr := compareClusterConfig(m.clusterConfig(nodeID), config)
if debug {
@@ -716,11 +740,12 @@ func (m *Model) saveIndex(repo string, dir string, fs []protocol.FileInfo) error
id := fmt.Sprintf("%x", sha1.Sum([]byte(m.repoCfgs[repo].Directory)))
name := id + ".idx.gz"
name = filepath.Join(dir, name)
idxf, err := os.Create(name + ".tmp")
tmp := fmt.Sprintf("%s.tmp.%d", name, time.Now().UnixNano())
idxf, err := os.OpenFile(tmp, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0644)
if err != nil {
return err
}
defer os.Remove(tmp)
gzw := gzip.NewWriter(idxf)
@@ -748,7 +773,7 @@ func (m *Model) saveIndex(repo string, dir string, fs []protocol.FileInfo) error
l.Debugln("wrote index,", n, "bytes uncompressed")
}
return osutil.Rename(name+".tmp", name)
return osutil.Rename(tmp, name)
}
func (m *Model) loadIndex(repo string, dir string) []protocol.FileInfo {