Commit 2867d0a5 authored by Audrius Butkevicius's avatar Audrius Butkevicius

Implement syncthing-cli

parents
package main
import (
"bytes"
"github.com/AudriusButkevicius/cli"
"net/http"
"strings"
)
type APIClient struct {
httpClient http.Client
endpoint string
apikey string
username string
password string
csrf string
}
var instance *APIClient
func getClient(c *cli.Context) *APIClient {
if instance != nil {
return instance
}
endpoint := c.GlobalString("endpoint")
if !strings.HasPrefix(endpoint, "http") {
endpoint = "http://" + endpoint
}
client := APIClient{
httpClient: http.Client{},
endpoint: endpoint,
apikey: c.GlobalString("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)
for _, item := range response.Cookies() {
if item.Name == "CSRF-Token" {
client.csrf = item.Value
goto csrffound
}
}
die("Failed to get CSRF token")
csrffound:
}
instance = &client
return &client
}
func (client *APIClient) handleRequest(request *http.Request) *http.Response {
if client.apikey != "" {
request.Header.Set("X-API-Key", client.apikey)
}
if client.username != "" || client.password != "" {
request.SetBasicAuth(client.username, client.password)
}
if client.csrf != "" {
request.Header.Set("X-CSRF-Token", client.csrf)
}
response, err := client.httpClient.Do(request)
die(err)
if response.StatusCode == 404 {
die("Invalid endpoint or API call")
} else if response.StatusCode == 401 {
die("Invalid username or password")
} else if response.StatusCode == 403 {
if client.apikey == "" {
die("Invalid CSRF token")
}
die("Invalid API key")
} else if response.StatusCode != 200 {
body := strings.TrimSpace(string(responseToBArray(response)))
if body != "" {
die(body)
}
die("Unknown HTTP status returned: " + response.Status)
}
return response
}
func httpGet(c *cli.Context, url string) *http.Response {
client := getClient(c)
request, err := http.NewRequest("GET", client.endpoint+"/rest/"+url, nil)
die(err)
return client.handleRequest(request)
}
func httpPost(c *cli.Context, url string, body string) *http.Response {
client := getClient(c)
request, err := http.NewRequest("POST", client.endpoint+"/rest/"+url, bytes.NewBufferString(body))
die(err)
return client.handleRequest(request)
}
package main
import (
"encoding/json"
"fmt"
"github.com/AudriusButkevicius/cli"
"strings"
)
var errorCommand = cli.Command{
Name: "errors",
HideHelp: true,
Usage: "Error command group",
Subcommands: []cli.Command{
{
Name: "show",
Usage: "Show pending errors",
Requires: &cli.Requires{},
Action: func(c *cli.Context) {
response := httpGet(c, "errors")
var data []map[string]interface{}
json.Unmarshal(responseToBArray(response), &data)
writer := newTableWriter()
for _, item := range data {
time := item["Time"].(string)[:19]
time = strings.Replace(time, "T", " ", 1)
err := item["Error"].(string)
err = strings.TrimSpace(err)
fmt.Fprintln(writer, time+":\t"+err)
}
writer.Flush()
},
},
{
Name: "push",
Usage: "Push an error to active clients",
Requires: &cli.Requires{"error message..."},
Action: func(c *cli.Context) {
err := ""
for _, item := range c.Args() {
err += " " + item
}
response := httpPost(c, "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)
}
},
},
{
Name: "clear",
Usage: "Clear pending errors",
Requires: &cli.Requires{},
Action: wrappedHttpPost("error/clear"),
},
},
}
package main
import (
"encoding/json"
"fmt"
"github.com/AudriusButkevicius/cli"
)
var generalCommands = []cli.Command{
{
Name: "id",
Usage: "Get ID of the Syncthing client",
Requires: &cli.Requires{},
Action: func(c *cli.Context) {
fmt.Println(getMyID(c))
},
},
{
Name: "status",
Usage: "Configuration status, whether or not a restart is required for changes to take effect",
Requires: &cli.Requires{},
Action: func(c *cli.Context) {
response := httpGet(c, "config/sync")
status := make(map[string]interface{})
json.Unmarshal(responseToBArray(response), &status)
if status["configInSync"] != true {
die("Config out of sync")
}
fmt.Println("Config in sync")
},
},
{
Name: "restart",
Usage: "Restart syncthing",
Requires: &cli.Requires{},
Action: wrappedHttpPost("restart"),
},
{
Name: "shutdown",
Usage: "Shutdown syncthing",
Requires: &cli.Requires{},
Action: wrappedHttpPost("shutdown"),
},
{
Name: "reset",
Usage: "Reset syncthing deleting all repositories and nodes",
Requires: &cli.Requires{},
Action: wrappedHttpPost("reset"),
},
{
Name: "upgrade",
Usage: "Upgrade syncthing (if a newer version is available)",
Requires: &cli.Requires{},
Action: wrappedHttpPost("upgrade"),
},
{
Name: "version",
Usage: "Syncthing client version",
Requires: &cli.Requires{},
Action: func(c *cli.Context) {
response := httpGet(c, "version")
output := string(responseToBArray(response))
fmt.Println(output)
},
},
}
package main
import (
"fmt"
"github.com/AudriusButkevicius/cli"
"strings"
)
var guiCommand = 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: func(c *cli.Context) {
cfg := getConfig(c).GUI
writer := newTableWriter()
fmt.Fprintf(writer, "Enabled:\t%t\t(enabled)\n", cfg.Enabled)
fmt.Fprintf(writer, "Use HTTPS:\t%t\t(tls)\n", cfg.UseTLS)
fmt.Fprintf(writer, "Listen Addresses:\t%s\t(address)\n", cfg.Address)
if cfg.User != "" {
fmt.Fprintf(writer, "Authentication User:\t%s\t(username)\n", cfg.User)
fmt.Fprintf(writer, "Authentication Password:\t%s\t(password)\n", cfg.Password)
}
if cfg.APIKey != "" {
fmt.Fprintf(writer, "API Key:\t%s\t(apikey)\n", cfg.APIKey)
}
writer.Flush()
},
},
{
Name: "get",
Usage: "Get a GUI configuration setting",
Requires: &cli.Requires{"setting"},
Action: func(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")
}
},
},
{
Name: "set",
Usage: "Set a GUI configuration setting",
Requires: &cli.Requires{"setting", "value"},
Action: func(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.UseTLS = parseBool(val)
case "address":
validAddress(val)
cfg.GUI.Address = 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)
},
},
{
Name: "unset",
Usage: "Unset a GUI configuration setting",
Requires: &cli.Requires{"setting"},
Action: func(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)
},
},
},
}
package main
import (
"fmt"
"github.com/AudriusButkevicius/cli"
"github.com/calmh/syncthing/config"
"strings"
)
var nodeCommand = cli.Command{
Name: "nodes",
HideHelp: true,
Usage: "Node command group",
Subcommands: []cli.Command{
{
Name: "list",
Usage: "List registered nodes",
Requires: &cli.Requires{},
Action: func(c *cli.Context) {
cfg := getConfig(c)
first := true
writer := newTableWriter()
for _, node := range cfg.Nodes {
if !first {
fmt.Fprintln(writer)
}
fmt.Fprintln(writer, "ID:\t", node.NodeID, "\t")
fmt.Fprintln(writer, "Name:\t", node.Name, "\t(name)")
fmt.Fprintf(writer, "Address:\t")
for _, item := range node.Addresses {
fmt.Fprintf(writer, " %s", item)
}
fmt.Fprintln(writer, "\t(address)")
first = false
}
writer.Flush()
},
},
{
Name: "add",
Usage: "Add a new node",
Requires: &cli.Requires{"node id", "node name?"},
Action: func(c *cli.Context) {
nid := c.Args()[0]
id := parseNodeID(nid)
newNode := config.NodeConfiguration{
NodeID: id,
Name: nid,
Addresses: []string{"dynamic"},
}
if len(c.Args()) > 1 {
newNode.Name = c.Args()[1]
}
if len(c.Args()) > 2 {
addresses := c.Args()[2:]
for _, item := range addresses {
if item == "dynamic" {
continue
}
validAddress(item)
}
newNode.Addresses = addresses
}
cfg := getConfig(c)
for _, node := range cfg.Nodes {
if node.NodeID == id {
die("Node " + nid + " already exists")
}
}
cfg.Nodes = append(cfg.Nodes, newNode)
setConfig(c, cfg)
},
},
{
Name: "remove",
Usage: "Remove an existing node",
Requires: &cli.Requires{"node id"},
Action: func(c *cli.Context) {
nid := c.Args()[0]
id := parseNodeID(nid)
if nid == getMyID(c) {
die("Cannot remove yourself")
}
cfg := getConfig(c)
for i, node := range cfg.Nodes {
if node.NodeID == id {
last := len(cfg.Nodes) - 1
cfg.Nodes[i] = cfg.Nodes[last]
cfg.Nodes = cfg.Nodes[:last]
setConfig(c, cfg)
return
}
}
die("Node " + nid + " not found")
},
},
{
Name: "get",
Usage: "Get a property of a node",
Requires: &cli.Requires{"node id", "property"},
Action: func(c *cli.Context) {
nid := c.Args()[0]
id := parseNodeID(nid)
arg := c.Args()[1]
cfg := getConfig(c)
for _, node := range cfg.Nodes {
if node.NodeID != id {
continue
}
switch strings.ToLower(arg) {
case "name":
fmt.Println(node.Name)
case "address":
for _, address := range node.Addresses {
fmt.Println(address)
}
default:
die("Invalid property: " + arg + "\nAvailable properties: name, address")
}
return
}
die("Node " + nid + " not found")
},
},
{
Name: "set",
Usage: "Set a property of a node",
Requires: &cli.Requires{"node id", "property", "value..."},
Action: func(c *cli.Context) {
nid := c.Args()[0]
id := parseNodeID(nid)
arg := c.Args()[1]
config := getConfig(c)
for i, node := range config.Nodes {
if node.NodeID != id {
continue
}
switch strings.ToLower(arg) {
case "name":
config.Nodes[i].Name = strings.Join(c.Args()[2:], " ")
case "address":
for _, item := range c.Args()[2:] {
if item == "dynamic" {
continue
}
validAddress(item)
}
config.Nodes[i].Addresses = c.Args()[2:]
default:
die("Invalid property: " + arg + "\nAvailable properties: name, address")
}
setConfig(c, config)
return
}
die("Node " + nid + " not found")
},
},
},
}
package main
import (
"fmt"
"github.com/AudriusButkevicius/cli"
"strings"
)
var optionsCommand = 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: func(c *cli.Context) {
cfg := getConfig(c).Options
writer := newTableWriter()
fmt.Fprint(writer, "Sync protocol listen addresses:\t")
for _, address := range cfg.ListenAddress {
fmt.Fprintf(writer, "%s ", address)
}
fmt.Fprintf(writer, "\t(address)\n")
fmt.Fprintf(writer, "Global discovery enabled:\t%t\t(globalannenabled)\n", cfg.GlobalAnnEnabled)
fmt.Fprintf(writer, "Global discovery server:\t%s\t(globalannserver)\n", cfg.GlobalAnnServer)
fmt.Fprintf(writer, "Local discovery enabled:\t%t\t(localannenabled)\n", cfg.LocalAnnEnabled)
fmt.Fprintf(writer, "Local discovery port:\t%d\t(localannport)\n", cfg.LocalAnnPort)
fmt.Fprintf(writer, "Maximum outstanding requests:\t%d\t(requests)\n", cfg.ParallelRequests)
fmt.Fprintf(writer, "Maximum file change rate in KiB/s:\t%d\t(maxchange)\n", cfg.MaxChangeKbps)
fmt.Fprintf(writer, "Outgoing rate limit in KiB/s:\t%d\t(maxsend)\n", cfg.MaxSendKbps)
fmt.Fprintf(writer, "Rescan interval in seconds:\t%d\t(rescan)\n", cfg.RescanIntervalS)
fmt.Fprintf(writer, "Reconnect interval in seconds:\t%d\t(reconnect)\n", cfg.ReconnectIntervalS)
fmt.Fprintf(writer, "Start browser:\t%t\t(browser)\n", cfg.StartBrowser)
fmt.Fprintf(writer, "Enable UPnP:\t%t\t(upnp)\n", cfg.UPnPEnabled)
fmt.Fprint(writer, "Anonymous usage reporting:\t")
switch cfg.URAccepted {
case -1:
fmt.Fprint(writer, "false")
case 0:
fmt.Fprint(writer, "undecided/false")
case 1:
fmt.Fprint(writer, "true")
default:
fmt.Fprint(writer, "unrecognized value")
}
fmt.Fprint(writer, "\t(reporting)\n")
writer.Flush()
},
},
{
Name: "get",
Usage: "Get a Syncthing option setting",
Requires: &cli.Requires{"setting"},
Action: func(c *cli.Context) {
cfg := getConfig(c).Options
arg := c.Args()[0]
switch strings.ToLower(arg) {
case "address":
for _, item := range cfg.ListenAddress {
fmt.Println(item)
}
case "globalannenabled":
fmt.Println(cfg.GlobalAnnEnabled)
case "globalannserver":
fmt.Println(cfg.GlobalAnnServer)
case "localannenabled":
fmt.Println(cfg.LocalAnnEnabled)
case "localannport":
fmt.Println(cfg.LocalAnnPort)
case "requests":
fmt.Println(cfg.ParallelRequests)
case "maxsend":
fmt.Println(cfg.MaxSendKbps)
case "maxchange":
fmt.Println(cfg.MaxChangeKbps)
case "rescan":
fmt.Println(cfg.RescanIntervalS)
case "reconnect":
fmt.Println(cfg.ReconnectIntervalS)
case "browser":
fmt.Println(cfg.StartBrowser)
case "upnp":
fmt.Println(cfg.UPnPEnabled)
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")
}
default:
die("Invalid setting: " + arg + "\nAvailable settings: address, globalannenabled, globalannserver, localannenabled, localannport, requests, maxsend, maxchange, rescan, reconnect, browser, upnp, reporting")
}
},
},
{
Name: "set",
Usage: "Set a Syncthing option setting",
Requires: &cli.Requires{"setting", "value..."},
Action: func(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.ListenAddress = c.Args().Tail()
case "globalannenabled":
config.Options.GlobalAnnEnabled = parseBool(val)
case "globalannserver":
validAddress(val)
config.Options.GlobalAnnServer = val
case "localannenabled":
config.Options.LocalAnnEnabled = parseBool(val)
case "localannport":
config.Options.LocalAnnPort = parsePort(val)
case "requests":
config.Options.ParallelRequests = parseUint(val)
case "maxsend":
config.Options.MaxSendKbps = parseUint(val)
case "maxchange":