syncthing/main.go

347 lines
8.5 KiB
Go
Raw Normal View History

2013-12-15 11:43:31 +01:00
package main
import (
"crypto/sha1"
"crypto/tls"
"fmt"
"log"
"net"
"net/http"
_ "net/http/pprof"
"os"
"path"
"strconv"
"strings"
"time"
"github.com/calmh/ini"
"github.com/calmh/syncthing/discover"
flags "github.com/calmh/syncthing/github.com/jessevdk/go-flags"
2013-12-15 11:43:31 +01:00
"github.com/calmh/syncthing/protocol"
)
2013-12-18 19:36:28 +01:00
type Options struct {
2013-12-29 18:18:59 +01:00
ConfDir string `short:"c" long:"cfg" description:"Configuration directory" default:"~/.syncthing" value-name:"DIR"`
Listen string `short:"l" long:"listen" description:"Listen address" default:":22000" value-name:"ADDR"`
ReadOnly bool `short:"r" long:"ro" description:"Repository is read only"`
Delete bool `short:"d" long:"delete" description:"Delete files deleted from cluster"`
NoSymlinks bool `long:"no-symlinks" description:"Don't follow first level symlinks in the repo"`
Discovery DiscoveryOptions `group:"Discovery Options"`
Advanced AdvancedOptions `group:"Advanced Options"`
Debug DebugOptions `group:"Debugging Options"`
2013-12-18 19:36:28 +01:00
}
type DebugOptions struct {
2013-12-31 03:21:31 +01:00
LogSource bool `long:"log-source"`
2013-12-18 19:36:28 +01:00
TraceFile bool `long:"trace-file"`
TraceNet bool `long:"trace-net"`
TraceIdx bool `long:"trace-idx"`
2013-12-22 22:29:23 +01:00
Profiler string `long:"profiler" value-name:"ADDR"`
}
type DiscoveryOptions struct {
ExternalServer string `long:"ext-server" description:"External discovery server" value-name:"NAME" default:"syncthing.nym.se"`
ExternalPort int `short:"e" long:"ext-port" description:"External listen port" value-name:"PORT" default:"22000"`
NoExternalDiscovery bool `short:"n" long:"no-ext-announce" description:"Do not announce presence externally"`
NoLocalDiscovery bool `short:"N" long:"no-local-announce" description:"Do not announce presence locally"`
2013-12-18 19:36:28 +01:00
}
2013-12-29 18:18:59 +01:00
type AdvancedOptions struct {
RequestsInFlight int `long:"reqs-in-flight" description:"Parallell in flight requests per file" default:"4" value-name:"REQS"`
FilesInFlight int `long:"files-in-flight" description:"Parallell in flight file pulls" default:"8" value-name:"FILES"`
2013-12-29 18:18:59 +01:00
ScanInterval time.Duration `long:"scan-intv" description:"Repository scan interval" default:"60s" value-name:"INTV"`
ConnInterval time.Duration `long:"conn-intv" description:"Node reconnect interval" default:"60s" value-name:"INTV"`
}
2013-12-18 19:36:28 +01:00
var opts Options
2013-12-30 16:05:27 +01:00
var Version string = "unknown-dev"
2013-12-18 19:36:28 +01:00
2013-12-15 11:43:31 +01:00
const (
confDirName = ".syncthing"
confFileName = "syncthing.ini"
)
var (
config ini.Config
nodeAddrs = make(map[string][]string)
)
// Options
var (
2013-12-18 19:36:28 +01:00
ConfDir = path.Join(getHomeDir(), confDirName)
2013-12-15 11:43:31 +01:00
)
func main() {
2013-12-18 19:36:28 +01:00
_, err := flags.Parse(&opts)
if err != nil {
os.Exit(0)
2013-12-15 11:43:31 +01:00
}
2013-12-31 03:21:31 +01:00
if opts.Debug.TraceFile || opts.Debug.TraceIdx || opts.Debug.TraceNet || opts.Debug.LogSource {
logger = log.New(os.Stderr, "", log.Lshortfile|log.Ldate|log.Ltime|log.Lmicroseconds)
}
2013-12-18 19:36:28 +01:00
if strings.HasPrefix(opts.ConfDir, "~/") {
opts.ConfDir = strings.Replace(opts.ConfDir, "~", getHomeDir(), 1)
2013-12-15 11:43:31 +01:00
}
2013-12-22 00:16:49 +01:00
infoln("Version", Version)
2013-12-15 11:43:31 +01:00
// Ensure that our home directory exists and that we have a certificate and key.
2013-12-22 00:16:49 +01:00
ensureDir(ConfDir, 0700)
2013-12-18 19:36:28 +01:00
cert, err := loadCert(ConfDir)
2013-12-15 11:43:31 +01:00
if err != nil {
2013-12-18 19:36:28 +01:00
newCertificate(ConfDir)
cert, err = loadCert(ConfDir)
2013-12-15 11:43:31 +01:00
fatalErr(err)
}
myID := string(certId(cert.Certificate[0]))
infoln("My ID:", myID)
2013-12-18 19:36:28 +01:00
if opts.Debug.Profiler != "" {
2013-12-15 11:43:31 +01:00
go func() {
2013-12-18 19:36:28 +01:00
err := http.ListenAndServe(opts.Debug.Profiler, nil)
if err != nil {
warnln(err)
}
2013-12-15 11:43:31 +01:00
}()
}
// The TLS configuration is used for both the listening socket and outgoing
// connections.
cfg := &tls.Config{
ClientAuth: tls.RequestClientCert,
ServerName: "syncthing",
NextProtos: []string{"bep/1.0"},
InsecureSkipVerify: true,
Certificates: []tls.Certificate{cert},
}
// Load the configuration file, if it exists.
2013-12-18 19:36:28 +01:00
cf, err := os.Open(path.Join(ConfDir, confFileName))
2013-12-15 11:43:31 +01:00
if err != nil {
fatalln("No config file")
config = ini.Config{}
}
config = ini.Parse(cf)
cf.Close()
var dir = config.Get("repository", "dir")
// Create a map of desired node connections based on the configuration file
// directives.
for nodeID, addrs := range config.OptionMap("nodes") {
addrs := strings.Fields(addrs)
nodeAddrs[nodeID] = addrs
}
2013-12-22 00:16:49 +01:00
ensureDir(dir, -1)
2013-12-15 11:43:31 +01:00
m := NewModel(dir)
// Walk the repository and update the local model before establishing any
// connections to other nodes.
infoln("Initial repository scan in progress")
2013-12-15 11:43:31 +01:00
loadIndex(m)
updateLocalModel(m)
// Routine to listen for incoming connections
infoln("Listening for incoming connections")
2013-12-18 19:36:28 +01:00
go listen(myID, opts.Listen, m, cfg)
2013-12-15 11:43:31 +01:00
// Routine to connect out to configured nodes
infoln("Attempting to connect to other nodes")
2013-12-18 19:36:28 +01:00
go connect(myID, opts.Listen, nodeAddrs, m, cfg)
2013-12-15 11:43:31 +01:00
// Routine to pull blocks from other nodes to synchronize the local
// repository. Does not run when we are in read only (publish only) mode.
2013-12-18 19:36:28 +01:00
if !opts.ReadOnly {
2013-12-15 11:43:31 +01:00
infoln("Cleaning out incomplete synchronizations")
CleanTempFiles(dir)
okln("Ready to synchronize")
m.Start()
}
// Periodically scan the repository and update the local model.
// XXX: Should use some fsnotify mechanism.
go func() {
for {
2013-12-29 18:18:59 +01:00
time.Sleep(opts.Advanced.ScanInterval)
2013-12-15 11:43:31 +01:00
updateLocalModel(m)
}
}()
select {}
}
func listen(myID string, addr string, m *Model, cfg *tls.Config) {
l, err := tls.Listen("tcp", addr, cfg)
fatalErr(err)
listen:
for {
conn, err := l.Accept()
if err != nil {
warnln(err)
continue
}
2013-12-18 19:36:28 +01:00
if opts.Debug.TraceNet {
2013-12-15 11:43:31 +01:00
debugln("NET: Connect from", conn.RemoteAddr())
}
tc := conn.(*tls.Conn)
err = tc.Handshake()
if err != nil {
warnln(err)
tc.Close()
continue
}
remoteID := certId(tc.ConnectionState().PeerCertificates[0].Raw)
if remoteID == myID {
warnf("Connect from myself (%s) - should not happen", remoteID)
conn.Close()
continue
}
if m.ConnectedTo(remoteID) {
warnf("Connect from connected node (%s)", remoteID)
}
for nodeID := range nodeAddrs {
if nodeID == remoteID {
m.AddConnection(conn, remoteID)
2013-12-15 11:43:31 +01:00
continue listen
}
}
conn.Close()
}
}
func connect(myID string, addr string, nodeAddrs map[string][]string, m *Model, cfg *tls.Config) {
_, portstr, err := net.SplitHostPort(addr)
fatalErr(err)
port, _ := strconv.Atoi(portstr)
2013-12-22 22:29:23 +01:00
if opts.Discovery.NoLocalDiscovery {
port = -1
} else {
infoln("Sending local discovery announcements")
}
if opts.Discovery.NoExternalDiscovery {
opts.Discovery.ExternalPort = -1
} else {
infoln("Sending external discovery announcements")
}
disc, err := discover.NewDiscoverer(myID, port, opts.Discovery.ExternalPort, opts.Discovery.ExternalServer)
2013-12-15 11:43:31 +01:00
if err != nil {
2013-12-22 22:29:23 +01:00
warnf("No discovery possible (%v)", err)
2013-12-15 11:43:31 +01:00
}
for {
nextNode:
for nodeID, addrs := range nodeAddrs {
if nodeID == myID {
continue
}
if m.ConnectedTo(nodeID) {
continue
}
for _, addr := range addrs {
if addr == "dynamic" {
var ok bool
if disc != nil {
addr, ok = disc.Lookup(nodeID)
}
if !ok {
continue
}
}
2013-12-18 19:36:28 +01:00
if opts.Debug.TraceNet {
2013-12-15 11:43:31 +01:00
debugln("NET: Dial", nodeID, addr)
}
conn, err := tls.Dial("tcp", addr, cfg)
if err != nil {
2013-12-18 19:36:28 +01:00
if opts.Debug.TraceNet {
2013-12-15 11:43:31 +01:00
debugln("NET:", err)
}
continue
}
remoteID := certId(conn.ConnectionState().PeerCertificates[0].Raw)
if remoteID != nodeID {
warnln("Unexpected nodeID", remoteID, "!=", nodeID)
conn.Close()
continue
}
m.AddConnection(conn, remoteID)
2013-12-15 11:43:31 +01:00
continue nextNode
}
}
2013-12-29 18:18:59 +01:00
time.Sleep(opts.Advanced.ConnInterval)
2013-12-15 11:43:31 +01:00
}
}
func updateLocalModel(m *Model) {
2013-12-18 19:36:28 +01:00
files := Walk(m.Dir(), m, !opts.NoSymlinks)
2013-12-15 11:43:31 +01:00
m.ReplaceLocal(files)
saveIndex(m)
}
func saveIndex(m *Model) {
2013-12-31 04:04:30 +01:00
name := fmt.Sprintf("%x.idx", sha1.Sum([]byte(m.Dir())))
fullName := path.Join(ConfDir, name)
idxf, err := os.Create(fullName + ".tmp")
2013-12-15 11:43:31 +01:00
if err != nil {
return
}
protocol.WriteIndex(idxf, m.ProtocolIndex())
idxf.Close()
2013-12-31 04:04:30 +01:00
os.Rename(fullName+".tmp", fullName)
2013-12-15 11:43:31 +01:00
}
func loadIndex(m *Model) {
fname := fmt.Sprintf("%x.idx", sha1.Sum([]byte(m.Dir())))
2013-12-18 19:36:28 +01:00
idxf, err := os.Open(path.Join(ConfDir, fname))
2013-12-15 11:43:31 +01:00
if err != nil {
return
}
defer idxf.Close()
idx, err := protocol.ReadIndex(idxf)
if err != nil {
return
}
m.SeedIndex(idx)
}
2013-12-22 00:16:49 +01:00
func ensureDir(dir string, mode int) {
2013-12-15 11:43:31 +01:00
fi, err := os.Stat(dir)
if os.IsNotExist(err) {
err := os.MkdirAll(dir, 0700)
fatalErr(err)
2013-12-22 00:16:49 +01:00
} else if mode >= 0 && err == nil && int(fi.Mode()&0777) != mode {
err := os.Chmod(dir, os.FileMode(mode))
2013-12-15 11:43:31 +01:00
fatalErr(err)
}
}
func getHomeDir() string {
home := os.Getenv("HOME")
if home == "" {
fatalln("No home directory?")
}
return home
}