mirror of
https://github.com/octoleo/syncthing.git
synced 2024-11-08 22:31:04 +00:00
all: Use new reflect based CLI (#5487)
This commit is contained in:
parent
7bac927ac8
commit
dc929946fe
8
build.go
8
build.go
@ -768,10 +768,10 @@ func ldflags() string {
|
|||||||
|
|
||||||
b := new(bytes.Buffer)
|
b := new(bytes.Buffer)
|
||||||
b.WriteString("-w")
|
b.WriteString("-w")
|
||||||
fmt.Fprintf(b, " -X main.Version%c%s", sep, version)
|
fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Version%c%s", sep, version)
|
||||||
fmt.Fprintf(b, " -X main.BuildStamp%c%d", sep, buildStamp())
|
fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Stamp%c%d", sep, buildStamp())
|
||||||
fmt.Fprintf(b, " -X main.BuildUser%c%s", sep, buildUser())
|
fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.User%c%s", sep, buildUser())
|
||||||
fmt.Fprintf(b, " -X main.BuildHost%c%s", sep, buildHost())
|
fmt.Fprintf(b, " -X github.com/syncthing/syncthing/lib/build.Host%c%s", sep, buildHost())
|
||||||
return b.String()
|
return b.String()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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.
|
|
@ -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
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
"context"
|
||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/AudriusButkevicius/cli"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
type APIClient struct {
|
type APIClient struct {
|
||||||
httpClient http.Client
|
http.Client
|
||||||
endpoint string
|
cfg config.GUIConfiguration
|
||||||
apikey string
|
apikey string
|
||||||
username string
|
|
||||||
password string
|
|
||||||
id string
|
|
||||||
csrf string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var instance *APIClient
|
func getClient(cfg config.GUIConfiguration) *APIClient {
|
||||||
|
|
||||||
func getClient(c *cli.Context) *APIClient {
|
|
||||||
if instance != nil {
|
|
||||||
return instance
|
|
||||||
}
|
|
||||||
endpoint := c.GlobalString("endpoint")
|
|
||||||
if !strings.HasPrefix(endpoint, "http") {
|
|
||||||
endpoint = "http://" + endpoint
|
|
||||||
}
|
|
||||||
httpClient := http.Client{
|
httpClient := http.Client{
|
||||||
Transport: &http.Transport{
|
Transport: &http.Transport{
|
||||||
TLSClientConfig: &tls.Config{
|
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{
|
return &APIClient{
|
||||||
httpClient: httpClient,
|
Client: httpClient,
|
||||||
endpoint: endpoint,
|
cfg: cfg,
|
||||||
apikey: c.GlobalString("apikey"),
|
apikey: cfg.APIKey,
|
||||||
username: c.GlobalString("username"),
|
|
||||||
password: c.GlobalString("password"),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
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 {
|
func (c *APIClient) Endpoint() string {
|
||||||
if client.apikey != "" {
|
if c.cfg.Network() == "unix" {
|
||||||
request.Header.Set("X-API-Key", client.apikey)
|
return "http://unix/"
|
||||||
}
|
}
|
||||||
if client.username != "" || client.password != "" {
|
url := c.cfg.URL()
|
||||||
request.SetBasicAuth(client.username, client.password)
|
if !strings.HasSuffix(url, "/") {
|
||||||
}
|
url += "/"
|
||||||
if client.csrf != "" {
|
|
||||||
request.Header.Set("X-CSRF-Token-"+client.id[:5], client.csrf)
|
|
||||||
}
|
}
|
||||||
|
return url
|
||||||
|
}
|
||||||
|
|
||||||
response, err := client.httpClient.Do(request)
|
func (c *APIClient) Do(req *http.Request) (*http.Response, error) {
|
||||||
die(err)
|
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 {
|
if response.StatusCode == 404 {
|
||||||
die("Invalid endpoint or API call")
|
return fmt.Errorf("Invalid endpoint or API call")
|
||||||
} else if response.StatusCode == 401 {
|
|
||||||
die("Invalid username or password")
|
|
||||||
} else if response.StatusCode == 403 {
|
} else if response.StatusCode == 403 {
|
||||||
if client.apikey == "" {
|
return fmt.Errorf("Invalid API key")
|
||||||
die("Invalid CSRF token")
|
|
||||||
}
|
|
||||||
die("Invalid API key")
|
|
||||||
} else if response.StatusCode != 200 {
|
} else if response.StatusCode != 200 {
|
||||||
body := strings.TrimSpace(string(responseToBArray(response)))
|
data, err := responseToBArray(response)
|
||||||
if body != "" {
|
if err != nil {
|
||||||
die(body)
|
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
|
return nil
|
||||||
}
|
|
||||||
|
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
|
@ -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")
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
||||||
}
|
|
@ -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-<key>")
|
|
||||||
}
|
|
||||||
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-<key>")
|
|
||||||
}
|
|
||||||
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-<key>")
|
|
||||||
}
|
|
||||||
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")
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
@ -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)
|
|
||||||
}
|
|
60
cmd/stcli/errors.go
Normal file
60
cmd/stcli/errors.go
Normal file
@ -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
|
||||||
|
}
|
@ -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",
|
|
||||||
}
|
|
@ -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
|
package main
|
||||||
|
|
||||||
import (
|
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() {
|
func main() {
|
||||||
app := cli.NewApp()
|
// This is somewhat a hack around a chicken and egg problem.
|
||||||
app.Name = "syncthing-cli"
|
// We need to set the home directory and potentially other flags to know where the syncthing instance is running
|
||||||
app.Author = "Audrius Butkevičius"
|
// in order to get it's config ... which we then use to construct the actual CLI ... at which point it's too late
|
||||||
app.Email = "audrius.butkevicius@gmail.com"
|
// to add flags there...
|
||||||
app.Usage = "Syncthing command line interface"
|
homeBaseDir := locations.GetBaseDir(locations.ConfigBaseDir)
|
||||||
app.Version = "0.1"
|
guiCfg := config.GUIConfiguration{}
|
||||||
app.HideHelp = true
|
|
||||||
|
|
||||||
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{
|
cli.StringFlag{
|
||||||
Name: "endpoint, e",
|
Name: "gui-address",
|
||||||
Value: "http://127.0.0.1:8384",
|
Value: guiCfg.RawAddress,
|
||||||
Usage: "End point to connect to",
|
Usage: "Override GUI address (e.g. \"http://192.0.2.42:8443\")",
|
||||||
EnvVar: "STENDPOINT",
|
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "apikey, k",
|
Name: "gui-apikey",
|
||||||
Value: "",
|
Value: guiCfg.APIKey,
|
||||||
Usage: "API Key",
|
Usage: "Override GUI API key",
|
||||||
EnvVar: "STAPIKEY",
|
|
||||||
},
|
},
|
||||||
cli.StringFlag{
|
cli.StringFlag{
|
||||||
Name: "username, u",
|
Name: "home",
|
||||||
Value: "",
|
Value: homeBaseDir,
|
||||||
Usage: "Username",
|
Usage: "Set configuration directory",
|
||||||
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",
|
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
sort.Sort(ByAlphabet(cliCommands))
|
// Do not print usage of these flags, and ignore errors as this can't understand plenty of things
|
||||||
app.Commands = cliCommands
|
flags.Usage = func() {}
|
||||||
app.RunAndExitOnError()
|
_ = 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))
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
78
cmd/stcli/operations.go
Normal file
78
cmd/stcli/operations.go
Normal file
@ -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")
|
||||||
|
}
|
44
cmd/stcli/show.go
Normal file
44
cmd/stcli/show.go
Normal file
@ -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")),
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
@ -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
|
package main
|
||||||
|
|
||||||
@ -8,78 +12,37 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"net/http"
|
"net/http"
|
||||||
"os"
|
"os"
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"text/tabwriter"
|
"text/tabwriter"
|
||||||
"unicode"
|
|
||||||
|
|
||||||
"github.com/AudriusButkevicius/cli"
|
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/urfave/cli"
|
||||||
)
|
)
|
||||||
|
|
||||||
func responseToBArray(response *http.Response) []byte {
|
func responseToBArray(response *http.Response) ([]byte, error) {
|
||||||
defer response.Body.Close()
|
|
||||||
bytes, err := ioutil.ReadAll(response.Body)
|
bytes, err := ioutil.ReadAll(response.Body)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
die(err)
|
return nil, err
|
||||||
}
|
}
|
||||||
return bytes
|
return bytes, response.Body.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
func die(vals ...interface{}) {
|
func emptyPost(url string) cli.ActionFunc {
|
||||||
if len(vals) > 1 || vals[0] != nil {
|
return func(c *cli.Context) error {
|
||||||
os.Stderr.WriteString(fmt.Sprintln(vals...))
|
client := c.App.Metadata["client"].(*APIClient)
|
||||||
os.Exit(1)
|
_, err := client.Post(url, "")
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func wrappedHTTPPost(url string) func(c *cli.Context) {
|
func dumpOutput(url string) cli.ActionFunc {
|
||||||
return func(c *cli.Context) {
|
return func(c *cli.Context) error {
|
||||||
httpPost(c, url, "")
|
client := c.App.Metadata["client"].(*APIClient)
|
||||||
}
|
response, err := client.Get(url)
|
||||||
}
|
if err != nil {
|
||||||
|
return err
|
||||||
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)
|
|
||||||
}
|
}
|
||||||
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 {
|
func newTableWriter() *tabwriter.Writer {
|
||||||
@ -88,78 +51,51 @@ func newTableWriter() *tabwriter.Writer {
|
|||||||
return writer
|
return writer
|
||||||
}
|
}
|
||||||
|
|
||||||
func getMyID(c *cli.Context) string {
|
func getConfig(c *APIClient) (config.Configuration, error) {
|
||||||
response := httpGet(c, "system/status")
|
cfg := config.Configuration{}
|
||||||
data := make(map[string]interface{})
|
response, err := c.Get("system/config")
|
||||||
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)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
die(input + " is not a valid value for a boolean")
|
return cfg, err
|
||||||
}
|
}
|
||||||
return val
|
bytes, err := responseToBArray(response)
|
||||||
}
|
|
||||||
|
|
||||||
func parseInt(input string) int {
|
|
||||||
val, err := strconv.ParseInt(input, 0, 64)
|
|
||||||
if err != nil {
|
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 {
|
func expects(n int, actionFunc cli.ActionFunc) cli.ActionFunc {
|
||||||
val, err := strconv.ParseUint(input, 0, 64)
|
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 {
|
if err != nil {
|
||||||
die(input + " is not a valid value for an unsigned integer")
|
return err
|
||||||
}
|
}
|
||||||
return int(val)
|
var data interface{}
|
||||||
}
|
if err := json.Unmarshal(bytes, &data); err != nil {
|
||||||
|
return err
|
||||||
func parsePort(input string) int {
|
}
|
||||||
port := parseUint(input)
|
// TODO: Check flag for pretty print format
|
||||||
if port < 1 || port > 65535 {
|
return prettyPrintJSON(data)
|
||||||
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 <ip or hostname>:<port>")
|
|
||||||
}
|
|
||||||
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 <ip or hostname>:<port>")
|
|
||||||
}
|
|
||||||
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
|
|
||||||
}
|
}
|
||||||
|
@ -28,12 +28,14 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
metrics "github.com/rcrowley/go-metrics"
|
metrics "github.com/rcrowley/go-metrics"
|
||||||
|
"github.com/syncthing/syncthing/lib/build"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/connections"
|
"github.com/syncthing/syncthing/lib/connections"
|
||||||
"github.com/syncthing/syncthing/lib/db"
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
"github.com/syncthing/syncthing/lib/discover"
|
"github.com/syncthing/syncthing/lib/discover"
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
"github.com/syncthing/syncthing/lib/fs"
|
"github.com/syncthing/syncthing/lib/fs"
|
||||||
|
"github.com/syncthing/syncthing/lib/locations"
|
||||||
"github.com/syncthing/syncthing/lib/logger"
|
"github.com/syncthing/syncthing/lib/logger"
|
||||||
"github.com/syncthing/syncthing/lib/model"
|
"github.com/syncthing/syncthing/lib/model"
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"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 {
|
func withDetailsMiddleware(id protocol.DeviceID, h http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
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())
|
w.Header().Set("X-Syncthing-ID", id.String())
|
||||||
h.ServeHTTP(w, r)
|
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) {
|
func (s *apiService) getSystemVersion(w http.ResponseWriter, r *http.Request) {
|
||||||
sendJSON(w, map[string]interface{}{
|
sendJSON(w, map[string]interface{}{
|
||||||
"version": Version,
|
"version": build.Version,
|
||||||
"codename": Codename,
|
"codename": build.Codename,
|
||||||
"longVersion": LongVersion,
|
"longVersion": build.LongVersion,
|
||||||
"os": runtime.GOOS,
|
"os": runtime.GOOS,
|
||||||
"arch": runtime.GOARCH,
|
"arch": runtime.GOARCH,
|
||||||
"isBeta": IsBeta,
|
"isBeta": build.IsBeta,
|
||||||
"isCandidate": IsCandidate,
|
"isCandidate": build.IsCandidate,
|
||||||
"isRelease": IsRelease,
|
"isRelease": build.IsRelease,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1080,7 +1082,7 @@ func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Panic files
|
// 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 {
|
for _, f := range panicFiles {
|
||||||
if panicFile, err := ioutil.ReadFile(f); err != nil {
|
if panicFile, err := ioutil.ReadFile(f); err != nil {
|
||||||
l.Warnf("Support bundle: failed to load %s: %s", filepath.Base(f), err)
|
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)
|
// 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})
|
files = append(files, fileEntry{name: "log-ondisk.txt", data: logFile})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Version and platform information as a JSON
|
// Version and platform information as a JSON
|
||||||
if versionPlatform, err := json.MarshalIndent(map[string]string{
|
if versionPlatform, err := json.MarshalIndent(map[string]string{
|
||||||
"now": time.Now().Format(time.RFC3339),
|
"now": time.Now().Format(time.RFC3339),
|
||||||
"version": Version,
|
"version": build.Version,
|
||||||
"codename": Codename,
|
"codename": build.Codename,
|
||||||
"longVersion": LongVersion,
|
"longVersion": build.LongVersion,
|
||||||
"os": runtime.GOOS,
|
"os": runtime.GOOS,
|
||||||
"arch": runtime.GOARCH,
|
"arch": runtime.GOARCH,
|
||||||
}, "", " "); err == nil {
|
}, "", " "); err == nil {
|
||||||
@ -1118,14 +1120,14 @@ func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Heap and CPU Proofs as a pprof extension
|
// Heap and CPU Proofs as a pprof extension
|
||||||
var heapBuffer, cpuBuffer bytes.Buffer
|
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()
|
runtime.GC()
|
||||||
if err := pprof.WriteHeapProfile(&heapBuffer); err == nil {
|
if err := pprof.WriteHeapProfile(&heapBuffer); err == nil {
|
||||||
files = append(files, fileEntry{name: filename, data: heapBuffer.Bytes()})
|
files = append(files, fileEntry{name: filename, data: heapBuffer.Bytes()})
|
||||||
}
|
}
|
||||||
|
|
||||||
const duration = 4 * time.Second
|
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 {
|
if err := pprof.StartCPUProfile(&cpuBuffer); err == nil {
|
||||||
time.Sleep(duration)
|
time.Sleep(duration)
|
||||||
pprof.StopCPUProfile()
|
pprof.StopCPUProfile()
|
||||||
@ -1142,7 +1144,7 @@ func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
// Set zip file name and path
|
// Set zip file name and path
|
||||||
zipFileName := fmt.Sprintf("support-bundle-%s-%s.zip", s.id.Short().String(), time.Now().Format("2006-01-02T150405"))
|
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)
|
// Write buffer zip to local zip file (back up)
|
||||||
if err := ioutil.WriteFile(zipFilePath, zipFilesBuffer.Bytes(), 0600); err != nil {
|
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
|
return
|
||||||
}
|
}
|
||||||
opts := s.cfg.Options()
|
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 {
|
if err != nil {
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
res := make(map[string]interface{})
|
res := make(map[string]interface{})
|
||||||
res["running"] = Version
|
res["running"] = build.Version
|
||||||
res["latest"] = rel.Tag
|
res["latest"] = rel.Tag
|
||||||
res["newer"] = upgrade.CompareVersions(rel.Tag, Version) == upgrade.Newer
|
res["newer"] = upgrade.CompareVersions(rel.Tag, build.Version) == upgrade.Newer
|
||||||
res["majorNewer"] = upgrade.CompareVersions(rel.Tag, Version) == upgrade.MajorNewer
|
res["majorNewer"] = upgrade.CompareVersions(rel.Tag, build.Version) == upgrade.MajorNewer
|
||||||
|
|
||||||
sendJSON(w, res)
|
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) {
|
func (s *apiService) postSystemUpgrade(w http.ResponseWriter, r *http.Request) {
|
||||||
opts := s.cfg.Options()
|
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 {
|
if err != nil {
|
||||||
l.Warnln("getting latest release:", err)
|
l.Warnln("getting latest release:", err)
|
||||||
http.Error(w, err.Error(), 500)
|
http.Error(w, err.Error(), 500)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if upgrade.CompareVersions(rel.Tag, Version) > upgrade.Equal {
|
if upgrade.CompareVersions(rel.Tag, build.Version) > upgrade.Equal {
|
||||||
err = upgrade.To(rel)
|
err = upgrade.To(rel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Warnln("upgrading:", err)
|
l.Warnln("upgrading:", err)
|
||||||
@ -1641,7 +1643,7 @@ func (s *apiService) getCPUProf(w http.ResponseWriter, r *http.Request) {
|
|||||||
duration = 30 * time.Second
|
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-Type", "application/octet-stream")
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
|
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) {
|
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-Type", "application/octet-stream")
|
||||||
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
|
w.Header().Set("Content-Disposition", "attachment; filename="+filename)
|
||||||
|
@ -14,6 +14,7 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
|
"github.com/syncthing/syncthing/lib/locations"
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
"github.com/syncthing/syncthing/lib/rand"
|
"github.com/syncthing/syncthing/lib/rand"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"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
|
// We're ignoring errors in here. It's not super critical and there's
|
||||||
// nothing relevant we can do about them anyway...
|
// nothing relevant we can do about them anyway...
|
||||||
|
|
||||||
name := locations[locCsrfTokens]
|
name := locations.Get(locations.CsrfTokens)
|
||||||
f, err := osutil.CreateAtomic(name)
|
f, err := osutil.CreateAtomic(name)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
@ -129,7 +130,7 @@ func saveCsrfTokens() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadCsrfTokens() {
|
func loadCsrfTokens() {
|
||||||
f, err := os.Open(locations[locCsrfTokens])
|
f, err := os.Open(locations.Get(locations.CsrfTokens))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
@ -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)
|
|
||||||
}
|
|
@ -16,12 +16,12 @@ import (
|
|||||||
"io/ioutil"
|
"io/ioutil"
|
||||||
"log"
|
"log"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
_ "net/http/pprof" // Need to import this to support STPROFILER.
|
||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path"
|
"path"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
|
||||||
"runtime"
|
"runtime"
|
||||||
"runtime/pprof"
|
"runtime/pprof"
|
||||||
"sort"
|
"sort"
|
||||||
@ -30,6 +30,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/build"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/connections"
|
"github.com/syncthing/syncthing/lib/connections"
|
||||||
"github.com/syncthing/syncthing/lib/db"
|
"github.com/syncthing/syncthing/lib/db"
|
||||||
@ -37,6 +38,7 @@ import (
|
|||||||
"github.com/syncthing/syncthing/lib/discover"
|
"github.com/syncthing/syncthing/lib/discover"
|
||||||
"github.com/syncthing/syncthing/lib/events"
|
"github.com/syncthing/syncthing/lib/events"
|
||||||
"github.com/syncthing/syncthing/lib/fs"
|
"github.com/syncthing/syncthing/lib/fs"
|
||||||
|
"github.com/syncthing/syncthing/lib/locations"
|
||||||
"github.com/syncthing/syncthing/lib/logger"
|
"github.com/syncthing/syncthing/lib/logger"
|
||||||
"github.com/syncthing/syncthing/lib/model"
|
"github.com/syncthing/syncthing/lib/model"
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
@ -47,23 +49,6 @@ import (
|
|||||||
"github.com/syncthing/syncthing/lib/upgrade"
|
"github.com/syncthing/syncthing/lib/upgrade"
|
||||||
|
|
||||||
"github.com/thejerf/suture"
|
"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 (
|
const (
|
||||||
@ -83,46 +68,6 @@ const (
|
|||||||
maxSystemLog = 250
|
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 (
|
var (
|
||||||
myID protocol.DeviceID
|
myID protocol.DeviceID
|
||||||
stop = make(chan int)
|
stop = make(chan int)
|
||||||
@ -320,8 +265,6 @@ func parseCommandLineOptions() RuntimeOptions {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
setBuildMetadata()
|
|
||||||
|
|
||||||
options := parseCommandLineOptions()
|
options := parseCommandLineOptions()
|
||||||
l.SetFlags(options.logFlags)
|
l.SetFlags(options.logFlags)
|
||||||
|
|
||||||
@ -355,27 +298,25 @@ func main() {
|
|||||||
l.Fatalln(err)
|
l.Fatalln(err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
baseDirs["config"] = options.confDir
|
if err := locations.SetBaseDir(locations.ConfigBaseDir, options.confDir); err != nil {
|
||||||
}
|
l.Fatalln(err)
|
||||||
|
}
|
||||||
if err := expandLocations(); err != nil {
|
|
||||||
l.Fatalln(err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.logFile == "" {
|
if options.logFile == "" {
|
||||||
// Blank means use the default logfile location. We must set this
|
// Blank means use the default logfile location. We must set this
|
||||||
// *after* expandLocations above.
|
// *after* expandLocations above.
|
||||||
options.logFile = locations[locLogFile]
|
options.logFile = locations.Get(locations.LogFile)
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.assetDir == "" {
|
if options.assetDir == "" {
|
||||||
// The asset dir is blank if STGUIASSETS wasn't set, in which case we
|
// The asset dir is blank if STGUIASSETS wasn't set, in which case we
|
||||||
// should look for extra assets in the default place.
|
// should look for extra assets in the default place.
|
||||||
options.assetDir = locations[locGUIAssets]
|
options.assetDir = locations.Get(locations.GUIAssets)
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.showVersion {
|
if options.showVersion {
|
||||||
fmt.Println(LongVersion)
|
fmt.Println(build.LongVersion)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -390,7 +331,10 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
if options.showDeviceId {
|
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 {
|
if err != nil {
|
||||||
l.Fatalln("Error reading device ID:", err)
|
l.Fatalln("Error reading device ID:", err)
|
||||||
}
|
}
|
||||||
@ -411,7 +355,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Ensure that our home directory exists.
|
// Ensure that our home directory exists.
|
||||||
ensureDir(baseDirs["config"], 0700)
|
ensureDir(locations.GetBaseDir(locations.ConfigBaseDir), 0700)
|
||||||
|
|
||||||
if options.upgradeTo != "" {
|
if options.upgradeTo != "" {
|
||||||
err := upgrade.ToURL(options.upgradeTo)
|
err := upgrade.ToURL(options.upgradeTo)
|
||||||
@ -521,24 +465,24 @@ func debugFacilities() string {
|
|||||||
func checkUpgrade() upgrade.Release {
|
func checkUpgrade() upgrade.Release {
|
||||||
cfg, _ := loadOrDefaultConfig()
|
cfg, _ := loadOrDefaultConfig()
|
||||||
opts := cfg.Options()
|
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 {
|
if err != nil {
|
||||||
l.Fatalln("Upgrade:", err)
|
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)."
|
noUpgradeMessage := "No upgrade available (current %q >= latest %q)."
|
||||||
l.Infof(noUpgradeMessage, Version, release.Tag)
|
l.Infof(noUpgradeMessage, build.Version, release.Tag)
|
||||||
os.Exit(exitNoUpgradeAvailable)
|
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
|
return release
|
||||||
}
|
}
|
||||||
|
|
||||||
func performUpgrade(release upgrade.Release) {
|
func performUpgrade(release upgrade.Release) {
|
||||||
// Use leveldb database locks to protect against concurrent upgrades
|
// Use leveldb database locks to protect against concurrent upgrades
|
||||||
_, err := db.Open(locations[locDatabase])
|
_, err := db.Open(locations.Get(locations.Database))
|
||||||
if err == nil {
|
if err == nil {
|
||||||
err = upgrade.To(release)
|
err = upgrade.To(release)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -636,10 +580,17 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
|||||||
osutil.MaximizeOpenFileLimit()
|
osutil.MaximizeOpenFileLimit()
|
||||||
|
|
||||||
// Ensure that we have a certificate and key.
|
// 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 {
|
if err != nil {
|
||||||
l.Infof("Generating ECDSA key and certificate for %s...", tlsDefaultCommonName)
|
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 {
|
if err != nil {
|
||||||
l.Fatalln(err)
|
l.Fatalln(err)
|
||||||
}
|
}
|
||||||
@ -648,7 +599,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
|||||||
myID = protocol.NewDeviceID(cert.Certificate[0])
|
myID = protocol.NewDeviceID(cert.Certificate[0])
|
||||||
l.SetPrefix(fmt.Sprintf("[%s] ", myID.String()[:5]))
|
l.SetPrefix(fmt.Sprintf("[%s] ", myID.String()[:5]))
|
||||||
|
|
||||||
l.Infoln(LongVersion)
|
l.Infoln(build.LongVersion)
|
||||||
l.Infoln("My ID:", myID)
|
l.Infoln("My ID:", myID)
|
||||||
|
|
||||||
// Select SHA256 implementation and report. Affected by the
|
// 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.
|
// Emit the Starting event, now that we know who we are.
|
||||||
|
|
||||||
events.Default.Log(events.Starting, map[string]string{
|
events.Default.Log(events.Starting, map[string]string{
|
||||||
"home": baseDirs["config"],
|
"home": locations.GetBaseDir(locations.ConfigBaseDir),
|
||||||
"myID": myID.String(),
|
"myID": myID.String(),
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -683,7 +634,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
|||||||
perf := cpuBench(3, 150*time.Millisecond, true)
|
perf := cpuBench(3, 150*time.Millisecond, true)
|
||||||
l.Infof("Hashing performance is %.02f MB/s", perf)
|
l.Infof("Hashing performance is %.02f MB/s", perf)
|
||||||
|
|
||||||
dbFile := locations[locDatabase]
|
dbFile := locations.Get(locations.Database)
|
||||||
ldb, err := db.Open(dbFile)
|
ldb, err := db.Open(dbFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Fatalln("Error opening database:", err)
|
l.Fatalln("Error opening database:", err)
|
||||||
@ -698,10 +649,10 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
protectedFiles := []string{
|
protectedFiles := []string{
|
||||||
locations[locDatabase],
|
locations.Get(locations.Database),
|
||||||
locations[locConfigFile],
|
locations.Get(locations.ConfigFile),
|
||||||
locations[locCertFile],
|
locations.Get(locations.CertFile),
|
||||||
locations[locKeyFile],
|
locations.Get(locations.KeyFile),
|
||||||
}
|
}
|
||||||
|
|
||||||
// Remove database entries for folders that no longer exist in the config
|
// 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.
|
// 0.14.45-pineapple is not.
|
||||||
|
|
||||||
prevParts := strings.Split(prevVersion, "-")
|
prevParts := strings.Split(prevVersion, "-")
|
||||||
curParts := strings.Split(Version, "-")
|
curParts := strings.Split(build.Version, "-")
|
||||||
if prevParts[0] != curParts[0] {
|
if prevParts[0] != curParts[0] {
|
||||||
if prevVersion != "" {
|
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
|
// Drop delta indexes in case we've changed random stuff we
|
||||||
@ -734,16 +685,16 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
|||||||
db.DropDeltaIndexIDs(ldb)
|
db.DropDeltaIndexIDs(ldb)
|
||||||
|
|
||||||
// Remember the new version.
|
// 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 t := os.Getenv("STDEADLOCKTIMEOUT"); t != "" {
|
||||||
if secs, _ := strconv.Atoi(t); secs > 0 {
|
if secs, _ := strconv.Atoi(t); secs > 0 {
|
||||||
m.StartDeadlockDetector(time.Duration(secs) * time.Second)
|
m.StartDeadlockDetector(time.Duration(secs) * time.Second)
|
||||||
}
|
}
|
||||||
} else if !IsRelease || IsBeta {
|
} else if !build.IsRelease || build.IsBeta {
|
||||||
m.StartDeadlockDetector(20 * time.Minute)
|
m.StartDeadlockDetector(20 * time.Minute)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -842,7 +793,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
|||||||
|
|
||||||
// Candidate builds always run with usage reporting.
|
// 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.")
|
l.Infoln("Anonymous usage reporting is always enabled for candidate releases.")
|
||||||
if opts.URAccepted != usageReportVersion {
|
if opts.URAccepted != usageReportVersion {
|
||||||
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
|
// unless we are in a build where it's disabled or the STNOUPGRADE
|
||||||
// environment variable is set.
|
// 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.")
|
l.Infoln("Automatic upgrade is always enabled for candidate releases.")
|
||||||
if opts := cfg.Options(); opts.AutoUpgradeIntervalH == 0 || opts.AutoUpgradeIntervalH > 24 {
|
if opts := cfg.Options(); opts.AutoUpgradeIntervalH == 0 || opts.AutoUpgradeIntervalH > 24 {
|
||||||
opts.AutoUpgradeIntervalH = 12
|
opts.AutoUpgradeIntervalH = 12
|
||||||
@ -943,7 +894,7 @@ func setupSignalHandling() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadOrDefaultConfig() (*config.Wrapper, error) {
|
func loadOrDefaultConfig() (*config.Wrapper, error) {
|
||||||
cfgFile := locations[locConfigFile]
|
cfgFile := locations.Get(locations.ConfigFile)
|
||||||
cfg, err := config.Load(cfgFile, myID)
|
cfg, err := config.Load(cfgFile, myID)
|
||||||
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@ -954,7 +905,7 @@ func loadOrDefaultConfig() (*config.Wrapper, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func loadConfigAtStartup() *config.Wrapper {
|
func loadConfigAtStartup() *config.Wrapper {
|
||||||
cfgFile := locations[locConfigFile]
|
cfgFile := locations.Get(locations.ConfigFile)
|
||||||
cfg, err := config.Load(cfgFile, myID)
|
cfg, err := config.Load(cfgFile, myID)
|
||||||
if os.IsNotExist(err) {
|
if os.IsNotExist(err) {
|
||||||
cfg = defaultConfig(cfgFile)
|
cfg = defaultConfig(cfgFile)
|
||||||
@ -1018,7 +969,7 @@ func startAuditing(mainService *suture.Supervisor, auditFile string) {
|
|||||||
auditDest = "stderr"
|
auditDest = "stderr"
|
||||||
} else {
|
} else {
|
||||||
if auditFile == "" {
|
if auditFile == "" {
|
||||||
auditFile = timestampedLoc(locAuditLog)
|
auditFile = locations.GetTimestamped(locations.AuditLog)
|
||||||
auditFlags = os.O_WRONLY | os.O_CREATE | os.O_EXCL
|
auditFlags = os.O_WRONLY | os.O_CREATE | os.O_EXCL
|
||||||
} else {
|
} else {
|
||||||
auditFlags = os.O_WRONLY | os.O_CREATE | os.O_APPEND
|
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()
|
cpu := newCPUService()
|
||||||
mainService.Add(cpu)
|
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)
|
cfg.Subscribe(api)
|
||||||
mainService.Add(api)
|
mainService.Add(api)
|
||||||
|
|
||||||
@ -1074,13 +1025,13 @@ func defaultConfig(cfgFile string) *config.Wrapper {
|
|||||||
return config.Wrap(cfgFile, newCfg)
|
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")
|
l.Infoln("Default folder created and/or linked to new config")
|
||||||
return config.Wrap(cfgFile, newCfg)
|
return config.Wrap(cfgFile, newCfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
func resetDB() error {
|
func resetDB() error {
|
||||||
return os.RemoveAll(locations[locDatabase])
|
return os.RemoveAll(locations.Get(locations.Database))
|
||||||
}
|
}
|
||||||
|
|
||||||
func restart() {
|
func restart() {
|
||||||
@ -1142,10 +1093,10 @@ func autoUpgrade(cfg *config.Wrapper) {
|
|||||||
select {
|
select {
|
||||||
case event := <-sub.C():
|
case event := <-sub.C():
|
||||||
data, ok := event.Data.(map[string]string)
|
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
|
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:
|
case <-timer.C:
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1157,7 +1108,7 @@ func autoUpgrade(cfg *config.Wrapper) {
|
|||||||
checkInterval = time.Hour
|
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 {
|
if err == upgrade.ErrUpgradeUnsupported {
|
||||||
events.Default.Unsubscribe(sub)
|
events.Default.Unsubscribe(sub)
|
||||||
return
|
return
|
||||||
@ -1170,13 +1121,13 @@ func autoUpgrade(cfg *config.Wrapper) {
|
|||||||
continue
|
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
|
// Skip equal, older or majorly newer (incompatible) versions
|
||||||
timer.Reset(checkInterval)
|
timer.Reset(checkInterval)
|
||||||
continue
|
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)
|
err = upgrade.To(rel)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Warnln("Automatic upgrade:", err)
|
l.Warnln("Automatic upgrade:", err)
|
||||||
@ -1209,7 +1160,7 @@ func cleanConfigDirectory() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
for pat, dur := range patterns {
|
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)
|
files, err := fs.Glob(pat)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
l.Infoln("Cleaning:", err)
|
l.Infoln("Cleaning:", err)
|
||||||
@ -1250,13 +1201,13 @@ func checkShortIDs(cfg *config.Wrapper) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func showPaths(options RuntimeOptions) {
|
func showPaths(options RuntimeOptions) {
|
||||||
fmt.Printf("Configuration file:\n\t%s\n\n", locations[locConfigFile])
|
fmt.Printf("Configuration file:\n\t%s\n\n", locations.Get(locations.ConfigFile))
|
||||||
fmt.Printf("Database directory:\n\t%s\n\n", locations[locDatabase])
|
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[locKeyFile], locations[locCertFile])
|
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[locHTTPSKeyFile], locations[locHTTPSCertFile])
|
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("Log file:\n\t%s\n\n", options.logFile)
|
||||||
fmt.Printf("GUI override directory:\n\t%s\n\n", options.assetDir)
|
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) {
|
func setPauseState(cfg *config.Wrapper, paused bool) {
|
||||||
|
@ -36,29 +36,3 @@ func TestShortIDCheck(t *testing.T) {
|
|||||||
t.Error("Should have gotten an error")
|
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
@ -17,6 +17,7 @@ import (
|
|||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/locations"
|
||||||
"github.com/syncthing/syncthing/lib/osutil"
|
"github.com/syncthing/syncthing/lib/osutil"
|
||||||
"github.com/syncthing/syncthing/lib/sync"
|
"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:") {
|
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 {
|
if err != nil {
|
||||||
l.Warnln("Create panic log:", err)
|
l.Warnln("Create panic log:", err)
|
||||||
continue
|
continue
|
||||||
|
@ -20,6 +20,7 @@ import (
|
|||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/syncthing/syncthing/lib/build"
|
||||||
"github.com/syncthing/syncthing/lib/config"
|
"github.com/syncthing/syncthing/lib/config"
|
||||||
"github.com/syncthing/syncthing/lib/connections"
|
"github.com/syncthing/syncthing/lib/connections"
|
||||||
"github.com/syncthing/syncthing/lib/dialer"
|
"github.com/syncthing/syncthing/lib/dialer"
|
||||||
@ -41,8 +42,8 @@ func reportData(cfg configIntf, m modelIntf, connectionsService connectionsIntf,
|
|||||||
res := make(map[string]interface{})
|
res := make(map[string]interface{})
|
||||||
res["urVersion"] = version
|
res["urVersion"] = version
|
||||||
res["uniqueID"] = opts.URUniqueID
|
res["uniqueID"] = opts.URUniqueID
|
||||||
res["version"] = Version
|
res["version"] = build.Version
|
||||||
res["longVersion"] = LongVersion
|
res["longVersion"] = build.LongVersion
|
||||||
res["platform"] = runtime.GOOS + "-" + runtime.GOARCH
|
res["platform"] = runtime.GOOS + "-" + runtime.GOARCH
|
||||||
res["numFolders"] = len(cfg.Folders())
|
res["numFolders"] = len(cfg.Folders())
|
||||||
res["numDevices"] = len(cfg.Devices())
|
res["numDevices"] = len(cfg.Devices())
|
||||||
|
7
go.mod
7
go.mod
@ -1,14 +1,15 @@
|
|||||||
module github.com/syncthing/syncthing
|
module github.com/syncthing/syncthing
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/AudriusButkevicius/cli v0.0.0-20140727204646-7f561c78b5a4
|
|
||||||
github.com/AudriusButkevicius/go-nat-pmp v0.0.0-20160522074932-452c97607362
|
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/bkaradzic/go-lz4 v0.0.0-20160924222819-7224d8d8f27e
|
||||||
github.com/calmh/du v1.0.1
|
github.com/calmh/du v1.0.1
|
||||||
github.com/calmh/xdr v1.1.0
|
github.com/calmh/xdr v1.1.0
|
||||||
github.com/chmduquesne/rollinghash v0.0.0-20180912150627-a60f8e7142b5
|
github.com/chmduquesne/rollinghash v0.0.0-20180912150627-a60f8e7142b5
|
||||||
github.com/d4l3k/messagediff v1.2.1
|
github.com/d4l3k/messagediff v1.2.1
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
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/gobwas/glob v0.0.0-20170212200151-51eb1ee00b6d
|
||||||
github.com/gogo/protobuf v1.2.0
|
github.com/gogo/protobuf v1.2.0
|
||||||
github.com/golang/groupcache v0.0.0-20171101203131-84a468cf14b4
|
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/kballard/go-shellquote v0.0.0-20170619183022-cd60e84ee657
|
||||||
github.com/kr/pretty v0.1.0 // indirect
|
github.com/kr/pretty v0.1.0 // indirect
|
||||||
github.com/lib/pq v1.0.0
|
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/minio/sha256-simd v0.0.0-20190117184323-cc1980cb0338
|
||||||
github.com/onsi/ginkgo v0.0.0-20171221013426-6c46eb8334b3 // indirect
|
github.com/onsi/ginkgo v0.0.0-20171221013426-6c46eb8334b3 // indirect
|
||||||
github.com/onsi/gomega v0.0.0-20171227184521-ba3724c94e4d // indirect
|
github.com/onsi/gomega v0.0.0-20171227184521-ba3724c94e4d // indirect
|
||||||
github.com/oschwald/geoip2-golang v1.1.0
|
github.com/oschwald/geoip2-golang v1.1.0
|
||||||
github.com/oschwald/maxminddb-golang v0.0.0-20170901134056-26fe5ace1c70 // indirect
|
github.com/oschwald/maxminddb-golang v0.0.0-20170901134056-26fe5ace1c70 // indirect
|
||||||
github.com/petermattis/goid v0.0.0-20170816195418-3db12ebb2a59 // 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/pmezard/go-difflib v1.0.0 // indirect
|
||||||
github.com/prometheus/client_golang v0.9.2
|
github.com/prometheus/client_golang v0.9.2
|
||||||
github.com/rcrowley/go-metrics v0.0.0-20171128170426-e181e095bae9
|
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/syncthing/notify v0.0.0-20181107104724-4e389ea6c0d8
|
||||||
github.com/syndtr/goleveldb v0.0.0-20171214120811-34011bf325bc
|
github.com/syndtr/goleveldb v0.0.0-20171214120811-34011bf325bc
|
||||||
github.com/thejerf/suture v3.0.2+incompatible
|
github.com/thejerf/suture v3.0.2+incompatible
|
||||||
|
github.com/urfave/cli v1.20.0
|
||||||
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0
|
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0
|
||||||
golang.org/x/crypto v0.0.0-20171231215028-0fcca4842a8d
|
golang.org/x/crypto v0.0.0-20171231215028-0fcca4842a8d
|
||||||
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc
|
golang.org/x/net v0.0.0-20181201002055-351d144fa1fc
|
||||||
|
14
go.sum
14
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 h1:l4qGIzSY0WhdXdR74XMYAtfc0Ri/RJVM4p6x/E/+WkA=
|
||||||
github.com/AudriusButkevicius/go-nat-pmp v0.0.0-20160522074932-452c97607362/go.mod h1:CEaBhA5lh1spxbPOELh5wNLKGsVQoahjUhVrJViVK8s=
|
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 h1:xJ4a3vCFaGF/jqvzLMYoU8P317H5OQ+Via4RmuPwCS0=
|
||||||
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
|
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=
|
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/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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
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 h1:IngNQgbqr5ZOU0exk395Szrvkzes9Ilk1fmJfkw7d+M=
|
||||||
github.com/gobwas/glob v0.0.0-20170212200151-51eb1ee00b6d/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
github.com/gobwas/glob v0.0.0-20170212200151-51eb1ee00b6d/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||||
github.com/gogo/protobuf v1.2.0 h1:xU6/SpYbvkNYiptHJYEDRseDLvYE7wSqhYYNy0QSUzI=
|
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/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 h1:X5PMW56eZitiTeO7tKzZxFCSpbFZJtkMMooicw2us9A=
|
||||||
github.com/lib/pq v1.0.0/go.mod h1:5WUZQaWbwv1U+lTReE5YruASi9Al49XbQIvNi/34Woo=
|
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 h1:4hp9jkHxhMHkqkrB3Ix0jegS5sx/RkqARlsWZ6pIwiU=
|
||||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
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=
|
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/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 h1:2pHcLyJYXivxVvpoCc29uo3GDU1qFfJ1ggXKGYMrM0E=
|
||||||
github.com/petermattis/goid v0.0.0-20170816195418-3db12ebb2a59/go.mod h1:jvVRKCrJTQWu0XVbaOlby/2lO20uSCHEMzzplHXte1o=
|
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.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I=
|
||||||
github.com/pkg/errors v0.0.0-20171216070316-e881fd58d78e/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
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=
|
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/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 h1:GtMydYcnK4zBJ0KL6Lx9vLzl6Oozb65wh252FTBxrvM=
|
||||||
github.com/thejerf/suture v3.0.2+incompatible/go.mod h1:ibKwrVj+Uzf3XZdAiNWUouPaAbSoemxOHLmJmwheEMc=
|
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 h1:okhMind4q9H1OxF44gNegWkiP4H/gsTFLalHFa4OOUI=
|
||||||
github.com/vitrun/qart v0.0.0-20160531060029-bf64b92db6b0/go.mod h1:TTbGUfE+cXXceWtbTHq6lqcTvYPBKLNejBEbnUsQJtU=
|
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=
|
golang.org/x/crypto v0.0.0-20171231215028-0fcca4842a8d h1:GrqEEc3+MtHKTsZrdIGVoYDgLpbSRzW1EF+nLu0PcHE=
|
||||||
|
81
lib/build/build.go
Normal file
81
lib/build/build.go
Normal file
@ -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, ", "))
|
||||||
|
}
|
||||||
|
}
|
37
lib/build/build_test.go
Normal file
37
lib/build/build_test.go
Normal file
@ -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)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
//+build noupgrade
|
//+build noupgrade
|
||||||
|
|
||||||
package main
|
package build
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
BuildTags = append(BuildTags, "noupgrade")
|
Tags = append(Tags, "noupgrade")
|
||||||
}
|
}
|
@ -6,8 +6,8 @@
|
|||||||
|
|
||||||
//+build race
|
//+build race
|
||||||
|
|
||||||
package main
|
package build
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
BuildTags = append(BuildTags, "race")
|
Tags = append(Tags, "race")
|
||||||
}
|
}
|
@ -10,12 +10,13 @@ import (
|
|||||||
"sort"
|
"sort"
|
||||||
|
|
||||||
"github.com/syncthing/syncthing/lib/protocol"
|
"github.com/syncthing/syncthing/lib/protocol"
|
||||||
|
"github.com/syncthing/syncthing/lib/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
type DeviceConfiguration struct {
|
type DeviceConfiguration struct {
|
||||||
DeviceID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
|
DeviceID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
|
||||||
Name string `xml:"name,attr,omitempty" json:"name"`
|
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"`
|
Compression protocol.Compression `xml:"compression,attr" json:"compression"`
|
||||||
CertName string `xml:"certName,attr,omitempty" json:"certName"`
|
CertName string `xml:"certName,attr,omitempty" json:"certName"`
|
||||||
Introducer bool `xml:"introducer,attr" json:"introducer"`
|
Introducer bool `xml:"introducer,attr" json:"introducer"`
|
||||||
@ -36,6 +37,9 @@ func NewDeviceConfiguration(id protocol.DeviceID, name string) DeviceConfigurati
|
|||||||
DeviceID: id,
|
DeviceID: id,
|
||||||
Name: name,
|
Name: name,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
util.SetDefaults(&d)
|
||||||
|
|
||||||
d.prepare(nil)
|
d.prepare(nil)
|
||||||
return d
|
return d
|
||||||
}
|
}
|
||||||
|
@ -32,12 +32,12 @@ type FolderConfiguration struct {
|
|||||||
Path string `xml:"path,attr" json:"path"`
|
Path string `xml:"path,attr" json:"path"`
|
||||||
Type FolderType `xml:"type,attr" json:"type"`
|
Type FolderType `xml:"type,attr" json:"type"`
|
||||||
Devices []FolderDeviceConfiguration `xml:"device" json:"devices"`
|
Devices []FolderDeviceConfiguration `xml:"device" json:"devices"`
|
||||||
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"`
|
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS" default:"3600"`
|
||||||
FSWatcherEnabled bool `xml:"fsWatcherEnabled,attr" json:"fsWatcherEnabled"`
|
FSWatcherEnabled bool `xml:"fsWatcherEnabled,attr" json:"fsWatcherEnabled" default:"true"`
|
||||||
FSWatcherDelayS int `xml:"fsWatcherDelayS,attr" json:"fsWatcherDelayS"`
|
FSWatcherDelayS int `xml:"fsWatcherDelayS,attr" json:"fsWatcherDelayS" default:"10"`
|
||||||
IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"`
|
IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"`
|
||||||
AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize"`
|
AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize" default:"true"`
|
||||||
MinDiskFree Size `xml:"minDiskFree" json:"minDiskFree"`
|
MinDiskFree Size `xml:"minDiskFree" json:"minDiskFree" default:"1%"`
|
||||||
Versioning VersioningConfiguration `xml:"versioning" json:"versioning"`
|
Versioning VersioningConfiguration `xml:"versioning" json:"versioning"`
|
||||||
Copiers int `xml:"copiers" json:"copiers"` // This defines how many files are handled concurrently.
|
Copiers int `xml:"copiers" json:"copiers"` // This defines how many files are handled concurrently.
|
||||||
PullerMaxPendingKiB int `xml:"pullerMaxPendingKiB" json:"pullerMaxPendingKiB"`
|
PullerMaxPendingKiB int `xml:"pullerMaxPendingKiB" json:"pullerMaxPendingKiB"`
|
||||||
@ -46,7 +46,7 @@ type FolderConfiguration struct {
|
|||||||
IgnoreDelete bool `xml:"ignoreDelete" json:"ignoreDelete"`
|
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)
|
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"`
|
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"`
|
DisableSparseFiles bool `xml:"disableSparseFiles" json:"disableSparseFiles"`
|
||||||
DisableTempIndexes bool `xml:"disableTempIndexes" json:"disableTempIndexes"`
|
DisableTempIndexes bool `xml:"disableTempIndexes" json:"disableTempIndexes"`
|
||||||
Paused bool `xml:"paused" json:"paused"`
|
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 {
|
func NewFolderConfiguration(myID protocol.DeviceID, id, label string, fsType fs.FilesystemType, path string) FolderConfiguration {
|
||||||
f := FolderConfiguration{
|
f := FolderConfiguration{
|
||||||
ID: id,
|
ID: id,
|
||||||
Label: label,
|
Label: label,
|
||||||
RescanIntervalS: 3600,
|
Devices: []FolderDeviceConfiguration{{DeviceID: myID}},
|
||||||
FSWatcherEnabled: true,
|
FilesystemType: fsType,
|
||||||
FSWatcherDelayS: 10,
|
Path: path,
|
||||||
MinDiskFree: Size{Value: 1, Unit: "%"},
|
|
||||||
Devices: []FolderDeviceConfiguration{{DeviceID: myID}},
|
|
||||||
AutoNormalize: true,
|
|
||||||
MaxConflicts: -1,
|
|
||||||
FilesystemType: fsType,
|
|
||||||
Path: path,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
util.SetDefaults(&f)
|
||||||
|
|
||||||
f.prepare()
|
f.prepare()
|
||||||
return f
|
return f
|
||||||
}
|
}
|
||||||
|
@ -14,7 +14,7 @@ import (
|
|||||||
|
|
||||||
type OptionsConfiguration struct {
|
type OptionsConfiguration struct {
|
||||||
ListenAddresses []string `xml:"listenAddress" json:"listenAddresses" default:"default"`
|
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"`
|
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true" restart:"true"`
|
||||||
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true" restart:"true"`
|
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true" restart:"true"`
|
||||||
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027" restart:"true"`
|
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21027" restart:"true"`
|
||||||
|
@ -72,8 +72,10 @@ func (s Size) String() string {
|
|||||||
return fmt.Sprintf("%v %s", s.Value, s.Unit)
|
return fmt.Sprintf("%v %s", s.Value, s.Unit)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Size) ParseDefault(s string) (interface{}, error) {
|
func (s *Size) ParseDefault(str string) error {
|
||||||
return ParseSize(s)
|
sz, err := ParseSize(str)
|
||||||
|
*s = sz
|
||||||
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func checkFreeSpace(req Size, usage fs.Usage) error {
|
func checkFreeSpace(req Size, usage fs.Usage) error {
|
||||||
|
@ -6,7 +6,28 @@
|
|||||||
|
|
||||||
package config
|
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) {
|
func TestParseSize(t *testing.T) {
|
||||||
cases := []struct {
|
cases := []struct {
|
||||||
|
161
lib/locations/locations.go
Normal file
161
lib/locations/locations.go
Normal file
@ -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)
|
||||||
|
}
|
@ -15,8 +15,12 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type defaultParser interface {
|
||||||
|
ParseDefault(string) error
|
||||||
|
}
|
||||||
|
|
||||||
// SetDefaults sets default values on a struct, based on the default annotation.
|
// 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()
|
s := reflect.ValueOf(data).Elem()
|
||||||
t := s.Type()
|
t := s.Type()
|
||||||
|
|
||||||
@ -26,15 +30,22 @@ func SetDefaults(data interface{}) error {
|
|||||||
|
|
||||||
v := tag.Get("default")
|
v := tag.Get("default")
|
||||||
if len(v) > 0 {
|
if len(v) > 0 {
|
||||||
if parser, ok := f.Interface().(interface {
|
if f.CanInterface() {
|
||||||
ParseDefault(string) (interface{}, error)
|
if parser, ok := f.Interface().(defaultParser); ok {
|
||||||
}); ok {
|
if err := parser.ParseDefault(v); err != nil {
|
||||||
val, err := parser.ParseDefault(v)
|
panic(err)
|
||||||
if 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) {
|
switch f.Interface().(type) {
|
||||||
@ -44,14 +55,14 @@ func SetDefaults(data interface{}) error {
|
|||||||
case int:
|
case int:
|
||||||
i, err := strconv.ParseInt(v, 10, 64)
|
i, err := strconv.ParseInt(v, 10, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
panic(err)
|
||||||
}
|
}
|
||||||
f.SetInt(i)
|
f.SetInt(i)
|
||||||
|
|
||||||
case float64:
|
case float64:
|
||||||
i, err := strconv.ParseFloat(v, 64)
|
i, err := strconv.ParseFloat(v, 64)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
panic(err)
|
||||||
}
|
}
|
||||||
f.SetFloat(i)
|
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.
|
// CopyMatchingTag copies fields tagged tag:"value" from "from" struct onto "to" struct.
|
||||||
|
@ -12,8 +12,9 @@ type Defaulter struct {
|
|||||||
Value string
|
Value string
|
||||||
}
|
}
|
||||||
|
|
||||||
func (Defaulter) ParseDefault(v string) (interface{}, error) {
|
func (d *Defaulter) ParseDefault(v string) error {
|
||||||
return Defaulter{Value: v}, nil
|
*d = Defaulter{Value: v}
|
||||||
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestSetDefaults(t *testing.T) {
|
func TestSetDefaults(t *testing.T) {
|
||||||
@ -37,9 +38,7 @@ func TestSetDefaults(t *testing.T) {
|
|||||||
t.Errorf("defaulter failed")
|
t.Errorf("defaulter failed")
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := SetDefaults(x); err != nil {
|
SetDefaults(x)
|
||||||
t.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if x.A != "string" {
|
if x.A != "string" {
|
||||||
t.Error("string failed")
|
t.Error("string failed")
|
||||||
|
Loading…
Reference in New Issue
Block a user