mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-05 04:19:10 -05:00
Compare commits
16 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
07d49b61d0 | ||
|
|
0c4e6ae7de | ||
|
|
65ec129dfb | ||
|
|
3e4d628f54 | ||
|
|
71684bfa45 | ||
|
|
e73b7e0398 | ||
|
|
35ebdc76ff | ||
|
|
90d0896848 | ||
|
|
5528db9693 | ||
|
|
aa78fbb09d | ||
|
|
d53b193e09 | ||
|
|
e0e16c371f | ||
|
|
53cd877899 | ||
|
|
1207223f3d | ||
|
|
39be6932b5 | ||
|
|
44a194d226 |
File diff suppressed because one or more lines are too long
3
build.sh
3
build.sh
@@ -6,7 +6,8 @@ distFiles=(README.md LICENSE) # apart from the binary itself
|
||||
version=$(git describe --always --dirty)
|
||||
date=$(date +%s)
|
||||
user=$(whoami)
|
||||
host=$(hostname -s)
|
||||
host=$(hostname)
|
||||
host=${host%%.*}
|
||||
ldflags="-w -X main.Version $version -X main.BuildStamp $date -X main.BuildUser $user -X main.BuildHost $host"
|
||||
|
||||
build() {
|
||||
|
||||
52
cmd/stpidx/main.go
Normal file
52
cmd/stpidx/main.go
Normal file
@@ -0,0 +1,52 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"compress/gzip"
|
||||
"flag"
|
||||
"log"
|
||||
"os"
|
||||
|
||||
"github.com/calmh/syncthing/protocol"
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(0)
|
||||
log.SetOutput(os.Stdout)
|
||||
|
||||
showBlocks := flag.Bool("b", false, "Show blocks")
|
||||
flag.Parse()
|
||||
name := flag.Arg(0)
|
||||
|
||||
idxf, err := os.Open(name)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer idxf.Close()
|
||||
|
||||
gzr, err := gzip.NewReader(idxf)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
defer gzr.Close()
|
||||
|
||||
var im protocol.IndexMessage
|
||||
err = im.DecodeXDR(gzr)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
log.Printf("Repo: %q, Files: %d", im.Repository, len(im.Files))
|
||||
for _, file := range im.Files {
|
||||
del := file.Flags&protocol.FlagDeleted != 0
|
||||
inv := file.Flags&protocol.FlagInvalid != 0
|
||||
dir := file.Flags&protocol.FlagDirectory != 0
|
||||
prm := file.Flags & 0777
|
||||
log.Printf("File: %q, Del: %v, Inv: %v, Dir: %v, Perm: 0%03o, Modified: %d, Blocks: %d",
|
||||
file.Name, del, inv, dir, prm, file.Modified, len(file.Blocks))
|
||||
if *showBlocks {
|
||||
for _, block := range file.Blocks {
|
||||
log.Printf(" Size: %6d, Hash: %x", block.Size, block.Hash)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"encoding/xml"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"sort"
|
||||
"strconv"
|
||||
@@ -153,7 +154,7 @@ func uniqueStrings(ss []string) []string {
|
||||
return us
|
||||
}
|
||||
|
||||
func readConfigXML(rd io.Reader) (Configuration, error) {
|
||||
func readConfigXML(rd io.Reader, myID string) (Configuration, error) {
|
||||
var cfg Configuration
|
||||
|
||||
setDefaults(&cfg)
|
||||
@@ -169,6 +170,7 @@ func readConfigXML(rd io.Reader) (Configuration, error) {
|
||||
|
||||
cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
|
||||
|
||||
// Check for missing or duplicate repository ID:s
|
||||
var seenRepos = map[string]bool{}
|
||||
for i := range cfg.Repositories {
|
||||
if cfg.Repositories[i].ID == "" {
|
||||
@@ -182,10 +184,12 @@ func readConfigXML(rd io.Reader) (Configuration, error) {
|
||||
seenRepos[id] = true
|
||||
}
|
||||
|
||||
// Upgrade to v2 configuration if appropriate
|
||||
if cfg.Version == 1 {
|
||||
convertV1V2(&cfg)
|
||||
}
|
||||
|
||||
// Hash old cleartext passwords
|
||||
if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(cfg.GUI.Password), 0)
|
||||
if err != nil {
|
||||
@@ -195,6 +199,20 @@ func readConfigXML(rd io.Reader) (Configuration, error) {
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure this node is present in all relevant places
|
||||
cfg.Nodes = ensureNodePresent(cfg.Nodes, myID)
|
||||
for i := range cfg.Repositories {
|
||||
cfg.Repositories[i].Nodes = ensureNodePresent(cfg.Repositories[i].Nodes, myID)
|
||||
}
|
||||
|
||||
// An empty address list is equivalent to a single "dynamic" entry
|
||||
for i := range cfg.Nodes {
|
||||
n := &cfg.Nodes[i]
|
||||
if len(n.Addresses) == 0 || len(n.Addresses) == 1 && n.Addresses[0] == "" {
|
||||
n.Addresses = []string{"dynamic"}
|
||||
}
|
||||
}
|
||||
|
||||
return cfg, err
|
||||
}
|
||||
|
||||
@@ -241,7 +259,7 @@ func (l NodeConfigurationList) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func cleanNodeList(nodes []NodeConfiguration, myID string) []NodeConfiguration {
|
||||
func ensureNodePresent(nodes []NodeConfiguration, myID string) []NodeConfiguration {
|
||||
var myIDExists bool
|
||||
for _, node := range nodes {
|
||||
if node.NodeID == myID {
|
||||
@@ -251,10 +269,10 @@ func cleanNodeList(nodes []NodeConfiguration, myID string) []NodeConfiguration {
|
||||
}
|
||||
|
||||
if !myIDExists {
|
||||
name, _ := os.Hostname()
|
||||
nodes = append(nodes, NodeConfiguration{
|
||||
NodeID: myID,
|
||||
Addresses: []string{"dynamic"},
|
||||
Name: "",
|
||||
NodeID: myID,
|
||||
Name: name,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ package main
|
||||
import (
|
||||
"bytes"
|
||||
"io"
|
||||
"os"
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
@@ -22,7 +23,7 @@ func TestDefaultValues(t *testing.T) {
|
||||
UPnPEnabled: true,
|
||||
}
|
||||
|
||||
cfg, err := readConfigXML(bytes.NewReader(nil))
|
||||
cfg, err := readConfigXML(bytes.NewReader(nil), "nodeID")
|
||||
if err != io.EOF {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -65,7 +66,7 @@ func TestNodeConfig(t *testing.T) {
|
||||
`)
|
||||
|
||||
for i, data := range [][]byte{v1data, v2data} {
|
||||
cfg, err := readConfigXML(bytes.NewReader(data))
|
||||
cfg, err := readConfigXML(bytes.NewReader(data), "node1")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -120,7 +121,7 @@ func TestNoListenAddress(t *testing.T) {
|
||||
</configuration>
|
||||
`)
|
||||
|
||||
cfg, err := readConfigXML(bytes.NewReader(data))
|
||||
cfg, err := readConfigXML(bytes.NewReader(data), "nodeID")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -169,7 +170,7 @@ func TestOverriddenValues(t *testing.T) {
|
||||
UPnPEnabled: false,
|
||||
}
|
||||
|
||||
cfg, err := readConfigXML(bytes.NewReader(data))
|
||||
cfg, err := readConfigXML(bytes.NewReader(data), "nodeID")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -178,3 +179,48 @@ func TestOverriddenValues(t *testing.T) {
|
||||
t.Errorf("Overridden config differs;\n E: %#v\n A: %#v", expected, cfg.Options)
|
||||
}
|
||||
}
|
||||
|
||||
func TestNodeAddresses(t *testing.T) {
|
||||
data := []byte(`
|
||||
<configuration version="2">
|
||||
<node id="n1">
|
||||
<address>dynamic</address>
|
||||
</node>
|
||||
<node id="n2">
|
||||
<address></address>
|
||||
</node>
|
||||
<node id="n3">
|
||||
</node>
|
||||
</configuration>
|
||||
`)
|
||||
|
||||
name, _ := os.Hostname()
|
||||
expected := []NodeConfiguration{
|
||||
{
|
||||
NodeID: "n1",
|
||||
Addresses: []string{"dynamic"},
|
||||
},
|
||||
{
|
||||
NodeID: "n2",
|
||||
Addresses: []string{"dynamic"},
|
||||
},
|
||||
{
|
||||
NodeID: "n3",
|
||||
Addresses: []string{"dynamic"},
|
||||
},
|
||||
{
|
||||
NodeID: "n4",
|
||||
Name: name, // Set when auto created
|
||||
Addresses: []string{"dynamic"},
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := readConfigXML(bytes.NewReader(data), "n4")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
if !reflect.DeepEqual(cfg.Nodes, expected) {
|
||||
t.Errorf("Nodes differ;\n E: %#v\n A: %#v", expected, cfg.Nodes)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,11 +35,10 @@ func startGUI(cfg GUIConfiguration, m *Model) {
|
||||
router := martini.NewRouter()
|
||||
router.Get("/", getRoot)
|
||||
router.Get("/rest/version", restGetVersion)
|
||||
router.Get("/rest/model/:repo", restGetModel)
|
||||
router.Get("/rest/model", restGetModel)
|
||||
router.Get("/rest/connections", restGetConnections)
|
||||
router.Get("/rest/config", restGetConfig)
|
||||
router.Get("/rest/config/sync", restGetConfigInSync)
|
||||
router.Get("/rest/need/:repo", restGetNeed)
|
||||
router.Get("/rest/system", restGetSystem)
|
||||
router.Get("/rest/errors", restGetErrors)
|
||||
|
||||
@@ -80,8 +79,9 @@ func restGetVersion() string {
|
||||
return Version
|
||||
}
|
||||
|
||||
func restGetModel(m *Model, w http.ResponseWriter, params martini.Params) {
|
||||
var repo = params["repo"]
|
||||
func restGetModel(m *Model, w http.ResponseWriter, r *http.Request) {
|
||||
var qs = r.URL.Query()
|
||||
var repo = qs.Get("repo")
|
||||
var res = make(map[string]interface{})
|
||||
|
||||
globalFiles, globalDeleted, globalBytes := m.GlobalSize(repo)
|
||||
@@ -168,17 +168,6 @@ func (f guiFile) MarshalJSON() ([]byte, error) {
|
||||
})
|
||||
}
|
||||
|
||||
func restGetNeed(m *Model, w http.ResponseWriter, params martini.Params) {
|
||||
repo := params["repo"]
|
||||
files := m.NeedFilesRepo(repo)
|
||||
gfs := make([]guiFile, len(files))
|
||||
for i, f := range files {
|
||||
gfs[i] = guiFile(f)
|
||||
}
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(gfs)
|
||||
}
|
||||
|
||||
var cpuUsagePercent [10]float64 // The last ten seconds
|
||||
var cpuUsageLock sync.RWMutex
|
||||
|
||||
|
||||
@@ -109,6 +109,22 @@ func main() {
|
||||
|
||||
confDir = expandTilde(confDir)
|
||||
|
||||
if _, err := os.Stat(confDir); err != nil && confDir == getDefaultConfDir() {
|
||||
// We are supposed to use the default configuration directory. It
|
||||
// doesn't exist. In the past our default has been ~/.syncthing, so if
|
||||
// that directory exists we move it to the new default location and
|
||||
// continue. We don't much care if this fails at this point, we will
|
||||
// be checking that later.
|
||||
|
||||
oldDefault := expandTilde("~/.syncthing")
|
||||
if _, err := os.Stat(oldDefault); err == nil {
|
||||
os.MkdirAll(filepath.Dir(confDir), 0700)
|
||||
if err := os.Rename(oldDefault, confDir); err == nil {
|
||||
infoln("Moved config dir", oldDefault, "to", confDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that our home directory exists and that we have a certificate and key.
|
||||
|
||||
ensureDir(confDir, 0700)
|
||||
@@ -137,22 +153,22 @@ func main() {
|
||||
cf, err := os.Open(cfgFile)
|
||||
if err == nil {
|
||||
// Read config.xml
|
||||
cfg, err = readConfigXML(cf)
|
||||
cfg, err = readConfigXML(cf, myID)
|
||||
if err != nil {
|
||||
fatalln(err)
|
||||
}
|
||||
cf.Close()
|
||||
}
|
||||
|
||||
if len(cfg.Repositories) == 0 {
|
||||
} else {
|
||||
infoln("No config file; starting with empty defaults")
|
||||
name, _ := os.Hostname()
|
||||
defaultRepo := filepath.Join(getHomeDir(), "Sync")
|
||||
ensureDir(defaultRepo, 0755)
|
||||
|
||||
cfg, err = readConfigXML(nil)
|
||||
cfg, err = readConfigXML(nil, myID)
|
||||
cfg.Repositories = []RepositoryConfiguration{
|
||||
{
|
||||
ID: "default",
|
||||
Directory: filepath.Join(getHomeDir(), "Sync"),
|
||||
Directory: defaultRepo,
|
||||
Nodes: []NodeConfiguration{{NodeID: myID}},
|
||||
},
|
||||
}
|
||||
@@ -205,11 +221,9 @@ func main() {
|
||||
|
||||
m := NewModel(cfg.Options.MaxChangeKbps * 1000)
|
||||
|
||||
for i := range cfg.Repositories {
|
||||
cfg.Repositories[i].Nodes = cleanNodeList(cfg.Repositories[i].Nodes, myID)
|
||||
dir := expandTilde(cfg.Repositories[i].Directory)
|
||||
ensureDir(dir, -1)
|
||||
m.AddRepo(cfg.Repositories[i].ID, dir, cfg.Repositories[i].Nodes)
|
||||
for _, repo := range cfg.Repositories {
|
||||
dir := expandTilde(repo.Directory)
|
||||
m.AddRepo(repo.ID, dir, repo.Nodes)
|
||||
}
|
||||
|
||||
// GUI
|
||||
@@ -244,6 +258,25 @@ func main() {
|
||||
|
||||
infoln("Populating repository index")
|
||||
m.LoadIndexes(confDir)
|
||||
|
||||
for _, repo := range cfg.Repositories {
|
||||
dir := expandTilde(repo.Directory)
|
||||
|
||||
// Safety check. If the cached index contains files but the repository
|
||||
// doesn't exist, we have a problem. We would assume that all files
|
||||
// have been deleted which might not be the case, so abort instead.
|
||||
|
||||
if files, _, _ := m.LocalSize(repo.ID); files > 0 {
|
||||
if fi, err := os.Stat(dir); err != nil || !fi.IsDir() {
|
||||
warnf("Configured repository %q has index but directory %q is missing; not starting.", repo.ID, repo.Directory)
|
||||
fatalf("Ensure that directory is present or remove repository from configuration.")
|
||||
}
|
||||
}
|
||||
|
||||
// Ensure that repository directories exist for newly configured repositories.
|
||||
ensureDir(dir, -1)
|
||||
}
|
||||
|
||||
m.ScanRepos()
|
||||
m.SaveIndexes(confDir)
|
||||
|
||||
@@ -561,39 +594,47 @@ func ensureDir(dir string, mode int) {
|
||||
}
|
||||
}
|
||||
|
||||
func getDefaultConfDir() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return filepath.Join(os.Getenv("AppData"), "Syncthing")
|
||||
|
||||
case "darwin":
|
||||
return expandTilde("~/Library/Application Support/Syncthing")
|
||||
|
||||
default:
|
||||
if xdgCfg := os.Getenv("XDG_CONFIG_HOME"); xdgCfg != "" {
|
||||
return filepath.Join(xdgCfg, "syncthing")
|
||||
} else {
|
||||
return expandTilde("~/.config/syncthing")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func expandTilde(p string) string {
|
||||
if runtime.GOOS == "windows" {
|
||||
if runtime.GOOS == "windows" || !strings.HasPrefix(p, "~/") {
|
||||
return p
|
||||
}
|
||||
|
||||
if strings.HasPrefix(p, "~/") {
|
||||
return strings.Replace(p, "~", getUnixHomeDir(), 1)
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func getUnixHomeDir() string {
|
||||
home := os.Getenv("HOME")
|
||||
if home == "" {
|
||||
fatalln("No home directory?")
|
||||
}
|
||||
return home
|
||||
return filepath.Join(getHomeDir(), p[2:])
|
||||
}
|
||||
|
||||
func getHomeDir() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
home := os.Getenv("HOMEDRIVE") + os.Getenv("HOMEPATH")
|
||||
if home == "" {
|
||||
home = os.Getenv("USERPROFILE")
|
||||
}
|
||||
return home
|
||||
}
|
||||
return getUnixHomeDir()
|
||||
}
|
||||
var home string
|
||||
|
||||
func getDefaultConfDir() string {
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(os.Getenv("AppData"), "syncthing")
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
home = filepath.Join(os.Getenv("HomeDrive"), os.Getenv("HomePath"))
|
||||
if home == "" {
|
||||
home = os.Getenv("UserProfile")
|
||||
}
|
||||
default:
|
||||
home = os.Getenv("HOME")
|
||||
}
|
||||
return expandTilde("~/.syncthing")
|
||||
|
||||
if home == "" {
|
||||
fatalln("No home directory found - set $HOME (or the platform equivalent).")
|
||||
}
|
||||
|
||||
return home
|
||||
}
|
||||
|
||||
@@ -313,6 +313,7 @@ func (p *puller) handleRequestResult(res requestResult) {
|
||||
t := time.Unix(f.Modified, 0)
|
||||
os.Chtimes(of.temp, t, t)
|
||||
os.Chmod(of.temp, os.FileMode(f.Flags&0777))
|
||||
defTempNamer.Show(of.temp)
|
||||
if debugPull {
|
||||
dlog.Printf("pull: rename %q / %q: %q", p.repo, f.Name, of.filepath)
|
||||
}
|
||||
@@ -371,6 +372,7 @@ func (p *puller) handleBlock(b bqBlock) {
|
||||
p.requestSlots <- true
|
||||
return
|
||||
}
|
||||
defTempNamer.Hide(of.temp)
|
||||
}
|
||||
|
||||
if of.err != nil {
|
||||
@@ -514,6 +516,7 @@ func (p *puller) handleEmptyBlock(b bqBlock) {
|
||||
t := time.Unix(f.Modified, 0)
|
||||
os.Chtimes(of.temp, t, t)
|
||||
os.Chmod(of.temp, os.FileMode(f.Flags&0777))
|
||||
defTempNamer.Show(of.temp)
|
||||
Rename(of.temp, of.filepath)
|
||||
}
|
||||
delete(p.openFiles, f.Name)
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -21,3 +23,11 @@ func (t tempNamer) TempName(name string) string {
|
||||
tname := fmt.Sprintf("%s.%s", t.prefix, filepath.Base(name))
|
||||
return filepath.Join(tdir, tname)
|
||||
}
|
||||
|
||||
func (t tempNamer) Hide(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t tempNamer) Show(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
56
cmd/syncthing/tempname_windows.go
Normal file
56
cmd/syncthing/tempname_windows.go
Normal file
@@ -0,0 +1,56 @@
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type tempNamer struct {
|
||||
prefix string
|
||||
}
|
||||
|
||||
var defTempNamer = tempNamer{"~syncthing~"}
|
||||
|
||||
func (t tempNamer) IsTemporary(name string) bool {
|
||||
return strings.HasPrefix(filepath.Base(name), t.prefix)
|
||||
}
|
||||
|
||||
func (t tempNamer) TempName(name string) string {
|
||||
tdir := filepath.Dir(name)
|
||||
tname := fmt.Sprintf("%s.%s.tmp", t.prefix, filepath.Base(name))
|
||||
return filepath.Join(tdir, tname)
|
||||
}
|
||||
|
||||
func (t tempNamer) Hide(path string) error {
|
||||
p, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs, err := syscall.GetFileAttributes(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs |= syscall.FILE_ATTRIBUTE_HIDDEN
|
||||
return syscall.SetFileAttributes(p, attrs)
|
||||
}
|
||||
|
||||
func (t tempNamer) Show(path string) error {
|
||||
p, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs, err := syscall.GetFileAttributes(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs &^= syscall.FILE_ATTRIBUTE_HIDDEN
|
||||
return syscall.SetFileAttributes(p, attrs)
|
||||
}
|
||||
@@ -117,8 +117,6 @@ func compareClusterConfig(local, remote protocol.ClusterConfigMessage) error {
|
||||
if lflags&protocol.FlagShareBits != rflags&protocol.FlagShareBits {
|
||||
return ClusterConfigMismatch(fmt.Errorf("remote has different sharing flags for node %q in repository %q", node, repo))
|
||||
}
|
||||
} else {
|
||||
return ClusterConfigMismatch(fmt.Errorf("remote is missing node %q in repository %q", node, repo))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
@@ -126,14 +124,8 @@ func compareClusterConfig(local, remote protocol.ClusterConfigMessage) error {
|
||||
}
|
||||
}
|
||||
|
||||
for repo, rnodes := range rm {
|
||||
if lnodes, ok := lm[repo]; ok {
|
||||
for node := range rnodes {
|
||||
if _, ok := lnodes[node]; !ok {
|
||||
return ClusterConfigMismatch(fmt.Errorf("remote has extra node %q in repository %q", node, repo))
|
||||
}
|
||||
}
|
||||
} else {
|
||||
for repo := range rm {
|
||||
if _, ok := lm[repo]; !ok {
|
||||
return ClusterConfigMismatch(fmt.Errorf("remote has extra repository %q", repo))
|
||||
}
|
||||
|
||||
|
||||
@@ -103,7 +103,7 @@ var testcases = []struct {
|
||||
{ID: "bar"},
|
||||
},
|
||||
},
|
||||
err: `remote is missing node "a" in repository "foo"`,
|
||||
err: "",
|
||||
},
|
||||
|
||||
{
|
||||
@@ -130,7 +130,7 @@ var testcases = []struct {
|
||||
{ID: "bar"},
|
||||
},
|
||||
},
|
||||
err: `remote has extra node "b" in repository "foo"`,
|
||||
err: "",
|
||||
},
|
||||
|
||||
{
|
||||
|
||||
@@ -4,6 +4,7 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
@@ -26,22 +27,28 @@ type Address struct {
|
||||
}
|
||||
|
||||
var (
|
||||
nodes = make(map[string]Node)
|
||||
lock sync.Mutex
|
||||
queries = 0
|
||||
answered = 0
|
||||
limited = 0
|
||||
debug = false
|
||||
limiter = lru.New(1024)
|
||||
nodes = make(map[string]Node)
|
||||
lock sync.Mutex
|
||||
queries = 0
|
||||
announces = 0
|
||||
answered = 0
|
||||
limited = 0
|
||||
unknowns = 0
|
||||
debug = false
|
||||
limiter = lru.New(1024)
|
||||
)
|
||||
|
||||
func main() {
|
||||
var listen string
|
||||
var timestamp bool
|
||||
var statsIntv int
|
||||
var statsFile string
|
||||
|
||||
flag.StringVar(&listen, "listen", ":22025", "Listen address")
|
||||
flag.BoolVar(&debug, "debug", false, "Enable debug output")
|
||||
flag.BoolVar(×tamp, "timestamp", true, "Timestamp the log output")
|
||||
flag.IntVar(&statsIntv, "stats-intv", 0, "Statistics output interval (s)")
|
||||
flag.StringVar(&statsFile, "stats-file", "/var/log/discosrv.stats", "Statistics file name")
|
||||
flag.Parse()
|
||||
|
||||
log.SetOutput(os.Stdout)
|
||||
@@ -55,7 +62,9 @@ func main() {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
go logStats()
|
||||
if statsIntv > 0 {
|
||||
go logStats(statsFile, statsIntv)
|
||||
}
|
||||
|
||||
var buf = make([]byte, 1024)
|
||||
for {
|
||||
@@ -80,17 +89,16 @@ func main() {
|
||||
magic := binary.BigEndian.Uint32(buf)
|
||||
|
||||
switch magic {
|
||||
case discover.AnnouncementMagicV1:
|
||||
handleAnnounceV1(addr, buf)
|
||||
|
||||
case discover.QueryMagicV1:
|
||||
handleQueryV1(conn, addr, buf)
|
||||
|
||||
case discover.AnnouncementMagicV2:
|
||||
handleAnnounceV2(addr, buf)
|
||||
|
||||
case discover.QueryMagicV2:
|
||||
handleQueryV2(conn, addr, buf)
|
||||
|
||||
default:
|
||||
lock.Lock()
|
||||
unknowns++
|
||||
lock.Unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -123,75 +131,6 @@ func limit(addr *net.UDPAddr) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func handleAnnounceV1(addr *net.UDPAddr, buf []byte) {
|
||||
var pkt discover.AnnounceV1
|
||||
err := pkt.UnmarshalXDR(buf)
|
||||
if err != nil {
|
||||
log.Println("AnnounceV1 Unmarshal:", err)
|
||||
log.Println(hex.Dump(buf))
|
||||
return
|
||||
}
|
||||
if debug {
|
||||
log.Printf("<- %v %#v", addr, pkt)
|
||||
}
|
||||
|
||||
ip := addr.IP.To4()
|
||||
if ip == nil {
|
||||
ip = addr.IP.To16()
|
||||
}
|
||||
node := Node{
|
||||
Addresses: []Address{{
|
||||
IP: ip,
|
||||
Port: pkt.Port,
|
||||
}},
|
||||
Updated: time.Now(),
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
nodes[pkt.NodeID] = node
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
func handleQueryV1(conn *net.UDPConn, addr *net.UDPAddr, buf []byte) {
|
||||
var pkt discover.QueryV1
|
||||
err := pkt.UnmarshalXDR(buf)
|
||||
if err != nil {
|
||||
log.Println("QueryV1 Unmarshal:", err)
|
||||
log.Println(hex.Dump(buf))
|
||||
return
|
||||
}
|
||||
if debug {
|
||||
log.Printf("<- %v %#v", addr, pkt)
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
node, ok := nodes[pkt.NodeID]
|
||||
queries++
|
||||
lock.Unlock()
|
||||
|
||||
if ok && len(node.Addresses) > 0 {
|
||||
pkt := discover.AnnounceV1{
|
||||
Magic: discover.AnnouncementMagicV1,
|
||||
NodeID: pkt.NodeID,
|
||||
Port: node.Addresses[0].Port,
|
||||
IP: node.Addresses[0].IP,
|
||||
}
|
||||
if debug {
|
||||
log.Printf("-> %v %#v", addr, pkt)
|
||||
}
|
||||
|
||||
tb := pkt.MarshalXDR()
|
||||
_, _, err = conn.WriteMsgUDP(tb, nil, addr)
|
||||
if err != nil {
|
||||
log.Println("QueryV1 response write:", err)
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
answered++
|
||||
lock.Unlock()
|
||||
}
|
||||
}
|
||||
|
||||
func handleAnnounceV2(addr *net.UDPAddr, buf []byte) {
|
||||
var pkt discover.AnnounceV2
|
||||
err := pkt.UnmarshalXDR(buf)
|
||||
@@ -204,6 +143,10 @@ func handleAnnounceV2(addr *net.UDPAddr, buf []byte) {
|
||||
log.Printf("<- %v %#v", addr, pkt)
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
announces++
|
||||
lock.Unlock()
|
||||
|
||||
ip := addr.IP.To4()
|
||||
if ip == nil {
|
||||
ip = addr.IP.To16()
|
||||
@@ -272,9 +215,21 @@ func handleQueryV2(conn *net.UDPConn, addr *net.UDPAddr, buf []byte) {
|
||||
}
|
||||
}
|
||||
|
||||
func logStats() {
|
||||
func next(intv int) time.Time {
|
||||
d := time.Duration(intv) * time.Second
|
||||
t0 := time.Now()
|
||||
t1 := t0.Add(d).Truncate(d)
|
||||
time.Sleep(t1.Sub(t0))
|
||||
return t1
|
||||
}
|
||||
|
||||
func logStats(file string, intv int) {
|
||||
f, err := os.OpenFile(file, os.O_WRONLY|os.O_CREATE|os.O_APPEND, 0644)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
for {
|
||||
time.Sleep(600 * time.Second)
|
||||
t := next(intv)
|
||||
|
||||
lock.Lock()
|
||||
|
||||
@@ -286,11 +241,15 @@ func logStats() {
|
||||
}
|
||||
}
|
||||
|
||||
log.Printf("Expired %d nodes; %d nodes in registry; %d queries (%d answered)", deleted, len(nodes), queries, answered)
|
||||
log.Printf("Limited %d queries; %d entries in limiter cache", limited, limiter.Len())
|
||||
fmt.Fprintf(f, "%d Nr:%d Ne:%d Qt:%d Qa:%d A:%d U:%d Lq:%d Lc:%d\n",
|
||||
t.Unix(), len(nodes), deleted, queries, answered, announces, unknowns, limited, limiter.Len())
|
||||
f.Sync()
|
||||
|
||||
queries = 0
|
||||
announces = 0
|
||||
answered = 0
|
||||
limited = 0
|
||||
unknowns = 0
|
||||
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
@@ -1,22 +1,5 @@
|
||||
package discover
|
||||
|
||||
const (
|
||||
AnnouncementMagicV1 = 0x20121025
|
||||
QueryMagicV1 = 0x19760309
|
||||
)
|
||||
|
||||
type QueryV1 struct {
|
||||
Magic uint32
|
||||
NodeID string // max:64
|
||||
}
|
||||
|
||||
type AnnounceV1 struct {
|
||||
Magic uint32
|
||||
Port uint16
|
||||
NodeID string // max:64
|
||||
IP []byte // max:16
|
||||
}
|
||||
|
||||
const (
|
||||
AnnouncementMagicV2 = 0x029E4C77
|
||||
QueryMagicV2 = 0x23D63A9A
|
||||
|
||||
@@ -7,89 +7,6 @@ import (
|
||||
"github.com/calmh/syncthing/xdr"
|
||||
)
|
||||
|
||||
func (o QueryV1) EncodeXDR(w io.Writer) (int, error) {
|
||||
var xw = xdr.NewWriter(w)
|
||||
return o.encodeXDR(xw)
|
||||
}
|
||||
|
||||
func (o QueryV1) MarshalXDR() []byte {
|
||||
var buf bytes.Buffer
|
||||
var xw = xdr.NewWriter(&buf)
|
||||
o.encodeXDR(xw)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (o QueryV1) encodeXDR(xw *xdr.Writer) (int, error) {
|
||||
xw.WriteUint32(o.Magic)
|
||||
if len(o.NodeID) > 64 {
|
||||
return xw.Tot(), xdr.ErrElementSizeExceeded
|
||||
}
|
||||
xw.WriteString(o.NodeID)
|
||||
return xw.Tot(), xw.Error()
|
||||
}
|
||||
|
||||
func (o *QueryV1) DecodeXDR(r io.Reader) error {
|
||||
xr := xdr.NewReader(r)
|
||||
return o.decodeXDR(xr)
|
||||
}
|
||||
|
||||
func (o *QueryV1) UnmarshalXDR(bs []byte) error {
|
||||
var buf = bytes.NewBuffer(bs)
|
||||
var xr = xdr.NewReader(buf)
|
||||
return o.decodeXDR(xr)
|
||||
}
|
||||
|
||||
func (o *QueryV1) decodeXDR(xr *xdr.Reader) error {
|
||||
o.Magic = xr.ReadUint32()
|
||||
o.NodeID = xr.ReadStringMax(64)
|
||||
return xr.Error()
|
||||
}
|
||||
|
||||
func (o AnnounceV1) EncodeXDR(w io.Writer) (int, error) {
|
||||
var xw = xdr.NewWriter(w)
|
||||
return o.encodeXDR(xw)
|
||||
}
|
||||
|
||||
func (o AnnounceV1) MarshalXDR() []byte {
|
||||
var buf bytes.Buffer
|
||||
var xw = xdr.NewWriter(&buf)
|
||||
o.encodeXDR(xw)
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
func (o AnnounceV1) encodeXDR(xw *xdr.Writer) (int, error) {
|
||||
xw.WriteUint32(o.Magic)
|
||||
xw.WriteUint16(o.Port)
|
||||
if len(o.NodeID) > 64 {
|
||||
return xw.Tot(), xdr.ErrElementSizeExceeded
|
||||
}
|
||||
xw.WriteString(o.NodeID)
|
||||
if len(o.IP) > 16 {
|
||||
return xw.Tot(), xdr.ErrElementSizeExceeded
|
||||
}
|
||||
xw.WriteBytes(o.IP)
|
||||
return xw.Tot(), xw.Error()
|
||||
}
|
||||
|
||||
func (o *AnnounceV1) DecodeXDR(r io.Reader) error {
|
||||
xr := xdr.NewReader(r)
|
||||
return o.decodeXDR(xr)
|
||||
}
|
||||
|
||||
func (o *AnnounceV1) UnmarshalXDR(bs []byte) error {
|
||||
var buf = bytes.NewBuffer(bs)
|
||||
var xr = xdr.NewReader(buf)
|
||||
return o.decodeXDR(xr)
|
||||
}
|
||||
|
||||
func (o *AnnounceV1) decodeXDR(xr *xdr.Reader) error {
|
||||
o.Magic = xr.ReadUint32()
|
||||
o.Port = xr.ReadUint16()
|
||||
o.NodeID = xr.ReadStringMax(64)
|
||||
o.IP = xr.ReadBytesMax(16)
|
||||
return xr.Error()
|
||||
}
|
||||
|
||||
func (o QueryV2) EncodeXDR(w io.Writer) (int, error) {
|
||||
var xw = xdr.NewWriter(w)
|
||||
return o.encodeXDR(xw)
|
||||
|
||||
@@ -82,7 +82,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
getFailed();
|
||||
});
|
||||
$scope.repos.forEach(function (repo) {
|
||||
$http.get('/rest/model/' + repo.ID).success(function (data) {
|
||||
$http.get('/rest/model?repo=' + encodeURIComponent(repo.ID)).success(function (data) {
|
||||
$scope.model[repo.ID] = data;
|
||||
});
|
||||
});
|
||||
@@ -252,6 +252,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
$scope.editNode = function (nodeCfg) {
|
||||
$scope.currentNode = $.extend({}, nodeCfg);
|
||||
$scope.editingExisting = true;
|
||||
$scope.editingSelf = (nodeCfg.NodeID == $scope.myID);
|
||||
$scope.currentNode.AddressesStr = nodeCfg.Addresses.join(', ');
|
||||
$('#editNode').modal({backdrop: 'static', keyboard: true});
|
||||
};
|
||||
@@ -259,6 +260,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
$scope.addNode = function () {
|
||||
$scope.currentNode = {AddressesStr: 'dynamic'};
|
||||
$scope.editingExisting = false;
|
||||
$scope.editingSelf = false;
|
||||
$('#editNode').modal({backdrop: 'static', keyboard: true});
|
||||
};
|
||||
|
||||
|
||||
@@ -278,7 +278,7 @@
|
||||
<div class="panel panel-warning">
|
||||
<div class="panel-heading"><h3 class="panel-title">Notice</h3></div>
|
||||
<div class="panel-body">
|
||||
<p ng-repeat="err in errorList()"><small>{{err.Time | date:"hh:mm:ss.sss"}}:</small> {{friendlyNodes(err.Error)}}</p>
|
||||
<p ng-repeat="err in errorList()"><small>{{err.Time | date:"H:mm:ss"}}:</small> {{friendlyNodes(err.Error)}}</p>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<button type="button" class="pull-right btn btn-sm btn-default" ng-click="clearErrors()">OK</button>
|
||||
@@ -382,7 +382,7 @@
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="saveNode()">Save</button>
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal">Close</button>
|
||||
<button ng-if="editingExisting" type="button" class="btn btn-danger pull-left" ng-click="deleteNode()">Delete</button>
|
||||
<button ng-if="editingExisting && !editingSelf" type="button" class="btn btn-danger pull-left" ng-click="deleteNode()">Delete</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
// +build ignore
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
|
||||
36
protocol/counting.go
Normal file
36
protocol/counting.go
Normal file
@@ -0,0 +1,36 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"io"
|
||||
"sync/atomic"
|
||||
)
|
||||
|
||||
type countingReader struct {
|
||||
io.Reader
|
||||
tot uint64
|
||||
}
|
||||
|
||||
func (c *countingReader) Read(bs []byte) (int, error) {
|
||||
n, err := c.Reader.Read(bs)
|
||||
atomic.AddUint64(&c.tot, uint64(n))
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *countingReader) Tot() uint64 {
|
||||
return atomic.LoadUint64(&c.tot)
|
||||
}
|
||||
|
||||
type countingWriter struct {
|
||||
io.Writer
|
||||
tot uint64
|
||||
}
|
||||
|
||||
func (c *countingWriter) Write(bs []byte) (int, error) {
|
||||
n, err := c.Writer.Write(bs)
|
||||
atomic.AddUint64(&c.tot, uint64(n))
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *countingWriter) Tot() uint64 {
|
||||
return atomic.LoadUint64(&c.tot)
|
||||
}
|
||||
@@ -69,8 +69,10 @@ type rawConnection struct {
|
||||
id string
|
||||
receiver Model
|
||||
reader io.ReadCloser
|
||||
cr *countingReader
|
||||
xr *xdr.Reader
|
||||
writer io.WriteCloser
|
||||
cw *countingWriter
|
||||
wb *bufio.Writer
|
||||
xw *xdr.Writer
|
||||
closed chan struct{}
|
||||
@@ -93,8 +95,11 @@ const (
|
||||
)
|
||||
|
||||
func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver Model) Connection {
|
||||
flrd := flate.NewReader(reader)
|
||||
flwr, err := flate.NewWriter(writer, flate.BestSpeed)
|
||||
cr := &countingReader{Reader: reader}
|
||||
cw := &countingWriter{Writer: writer}
|
||||
|
||||
flrd := flate.NewReader(cr)
|
||||
flwr, err := flate.NewWriter(cw, flate.BestSpeed)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
@@ -104,8 +109,10 @@ func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver M
|
||||
id: nodeID,
|
||||
receiver: nativeModel{receiver},
|
||||
reader: flrd,
|
||||
cr: cr,
|
||||
xr: xdr.NewReader(flrd),
|
||||
writer: flwr,
|
||||
cw: cw,
|
||||
wb: wb,
|
||||
xw: xdr.NewWriter(wb),
|
||||
closed: make(chan struct{}),
|
||||
@@ -461,7 +468,7 @@ type Statistics struct {
|
||||
func (c *rawConnection) Statistics() Statistics {
|
||||
return Statistics{
|
||||
At: time.Now(),
|
||||
InBytesTotal: int(c.xr.Tot()),
|
||||
OutBytesTotal: int(c.xw.Tot()),
|
||||
InBytesTotal: int(c.cr.Tot()),
|
||||
OutBytesTotal: int(c.cw.Tot()),
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user