Compare commits

...

38 Commits

Author SHA1 Message Date
Jakob Borg
57a5d13c47 Translation and docs update 2015-07-05 11:24:21 +02:00
Jakob Borg
500b96240b Don't show Failed Items on folder masters 2015-07-05 11:21:15 +02:00
Jakob Borg
13d961d41d Correctly show Override button when out of sync 2015-07-05 11:20:59 +02:00
Jakob Borg
fddc4c2fc0 Rebuild assets 2015-07-05 11:13:35 +02:00
Jakob Borg
061ec7369f Merge pull request #2027 from calmh/bootstrap
Update bootstrap
2015-07-05 11:07:13 +02:00
Jakob Borg
8366dbd8e0 Add link to home page (fixes #1993, fixes #1999) 2015-07-05 11:06:29 +02:00
Jakob Borg
b02047e4b5 Update Bootstrap 3.1.0 -> 3.3.5 2015-07-05 11:05:38 +02:00
Jakob Borg
e9545c4961 Merge pull request #2022 from brgmnn/master
Preserve setgid bit on local directores (fixes #2012)
2015-07-04 19:37:21 +02:00
Audrius Butkevicius
966a2b1df5 Merge pull request #2023 from calmh/advedit
Advanced configuration dialog
2015-07-04 15:08:40 +01:00
Jakob Borg
dec6540967 Implement "advanced configuration" dialog (fixes #2010) 2015-07-04 13:47:43 +02:00
Daniel Bergmann
3fe1673ce9 Preserve setgid bit on local directores (fixes #2012)
When setting the permissions on directories with ignore permissions off,
preserve the setgid bit to it's original value instead of setting it
off.
2015-07-04 09:01:34 +01:00
Jakob Borg
e9e13474c9 Merge pull request #2021 from brgmnn/master
Fixed add device button being overlapped by footer (fixes #1950)
2015-07-03 08:56:33 +02:00
Daniel Bergmann
aee9093848 Fixed add device button being overlapped by footer (fixes #1950) 2015-07-02 16:16:42 +01:00
Jakob Borg
76822c7c34 Merge pull request #2018 from brgmnn/master
Added select ID text on click to gui
2015-07-02 11:01:37 +02:00
Daniel Bergmann
5c18d34d89 Added a contact email address for myself.
Added myself to the AUTHORS, NICKS and GUI contributors files. Also
fixed the sort order in AUTHORS and NICKS when adding myself as there
were a couple of entries in both that were not quite in alphabetical
order.
2015-07-02 09:45:22 +01:00
Daniel Bergmann
970a9c7552 Added select ID text on click to gui 2015-07-02 09:34:12 +01:00
Audrius Butkevicius
37a42dc408 Fix CSRF tests (fixes #2009) 2015-06-30 19:38:27 +01:00
Audrius Butkevicius
a03c9f9457 Merge pull request #2001 from calmh/failed-files
Show failed files in web UI
2015-06-30 15:26:24 +01:00
Jakob Borg
60004ebff1 Show FolderErrors result in UI (fixes #1437) 2015-06-30 14:41:48 +02:00
Jakob Borg
2d9fcf6828 Collect puller errors, send FolderErrors event 2015-06-30 14:41:47 +02:00
Jakob Borg
c8ac9721d7 Translation and docs update 2015-06-28 21:10:57 +02:00
Audrius Butkevicius
b1b68b58fe Update protocol package 2015-06-28 11:40:53 +01:00
Jakob Borg
ca21db9481 Merge pull request #2006 from AudriusButkevicius/timeout
Make ping timeout configurable (fixes #1751)
2015-06-28 07:45:02 +02:00
Audrius Butkevicius
93ad803073 Make ping timeout configurable (fixes #1751) 2015-06-27 12:34:41 +01:00
Audrius Butkevicius
6cc7f70a65 Update protocol package 2015-06-27 12:07:42 +01:00
Jakob Borg
2b0c33f74d Merge pull request #1996 from AudriusButkevicius/checkrace
Potential race between folder being added and scan (fixes #1986)
2015-06-26 12:56:07 +02:00
Audrius Butkevicius
dae1d36a23 Trim string slices upon loading config (fixes #1750) 2015-06-25 16:50:27 +01:00
Audrius Butkevicius
824fa8f17a Fix go lint warnings 2015-06-24 22:05:27 +01:00
Audrius Butkevicius
31cd0b943c Potential race between folder being added and scan (potentially fixes #1986) 2015-06-24 21:59:03 +01:00
Jakob Borg
070eced2f6 Merge pull request #1985 from calmh/fix-reset
Fix reset DB
2015-06-24 14:07:15 +02:00
Audrius Butkevicius
986f8dfb2e Remove dead variable 2015-06-24 08:43:33 +01:00
Audrius Butkevicius
8c0c03eb38 Merge pull request #1989 from AudriusButkevicius/session
Use different session cookies per device
2015-06-23 13:56:14 +01:00
Audrius Butkevicius
fd9bc20bc5 Merge pull request #1988 from calmh/dups
Don't rename duplicate folders (fixes #1675)
2015-06-23 11:17:30 +01:00
Audrius Butkevicius
089fca2319 Use different session cookies per device 2015-06-22 19:51:46 +01:00
Jakob Borg
e936890927 Don't rename duplicate folders (fixes #1675)
Renaming them puts the user in a difficult situation as they can't
rename them back in the GUI. This way, they need to fix the config in
the same way it got broken (manual editing or external tool).
2015-06-22 11:27:47 +02:00
Zillode
0450d48f89 Merge pull request #1856 from calmh/fix-1391
Serialize scans (fixes #1391)
2015-06-21 14:26:17 +02:00
Jakob Borg
2b2cae2d50 Fix reset DB
The reset of all folders failed when there was no data for a given
folder, as it was not returned by db.ListFolders then. But we don't
really care about that, we can "reset" it anyway...
2015-06-21 09:35:41 +02:00
Jakob Borg
f73d5a9ab2 Serialize scans and pulls (fixes #1391) 2015-06-20 23:01:40 +02:00
73 changed files with 10484 additions and 351 deletions

View File

@@ -3,8 +3,8 @@
Aaron Bieber <qbit@deftly.net>
Alexander Graf <register-github@alex-graf.de>
Andrew Dunham <andrew@du.nham.ca>
Audrius Butkevicius <audrius.butkevicius@gmail.com>
Arthur Axel fREW Schmidt <frew@afoolishmanifesto.com> <frioux@gmail.com>
Audrius Butkevicius <audrius.butkevicius@gmail.com>
Bart De Vries <devriesb@gmail.com>
Ben Curthoys <ben@bencurthoys.com>
Ben Schulz <ueomkail@gmail.com> <uok@users.noreply.github.com>
@@ -17,6 +17,7 @@ Cathryne Linenweaver <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.gi
Chris Howie <me@chrishowie.com>
Chris Joel <chris@scriptolo.gy>
Colin Kennedy <moshen.colin@gmail.com>
Daniel Bergmann <dan.arne.bergmann@gmail.com> <brgmnn@users.noreply.github.com>
Daniel Martí <mvdan@mvdan.cc>
Dennis Wilson <dw@risu.io>
Dominik Heidler <dominik@heidler.eu>
@@ -39,9 +40,9 @@ Karol Różycki <rozycki.karol@gmail.com>
Ken'ichi Kamada <kamada@nanohz.org>
Lode Hoste <zillode@zillode.be>
Lord Landon Agahnim <lordlandon@gmail.com>
Marcin Dziadus <dziadus.marcin@gmail.com>
Marc Laporte <marc@marclaporte.com> <marc@laporte.name>
Marc Pujol <kilburn@la3.org>
Marcin Dziadus <dziadus.marcin@gmail.com>
Michael Jephcote <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
Michael Tilli <pyfisch@gmail.com>
Pascal Jungblut <github@pascalj.com> <mail@pascal-jungblut.com>

2
Godeps/Godeps.json generated
View File

@@ -35,7 +35,7 @@
},
{
"ImportPath": "github.com/syncthing/protocol",
"Rev": "e7db2648034fb71b051902a02bc25d4468ed492e"
"Rev": "95e15c95f21b81b09772f07de5c142a3e68f78db"
},
{
"ImportPath": "github.com/syndtr/goleveldb/leveldb",

View File

@@ -31,8 +31,7 @@ const (
const (
stateInitial = iota
stateCCRcvd
stateIdxRcvd
stateReady
)
// FileInfo flags
@@ -103,7 +102,6 @@ type rawConnection struct {
id DeviceID
name string
receiver Model
state int
cr *countingReader
cw *countingWriter
@@ -142,9 +140,9 @@ type isEofer interface {
IsEOF() bool
}
const (
pingTimeout = 30 * time.Second
pingIdleTime = 60 * time.Second
var (
PingTimeout = 30 * time.Second
PingIdleTime = 60 * time.Second
)
func NewConnection(deviceID DeviceID, reader io.Reader, writer io.Writer, receiver Model, name string, compress Compression) Connection {
@@ -155,7 +153,6 @@ func NewConnection(deviceID DeviceID, reader io.Reader, writer io.Writer, receiv
id: deviceID,
name: name,
receiver: nativeModel{receiver},
state: stateInitial,
cr: cr,
cw: cw,
outbox: make(chan hdrMsg),
@@ -285,6 +282,7 @@ func (c *rawConnection) readerLoop() (err error) {
c.close(err)
}()
state := stateInitial
for {
select {
case <-c.closed:
@@ -298,47 +296,54 @@ func (c *rawConnection) readerLoop() (err error) {
}
switch msg := msg.(type) {
case ClusterConfigMessage:
if state != stateInitial {
return fmt.Errorf("protocol error: cluster config message in state %d", state)
}
go c.receiver.ClusterConfig(c.id, msg)
state = stateReady
case IndexMessage:
switch hdr.msgType {
case messageTypeIndex:
if c.state < stateCCRcvd {
return fmt.Errorf("protocol error: index message in state %d", c.state)
if state != stateReady {
return fmt.Errorf("protocol error: index message in state %d", state)
}
c.handleIndex(msg)
c.state = stateIdxRcvd
state = stateReady
case messageTypeIndexUpdate:
if c.state < stateIdxRcvd {
return fmt.Errorf("protocol error: index update message in state %d", c.state)
if state != stateReady {
return fmt.Errorf("protocol error: index update message in state %d", state)
}
c.handleIndexUpdate(msg)
state = stateReady
}
case RequestMessage:
if c.state < stateIdxRcvd {
return fmt.Errorf("protocol error: request message in state %d", c.state)
if state != stateReady {
return fmt.Errorf("protocol error: request message in state %d", state)
}
// Requests are handled asynchronously
go c.handleRequest(hdr.msgID, msg)
case ResponseMessage:
if c.state < stateIdxRcvd {
return fmt.Errorf("protocol error: response message in state %d", c.state)
if state != stateReady {
return fmt.Errorf("protocol error: response message in state %d", state)
}
c.handleResponse(hdr.msgID, msg)
case pingMessage:
if state != stateReady {
return fmt.Errorf("protocol error: ping message in state %d", state)
}
c.send(hdr.msgID, messageTypePong, pongMessage{})
case pongMessage:
c.handlePong(hdr.msgID)
case ClusterConfigMessage:
if c.state != stateInitial {
return fmt.Errorf("protocol error: cluster config message in state %d", c.state)
if state != stateReady {
return fmt.Errorf("protocol error: pong message in state %d", state)
}
go c.receiver.ClusterConfig(c.id, msg)
c.state = stateCCRcvd
c.handlePong(hdr.msgID)
case CloseMessage:
return errors.New(msg.Reason)
@@ -679,17 +684,17 @@ func (c *rawConnection) idGenerator() {
func (c *rawConnection) pingerLoop() {
var rc = make(chan bool, 1)
ticker := time.Tick(pingIdleTime / 2)
ticker := time.Tick(PingIdleTime / 2)
for {
select {
case <-ticker:
if d := time.Since(c.cr.Last()); d < pingIdleTime {
if d := time.Since(c.cr.Last()); d < PingIdleTime {
if debug {
l.Debugln(c.id, "ping skipped after rd", d)
}
continue
}
if d := time.Since(c.cw.Last()); d < pingIdleTime {
if d := time.Since(c.cw.Last()); d < PingIdleTime {
if debug {
l.Debugln(c.id, "ping skipped after wr", d)
}
@@ -709,7 +714,7 @@ func (c *rawConnection) pingerLoop() {
if !ok {
c.close(fmt.Errorf("ping failure"))
}
case <-time.After(pingTimeout):
case <-time.After(PingTimeout):
c.close(fmt.Errorf("ping timeout"))
case <-c.closed:
return

View File

@@ -67,8 +67,10 @@ func TestPing(t *testing.T) {
ar, aw := io.Pipe()
br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, nil, "name", CompressAlways).(wireFormatConnection).next.(*rawConnection)
c1 := NewConnection(c1ID, br, aw, nil, "name", CompressAlways).(wireFormatConnection).next.(*rawConnection)
c0 := NewConnection(c0ID, ar, bw, newTestModel(), "name", CompressAlways).(wireFormatConnection).next.(*rawConnection)
c1 := NewConnection(c1ID, br, aw, newTestModel(), "name", CompressAlways).(wireFormatConnection).next.(*rawConnection)
c0.ClusterConfig(ClusterConfigMessage{})
c1.ClusterConfig(ClusterConfigMessage{})
if ok := c0.ping(); !ok {
t.Error("c0 ping failed")
@@ -81,8 +83,8 @@ func TestPing(t *testing.T) {
func TestPingErr(t *testing.T) {
e := errors.New("something broke")
for i := 0; i < 16; i++ {
for j := 0; j < 16; j++ {
for i := 0; i < 32; i++ {
for j := 0; j < 32; j++ {
m0 := newTestModel()
m1 := newTestModel()
@@ -92,12 +94,16 @@ func TestPingErr(t *testing.T) {
ebw := &ErrPipe{PipeWriter: *bw, max: j, err: e}
c0 := NewConnection(c0ID, ar, ebw, m0, "name", CompressAlways).(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, eaw, m1, "name", CompressAlways)
c1 := NewConnection(c1ID, br, eaw, m1, "name", CompressAlways)
c0.ClusterConfig(ClusterConfigMessage{})
c1.ClusterConfig(ClusterConfigMessage{})
res := c0.ping()
if (i < 8 || j < 8) && res {
// This should have resulted in failure, as there is no way an empty ClusterConfig plus a Ping message fits in eight bytes.
t.Errorf("Unexpected ping success; i=%d, j=%d", i, j)
} else if (i >= 12 && j >= 12) && !res {
} else if (i >= 28 && j >= 28) && !res {
// This should have worked though, as 28 bytes is plenty for both.
t.Errorf("Unexpected ping fail; i=%d, j=%d", i, j)
}
}
@@ -168,7 +174,9 @@ func TestVersionErr(t *testing.T) {
br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, m0, "name", CompressAlways).(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, aw, m1, "name", CompressAlways)
c1 := NewConnection(c1ID, br, aw, m1, "name", CompressAlways)
c0.ClusterConfig(ClusterConfigMessage{})
c1.ClusterConfig(ClusterConfigMessage{})
w := xdr.NewWriter(c0.cw)
w.WriteUint32(encodeHeader(header{
@@ -191,7 +199,9 @@ func TestTypeErr(t *testing.T) {
br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, m0, "name", CompressAlways).(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, aw, m1, "name", CompressAlways)
c1 := NewConnection(c1ID, br, aw, m1, "name", CompressAlways)
c0.ClusterConfig(ClusterConfigMessage{})
c1.ClusterConfig(ClusterConfigMessage{})
w := xdr.NewWriter(c0.cw)
w.WriteUint32(encodeHeader(header{
@@ -214,7 +224,9 @@ func TestClose(t *testing.T) {
br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, m0, "name", CompressAlways).(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, aw, m1, "name", CompressAlways)
c1 := NewConnection(c1ID, br, aw, m1, "name", CompressAlways)
c0.ClusterConfig(ClusterConfigMessage{})
c1.ClusterConfig(ClusterConfigMessage{})
c0.close(nil)

5
NICKS
View File

@@ -15,6 +15,7 @@ asdil12 <dominik@heidler.eu>
bencurthoys <ben@bencurthoys.com>
bigbear2nd <bigbear2nd@gmail.com>
brendanlong <self@brendanlong.com>
brgmnn <dan.arne.bergmann@gmail.com> <brgmnn@users.noreply.github.com>
bsidhom <bsidhom@gmail.com>
calmh <jakob@nym.se>
cdata <chris@scriptolo.gy>
@@ -36,8 +37,8 @@ kozec <kozec@kozec.com>
krozycki <rozycki.karol@gmail.com>
marcindziadus <dziadus.marcin@gmail.com>
marclaporte <marc@marclaporte.com>
moshen <moshen.colin@gmail.com>
mogwa1 <devriesb@gmail.com>
moshen <moshen.colin@gmail.com>
mvdan <mvdan@mvdan.cc>
pascalj <github@pascalj.com> <mail@pascal-jungblut.com>
peterhoeg <peter@speartail.com>
@@ -45,9 +46,9 @@ philips <brandon@ifup.org>
piobpl <piotrb10@gmail.com>
pluby <phill.luby@newredo.com>
pyfisch <pyfisch@gmail.com>
qbit <qbit@deftly.net>
ralder <ralder@yandex.ru>
rumpelsepp <stefan@sevenbyte.org>
qbit <qbit@deftly.net>
sciurius <jvromans@squirrel.nl>
seehuhn <voss@seehuhn.de>
snnd <dw@risu.io>

View File

@@ -52,6 +52,7 @@ var (
)
type apiSvc struct {
id protocol.DeviceID
cfg config.GUIConfiguration
assetDir string
model *model.Model
@@ -62,8 +63,9 @@ type apiSvc struct {
eventSub *events.BufferedSubscription
}
func newAPISvc(cfg config.GUIConfiguration, assetDir string, m *model.Model, eventSub *events.BufferedSubscription) (*apiSvc, error) {
func newAPISvc(id protocol.DeviceID, cfg config.GUIConfiguration, assetDir string, m *model.Model, eventSub *events.BufferedSubscription) (*apiSvc, error) {
svc := &apiSvc{
id: id,
cfg: cfg,
assetDir: assetDir,
model: m,
@@ -188,14 +190,14 @@ func (s *apiSvc) Serve() {
// Wrap everything in CSRF protection. The /rest prefix should be
// protected, other requests will grant cookies.
handler := csrfMiddleware("/rest", s.cfg.APIKey, mux)
handler := csrfMiddleware(s.id.String()[:5], "/rest", s.cfg.APIKey, mux)
// Add our version as a header to responses
handler = withVersionMiddleware(handler)
// Add our version and ID as a header to responses
handler = withDetailsMiddleware(s.id, handler)
// Wrap everything in basic auth, if user/password is set.
if len(s.cfg.User) > 0 && len(s.cfg.Password) > 0 {
handler = basicAuthAndSessionMiddleware(s.cfg, handler)
handler = basicAuthAndSessionMiddleware("sessionid-"+s.id.String()[:5], s.cfg, handler)
}
// Redirect to HTTPS if we are supposed to
@@ -334,9 +336,10 @@ func noCacheMiddleware(h http.Handler) http.Handler {
})
}
func withVersionMiddleware(h http.Handler) http.Handler {
func withDetailsMiddleware(id protocol.DeviceID, h http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
w.Header().Set("X-Syncthing-Version", Version)
w.Header().Set("X-Syncthing-ID", id.String())
h.ServeHTTP(w, r)
})
}
@@ -425,7 +428,10 @@ func folderSummary(m *model.Model, folder string) map[string]interface{} {
res["error"] = err.Error()
}
res["version"] = m.CurrentLocalVersion(folder) + m.RemoteLocalVersion(folder)
lv, _ := m.CurrentLocalVersion(folder)
rv, _ := m.RemoteLocalVersion(folder)
res["version"] = lv + rv
ignorePatterns, _, _ := m.GetIgnores(folder)
res["ignorePatterns"] = false
@@ -570,26 +576,26 @@ func (s *apiSvc) postSystemRestart(w http.ResponseWriter, r *http.Request) {
func (s *apiSvc) postSystemReset(w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
folder := qs.Get("folder")
var err error
if len(folder) == 0 {
for folder := range cfg.Folders() {
err = s.model.ResetFolder(folder)
if err != nil {
break
}
if len(folder) > 0 {
if _, ok := cfg.Folders()[folder]; !ok {
http.Error(w, "Invalid folder ID", 500)
return
}
} else {
err = s.model.ResetFolder(folder)
}
if err != nil {
http.Error(w, err.Error(), 500)
return
}
if len(folder) == 0 {
// Reset all folders.
for folder := range cfg.Folders() {
s.model.ResetFolder(folder)
}
s.flushResponse(`{"ok": "resetting database"}`, w)
} else {
// Reset a specific folder, assuming it's supposed to exist.
s.model.ResetFolder(folder)
s.flushResponse(`{"ok": "resetting folder `+folder+`"}`, w)
}
go restart()
}

View File

@@ -24,14 +24,15 @@ var (
sessionsMut = sync.NewMutex()
)
func basicAuthAndSessionMiddleware(cfg config.GUIConfiguration, next http.Handler) http.Handler {
func basicAuthAndSessionMiddleware(cookieName string, cfg config.GUIConfiguration, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if cfg.APIKey != "" && r.Header.Get("X-API-Key") == cfg.APIKey {
next.ServeHTTP(w, r)
return
}
cookie, err := r.Cookie("sessionid")
cookie, err := r.Cookie(cookieName)
if err == nil && cookie != nil {
sessionsMut.Lock()
_, ok := sessions[cookie.Value]
@@ -86,7 +87,7 @@ func basicAuthAndSessionMiddleware(cfg config.GUIConfiguration, next http.Handle
sessions[sessionid] = true
sessionsMut.Unlock()
http.SetCookie(w, &http.Cookie{
Name: "sessionid",
Name: cookieName,
Value: sessionid,
MaxAge: 0,
})

View File

@@ -24,7 +24,7 @@ var csrfMut = sync.NewMutex()
// Check for CSRF token on /rest/ URLs. If a correct one is not given, reject
// the request with 403. For / and /index.html, set a new CSRF cookie if none
// is currently set.
func csrfMiddleware(prefix, apiKey string, next http.Handler) http.Handler {
func csrfMiddleware(unique, prefix, apiKey string, next http.Handler) http.Handler {
loadCsrfTokens()
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
// Allow requests carrying a valid API key
@@ -35,10 +35,10 @@ func csrfMiddleware(prefix, apiKey string, next http.Handler) http.Handler {
// Allow requests for the front page, and set a CSRF cookie if there isn't already a valid one.
if !strings.HasPrefix(r.URL.Path, prefix) {
cookie, err := r.Cookie("CSRF-Token")
cookie, err := r.Cookie("CSRF-Token-" + unique)
if err != nil || !validCsrfToken(cookie.Value) {
cookie = &http.Cookie{
Name: "CSRF-Token",
Name: "CSRF-Token-" + unique,
Value: newCsrfToken(),
}
http.SetCookie(w, cookie)
@@ -54,7 +54,7 @@ func csrfMiddleware(prefix, apiKey string, next http.Handler) http.Handler {
}
// Verify the CSRF token
token := r.Header.Get("X-CSRF-Token")
token := r.Header.Get("X-CSRF-Token-" + unique)
if !validCsrfToken(token) {
http.Error(w, "CSRF Error", 403)
return

View File

@@ -565,6 +565,9 @@ func syncthingMain() {
symlinks.Supported = false
}
protocol.PingTimeout = time.Duration(opts.PingTimeoutS) * time.Second
protocol.PingIdleTime = time.Duration(opts.PingIdleTimeS) * time.Second
if opts.MaxSendKbps > 0 {
writeRateLimit = ratelimit.NewBucketWithRate(float64(1000*opts.MaxSendKbps), int64(5*1000*opts.MaxSendKbps))
}
@@ -808,7 +811,7 @@ func setupGUI(mainSvc *suture.Supervisor, cfg *config.Wrapper, m *model.Model, a
urlShow := fmt.Sprintf("%s://%s/", proto, net.JoinHostPort(hostShow, strconv.Itoa(addr.Port)))
l.Infoln("Starting web GUI on", urlShow)
api, err := newAPISvc(guiCfg, guiAssets, m, apiSub)
api, err := newAPISvc(myID, guiCfg, guiAssets, m, apiSub)
if err != nil {
l.Fatalln("Cannot start GUI:", err)
}

View File

@@ -245,7 +245,7 @@ ul.three-columns li, ul.two-columns li {
/** Footer nav on small devices **/
@media (max-width: 767px) {
@media (max-width: 991px) {
body {
padding-bottom: 0;
}

View File

@@ -30,7 +30,7 @@
"Copied from original": "Копиран от оригинала",
"Copyright © 2015 the following Contributors:": "Правата запазени © 2015 Сътрудници:",
"Delete": "Изтрий",
"Deleted": "Deleted",
"Deleted": "Изтрито",
"Device ID": "Идентификатор на устройство",
"Device Identification": "Идентификация на устройство",
"Device Name": "Име на устройство",
@@ -81,7 +81,7 @@
"Later": "По-късно",
"Local Discovery": "Локално Откриване",
"Local State": "Локално състояние",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Локално Състояние (Общо)",
"Major Upgrade": "Основно Обновяване",
"Maximum Age": "Максимална Възраст",
"Metadata Only": "Само мета информация",
@@ -177,7 +177,7 @@
"Unshared": "Споделянето прекратено",
"Unused": "Неизползван",
"Up to Date": "Актуален",
"Updated": "Updated",
"Updated": "Обновено",
"Upgrade": "Обнови",
"Upgrade To {%version%}": "Обновен До {{version}}",
"Upgrading": "Обновяване",

View File

@@ -1,9 +1,9 @@
{
"A negative number of days doesn't make sense.": "A negative number of days doesn't make sense.",
"A negative number of days doesn't make sense.": "Un nombre negatiu de dies no té sentit.",
"A new major version may not be compatible with previous versions.": "Una nova versión amb canvis importants pot no ser compatible amb versions prèvies.",
"API Key": "Clau API",
"About": "Sobre",
"Actions": "Actions",
"Actions": "Accions",
"Add": "Afegir",
"Add Device": "Afegir dispositiu",
"Add Folder": "Afegir carpeta",
@@ -20,7 +20,7 @@
"Bugs": "Errors (Bugs)",
"CPU Utilization": "Utilització de la CPU",
"Changelog": "Registre de canvis",
"Clean out after": "Clean out after",
"Clean out after": "Netejar després de",
"Close": "Tancar",
"Command": "Comando",
"Comment, when used at the start of a line": "Comentar, quant s'utilitza al principi d'una línia",
@@ -30,7 +30,7 @@
"Copied from original": "Copiat de l'original",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 els següents Col·laboradors:",
"Delete": "Esborrar",
"Deleted": "Deleted",
"Deleted": "Esborrat",
"Device ID": "ID del dispositiu",
"Device Identification": "Identificació del dispositiu",
"Device Name": "Nom del dispositiu",
@@ -53,7 +53,7 @@
"File Pull Order": "Ordre de fitxers del pull",
"File Versioning": "Versionat de fitxer",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Els bits de permís del fitxer són ignorats quant es busquen els canvis. Utilitzar en sistemes de fitxers FAT.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Files are moved to .stversions folder when replaced or deleted by Syncthing.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Els arxius es menejen a la carpeta .stversions quant són substituïts o esborrats per Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Els fitxers són canviats a versions amb indicació de data en una carpeta \".stversions\" quant són reemplaçats o esborrats per Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Els fitxers són protegits dels canvis fets en altres dispositius, però els canvis fets en aquest dispositiu seràn enviats a la resta del grup (cluster).",
"Folder ID": "ID de carpeta",
@@ -67,7 +67,7 @@
"Global Discovery": "Descobriment global",
"Global Discovery Server": "Servidor de descobriment global",
"Global State": "Estat global",
"Help": "Help",
"Help": "Ajuda",
"Ignore": "Ignorar",
"Ignore Patterns": "Patrons a ignorar",
"Ignore Permissions": "Permisos a ignorar",
@@ -81,7 +81,7 @@
"Later": "Més tard",
"Local Discovery": "Descobriment local",
"Local State": "Estat local",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Estat Local (Total)",
"Major Upgrade": "Actualització important",
"Maximum Age": "Edat màxima",
"Metadata Only": "Sols metadades",
@@ -165,19 +165,19 @@
"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.": "S'utilitzen els següents intervals: per a la primera hora es guarda una versió cada 30 segons, per al primer dia es guarda una versió cada hora, per als primers 30 dies es guarda una versió diaria, fins l'edat màxima es guarda una versió cada setmana.",
"The maximum age must be a number and cannot be blank.": "L'edat màxima deu ser un nombre i no pot estar buida.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El temps màxim per a guardar una versió (en dies, ficar 0 per a guardar les versions per a sempre).",
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
"The number of days to keep files in the trash can. Zero means forever.": "The number of days to keep files in the trash can. Zero means forever.",
"The number of days must be a number and cannot be blank.": "El nombre de dies deu ser un nombre i no pot estar en blanc.",
"The number of days to keep files in the trash can. Zero means forever.": "El nombre de dies per a mantindre els arxius a la paperera. Cero vol dir \"per a sempre\".",
"The number of old versions to keep, per file.": "El nombre de versions antigues per a guardar, per cada fitxer.",
"The number of versions must be a number and cannot be blank.": "El nombre de versions deu ser un nombre i no pot estar buit.",
"The path cannot be blank.": "La ruta no pot estar buida.",
"The rescan interval must be a non-negative number of seconds.": "L'interval de reescaneig deu ser un nombre positiu de segons.",
"This is a major version upgrade.": "Aquesta és una actualització important de la versió.",
"Trash Can File Versioning": "Trash Can File Versioning",
"Trash Can File Versioning": "Versionat d'arxius de la paperera",
"Unknown": "Desconegut",
"Unshared": "No compartit",
"Unused": "No utilitzat",
"Up to Date": "Actualitzat",
"Updated": "Updated",
"Updated": "Actualitzat",
"Upgrade": "Actualitzar",
"Upgrade To {%version%}": "Actualitzar a {{version}}",
"Upgrading": "Actualitzant",

View File

@@ -30,7 +30,7 @@
"Copied from original": "Zkopírováno z originálu",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 následující přispěvatelé:",
"Delete": "Smazat",
"Deleted": "Deleted",
"Deleted": "Smazáno",
"Device ID": "ID přístroje",
"Device Identification": "Identifikace přístroje",
"Device Name": "Jméno přístroje",
@@ -81,7 +81,7 @@
"Later": "Později",
"Local Discovery": "Místní oznamování",
"Local State": "Místní status",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Místní status (Celkem)",
"Major Upgrade": "Důležitá aktualizace",
"Maximum Age": "Maximální časový limit",
"Metadata Only": "Pouze metadata",
@@ -177,7 +177,7 @@
"Unshared": "Nesdílený",
"Unused": "Nepoužitý",
"Up to Date": "Aktuální",
"Updated": "Updated",
"Updated": "Aktualizováno",
"Upgrade": "Aktualizace",
"Upgrade To {%version%}": "Aktualizovat na {{version}}",
"Upgrading": "Aktualizuji",

View File

@@ -30,7 +30,7 @@
"Copied from original": "Vom Original kopiert",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 die folgenden Unterstützer:",
"Delete": "Löschen",
"Deleted": "Deleted",
"Deleted": "gelöscht",
"Device ID": "Geräte ID",
"Device Identification": "Gerät Identifikation",
"Device Name": "Gerätename",
@@ -81,7 +81,7 @@
"Later": "Später",
"Local Discovery": "Lokale Gerätesuche",
"Local State": "Lokaler Status",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Lokaler Status (total)",
"Major Upgrade": "Hauptversionsupgrade",
"Maximum Age": "Höchstalter",
"Metadata Only": "Nur Metadaten",
@@ -99,7 +99,7 @@
"Oldest First": "Älteste zuerst",
"Out Of Sync": "Nicht synchronisiert",
"Out of Sync Items": "Nicht synchronisierte Objekte",
"Outgoing Rate Limit (KiB/s)": "Ausgehendes Datenratelimit (KB/s)",
"Outgoing Rate Limit (KiB/s)": "Limit Datenrate (ausgehend) (KB/s)",
"Override Changes": "Änderungen überschreiben",
"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": "Pfad zum Verzeichnis auf dem lokalen Rechner. Wird erzeugt, wenn es nicht existiert. Das Tilden-Zeichen (~) kann als Abkürzung benutzt werden für",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Pfad in dem die Versionen gespeichert werden sollen (ohne Angabe wird das Verzeichnis .stversions im Verzeichnis verwendet).",
@@ -177,7 +177,7 @@
"Unshared": "Ungeteilt",
"Unused": "Ungenutzt",
"Up to Date": "Aktuell",
"Updated": "Updated",
"Updated": "aktualisiert",
"Upgrade": "Upgrade",
"Upgrade To {%version%}": "Update auf {{version}}",
"Upgrading": "Wird aktualisiert",

View File

@@ -20,7 +20,7 @@
"Bugs": "Bugs",
"CPU Utilization": "Επιβάρυνση του επεξεργαστή",
"Changelog": "Πληροφορίες εκδόσεων",
"Clean out after": "Clean out after",
"Clean out after": "Μετά από αυτό, εκκαθάρισε",
"Close": "Τέλος",
"Command": "Εντολή",
"Comment, when used at the start of a line": "Σχόλιο, όταν χρησιμοποιείται στην αρχή μιας γραμμής",
@@ -30,7 +30,7 @@
"Copied from original": "Έχει αντιγραφεί από το πρωτότυπο",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 από τους παρακάτω συνεισφορείς:",
"Delete": "Διαγραφή",
"Deleted": "Deleted",
"Deleted": "Διαγραμμένα",
"Device ID": "Ταυτότητα συσκευής",
"Device Identification": "Ταυτότητα συσκευής",
"Device Name": "Όνομα συσκευής",
@@ -81,7 +81,7 @@
"Later": "Αργότερα",
"Local Discovery": "Τοπική ανεύρεση",
"Local State": "Τοπική κατάσταση",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Τοπική κατάσταση (συνολικά)",
"Major Upgrade": "Σημαντική αναβάθμιση",
"Maximum Age": "Μέγιστη ηλικία",
"Metadata Only": "Μόνο μεταδεδομένα",
@@ -177,7 +177,7 @@
"Unshared": "Δε μοιράζεται",
"Unused": "Δε χρησιμοποιείται",
"Up to Date": "Ενημερωμένος",
"Updated": "Updated",
"Updated": "Ενημερωμένο",
"Upgrade": "Αναβάθμιση",
"Upgrade To {%version%}": "Αναβάθμιση στην έκδοση {{version}}",
"Upgrading": "Αναβάθμιση",

View File

@@ -172,7 +172,7 @@
"The path cannot be blank.": "The path cannot be blank.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"This is a major version upgrade.": "This is a major version upgrade.",
"Trash Can File Versioning": "Trash Can File Versioning",
"Trash Can File Versioning": "Rubbish Bin File Versioning",
"Unknown": "Unknown",
"Unshared": "Unshared",
"Unused": "Unused",

View File

@@ -10,6 +10,8 @@
"Add new folder?": "Add new folder?",
"Address": "Address",
"Addresses": "Addresses",
"Advanced": "Advanced",
"Advanced Configuration": "Advanced Configuration",
"All Data": "All Data",
"Allow Anonymous Usage Reporting?": "Allow Anonymous Usage Reporting?",
"Alphabetic": "Alphabetic",
@@ -17,6 +19,7 @@
"Anonymous Usage Reporting": "Anonymous Usage Reporting",
"Any devices configured on an introducer device will be added to this device as well.": "Any devices configured on an introducer device will be added to this device as well.",
"Automatic upgrades": "Automatic upgrades",
"Be careful!": "Be careful!",
"Bugs": "Bugs",
"CPU Utilization": "CPU Utilization",
"Changelog": "Changelog",
@@ -50,16 +53,19 @@
"Enter ignore patterns, one per line.": "Enter ignore patterns, one per line.",
"Error": "Error",
"External File Versioning": "External File Versioning",
"Failed Items": "Failed Items",
"File Pull Order": "File Pull Order",
"File Versioning": "File Versioning",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Files are moved to .stversions folder when replaced or deleted by Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.",
"Folder": "Folder",
"Folder ID": "Folder ID",
"Folder Master": "Folder Master",
"Folder Path": "Folder Path",
"Folders": "Folders",
"GUI": "GUI",
"GUI Authentication Password": "GUI Authentication Password",
"GUI Authentication User": "GUI Authentication User",
"GUI Listen Addresses": "GUI Listen Addresses",
@@ -72,6 +78,7 @@
"Ignore Patterns": "Ignore Patterns",
"Ignore Permissions": "Ignore Permissions",
"Incoming Rate Limit (KiB/s)": "Incoming Rate Limit (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Incorrect configuration may damage your folder contents and render Syncthing inoperable.",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "Inversion of the given condition (i.e. do not exclude)",
"Keep Versions": "Keep Versions",
@@ -97,7 +104,8 @@
"OK": "OK",
"Off": "Off",
"Oldest First": "Oldest First",
"Out Of Sync": "Out Of Sync",
"Options": "Options",
"Out of Sync": "Out of Sync",
"Out of Sync Items": "Out of Sync Items",
"Outgoing Rate Limit (KiB/s)": "Outgoing Rate Limit (KiB/s)",
"Override Changes": "Override Changes",
@@ -163,6 +171,7 @@
"The folder ID must be unique.": "The folder ID must be unique.",
"The folder path cannot be blank.": "The folder path cannot be blank.",
"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.": "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.",
"The following items could not be synchronized.": "The following items could not be synchronized.",
"The maximum age must be a number and cannot be blank.": "The maximum age must be a number and cannot be blank.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "The maximum time to keep a version (in days, set to 0 to keep versions forever).",
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
@@ -171,6 +180,7 @@
"The number of versions must be a number and cannot be blank.": "The number of versions must be a number and cannot be blank.",
"The path cannot be blank.": "The path cannot be blank.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"They are retried automatically and will be synced when the error is resolved.": "They are retried automatically and will be synced when the error is resolved.",
"This is a major version upgrade.": "This is a major version upgrade.",
"Trash Can File Versioning": "Trash Can File Versioning",
"Unknown": "Unknown",

View File

@@ -1,9 +1,9 @@
{
"A negative number of days doesn't make sense.": "A negative number of days doesn't make sense.",
"A negative number of days doesn't make sense.": "Un número negativo de días no tiene sentido.",
"A new major version may not be compatible with previous versions.": "Una nueva versión con cambios importantes puede no ser compatible con versiones anteriores.",
"API Key": "Clave del API",
"About": "Acerca de",
"Actions": "Actions",
"Actions": "Acciones",
"Add": "Añadir",
"Add Device": "Añadir dispositivo",
"Add Folder": "Añadir repositorio",
@@ -20,7 +20,7 @@
"Bugs": "Errores (bugs)",
"CPU Utilization": "Uso de CPU",
"Changelog": "Informe de cambios",
"Clean out after": "Clean out after",
"Clean out after": "Limpiar tras",
"Close": "Cerrar",
"Command": "Comando",
"Comment, when used at the start of a line": "Comentar, cuando se usa al comienzo de una línea",
@@ -30,7 +30,7 @@
"Copied from original": "Copiado del original",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 los siguientes Colaboradores:",
"Delete": "Borrar",
"Deleted": "Deleted",
"Deleted": "Borrado",
"Device ID": "ID del dispositivo",
"Device Identification": "Identificación del dispositivo",
"Device Name": "Nombre del dispositivo",
@@ -53,7 +53,7 @@
"File Pull Order": "Orden de ficheros del pull",
"File Versioning": "Versionado de ficheros",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Los bits de permiso de ficheros son ignorados cuando se buscan cambios. Utilizar en sistemas de ficheros FAT.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Files are moved to .stversions folder when replaced or deleted by Syncthing.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Los archivos serán movidos a la carpeta .stversions cuando sean reemplazados o borrados por Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Los ficheros son cambiados a versiones con indicación de fecha en una carpeta \".stversions\" cuando son reemplazados o borrados por Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Los ficheros son protegidos por los cambios hechos en otros dispositivos, pero los cambios hechos en este dispositivo serán enviados al resto del grupo (cluster).",
"Folder ID": "ID de carpeta",
@@ -67,7 +67,7 @@
"Global Discovery": "Descubrimiento global",
"Global Discovery Server": "Servidor de descubrimiento global",
"Global State": "Estado global",
"Help": "Help",
"Help": "Ayuda",
"Ignore": "Ignorar",
"Ignore Patterns": "Patrones a ignorar",
"Ignore Permissions": "Permisos a ignorar",
@@ -81,7 +81,7 @@
"Later": "Más tarde",
"Local Discovery": "Descubrimiento local",
"Local State": "Estado local",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Estado Local (Total)",
"Major Upgrade": "Actualización importante",
"Maximum Age": "Edad máxima",
"Metadata Only": "Sólo metadatos",
@@ -165,19 +165,19 @@
"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.": "Se utilizan los siguientes intervalos: para la primera hora se mantiene una versión cada 30 segundos, para el primer día se mantiene una versión cada hora, para los primeros 30 días se mantiene una versión diaria hasta la edad máxima de una semana.",
"The maximum age must be a number and cannot be blank.": "La edad máxima debe ser un número y no puede estar vacía.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "El tiempo máximo para mantener una versión en días (introducir 0 para mantener las versiones indefinidamente).",
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
"The number of days to keep files in the trash can. Zero means forever.": "The number of days to keep files in the trash can. Zero means forever.",
"The number of days must be a number and cannot be blank.": "El número de días debe ser un número y no puede estar en blanco.",
"The number of days to keep files in the trash can. Zero means forever.": "El número de días para mantener los archivos en la papelera. Cero significa \"para siempre\".",
"The number of old versions to keep, per file.": "El número de versiones a antiguas a mantener para cada fichero.",
"The number of versions must be a number and cannot be blank.": "El número de versiones debe ser un número y no puede estar vacío.",
"The path cannot be blank.": "La ruta no puede estar vacía.",
"The rescan interval must be a non-negative number of seconds.": "El intervalo de actualización debe ser un número positivo de segundos.",
"This is a major version upgrade.": "Hay una actualización importante.",
"Trash Can File Versioning": "Trash Can File Versioning",
"Trash Can File Versioning": "Versionado de archivos de la papelera",
"Unknown": "Desconocido",
"Unshared": "No compartido",
"Unused": "No usado",
"Up to Date": "Actualizado",
"Updated": "Updated",
"Updated": "Actualizado",
"Upgrade": "Actualizar",
"Upgrade To {%version%}": "Actualizar a {{version}}",
"Upgrading": "Actualizando",

View File

@@ -30,7 +30,7 @@
"Copied from original": "Copiado del original",
"Copyright © 2015 the following Contributors:": "Derechos de autor © 2015 los siguientes colaboradores:",
"Delete": "Suprimir",
"Deleted": "Deleted",
"Deleted": "Suprimido",
"Device ID": "ID del dispositivo",
"Device Identification": "Identificación del dispositivo",
"Device Name": "Nombre del dispositivo",
@@ -81,7 +81,7 @@
"Later": "Más tarde",
"Local Discovery": "Búsqueda en red local",
"Local State": "Estado local",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Estado local (total)",
"Major Upgrade": "Actualización mayor",
"Maximum Age": "Edad máxima",
"Metadata Only": "Sólo metadatos",
@@ -177,7 +177,7 @@
"Unshared": "No compartido",
"Unused": "No utilizado",
"Up to Date": "Actualizado",
"Updated": "Updated",
"Updated": "Actualizado",
"Upgrade": "Actualizar",
"Upgrade To {%version%}": "Actualizar a {{version}}",
"Upgrading": "Actualizando",

View File

@@ -30,7 +30,7 @@
"Copied from original": "Copié de l'original",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 Les contributeurs suivants:",
"Delete": "Supprimer",
"Deleted": "Deleted",
"Deleted": "Supprimé",
"Device ID": "ID du périphérique",
"Device Identification": "Identification de l'appareil",
"Device Name": "Nom du périphérique",
@@ -81,7 +81,7 @@
"Later": "Plus tard",
"Local Discovery": "Recherche locale",
"Local State": "État local",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Etat local (Total)",
"Major Upgrade": "Mise à jour majeure",
"Maximum Age": "Ancienneté maximum",
"Metadata Only": "Métadonnées uniquement",
@@ -177,7 +177,7 @@
"Unshared": "Non partagé",
"Unused": "Non utilisé",
"Up to Date": "Synchronisé",
"Updated": "Updated",
"Updated": "Mis à jour",
"Upgrade": "Mise à jour",
"Upgrade To {%version%}": "Mettre à jour vers {{version}}",
"Upgrading": "Mise à jour de Syncthing",

View File

@@ -1,9 +1,9 @@
{
"A negative number of days doesn't make sense.": "A negative number of days doesn't make sense.",
"A negative number of days doesn't make sense.": "Negatív számú nap nincs értelmezve.",
"A new major version may not be compatible with previous versions.": "Az új főverzió nem kompatibilis az előző főverzióval.",
"API Key": "API kulcs",
"About": "Névjegy",
"Actions": "Actions",
"Actions": "Tevékenységek",
"Add": "Hozzáadás",
"Add Device": "Eszköz hozzáadása",
"Add Folder": "Mappa hozzáadása",
@@ -30,7 +30,7 @@
"Copied from original": "Másolva az eredetiről",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 az alábbi Közreműködők",
"Delete": "Törlés",
"Deleted": "Deleted",
"Deleted": "Törölve",
"Device ID": "Eszköz azonosító",
"Device Identification": "Eszköz azonosító",
"Device Name": "Eszköz neve",
@@ -53,8 +53,8 @@
"File Pull Order": "Fájl küldési sorrend",
"File Versioning": "Fájl verziózás",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Fájl jogosultságok figyelmen kívül hagyása változások keresésekor. FAT fájlrendszereken használatakor.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Files are moved to .stversions folder when replaced or deleted by Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Ha a Syncthing áthelyezi vagy törli a fájlokat, akkor azok a .stversions mappába lesznek áthelyezve.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Ha a Syncthing áthelyezi vagy törli a fájlokat, akkor azok a .stversions mappába lesznek áthelyezve, időbélyegzővel ellátva.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "A fájlok védve vannak a más eszközökön történt változásokkal szemben, de az ezen az eszközön történt változások érvényesek lesznek a többire.",
"Folder ID": "Mappa azonosító",
"Folder Master": "Központi mappa",
@@ -81,7 +81,7 @@
"Later": "Később",
"Local Discovery": "Helyi felfedezés",
"Local State": "Helyi állapot",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Helyi állapot (Teljes)",
"Major Upgrade": "Főverzió frissítés",
"Maximum Age": "Maximális kor",
"Metadata Only": "Csak metaadatok",
@@ -165,19 +165,19 @@
"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.": "A következő intervallumokat használjuk: egy régi verziót őrzünk meg az első órában minden 30 másodpercben, az első nap minden órában, az első 30 napban minden nap, egészen addig amíg el nem érjük a maximálisan megtartható verziók számát minden héten.",
"The maximum age must be a number and cannot be blank.": "A maximális kornak számnak kell lenni és nem lehet üres",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "A verziók megtartásának maximális ideje (napokban, ha 0-t adsz meg örökre megmaradnak).",
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
"The number of days to keep files in the trash can. Zero means forever.": "The number of days to keep files in the trash can. Zero means forever.",
"The number of days must be a number and cannot be blank.": "A napok száma szám kell legyen és nem lehet üres.",
"The number of days to keep files in the trash can. Zero means forever.": "A napok száma ameddig a fájlok meg lesznek tartva a lomtárban. A 0 azt jelenti örökre.",
"The number of old versions to keep, per file.": "A megtartott régi verziók száma, fájlonként.",
"The number of versions must be a number and cannot be blank.": "A megtartott verziók száma nem lehet üres",
"The path cannot be blank.": "Elérési út nem lehet üres.",
"The rescan interval must be a non-negative number of seconds.": "Az átnézési intervallum nullánál nagyobb másodperc érték kell legyen",
"This is a major version upgrade.": "Ez egy főverzió frissítés.",
"Trash Can File Versioning": "Trash Can File Versioning",
"Trash Can File Versioning": "Lomtár fájl verziózás",
"Unknown": "Ismeretlen",
"Unshared": "Nincs megosztva",
"Unused": "Nincs használatban",
"Up to Date": "Friss",
"Updated": "Updated",
"Updated": "Frissítve",
"Upgrade": "Frissítés",
"Upgrade To {%version%}": "Frissítés a {{version}} verzióra",
"Upgrading": "Frissítés",

View File

@@ -1,5 +1,5 @@
{
"A negative number of days doesn't make sense.": "A negative number of days doesn't make sense.",
"A negative number of days doesn't make sense.": "Un numero di giorni negativo non ha alcun senso.",
"A new major version may not be compatible with previous versions.": "Una nuova versione principale potrebbe non essere compatibile con le versioni precedenti.",
"API Key": "Chiave API",
"About": "Informazioni",
@@ -20,7 +20,7 @@
"Bugs": "Bug",
"CPU Utilization": "Utilizzo CPU",
"Changelog": "Changelog",
"Clean out after": "Clean out after",
"Clean out after": "Svuota dopo",
"Close": "Chiudi",
"Command": "Comando",
"Comment, when used at the start of a line": "Per commentare, va inserito all'inizio di una riga",
@@ -30,7 +30,7 @@
"Copied from original": "Copiato dall'originale",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 i seguenti Collaboratori:",
"Delete": "Elimina",
"Deleted": "Deleted",
"Deleted": "Cancellato",
"Device ID": "ID Dispositivo",
"Device Identification": "Identificazione Dispositivo",
"Device Name": "Nome Dispositivo",
@@ -53,7 +53,7 @@
"File Pull Order": "Ordine di prelievo dei file",
"File Versioning": "Controllo Versione dei File",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Il software evita i bit dei permessi dei file durante il controllo delle modifiche. Utilizzato nei filesystem FAT.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Files are moved to .stversions folder when replaced or deleted by Syncthing.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "I file sono spostati nella certella .stversions quando vengono sostituiti o cancellati da Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "I file sostituiti o eliminati da Syncthing vengono datati e spostati in una cartella .stversions.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "I file sono protetti dalle modifiche effettuate negli altri dispositivi, ma le modifiche effettuate in questo dispositivo verranno inviate anche al resto del cluster.",
"Folder ID": "ID Cartella",
@@ -81,7 +81,7 @@
"Later": "Più Tardi",
"Local Discovery": "Individuazione Locale",
"Local State": "Stato Locale",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Stato Locale (Totale)",
"Major Upgrade": "Aggiornamento principale",
"Maximum Age": "Durata Massima",
"Metadata Only": "Solo i Metadati",
@@ -165,19 +165,19 @@
"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.": "Vengono utilizzati i seguenti intervalli temporali: per la prima ora viene mantenuta una versione ogni 30 secondi, per il primo giorno viene mantenuta una versione ogni ora, per i primi 30 giorni viene mantenuta una versione al giorno, successivamente viene mantenuta una versione ogni settimana fino al periodo massimo impostato.",
"The maximum age must be a number and cannot be blank.": "La durata massima dev'essere un numero e non può essere vuoto.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "La durata massima di una versione (in giorni, imposta a 0 per mantenere le versioni per sempre).",
"The number of days must be a number and cannot be blank.": "The number of days must be a number and cannot be blank.",
"The number of days to keep files in the trash can. Zero means forever.": "The number of days to keep files in the trash can. Zero means forever.",
"The number of days must be a number and cannot be blank.": "Il numero di giorni deve essere un numero e non può essere vuoto.",
"The number of days to keep files in the trash can. Zero means forever.": "Il numero di giorni per conservare i file nel cestino. Zero significa per sempre.",
"The number of old versions to keep, per file.": "Il numero di vecchie versioni da mantenere, per file.",
"The number of versions must be a number and cannot be blank.": "Il numero di versioni dev'essere un numero e non può essere vuoto.",
"The path cannot be blank.": "Il percorso non può essere vuoto.",
"The rescan interval must be a non-negative number of seconds.": "L'intervallo di scansione deve essere un numero superiore a zero secondi.",
"This is a major version upgrade.": "Questo è un aggiornamento di versione principale",
"Trash Can File Versioning": "Trash Can File Versioning",
"Trash Can File Versioning": "Controllo Versione con Cestino",
"Unknown": "Sconosciuto",
"Unshared": "Non Condiviso",
"Unused": "Non Utilizzato",
"Up to Date": "Sincronizzato",
"Updated": "Updated",
"Updated": "Aggiornato",
"Upgrade": "Aggiornamento",
"Upgrade To {%version%}": "Aggiorna alla {{version}}",
"Upgrading": "Aggiornamento",

View File

@@ -0,0 +1,197 @@
{
"A negative number of days doesn't make sense.": "負の日数は無理です。",
"A new major version may not be compatible with previous versions.": "新しいメジャーバージョンは以前のバージョンと互換性がないかもしれません",
"API Key": "APIキー",
"About": "Syncthingについて",
"Actions": "メニュー",
"Add": "追加",
"Add Device": "デバイスの追加",
"Add Folder": "フォルダの追加",
"Add new folder?": "フォルダを新規作成しますか?",
"Address": "アドレス",
"Addresses": "アドレス",
"All Data": "全てのデータ",
"Allow Anonymous Usage Reporting?": "匿名での利用者状況のレポートを許可しますか?",
"Alphabetic": "ABC順",
"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.": "紹介デバイスで設定されたデバイスはここにも追加されます。",
"Automatic upgrades": "自動アップデート",
"Bugs": "バグ",
"CPU Utilization": "CPU使用率",
"Changelog": "更新履歴",
"Clean out after": "後で掃除",
"Close": "閉じる",
"Command": "コマンド",
"Comment, when used at the start of a line": "行頭で使用されるコメント",
"Compression": "圧縮",
"Connection Error": "接続エラー",
"Copied from elsewhere": "他の所からコピーしました",
"Copied from original": "オリジナルからコピーしました",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 以下の協力者たちの皆さん:",
"Delete": "削除",
"Deleted": "削除した",
"Device ID": "デバイスID",
"Device Identification": "デバイスの身分証明書",
"Device Name": "デバイスの名前",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "デバイス{{device}} ({{address}})が接続しますか? ",
"Devices": "デバイス",
"Disconnected": "切断されました",
"Documentation": "マニュアル",
"Download Rate": "ダウンロード率",
"Downloaded": "ダウンロード済",
"Downloading": "ダウンロード中",
"Edit": "編集",
"Edit Device": "デバイスの変更",
"Edit Folder": "フォルダーの変更",
"Editing": "編集中",
"Enable UPnP": "UPnPを許可する",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "自動接続の場合は「dynamic」またはカンマ区切り「IPアドレス:ポート」を入力をしてください",
"Enter ignore patterns, one per line.": "無視パターンを入力してください。一列一条件。",
"Error": "エラー",
"External File Versioning": "外部ファイルバージョニング",
"File Pull Order": "ファイルの引き順番",
"File Versioning": "ファイルバージョニング",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "更新時、ファイルパーミッションの設定が無視されます。FATファイルシステムでご利用ください。",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Syncthingによって移動や削除が行われるとファイルは.stversionsフォルダに移されます。",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Syncthingによって移動や削除が行われるとファイルは.stversionsフォルダ内のタイムスタンプバージョンに移されます。",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "ファイルは他デバイスによる変更から保護されます。しかしこのデバイス上での変更は他のクラスタに送信されます。",
"Folder ID": "フォルダID",
"Folder Master": "フォルダのマスター",
"Folder Path": "フォルダパス",
"Folders": "フォルダ",
"GUI Authentication Password": "GUI 認証パスワード",
"GUI Authentication User": "GUI 認証ユーザー",
"GUI Listen Addresses": "GUIリスンアドレス",
"Generate": "生成",
"Global Discovery": "グローバルディスカバリー",
"Global Discovery Server": "グローバルディスカバリーサーバー",
"Global State": "グローバル状態",
"Help": "ヘルプ",
"Ignore": "無視",
"Ignore Patterns": "パターンを無視する",
"Ignore Permissions": "アクセス許可を無視する",
"Incoming Rate Limit (KiB/s)": "着信率制限(KiB/s)",
"Introducer": "紹介デバイス",
"Inversion of the given condition (i.e. do not exclude)": "条件の裏(と言うのは省かないで)",
"Keep Versions": "バージョン保持",
"Largest First": "大きい順",
"Last File Received": "最後に受けとったファイル",
"Last seen": "最後に見た",
"Later": "後",
"Local Discovery": "ローカルディスカバリー",
"Local State": "ローカル状態",
"Local State (Total)": "ローカル状態(総和)",
"Major Upgrade": "メジャーアップグレード",
"Maximum Age": "再",
"Metadata Only": "メータデータだけ",
"Move to top of queue": "最優先にする",
"Multi level wildcard (matches multiple directory levels)": "広範なワイルドカード(複数のディレクトリに適用されます)",
"Never": "決して",
"New Device": "新規デバイス",
"New Folder": "新規フォルダ",
"Newest First": "新しい順",
"No": "いいえ",
"No File Versioning": "ファイルバージョニング不利用",
"Notice": "通知",
"OK": "OK",
"Off": "オフ",
"Oldest First": "古い順",
"Out Of Sync": "シンク外",
"Out of Sync Items": "シンクアイテム外",
"Outgoing Rate Limit (KiB/s)": "発信率制限(KiB/s)",
"Override Changes": "変更をオーバーライドする",
"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": "ローカルコンピュータ上のフォルダパス。存在しない場合は作成されます。チルダ(~)をショートカットで利用することができます",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "バージョンが保持されるパス(空欄の場合、デフォルトで.stversionsになります)",
"Please consult the release notes before performing a major upgrade.": "メジャーアップグレードをする前にリリースノートを参考してください。",
"Please wait": "お待ちください",
"Preview": "プレビュー",
"Preview Usage Report": "利用状況レポートのプレビュー",
"Quick guide to supported patterns": "サポートされているパターンの簡易ガイド",
"RAM Utilization": "メモリ利用率",
"Random": "ランダム",
"Release Notes": "リリースノート",
"Rescan": "再スキャン",
"Rescan All": "すべて再スキャン",
"Rescan Interval": "再スキャンの間隔",
"Restart": "再起動",
"Restart Needed": "再起動が必要です",
"Restarting": "再起動中",
"Reused": "再使用されている",
"Save": "保存",
"Scanning": "スキャン中",
"Select the devices to share this folder with.": "このフォルダをシェアするデバイスを選んでください。",
"Select the folders to share with this device.": "このデバイスでシェアしたいフォルダを選んでください",
"Settings": "設定",
"Share": "共有",
"Share Folder": "フォルダを共有する",
"Share Folders With Device": "デバイスでフォルダをシェアする",
"Share With Devices": "デバイスでシェアする",
"Share this folder?": "このフォルダを共有しますか?",
"Shared With": "シェアされている",
"Short identifier for the folder. Must be the same on all cluster devices.": "このフォルダの短いID。全てのデバイス上で同じである必要があります。",
"Show ID": "IDを表示",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "クラスタステータスでデバイスIDの代わりに表示されます。他のデバイス上でもこれがデフォルトとして表示されます。",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "クラスタステータスでデバイスIDの代わりに表示されます。空欄の場合デバイスが要請する名前に更新されます。",
"Shutdown": "シャットダウン",
"Shutdown Complete": "シャットダウン完了",
"Simple File Versioning": "簡易ファイルバージョニング",
"Single level wildcard (matches within a directory only)": "ワイルドカード(一つのディレクトリだけに適用されます)",
"Smallest First": "小さい順",
"Source Code": "ソースコード",
"Staggered File Versioning": "簡易ファイルバージョニング",
"Start Browser": "ブラウザーを起動する",
"Stopped": "止り",
"Support": "サポート",
"Sync Protocol Listen Addresses": "同期プロトコル待ち受けるアドレス",
"Syncing": "同期中",
"Syncthing has been shut down.": "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が落ちているか、インターネット接続に問題があります。リトライ中です…",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "リクエストの処理に問題があるようです。問題が継続する場合、ページを更新するかSyncthingを再起動してください。",
"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.": "デバイスIDは空欄にできません",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ここで入力したデバイスIDは他デバイス上の\"編集 > IDを表示\"で見ることができます。スペースとハイフンは無視されます。",
"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.": "暗号化された使用状況レポートが\b日ごとに送られます。これはプラットフォーム、フォルダの大きさ、アプリのバージョンを追跡するために利用されます。レポートのデータが変更された場合、このダイアログがまた表示されます。",
"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.": "入力されたデバイスIDが正しくありません。52から56文字のアルファベットと数字かスペース、ハイフンの列である必要があります。",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "第一コマンドパラメータはフォルダパス、第二パラメータはフォルダ内の相対パスです。",
"The folder ID cannot be blank.": "フォルダIDは空欄にできません",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "フォルダID(64文字以内)は数字、ドット(.)、ハイフン(-)、アンダースコア(_)で構成されている必要があります。",
"The folder ID must be unique.": "フォルダIDは固有である必要があります。",
"The folder path cannot be blank.": "フォルダーパスは空欄にできません",
"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.": "以下の間隔が使われます: 最初の一時間はバージョンは30秒ごとに保持、最初の一日は一時間ごとに、最初の30日は一日ごとに、最大寿命までは一週間ごとに。",
"The maximum age must be a number and cannot be blank.": "最大日数は番号である必要があり、空欄ではいけません。",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "バージョンを保持する最大日数(0にすると永続的に保持します)",
"The number of days must be a number and cannot be blank.": "日数は番号である必要があり、空欄ではいけません。",
"The number of days to keep files in the trash can. Zero means forever.": "ゴミ箱にファイルを保持する日数。0だと永続的に保持します。",
"The number of old versions to keep, per file.": "ファイルごとの保持する古いバージョンの数",
"The number of versions must be a number and cannot be blank.": "バージョンの数は番号である必要があり、空欄ではいけません。",
"The path cannot be blank.": "パスは空欄にできません",
"The rescan interval must be a non-negative number of seconds.": "リスキャン間隔はマイナス秒ではいけません。",
"This is a major version upgrade.": "メージャーアップグレードです。",
"Trash Can File Versioning": "ゴミ箱のファイルバージョニング",
"Unknown": "不明",
"Unshared": "シェアされていない",
"Unused": "使われていない",
"Up to Date": "最新",
"Updated": "更新済み",
"Upgrade": "アップグレード",
"Upgrade To {%version%}": "{{version}}にアップグレードする",
"Upgrading": "アップグレード中",
"Upload Rate": "アップロード率",
"Uptime": "稼働時間",
"Use HTTPS for GUI": "GUIにHTTPSを使う",
"Version": "バージョン",
"Versions Path": "バージョンパス",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "バージョンは、最大寿命もしくは最大同時数を超えた場合、自動的に削除されます。",
"When adding a new device, keep in mind that this device must be added on the other side too.": "新しいデバイスを加える際、そのデバイスにもこのデバイスを加える必要があることを留意してください。",
"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.": "新しいフォルダを追加する際、フォルダIDはケースセンシティブで全てのデバイス上で完全に同じである必要があります。",
"Yes": "はい",
"You must keep at least one version.": "バージョン一つ少なくとも保持してください",
"full documentation": "完全マニュアル",
"items": "アイテム",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}}がフォルダ\"{{folder}}\"をシェアしがたっています。"
}

View File

@@ -30,7 +30,7 @@
"Copied from original": "Nukopijuota iš originalo",
"Copyright © 2015 the following Contributors:": "Visos teisės saugomos © 2015 šių bendraautorių:",
"Delete": "Trinti",
"Deleted": "Deleted",
"Deleted": "Ištrinta",
"Device ID": "Įrenginio ID",
"Device Identification": "Įrenginio identifikacija",
"Device Name": "Įrenginio pavadinimas",
@@ -81,7 +81,7 @@
"Later": "Vėliau",
"Local Discovery": "Vietinis matomumas",
"Local State": "Vietinė būsena",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Vietinė būsena (Bendrai)",
"Major Upgrade": "Stambus atnaujinimas",
"Maximum Age": "Maksimalus amžius",
"Metadata Only": "Metaduomenims",
@@ -172,12 +172,12 @@
"The path cannot be blank.": "Kelias negali būti tuščias.",
"The rescan interval must be a non-negative number of seconds.": "Nuskaitymo dažnis negali būti neigiamas skaičius.",
"This is a major version upgrade.": "Tai yra stambus atnaujinimas.",
"Trash Can File Versioning": "Šiukšliadėžės versiju valdymas",
"Trash Can File Versioning": "Šiukšliadėžės versijų valdymas",
"Unknown": "Nežinoma",
"Unshared": "Nesidalinama",
"Unused": "Nenaudojamas",
"Up to Date": "Atnaujinta",
"Updated": "Updated",
"Updated": "Atnaujinta",
"Upgrade": "Atnaujinimas",
"Upgrade To {%version%}": "Atnaujinti į {{version}}",
"Upgrading": "Atnaujinama",

View File

@@ -30,7 +30,7 @@
"Copied from original": "Kopiert fra original",
"Copyright © 2015 the following Contributors:": "Kopirett © 2015 de følgende bidragsytere:",
"Delete": "Slett",
"Deleted": "Deleted",
"Deleted": "Slettet",
"Device ID": "Enhets ID",
"Device Identification": "Enhetskjennemerke",
"Device Name": "Navn På Enhet",
@@ -81,7 +81,7 @@
"Later": "Senere",
"Local Discovery": "Lokal Søking",
"Local State": "Lokal Tilstand",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Lokal Tilstand (Total)",
"Major Upgrade": "Hovedoppgradering",
"Maximum Age": "Maksimal Levetid",
"Metadata Only": "Kun metadata",
@@ -177,7 +177,7 @@
"Unshared": "Ikke delt",
"Unused": "Ikke i bruk",
"Up to Date": "Oppdatert",
"Updated": "Updated",
"Updated": "Oppdatert",
"Upgrade": "Oppgradere",
"Upgrade To {%version%}": "Oppgrader Til {{version}}",
"Upgrading": "Oppgraderer",

View File

@@ -30,7 +30,7 @@
"Copied from original": "Copiado do original",
"Copyright © 2015 the following Contributors:": "Copyright © 2015. Direitos reservados aos seguintes colaboradores:",
"Delete": "Apagar",
"Deleted": "Deleted",
"Deleted": "Apagado",
"Device ID": "ID do dispositivo",
"Device Identification": "Identificação do dispositivo",
"Device Name": "Nome do dispositivo",
@@ -81,7 +81,7 @@
"Later": "Depois",
"Local Discovery": "Descoberta local",
"Local State": "Estado local",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Estado local (total)",
"Major Upgrade": "Atualização \"major\"",
"Maximum Age": "Idade máxima",
"Metadata Only": "Somente metadados",
@@ -97,8 +97,8 @@
"OK": "OK",
"Off": "Desligada",
"Oldest First": "Mais antigo primeiro",
"Out Of Sync": "Não sincronizado",
"Out of Sync Items": "Itens não sincronizados",
"Out Of Sync": "Fora de sincronia",
"Out of Sync Items": "Itens fora de sincronia",
"Outgoing Rate Limit (KiB/s)": "Limite de velocidade de envio (KiB/s)",
"Override Changes": "Sobrescrever mudanças",
"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": "Caminho para a pasta na máquina local. Será criado caso não exista. O caractere til (~) pode ser usado como um atalho para",
@@ -177,7 +177,7 @@
"Unshared": "Não compartilhada",
"Unused": "Não utilizado",
"Up to Date": "Sincronizada",
"Updated": "Updated",
"Updated": "Atualizado",
"Upgrade": "Atualização",
"Upgrade To {%version%}": "Atualizar para {{version}}",
"Upgrading": "Atualizando",

View File

@@ -20,7 +20,7 @@
"Bugs": "Erros",
"CPU Utilization": "Utilização da CPU",
"Changelog": "Registo de alterações",
"Clean out after": "Limpar e arrumar após a acção",
"Clean out after": "Esvaziar ao fim de",
"Close": "Fechar",
"Command": "Comando",
"Comment, when used at the start of a line": "Comentário, quando usado no início de uma linha",
@@ -30,7 +30,7 @@
"Copied from original": "Copiado do original",
"Copyright © 2015 the following Contributors:": "Copyright © 2015 os seguintes contribuidores:",
"Delete": "Eliminar",
"Deleted": "Deleted",
"Deleted": "Eliminado",
"Device ID": "ID do dispositivo",
"Device Identification": "Identificação do dispositivo",
"Device Name": "Nome do dispositivo",
@@ -81,7 +81,7 @@
"Later": "Mais tarde",
"Local Discovery": "Busca local",
"Local State": "Estado local",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Estado local (total)",
"Major Upgrade": "Actualização importante",
"Maximum Age": "Idade máxima",
"Metadata Only": "Metadados apenas",
@@ -95,7 +95,7 @@
"No File Versioning": "Nenhuma",
"Notice": "Avisos",
"OK": "OK",
"Off": "Desligado",
"Off": "Desligada",
"Oldest First": "Primeiro os mais antigos",
"Out Of Sync": "Não sincronizado",
"Out of Sync Items": "Itens por sincronizar",
@@ -164,7 +164,7 @@
"The folder path cannot be blank.": "O caminho da pasta não pode estar vazio.",
"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.": "São utilizados os seguintes intervalos: na primeira hora é guardada uma versão a cada 30 segundos, no primeiro dia é guardada uma versão a cada hora, nos primeiros 30 dias é guardada uma versão por dia e, até que atinja a idade máxima, é guardada uma versão por semana.",
"The maximum age must be a number and cannot be blank.": "A idade máxima tem que ser um número e não pode estar vazia.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Tempo máximo para manter uma versão (em dias, use 0 para manter a versão para sempre).",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Tempo máximo, em dias, para manter uma versão (use 0 para manter a versão para sempre).",
"The number of days must be a number and cannot be blank.": "O número de dias tem que ser um número e não pode estar em branco.",
"The number of days to keep files in the trash can. Zero means forever.": "O número de dias a manter os ficheiros na reciclagem. Zero significa para sempre.",
"The number of old versions to keep, per file.": "O número de versões antigas a manter, por ficheiro.",
@@ -177,7 +177,7 @@
"Unshared": "Não partilhada",
"Unused": "Não utilizado",
"Up to Date": "Sincronizado",
"Updated": "Updated",
"Updated": "Actualizado",
"Upgrade": "Actualizar",
"Upgrade To {%version%}": "Actualizar para {{version}}",
"Upgrading": "Actualizando",

View File

@@ -30,7 +30,7 @@
"Copied from original": "Скопировано с оригинала",
"Copyright © 2015 the following Contributors:": "Все права защищены ©, 2015 участники:",
"Delete": "Удалить",
"Deleted": "Deleted",
"Deleted": "Удалено",
"Device ID": "ID устройства",
"Device Identification": "Идентификация устройства",
"Device Name": "Имя устройства",
@@ -81,7 +81,7 @@
"Later": "Потом",
"Local Discovery": "Локальное обнаружение",
"Local State": "Локальное состояние",
"Local State (Total)": "Local State (Total)",
"Local State (Total)": "Локально (всего)",
"Major Upgrade": "Обновление основной версии",
"Maximum Age": "Максимальный срок",
"Metadata Only": "Только метаданные",
@@ -177,7 +177,7 @@
"Unshared": "Необщедоступно",
"Unused": "Не используется",
"Up to Date": "Обновлено",
"Updated": "Updated",
"Updated": "Обновлено",
"Upgrade": "Обновить",
"Upgrade To {%version%}": "Обновить до {{version}}",
"Upgrading": "Обновление",

View File

@@ -1 +1 @@
var langPrettyprint = {"bg":"Bulgarian","ca":"Catalan","ca@valencia":"Catalan (Valencian)","cs":"Czech","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","hu":"Hungarian","it":"Italian","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro-RO":"Romanian (Romania)","ru":"Russian","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
var langPrettyprint = {"bg":"Bulgarian","ca":"Catalan","ca@valencia":"Catalan (Valencian)","cs":"Czech","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ro-RO":"Romanian (Romania)","ru":"Russian","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}

View File

@@ -1 +1 @@
var validLangs = ["bg","ca","ca@valencia","cs","de","el","en","en-GB","es","es-ES","fi","fr","hu","it","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ro-RO","ru","sv","tr","uk","zh-CN","zh-TW"]
var validLangs = ["bg","ca","ca@valencia","cs","de","el","en","en-GB","es","es-ES","fi","fr","hu","it","ja","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ro-RO","ru","sv","tr","uk","zh-CN","zh-TW"]

View File

@@ -63,6 +63,8 @@
</a>
</li>
<li><a href="" ng-click="about()"><span class="glyphicon glyphicon-heart-empty"></span>&nbsp;<span translate>About</span></a></li>
<li class="divider"></li>
<li><a href="" ng-click="advanced()"><span class="glyphicon glyphicon-cog"></span>&nbsp;<span translate>Advanced</span></a></li>
</ul>
</li>
</ul>
@@ -196,6 +198,7 @@
<span class="hidden-xs" translate>Syncing</span>
({{syncPercentage(folder.id)}}%)
</span>
<span ng-switch-when="outofsync"><span class="hidden-xs" translate>Out of Sync</span><span class="visible-xs">&#9724;</span></span>
</span>
</h3>
</div>
@@ -220,11 +223,22 @@
<td class="text-right">{{model[folder.id].localFiles | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].localBytes | binary}}B</td>
</tr>
<tr ng-if="model[folder.id].needFiles > 0">
<th><span class="glyphicon glyphicon-cloud-download"></span>&nbsp;<span translate>Out Of Sync</span></th>
<th><span class="glyphicon glyphicon-cloud-download"></span>&nbsp;<span translate>Out of Sync</span></th>
<td class="text-right">
<a ng-click="showNeed(folder.id)" href="">{{model[folder.id].needFiles | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].needBytes | binary}}B</a>
</td>
</tr>
<tr ng-if="!folder.readOnly && (folderStatus(folder) === 'outofsync' || hasFailedFiles(folder.id))">
<th><span class="glyphicon glyphicon-exclamation-sign"></span>&nbsp;<span translate>Failed Items</span></th>
<!-- Show the number of failed items as a link to bring up the list. -->
<td ng-if="hasFailedFiles(folder.id)" class="text-right">
<a ng-click="showFailed(folder.id)" href="">{{failed[folder.id].length | alwaysNumber}}&nbsp;<span translate>items</span></a>
</td>
<!-- The list of failed items hasn't loaded yet; show an ellipsis for the time being. -->
<td ng-if="!hasFailedFiles(folder.id)" class="text-right">
...
</td>
</tr>
<tr ng-if="folder.readOnly">
<th><span class="glyphicon glyphicon-lock"></span>&nbsp;<span translate>Folder Master</span></th>
<td class="text-right">
@@ -285,7 +299,7 @@
</table>
</div>
<div class="panel-footer">
<button class="btn btn-sm btn-danger pull-left" ng-if="folderStatus(folder) == 'idle' && folder.readOnly && model[folder.id].needFiles > 0" ng-click="override(folder.id)" href=""><span class="glyphicon glyphicon-upload"></span>&nbsp;<span translate>Override Changes</span></button>
<button class="btn btn-sm btn-danger pull-left" ng-if="folderStatus(folder) == 'outofsync' && folder.readOnly" ng-click="override(folder.id)" href=""><span class="glyphicon glyphicon-upload"></span>&nbsp;<span translate>Override Changes</span></button>
<span class="pull-right">
<button class="btn btn-sm btn-default" href="" ng-show="['idle', 'stopped'].indexOf(folderStatus(folder)) > -1" ng-click="rescanFolder(folder.id)"><span class="glyphicon glyphicon-refresh"></span>&nbsp;<span translate>Rescan</span></button>
<button class="btn btn-sm btn-default" href="" ng-click="editFolder(folder)"><span class="glyphicon glyphicon-pencil"></span>&nbsp;<span translate>Edit</span></button>
@@ -448,6 +462,7 @@
<nav class="navbar navbar-default navbar-fixed-bottom">
<div class="container">
<ul class="nav navbar-nav">
<li><a class="navbar-link" href="http://syncthing.net/" target="_blank"><span class="glyphicon glyphicon-home"></span>&nbsp;<span translate>Home page</span></a></li>
<li><a class="navbar-link" href="http://docs.syncthing.net/" target="_blank"><span class="glyphicon glyphicon-book"></span>&nbsp;<span translate>Documentation</span></a></li>
<li><a class="navbar-link" href="https://forum.syncthing.net" target="_blank"><span class="glyphicon glyphicon-question-sign"></span>&nbsp;<span translate>Support</span></a></li>
<li><a class="navbar-link" href="https://github.com/syncthing/syncthing/releases" target="_blank"><span class="glyphicon glyphicon-info-sign"></span>&nbsp;<span translate>Changelog</span></a></li>
@@ -495,7 +510,7 @@
<!-- ID modal -->
<modal id="idqr" large="yes" status="info" close="yes" icon="qrcode" title="{{'Device Identification' | translate}} &mdash; {{deviceName(thisDevice())}}">
<div class="well well-sm text-monospace text-center">{{myID}}</div>
<div select-on-click class="well well-sm text-monospace text-center">{{myID}}</div>
<img ng-if="myID" class="center-block img-thumbnail" ng-src="qr/?text={{myID}}"/>
</modal>
@@ -561,7 +576,7 @@
</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>
<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 "ip:port" addresses or "dynamic" to perform automatic discovery of the address.</p>
</div>
<div ng-if="!editingSelf" class="form-group">
@@ -825,7 +840,7 @@
</div>
<div class="form-group">
<label translate for="ListenAddressStr">Sync Protocol Listen Addresses</label>
<input id="ListenAddressStr" class="form-control" type="text" ng-model="tmpOptions.listenAddressStr">
<input id="ListenAddressStr" class="form-control" type="text" ng-model="tmpOptions._listenAddressStr">
</div>
<div class="form-group">
<label translate for="MaxRecvKbps">Incoming Rate Limit (KiB/s)</label>
@@ -869,7 +884,7 @@
</div>
<div class="form-group">
<label translate for="GlobalAnnServersStr">Global Discovery Server</label>
<input ng-disabled="!tmpOptions.globalAnnounceEnabled" id="GlobalAnnServersStr" class="form-control" type="text" ng-model="tmpOptions.globalAnnounceServersStr">
<input ng-disabled="!tmpOptions.globalAnnounceEnabled" id="GlobalAnnServersStr" class="form-control" type="text" ng-model="tmpOptions._globalAnnounceServersStr">
</div>
</div>
@@ -985,7 +1000,7 @@
<table class="table table-striped table-condensed">
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="neededTotal">
<tr dir-paginate="f in needed | itemsPerPage: neededPageSize" current-page="neededCurrentPage" total-items="neededTotal" pagination-id="needed">
<!-- Icon -->
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[f.action]}}"></span> {{needActions[f.action]}}</td>
@@ -1018,15 +1033,37 @@
</tr>
</table>
<dir-pagination-controls on-page-change="neededPageChanged(newPageNumber)"></dir-pagination-controls>
<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, 20, 30, 50, 100]" ng-class="{ active: neededPageSize == option }">
<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>
<!-- Failed Items modal -->
<modal id="failed" large="yes" status="warning" icon="exclamation-sign" close="yes" title="{{'Failed Items' | translate}}">
<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>
<!-- About modal -->
<modal id="about" large="yes" close="yes" status="info" title="{{'About' | translate}}">
@@ -1054,6 +1091,7 @@
<li class="auto-generated">Chris Howie</li>
<li class="auto-generated">Chris Joel</li>
<li class="auto-generated">Colin Kennedy</li>
<li class="auto-generated">Daniel Bergmann</li>
<li class="auto-generated">Daniel Martí</li>
<li class="auto-generated">Dennis Wilson</li>
<li class="auto-generated">Dominik Heidler</li>
@@ -1114,12 +1152,95 @@
</ul>
</modal>
<!-- Advanced modal -->
<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 translate class="modal-title">Advanced Configuration</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 config.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="config.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 config.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="config.options[key]" />
</div>
</div>
</form>
</div>
</div>
</div>
<div class="panel panel-default" ng-repeat="(folder, _) in folders">
<div class="panel-heading" role="tab" id="folder{{folder}}Heading" data-toggle="collapse" data-parent="#advancedAccordion" href="#folder{{folder}}Config" aria-expanded="true" aria-controls="folder{{folder}}Config" style="cursor: pointer">
<h4 class="panel-title">
<span translate>Folder</span> "{{folder}}"
</h4>
</div>
<div id="folder{{folder}}Config" class="panel-collapse collapse" role="tabpanel" aria-labelledby="folder{{folder}}Heading">
<div class="panel-body">
<form class="form-horizontal" role="form">
<div ng-repeat="(key, value) in folders[folder]" ng-if="inputTypeFor(key, value) != 'skip'" class="form-group">
<label for="foldre{{folder}}Input{{$index}}" class="col-sm-4 control-label">{{key}}</label>
<div class="col-sm-8">
<input id="folder{{folder}}Input{{$index}}" class="form-control" type="{{inputTypeFor(key, value)}}" ng-model="folders[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="glyphicon glyphicon-ok"></span>&nbsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&nbsp;<span translate>Close</span></button>
</div>
</div>
</div>
</div>
</div>
<!-- vendor scripts -->
<script src="vendor/jquery/jquery-2.0.3.min.js"></script>
<script src="vendor/angular/angular.min.js"></script>
<script src="vendor/angular/angular-translate.min.js"></script>
<script src="vendor/angular/angular-translate-loader.min.js"></script>
<script src="vendor/angular/angular-dirPagination.js"></script>
<script src="vendor/jquery/jquery-2.0.3.min.js"></script>
<script src="vendor/bootstrap/js/bootstrap.min.js"></script>
<!-- / vendor scripts -->
@@ -1133,11 +1254,13 @@
<script src="scripts/syncthing/core/directives/uniqueFolderDirective.js"></script>
<script src="scripts/syncthing/core/directives/validDeviceidDirective.js"></script>
<script src="scripts/syncthing/core/directives/popoverDirective.js"></script>
<script src="scripts/syncthing/core/directives/selectOnClickDirective.js"></script>
<script src="scripts/syncthing/core/filters/alwaysNumberFilter.js"></script>
<script src="scripts/syncthing/core/filters/basenameFilter.js"></script>
<script src="scripts/syncthing/core/filters/binaryFilter.js"></script>
<script src="scripts/syncthing/core/filters/durationFilter.js"></script>
<script src="scripts/syncthing/core/filters/naturalFilter.js"></script>
<script src="scripts/syncthing/core/filters/lastErrorComponentFilter.js"></script>
<script src="scripts/syncthing/core/services/localeService.js"></script>
<script src="assets/lang/valid-langs.js"></script>

View File

@@ -11,16 +11,14 @@
var syncthing = angular.module('syncthing', [
'angularUtils.directives.dirPagination',
'pascalprecht.translate',
'syncthing.core'
]);
var urlbase = 'rest';
var guiVersion = null;
var deviceId = null;
syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvider) {
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token';
$httpProvider.interceptors.push(function () {
return {
response: function (response) {
@@ -30,6 +28,14 @@ syncthing.config(function ($httpProvider, $translateProvider, LocaleServiceProvi
} else if (guiVersion != responseVersion) {
document.location.reload(true);
}
if (!deviceId) {
deviceId = response.headers()['x-syncthing-id'];
if (deviceId) {
var deviceIdShort = deviceId.substring(0, 5);
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token-' + deviceIdShort;
$httpProvider.defaults.xsrfCookieName = 'CSRF-Token-' + deviceIdShort;
}
}
return response;
}
};

View File

@@ -18,8 +18,7 @@ angular.module('syncthing.core')
Events.start();
}
// pubic/scope definitions
// public/scope definitions
$scope.completion = {};
$scope.config = {};
@@ -47,6 +46,10 @@ angular.module('syncthing.core')
$scope.neededPageSize = 10;
$scope.foldersTotalLocalBytes = 0;
$scope.foldersTotalLocalFiles = 0;
$scope.failed = {};
$scope.failedCurrentPage = 1;
$scope.failedCurrentFolder = undefined;
$scope.failedPageSize = 10;
$(window).bind('beforeunload', function () {
navigatingAway = true;
@@ -144,6 +147,13 @@ angular.module('syncthing.core')
if ($scope.model[data.folder]) {
$scope.model[data.folder].state = data.to;
$scope.model[data.folder].error = data.error;
// If a folder has started syncing, then any old list of
// errors is obsolete. We may get a new list of errors very
// shortly though.
if (data.to === 'syncing') {
$scope.failed[data.folder] = [];
}
}
});
@@ -151,14 +161,6 @@ angular.module('syncthing.core')
refreshFolderStats();
});
/* currently not using
$scope.$on('Events.REMOTE_INDEX_UPDATED', function (event, arg) {
// Nothing
});
*/
$scope.$on(Events.DEVICE_DISCONNECTED, function (event, arg) {
delete $scope.connections[arg.data.id];
refreshDeviceStats();
@@ -284,6 +286,11 @@ angular.module('syncthing.core')
$scope.completion[data.device]._total = tot / cnt;
});
$scope.$on(Events.FOLDER_ERRORS, function (event, arg) {
var data = arg.data;
$scope.failed[data.folder] = data.errors;
});
$scope.emitHTTPError = function (data, status, headers, config) {
$scope.$emit('HTTPError', {data: data, status: status, headers: headers, config: config});
};
@@ -307,8 +314,8 @@ angular.module('syncthing.core')
var hasConfig = !isEmptyObject($scope.config);
$scope.config = config;
$scope.config.options.listenAddressStr = $scope.config.options.listenAddress.join(', ');
$scope.config.options.globalAnnounceServersStr = $scope.config.options.globalAnnounceServers.join(', ');
$scope.config.options._listenAddressStr = $scope.config.options.listenAddress.join(', ');
$scope.config.options._globalAnnounceServersStr = $scope.config.options.globalAnnounceServers.join(', ');
$scope.devices = $scope.config.devices;
$scope.devices.forEach(function (deviceCfg) {
@@ -492,6 +499,14 @@ angular.module('syncthing.core')
refreshNeed($scope.neededFolder);
};
$scope.failedPageChanged = function (page) {
$scope.failedCurrentPage = page;
};
$scope.failedChangePageSize = function (perpage) {
$scope.failedPageSize = perpage;
};
var refreshDeviceStats = debounce(function () {
$http.get(urlbase + "/stats/device").success(function (data) {
$scope.deviceStats = data;
@@ -526,6 +541,11 @@ angular.module('syncthing.core')
return 'unknown';
}
// after restart syncthing process state may be empty
if (!$scope.model[folderCfg.id].state) {
return 'unknown';
}
if (folderCfg.devices.length <= 1) {
return 'unshared';
}
@@ -534,47 +554,36 @@ angular.module('syncthing.core')
return 'stopped';
}
if ($scope.model[folderCfg.id].state == 'error') {
var state = '' + $scope.model[folderCfg.id].state;
if (state === 'error') {
return 'stopped'; // legacy, the state is called "stopped" in the GUI
}
// after restart syncthing process state may be empty
if (!$scope.model[folderCfg.id].state) {
return 'unknown';
if (state === 'idle' && $scope.model[folderCfg.id].needFiles > 0) {
return 'outofsync';
}
return '' + $scope.model[folderCfg.id].state;
return state;
};
$scope.folderClass = function (folderCfg) {
if (typeof $scope.model[folderCfg.id] === 'undefined') {
// Unknown
return 'info';
}
var status = $scope.folderStatus(folderCfg);
if (folderCfg.devices.length <= 1) {
// Unshared
return 'warning';
}
if ($scope.model[folderCfg.id].invalid !== '') {
// Errored
return 'danger';
}
var state = '' + $scope.model[folderCfg.id].state;
if (state == 'idle') {
if (status == 'idle') {
return 'success';
}
if (state == 'syncing') {
if (status == 'syncing' || status == 'scanning') {
return 'primary';
}
if (state == 'scanning') {
return 'primary';
if (status === 'unknown') {
return 'info';
}
if (state == 'error') {
if (status === 'unshared') {
return 'warning';
}
if (status === 'stopped' || status === 'outofsync' || status === 'error') {
return 'danger';
}
return 'info';
};
@@ -737,7 +746,7 @@ angular.module('syncthing.core')
$scope.config.gui = angular.copy($scope.tmpGUI);
['listenAddress', 'globalAnnounceServers'].forEach(function (key) {
$scope.config.options[key] = $scope.config.options[key + "Str"].split(/[ ,]+/).map(function (x) {
$scope.config.options[key] = $scope.config.options["_" + key + "Str"].split(/[ ,]+/).map(function (x) {
return x.trim();
});
});
@@ -748,6 +757,11 @@ angular.module('syncthing.core')
$('#settings').modal("hide");
};
$scope.saveAdvanced = function () {
$scope.saveConfig();
$('#advanced').modal("hide");
};
$scope.restart = function () {
restarting = true;
$('#restarting').modal();
@@ -798,7 +812,7 @@ angular.module('syncthing.core')
$scope.currentDevice = $.extend({}, deviceCfg);
$scope.editingExisting = true;
$scope.editingSelf = (deviceCfg.deviceID == $scope.myID);
$scope.currentDevice.addressesStr = deviceCfg.addresses.join(', ');
$scope.currentDevice._addressesStr = deviceCfg.addresses.join(', ');
if (!$scope.editingSelf) {
$scope.currentDevice.selectedFolders = {};
$scope.deviceFolders($scope.currentDevice).forEach(function (folder) {
@@ -820,7 +834,7 @@ angular.module('syncthing.core')
})
.then(function () {
$scope.currentDevice = {
addressesStr: 'dynamic',
_addressesStr: 'dynamic',
compression: 'metadata',
introducer: false,
selectedFolders: {}
@@ -865,7 +879,7 @@ angular.module('syncthing.core')
$scope.addNewDeviceID = function (device) {
var deviceCfg = {
deviceID: device,
addressesStr: 'dynamic',
_addressesStr: 'dynamic',
compression: 'metadata',
introducer: false,
selectedFolders: {}
@@ -876,7 +890,7 @@ angular.module('syncthing.core')
$scope.saveDeviceConfig = function (deviceCfg) {
var done, i;
deviceCfg.addresses = deviceCfg.addressesStr.split(',').map(function (x) {
deviceCfg.addresses = deviceCfg._addressesStr.split(',').map(function (x) {
return x.trim();
});
@@ -1277,6 +1291,23 @@ angular.module('syncthing.core')
});
};
$scope.showFailed = function (folder) {
$scope.failedCurrent = $scope.failed[folder]
$('#failed').modal().on('hidden.bs.modal', function () {
$scope.failedCurrent = undefined;
});
};
$scope.hasFailedFiles = function (folder) {
if (!$scope.failed[folder]) {
return false;
}
if ($scope.failed[folder].length == 0) {
return false;
}
return true
};
$scope.override = function (folder) {
$http.post(urlbase + "/db/override?folder=" + encodeURIComponent(folder));
};
@@ -1285,6 +1316,10 @@ angular.module('syncthing.core')
$('#about').modal('show');
};
$scope.advanced = function () {
$('#advanced').modal('show');
};
$scope.showReportPreview = function () {
$scope.reportPreview = true;
};
@@ -1335,6 +1370,22 @@ angular.module('syncthing.core')
return $scope.version.version + ', ' + os + ' (' + arch + ')';
};
$scope.inputTypeFor = function (key, value) {
if (key.substr(0, 1) === '_') {
return 'skip';
}
if (typeof value === 'number') {
return 'number';
}
if (typeof value === 'boolean') {
return 'checkbox';
}
if (typeof value === 'object') {
return 'skip';
}
return 'text';
};
// pseudo main. called on all definitions assigned
initController();
});

View File

@@ -0,0 +1,14 @@
angular.module('syncthing.core')
.directive('selectOnClick', function ($window) {
return {
link: function (scope, element, attrs) {
element.on('click', function() {
var selection = $window.getSelection();
var range = document.createRange();
range.selectNodeContents(element[0]);
selection.removeAllRanges();
selection.addRange(range);
});
}
};
});

View File

@@ -0,0 +1,12 @@
angular.module('syncthing.core')
.filter('lastErrorComponent', function () {
return function (input) {
if (input === undefined)
return "";
var parts = input.split(/:\s*/);
if (!parts || parts.length < 1) {
return input;
}
return parts[parts.length - 1];
};
});

View File

@@ -75,6 +75,7 @@ angular.module('syncthing.core')
STARTING: 'Starting', // Emitted exactly once, when Syncthing starts, before parsing configuration etc
STARTUP_COMPLETED: 'StartupCompleted', // Emitted exactly once, when initialization is complete and Syncthing is ready to start exchanging data with other devices
STATE_CHANGED: 'StateChanged', // Emitted when a folder changes state
FOLDER_ERRORS: 'FolderErrors', // Emitted when a folder has errors preventing a full sync
start: function() {
$http.get(urlbase + '/events?limit=1')

427
gui/vendor/bootstrap/config.json vendored Normal file
View File

@@ -0,0 +1,427 @@
{
"vars": {
"@gray-base": "#000",
"@gray-darker": "lighten(@gray-base, 13.5%)",
"@gray-dark": "lighten(@gray-base, 20%)",
"@gray": "lighten(@gray-base, 33.5%)",
"@gray-light": "lighten(@gray-base, 46.7%)",
"@gray-lighter": "lighten(@gray-base, 93.5%)",
"@brand-primary": "#3498db",
"@brand-success": "#2ecc71",
"@brand-info": "#9b59b6",
"@brand-warning": "#f1c40f",
"@brand-danger": "#e74c3c",
"@body-bg": "#fff",
"@text-color": "@gray-darker",
"@link-color": "@brand-primary",
"@link-hover-color": "darken(@link-color, 15%)",
"@link-hover-decoration": "underline",
"@font-family-sans-serif": "\"Helvetica Neue\", Helvetica, Arial, sans-serif",
"@font-family-serif": "Georgia, \"Times New Roman\", Times, serif",
"@font-family-monospace": "Menlo, Monaco, Consolas, \"Courier New\", monospace",
"@font-family-base": "@font-family-sans-serif",
"@font-size-base": "16px",
"@font-size-large": "ceil((@font-size-base * 1.25))",
"@font-size-small": "ceil((@font-size-base * 0.85))",
"@font-size-h1": "floor((@font-size-base * 2.6))",
"@font-size-h2": "floor((@font-size-base * 2.15))",
"@font-size-h3": "ceil((@font-size-base * 1.7))",
"@font-size-h4": "ceil((@font-size-base * 1.25))",
"@font-size-h5": "@font-size-base",
"@font-size-h6": "ceil((@font-size-base * 0.85))",
"@line-height-base": "1.5",
"@line-height-computed": "floor((@font-size-base * @line-height-base))",
"@headings-font-family": "inherit",
"@headings-font-weight": "500",
"@headings-line-height": "1.1",
"@headings-color": "inherit",
"@icon-font-path": "\"../fonts/\"",
"@icon-font-name": "\"glyphicons-halflings-regular\"",
"@icon-font-svg-id": "\"glyphicons_halflingsregular\"",
"@padding-base-vertical": "6px",
"@padding-base-horizontal": "12px",
"@padding-large-vertical": "10px",
"@padding-large-horizontal": "16px",
"@padding-small-vertical": "5px",
"@padding-small-horizontal": "10px",
"@padding-xs-vertical": "1px",
"@padding-xs-horizontal": "5px",
"@line-height-large": "1.3333333",
"@line-height-small": "1.5",
"@border-radius-base": "2px",
"@border-radius-large": "3px",
"@border-radius-small": "1px",
"@component-active-color": "#fff",
"@component-active-bg": "@brand-primary",
"@caret-width-base": "4px",
"@caret-width-large": "5px",
"@table-cell-padding": "8px",
"@table-condensed-cell-padding": "2px",
"@table-bg": "transparent",
"@table-bg-accent": "#f9f9f9",
"@table-bg-hover": "#f5f5f5",
"@table-bg-active": "@table-bg-hover",
"@table-border-color": "#ddd",
"@btn-font-weight": "normal",
"@btn-default-color": "#333",
"@btn-default-bg": "#fff",
"@btn-default-border": "#ccc",
"@btn-primary-color": "#fff",
"@btn-primary-bg": "@brand-primary",
"@btn-primary-border": "darken(@btn-primary-bg, 5%)",
"@btn-success-color": "#fff",
"@btn-success-bg": "@brand-success",
"@btn-success-border": "darken(@btn-success-bg, 5%)",
"@btn-info-color": "#fff",
"@btn-info-bg": "@brand-info",
"@btn-info-border": "darken(@btn-info-bg, 5%)",
"@btn-warning-color": "#fff",
"@btn-warning-bg": "@brand-warning",
"@btn-warning-border": "darken(@btn-warning-bg, 5%)",
"@btn-danger-color": "#fff",
"@btn-danger-bg": "@brand-danger",
"@btn-danger-border": "darken(@btn-danger-bg, 5%)",
"@btn-link-disabled-color": "@gray-light",
"@btn-border-radius-base": "@border-radius-base",
"@btn-border-radius-large": "@border-radius-large",
"@btn-border-radius-small": "@border-radius-small",
"@input-bg": "#fff",
"@input-bg-disabled": "@gray-lighter",
"@input-color": "@gray",
"@input-border": "#ccc",
"@input-border-radius": "@border-radius-base",
"@input-border-radius-large": "@border-radius-large",
"@input-border-radius-small": "@border-radius-small",
"@input-border-focus": "#66afe9",
"@input-color-placeholder": "#999",
"@input-height-base": "(@line-height-computed + (@padding-base-vertical * 2) + 2)",
"@input-height-large": "(ceil(@font-size-large * @line-height-large) + (@padding-large-vertical * 2) + 2)",
"@input-height-small": "(floor(@font-size-small * @line-height-small) + (@padding-small-vertical * 2) + 2)",
"@form-group-margin-bottom": "15px",
"@legend-color": "@gray-dark",
"@legend-border-color": "#e5e5e5",
"@input-group-addon-bg": "@gray-lighter",
"@input-group-addon-border-color": "@input-border",
"@cursor-disabled": "not-allowed",
"@dropdown-bg": "#fff",
"@dropdown-border": "rgba(0,0,0,.15)",
"@dropdown-fallback-border": "#ccc",
"@dropdown-divider-bg": "#e5e5e5",
"@dropdown-link-color": "@gray-dark",
"@dropdown-link-hover-color": "darken(@gray-dark, 5%)",
"@dropdown-link-hover-bg": "#f5f5f5",
"@dropdown-link-active-color": "@component-active-color",
"@dropdown-link-active-bg": "@component-active-bg",
"@dropdown-link-disabled-color": "@gray-light",
"@dropdown-header-color": "@gray-light",
"@dropdown-caret-color": "#000",
"@screen-xs": "480px",
"@screen-xs-min": "@screen-xs",
"@screen-phone": "@screen-xs-min",
"@screen-sm": "768px",
"@screen-sm-min": "@screen-sm",
"@screen-tablet": "@screen-sm-min",
"@screen-md": "992px",
"@screen-md-min": "@screen-md",
"@screen-desktop": "@screen-md-min",
"@screen-lg": "1200px",
"@screen-lg-min": "@screen-lg",
"@screen-lg-desktop": "@screen-lg-min",
"@screen-xs-max": "(@screen-sm-min - 1)",
"@screen-sm-max": "(@screen-md-min - 1)",
"@screen-md-max": "(@screen-lg-min - 1)",
"@grid-columns": "12",
"@grid-gutter-width": "30px",
"@grid-float-breakpoint": "@screen-sm-min",
"@grid-float-breakpoint-max": "(@grid-float-breakpoint - 1)",
"@container-tablet": "(720px + @grid-gutter-width)",
"@container-sm": "@container-tablet",
"@container-desktop": "(940px + @grid-gutter-width)",
"@container-md": "@container-desktop",
"@container-large-desktop": "(1140px + @grid-gutter-width)",
"@container-lg": "@container-large-desktop",
"@navbar-height": "50px",
"@navbar-margin-bottom": "@line-height-computed",
"@navbar-border-radius": "@border-radius-base",
"@navbar-padding-horizontal": "floor((@grid-gutter-width / 2))",
"@navbar-padding-vertical": "((@navbar-height - @line-height-computed) / 2)",
"@navbar-collapse-max-height": "340px",
"@navbar-default-color": "#555",
"@navbar-default-bg": "#f8f8f8",
"@navbar-default-border": "darken(@navbar-default-bg, 6.5%)",
"@navbar-default-link-color": "#555",
"@navbar-default-link-hover-color": "#333",
"@navbar-default-link-hover-bg": "transparent",
"@navbar-default-link-active-color": "#333",
"@navbar-default-link-active-bg": "darken(@navbar-default-bg, 6.5%)",
"@navbar-default-link-disabled-color": "#ccc",
"@navbar-default-link-disabled-bg": "transparent",
"@navbar-default-brand-color": "@navbar-default-link-color",
"@navbar-default-brand-hover-color": "darken(@navbar-default-brand-color, 10%)",
"@navbar-default-brand-hover-bg": "transparent",
"@navbar-default-toggle-hover-bg": "#ddd",
"@navbar-default-toggle-icon-bar-bg": "#888",
"@navbar-default-toggle-border-color": "#ddd",
"@navbar-inverse-color": "lighten(@gray-light, 15%)",
"@navbar-inverse-bg": "#222",
"@navbar-inverse-border": "darken(@navbar-inverse-bg, 10%)",
"@navbar-inverse-link-color": "lighten(@gray-light, 15%)",
"@navbar-inverse-link-hover-color": "#fff",
"@navbar-inverse-link-hover-bg": "transparent",
"@navbar-inverse-link-active-color": "@navbar-inverse-link-hover-color",
"@navbar-inverse-link-active-bg": "darken(@navbar-inverse-bg, 10%)",
"@navbar-inverse-link-disabled-color": "#444",
"@navbar-inverse-link-disabled-bg": "transparent",
"@navbar-inverse-brand-color": "@navbar-inverse-link-color",
"@navbar-inverse-brand-hover-color": "#fff",
"@navbar-inverse-brand-hover-bg": "transparent",
"@navbar-inverse-toggle-hover-bg": "#333",
"@navbar-inverse-toggle-icon-bar-bg": "#fff",
"@navbar-inverse-toggle-border-color": "#333",
"@nav-link-padding": "10px 15px",
"@nav-link-hover-bg": "@gray-lighter",
"@nav-disabled-link-color": "@gray-light",
"@nav-disabled-link-hover-color": "@gray-light",
"@nav-tabs-border-color": "#ddd",
"@nav-tabs-link-hover-border-color": "@gray-lighter",
"@nav-tabs-active-link-hover-bg": "@body-bg",
"@nav-tabs-active-link-hover-color": "@gray",
"@nav-tabs-active-link-hover-border-color": "#ddd",
"@nav-tabs-justified-link-border-color": "#ddd",
"@nav-tabs-justified-active-link-border-color": "@body-bg",
"@nav-pills-border-radius": "@border-radius-base",
"@nav-pills-active-link-hover-bg": "@component-active-bg",
"@nav-pills-active-link-hover-color": "@component-active-color",
"@pagination-color": "@link-color",
"@pagination-bg": "#fff",
"@pagination-border": "#ddd",
"@pagination-hover-color": "@link-hover-color",
"@pagination-hover-bg": "@gray-lighter",
"@pagination-hover-border": "#ddd",
"@pagination-active-color": "#fff",
"@pagination-active-bg": "@brand-primary",
"@pagination-active-border": "@brand-primary",
"@pagination-disabled-color": "@gray-light",
"@pagination-disabled-bg": "#fff",
"@pagination-disabled-border": "#ddd",
"@pager-bg": "@pagination-bg",
"@pager-border": "@pagination-border",
"@pager-border-radius": "15px",
"@pager-hover-bg": "@pagination-hover-bg",
"@pager-active-bg": "@pagination-active-bg",
"@pager-active-color": "@pagination-active-color",
"@pager-disabled-color": "@pagination-disabled-color",
"@jumbotron-padding": "30px",
"@jumbotron-color": "inherit",
"@jumbotron-bg": "@gray-lighter",
"@jumbotron-heading-color": "inherit",
"@jumbotron-font-size": "ceil((@font-size-base * 1.5))",
"@jumbotron-heading-font-size": "ceil((@font-size-base * 4.5))",
"@state-success-text": "darken(spin(@brand-success, -10), 5%)",
"@state-success-bg": "@brand-success",
"@state-success-border": "darken(spin(@state-success-bg, -10), 5%)",
"@state-info-text": "darken(spin(@brand-info, -10), 7%)",
"@state-info-bg": "@brand-info",
"@state-info-border": "darken(spin(@state-info-bg, -10), 7%)",
"@state-warning-text": "darken(spin(@brand-warning, -10), 5%)",
"@state-warning-bg": "@brand-warning",
"@state-warning-border": "darken(spin(@state-warning-bg, -10), 5%)",
"@state-danger-text": "darken(spin(@brand-danger, -10), 5%)",
"@state-danger-bg": "@brand-danger",
"@state-danger-border": "darken(spin(@state-danger-bg, -10), 5%)",
"@tooltip-max-width": "200px",
"@tooltip-color": "#fff",
"@tooltip-bg": "#000",
"@tooltip-opacity": ".9",
"@tooltip-arrow-width": "5px",
"@tooltip-arrow-color": "@tooltip-bg",
"@popover-bg": "#fff",
"@popover-max-width": "276px",
"@popover-border-color": "rgba(0,0,0,.2)",
"@popover-fallback-border-color": "#ccc",
"@popover-title-bg": "darken(@popover-bg, 3%)",
"@popover-arrow-width": "10px",
"@popover-arrow-color": "@popover-bg",
"@popover-arrow-outer-width": "(@popover-arrow-width + 1)",
"@popover-arrow-outer-color": "fadein(@popover-border-color, 5%)",
"@popover-arrow-outer-fallback-color": "darken(@popover-fallback-border-color, 20%)",
"@label-default-bg": "@gray-light",
"@label-primary-bg": "@brand-primary",
"@label-success-bg": "@brand-success",
"@label-info-bg": "@brand-info",
"@label-warning-bg": "@brand-warning",
"@label-danger-bg": "@brand-danger",
"@label-color": "#fff",
"@label-link-hover-color": "#fff",
"@modal-inner-padding": "15px",
"@modal-title-padding": "15px",
"@modal-title-line-height": "@line-height-base",
"@modal-content-bg": "#fff",
"@modal-content-border-color": "rgba(0,0,0,.2)",
"@modal-content-fallback-border-color": "#999",
"@modal-backdrop-bg": "#000",
"@modal-backdrop-opacity": ".5",
"@modal-header-border-color": "#e5e5e5",
"@modal-footer-border-color": "@modal-header-border-color",
"@modal-lg": "900px",
"@modal-md": "600px",
"@modal-sm": "300px",
"@alert-padding": "15px",
"@alert-border-radius": "@border-radius-base",
"@alert-link-font-weight": "bold",
"@alert-success-bg": "@state-success-bg",
"@alert-success-text": "#fff",
"@alert-success-border": "@state-success-border",
"@alert-info-bg": "@state-info-bg",
"@alert-info-text": "#fff",
"@alert-info-border": "@state-info-border",
"@alert-warning-bg": "@state-warning-bg",
"@alert-warning-text": "#fff",
"@alert-warning-border": "@state-warning-border",
"@alert-danger-bg": "@state-danger-bg",
"@alert-danger-text": "#fff",
"@alert-danger-border": "@state-danger-border",
"@progress-bg": "#f5f5f5",
"@progress-bar-color": "#fff",
"@progress-border-radius": "@border-radius-base",
"@progress-bar-bg": "@brand-primary",
"@progress-bar-success-bg": "@brand-success",
"@progress-bar-warning-bg": "@brand-warning",
"@progress-bar-danger-bg": "@brand-danger",
"@progress-bar-info-bg": "@brand-info",
"@list-group-bg": "#fff",
"@list-group-border": "#ddd",
"@list-group-border-radius": "@border-radius-base",
"@list-group-hover-bg": "#f5f5f5",
"@list-group-active-color": "@component-active-color",
"@list-group-active-bg": "@component-active-bg",
"@list-group-active-border": "@list-group-active-bg",
"@list-group-active-text-color": "lighten(@list-group-active-bg, 40%)",
"@list-group-disabled-color": "@gray-light",
"@list-group-disabled-bg": "@gray-lighter",
"@list-group-disabled-text-color": "@list-group-disabled-color",
"@list-group-link-color": "#555",
"@list-group-link-hover-color": "@list-group-link-color",
"@list-group-link-heading-color": "#333",
"@panel-bg": "#fff",
"@panel-body-padding": "15px",
"@panel-heading-padding": "10px 15px",
"@panel-footer-padding": "@panel-heading-padding",
"@panel-border-radius": "@border-radius-base",
"@panel-inner-border": "#ddd",
"@panel-footer-bg": "#f5f5f5",
"@panel-default-text": "@gray-dark",
"@panel-default-border": "#ddd",
"@panel-default-heading-bg": "#f5f5f5",
"@panel-primary-text": "#fff",
"@panel-primary-border": "@brand-primary",
"@panel-primary-heading-bg": "@brand-primary",
"@panel-success-text": "#fff",
"@panel-success-border": "@state-success-border",
"@panel-success-heading-bg": "@state-success-bg",
"@panel-info-text": "#fff",
"@panel-info-border": "@state-info-border",
"@panel-info-heading-bg": "@state-info-bg",
"@panel-warning-text": "#fff",
"@panel-warning-border": "@state-warning-border",
"@panel-warning-heading-bg": "@state-warning-bg",
"@panel-danger-text": "#fff",
"@panel-danger-border": "@state-danger-border",
"@panel-danger-heading-bg": "@state-danger-bg",
"@thumbnail-padding": "4px",
"@thumbnail-bg": "@body-bg",
"@thumbnail-border": "#ddd",
"@thumbnail-border-radius": "@border-radius-base",
"@thumbnail-caption-color": "@text-color",
"@thumbnail-caption-padding": "9px",
"@well-bg": "#f5f5f5",
"@well-border": "darken(@well-bg, 7%)",
"@badge-color": "#fff",
"@badge-link-hover-color": "#fff",
"@badge-bg": "@gray-light",
"@badge-active-color": "@link-color",
"@badge-active-bg": "#fff",
"@badge-font-weight": "bold",
"@badge-line-height": "1",
"@badge-border-radius": "10px",
"@breadcrumb-padding-vertical": "8px",
"@breadcrumb-padding-horizontal": "15px",
"@breadcrumb-bg": "#f5f5f5",
"@breadcrumb-color": "#ccc",
"@breadcrumb-active-color": "@gray-light",
"@breadcrumb-separator": "\"/\"",
"@carousel-text-shadow": "0 1px 2px rgba(0,0,0,.6)",
"@carousel-control-color": "#fff",
"@carousel-control-width": "15%",
"@carousel-control-opacity": ".5",
"@carousel-control-font-size": "20px",
"@carousel-indicator-active-bg": "#fff",
"@carousel-indicator-border-color": "#fff",
"@carousel-caption-color": "#fff",
"@close-font-weight": "bold",
"@close-color": "#000",
"@close-text-shadow": "0 1px 0 #fff",
"@code-color": "#c7254e",
"@code-bg": "#f9f2f4",
"@kbd-color": "#fff",
"@kbd-bg": "#333",
"@pre-bg": "#f5f5f5",
"@pre-color": "@gray-dark",
"@pre-border-color": "#ccc",
"@pre-scrollable-max-height": "340px",
"@component-offset-horizontal": "180px",
"@text-muted": "@gray-light",
"@abbr-border-color": "@gray-light",
"@headings-small-color": "@gray-light",
"@blockquote-small-color": "@gray-light",
"@blockquote-font-size": "(@font-size-base * 1.25)",
"@blockquote-border-color": "@gray-lighter",
"@page-header-border-color": "@gray-lighter",
"@dl-horizontal-offset": "@component-offset-horizontal",
"@hr-border": "@gray-lighter"
},
"css": [
"type.less",
"code.less",
"grid.less",
"tables.less",
"forms.less",
"buttons.less",
"responsive-utilities.less",
"glyphicons.less",
"button-groups.less",
"input-groups.less",
"navs.less",
"navbar.less",
"pagination.less",
"pager.less",
"labels.less",
"badges.less",
"thumbnails.less",
"alerts.less",
"progress-bars.less",
"media.less",
"list-group.less",
"panels.less",
"responsive-embed.less",
"wells.less",
"close.less",
"component-animations.less",
"dropdowns.less",
"tooltip.less",
"popovers.less",
"modals.less"
],
"js": [
"alert.js",
"button.js",
"dropdown.js",
"modal.js",
"tooltip.js",
"popover.js",
"tab.js",
"collapse.js",
"transition.js"
],
"customizerUrl": "http://getbootstrap.com/customize/?id=4012f96c807ffd49c697"
}

View File

@@ -0,0 +1,596 @@
/*!
* Bootstrap v3.3.5 (http://getbootstrap.com)
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
/*!
* Generated using the Bootstrap Customizer (http://getbootstrap.com/customize/?id=4012f96c807ffd49c697)
* Config saved to config.json and https://gist.github.com/4012f96c807ffd49c697
*/
/*!
* Bootstrap v3.3.5 (http://getbootstrap.com)
* Copyright 2011-2015 Twitter, Inc.
* Licensed under MIT (https://github.com/twbs/bootstrap/blob/master/LICENSE)
*/
.btn-default,
.btn-primary,
.btn-success,
.btn-info,
.btn-warning,
.btn-danger {
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 1px rgba(0, 0, 0, 0.075);
}
.btn-default:active,
.btn-primary:active,
.btn-success:active,
.btn-info:active,
.btn-warning:active,
.btn-danger:active,
.btn-default.active,
.btn-primary.active,
.btn-success.active,
.btn-info.active,
.btn-warning.active,
.btn-danger.active {
-webkit-box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
box-shadow: inset 0 3px 5px rgba(0, 0, 0, 0.125);
}
.btn-default.disabled,
.btn-primary.disabled,
.btn-success.disabled,
.btn-info.disabled,
.btn-warning.disabled,
.btn-danger.disabled,
.btn-default[disabled],
.btn-primary[disabled],
.btn-success[disabled],
.btn-info[disabled],
.btn-warning[disabled],
.btn-danger[disabled],
fieldset[disabled] .btn-default,
fieldset[disabled] .btn-primary,
fieldset[disabled] .btn-success,
fieldset[disabled] .btn-info,
fieldset[disabled] .btn-warning,
fieldset[disabled] .btn-danger {
-webkit-box-shadow: none;
box-shadow: none;
}
.btn-default .badge,
.btn-primary .badge,
.btn-success .badge,
.btn-info .badge,
.btn-warning .badge,
.btn-danger .badge {
text-shadow: none;
}
.btn:active,
.btn.active {
background-image: none;
}
.btn-default {
background-image: -webkit-linear-gradient(top, #ffffff 0%, #e0e0e0 100%);
background-image: -o-linear-gradient(top, #ffffff 0%, #e0e0e0 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#ffffff), to(#e0e0e0));
background-image: linear-gradient(to bottom, #ffffff 0%, #e0e0e0 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#ffe0e0e0', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #dbdbdb;
text-shadow: 0 1px 0 #fff;
border-color: #ccc;
}
.btn-default:hover,
.btn-default:focus {
background-color: #e0e0e0;
background-position: 0 -15px;
}
.btn-default:active,
.btn-default.active {
background-color: #e0e0e0;
border-color: #dbdbdb;
}
.btn-default.disabled,
.btn-default[disabled],
fieldset[disabled] .btn-default,
.btn-default.disabled:hover,
.btn-default[disabled]:hover,
fieldset[disabled] .btn-default:hover,
.btn-default.disabled:focus,
.btn-default[disabled]:focus,
fieldset[disabled] .btn-default:focus,
.btn-default.disabled.focus,
.btn-default[disabled].focus,
fieldset[disabled] .btn-default.focus,
.btn-default.disabled:active,
.btn-default[disabled]:active,
fieldset[disabled] .btn-default:active,
.btn-default.disabled.active,
.btn-default[disabled].active,
fieldset[disabled] .btn-default.active {
background-color: #e0e0e0;
background-image: none;
}
.btn-primary {
background-image: -webkit-linear-gradient(top, #3498db 0%, #2077b2 100%);
background-image: -o-linear-gradient(top, #3498db 0%, #2077b2 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#3498db), to(#2077b2));
background-image: linear-gradient(to bottom, #3498db 0%, #2077b2 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3498db', endColorstr='#ff2077b2', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #1e72aa;
}
.btn-primary:hover,
.btn-primary:focus {
background-color: #2077b2;
background-position: 0 -15px;
}
.btn-primary:active,
.btn-primary.active {
background-color: #2077b2;
border-color: #1e72aa;
}
.btn-primary.disabled,
.btn-primary[disabled],
fieldset[disabled] .btn-primary,
.btn-primary.disabled:hover,
.btn-primary[disabled]:hover,
fieldset[disabled] .btn-primary:hover,
.btn-primary.disabled:focus,
.btn-primary[disabled]:focus,
fieldset[disabled] .btn-primary:focus,
.btn-primary.disabled.focus,
.btn-primary[disabled].focus,
fieldset[disabled] .btn-primary.focus,
.btn-primary.disabled:active,
.btn-primary[disabled]:active,
fieldset[disabled] .btn-primary:active,
.btn-primary.disabled.active,
.btn-primary[disabled].active,
fieldset[disabled] .btn-primary.active {
background-color: #2077b2;
background-image: none;
}
.btn-success {
background-image: -webkit-linear-gradient(top, #2ecc71 0%, #239a55 100%);
background-image: -o-linear-gradient(top, #2ecc71 0%, #239a55 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#2ecc71), to(#239a55));
background-image: linear-gradient(to bottom, #2ecc71 0%, #239a55 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff2ecc71', endColorstr='#ff239a55', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #219251;
}
.btn-success:hover,
.btn-success:focus {
background-color: #239a55;
background-position: 0 -15px;
}
.btn-success:active,
.btn-success.active {
background-color: #239a55;
border-color: #219251;
}
.btn-success.disabled,
.btn-success[disabled],
fieldset[disabled] .btn-success,
.btn-success.disabled:hover,
.btn-success[disabled]:hover,
fieldset[disabled] .btn-success:hover,
.btn-success.disabled:focus,
.btn-success[disabled]:focus,
fieldset[disabled] .btn-success:focus,
.btn-success.disabled.focus,
.btn-success[disabled].focus,
fieldset[disabled] .btn-success.focus,
.btn-success.disabled:active,
.btn-success[disabled]:active,
fieldset[disabled] .btn-success:active,
.btn-success.disabled.active,
.btn-success[disabled].active,
fieldset[disabled] .btn-success.active {
background-color: #239a55;
background-image: none;
}
.btn-info {
background-image: -webkit-linear-gradient(top, #9b59b6 0%, #7a4092 100%);
background-image: -o-linear-gradient(top, #9b59b6 0%, #7a4092 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#9b59b6), to(#7a4092));
background-image: linear-gradient(to bottom, #9b59b6 0%, #7a4092 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff9b59b6', endColorstr='#ff7a4092', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #743d8b;
}
.btn-info:hover,
.btn-info:focus {
background-color: #7a4092;
background-position: 0 -15px;
}
.btn-info:active,
.btn-info.active {
background-color: #7a4092;
border-color: #743d8b;
}
.btn-info.disabled,
.btn-info[disabled],
fieldset[disabled] .btn-info,
.btn-info.disabled:hover,
.btn-info[disabled]:hover,
fieldset[disabled] .btn-info:hover,
.btn-info.disabled:focus,
.btn-info[disabled]:focus,
fieldset[disabled] .btn-info:focus,
.btn-info.disabled.focus,
.btn-info[disabled].focus,
fieldset[disabled] .btn-info.focus,
.btn-info.disabled:active,
.btn-info[disabled]:active,
fieldset[disabled] .btn-info:active,
.btn-info.disabled.active,
.btn-info[disabled].active,
fieldset[disabled] .btn-info.active {
background-color: #7a4092;
background-image: none;
}
.btn-warning {
background-image: -webkit-linear-gradient(top, #f1c40f 0%, #b8960b 100%);
background-image: -o-linear-gradient(top, #f1c40f 0%, #b8960b 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f1c40f), to(#b8960b));
background-image: linear-gradient(to bottom, #f1c40f 0%, #b8960b 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff1c40f', endColorstr='#ffb8960b', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #ae8e0a;
}
.btn-warning:hover,
.btn-warning:focus {
background-color: #b8960b;
background-position: 0 -15px;
}
.btn-warning:active,
.btn-warning.active {
background-color: #b8960b;
border-color: #ae8e0a;
}
.btn-warning.disabled,
.btn-warning[disabled],
fieldset[disabled] .btn-warning,
.btn-warning.disabled:hover,
.btn-warning[disabled]:hover,
fieldset[disabled] .btn-warning:hover,
.btn-warning.disabled:focus,
.btn-warning[disabled]:focus,
fieldset[disabled] .btn-warning:focus,
.btn-warning.disabled.focus,
.btn-warning[disabled].focus,
fieldset[disabled] .btn-warning.focus,
.btn-warning.disabled:active,
.btn-warning[disabled]:active,
fieldset[disabled] .btn-warning:active,
.btn-warning.disabled.active,
.btn-warning[disabled].active,
fieldset[disabled] .btn-warning.active {
background-color: #b8960b;
background-image: none;
}
.btn-danger {
background-image: -webkit-linear-gradient(top, #e74c3c 0%, #cd2a19 100%);
background-image: -o-linear-gradient(top, #e74c3c 0%, #cd2a19 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#e74c3c), to(#cd2a19));
background-image: linear-gradient(to bottom, #e74c3c 0%, #cd2a19 100%);
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe74c3c', endColorstr='#ffcd2a19', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
background-repeat: repeat-x;
border-color: #c42818;
}
.btn-danger:hover,
.btn-danger:focus {
background-color: #cd2a19;
background-position: 0 -15px;
}
.btn-danger:active,
.btn-danger.active {
background-color: #cd2a19;
border-color: #c42818;
}
.btn-danger.disabled,
.btn-danger[disabled],
fieldset[disabled] .btn-danger,
.btn-danger.disabled:hover,
.btn-danger[disabled]:hover,
fieldset[disabled] .btn-danger:hover,
.btn-danger.disabled:focus,
.btn-danger[disabled]:focus,
fieldset[disabled] .btn-danger:focus,
.btn-danger.disabled.focus,
.btn-danger[disabled].focus,
fieldset[disabled] .btn-danger.focus,
.btn-danger.disabled:active,
.btn-danger[disabled]:active,
fieldset[disabled] .btn-danger:active,
.btn-danger.disabled.active,
.btn-danger[disabled].active,
fieldset[disabled] .btn-danger.active {
background-color: #cd2a19;
background-image: none;
}
.thumbnail,
.img-thumbnail {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
}
.dropdown-menu > li > a:hover,
.dropdown-menu > li > a:focus {
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
background-color: #e8e8e8;
}
.dropdown-menu > .active > a,
.dropdown-menu > .active > a:hover,
.dropdown-menu > .active > a:focus {
background-image: -webkit-linear-gradient(top, #3498db 0%, #258cd1 100%);
background-image: -o-linear-gradient(top, #3498db 0%, #258cd1 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#3498db), to(#258cd1));
background-image: linear-gradient(to bottom, #3498db 0%, #258cd1 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3498db', endColorstr='#ff258cd1', GradientType=0);
background-color: #258cd1;
}
.navbar-default {
background-image: -webkit-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);
background-image: -o-linear-gradient(top, #ffffff 0%, #f8f8f8 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#ffffff), to(#f8f8f8));
background-image: linear-gradient(to bottom, #ffffff 0%, #f8f8f8 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffffffff', endColorstr='#fff8f8f8', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
border-radius: 2px;
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.15), 0 1px 5px rgba(0, 0, 0, 0.075);
}
.navbar-default .navbar-nav > .open > a,
.navbar-default .navbar-nav > .active > a {
background-image: -webkit-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
background-image: -o-linear-gradient(top, #dbdbdb 0%, #e2e2e2 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#dbdbdb), to(#e2e2e2));
background-image: linear-gradient(to bottom, #dbdbdb 0%, #e2e2e2 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffdbdbdb', endColorstr='#ffe2e2e2', GradientType=0);
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.075);
}
.navbar-brand,
.navbar-nav > li > a {
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.25);
}
.navbar-inverse {
background-image: -webkit-linear-gradient(top, #3c3c3c 0%, #222222 100%);
background-image: -o-linear-gradient(top, #3c3c3c 0%, #222222 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#3c3c3c), to(#222222));
background-image: linear-gradient(to bottom, #3c3c3c 0%, #222222 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3c3c3c', endColorstr='#ff222222', GradientType=0);
filter: progid:DXImageTransform.Microsoft.gradient(enabled = false);
border-radius: 2px;
}
.navbar-inverse .navbar-nav > .open > a,
.navbar-inverse .navbar-nav > .active > a {
background-image: -webkit-linear-gradient(top, #080808 0%, #0f0f0f 100%);
background-image: -o-linear-gradient(top, #080808 0%, #0f0f0f 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#080808), to(#0f0f0f));
background-image: linear-gradient(to bottom, #080808 0%, #0f0f0f 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff080808', endColorstr='#ff0f0f0f', GradientType=0);
-webkit-box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);
box-shadow: inset 0 3px 9px rgba(0, 0, 0, 0.25);
}
.navbar-inverse .navbar-brand,
.navbar-inverse .navbar-nav > li > a {
text-shadow: 0 -1px 0 rgba(0, 0, 0, 0.25);
}
.navbar-static-top,
.navbar-fixed-top,
.navbar-fixed-bottom {
border-radius: 0;
}
@media (max-width: 767px) {
.navbar .navbar-nav .open .dropdown-menu > .active > a,
.navbar .navbar-nav .open .dropdown-menu > .active > a:hover,
.navbar .navbar-nav .open .dropdown-menu > .active > a:focus {
color: #fff;
background-image: -webkit-linear-gradient(top, #3498db 0%, #258cd1 100%);
background-image: -o-linear-gradient(top, #3498db 0%, #258cd1 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#3498db), to(#258cd1));
background-image: linear-gradient(to bottom, #3498db 0%, #258cd1 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3498db', endColorstr='#ff258cd1', GradientType=0);
}
}
.alert {
text-shadow: 0 1px 0 rgba(255, 255, 255, 0.2);
-webkit-box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
box-shadow: inset 0 1px 0 rgba(255, 255, 255, 0.25), 0 1px 2px rgba(0, 0, 0, 0.05);
}
.alert-success {
background-image: -webkit-linear-gradient(top, #2ecc71 0%, #27ad60 100%);
background-image: -o-linear-gradient(top, #2ecc71 0%, #27ad60 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#2ecc71), to(#27ad60));
background-image: linear-gradient(to bottom, #2ecc71 0%, #27ad60 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff2ecc71', endColorstr='#ff27ad60', GradientType=0);
border-color: #208e4e;
}
.alert-info {
background-image: -webkit-linear-gradient(top, #9b59b6 0%, #8747a2 100%);
background-image: -o-linear-gradient(top, #9b59b6 0%, #8747a2 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#9b59b6), to(#8747a2));
background-image: linear-gradient(to bottom, #9b59b6 0%, #8747a2 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff9b59b6', endColorstr='#ff8747a2', GradientType=0);
border-color: #713b87;
}
.alert-warning {
background-image: -webkit-linear-gradient(top, #f1c40f 0%, #cea70c 100%);
background-image: -o-linear-gradient(top, #f1c40f 0%, #cea70c 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f1c40f), to(#cea70c));
background-image: linear-gradient(to bottom, #f1c40f 0%, #cea70c 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff1c40f', endColorstr='#ffcea70c', GradientType=0);
border-color: #aa8a0a;
}
.alert-danger {
background-image: -webkit-linear-gradient(top, #e74c3c 0%, #e12e1c 100%);
background-image: -o-linear-gradient(top, #e74c3c 0%, #e12e1c 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#e74c3c), to(#e12e1c));
background-image: linear-gradient(to bottom, #e74c3c 0%, #e12e1c 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe74c3c', endColorstr='#ffe12e1c', GradientType=0);
border-color: #bf2718;
}
.progress {
background-image: -webkit-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
background-image: -o-linear-gradient(top, #ebebeb 0%, #f5f5f5 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#ebebeb), to(#f5f5f5));
background-image: linear-gradient(to bottom, #ebebeb 0%, #f5f5f5 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffebebeb', endColorstr='#fff5f5f5', GradientType=0);
}
.progress-bar {
background-image: -webkit-linear-gradient(top, #3498db 0%, #217dbb 100%);
background-image: -o-linear-gradient(top, #3498db 0%, #217dbb 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#3498db), to(#217dbb));
background-image: linear-gradient(to bottom, #3498db 0%, #217dbb 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3498db', endColorstr='#ff217dbb', GradientType=0);
}
.progress-bar-success {
background-image: -webkit-linear-gradient(top, #2ecc71 0%, #25a25a 100%);
background-image: -o-linear-gradient(top, #2ecc71 0%, #25a25a 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#2ecc71), to(#25a25a));
background-image: linear-gradient(to bottom, #2ecc71 0%, #25a25a 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff2ecc71', endColorstr='#ff25a25a', GradientType=0);
}
.progress-bar-info {
background-image: -webkit-linear-gradient(top, #9b59b6 0%, #804399 100%);
background-image: -o-linear-gradient(top, #9b59b6 0%, #804399 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#9b59b6), to(#804399));
background-image: linear-gradient(to bottom, #9b59b6 0%, #804399 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff9b59b6', endColorstr='#ff804399', GradientType=0);
}
.progress-bar-warning {
background-image: -webkit-linear-gradient(top, #f1c40f 0%, #c29d0b 100%);
background-image: -o-linear-gradient(top, #f1c40f 0%, #c29d0b 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f1c40f), to(#c29d0b));
background-image: linear-gradient(to bottom, #f1c40f 0%, #c29d0b 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff1c40f', endColorstr='#ffc29d0b', GradientType=0);
}
.progress-bar-danger {
background-image: -webkit-linear-gradient(top, #e74c3c 0%, #d62c1a 100%);
background-image: -o-linear-gradient(top, #e74c3c 0%, #d62c1a 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#e74c3c), to(#d62c1a));
background-image: linear-gradient(to bottom, #e74c3c 0%, #d62c1a 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe74c3c', endColorstr='#ffd62c1a', GradientType=0);
}
.progress-bar-striped {
background-image: -webkit-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
background-image: -o-linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
background-image: linear-gradient(45deg, rgba(255, 255, 255, 0.15) 25%, transparent 25%, transparent 50%, rgba(255, 255, 255, 0.15) 50%, rgba(255, 255, 255, 0.15) 75%, transparent 75%, transparent);
}
.list-group {
border-radius: 2px;
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.075);
}
.list-group-item.active,
.list-group-item.active:hover,
.list-group-item.active:focus {
text-shadow: 0 -1px 0 #217dbb;
background-image: -webkit-linear-gradient(top, #3498db 0%, #2384c6 100%);
background-image: -o-linear-gradient(top, #3498db 0%, #2384c6 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#3498db), to(#2384c6));
background-image: linear-gradient(to bottom, #3498db 0%, #2384c6 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3498db', endColorstr='#ff2384c6', GradientType=0);
border-color: #2384c6;
}
.list-group-item.active .badge,
.list-group-item.active:hover .badge,
.list-group-item.active:focus .badge {
text-shadow: none;
}
.panel {
-webkit-box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.05);
}
.panel-default > .panel-heading {
background-image: -webkit-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: -o-linear-gradient(top, #f5f5f5 0%, #e8e8e8 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f5f5f5), to(#e8e8e8));
background-image: linear-gradient(to bottom, #f5f5f5 0%, #e8e8e8 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff5f5f5', endColorstr='#ffe8e8e8', GradientType=0);
}
.panel-primary > .panel-heading {
background-image: -webkit-linear-gradient(top, #3498db 0%, #258cd1 100%);
background-image: -o-linear-gradient(top, #3498db 0%, #258cd1 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#3498db), to(#258cd1));
background-image: linear-gradient(to bottom, #3498db 0%, #258cd1 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff3498db', endColorstr='#ff258cd1', GradientType=0);
}
.panel-success > .panel-heading {
background-image: -webkit-linear-gradient(top, #2ecc71 0%, #29b765 100%);
background-image: -o-linear-gradient(top, #2ecc71 0%, #29b765 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#2ecc71), to(#29b765));
background-image: linear-gradient(to bottom, #2ecc71 0%, #29b765 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff2ecc71', endColorstr='#ff29b765', GradientType=0);
}
.panel-info > .panel-heading {
background-image: -webkit-linear-gradient(top, #9b59b6 0%, #8f4bab 100%);
background-image: -o-linear-gradient(top, #9b59b6 0%, #8f4bab 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#9b59b6), to(#8f4bab));
background-image: linear-gradient(to bottom, #9b59b6 0%, #8f4bab 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ff9b59b6', endColorstr='#ff8f4bab', GradientType=0);
}
.panel-warning > .panel-heading {
background-image: -webkit-linear-gradient(top, #f1c40f 0%, #dab10d 100%);
background-image: -o-linear-gradient(top, #f1c40f 0%, #dab10d 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#f1c40f), to(#dab10d));
background-image: linear-gradient(to bottom, #f1c40f 0%, #dab10d 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#fff1c40f', endColorstr='#ffdab10d', GradientType=0);
}
.panel-danger > .panel-heading {
background-image: -webkit-linear-gradient(top, #e74c3c 0%, #e43725 100%);
background-image: -o-linear-gradient(top, #e74c3c 0%, #e43725 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#e74c3c), to(#e43725));
background-image: linear-gradient(to bottom, #e74c3c 0%, #e43725 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe74c3c', endColorstr='#ffe43725', GradientType=0);
}
.well {
background-image: -webkit-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
background-image: -o-linear-gradient(top, #e8e8e8 0%, #f5f5f5 100%);
background-image: -webkit-gradient(linear, left top, left bottom, from(#e8e8e8), to(#f5f5f5));
background-image: linear-gradient(to bottom, #e8e8e8 0%, #f5f5f5 100%);
background-repeat: repeat-x;
filter: progid:DXImageTransform.Microsoft.gradient(startColorstr='#ffe8e8e8', endColorstr='#fff5f5f5', GradientType=0);
border-color: #dcdcdc;
-webkit-box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);
box-shadow: inset 0 1px 3px rgba(0, 0, 0, 0.05), 0 1px 0 rgba(255, 255, 255, 0.1);
}

13
gui/vendor/bootstrap/css/bootstrap-theme.min.css vendored Executable file → Normal file
View File

File diff suppressed because one or more lines are too long

6447
gui/vendor/bootstrap/css/bootstrap.css vendored Normal file
View File

File diff suppressed because it is too large Load Diff

View File

File diff suppressed because one or more lines are too long

BIN
gui/vendor/bootstrap/fonts/glyphicons-halflings-regular.eot vendored Executable file → Normal file
View File

Binary file not shown.

BIN
gui/vendor/bootstrap/fonts/glyphicons-halflings-regular.svg vendored Executable file → Normal file
View File

Binary file not shown.

Before

Width:  |  Height:  |  Size: 61 KiB

After

Width:  |  Height:  |  Size: 106 KiB

BIN
gui/vendor/bootstrap/fonts/glyphicons-halflings-regular.ttf vendored Executable file → Normal file
View File

Binary file not shown.

BIN
gui/vendor/bootstrap/fonts/glyphicons-halflings-regular.woff vendored Executable file → Normal file
View File

Binary file not shown.

View File

Binary file not shown.

1792
gui/vendor/bootstrap/js/bootstrap.js vendored Normal file
View File

File diff suppressed because it is too large Load Diff

10
gui/vendor/bootstrap/js/bootstrap.min.js vendored Executable file → Normal file
View File

File diff suppressed because one or more lines are too long

View File

File diff suppressed because one or more lines are too long

View File

@@ -9,7 +9,6 @@ package config
import (
"encoding/xml"
"fmt"
"io"
"math/rand"
"os"
@@ -238,6 +237,8 @@ type OptionsConfiguration struct {
SymlinksEnabled bool `xml:"symlinksEnabled" json:"symlinksEnabled" default:"true"`
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
DatabaseBlockCacheMiB int `xml:"databaseBlockCacheMiB" json:"databaseBlockCacheMiB" default:"0"`
PingTimeoutS int `xml:"pingTimeoutS" json:"pingTimeoutS" default:"30"`
PingIdleTimeS int `xml:"pingIdleTimeS" json:"pingIdleTimeS" default:"60"`
}
func (orig OptionsConfiguration) Copy() OptionsConfiguration {
@@ -310,7 +311,6 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
// Check for missing, bad or duplicate folder ID:s
var seenFolders = map[string]*FolderConfiguration{}
var uniqueCounter int
for i := range cfg.Folders {
folder := &cfg.Folders[i]
@@ -339,15 +339,8 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
if seen, ok := seenFolders[folder.ID]; ok {
l.Warnf("Multiple folders with ID %q; disabling", folder.ID)
seen.Invalid = "duplicate folder ID"
if seen.ID == folder.ID {
uniqueCounter++
seen.ID = fmt.Sprintf("%s~%d", folder.ID, uniqueCounter)
}
folder.Invalid = "duplicate folder ID"
uniqueCounter++
folder.ID = fmt.Sprintf("%s~%d", folder.ID, uniqueCounter)
} else {
seenFolders[folder.ID] = folder
}
@@ -587,7 +580,7 @@ func fillNilSlices(data interface{}) error {
func uniqueStrings(ss []string) []string {
var m = make(map[string]bool, len(ss))
for _, s := range ss {
m[s] = true
m[strings.Trim(s, " ")] = true
}
var us = make([]string, 0, len(m))

View File

@@ -53,6 +53,8 @@ func TestDefaultValues(t *testing.T) {
SymlinksEnabled: true,
LimitBandwidthInLan: false,
DatabaseBlockCacheMiB: 0,
PingTimeoutS: 30,
PingIdleTimeS: 60,
}
cfg := New(device1)
@@ -160,6 +162,8 @@ func TestOverriddenValues(t *testing.T) {
SymlinksEnabled: false,
LimitBandwidthInLan: true,
DatabaseBlockCacheMiB: 42,
PingTimeoutS: 60,
PingIdleTimeS: 120,
}
cfg, err := Load("testdata/overridenvalues.xml", device1)
@@ -317,6 +321,29 @@ func TestIssue1262(t *testing.T) {
}
}
func TestIssue1750(t *testing.T) {
cfg, err := Load("testdata/issue-1750.xml", device4)
if err != nil {
t.Fatal(err)
}
if cfg.Options().ListenAddress[0] != ":23000" {
t.Errorf("%q != %q", cfg.Options().ListenAddress[0], ":23000")
}
if cfg.Options().ListenAddress[1] != ":23001" {
t.Errorf("%q != %q", cfg.Options().ListenAddress[1], ":23001")
}
if cfg.Options().GlobalAnnServers[0] != "udp4://syncthing.nym.se:22026" {
t.Errorf("%q != %q", cfg.Options().GlobalAnnServers[0], "udp4://syncthing.nym.se:22026")
}
if cfg.Options().GlobalAnnServers[1] != "udp4://syncthing.nym.se:22027" {
t.Errorf("%q != %q", cfg.Options().GlobalAnnServers[1], "udp4://syncthing.nym.se:22027")
}
}
func TestWindowsPaths(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Not useful on non-Windows")

View File

@@ -0,0 +1,8 @@
<configuration version="9">
<options>
<listenAddress> :23000</listenAddress>
<listenAddress> :23001 </listenAddress>
<globalAnnounceServer> udp4://syncthing.nym.se:22026</globalAnnounceServer>
<globalAnnounceServer> udp4://syncthing.nym.se:22027 </globalAnnounceServer>
</options>
</configuration>

View File

@@ -24,5 +24,7 @@
<symlinksEnabled>false</symlinksEnabled>
<limitBandwidthInLan>true</limitBandwidthInLan>
<databaseBlockCacheMiB>42</databaseBlockCacheMiB>
<pingTimeoutS>60</pingTimeoutS>
<pingIdleTimeS>120</pingIdleTimeS>
</options>
</configuration>

View File

@@ -35,6 +35,7 @@ const (
DownloadProgress
FolderSummary
FolderCompletion
FolderErrors
AllEvents = (1 << iota) - 1
)
@@ -75,6 +76,8 @@ func (t EventType) String() string {
return "FolderSummary"
case FolderCompletion:
return "FolderCompletion"
case FolderErrors:
return "FolderErrors"
default:
return "Unknown"
}

View File

@@ -55,6 +55,7 @@ type service interface {
BringToFront(string)
DelayScan(d time.Duration)
IndexUpdated() // Remote index was updated notification
Scan(subs []string) error
setState(state folderState)
setError(err error)
@@ -91,8 +92,7 @@ type Model struct {
deviceVer map[protocol.DeviceID]string
pmut sync.RWMutex // protects protoConn and rawConn
addedFolder bool
started bool
started bool
reqValidationCache map[string]time.Time // folder / file name => time when confirmed to exist
rvmut sync.RWMutex // protects reqValidationCache
@@ -1179,7 +1179,6 @@ func (m *Model) AddFolder(cfg config.FolderConfiguration) {
_ = ignores.Load(filepath.Join(cfg.Path(), ".stignore")) // Ignore error, there might not be an .stignore
m.folderIgnores[cfg.ID] = ignores
m.addedFolder = true
m.fmut.Unlock()
}
@@ -1226,6 +1225,21 @@ func (m *Model) ScanFolder(folder string) error {
}
func (m *Model) ScanFolderSubs(folder string, subs []string) error {
m.fmut.Lock()
runner, ok := m.folderRunners[folder]
m.fmut.Unlock()
// Folders are added to folderRunners only when they are started. We can't
// scan them before they have started, so that's what we need to check for
// here.
if !ok {
return errors.New("no such folder")
}
return runner.Scan(subs)
}
func (m *Model) internalScanFolderSubs(folder string, subs []string) error {
for i, sub := range subs {
sub = osutil.NativeFilename(sub)
if p := filepath.Clean(filepath.Join(folder, sub)); !strings.HasPrefix(p, folder) {
@@ -1540,23 +1554,23 @@ func (m *Model) Override(folder string) {
// CurrentLocalVersion returns the change version for the given folder.
// This is guaranteed to increment if the contents of the local folder has
// changed.
func (m *Model) CurrentLocalVersion(folder string) int64 {
func (m *Model) CurrentLocalVersion(folder string) (int64, bool) {
m.fmut.RLock()
fs, ok := m.folderFiles[folder]
m.fmut.RUnlock()
if !ok {
// The folder might not exist, since this can be called with a user
// specified folder name from the REST interface.
return 0
return 0, false
}
return fs.LocalVersion(protocol.LocalDeviceID)
return fs.LocalVersion(protocol.LocalDeviceID), true
}
// RemoteLocalVersion returns the change version for the given folder, as
// sent by remote peers. This is guaranteed to increment if the contents of
// the remote or global folder has changed.
func (m *Model) RemoteLocalVersion(folder string) int64 {
func (m *Model) RemoteLocalVersion(folder string) (int64, bool) {
m.fmut.RLock()
defer m.fmut.RUnlock()
@@ -1564,7 +1578,7 @@ func (m *Model) RemoteLocalVersion(folder string) int64 {
if !ok {
// The folder might not exist, since this can be called with a user
// specified folder name from the REST interface.
return 0
return 0, false
}
var ver int64
@@ -1572,7 +1586,7 @@ func (m *Model) RemoteLocalVersion(folder string) int64 {
ver += fs.LocalVersion(n)
}
return ver
return ver, true
}
func (m *Model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{} {
@@ -1681,7 +1695,7 @@ func (m *Model) CheckFolderHealth(id string) error {
}
fi, err := os.Stat(folder.Path())
if m.CurrentLocalVersion(id) > 0 {
if v, ok := m.CurrentLocalVersion(id); ok && v > 0 {
// Safety check. If the cached index contains files but the
// folder doesn't exist, we have a problem. We would assume
// that all files have been deleted which might not be the case,
@@ -1732,15 +1746,9 @@ func (m *Model) CheckFolderHealth(id string) error {
return err
}
func (m *Model) ResetFolder(folder string) error {
for _, f := range db.ListFolders(m.db) {
if f == folder {
l.Infof("Cleaning data for folder %q", folder)
db.DropFolder(m.db, folder)
return nil
}
}
return fmt.Errorf("Unknown folder %q", folder)
func (m *Model) ResetFolder(folder string) {
l.Infof("Cleaning data for folder %q", folder)
db.DropFolder(m.db, folder)
}
func (m *Model) String() string {

View File

@@ -22,9 +22,15 @@ type roFolder struct {
timer *time.Timer
model *Model
stop chan struct{}
scanNow chan rescanRequest
delayScan chan time.Duration
}
type rescanRequest struct {
subs []string
err chan error
}
func newROFolder(model *Model, folder string, interval time.Duration) *roFolder {
return &roFolder{
stateTracker: stateTracker{
@@ -36,6 +42,7 @@ func newROFolder(model *Model, folder string, interval time.Duration) *roFolder
timer: time.NewTimer(time.Millisecond),
model: model,
stop: make(chan struct{}),
scanNow: make(chan rescanRequest),
delayScan: make(chan time.Duration),
}
}
@@ -76,7 +83,7 @@ func (s *roFolder) Serve() {
l.Debugln(s, "rescan")
}
if err := s.model.ScanFolder(s.folder); err != nil {
if err := s.model.internalScanFolderSubs(s.folder, nil); err != nil {
// Potentially sets the error twice, once in the scanner just
// by doing a check, and once here, if the error returned is
// the same one as returned by CheckFolderHealth, though
@@ -92,11 +99,34 @@ func (s *roFolder) Serve() {
}
if s.intv == 0 {
return
continue
}
reschedule()
case req := <-s.scanNow:
if err := s.model.CheckFolderHealth(s.folder); err != nil {
l.Infoln("Skipping folder", s.folder, "scan due to folder error:", err)
req.err <- err
continue
}
if debug {
l.Debugln(s, "forced rescan")
}
if err := s.model.internalScanFolderSubs(s.folder, req.subs); err != nil {
// Potentially sets the error twice, once in the scanner just
// by doing a check, and once here, if the error returned is
// the same one as returned by CheckFolderHealth, though
// duplicate set is handled by setError.
s.setError(err)
req.err <- err
continue
}
req.err <- nil
case next := <-s.delayScan:
s.timer.Reset(next)
}
@@ -110,6 +140,15 @@ func (s *roFolder) Stop() {
func (s *roFolder) IndexUpdated() {
}
func (s *roFolder) Scan(subs []string) error {
req := rescanRequest{
subs: subs,
err: make(chan error),
}
s.scanNow <- req
return <-req.err
}
func (s *roFolder) String() string {
return fmt.Sprintf("roFolder/%s@%p", s.folder, s)
}

View File

@@ -13,6 +13,7 @@ import (
"math/rand"
"os"
"path/filepath"
"sort"
"time"
"github.com/syncthing/protocol"
@@ -32,7 +33,7 @@ import (
const (
pauseIntv = 60 * time.Second
nextPullIntv = 10 * time.Second
shortPullIntv = 5 * time.Second
shortPullIntv = time.Second
)
// A pullBlockState is passed to the puller routine for each block that needs
@@ -49,6 +50,9 @@ type copyBlocksState struct {
blocks []protocol.BlockInfo
}
// Which filemode bits to preserve
const retainBits = os.ModeSetgid | os.ModeSetuid | os.ModeSticky
var (
activity = newDeviceActivity()
errNoDevice = errors.New("no available source device")
@@ -90,7 +94,11 @@ type rwFolder struct {
scanTimer *time.Timer
pullTimer *time.Timer
delayScan chan time.Duration
scanNow chan rescanRequest
remoteIndex chan struct{} // An index update was received, we should re-evaluate needs
errors map[string]string // path -> error string
errorsMut sync.Mutex
}
func newRWFolder(m *Model, shortID uint64, cfg config.FolderConfiguration) *rwFolder {
@@ -118,7 +126,10 @@ func newRWFolder(m *Model, shortID uint64, cfg config.FolderConfiguration) *rwFo
pullTimer: time.NewTimer(shortPullIntv),
scanTimer: time.NewTimer(time.Millisecond), // The first scan should be done immediately.
delayScan: make(chan time.Duration),
scanNow: make(chan rescanRequest),
remoteIndex: make(chan struct{}, 1), // This needs to be 1-buffered so that we queue a notification if we're busy doing a pull when it comes.
errorsMut: sync.NewMutex(),
}
}
@@ -202,10 +213,10 @@ func (p *rwFolder) Serve() {
}
// RemoteLocalVersion() is a fast call, doesn't touch the database.
curVer := p.model.RemoteLocalVersion(p.folder)
if curVer == prevVer {
curVer, ok := p.model.RemoteLocalVersion(p.folder)
if !ok || curVer == prevVer {
if debug {
l.Debugln(p, "skip (curVer == prevVer)", prevVer)
l.Debugln(p, "skip (curVer == prevVer)", prevVer, ok)
}
p.pullTimer.Reset(nextPullIntv)
continue
@@ -214,8 +225,11 @@ func (p *rwFolder) Serve() {
if debug {
l.Debugln(p, "pulling", prevVer, curVer)
}
p.setState(FolderSyncing)
p.clearErrors()
tries := 0
for {
tries++
@@ -229,7 +243,7 @@ func (p *rwFolder) Serve() {
// sync. Remember the local version number and
// schedule a resync a little bit into the future.
if lv := p.model.RemoteLocalVersion(p.folder); lv < curVer {
if lv, ok := p.model.RemoteLocalVersion(p.folder); ok && lv < curVer {
// There's a corner case where the device we needed
// files from disconnected during the puller
// iteration. The files will have been removed from
@@ -254,10 +268,18 @@ func (p *rwFolder) Serve() {
// we're not making it. Probably there are write
// errors preventing us. Flag this with a warning and
// wait a bit longer before retrying.
l.Warnf("Folder %q isn't making progress - check logs for possible root cause. Pausing puller for %v.", p.folder, pauseIntv)
l.Infof("Folder %q isn't making progress. Pausing puller for %v.", p.folder, pauseIntv)
if debug {
l.Debugln(p, "next pull in", pauseIntv)
}
if folderErrors := p.currentErrors(); len(folderErrors) > 0 {
events.Default.Log(events.FolderErrors, map[string]interface{}{
"folder": p.folder,
"errors": folderErrors,
})
}
p.pullTimer.Reset(pauseIntv)
break
}
@@ -278,7 +300,7 @@ func (p *rwFolder) Serve() {
l.Debugln(p, "rescan")
}
if err := p.model.ScanFolder(p.folder); err != nil {
if err := p.model.internalScanFolderSubs(p.folder, nil); err != nil {
// Potentially sets the error twice, once in the scanner just
// by doing a check, and once here, if the error returned is
// the same one as returned by CheckFolderHealth, though
@@ -296,6 +318,29 @@ func (p *rwFolder) Serve() {
initialScanCompleted = true
}
case req := <-p.scanNow:
if err := p.model.CheckFolderHealth(p.folder); err != nil {
l.Infoln("Skipping folder", p.folder, "scan due to folder error:", err)
req.err <- err
continue
}
if debug {
l.Debugln(p, "forced rescan")
}
if err := p.model.internalScanFolderSubs(p.folder, req.subs); err != nil {
// Potentially sets the error twice, once in the scanner just
// by doing a check, and once here, if the error returned is
// the same one as returned by CheckFolderHealth, though
// duplicate set is handled by setError.
p.setError(err)
req.err <- err
continue
}
req.err <- nil
case next := <-p.delayScan:
p.scanTimer.Reset(next)
}
@@ -317,6 +362,15 @@ func (p *rwFolder) IndexUpdated() {
}
}
func (p *rwFolder) Scan(subs []string) error {
req := rescanRequest{
subs: subs,
err: make(chan error),
}
p.scanNow <- req
return <-req.err
}
func (p *rwFolder) String() string {
return fmt.Sprintf("rwFolder/%s@%p", p.folder, p)
}
@@ -578,6 +632,7 @@ func (p *rwFolder) handleDir(file protocol.FileInfo) {
err = osutil.InWritableDir(osutil.Remove, realName)
if err != nil {
l.Infof("Puller (folder %q, dir %q): %v", p.folder, file.Name, err)
p.newError(file.Name, err)
return
}
fallthrough
@@ -592,32 +647,43 @@ func (p *rwFolder) handleDir(file protocol.FileInfo) {
if err != nil || p.ignorePermissions(file) {
return err
}
return os.Chmod(path, mode)
// Stat the directory so we can check its permissions.
info, err := osutil.Lstat(path)
if err != nil {
return err
}
// Mask for the bits we want to preserve and add them in to the
// directories permissions.
return os.Chmod(path, mode|(info.Mode()&retainBits))
}
if err = osutil.InWritableDir(mkdir, realName); err == nil {
p.dbUpdates <- dbUpdateJob{file, dbUpdateHandleDir}
} else {
l.Infof("Puller (folder %q, dir %q): %v", p.folder, file.Name, err)
p.newError(file.Name, err)
}
return
// Weird error when stat()'ing the dir. Probably won't work to do
// anything else with it if we can't even stat() it.
case err != nil:
l.Infof("Puller (folder %q, dir %q): %v", p.folder, file.Name, err)
p.newError(file.Name, err)
return
}
// The directory already exists, so we just correct the mode bits. (We
// don't handle modification times on directories, because that sucks...)
// It's OK to change mode bits on stuff within non-writable directories.
if p.ignorePermissions(file) {
p.dbUpdates <- dbUpdateJob{file, dbUpdateHandleDir}
} else if err := os.Chmod(realName, mode); err == nil {
} else if err := os.Chmod(realName, mode|(info.Mode()&retainBits)); err == nil {
p.dbUpdates <- dbUpdateJob{file, dbUpdateHandleDir}
} else {
l.Infof("Puller (folder %q, dir %q): %v", p.folder, file.Name, err)
p.newError(file.Name, err)
}
}
@@ -664,6 +730,7 @@ func (p *rwFolder) deleteDir(file protocol.FileInfo) {
p.dbUpdates <- dbUpdateJob{file, dbUpdateDeleteDir}
} else {
l.Infof("Puller (folder %q, dir %q): delete: %v", p.folder, file.Name, err)
p.newError(file.Name, err)
}
}
@@ -712,6 +779,7 @@ func (p *rwFolder) deleteFile(file protocol.FileInfo) {
p.dbUpdates <- dbUpdateJob{file, dbUpdateDeleteFile}
} else {
l.Infof("Puller (folder %q, file %q): delete: %v", p.folder, file.Name, err)
p.newError(file.Name, err)
}
}
@@ -774,6 +842,7 @@ func (p *rwFolder) renameFile(source, target protocol.FileInfo) {
err = p.shortcutFile(target)
if err != nil {
l.Infof("Puller (folder %q, file %q): rename from %q metadata: %v", p.folder, target.Name, source.Name, err)
p.newError(target.Name, err)
return
}
@@ -786,6 +855,7 @@ func (p *rwFolder) renameFile(source, target protocol.FileInfo) {
err = osutil.InWritableDir(osutil.Remove, from)
if err != nil {
l.Infof("Puller (folder %q, file %q): delete %q after failed rename: %v", p.folder, target.Name, source.Name, err)
p.newError(target.Name, err)
return
}
@@ -866,6 +936,7 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
if err != nil {
l.Infoln("Puller: shortcut:", err)
p.newError(file.Name, err)
} else {
p.dbUpdates <- dbUpdateJob{file, dbUpdateShortcutFile}
}
@@ -954,6 +1025,7 @@ func (p *rwFolder) shortcutFile(file protocol.FileInfo) error {
if !p.ignorePermissions(file) {
if err := os.Chmod(realName, os.FileMode(file.Flags&0777)); err != nil {
l.Infof("Puller (folder %q, file %q): shortcut: chmod: %v", p.folder, file.Name, err)
p.newError(file.Name, err)
return err
}
}
@@ -964,6 +1036,7 @@ func (p *rwFolder) shortcutFile(file protocol.FileInfo) error {
info, err := os.Stat(realName)
if err != nil {
l.Infof("Puller (folder %q, file %q): shortcut: unable to stat file: %v", p.folder, file.Name, err)
p.newError(file.Name, err)
return err
}
@@ -984,6 +1057,7 @@ func (p *rwFolder) shortcutSymlink(file protocol.FileInfo) (err error) {
err = symlinks.ChangeType(filepath.Join(p.dir, file.Name), file.Flags)
if err != nil {
l.Infof("Puller (folder %q, file %q): symlink shortcut: %v", p.folder, file.Name, err)
p.newError(file.Name, err)
}
return
}
@@ -1221,6 +1295,7 @@ func (p *rwFolder) finisherRoutine(in <-chan *sharedPullerState) {
if err != nil {
l.Infoln("Puller: final:", err)
p.newError(state.file.Name, err)
}
events.Default.Log(events.ItemFinished, map[string]interface{}{
"folder": p.folder,
@@ -1368,3 +1443,54 @@ func moveForConflict(name string) error {
}
return err
}
func (p *rwFolder) newError(path string, err error) {
p.errorsMut.Lock()
defer p.errorsMut.Unlock()
// We might get more than one error report for a file (i.e. error on
// Write() followed by Close()); we keep the first error as that is
// probably closer to the root cause.
if _, ok := p.errors[path]; ok {
return
}
p.errors[path] = err.Error()
}
func (p *rwFolder) clearErrors() {
p.errorsMut.Lock()
p.errors = make(map[string]string)
p.errorsMut.Unlock()
}
func (p *rwFolder) currentErrors() []fileError {
p.errorsMut.Lock()
errors := make([]fileError, 0, len(p.errors))
for path, err := range p.errors {
errors = append(errors, fileError{path, err})
}
sort.Sort(fileErrorList(errors))
p.errorsMut.Unlock()
return errors
}
// A []fileError is sent as part of an event and will be JSON serialized.
type fileError struct {
Path string `json:"path"`
Err string `json:"error"`
}
type fileErrorList []fileError
func (l fileErrorList) Len() int {
return len(l)
}
func (l fileErrorList) Less(a, b int) bool {
return l[a].Path < l[b].Path
}
func (l fileErrorList) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}

View File

@@ -14,6 +14,7 @@ import (
"github.com/syncthing/protocol"
"github.com/syncthing/syncthing/internal/scanner"
"github.com/syncthing/syncthing/internal/sync"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/storage"
@@ -73,9 +74,11 @@ func TestHandleFile(t *testing.T) {
m.updateLocals("default", []protocol.FileInfo{existingFile})
p := rwFolder{
folder: "default",
dir: "testdata",
model: m,
folder: "default",
dir: "testdata",
model: m,
errors: make(map[string]string),
errorsMut: sync.NewMutex(),
}
copyChan := make(chan copyBlocksState, 1)
@@ -127,9 +130,11 @@ func TestHandleFileWithTemp(t *testing.T) {
m.updateLocals("default", []protocol.FileInfo{existingFile})
p := rwFolder{
folder: "default",
dir: "testdata",
model: m,
folder: "default",
dir: "testdata",
model: m,
errors: make(map[string]string),
errorsMut: sync.NewMutex(),
}
copyChan := make(chan copyBlocksState, 1)
@@ -198,9 +203,11 @@ func TestCopierFinder(t *testing.T) {
}
p := rwFolder{
folder: "default",
dir: "testdata",
model: m,
folder: "default",
dir: "testdata",
model: m,
errors: make(map[string]string),
errorsMut: sync.NewMutex(),
}
copyChan := make(chan copyBlocksState)
@@ -332,9 +339,11 @@ func TestLastResortPulling(t *testing.T) {
}
p := rwFolder{
folder: "default",
dir: "testdata",
model: m,
folder: "default",
dir: "testdata",
model: m,
errors: make(map[string]string),
errorsMut: sync.NewMutex(),
}
copyChan := make(chan copyBlocksState)
@@ -390,6 +399,8 @@ func TestDeregisterOnFailInCopy(t *testing.T) {
model: m,
queue: newJobQueue(),
progressEmitter: emitter,
errors: make(map[string]string),
errorsMut: sync.NewMutex(),
}
// queue.Done should be called by the finisher routine
@@ -477,6 +488,8 @@ func TestDeregisterOnFailInPull(t *testing.T) {
model: m,
queue: newJobQueue(),
progressEmitter: emitter,
errors: make(map[string]string),
errorsMut: sync.NewMutex(),
}
// queue.Done should be called by the finisher routine

View File

@@ -15,6 +15,7 @@ import (
"strings"
)
// Glob implements filepath.Glob, but works with Windows long path prefixes.
// Deals with https://github.com/golang/go/issues/10577
func Glob(pattern string) (matches []string, err error) {
if !hasMeta(pattern) {

View File

@@ -21,11 +21,11 @@ import (
)
const (
FSCTL_GET_REPARSE_POINT = 0x900a8
FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
FILE_ATTRIBUTE_REPARSE_POINT = 0x400
IO_REPARSE_TAG_SYMLINK = 0xA000000C
SYMBOLIC_LINK_FLAG_DIRECTORY = 0x1
Win32FsctlGetReparsePoint = 0x900a8
Win32FileFlagOpenReparsePoint = 0x00200000
Win32FileAttributeReparsePoint = 0x400
Win32IOReparseTagSymlink = 0xA000000C
Win32SymbolicLinkFlagDirectory = 0x1
)
var (
@@ -106,7 +106,7 @@ func Read(path string) (string, uint32, error) {
if err != nil {
return "", protocol.FlagSymlinkMissingTarget, err
}
handle, err := syscall.CreateFile(ptr, 0, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OPEN_REPARSE_POINT, 0)
handle, err := syscall.CreateFile(ptr, 0, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS|Win32FileFlagOpenReparsePoint, 0)
if err != nil || handle == syscall.InvalidHandle {
return "", protocol.FlagSymlinkMissingTarget, err
}
@@ -114,12 +114,12 @@ func Read(path string) (string, uint32, error) {
var ret uint16
var data reparseData
r1, _, err := syscall.Syscall9(procDeviceIoControl.Addr(), 8, uintptr(handle), FSCTL_GET_REPARSE_POINT, 0, 0, uintptr(unsafe.Pointer(&data)), unsafe.Sizeof(data), uintptr(unsafe.Pointer(&ret)), 0, 0)
r1, _, err := syscall.Syscall9(procDeviceIoControl.Addr(), 8, uintptr(handle), Win32FsctlGetReparsePoint, 0, 0, uintptr(unsafe.Pointer(&data)), unsafe.Sizeof(data), uintptr(unsafe.Pointer(&ret)), 0, 0)
if r1 == 0 {
return "", protocol.FlagSymlinkMissingTarget, err
}
var flags uint32 = 0
var flags uint32
attr, err := syscall.GetFileAttributes(ptr)
if err != nil {
flags = protocol.FlagSymlinkMissingTarget
@@ -154,10 +154,10 @@ func Create(source, target string, flags uint32) error {
stat, err := os.Stat(path)
if err == nil && stat.IsDir() {
mode = SYMBOLIC_LINK_FLAG_DIRECTORY
mode = Win32SymbolicLinkFlagDirectory
}
} else if flags&protocol.FlagDirectory != 0 {
mode = SYMBOLIC_LINK_FLAG_DIRECTORY
mode = Win32SymbolicLinkFlagDirectory
}
r0, _, err := syscall.Syscall(procCreateSymbolicLink.Addr(), 3, uintptr(unsafe.Pointer(srcp)), uintptr(unsafe.Pointer(trgp)), uintptr(mode))

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-CONFIG" "5" "June 20, 2015" "v0.11" "Syncthing"
.TH "SYNCTHING-CONFIG" "5" "July 01, 2015" "v0.11" "Syncthing"
.SH NAME
syncthing-config \- Syncthing Configuration
.
@@ -135,7 +135,8 @@ This is the root element.
.INDENT 0.0
.TP
.B version
The config version. Increments whenever a change is made that requires migration from previous formats.
The config version. Increments whenever a change is made that requires
migration from previous formats.
.UNINDENT
.SH FOLDER ELEMENT
.INDENT 0.0
@@ -210,11 +211,13 @@ advanced users only; do not change unless requested to or you\(aqve actually
read and understood the code yourself. :)
.TP
.B order
The order in which needed files should be pulled from the cluster. The possibles values are:
The order in which needed files should be pulled from the cluster.
The possibles values are:
.INDENT 7.0
.TP
.B random
Pull files in random order. This optimizes for balancing resources among the devices in a cluster.
Pull files in random order. This optimizes for balancing resources among
the devices in a cluster.
.TP
.B alphabetic
Pull files ordered by file name alphabetically.
@@ -223,7 +226,8 @@ Pull files ordered by file name alphabetically.
Pull files ordered by file size; smallest and largest first respectively.
.TP
.B oldestFirst, newestFirst
Pull files ordered by modification time; oldest and newest first respectively.
Pull files ordered by modification time; oldest and newest first
respectively.
.UNINDENT
.UNINDENT
.SH DEVICE ELEMENT
@@ -292,10 +296,12 @@ The address and port is used as given.
The default port (22000) is used.
.TP
.B IPv6 address and port (\fB[2001:db8::23:42]:12345\fP)
The address and port is used as given. The address must be enclosed in angled brackets.
The address and port is used as given. The address must be enclosed in
angled brackets.
.TP
.B \fBdynamic\fP
The word \fBdynamic\fP means to use local and global discovery to find the device.
The word \fBdynamic\fP means to use local and global discovery to find the
device.
.UNINDENT
.SH GUI ELEMENT
.INDENT 0.0
@@ -338,10 +344,12 @@ Allowed address formats are:
The address and port is used as given.
.TP
.B IPv6 address and port (\fB[::1]:8384\fP)
The address and port is used as given. The address must be enclosed in angled brackets.
The address and port is used as given. The address must be enclosed in
angled brackets.
.TP
.B Wildcard and port (\fB0.0.0.0:12345\fP, \fB[::]:12345\fP, \fB:12345\fP)
These are equivalent and will result in Syncthing listening on all interfaces and both IPv4 and IPv6.
These are equivalent and will result in Syncthing listening on all
interfaces and both IPv4 and IPv6.
.UNINDENT
.TP
.B username
@@ -385,6 +393,8 @@ If set, this is the API key that enables usage of the REST interface.
<symlinksEnabled>true</symlinksEnabled>
<limitBandwidthInLan>false</limitBandwidthInLan>
<databaseBlockCacheMiB>0</databaseBlockCacheMiB>
<pingTimeoutS>60</pingTimeoutS>
<pingIdleTimeS>120</pingIdleTimeS>
</options>
.ft P
.fi
@@ -431,7 +441,8 @@ unconnected devices.
Whether to attempt to start a browser to show the GUI when Syncthing starts.
.TP
.B upnpEnabled
Whether to attempt to perform an UPnP port mapping for incoming sync connections.
Whether to attempt to perform an UPnP port mapping for incoming sync
connections.
.TP
.B upnpLeaseMinutes
Request a lease for this many minutes; zero to request a permanent lease.
@@ -488,6 +499,13 @@ as the local device.
Override the automatically calculated database block cache size. Don\(aqt,
unless you\(aqre very short on memory, in which case you want to set this to
\fB8\fP\&.
.TP
.B pingTimeoutS
Ping\-timeout in seconds. Don\(aqt change it unless you are having issues due to
slow response time (slow connection/cpu) and large index exchanges
.TP
.B pingIdleTimeS
ping interval in seconds. Don\(aqt change it unless you feel it\(aqs necessary.
.UNINDENT
.SH AUTHOR
The Syncthing Authors

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-DEVICE-IDS" "7" "June 20, 2015" "v0.11" "Syncthing"
.TH "SYNCTHING-DEVICE-IDS" "7" "July 01, 2015" "v0.11" "Syncthing"
.SH NAME
syncthing-device-ids \- Understanding Device IDs
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-EVENT-API" "7" "June 20, 2015" "v0.11" "Syncthing"
.TH "SYNCTHING-EVENT-API" "7" "July 01, 2015" "v0.11" "Syncthing"
.SH NAME
syncthing-event-api \- Event API
.
@@ -334,6 +334,46 @@ device.
.fi
.UNINDENT
.UNINDENT
.SS FolderErrors
.sp
The \fBFolderErrors\fP event is emitted when a folder cannot be successfully
synchronized. The event contains the ID of the affected folder and a list of
errors for files or directories therein. This list of errors is obsolete once
the folder changes state to \fBsyncing\fP \- if errors remain after the next
synchronization attempt, a new \fBFolderErrors\fP event is emitted.
.INDENT 0.0
.INDENT 3.5
.sp
.nf
.ft C
{
"id": 132,
"type": "FolderErrors",
"time": "2015\-06\-26T13:39:24.697401384+02:00",
"data": {
"errors": [
{
"error": "open /Users/jb/src/github.com/syncthing/syncthing/test/s2/h2j/.syncthing.aslkjd.tmp: permission denied",
"path": "h2j/aslkjd"
}
],
"folder": "default"
}
}
.ft P
.fi
.UNINDENT
.UNINDENT
.sp
New in version 0.11.12.
.sp
\fBSEE ALSO:\fP
.INDENT 0.0
.INDENT 3.5
The statechanged event.
.UNINDENT
.UNINDENT
.SS FolderRejected
.sp
Emitted when a device sends index information for a folder we do not

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-FAQ" "7" "June 20, 2015" "v0.11" "Syncthing"
.TH "SYNCTHING-FAQ" "7" "July 01, 2015" "v0.11" "Syncthing"
.SH NAME
syncthing-faq \- Frequently Asked Questions
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-NETWORKING" "7" "June 20, 2015" "v0.11" "Syncthing"
.TH "SYNCTHING-NETWORKING" "7" "July 01, 2015" "v0.11" "Syncthing"
.SH NAME
syncthing-networking \- Firewall Setup
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-REST-API" "7" "June 20, 2015" "v0.11" "Syncthing"
.TH "SYNCTHING-REST-API" "7" "July 01, 2015" "v0.11" "Syncthing"
.SH NAME
syncthing-rest-api \- REST API
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-SECURITY" "7" "June 20, 2015" "v0.11" "Syncthing"
.TH "SYNCTHING-SECURITY" "7" "July 01, 2015" "v0.11" "Syncthing"
.SH NAME
syncthing-security \- Security Principles
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-STIGNORE" "5" "June 20, 2015" "v0.11" "Syncthing"
.TH "SYNCTHING-STIGNORE" "5" "July 01, 2015" "v0.11" "Syncthing"
.SH NAME
syncthing-stignore \- Prevent files from being synchronized to other nodes
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "TODO" "7" "June 20, 2015" "v0.11" "Syncthing"
.TH "TODO" "7" "July 01, 2015" "v0.11" "Syncthing"
.SH NAME
Todo \- Keep automatic backups of deleted files by other nodes
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING" "1" "June 20, 2015" "v0.11" "Syncthing"
.TH "SYNCTHING" "1" "July 01, 2015" "v0.11" "Syncthing"
.SH NAME
syncthing \- Syncthing
.
@@ -162,7 +162,7 @@ Upgrade not available
.B 3
Restarting
.TP
.B 5
.B 4
Upgrading
.UNINDENT
.sp
@@ -233,21 +233,22 @@ HTTP access.
Write a CPU profile to cpu\-$pid.pprof on exit.
.TP
.B STHEAPPROFILE
Write heap profiles to heap\-$pid\-$timestamp.pprof each time heap usage
Write heap profiles to \fBheap\-$pid\-$timestamp.pprof\fP each time heap usage
increases.
.TP
.B STBLOCKPROFILE
Write block profiles to \fBblock\-$pid\-$timestamp.pprof\fP every 20 seconds.
.TP
.B STPERFSTATS
Write running performance statistics to perf\-$pid.csv. Not supported on
Write running performance statistics to \fBperf\-$pid.csv\fP\&. Not supported on
Windows.
.TP
.B STNOUPGRADE
Disable automatic upgrades.
.TP
.B GOMAXPROCS
Set the maximum number of CPU cores to use. Defaults to all available CPU cores.
Set the maximum number of CPU cores to use. Defaults to all available CPU
cores.
.TP
.B GOGC
Percentage of heap growth at which to trigger GC. Default is 100. Lower

View File

@@ -204,6 +204,7 @@ func TestPOSTWithoutCSRF(t *testing.T) {
}
res.Body.Close()
hdr := res.Header.Get("Set-Cookie")
id := res.Header.Get("X-Syncthing-ID")[:5]
if !strings.Contains(hdr, "CSRF-Token") {
t.Error("Missing CSRF-Token in", hdr)
}
@@ -214,7 +215,8 @@ func TestPOSTWithoutCSRF(t *testing.T) {
if err != nil {
t.Fatal(err)
}
req.Header.Set("X-CSRF-Token", hdr[len("CSRF-Token="):])
req.Header.Set("X-CSRF-Token-"+id, hdr[len("CSRF-Token-"+id+"="):])
res, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)
@@ -230,7 +232,7 @@ func TestPOSTWithoutCSRF(t *testing.T) {
if err != nil {
t.Fatal(err)
}
req.Header.Set("X-CSRF-Token", hdr[len("CSRF-Token="):]+"X")
req.Header.Set("X-CSRF-Token-"+id, hdr[len("CSRF-Token-"+id+"="):]+"X")
res, err = http.DefaultClient.Do(req)
if err != nil {
t.Fatal(err)

View File

@@ -9,6 +9,7 @@
package integration
import (
"bytes"
"log"
"os"
"path/filepath"
@@ -67,17 +68,17 @@ func TestReset(t *testing.T) {
// Reset indexes of the default folder
log.Println("Reset indexes of default folder")
_, err = p.Post("/rest/system/reset?folder=default", nil)
bs, err := p.Post("/rest/system/reset?folder=default", nil)
if err != nil {
t.Fatal("Failed to reset indexes of the default folder:", err)
t.Fatalf("Failed to reset indexes (default): %v (%s)", err, bytes.TrimSpace(bs))
}
// Syncthing restarts on reset. But we set STNORESTART=1 for the tests. So
// we wait for it to exit, then do a stop so the rc.Process is happy and
// restart it again.
time.Sleep(time.Second)
checkedStop(t, p)
p = startInstance(t, 1)
defer checkedStop(t, p)
m, err = p.Model("default")
if err != nil {
@@ -108,17 +109,14 @@ func TestReset(t *testing.T) {
// Reset all indexes
log.Println("Reset DB...")
_, err = p.Post("/rest/system/reset?folder=default", nil)
bs, err = p.Post("/rest/system/reset", nil)
if err != nil {
t.Fatalf("Failed to reset indexes", err)
t.Fatalf("Failed to reset indexes (all): %v (%s)", err, bytes.TrimSpace(bs))
}
// Syncthing restarts on reset. But we set STNORESTART=1 for the tests. So
// we wait for it to exit, then do a stop so the rc.Process is happy and
// restart it again.
// we wait for it to exit, then restart it again.
time.Sleep(time.Second)
checkedStop(t, p)
p = startInstance(t, 1)
defer checkedStop(t, p)

View File

@@ -10,6 +10,7 @@ package integration
import (
"fmt"
"io/ioutil"
"log"
"math/rand"
"os"
@@ -82,6 +83,18 @@ func TestSyncClusterStaggeredVersioning(t *testing.T) {
testSyncCluster(t)
}
func TestSyncClusterForcedRescan(t *testing.T) {
// Use no versioning
id, _ := protocol.DeviceIDFromString(id2)
cfg, _ := config.Load("h2/config.xml", id)
fld := cfg.Folders()["default"]
fld.Versioning = config.VersioningConfiguration{}
cfg.SetFolder(fld)
cfg.Save()
testSyncClusterForcedRescan(t)
}
func testSyncCluster(t *testing.T) {
// This tests syncing files back and forth between three cluster members.
// Their configs are in h1, h2 and h3. The folder "default" is shared
@@ -287,6 +300,116 @@ func testSyncCluster(t *testing.T) {
}
}
func testSyncClusterForcedRescan(t *testing.T) {
// During this test, we create 1K files, remove and then create them
// again. However, during these operations we will perform scan operations
// such that other nodes will retrieve these options while data is
// changing.
// When -short is passed, keep it more reasonable.
timeLimit := longTimeLimit
if testing.Short() {
timeLimit = shortTimeLimit
}
log.Println("Cleaning...")
err := removeAll("s1", "s12-1",
"s2", "s12-2", "s23-2",
"s3", "s23-3",
"h1/index*", "h2/index*", "h3/index*")
if err != nil {
t.Fatal(err)
}
// Create initial folder contents. All three devices have stuff in
// "default", which should be merged. The other two folders are initially
// empty on one side.
log.Println("Generating files...")
if err := os.MkdirAll("s1/test-stable-files", 0755); err != nil {
t.Fatal(err)
}
for i := 0; i < 1000; i++ {
name := fmt.Sprintf("s1/test-stable-files/%d", i)
if err := ioutil.WriteFile(name, []byte(time.Now().Format(time.RFC3339Nano)), 0644); err != nil {
t.Fatal(err)
}
}
// Prepare the expected state of folders after the sync
expected, err := directoryContents("s1")
if err != nil {
t.Fatal(err)
}
// Start the syncers
p0 := startInstance(t, 1)
defer checkedStop(t, p0)
p1 := startInstance(t, 2)
defer checkedStop(t, p1)
p2 := startInstance(t, 3)
defer checkedStop(t, p2)
p := []*rc.Process{p0, p1, p2}
start := time.Now()
for time.Since(start) < timeLimit {
rescan := func() {
for i := range p {
if err := p[i].Rescan("default"); err != nil {
t.Fatal(err)
}
}
}
log.Println("Forcing rescan...")
rescan()
// Sync stuff and verify it looks right
err = scSyncAndCompare(p, [][]fileInfo{expected})
if err != nil {
t.Fatal(err)
}
log.Println("Altering...")
// Delete and recreate stable files while scanners and pullers are active
for i := 0; i < 1000; i++ {
name := fmt.Sprintf("s1/test-stable-files/%d", i)
if err := os.Remove(name); err != nil {
t.Fatal(err)
}
if rand.Intn(10) == 0 {
rescan()
}
}
rescan()
time.Sleep(50 * time.Millisecond)
for i := 0; i < 1000; i++ {
name := fmt.Sprintf("s1/test-stable-files/%d", i)
if err := ioutil.WriteFile(name, []byte(time.Now().Format(time.RFC3339Nano)), 0644); err != nil {
t.Fatal(err)
}
if rand.Intn(10) == 0 {
rescan()
}
}
rescan()
// Prepare the expected state of folders after the sync
expected, err = directoryContents("s1")
if err != nil {
t.Fatal(err)
}
if len(expected) != 1001 {
t.Fatal("s1 does not have 1001 files;", len(expected))
}
}
}
func scSyncAndCompare(p []*rc.Process, expected [][]fileInfo) error {
log.Println("Syncing...")