From dc929946fe0835e33597c556f127cffc435ce838 Mon Sep 17 00:00:00 2001 From: Audrius Butkevicius Date: Tue, 12 Feb 2019 06:58:24 +0000 Subject: [PATCH] all: Use new reflect based CLI (#5487) --- build.go | 8 +- cmd/stcli/LICENSE | 19 - cmd/stcli/client.go | 144 +++---- cmd/stcli/cmd_devices.go | 188 --------- cmd/stcli/cmd_errors.go | 67 ---- cmd/stcli/cmd_folders.go | 361 ------------------ cmd/stcli/cmd_general.go | 94 ----- cmd/stcli/cmd_gui.go | 127 ------ cmd/stcli/cmd_options.go | 173 --------- cmd/stcli/cmd_report.go | 72 ---- cmd/stcli/errors.go | 60 +++ cmd/stcli/labels.go | 31 -- cmd/stcli/main.go | 219 ++++++++--- cmd/stcli/operations.go | 78 ++++ cmd/stcli/show.go | 44 +++ cmd/stcli/utils.go | 184 +++------ cmd/syncthing/gui.go | 48 +-- cmd/syncthing/gui_csrf.go | 5 +- cmd/syncthing/locations.go | 125 ------ cmd/syncthing/main.go | 167 +++----- cmd/syncthing/main_test.go | 26 -- cmd/syncthing/monitor.go | 3 +- cmd/syncthing/usage_report.go | 5 +- go.mod | 7 +- go.sum | 14 +- lib/build/build.go | 81 ++++ lib/build/build_test.go | 37 ++ .../build/tags_noupgrade.go | 4 +- .../build/tags_race.go | 4 +- lib/config/deviceconfiguration.go | 6 +- lib/config/folderconfiguration.go | 31 +- lib/config/optionsconfiguration.go | 2 +- lib/config/size.go | 6 +- lib/config/size_test.go | 23 +- lib/locations/locations.go | 161 ++++++++ lib/util/utils.go | 34 +- lib/util/utils_test.go | 9 +- 37 files changed, 944 insertions(+), 1723 deletions(-) delete mode 100644 cmd/stcli/LICENSE delete mode 100644 cmd/stcli/cmd_devices.go delete mode 100644 cmd/stcli/cmd_errors.go delete mode 100644 cmd/stcli/cmd_folders.go delete mode 100644 cmd/stcli/cmd_general.go delete mode 100644 cmd/stcli/cmd_gui.go delete mode 100644 cmd/stcli/cmd_options.go delete mode 100644 cmd/stcli/cmd_report.go create mode 100644 cmd/stcli/errors.go delete mode 100644 cmd/stcli/labels.go create mode 100644 cmd/stcli/operations.go create mode 100644 cmd/stcli/show.go delete mode 100644 cmd/syncthing/locations.go create mode 100644 lib/build/build.go create mode 100644 lib/build/build_test.go rename cmd/syncthing/buildtag_noupgrade.go => lib/build/tags_noupgrade.go (83%) rename cmd/syncthing/buildtag_race.go => lib/build/tags_race.go (84%) create mode 100644 lib/locations/locations.go diff --git a/build.go b/build.go index b8c933dfd..304642458 100644 --- a/build.go +++ b/build.go @@ -768,10 +768,10 @@ func ldflags() string { b := new(bytes.Buffer) b.WriteString("-w") - fmt.Fprintf(b, " -X main.Version%c%s", sep, version) - fmt.Fprintf(b, " -X main.BuildStamp%c%d", sep, buildStamp()) - fmt.Fprintf(b, " -X main.BuildUser%c%s", sep, buildUser()) - fmt.Fprintf(b, " -X main.BuildHost%c%s", sep, buildHost()) + fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Version%c%s", sep, version) + fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Stamp%c%d", sep, buildStamp()) + fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.User%c%s", sep, buildUser()) + fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Host%c%s", sep, buildHost()) return b.String() } diff --git a/cmd/stcli/LICENSE b/cmd/stcli/LICENSE deleted file mode 100644 index a127d219a..000000000 --- a/cmd/stcli/LICENSE +++ /dev/null @@ -1,19 +0,0 @@ -Copyright (C) 2014 Audrius Butkevičius - -Permission is hereby granted, free of charge, to any person obtaining a copy of -this software and associated documentation files (the "Software"), to deal in -the Software without restriction, including without limitation the rights to -use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies -of the Software, and to permit persons to whom the Software is furnished to do -so, subject to the following conditions: - -- The above copyright notice and this permission notice shall be included in - all copies or substantial portions of the Software. - -THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR -IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, -FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE -AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER -LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, -OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE -SOFTWARE. diff --git a/cmd/stcli/client.go b/cmd/stcli/client.go index 66bc6c2d3..72403ce52 100644 --- a/cmd/stcli/client.go +++ b/cmd/stcli/client.go @@ -1,115 +1,95 @@ -// Copyright (C) 2014 Audrius Butkevičius +// Copyright (C) 2019 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. package main import ( "bytes" + "context" "crypto/tls" + "fmt" + "net" "net/http" "strings" - "github.com/AudriusButkevicius/cli" + "github.com/syncthing/syncthing/lib/config" ) type APIClient struct { - httpClient http.Client - endpoint string - apikey string - username string - password string - id string - csrf string + http.Client + cfg config.GUIConfiguration + apikey string } -var instance *APIClient - -func getClient(c *cli.Context) *APIClient { - if instance != nil { - return instance - } - endpoint := c.GlobalString("endpoint") - if !strings.HasPrefix(endpoint, "http") { - endpoint = "http://" + endpoint - } +func getClient(cfg config.GUIConfiguration) *APIClient { httpClient := http.Client{ Transport: &http.Transport{ TLSClientConfig: &tls.Config{ - InsecureSkipVerify: c.GlobalBool("insecure"), + InsecureSkipVerify: true, + }, + DialContext: func(_ context.Context, _, _ string) (net.Conn, error) { + return net.Dial(cfg.Network(), cfg.Address()) }, }, } - client := APIClient{ - httpClient: httpClient, - endpoint: endpoint, - apikey: c.GlobalString("apikey"), - username: c.GlobalString("username"), - password: c.GlobalString("password"), + return &APIClient{ + Client: httpClient, + cfg: cfg, + apikey: cfg.APIKey, } - - if client.apikey == "" { - request, err := http.NewRequest("GET", client.endpoint, nil) - die(err) - response := client.handleRequest(request) - client.id = response.Header.Get("X-Syncthing-ID") - if client.id == "" { - die("Failed to get device ID") - } - for _, item := range response.Cookies() { - if item.Name == "CSRF-Token-"+client.id[:5] { - client.csrf = item.Value - goto csrffound - } - } - die("Failed to get CSRF token") - csrffound: - } - instance = &client - return &client } -func (client *APIClient) handleRequest(request *http.Request) *http.Response { - if client.apikey != "" { - request.Header.Set("X-API-Key", client.apikey) +func (c *APIClient) Endpoint() string { + if c.cfg.Network() == "unix" { + return "http://unix/" } - if client.username != "" || client.password != "" { - request.SetBasicAuth(client.username, client.password) - } - if client.csrf != "" { - request.Header.Set("X-CSRF-Token-"+client.id[:5], client.csrf) + url := c.cfg.URL() + if !strings.HasSuffix(url, "/") { + url += "/" } + return url +} - response, err := client.httpClient.Do(request) - die(err) +func (c *APIClient) Do(req *http.Request) (*http.Response, error) { + req.Header.Set("X-API-Key", c.apikey) + resp, err := c.Client.Do(req) + if err != nil { + return nil, err + } + return resp, checkResponse(resp) +} +func (c *APIClient) Get(url string) (*http.Response, error) { + request, err := http.NewRequest("GET", c.Endpoint()+"rest/"+url, nil) + if err != nil { + return nil, err + } + return c.Do(request) +} + +func (c *APIClient) Post(url, body string) (*http.Response, error) { + request, err := http.NewRequest("POST", c.Endpoint()+"rest/"+url, bytes.NewBufferString(body)) + if err != nil { + return nil, err + } + return c.Do(request) +} + +func checkResponse(response *http.Response) error { if response.StatusCode == 404 { - die("Invalid endpoint or API call") - } else if response.StatusCode == 401 { - die("Invalid username or password") + return fmt.Errorf("Invalid endpoint or API call") } else if response.StatusCode == 403 { - if client.apikey == "" { - die("Invalid CSRF token") - } - die("Invalid API key") + return fmt.Errorf("Invalid API key") } else if response.StatusCode != 200 { - body := strings.TrimSpace(string(responseToBArray(response))) - if body != "" { - die(body) + data, err := responseToBArray(response) + if err != nil { + return err } - die("Unknown HTTP status returned: " + response.Status) + body := strings.TrimSpace(string(data)) + return fmt.Errorf("Unexpected HTTP status returned: %s\n%s", response.Status, body) } - return response -} - -func httpGet(c *cli.Context, url string) *http.Response { - client := getClient(c) - request, err := http.NewRequest("GET", client.endpoint+"/rest/"+url, nil) - die(err) - return client.handleRequest(request) -} - -func httpPost(c *cli.Context, url string, body string) *http.Response { - client := getClient(c) - request, err := http.NewRequest("POST", client.endpoint+"/rest/"+url, bytes.NewBufferString(body)) - die(err) - return client.handleRequest(request) + return nil } diff --git a/cmd/stcli/cmd_devices.go b/cmd/stcli/cmd_devices.go deleted file mode 100644 index 50c5e4c09..000000000 --- a/cmd/stcli/cmd_devices.go +++ /dev/null @@ -1,188 +0,0 @@ -// Copyright (C) 2014 Audrius Butkevičius - -package main - -import ( - "fmt" - "strings" - - "github.com/AudriusButkevicius/cli" - "github.com/syncthing/syncthing/lib/config" -) - -func init() { - cliCommands = append(cliCommands, cli.Command{ - Name: "devices", - HideHelp: true, - Usage: "Device command group", - Subcommands: []cli.Command{ - { - Name: "list", - Usage: "List registered devices", - Requires: &cli.Requires{}, - Action: devicesList, - }, - { - Name: "add", - Usage: "Add a new device", - Requires: &cli.Requires{"device id", "device name?"}, - Action: devicesAdd, - }, - { - Name: "remove", - Usage: "Remove an existing device", - Requires: &cli.Requires{"device id"}, - Action: devicesRemove, - }, - { - Name: "get", - Usage: "Get a property of a device", - Requires: &cli.Requires{"device id", "property"}, - Action: devicesGet, - }, - { - Name: "set", - Usage: "Set a property of a device", - Requires: &cli.Requires{"device id", "property", "value..."}, - Action: devicesSet, - }, - }, - }) -} - -func devicesList(c *cli.Context) { - cfg := getConfig(c) - first := true - writer := newTableWriter() - for _, device := range cfg.Devices { - if !first { - fmt.Fprintln(writer) - } - fmt.Fprintln(writer, "ID:\t", device.DeviceID, "\t") - fmt.Fprintln(writer, "Name:\t", device.Name, "\t(name)") - fmt.Fprintln(writer, "Address:\t", strings.Join(device.Addresses, " "), "\t(address)") - fmt.Fprintln(writer, "Compression:\t", device.Compression, "\t(compression)") - fmt.Fprintln(writer, "Certificate name:\t", device.CertName, "\t(certname)") - fmt.Fprintln(writer, "Introducer:\t", device.Introducer, "\t(introducer)") - first = false - } - writer.Flush() -} - -func devicesAdd(c *cli.Context) { - nid := c.Args()[0] - id := parseDeviceID(nid) - - newDevice := config.DeviceConfiguration{ - DeviceID: id, - Name: nid, - Addresses: []string{"dynamic"}, - } - - if len(c.Args()) > 1 { - newDevice.Name = c.Args()[1] - } - - if len(c.Args()) > 2 { - addresses := c.Args()[2:] - for _, item := range addresses { - if item == "dynamic" { - continue - } - validAddress(item) - } - newDevice.Addresses = addresses - } - - cfg := getConfig(c) - for _, device := range cfg.Devices { - if device.DeviceID == id { - die("Device " + nid + " already exists") - } - } - cfg.Devices = append(cfg.Devices, newDevice) - setConfig(c, cfg) -} - -func devicesRemove(c *cli.Context) { - nid := c.Args()[0] - id := parseDeviceID(nid) - if nid == getMyID(c) { - die("Cannot remove yourself") - } - cfg := getConfig(c) - for i, device := range cfg.Devices { - if device.DeviceID == id { - last := len(cfg.Devices) - 1 - cfg.Devices[i] = cfg.Devices[last] - cfg.Devices = cfg.Devices[:last] - setConfig(c, cfg) - return - } - } - die("Device " + nid + " not found") -} - -func devicesGet(c *cli.Context) { - nid := c.Args()[0] - id := parseDeviceID(nid) - arg := c.Args()[1] - cfg := getConfig(c) - for _, device := range cfg.Devices { - if device.DeviceID != id { - continue - } - switch strings.ToLower(arg) { - case "name": - fmt.Println(device.Name) - case "address": - fmt.Println(strings.Join(device.Addresses, "\n")) - case "compression": - fmt.Println(device.Compression.String()) - case "certname": - fmt.Println(device.CertName) - case "introducer": - fmt.Println(device.Introducer) - default: - die("Invalid property: " + arg + "\nAvailable properties: name, address, compression, certname, introducer") - } - return - } - die("Device " + nid + " not found") -} - -func devicesSet(c *cli.Context) { - nid := c.Args()[0] - id := parseDeviceID(nid) - arg := c.Args()[1] - config := getConfig(c) - for i, device := range config.Devices { - if device.DeviceID != id { - continue - } - switch strings.ToLower(arg) { - case "name": - config.Devices[i].Name = strings.Join(c.Args()[2:], " ") - case "address": - for _, item := range c.Args()[2:] { - if item == "dynamic" { - continue - } - validAddress(item) - } - config.Devices[i].Addresses = c.Args()[2:] - case "compression": - err := config.Devices[i].Compression.UnmarshalText([]byte(c.Args()[2])) - die(err) - case "certname": - config.Devices[i].CertName = strings.Join(c.Args()[2:], " ") - case "introducer": - config.Devices[i].Introducer = parseBool(c.Args()[2]) - default: - die("Invalid property: " + arg + "\nAvailable properties: name, address, compression, certname, introducer") - } - setConfig(c, config) - return - } - die("Device " + nid + " not found") -} diff --git a/cmd/stcli/cmd_errors.go b/cmd/stcli/cmd_errors.go deleted file mode 100644 index 0633fef3b..000000000 --- a/cmd/stcli/cmd_errors.go +++ /dev/null @@ -1,67 +0,0 @@ -// Copyright (C) 2014 Audrius Butkevičius - -package main - -import ( - "encoding/json" - "fmt" - "strings" - - "github.com/AudriusButkevicius/cli" -) - -func init() { - cliCommands = append(cliCommands, cli.Command{ - Name: "errors", - HideHelp: true, - Usage: "Error command group", - Subcommands: []cli.Command{ - { - Name: "show", - Usage: "Show pending errors", - Requires: &cli.Requires{}, - Action: errorsShow, - }, - { - Name: "push", - Usage: "Push an error to active clients", - Requires: &cli.Requires{"error message..."}, - Action: errorsPush, - }, - { - Name: "clear", - Usage: "Clear pending errors", - Requires: &cli.Requires{}, - Action: wrappedHTTPPost("system/error/clear"), - }, - }, - }) -} - -func errorsShow(c *cli.Context) { - response := httpGet(c, "system/error") - var data map[string][]map[string]interface{} - json.Unmarshal(responseToBArray(response), &data) - writer := newTableWriter() - for _, item := range data["errors"] { - time := item["when"].(string)[:19] - time = strings.Replace(time, "T", " ", 1) - err := item["message"].(string) - err = strings.TrimSpace(err) - fmt.Fprintln(writer, time+":\t"+err) - } - writer.Flush() -} - -func errorsPush(c *cli.Context) { - err := strings.Join(c.Args(), " ") - response := httpPost(c, "system/error", strings.TrimSpace(err)) - if response.StatusCode != 200 { - err = fmt.Sprint("Failed to push error\nStatus code: ", response.StatusCode) - body := string(responseToBArray(response)) - if body != "" { - err += "\nBody: " + body - } - die(err) - } -} diff --git a/cmd/stcli/cmd_folders.go b/cmd/stcli/cmd_folders.go deleted file mode 100644 index 3190a441a..000000000 --- a/cmd/stcli/cmd_folders.go +++ /dev/null @@ -1,361 +0,0 @@ -// Copyright (C) 2014 Audrius Butkevičius - -package main - -import ( - "fmt" - "path/filepath" - "strings" - - "github.com/AudriusButkevicius/cli" - "github.com/syncthing/syncthing/lib/config" - "github.com/syncthing/syncthing/lib/fs" -) - -func init() { - cliCommands = append(cliCommands, cli.Command{ - Name: "folders", - HideHelp: true, - Usage: "Folder command group", - Subcommands: []cli.Command{ - { - Name: "list", - Usage: "List available folders", - Requires: &cli.Requires{}, - Action: foldersList, - }, - { - Name: "add", - Usage: "Add a new folder", - Requires: &cli.Requires{"folder id", "directory"}, - Action: foldersAdd, - }, - { - Name: "remove", - Usage: "Remove an existing folder", - Requires: &cli.Requires{"folder id"}, - Action: foldersRemove, - }, - { - Name: "override", - Usage: "Override changes from other nodes for a master folder", - Requires: &cli.Requires{"folder id"}, - Action: foldersOverride, - }, - { - Name: "get", - Usage: "Get a property of a folder", - Requires: &cli.Requires{"folder id", "property"}, - Action: foldersGet, - }, - { - Name: "set", - Usage: "Set a property of a folder", - Requires: &cli.Requires{"folder id", "property", "value..."}, - Action: foldersSet, - }, - { - Name: "unset", - Usage: "Unset a property of a folder", - Requires: &cli.Requires{"folder id", "property"}, - Action: foldersUnset, - }, - { - Name: "devices", - Usage: "Folder devices command group", - HideHelp: true, - Subcommands: []cli.Command{ - { - Name: "list", - Usage: "List of devices which the folder is shared with", - Requires: &cli.Requires{"folder id"}, - Action: foldersDevicesList, - }, - { - Name: "add", - Usage: "Share a folder with a device", - Requires: &cli.Requires{"folder id", "device id"}, - Action: foldersDevicesAdd, - }, - { - Name: "remove", - Usage: "Unshare a folder with a device", - Requires: &cli.Requires{"folder id", "device id"}, - Action: foldersDevicesRemove, - }, - { - Name: "clear", - Usage: "Unshare a folder with all devices", - Requires: &cli.Requires{"folder id"}, - Action: foldersDevicesClear, - }, - }, - }, - }, - }) -} - -func foldersList(c *cli.Context) { - cfg := getConfig(c) - first := true - writer := newTableWriter() - for _, folder := range cfg.Folders { - if !first { - fmt.Fprintln(writer) - } - fs := folder.Filesystem() - fmt.Fprintln(writer, "ID:\t", folder.ID, "\t") - fmt.Fprintln(writer, "Path:\t", fs.URI(), "\t(directory)") - fmt.Fprintln(writer, "Path type:\t", fs.Type(), "\t(directory-type)") - fmt.Fprintln(writer, "Folder type:\t", folder.Type, "\t(type)") - fmt.Fprintln(writer, "Ignore permissions:\t", folder.IgnorePerms, "\t(permissions)") - fmt.Fprintln(writer, "Rescan interval in seconds:\t", folder.RescanIntervalS, "\t(rescan)") - - if folder.Versioning.Type != "" { - fmt.Fprintln(writer, "Versioning:\t", folder.Versioning.Type, "\t(versioning)") - for key, value := range folder.Versioning.Params { - fmt.Fprintf(writer, "Versioning %s:\t %s \t(versioning-%s)\n", key, value, key) - } - } - first = false - } - writer.Flush() -} - -func foldersAdd(c *cli.Context) { - cfg := getConfig(c) - abs, err := filepath.Abs(c.Args()[1]) - die(err) - folder := config.FolderConfiguration{ - ID: c.Args()[0], - Path: filepath.Clean(abs), - FilesystemType: fs.FilesystemTypeBasic, - } - cfg.Folders = append(cfg.Folders, folder) - setConfig(c, cfg) -} - -func foldersRemove(c *cli.Context) { - cfg := getConfig(c) - rid := c.Args()[0] - for i, folder := range cfg.Folders { - if folder.ID == rid { - last := len(cfg.Folders) - 1 - cfg.Folders[i] = cfg.Folders[last] - cfg.Folders = cfg.Folders[:last] - setConfig(c, cfg) - return - } - } - die("Folder " + rid + " not found") -} - -func foldersOverride(c *cli.Context) { - cfg := getConfig(c) - rid := c.Args()[0] - for _, folder := range cfg.Folders { - if folder.ID == rid && folder.Type == config.FolderTypeSendOnly { - response := httpPost(c, "db/override", "") - if response.StatusCode != 200 { - err := fmt.Sprint("Failed to override changes\nStatus code: ", response.StatusCode) - body := string(responseToBArray(response)) - if body != "" { - err += "\nBody: " + body - } - die(err) - } - return - } - } - die("Folder " + rid + " not found or folder not master") -} - -func foldersGet(c *cli.Context) { - cfg := getConfig(c) - rid := c.Args()[0] - arg := strings.ToLower(c.Args()[1]) - for _, folder := range cfg.Folders { - if folder.ID != rid { - continue - } - if strings.HasPrefix(arg, "versioning-") { - arg = arg[11:] - value, ok := folder.Versioning.Params[arg] - if ok { - fmt.Println(value) - return - } - die("Versioning property " + c.Args()[1][11:] + " not found") - } - switch arg { - case "directory": - fmt.Println(folder.Filesystem().URI()) - case "directory-type": - fmt.Println(folder.Filesystem().Type()) - case "type": - fmt.Println(folder.Type) - case "permissions": - fmt.Println(folder.IgnorePerms) - case "rescan": - fmt.Println(folder.RescanIntervalS) - case "versioning": - if folder.Versioning.Type != "" { - fmt.Println(folder.Versioning.Type) - } - default: - die("Invalid property: " + c.Args()[1] + "\nAvailable properties: directory, directory-type, type, permissions, versioning, versioning-") - } - return - } - die("Folder " + rid + " not found") -} - -func foldersSet(c *cli.Context) { - rid := c.Args()[0] - arg := strings.ToLower(c.Args()[1]) - val := strings.Join(c.Args()[2:], " ") - cfg := getConfig(c) - for i, folder := range cfg.Folders { - if folder.ID != rid { - continue - } - if strings.HasPrefix(arg, "versioning-") { - cfg.Folders[i].Versioning.Params[arg[11:]] = val - setConfig(c, cfg) - return - } - switch arg { - case "directory": - cfg.Folders[i].Path = val - case "directory-type": - var fsType fs.FilesystemType - fsType.UnmarshalText([]byte(val)) - cfg.Folders[i].FilesystemType = fsType - case "type": - var t config.FolderType - if err := t.UnmarshalText([]byte(val)); err != nil { - die("Invalid folder type: " + err.Error()) - } - cfg.Folders[i].Type = t - case "permissions": - cfg.Folders[i].IgnorePerms = parseBool(val) - case "rescan": - cfg.Folders[i].RescanIntervalS = parseInt(val) - case "versioning": - cfg.Folders[i].Versioning.Type = val - default: - die("Invalid property: " + c.Args()[1] + "\nAvailable properties: directory, master, permissions, versioning, versioning-") - } - setConfig(c, cfg) - return - } - die("Folder " + rid + " not found") -} - -func foldersUnset(c *cli.Context) { - rid := c.Args()[0] - arg := strings.ToLower(c.Args()[1]) - cfg := getConfig(c) - for i, folder := range cfg.Folders { - if folder.ID != rid { - continue - } - if strings.HasPrefix(arg, "versioning-") { - arg = arg[11:] - if _, ok := folder.Versioning.Params[arg]; ok { - delete(cfg.Folders[i].Versioning.Params, arg) - setConfig(c, cfg) - return - } - die("Versioning property " + c.Args()[1][11:] + " not found") - } - switch arg { - case "versioning": - cfg.Folders[i].Versioning.Type = "" - cfg.Folders[i].Versioning.Params = make(map[string]string) - default: - die("Invalid property: " + c.Args()[1] + "\nAvailable properties: versioning, versioning-") - } - setConfig(c, cfg) - return - } - die("Folder " + rid + " not found") -} - -func foldersDevicesList(c *cli.Context) { - rid := c.Args()[0] - cfg := getConfig(c) - for _, folder := range cfg.Folders { - if folder.ID != rid { - continue - } - for _, device := range folder.Devices { - fmt.Println(device.DeviceID) - } - return - } - die("Folder " + rid + " not found") -} - -func foldersDevicesAdd(c *cli.Context) { - rid := c.Args()[0] - nid := parseDeviceID(c.Args()[1]) - cfg := getConfig(c) - for i, folder := range cfg.Folders { - if folder.ID != rid { - continue - } - for _, device := range folder.Devices { - if device.DeviceID == nid { - die("Device " + c.Args()[1] + " is already part of this folder") - } - } - for _, device := range cfg.Devices { - if device.DeviceID == nid { - cfg.Folders[i].Devices = append(folder.Devices, config.FolderDeviceConfiguration{ - DeviceID: device.DeviceID, - }) - setConfig(c, cfg) - return - } - } - die("Device " + c.Args()[1] + " not found in device list") - } - die("Folder " + rid + " not found") -} - -func foldersDevicesRemove(c *cli.Context) { - rid := c.Args()[0] - nid := parseDeviceID(c.Args()[1]) - cfg := getConfig(c) - for ri, folder := range cfg.Folders { - if folder.ID != rid { - continue - } - for ni, device := range folder.Devices { - if device.DeviceID == nid { - last := len(folder.Devices) - 1 - cfg.Folders[ri].Devices[ni] = folder.Devices[last] - cfg.Folders[ri].Devices = cfg.Folders[ri].Devices[:last] - setConfig(c, cfg) - return - } - } - die("Device " + c.Args()[1] + " not found") - } - die("Folder " + rid + " not found") -} - -func foldersDevicesClear(c *cli.Context) { - rid := c.Args()[0] - cfg := getConfig(c) - for i, folder := range cfg.Folders { - if folder.ID != rid { - continue - } - cfg.Folders[i].Devices = []config.FolderDeviceConfiguration{} - setConfig(c, cfg) - return - } - die("Folder " + rid + " not found") -} diff --git a/cmd/stcli/cmd_general.go b/cmd/stcli/cmd_general.go deleted file mode 100644 index 581f7a3b0..000000000 --- a/cmd/stcli/cmd_general.go +++ /dev/null @@ -1,94 +0,0 @@ -// Copyright (C) 2014 Audrius Butkevičius - -package main - -import ( - "encoding/json" - "fmt" - "os" - - "github.com/AudriusButkevicius/cli" -) - -func init() { - cliCommands = append(cliCommands, []cli.Command{ - { - Name: "id", - Usage: "Get ID of the Syncthing client", - Requires: &cli.Requires{}, - Action: generalID, - }, - { - Name: "status", - Usage: "Configuration status, whether or not a restart is required for changes to take effect", - Requires: &cli.Requires{}, - Action: generalStatus, - }, - { - Name: "config", - Usage: "Configuration", - Requires: &cli.Requires{}, - Action: generalConfiguration, - }, - { - Name: "restart", - Usage: "Restart syncthing", - Requires: &cli.Requires{}, - Action: wrappedHTTPPost("system/restart"), - }, - { - Name: "shutdown", - Usage: "Shutdown syncthing", - Requires: &cli.Requires{}, - Action: wrappedHTTPPost("system/shutdown"), - }, - { - Name: "reset", - Usage: "Reset syncthing deleting all folders and devices", - Requires: &cli.Requires{}, - Action: wrappedHTTPPost("system/reset"), - }, - { - Name: "upgrade", - Usage: "Upgrade syncthing (if a newer version is available)", - Requires: &cli.Requires{}, - Action: wrappedHTTPPost("system/upgrade"), - }, - { - Name: "version", - Usage: "Syncthing client version", - Requires: &cli.Requires{}, - Action: generalVersion, - }, - }...) -} - -func generalID(c *cli.Context) { - fmt.Println(getMyID(c)) -} - -func generalStatus(c *cli.Context) { - response := httpGet(c, "system/config/insync") - var status struct{ ConfigInSync bool } - json.Unmarshal(responseToBArray(response), &status) - if !status.ConfigInSync { - die("Config out of sync") - } - fmt.Println("Config in sync") -} - -func generalConfiguration(c *cli.Context) { - response := httpGet(c, "system/config") - var jsResponse interface{} - json.Unmarshal(responseToBArray(response), &jsResponse) - enc := json.NewEncoder(os.Stdout) - enc.SetIndent("", " ") - enc.Encode(jsResponse) -} - -func generalVersion(c *cli.Context) { - response := httpGet(c, "system/version") - version := make(map[string]interface{}) - json.Unmarshal(responseToBArray(response), &version) - prettyPrintJSON(version) -} diff --git a/cmd/stcli/cmd_gui.go b/cmd/stcli/cmd_gui.go deleted file mode 100644 index 8ff222c6d..000000000 --- a/cmd/stcli/cmd_gui.go +++ /dev/null @@ -1,127 +0,0 @@ -// Copyright (C) 2014 Audrius Butkevičius - -package main - -import ( - "fmt" - "strings" - - "github.com/AudriusButkevicius/cli" -) - -func init() { - cliCommands = append(cliCommands, cli.Command{ - Name: "gui", - HideHelp: true, - Usage: "GUI command group", - Subcommands: []cli.Command{ - { - Name: "dump", - Usage: "Show all GUI configuration settings", - Requires: &cli.Requires{}, - Action: guiDump, - }, - { - Name: "get", - Usage: "Get a GUI configuration setting", - Requires: &cli.Requires{"setting"}, - Action: guiGet, - }, - { - Name: "set", - Usage: "Set a GUI configuration setting", - Requires: &cli.Requires{"setting", "value"}, - Action: guiSet, - }, - { - Name: "unset", - Usage: "Unset a GUI configuration setting", - Requires: &cli.Requires{"setting"}, - Action: guiUnset, - }, - }, - }) -} - -func guiDump(c *cli.Context) { - cfg := getConfig(c).GUI - writer := newTableWriter() - fmt.Fprintln(writer, "Enabled:\t", cfg.Enabled, "\t(enabled)") - fmt.Fprintln(writer, "Use HTTPS:\t", cfg.UseTLS(), "\t(tls)") - fmt.Fprintln(writer, "Listen Addresses:\t", cfg.Address(), "\t(address)") - if cfg.User != "" { - fmt.Fprintln(writer, "Authentication User:\t", cfg.User, "\t(username)") - fmt.Fprintln(writer, "Authentication Password:\t", cfg.Password, "\t(password)") - } - if cfg.APIKey != "" { - fmt.Fprintln(writer, "API Key:\t", cfg.APIKey, "\t(apikey)") - } - writer.Flush() -} - -func guiGet(c *cli.Context) { - cfg := getConfig(c).GUI - arg := c.Args()[0] - switch strings.ToLower(arg) { - case "enabled": - fmt.Println(cfg.Enabled) - case "tls": - fmt.Println(cfg.UseTLS()) - case "address": - fmt.Println(cfg.Address()) - case "user": - if cfg.User != "" { - fmt.Println(cfg.User) - } - case "password": - if cfg.User != "" { - fmt.Println(cfg.Password) - } - case "apikey": - if cfg.APIKey != "" { - fmt.Println(cfg.APIKey) - } - default: - die("Invalid setting: " + arg + "\nAvailable settings: enabled, tls, address, user, password, apikey") - } -} - -func guiSet(c *cli.Context) { - cfg := getConfig(c) - arg := c.Args()[0] - val := c.Args()[1] - switch strings.ToLower(arg) { - case "enabled": - cfg.GUI.Enabled = parseBool(val) - case "tls": - cfg.GUI.RawUseTLS = parseBool(val) - case "address": - validAddress(val) - cfg.GUI.RawAddress = val - case "user": - cfg.GUI.User = val - case "password": - cfg.GUI.Password = val - case "apikey": - cfg.GUI.APIKey = val - default: - die("Invalid setting: " + arg + "\nAvailable settings: enabled, tls, address, user, password, apikey") - } - setConfig(c, cfg) -} - -func guiUnset(c *cli.Context) { - cfg := getConfig(c) - arg := c.Args()[0] - switch strings.ToLower(arg) { - case "user": - cfg.GUI.User = "" - case "password": - cfg.GUI.Password = "" - case "apikey": - cfg.GUI.APIKey = "" - default: - die("Invalid setting: " + arg + "\nAvailable settings: user, password, apikey") - } - setConfig(c, cfg) -} diff --git a/cmd/stcli/cmd_options.go b/cmd/stcli/cmd_options.go deleted file mode 100644 index 49178140c..000000000 --- a/cmd/stcli/cmd_options.go +++ /dev/null @@ -1,173 +0,0 @@ -// Copyright (C) 2014 Audrius Butkevičius - -package main - -import ( - "fmt" - "strings" - - "github.com/AudriusButkevicius/cli" -) - -func init() { - cliCommands = append(cliCommands, cli.Command{ - Name: "options", - HideHelp: true, - Usage: "Options command group", - Subcommands: []cli.Command{ - { - Name: "dump", - Usage: "Show all Syncthing option settings", - Requires: &cli.Requires{}, - Action: optionsDump, - }, - { - Name: "get", - Usage: "Get a Syncthing option setting", - Requires: &cli.Requires{"setting"}, - Action: optionsGet, - }, - { - Name: "set", - Usage: "Set a Syncthing option setting", - Requires: &cli.Requires{"setting", "value..."}, - Action: optionsSet, - }, - }, - }) -} - -func optionsDump(c *cli.Context) { - cfg := getConfig(c).Options - writer := newTableWriter() - - fmt.Fprintln(writer, "Sync protocol listen addresses:\t", strings.Join(cfg.ListenAddresses, " "), "\t(addresses)") - fmt.Fprintln(writer, "Global discovery enabled:\t", cfg.GlobalAnnEnabled, "\t(globalannenabled)") - fmt.Fprintln(writer, "Global discovery servers:\t", strings.Join(cfg.GlobalAnnServers, " "), "\t(globalannserver)") - - fmt.Fprintln(writer, "Local discovery enabled:\t", cfg.LocalAnnEnabled, "\t(localannenabled)") - fmt.Fprintln(writer, "Local discovery port:\t", cfg.LocalAnnPort, "\t(localannport)") - - fmt.Fprintln(writer, "Outgoing rate limit in KiB/s:\t", cfg.MaxSendKbps, "\t(maxsend)") - fmt.Fprintln(writer, "Incoming rate limit in KiB/s:\t", cfg.MaxRecvKbps, "\t(maxrecv)") - fmt.Fprintln(writer, "Reconnect interval in seconds:\t", cfg.ReconnectIntervalS, "\t(reconnect)") - fmt.Fprintln(writer, "Start browser:\t", cfg.StartBrowser, "\t(browser)") - fmt.Fprintln(writer, "Enable UPnP:\t", cfg.NATEnabled, "\t(nat)") - fmt.Fprintln(writer, "UPnP Lease in minutes:\t", cfg.NATLeaseM, "\t(natlease)") - fmt.Fprintln(writer, "UPnP Renewal period in minutes:\t", cfg.NATRenewalM, "\t(natrenew)") - fmt.Fprintln(writer, "Restart on Wake Up:\t", cfg.RestartOnWakeup, "\t(wake)") - - reporting := "unrecognized value" - switch cfg.URAccepted { - case -1: - reporting = "false" - case 0: - reporting = "undecided/false" - case 1: - reporting = "true" - } - fmt.Fprintln(writer, "Anonymous usage reporting:\t", reporting, "\t(reporting)") - - writer.Flush() -} - -func optionsGet(c *cli.Context) { - cfg := getConfig(c).Options - arg := c.Args()[0] - switch strings.ToLower(arg) { - case "address": - fmt.Println(strings.Join(cfg.ListenAddresses, "\n")) - case "globalannenabled": - fmt.Println(cfg.GlobalAnnEnabled) - case "globalannservers": - fmt.Println(strings.Join(cfg.GlobalAnnServers, "\n")) - case "localannenabled": - fmt.Println(cfg.LocalAnnEnabled) - case "localannport": - fmt.Println(cfg.LocalAnnPort) - case "maxsend": - fmt.Println(cfg.MaxSendKbps) - case "maxrecv": - fmt.Println(cfg.MaxRecvKbps) - case "reconnect": - fmt.Println(cfg.ReconnectIntervalS) - case "browser": - fmt.Println(cfg.StartBrowser) - case "nat": - fmt.Println(cfg.NATEnabled) - case "natlease": - fmt.Println(cfg.NATLeaseM) - case "natrenew": - fmt.Println(cfg.NATRenewalM) - case "reporting": - switch cfg.URAccepted { - case -1: - fmt.Println("false") - case 0: - fmt.Println("undecided/false") - case 1: - fmt.Println("true") - default: - fmt.Println("unknown") - } - case "wake": - fmt.Println(cfg.RestartOnWakeup) - default: - die("Invalid setting: " + arg + "\nAvailable settings: address, globalannenabled, globalannserver, localannenabled, localannport, maxsend, maxrecv, reconnect, browser, upnp, upnplease, upnprenew, reporting, wake") - } -} - -func optionsSet(c *cli.Context) { - config := getConfig(c) - arg := c.Args()[0] - val := c.Args()[1] - switch strings.ToLower(arg) { - case "address": - for _, item := range c.Args().Tail() { - validAddress(item) - } - config.Options.ListenAddresses = c.Args().Tail() - case "globalannenabled": - config.Options.GlobalAnnEnabled = parseBool(val) - case "globalannserver": - for _, item := range c.Args().Tail() { - validAddress(item) - } - config.Options.GlobalAnnServers = c.Args().Tail() - case "localannenabled": - config.Options.LocalAnnEnabled = parseBool(val) - case "localannport": - config.Options.LocalAnnPort = parsePort(val) - case "maxsend": - config.Options.MaxSendKbps = parseUint(val) - case "maxrecv": - config.Options.MaxRecvKbps = parseUint(val) - case "reconnect": - config.Options.ReconnectIntervalS = parseUint(val) - case "browser": - config.Options.StartBrowser = parseBool(val) - case "nat": - config.Options.NATEnabled = parseBool(val) - case "natlease": - config.Options.NATLeaseM = parseUint(val) - case "natrenew": - config.Options.NATRenewalM = parseUint(val) - case "reporting": - switch strings.ToLower(val) { - case "u", "undecided", "unset": - config.Options.URAccepted = 0 - default: - boolvalue := parseBool(val) - if boolvalue { - config.Options.URAccepted = 1 - } else { - config.Options.URAccepted = -1 - } - } - case "wake": - config.Options.RestartOnWakeup = parseBool(val) - default: - die("Invalid setting: " + arg + "\nAvailable settings: address, globalannenabled, globalannserver, localannenabled, localannport, maxsend, maxrecv, reconnect, browser, upnp, upnplease, upnprenew, reporting, wake") - } - setConfig(c, config) -} diff --git a/cmd/stcli/cmd_report.go b/cmd/stcli/cmd_report.go deleted file mode 100644 index d86540e5a..000000000 --- a/cmd/stcli/cmd_report.go +++ /dev/null @@ -1,72 +0,0 @@ -// Copyright (C) 2014 Audrius Butkevičius - -package main - -import ( - "encoding/json" - "fmt" - - "github.com/AudriusButkevicius/cli" -) - -func init() { - cliCommands = append(cliCommands, cli.Command{ - Name: "report", - HideHelp: true, - Usage: "Reporting command group", - Subcommands: []cli.Command{ - { - Name: "system", - Usage: "Report system state", - Requires: &cli.Requires{}, - Action: reportSystem, - }, - { - Name: "connections", - Usage: "Report about connections to other devices", - Requires: &cli.Requires{}, - Action: reportConnections, - }, - { - Name: "usage", - Usage: "Usage report", - Requires: &cli.Requires{}, - Action: reportUsage, - }, - }, - }) -} - -func reportSystem(c *cli.Context) { - response := httpGet(c, "system/status") - data := make(map[string]interface{}) - json.Unmarshal(responseToBArray(response), &data) - prettyPrintJSON(data) -} - -func reportConnections(c *cli.Context) { - response := httpGet(c, "system/connections") - data := make(map[string]map[string]interface{}) - json.Unmarshal(responseToBArray(response), &data) - var overall map[string]interface{} - for key, value := range data { - if key == "total" { - overall = value - continue - } - value["Device ID"] = key - prettyPrintJSON(value) - fmt.Println() - } - if overall != nil { - fmt.Println("=== Overall statistics ===") - prettyPrintJSON(overall) - } -} - -func reportUsage(c *cli.Context) { - response := httpGet(c, "svc/report") - report := make(map[string]interface{}) - json.Unmarshal(responseToBArray(response), &report) - prettyPrintJSON(report) -} diff --git a/cmd/stcli/errors.go b/cmd/stcli/errors.go new file mode 100644 index 000000000..a535e9f37 --- /dev/null +++ b/cmd/stcli/errors.go @@ -0,0 +1,60 @@ +// Copyright (C) 2019 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +package main + +import ( + "fmt" + "strings" + + "github.com/urfave/cli" +) + +var errorsCommand = cli.Command{ + Name: "errors", + HideHelp: true, + Usage: "Error command group", + Subcommands: []cli.Command{ + { + Name: "show", + Usage: "Show pending errors", + Action: expects(0, dumpOutput("system/error")), + }, + { + Name: "push", + Usage: "Push an error to active clients", + ArgsUsage: "[error message]", + Action: expects(1, errorsPush), + }, + { + Name: "clear", + Usage: "Clear pending errors", + Action: expects(0, emptyPost("system/error/clear")), + }, + }, +} + +func errorsPush(c *cli.Context) error { + client := c.App.Metadata["client"].(*APIClient) + errStr := strings.Join(c.Args(), " ") + response, err := client.Post("system/error", strings.TrimSpace(errStr)) + if err != nil { + return err + } + if response.StatusCode != 200 { + errStr = fmt.Sprint("Failed to push error\nStatus code: ", response.StatusCode) + bytes, err := responseToBArray(response) + if err != nil { + return err + } + body := string(bytes) + if body != "" { + errStr += "\nBody: " + body + } + return fmt.Errorf(errStr) + } + return nil +} diff --git a/cmd/stcli/labels.go b/cmd/stcli/labels.go deleted file mode 100644 index d05d6b1f4..000000000 --- a/cmd/stcli/labels.go +++ /dev/null @@ -1,31 +0,0 @@ -// Copyright (C) 2014 Audrius Butkevičius - -package main - -var jsonAttributeLabels = map[string]string{ - "folderMaxMiB": "Largest folder size in MiB", - "folderMaxFiles": "Largest folder file count", - "longVersion": "Long version", - "totMiB": "Total size in MiB", - "totFiles": "Total files", - "uniqueID": "Unique ID", - "numFolders": "Folder count", - "numDevices": "Device count", - "memoryUsageMiB": "Memory usage in MiB", - "memorySize": "Total memory in MiB", - "sha256Perf": "SHA256 Benchmark", - "At": "Last contacted", - "Completion": "Percent complete", - "InBytesTotal": "Total bytes received", - "OutBytesTotal": "Total bytes sent", - "ClientVersion": "Client version", - "alloc": "Memory allocated in bytes", - "sys": "Memory using in bytes", - "cpuPercent": "CPU load in percent", - "extAnnounceOK": "External announcments working", - "goroutines": "Number of Go routines", - "myID": "Client ID", - "tilde": "Tilde expands to", - "arch": "Architecture", - "os": "OS", -} diff --git a/cmd/stcli/main.go b/cmd/stcli/main.go index 7ae5c2bd8..920778479 100644 --- a/cmd/stcli/main.go +++ b/cmd/stcli/main.go @@ -1,63 +1,192 @@ -// Copyright (C) 2014 Audrius Butkevičius +// Copyright (C) 2019 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. package main import ( - "sort" + "bufio" + "crypto/tls" + "encoding/json" + "flag" + "log" + "os" + "reflect" + "strings" - "github.com/AudriusButkevicius/cli" + "github.com/AudriusButkevicius/recli" + "github.com/flynn-archive/go-shlex" + "github.com/mattn/go-isatty" + "github.com/pkg/errors" + "github.com/syncthing/syncthing/lib/build" + "github.com/syncthing/syncthing/lib/config" + "github.com/syncthing/syncthing/lib/locations" + "github.com/syncthing/syncthing/lib/protocol" + "github.com/urfave/cli" ) -type ByAlphabet []cli.Command - -func (a ByAlphabet) Len() int { return len(a) } -func (a ByAlphabet) Swap(i, j int) { a[i], a[j] = a[j], a[i] } -func (a ByAlphabet) Less(i, j int) bool { return a[i].Name < a[j].Name } - -var cliCommands []cli.Command - func main() { - app := cli.NewApp() - app.Name = "syncthing-cli" - app.Author = "Audrius Butkevičius" - app.Email = "audrius.butkevicius@gmail.com" - app.Usage = "Syncthing command line interface" - app.Version = "0.1" - app.HideHelp = true + // This is somewhat a hack around a chicken and egg problem. + // We need to set the home directory and potentially other flags to know where the syncthing instance is running + // in order to get it's config ... which we then use to construct the actual CLI ... at which point it's too late + // to add flags there... + homeBaseDir := locations.GetBaseDir(locations.ConfigBaseDir) + guiCfg := config.GUIConfiguration{} - app.Flags = []cli.Flag{ + flags := flag.NewFlagSet("", flag.ContinueOnError) + flags.StringVar(&guiCfg.RawAddress, "gui-address", guiCfg.RawAddress, "Override GUI address (e.g. \"http://192.0.2.42:8443\")") + flags.StringVar(&guiCfg.APIKey, "gui-apikey", guiCfg.APIKey, "Override GUI API key") + flags.StringVar(&homeBaseDir, "home", homeBaseDir, "Set configuration directory") + + // Implement the same flags at the lower CLI, with the same default values (pre-parse), but do nothing with them. + // This is so that we could reuse os.Args + fakeFlags := []cli.Flag{ cli.StringFlag{ - Name: "endpoint, e", - Value: "http://127.0.0.1:8384", - Usage: "End point to connect to", - EnvVar: "STENDPOINT", + Name: "gui-address", + Value: guiCfg.RawAddress, + Usage: "Override GUI address (e.g. \"http://192.0.2.42:8443\")", }, cli.StringFlag{ - Name: "apikey, k", - Value: "", - Usage: "API Key", - EnvVar: "STAPIKEY", + Name: "gui-apikey", + Value: guiCfg.APIKey, + Usage: "Override GUI API key", }, cli.StringFlag{ - Name: "username, u", - Value: "", - Usage: "Username", - EnvVar: "STUSERNAME", - }, - cli.StringFlag{ - Name: "password, p", - Value: "", - Usage: "Password", - EnvVar: "STPASSWORD", - }, - cli.BoolFlag{ - Name: "insecure, i", - Usage: "Do not verify SSL certificate", - EnvVar: "STINSECURE", + Name: "home", + Value: homeBaseDir, + Usage: "Set configuration directory", }, } - sort.Sort(ByAlphabet(cliCommands)) - app.Commands = cliCommands - app.RunAndExitOnError() + // Do not print usage of these flags, and ignore errors as this can't understand plenty of things + flags.Usage = func() {} + _ = flags.Parse(os.Args[1:]) + + // Now if the API key and address is not provided (we are not connecting to a remote instance), + // try to rip it out of the config. + if guiCfg.RawAddress == "" && guiCfg.APIKey == "" { + // Update the base directory + err := locations.SetBaseDir(locations.ConfigBaseDir, homeBaseDir) + if err != nil { + log.Fatal(errors.Wrap(err, "setting home")) + } + + // Load the certs and get the ID + cert, err := tls.LoadX509KeyPair( + locations.Get(locations.CertFile), + locations.Get(locations.KeyFile), + ) + if err != nil { + log.Fatal(errors.Wrap(err, "reading device ID")) + } + + myID := protocol.NewDeviceID(cert.Certificate[0]) + + // Load the config + cfg, err := config.Load(locations.Get(locations.ConfigFile), myID) + if err != nil { + log.Fatalln(errors.Wrap(err, "loading config")) + } + + guiCfg = cfg.GUI() + } else if guiCfg.Address() == "" || guiCfg.APIKey == "" { + log.Fatalln("Both -gui-address and -gui-apikey should be specified") + } + + if guiCfg.Address() == "" { + log.Fatalln("Could not find GUI Address") + } + + if guiCfg.APIKey == "" { + log.Fatalln("Could not find GUI API key") + } + + client := getClient(guiCfg) + + cfg, err := getConfig(client) + original := cfg.Copy() + if err != nil { + log.Fatalln(errors.Wrap(err, "getting config")) + } + + // Copy the config and set the default flags + recliCfg := recli.DefaultConfig + recliCfg.IDTag.Name = "xml" + recliCfg.SkipTag.Name = "json" + + commands, err := recli.New(recliCfg).Construct(&cfg) + if err != nil { + log.Fatalln(errors.Wrap(err, "config reflect")) + } + + // Construct the actual CLI + app := cli.NewApp() + app.Name = "stcli" + app.HelpName = app.Name + app.Author = "The Syncthing Authors" + app.Usage = "Syncthing command line interface" + app.Version = strings.Replace(build.LongVersion, "syncthing", app.Name, 1) + app.Flags = fakeFlags + app.Metadata = map[string]interface{}{ + "client": client, + } + app.Commands = []cli.Command{ + { + Name: "config", + HideHelp: true, + Usage: "Configuration modification command group", + Subcommands: commands, + }, + showCommand, + operationCommand, + errorsCommand, + } + + tty := isatty.IsTerminal(os.Stdin.Fd()) || isatty.IsCygwinTerminal(os.Stdin.Fd()) + if !tty { + // Not a TTY, consume from stdin + scanner := bufio.NewScanner(os.Stdin) + for scanner.Scan() { + input, err := shlex.Split(scanner.Text()) + if err != nil { + log.Fatalln(errors.Wrap(err, "parsing input")) + } + if len(input) == 0 { + continue + } + err = app.Run(append(os.Args, input...)) + if err != nil { + log.Fatalln(err) + } + } + err = scanner.Err() + if err != nil { + log.Fatalln(err) + } + } else { + err = app.Run(os.Args) + if err != nil { + log.Fatalln(err) + } + } + + if !reflect.DeepEqual(cfg, original) { + body, err := json.MarshalIndent(cfg, "", " ") + if err != nil { + log.Fatalln(err) + } + resp, err := client.Post("system/config", string(body)) + if err != nil { + log.Fatalln(err) + } + if resp.StatusCode != 200 { + body, err := responseToBArray(resp) + if err != nil { + log.Fatalln(err) + } + log.Fatalln(string(body)) + } + } } diff --git a/cmd/stcli/operations.go b/cmd/stcli/operations.go new file mode 100644 index 000000000..9efdb7996 --- /dev/null +++ b/cmd/stcli/operations.go @@ -0,0 +1,78 @@ +// Copyright (C) 2019 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +package main + +import ( + "fmt" + + "github.com/urfave/cli" +) + +var operationCommand = cli.Command{ + Name: "operations", + HideHelp: true, + Usage: "Operation command group", + Subcommands: []cli.Command{ + { + Name: "restart", + Usage: "Restart syncthing", + Action: expects(0, emptyPost("system/restart")), + }, + { + Name: "shutdown", + Usage: "Shutdown syncthing", + Action: expects(0, emptyPost("system/shutdown")), + }, + { + Name: "reset", + Usage: "Reset syncthing deleting all folders and devices", + Action: expects(0, emptyPost("system/reset")), + }, + { + Name: "upgrade", + Usage: "Upgrade syncthing (if a newer version is available)", + Action: expects(0, emptyPost("system/upgrade")), + }, + { + Name: "folder-override", + Usage: "Override changes on folder (remote for sendonly, local for receiveonly)", + ArgsUsage: "[folder id]", + Action: expects(1, foldersOverride), + }, + }, +} + +func foldersOverride(c *cli.Context) error { + client := c.App.Metadata["client"].(*APIClient) + cfg, err := getConfig(client) + if err != nil { + return err + } + rid := c.Args()[0] + for _, folder := range cfg.Folders { + if folder.ID == rid { + response, err := client.Post("db/override", "") + if err != nil { + return err + } + if response.StatusCode != 200 { + errStr := fmt.Sprint("Failed to override changes\nStatus code: ", response.StatusCode) + bytes, err := responseToBArray(response) + if err != nil { + return err + } + body := string(bytes) + if body != "" { + errStr += "\nBody: " + body + } + return fmt.Errorf(errStr) + } + return nil + } + } + return fmt.Errorf("Folder " + rid + " not found") +} diff --git a/cmd/stcli/show.go b/cmd/stcli/show.go new file mode 100644 index 000000000..9ae318296 --- /dev/null +++ b/cmd/stcli/show.go @@ -0,0 +1,44 @@ +// Copyright (C) 2019 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +package main + +import ( + "github.com/urfave/cli" +) + +var showCommand = cli.Command{ + Name: "show", + HideHelp: true, + Usage: "Show command group", + Subcommands: []cli.Command{ + { + Name: "version", + Usage: "Show syncthing client version", + Action: expects(0, dumpOutput("system/version")), + }, + { + Name: "config-status", + Usage: "Show configuration status, whether or not a restart is required for changes to take effect", + Action: expects(0, dumpOutput("system/config/insync")), + }, + { + Name: "system", + Usage: "Show system status", + Action: expects(0, dumpOutput("system/status")), + }, + { + Name: "connections", + Usage: "Report about connections to other devices", + Action: expects(0, dumpOutput("system/connections")), + }, + { + Name: "usage", + Usage: "Show usage report", + Action: expects(0, dumpOutput("svc/report")), + }, + }, +} diff --git a/cmd/stcli/utils.go b/cmd/stcli/utils.go index f9c3fbb1f..0076e6a5f 100644 --- a/cmd/stcli/utils.go +++ b/cmd/stcli/utils.go @@ -1,4 +1,8 @@ -// Copyright (C) 2014 Audrius Butkevičius +// Copyright (C) 2019 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. package main @@ -8,78 +12,37 @@ import ( "io/ioutil" "net/http" "os" - "regexp" - "sort" - "strconv" - "strings" "text/tabwriter" - "unicode" - "github.com/AudriusButkevicius/cli" "github.com/syncthing/syncthing/lib/config" - "github.com/syncthing/syncthing/lib/protocol" + "github.com/urfave/cli" ) -func responseToBArray(response *http.Response) []byte { - defer response.Body.Close() +func responseToBArray(response *http.Response) ([]byte, error) { bytes, err := ioutil.ReadAll(response.Body) if err != nil { - die(err) + return nil, err } - return bytes + return bytes, response.Body.Close() } -func die(vals ...interface{}) { - if len(vals) > 1 || vals[0] != nil { - os.Stderr.WriteString(fmt.Sprintln(vals...)) - os.Exit(1) +func emptyPost(url string) cli.ActionFunc { + return func(c *cli.Context) error { + client := c.App.Metadata["client"].(*APIClient) + _, err := client.Post(url, "") + return err } } -func wrappedHTTPPost(url string) func(c *cli.Context) { - return func(c *cli.Context) { - httpPost(c, url, "") - } -} - -func prettyPrintJSON(json map[string]interface{}) { - writer := newTableWriter() - remap := make(map[string]interface{}) - for k, v := range json { - key, ok := jsonAttributeLabels[k] - if !ok { - key = firstUpper(k) +func dumpOutput(url string) cli.ActionFunc { + return func(c *cli.Context) error { + client := c.App.Metadata["client"].(*APIClient) + response, err := client.Get(url) + if err != nil { + return err } - remap[key] = v + return prettyPrintResponse(c, response) } - - jsonKeys := make([]string, 0, len(remap)) - for key := range remap { - jsonKeys = append(jsonKeys, key) - } - sort.Strings(jsonKeys) - for _, k := range jsonKeys { - var value string - rvalue := remap[k] - switch rvalue.(type) { - case int, int16, int32, int64, uint, uint16, uint32, uint64, float32, float64: - value = fmt.Sprintf("%.0f", rvalue) - default: - value = fmt.Sprint(rvalue) - } - if value == "" { - continue - } - fmt.Fprintln(writer, k+":\t"+value) - } - writer.Flush() -} - -func firstUpper(str string) string { - for i, v := range str { - return string(unicode.ToUpper(v)) + str[i+1:] - } - return "" } func newTableWriter() *tabwriter.Writer { @@ -88,78 +51,51 @@ func newTableWriter() *tabwriter.Writer { return writer } -func getMyID(c *cli.Context) string { - response := httpGet(c, "system/status") - data := make(map[string]interface{}) - json.Unmarshal(responseToBArray(response), &data) - return data["myID"].(string) -} - -func getConfig(c *cli.Context) config.Configuration { - response := httpGet(c, "system/config") - config := config.Configuration{} - json.Unmarshal(responseToBArray(response), &config) - return config -} - -func setConfig(c *cli.Context, cfg config.Configuration) { - body, err := json.Marshal(cfg) - die(err) - response := httpPost(c, "system/config", string(body)) - if response.StatusCode != 200 { - die("Unexpected status code", response.StatusCode) - } -} - -func parseBool(input string) bool { - val, err := strconv.ParseBool(input) +func getConfig(c *APIClient) (config.Configuration, error) { + cfg := config.Configuration{} + response, err := c.Get("system/config") if err != nil { - die(input + " is not a valid value for a boolean") + return cfg, err } - return val -} - -func parseInt(input string) int { - val, err := strconv.ParseInt(input, 0, 64) + bytes, err := responseToBArray(response) if err != nil { - die(input + " is not a valid value for an integer") + return cfg, err } - return int(val) + err = json.Unmarshal(bytes, &cfg) + if err == nil { + return cfg, err + } + return cfg, nil } -func parseUint(input string) int { - val, err := strconv.ParseUint(input, 0, 64) +func expects(n int, actionFunc cli.ActionFunc) cli.ActionFunc { + return func(ctx *cli.Context) error { + if ctx.NArg() != n { + plural := "" + if n != 1 { + plural = "s" + } + return fmt.Errorf("expected %d argument%s, got %d", n, plural, ctx.NArg()) + } + return actionFunc(ctx) + } +} + +func prettyPrintJSON(data interface{}) error { + enc := json.NewEncoder(os.Stdout) + enc.SetIndent("", " ") + return enc.Encode(data) +} + +func prettyPrintResponse(c *cli.Context, response *http.Response) error { + bytes, err := responseToBArray(response) if err != nil { - die(input + " is not a valid value for an unsigned integer") + return err } - return int(val) -} - -func parsePort(input string) int { - port := parseUint(input) - if port < 1 || port > 65535 { - die(input + " is not a valid port\nExpected value between 1 and 65535") - } - return port -} - -func validAddress(input string) { - tokens := strings.Split(input, ":") - if len(tokens) != 2 { - die(input + " is not a valid value for an address\nExpected format :") - } - matched, err := regexp.MatchString("^[a-zA-Z0-9]+([-a-zA-Z0-9.]+[-a-zA-Z0-9]+)?$", tokens[0]) - die(err) - if !matched { - die(input + " is not a valid value for an address\nExpected format :") - } - parsePort(tokens[1]) -} - -func parseDeviceID(input string) protocol.DeviceID { - device, err := protocol.DeviceIDFromString(input) - if err != nil { - die(input + " is not a valid device id") - } - return device + var data interface{} + if err := json.Unmarshal(bytes, &data); err != nil { + return err + } + // TODO: Check flag for pretty print format + return prettyPrintJSON(data) } diff --git a/cmd/syncthing/gui.go b/cmd/syncthing/gui.go index 96832b1e1..105d4258e 100644 --- a/cmd/syncthing/gui.go +++ b/cmd/syncthing/gui.go @@ -28,12 +28,14 @@ import ( "time" metrics "github.com/rcrowley/go-metrics" + "github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/connections" "github.com/syncthing/syncthing/lib/db" "github.com/syncthing/syncthing/lib/discover" "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/fs" + "github.com/syncthing/syncthing/lib/locations" "github.com/syncthing/syncthing/lib/logger" "github.com/syncthing/syncthing/lib/model" "github.com/syncthing/syncthing/lib/protocol" @@ -567,7 +569,7 @@ func noCacheMiddleware(h http.Handler) http.Handler { func withDetailsMiddleware(id protocol.DeviceID, h http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - w.Header().Set("X-Syncthing-Version", Version) + w.Header().Set("X-Syncthing-Version", build.Version) w.Header().Set("X-Syncthing-ID", id.String()) h.ServeHTTP(w, r) }) @@ -609,14 +611,14 @@ func (s *apiService) getJSMetadata(w http.ResponseWriter, r *http.Request) { func (s *apiService) getSystemVersion(w http.ResponseWriter, r *http.Request) { sendJSON(w, map[string]interface{}{ - "version": Version, - "codename": Codename, - "longVersion": LongVersion, + "version": build.Version, + "codename": build.Codename, + "longVersion": build.LongVersion, "os": runtime.GOOS, "arch": runtime.GOARCH, - "isBeta": IsBeta, - "isCandidate": IsCandidate, - "isRelease": IsRelease, + "isBeta": build.IsBeta, + "isCandidate": build.IsCandidate, + "isRelease": build.IsRelease, }) } @@ -1080,7 +1082,7 @@ func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) { } // Panic files - if panicFiles, err := filepath.Glob(filepath.Join(baseDirs["config"], "panic*")); err == nil { + if panicFiles, err := filepath.Glob(filepath.Join(locations.GetBaseDir(locations.ConfigBaseDir), "panic*")); err == nil { for _, f := range panicFiles { if panicFile, err := ioutil.ReadFile(f); err != nil { l.Warnf("Support bundle: failed to load %s: %s", filepath.Base(f), err) @@ -1091,16 +1093,16 @@ func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) { } // Archived log (default on Windows) - if logFile, err := ioutil.ReadFile(locations[locLogFile]); err == nil { + if logFile, err := ioutil.ReadFile(locations.Get(locations.LogFile)); err == nil { files = append(files, fileEntry{name: "log-ondisk.txt", data: logFile}) } // Version and platform information as a JSON if versionPlatform, err := json.MarshalIndent(map[string]string{ "now": time.Now().Format(time.RFC3339), - "version": Version, - "codename": Codename, - "longVersion": LongVersion, + "version": build.Version, + "codename": build.Codename, + "longVersion": build.LongVersion, "os": runtime.GOOS, "arch": runtime.GOARCH, }, "", " "); err == nil { @@ -1118,14 +1120,14 @@ func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) { // Heap and CPU Proofs as a pprof extension var heapBuffer, cpuBuffer bytes.Buffer - filename := fmt.Sprintf("syncthing-heap-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, Version, time.Now().Format("150405")) // hhmmss + filename := fmt.Sprintf("syncthing-heap-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, build.Version, time.Now().Format("150405")) // hhmmss runtime.GC() if err := pprof.WriteHeapProfile(&heapBuffer); err == nil { files = append(files, fileEntry{name: filename, data: heapBuffer.Bytes()}) } const duration = 4 * time.Second - filename = fmt.Sprintf("syncthing-cpu-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, Version, time.Now().Format("150405")) // hhmmss + filename = fmt.Sprintf("syncthing-cpu-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, build.Version, time.Now().Format("150405")) // hhmmss if err := pprof.StartCPUProfile(&cpuBuffer); err == nil { time.Sleep(duration) pprof.StopCPUProfile() @@ -1142,7 +1144,7 @@ func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) { // Set zip file name and path zipFileName := fmt.Sprintf("support-bundle-%s-%s.zip", s.id.Short().String(), time.Now().Format("2006-01-02T150405")) - zipFilePath := filepath.Join(baseDirs["config"], zipFileName) + zipFilePath := filepath.Join(locations.GetBaseDir(locations.ConfigBaseDir), zipFileName) // Write buffer zip to local zip file (back up) if err := ioutil.WriteFile(zipFilePath, zipFilesBuffer.Bytes(), 0600); err != nil { @@ -1323,16 +1325,16 @@ func (s *apiService) getSystemUpgrade(w http.ResponseWriter, r *http.Request) { return } opts := s.cfg.Options() - rel, err := upgrade.LatestRelease(opts.ReleasesURL, Version, opts.UpgradeToPreReleases) + rel, err := upgrade.LatestRelease(opts.ReleasesURL, build.Version, opts.UpgradeToPreReleases) if err != nil { http.Error(w, err.Error(), 500) return } res := make(map[string]interface{}) - res["running"] = Version + res["running"] = build.Version res["latest"] = rel.Tag - res["newer"] = upgrade.CompareVersions(rel.Tag, Version) == upgrade.Newer - res["majorNewer"] = upgrade.CompareVersions(rel.Tag, Version) == upgrade.MajorNewer + res["newer"] = upgrade.CompareVersions(rel.Tag, build.Version) == upgrade.Newer + res["majorNewer"] = upgrade.CompareVersions(rel.Tag, build.Version) == upgrade.MajorNewer sendJSON(w, res) } @@ -1365,14 +1367,14 @@ func (s *apiService) getLang(w http.ResponseWriter, r *http.Request) { func (s *apiService) postSystemUpgrade(w http.ResponseWriter, r *http.Request) { opts := s.cfg.Options() - rel, err := upgrade.LatestRelease(opts.ReleasesURL, Version, opts.UpgradeToPreReleases) + rel, err := upgrade.LatestRelease(opts.ReleasesURL, build.Version, opts.UpgradeToPreReleases) if err != nil { l.Warnln("getting latest release:", err) http.Error(w, err.Error(), 500) return } - if upgrade.CompareVersions(rel.Tag, Version) > upgrade.Equal { + if upgrade.CompareVersions(rel.Tag, build.Version) > upgrade.Equal { err = upgrade.To(rel) if err != nil { l.Warnln("upgrading:", err) @@ -1641,7 +1643,7 @@ func (s *apiService) getCPUProf(w http.ResponseWriter, r *http.Request) { duration = 30 * time.Second } - filename := fmt.Sprintf("syncthing-cpu-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, Version, time.Now().Format("150405")) // hhmmss + filename := fmt.Sprintf("syncthing-cpu-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, build.Version, time.Now().Format("150405")) // hhmmss w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Disposition", "attachment; filename="+filename) @@ -1653,7 +1655,7 @@ func (s *apiService) getCPUProf(w http.ResponseWriter, r *http.Request) { } func (s *apiService) getHeapProf(w http.ResponseWriter, r *http.Request) { - filename := fmt.Sprintf("syncthing-heap-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, Version, time.Now().Format("150405")) // hhmmss + filename := fmt.Sprintf("syncthing-heap-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, build.Version, time.Now().Format("150405")) // hhmmss w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("Content-Disposition", "attachment; filename="+filename) diff --git a/cmd/syncthing/gui_csrf.go b/cmd/syncthing/gui_csrf.go index 83f7da618..7590d5d21 100644 --- a/cmd/syncthing/gui_csrf.go +++ b/cmd/syncthing/gui_csrf.go @@ -14,6 +14,7 @@ import ( "strings" "github.com/syncthing/syncthing/lib/config" + "github.com/syncthing/syncthing/lib/locations" "github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/rand" "github.com/syncthing/syncthing/lib/sync" @@ -115,7 +116,7 @@ func saveCsrfTokens() { // We're ignoring errors in here. It's not super critical and there's // nothing relevant we can do about them anyway... - name := locations[locCsrfTokens] + name := locations.Get(locations.CsrfTokens) f, err := osutil.CreateAtomic(name) if err != nil { return @@ -129,7 +130,7 @@ func saveCsrfTokens() { } func loadCsrfTokens() { - f, err := os.Open(locations[locCsrfTokens]) + f, err := os.Open(locations.Get(locations.CsrfTokens)) if err != nil { return } diff --git a/cmd/syncthing/locations.go b/cmd/syncthing/locations.go deleted file mode 100644 index 94a329471..000000000 --- a/cmd/syncthing/locations.go +++ /dev/null @@ -1,125 +0,0 @@ -// Copyright (C) 2015 The Syncthing Authors. -// -// This Source Code Form is subject to the terms of the Mozilla Public -// License, v. 2.0. If a copy of the MPL was not distributed with this file, -// You can obtain one at https://mozilla.org/MPL/2.0/. - -package main - -import ( - "os" - "path/filepath" - "runtime" - "strings" - "time" - - "github.com/syncthing/syncthing/lib/fs" -) - -type locationEnum string - -// Use strings as keys to make printout and serialization of the locations map -// more meaningful. -const ( - locConfigFile locationEnum = "config" - locCertFile locationEnum = "certFile" - locKeyFile locationEnum = "keyFile" - locHTTPSCertFile locationEnum = "httpsCertFile" - locHTTPSKeyFile locationEnum = "httpsKeyFile" - locDatabase locationEnum = "database" - locLogFile locationEnum = "logFile" - locCsrfTokens locationEnum = "csrfTokens" - locPanicLog locationEnum = "panicLog" - locAuditLog locationEnum = "auditLog" - locGUIAssets locationEnum = "GUIAssets" - locDefFolder locationEnum = "defFolder" -) - -// Platform dependent directories -var baseDirs = map[string]string{ - "config": defaultConfigDir(), // Overridden by -home flag - "home": homeDir(), // User's home directory, *not* -home flag -} - -// Use the variables from baseDirs here -var locations = map[locationEnum]string{ - locConfigFile: "${config}/config.xml", - locCertFile: "${config}/cert.pem", - locKeyFile: "${config}/key.pem", - locHTTPSCertFile: "${config}/https-cert.pem", - locHTTPSKeyFile: "${config}/https-key.pem", - locDatabase: "${config}/index-v0.14.0.db", - locLogFile: "${config}/syncthing.log", // -logfile on Windows - locCsrfTokens: "${config}/csrftokens.txt", - locPanicLog: "${config}/panic-${timestamp}.log", - locAuditLog: "${config}/audit-${timestamp}.log", - locGUIAssets: "${config}/gui", - locDefFolder: "${home}/Sync", -} - -// expandLocations replaces the variables in the location map with actual -// directory locations. -func expandLocations() error { - for key, dir := range locations { - for varName, value := range baseDirs { - dir = strings.Replace(dir, "${"+varName+"}", value, -1) - } - var err error - dir, err = fs.ExpandTilde(dir) - if err != nil { - return err - } - locations[key] = dir - } - return nil -} - -// defaultConfigDir returns the default configuration directory, as figured -// out by various the environment variables present on each platform, or dies -// trying. -func defaultConfigDir() string { - switch runtime.GOOS { - case "windows": - if p := os.Getenv("LocalAppData"); p != "" { - return filepath.Join(p, "Syncthing") - } - return filepath.Join(os.Getenv("AppData"), "Syncthing") - - case "darwin": - dir, err := fs.ExpandTilde("~/Library/Application Support/Syncthing") - if err != nil { - l.Fatalln(err) - } - return dir - - default: - if xdgCfg := os.Getenv("XDG_CONFIG_HOME"); xdgCfg != "" { - return filepath.Join(xdgCfg, "syncthing") - } - dir, err := fs.ExpandTilde("~/.config/syncthing") - if err != nil { - l.Fatalln(err) - } - return dir - } -} - -// homeDir returns the user's home directory, or dies trying. -func homeDir() string { - home, err := fs.ExpandTilde("~") - if err != nil { - l.Fatalln(err) - } - return home -} - -func timestampedLoc(key locationEnum) string { - // We take the roundtrip via "${timestamp}" instead of passing the path - // directly through time.Format() to avoid issues when the path we are - // expanding contains numbers; otherwise for example - // /home/user2006/.../panic-20060102-150405.log would get both instances of - // 2006 replaced by 2015... - tpl := locations[key] - now := time.Now().Format("20060102-150405") - return strings.Replace(tpl, "${timestamp}", now, -1) -} diff --git a/cmd/syncthing/main.go b/cmd/syncthing/main.go index 51cbff988..8f0b9244c 100644 --- a/cmd/syncthing/main.go +++ b/cmd/syncthing/main.go @@ -16,12 +16,12 @@ import ( "io/ioutil" "log" "net/http" + _ "net/http/pprof" // Need to import this to support STPROFILER. "net/url" "os" "os/signal" "path" "path/filepath" - "regexp" "runtime" "runtime/pprof" "sort" @@ -30,6 +30,7 @@ import ( "syscall" "time" + "github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/connections" "github.com/syncthing/syncthing/lib/db" @@ -37,6 +38,7 @@ import ( "github.com/syncthing/syncthing/lib/discover" "github.com/syncthing/syncthing/lib/events" "github.com/syncthing/syncthing/lib/fs" + "github.com/syncthing/syncthing/lib/locations" "github.com/syncthing/syncthing/lib/logger" "github.com/syncthing/syncthing/lib/model" "github.com/syncthing/syncthing/lib/osutil" @@ -47,23 +49,6 @@ import ( "github.com/syncthing/syncthing/lib/upgrade" "github.com/thejerf/suture" - - _ "net/http/pprof" // Need to import this to support STPROFILER. -) - -var ( - Version = "unknown-dev" - Codename = "Erbium Earthworm" - BuildStamp = "0" - BuildDate time.Time - BuildHost = "unknown" - BuildUser = "unknown" - IsRelease bool - IsCandidate bool - IsBeta bool - LongVersion string - BuildTags []string - allowedVersionExp = regexp.MustCompile(`^v\d+\.\d+\.\d+(-[a-z0-9]+)*(\.\d+)*(\+\d+-g[0-9a-f]+)?(-[^\s]+)?$`) ) const ( @@ -83,46 +68,6 @@ const ( maxSystemLog = 250 ) -func init() { - if Version != "unknown-dev" { - // If not a generic dev build, version string should come from git describe - if !allowedVersionExp.MatchString(Version) { - l.Fatalf("Invalid version string %q;\n\tdoes not match regexp %v", Version, allowedVersionExp) - } - } -} - -func setBuildMetadata() { - // Check for a clean release build. A release is something like - // "v0.1.2", with an optional suffix of letters and dot separated - // numbers like "-beta3.47". If there's more stuff, like a plus sign and - // a commit hash and so on, then it's not a release. If it has a dash in - // it, it's some sort of beta, release candidate or special build. If it - // has "-rc." in it, like "v0.14.35-rc.42", then it's a candidate build. - // - // So, every build that is not a stable release build has IsBeta = true. - // This is used to enable some extra debugging (the deadlock detector). - // - // Release candidate builds are also "betas" from this point of view and - // will have that debugging enabled. In addition, some features are - // forced for release candidates - auto upgrade, and usage reporting. - - exp := regexp.MustCompile(`^v\d+\.\d+\.\d+(-[a-z]+[\d\.]+)?$`) - IsRelease = exp.MatchString(Version) - IsCandidate = strings.Contains(Version, "-rc.") - IsBeta = strings.Contains(Version, "-") - - stamp, _ := strconv.Atoi(BuildStamp) - BuildDate = time.Unix(int64(stamp), 0) - - date := BuildDate.UTC().Format("2006-01-02 15:04:05 MST") - LongVersion = fmt.Sprintf(`syncthing %s "%s" (%s %s-%s) %s@%s %s`, Version, Codename, runtime.Version(), runtime.GOOS, runtime.GOARCH, BuildUser, BuildHost, date) - - if len(BuildTags) > 0 { - LongVersion = fmt.Sprintf("%s [%s]", LongVersion, strings.Join(BuildTags, ", ")) - } -} - var ( myID protocol.DeviceID stop = make(chan int) @@ -320,8 +265,6 @@ func parseCommandLineOptions() RuntimeOptions { } func main() { - setBuildMetadata() - options := parseCommandLineOptions() l.SetFlags(options.logFlags) @@ -355,27 +298,25 @@ func main() { l.Fatalln(err) } } - baseDirs["config"] = options.confDir - } - - if err := expandLocations(); err != nil { - l.Fatalln(err) + if err := locations.SetBaseDir(locations.ConfigBaseDir, options.confDir); err != nil { + l.Fatalln(err) + } } if options.logFile == "" { // Blank means use the default logfile location. We must set this // *after* expandLocations above. - options.logFile = locations[locLogFile] + options.logFile = locations.Get(locations.LogFile) } if options.assetDir == "" { // The asset dir is blank if STGUIASSETS wasn't set, in which case we // should look for extra assets in the default place. - options.assetDir = locations[locGUIAssets] + options.assetDir = locations.Get(locations.GUIAssets) } if options.showVersion { - fmt.Println(LongVersion) + fmt.Println(build.LongVersion) return } @@ -390,7 +331,10 @@ func main() { } if options.showDeviceId { - cert, err := tls.LoadX509KeyPair(locations[locCertFile], locations[locKeyFile]) + cert, err := tls.LoadX509KeyPair( + locations.Get(locations.CertFile), + locations.Get(locations.KeyFile), + ) if err != nil { l.Fatalln("Error reading device ID:", err) } @@ -411,7 +355,7 @@ func main() { } // Ensure that our home directory exists. - ensureDir(baseDirs["config"], 0700) + ensureDir(locations.GetBaseDir(locations.ConfigBaseDir), 0700) if options.upgradeTo != "" { err := upgrade.ToURL(options.upgradeTo) @@ -521,24 +465,24 @@ func debugFacilities() string { func checkUpgrade() upgrade.Release { cfg, _ := loadOrDefaultConfig() opts := cfg.Options() - release, err := upgrade.LatestRelease(opts.ReleasesURL, Version, opts.UpgradeToPreReleases) + release, err := upgrade.LatestRelease(opts.ReleasesURL, build.Version, opts.UpgradeToPreReleases) if err != nil { l.Fatalln("Upgrade:", err) } - if upgrade.CompareVersions(release.Tag, Version) <= 0 { + if upgrade.CompareVersions(release.Tag, build.Version) <= 0 { noUpgradeMessage := "No upgrade available (current %q >= latest %q)." - l.Infof(noUpgradeMessage, Version, release.Tag) + l.Infof(noUpgradeMessage, build.Version, release.Tag) os.Exit(exitNoUpgradeAvailable) } - l.Infof("Upgrade available (current %q < latest %q)", Version, release.Tag) + l.Infof("Upgrade available (current %q < latest %q)", build.Version, release.Tag) return release } func performUpgrade(release upgrade.Release) { // Use leveldb database locks to protect against concurrent upgrades - _, err := db.Open(locations[locDatabase]) + _, err := db.Open(locations.Get(locations.Database)) if err == nil { err = upgrade.To(release) if err != nil { @@ -636,10 +580,17 @@ func syncthingMain(runtimeOptions RuntimeOptions) { osutil.MaximizeOpenFileLimit() // Ensure that we have a certificate and key. - cert, err := tls.LoadX509KeyPair(locations[locCertFile], locations[locKeyFile]) + cert, err := tls.LoadX509KeyPair( + locations.Get(locations.CertFile), + locations.Get(locations.KeyFile), + ) if err != nil { l.Infof("Generating ECDSA key and certificate for %s...", tlsDefaultCommonName) - cert, err = tlsutil.NewCertificate(locations[locCertFile], locations[locKeyFile], tlsDefaultCommonName) + cert, err = tlsutil.NewCertificate( + locations.Get(locations.CertFile), + locations.Get(locations.KeyFile), + tlsDefaultCommonName, + ) if err != nil { l.Fatalln(err) } @@ -648,7 +599,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { myID = protocol.NewDeviceID(cert.Certificate[0]) l.SetPrefix(fmt.Sprintf("[%s] ", myID.String()[:5])) - l.Infoln(LongVersion) + l.Infoln(build.LongVersion) l.Infoln("My ID:", myID) // Select SHA256 implementation and report. Affected by the @@ -659,7 +610,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { // Emit the Starting event, now that we know who we are. events.Default.Log(events.Starting, map[string]string{ - "home": baseDirs["config"], + "home": locations.GetBaseDir(locations.ConfigBaseDir), "myID": myID.String(), }) @@ -683,7 +634,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { perf := cpuBench(3, 150*time.Millisecond, true) l.Infof("Hashing performance is %.02f MB/s", perf) - dbFile := locations[locDatabase] + dbFile := locations.Get(locations.Database) ldb, err := db.Open(dbFile) if err != nil { l.Fatalln("Error opening database:", err) @@ -698,10 +649,10 @@ func syncthingMain(runtimeOptions RuntimeOptions) { } protectedFiles := []string{ - locations[locDatabase], - locations[locConfigFile], - locations[locCertFile], - locations[locKeyFile], + locations.Get(locations.Database), + locations.Get(locations.ConfigFile), + locations.Get(locations.CertFile), + locations.Get(locations.KeyFile), } // Remove database entries for folders that no longer exist in the config @@ -723,10 +674,10 @@ func syncthingMain(runtimeOptions RuntimeOptions) { // 0.14.45-pineapple is not. prevParts := strings.Split(prevVersion, "-") - curParts := strings.Split(Version, "-") + curParts := strings.Split(build.Version, "-") if prevParts[0] != curParts[0] { if prevVersion != "" { - l.Infoln("Detected upgrade from", prevVersion, "to", Version) + l.Infoln("Detected upgrade from", prevVersion, "to", build.Version) } // Drop delta indexes in case we've changed random stuff we @@ -734,16 +685,16 @@ func syncthingMain(runtimeOptions RuntimeOptions) { db.DropDeltaIndexIDs(ldb) // Remember the new version. - miscDB.PutString("prevVersion", Version) + miscDB.PutString("prevVersion", build.Version) } - m := model.NewModel(cfg, myID, "syncthing", Version, ldb, protectedFiles) + m := model.NewModel(cfg, myID, "syncthing", build.Version, ldb, protectedFiles) if t := os.Getenv("STDEADLOCKTIMEOUT"); t != "" { if secs, _ := strconv.Atoi(t); secs > 0 { m.StartDeadlockDetector(time.Duration(secs) * time.Second) } - } else if !IsRelease || IsBeta { + } else if !build.IsRelease || build.IsBeta { m.StartDeadlockDetector(20 * time.Minute) } @@ -842,7 +793,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { // Candidate builds always run with usage reporting. - if opts := cfg.Options(); IsCandidate { + if opts := cfg.Options(); build.IsCandidate { l.Infoln("Anonymous usage reporting is always enabled for candidate releases.") if opts.URAccepted != usageReportVersion { opts.URAccepted = usageReportVersion @@ -870,7 +821,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) { // unless we are in a build where it's disabled or the STNOUPGRADE // environment variable is set. - if IsCandidate && !upgrade.DisabledByCompilation && !noUpgradeFromEnv { + if build.IsCandidate && !upgrade.DisabledByCompilation && !noUpgradeFromEnv { l.Infoln("Automatic upgrade is always enabled for candidate releases.") if opts := cfg.Options(); opts.AutoUpgradeIntervalH == 0 || opts.AutoUpgradeIntervalH > 24 { opts.AutoUpgradeIntervalH = 12 @@ -943,7 +894,7 @@ func setupSignalHandling() { } func loadOrDefaultConfig() (*config.Wrapper, error) { - cfgFile := locations[locConfigFile] + cfgFile := locations.Get(locations.ConfigFile) cfg, err := config.Load(cfgFile, myID) if err != nil { @@ -954,7 +905,7 @@ func loadOrDefaultConfig() (*config.Wrapper, error) { } func loadConfigAtStartup() *config.Wrapper { - cfgFile := locations[locConfigFile] + cfgFile := locations.Get(locations.ConfigFile) cfg, err := config.Load(cfgFile, myID) if os.IsNotExist(err) { cfg = defaultConfig(cfgFile) @@ -1018,7 +969,7 @@ func startAuditing(mainService *suture.Supervisor, auditFile string) { auditDest = "stderr" } else { if auditFile == "" { - auditFile = timestampedLoc(locAuditLog) + auditFile = locations.GetTimestamped(locations.AuditLog) auditFlags = os.O_WRONLY | os.O_CREATE | os.O_EXCL } else { auditFlags = os.O_WRONLY | os.O_CREATE | os.O_APPEND @@ -1054,7 +1005,7 @@ func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Mode cpu := newCPUService() mainService.Add(cpu) - api := newAPIService(myID, cfg, locations[locHTTPSCertFile], locations[locHTTPSKeyFile], runtimeOptions.assetDir, m, defaultSub, diskSub, discoverer, connectionsService, errors, systemLog, cpu) + api := newAPIService(myID, cfg, locations.Get(locations.HTTPSCertFile), locations.Get(locations.HTTPSKeyFile), runtimeOptions.assetDir, m, defaultSub, diskSub, discoverer, connectionsService, errors, systemLog, cpu) cfg.Subscribe(api) mainService.Add(api) @@ -1074,13 +1025,13 @@ func defaultConfig(cfgFile string) *config.Wrapper { return config.Wrap(cfgFile, newCfg) } - newCfg.Folders = append(newCfg.Folders, config.NewFolderConfiguration(myID, "default", "Default Folder", fs.FilesystemTypeBasic, locations[locDefFolder])) + newCfg.Folders = append(newCfg.Folders, config.NewFolderConfiguration(myID, "default", "Default Folder", fs.FilesystemTypeBasic, locations.Get(locations.DefFolder))) l.Infoln("Default folder created and/or linked to new config") return config.Wrap(cfgFile, newCfg) } func resetDB() error { - return os.RemoveAll(locations[locDatabase]) + return os.RemoveAll(locations.Get(locations.Database)) } func restart() { @@ -1142,10 +1093,10 @@ func autoUpgrade(cfg *config.Wrapper) { select { case event := <-sub.C(): data, ok := event.Data.(map[string]string) - if !ok || data["clientName"] != "syncthing" || upgrade.CompareVersions(data["clientVersion"], Version) != upgrade.Newer { + if !ok || data["clientName"] != "syncthing" || upgrade.CompareVersions(data["clientVersion"], build.Version) != upgrade.Newer { continue } - l.Infof("Connected to device %s with a newer version (current %q < remote %q). Checking for upgrades.", data["id"], Version, data["clientVersion"]) + l.Infof("Connected to device %s with a newer version (current %q < remote %q). Checking for upgrades.", data["id"], build.Version, data["clientVersion"]) case <-timer.C: } @@ -1157,7 +1108,7 @@ func autoUpgrade(cfg *config.Wrapper) { checkInterval = time.Hour } - rel, err := upgrade.LatestRelease(opts.ReleasesURL, Version, opts.UpgradeToPreReleases) + rel, err := upgrade.LatestRelease(opts.ReleasesURL, build.Version, opts.UpgradeToPreReleases) if err == upgrade.ErrUpgradeUnsupported { events.Default.Unsubscribe(sub) return @@ -1170,13 +1121,13 @@ func autoUpgrade(cfg *config.Wrapper) { continue } - if upgrade.CompareVersions(rel.Tag, Version) != upgrade.Newer { + if upgrade.CompareVersions(rel.Tag, build.Version) != upgrade.Newer { // Skip equal, older or majorly newer (incompatible) versions timer.Reset(checkInterval) continue } - l.Infof("Automatic upgrade (current %q < latest %q)", Version, rel.Tag) + l.Infof("Automatic upgrade (current %q < latest %q)", build.Version, rel.Tag) err = upgrade.To(rel) if err != nil { l.Warnln("Automatic upgrade:", err) @@ -1209,7 +1160,7 @@ func cleanConfigDirectory() { } for pat, dur := range patterns { - fs := fs.NewFilesystem(fs.FilesystemTypeBasic, baseDirs["config"]) + fs := fs.NewFilesystem(fs.FilesystemTypeBasic, locations.GetBaseDir(locations.ConfigBaseDir)) files, err := fs.Glob(pat) if err != nil { l.Infoln("Cleaning:", err) @@ -1250,13 +1201,13 @@ func checkShortIDs(cfg *config.Wrapper) error { } func showPaths(options RuntimeOptions) { - fmt.Printf("Configuration file:\n\t%s\n\n", locations[locConfigFile]) - fmt.Printf("Database directory:\n\t%s\n\n", locations[locDatabase]) - fmt.Printf("Device private key & certificate files:\n\t%s\n\t%s\n\n", locations[locKeyFile], locations[locCertFile]) - fmt.Printf("HTTPS private key & certificate files:\n\t%s\n\t%s\n\n", locations[locHTTPSKeyFile], locations[locHTTPSCertFile]) + fmt.Printf("Configuration file:\n\t%s\n\n", locations.Get(locations.ConfigFile)) + fmt.Printf("Database directory:\n\t%s\n\n", locations.Get(locations.Database)) + fmt.Printf("Device private key & certificate files:\n\t%s\n\t%s\n\n", locations.Get(locations.KeyFile), locations.Get(locations.CertFile)) + fmt.Printf("HTTPS private key & certificate files:\n\t%s\n\t%s\n\n", locations.Get(locations.HTTPSKeyFile), locations.Get(locations.HTTPSCertFile)) fmt.Printf("Log file:\n\t%s\n\n", options.logFile) fmt.Printf("GUI override directory:\n\t%s\n\n", options.assetDir) - fmt.Printf("Default sync folder directory:\n\t%s\n\n", locations[locDefFolder]) + fmt.Printf("Default sync folder directory:\n\t%s\n\n", locations.Get(locations.DefFolder)) } func setPauseState(cfg *config.Wrapper, paused bool) { diff --git a/cmd/syncthing/main_test.go b/cmd/syncthing/main_test.go index 60c655be6..5dc4fd714 100644 --- a/cmd/syncthing/main_test.go +++ b/cmd/syncthing/main_test.go @@ -36,29 +36,3 @@ func TestShortIDCheck(t *testing.T) { t.Error("Should have gotten an error") } } - -func TestAllowedVersions(t *testing.T) { - testcases := []struct { - ver string - allowed bool - }{ - {"v0.13.0", true}, - {"v0.12.11+22-gabcdef0", true}, - {"v0.13.0-beta0", true}, - {"v0.13.0-beta47", true}, - {"v0.13.0-beta47+1-gabcdef0", true}, - {"v0.13.0-beta.0", true}, - {"v0.13.0-beta.47", true}, - {"v0.13.0-beta.0+1-gabcdef0", true}, - {"v0.13.0-beta.47+1-gabcdef0", true}, - {"v0.13.0-some-weird-but-allowed-tag", true}, - {"v0.13.0-allowed.to.do.this", true}, - {"v0.13.0+not.allowed.to.do.this", false}, - } - - for i, c := range testcases { - if allowed := allowedVersionExp.MatchString(c.ver); allowed != c.allowed { - t.Errorf("%d: incorrect result %v != %v for %q", i, allowed, c.allowed, c.ver) - } - } -} diff --git a/cmd/syncthing/monitor.go b/cmd/syncthing/monitor.go index 0909dd2c6..bc9c88cc7 100644 --- a/cmd/syncthing/monitor.go +++ b/cmd/syncthing/monitor.go @@ -17,6 +17,7 @@ import ( "syscall" "time" + "github.com/syncthing/syncthing/lib/locations" "github.com/syncthing/syncthing/lib/osutil" "github.com/syncthing/syncthing/lib/sync" ) @@ -198,7 +199,7 @@ func copyStderr(stderr io.Reader, dst io.Writer) { } if strings.HasPrefix(line, "panic:") || strings.HasPrefix(line, "fatal error:") { - panicFd, err = os.Create(timestampedLoc(locPanicLog)) + panicFd, err = os.Create(locations.GetTimestamped(locations.PanicLog)) if err != nil { l.Warnln("Create panic log:", err) continue diff --git a/cmd/syncthing/usage_report.go b/cmd/syncthing/usage_report.go index 0f145a4bc..2677b21f8 100644 --- a/cmd/syncthing/usage_report.go +++ b/cmd/syncthing/usage_report.go @@ -20,6 +20,7 @@ import ( "sync" "time" + "github.com/syncthing/syncthing/lib/build" "github.com/syncthing/syncthing/lib/config" "github.com/syncthing/syncthing/lib/connections" "github.com/syncthing/syncthing/lib/dialer" @@ -41,8 +42,8 @@ func reportData(cfg configIntf, m modelIntf, connectionsService connectionsIntf, res := make(map[string]interface{}) res["urVersion"] = version res["uniqueID"] = opts.URUniqueID - res["version"] = Version - res["longVersion"] = LongVersion + res["version"] = build.Version + res["longVersion"] = build.LongVersion res["platform"] = runtime.GOOS + "-" + runtime.GOARCH res["numFolders"] = len(cfg.Folders()) res["numDevices"] = len(cfg.Devices()) diff --git a/go.mod b/go.mod index 908ba3a0d..7d068da1a 100644 --- a/go.mod +++ b/go.mod @@ -1,14 +1,15 @@ module github.com/syncthing/syncthing require ( - github.com/AudriusButkevicius/cli v0.0.0-20140727204646-7f561c78b5a4 github.com/AudriusButkevicius/go-nat-pmp v0.0.0-20160522074932-452c97607362 + github.com/AudriusButkevicius/recli v0.0.5 github.com/bkaradzic/go-lz4 v0.0.0-20160924222819-7224d8d8f27e github.com/calmh/du v1.0.1 github.com/calmh/xdr v1.1.0 github.com/chmduquesne/rollinghash v0.0.0-20180912150627-a60f8e7142b5 github.com/d4l3k/messagediff v1.2.1 github.com/davecgh/go-spew v1.1.1 // indirect + github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 github.com/gobwas/glob v0.0.0-20170212200151-51eb1ee00b6d github.com/gogo/protobuf v1.2.0 github.com/golang/groupcache v0.0.0-20171101203131-84a468cf14b4 @@ -17,13 +18,14 @@ require ( github.com/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657 github.com/kr/pretty v0.1.0 // indirect github.com/lib/pq v1.0.0 + github.com/mattn/go-isatty v0.0.4 github.com/minio/sha256-simd v0.0.0-20190117184323-cc1980cb0338 github.com/onsi/ginkgo v0.0.0-20171221013426-6c46eb8334b3 // indirect github.com/onsi/gomega v0.0.0-20171227184521-ba3724c94e4d // indirect github.com/oschwald/geoip2-golang v1.1.0 github.com/oschwald/maxminddb-golang v0.0.0-20170901134056-26fe5ace1c70 // indirect github.com/petermattis/goid v0.0.0-20170816195418-3db12ebb2a59 // indirect - github.com/pkg/errors v0.0.0-20171216070316-e881fd58d78e + github.com/pkg/errors v0.8.1 github.com/pmezard/go-difflib v1.0.0 // indirect github.com/prometheus/client_golang v0.9.2 github.com/rcrowley/go-metrics v0.0.0-20171128170426-e181e095bae9 @@ -32,6 +34,7 @@ require ( github.com/syncthing/notify v0.0.0-20181107104724-4e389ea6c0d8 github.com/syndtr/goleveldb v0.0.0-20171214120811-34011bf325bc github.com/thejerf/suture v3.0.2+incompatible + github.com/urfave/cli v1.20.0 github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0 golang.org/x/crypto v0.0.0-20171231215028-0fcca4842a8d golang.org/x/net v0.0.0-20181201002055-351d144fa1fc diff --git a/go.sum b/go.sum index 1f0b0628e..9599172a8 100644 --- a/go.sum +++ b/go.sum @@ -1,7 +1,7 @@ -github.com/AudriusButkevicius/cli v0.0.0-20140727204646-7f561c78b5a4 h1:Cy4N5BdzSyWRnkNyzkIMKPSuzENT4AGxC+YFo0OOcCI= -github.com/AudriusButkevicius/cli v0.0.0-20140727204646-7f561c78b5a4/go.mod h1:mK5FQv1k6rd64lZeDQ+JgG5hSERyVEYeC3qXrbN+2nw= github.com/AudriusButkevicius/go-nat-pmp v0.0.0-20160522074932-452c97607362 h1:l4qGIzSY0WhdXdR74XMYAtfc0Ri/RJVM4p6x/E/+WkA= github.com/AudriusButkevicius/go-nat-pmp v0.0.0-20160522074932-452c97607362/go.mod h1:CEaBhA5lh1spxbPOELh5wNLKGsVQoahjUhVrJViVK8s= +github.com/AudriusButkevicius/recli v0.0.5 h1:xUa55PvWTHBm17T6RvjElRO3y5tALpdceH86vhzQ5wg= +github.com/AudriusButkevicius/recli v0.0.5/go.mod h1:Q2E26yc6RvWWEz/TJ/goUp6yXvipYdJI096hpoaqsNs= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/bkaradzic/go-lz4 v0.0.0-20160924222819-7224d8d8f27e h1:2augTYh6E+XoNrrivZJBadpThP/dsvYKj0nzqfQ8tM4= @@ -16,6 +16,8 @@ github.com/d4l3k/messagediff v1.2.1 h1:ZcAIMYsUg0EAp9X+tt8/enBE/Q8Yd5kzPynLyKptt github.com/d4l3k/messagediff v1.2.1/go.mod h1:Oozbb1TVXFac9FtSIxHBMnBCq2qeH/2KkEQxENCrlLo= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= +github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568 h1:BMXYYRWTLOJKlh+lOBt6nUQgXAfB7oVIQt5cNreqSLI= +github.com/flynn-archive/go-shlex v0.0.0-20150515145356-3f9db97f8568/go.mod h1:rZfgFAXFS/z/lEd6LJmf9HVZ1LkgYiHx5pHhV5DR16M= github.com/gobwas/glob v0.0.0-20170212200151-51eb1ee00b6d h1:IngNQgbqr5ZOU0exk395Szrvkzes9Ilk1fmJfkw7d+M= github.com/gobwas/glob v0.0.0-20170212200151-51eb1ee00b6d/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8= github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI= @@ -37,6 +39,8 @@ github.com/kr/text v0.1.0 h1:45sCR5RtlFHMR4UwH9sdQ5TC8v0qDQCHnXt+kaKSTVE= github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI= github.com/lib/pq v1.0.0 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A= github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo= +github.com/mattn/go-isatty v0.0.4 h1:bnP0vzxcAdeI1zdubAl5PjU6zsERjGZb7raWodagDYs= +github.com/mattn/go-isatty v0.0.4/go.mod h1:M+lRXTBqGeGNdLjl/ufCoiOlB5xdOkqRJdNxMWT7Zi4= github.com/matttproud/golang_protobuf_extensions v1.0.1 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU= github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0= github.com/minio/sha256-simd v0.0.0-20190117184323-cc1980cb0338 h1:USW1+zAUkUSvk097CAX/i8KR3r6f+DHNhk6Xe025Oyw= @@ -51,8 +55,8 @@ github.com/oschwald/maxminddb-golang v0.0.0-20170901134056-26fe5ace1c70 h1:XGLYU github.com/oschwald/maxminddb-golang v0.0.0-20170901134056-26fe5ace1c70/go.mod h1:3jhIUymTJ5VREKyIhWm66LJiQt04F0UCDdodShpjWsY= github.com/petermattis/goid v0.0.0-20170816195418-3db12ebb2a59 h1:2pHcLyJYXivxVvpoCc29uo3GDU1qFfJ1ggXKGYMrM0E= github.com/petermattis/goid v0.0.0-20170816195418-3db12ebb2a59/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o= -github.com/pkg/errors v0.0.0-20171216070316-e881fd58d78e h1:+RHxT/gm0O3UF7nLJbdNzAmULvCFt4XfXHWzh3XI/zs= -github.com/pkg/errors v0.0.0-20171216070316-e881fd58d78e/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= +github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/prometheus/client_golang v0.9.2 h1:awm861/B8OKDd2I/6o1dy3ra4BamzKhYOiGItCeZ740= @@ -75,6 +79,8 @@ github.com/syndtr/goleveldb v0.0.0-20171214120811-34011bf325bc h1:yhWARKbbDg8UBR github.com/syndtr/goleveldb v0.0.0-20171214120811-34011bf325bc/go.mod h1:Z4AUp2Km+PwemOoO/VB5AOx9XSsIItzFjoJlOSiYmn0= github.com/thejerf/suture v3.0.2+incompatible h1:GtMydYcnK4zBJ0KL6Lx9vLzl6Oozb65wh252FTBxrvM= github.com/thejerf/suture v3.0.2+incompatible/go.mod h1:ibKwrVj+Uzf3XZdAiNWUouPaAbSoemxOHLmJmwheEMc= +github.com/urfave/cli v1.20.0 h1:fDqGv3UG/4jbVl/QkFwEdddtEDjh/5Ov6X+0B/3bPaw= +github.com/urfave/cli v1.20.0/go.mod h1:70zkFmudgCuE/ngEzBv17Jvp/497gISqfk5gWijbERA= github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0 h1:okhMind4q9H1OxF44gNegWkiP4H/gsTFLalHFa4OOUI= github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0/go.mod h1:TTbGUfE+cXXceWtbTHq6lqcTvYPBKLNejBEbnUsQJtU= golang.org/x/crypto v0.0.0-20171231215028-0fcca4842a8d h1:GrqEEc3+MtHKTsZrdIGVoYDgLpbSRzW1EF+nLu0PcHE= diff --git a/lib/build/build.go b/lib/build/build.go new file mode 100644 index 000000000..69e196ab9 --- /dev/null +++ b/lib/build/build.go @@ -0,0 +1,81 @@ +// Copyright (C) 2019 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +package build + +import ( + "fmt" + "log" + "regexp" + "runtime" + "strconv" + "strings" + "time" +) + +var ( + // Injected by build script + Version = "unknown-dev" + Host = "unknown" // Set by build script + User = "unknown" // Set by build script + Stamp = "0" // Set by build script + + // Static + Codename = "Erbium Earthworm" + + // Set by init() + Date time.Time + IsRelease bool + IsCandidate bool + IsBeta bool + LongVersion string + + // Set by Go build tags + Tags []string + + allowedVersionExp = regexp.MustCompile(`^v\d+\.\d+\.\d+(-[a-z0-9]+)*(\.\d+)*(\+\d+-g[0-9a-f]+)?(-[^\s]+)?$`) +) + +func init() { + if Version != "unknown-dev" { + // If not a generic dev build, version string should come from git describe + if !allowedVersionExp.MatchString(Version) { + log.Fatalf("Invalid version string %q;\n\tdoes not match regexp %v", Version, allowedVersionExp) + } + } + setBuildData() +} + +func setBuildData() { + // Check for a clean release build. A release is something like + // "v0.1.2", with an optional suffix of letters and dot separated + // numbers like "-beta3.47". If there's more stuff, like a plus sign and + // a commit hash and so on, then it's not a release. If it has a dash in + // it, it's some sort of beta, release candidate or special build. If it + // has "-rc." in it, like "v0.14.35-rc.42", then it's a candidate build. + // + // So, every build that is not a stable release build has IsBeta = true. + // This is used to enable some extra debugging (the deadlock detector). + // + // Release candidate builds are also "betas" from this point of view and + // will have that debugging enabled. In addition, some features are + // forced for release candidates - auto upgrade, and usage reporting. + + exp := regexp.MustCompile(`^v\d+\.\d+\.\d+(-[a-z]+[\d\.]+)?$`) + IsRelease = exp.MatchString(Version) + IsCandidate = strings.Contains(Version, "-rc.") + IsBeta = strings.Contains(Version, "-") + + stamp, _ := strconv.Atoi(Stamp) + Date = time.Unix(int64(stamp), 0) + + date := Date.UTC().Format("2006-01-02 15:04:05 MST") + LongVersion = fmt.Sprintf(`syncthing %s "%s" (%s %s-%s) %s@%s %s`, Version, Codename, runtime.Version(), runtime.GOOS, runtime.GOARCH, User, Host, date) + + if len(Tags) > 0 { + LongVersion = fmt.Sprintf("%s [%s]", LongVersion, strings.Join(Tags, ", ")) + } +} diff --git a/lib/build/build_test.go b/lib/build/build_test.go new file mode 100644 index 000000000..797611694 --- /dev/null +++ b/lib/build/build_test.go @@ -0,0 +1,37 @@ +// Copyright (C) 2019 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +package build + +import ( + "testing" +) + +func TestAllowedVersions(t *testing.T) { + testcases := []struct { + ver string + allowed bool + }{ + {"v0.13.0", true}, + {"v0.12.11+22-gabcdef0", true}, + {"v0.13.0-beta0", true}, + {"v0.13.0-beta47", true}, + {"v0.13.0-beta47+1-gabcdef0", true}, + {"v0.13.0-beta.0", true}, + {"v0.13.0-beta.47", true}, + {"v0.13.0-beta.0+1-gabcdef0", true}, + {"v0.13.0-beta.47+1-gabcdef0", true}, + {"v0.13.0-some-weird-but-allowed-tag", true}, + {"v0.13.0-allowed.to.do.this", true}, + {"v0.13.0+not.allowed.to.do.this", false}, + } + + for i, c := range testcases { + if allowed := allowedVersionExp.MatchString(c.ver); allowed != c.allowed { + t.Errorf("%d: incorrect result %v != %v for %q", i, allowed, c.allowed, c.ver) + } + } +} diff --git a/cmd/syncthing/buildtag_noupgrade.go b/lib/build/tags_noupgrade.go similarity index 83% rename from cmd/syncthing/buildtag_noupgrade.go rename to lib/build/tags_noupgrade.go index 6be9ef17f..77e5f3b9e 100644 --- a/cmd/syncthing/buildtag_noupgrade.go +++ b/lib/build/tags_noupgrade.go @@ -6,8 +6,8 @@ //+build noupgrade -package main +package build func init() { - BuildTags = append(BuildTags, "noupgrade") + Tags = append(Tags, "noupgrade") } diff --git a/cmd/syncthing/buildtag_race.go b/lib/build/tags_race.go similarity index 84% rename from cmd/syncthing/buildtag_race.go rename to lib/build/tags_race.go index 924f170ac..706d5606c 100644 --- a/cmd/syncthing/buildtag_race.go +++ b/lib/build/tags_race.go @@ -6,8 +6,8 @@ //+build race -package main +package build func init() { - BuildTags = append(BuildTags, "race") + Tags = append(Tags, "race") } diff --git a/lib/config/deviceconfiguration.go b/lib/config/deviceconfiguration.go index 802571109..3a7a45100 100644 --- a/lib/config/deviceconfiguration.go +++ b/lib/config/deviceconfiguration.go @@ -10,12 +10,13 @@ import ( "sort" "github.com/syncthing/syncthing/lib/protocol" + "github.com/syncthing/syncthing/lib/util" ) type DeviceConfiguration struct { DeviceID protocol.DeviceID `xml:"id,attr" json:"deviceID"` Name string `xml:"name,attr,omitempty" json:"name"` - Addresses []string `xml:"address,omitempty" json:"addresses"` + Addresses []string `xml:"address,omitempty" json:"addresses" default:"dynamic"` Compression protocol.Compression `xml:"compression,attr" json:"compression"` CertName string `xml:"certName,attr,omitempty" json:"certName"` Introducer bool `xml:"introducer,attr" json:"introducer"` @@ -36,6 +37,9 @@ func NewDeviceConfiguration(id protocol.DeviceID, name string) DeviceConfigurati DeviceID: id, Name: name, } + + util.SetDefaults(&d) + d.prepare(nil) return d } diff --git a/lib/config/folderconfiguration.go b/lib/config/folderconfiguration.go index 92c79cda3..f02203e62 100644 --- a/lib/config/folderconfiguration.go +++ b/lib/config/folderconfiguration.go @@ -32,12 +32,12 @@ type FolderConfiguration struct { Path string `xml:"path,attr" json:"path"` Type FolderType `xml:"type,attr" json:"type"` Devices []FolderDeviceConfiguration `xml:"device" json:"devices"` - RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"` - FSWatcherEnabled bool `xml:"fsWatcherEnabled,attr" json:"fsWatcherEnabled"` - FSWatcherDelayS int `xml:"fsWatcherDelayS,attr" json:"fsWatcherDelayS"` + RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS" default:"3600"` + FSWatcherEnabled bool `xml:"fsWatcherEnabled,attr" json:"fsWatcherEnabled" default:"true"` + FSWatcherDelayS int `xml:"fsWatcherDelayS,attr" json:"fsWatcherDelayS" default:"10"` IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"` - AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize"` - MinDiskFree Size `xml:"minDiskFree" json:"minDiskFree"` + AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize" default:"true"` + MinDiskFree Size `xml:"minDiskFree" json:"minDiskFree" default:"1%"` Versioning VersioningConfiguration `xml:"versioning" json:"versioning"` Copiers int `xml:"copiers" json:"copiers"` // This defines how many files are handled concurrently. PullerMaxPendingKiB int `xml:"pullerMaxPendingKiB" json:"pullerMaxPendingKiB"` @@ -46,7 +46,7 @@ type FolderConfiguration struct { IgnoreDelete bool `xml:"ignoreDelete" json:"ignoreDelete"` ScanProgressIntervalS int `xml:"scanProgressIntervalS" json:"scanProgressIntervalS"` // Set to a negative value to disable. Value of 0 will get replaced with value of 2 (default value) PullerPauseS int `xml:"pullerPauseS" json:"pullerPauseS"` - MaxConflicts int `xml:"maxConflicts" json:"maxConflicts"` + MaxConflicts int `xml:"maxConflicts" json:"maxConflicts" default:"-1"` DisableSparseFiles bool `xml:"disableSparseFiles" json:"disableSparseFiles"` DisableTempIndexes bool `xml:"disableTempIndexes" json:"disableTempIndexes"` Paused bool `xml:"paused" json:"paused"` @@ -69,18 +69,15 @@ type FolderDeviceConfiguration struct { func NewFolderConfiguration(myID protocol.DeviceID, id, label string, fsType fs.FilesystemType, path string) FolderConfiguration { f := FolderConfiguration{ - ID: id, - Label: label, - RescanIntervalS: 3600, - FSWatcherEnabled: true, - FSWatcherDelayS: 10, - MinDiskFree: Size{Value: 1, Unit: "%"}, - Devices: []FolderDeviceConfiguration{{DeviceID: myID}}, - AutoNormalize: true, - MaxConflicts: -1, - FilesystemType: fsType, - Path: path, + ID: id, + Label: label, + Devices: []FolderDeviceConfiguration{{DeviceID: myID}}, + FilesystemType: fsType, + Path: path, } + + util.SetDefaults(&f) + f.prepare() return f } diff --git a/lib/config/optionsconfiguration.go b/lib/config/optionsconfiguration.go index 0a5dae73d..22e7a6cc9 100644 --- a/lib/config/optionsconfiguration.go +++ b/lib/config/optionsconfiguration.go @@ -14,7 +14,7 @@ import ( type OptionsConfiguration struct { ListenAddresses []string `xml:"listenAddress" json:"listenAddresses" default:"default"` - GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"default" restart:"true"` + GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" default:"default" restart:"true"` GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true" restart:"true"` LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true" restart:"true"` LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027" restart:"true"` diff --git a/lib/config/size.go b/lib/config/size.go index 20c3f38d1..35e983fd6 100644 --- a/lib/config/size.go +++ b/lib/config/size.go @@ -72,8 +72,10 @@ func (s Size) String() string { return fmt.Sprintf("%v %s", s.Value, s.Unit) } -func (Size) ParseDefault(s string) (interface{}, error) { - return ParseSize(s) +func (s *Size) ParseDefault(str string) error { + sz, err := ParseSize(str) + *s = sz + return err } func checkFreeSpace(req Size, usage fs.Usage) error { diff --git a/lib/config/size_test.go b/lib/config/size_test.go index 8e49a88b9..598c13981 100644 --- a/lib/config/size_test.go +++ b/lib/config/size_test.go @@ -6,7 +6,28 @@ package config -import "testing" +import ( + "testing" + + "github.com/syncthing/syncthing/lib/util" +) + +type TestStruct struct { + Size Size `default:"10%"` +} + +func TestSizeDefaults(t *testing.T) { + x := &TestStruct{} + + util.SetDefaults(x) + + if !x.Size.Percentage() { + t.Error("not percentage") + } + if x.Size.Value != 10 { + t.Error("not ten") + } +} func TestParseSize(t *testing.T) { cases := []struct { diff --git a/lib/locations/locations.go b/lib/locations/locations.go new file mode 100644 index 000000000..f730dc8fb --- /dev/null +++ b/lib/locations/locations.go @@ -0,0 +1,161 @@ +// Copyright (C) 2019 The Syncthing Authors. +// +// This Source Code Form is subject to the terms of the Mozilla Public +// License, v. 2.0. If a copy of the MPL was not distributed with this file, +// You can obtain one at https://mozilla.org/MPL/2.0/. + +package locations + +import ( + "fmt" + "os" + "path/filepath" + "runtime" + "strings" + "time" + + "github.com/syncthing/syncthing/lib/fs" +) + +type LocationEnum string + +// Use strings as keys to make printout and serialization of the locations map +// more meaningful. +const ( + ConfigFile LocationEnum = "config" + CertFile LocationEnum = "certFile" + KeyFile LocationEnum = "keyFile" + HTTPSCertFile LocationEnum = "httpsCertFile" + HTTPSKeyFile LocationEnum = "httpsKeyFile" + Database LocationEnum = "database" + LogFile LocationEnum = "logFile" + CsrfTokens LocationEnum = "csrfTokens" + PanicLog LocationEnum = "panicLog" + AuditLog LocationEnum = "auditLog" + GUIAssets LocationEnum = "GUIAssets" + DefFolder LocationEnum = "defFolder" +) + +type BaseDirEnum string + +const ( + ConfigBaseDir BaseDirEnum = "config" + HomeBaseDir BaseDirEnum = "home" +) + +func init() { + err := expandLocations() + if err != nil { + panic(err) + } +} + +func SetBaseDir(baseDirName BaseDirEnum, path string) error { + _, ok := baseDirs[baseDirName] + if !ok { + return fmt.Errorf("unknown base dir: %s", baseDirName) + } + baseDirs[baseDirName] = filepath.Clean(path) + return expandLocations() +} + +func Get(location LocationEnum) string { + return locations[location] +} + +func GetBaseDir(baseDir BaseDirEnum) string { + return baseDirs[baseDir] +} + +// Platform dependent directories +var baseDirs = map[BaseDirEnum]string{ + ConfigBaseDir: defaultConfigDir(), // Overridden by -home flag + HomeBaseDir: homeDir(), // User's home directory, *not* -home flag +} + +// Use the variables from baseDirs here +var locationTemplates = map[LocationEnum]string{ + ConfigFile: "${config}/config.xml", + CertFile: "${config}/cert.pem", + KeyFile: "${config}/key.pem", + HTTPSCertFile: "${config}/https-cert.pem", + HTTPSKeyFile: "${config}/https-key.pem", + Database: "${config}/index-v0.14.0.db", + LogFile: "${config}/syncthing.log", // -logfile on Windows + CsrfTokens: "${config}/csrftokens.txt", + PanicLog: "${config}/panic-${timestamp}.log", + AuditLog: "${config}/audit-${timestamp}.log", + GUIAssets: "${config}/gui", + DefFolder: "${home}/Sync", +} + +var locations = make(map[LocationEnum]string) + +// expandLocations replaces the variables in the locations map with actual +// directory locations. +func expandLocations() error { + newLocations := make(map[LocationEnum]string) + for key, dir := range locationTemplates { + for varName, value := range baseDirs { + dir = strings.Replace(dir, "${"+string(varName)+"}", value, -1) + } + var err error + dir, err = fs.ExpandTilde(dir) + if err != nil { + return err + } + newLocations[key] = filepath.Clean(dir) + } + locations = newLocations + return nil +} + +// defaultConfigDir returns the default configuration directory, as figured +// out by various the environment variables present on each platform, or dies +// trying. +func defaultConfigDir() string { + switch runtime.GOOS { + case "windows": + if p := os.Getenv("LocalAppData"); p != "" { + return filepath.Join(p, "Syncthing") + } + return filepath.Join(os.Getenv("AppData"), "Syncthing") + + case "darwin": + dir, err := fs.ExpandTilde("~/Library/Application Support/Syncthing") + if err != nil { + panic(err) + } + return dir + + default: + if xdgCfg := os.Getenv("XDG_CONFIG_HOME"); xdgCfg != "" { + return filepath.Join(xdgCfg, "syncthing") + } + dir, err := fs.ExpandTilde("~/.config/syncthing") + if err != nil { + panic(err) + } + return dir + } +} + +// homeDir returns the user's home directory, or dies trying. +func homeDir() string { + home, err := fs.ExpandTilde("~") + if err != nil { + panic(err) + } + return home +} + +func GetTimestamped(key LocationEnum) string { + // We take the roundtrip via "${timestamp}" instead of passing the path + // directly through time.Format() to avoid issues when the path we are + // expanding contains numbers; otherwise for example + // /home/user2006/.../panic-20060102-150405.log would get both instances of + // 2006 replaced by 2015... + tpl := locations[key] + now := time.Now().Format("20060102-150405") + return strings.Replace(tpl, "${timestamp}", now, -1) +} diff --git a/lib/util/utils.go b/lib/util/utils.go index d2331a25e..15fb6db57 100644 --- a/lib/util/utils.go +++ b/lib/util/utils.go @@ -15,8 +15,12 @@ import ( "strings" ) +type defaultParser interface { + ParseDefault(string) error +} + // SetDefaults sets default values on a struct, based on the default annotation. -func SetDefaults(data interface{}) error { +func SetDefaults(data interface{}) { s := reflect.ValueOf(data).Elem() t := s.Type() @@ -26,15 +30,22 @@ func SetDefaults(data interface{}) error { v := tag.Get("default") if len(v) > 0 { - if parser, ok := f.Interface().(interface { - ParseDefault(string) (interface{}, error) - }); ok { - val, err := parser.ParseDefault(v) - if err != nil { - panic(err) + if f.CanInterface() { + if parser, ok := f.Interface().(defaultParser); ok { + if err := parser.ParseDefault(v); err != nil { + panic(err) + } + continue + } + } + + if f.CanAddr() && f.Addr().CanInterface() { + if parser, ok := f.Addr().Interface().(defaultParser); ok { + if err := parser.ParseDefault(v); err != nil { + panic(err) + } + continue } - f.Set(reflect.ValueOf(val)) - continue } switch f.Interface().(type) { @@ -44,14 +55,14 @@ func SetDefaults(data interface{}) error { case int: i, err := strconv.ParseInt(v, 10, 64) if err != nil { - return err + panic(err) } f.SetInt(i) case float64: i, err := strconv.ParseFloat(v, 64) if err != nil { - return err + panic(err) } f.SetFloat(i) @@ -68,7 +79,6 @@ func SetDefaults(data interface{}) error { } } } - return nil } // CopyMatchingTag copies fields tagged tag:"value" from "from" struct onto "to" struct. diff --git a/lib/util/utils_test.go b/lib/util/utils_test.go index aba452d35..837d45bf4 100644 --- a/lib/util/utils_test.go +++ b/lib/util/utils_test.go @@ -12,8 +12,9 @@ type Defaulter struct { Value string } -func (Defaulter) ParseDefault(v string) (interface{}, error) { - return Defaulter{Value: v}, nil +func (d *Defaulter) ParseDefault(v string) error { + *d = Defaulter{Value: v} + return nil } func TestSetDefaults(t *testing.T) { @@ -37,9 +38,7 @@ func TestSetDefaults(t *testing.T) { t.Errorf("defaulter failed") } - if err := SetDefaults(x); err != nil { - t.Error(err) - } + SetDefaults(x) if x.A != "string" { t.Error("string failed")