mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-15 01:10:54 -05:00
Compare commits
21 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb5b1f8f01 | ||
|
|
c1a96d4900 | ||
|
|
de298da532 | ||
|
|
6f5ca53f99 | ||
|
|
d507126101 | ||
|
|
9a25df01fe | ||
|
|
11b9212948 | ||
|
|
b4e2914b70 | ||
|
|
09b7348595 | ||
|
|
d2bb6e0c0a | ||
|
|
8632a03662 | ||
|
|
e71c78ae84 | ||
|
|
03a8027efc | ||
|
|
b7e186b370 | ||
|
|
4a69f3987f | ||
|
|
343dc486e0 | ||
|
|
5aacfd1639 | ||
|
|
06e63aedea | ||
|
|
0320194757 | ||
|
|
1753771356 | ||
|
|
bc794e7c15 |
1
AUTHORS
1
AUTHORS
@@ -12,6 +12,7 @@ Alexander Graf (alex2108) <register-github@alex-graf.de>
|
||||
Alexandre Viau (aviau) <alexandre@alexandreviau.net> <aviau@debian.org>
|
||||
Anderson Mesquita (andersonvom) <andersonvom@gmail.com>
|
||||
Andrew Dunham (andrew-d) <andrew@du.nham.ca>
|
||||
Andrey D (scienmind) <scintertech@cryptolab.net>
|
||||
Antony Male (canton7) <antony.male@gmail.com>
|
||||
Arthur Axel fREW Schmidt (frioux) <frew@afoolishmanifesto.com> <frioux@gmail.com>
|
||||
Audrius Butkevicius (AudriusButkevicius) <audrius.butkevicius@gmail.com>
|
||||
|
||||
1
NICKS
1
NICKS
@@ -89,6 +89,7 @@ Rewt0r <rewt0r@gmx.com>
|
||||
Rewt0r <Rewt0r@users.noreply.github.com>
|
||||
rumpelsepp <stefan@sevenbyte.org>
|
||||
rumpelsepp <rumpelsepp@sevenbyte.org>
|
||||
scienmind <scintertech@cryptolab.net>
|
||||
sciurius <jvromans@squirrel.nl>
|
||||
seehuhn <voss@seehuhn.de>
|
||||
simplypeachy <aD@simplypeachy.co.uk>
|
||||
|
||||
97
build.go
97
build.go
@@ -39,6 +39,7 @@ var (
|
||||
version string
|
||||
goVersion float64
|
||||
race bool
|
||||
debug = os.Getenv("BUILDDEBUG") != ""
|
||||
)
|
||||
|
||||
type target struct {
|
||||
@@ -154,6 +155,13 @@ func main() {
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetFlags(0)
|
||||
|
||||
if debug {
|
||||
t0 := time.Now()
|
||||
defer func() {
|
||||
log.Println("... build completed in", time.Since(t0))
|
||||
}()
|
||||
}
|
||||
|
||||
if os.Getenv("GOPATH") == "" {
|
||||
setGoPath()
|
||||
}
|
||||
@@ -167,44 +175,42 @@ func main() {
|
||||
|
||||
parseFlags()
|
||||
|
||||
checkArchitecture()
|
||||
goVersion, _ = checkRequiredGoVersion()
|
||||
|
||||
// Invoking build.go with no parameters at all builds everything (incrementally),
|
||||
// which is what you want for maximum error checking during development.
|
||||
if flag.NArg() == 0 {
|
||||
runCommand("install", targets["all"])
|
||||
runCommand("vet", target{})
|
||||
runCommand("lint", target{})
|
||||
} else {
|
||||
// with any command given but not a target, the target is
|
||||
// "syncthing". So "go run build.go install" is "go run build.go install
|
||||
// syncthing" etc.
|
||||
targetName := "syncthing"
|
||||
if flag.NArg() > 1 {
|
||||
targetName = flag.Arg(1)
|
||||
}
|
||||
target, ok := targets[targetName]
|
||||
if !ok {
|
||||
log.Fatalln("Unknown target", target)
|
||||
}
|
||||
|
||||
runCommand(flag.Arg(0), target)
|
||||
}
|
||||
}
|
||||
|
||||
func checkArchitecture() {
|
||||
switch goarch {
|
||||
case "386", "amd64", "arm", "arm64", "ppc64", "ppc64le":
|
||||
break
|
||||
default:
|
||||
log.Printf("Unknown goarch %q; proceed with caution!", goarch)
|
||||
}
|
||||
}
|
||||
|
||||
goVersion, _ = checkRequiredGoVersion()
|
||||
|
||||
// Invoking build.go with no parameters at all is equivalent to "go run
|
||||
// build.go install all" as that builds everything (incrementally),
|
||||
// which is what you want for maximum error checking during development.
|
||||
if flag.NArg() == 0 {
|
||||
var tags []string
|
||||
if noupgrade {
|
||||
tags = []string{"noupgrade"}
|
||||
}
|
||||
install(targets["all"], tags)
|
||||
|
||||
vet("cmd", "lib")
|
||||
lint("./cmd/...")
|
||||
lint("./lib/...")
|
||||
return
|
||||
}
|
||||
|
||||
// Otherwise, with any command given but not a target, the target is
|
||||
// "syncthing". So "go run build.go install" is "go run build.go install
|
||||
// syncthing" etc.
|
||||
targetName := "syncthing"
|
||||
if flag.NArg() > 1 {
|
||||
targetName = flag.Arg(1)
|
||||
}
|
||||
target, ok := targets[targetName]
|
||||
if !ok {
|
||||
log.Fatalln("Unknown target", target)
|
||||
}
|
||||
|
||||
cmd := flag.Arg(0)
|
||||
func runCommand(cmd string, target target) {
|
||||
switch cmd {
|
||||
case "setup":
|
||||
setup()
|
||||
@@ -261,6 +267,8 @@ func main() {
|
||||
lint(".")
|
||||
lint("./cmd/...")
|
||||
lint("./lib/...")
|
||||
|
||||
case "metalint":
|
||||
if isGometalinterInstalled() {
|
||||
dirs := []string{".", "./cmd/...", "./lib/..."}
|
||||
gometalinter("deadcode", dirs, "test/util.go")
|
||||
@@ -362,7 +370,7 @@ func install(target target, tags []string) {
|
||||
os.Setenv("GOBIN", filepath.Join(cwd, "bin"))
|
||||
args := []string{"install", "-v", "-ldflags", ldflags()}
|
||||
if len(tags) > 0 {
|
||||
args = append(args, "-tags", strings.Join(tags, ","))
|
||||
args = append(args, "-tags", strings.Join(tags, " "))
|
||||
}
|
||||
if race {
|
||||
args = append(args, "-race")
|
||||
@@ -382,7 +390,7 @@ func build(target target, tags []string) {
|
||||
rmr(target.binaryName)
|
||||
args := []string{"build", "-i", "-v", "-ldflags", ldflags()}
|
||||
if len(tags) > 0 {
|
||||
args = append(args, "-tags", strings.Join(tags, ","))
|
||||
args = append(args, "-tags", strings.Join(tags, " "))
|
||||
}
|
||||
if race {
|
||||
args = append(args, "-race")
|
||||
@@ -746,13 +754,26 @@ func archiveName(target target) string {
|
||||
}
|
||||
|
||||
func runError(cmd string, args ...string) ([]byte, error) {
|
||||
if debug {
|
||||
t0 := time.Now()
|
||||
log.Println("runError:", cmd, strings.Join(args, " "))
|
||||
defer func() {
|
||||
log.Println("... in", time.Since(t0))
|
||||
}()
|
||||
}
|
||||
ecmd := exec.Command(cmd, args...)
|
||||
bs, err := ecmd.CombinedOutput()
|
||||
return bytes.TrimSpace(bs), err
|
||||
}
|
||||
|
||||
func runPrint(cmd string, args ...string) {
|
||||
log.Println(cmd, strings.Join(args, " "))
|
||||
if debug {
|
||||
t0 := time.Now()
|
||||
log.Println("runPrint:", cmd, strings.Join(args, " "))
|
||||
defer func() {
|
||||
log.Println("... in", time.Since(t0))
|
||||
}()
|
||||
}
|
||||
ecmd := exec.Command(cmd, args...)
|
||||
ecmd.Stdout = os.Stdout
|
||||
ecmd.Stderr = os.Stderr
|
||||
@@ -763,7 +784,13 @@ func runPrint(cmd string, args ...string) {
|
||||
}
|
||||
|
||||
func runPipe(file, cmd string, args ...string) {
|
||||
log.Println(cmd, strings.Join(args, " "), ">", file)
|
||||
if debug {
|
||||
t0 := time.Now()
|
||||
log.Println("runPipe:", cmd, strings.Join(args, " "))
|
||||
defer func() {
|
||||
log.Println("... in", time.Since(t0))
|
||||
}()
|
||||
}
|
||||
fd, err := os.Create(file)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
|
||||
@@ -52,7 +52,7 @@ var (
|
||||
certFile = "cert.pem"
|
||||
keyFile = "key.pem"
|
||||
debug = false
|
||||
useHttp = false
|
||||
useHTTP = false
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -76,14 +76,14 @@ func main() {
|
||||
flag.StringVar(&certFile, "cert", certFile, "Certificate file")
|
||||
flag.StringVar(&keyFile, "key", keyFile, "Key file")
|
||||
flag.BoolVar(&debug, "debug", debug, "Debug")
|
||||
flag.BoolVar(&useHttp, "http", useHttp, "Listen on HTTP (behind an HTTPS proxy)")
|
||||
flag.BoolVar(&useHTTP, "http", useHTTP, "Listen on HTTP (behind an HTTPS proxy)")
|
||||
flag.Parse()
|
||||
|
||||
log.Println(LongVersion)
|
||||
|
||||
var cert tls.Certificate
|
||||
var err error
|
||||
if !useHttp {
|
||||
if !useHTTP {
|
||||
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to load X509 key pair:", err)
|
||||
|
||||
@@ -83,7 +83,7 @@ func (s *querysrv) Serve() {
|
||||
Cache: lru.New(lruSize),
|
||||
}
|
||||
|
||||
if useHttp {
|
||||
if useHTTP {
|
||||
listener, err := net.Listen("tcp", s.addr)
|
||||
if err != nil {
|
||||
log.Println("Listen:", err)
|
||||
@@ -153,7 +153,7 @@ func (s *querysrv) handler(w http.ResponseWriter, req *http.Request) {
|
||||
}()
|
||||
|
||||
var remoteIP net.IP
|
||||
if useHttp {
|
||||
if useHTTP {
|
||||
remoteIP = net.ParseIP(req.Header.Get("X-Forwarded-For"))
|
||||
} else {
|
||||
addr, err := net.ResolveTCPAddr("tcp", req.RemoteAddr)
|
||||
|
||||
@@ -47,14 +47,14 @@ func init() {
|
||||
|
||||
var (
|
||||
listen string
|
||||
debug bool = false
|
||||
debug bool
|
||||
|
||||
sessionAddress []byte
|
||||
sessionPort uint16
|
||||
|
||||
networkTimeout time.Duration = 2 * time.Minute
|
||||
pingInterval time.Duration = time.Minute
|
||||
messageTimeout time.Duration = time.Minute
|
||||
networkTimeout = 2 * time.Minute
|
||||
pingInterval = time.Minute
|
||||
messageTimeout = time.Minute
|
||||
|
||||
limitCheckTimer *time.Timer
|
||||
|
||||
@@ -69,7 +69,7 @@ var (
|
||||
poolAddrs string
|
||||
pools []string
|
||||
providedBy string
|
||||
defaultPoolAddrs string = "https://relays.syncthing.net/endpoint"
|
||||
defaultPoolAddrs = "https://relays.syncthing.net/endpoint"
|
||||
)
|
||||
|
||||
func main() {
|
||||
|
||||
@@ -7,13 +7,10 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -26,7 +23,6 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/rcrowley/go-metrics"
|
||||
"github.com/syncthing/syncthing/lib/auto"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/discover"
|
||||
@@ -54,8 +50,7 @@ type apiService struct {
|
||||
cfg configIntf
|
||||
httpsCertFile string
|
||||
httpsKeyFile string
|
||||
assetDir string
|
||||
themes []string
|
||||
statics *staticsServer
|
||||
model modelIntf
|
||||
eventSub events.BufferedSubscription
|
||||
discoverer discover.CachingMux
|
||||
@@ -64,10 +59,8 @@ type apiService struct {
|
||||
systemConfigMut sync.Mutex // serializes posts to /rest/system/config
|
||||
stop chan struct{} // signals intentional stop
|
||||
configChanged chan struct{} // signals intentional listener close due to config change
|
||||
started chan struct{} // signals startup complete, for testing only
|
||||
|
||||
listener net.Listener
|
||||
listenerMut sync.Mutex
|
||||
started chan string // signals startup complete by sending the listener address, for testing only
|
||||
startedOnce bool // the service has started successfully at least once
|
||||
|
||||
guiErrors logger.Recorder
|
||||
systemLog logger.Recorder
|
||||
@@ -119,13 +112,13 @@ type connectionsIntf interface {
|
||||
Status() map[string]interface{}
|
||||
}
|
||||
|
||||
func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, eventSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connectionsIntf, errors, systemLog logger.Recorder) (*apiService, error) {
|
||||
func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, eventSub events.BufferedSubscription, discoverer discover.CachingMux, connectionsService connectionsIntf, errors, systemLog logger.Recorder) *apiService {
|
||||
service := &apiService{
|
||||
id: id,
|
||||
cfg: cfg,
|
||||
httpsCertFile: httpsCertFile,
|
||||
httpsKeyFile: httpsKeyFile,
|
||||
assetDir: assetDir,
|
||||
statics: newStaticsServer(cfg.GUI().Theme, assetDir),
|
||||
model: m,
|
||||
eventSub: eventSub,
|
||||
discoverer: discoverer,
|
||||
@@ -133,33 +126,11 @@ func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKey
|
||||
systemConfigMut: sync.NewMutex(),
|
||||
stop: make(chan struct{}),
|
||||
configChanged: make(chan struct{}),
|
||||
listenerMut: sync.NewMutex(),
|
||||
guiErrors: errors,
|
||||
systemLog: systemLog,
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
// Load themes from compiled in assets.
|
||||
for file := range auto.Assets() {
|
||||
theme := strings.Split(file, "/")[0]
|
||||
if _, ok := seen[theme]; !ok {
|
||||
seen[theme] = struct{}{}
|
||||
service.themes = append(service.themes, theme)
|
||||
}
|
||||
}
|
||||
if assetDir != "" {
|
||||
// Load any extra themes from the asset override dir.
|
||||
for _, dir := range dirNames(assetDir) {
|
||||
if _, ok := seen[dir]; !ok {
|
||||
seen[dir] = struct{}{}
|
||||
service.themes = append(service.themes, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
service.listener, err = service.getListener(cfg.GUI())
|
||||
return service, err
|
||||
return service
|
||||
}
|
||||
|
||||
func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener, error) {
|
||||
@@ -226,9 +197,22 @@ func sendJSON(w http.ResponseWriter, jsonObject interface{}) {
|
||||
}
|
||||
|
||||
func (s *apiService) Serve() {
|
||||
s.listenerMut.Lock()
|
||||
listener := s.listener
|
||||
s.listenerMut.Unlock()
|
||||
listener, err := s.getListener(s.cfg.GUI())
|
||||
if err != nil {
|
||||
if !s.startedOnce {
|
||||
// This is during initialization. A failure here should be fatal
|
||||
// as there will be no way for the user to communicate with us
|
||||
// otherwise anyway.
|
||||
l.Fatalln("Starting API/GUI:", err)
|
||||
}
|
||||
|
||||
// We let this be a loud user-visible warning as it may be the only
|
||||
// indication they get that the GUI won't be available on startup.
|
||||
l.Warnln("Starting API/GUI:", err)
|
||||
return
|
||||
}
|
||||
s.startedOnce = true
|
||||
defer listener.Close()
|
||||
|
||||
if listener == nil {
|
||||
// Not much we can do here other than exit quickly. The supervisor
|
||||
@@ -297,20 +281,11 @@ func (s *apiService) Serve() {
|
||||
mux.HandleFunc("/qr/", s.getQR)
|
||||
|
||||
// Serve compiled in assets unless an asset directory was set (for development)
|
||||
assets := &embeddedStatic{
|
||||
theme: s.cfg.GUI().Theme,
|
||||
lastModified: time.Now().Truncate(time.Second), // must truncate, for the wire precision is 1s
|
||||
mut: sync.NewRWMutex(),
|
||||
assetDir: s.assetDir,
|
||||
assets: auto.Assets(),
|
||||
}
|
||||
mux.Handle("/", assets)
|
||||
mux.Handle("/", s.statics)
|
||||
|
||||
// Handle the special meta.js path
|
||||
mux.HandleFunc("/meta.js", s.getJSMetadata)
|
||||
|
||||
s.cfg.Subscribe(assets)
|
||||
|
||||
guiCfg := s.cfg.GUI()
|
||||
|
||||
// Wrap everything in CSRF protection. The /rest prefix should be
|
||||
@@ -348,33 +323,33 @@ func (s *apiService) Serve() {
|
||||
l.Infoln("Access the GUI via the following URL:", guiCfg.URL())
|
||||
if s.started != nil {
|
||||
// only set when run by the tests
|
||||
close(s.started)
|
||||
s.started <- listener.Addr().String()
|
||||
}
|
||||
err := srv.Serve(listener)
|
||||
|
||||
// The return could be due to an intentional close. Wait for the stop
|
||||
// signal before returning. IF there is no stop signal within a second, we
|
||||
// assume it was unintentional and log the error before retrying.
|
||||
// Serve in the background
|
||||
|
||||
serveError := make(chan error, 1)
|
||||
go func() {
|
||||
serveError <- srv.Serve(listener)
|
||||
}()
|
||||
|
||||
// Wait for stop, restart or error signals
|
||||
|
||||
select {
|
||||
case <-s.stop:
|
||||
// Shutting down permanently
|
||||
l.Debugln("shutting down (stop)")
|
||||
case <-s.configChanged:
|
||||
case <-time.After(time.Second):
|
||||
l.Warnln("API:", err)
|
||||
// Soft restart due to configuration change
|
||||
l.Debugln("restarting (config changed)")
|
||||
case <-serveError:
|
||||
// Restart due to listen/serve failure
|
||||
l.Warnln("GUI/API:", err, "(restarting)")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiService) Stop() {
|
||||
s.listenerMut.Lock()
|
||||
listener := s.listener
|
||||
s.listenerMut.Unlock()
|
||||
|
||||
close(s.stop)
|
||||
|
||||
// listener may be nil here if we've had a config change to a broken
|
||||
// configuration, in which case we shouldn't try to close it.
|
||||
if listener != nil {
|
||||
listener.Close()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *apiService) String() string {
|
||||
@@ -382,6 +357,9 @@ func (s *apiService) String() string {
|
||||
}
|
||||
|
||||
func (s *apiService) VerifyConfiguration(from, to config.Configuration) error {
|
||||
if _, err := net.ResolveTCPAddr("tcp", to.GUI.Address()); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -390,27 +368,11 @@ func (s *apiService) CommitConfiguration(from, to config.Configuration) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
// Order here is important. We must close the listener to stop Serve(). We
|
||||
// must create a new listener before Serve() starts again. We can't create
|
||||
// a new listener on the same port before the previous listener is closed.
|
||||
// To assist in this little dance the Serve() method will wait for a
|
||||
// signal on the configChanged channel after the listener has closed.
|
||||
|
||||
s.listenerMut.Lock()
|
||||
defer s.listenerMut.Unlock()
|
||||
|
||||
s.listener.Close()
|
||||
|
||||
var err error
|
||||
s.listener, err = s.getListener(to.GUI)
|
||||
if err != nil {
|
||||
// Ideally this should be a verification error, but we check it by
|
||||
// creating a new listener which requires shutting down the previous
|
||||
// one first, which is too destructive for the VerifyConfiguration
|
||||
// method.
|
||||
return false
|
||||
if to.GUI.Theme != from.GUI.Theme {
|
||||
s.statics.setTheme(to.GUI.Theme)
|
||||
}
|
||||
|
||||
// Tell the serve loop to restart
|
||||
s.configChanged <- struct{}{}
|
||||
|
||||
return true
|
||||
@@ -851,7 +813,6 @@ func (s *apiService) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
||||
res["pathSeparator"] = string(filepath.Separator)
|
||||
res["uptime"] = int(time.Since(startTime).Seconds())
|
||||
res["startTime"] = startTime
|
||||
res["themes"] = s.themes
|
||||
|
||||
sendJSON(w, res)
|
||||
}
|
||||
@@ -1187,150 +1148,17 @@ func (s *apiService) getSystemBrowse(w http.ResponseWriter, r *http.Request) {
|
||||
search = search + pathSeparator
|
||||
}
|
||||
subdirectories, _ := osutil.Glob(search + "*")
|
||||
ret := make([]string, 0, 10)
|
||||
ret := make([]string, 0, len(subdirectories))
|
||||
for _, subdirectory := range subdirectories {
|
||||
info, err := os.Stat(subdirectory)
|
||||
if err == nil && info.IsDir() {
|
||||
ret = append(ret, subdirectory+pathSeparator)
|
||||
if len(ret) > 9 {
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
sendJSON(w, ret)
|
||||
}
|
||||
|
||||
type embeddedStatic struct {
|
||||
theme string
|
||||
lastModified time.Time
|
||||
mut sync.RWMutex
|
||||
assetDir string
|
||||
assets map[string][]byte
|
||||
}
|
||||
|
||||
func (s embeddedStatic) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
file := r.URL.Path
|
||||
|
||||
if file[0] == '/' {
|
||||
file = file[1:]
|
||||
}
|
||||
|
||||
if len(file) == 0 {
|
||||
file = "index.html"
|
||||
}
|
||||
|
||||
s.mut.RLock()
|
||||
theme := s.theme
|
||||
modified := s.lastModified
|
||||
s.mut.RUnlock()
|
||||
|
||||
// Check for an override for the current theme.
|
||||
if s.assetDir != "" {
|
||||
p := filepath.Join(s.assetDir, s.theme, filepath.FromSlash(file))
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
http.ServeFile(w, r, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a compiled in asset for the current theme.
|
||||
bs, ok := s.assets[theme+"/"+file]
|
||||
if !ok {
|
||||
// Check for an overridden default asset.
|
||||
if s.assetDir != "" {
|
||||
p := filepath.Join(s.assetDir, config.DefaultTheme, filepath.FromSlash(file))
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
http.ServeFile(w, r, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a compiled in default asset.
|
||||
bs, ok = s.assets[config.DefaultTheme+"/"+file]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
modifiedSince, err := http.ParseTime(r.Header.Get("If-Modified-Since"))
|
||||
if err == nil && !modified.After(modifiedSince) {
|
||||
w.WriteHeader(http.StatusNotModified)
|
||||
return
|
||||
}
|
||||
|
||||
mtype := s.mimeTypeForFile(file)
|
||||
if len(mtype) != 0 {
|
||||
w.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
} else {
|
||||
// ungzip if browser not send gzip accepted header
|
||||
var gr *gzip.Reader
|
||||
gr, _ = gzip.NewReader(bytes.NewReader(bs))
|
||||
bs, _ = ioutil.ReadAll(gr)
|
||||
gr.Close()
|
||||
}
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
|
||||
w.Header().Set("Last-Modified", modified.UTC().Format(http.TimeFormat))
|
||||
// Strictly, no-cache means the same as this. However FF and IE treat no-cache as
|
||||
// "don't hold a local cache at all", whereas everyone seems to treat this as
|
||||
// you can hold a local cache, but you must revalidate it before using it.
|
||||
w.Header().Set("Cache-Control", "max-age=0, must-revalidate")
|
||||
|
||||
w.Write(bs)
|
||||
}
|
||||
|
||||
func (s embeddedStatic) mimeTypeForFile(file string) string {
|
||||
// We use a built in table of the common types since the system
|
||||
// TypeByExtension might be unreliable. But if we don't know, we delegate
|
||||
// to the system.
|
||||
ext := filepath.Ext(file)
|
||||
switch ext {
|
||||
case ".htm", ".html":
|
||||
return "text/html"
|
||||
case ".css":
|
||||
return "text/css"
|
||||
case ".js":
|
||||
return "application/javascript"
|
||||
case ".json":
|
||||
return "application/json"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".ttf":
|
||||
return "application/x-font-ttf"
|
||||
case ".woff":
|
||||
return "application/x-font-woff"
|
||||
case ".svg":
|
||||
return "image/svg+xml"
|
||||
default:
|
||||
return mime.TypeByExtension(ext)
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyConfiguration implements the config.Committer interface
|
||||
func (s *embeddedStatic) VerifyConfiguration(from, to config.Configuration) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// CommitConfiguration implements the config.Committer interface
|
||||
func (s *embeddedStatic) CommitConfiguration(from, to config.Configuration) bool {
|
||||
s.mut.Lock()
|
||||
if s.theme != to.GUI.Theme {
|
||||
s.theme = to.GUI.Theme
|
||||
s.lastModified = time.Now()
|
||||
}
|
||||
s.mut.Unlock()
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (s *embeddedStatic) String() string {
|
||||
return fmt.Sprintf("embeddedStatic@%p", s)
|
||||
}
|
||||
|
||||
func (s *apiService) toNeedSlice(fs []db.FileInfoTruncated) []jsonDBFileInfo {
|
||||
res := make([]jsonDBFileInfo, len(fs))
|
||||
for i, f := range fs {
|
||||
|
||||
176
cmd/syncthing/gui_statics.go
Normal file
176
cmd/syncthing/gui_statics.go
Normal file
@@ -0,0 +1,176 @@
|
||||
// Copyright (C) 2014 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 http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"mime"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/auto"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
type staticsServer struct {
|
||||
assetDir string
|
||||
assets map[string][]byte
|
||||
availableThemes []string
|
||||
|
||||
mut sync.RWMutex
|
||||
theme string
|
||||
}
|
||||
|
||||
func newStaticsServer(theme, assetDir string) *staticsServer {
|
||||
s := &staticsServer{
|
||||
assetDir: assetDir,
|
||||
assets: auto.Assets(),
|
||||
mut: sync.NewRWMutex(),
|
||||
theme: theme,
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
// Load themes from compiled in assets.
|
||||
for file := range auto.Assets() {
|
||||
theme := strings.Split(file, "/")[0]
|
||||
if _, ok := seen[theme]; !ok {
|
||||
seen[theme] = struct{}{}
|
||||
s.availableThemes = append(s.availableThemes, theme)
|
||||
}
|
||||
}
|
||||
if assetDir != "" {
|
||||
// Load any extra themes from the asset override dir.
|
||||
for _, dir := range dirNames(assetDir) {
|
||||
if _, ok := seen[dir]; !ok {
|
||||
seen[dir] = struct{}{}
|
||||
s.availableThemes = append(s.availableThemes, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *staticsServer) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
switch r.URL.Path {
|
||||
case "/themes.json":
|
||||
s.serveThemes(w, r)
|
||||
default:
|
||||
s.serveAsset(w, r)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *staticsServer) serveAsset(w http.ResponseWriter, r *http.Request) {
|
||||
file := r.URL.Path
|
||||
|
||||
if file[0] == '/' {
|
||||
file = file[1:]
|
||||
}
|
||||
|
||||
if len(file) == 0 {
|
||||
file = "index.html"
|
||||
}
|
||||
|
||||
s.mut.RLock()
|
||||
theme := s.theme
|
||||
s.mut.RUnlock()
|
||||
|
||||
// Check for an override for the current theme.
|
||||
if s.assetDir != "" {
|
||||
p := filepath.Join(s.assetDir, theme, filepath.FromSlash(file))
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
http.ServeFile(w, r, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a compiled in asset for the current theme.
|
||||
bs, ok := s.assets[theme+"/"+file]
|
||||
if !ok {
|
||||
// Check for an overridden default asset.
|
||||
if s.assetDir != "" {
|
||||
p := filepath.Join(s.assetDir, config.DefaultTheme, filepath.FromSlash(file))
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
http.ServeFile(w, r, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a compiled in default asset.
|
||||
bs, ok = s.assets[config.DefaultTheme+"/"+file]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
mtype := s.mimeTypeForFile(file)
|
||||
if len(mtype) != 0 {
|
||||
w.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
if strings.Contains(r.Header.Get("Accept-Encoding"), "gzip") {
|
||||
w.Header().Set("Content-Encoding", "gzip")
|
||||
} else {
|
||||
// ungzip if browser not send gzip accepted header
|
||||
var gr *gzip.Reader
|
||||
gr, _ = gzip.NewReader(bytes.NewReader(bs))
|
||||
bs, _ = ioutil.ReadAll(gr)
|
||||
gr.Close()
|
||||
}
|
||||
w.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
|
||||
|
||||
w.Write(bs)
|
||||
}
|
||||
|
||||
func (s *staticsServer) serveThemes(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, map[string][]string{
|
||||
"themes": s.availableThemes,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *staticsServer) mimeTypeForFile(file string) string {
|
||||
// We use a built in table of the common types since the system
|
||||
// TypeByExtension might be unreliable. But if we don't know, we delegate
|
||||
// to the system.
|
||||
ext := filepath.Ext(file)
|
||||
switch ext {
|
||||
case ".htm", ".html":
|
||||
return "text/html"
|
||||
case ".css":
|
||||
return "text/css"
|
||||
case ".js":
|
||||
return "application/javascript"
|
||||
case ".json":
|
||||
return "application/json"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".ttf":
|
||||
return "application/x-font-ttf"
|
||||
case ".woff":
|
||||
return "application/x-font-woff"
|
||||
case ".svg":
|
||||
return "image/svg+xml"
|
||||
default:
|
||||
return mime.TypeByExtension(ext)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *staticsServer) setTheme(theme string) {
|
||||
s.mut.Lock()
|
||||
s.theme = theme
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
func (s *staticsServer) String() string {
|
||||
return fmt.Sprintf("staticsServer@%p", s)
|
||||
}
|
||||
@@ -68,11 +68,8 @@ func TestStopAfterBrokenConfig(t *testing.T) {
|
||||
}
|
||||
w := config.Wrap("/dev/null", cfg)
|
||||
|
||||
srv, err := newAPIService(protocol.LocalDeviceID, w, "../../test/h1/https-cert.pem", "../../test/h1/https-key.pem", "", nil, nil, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
srv.started = make(chan struct{})
|
||||
srv := newAPIService(protocol.LocalDeviceID, w, "../../test/h1/https-cert.pem", "../../test/h1/https-key.pem", "", nil, nil, nil, nil, nil, nil)
|
||||
srv.started = make(chan string)
|
||||
|
||||
sup := suture.NewSimple("test")
|
||||
sup.Add(srv)
|
||||
@@ -90,8 +87,8 @@ func TestStopAfterBrokenConfig(t *testing.T) {
|
||||
RawUseTLS: false,
|
||||
},
|
||||
}
|
||||
if srv.CommitConfiguration(cfg, newCfg) {
|
||||
t.Fatal("Config commit should have failed")
|
||||
if err := srv.VerifyConfiguration(cfg, newCfg); err == nil {
|
||||
t.Fatal("Verify config should have failed")
|
||||
}
|
||||
|
||||
// Nonetheless, it should be fine to Stop() it without panic.
|
||||
@@ -119,7 +116,7 @@ func TestAssetsDir(t *testing.T) {
|
||||
gw.Close()
|
||||
foo := buf.Bytes()
|
||||
|
||||
e := embeddedStatic{
|
||||
e := &staticsServer{
|
||||
theme: "foo",
|
||||
mut: sync.NewRWMutex(),
|
||||
assetDir: "testdata",
|
||||
@@ -475,30 +472,26 @@ func startHTTP(cfg *mockedConfig) (string, error) {
|
||||
connections := new(mockedConnections)
|
||||
errorLog := new(mockedLoggerRecorder)
|
||||
systemLog := new(mockedLoggerRecorder)
|
||||
addrChan := make(chan string)
|
||||
|
||||
// Instantiate the API service
|
||||
svc, err := newAPIService(protocol.LocalDeviceID, cfg, httpsCertFile, httpsKeyFile, assetDir, model,
|
||||
svc := newAPIService(protocol.LocalDeviceID, cfg, httpsCertFile, httpsKeyFile, assetDir, model,
|
||||
eventSub, discoverer, connections, errorLog, systemLog)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// Make sure the API service is listening, and get the URL to use.
|
||||
addr := svc.listener.Addr()
|
||||
if addr == nil {
|
||||
return "", fmt.Errorf("Nil listening address from API service")
|
||||
}
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr.String())
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Weird address from API service: %v", err)
|
||||
}
|
||||
baseURL := fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port)
|
||||
svc.started = addrChan
|
||||
|
||||
// Actually start the API service
|
||||
supervisor := suture.NewSimple("API test")
|
||||
supervisor.Add(svc)
|
||||
supervisor.ServeBackground()
|
||||
|
||||
// Make sure the API service is listening, and get the URL to use.
|
||||
addr := <-addrChan
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Weird address from API service: %v", err)
|
||||
}
|
||||
baseURL := fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port)
|
||||
|
||||
return baseURL, nil
|
||||
}
|
||||
|
||||
|
||||
@@ -933,10 +933,7 @@ func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Mode
|
||||
l.Warnln("Insecure admin access is enabled.")
|
||||
}
|
||||
|
||||
api, err := newAPIService(myID, cfg, locations[locHTTPSCertFile], locations[locHTTPSKeyFile], runtimeOptions.assetDir, m, apiSub, discoverer, connectionsService, errors, systemLog)
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot start GUI:", err)
|
||||
}
|
||||
api := newAPIService(myID, cfg, locations[locHTTPSCertFile], locations[locHTTPSKeyFile], runtimeOptions.assetDir, m, apiSub, discoverer, connectionsService, errors, systemLog)
|
||||
cfg.Subscribe(api)
|
||||
mainService.Add(api)
|
||||
|
||||
|
||||
@@ -183,10 +183,14 @@ table.table-condensed td.no-overflow-ellipse {
|
||||
margin-bottom: 0.25em;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
button.panel-heading {
|
||||
display: block;
|
||||
cursor: pointer;
|
||||
position: relative;
|
||||
width: 100%;
|
||||
text-align: left;
|
||||
border-top-width: 0;
|
||||
border-left-width: 0;
|
||||
border-right-width: 0;
|
||||
}
|
||||
|
||||
.panel-heading .panel-title-text {
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 6.5 KiB After Width: | Height: | Size: 4.9 KiB |
@@ -4,11 +4,11 @@
|
||||
"A new major version may not be compatible with previous versions.": "Нова основна версия, която може да не е съвмеситима с предишни версии.",
|
||||
"API Key": "API Ключ",
|
||||
"About": "За програмата",
|
||||
"Actions": "Действия",
|
||||
"Actions": "Меню",
|
||||
"Add": "Добави",
|
||||
"Add Device": "Добави устройство",
|
||||
"Add Folder": "Добави папка",
|
||||
"Add Remote Device": "Добави отдалечено устройство",
|
||||
"Add Remote Device": "Добави ново устройство",
|
||||
"Add new folder?": "Добави нова папка?",
|
||||
"Address": "Адрес",
|
||||
"Addresses": "Адреси",
|
||||
@@ -20,11 +20,11 @@
|
||||
"Alphabetic": "Азбучен ред",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Друга команда се занимава с версиите. Тази команда трябва да премахни файла от синхронизираната папка.",
|
||||
"Anonymous Usage Reporting": "Анонимен доклад",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Устройства настроени на introducer компютъра също ще бъдат добавени към този компютър.",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Устройства настроени да представят други устройства също ще бъдат добавени към това устройство.",
|
||||
"Automatic upgrades": "Автоматично обновяване",
|
||||
"Be careful!": "Внимание!",
|
||||
"Bugs": "Бъгове",
|
||||
"CPU Utilization": "Процесор в употреба",
|
||||
"CPU Utilization": "Използван процесор",
|
||||
"Changelog": "Списък с промени",
|
||||
"Clean out after": "Изчисти след",
|
||||
"Close": "Затвори",
|
||||
@@ -43,7 +43,7 @@
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Устройство \"{{name}}\" ({{device}}) на {{address}} желае да се свърже. Добави ново устройство?",
|
||||
"Device ID": "Идентификатор на устройство",
|
||||
"Device Identification": "Идентификатор на устройство",
|
||||
"Device Name": "Име на устройство",
|
||||
"Device Name": "Име на устройството",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Устройство {{device}} ({{address}}) желае да се свърже. Добави ново устройство?",
|
||||
"Devices": "Устройства",
|
||||
"Disconnected": "Не е свързано",
|
||||
@@ -78,8 +78,8 @@
|
||||
"Folder Type": "Вид папка",
|
||||
"Folders": "Папки",
|
||||
"GUI": "Потребителски интерфейс",
|
||||
"GUI Authentication Password": "Парола за потребителския интерфейс",
|
||||
"GUI Authentication User": "Потребител за потребителския интерфейс",
|
||||
"GUI Authentication Password": "Парола за интерфейса",
|
||||
"GUI Authentication User": "Потребителско име за интерфейса",
|
||||
"GUI Listen Addresses": "Адрес за свързване с потребителския интерфейс",
|
||||
"Generate": "Генерирай",
|
||||
"Global Discovery": "Глобално откриване",
|
||||
@@ -93,17 +93,18 @@
|
||||
"Ignore Permissions": "Игнорирай правата за достъп",
|
||||
"Incoming Rate Limit (KiB/s)": "Лимит на скоростта за сваляне (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Неправилни настройки могат да повредят файловете и да попречат на синхронизирането.",
|
||||
"Introducer": "Introducer",
|
||||
"Introducer": "Може да предлага други устройства",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Обратното на даденото условие (пр. не изключвай)",
|
||||
"Keep Versions": "Пази версии",
|
||||
"Largest First": " Първо най-големите",
|
||||
"Last File Received": "Последния получен файл",
|
||||
"Last seen": "Последно видян",
|
||||
"Last Scan": "Последно сканиран",
|
||||
"Last seen": "Последно видяно",
|
||||
"Later": "По-късно",
|
||||
"Listeners": "Слушащи",
|
||||
"Listeners": "Синхронизиращи устройства",
|
||||
"Local Discovery": "Локално откриване",
|
||||
"Local State": "Локално състояние",
|
||||
"Local State (Total)": "Локално състояние (Общо)",
|
||||
"Local State (Total)": "Локално състояние (общо)",
|
||||
"Major Upgrade": "Основно Обновяване",
|
||||
"Master": "Главен",
|
||||
"Maximum Age": "Максимална възраст",
|
||||
@@ -111,7 +112,7 @@
|
||||
"Minimum Free Disk Space": "Минимално свободно дисково пространство",
|
||||
"Move to top of queue": "Премести в началото на опашката",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Маска на много нива (покрива папки с много нива)",
|
||||
"Never": "Никога",
|
||||
"Never": "никога",
|
||||
"New Device": "Ново устройство",
|
||||
"New Folder": "Нова папка",
|
||||
"Newest First": "Първо най-новите",
|
||||
@@ -124,7 +125,7 @@
|
||||
"Oldest First": "Първо най-старите",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Допълнително разяснеие за етикета на папката. Може да бъде различно всяко устройство.",
|
||||
"Options": "Настройки",
|
||||
"Out of Sync": "Несинхронизирано",
|
||||
"Out of Sync": "Несинхронизирана",
|
||||
"Out of Sync Items": "Несинхронизирани елементи",
|
||||
"Outgoing Rate Limit (KiB/s)": "Лимит на скорост за качване (KiB/s)",
|
||||
"Override Changes": "Наложи локалните промени",
|
||||
@@ -138,17 +139,17 @@
|
||||
"Preview": "Преглед",
|
||||
"Preview Usage Report": "Разгледай доклада за използване",
|
||||
"Quick guide to supported patterns": "Бърз наръчник към поддържаните шаблони",
|
||||
"RAM Utilization": "RAM в употреба",
|
||||
"RAM Utilization": "Използван RAM",
|
||||
"Random": "Произволен",
|
||||
"Relay Servers": "Препращащи сървъри",
|
||||
"Relayed via": "Препратено през",
|
||||
"Relays": "Препращачи",
|
||||
"Release Notes": "Бележки по обновяването",
|
||||
"Remote Devices": "Отделечени устройства",
|
||||
"Remote Devices": "Чужди устройства",
|
||||
"Remove": "Премахни",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Задължителен идентификатор за тази папка. Трябва да бъде един и същ на всички устройства.",
|
||||
"Rescan": "Сканирай повторно",
|
||||
"Rescan All": "Сканирай повторно всички",
|
||||
"Rescan": "Сканирай",
|
||||
"Rescan All": "Сканирай всички",
|
||||
"Rescan Interval": "Интервал за повторно сканиране",
|
||||
"Restart": "Рестартирай",
|
||||
"Restart Needed": "Изисква се рестартиране",
|
||||
@@ -186,7 +187,7 @@
|
||||
"Sync Protocol Listen Addresses": "Адрес за слушане на синхронизиращия протокол",
|
||||
"Syncing": "Синхронизиране",
|
||||
"Syncthing has been shut down.": "Syncthing е спрян.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing включва следният софтуер пълно или частично:",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing уползотворява частично или изцяло следните софтуерни продукти:",
|
||||
"Syncthing is restarting.": "Syncthing се рестартира",
|
||||
"Syncthing is upgrading.": "Syncthing се обновява.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing изглежда не е включен, или има проблем с интерент връзката. Повторен опит...",
|
||||
@@ -218,14 +219,14 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Ограничението на скоростта трябва да бъде положително число (0: неограничено)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Интервала на сканиране трябва да бъде не отрицателно число в секунди.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Ще бъдат спрени и автоматично синхронизирани, когато грешката бъде оправена.",
|
||||
"This Device": "Това устройство",
|
||||
"This Device": "Вашето устройство",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Това дава лесен достъп на хакери да разглеждат и променят всякакви файлове на компютъра Ви.",
|
||||
"This is a major version upgrade.": "Това е нова основна версия.",
|
||||
"Trash Can File Versioning": "Само на файловете в кошчето",
|
||||
"Unknown": "Неясно",
|
||||
"Unshared": "Несподелена",
|
||||
"Unused": "Неизползван",
|
||||
"Up to Date": "Синхронизирано",
|
||||
"Up to Date": "Синхронизирана",
|
||||
"Updated": "Обновено",
|
||||
"Upgrade": "Обнови",
|
||||
"Upgrade To {%version%}": "Обновен до {{version}}",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Mantenir Versions",
|
||||
"Largest First": "Més gran primer",
|
||||
"Last File Received": "Últim fitxer rebut",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Vist per última vegada",
|
||||
"Later": "Després",
|
||||
"Listeners": "Listeners",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Mantindre versions",
|
||||
"Largest First": "El més gran primer",
|
||||
"Last File Received": "Darrer fitxer rebut",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Vist per última vegada",
|
||||
"Later": "Més tard",
|
||||
"Listeners": "Listeners",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Ponechat verze",
|
||||
"Largest First": "Od největšího",
|
||||
"Last File Received": "Poslední přijatý soubor",
|
||||
"Last Scan": "Poslední sken",
|
||||
"Last seen": "Naposledy spatřen",
|
||||
"Later": "Později",
|
||||
"Listeners": "Naslouchající",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Behold versioner",
|
||||
"Largest First": "Største først",
|
||||
"Last File Received": "Sidste modtaget fil",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Sidst set",
|
||||
"Later": "Senere",
|
||||
"Listeners": "Listeners",
|
||||
|
||||
@@ -75,7 +75,7 @@
|
||||
"Folder Label": "Verzeichnisbezeichnung",
|
||||
"Folder Master": "Master Verzeichnis - schreibgeschützt",
|
||||
"Folder Path": "Verzeichnispfad",
|
||||
"Folder Type": "Ordnertyp",
|
||||
"Folder Type": "Verzeichnistyp",
|
||||
"Folders": "Verzeichnisse",
|
||||
"GUI": "GUI",
|
||||
"GUI Authentication Password": "Passwort für Zugang zur Benutzeroberfläche",
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Versionen erhalten",
|
||||
"Largest First": "Größte zuerst",
|
||||
"Last File Received": "Letzte Änderung",
|
||||
"Last Scan": "Letzter Scan",
|
||||
"Last seen": "Zuletzt online",
|
||||
"Later": "Später",
|
||||
"Listeners": "Zuhörer",
|
||||
@@ -192,7 +193,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing scheint nicht erreichbar zu sein oder es gibt ein Problem mit Deiner Internetverbindung. Versuche erneut...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing scheint ein Problem mit der Verarbeitung Deiner Eingabe zu haben. Bitte lade die Seite neu oder führe einen Neustart durch, falls das Problem weiterhin besteht.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Die Syncthing-Oberfläche erlaubt mit den jetzigen Einstellungen einen Zugriff ohne Passwort.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Die gesammelten Statistiken sind öffentlich verfügbar unter der untenstehenden URL.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Die gesammelten Statistiken sind öffentlich unter der nachfolgenden URL verfügbar.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Die gesammelten Statistiken sind öffentlich verfügbar unter {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Die Konfiguration wurde gespeichert, aber noch nicht aktiviert. Syncthing muss neugestartet werden, um die neue Konfiguration zu übernehmen.",
|
||||
"The device ID cannot be blank.": "Die Geräte ID darf nicht leer sein.",
|
||||
@@ -246,5 +247,5 @@
|
||||
"items": "Objekte",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} möchte das Verzeichnis \"{{folder}}\" teilen.",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} möchte das Verzeichnis \"{{folderLabel}}\" ({{folder}}) teilen.",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} möchte den Ordner \"{{folderLabel}}\" ({{folder}}) teilen."
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} möchte das Verzeichnis \"{{folderLabel}}\" ({{folder}}) teilen."
|
||||
}
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Διατήρηση εκδόσεων",
|
||||
"Largest First": "Το μεγαλύτερο πρώτα",
|
||||
"Last File Received": "Πιο πρόσφατο αρχείο",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Τελευταία φορά συνδεδεμένος",
|
||||
"Later": "Αργότερα",
|
||||
"Listeners": "Listeners",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Keep Versions",
|
||||
"Largest First": "Largest First",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Last seen",
|
||||
"Later": "Later",
|
||||
"Listeners": "Listeners",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Mantener versiones",
|
||||
"Largest First": "Más grande primero",
|
||||
"Last File Received": "Último fichero recibido",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Visto por última vez",
|
||||
"Later": "Más tarde",
|
||||
"Listeners": "Listeners",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Conservar versiones",
|
||||
"Largest First": "Más grande primero",
|
||||
"Last File Received": "Último archivo recibido",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Visto por ultima vez",
|
||||
"Later": "Más tarde",
|
||||
"Listeners": "Listeners",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Säilytä versiot",
|
||||
"Largest First": "Suurin ensin",
|
||||
"Last File Received": "Viimeksi vastaanotettu tiedosto",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Nähty viimeksi",
|
||||
"Later": "Myöhemmin",
|
||||
"Listeners": "Listeners",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Conserver les versions",
|
||||
"Largest First": "Les plus volumineux en premier",
|
||||
"Last File Received": "Dernier fichier reçu",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Dernière apparition",
|
||||
"Later": "Plus tard",
|
||||
"Listeners": "Listeners",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Conserver les versions",
|
||||
"Largest First": "Les plus volumineux en premier",
|
||||
"Last File Received": "Dernier fichier reçu",
|
||||
"Last Scan": "Dernière analyse",
|
||||
"Last seen": "Dernière apparition",
|
||||
"Later": "Plus tard",
|
||||
"Listeners": "Systèmes en écoute",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Ferzjes bewarje",
|
||||
"Largest First": "Grutste earst",
|
||||
"Last File Received": "Leste triem ûntfongen",
|
||||
"Last Scan": "Lêst Skent",
|
||||
"Last seen": "Lêst sjoen",
|
||||
"Later": "Letter",
|
||||
"Listeners": "Harkers",
|
||||
|
||||
@@ -17,7 +17,7 @@
|
||||
"Advanced settings": "Haladó beállítások",
|
||||
"All Data": "Minden adat",
|
||||
"Allow Anonymous Usage Reporting?": "Engedélyezed a névtelen felhasználási adatok küldését?",
|
||||
"Alphabetic": "ABC rendben",
|
||||
"Alphabetic": "ABC sorrendben",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Külső program kezeli a fájl verziókövetést. A fájlt el kell távolítania a szinkronizált mappából.",
|
||||
"Anonymous Usage Reporting": "Névtelen felhasználási adatok küldése",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "A bevezető eszközön beállított minden eszköz hozzá lesz adva ehhez az eszközhöz is.",
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Megtartott verziók",
|
||||
"Largest First": "Nagyobb először",
|
||||
"Last File Received": "Utolsó beérkezett fájl",
|
||||
"Last Scan": "Utolsó vizsgálat",
|
||||
"Last seen": "Utoljára látva",
|
||||
"Later": "Később",
|
||||
"Listeners": "Kapcsolatok",
|
||||
@@ -176,7 +177,7 @@
|
||||
"Shutdown Complete": "Leállítás kész",
|
||||
"Simple File Versioning": "Egyszerű fájl verziókövetés",
|
||||
"Single level wildcard (matches within a directory only)": "Egyszintű helyettesítő karakter (csak egy mappára érvényes)",
|
||||
"Smallest First": "Kisebb előbb",
|
||||
"Smallest First": "Kisebb először",
|
||||
"Source Code": "Forráskód",
|
||||
"Staggered File Versioning": "Többszintű fájl verziókövetés",
|
||||
"Start Browser": "Böngésző indítása",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Keep Versions",
|
||||
"Largest First": "Largest First",
|
||||
"Last File Received": "Last File Received",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Last seen",
|
||||
"Later": "Later",
|
||||
"Listeners": "Listeners",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Versioni Mantenute",
|
||||
"Largest First": "Prima il più grande",
|
||||
"Last File Received": "Ultimo File Ricevuto",
|
||||
"Last Scan": "Ultima scansione",
|
||||
"Last seen": "Ultima connessione",
|
||||
"Later": "Più Tardi",
|
||||
"Listeners": "In ascolto",
|
||||
@@ -192,7 +193,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing sembra inattivo, oppure c'è un problema con la tua connessione a Internet. Nuovo tentativo…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Sembra che Syncthing non sia in grado di elaborare il tuo comando. Se il problema persiste prova a ricaricare la pagina nel tuo navigatore oppure prova a riavviare Syncthing.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interfaccia di amministrazione di Syncthing è configurata in modo da permettere l'accesso senza password.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Le statistiche aggregate sono disponibili pubblicamente all'URL seguente.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Le statistiche aggregate sono disponibili pubblicamente su {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configurazione è stata salvata ma non attivata. Devi riavviare Syncthing per attivare la nuova configurazione.",
|
||||
"The device ID cannot be blank.": "L'ID del dispositivo non può essere vuoto.",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "保存するバージョンの数",
|
||||
"Largest First": "大きい順",
|
||||
"Last File Received": "最後に受信したファイル",
|
||||
"Last Scan": "最終スキャン時刻",
|
||||
"Last seen": "最終接続日時",
|
||||
"Later": "後で設定",
|
||||
"Listeners": "待ち受けポート",
|
||||
@@ -111,7 +112,7 @@
|
||||
"Minimum Free Disk Space": "同期を停止する最小空きディスク容量",
|
||||
"Move to top of queue": "最優先にする",
|
||||
"Multi level wildcard (matches multiple directory levels)": "多階層ワイルドカード (複数のディレクトリ階層にマッチします)",
|
||||
"Never": "接続記録なし",
|
||||
"Never": "記録なし",
|
||||
"New Device": "新規デバイス",
|
||||
"New Folder": "新規フォルダー",
|
||||
"Newest First": "新しい順",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "버전 보관",
|
||||
"Largest First": "큰 파일 순",
|
||||
"Last File Received": "마지막으로 받은 파일",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "마지막 접속",
|
||||
"Later": "나중에",
|
||||
"Listeners": "수신자",
|
||||
|
||||
@@ -98,14 +98,15 @@
|
||||
"Keep Versions": "Saugojamų versijų kiekis",
|
||||
"Largest First": "Didžiausi pirmiau",
|
||||
"Last File Received": "Paskutinis priimtas failas",
|
||||
"Last Scan": "Paskutinis nuskaitymas",
|
||||
"Last seen": "Paskutinį kartą matytas",
|
||||
"Later": "Vėliau",
|
||||
"Listeners": "Listeners",
|
||||
"Listeners": "Klausytojai",
|
||||
"Local Discovery": "Vietinis matomumas",
|
||||
"Local State": "Vietinė būsena",
|
||||
"Local State (Total)": "Vietinė būsena (Bendrai)",
|
||||
"Major Upgrade": "Stambus atnaujinimas",
|
||||
"Master": "Master",
|
||||
"Master": "Pagrindinis",
|
||||
"Maximum Age": "Maksimalus amžius",
|
||||
"Metadata Only": "Metaduomenims",
|
||||
"Minimum Free Disk Space": "Minimum laisvos vietos diske",
|
||||
@@ -117,7 +118,7 @@
|
||||
"Newest First": "Naujausi pirmiau",
|
||||
"No": "Ne",
|
||||
"No File Versioning": "Nėra versijų valdymo",
|
||||
"Normal": "Normal",
|
||||
"Normal": "Normalus",
|
||||
"Notice": "Įspėjimas",
|
||||
"OK": "Gerai",
|
||||
"Off": "Netaikoma",
|
||||
@@ -218,7 +219,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Srauto maksimalus greitis privalo būti ne neigiamas skaičius (0: nėra apribojimo)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Nuskaitymo dažnis negali būti neigiamas skaičius.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Failus bus automatiškai bandoma parsiųsti dar kartą kai išspręsite klaidas",
|
||||
"This Device": "This Device",
|
||||
"This Device": "Šis įrenginys",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Tai gali suteikti programišiams lengvą prieigą skaityti ir keisti bet kokius failus jūsų kompiuteryje.",
|
||||
"This is a major version upgrade.": "Tai yra stambus atnaujinimas.",
|
||||
"Trash Can File Versioning": "Šiukšliadėžės versijų valdymas",
|
||||
|
||||
@@ -97,7 +97,8 @@
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Invers av den gitte tilstanden (t.d. ikke ekskluder)",
|
||||
"Keep Versions": "Behold Versjoner",
|
||||
"Largest First": "Største fil",
|
||||
"Last File Received": "Sist Mottatte Fil",
|
||||
"Last File Received": "Siste mottatte fil",
|
||||
"Last Scan": "Siste gjennomsøking",
|
||||
"Last seen": "Sist sett",
|
||||
"Later": "Senere",
|
||||
"Listeners": "Lyttere",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Versies behouden",
|
||||
"Largest First": "Grootste eerst",
|
||||
"Last File Received": "Laatst ontvangen bestand",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Laatst gezien op",
|
||||
"Later": "Later",
|
||||
"Listeners": "Luisteraars",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Behald Versjonar",
|
||||
"Largest First": "Største fyrst",
|
||||
"Last File Received": "Siste mottatte fila",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Sist sett",
|
||||
"Later": "Seinare",
|
||||
"Listeners": "Listeners",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Zachowuj wersje",
|
||||
"Largest First": "Największe na początku",
|
||||
"Last File Received": "Ostatni otrzymany plik",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Ostatnio widziany",
|
||||
"Later": "Później",
|
||||
"Listeners": "Nasłuchujący",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Manter versões",
|
||||
"Largest First": "Maior primeiro",
|
||||
"Last File Received": "Último arquivo recebido",
|
||||
"Last Scan": "Última verificação",
|
||||
"Last seen": "Visto por último em",
|
||||
"Later": "Depois",
|
||||
"Listeners": "Escutadores",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Manter versões",
|
||||
"Largest First": "Primeiro os maiores",
|
||||
"Last File Received": "Último ficheiro recebido",
|
||||
"Last Scan": "Última verificação",
|
||||
"Last seen": "Última vez que foi verificado",
|
||||
"Later": "Mais tarde",
|
||||
"Listeners": "Auscultadores",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Количество хранимых версий",
|
||||
"Largest First": "Сначала большие",
|
||||
"Last File Received": "Последний полученный файл",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Был доступен",
|
||||
"Later": "Позже",
|
||||
"Listeners": "Прослушиватель",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Behåll versioner",
|
||||
"Largest First": "Störst först",
|
||||
"Last File Received": "Senast Mottagna Fil",
|
||||
"Last Scan": "Senast skanning",
|
||||
"Last seen": "Senast online",
|
||||
"Later": "Senare",
|
||||
"Listeners": "Lyssnare",
|
||||
@@ -192,7 +193,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar avstängd, eller finns det problem med din Internetanslutning. Försöker igen...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administratör gränssnittet är konfigurerat för att tillåta fjärrtillträde utan ett lösenord.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Den aggregerade statistiken är offentligt tillgängliga på webbadressen nedan.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Sammanställd statistik finns publikt tillgänglig på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
|
||||
"The device ID cannot be blank.": "Enhets-ID kan inte vara tomt.",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Sürümleri Tut",
|
||||
"Largest First": "En büyük olan önce",
|
||||
"Last File Received": "Alınan Son Dosya",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Son Görülen",
|
||||
"Later": "Sonra",
|
||||
"Listeners": "Listeners",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Зберігати версії",
|
||||
"Largest First": "Спершу найбільші",
|
||||
"Last File Received": "Останній завантажений файл",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "З’являвся останній раз",
|
||||
"Later": "Пізніше",
|
||||
"Listeners": "Приймачі (TCP & Relay)",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "Giữ các ph.bản",
|
||||
"Largest First": "Lớn nhất đầu tiên",
|
||||
"Last File Received": "T.tin nhận được gần đây",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Thấy lần cuối",
|
||||
"Later": "Để sau",
|
||||
"Listeners": "Listeners",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "保留历史版本数量",
|
||||
"Largest First": "大文件优先",
|
||||
"Last File Received": "最后接收的文件",
|
||||
"Last Scan": "最后扫描",
|
||||
"Last seen": "最后可见",
|
||||
"Later": "稍后",
|
||||
"Listeners": "侦听程序",
|
||||
@@ -192,7 +193,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎关闭了,或者您的网络连接存在故障。重试中...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing 在处理您的请求时似乎遇到了问题。如果问题持续,请刷新页面,或重启 Syncthing。",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "当前配置允许在不使用密码的情况下远程访问 Syncthing 管理界面",
|
||||
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "全局统计数据公布于以下 URL。",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "全局统计公布于 {{url}}",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "设置已经保存,但是还未生效。Syncthing 需要重启以启用新的设置。",
|
||||
"The device ID cannot be blank.": "设备标识不能为空",
|
||||
|
||||
@@ -98,6 +98,7 @@
|
||||
"Keep Versions": "保留歷史版本數",
|
||||
"Largest First": "最大的優先",
|
||||
"Last File Received": "最後接收的檔案",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "最後發現時間",
|
||||
"Later": "稍後",
|
||||
"Listeners": "Listeners",
|
||||
|
||||
@@ -45,7 +45,7 @@
|
||||
</button>
|
||||
</li>
|
||||
<li ng-if="upgradeInfo && upgradeInfo.majorNewer" class="upgrade-newer-major">
|
||||
<button type="button" class="btn navbar-btn btn-danger btn-sm" ng-click="upgradeMajor()">
|
||||
<button type="button" class="btn navbar-btn btn-danger btn-sm" data-toggle="modal" data-target="#majorUpgrade">
|
||||
<span class="fa fa-arrow-circle-up"></span>
|
||||
<span class="hidden-xs" translate translate-value-version="{{upgradeInfo.latest}}">Upgrade To {%version%}</span>
|
||||
</button>
|
||||
@@ -65,7 +65,7 @@
|
||||
</a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a href="" ng-click="editSettings()"><span class="fa fa-fw fa-cog"></span> <span translate>Settings</span></a></li>
|
||||
<li><a href="" ng-click="idDevice(thisDevice())"><span class="fa fa-fw fa-qrcode"></span> <span translate>Show ID</span></a></li>
|
||||
<li><a href="" data-toggle="modal" data-target="#idqr" ng-click="currentDevice=thisDevice()"><span class="fa fa-fw fa-qrcode"></span> <span translate>Show ID</span></a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="" ng-click="shutdown()"><span class="fa fa-fw fa-power-off"></span> <span translate>Shutdown</span></a></li>
|
||||
<li><a href="" ng-click="restart()"><span class="fa fa-fw fa-refresh"></span> <span translate>Restart</span></a></li>
|
||||
@@ -75,7 +75,7 @@
|
||||
<span class="fa fa-fw fa-book"></span> <span translate>Help</span>
|
||||
</a>
|
||||
</li>
|
||||
<li><a href="" ng-click="about()"><span class="fa fa-fw fa-heart-o"></span> <span translate>About</span></a></li>
|
||||
<li><a href="" data-toggle="modal" data-target="#about"><span class="fa fa-fw fa-heart-o"></span> <span translate>About</span></a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a href="" ng-click="advanced()"><span class="fa fa-fw fa-cogs"></span> <span translate>Advanced</span></a></li>
|
||||
</ul>
|
||||
@@ -258,7 +258,7 @@
|
||||
<h3 translate>Folders</h3>
|
||||
<div class="panel-group" id="folders">
|
||||
<div class="panel panel-default" ng-repeat="folder in folderList()">
|
||||
<a class="panel-heading" data-toggle="collapse" data-parent="#folders" href="#folder-{{$index}}">
|
||||
<button class="btn panel-heading" data-toggle="collapse" data-parent="#folders" data-target="#folder-{{$index}}">
|
||||
<div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.id)}}%"></div>
|
||||
<div class="panel-progress" ng-show="folderStatus(folder) == 'scanning' && scanProgress[folder.id] != undefined" ng-attr-style="width: {{scanPercentage(folder.id)}}%"></div>
|
||||
<h4 class="panel-title">
|
||||
@@ -287,7 +287,7 @@
|
||||
<span tooltip data-original-title="{{folder.label.length != 0 ? folder.id : ''}}">{{folder.label.length != 0 ? folder.label : folder.id}}</span>
|
||||
</div>
|
||||
</h4>
|
||||
</a>
|
||||
</button>
|
||||
<div id="folder-{{$index}}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed table-striped">
|
||||
@@ -440,12 +440,12 @@
|
||||
<div class="col-md-6">
|
||||
<h3 translate>This Device</h3>
|
||||
<div class="panel panel-default" ng-repeat="deviceCfg in [thisDevice()]">
|
||||
<a class="panel-heading" data-toggle="collapse" href="#device-this">
|
||||
<button class="btn panel-heading" data-toggle="collapse" data-target="#device-this">
|
||||
<h4 class="panel-title">
|
||||
<identicon class="panel-icon" data-value="deviceCfg.deviceID"></identicon>
|
||||
<div class="panel-title-text">{{deviceName(deviceCfg)}}</div>
|
||||
</h4>
|
||||
</a>
|
||||
</button>
|
||||
<div id="device-this" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed table-striped">
|
||||
@@ -516,11 +516,10 @@
|
||||
<h3 translate>Remote Devices</h3>
|
||||
<div class="panel-group" id="devices">
|
||||
<div class="panel panel-default" ng-repeat="deviceCfg in otherDevices()">
|
||||
<a class="panel-heading" data-toggle="collapse" data-parent="#devices" href="#device-{{$index}}">
|
||||
<button class="btn panel-heading" data-toggle="collapse" data-parent="#devices" data-target="#device-{{$index}}">
|
||||
<div class="panel-progress" ng-show="deviceStatus(deviceCfg) == 'syncing'" ng-attr-style="width: {{completion[deviceCfg.deviceID]._total | number:0}}%"></div>
|
||||
<h4 class="panel-title">
|
||||
<identicon class="panel-icon" data-value="deviceCfg.deviceID"></identicon>
|
||||
<span>{{deviceName(deviceCfg)}}</span>
|
||||
<span ng-switch="deviceStatus(deviceCfg)" class="pull-right text-{{deviceClass(deviceCfg)}}">
|
||||
<span ng-switch-when="insync"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="syncing">
|
||||
@@ -530,8 +529,9 @@
|
||||
<span ng-switch-when="disconnected"><span class="hidden-xs" translate>Disconnected</span><span class="visible-xs">◼</span></span>
|
||||
<span ng-switch-when="unused"><span class="hidden-xs" translate>Unused</span><span class="visible-xs">◼</span></span>
|
||||
</span>
|
||||
<span>{{deviceName(deviceCfg)}}</span>
|
||||
</h4>
|
||||
</a>
|
||||
</button>
|
||||
<div id="device-{{$index}}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<table class="table table-condensed table-striped">
|
||||
@@ -585,15 +585,15 @@
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<span class="pull-right">
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="editDevice(deviceCfg)">
|
||||
<span class="fa fa-pencil"></span> <span translate>Edit</span>
|
||||
</button>
|
||||
<button ng-if="!connections[deviceCfg.deviceID].paused" type="button" class="btn btn-sm btn-default" ng-click="pauseDevice(deviceCfg.deviceID)">
|
||||
<span class="fa fa-pause"></span> <span translate>Pause</span>
|
||||
</button>
|
||||
<button ng-if="connections[deviceCfg.deviceID].paused" type="button" class="btn btn-sm btn-default" ng-click="resumeDevice(deviceCfg.deviceID)">
|
||||
<span class="fa fa-play"></span> <span translate>Resume</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="editDevice(deviceCfg)">
|
||||
<span class="fa fa-pencil"></span> <span translate>Edit</span>
|
||||
</button>
|
||||
</span>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
@@ -628,23 +628,23 @@
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
<div network-error-dialog></div>
|
||||
<div http-error-dialog></div>
|
||||
<div restarting-dialog></div>
|
||||
<div upgrading-dialog></div>
|
||||
<div shutdown-dialog></div>
|
||||
<div idqr-modal></div>
|
||||
<div major-upgrade-modal></div>
|
||||
<div edit-device-modal></div>
|
||||
<div edit-folder-modal></div>
|
||||
<div edit-ignores-modal></div>
|
||||
<div settings-modal></div>
|
||||
<div advanced-settings-modal></div>
|
||||
<div usage-report-modal></div>
|
||||
<div usage-report-preview-modal></div>
|
||||
<div needed-files-modal></div>
|
||||
<div failed-files-modal></div>
|
||||
<div about-modal></div>
|
||||
<ng-include src="'syncthing/core/networkErrorDialogView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/core/httpErrorDialogView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/core/restartingDialogView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/core/upgradingDialogView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/core/shutdownDialogView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/device/idqrModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/device/editDeviceModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/folder/editFolderModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/folder/editIgnoresModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/settings/settingsModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/settings/advancedSettingsModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/usagereport/usageReportModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/usagereport/usageReportPreviewModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/transfer/neededFilesModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/transfer/failedFilesModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/core/majorUpgradeModalView.html'"></ng-include>
|
||||
<ng-include src="'syncthing/core/aboutModalView.html'"></ng-include>
|
||||
|
||||
<!-- vendor scripts -->
|
||||
<script src="vendor/jquery/jquery-2.2.2.js"></script>
|
||||
@@ -657,52 +657,24 @@
|
||||
|
||||
<!-- gui application code -->
|
||||
<script src="syncthing/core/module.js"></script>
|
||||
<script src="syncthing/core/aboutModalDirective.js"></script>
|
||||
<script src="syncthing/core/alwaysNumberFilter.js"></script>
|
||||
<script src="syncthing/core/basenameFilter.js"></script>
|
||||
<script src="syncthing/core/binaryFilter.js"></script>
|
||||
<script src="syncthing/core/durationFilter.js"></script>
|
||||
<script src="syncthing/core/eventService.js"></script>
|
||||
<script src="syncthing/core/httpErrorDialogDirective.js"></script>
|
||||
<script src="syncthing/core/identiconDirective.js"></script>
|
||||
<script src="syncthing/core/languageSelectDirective.js"></script>
|
||||
<script src="syncthing/core/lastErrorComponentFilter.js"></script>
|
||||
<script src="syncthing/core/localeService.js"></script>
|
||||
<script src="syncthing/core/majorUpgradeModalDirective.js"></script>
|
||||
<script src="syncthing/core/modalDirective.js"></script>
|
||||
<script src="syncthing/core/naturalFilter.js"></script>
|
||||
<script src="syncthing/core/networkErrorDialogDirective.js"></script>
|
||||
<script src="syncthing/core/pathIsSubDirDirective.js"></script>
|
||||
<script src="syncthing/core/popoverDirective.js"></script>
|
||||
<script src="syncthing/core/restartingDialogDirective.js"></script>
|
||||
<script src="syncthing/core/selectOnClickDirective.js"></script>
|
||||
<script src="syncthing/core/shutdownDialogDirective.js"></script>
|
||||
<script src="syncthing/core/syncthingController.js"></script>
|
||||
<script src="syncthing/core/tooltipDirective.js"></script>
|
||||
<script src="syncthing/core/uniqueFolderDirective.js"></script>
|
||||
<script src="syncthing/core/upgradingDialogDirective.js"></script>
|
||||
<script src="syncthing/core/validDeviceidDirective.js"></script>
|
||||
|
||||
<script src="syncthing/device/module.js"></script>
|
||||
<script src="syncthing/device/editDeviceModalDirective.js"></script>
|
||||
<script src="syncthing/device/idqrModalDirective.js"></script>
|
||||
|
||||
<script src="syncthing/folder/module.js"></script>
|
||||
<script src="syncthing/folder/editFolderModalDirective.js"></script>
|
||||
<script src="syncthing/folder/editIgnoresModalDirective.js"></script>
|
||||
|
||||
<script src="syncthing/settings/module.js"></script>
|
||||
<script src="syncthing/settings/settingsModalDirective.js"></script>
|
||||
<script src="syncthing/settings/advancedSettingsModalDirective.js"></script>
|
||||
|
||||
<script src="syncthing/transfer/module.js"></script>
|
||||
<script src="syncthing/transfer/failedFilesModalDirective.js"></script>
|
||||
<script src="syncthing/transfer/neededFilesModalDirective.js"></script>
|
||||
|
||||
<script src="syncthing/usagereport/module.js"></script>
|
||||
<script src="syncthing/usagereport/usageReportModalDirective.js"></script>
|
||||
<script src="syncthing/usagereport/usageReportPreviewModalDirective.js"></script>
|
||||
|
||||
<script src="assets/lang/valid-langs.js"></script>
|
||||
<script src="assets/lang/prettyprint.js"></script>
|
||||
<script src="meta.js"></script>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<div class="modal fade" tabindex="-1" ng-attr-data-backdrop="{{ close ? true : 'static' }}" ng-attr-data-keyboard="{{ Boolean(close) }}">
|
||||
<div class="modal fade" tabindex="-1" ng-attr-data-backdrop="{{ closeable == 'yes' ? true : 'static' }}" ng-attr-data-keyboard="{{ closeable == 'yes' ? true : false }}">
|
||||
<!--
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
@@ -6,23 +6,17 @@
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
-->
|
||||
<div class="modal-dialog" ng-class="{'modal-lg': large}">
|
||||
<div class="modal-dialog {{ large == 'yes' ? 'modal-lg' : '' }}">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header alert alert-{{status}}">
|
||||
<div class="modal-header {{status == 'default' ? '' : 'alert alert-'+status }}">
|
||||
<h4 class="modal-title">
|
||||
<div class="panel-icon">
|
||||
<span ng-if="icon" class="fa fa-{{icon}}"></span>
|
||||
<div ng-if="icon" class="panel-icon">
|
||||
<span class="fa fa-{{icon}}"></span>
|
||||
</div>
|
||||
{{title}}
|
||||
{{heading}}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body" ng-transclude>
|
||||
</div>
|
||||
<div ng-if="close" class="modal-footer">
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
<div ng-transclude></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -12,12 +12,7 @@ var syncthing = angular.module('syncthing', [
|
||||
'angularUtils.directives.dirPagination',
|
||||
'pascalprecht.translate',
|
||||
|
||||
'syncthing.core',
|
||||
'syncthing.device',
|
||||
'syncthing.folder',
|
||||
'syncthing.settings',
|
||||
'syncthing.transfer',
|
||||
'syncthing.usagereport'
|
||||
'syncthing.core'
|
||||
]);
|
||||
|
||||
var urlbase = 'rest';
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.core')
|
||||
.directive('aboutModal', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/core/aboutModalView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,33 +1,40 @@
|
||||
<modal id="about" status="info" icon="heart-o" title="{{'About' | translate}}" large="yes" close="yes">
|
||||
<h1 class="text-center">
|
||||
<img alt="Syncthing" title="Syncthing" src="assets/img/logo-horizontal.svg" style="vertical-align: -16px" height="100" width="366"/>
|
||||
<br/>
|
||||
<small>{{versionString()}}</small>
|
||||
<br/>
|
||||
<small><i>"{{version.codename}}"</i></small>
|
||||
</h1>
|
||||
<hr/>
|
||||
<modal id="about" status="info" icon="heart-o" heading="{{'About' | translate}}" large="yes" closeable="yes">
|
||||
<div class="modal-body">
|
||||
<h1 class="text-center">
|
||||
<img alt="Syncthing" src="assets/img/logo-horizontal.svg" style="vertical-align: -16px" height="100" width="366"/>
|
||||
<br/>
|
||||
<small>{{versionString()}}</small>
|
||||
<br/>
|
||||
<small><i>"{{version.codename}}"</i></small>
|
||||
</h1>
|
||||
<hr/>
|
||||
|
||||
<p translate>Copyright © 2014-2016 the following Contributors:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-12" id="contributor-list">
|
||||
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Alexandre Viau, Andrew Dunham, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Laurent Etiemble, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Stefan Kuntz, Tim Abell, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Yannic A.
|
||||
<p translate>Copyright © 2014-2016 the following Contributors:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-12" id="contributor-list">
|
||||
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Alexandre Viau, Andrew Dunham, Andrey D, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Laurent Etiemble, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Stefan Kuntz, Tim Abell, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Yannic A.
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
<p translate>Syncthing includes the following software or portions thereof:</p>
|
||||
<ul class="list-unstyled two-columns">
|
||||
<li><a href="https://golang.org">The Go Programming Language</a>, Copyright © 2012 The Go Authors.</li>
|
||||
<li><a href="https://github.com/bkaradzic/go-lz4">bkaradzic/go-lz4</a>, Copyright © 2011-2012 Branimir Karadzic, 2013 Damian Gryski.</li>
|
||||
<li><a href="https://github.com/kardianos/osext">kardianos/osext</a>, Copyright © 2012 Daniel Theophanes.</li>
|
||||
<li><a href="https://github.com/golang/snappy">golang/snappy</a>, Copyright © 2011 The Snappy-Go Authors.</li>
|
||||
<li><a href="https://github.com/juju/ratelimit">juju/ratelimit</a>, Copyright © 2015 Canonical Ltd.</li>
|
||||
<li><a href="https://github.com/thejerf/suture">thejerf/suture</a>, Copyright © 2014-2015 Barracuda Networks, Inc.</li>
|
||||
<li><a href="https://github.com/syndtr/goleveldb">syndtr/goleveldb</a>, Copyright © 2012, Suryandaru Triandana</li>
|
||||
<li><a href="https://github.com/vitrun/qart">vitrun/qart</a>, Copyright © The Go Authors.</li>
|
||||
<li><a href="https://angularjs.org/">AngularJS</a>, Copyright © 2010-2015 Google, Inc.</li>
|
||||
<li><a href="http://getbootstrap.com/">Bootstrap</a>, Copyright © 2011-2015 Twitter, Inc.</li>
|
||||
<li><a href="http://fontawesome.io/">Font Awesome</a>, Copyright © 2015 Dave Gandy</li>
|
||||
</ul>
|
||||
<p translate>Syncthing includes the following software or portions thereof:</p>
|
||||
<ul class="list-unstyled two-columns">
|
||||
<li><a href="https://golang.org">The Go Programming Language</a>, Copyright © 2012 The Go Authors.</li>
|
||||
<li><a href="https://github.com/bkaradzic/go-lz4">bkaradzic/go-lz4</a>, Copyright © 2011-2012 Branimir Karadzic, 2013 Damian Gryski.</li>
|
||||
<li><a href="https://github.com/kardianos/osext">kardianos/osext</a>, Copyright © 2012 Daniel Theophanes.</li>
|
||||
<li><a href="https://github.com/golang/snappy">golang/snappy</a>, Copyright © 2011 The Snappy-Go Authors.</li>
|
||||
<li><a href="https://github.com/juju/ratelimit">juju/ratelimit</a>, Copyright © 2015 Canonical Ltd.</li>
|
||||
<li><a href="https://github.com/thejerf/suture">thejerf/suture</a>, Copyright © 2014-2015 Barracuda Networks, Inc.</li>
|
||||
<li><a href="https://github.com/syndtr/goleveldb">syndtr/goleveldb</a>, Copyright © 2012, Suryandaru Triandana</li>
|
||||
<li><a href="https://github.com/vitrun/qart">vitrun/qart</a>, Copyright © The Go Authors.</li>
|
||||
<li><a href="https://angularjs.org/">AngularJS</a>, Copyright © 2010-2015 Google, Inc.</li>
|
||||
<li><a href="http://getbootstrap.com/">Bootstrap</a>, Copyright © 2011-2015 Twitter, Inc.</li>
|
||||
<li><a href="http://fontawesome.io/">Font Awesome</a>, Copyright © 2015 Dave Gandy</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.core')
|
||||
.directive('httpErrorDialog', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/core/httpErrorDialogView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
<modal id="httpError" status="danger" icon="exclamation-circle" title="{{'Connection Error' | translate}}">
|
||||
<p translate>
|
||||
Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.
|
||||
</p>
|
||||
<modal id="httpError" status="danger" icon="exclamation-circle" heading="{{'Connection Error' | translate}}" large="no" closeable="no">
|
||||
<div class="modal-body">
|
||||
<p translate>
|
||||
Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.
|
||||
</p>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.core')
|
||||
.directive('majorUpgradeModal', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/core/majorUpgradeModalView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,32 +1,20 @@
|
||||
<div id="majorUpgrade" class="modal fade" tabindex="-1" data-backdrop="true" data-keyboard="true">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header alert alert-danger">
|
||||
<h4 class="modal-title">
|
||||
<div class="panel-icon">
|
||||
<span class="fa fa-arrow-circle-up"></span>
|
||||
</div>
|
||||
<span translate>Major Upgrade</span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<span translate>This is a major version upgrade.</span>
|
||||
<span translate>A new major version may not be compatible with previous versions.</span>
|
||||
<span translate>Please consult the release notes before performing a major upgrade.</span>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/syncthing/syncthing/releases/tag/{{upgradeInfo.latest}}" target="_blank" translate>Release Notes</a>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="upgrade()">
|
||||
<span class="fa fa-check"></span> <span translate>Upgrade</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<modal id="majorUpgrade" status="danger" icon="arrow-circle-up" heading="{{'Major Upgrade' | translate}}" large="no" closeable="yes">
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<span translate>This is a major version upgrade.</span>
|
||||
<span translate>A new major version may not be compatible with previous versions.</span>
|
||||
<span translate>Please consult the release notes before performing a major upgrade.</span>
|
||||
</p>
|
||||
<p>
|
||||
<a href="https://github.com/syncthing/syncthing/releases/tag/{{upgradeInfo.latest}}" target="_blank" translate>Release Notes</a>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="upgrade()">
|
||||
<span class="fa fa-check"></span> <span translate>Upgrade</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -6,11 +6,67 @@ angular.module('syncthing.core')
|
||||
replace: true,
|
||||
transclude: true,
|
||||
scope: {
|
||||
title: '@',
|
||||
heading: '@',
|
||||
status: '@',
|
||||
icon: '@',
|
||||
close: '@',
|
||||
closeable: '@',
|
||||
large: '@'
|
||||
},
|
||||
link: function (scope, element, attrs) {
|
||||
|
||||
// before modal show animation
|
||||
$(element).on('show.bs.modal', function () {
|
||||
|
||||
// cycle through open modals, acertain modal with highest z-index
|
||||
var largestZ = 1040;
|
||||
$('.modal:visible').each(function (i) {
|
||||
var thisZ = parseInt($(this).css('zIndex'));
|
||||
if (thisZ > largestZ) {
|
||||
largestZ = thisZ;
|
||||
}
|
||||
});
|
||||
|
||||
// set this modal's z-index to be 10 above the highest z
|
||||
var aboveLargestZ = largestZ + 10;
|
||||
$(element).css('zIndex', aboveLargestZ);
|
||||
|
||||
// set backdrop z-index. timeout used because element does not exist immediatly
|
||||
setTimeout(function () {
|
||||
$('.modal-backdrop:not(:last)').removeClass('in').addClass('out');
|
||||
$('.modal-backdrop:last').attr('for-modal-id', $(element).attr('id')).css('zIndex', aboveLargestZ - 5);
|
||||
}, 0);
|
||||
|
||||
});
|
||||
|
||||
// BEFORE modal hide animation
|
||||
$(element).on('hide.bs.modal', function () {
|
||||
|
||||
// find and unhide the next backdrop down in z order
|
||||
var sel = false, largestZ = 0;
|
||||
$('.modal-backdrop').each(function (i) {
|
||||
var thisZ = parseInt($(this).css('zIndex'));
|
||||
if (thisZ > largestZ && $(this).attr('for-modal-id') !== $(element).attr('id')) {
|
||||
largestZ = thisZ;
|
||||
sel = i;
|
||||
}
|
||||
});
|
||||
if (sel !== false) {
|
||||
$('.modal-backdrop:eq(' + sel + ')').removeClass('out').addClass('in');
|
||||
}
|
||||
});
|
||||
|
||||
// AFTER modal hide animation
|
||||
$(element).on('hidden.bs.modal', function () {
|
||||
|
||||
// reset z-index of modal to normal
|
||||
$(element).css('zIndex', 1040);
|
||||
|
||||
// fix scrolling by re-adding .modal-open to body
|
||||
if ($('.modal:visible').length > 0) {
|
||||
$('body').addClass('modal-open');
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.core')
|
||||
.directive('networkErrorDialog', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/core/networkErrorDialogView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,5 +1,7 @@
|
||||
<modal id="networkError" status="danger" icon="exclamation-circle" title="{{'Connection Error' | translate}}">
|
||||
<p translate>
|
||||
Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…
|
||||
</p>
|
||||
<modal id="networkError" status="danger" icon="exclamation-circle" heading="{{'Connection Error' | translate}}" large="no" closeable="no">
|
||||
<div class="modal-body">
|
||||
<p translate>
|
||||
Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…
|
||||
</p>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.core')
|
||||
.directive('restartingDialog', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/core/restartingDialogView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,3 +1,8 @@
|
||||
<modal id="restarting" status="info" icon="refresh" title="{{'Restarting' | translate}}">
|
||||
<p><span translate>Syncthing is restarting.</span> <span translate>Please wait</span>...</p>
|
||||
<modal id="restarting" status="info" icon="refresh" heading="{{'Restarting' | translate}}" large="no" closeable="no">
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<span translate>Syncthing is restarting.</span>
|
||||
<span translate>Please wait</span>...
|
||||
</p>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.core')
|
||||
.directive('shutdownDialog', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/core/shutdownDialogView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,3 +1,7 @@
|
||||
<modal id="shutdown" status="success" icon="power-off" title="{{'Shutdown Complete' | translate}}">
|
||||
<p translate>Syncthing has been shut down.</p>
|
||||
<modal id="shutdown" status="success" icon="power-off" heading="{{'Shutdown Complete' | translate}}" large="no" closeable="no">
|
||||
<div class="modal-body">
|
||||
<p translate>
|
||||
Syncthing has been shut down.
|
||||
</p>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -49,6 +49,7 @@ angular.module('syncthing.core')
|
||||
$scope.failedCurrentFolder = undefined;
|
||||
$scope.failedPageSize = 10;
|
||||
$scope.scanProgress = {};
|
||||
$scope.themes = [];
|
||||
|
||||
$scope.localStateTotal = {
|
||||
bytes: 0,
|
||||
@@ -88,6 +89,7 @@ angular.module('syncthing.core')
|
||||
refreshConnectionStats();
|
||||
refreshDeviceStats();
|
||||
refreshFolderStats();
|
||||
refreshThemes();
|
||||
|
||||
$http.get(urlbase + '/system/version').success(function (data) {
|
||||
if ($scope.version.version && $scope.version.version !== data.version) {
|
||||
@@ -219,7 +221,9 @@ angular.module('syncthing.core')
|
||||
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30 * 24 * 3600;
|
||||
} else {
|
||||
if (+firstVisit < Date.now() - 4 * 3600 * 1000) {
|
||||
$('#ur').modal();
|
||||
setTimeout(function () {
|
||||
$('#ur').modal();
|
||||
}, 2500);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -599,6 +603,12 @@ angular.module('syncthing.core')
|
||||
}).error($scope.emitHTTPError);
|
||||
}, 2500);
|
||||
|
||||
var refreshThemes = debounce(function () {
|
||||
$http.get("themes.json").success(function (data) { // no urlbase here as this is served by the asset handler
|
||||
$scope.themes = data.themes;
|
||||
}).error($scope.emitHTTPError);
|
||||
}, 2500);
|
||||
|
||||
$scope.refresh = function () {
|
||||
refreshSystem();
|
||||
refreshConnectionStats();
|
||||
@@ -627,7 +637,7 @@ angular.module('syncthing.core')
|
||||
return 'outofsync';
|
||||
}
|
||||
if (state === 'scanning') {
|
||||
return state;
|
||||
return state;
|
||||
}
|
||||
|
||||
if (folderCfg.devices.length <= 1) {
|
||||
@@ -1020,10 +1030,6 @@ angular.module('syncthing.core')
|
||||
});
|
||||
};
|
||||
|
||||
$scope.upgradeMajor = function () {
|
||||
$('#majorUpgrade').modal();
|
||||
};
|
||||
|
||||
$scope.shutdown = function () {
|
||||
restarting = true;
|
||||
$http.post(urlbase + '/system/shutdown').success(function () {
|
||||
@@ -1044,12 +1050,6 @@ angular.module('syncthing.core')
|
||||
$('#editDevice').modal();
|
||||
};
|
||||
|
||||
$scope.idDevice = function (deviceCfg) {
|
||||
$scope.currentDevice = deviceCfg;
|
||||
$('#editDevice').modal('hide');
|
||||
$('#idqr').modal('show');
|
||||
};
|
||||
|
||||
$scope.addDevice = function (deviceID, name) {
|
||||
return $http.get(urlbase + '/system/discovery')
|
||||
.success(function (registry) {
|
||||
@@ -1221,6 +1221,18 @@ angular.module('syncthing.core')
|
||||
}).error($scope.emitHTTPError);
|
||||
});
|
||||
|
||||
$scope.loadFormIntoScope = function (form) {
|
||||
console.log('loadFormIntoScope',form.$name);
|
||||
switch (form.$name) {
|
||||
case 'deviceEditor':
|
||||
$scope.deviceEditor = form;
|
||||
break;
|
||||
case 'folderEditor':
|
||||
$scope.folderEditor = form;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
$scope.editFolder = function (folderCfg) {
|
||||
$scope.currentFolder = angular.copy(folderCfg);
|
||||
if ($scope.currentFolder.path.slice(-1) === $scope.system.pathSeparator) {
|
||||
@@ -1456,20 +1468,11 @@ angular.module('syncthing.core')
|
||||
$http.get(urlbase + '/db/ignores?folder=' + encodeURIComponent($scope.currentFolder.id))
|
||||
.success(function (data) {
|
||||
data.ignore = data.ignore || [];
|
||||
|
||||
$('#editFolder').modal('hide')
|
||||
.one('hidden.bs.modal', function() {
|
||||
var textArea = $('#editIgnores textarea');
|
||||
|
||||
textArea.val(data.ignore.join('\n'));
|
||||
|
||||
$('#editIgnores').modal()
|
||||
.one('hidden.bs.modal', function () {
|
||||
$('#editFolder').modal();
|
||||
})
|
||||
.one('shown.bs.modal', function () {
|
||||
textArea.focus();
|
||||
});
|
||||
var textArea = $('#editIgnores textarea');
|
||||
textArea.val(data.ignore.join('\n'));
|
||||
$('#editIgnores').modal()
|
||||
.one('shown.bs.modal', function () {
|
||||
textArea.focus();
|
||||
});
|
||||
})
|
||||
.then(function () {
|
||||
@@ -1493,16 +1496,6 @@ angular.module('syncthing.core')
|
||||
});
|
||||
};
|
||||
|
||||
$scope.showURPreview = function () {
|
||||
$('#settings').modal('hide')
|
||||
.one('hidden.bs.modal', function() {
|
||||
$('#urPreview').modal()
|
||||
.one('hidden.bs.modal', function () {
|
||||
$('#settings').modal();
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
$scope.acceptUR = function () {
|
||||
$scope.config.options.urAccepted = 1000; // Larger than the largest existing report version
|
||||
$scope.saveConfig();
|
||||
@@ -1547,10 +1540,6 @@ angular.module('syncthing.core')
|
||||
$http.post(urlbase + "/db/override?folder=" + encodeURIComponent(folder));
|
||||
};
|
||||
|
||||
$scope.about = function () {
|
||||
$('#about').modal('show');
|
||||
};
|
||||
|
||||
$scope.advanced = function () {
|
||||
$scope.advancedConfig = angular.copy($scope.config);
|
||||
$('#advanced').modal('show');
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.core')
|
||||
.directive('upgradingDialog', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/core/upgradingDialogView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,3 +1,8 @@
|
||||
<modal id="upgrading" status="info" icon="arrow-circle-up" title="{{'Upgrading' | translate}}">
|
||||
<p><span translate>Syncthing is upgrading.</span> <span translate>Please wait</span>...</p>
|
||||
<modal id="upgrading" status="info" icon="arrow-circle-up" heading="{{'Upgrading' | translate}}" large="no" closeable="no">
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<span translate>Syncthing is upgrading.</span>
|
||||
<span translate>Please wait</span>...
|
||||
</p>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.device')
|
||||
.directive('editDeviceModal', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/device/editDeviceModalView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,104 +1,82 @@
|
||||
<div id="editDevice" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
<span ng-show="!editingExisting">
|
||||
<div class="panel-icon">
|
||||
<span class="fa fa-desktop"></span>
|
||||
</div>
|
||||
<span translate>Add Device</span>
|
||||
</span>
|
||||
<span ng-show="editingExisting">
|
||||
<div class="panel-icon">
|
||||
<span class="fa fa-pencil"></span>
|
||||
</div>
|
||||
<span translate>Edit Device</span>
|
||||
</span>
|
||||
</h4>
|
||||
<modal id="editDevice" status="default" icon="{{editingExisting ? 'pencil' : 'desktop'}}" heading="{{editingExisting ? 'Edit Device' : 'Add Device' | translate}}" large="yes" closeable="yes">
|
||||
<div class="modal-body">
|
||||
<form role="form" name="deviceEditor">
|
||||
<div class="form-group" ng-class="{'has-error': deviceEditor.deviceID.$invalid && deviceEditor.deviceID.$dirty}" ng-init="loadFormIntoScope(deviceEditor)">
|
||||
<label translate for="deviceID">Device ID</label>
|
||||
<input ng-if="!editingExisting" name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.deviceID" required valid-deviceid list="discovery-list" />
|
||||
<datalist id="discovery-list" ng-if="!editingExisting">
|
||||
<option ng-repeat="(id, data) in discovery" value="{{id}}" />
|
||||
</datalist>
|
||||
<div ng-if="editingExisting" class="well well-sm text-monospace" select-on-click>{{currentDevice.deviceID}}</div>
|
||||
<p class="help-block">
|
||||
<span translate ng-if="deviceEditor.deviceID.$valid || deviceEditor.deviceID.$pristine">The device ID to enter here can be found in the "Actions > Show ID" dialog on the other device. Spaces and dashes are optional (ignored).</span>
|
||||
<span translate ng-show="!editingExisting && (deviceEditor.deviceID.$valid || deviceEditor.deviceID.$pristine)">When adding a new device, keep in mind that this device must be added on the other side too.</span>
|
||||
<span translate ng-if="deviceEditor.deviceID.$error.required && deviceEditor.deviceID.$dirty">The device ID cannot be blank.</span>
|
||||
<span translate ng-if="deviceEditor.deviceID.$error.validDeviceid && deviceEditor.deviceID.$dirty">The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.</span>
|
||||
<span translate ng-if="deviceEditor.deviceID.$error.unique && deviceEditor.deviceID.$dirty">A device with that ID is already added.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form" name="deviceEditor">
|
||||
<div class="form-group" ng-class="{'has-error': deviceEditor.deviceID.$invalid && deviceEditor.deviceID.$dirty}">
|
||||
<label translate for="deviceID">Device ID</label>
|
||||
<input ng-if="!editingExisting" name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.deviceID" required valid-deviceid list="discovery-list" />
|
||||
<datalist id="discovery-list" ng-if="!editingExisting">
|
||||
<option ng-repeat="(id, data) in discovery" value="{{id}}" />
|
||||
</datalist>
|
||||
<div ng-if="editingExisting" class="well well-sm text-monospace" select-on-click>{{currentDevice.deviceID}}</div>
|
||||
<p class="help-block">
|
||||
<span translate ng-if="deviceEditor.deviceID.$valid || deviceEditor.deviceID.$pristine">The device ID to enter here can be found in the "Actions > Show ID" dialog on the other device. Spaces and dashes are optional (ignored).</span>
|
||||
<span translate ng-show="!editingExisting && (deviceEditor.deviceID.$valid || deviceEditor.deviceID.$pristine)">When adding a new device, keep in mind that this device must be added on the other side too.</span>
|
||||
<span translate ng-if="deviceEditor.deviceID.$error.required && deviceEditor.deviceID.$dirty">The device ID cannot be blank.</span>
|
||||
<span translate ng-if="deviceEditor.deviceID.$error.validDeviceid && deviceEditor.deviceID.$dirty">The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.</span>
|
||||
<span translate ng-if="deviceEditor.deviceID.$error.unique && deviceEditor.deviceID.$dirty">A device with that ID is already added.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate for="name">Device Name</label>
|
||||
<input id="name" class="form-control" type="text" ng-model="currentDevice.name"></input>
|
||||
<p translate ng-if="currentDevice.deviceID == myID" class="help-block">Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.</p>
|
||||
<p translate ng-if="currentDevice.deviceID != myID" class="help-block">Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate for="addresses">Addresses</label>
|
||||
<input ng-disabled="currentDevice.deviceID == myID" id="addresses" class="form-control" type="text" ng-model="currentDevice._addressesStr"></input>
|
||||
<p translate class="help-block">Enter comma separated ("tcp://ip:port", "tcp://host:port") addresses or "dynamic" to perform automatic discovery of the address.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate>Compression</label>
|
||||
<select class="form-control" ng-model="currentDevice.compression">
|
||||
<option value="always" translate>All Data</option>
|
||||
<option value="metadata" translate>Metadata Only</option>
|
||||
<option value="never" translate>Off</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="currentDevice.introducer"> <span translate>Introducer</span>
|
||||
</label>
|
||||
<p translate class="help-block">Any devices configured on an introducer device will be added to this device as well.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label translate for="name">Device Name</label>
|
||||
<input id="name" class="form-control" type="text" ng-model="currentDevice.name"></input>
|
||||
<p translate ng-if="currentDevice.deviceID == myID" class="help-block">Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.</p>
|
||||
<p translate ng-if="currentDevice.deviceID != myID" class="help-block">Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate for="addresses">Addresses</label>
|
||||
<input ng-disabled="currentDevice.deviceID == myID" id="addresses" class="form-control" type="text" ng-model="currentDevice._addressesStr"></input>
|
||||
<p translate class="help-block">Enter comma separated ("tcp://ip:port", "tcp://host:port") addresses or "dynamic" to perform automatic discovery of the address.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate>Compression</label>
|
||||
<select class="form-control" ng-model="currentDevice.compression">
|
||||
<option value="always" translate>All Data</option>
|
||||
<option value="metadata" translate>Metadata Only</option>
|
||||
<option value="never" translate>Off</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="currentDevice.introducer"> <span translate>Introducer</span>
|
||||
</label>
|
||||
<p translate class="help-block">Any devices configured on an introducer device will be added to this device as well.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label translate for="folders">Share Folders With Device</label>
|
||||
<p translate class="help-block">Select the folders to share with this device.</p>
|
||||
<div class="row">
|
||||
<div class="col-md-4" ng-repeat="folder in folderList()">
|
||||
<div class="checkbox">
|
||||
<label ng-if="folder.label.length == 0">
|
||||
<input type="checkbox" ng-model="currentDevice.selectedFolders[folder.id]"> {{folder.id}}
|
||||
</label>
|
||||
<label ng-if="folder.label.length != 0">
|
||||
<input type="checkbox" ng-model="currentDevice.selectedFolders[folder.id]"> <span title="{{folder.id}}">{{folder.label}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<label translate for="folders">Share Folders With Device</label>
|
||||
<p translate class="help-block">Select the folders to share with this device.</p>
|
||||
<div class="row">
|
||||
<div class="col-md-4" ng-repeat="folder in folderList()">
|
||||
<div class="checkbox">
|
||||
<label ng-if="folder.label.length == 0">
|
||||
<input type="checkbox" ng-model="currentDevice.selectedFolders[folder.id]"> {{folder.id}}
|
||||
</label>
|
||||
<label ng-if="folder.label.length != 0">
|
||||
<input type="checkbox" ng-model="currentDevice.selectedFolders[folder.id]"> <span tooltip data-original-title="{{folder.id}}">{{folder.label}}</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="saveDevice()" ng-disabled="deviceEditor.$invalid">
|
||||
<span class="fa fa-check"></span> <span translate>Save</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" ng-click="idDevice(currentDevice)" ng-if="editingExisting || deviceEditor.deviceID.$valid">
|
||||
<span class="fa fa-qrcode"></span> <span translate>Show QR</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning pull-left btn-sm" ng-click="deleteDevice()" ng-if="editingExisting">
|
||||
<span class="fa fa-minus-circle"></span> <span translate>Remove</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="saveDevice()" ng-disabled="deviceEditor.$invalid">
|
||||
<span class="fa fa-check"></span> <span translate>Save</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-toggle="modal" data-target="#idqr" ng-if="editingExisting || deviceEditor.deviceID.$valid">
|
||||
<span class="fa fa-qrcode"></span> <span translate>Show QR</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning pull-left btn-sm" ng-click="deleteDevice()" ng-if="editingExisting">
|
||||
<span class="fa fa-minus-circle"></span> <span translate>Remove</span>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.device')
|
||||
.directive('idqrModal', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/device/idqrModalView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,4 +1,11 @@
|
||||
<modal id="idqr" status="info" icon="qrcode" title="{{'Device Identification' | translate}} - {{deviceName(currentDevice)}}" large="yes" close="yes">
|
||||
<div class="well well-sm text-monospace text-center" select-on-click>{{currentDevice.deviceID}}</div>
|
||||
<img ng-if="currentDevice.deviceID" class="center-block img-thumbnail" ng-src="qr/?text={{currentDevice.deviceID}}"/>
|
||||
<modal id="idqr" status="info" icon="qrcode" heading="{{'Device Identification' | translate}} - {{deviceName(currentDevice)}}" large="yes" closeable="yes">
|
||||
<div class="modal-body">
|
||||
<div class="well well-sm text-monospace text-center" select-on-click>{{currentDevice.deviceID}}</div>
|
||||
<img ng-if="currentDevice.deviceID" class="center-block img-thumbnail" ng-src="qr/?text={{currentDevice.deviceID}}"/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
angular.module('syncthing.device', []);
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.folder')
|
||||
.directive('editFolderModal', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/folder/editFolderModalView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,214 +1,194 @@
|
||||
<div id="editFolder" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
<span ng-show="!editingExisting">
|
||||
<div class="panel-icon">
|
||||
<span class="fa fa-folder"></span>
|
||||
</div>
|
||||
<span translate>Add Folder</span>
|
||||
</span>
|
||||
<span ng-show="editingExisting">
|
||||
<div class="panel-icon">
|
||||
<span class="fa fa-pencil"></span>
|
||||
</div>
|
||||
<span translate>Edit Folder</span>
|
||||
</span>
|
||||
</h4>
|
||||
<modal id="editFolder" status="default" icon="{{editingExisting ? 'pencil' : 'folder'}}" heading="{{editingExisting ? 'Edit Folder' : 'Add Folder' | translate}}" large="yes" closeable="yes">
|
||||
<div class="modal-body">
|
||||
<form role="form" name="folderEditor">
|
||||
<div class="row" ng-init="loadFormIntoScope(folderEditor)">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group" ng-class="{'has-error': folderEditor.folderLabel.$invalid && folderEditor.folderLabel.$dirty}">
|
||||
<label for="folderLabel"><span translate>Folder Label</span></label>
|
||||
<input name="folderLabel" id="folderLabel" class="form-control" type="text" ng-model="currentFolder.label" value="{{currentFolder.label}}"/>
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.folderLabel.$valid || folderEditor.folderLabel.$pristine">Optional descriptive label for the folder. Can be different on each device.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': folderEditor.folderID.$invalid && folderEditor.folderID.$dirty}">
|
||||
<label for="folderID"><span translate>Folder ID</span></label>
|
||||
<input name="folderID" ng-readonly="editingExisting || (!editingExisting && currentFolder.viewFlags.importFromOtherDevice)" id="folderID" class="form-control" type="text" ng-model="currentFolder.id" required unique-folder value="{{currentFolder.id}}"/>
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.folderID.$valid || folderEditor.folderID.$pristine">Required identifier for the folder. Must be the same on all cluster devices.</span>
|
||||
<span translate ng-if="folderEditor.folderID.$error.uniqueFolder">The folder ID must be unique.</span>
|
||||
<span translate ng-if="folderEditor.folderID.$error.required && folderEditor.folderID.$dirty">The folder ID cannot be blank.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': folderEditor.folderPath.$invalid && folderEditor.folderPath.$dirty}">
|
||||
<label translate for="folderPath">Folder Path</label>
|
||||
<input name="folderPath" ng-readonly="editingExisting" id="folderPath" class="form-control" type="text" ng-model="currentFolder.path" list="directory-list" required path-is-sub-dir/>
|
||||
<datalist id="directory-list">
|
||||
<option ng-repeat="directory in directoryList" value="{{ directory }}" />
|
||||
</datalist>
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.folderPath.$valid || folderEditor.folderPath.$pristine">Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for</span> <code>{{system.tilde}}</code>.
|
||||
<span translate ng-if="folderEditor.folderPath.$error.required && folderEditor.folderPath.$dirty">The folder path cannot be blank.</span>
|
||||
<span class="text-danger" translate translate-value-other-folder="{{otherFolder}}" ng-if="pathIsSubFolder">Warning, this path is a subdirectory of an existing folder "{%otherFolder%}".</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form" name="folderEditor">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group" ng-class="{'has-error': folderEditor.folderLabel.$invalid && folderEditor.folderLabel.$dirty}">
|
||||
<label for="folderLabel"><span translate>Folder Label</span></label>
|
||||
<input name="folderLabel" id="folderLabel" class="form-control" type="text" ng-model="currentFolder.label" value="{{currentFolder.label}}"/>
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.folderLabel.$valid || folderEditor.folderLabel.$pristine">Optional descriptive label for the folder. Can be different on each device.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': folderEditor.folderID.$invalid && folderEditor.folderID.$dirty}">
|
||||
<label for="folderID"><span translate>Folder ID</span></label>
|
||||
<input name="folderID" ng-readonly="editingExisting || (!editingExisting && currentFolder.viewFlags.importFromOtherDevice)" id="folderID" class="form-control" type="text" ng-model="currentFolder.id" required unique-folder value="{{currentFolder.id}}"/>
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.folderID.$valid || folderEditor.folderID.$pristine">Required identifier for the folder. Must be the same on all cluster devices.</span>
|
||||
<span translate ng-if="folderEditor.folderID.$error.uniqueFolder">The folder ID must be unique.</span>
|
||||
<span translate ng-if="folderEditor.folderID.$error.required && folderEditor.folderID.$dirty">The folder ID cannot be blank.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': folderEditor.folderPath.$invalid && folderEditor.folderPath.$dirty}">
|
||||
<label translate for="folderPath">Folder Path</label>
|
||||
<input name="folderPath" ng-readonly="editingExisting" id="folderPath" class="form-control" type="text" ng-model="currentFolder.path" list="directory-list" required path-is-sub-dir/>
|
||||
<datalist id="directory-list">
|
||||
<option ng-repeat="directory in directoryList" value="{{ directory }}" />
|
||||
</datalist>
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.folderPath.$valid || folderEditor.folderPath.$pristine">Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for</span> <code>{{system.tilde}}</code>.
|
||||
<span translate ng-if="folderEditor.folderPath.$error.required && folderEditor.folderPath.$dirty">The folder path cannot be blank.</span>
|
||||
<span class="text-danger" translate translate-value-other-folder="{{otherFolder}}" ng-if="pathIsSubFolder">Warning, this path is a subdirectory of an existing folder "{%otherFolder%}".</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label translate for="devices">Share With Devices</label>
|
||||
<p translate class="help-block">Select the devices to share this folder with.</p>
|
||||
<div class="row">
|
||||
<div class="col-md-4" ng-repeat="device in otherDevices()">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="currentFolder.selectedDevices[device.deviceID]"> {{deviceName(device)}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div translate ng-show="!editingExisting" class="help-block">When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label data-target="#folder-advanced" class="folder-advanced-toggle collapsed" data-toggle="collapse">
|
||||
<span translate>Advanced settings</span>
|
||||
<i class="expand fa fa-plus-square"></i>
|
||||
<i class="collapse fa fa-minus-square"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="folder-advanced" class="folder-advanced collapse">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group">
|
||||
<label translate for="devices">Share With Devices</label>
|
||||
<p translate class="help-block">Select the devices to share this folder with.</p>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group" ng-class="{'has-error': folderEditor.rescanIntervalS.$invalid && folderEditor.rescanIntervalS.$dirty}">
|
||||
<label for="rescanIntervalS"><span translate>Rescan Interval</span> (s)</label>
|
||||
<input name="rescanIntervalS" id="rescanIntervalS" class="form-control" type="number" ng-model="currentFolder.rescanIntervalS" required min="0">
|
||||
<p class="help-block">
|
||||
<span translate ng-if="!folderEditor.rescanIntervalS.$valid && folderEditor.rescanIntervalS.$dirty">The rescan interval must be a non-negative number of seconds.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': folderEditor.minDiskFreePct.$invalid && folderEditor.minDiskFreePct.$dirty}">
|
||||
<label for="minDiskFreePct"><span translate>Minimum Free Disk Space</span> (0.0 - 100.0%)</label>
|
||||
<input name="minDiskFreePct" id="minDiskFreePct" class="form-control" type="number" ng-model="currentFolder.minDiskFreePct" required min="0.0" max="100.0">
|
||||
<p class="help-block">
|
||||
<span translate ng-if="!folderEditor.minDiskFreePct.$valid && folderEditor.minDiskFreePct.$dirty">The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).</span>
|
||||
</p>
|
||||
<div class="col-md-4" ng-repeat="device in otherDevices()">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="currentFolder.selectedDevices[device.deviceID]"> {{deviceName(device)}}
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- Left column -->
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label translate>Folder Type</label>
|
||||
<a href="http://docs.syncthing.net/users/foldermaster.html" target="_blank"><span class="fa fa-book"></span> <span translate>Help</span></a>
|
||||
<select class="form-control" ng-model="currentFolder.type">
|
||||
<option value="readwrite" translate>Normal</option>
|
||||
<option value="readonly" translate>Master</option>
|
||||
</select>
|
||||
<p ng-if="currentFolder.type == 'readonly'" translate class="help-block">Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="currentFolder.ignorePerms"> <span translate>Ignore Permissions</span>
|
||||
</label>
|
||||
</div>
|
||||
<p translate class="help-block">File permission bits are ignored when looking for changes. Use on FAT file systems.</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div translate ng-show="!editingExisting" class="help-block">When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.</div>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<label data-target="#folder-advanced" class="folder-advanced-toggle collapsed" data-toggle="collapse">
|
||||
<span translate>Advanced settings</span>
|
||||
<i class="expand fa fa-plus-square"></i>
|
||||
<i class="collapse fa fa-minus-square"></i>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div id="folder-advanced" class="folder-advanced collapse">
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group" ng-class="{'has-error': folderEditor.rescanIntervalS.$invalid && folderEditor.rescanIntervalS.$dirty}">
|
||||
<label for="rescanIntervalS"><span translate>Rescan Interval</span> (s)</label>
|
||||
<input name="rescanIntervalS" id="rescanIntervalS" class="form-control" type="number" ng-model="currentFolder.rescanIntervalS" required min="0">
|
||||
<p class="help-block">
|
||||
<span translate ng-if="!folderEditor.rescanIntervalS.$valid && folderEditor.rescanIntervalS.$dirty">The rescan interval must be a non-negative number of seconds.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': folderEditor.minDiskFreePct.$invalid && folderEditor.minDiskFreePct.$dirty}">
|
||||
<label for="minDiskFreePct"><span translate>Minimum Free Disk Space</span> (0.0 - 100.0%)</label>
|
||||
<input name="minDiskFreePct" id="minDiskFreePct" class="form-control" type="number" ng-model="currentFolder.minDiskFreePct" required min="0.0" max="100.0">
|
||||
<p class="help-block">
|
||||
<span translate ng-if="!folderEditor.minDiskFreePct.$valid && folderEditor.minDiskFreePct.$dirty">The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="row">
|
||||
<!-- Left column -->
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label translate>Folder Type</label>
|
||||
<a href="http://docs.syncthing.net/users/foldermaster.html" target="_blank"><span class="fa fa-book"></span> <span translate>Help</span></a>
|
||||
<select class="form-control" ng-model="currentFolder.type">
|
||||
<option value="readwrite" translate>Normal</option>
|
||||
<option value="readonly" translate>Master</option>
|
||||
</select>
|
||||
<p ng-if="currentFolder.type == 'readonly'" translate class="help-block">Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="currentFolder.ignorePerms"> <span translate>Ignore Permissions</span>
|
||||
</label>
|
||||
</div>
|
||||
<p translate class="help-block">File permission bits are ignored when looking for changes. Use on FAT file systems.</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Right column-->
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label translate>File Pull Order</label>
|
||||
<select class="form-control" ng-model="currentFolder.order">
|
||||
<option value="random" translate>Random</option>
|
||||
<option value="alphabetic" translate>Alphabetic</option>
|
||||
<option value="smallestFirst" translate>Smallest First</option>
|
||||
<option value="largestFirst" translate>Largest First</option>
|
||||
<option value="oldestFirst" translate>Oldest First</option>
|
||||
<option value="newestFirst" translate>Newest First</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate>File Versioning</label> <a href="http://docs.syncthing.net/users/versioning.html" target="_blank"><span class="fa fa-book"></span> <span translate>Help</span></a>
|
||||
<select class="form-control" ng-model="currentFolder.fileVersioningSelector">
|
||||
<option value="none" translate>No File Versioning</option>
|
||||
<option value="trashcan" translate>Trash Can File Versioning</option>
|
||||
<option value="simple" translate>Simple File Versioning</option>
|
||||
<option value="staggered" translate>Staggered File Versioning</option>
|
||||
<option value="external" translate>External File Versioning</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" ng-if="currentFolder.fileVersioningSelector=='trashcan'" ng-class="{'has-error': folderEditor.trashcanClean.$invalid && folderEditor.trashcanClean.$dirty}">
|
||||
<p translate class="help-block">Files are moved to .stversions folder when replaced or deleted by Syncthing.</p>
|
||||
<label translate for="trashcanClean">Clean out after</label>
|
||||
<div class="input-group">
|
||||
<input name="trashcanClean" id="trashcanClean" class="form-control text-right" type="number" ng-model="currentFolder.trashcanClean" required min="0">
|
||||
<div class="input-group-addon" translate>days</div>
|
||||
</div>
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.trashcanClean.$valid || folderEditor.trashcanClean.$pristine">The number of days to keep files in the trash can. Zero means forever.</span>
|
||||
<span translate ng-if="folderEditor.trashcanClean.$error.required && folderEditor.trashcanClean.$dirty">The number of days must be a number and cannot be blank.</span>
|
||||
<span translate ng-if="folderEditor.trashcanClean.$error.min && folderEditor.trashcanClean.$dirty">A negative number of days doesn't make sense.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-if="currentFolder.fileVersioningSelector=='simple'" ng-class="{'has-error': folderEditor.simpleKeep.$invalid && folderEditor.simpleKeep.$dirty}">
|
||||
<p translate class="help-block">Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.</p>
|
||||
<label translate for="simpleKeep">Keep Versions</label>
|
||||
<input name="simpleKeep" id="simpleKeep" class="form-control" type="number" ng-model="currentFolder.simpleKeep" required min="1">
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.simpleKeep.$valid || folderEditor.simpleKeep.$pristine">The number of old versions to keep, per file.</span>
|
||||
<span translate ng-if="folderEditor.simpleKeep.$error.required && folderEditor.simpleKeep.$dirty">The number of versions must be a number and cannot be blank.</span>
|
||||
<span translate ng-if="folderEditor.simpleKeep.$error.min && folderEditor.simpleKeep.$dirty">You must keep at least one version.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-if="currentFolder.fileVersioningSelector=='staggered'" ng-class="{'has-error': folderEditor.staggeredMaxAge.$invalid && folderEditor.staggeredMaxAge.$dirty}">
|
||||
<p class="help-block"><span translate>Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.</span> <span translate>Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.</span></p>
|
||||
<p translate class="help-block">The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.</p>
|
||||
<label translate for="staggeredMaxAge">Maximum Age</label>
|
||||
<input name="staggeredMaxAge" id="staggeredMaxAge" class="form-control" type="number" ng-model="currentFolder.staggeredMaxAge" required min="0">
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.staggeredMaxAge.$valid || folderEditor.staggeredMaxAge.$pristine">The maximum time to keep a version (in days, set to 0 to keep versions forever).</span>
|
||||
<span translate ng-if="folderEditor.staggeredMaxAge.$error.required && folderEditor.staggeredMaxAge.$dirty">The maximum age must be a number and cannot be blank.</span>
|
||||
<span translate ng-if="folderEditor.staggeredMaxAge.$error.min && folderEditor.staggeredMaxAge.$dirty">A negative number of days doesn't make sense.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-if="currentFolder.fileVersioningSelector == 'staggered'">
|
||||
<label translate for="staggeredVersionsPath">Versions Path</label>
|
||||
<input name="staggeredVersionsPath" id="staggeredVersionsPath" class="form-control" type="text" ng-model="currentFolder.staggeredVersionsPath">
|
||||
<p translate class="help-block">Path where versions should be stored (leave empty for the default .stversions folder in the folder).</p>
|
||||
</div>
|
||||
<div class="form-group" ng-if="currentFolder.fileVersioningSelector=='external'" ng-class="{'has-error': folderEditor.externalCommand.$invalid && folderEditor.externalCommand.$dirty}">
|
||||
<p translate class="help-block">An external command handles the versioning. It has to remove the file from the synced folder.</p>
|
||||
<label translate for="externalCommand">Command</label>
|
||||
<input name="externalCommand" id="externalCommand" class="form-control" type="text" ng-model="currentFolder.externalCommand" required>
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.externalCommand.$valid || folderEditor.externalCommand.$pristine">The first command line parameter is the folder path and the second parameter is the relative path in the folder.</span>
|
||||
<span translate ng-if="folderEditor.externalCommand.$error.required && folderEditor.externalCommand.$dirty">The path cannot be blank.</span>
|
||||
</p>
|
||||
</div>
|
||||
<!-- Right column-->
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label translate>File Pull Order</label>
|
||||
<select class="form-control" ng-model="currentFolder.order">
|
||||
<option value="random" translate>Random</option>
|
||||
<option value="alphabetic" translate>Alphabetic</option>
|
||||
<option value="smallestFirst" translate>Smallest First</option>
|
||||
<option value="largestFirst" translate>Largest First</option>
|
||||
<option value="oldestFirst" translate>Oldest First</option>
|
||||
<option value="newestFirst" translate>Newest First</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate>File Versioning</label> <a href="http://docs.syncthing.net/users/versioning.html" target="_blank"><span class="fa fa-book"></span> <span translate>Help</span></a>
|
||||
<select class="form-control" ng-model="currentFolder.fileVersioningSelector">
|
||||
<option value="none" translate>No File Versioning</option>
|
||||
<option value="trashcan" translate>Trash Can File Versioning</option>
|
||||
<option value="simple" translate>Simple File Versioning</option>
|
||||
<option value="staggered" translate>Staggered File Versioning</option>
|
||||
<option value="external" translate>External File Versioning</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" ng-if="currentFolder.fileVersioningSelector=='trashcan'" ng-class="{'has-error': folderEditor.trashcanClean.$invalid && folderEditor.trashcanClean.$dirty}">
|
||||
<p translate class="help-block">Files are moved to .stversions folder when replaced or deleted by Syncthing.</p>
|
||||
<label translate for="trashcanClean">Clean out after</label>
|
||||
<div class="input-group">
|
||||
<input name="trashcanClean" id="trashcanClean" class="form-control text-right" type="number" ng-model="currentFolder.trashcanClean" required min="0">
|
||||
<div class="input-group-addon" translate>days</div>
|
||||
</div>
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.trashcanClean.$valid || folderEditor.trashcanClean.$pristine">The number of days to keep files in the trash can. Zero means forever.</span>
|
||||
<span translate ng-if="folderEditor.trashcanClean.$error.required && folderEditor.trashcanClean.$dirty">The number of days must be a number and cannot be blank.</span>
|
||||
<span translate ng-if="folderEditor.trashcanClean.$error.min && folderEditor.trashcanClean.$dirty">A negative number of days doesn't make sense.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-if="currentFolder.fileVersioningSelector=='simple'" ng-class="{'has-error': folderEditor.simpleKeep.$invalid && folderEditor.simpleKeep.$dirty}">
|
||||
<p translate class="help-block">Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.</p>
|
||||
<label translate for="simpleKeep">Keep Versions</label>
|
||||
<input name="simpleKeep" id="simpleKeep" class="form-control" type="number" ng-model="currentFolder.simpleKeep" required min="1">
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.simpleKeep.$valid || folderEditor.simpleKeep.$pristine">The number of old versions to keep, per file.</span>
|
||||
<span translate ng-if="folderEditor.simpleKeep.$error.required && folderEditor.simpleKeep.$dirty">The number of versions must be a number and cannot be blank.</span>
|
||||
<span translate ng-if="folderEditor.simpleKeep.$error.min && folderEditor.simpleKeep.$dirty">You must keep at least one version.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-if="currentFolder.fileVersioningSelector=='staggered'" ng-class="{'has-error': folderEditor.staggeredMaxAge.$invalid && folderEditor.staggeredMaxAge.$dirty}">
|
||||
<p class="help-block"><span translate>Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.</span> <span translate>Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.</span></p>
|
||||
<p translate class="help-block">The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.</p>
|
||||
<label translate for="staggeredMaxAge">Maximum Age</label>
|
||||
<input name="staggeredMaxAge" id="staggeredMaxAge" class="form-control" type="number" ng-model="currentFolder.staggeredMaxAge" required min="0">
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.staggeredMaxAge.$valid || folderEditor.staggeredMaxAge.$pristine">The maximum time to keep a version (in days, set to 0 to keep versions forever).</span>
|
||||
<span translate ng-if="folderEditor.staggeredMaxAge.$error.required && folderEditor.staggeredMaxAge.$dirty">The maximum age must be a number and cannot be blank.</span>
|
||||
<span translate ng-if="folderEditor.staggeredMaxAge.$error.min && folderEditor.staggeredMaxAge.$dirty">A negative number of days doesn't make sense.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-if="currentFolder.fileVersioningSelector == 'staggered'">
|
||||
<label translate for="staggeredVersionsPath">Versions Path</label>
|
||||
<input name="staggeredVersionsPath" id="staggeredVersionsPath" class="form-control" type="text" ng-model="currentFolder.staggeredVersionsPath">
|
||||
<p translate class="help-block">Path where versions should be stored (leave empty for the default .stversions folder in the folder).</p>
|
||||
</div>
|
||||
<div class="form-group" ng-if="currentFolder.fileVersioningSelector=='external'" ng-class="{'has-error': folderEditor.externalCommand.$invalid && folderEditor.externalCommand.$dirty}">
|
||||
<p translate class="help-block">An external command handles the versioning. It has to remove the file from the synced folder.</p>
|
||||
<label translate for="externalCommand">Command</label>
|
||||
<input name="externalCommand" id="externalCommand" class="form-control" type="text" ng-model="currentFolder.externalCommand" required>
|
||||
<p class="help-block">
|
||||
<span translate ng-if="folderEditor.externalCommand.$valid || folderEditor.externalCommand.$pristine">The first command line parameter is the folder path and the second parameter is the relative path in the folder.</span>
|
||||
<span translate ng-if="folderEditor.externalCommand.$error.required && folderEditor.externalCommand.$dirty">The path cannot be blank.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="saveFolder()" ng-disabled="folderEditor.$invalid">
|
||||
<span class="fa fa-check"></span> <span translate>Save</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning pull-left btn-sm" ng-click="deleteFolder(currentFolder.id)" ng-if="editingExisting">
|
||||
<span class="fa fa-minus-circle"></span> <span translate>Remove</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default pull-left btn-sm" id="editIgnoresButton" ng-click="editIgnores()" ng-if="editingExisting">
|
||||
<span class="fa fa-eye-slash"></span> <span translate>Ignore Patterns</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="saveFolder()" ng-disabled="folderEditor.$invalid">
|
||||
<span class="fa fa-check"></span> <span translate>Save</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning pull-left btn-sm" ng-click="deleteFolder(currentFolder.id)" ng-if="editingExisting">
|
||||
<span class="fa fa-minus-circle"></span> <span translate>Remove</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default pull-left btn-sm" id="editIgnoresButton" ng-click="editIgnores()" ng-if="editingExisting">
|
||||
<span class="fa fa-eye-slash"></span> <span translate>Ignore Patterns</span>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.folder')
|
||||
.directive('editIgnoresModal', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/folder/editIgnoresModalView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,32 +1,25 @@
|
||||
<div id="editIgnores" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title" translate>Ignore Patterns</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p translate>Enter ignore patterns, one per line.</p>
|
||||
<textarea class="form-control" rows="15"></textarea>
|
||||
<modal id="editIgnores" status="default" heading="{{'Ignore Patterns' | translate}}" large="yes" closeable="yes">
|
||||
<div class="modal-body">
|
||||
<p translate>Enter ignore patterns, one per line.</p>
|
||||
<textarea class="form-control" rows="15"></textarea>
|
||||
|
||||
<hr/>
|
||||
<hr/>
|
||||
|
||||
<p class="small"><span translate>Quick guide to supported patterns</span> (<a href="http://docs.syncthing.net/users/ignoring.html" target="_blank" translate>full documentation</a>):</p>
|
||||
<dl class="dl-horizontal dl-narrow small">
|
||||
<dt><code>!</code></dt> <dd><span translate>Inversion of the given condition (i.e. do not exclude)</span></dd>
|
||||
<dt><code>*</code></dt> <dd><span translate>Single level wildcard (matches within a directory only)</span></dd>
|
||||
<dt><code>**</code></dt> <dd><span translate>Multi level wildcard (matches multiple directory levels)</span></dd>
|
||||
<dt><code>//</code></dt> <dd><span translate>Comment, when used at the start of a line</span></dd>
|
||||
</dl>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="pull-left"><span translate>Editing</span> <code>{{currentFolder.path}}{{system.pathSeparator}}.stignore</code></div>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="saveIgnores()" data-dismiss="modal">
|
||||
<span class="fa fa-check"></span> <span translate>Save</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
<p class="small"><span translate>Quick guide to supported patterns</span> (<a href="http://docs.syncthing.net/users/ignoring.html" target="_blank" translate>full documentation</a>):</p>
|
||||
<dl class="dl-horizontal dl-narrow small">
|
||||
<dt><code>!</code></dt> <dd><span translate>Inversion of the given condition (i.e. do not exclude)</span></dd>
|
||||
<dt><code>*</code></dt> <dd><span translate>Single level wildcard (matches within a directory only)</span></dd>
|
||||
<dt><code>**</code></dt> <dd><span translate>Multi level wildcard (matches multiple directory levels)</span></dd>
|
||||
<dt><code>//</code></dt> <dd><span translate>Comment, when used at the start of a line</span></dd>
|
||||
</dl>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<div class="pull-left"><span translate>Editing</span> <code>{{currentFolder.path}}{{system.pathSeparator}}.stignore</code></div>
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="saveIgnores()" data-dismiss="modal">
|
||||
<span class="fa fa-check"></span> <span translate>Save</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
@@ -1 +0,0 @@
|
||||
angular.module('syncthing.folder', []);
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.settings')
|
||||
.directive('advancedSettingsModal', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/settings/advancedSettingsModalView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,92 +1,82 @@
|
||||
<div id="advanced" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header alert alert-danger">
|
||||
<h4 class="modal-title">
|
||||
<div class="panel-icon">
|
||||
<span class="fa fa-cog"></span>
|
||||
</div>
|
||||
<span translate>Advanced Configuration</span>
|
||||
<modal id="advanced" status="danger" icon="cog" heading="{{'Advanced Configuration' | translate}}" large="yes" closeable="yes">
|
||||
<div class="modal-body">
|
||||
|
||||
<p class="text-danger">
|
||||
<b translate>Be careful!</b>
|
||||
<span translate>Incorrect configuration may damage your folder contents and render Syncthing inoperable.</span>
|
||||
</p>
|
||||
|
||||
<div class="panel-group" id="advancedAccordion" role="tablist" aria-multiselectable="true">
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="guiHeading" data-toggle="collapse" data-parent="#advancedAccordion" href="#guiConfig" aria-expanded="true" aria-controls="guiConfig" style="cursor: pointer">
|
||||
<h4 class="panel-title" translate>GUI</h4>
|
||||
</div>
|
||||
<div id="guiConfig" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="guiHeading">
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div ng-repeat="(key, value) in advancedConfig.gui" ng-init="type = inputTypeFor(key, value)" ng-if="type != 'skip'" class="form-group">
|
||||
<label for="guiInput{{$index}}" class="col-sm-4 control-label">{{key}}</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="guiInput{{$index}}" class="form-control" type="{{type}}" ng-model="advancedConfig.gui[key]" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="optionsHeading" data-toggle="collapse" data-parent="#advancedAccordion" href="#optionsConfig" aria-expanded="true" aria-controls="optionsConfig" style="cursor: pointer">
|
||||
<h4 class="panel-title" translate>Options</h4>
|
||||
</div>
|
||||
<div id="optionsConfig" class="panel-collapse collapse" role="tabpanel" aria-labelledby="optionsHeading">
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div ng-repeat="(key, value) in advancedConfig.options" ng-if="inputTypeFor(key, value) != 'skip'" class="form-group">
|
||||
<label for="optionsInput{{$index}}" class="col-sm-4 control-label">{{key}}</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="optionsInput{{$index}}" class="form-control" type="{{inputTypeFor(key, value)}}" ng-model="advancedConfig.options[key]" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default" ng-repeat="folder in advancedConfig.folders">
|
||||
<div class="panel-heading" role="tab" id="folder{{$index}}Heading" data-toggle="collapse" data-parent="#advancedAccordion" href="#folder{{$index}}Config" aria-expanded="true" aria-controls="folder{{$index}}Config" style="cursor: pointer">
|
||||
<h4 ng-if="folder.label.length == 0" class="panel-title">
|
||||
<span translate>Folder</span> "{{folder.id}}"
|
||||
</h4>
|
||||
<h4 ng-if="folder.label.length != 0" class="panel-title">
|
||||
<span translate>Folder</span> "{{folder.label}}" ({{folder.id}})
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
|
||||
<p class="text-danger">
|
||||
<b translate>Be careful!</b>
|
||||
<span translate>Incorrect configuration may damage your folder contents and render Syncthing inoperable.</span>
|
||||
</p>
|
||||
|
||||
<div class="panel-group" id="advancedAccordion" role="tablist" aria-multiselectable="true">
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="guiHeading" data-toggle="collapse" data-parent="#advancedAccordion" href="#guiConfig" aria-expanded="true" aria-controls="guiConfig" style="cursor: pointer">
|
||||
<h4 class="panel-title" translate>GUI</h4>
|
||||
</div>
|
||||
<div id="guiConfig" class="panel-collapse collapse in" role="tabpanel" aria-labelledby="guiHeading">
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div ng-repeat="(key, value) in advancedConfig.gui" ng-init="type = inputTypeFor(key, value)" ng-if="type != 'skip'" class="form-group">
|
||||
<label for="guiInput{{$index}}" class="col-sm-4 control-label">{{key}}</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="guiInput{{$index}}" class="form-control" type="{{type}}" ng-model="advancedConfig.gui[key]" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
<div id="folder{{$index}}Config" class="panel-collapse collapse" role="tabpanel" aria-labelledby="folder{{$index}}Heading">
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div ng-repeat="(key, value) in folder" ng-if="inputTypeFor(key, value) != 'skip'" class="form-group">
|
||||
<label for="folder{{$index}}Input{{$index}}" class="col-sm-4 control-label">{{key}}</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="folder{{$index}}Input{{$index}}" class="form-control" type="{{inputTypeFor(key, value)}}" ng-model="folder[key]" />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default">
|
||||
<div class="panel-heading" role="tab" id="optionsHeading" data-toggle="collapse" data-parent="#advancedAccordion" href="#optionsConfig" aria-expanded="true" aria-controls="optionsConfig" style="cursor: pointer">
|
||||
<h4 class="panel-title" translate>Options</h4>
|
||||
</div>
|
||||
<div id="optionsConfig" class="panel-collapse collapse" role="tabpanel" aria-labelledby="optionsHeading">
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div ng-repeat="(key, value) in advancedConfig.options" ng-if="inputTypeFor(key, value) != 'skip'" class="form-group">
|
||||
<label for="optionsInput{{$index}}" class="col-sm-4 control-label">{{key}}</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="optionsInput{{$index}}" class="form-control" type="{{inputTypeFor(key, value)}}" ng-model="advancedConfig.options[key]" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-default" ng-repeat="folder in advancedConfig.folders">
|
||||
<div class="panel-heading" role="tab" id="folder{{$index}}Heading" data-toggle="collapse" data-parent="#advancedAccordion" href="#folder{{$index}}Config" aria-expanded="true" aria-controls="folder{{$index}}Config" style="cursor: pointer">
|
||||
<h4 ng-if="folder.label.length == 0" class="panel-title">
|
||||
<span translate>Folder</span> "{{folder.id}}"
|
||||
</h4>
|
||||
<h4 ng-if="folder.label.length != 0" class="panel-title">
|
||||
<span translate>Folder</span> "{{folder.label}}" ({{folder.id}})
|
||||
</h4>
|
||||
</div>
|
||||
<div id="folder{{$index}}Config" class="panel-collapse collapse" role="tabpanel" aria-labelledby="folder{{$index}}Heading">
|
||||
<div class="panel-body">
|
||||
<form class="form-horizontal" role="form">
|
||||
<div ng-repeat="(key, value) in folder" ng-if="inputTypeFor(key, value) != 'skip'" class="form-group">
|
||||
<label for="folder{{$index}}Input{{$index}}" class="col-sm-4 control-label">{{key}}</label>
|
||||
<div class="col-sm-8">
|
||||
<input id="folder{{$index}}Input{{$index}}" class="form-control" type="{{inputTypeFor(key, value)}}" ng-model="folder[key]" />
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="saveAdvanced()">
|
||||
<span class="fa fa-check"></span> <span translate>Save</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="saveAdvanced()">
|
||||
<span class="fa fa-check"></span> <span translate>Save</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</modal>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
angular.module('syncthing.settings', []);
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.settings')
|
||||
.directive('settingsModal', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/settings/settingsModalView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,167 +1,157 @@
|
||||
<div id="settings" class="modal fade" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
<div class="panel-icon">
|
||||
<span class="fa fa-cog"></span>
|
||||
</div>
|
||||
<span translate>Settings</span>
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form" name="settingsEditor">
|
||||
<div class="row">
|
||||
<modal id="settings" status="default" icon="cog" heading="{{'Settings' | translate}}" large="yes" closeable="yes">
|
||||
<div class="modal-body">
|
||||
<form role="form" name="settingsEditor">
|
||||
<div class="row">
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label translate for="DeviceName">Device Name</label>
|
||||
<input id="DeviceName" class="form-control" type="text" ng-model="tmpOptions.deviceName">
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label translate for="DeviceName">Device Name</label>
|
||||
<input id="DeviceName" class="form-control" type="text" ng-model="tmpOptions.deviceName">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label translate for="ListenAddressesStr">Sync Protocol Listen Addresses</label>
|
||||
<input id="ListenAddressesStr" class="form-control" type="text" ng-model="tmpOptions._listenAddressesStr">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate for="ListenAddressesStr">Sync Protocol Listen Addresses</label>
|
||||
<input id="ListenAddressesStr" class="form-control" type="text" ng-model="tmpOptions._listenAddressesStr">
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxRecvKbps.$invalid && settingsEditor.MaxRecvKbps.$dirty}">
|
||||
<label translate for="MaxRecvKbps">Incoming Rate Limit (KiB/s)</label>
|
||||
<input id="MaxRecvKbps" name="MaxRecvKbps" class="form-control" type="number" ng-model="tmpOptions.maxRecvKbps" min="0">
|
||||
<p class="help-block">
|
||||
<span translate ng-if="settingsEditor.MaxRecvKbps.$error.min && settingsEditor.MaxRecvKbps.$dirty">The rate limit must be a non-negative number (0: no limit)</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxRecvKbps.$invalid && settingsEditor.MaxRecvKbps.$dirty}">
|
||||
<label translate for="MaxRecvKbps">Incoming Rate Limit (KiB/s)</label>
|
||||
<input id="MaxRecvKbps" name="MaxRecvKbps" class="form-control" type="number" ng-model="tmpOptions.maxRecvKbps" min="0">
|
||||
<p class="help-block">
|
||||
<span translate ng-if="settingsEditor.MaxRecvKbps.$error.min && settingsEditor.MaxRecvKbps.$dirty">The rate limit must be a non-negative number (0: no limit)</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxSendKbps.$invalid && settingsEditor.MaxSendKbps.$dirty}">
|
||||
<label translate for="MaxSendKbps">Outgoing Rate Limit (KiB/s)</label>
|
||||
<input id="MaxSendKbps" name="MaxSendKbps" class="form-control" type="number" ng-model="tmpOptions.maxSendKbps" min="0">
|
||||
<p class="help-block">
|
||||
<span translate ng-if="settingsEditor.MaxSendKbps.$error.min && settingsEditor.MaxSendKbps.$dirty">The rate limit must be a non-negative number (0: no limit)</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': settingsEditor.MaxSendKbps.$invalid && settingsEditor.MaxSendKbps.$dirty}">
|
||||
<label translate for="MaxSendKbps">Outgoing Rate Limit (KiB/s)</label>
|
||||
<input id="MaxSendKbps" name="MaxSendKbps" class="form-control" type="number" ng-model="tmpOptions.maxSendKbps" min="0">
|
||||
<p class="help-block">
|
||||
<span translate ng-if="settingsEditor.MaxSendKbps.$error.min && settingsEditor.MaxSendKbps.$dirty">The rate limit must be a non-negative number (0: no limit)</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="NATEnabled" type="checkbox" ng-model="tmpOptions.natEnabled"> <span translate>Enable NAT traversal</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="LocalAnnEnabled" type="checkbox" ng-model="tmpOptions.localAnnounceEnabled"> <span translate>Local Discovery</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="NATEnabled" type="checkbox" ng-model="tmpOptions.natEnabled"> <span translate>Enable NAT traversal</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="GlobalAnnEnabled" type="checkbox" ng-model="tmpOptions.globalAnnounceEnabled"> <span translate>Global Discovery</span>
|
||||
</label>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="LocalAnnEnabled" type="checkbox" ng-model="tmpOptions.localAnnounceEnabled"> <span translate>Local Discovery</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="RelaysEnabled" type="checkbox" ng-model="tmpOptions.relaysEnabled"> <span translate>Enable Relaying</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
<div class="form-group">
|
||||
<label translate for="GlobalAnnServersStr">Global Discovery Servers</label>
|
||||
<input ng-disabled="!tmpOptions.globalAnnounceEnabled" id="GlobalAnnServersStr" class="form-control" type="text" ng-model="tmpOptions._globalAnnounceServersStr">
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label translate for="Address">GUI Listen Addresses</label>
|
||||
<input id="Address" class="form-control" type="text" ng-model="tmpGUI.address">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate for="User">GUI Authentication User</label>
|
||||
<input id="User" class="form-control" type="text" ng-model="tmpGUI.user">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate for="Password">GUI Authentication Password</label>
|
||||
<input id="Password" class="form-control" type="password" ng-model="tmpGUI.password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="UseTLS" type="checkbox" ng-model="tmpGUI.useTLS"> <span translate>Use HTTPS for GUI</span>
|
||||
</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="GlobalAnnEnabled" type="checkbox" ng-model="tmpOptions.globalAnnounceEnabled"> <span translate>Global Discovery</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="StartBrowser" type="checkbox" ng-model="tmpOptions.startBrowser"> <span translate>Start Browser</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="UREnabled" type="checkbox" ng-model="tmpOptions.urEnabled"> <span translate>Anonymous Usage Reporting</span> (<a translate ng-click="showURPreview()" href="#">Preview</a>)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="upgradeInfo">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.autoUpgradeEnabled"> <span translate>Automatic upgrades</span>
|
||||
</label>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="RelaysEnabled" type="checkbox" ng-model="tmpOptions.relaysEnabled"> <span translate>Enable Relaying</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
<label translate>API Key</label>
|
||||
<div class="well well-sm text-monospace" select-on-click>{{tmpGUI.apiKey || "-"}}</div>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="setAPIKey(tmpGUI)">
|
||||
<span class="fa fa-repeat"></span> <span translate>Generate</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="system.themes.length > 1">
|
||||
<label>GUI Theme</label>
|
||||
<select class="form-control" ng-model="tmpGUI.theme">
|
||||
<option ng-repeat="theme in system.themes.sort()" value="{{ theme }}">
|
||||
{{ themeName(theme) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="clearfix"></div>
|
||||
<div class="form-group">
|
||||
<label translate for="GlobalAnnServersStr">Global Discovery Servers</label>
|
||||
<input ng-disabled="!tmpOptions.globalAnnounceEnabled" id="GlobalAnnServersStr" class="form-control" type="text" ng-model="tmpOptions._globalAnnounceServersStr">
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="saveSettings()">
|
||||
<span class="fa fa-check"></span> <span translate>Save</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<label translate for="Address">GUI Listen Addresses</label>
|
||||
<input id="Address" class="form-control" type="text" ng-model="tmpGUI.address">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate for="User">GUI Authentication User</label>
|
||||
<input id="User" class="form-control" type="text" ng-model="tmpGUI.user">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label translate for="Password">GUI Authentication Password</label>
|
||||
<input id="Password" class="form-control" type="password" ng-model="tmpGUI.password">
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="UseTLS" type="checkbox" ng-model="tmpGUI.useTLS"> <span translate>Use HTTPS for GUI</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="StartBrowser" type="checkbox" ng-model="tmpOptions.startBrowser"> <span translate>Start Browser</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="UREnabled" type="checkbox" ng-model="tmpOptions.urEnabled"> <span translate>Anonymous Usage Reporting</span> (<a href="" translate data-toggle="modal" data-target="#urPreview">Preview</a>)
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
<div class="form-group" ng-if="upgradeInfo">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.autoUpgradeEnabled"> <span translate>Automatic upgrades</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<hr />
|
||||
|
||||
<div class="form-group">
|
||||
<label translate>API Key</label>
|
||||
<div class="well well-sm text-monospace" select-on-click>{{tmpGUI.apiKey || "-"}}</div>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="setAPIKey(tmpGUI)">
|
||||
<span class="fa fa-repeat"></span> <span translate>Generate</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="form-group" ng-if="themes.length > 1">
|
||||
<label>GUI Theme</label>
|
||||
<select class="form-control" ng-model="tmpGUI.theme">
|
||||
<option ng-repeat="theme in themes.sort()" value="{{ theme }}">
|
||||
{{ themeName(theme) }}
|
||||
</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary btn-sm" ng-click="saveSettings()">
|
||||
<span class="fa fa-check"></span> <span translate>Save</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
</modal>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.transfer')
|
||||
.directive('failedFilesModal', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/transfer/failedFilesModalView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,19 +1,26 @@
|
||||
<modal id="failed" status="warning" icon="exclamation-circle" title="{{'Failed Items' | translate}}" large="yes" close="yes">
|
||||
<p>
|
||||
<span translate>The following items could not be synchronized.</span>
|
||||
<span translate>They are retried automatically and will be synced when the error is resolved.</span>
|
||||
</p>
|
||||
<table class="table table-striped table-condensed">
|
||||
<tr dir-paginate="e in failedCurrent | itemsPerPage: failedPageSize" current-page="failedCurrentPage" pagination-id="failed">
|
||||
<td><abbr title="{{e.path}}">{{e.path | basename}}</abbr></td>
|
||||
<td><abbr title="{{e.error}}">{{e.error | lastErrorComponent}}</abbr></td>
|
||||
</tr>
|
||||
</table>
|
||||
<dir-pagination-controls on-page-change="failedPageChanged(newPageNumber)" pagination-id="failed"></dir-pagination-controls>
|
||||
<ul class="pagination pull-right">
|
||||
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: failedPageSize == option }">
|
||||
<a href="#" ng-click="failedChangePageSize(option)">{{option}}</a>
|
||||
<li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
<modal id="failed" status="warning" icon="exclamation-circle" heading="{{'Failed Items' | translate}}" large="yes" closeable="yes">
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
<span translate>The following items could not be synchronized.</span>
|
||||
<span translate>They are retried automatically and will be synced when the error is resolved.</span>
|
||||
</p>
|
||||
<table class="table table-striped table-condensed">
|
||||
<tr dir-paginate="e in failedCurrent | itemsPerPage: failedPageSize" current-page="failedCurrentPage" pagination-id="failed">
|
||||
<td><abbr tooltip data-original-title="{{e.path}}">{{e.path | basename}}</abbr></td>
|
||||
<td><abbr tooltip data-original-title="{{e.error}}">{{e.error | lastErrorComponent}}</abbr></td>
|
||||
</tr>
|
||||
</table>
|
||||
<dir-pagination-controls on-page-change="failedPageChanged(newPageNumber)" pagination-id="failed"></dir-pagination-controls>
|
||||
<ul class="pagination pull-right">
|
||||
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: failedPageSize == option }">
|
||||
<a href="#" ng-click="failedChangePageSize(option)">{{option}}</a>
|
||||
<li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
angular.module('syncthing.transfer', []);
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.transfer')
|
||||
.directive('neededFilesModal', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/transfer/neededFilesModalView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,54 +1,65 @@
|
||||
<modal id="needed" status="info" icon="cloud-download" title="{{'Out of Sync Items' | translate}}" large="yes" close="yes" tabindex="-1">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" style="width: 20%"><span translate class="show">Reused</span></div>
|
||||
<div class="progress-bar" style="width: 20%"><span translate class="show">Copied from original</span></div>
|
||||
<div class="progress-bar progress-bar-info" style="width: 20%"><span translate class="show">Copied from elsewhere</span></div>
|
||||
<div class="progress-bar progress-bar-warning" style="width: 20%"><span translate class="show">Downloaded</span></div>
|
||||
<div class="progress-bar progress-bar-danger progress-bar-striped active" style="width: 20%"><span translate class="show">Downloading</span></div>
|
||||
<modal id="needed" status="info" icon="cloud-download" heading="{{'Out of Sync Items' | translate}}" large="yes" closeable="yes">
|
||||
|
||||
<div class="modal-body">
|
||||
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" style="width: 20%"><span translate class="show">Reused</span></div>
|
||||
<div class="progress-bar" style="width: 20%"><span translate class="show">Copied from original</span></div>
|
||||
<div class="progress-bar progress-bar-info" style="width: 20%"><span translate class="show">Copied from elsewhere</span></div>
|
||||
<div class="progress-bar progress-bar-warning" style="width: 20%"><span translate class="show">Downloaded</span></div>
|
||||
<div class="progress-bar progress-bar-danger progress-bar-striped active" style="width: 20%"><span translate class="show">Downloading</span></div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
|
||||
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="neededTotal" pagination-id="needed">
|
||||
<!-- Icon -->
|
||||
<td class="small-data"><span class="fa fa-fw fa-{{needIcons[f.action]}}"></span> {{needActions[f.action]}}</td>
|
||||
|
||||
<!-- Name -->
|
||||
<td ng-if="f.type != 'queued'" tooltip data-original-title="{{f.name}}">{{f.name | basename}}</td>
|
||||
<td ng-if="f.type == 'queued'">
|
||||
<a href="" ng-click="bumpFile(neededFolder, f.name)" tooltip data-original-title="{{'Move to top of queue' | translate}}">
|
||||
<span class="fa fa-eject"></span>
|
||||
</a>
|
||||
<span tooltip data-original-title="{{f.name}}"> {{f.name | basename}}</span>
|
||||
</td>
|
||||
|
||||
<!-- Size/Progress -->
|
||||
<td ng-if="f.type == 'progress' && f.action == 'sync' && progress[neededFolder] && progress[neededFolder][f.name]">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" style="width: {{progress[neededFolder][f.name].reused}}%"></div>
|
||||
<div class="progress-bar" style="width: {{progress[neededFolder][f.name].copiedFromOrigin}}%"></div>
|
||||
<div class="progress-bar progress-bar-info" style="width: {{progress[neededFolder][f.name].copiedFromElsewhere}}%"></div>
|
||||
<div class="progress-bar progress-bar-warning" style="width: {{progress[neededFolder][f.name].pulled}}%"></div>
|
||||
<div class="progress-bar progress-bar-danger progress-bar-striped active" style="width: {{progress[neededFolder][f.name].pulling}}%"></div>
|
||||
<span class="show frontal">
|
||||
{{progress[neededFolder][f.name].bytesDone | binary}}B / {{progress[neededFolder][f.name].bytesTotal | binary}}B
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right small-data" ng-if="f.type != 'progress' || f.action != 'sync' || !progress[neededFolder] || !progress[neededFolder][f.name]">
|
||||
<span ng-if="f.size > 0">{{f.size | binary}}B</span>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<dir-pagination-controls on-page-change="neededPageChanged(newPageNumber)" pagination-id="needed"></dir-pagination-controls>
|
||||
<ul class="pagination pull-right">
|
||||
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: neededPageSize == option }">
|
||||
<a href="#" ng-click="neededChangePageSize(option)">{{option}}</a>
|
||||
<li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
|
||||
<hr/>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<table class="table table-striped table-condensed">
|
||||
|
||||
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="neededTotal" pagination-id="needed">
|
||||
<!-- Icon -->
|
||||
<td class="small-data"><span class="fa fa-fw fa-{{needIcons[f.action]}}"></span> {{needActions[f.action]}}</td>
|
||||
|
||||
<!-- Name -->
|
||||
<td ng-if="f.type != 'queued'" title="{{f.name}}">{{f.name | basename}}</td>
|
||||
<td ng-if="f.type == 'queued'">
|
||||
<a href="" ng-click="bumpFile(neededFolder, f.name)" title="{{'Move to top of queue' | translate}}">
|
||||
<span class="fa fa-eject"></span>
|
||||
</a>
|
||||
<span title="{{f.name}}"> {{f.name | basename}}</span>
|
||||
</td>
|
||||
|
||||
<!-- Size/Progress -->
|
||||
<td ng-if="f.type == 'progress' && f.action == 'sync' && progress[neededFolder] && progress[neededFolder][f.name]">
|
||||
<div class="progress">
|
||||
<div class="progress-bar progress-bar-success" style="width: {{progress[neededFolder][f.name].reused}}%"></div>
|
||||
<div class="progress-bar" style="width: {{progress[neededFolder][f.name].copiedFromOrigin}}%"></div>
|
||||
<div class="progress-bar progress-bar-info" style="width: {{progress[neededFolder][f.name].copiedFromElsewhere}}%"></div>
|
||||
<div class="progress-bar progress-bar-warning" style="width: {{progress[neededFolder][f.name].pulled}}%"></div>
|
||||
<div class="progress-bar progress-bar-danger progress-bar-striped active" style="width: {{progress[neededFolder][f.name].pulling}}%"></div>
|
||||
<span class="show frontal">
|
||||
{{progress[neededFolder][f.name].bytesDone | binary}}B / {{progress[neededFolder][f.name].bytesTotal | binary}}B
|
||||
</span>
|
||||
</div>
|
||||
</td>
|
||||
<td class="text-right small-data" ng-if="f.type != 'progress' || f.action != 'sync' || !progress[neededFolder] || !progress[neededFolder][f.name]">
|
||||
<span ng-if="f.size > 0">{{f.size | binary}}B</span>
|
||||
</td>
|
||||
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
<dir-pagination-controls on-page-change="neededPageChanged(newPageNumber)" pagination-id="needed"></dir-pagination-controls>
|
||||
<ul class="pagination pull-right">
|
||||
<li ng-repeat="option in [10, 25, 50]" ng-class="{ active: neededPageSize == option }">
|
||||
<a href="#" ng-click="neededChangePageSize(option)">{{option}}</a>
|
||||
<li>
|
||||
</ul>
|
||||
<div class="clearfix"></div>
|
||||
</modal>
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
angular.module('syncthing.usagereport', []);
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.usagereport')
|
||||
.directive('usageReportModal', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/usagereport/usageReportModalView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,26 +1,19 @@
|
||||
<div id="ur" class="modal fade" data-backdrop="static" data-keyboard="false" tabindex="-1">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header alert alert-info">
|
||||
<h4 class="modal-title"><span class="fa fa-bar-chart"></span><span translate>Allow Anonymous Usage Reporting?</span></h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p translate>The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.</p>
|
||||
<p translate>The aggregated statistics are publicly available at the URL below.</p>
|
||||
<p><a href="https://data.syncthing.net/" target="_blank">https://data.syncthing.net/</a></p>
|
||||
<button type="button" class="btn btn-default btn-sm" ng-click="showReportPreview()" ng-show="!reportPreview">
|
||||
<span class="fa fa-file-text-o"></span> <span translate>Preview Usage Report</span>
|
||||
</button>
|
||||
<pre ng-if="reportPreview"><small>{{reportData | json}}</small></pre>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success btn-sm" ng-click="acceptUR()">
|
||||
<span class="fa fa-check"></span> <span translate>Yes</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-click="declineUR()">
|
||||
<span class="fa fa-times"></span> <span translate>No</span>
|
||||
</button>
|
||||
</div>
|
||||
<modal id="ur" status="info" icon="bar-chart" heading="{{'Allow Anonymous Usage Reporting?' | translate}}" large="yes" closeable="no">
|
||||
<div class="modal-body">
|
||||
<p translate>The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.</p>
|
||||
<p translate>The aggregated statistics are publicly available at the URL below.</p>
|
||||
<p><a href="https://data.syncthing.net/" target="_blank">https://data.syncthing.net/</a></p>
|
||||
<button type="button" class="btn btn-default btn-sm" ng-click="showReportPreview()" ng-show="!reportPreview">
|
||||
<span class="fa fa-file-text-o"></span> <span translate>Preview Usage Report</span>
|
||||
</button>
|
||||
<pre ng-if="reportPreview"><small>{{reportData | json}}</small></pre>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-success btn-sm" ng-click="acceptUR()">
|
||||
<span class="fa fa-check"></span> <span translate>Yes</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-danger btn-sm" ng-click="declineUR()">
|
||||
<span class="fa fa-times"></span> <span translate>No</span>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -1,7 +0,0 @@
|
||||
angular.module('syncthing.usagereport')
|
||||
.directive('usageReportPreviewModal', function () {
|
||||
return {
|
||||
restrict: 'A',
|
||||
templateUrl: 'syncthing/usagereport/usageReportPreviewModalView.html'
|
||||
};
|
||||
});
|
||||
@@ -1,7 +1,18 @@
|
||||
<modal id="urPreview" status="success" icon="bar-chart" title="{{'Anonymous Usage Reporting' | translate}}" large="yes" close="yes" tabindex="-1">
|
||||
<p translate>The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.</p>
|
||||
<p translate translate-value-url="<a href="https://data.syncthing.net" target="_blank">https://data.syncthing.net</a>">The aggregated statistics are publicly available at {%url%}.</p>
|
||||
<form>
|
||||
<textarea class="form-control" rows="20">{{reportData | json}}</textarea>
|
||||
</form>
|
||||
<modal id="urPreview" status="success" icon="bar-chart" heading="{{'Anonymous Usage Reporting' | translate}}" large="yes" closeable="yes">
|
||||
<div class="modal-body">
|
||||
<p translate>
|
||||
The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.
|
||||
</p>
|
||||
<p translate translate-value-url="<a href="https://data.syncthing.net" target="_blank">https://data.syncthing.net</a>">
|
||||
The aggregated statistics are publicly available at {%url%}.
|
||||
</p>
|
||||
<form>
|
||||
<textarea class="form-control" rows="20">{{reportData | json}}</textarea>
|
||||
</form>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -69,7 +69,7 @@ func (t *relayListener) Serve() {
|
||||
|
||||
conn, err := client.JoinSession(inv)
|
||||
if err != nil {
|
||||
l.Warnln("Joining relay session (BEP/relay):", err)
|
||||
l.Infoln("Joining relay session (BEP/relay):", err)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ package connections
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
@@ -37,6 +36,8 @@ var (
|
||||
listeners = make(map[string]listenerFactory, 0)
|
||||
)
|
||||
|
||||
const perDeviceWarningRate = 1.0 / (15 * 60) // Once per 15 minutes
|
||||
|
||||
// Service listens and dials all configured unconnected devices, via supported
|
||||
// dialers. Successful connections are handed to the model.
|
||||
type Service struct {
|
||||
@@ -153,12 +154,22 @@ next:
|
||||
continue
|
||||
}
|
||||
|
||||
hello, err := exchangeHello(c, s.model.GetHello(remoteID))
|
||||
c.SetDeadline(time.Now().Add(20 * time.Second))
|
||||
hello, err := protocol.ExchangeHello(c, s.model.GetHello(remoteID))
|
||||
if err != nil {
|
||||
l.Infof("Failed to exchange Hello messages with %s (%s): %s", remoteID, c.RemoteAddr(), err)
|
||||
if protocol.IsVersionMismatch(err) {
|
||||
// The error will be a relatively user friendly description
|
||||
// of what's wrong with the version compatibility
|
||||
msg := fmt.Sprintf("Connecting to %s (%s): %s", remoteID, c.RemoteAddr(), err)
|
||||
warningFor(remoteID, msg)
|
||||
} else {
|
||||
// It's something else - connection reset or whatever
|
||||
l.Infof("Failed to exchange Hello messages with %s (%s): %s", remoteID, c.RemoteAddr(), err)
|
||||
}
|
||||
c.Close()
|
||||
continue
|
||||
}
|
||||
c.SetDeadline(time.Time{})
|
||||
|
||||
s.model.OnHello(remoteID, c.RemoteAddr(), hello)
|
||||
|
||||
@@ -553,54 +564,6 @@ func (s *Service) getListenerFactory(cfg config.Configuration, uri *url.URL) (li
|
||||
return listenerFactory, nil
|
||||
}
|
||||
|
||||
func exchangeHello(c net.Conn, h protocol.HelloMessage) (protocol.HelloMessage, error) {
|
||||
if err := c.SetDeadline(time.Now().Add(2 * time.Second)); err != nil {
|
||||
return protocol.HelloMessage{}, err
|
||||
}
|
||||
defer c.SetDeadline(time.Time{})
|
||||
|
||||
header := make([]byte, 8)
|
||||
msg := h.MustMarshalXDR()
|
||||
|
||||
binary.BigEndian.PutUint32(header[:4], protocol.HelloMessageMagic)
|
||||
binary.BigEndian.PutUint32(header[4:], uint32(len(msg)))
|
||||
|
||||
if _, err := c.Write(header); err != nil {
|
||||
return protocol.HelloMessage{}, err
|
||||
}
|
||||
|
||||
if _, err := c.Write(msg); err != nil {
|
||||
return protocol.HelloMessage{}, err
|
||||
}
|
||||
|
||||
if _, err := io.ReadFull(c, header); err != nil {
|
||||
return protocol.HelloMessage{}, err
|
||||
}
|
||||
|
||||
if binary.BigEndian.Uint32(header[:4]) != protocol.HelloMessageMagic {
|
||||
return protocol.HelloMessage{}, fmt.Errorf("incorrect magic")
|
||||
}
|
||||
|
||||
msgSize := binary.BigEndian.Uint32(header[4:])
|
||||
if msgSize > 1024 {
|
||||
return protocol.HelloMessage{}, fmt.Errorf("hello message too big")
|
||||
}
|
||||
|
||||
buf := make([]byte, msgSize)
|
||||
|
||||
var hello protocol.HelloMessage
|
||||
|
||||
if _, err := io.ReadFull(c, buf); err != nil {
|
||||
return protocol.HelloMessage{}, err
|
||||
}
|
||||
|
||||
if err := hello.UnmarshalXDR(buf); err != nil {
|
||||
return protocol.HelloMessage{}, err
|
||||
}
|
||||
|
||||
return hello, nil
|
||||
}
|
||||
|
||||
func filterAndFindSleepDuration(nextDial map[string]time.Time, seen []string, now time.Time) (map[string]time.Time, time.Duration) {
|
||||
newNextDial := make(map[string]time.Time)
|
||||
|
||||
@@ -628,3 +591,19 @@ func urlsToStrings(urls []*url.URL) []string {
|
||||
}
|
||||
return strings
|
||||
}
|
||||
|
||||
var warningLimiters = make(map[protocol.DeviceID]*ratelimit.Bucket)
|
||||
var warningLimitersMut = sync.NewMutex()
|
||||
|
||||
func warningFor(dev protocol.DeviceID, msg string) {
|
||||
warningLimitersMut.Lock()
|
||||
defer warningLimitersMut.Unlock()
|
||||
lim, ok := warningLimiters[dev]
|
||||
if !ok {
|
||||
lim = ratelimit.NewBucketWithRate(perDeviceWarningRate, 1)
|
||||
warningLimiters[dev] = lim
|
||||
}
|
||||
if lim.TakeAvailable(1) == 1 {
|
||||
l.Warnln(msg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -66,11 +66,11 @@ type genericListener interface {
|
||||
|
||||
type Model interface {
|
||||
protocol.Model
|
||||
AddConnection(conn Connection, hello protocol.HelloMessage)
|
||||
AddConnection(conn Connection, hello protocol.HelloResult)
|
||||
ConnectedTo(remoteID protocol.DeviceID) bool
|
||||
IsPaused(remoteID protocol.DeviceID) bool
|
||||
OnHello(protocol.DeviceID, net.Addr, protocol.HelloMessage)
|
||||
GetHello(protocol.DeviceID) protocol.HelloMessage
|
||||
OnHello(protocol.DeviceID, net.Addr, protocol.HelloResult)
|
||||
GetHello(protocol.DeviceID) protocol.Version13HelloMessage
|
||||
}
|
||||
|
||||
// serviceFunc wraps a function to create a suture.Service without stop
|
||||
|
||||
@@ -94,7 +94,7 @@ type Model struct {
|
||||
fmut sync.RWMutex // protects the above
|
||||
|
||||
conn map[protocol.DeviceID]connections.Connection
|
||||
helloMessages map[protocol.DeviceID]protocol.HelloMessage
|
||||
helloMessages map[protocol.DeviceID]protocol.HelloResult
|
||||
deviceClusterConf map[protocol.DeviceID]protocol.ClusterConfigMessage
|
||||
devicePaused map[protocol.DeviceID]bool
|
||||
deviceDownloads map[protocol.DeviceID]*deviceDownloadState
|
||||
@@ -139,7 +139,7 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName,
|
||||
folderRunnerTokens: make(map[string][]suture.ServiceToken),
|
||||
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
|
||||
conn: make(map[protocol.DeviceID]connections.Connection),
|
||||
helloMessages: make(map[protocol.DeviceID]protocol.HelloMessage),
|
||||
helloMessages: make(map[protocol.DeviceID]protocol.HelloResult),
|
||||
deviceClusterConf: make(map[protocol.DeviceID]protocol.ClusterConfigMessage),
|
||||
devicePaused: make(map[protocol.DeviceID]bool),
|
||||
deviceDownloads: make(map[protocol.DeviceID]*deviceDownloadState),
|
||||
@@ -983,7 +983,7 @@ func (m *Model) SetIgnores(folder string, content []string) error {
|
||||
// OnHello is called when an device connects to us.
|
||||
// This allows us to extract some information from the Hello message
|
||||
// and add it to a list of known devices ahead of any checks.
|
||||
func (m *Model) OnHello(remoteID protocol.DeviceID, addr net.Addr, hello protocol.HelloMessage) {
|
||||
func (m *Model) OnHello(remoteID protocol.DeviceID, addr net.Addr, hello protocol.HelloResult) {
|
||||
for deviceID := range m.cfg.Devices() {
|
||||
if deviceID == remoteID {
|
||||
// Existing device, we will get the hello message in AddConnection
|
||||
@@ -1003,8 +1003,8 @@ func (m *Model) OnHello(remoteID protocol.DeviceID, addr net.Addr, hello protoco
|
||||
}
|
||||
|
||||
// GetHello is called when we are about to connect to some remote device.
|
||||
func (m *Model) GetHello(protocol.DeviceID) protocol.HelloMessage {
|
||||
return protocol.HelloMessage{
|
||||
func (m *Model) GetHello(protocol.DeviceID) protocol.Version13HelloMessage {
|
||||
return protocol.Version13HelloMessage{
|
||||
DeviceName: m.deviceName,
|
||||
ClientName: m.clientName,
|
||||
ClientVersion: m.clientVersion,
|
||||
@@ -1014,7 +1014,7 @@ func (m *Model) GetHello(protocol.DeviceID) protocol.HelloMessage {
|
||||
// AddConnection adds a new peer connection to the model. An initial index will
|
||||
// be sent to the connected peer, thereafter index updates whenever the local
|
||||
// folder changes.
|
||||
func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloMessage) {
|
||||
func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloResult) {
|
||||
deviceID := conn.ID()
|
||||
|
||||
m.pmut.Lock()
|
||||
|
||||
@@ -303,7 +303,7 @@ func BenchmarkRequest(b *testing.B) {
|
||||
Priority: 10,
|
||||
},
|
||||
Connection: fc,
|
||||
}, protocol.HelloMessage{})
|
||||
}, protocol.HelloResult{})
|
||||
m.Index(device1, "default", files, 0, nil)
|
||||
|
||||
b.ResetTimer()
|
||||
@@ -319,7 +319,7 @@ func BenchmarkRequest(b *testing.B) {
|
||||
}
|
||||
|
||||
func TestDeviceRename(t *testing.T) {
|
||||
hello := protocol.HelloMessage{
|
||||
hello := protocol.HelloResult{
|
||||
ClientName: "syncthing",
|
||||
ClientVersion: "v0.9.4",
|
||||
}
|
||||
|
||||
105
lib/protocol/hello.go
Normal file
105
lib/protocol/hello.go
Normal file
@@ -0,0 +1,105 @@
|
||||
// Copyright (C) 2016 The Protocol Authors.
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
)
|
||||
|
||||
// The HelloMessage interface is implemented by the version specific hello
|
||||
// message. It knows its magic number and how to serialize itself to a byte
|
||||
// buffer.
|
||||
type HelloMessage interface {
|
||||
Magic() uint32
|
||||
Marshal() ([]byte, error)
|
||||
}
|
||||
|
||||
// The HelloResult is the non version specific interpretation of the other
|
||||
// side's Hello message.
|
||||
type HelloResult struct {
|
||||
DeviceName string
|
||||
ClientName string
|
||||
ClientVersion string
|
||||
}
|
||||
|
||||
var (
|
||||
// ErrTooOldVersion12 is returned by ExchangeHello when the other side
|
||||
// speaks the older, incompatible version 0.12 of the protocol.
|
||||
ErrTooOldVersion12 = errors.New("the remote device speaks an older version of the protocol (v0.12) not compatible with this version")
|
||||
// ErrUnknownMagic is returned by ExchangeHellow when the other side
|
||||
// speaks something entirely unknown.
|
||||
ErrUnknownMagic = errors.New("the remote device speaks an unknown (newer?) version of the protocol")
|
||||
)
|
||||
|
||||
func ExchangeHello(c io.ReadWriter, h HelloMessage) (HelloResult, error) {
|
||||
if err := writeHello(c, h); err != nil {
|
||||
return HelloResult{}, err
|
||||
}
|
||||
return readHello(c)
|
||||
}
|
||||
|
||||
// IsVersionMismatch returns true if the error is a reliable indication of a
|
||||
// version mismatch that we might want to alert the user about.
|
||||
func IsVersionMismatch(err error) bool {
|
||||
switch err {
|
||||
case ErrTooOldVersion12, ErrUnknownMagic:
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func readHello(c io.Reader) (HelloResult, error) {
|
||||
header := make([]byte, 8)
|
||||
if _, err := io.ReadFull(c, header); err != nil {
|
||||
return HelloResult{}, err
|
||||
}
|
||||
|
||||
switch binary.BigEndian.Uint32(header[:4]) {
|
||||
case Version13HelloMagic:
|
||||
// This is a v0.13 Hello message in XDR format
|
||||
msgSize := binary.BigEndian.Uint32(header[4:])
|
||||
if msgSize > 1024 {
|
||||
return HelloResult{}, fmt.Errorf("hello message too big")
|
||||
}
|
||||
buf := make([]byte, msgSize)
|
||||
if _, err := io.ReadFull(c, buf); err != nil {
|
||||
return HelloResult{}, err
|
||||
}
|
||||
|
||||
var hello Version13HelloMessage
|
||||
if err := hello.UnmarshalXDR(buf); err != nil {
|
||||
return HelloResult{}, err
|
||||
}
|
||||
res := HelloResult{
|
||||
DeviceName: hello.DeviceName,
|
||||
ClientName: hello.ClientName,
|
||||
ClientVersion: hello.ClientVersion,
|
||||
}
|
||||
return res, nil
|
||||
|
||||
case 0x00010001, 0x00010000:
|
||||
// This is the first word of a v0.12 cluster config message.
|
||||
// (Version 0, message ID 1, message type 0, compression enabled or disabled)
|
||||
return HelloResult{}, ErrTooOldVersion12
|
||||
}
|
||||
|
||||
return HelloResult{}, ErrUnknownMagic
|
||||
}
|
||||
|
||||
func writeHello(c io.Writer, h HelloMessage) error {
|
||||
msg, err := h.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
header := make([]byte, 8)
|
||||
binary.BigEndian.PutUint32(header[:4], h.Magic())
|
||||
binary.BigEndian.PutUint32(header[4:], uint32(len(msg)))
|
||||
|
||||
_, err = c.Write(append(header, msg...))
|
||||
return err
|
||||
}
|
||||
150
lib/protocol/hello_test.go
Normal file
150
lib/protocol/hello_test.go
Normal file
@@ -0,0 +1,150 @@
|
||||
// Copyright (C) 2016 The Protocol Authors.
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"encoding/hex"
|
||||
"io"
|
||||
"regexp"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var spaceRe = regexp.MustCompile(`\s`)
|
||||
|
||||
func TestVersion13Hello(t *testing.T) {
|
||||
// Tests that we can send and receive a version 0.13 hello message.
|
||||
|
||||
expected := Version13HelloMessage{
|
||||
DeviceName: "test device",
|
||||
ClientName: "syncthing",
|
||||
ClientVersion: "v0.13.5",
|
||||
}
|
||||
msgBuf := expected.MustMarshalXDR()
|
||||
|
||||
hdrBuf := make([]byte, 8)
|
||||
binary.BigEndian.PutUint32(hdrBuf, Version13HelloMagic)
|
||||
binary.BigEndian.PutUint32(hdrBuf[4:], uint32(len(msgBuf)))
|
||||
|
||||
outBuf := new(bytes.Buffer)
|
||||
outBuf.Write(hdrBuf)
|
||||
outBuf.Write(msgBuf)
|
||||
|
||||
inBuf := new(bytes.Buffer)
|
||||
|
||||
conn := &readWriter{outBuf, inBuf}
|
||||
|
||||
send := Version13HelloMessage{
|
||||
DeviceName: "this device",
|
||||
ClientName: "other client",
|
||||
ClientVersion: "v0.13.6",
|
||||
}
|
||||
|
||||
res, err := ExchangeHello(conn, send)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
if res.ClientName != expected.ClientName {
|
||||
t.Errorf("incorrect ClientName %q != expected %q", res.ClientName, expected.ClientName)
|
||||
}
|
||||
if res.ClientVersion != expected.ClientVersion {
|
||||
t.Errorf("incorrect ClientVersion %q != expected %q", res.ClientVersion, expected.ClientVersion)
|
||||
}
|
||||
if res.DeviceName != expected.DeviceName {
|
||||
t.Errorf("incorrect DeviceName %q != expected %q", res.DeviceName, expected.DeviceName)
|
||||
}
|
||||
}
|
||||
|
||||
func TestVersion12Hello(t *testing.T) {
|
||||
// Tests that we can correctly interpret the lack of a hello message
|
||||
// from a v0.12 client.
|
||||
|
||||
// This is the typical v0.12 connection start - our message header for a
|
||||
// ClusterConfig message and then the cluster config message data. Taken
|
||||
// from a protocol dump of a recent v0.12 client.
|
||||
msg, _ := hex.DecodeString(spaceRe.ReplaceAllString(`
|
||||
00010001
|
||||
0000014a
|
||||
7802000070000000027332000100a00973796e637468696e670e00b000000876
|
||||
302e31322e32352400b00000000764656661756c741e00f01603000000204794
|
||||
03ffdef496b5f5e5bc9c0a15221e70073164509fa30761af63094f6f945c3800
|
||||
2073312f00f20b0001000000157463703a2f2f3132372e302e302e313a323230
|
||||
301f00012400080500003000001000f1122064516fb94d24e7b637d20d9846eb
|
||||
aeffb09556ef3968c8276fefc3fe24c144c2640002c0000034000f640002021f
|
||||
00004f00090400003000001100f11220dff67945f05bdab4270acd6057f1eacf
|
||||
a3ac93cade07ce6a89384c181ad6b80e640010332b000fc80007021f00012400
|
||||
080500046400041400f21f2dc2af5c5f28e38384295f2fc2af2052c3a46b736d
|
||||
c3b67267c3a57320e58aa8e4bd9c20d090d0b4d180d0b5d18136001f026c01b8
|
||||
90000000000000000000`, ``))
|
||||
|
||||
outBuf := new(bytes.Buffer)
|
||||
outBuf.Write(msg)
|
||||
|
||||
inBuf := new(bytes.Buffer)
|
||||
|
||||
conn := &readWriter{outBuf, inBuf}
|
||||
|
||||
send := Version13HelloMessage{
|
||||
DeviceName: "this device",
|
||||
ClientName: "other client",
|
||||
ClientVersion: "v0.13.6",
|
||||
}
|
||||
|
||||
_, err := ExchangeHello(conn, send)
|
||||
if err != ErrTooOldVersion12 {
|
||||
t.Errorf("unexpected error %v != ErrTooOld", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUnknownHello(t *testing.T) {
|
||||
// Tests that we react correctly to a completely unknown magic number.
|
||||
|
||||
// This is an unknown magic follow byte some message data.
|
||||
msg, _ := hex.DecodeString(spaceRe.ReplaceAllString(`
|
||||
12345678
|
||||
0000014a
|
||||
7802000070000000027332000100a00973796e637468696e670e00b000000876
|
||||
302e31322e32352400b00000000764656661756c741e00f01603000000204794
|
||||
03ffdef496b5f5e5bc9c0a15221e70073164509fa30761af63094f6f945c3800
|
||||
2073312f00f20b0001000000157463703a2f2f3132372e302e302e313a323230
|
||||
301f00012400080500003000001000f1122064516fb94d24e7b637d20d9846eb
|
||||
aeffb09556ef3968c8276fefc3fe24c144c2640002c0000034000f640002021f
|
||||
00004f00090400003000001100f11220dff67945f05bdab4270acd6057f1eacf
|
||||
a3ac93cade07ce6a89384c181ad6b80e640010332b000fc80007021f00012400
|
||||
080500046400041400f21f2dc2af5c5f28e38384295f2fc2af2052c3a46b736d
|
||||
c3b67267c3a57320e58aa8e4bd9c20d090d0b4d180d0b5d18136001f026c01b8
|
||||
90000000000000000000`, ``))
|
||||
|
||||
outBuf := new(bytes.Buffer)
|
||||
outBuf.Write(msg)
|
||||
|
||||
inBuf := new(bytes.Buffer)
|
||||
|
||||
conn := &readWriter{outBuf, inBuf}
|
||||
|
||||
send := Version13HelloMessage{
|
||||
DeviceName: "this device",
|
||||
ClientName: "other client",
|
||||
ClientVersion: "v0.13.6",
|
||||
}
|
||||
|
||||
_, err := ExchangeHello(conn, send)
|
||||
if err != ErrUnknownMagic {
|
||||
t.Errorf("unexpected error %v != ErrUnknownMagic", err)
|
||||
}
|
||||
}
|
||||
|
||||
type readWriter struct {
|
||||
r io.Reader
|
||||
w io.Writer
|
||||
}
|
||||
|
||||
func (rw *readWriter) Write(data []byte) (int, error) {
|
||||
return rw.w.Write(data)
|
||||
}
|
||||
|
||||
func (rw *readWriter) Read(data []byte) (int, error) {
|
||||
return rw.r.Read(data)
|
||||
}
|
||||
24
lib/protocol/hello_v0.13.go
Normal file
24
lib/protocol/hello_v0.13.go
Normal file
@@ -0,0 +1,24 @@
|
||||
// Copyright (C) 2016 The Protocol Authors.
|
||||
|
||||
//go:generate -command genxdr go run ../../vendor/github.com/calmh/xdr/cmd/genxdr/main.go
|
||||
//go:generate genxdr -o hello_v0.13_xdr.go hello_v0.13.go
|
||||
|
||||
package protocol
|
||||
|
||||
var (
|
||||
Version13HelloMagic uint32 = 0x9F79BC40
|
||||
)
|
||||
|
||||
type Version13HelloMessage struct {
|
||||
DeviceName string // max:64
|
||||
ClientName string // max:64
|
||||
ClientVersion string // max:64
|
||||
}
|
||||
|
||||
func (m Version13HelloMessage) Magic() uint32 {
|
||||
return Version13HelloMagic
|
||||
}
|
||||
|
||||
func (m Version13HelloMessage) Marshal() ([]byte, error) {
|
||||
return m.MarshalXDR()
|
||||
}
|
||||
85
lib/protocol/hello_v0.13_xdr.go
Normal file
85
lib/protocol/hello_v0.13_xdr.go
Normal file
@@ -0,0 +1,85 @@
|
||||
// ************************************************************
|
||||
// This file is automatically generated by genxdr. Do not edit.
|
||||
// ************************************************************
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"github.com/calmh/xdr"
|
||||
)
|
||||
|
||||
/*
|
||||
|
||||
Version13HelloMessage Structure:
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Device Name (length + padded data) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Client Name (length + padded data) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Client Version (length + padded data) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
|
||||
struct Version13HelloMessage {
|
||||
string DeviceName<64>;
|
||||
string ClientName<64>;
|
||||
string ClientVersion<64>;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func (o Version13HelloMessage) XDRSize() int {
|
||||
return 4 + len(o.DeviceName) + xdr.Padding(len(o.DeviceName)) +
|
||||
4 + len(o.ClientName) + xdr.Padding(len(o.ClientName)) +
|
||||
4 + len(o.ClientVersion) + xdr.Padding(len(o.ClientVersion))
|
||||
}
|
||||
|
||||
func (o Version13HelloMessage) MarshalXDR() ([]byte, error) {
|
||||
buf := make([]byte, o.XDRSize())
|
||||
m := &xdr.Marshaller{Data: buf}
|
||||
return buf, o.MarshalXDRInto(m)
|
||||
}
|
||||
|
||||
func (o Version13HelloMessage) MustMarshalXDR() []byte {
|
||||
bs, err := o.MarshalXDR()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
func (o Version13HelloMessage) MarshalXDRInto(m *xdr.Marshaller) error {
|
||||
if l := len(o.DeviceName); l > 64 {
|
||||
return xdr.ElementSizeExceeded("DeviceName", l, 64)
|
||||
}
|
||||
m.MarshalString(o.DeviceName)
|
||||
if l := len(o.ClientName); l > 64 {
|
||||
return xdr.ElementSizeExceeded("ClientName", l, 64)
|
||||
}
|
||||
m.MarshalString(o.ClientName)
|
||||
if l := len(o.ClientVersion); l > 64 {
|
||||
return xdr.ElementSizeExceeded("ClientVersion", l, 64)
|
||||
}
|
||||
m.MarshalString(o.ClientVersion)
|
||||
return m.Error
|
||||
}
|
||||
|
||||
func (o *Version13HelloMessage) UnmarshalXDR(bs []byte) error {
|
||||
u := &xdr.Unmarshaller{Data: bs}
|
||||
return o.UnmarshalXDRFrom(u)
|
||||
}
|
||||
func (o *Version13HelloMessage) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
|
||||
o.DeviceName = u.UnmarshalStringMax(64)
|
||||
o.ClientName = u.UnmarshalStringMax(64)
|
||||
o.ClientVersion = u.UnmarshalStringMax(64)
|
||||
return u.Error
|
||||
}
|
||||
@@ -12,16 +12,9 @@ import (
|
||||
)
|
||||
|
||||
var (
|
||||
sha256OfEmptyBlock = sha256.Sum256(make([]byte, BlockSize))
|
||||
HelloMessageMagic uint32 = 0x9F79BC40
|
||||
sha256OfEmptyBlock = sha256.Sum256(make([]byte, BlockSize))
|
||||
)
|
||||
|
||||
type HelloMessage struct {
|
||||
DeviceName string // max:64
|
||||
ClientName string // max:64
|
||||
ClientVersion string // max:64
|
||||
}
|
||||
|
||||
type IndexMessage struct {
|
||||
Folder string // max:256
|
||||
Files []FileInfo // max:1000000
|
||||
|
||||
@@ -10,82 +10,6 @@ import (
|
||||
|
||||
/*
|
||||
|
||||
HelloMessage Structure:
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Device Name (length + padded data) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Client Name (length + padded data) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Client Version (length + padded data) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
|
||||
struct HelloMessage {
|
||||
string DeviceName<64>;
|
||||
string ClientName<64>;
|
||||
string ClientVersion<64>;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func (o HelloMessage) XDRSize() int {
|
||||
return 4 + len(o.DeviceName) + xdr.Padding(len(o.DeviceName)) +
|
||||
4 + len(o.ClientName) + xdr.Padding(len(o.ClientName)) +
|
||||
4 + len(o.ClientVersion) + xdr.Padding(len(o.ClientVersion))
|
||||
}
|
||||
|
||||
func (o HelloMessage) MarshalXDR() ([]byte, error) {
|
||||
buf := make([]byte, o.XDRSize())
|
||||
m := &xdr.Marshaller{Data: buf}
|
||||
return buf, o.MarshalXDRInto(m)
|
||||
}
|
||||
|
||||
func (o HelloMessage) MustMarshalXDR() []byte {
|
||||
bs, err := o.MarshalXDR()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
func (o HelloMessage) MarshalXDRInto(m *xdr.Marshaller) error {
|
||||
if l := len(o.DeviceName); l > 64 {
|
||||
return xdr.ElementSizeExceeded("DeviceName", l, 64)
|
||||
}
|
||||
m.MarshalString(o.DeviceName)
|
||||
if l := len(o.ClientName); l > 64 {
|
||||
return xdr.ElementSizeExceeded("ClientName", l, 64)
|
||||
}
|
||||
m.MarshalString(o.ClientName)
|
||||
if l := len(o.ClientVersion); l > 64 {
|
||||
return xdr.ElementSizeExceeded("ClientVersion", l, 64)
|
||||
}
|
||||
m.MarshalString(o.ClientVersion)
|
||||
return m.Error
|
||||
}
|
||||
|
||||
func (o *HelloMessage) UnmarshalXDR(bs []byte) error {
|
||||
u := &xdr.Unmarshaller{Data: bs}
|
||||
return o.UnmarshalXDRFrom(u)
|
||||
}
|
||||
func (o *HelloMessage) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
|
||||
o.DeviceName = u.UnmarshalStringMax(64)
|
||||
o.ClientName = u.UnmarshalStringMax(64)
|
||||
o.ClientVersion = u.UnmarshalStringMax(64)
|
||||
return u.Error
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
IndexMessage Structure:
|
||||
|
||||
0 1 2 3
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user