func main() { var reset bool var showVersion bool var doUpgrade bool flag.StringVar(&confDir, "home", getDefaultConfDir(), "Set configuration directory") flag.BoolVar(&reset, "reset", false, "Prepare to resync from cluster") flag.BoolVar(&showVersion, "version", false, "Show version") flag.BoolVar(&doUpgrade, "upgrade", false, "Perform upgrade") flag.Usage = usageFor(flag.CommandLine, usage, extraUsage) flag.Parse() if len(os.Getenv("STRESTART")) > 0 { // Give the parent process time to exit and release sockets etc. time.Sleep(1 * time.Second) } if showVersion { fmt.Println(LongVersion) return } if doUpgrade { err := upgrade() if err != nil { l.Fatalln(err) } return } if len(os.Getenv("GOGC")) == 0 { debug.SetGCPercent(25) } if len(os.Getenv("GOMAXPROCS")) == 0 { runtime.GOMAXPROCS(runtime.NumCPU()) } 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 { l.Infoln("Moved config dir", oldDefault, "to", confDir) } } } // Ensure that our home directory exists and that we have a certificate and key. ensureDir(confDir, 0700) cert, err := loadCert(confDir) if err != nil { newCertificate(confDir) cert, err = loadCert(confDir) l.FatalErr(err) } myID = certID(cert.Certificate[0]) l.SetPrefix(fmt.Sprintf("[%s] ", myID[:5])) l.Infoln(LongVersion) l.Infoln("My ID:", myID) // Prepare to be able to save configuration cfgFile := filepath.Join(confDir, "config.xml") go saveConfigLoop(cfgFile) // Load the configuration file, if it exists. // If it does not, create a template. cf, err := os.Open(cfgFile) if err == nil { // Read config.xml cfg, err = config.Load(cf, myID) if err != nil { l.Fatalln(err) } cf.Close() } else { l.Infoln("No config file; starting with empty defaults") name, _ := os.Hostname() defaultRepo := filepath.Join(getHomeDir(), "Sync") ensureDir(defaultRepo, 0755) cfg, err = config.Load(nil, myID) cfg.Repositories = []config.RepositoryConfiguration{ { ID: "default", Directory: defaultRepo, Nodes: []config.NodeConfiguration{{NodeID: myID}}, }, } cfg.Nodes = []config.NodeConfiguration{ { NodeID: myID, Addresses: []string{"dynamic"}, Name: name, }, } port, err := getFreePort("127.0.0.1", 8080) l.FatalErr(err) cfg.GUI.Address = fmt.Sprintf("127.0.0.1:%d", port) port, err = getFreePort("", 22000) l.FatalErr(err) cfg.Options.ListenAddress = []string{fmt.Sprintf(":%d", port)} saveConfig() l.Infof("Edit %s to taste or use the GUI\n", cfgFile) } if reset { resetRepositories() return } if profiler := os.Getenv("STPROFILER"); len(profiler) > 0 { go func() { l.Debugln("Starting profiler on", profiler) err := http.ListenAndServe(profiler, nil) if err != nil { l.Fatalln(err) } }() } // The TLS configuration is used for both the listening socket and outgoing // connections. tlsCfg := &tls.Config{ Certificates: []tls.Certificate{cert}, NextProtos: []string{"bep/1.0"}, ServerName: myID, ClientAuth: tls.RequestClientCert, SessionTicketsDisabled: true, InsecureSkipVerify: true, MinVersion: tls.VersionTLS12, } // If the write rate should be limited, set up a rate limiter for it. // This will be used on connections created in the connect and listen routines. if cfg.Options.MaxSendKbps > 0 { rateBucket = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxSendKbps), int64(5*1000*cfg.Options.MaxSendKbps)) } m := model.NewModel(confDir, &cfg, "syncthing", Version) for _, repo := range cfg.Repositories { if repo.Invalid != "" { continue } dir := expandTilde(repo.Directory) m.AddRepo(repo.ID, dir, repo.Nodes) } // GUI if cfg.GUI.Enabled && cfg.GUI.Address != "" { addr, err := net.ResolveTCPAddr("tcp", cfg.GUI.Address) if err != nil { l.Fatalf("Cannot start GUI on %q: %v", cfg.GUI.Address, err) } else { var hostOpen, hostShow string switch { case addr.IP == nil: hostOpen = "localhost" hostShow = "0.0.0.0" case addr.IP.IsUnspecified(): hostOpen = "localhost" hostShow = addr.IP.String() default: hostOpen = addr.IP.String() hostShow = hostOpen } l.Infof("Starting web GUI on http://%s:%d/", hostShow, addr.Port) err := startGUI(cfg.GUI, m) if err != nil { l.Fatalln("Cannot start GUI:", err) } if cfg.Options.StartBrowser && len(os.Getenv("STRESTART")) == 0 { openURL(fmt.Sprintf("http://%s:%d", hostOpen, addr.Port)) } } } // Walk the repository and update the local model before establishing any // connections to other nodes. l.Infoln("Populating repository index") m.LoadIndexes(confDir) for _, repo := range cfg.Repositories { if repo.Invalid != "" { continue } 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() { l.Warnf("Configured repository %q has index but directory %q is missing; not starting.", repo.ID, repo.Directory) l.Fatalf("Ensure that directory is present or remove repository from configuration.") } } // Ensure that repository directories exist for newly configured repositories. ensureDir(dir, -1) } m.CleanRepos() m.ScanRepos() m.SaveIndexes(confDir) // UPnP var externalPort = 0 if cfg.Options.UPnPEnabled { // We seed the random number generator with the node ID to get a // repeatable sequence of random external ports. rand.Seed(certSeed(cert.Certificate[0])) externalPort = setupUPnP() } // Routine to connect out to configured nodes discoverer = discovery(externalPort) go listenConnect(myID, m, tlsCfg) for _, repo := range cfg.Repositories { if repo.Invalid != "" { continue } // Routine to pull blocks from other nodes to synchronize the local // repository. Does not run when we are in read only (publish only) mode. if repo.ReadOnly { l.Okf("Ready to synchronize %s (read only; no external updates accepted)", repo.ID) m.StartRepoRO(repo.ID) } else { l.Okf("Ready to synchronize %s (read-write)", repo.ID) m.StartRepoRW(repo.ID, cfg.Options.ParallelRequests) } } if cpuprof := os.Getenv("STCPUPROFILE"); len(cpuprof) > 0 { f, err := os.Create(cpuprof) if err != nil { log.Fatal(err) } pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() } <-stop }
func main() { var reset bool var showVersion bool var doUpgrade bool flag.StringVar(&confDir, "home", getDefaultConfDir(), "Set configuration directory") flag.BoolVar(&reset, "reset", false, "Prepare to resync from cluster") flag.BoolVar(&showVersion, "version", false, "Show version") flag.BoolVar(&doUpgrade, "upgrade", false, "Perform upgrade") flag.IntVar(&logFlags, "logflags", logFlags, "Set log flags") flag.Usage = usageFor(flag.CommandLine, usage, extraUsage) flag.Parse() if showVersion { fmt.Println(LongVersion) return } l.SetFlags(logFlags) if doUpgrade { err := upgrade() if err != nil { l.Fatalln(err) } return } if len(os.Getenv("GOGC")) == 0 { debug.SetGCPercent(25) } if len(os.Getenv("GOMAXPROCS")) == 0 { runtime.GOMAXPROCS(runtime.NumCPU()) } confDir = expandTilde(confDir) events.Default.Log(events.Starting, map[string]string{"home": 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. var oldDefault string if runtime.GOOS == "windows" { oldDefault = filepath.Join(os.Getenv("AppData"), "Syncthing") } else { oldDefault = expandTilde("~/.syncthing") } if _, err := os.Stat(oldDefault); err == nil { os.MkdirAll(filepath.Dir(confDir), 0700) if err := os.Rename(oldDefault, confDir); err == nil { l.Infoln("Moved config dir", oldDefault, "to", confDir) } } } // Ensure that our home directory exists and that we have a certificate and key. ensureDir(confDir, 0700) cert, err := loadCert(confDir, "") if err != nil { newCertificate(confDir, "") cert, err = loadCert(confDir, "") l.FatalErr(err) } myID = protocol.NewNodeID(cert.Certificate[0]) l.SetPrefix(fmt.Sprintf("[%s] ", myID.String()[:5])) l.Infoln(LongVersion) l.Infoln("My ID:", myID) // Prepare to be able to save configuration cfgFile := filepath.Join(confDir, "config.xml") go saveConfigLoop(cfgFile) // Load the configuration file, if it exists. // If it does not, create a template. cf, err := os.Open(cfgFile) if err == nil { // Read config.xml cfg, err = config.Load(cf, myID) if err != nil { l.Fatalln(err) } cf.Close() } else { l.Infoln("No config file; starting with empty defaults") name, _ := os.Hostname() defaultRepo := filepath.Join(getHomeDir(), "Sync") ensureDir(defaultRepo, 0755) cfg, err = config.Load(nil, myID) cfg.Repositories = []config.RepositoryConfiguration{ { ID: "default", Directory: defaultRepo, Nodes: []config.NodeConfiguration{{NodeID: myID}}, }, } cfg.Nodes = []config.NodeConfiguration{ { NodeID: myID, Addresses: []string{"dynamic"}, Name: name, }, } port, err := getFreePort("127.0.0.1", 8080) l.FatalErr(err) cfg.GUI.Address = fmt.Sprintf("127.0.0.1:%d", port) port, err = getFreePort("0.0.0.0", 22000) l.FatalErr(err) cfg.Options.ListenAddress = []string{fmt.Sprintf("0.0.0.0:%d", port)} saveConfig() l.Infof("Edit %s to taste or use the GUI\n", cfgFile) } if reset { resetRepositories() return } if profiler := os.Getenv("STPROFILER"); len(profiler) > 0 { go func() { l.Debugln("Starting profiler on", profiler) runtime.SetBlockProfileRate(1) err := http.ListenAndServe(profiler, nil) if err != nil { l.Fatalln(err) } }() } if len(os.Getenv("STRESTART")) > 0 { waitForParentExit() } // The TLS configuration is used for both the listening socket and outgoing // connections. tlsCfg := &tls.Config{ Certificates: []tls.Certificate{cert}, NextProtos: []string{"bep/1.0"}, ServerName: myID.String(), ClientAuth: tls.RequestClientCert, SessionTicketsDisabled: true, InsecureSkipVerify: true, MinVersion: tls.VersionTLS12, } // If the write rate should be limited, set up a rate limiter for it. // This will be used on connections created in the connect and listen routines. if cfg.Options.MaxSendKbps > 0 { rateBucket = ratelimit.NewBucketWithRate(float64(1000*cfg.Options.MaxSendKbps), int64(5*1000*cfg.Options.MaxSendKbps)) } removeLegacyIndexes() db, err := leveldb.OpenFile(filepath.Join(confDir, "index"), nil) if err != nil { l.Fatalln("leveldb.OpenFile():", err) } m := model.NewModel(confDir, &cfg, "syncthing", Version, db) nextRepo: for i, repo := range cfg.Repositories { if repo.Invalid != "" { continue } repo.Directory = 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. id := fmt.Sprintf("%x", sha1.Sum([]byte(repo.Directory))) idxFile := filepath.Join(confDir, id+".idx.gz") if _, err := os.Stat(idxFile); err == nil { if fi, err := os.Stat(repo.Directory); err != nil || !fi.IsDir() { cfg.Repositories[i].Invalid = "repo directory missing" continue nextRepo } } ensureDir(repo.Directory, -1) m.AddRepo(repo) } // GUI if cfg.GUI.Enabled && cfg.GUI.Address != "" { addr, err := net.ResolveTCPAddr("tcp", cfg.GUI.Address) if err != nil { l.Fatalf("Cannot start GUI on %q: %v", cfg.GUI.Address, err) } else { var hostOpen, hostShow string switch { case addr.IP == nil: hostOpen = "localhost" hostShow = "0.0.0.0" case addr.IP.IsUnspecified(): hostOpen = "localhost" hostShow = addr.IP.String() default: hostOpen = addr.IP.String() hostShow = hostOpen } var proto = "http" if cfg.GUI.UseTLS { proto = "https" } l.Infof("Starting web GUI on %s://%s:%d/", proto, hostShow, addr.Port) err := startGUI(cfg.GUI, os.Getenv("STGUIASSETS"), m) if err != nil { l.Fatalln("Cannot start GUI:", err) } if cfg.Options.StartBrowser && len(os.Getenv("STRESTART")) == 0 { openURL(fmt.Sprintf("%s://%s:%d", proto, hostOpen, addr.Port)) } } } // Walk the repository and update the local model before establishing any // connections to other nodes. m.CleanRepos() l.Infoln("Performing initial repository scan") m.ScanRepos() // Remove all .idx* files that don't belong to an active repo. validIndexes := make(map[string]bool) for _, repo := range cfg.Repositories { dir := expandTilde(repo.Directory) id := fmt.Sprintf("%x", sha1.Sum([]byte(dir))) validIndexes[id] = true } allIndexes, err := filepath.Glob(filepath.Join(confDir, "*.idx*")) if err == nil { for _, idx := range allIndexes { bn := filepath.Base(idx) fs := strings.Split(bn, ".") if len(fs) > 1 { if _, ok := validIndexes[fs[0]]; !ok { l.Infoln("Removing old index", bn) os.Remove(idx) } } } } // UPnP var externalPort = 0 if cfg.Options.UPnPEnabled { // We seed the random number generator with the node ID to get a // repeatable sequence of random external ports. externalPort = setupUPnP(rand.NewSource(certSeed(cert.Certificate[0]))) } // Routine to connect out to configured nodes discoverer = discovery(externalPort) go listenConnect(myID, m, tlsCfg) for _, repo := range cfg.Repositories { if repo.Invalid != "" { continue } // Routine to pull blocks from other nodes to synchronize the local // repository. Does not run when we are in read only (publish only) mode. if repo.ReadOnly { l.Okf("Ready to synchronize %s (read only; no external updates accepted)", repo.ID) m.StartRepoRO(repo.ID) } else { l.Okf("Ready to synchronize %s (read-write)", repo.ID) m.StartRepoRW(repo.ID, cfg.Options.ParallelRequests) } } if cpuprof := os.Getenv("STCPUPROFILE"); len(cpuprof) > 0 { f, err := os.Create(cpuprof) if err != nil { log.Fatal(err) } pprof.StartCPUProfile(f) defer pprof.StopCPUProfile() } for _, node := range cfg.Nodes { if len(node.Name) > 0 { l.Infof("Node %s is %q at %v", node.NodeID, node.Name, node.Addresses) } } if cfg.Options.URAccepted > 0 && cfg.Options.URAccepted < usageReportVersion { l.Infoln("Anonymous usage report has changed; revoking acceptance") cfg.Options.URAccepted = 0 } if cfg.Options.URAccepted >= usageReportVersion { go usageReportingLoop(m) go func() { time.Sleep(10 * time.Minute) err := sendUsageReport(m) if err != nil { l.Infoln("Usage report:", err) } }() } events.Default.Log(events.StartupComplete, nil) go generateEvents() <-stop l.Okln("Exiting") }
func main() { _, err := flags.Parse(&opts) if err != nil { if err, ok := err.(*flags.Error); ok { if err.Type == flags.ErrHelp { os.Exit(0) } } fatalln(err) } if opts.ShowVersion { fmt.Println(Version) os.Exit(0) } if len(os.Getenv("GOGC")) == 0 { debug.SetGCPercent(25) } if len(os.Getenv("GOMAXPROCS")) == 0 { runtime.GOMAXPROCS(runtime.NumCPU()) } log.SetOutput(os.Stderr) logger = log.New(os.Stderr, "", log.Flags()) if len(opts.Debug.TraceModel) > 0 || opts.Debug.LogSource { log.SetFlags(log.Lshortfile | log.Ldate | log.Ltime | log.Lmicroseconds) logger.SetFlags(log.Lshortfile | log.Ldate | log.Ltime | log.Lmicroseconds) } opts.ConfDir = expandTilde(opts.ConfDir) infoln("Version", Version) // Ensure that our home directory exists and that we have a certificate and key. ensureDir(opts.ConfDir, 0700) cert, err := loadCert(opts.ConfDir) if err != nil { newCertificate(opts.ConfDir) cert, err = loadCert(opts.ConfDir) fatalErr(err) } myID = string(certId(cert.Certificate[0])) infoln("My ID:", myID) log.SetPrefix("[" + myID[0:5] + "] ") logger.SetPrefix("[" + myID[0:5] + "] ") if opts.Debug.Profiler != "" { go func() { err := http.ListenAndServe(opts.Debug.Profiler, nil) if err != nil { warnln(err) } }() } // The TLS configuration is used for both the listening socket and outgoing // connections. cfg := &tls.Config{ Certificates: []tls.Certificate{cert}, NextProtos: []string{"bep/1.0"}, ServerName: myID, ClientAuth: tls.RequestClientCert, SessionTicketsDisabled: true, InsecureSkipVerify: true, MinVersion: tls.VersionTLS12, } // Load the configuration file, if it exists. cf, err := os.Open(path.Join(opts.ConfDir, confFileName)) if err != nil { fatalln("No config file") config = ini.Config{} } config = ini.Parse(cf) cf.Close() var dir = expandTilde(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 } ensureDir(dir, -1) m := model.NewModel(dir) for _, t := range opts.Debug.TraceModel { m.Trace(t) } if opts.Advanced.LimitRate > 0 { m.LimitRate(opts.Advanced.LimitRate) } // GUI if !opts.NoGUI && opts.GUIAddr != "" { host, port, err := net.SplitHostPort(opts.GUIAddr) if err != nil { warnf("Cannot start GUI on %q: %v", opts.GUIAddr, err) } else { if len(host) > 0 { infof("Starting web GUI on http://%s", opts.GUIAddr) } else { infof("Starting web GUI on port %s", port) } startGUI(opts.GUIAddr, m) } } // Walk the repository and update the local model before establishing any // connections to other nodes. if !opts.Rehash { infoln("Loading index cache") loadIndex(m) } infoln("Populating repository index") updateLocalModel(m) // Routine to listen for incoming connections infoln("Listening for incoming connections") go listen(myID, opts.Listen, m, cfg) // Routine to connect out to configured nodes infoln("Attempting to connect to other nodes") go connect(myID, opts.Listen, nodeAddrs, m, cfg) // Routine to pull blocks from other nodes to synchronize the local // repository. Does not run when we are in read only (publish only) mode. if !opts.ReadOnly { if opts.NoDelete { infoln("Deletes from peer nodes will be ignored") } else { infoln("Deletes from peer nodes are allowed") } okln("Ready to synchronize (read-write)") m.StartRW(!opts.NoDelete, opts.Advanced.RequestsInFlight) } else { okln("Ready to synchronize (read only; no external updates accepted)") } // Periodically scan the repository and update the local model. // XXX: Should use some fsnotify mechanism. go func() { for { time.Sleep(opts.Advanced.ScanInterval) if m.LocalAge() > opts.Advanced.ScanInterval.Seconds()/2 { updateLocalModel(m) } } }() if !opts.NoStats { // Periodically print statistics go printStatsLoop(m) } select {} }