Compare commits
48
Commits
+1
-2
@@ -2,8 +2,7 @@
|
||||
* text=auto
|
||||
|
||||
# Except the dependencies, which we leave alone
|
||||
Godeps/** -text=auto
|
||||
vendor/** -text=auto
|
||||
|
||||
# Diffs on these files are meaningless
|
||||
gui.files.go -diff
|
||||
*.svg -diff
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
syncthing
|
||||
!gui/syncthing
|
||||
!debian/syncthing
|
||||
!Godeps/_workspace/src/github.com/syncthing
|
||||
syncthing.exe
|
||||
*.tar.gz
|
||||
@@ -14,3 +15,4 @@ coverage.xml
|
||||
syncthing.sig
|
||||
RELEASE
|
||||
deb
|
||||
lib/auto/gui.files.go
|
||||
|
||||
@@ -24,7 +24,9 @@ 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 Harte <daniel@harte.me> <daniel@danielharte.co.uk> <norgeous@users.noreply.github.com>
|
||||
Daniel Martí <mvdan@mvdan.cc>
|
||||
David Rimmer <dinosore@dbrsoftware.co.uk>
|
||||
Denis A. <denisva@gmail.com>
|
||||
Dennis Wilson <dw@risu.io>
|
||||
Dominik Heidler <dominik@heidler.eu>
|
||||
@@ -47,8 +49,10 @@ Jens Diemer <github.com@jensdiemer.de> <git@jensdiemer.de>
|
||||
Jochen Voss <voss@seehuhn.de>
|
||||
Johan Vromans <jvromans@squirrel.nl>
|
||||
Karol Różycki <rozycki.karol@gmail.com>
|
||||
Kelong Cong <kc04bc@gmx.com> <kc1212@users.noreply.github.com>
|
||||
Ken'ichi Kamada <kamada@nanohz.org>
|
||||
Kevin Allen <kma1660@gmail.com>
|
||||
Lars K.W. Gohlke <lkwg82@gmx.de>
|
||||
Laurent Etiemble <laurent.etiemble@gmail.com> <laurent.etiemble@monobjc.net>
|
||||
Lode Hoste <zillode@zillode.be>
|
||||
Lord Landon Agahnim <lordlandon@gmail.com>
|
||||
|
||||
Generated
-90
@@ -1,90 +0,0 @@
|
||||
{
|
||||
"ImportPath": "github.com/syncthing/syncthing",
|
||||
"GoVersion": "go1.5.2",
|
||||
"Packages": [
|
||||
"./cmd/..."
|
||||
],
|
||||
"Deps": [
|
||||
{
|
||||
"ImportPath": "github.com/bkaradzic/go-lz4",
|
||||
"Rev": "74ddf82598bc4745b965729e9c6a463bedd33049"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/calmh/du",
|
||||
"Rev": "3c0690cca16228b97741327b1b6781397afbdb24"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/calmh/luhn",
|
||||
"Rev": "0c8388ff95fa92d4094011e5a04fc99dea3d1632"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/calmh/xdr",
|
||||
"Rev": "9eb3e1a622d9364deb39c831f7e5f164393d7e37"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/golang/snappy",
|
||||
"Rev": "723cc1e459b8eea2dea4583200fd60757d40097a"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/juju/ratelimit",
|
||||
"Rev": "772f5c38e468398c4511514f4f6aa9a4185bc0a0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/kardianos/osext",
|
||||
"Rev": "29ae4ffbc9a6fe9fb2bc5029050ce6996ea1d3bc"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/rcrowley/go-metrics",
|
||||
"Rev": "1ce93efbc8f9c568886b2ef85ce305b2217b3de3"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
|
||||
"Rev": "1a9d62f03ea92815b46fcaab357cfd4df264b1a0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/thejerf/suture",
|
||||
"Comment": "v1.0.1",
|
||||
"Rev": "99c1f2d613756768fc4299acd9dc621e11ed3fd7"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vitrun/qart/coding",
|
||||
"Rev": "ccb109cf25f0cd24474da73b9fee4e7a3e8a8ce0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vitrun/qart/gf256",
|
||||
"Rev": "ccb109cf25f0cd24474da73b9fee4e7a3e8a8ce0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vitrun/qart/qr",
|
||||
"Rev": "ccb109cf25f0cd24474da73b9fee4e7a3e8a8ce0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/bcrypt",
|
||||
"Rev": "575fdbe86e5dd89229707ebec0575ce7d088a4a6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/crypto/blowfish",
|
||||
"Rev": "575fdbe86e5dd89229707ebec0575ce7d088a4a6"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/internal/iana",
|
||||
"Rev": "042ba42fa6633b34205efc66ba5719cd3afd8d38"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/ipv6",
|
||||
"Rev": "042ba42fa6633b34205efc66ba5719cd3afd8d38"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/net/proxy",
|
||||
"Rev": "042ba42fa6633b34205efc66ba5719cd3afd8d38"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/text/transform",
|
||||
"Rev": "5eb8d4684c4796dd36c74f6452f2c0fa6c79597e"
|
||||
},
|
||||
{
|
||||
"ImportPath": "golang.org/x/text/unicode/norm",
|
||||
"Rev": "5eb8d4684c4796dd36c74f6452f2c0fa6c79597e"
|
||||
}
|
||||
]
|
||||
}
|
||||
Generated
-5
@@ -1,5 +0,0 @@
|
||||
This directory tree is generated automatically by godep.
|
||||
|
||||
Please do not edit.
|
||||
|
||||
See https://github.com/tools/godep for more information.
|
||||
Generated
-2
@@ -1,2 +0,0 @@
|
||||
/pkg
|
||||
/bin
|
||||
@@ -1 +0,0 @@
|
||||
/lz4-example/lz4-example
|
||||
-9
@@ -1,9 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.1
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
- 1.5
|
||||
- tip
|
||||
@@ -1 +0,0 @@
|
||||
coverage.out
|
||||
-19
@@ -1,19 +0,0 @@
|
||||
language: go
|
||||
go:
|
||||
- tip
|
||||
|
||||
install:
|
||||
- export PATH=$PATH:$HOME/gopath/bin
|
||||
- go get golang.org/x/tools/cover
|
||||
- go get github.com/mattn/goveralls
|
||||
|
||||
script:
|
||||
- ./generate.sh
|
||||
- go test -coverprofile=coverage.out
|
||||
|
||||
after_success:
|
||||
- goveralls -coverprofile=coverage.out -service=travis-ci -package=calmh/xdr -repotoken="$COVERALLS_TOKEN"
|
||||
|
||||
env:
|
||||
global:
|
||||
secure: SmgnrGfp2zLrA44ChRMpjPeujubt9veZ8Fx/OseMWECmacyV5N/TuDhzIbwo6QwV4xB0sBacoPzvxQbJRVjNKsPiSu72UbcQmQ7flN4Tf7nW09tSh1iW8NgrpBCq/3UYLoBu2iPBEBKm93IK0aGNAKs6oEkB0fU27iTVBwiTXOY=
|
||||
-16
@@ -1,16 +0,0 @@
|
||||
// Copyright (C) 2014 Jakob Borg. All rights reserved. Use of this source code
|
||||
// is governed by an MIT-style license that can be found in the LICENSE file.
|
||||
|
||||
package xdr
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
)
|
||||
|
||||
var (
|
||||
debug = len(os.Getenv("XDRTRACE")) > 0
|
||||
dl = log.New(os.Stdout, "xdr: ", log.Lshortfile|log.Ltime|log.Lmicroseconds)
|
||||
)
|
||||
|
||||
const maxDebugBytes = 32
|
||||
-10
@@ -1,10 +0,0 @@
|
||||
// Copyright (C) 2014 Jakob Borg. All rights reserved. Use of this source code
|
||||
// is governed by an MIT-style license that can be found in the LICENSE file.
|
||||
|
||||
// +build ipdr
|
||||
|
||||
package xdr
|
||||
|
||||
func pad(l int) int {
|
||||
return 0
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
// Copyright (C) 2014 Jakob Borg. All rights reserved. Use of this source code
|
||||
// is governed by an MIT-style license that can be found in the LICENSE file.
|
||||
|
||||
// +build !ipdr
|
||||
|
||||
package xdr
|
||||
|
||||
func pad(l int) int {
|
||||
d := l % 4
|
||||
if d == 0 {
|
||||
return 0
|
||||
}
|
||||
return 4 - d
|
||||
}
|
||||
-171
@@ -1,171 +0,0 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package xdr
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
type Reader struct {
|
||||
r io.Reader
|
||||
err error
|
||||
b [8]byte
|
||||
}
|
||||
|
||||
func NewReader(r io.Reader) *Reader {
|
||||
return &Reader{
|
||||
r: r,
|
||||
}
|
||||
}
|
||||
|
||||
func (r *Reader) ReadRaw(bs []byte) (int, error) {
|
||||
if r.err != nil {
|
||||
return 0, r.err
|
||||
}
|
||||
|
||||
var n int
|
||||
n, r.err = io.ReadFull(r.r, bs)
|
||||
return n, r.err
|
||||
}
|
||||
|
||||
func (r *Reader) ReadString() string {
|
||||
return r.ReadStringMax(0)
|
||||
}
|
||||
|
||||
func (r *Reader) ReadStringMax(max int) string {
|
||||
buf := r.ReadBytesMaxInto(max, nil)
|
||||
bh := (*reflect.SliceHeader)(unsafe.Pointer(&buf))
|
||||
sh := reflect.StringHeader{
|
||||
Data: bh.Data,
|
||||
Len: bh.Len,
|
||||
}
|
||||
return *((*string)(unsafe.Pointer(&sh)))
|
||||
}
|
||||
|
||||
func (r *Reader) ReadBytes() []byte {
|
||||
return r.ReadBytesInto(nil)
|
||||
}
|
||||
|
||||
func (r *Reader) ReadBytesMax(max int) []byte {
|
||||
return r.ReadBytesMaxInto(max, nil)
|
||||
}
|
||||
|
||||
func (r *Reader) ReadBytesInto(dst []byte) []byte {
|
||||
return r.ReadBytesMaxInto(0, dst)
|
||||
}
|
||||
|
||||
func (r *Reader) ReadBytesMaxInto(max int, dst []byte) []byte {
|
||||
if r.err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
l := int(r.ReadUint32())
|
||||
if r.err != nil {
|
||||
return nil
|
||||
}
|
||||
if l < 0 || max > 0 && l > max {
|
||||
// l may be negative on 32 bit builds
|
||||
r.err = ElementSizeExceeded("bytes field", l, max)
|
||||
return nil
|
||||
}
|
||||
|
||||
if fullLen := l + pad(l); fullLen > len(dst) {
|
||||
dst = make([]byte, fullLen)
|
||||
} else {
|
||||
dst = dst[:fullLen]
|
||||
}
|
||||
|
||||
var n int
|
||||
n, r.err = io.ReadFull(r.r, dst)
|
||||
if r.err != nil {
|
||||
if debug {
|
||||
dl.Printf("rd bytes (%d): %v", len(dst), r.err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if debug {
|
||||
if n > maxDebugBytes {
|
||||
dl.Printf("rd bytes (%d): %x...", len(dst), dst[:maxDebugBytes])
|
||||
} else {
|
||||
dl.Printf("rd bytes (%d): %x", len(dst), dst)
|
||||
}
|
||||
}
|
||||
return dst[:l]
|
||||
}
|
||||
|
||||
func (r *Reader) ReadBool() bool {
|
||||
return r.ReadUint8() != 0
|
||||
}
|
||||
|
||||
func (r *Reader) ReadUint32() uint32 {
|
||||
if r.err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
_, r.err = io.ReadFull(r.r, r.b[:4])
|
||||
if r.err != nil {
|
||||
if debug {
|
||||
dl.Printf("rd uint32: %v", r.err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
v := uint32(r.b[3]) | uint32(r.b[2])<<8 | uint32(r.b[1])<<16 | uint32(r.b[0])<<24
|
||||
|
||||
if debug {
|
||||
dl.Printf("rd uint32=%d (0x%08x)", v, v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func (r *Reader) ReadUint64() uint64 {
|
||||
if r.err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
_, r.err = io.ReadFull(r.r, r.b[:8])
|
||||
if r.err != nil {
|
||||
if debug {
|
||||
dl.Printf("rd uint64: %v", r.err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
v := uint64(r.b[7]) | uint64(r.b[6])<<8 | uint64(r.b[5])<<16 | uint64(r.b[4])<<24 |
|
||||
uint64(r.b[3])<<32 | uint64(r.b[2])<<40 | uint64(r.b[1])<<48 | uint64(r.b[0])<<56
|
||||
|
||||
if debug {
|
||||
dl.Printf("rd uint64=%d (0x%016x)", v, v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
type XDRError struct {
|
||||
op string
|
||||
err error
|
||||
}
|
||||
|
||||
func (e XDRError) Error() string {
|
||||
return "xdr " + e.op + ": " + e.err.Error()
|
||||
}
|
||||
|
||||
func (e XDRError) IsEOF() bool {
|
||||
return e.err == io.EOF
|
||||
}
|
||||
|
||||
func (r *Reader) Error() error {
|
||||
if r.err == nil {
|
||||
return nil
|
||||
}
|
||||
return XDRError{"read", r.err}
|
||||
}
|
||||
|
||||
func ElementSizeExceeded(field string, size, limit int) error {
|
||||
return fmt.Errorf("%s exceeds size limit; %d > %d", field, size, limit)
|
||||
}
|
||||
-49
@@ -1,49 +0,0 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build ipdr
|
||||
|
||||
package xdr
|
||||
|
||||
import "io"
|
||||
|
||||
func (r *Reader) ReadUint8() uint8 {
|
||||
if r.err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
_, r.err = io.ReadFull(r.r, r.b[:1])
|
||||
if r.err != nil {
|
||||
if debug {
|
||||
dl.Printf("rd uint8: %v", r.err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
if debug {
|
||||
dl.Printf("rd uint8=%d (0x%02x)", r.b[0], r.b[0])
|
||||
}
|
||||
return r.b[0]
|
||||
}
|
||||
|
||||
func (r *Reader) ReadUint16() uint16 {
|
||||
if r.err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
_, r.err = io.ReadFull(r.r, r.b[:2])
|
||||
if r.err != nil {
|
||||
if debug {
|
||||
dl.Printf("rd uint16: %v", r.err)
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
v := uint16(r.b[1]) | uint16(r.b[0])<<8
|
||||
|
||||
if debug {
|
||||
dl.Printf("rd uint16=%d (0x%04x)", v, v)
|
||||
}
|
||||
return v
|
||||
}
|
||||
-15
@@ -1,15 +0,0 @@
|
||||
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
|
||||
// All rights reserved. Use of this source code is governed by an MIT-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// +build !ipdr
|
||||
|
||||
package xdr
|
||||
|
||||
func (r *Reader) ReadUint8() uint8 {
|
||||
return uint8(r.ReadUint32())
|
||||
}
|
||||
|
||||
func (r *Reader) ReadUint16() uint16 {
|
||||
return uint16(r.ReadUint32())
|
||||
}
|
||||
-146
@@ -1,146 +0,0 @@
|
||||
// Copyright (C) 2014 Jakob Borg. All rights reserved. Use of this source code
|
||||
// is governed by an MIT-style license that can be found in the LICENSE file.
|
||||
|
||||
package xdr
|
||||
|
||||
import (
|
||||
"io"
|
||||
"reflect"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
var padBytes = []byte{0, 0, 0}
|
||||
|
||||
type Writer struct {
|
||||
w io.Writer
|
||||
tot int
|
||||
err error
|
||||
b [8]byte
|
||||
}
|
||||
|
||||
type AppendWriter []byte
|
||||
|
||||
func (w *AppendWriter) Write(bs []byte) (int, error) {
|
||||
*w = append(*w, bs...)
|
||||
return len(bs), nil
|
||||
}
|
||||
|
||||
func NewWriter(w io.Writer) *Writer {
|
||||
return &Writer{
|
||||
w: w,
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Writer) WriteRaw(bs []byte) (int, error) {
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
var n int
|
||||
n, w.err = w.w.Write(bs)
|
||||
return n, w.err
|
||||
}
|
||||
|
||||
func (w *Writer) WriteString(s string) (int, error) {
|
||||
sh := *((*reflect.StringHeader)(unsafe.Pointer(&s)))
|
||||
bh := reflect.SliceHeader{
|
||||
Data: sh.Data,
|
||||
Len: sh.Len,
|
||||
Cap: sh.Len,
|
||||
}
|
||||
return w.WriteBytes(*(*[]byte)(unsafe.Pointer(&bh)))
|
||||
}
|
||||
|
||||
func (w *Writer) WriteBytes(bs []byte) (int, error) {
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
w.WriteUint32(uint32(len(bs)))
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
if debug {
|
||||
if len(bs) > maxDebugBytes {
|
||||
dl.Printf("wr bytes (%d): %x...", len(bs), bs[:maxDebugBytes])
|
||||
} else {
|
||||
dl.Printf("wr bytes (%d): %x", len(bs), bs)
|
||||
}
|
||||
}
|
||||
|
||||
var l, n int
|
||||
n, w.err = w.w.Write(bs)
|
||||
l += n
|
||||
|
||||
if p := pad(len(bs)); w.err == nil && p > 0 {
|
||||
n, w.err = w.w.Write(padBytes[:p])
|
||||
l += n
|
||||
}
|
||||
|
||||
w.tot += l
|
||||
return l, w.err
|
||||
}
|
||||
|
||||
func (w *Writer) WriteBool(v bool) (int, error) {
|
||||
if v {
|
||||
return w.WriteUint8(1)
|
||||
} else {
|
||||
return w.WriteUint8(0)
|
||||
}
|
||||
}
|
||||
|
||||
func (w *Writer) WriteUint32(v uint32) (int, error) {
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
if debug {
|
||||
dl.Printf("wr uint32=%d", v)
|
||||
}
|
||||
|
||||
w.b[0] = byte(v >> 24)
|
||||
w.b[1] = byte(v >> 16)
|
||||
w.b[2] = byte(v >> 8)
|
||||
w.b[3] = byte(v)
|
||||
|
||||
var l int
|
||||
l, w.err = w.w.Write(w.b[:4])
|
||||
w.tot += l
|
||||
return l, w.err
|
||||
}
|
||||
|
||||
func (w *Writer) WriteUint64(v uint64) (int, error) {
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
if debug {
|
||||
dl.Printf("wr uint64=%d", v)
|
||||
}
|
||||
|
||||
w.b[0] = byte(v >> 56)
|
||||
w.b[1] = byte(v >> 48)
|
||||
w.b[2] = byte(v >> 40)
|
||||
w.b[3] = byte(v >> 32)
|
||||
w.b[4] = byte(v >> 24)
|
||||
w.b[5] = byte(v >> 16)
|
||||
w.b[6] = byte(v >> 8)
|
||||
w.b[7] = byte(v)
|
||||
|
||||
var l int
|
||||
l, w.err = w.w.Write(w.b[:8])
|
||||
w.tot += l
|
||||
return l, w.err
|
||||
}
|
||||
|
||||
func (w *Writer) Tot() int {
|
||||
return w.tot
|
||||
}
|
||||
|
||||
func (w *Writer) Error() error {
|
||||
if w.err == nil {
|
||||
return nil
|
||||
}
|
||||
return XDRError{"write", w.err}
|
||||
}
|
||||
-41
@@ -1,41 +0,0 @@
|
||||
// Copyright (C) 2014 Jakob Borg. All rights reserved. Use of this source code
|
||||
// is governed by an MIT-style license that can be found in the LICENSE file.
|
||||
|
||||
// +build ipdr
|
||||
|
||||
package xdr
|
||||
|
||||
func (w *Writer) WriteUint8(v uint8) (int, error) {
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
if debug {
|
||||
dl.Printf("wr uint8=%d", v)
|
||||
}
|
||||
|
||||
w.b[0] = byte(v)
|
||||
|
||||
var l int
|
||||
l, w.err = w.w.Write(w.b[:1])
|
||||
w.tot += l
|
||||
return l, w.err
|
||||
}
|
||||
|
||||
func (w *Writer) WriteUint16(v uint16) (int, error) {
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
if debug {
|
||||
dl.Printf("wr uint8=%d", v)
|
||||
}
|
||||
|
||||
w.b[0] = byte(v >> 8)
|
||||
w.b[1] = byte(v)
|
||||
|
||||
var l int
|
||||
l, w.err = w.w.Write(w.b[:2])
|
||||
w.tot += l
|
||||
return l, w.err
|
||||
}
|
||||
-14
@@ -1,14 +0,0 @@
|
||||
// Copyright (C) 2014 Jakob Borg. All rights reserved. Use of this source code
|
||||
// is governed by an MIT-style license that can be found in the LICENSE file.
|
||||
|
||||
// +build !ipdr
|
||||
|
||||
package xdr
|
||||
|
||||
func (w *Writer) WriteUint8(v uint8) (int, error) {
|
||||
return w.WriteUint32(uint32(v))
|
||||
}
|
||||
|
||||
func (w *Writer) WriteUint16(v uint16) (int, error) {
|
||||
return w.WriteUint32(uint32(v))
|
||||
}
|
||||
-254
@@ -1,254 +0,0 @@
|
||||
// Copyright 2011 The Snappy-Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package snappy
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io"
|
||||
)
|
||||
|
||||
// We limit how far copy back-references can go, the same as the C++ code.
|
||||
const maxOffset = 1 << 15
|
||||
|
||||
// emitLiteral writes a literal chunk and returns the number of bytes written.
|
||||
func emitLiteral(dst, lit []byte) int {
|
||||
i, n := 0, uint(len(lit)-1)
|
||||
switch {
|
||||
case n < 60:
|
||||
dst[0] = uint8(n)<<2 | tagLiteral
|
||||
i = 1
|
||||
case n < 1<<8:
|
||||
dst[0] = 60<<2 | tagLiteral
|
||||
dst[1] = uint8(n)
|
||||
i = 2
|
||||
case n < 1<<16:
|
||||
dst[0] = 61<<2 | tagLiteral
|
||||
dst[1] = uint8(n)
|
||||
dst[2] = uint8(n >> 8)
|
||||
i = 3
|
||||
case n < 1<<24:
|
||||
dst[0] = 62<<2 | tagLiteral
|
||||
dst[1] = uint8(n)
|
||||
dst[2] = uint8(n >> 8)
|
||||
dst[3] = uint8(n >> 16)
|
||||
i = 4
|
||||
case int64(n) < 1<<32:
|
||||
dst[0] = 63<<2 | tagLiteral
|
||||
dst[1] = uint8(n)
|
||||
dst[2] = uint8(n >> 8)
|
||||
dst[3] = uint8(n >> 16)
|
||||
dst[4] = uint8(n >> 24)
|
||||
i = 5
|
||||
default:
|
||||
panic("snappy: source buffer is too long")
|
||||
}
|
||||
if copy(dst[i:], lit) != len(lit) {
|
||||
panic("snappy: destination buffer is too short")
|
||||
}
|
||||
return i + len(lit)
|
||||
}
|
||||
|
||||
// emitCopy writes a copy chunk and returns the number of bytes written.
|
||||
func emitCopy(dst []byte, offset, length int) int {
|
||||
i := 0
|
||||
for length > 0 {
|
||||
x := length - 4
|
||||
if 0 <= x && x < 1<<3 && offset < 1<<11 {
|
||||
dst[i+0] = uint8(offset>>8)&0x07<<5 | uint8(x)<<2 | tagCopy1
|
||||
dst[i+1] = uint8(offset)
|
||||
i += 2
|
||||
break
|
||||
}
|
||||
|
||||
x = length
|
||||
if x > 1<<6 {
|
||||
x = 1 << 6
|
||||
}
|
||||
dst[i+0] = uint8(x-1)<<2 | tagCopy2
|
||||
dst[i+1] = uint8(offset)
|
||||
dst[i+2] = uint8(offset >> 8)
|
||||
i += 3
|
||||
length -= x
|
||||
}
|
||||
return i
|
||||
}
|
||||
|
||||
// Encode returns the encoded form of src. The returned slice may be a sub-
|
||||
// slice of dst if dst was large enough to hold the entire encoded block.
|
||||
// Otherwise, a newly allocated slice will be returned.
|
||||
// It is valid to pass a nil dst.
|
||||
func Encode(dst, src []byte) []byte {
|
||||
if n := MaxEncodedLen(len(src)); len(dst) < n {
|
||||
dst = make([]byte, n)
|
||||
}
|
||||
|
||||
// The block starts with the varint-encoded length of the decompressed bytes.
|
||||
d := binary.PutUvarint(dst, uint64(len(src)))
|
||||
|
||||
// Return early if src is short.
|
||||
if len(src) <= 4 {
|
||||
if len(src) != 0 {
|
||||
d += emitLiteral(dst[d:], src)
|
||||
}
|
||||
return dst[:d]
|
||||
}
|
||||
|
||||
// Initialize the hash table. Its size ranges from 1<<8 to 1<<14 inclusive.
|
||||
const maxTableSize = 1 << 14
|
||||
shift, tableSize := uint(32-8), 1<<8
|
||||
for tableSize < maxTableSize && tableSize < len(src) {
|
||||
shift--
|
||||
tableSize *= 2
|
||||
}
|
||||
var table [maxTableSize]int
|
||||
|
||||
// Iterate over the source bytes.
|
||||
var (
|
||||
s int // The iterator position.
|
||||
t int // The last position with the same hash as s.
|
||||
lit int // The start position of any pending literal bytes.
|
||||
)
|
||||
for s+3 < len(src) {
|
||||
// Update the hash table.
|
||||
b0, b1, b2, b3 := src[s], src[s+1], src[s+2], src[s+3]
|
||||
h := uint32(b0) | uint32(b1)<<8 | uint32(b2)<<16 | uint32(b3)<<24
|
||||
p := &table[(h*0x1e35a7bd)>>shift]
|
||||
// We need to to store values in [-1, inf) in table. To save
|
||||
// some initialization time, (re)use the table's zero value
|
||||
// and shift the values against this zero: add 1 on writes,
|
||||
// subtract 1 on reads.
|
||||
t, *p = *p-1, s+1
|
||||
// If t is invalid or src[s:s+4] differs from src[t:t+4], accumulate a literal byte.
|
||||
if t < 0 || s-t >= maxOffset || b0 != src[t] || b1 != src[t+1] || b2 != src[t+2] || b3 != src[t+3] {
|
||||
s++
|
||||
continue
|
||||
}
|
||||
// Otherwise, we have a match. First, emit any pending literal bytes.
|
||||
if lit != s {
|
||||
d += emitLiteral(dst[d:], src[lit:s])
|
||||
}
|
||||
// Extend the match to be as long as possible.
|
||||
s0 := s
|
||||
s, t = s+4, t+4
|
||||
for s < len(src) && src[s] == src[t] {
|
||||
s++
|
||||
t++
|
||||
}
|
||||
// Emit the copied bytes.
|
||||
d += emitCopy(dst[d:], s-t, s-s0)
|
||||
lit = s
|
||||
}
|
||||
|
||||
// Emit any final pending literal bytes and return.
|
||||
if lit != len(src) {
|
||||
d += emitLiteral(dst[d:], src[lit:])
|
||||
}
|
||||
return dst[:d]
|
||||
}
|
||||
|
||||
// MaxEncodedLen returns the maximum length of a snappy block, given its
|
||||
// uncompressed length.
|
||||
func MaxEncodedLen(srcLen int) int {
|
||||
// Compressed data can be defined as:
|
||||
// compressed := item* literal*
|
||||
// item := literal* copy
|
||||
//
|
||||
// The trailing literal sequence has a space blowup of at most 62/60
|
||||
// since a literal of length 60 needs one tag byte + one extra byte
|
||||
// for length information.
|
||||
//
|
||||
// Item blowup is trickier to measure. Suppose the "copy" op copies
|
||||
// 4 bytes of data. Because of a special check in the encoding code,
|
||||
// we produce a 4-byte copy only if the offset is < 65536. Therefore
|
||||
// the copy op takes 3 bytes to encode, and this type of item leads
|
||||
// to at most the 62/60 blowup for representing literals.
|
||||
//
|
||||
// Suppose the "copy" op copies 5 bytes of data. If the offset is big
|
||||
// enough, it will take 5 bytes to encode the copy op. Therefore the
|
||||
// worst case here is a one-byte literal followed by a five-byte copy.
|
||||
// That is, 6 bytes of input turn into 7 bytes of "compressed" data.
|
||||
//
|
||||
// This last factor dominates the blowup, so the final estimate is:
|
||||
return 32 + srcLen + srcLen/6
|
||||
}
|
||||
|
||||
// NewWriter returns a new Writer that compresses to w, using the framing
|
||||
// format described at
|
||||
// https://github.com/google/snappy/blob/master/framing_format.txt
|
||||
func NewWriter(w io.Writer) *Writer {
|
||||
return &Writer{
|
||||
w: w,
|
||||
enc: make([]byte, MaxEncodedLen(maxUncompressedChunkLen)),
|
||||
}
|
||||
}
|
||||
|
||||
// Writer is an io.Writer than can write Snappy-compressed bytes.
|
||||
type Writer struct {
|
||||
w io.Writer
|
||||
err error
|
||||
enc []byte
|
||||
buf [checksumSize + chunkHeaderSize]byte
|
||||
wroteHeader bool
|
||||
}
|
||||
|
||||
// Reset discards the writer's state and switches the Snappy writer to write to
|
||||
// w. This permits reusing a Writer rather than allocating a new one.
|
||||
func (w *Writer) Reset(writer io.Writer) {
|
||||
w.w = writer
|
||||
w.err = nil
|
||||
w.wroteHeader = false
|
||||
}
|
||||
|
||||
// Write satisfies the io.Writer interface.
|
||||
func (w *Writer) Write(p []byte) (n int, errRet error) {
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
}
|
||||
if !w.wroteHeader {
|
||||
copy(w.enc, magicChunk)
|
||||
if _, err := w.w.Write(w.enc[:len(magicChunk)]); err != nil {
|
||||
w.err = err
|
||||
return n, err
|
||||
}
|
||||
w.wroteHeader = true
|
||||
}
|
||||
for len(p) > 0 {
|
||||
var uncompressed []byte
|
||||
if len(p) > maxUncompressedChunkLen {
|
||||
uncompressed, p = p[:maxUncompressedChunkLen], p[maxUncompressedChunkLen:]
|
||||
} else {
|
||||
uncompressed, p = p, nil
|
||||
}
|
||||
checksum := crc(uncompressed)
|
||||
|
||||
// Compress the buffer, discarding the result if the improvement
|
||||
// isn't at least 12.5%.
|
||||
chunkType := uint8(chunkTypeCompressedData)
|
||||
chunkBody := Encode(w.enc, uncompressed)
|
||||
if len(chunkBody) >= len(uncompressed)-len(uncompressed)/8 {
|
||||
chunkType, chunkBody = chunkTypeUncompressedData, uncompressed
|
||||
}
|
||||
|
||||
chunkLen := 4 + len(chunkBody)
|
||||
w.buf[0] = chunkType
|
||||
w.buf[1] = uint8(chunkLen >> 0)
|
||||
w.buf[2] = uint8(chunkLen >> 8)
|
||||
w.buf[3] = uint8(chunkLen >> 16)
|
||||
w.buf[4] = uint8(checksum >> 0)
|
||||
w.buf[5] = uint8(checksum >> 8)
|
||||
w.buf[6] = uint8(checksum >> 16)
|
||||
w.buf[7] = uint8(checksum >> 24)
|
||||
if _, err := w.w.Write(w.buf[:]); err != nil {
|
||||
w.err = err
|
||||
return n, err
|
||||
}
|
||||
if _, err := w.w.Write(chunkBody); err != nil {
|
||||
w.err = err
|
||||
return n, err
|
||||
}
|
||||
n += len(uncompressed)
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
*.[68]
|
||||
*.a
|
||||
*.out
|
||||
*.swp
|
||||
_obj
|
||||
_testmain.go
|
||||
cmd/metrics-bench/metrics-bench
|
||||
cmd/metrics-example/metrics-example
|
||||
cmd/never-read/never-read
|
||||
-13
@@ -1,13 +0,0 @@
|
||||
language: go
|
||||
|
||||
go:
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
|
||||
script:
|
||||
- ./validate.sh
|
||||
|
||||
# this should give us faster builds according to
|
||||
# http://docs.travis-ci.com/user/migrating-from-legacy/
|
||||
sudo: false
|
||||
Generated
Vendored
-543
@@ -1,543 +0,0 @@
|
||||
// Copyright (c) 2012, Suryandaru Triandana <syndtr@gmail.com>
|
||||
// All rights reservefs.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
var errFileOpen = errors.New("leveldb/storage: file still open")
|
||||
|
||||
type fileLock interface {
|
||||
release() error
|
||||
}
|
||||
|
||||
type fileStorageLock struct {
|
||||
fs *fileStorage
|
||||
}
|
||||
|
||||
func (lock *fileStorageLock) Release() {
|
||||
fs := lock.fs
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
if fs.slock == lock {
|
||||
fs.slock = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// fileStorage is a file-system backed storage.
|
||||
type fileStorage struct {
|
||||
path string
|
||||
|
||||
mu sync.Mutex
|
||||
flock fileLock
|
||||
slock *fileStorageLock
|
||||
logw *os.File
|
||||
buf []byte
|
||||
// Opened file counter; if open < 0 means closed.
|
||||
open int
|
||||
day int
|
||||
}
|
||||
|
||||
// OpenFile returns a new filesytem-backed storage implementation with the given
|
||||
// path. This also hold a file lock, so any subsequent attempt to open the same
|
||||
// path will fail.
|
||||
//
|
||||
// The storage must be closed after use, by calling Close method.
|
||||
func OpenFile(path string) (Storage, error) {
|
||||
if err := os.MkdirAll(path, 0755); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
flock, err := newFileLock(filepath.Join(path, "LOCK"))
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
defer func() {
|
||||
if err != nil {
|
||||
flock.release()
|
||||
}
|
||||
}()
|
||||
|
||||
rename(filepath.Join(path, "LOG"), filepath.Join(path, "LOG.old"))
|
||||
logw, err := os.OpenFile(filepath.Join(path, "LOG"), os.O_WRONLY|os.O_CREATE, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
fs := &fileStorage{path: path, flock: flock, logw: logw}
|
||||
runtime.SetFinalizer(fs, (*fileStorage).Close)
|
||||
return fs, nil
|
||||
}
|
||||
|
||||
func (fs *fileStorage) Lock() (util.Releaser, error) {
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
if fs.open < 0 {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
if fs.slock != nil {
|
||||
return nil, ErrLocked
|
||||
}
|
||||
fs.slock = &fileStorageLock{fs: fs}
|
||||
return fs.slock, nil
|
||||
}
|
||||
|
||||
func itoa(buf []byte, i int, wid int) []byte {
|
||||
var u uint = uint(i)
|
||||
if u == 0 && wid <= 1 {
|
||||
return append(buf, '0')
|
||||
}
|
||||
|
||||
// Assemble decimal in reverse order.
|
||||
var b [32]byte
|
||||
bp := len(b)
|
||||
for ; u > 0 || wid > 0; u /= 10 {
|
||||
bp--
|
||||
wid--
|
||||
b[bp] = byte(u%10) + '0'
|
||||
}
|
||||
return append(buf, b[bp:]...)
|
||||
}
|
||||
|
||||
func (fs *fileStorage) printDay(t time.Time) {
|
||||
if fs.day == t.Day() {
|
||||
return
|
||||
}
|
||||
fs.day = t.Day()
|
||||
fs.logw.Write([]byte("=============== " + t.Format("Jan 2, 2006 (MST)") + " ===============\n"))
|
||||
}
|
||||
|
||||
func (fs *fileStorage) doLog(t time.Time, str string) {
|
||||
fs.printDay(t)
|
||||
hour, min, sec := t.Clock()
|
||||
msec := t.Nanosecond() / 1e3
|
||||
// time
|
||||
fs.buf = itoa(fs.buf[:0], hour, 2)
|
||||
fs.buf = append(fs.buf, ':')
|
||||
fs.buf = itoa(fs.buf, min, 2)
|
||||
fs.buf = append(fs.buf, ':')
|
||||
fs.buf = itoa(fs.buf, sec, 2)
|
||||
fs.buf = append(fs.buf, '.')
|
||||
fs.buf = itoa(fs.buf, msec, 6)
|
||||
fs.buf = append(fs.buf, ' ')
|
||||
// write
|
||||
fs.buf = append(fs.buf, []byte(str)...)
|
||||
fs.buf = append(fs.buf, '\n')
|
||||
fs.logw.Write(fs.buf)
|
||||
}
|
||||
|
||||
func (fs *fileStorage) Log(str string) {
|
||||
t := time.Now()
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
if fs.open < 0 {
|
||||
return
|
||||
}
|
||||
fs.doLog(t, str)
|
||||
}
|
||||
|
||||
func (fs *fileStorage) log(str string) {
|
||||
fs.doLog(time.Now(), str)
|
||||
}
|
||||
|
||||
func (fs *fileStorage) GetFile(num uint64, t FileType) File {
|
||||
return &file{fs: fs, num: num, t: t}
|
||||
}
|
||||
|
||||
func (fs *fileStorage) GetFiles(t FileType) (ff []File, err error) {
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
if fs.open < 0 {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
dir, err := os.Open(fs.path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fnn, err := dir.Readdirnames(0)
|
||||
// Close the dir first before checking for Readdirnames error.
|
||||
if err := dir.Close(); err != nil {
|
||||
fs.log(fmt.Sprintf("close dir: %v", err))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
f := &file{fs: fs}
|
||||
for _, fn := range fnn {
|
||||
if f.parse(fn) && (f.t&t) != 0 {
|
||||
ff = append(ff, f)
|
||||
f = &file{fs: fs}
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (fs *fileStorage) GetManifest() (f File, err error) {
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
if fs.open < 0 {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
dir, err := os.Open(fs.path)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
fnn, err := dir.Readdirnames(0)
|
||||
// Close the dir first before checking for Readdirnames error.
|
||||
if err := dir.Close(); err != nil {
|
||||
fs.log(fmt.Sprintf("close dir: %v", err))
|
||||
}
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
// Find latest CURRENT file.
|
||||
var rem []string
|
||||
var pend bool
|
||||
var cerr error
|
||||
for _, fn := range fnn {
|
||||
if strings.HasPrefix(fn, "CURRENT") {
|
||||
pend1 := len(fn) > 7
|
||||
// Make sure it is valid name for a CURRENT file, otherwise skip it.
|
||||
if pend1 {
|
||||
if fn[7] != '.' || len(fn) < 9 {
|
||||
fs.log(fmt.Sprintf("skipping %s: invalid file name", fn))
|
||||
continue
|
||||
}
|
||||
if _, e1 := strconv.ParseUint(fn[8:], 10, 0); e1 != nil {
|
||||
fs.log(fmt.Sprintf("skipping %s: invalid file num: %v", fn, e1))
|
||||
continue
|
||||
}
|
||||
}
|
||||
path := filepath.Join(fs.path, fn)
|
||||
r, e1 := os.OpenFile(path, os.O_RDONLY, 0)
|
||||
if e1 != nil {
|
||||
return nil, e1
|
||||
}
|
||||
b, e1 := ioutil.ReadAll(r)
|
||||
if e1 != nil {
|
||||
r.Close()
|
||||
return nil, e1
|
||||
}
|
||||
f1 := &file{fs: fs}
|
||||
if len(b) < 1 || b[len(b)-1] != '\n' || !f1.parse(string(b[:len(b)-1])) {
|
||||
fs.log(fmt.Sprintf("skipping %s: corrupted or incomplete", fn))
|
||||
if pend1 {
|
||||
rem = append(rem, fn)
|
||||
}
|
||||
if !pend1 || cerr == nil {
|
||||
cerr = &ErrCorrupted{
|
||||
File: fsParseName(filepath.Base(fn)),
|
||||
Err: errors.New("leveldb/storage: corrupted or incomplete manifest file"),
|
||||
}
|
||||
}
|
||||
} else if f != nil && f1.Num() < f.Num() {
|
||||
fs.log(fmt.Sprintf("skipping %s: obsolete", fn))
|
||||
if pend1 {
|
||||
rem = append(rem, fn)
|
||||
}
|
||||
} else {
|
||||
f = f1
|
||||
pend = pend1
|
||||
}
|
||||
if err := r.Close(); err != nil {
|
||||
fs.log(fmt.Sprintf("close %s: %v", fn, err))
|
||||
}
|
||||
}
|
||||
}
|
||||
// Don't remove any files if there is no valid CURRENT file.
|
||||
if f == nil {
|
||||
if cerr != nil {
|
||||
err = cerr
|
||||
} else {
|
||||
err = os.ErrNotExist
|
||||
}
|
||||
return
|
||||
}
|
||||
// Rename pending CURRENT file to an effective CURRENT.
|
||||
if pend {
|
||||
path := fmt.Sprintf("%s.%d", filepath.Join(fs.path, "CURRENT"), f.Num())
|
||||
if err := rename(path, filepath.Join(fs.path, "CURRENT")); err != nil {
|
||||
fs.log(fmt.Sprintf("CURRENT.%d -> CURRENT: %v", f.Num(), err))
|
||||
}
|
||||
}
|
||||
// Remove obsolete or incomplete pending CURRENT files.
|
||||
for _, fn := range rem {
|
||||
path := filepath.Join(fs.path, fn)
|
||||
if err := os.Remove(path); err != nil {
|
||||
fs.log(fmt.Sprintf("remove %s: %v", fn, err))
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (fs *fileStorage) SetManifest(f File) (err error) {
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
if fs.open < 0 {
|
||||
return ErrClosed
|
||||
}
|
||||
f2, ok := f.(*file)
|
||||
if !ok || f2.t != TypeManifest {
|
||||
return ErrInvalidFile
|
||||
}
|
||||
defer func() {
|
||||
if err != nil {
|
||||
fs.log(fmt.Sprintf("CURRENT: %v", err))
|
||||
}
|
||||
}()
|
||||
path := fmt.Sprintf("%s.%d", filepath.Join(fs.path, "CURRENT"), f2.Num())
|
||||
w, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = fmt.Fprintln(w, f2.name())
|
||||
// Close the file first.
|
||||
if err := w.Close(); err != nil {
|
||||
fs.log(fmt.Sprintf("close CURRENT.%d: %v", f2.num, err))
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return rename(path, filepath.Join(fs.path, "CURRENT"))
|
||||
}
|
||||
|
||||
func (fs *fileStorage) Close() error {
|
||||
fs.mu.Lock()
|
||||
defer fs.mu.Unlock()
|
||||
if fs.open < 0 {
|
||||
return ErrClosed
|
||||
}
|
||||
// Clear the finalizer.
|
||||
runtime.SetFinalizer(fs, nil)
|
||||
|
||||
if fs.open > 0 {
|
||||
fs.log(fmt.Sprintf("close: warning, %d files still open", fs.open))
|
||||
}
|
||||
fs.open = -1
|
||||
e1 := fs.logw.Close()
|
||||
err := fs.flock.release()
|
||||
if err == nil {
|
||||
err = e1
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type fileWrap struct {
|
||||
*os.File
|
||||
f *file
|
||||
}
|
||||
|
||||
func (fw fileWrap) Sync() error {
|
||||
if err := fw.File.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
if fw.f.Type() == TypeManifest {
|
||||
// Also sync parent directory if file type is manifest.
|
||||
// See: https://code.google.com/p/leveldb/issues/detail?id=190.
|
||||
if err := syncDir(fw.f.fs.path); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fw fileWrap) Close() error {
|
||||
f := fw.f
|
||||
f.fs.mu.Lock()
|
||||
defer f.fs.mu.Unlock()
|
||||
if !f.open {
|
||||
return ErrClosed
|
||||
}
|
||||
f.open = false
|
||||
f.fs.open--
|
||||
err := fw.File.Close()
|
||||
if err != nil {
|
||||
f.fs.log(fmt.Sprintf("close %s.%d: %v", f.Type(), f.Num(), err))
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
type file struct {
|
||||
fs *fileStorage
|
||||
num uint64
|
||||
t FileType
|
||||
open bool
|
||||
}
|
||||
|
||||
func (f *file) Open() (Reader, error) {
|
||||
f.fs.mu.Lock()
|
||||
defer f.fs.mu.Unlock()
|
||||
if f.fs.open < 0 {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
if f.open {
|
||||
return nil, errFileOpen
|
||||
}
|
||||
of, err := os.OpenFile(f.path(), os.O_RDONLY, 0)
|
||||
if err != nil {
|
||||
if f.hasOldName() && os.IsNotExist(err) {
|
||||
of, err = os.OpenFile(f.oldPath(), os.O_RDONLY, 0)
|
||||
if err == nil {
|
||||
goto ok
|
||||
}
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
ok:
|
||||
f.open = true
|
||||
f.fs.open++
|
||||
return fileWrap{of, f}, nil
|
||||
}
|
||||
|
||||
func (f *file) Create() (Writer, error) {
|
||||
f.fs.mu.Lock()
|
||||
defer f.fs.mu.Unlock()
|
||||
if f.fs.open < 0 {
|
||||
return nil, ErrClosed
|
||||
}
|
||||
if f.open {
|
||||
return nil, errFileOpen
|
||||
}
|
||||
of, err := os.OpenFile(f.path(), os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
f.open = true
|
||||
f.fs.open++
|
||||
return fileWrap{of, f}, nil
|
||||
}
|
||||
|
||||
func (f *file) Replace(newfile File) error {
|
||||
f.fs.mu.Lock()
|
||||
defer f.fs.mu.Unlock()
|
||||
if f.fs.open < 0 {
|
||||
return ErrClosed
|
||||
}
|
||||
newfile2, ok := newfile.(*file)
|
||||
if !ok {
|
||||
return ErrInvalidFile
|
||||
}
|
||||
if f.open || newfile2.open {
|
||||
return errFileOpen
|
||||
}
|
||||
return rename(newfile2.path(), f.path())
|
||||
}
|
||||
|
||||
func (f *file) Type() FileType {
|
||||
return f.t
|
||||
}
|
||||
|
||||
func (f *file) Num() uint64 {
|
||||
return f.num
|
||||
}
|
||||
|
||||
func (f *file) Remove() error {
|
||||
f.fs.mu.Lock()
|
||||
defer f.fs.mu.Unlock()
|
||||
if f.fs.open < 0 {
|
||||
return ErrClosed
|
||||
}
|
||||
if f.open {
|
||||
return errFileOpen
|
||||
}
|
||||
err := os.Remove(f.path())
|
||||
if err != nil {
|
||||
f.fs.log(fmt.Sprintf("remove %s.%d: %v", f.Type(), f.Num(), err))
|
||||
}
|
||||
// Also try remove file with old name, just in case.
|
||||
if f.hasOldName() {
|
||||
if e1 := os.Remove(f.oldPath()); !os.IsNotExist(e1) {
|
||||
f.fs.log(fmt.Sprintf("remove %s.%d: %v (old name)", f.Type(), f.Num(), err))
|
||||
err = e1
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (f *file) hasOldName() bool {
|
||||
return f.t == TypeTable
|
||||
}
|
||||
|
||||
func (f *file) oldName() string {
|
||||
switch f.t {
|
||||
case TypeTable:
|
||||
return fmt.Sprintf("%06d.sst", f.num)
|
||||
}
|
||||
return f.name()
|
||||
}
|
||||
|
||||
func (f *file) oldPath() string {
|
||||
return filepath.Join(f.fs.path, f.oldName())
|
||||
}
|
||||
|
||||
func (f *file) name() string {
|
||||
switch f.t {
|
||||
case TypeManifest:
|
||||
return fmt.Sprintf("MANIFEST-%06d", f.num)
|
||||
case TypeJournal:
|
||||
return fmt.Sprintf("%06d.log", f.num)
|
||||
case TypeTable:
|
||||
return fmt.Sprintf("%06d.ldb", f.num)
|
||||
case TypeTemp:
|
||||
return fmt.Sprintf("%06d.tmp", f.num)
|
||||
default:
|
||||
panic("invalid file type")
|
||||
}
|
||||
}
|
||||
|
||||
func (f *file) path() string {
|
||||
return filepath.Join(f.fs.path, f.name())
|
||||
}
|
||||
|
||||
func fsParseName(name string) *FileInfo {
|
||||
fi := &FileInfo{}
|
||||
var tail string
|
||||
_, err := fmt.Sscanf(name, "%d.%s", &fi.Num, &tail)
|
||||
if err == nil {
|
||||
switch tail {
|
||||
case "log":
|
||||
fi.Type = TypeJournal
|
||||
case "ldb", "sst":
|
||||
fi.Type = TypeTable
|
||||
case "tmp":
|
||||
fi.Type = TypeTemp
|
||||
default:
|
||||
return nil
|
||||
}
|
||||
return fi
|
||||
}
|
||||
n, _ := fmt.Sscanf(name, "MANIFEST-%d%s", &fi.Num, &tail)
|
||||
if n == 1 {
|
||||
fi.Type = TypeManifest
|
||||
return fi
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *file) parse(name string) bool {
|
||||
fi := fsParseName(name)
|
||||
if fi == nil {
|
||||
return false
|
||||
}
|
||||
f.t = fi.Type
|
||||
f.num = fi.Num
|
||||
return true
|
||||
}
|
||||
-203
@@ -1,203 +0,0 @@
|
||||
// Copyright (c) 2013, Suryandaru Triandana <syndtr@gmail.com>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package storage
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"sync"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
const typeShift = 3
|
||||
|
||||
type memStorageLock struct {
|
||||
ms *memStorage
|
||||
}
|
||||
|
||||
func (lock *memStorageLock) Release() {
|
||||
ms := lock.ms
|
||||
ms.mu.Lock()
|
||||
defer ms.mu.Unlock()
|
||||
if ms.slock == lock {
|
||||
ms.slock = nil
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// memStorage is a memory-backed storage.
|
||||
type memStorage struct {
|
||||
mu sync.Mutex
|
||||
slock *memStorageLock
|
||||
files map[uint64]*memFile
|
||||
manifest *memFilePtr
|
||||
}
|
||||
|
||||
// NewMemStorage returns a new memory-backed storage implementation.
|
||||
func NewMemStorage() Storage {
|
||||
return &memStorage{
|
||||
files: make(map[uint64]*memFile),
|
||||
}
|
||||
}
|
||||
|
||||
func (ms *memStorage) Lock() (util.Releaser, error) {
|
||||
ms.mu.Lock()
|
||||
defer ms.mu.Unlock()
|
||||
if ms.slock != nil {
|
||||
return nil, ErrLocked
|
||||
}
|
||||
ms.slock = &memStorageLock{ms: ms}
|
||||
return ms.slock, nil
|
||||
}
|
||||
|
||||
func (*memStorage) Log(str string) {}
|
||||
|
||||
func (ms *memStorage) GetFile(num uint64, t FileType) File {
|
||||
return &memFilePtr{ms: ms, num: num, t: t}
|
||||
}
|
||||
|
||||
func (ms *memStorage) GetFiles(t FileType) ([]File, error) {
|
||||
ms.mu.Lock()
|
||||
var ff []File
|
||||
for x, _ := range ms.files {
|
||||
num, mt := x>>typeShift, FileType(x)&TypeAll
|
||||
if mt&t == 0 {
|
||||
continue
|
||||
}
|
||||
ff = append(ff, &memFilePtr{ms: ms, num: num, t: mt})
|
||||
}
|
||||
ms.mu.Unlock()
|
||||
return ff, nil
|
||||
}
|
||||
|
||||
func (ms *memStorage) GetManifest() (File, error) {
|
||||
ms.mu.Lock()
|
||||
defer ms.mu.Unlock()
|
||||
if ms.manifest == nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return ms.manifest, nil
|
||||
}
|
||||
|
||||
func (ms *memStorage) SetManifest(f File) error {
|
||||
fm, ok := f.(*memFilePtr)
|
||||
if !ok || fm.t != TypeManifest {
|
||||
return ErrInvalidFile
|
||||
}
|
||||
ms.mu.Lock()
|
||||
ms.manifest = fm
|
||||
ms.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
func (*memStorage) Close() error { return nil }
|
||||
|
||||
type memReader struct {
|
||||
*bytes.Reader
|
||||
m *memFile
|
||||
}
|
||||
|
||||
func (mr *memReader) Close() error {
|
||||
return mr.m.Close()
|
||||
}
|
||||
|
||||
type memFile struct {
|
||||
bytes.Buffer
|
||||
ms *memStorage
|
||||
open bool
|
||||
}
|
||||
|
||||
func (*memFile) Sync() error { return nil }
|
||||
func (m *memFile) Close() error {
|
||||
m.ms.mu.Lock()
|
||||
m.open = false
|
||||
m.ms.mu.Unlock()
|
||||
return nil
|
||||
}
|
||||
|
||||
type memFilePtr struct {
|
||||
ms *memStorage
|
||||
num uint64
|
||||
t FileType
|
||||
}
|
||||
|
||||
func (p *memFilePtr) x() uint64 {
|
||||
return p.Num()<<typeShift | uint64(p.Type())
|
||||
}
|
||||
|
||||
func (p *memFilePtr) Open() (Reader, error) {
|
||||
ms := p.ms
|
||||
ms.mu.Lock()
|
||||
defer ms.mu.Unlock()
|
||||
if m, exist := ms.files[p.x()]; exist {
|
||||
if m.open {
|
||||
return nil, errFileOpen
|
||||
}
|
||||
m.open = true
|
||||
return &memReader{Reader: bytes.NewReader(m.Bytes()), m: m}, nil
|
||||
}
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
func (p *memFilePtr) Create() (Writer, error) {
|
||||
ms := p.ms
|
||||
ms.mu.Lock()
|
||||
defer ms.mu.Unlock()
|
||||
m, exist := ms.files[p.x()]
|
||||
if exist {
|
||||
if m.open {
|
||||
return nil, errFileOpen
|
||||
}
|
||||
m.Reset()
|
||||
} else {
|
||||
m = &memFile{ms: ms}
|
||||
ms.files[p.x()] = m
|
||||
}
|
||||
m.open = true
|
||||
return m, nil
|
||||
}
|
||||
|
||||
func (p *memFilePtr) Replace(newfile File) error {
|
||||
p1, ok := newfile.(*memFilePtr)
|
||||
if !ok {
|
||||
return ErrInvalidFile
|
||||
}
|
||||
ms := p.ms
|
||||
ms.mu.Lock()
|
||||
defer ms.mu.Unlock()
|
||||
m1, exist := ms.files[p1.x()]
|
||||
if !exist {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
m0, exist := ms.files[p.x()]
|
||||
if (exist && m0.open) || m1.open {
|
||||
return errFileOpen
|
||||
}
|
||||
delete(ms.files, p1.x())
|
||||
ms.files[p.x()] = m1
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *memFilePtr) Type() FileType {
|
||||
return p.t
|
||||
}
|
||||
|
||||
func (p *memFilePtr) Num() uint64 {
|
||||
return p.num
|
||||
}
|
||||
|
||||
func (p *memFilePtr) Remove() error {
|
||||
ms := p.ms
|
||||
ms.mu.Lock()
|
||||
defer ms.mu.Unlock()
|
||||
if _, exist := ms.files[p.x()]; exist {
|
||||
delete(ms.files, p.x())
|
||||
return nil
|
||||
}
|
||||
return os.ErrNotExist
|
||||
}
|
||||
-187
@@ -1,187 +0,0 @@
|
||||
// Copyright (c) 2014, Suryandaru Triandana <syndtr@gmail.com>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
|
||||
. "github.com/onsi/ginkgo"
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
func KeyValueTesting(rnd *rand.Rand, kv KeyValue, p DB, setup func(KeyValue) DB, teardown func(DB)) {
|
||||
if rnd == nil {
|
||||
rnd = NewRand()
|
||||
}
|
||||
|
||||
if p == nil {
|
||||
BeforeEach(func() {
|
||||
p = setup(kv)
|
||||
})
|
||||
if teardown != nil {
|
||||
AfterEach(func() {
|
||||
teardown(p)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
It("Should find all keys with Find", func() {
|
||||
if db, ok := p.(Find); ok {
|
||||
ShuffledIndex(nil, kv.Len(), 1, func(i int) {
|
||||
key_, key, value := kv.IndexInexact(i)
|
||||
|
||||
// Using exact key.
|
||||
rkey, rvalue, err := db.TestFind(key)
|
||||
Expect(err).ShouldNot(HaveOccurred(), "Error for key %q", key)
|
||||
Expect(rkey).Should(Equal(key), "Key")
|
||||
Expect(rvalue).Should(Equal(value), "Value for key %q", key)
|
||||
|
||||
// Using inexact key.
|
||||
rkey, rvalue, err = db.TestFind(key_)
|
||||
Expect(err).ShouldNot(HaveOccurred(), "Error for key %q (%q)", key_, key)
|
||||
Expect(rkey).Should(Equal(key))
|
||||
Expect(rvalue).Should(Equal(value), "Value for key %q (%q)", key_, key)
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
It("Should return error if the key is not present", func() {
|
||||
if db, ok := p.(Find); ok {
|
||||
var key []byte
|
||||
if kv.Len() > 0 {
|
||||
key_, _ := kv.Index(kv.Len() - 1)
|
||||
key = BytesAfter(key_)
|
||||
}
|
||||
rkey, _, err := db.TestFind(key)
|
||||
Expect(err).Should(HaveOccurred(), "Find for key %q yield key %q", key, rkey)
|
||||
Expect(err).Should(Equal(errors.ErrNotFound))
|
||||
}
|
||||
})
|
||||
|
||||
It("Should only find exact key with Get", func() {
|
||||
if db, ok := p.(Get); ok {
|
||||
ShuffledIndex(nil, kv.Len(), 1, func(i int) {
|
||||
key_, key, value := kv.IndexInexact(i)
|
||||
|
||||
// Using exact key.
|
||||
rvalue, err := db.TestGet(key)
|
||||
Expect(err).ShouldNot(HaveOccurred(), "Error for key %q", key)
|
||||
Expect(rvalue).Should(Equal(value), "Value for key %q", key)
|
||||
|
||||
// Using inexact key.
|
||||
if len(key_) > 0 {
|
||||
_, err = db.TestGet(key_)
|
||||
Expect(err).Should(HaveOccurred(), "Error for key %q", key_)
|
||||
Expect(err).Should(Equal(errors.ErrNotFound))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
It("Should only find present key with Has", func() {
|
||||
if db, ok := p.(Has); ok {
|
||||
ShuffledIndex(nil, kv.Len(), 1, func(i int) {
|
||||
key_, key, _ := kv.IndexInexact(i)
|
||||
|
||||
// Using exact key.
|
||||
ret, err := db.TestHas(key)
|
||||
Expect(err).ShouldNot(HaveOccurred(), "Error for key %q", key)
|
||||
Expect(ret).Should(BeTrue(), "False for key %q", key)
|
||||
|
||||
// Using inexact key.
|
||||
if len(key_) > 0 {
|
||||
ret, err = db.TestHas(key_)
|
||||
Expect(err).ShouldNot(HaveOccurred(), "Error for key %q", key_)
|
||||
Expect(ret).ShouldNot(BeTrue(), "True for key %q", key)
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
TestIter := func(r *util.Range, _kv KeyValue) {
|
||||
if db, ok := p.(NewIterator); ok {
|
||||
iter := db.TestNewIterator(r)
|
||||
Expect(iter.Error()).ShouldNot(HaveOccurred())
|
||||
|
||||
t := IteratorTesting{
|
||||
KeyValue: _kv,
|
||||
Iter: iter,
|
||||
}
|
||||
|
||||
DoIteratorTesting(&t)
|
||||
iter.Release()
|
||||
}
|
||||
}
|
||||
|
||||
It("Should iterates and seeks correctly", func(done Done) {
|
||||
TestIter(nil, kv.Clone())
|
||||
done <- true
|
||||
}, 3.0)
|
||||
|
||||
RandomIndex(rnd, kv.Len(), Min(kv.Len(), 50), func(i int) {
|
||||
type slice struct {
|
||||
r *util.Range
|
||||
start, limit int
|
||||
}
|
||||
|
||||
key_, _, _ := kv.IndexInexact(i)
|
||||
for _, x := range []slice{
|
||||
{&util.Range{Start: key_, Limit: nil}, i, kv.Len()},
|
||||
{&util.Range{Start: nil, Limit: key_}, 0, i},
|
||||
} {
|
||||
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", x.start, x.limit), func(done Done) {
|
||||
TestIter(x.r, kv.Slice(x.start, x.limit))
|
||||
done <- true
|
||||
}, 3.0)
|
||||
}
|
||||
})
|
||||
|
||||
RandomRange(rnd, kv.Len(), Min(kv.Len(), 50), func(start, limit int) {
|
||||
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", start, limit), func(done Done) {
|
||||
r := kv.Range(start, limit)
|
||||
TestIter(&r, kv.Slice(start, limit))
|
||||
done <- true
|
||||
}, 3.0)
|
||||
})
|
||||
}
|
||||
|
||||
func AllKeyValueTesting(rnd *rand.Rand, body, setup func(KeyValue) DB, teardown func(DB)) {
|
||||
Test := func(kv *KeyValue) func() {
|
||||
return func() {
|
||||
var p DB
|
||||
if setup != nil {
|
||||
Defer("setup", func() {
|
||||
p = setup(*kv)
|
||||
})
|
||||
}
|
||||
if teardown != nil {
|
||||
Defer("teardown", func() {
|
||||
teardown(p)
|
||||
})
|
||||
}
|
||||
if body != nil {
|
||||
p = body(*kv)
|
||||
}
|
||||
KeyValueTesting(rnd, *kv, p, func(KeyValue) DB {
|
||||
return p
|
||||
}, nil)
|
||||
}
|
||||
}
|
||||
|
||||
Describe("with no key/value (empty)", Test(&KeyValue{}))
|
||||
Describe("with empty key", Test(KeyValue_EmptyKey()))
|
||||
Describe("with empty value", Test(KeyValue_EmptyValue()))
|
||||
Describe("with one key/value", Test(KeyValue_OneKeyValue()))
|
||||
Describe("with big value", Test(KeyValue_BigValue()))
|
||||
Describe("with special key", Test(KeyValue_SpecialKey()))
|
||||
Describe("with multiple key/value", Test(KeyValue_MultipleKeyValue()))
|
||||
Describe("with generated key/value", Test(KeyValue_Generate(nil, 120, 1, 50, 10, 120)))
|
||||
}
|
||||
-586
@@ -1,586 +0,0 @@
|
||||
// Copyright (c) 2014, Suryandaru Triandana <syndtr@gmail.com>
|
||||
// All rights reserved.
|
||||
//
|
||||
// Use of this source code is governed by a BSD-style license that can be
|
||||
// found in the LICENSE file.
|
||||
|
||||
package testutil
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
. "github.com/onsi/gomega"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
var (
|
||||
storageMu sync.Mutex
|
||||
storageUseFS bool = true
|
||||
storageKeepFS bool = false
|
||||
storageNum int
|
||||
)
|
||||
|
||||
type StorageMode int
|
||||
|
||||
const (
|
||||
ModeOpen StorageMode = 1 << iota
|
||||
ModeCreate
|
||||
ModeRemove
|
||||
ModeRead
|
||||
ModeWrite
|
||||
ModeSync
|
||||
ModeClose
|
||||
)
|
||||
|
||||
const (
|
||||
modeOpen = iota
|
||||
modeCreate
|
||||
modeRemove
|
||||
modeRead
|
||||
modeWrite
|
||||
modeSync
|
||||
modeClose
|
||||
|
||||
modeCount
|
||||
)
|
||||
|
||||
const (
|
||||
typeManifest = iota
|
||||
typeJournal
|
||||
typeTable
|
||||
typeTemp
|
||||
|
||||
typeCount
|
||||
)
|
||||
|
||||
const flattenCount = modeCount * typeCount
|
||||
|
||||
func flattenType(m StorageMode, t storage.FileType) int {
|
||||
var x int
|
||||
switch m {
|
||||
case ModeOpen:
|
||||
x = modeOpen
|
||||
case ModeCreate:
|
||||
x = modeCreate
|
||||
case ModeRemove:
|
||||
x = modeRemove
|
||||
case ModeRead:
|
||||
x = modeRead
|
||||
case ModeWrite:
|
||||
x = modeWrite
|
||||
case ModeSync:
|
||||
x = modeSync
|
||||
case ModeClose:
|
||||
x = modeClose
|
||||
default:
|
||||
panic("invalid storage mode")
|
||||
}
|
||||
x *= typeCount
|
||||
switch t {
|
||||
case storage.TypeManifest:
|
||||
return x + typeManifest
|
||||
case storage.TypeJournal:
|
||||
return x + typeJournal
|
||||
case storage.TypeTable:
|
||||
return x + typeTable
|
||||
case storage.TypeTemp:
|
||||
return x + typeTemp
|
||||
default:
|
||||
panic("invalid file type")
|
||||
}
|
||||
}
|
||||
|
||||
func listFlattenType(m StorageMode, t storage.FileType) []int {
|
||||
ret := make([]int, 0, flattenCount)
|
||||
add := func(x int) {
|
||||
x *= typeCount
|
||||
switch {
|
||||
case t&storage.TypeManifest != 0:
|
||||
ret = append(ret, x+typeManifest)
|
||||
case t&storage.TypeJournal != 0:
|
||||
ret = append(ret, x+typeJournal)
|
||||
case t&storage.TypeTable != 0:
|
||||
ret = append(ret, x+typeTable)
|
||||
case t&storage.TypeTemp != 0:
|
||||
ret = append(ret, x+typeTemp)
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case m&ModeOpen != 0:
|
||||
add(modeOpen)
|
||||
case m&ModeCreate != 0:
|
||||
add(modeCreate)
|
||||
case m&ModeRemove != 0:
|
||||
add(modeRemove)
|
||||
case m&ModeRead != 0:
|
||||
add(modeRead)
|
||||
case m&ModeWrite != 0:
|
||||
add(modeWrite)
|
||||
case m&ModeSync != 0:
|
||||
add(modeSync)
|
||||
case m&ModeClose != 0:
|
||||
add(modeClose)
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
func packFile(num uint64, t storage.FileType) uint64 {
|
||||
if num>>(64-typeCount) != 0 {
|
||||
panic("overflow")
|
||||
}
|
||||
return num<<typeCount | uint64(t)
|
||||
}
|
||||
|
||||
func unpackFile(x uint64) (uint64, storage.FileType) {
|
||||
return x >> typeCount, storage.FileType(x) & storage.TypeAll
|
||||
}
|
||||
|
||||
type emulatedError struct {
|
||||
err error
|
||||
}
|
||||
|
||||
func (err emulatedError) Error() string {
|
||||
return fmt.Sprintf("emulated storage error: %v", err.err)
|
||||
}
|
||||
|
||||
type storageLock struct {
|
||||
s *Storage
|
||||
r util.Releaser
|
||||
}
|
||||
|
||||
func (l storageLock) Release() {
|
||||
l.r.Release()
|
||||
l.s.logI("storage lock released")
|
||||
}
|
||||
|
||||
type reader struct {
|
||||
f *file
|
||||
storage.Reader
|
||||
}
|
||||
|
||||
func (r *reader) Read(p []byte) (n int, err error) {
|
||||
err = r.f.s.emulateError(ModeRead, r.f.Type())
|
||||
if err == nil {
|
||||
r.f.s.stall(ModeRead, r.f.Type())
|
||||
n, err = r.Reader.Read(p)
|
||||
}
|
||||
r.f.s.count(ModeRead, r.f.Type(), n)
|
||||
if err != nil && err != io.EOF {
|
||||
r.f.s.logI("read error, num=%d type=%v n=%d err=%v", r.f.Num(), r.f.Type(), n, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *reader) ReadAt(p []byte, off int64) (n int, err error) {
|
||||
err = r.f.s.emulateError(ModeRead, r.f.Type())
|
||||
if err == nil {
|
||||
r.f.s.stall(ModeRead, r.f.Type())
|
||||
n, err = r.Reader.ReadAt(p, off)
|
||||
}
|
||||
r.f.s.count(ModeRead, r.f.Type(), n)
|
||||
if err != nil && err != io.EOF {
|
||||
r.f.s.logI("readAt error, num=%d type=%v offset=%d n=%d err=%v", r.f.Num(), r.f.Type(), off, n, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (r *reader) Close() (err error) {
|
||||
return r.f.doClose(r.Reader)
|
||||
}
|
||||
|
||||
type writer struct {
|
||||
f *file
|
||||
storage.Writer
|
||||
}
|
||||
|
||||
func (w *writer) Write(p []byte) (n int, err error) {
|
||||
err = w.f.s.emulateError(ModeWrite, w.f.Type())
|
||||
if err == nil {
|
||||
w.f.s.stall(ModeWrite, w.f.Type())
|
||||
n, err = w.Writer.Write(p)
|
||||
}
|
||||
w.f.s.count(ModeWrite, w.f.Type(), n)
|
||||
if err != nil && err != io.EOF {
|
||||
w.f.s.logI("write error, num=%d type=%v n=%d err=%v", w.f.Num(), w.f.Type(), n, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (w *writer) Sync() (err error) {
|
||||
err = w.f.s.emulateError(ModeSync, w.f.Type())
|
||||
if err == nil {
|
||||
w.f.s.stall(ModeSync, w.f.Type())
|
||||
err = w.Writer.Sync()
|
||||
}
|
||||
w.f.s.count(ModeSync, w.f.Type(), 0)
|
||||
if err != nil {
|
||||
w.f.s.logI("sync error, num=%d type=%v err=%v", w.f.Num(), w.f.Type(), err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (w *writer) Close() (err error) {
|
||||
return w.f.doClose(w.Writer)
|
||||
}
|
||||
|
||||
type file struct {
|
||||
s *Storage
|
||||
storage.File
|
||||
}
|
||||
|
||||
func (f *file) pack() uint64 {
|
||||
return packFile(f.Num(), f.Type())
|
||||
}
|
||||
|
||||
func (f *file) assertOpen() {
|
||||
ExpectWithOffset(2, f.s.opens).NotTo(HaveKey(f.pack()), "File open, num=%d type=%v writer=%v", f.Num(), f.Type(), f.s.opens[f.pack()])
|
||||
}
|
||||
|
||||
func (f *file) doClose(closer io.Closer) (err error) {
|
||||
err = f.s.emulateError(ModeClose, f.Type())
|
||||
if err == nil {
|
||||
f.s.stall(ModeClose, f.Type())
|
||||
}
|
||||
f.s.mu.Lock()
|
||||
defer f.s.mu.Unlock()
|
||||
if err == nil {
|
||||
ExpectWithOffset(2, f.s.opens).To(HaveKey(f.pack()), "File closed, num=%d type=%v", f.Num(), f.Type())
|
||||
err = closer.Close()
|
||||
}
|
||||
f.s.countNB(ModeClose, f.Type(), 0)
|
||||
writer := f.s.opens[f.pack()]
|
||||
if err != nil {
|
||||
f.s.logISkip(1, "file close failed, num=%d type=%v writer=%v err=%v", f.Num(), f.Type(), writer, err)
|
||||
} else {
|
||||
f.s.logISkip(1, "file closed, num=%d type=%v writer=%v", f.Num(), f.Type(), writer)
|
||||
delete(f.s.opens, f.pack())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (f *file) Open() (r storage.Reader, err error) {
|
||||
err = f.s.emulateError(ModeOpen, f.Type())
|
||||
if err == nil {
|
||||
f.s.stall(ModeOpen, f.Type())
|
||||
}
|
||||
f.s.mu.Lock()
|
||||
defer f.s.mu.Unlock()
|
||||
if err == nil {
|
||||
f.assertOpen()
|
||||
f.s.countNB(ModeOpen, f.Type(), 0)
|
||||
r, err = f.File.Open()
|
||||
}
|
||||
if err != nil {
|
||||
f.s.logI("file open failed, num=%d type=%v err=%v", f.Num(), f.Type(), err)
|
||||
} else {
|
||||
f.s.logI("file opened, num=%d type=%v", f.Num(), f.Type())
|
||||
f.s.opens[f.pack()] = false
|
||||
r = &reader{f, r}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (f *file) Create() (w storage.Writer, err error) {
|
||||
err = f.s.emulateError(ModeCreate, f.Type())
|
||||
if err == nil {
|
||||
f.s.stall(ModeCreate, f.Type())
|
||||
}
|
||||
f.s.mu.Lock()
|
||||
defer f.s.mu.Unlock()
|
||||
if err == nil {
|
||||
f.assertOpen()
|
||||
f.s.countNB(ModeCreate, f.Type(), 0)
|
||||
w, err = f.File.Create()
|
||||
}
|
||||
if err != nil {
|
||||
f.s.logI("file create failed, num=%d type=%v err=%v", f.Num(), f.Type(), err)
|
||||
} else {
|
||||
f.s.logI("file created, num=%d type=%v", f.Num(), f.Type())
|
||||
f.s.opens[f.pack()] = true
|
||||
w = &writer{f, w}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (f *file) Remove() (err error) {
|
||||
err = f.s.emulateError(ModeRemove, f.Type())
|
||||
if err == nil {
|
||||
f.s.stall(ModeRemove, f.Type())
|
||||
}
|
||||
f.s.mu.Lock()
|
||||
defer f.s.mu.Unlock()
|
||||
if err == nil {
|
||||
f.assertOpen()
|
||||
f.s.countNB(ModeRemove, f.Type(), 0)
|
||||
err = f.File.Remove()
|
||||
}
|
||||
if err != nil {
|
||||
f.s.logI("file remove failed, num=%d type=%v err=%v", f.Num(), f.Type(), err)
|
||||
} else {
|
||||
f.s.logI("file removed, num=%d type=%v", f.Num(), f.Type())
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
type Storage struct {
|
||||
storage.Storage
|
||||
closeFn func() error
|
||||
|
||||
lmu sync.Mutex
|
||||
lb bytes.Buffer
|
||||
|
||||
mu sync.Mutex
|
||||
// Open files, true=writer, false=reader
|
||||
opens map[uint64]bool
|
||||
counters [flattenCount]int
|
||||
bytesCounter [flattenCount]int64
|
||||
emulatedError [flattenCount]error
|
||||
stallCond sync.Cond
|
||||
stalled [flattenCount]bool
|
||||
}
|
||||
|
||||
func (s *Storage) log(skip int, str string) {
|
||||
s.lmu.Lock()
|
||||
defer s.lmu.Unlock()
|
||||
_, file, line, ok := runtime.Caller(skip + 2)
|
||||
if ok {
|
||||
// Truncate file name at last file name separator.
|
||||
if index := strings.LastIndex(file, "/"); index >= 0 {
|
||||
file = file[index+1:]
|
||||
} else if index = strings.LastIndex(file, "\\"); index >= 0 {
|
||||
file = file[index+1:]
|
||||
}
|
||||
} else {
|
||||
file = "???"
|
||||
line = 1
|
||||
}
|
||||
fmt.Fprintf(&s.lb, "%s:%d: ", file, line)
|
||||
lines := strings.Split(str, "\n")
|
||||
if l := len(lines); l > 1 && lines[l-1] == "" {
|
||||
lines = lines[:l-1]
|
||||
}
|
||||
for i, line := range lines {
|
||||
if i > 0 {
|
||||
s.lb.WriteString("\n\t")
|
||||
}
|
||||
s.lb.WriteString(line)
|
||||
}
|
||||
s.lb.WriteByte('\n')
|
||||
}
|
||||
|
||||
func (s *Storage) logISkip(skip int, format string, args ...interface{}) {
|
||||
pc, _, _, ok := runtime.Caller(skip + 1)
|
||||
if ok {
|
||||
if f := runtime.FuncForPC(pc); f != nil {
|
||||
fname := f.Name()
|
||||
if index := strings.LastIndex(fname, "."); index >= 0 {
|
||||
fname = fname[index+1:]
|
||||
}
|
||||
format = fname + ": " + format
|
||||
}
|
||||
}
|
||||
s.log(skip+1, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func (s *Storage) logI(format string, args ...interface{}) {
|
||||
s.logISkip(1, format, args...)
|
||||
}
|
||||
|
||||
func (s *Storage) Log(str string) {
|
||||
s.log(1, "Log: "+str)
|
||||
s.Storage.Log(str)
|
||||
}
|
||||
|
||||
func (s *Storage) Lock() (r util.Releaser, err error) {
|
||||
r, err = s.Storage.Lock()
|
||||
if err != nil {
|
||||
s.logI("storage locking failed, err=%v", err)
|
||||
} else {
|
||||
s.logI("storage locked")
|
||||
r = storageLock{s, r}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Storage) GetFile(num uint64, t storage.FileType) storage.File {
|
||||
return &file{s, s.Storage.GetFile(num, t)}
|
||||
}
|
||||
|
||||
func (s *Storage) GetFiles(t storage.FileType) (files []storage.File, err error) {
|
||||
rfiles, err := s.Storage.GetFiles(t)
|
||||
if err != nil {
|
||||
s.logI("get files failed, err=%v", err)
|
||||
return
|
||||
}
|
||||
files = make([]storage.File, len(rfiles))
|
||||
for i, f := range rfiles {
|
||||
files[i] = &file{s, f}
|
||||
}
|
||||
s.logI("get files, type=0x%x count=%d", int(t), len(files))
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Storage) GetManifest() (f storage.File, err error) {
|
||||
manifest, err := s.Storage.GetManifest()
|
||||
if err != nil {
|
||||
if !os.IsNotExist(err) {
|
||||
s.logI("get manifest failed, err=%v", err)
|
||||
}
|
||||
return
|
||||
}
|
||||
s.logI("get manifest, num=%d", manifest.Num())
|
||||
return &file{s, manifest}, nil
|
||||
}
|
||||
|
||||
func (s *Storage) SetManifest(f storage.File) error {
|
||||
f_, ok := f.(*file)
|
||||
ExpectWithOffset(1, ok).To(BeTrue())
|
||||
ExpectWithOffset(1, f_.Type()).To(Equal(storage.TypeManifest))
|
||||
err := s.Storage.SetManifest(f_.File)
|
||||
if err != nil {
|
||||
s.logI("set manifest failed, err=%v", err)
|
||||
} else {
|
||||
s.logI("set manifest, num=%d", f_.Num())
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Storage) openFiles() string {
|
||||
out := "Open files:"
|
||||
for x, writer := range s.opens {
|
||||
num, t := unpackFile(x)
|
||||
out += fmt.Sprintf("\n · num=%d type=%v writer=%v", num, t, writer)
|
||||
}
|
||||
return out
|
||||
}
|
||||
|
||||
func (s *Storage) Close() error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
ExpectWithOffset(1, s.opens).To(BeEmpty(), s.openFiles())
|
||||
err := s.Storage.Close()
|
||||
if err != nil {
|
||||
s.logI("storage closing failed, err=%v", err)
|
||||
} else {
|
||||
s.logI("storage closed")
|
||||
}
|
||||
if s.closeFn != nil {
|
||||
if err1 := s.closeFn(); err1 != nil {
|
||||
s.logI("close func error, err=%v", err1)
|
||||
}
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *Storage) countNB(m StorageMode, t storage.FileType, n int) {
|
||||
s.counters[flattenType(m, t)]++
|
||||
s.bytesCounter[flattenType(m, t)] += int64(n)
|
||||
}
|
||||
|
||||
func (s *Storage) count(m StorageMode, t storage.FileType, n int) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
s.countNB(m, t, n)
|
||||
}
|
||||
|
||||
func (s *Storage) ResetCounter(m StorageMode, t storage.FileType) {
|
||||
for _, x := range listFlattenType(m, t) {
|
||||
s.counters[x] = 0
|
||||
s.bytesCounter[x] = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) Counter(m StorageMode, t storage.FileType) (count int, bytes int64) {
|
||||
for _, x := range listFlattenType(m, t) {
|
||||
count += s.counters[x]
|
||||
bytes += s.bytesCounter[x]
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func (s *Storage) emulateError(m StorageMode, t storage.FileType) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
err := s.emulatedError[flattenType(m, t)]
|
||||
if err != nil {
|
||||
return emulatedError{err}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *Storage) EmulateError(m StorageMode, t storage.FileType, err error) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, x := range listFlattenType(m, t) {
|
||||
s.emulatedError[x] = err
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) stall(m StorageMode, t storage.FileType) {
|
||||
x := flattenType(m, t)
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for s.stalled[x] {
|
||||
s.stallCond.Wait()
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) Stall(m StorageMode, t storage.FileType) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, x := range listFlattenType(m, t) {
|
||||
s.stalled[x] = true
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Storage) Release(m StorageMode, t storage.FileType) {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
for _, x := range listFlattenType(m, t) {
|
||||
s.stalled[x] = false
|
||||
}
|
||||
s.stallCond.Broadcast()
|
||||
}
|
||||
|
||||
func NewStorage() *Storage {
|
||||
var stor storage.Storage
|
||||
var closeFn func() error
|
||||
if storageUseFS {
|
||||
for {
|
||||
storageMu.Lock()
|
||||
num := storageNum
|
||||
storageNum++
|
||||
storageMu.Unlock()
|
||||
path := filepath.Join(os.TempDir(), fmt.Sprintf("goleveldb-test%d0%d0%d", os.Getuid(), os.Getpid(), num))
|
||||
if _, err := os.Stat(path); os.IsNotExist(err) {
|
||||
stor, err = storage.OpenFile(path)
|
||||
ExpectWithOffset(1, err).NotTo(HaveOccurred(), "creating storage at %s", path)
|
||||
closeFn = func() error {
|
||||
if storageKeepFS {
|
||||
return nil
|
||||
}
|
||||
return os.RemoveAll(path)
|
||||
}
|
||||
break
|
||||
}
|
||||
}
|
||||
} else {
|
||||
stor = storage.NewMemStorage()
|
||||
}
|
||||
s := &Storage{
|
||||
Storage: stor,
|
||||
closeFn: closeFn,
|
||||
opens: make(map[uint64]bool),
|
||||
}
|
||||
s.stallCond.L = &s.mu
|
||||
return s
|
||||
}
|
||||
-7
@@ -1,7 +0,0 @@
|
||||
language: go
|
||||
go:
|
||||
- 1.1
|
||||
- 1.2
|
||||
- 1.3
|
||||
- 1.4
|
||||
- tip
|
||||
@@ -0,0 +1,24 @@
|
||||
If your issue is a support request ("How do I get my devices to connect?"
|
||||
or similar), please use the support forum at https://forum.syncthing.net/
|
||||
where a large number of helpful people hang out. This issue tracker is for
|
||||
reporting bugs or feature requests directly to the developers.
|
||||
|
||||
If your issue is a bug report, replace this boilerplate with a description
|
||||
of the problem, being sure to include at least:
|
||||
|
||||
- what happened,
|
||||
- what you expected to happen instead, and
|
||||
- any steps to reproduce the problem.
|
||||
|
||||
Also fill out the version information below and add log output or
|
||||
screenshots as appropriate.
|
||||
|
||||
If your issue is a feature request, simply replace this template text in
|
||||
its entirety.
|
||||
|
||||
### Version Information
|
||||
|
||||
Syncthing Version: v0.x.y
|
||||
OS Version: Windows 7 / Ubuntu 14.04 / ...
|
||||
Browser Version: (if applicable, for GUI issues)
|
||||
|
||||
@@ -22,6 +22,7 @@ cdata <chris@scriptolo.gy>
|
||||
cdhowie <me@chrishowie.com>
|
||||
ceh <emil@hessman.se>
|
||||
cqcallaw <enlightened.despot@gmail.com>
|
||||
dinosore <dinosore@dbrsoftware.co.uk>
|
||||
dva <denisva@gmail.com>
|
||||
dzarda <dzardacz@gmail.com>
|
||||
eipiminus1 <eipiminusone+github@gmail.com> <eipiminus1@users.noreply.github.com>
|
||||
@@ -45,6 +46,7 @@ kralo <max.schulze@online.de>
|
||||
krozycki <rozycki.karol@gmail.com>
|
||||
letiemble <laurent.etiemble@gmail.com> <laurent.etiemble@monobjc.net>
|
||||
LordLandon <lordlandon@gmail.com>
|
||||
lkwg82 <lkwg82@gmx.de>
|
||||
marcindziadus <dziadus.marcin@gmail.com>
|
||||
marclaporte <marc@marclaporte.com>
|
||||
mateon1 <matin1111@wp.pl>
|
||||
@@ -52,6 +54,7 @@ mogwa1 <devriesb@gmail.com>
|
||||
moshen <moshen.colin@gmail.com>
|
||||
Moter8 <moter8@gmail.com>
|
||||
mvdan <mvdan@mvdan.cc>
|
||||
norgeous <daniel@harte.me> <daniel@danielharte.co.uk> <norgeous@users.noreply.github.com>
|
||||
nrm21 <natemorrison@gmail.com>
|
||||
Nutomic <me@nutomic.com>
|
||||
pascalj <github@pascalj.com> <mail@pascal-jungblut.com>
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
### Purpose
|
||||
|
||||
Describe the purpose of this change. If there is an existing issue that is
|
||||
resolved by this pull request, ensure that the commit subject is on the form
|
||||
`Some short description (fixes #1234)` where 1234 is the issue number.
|
||||
|
||||
### Testing
|
||||
|
||||
Describe what testing has been done, and how the reviewer can test the change
|
||||
if new tests are not included.
|
||||
|
||||
### Screenshots
|
||||
|
||||
If this is a GUI change, include screenshots of the change. If not, please
|
||||
feel free to just delete this section.
|
||||
|
||||
### Documentation
|
||||
|
||||
If this is a user visible change (including API and protocol changes), add a link here
|
||||
to the corresponding pull request on https://github.com/syncthing/docs or describe
|
||||
the documentation changes necessary.
|
||||
|
||||
### Authorship
|
||||
|
||||
Every author of a code contribution (Go, Javascript, HTML, CSS etc, with the
|
||||
possible exception of minor typo corrections and similar) is recorded in the
|
||||
AUTHORS and NICKS files and the in-GUI credits. If this is your first
|
||||
contribution, a maintainer will add you properly before accepting the
|
||||
contribution. You need not do so yourself or worry about the fact that the
|
||||
"authors" automated test fails. However, if your name (such as you want it
|
||||
presented in the credits) is not visible on your Github profile or in your
|
||||
commit messages, please assist by providing it here.
|
||||
@@ -25,26 +25,30 @@ for incompatible changes.
|
||||
Take a look at the [getting started guide][2].
|
||||
|
||||
There are a few examples for keeping Syncthing running in the background
|
||||
on your system in [the etc directory][3].
|
||||
on your system in [the etc directory][3]. There are also several [GUI
|
||||
implementations][11] for Windows, Mac and Linux.
|
||||
|
||||
## Getting in Touch
|
||||
|
||||
The first and best point of contact is the [Forum][8]. There is also an IRC
|
||||
channel, `#syncthing` on [Freenode][4] (with a [web client][9]), for talking
|
||||
channel, `#syncthing` on [freenode][4] (with a [web client][9]), for talking
|
||||
directly to developers and users. If you've found something that is clearly a
|
||||
bug, feel free to report it in the [GitHub issue tracker][10].
|
||||
|
||||
## Building
|
||||
|
||||
Building Syncthing from source is easy, and there's a [guide][5].
|
||||
Building Syncthing from source is easy, and there's a [guide][5]
|
||||
that describes it for both Unix and Windows systems.
|
||||
|
||||
## Signed Releases
|
||||
|
||||
As of v0.10.15 and onwards, git tags and release binaries are GPG signed
|
||||
with the key D26E6ED000654A3E (see https://syncthing.net/security.html).
|
||||
For release binaries, MD5 and SHA1 checksums are calculated and signed,
|
||||
available in the md5sum.txt.asc and sha1sum.txt.asc files.
|
||||
As of v0.10.15 and onwards release binaries are GPG signed with the key
|
||||
D26E6ED000654A3E, available from https://syncthing.net/security.html and
|
||||
most key servers.
|
||||
|
||||
There is also a built in automatic upgrade mechanism (disabled in some
|
||||
distribution channels) which uses a compiled in ECDSA signature. Mac OS
|
||||
X binaries are also properly code signed.
|
||||
|
||||
## Documentation
|
||||
|
||||
@@ -62,3 +66,4 @@ All code is licensed under the [MPLv2 License][7].
|
||||
[8]: https://forum.syncthing.net/
|
||||
[9]: https://kiwiirc.com/client/irc.freenode.net/#syncthing
|
||||
[10]: https://github.com/syncthing/syncthing/issues
|
||||
[11]: http://docs.syncthing.net/users/contrib.html#gui-wrappers
|
||||
|
||||
@@ -40,12 +40,84 @@ var (
|
||||
race bool
|
||||
)
|
||||
|
||||
type target struct {
|
||||
name string
|
||||
buildPkg string
|
||||
binaryName string
|
||||
archiveFiles []archiveFile
|
||||
debianFiles []archiveFile
|
||||
}
|
||||
|
||||
type archiveFile struct {
|
||||
src string
|
||||
dst string
|
||||
perm os.FileMode
|
||||
}
|
||||
|
||||
var targets = map[string]target{
|
||||
"all": {
|
||||
// Only valid for the "build" and "install" commands as it lacks all
|
||||
// the archive creation stuff.
|
||||
buildPkg: "./cmd/...",
|
||||
},
|
||||
"syncthing": {
|
||||
// The default target for "build", "install", "tar", "zip", "deb", etc.
|
||||
name: "syncthing",
|
||||
buildPkg: "./cmd/syncthing",
|
||||
binaryName: "syncthing", // .exe will be added automatically for Windows builds
|
||||
archiveFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "{{binary}}", perm: 0755},
|
||||
{src: "README.md", dst: "README.txt", perm: 0644},
|
||||
{src: "LICENSE", dst: "LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "AUTHORS.txt", perm: 0644},
|
||||
// All files from etc/ and extra/ added automatically in init().
|
||||
},
|
||||
debianFiles: []archiveFile{
|
||||
{src: "{{binary}}", dst: "deb/usr/bin/{{binary}}", perm: 0755},
|
||||
{src: "README.md", dst: "deb/usr/share/doc/syncthing/README.txt", perm: 0644},
|
||||
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing/LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing/AUTHORS.txt", perm: 0644},
|
||||
{src: "man/syncthing.1", dst: "deb/usr/share/man/man1/syncthing.1", perm: 0644},
|
||||
{src: "man/syncthing-config.5", dst: "deb/usr/share/man/man5/syncthing-config.5", perm: 0644},
|
||||
{src: "man/syncthing-stignore.5", dst: "deb/usr/share/man/man5/syncthing-stignore.5", perm: 0644},
|
||||
{src: "man/syncthing-device-ids.7", dst: "deb/usr/share/man/man7/syncthing-device-ids.7", perm: 0644},
|
||||
{src: "man/syncthing-event-api.7", dst: "deb/usr/share/man/man7/syncthing-event-api.7", perm: 0644},
|
||||
{src: "man/syncthing-faq.7", dst: "deb/usr/share/man/man7/syncthing-faq.7", perm: 0644},
|
||||
{src: "man/syncthing-networking.7", dst: "deb/usr/share/man/man7/syncthing-networking.7", perm: 0644},
|
||||
{src: "man/syncthing-rest-api.7", dst: "deb/usr/share/man/man7/syncthing-rest-api.7", perm: 0644},
|
||||
{src: "man/syncthing-security.7", dst: "deb/usr/share/man/man7/syncthing-security.7", perm: 0644},
|
||||
{src: "man/syncthing-versioning.7", dst: "deb/usr/share/man/man7/syncthing-versioning.7", perm: 0644},
|
||||
{src: "etc/linux-systemd/system/syncthing@.service", dst: "deb/lib/systemd/system/syncthing@.service", perm: 0644},
|
||||
{src: "etc/linux-systemd/system/syncthing-resume.service", dst: "deb/lib/systemd/system/syncthing-resume.service", perm: 0644},
|
||||
{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0644},
|
||||
},
|
||||
},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// The "syncthing" target includes a few more files found in the "etc"
|
||||
// and "extra" dirs.
|
||||
syncthingPkg := targets["syncthing"]
|
||||
for _, file := range listFiles("etc") {
|
||||
syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0644})
|
||||
}
|
||||
for _, file := range listFiles("extra") {
|
||||
syncthingPkg.archiveFiles = append(syncthingPkg.archiveFiles, archiveFile{src: file, dst: file, perm: 0644})
|
||||
}
|
||||
for _, file := range listFiles("extra") {
|
||||
syncthingPkg.debianFiles = append(syncthingPkg.debianFiles, archiveFile{src: file, dst: "deb/usr/share/doc/syncthing/" + filepath.Base(file), perm: 0644})
|
||||
}
|
||||
targets["syncthing"] = syncthingPkg
|
||||
}
|
||||
|
||||
const minGoVersion = 1.3
|
||||
|
||||
func main() {
|
||||
log.SetOutput(os.Stdout)
|
||||
log.SetFlags(0)
|
||||
|
||||
// If GOPATH isn't set, set it correctly with the assumption that we are
|
||||
// in $GOPATH/src/github.com/syncthing/syncthing.
|
||||
if os.Getenv("GOPATH") == "" {
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
@@ -55,6 +127,12 @@ func main() {
|
||||
log.Println("GOPATH is", gopath)
|
||||
os.Setenv("GOPATH", gopath)
|
||||
}
|
||||
|
||||
// We use Go 1.5+ vendoring.
|
||||
os.Setenv("GO15VENDOREXPERIMENT", "1")
|
||||
|
||||
// Set path to $GOPATH/bin:$PATH so that we can for sure find tools we
|
||||
// might have installed during "build.go setup".
|
||||
os.Setenv("PATH", fmt.Sprintf("%s%cbin%c%s", os.Getenv("GOPATH"), os.PathSeparator, os.PathListSeparator, os.Getenv("PATH")))
|
||||
|
||||
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
|
||||
@@ -73,12 +151,15 @@ func main() {
|
||||
|
||||
goVersion, _ = checkRequiredGoVersion()
|
||||
|
||||
// Invoking build.go with no parameters at all is equivalent to "go run
|
||||
// build.go install all" as that builds everything (incrementally),
|
||||
// which is what you want for maximum error checking during development.
|
||||
if flag.NArg() == 0 {
|
||||
var tags []string
|
||||
if noupgrade {
|
||||
tags = []string{"noupgrade"}
|
||||
}
|
||||
install("./cmd/...", tags)
|
||||
install(targets["all"], tags)
|
||||
|
||||
vet("./cmd/syncthing")
|
||||
vet("./lib/...")
|
||||
@@ -87,78 +168,84 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
for _, cmd := range flag.Args() {
|
||||
switch cmd {
|
||||
case "setup":
|
||||
setup()
|
||||
// Otherwise, with any command given but not a target, the target is
|
||||
// "syncthing". So "go run build.go install" is "go run build.go install
|
||||
// syncthing" etc.
|
||||
targetName := "syncthing"
|
||||
if flag.NArg() > 1 {
|
||||
targetName = flag.Arg(1)
|
||||
}
|
||||
target, ok := targets[targetName]
|
||||
if !ok {
|
||||
log.Fatalln("Unknown target", target)
|
||||
}
|
||||
|
||||
case "install":
|
||||
pkg := "./cmd/..."
|
||||
var tags []string
|
||||
if noupgrade {
|
||||
tags = []string{"noupgrade"}
|
||||
}
|
||||
install(pkg, tags)
|
||||
cmd := flag.Arg(0)
|
||||
switch cmd {
|
||||
case "setup":
|
||||
setup()
|
||||
|
||||
case "build":
|
||||
pkg := "./cmd/syncthing"
|
||||
var tags []string
|
||||
if noupgrade {
|
||||
tags = []string{"noupgrade"}
|
||||
}
|
||||
build(pkg, tags)
|
||||
|
||||
case "test":
|
||||
test("./...")
|
||||
|
||||
case "bench":
|
||||
bench("./...")
|
||||
|
||||
case "assets":
|
||||
assets()
|
||||
|
||||
case "xdr":
|
||||
xdr()
|
||||
|
||||
case "translate":
|
||||
translate()
|
||||
|
||||
case "transifex":
|
||||
transifex()
|
||||
|
||||
case "deps":
|
||||
deps()
|
||||
|
||||
case "tar":
|
||||
buildTar()
|
||||
|
||||
case "zip":
|
||||
buildZip()
|
||||
|
||||
case "deb":
|
||||
buildDeb()
|
||||
|
||||
case "clean":
|
||||
clean()
|
||||
|
||||
case "vet":
|
||||
vet("./cmd/syncthing")
|
||||
vet("./lib/...")
|
||||
|
||||
case "lint":
|
||||
lint("./cmd/syncthing")
|
||||
lint("./lib/...")
|
||||
|
||||
default:
|
||||
log.Fatalf("Unknown command %q", cmd)
|
||||
case "install":
|
||||
var tags []string
|
||||
if noupgrade {
|
||||
tags = []string{"noupgrade"}
|
||||
}
|
||||
install(target, tags)
|
||||
|
||||
case "build":
|
||||
var tags []string
|
||||
if noupgrade {
|
||||
tags = []string{"noupgrade"}
|
||||
}
|
||||
build(target, tags)
|
||||
|
||||
case "test":
|
||||
test("./lib/...", "./cmd/...")
|
||||
|
||||
case "bench":
|
||||
bench("./lib/...", "./cmd/...")
|
||||
|
||||
case "assets":
|
||||
rebuildAssets()
|
||||
|
||||
case "xdr":
|
||||
xdr()
|
||||
|
||||
case "translate":
|
||||
translate()
|
||||
|
||||
case "transifex":
|
||||
transifex()
|
||||
|
||||
case "tar":
|
||||
buildTar(target)
|
||||
|
||||
case "zip":
|
||||
buildZip(target)
|
||||
|
||||
case "deb":
|
||||
buildDeb(target)
|
||||
|
||||
case "clean":
|
||||
clean()
|
||||
|
||||
case "vet":
|
||||
vet("./cmd/syncthing")
|
||||
vet("./lib/...")
|
||||
|
||||
case "lint":
|
||||
lint("./cmd/syncthing")
|
||||
lint("./lib/...")
|
||||
|
||||
default:
|
||||
log.Fatalf("Unknown command %q", cmd)
|
||||
}
|
||||
}
|
||||
|
||||
func checkRequiredGoVersion() (float64, bool) {
|
||||
ver := run("go", "version")
|
||||
re := regexp.MustCompile(`go version go(\d+\.\d+)`)
|
||||
if m := re.FindSubmatch(ver); len(m) == 2 {
|
||||
re := regexp.MustCompile(`go(\d+\.\d+)`)
|
||||
ver := runtime.Version()
|
||||
if m := re.FindStringSubmatch(ver); len(m) == 2 {
|
||||
vs := string(m[1])
|
||||
// This is a standard go build. Verify that it's new enough.
|
||||
f, err := strconv.ParseFloat(vs, 64)
|
||||
@@ -166,7 +253,9 @@ func checkRequiredGoVersion() (float64, bool) {
|
||||
log.Printf("*** Couldn't parse Go version out of %q.\n*** This isn't known to work, proceed on your own risk.", vs)
|
||||
return 0, false
|
||||
}
|
||||
if f < minGoVersion {
|
||||
if f < 1.5 {
|
||||
log.Printf("*** Go version %.01f doesn't support the vendoring mechanism.\n*** Ensure correct dependencies in your $GOPATH.", f)
|
||||
} else if f < minGoVersion {
|
||||
log.Fatalf("*** Go version %.01f is less than required %.01f.\n*** This is known not to work, not proceeding.", f, minGoVersion)
|
||||
}
|
||||
return f, true
|
||||
@@ -178,16 +267,16 @@ func checkRequiredGoVersion() (float64, bool) {
|
||||
|
||||
func setup() {
|
||||
runPrint("go", "get", "-v", "golang.org/x/tools/cmd/cover")
|
||||
runPrint("go", "get", "-v", "golang.org/x/tools/cmd/vet")
|
||||
runPrint("go", "get", "-v", "golang.org/x/net/html")
|
||||
runPrint("go", "get", "-v", "github.com/tools/godep")
|
||||
runPrint("go", "get", "-v", "github.com/FiloSottile/gvt")
|
||||
runPrint("go", "get", "-v", "github.com/axw/gocov/gocov")
|
||||
runPrint("go", "get", "-v", "github.com/AlekSi/gocov-xml")
|
||||
runPrint("go", "get", "-v", "bitbucket.org/tebeka/go2xunit")
|
||||
}
|
||||
|
||||
func test(pkg string) {
|
||||
setBuildEnv()
|
||||
func test(pkgs ...string) {
|
||||
lazyRebuildAssets()
|
||||
|
||||
useRace := runtime.GOARCH == "amd64"
|
||||
switch runtime.GOOS {
|
||||
case "darwin", "linux", "freebsd", "windows":
|
||||
@@ -196,18 +285,20 @@ func test(pkg string) {
|
||||
}
|
||||
|
||||
if useRace {
|
||||
runPrint("go", "test", "-short", "-race", "-timeout", "60s", pkg)
|
||||
runPrint("go", append([]string{"test", "-short", "-race", "-timeout", "60s"}, pkgs...)...)
|
||||
} else {
|
||||
runPrint("go", "test", "-short", "-timeout", "60s", pkg)
|
||||
runPrint("go", append([]string{"test", "-short", "-timeout", "60s"}, pkgs...)...)
|
||||
}
|
||||
}
|
||||
|
||||
func bench(pkg string) {
|
||||
setBuildEnv()
|
||||
runPrint("go", "test", "-run", "NONE", "-bench", ".", pkg)
|
||||
func bench(pkgs ...string) {
|
||||
lazyRebuildAssets()
|
||||
runPrint("go", append([]string{"test", "-run", "NONE", "-bench", "."}, pkgs...)...)
|
||||
}
|
||||
|
||||
func install(pkg string, tags []string) {
|
||||
func install(target target, tags []string) {
|
||||
lazyRebuildAssets()
|
||||
|
||||
cwd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -220,18 +311,17 @@ func install(pkg string, tags []string) {
|
||||
if race {
|
||||
args = append(args, "-race")
|
||||
}
|
||||
args = append(args, pkg)
|
||||
setBuildEnv()
|
||||
args = append(args, target.buildPkg)
|
||||
|
||||
os.Setenv("GOOS", goos)
|
||||
os.Setenv("GOARCH", goarch)
|
||||
runPrint("go", args...)
|
||||
}
|
||||
|
||||
func build(pkg string, tags []string) {
|
||||
binary := "syncthing"
|
||||
if goos == "windows" {
|
||||
binary += ".exe"
|
||||
}
|
||||
func build(target target, tags []string) {
|
||||
lazyRebuildAssets()
|
||||
|
||||
rmr(binary)
|
||||
rmr(target.binaryName)
|
||||
args := []string{"build", "-i", "-v", "-ldflags", ldflags()}
|
||||
if len(tags) > 0 {
|
||||
args = append(args, "-tags", strings.Join(tags, ","))
|
||||
@@ -239,66 +329,64 @@ func build(pkg string, tags []string) {
|
||||
if race {
|
||||
args = append(args, "-race")
|
||||
}
|
||||
args = append(args, pkg)
|
||||
setBuildEnv()
|
||||
args = append(args, target.buildPkg)
|
||||
|
||||
os.Setenv("GOOS", goos)
|
||||
os.Setenv("GOARCH", goarch)
|
||||
runPrint("go", args...)
|
||||
}
|
||||
|
||||
func buildTar() {
|
||||
name := archiveName()
|
||||
func buildTar(target target) {
|
||||
name := archiveName(target)
|
||||
filename := name + ".tar.gz"
|
||||
|
||||
var tags []string
|
||||
if noupgrade {
|
||||
tags = []string{"noupgrade"}
|
||||
name += "-noupgrade"
|
||||
}
|
||||
build("./cmd/syncthing", tags)
|
||||
filename := name + ".tar.gz"
|
||||
files := []archiveFile{
|
||||
{src: "README.md", dst: name + "/README.txt"},
|
||||
{src: "LICENSE", dst: name + "/LICENSE.txt"},
|
||||
{src: "AUTHORS", dst: name + "/AUTHORS.txt"},
|
||||
{src: "syncthing", dst: name + "/syncthing"},
|
||||
}
|
||||
|
||||
for _, file := range listFiles("etc") {
|
||||
files = append(files, archiveFile{src: file, dst: name + "/" + file})
|
||||
}
|
||||
for _, file := range listFiles("extra") {
|
||||
files = append(files, archiveFile{src: file, dst: name + "/" + filepath.Base(file)})
|
||||
}
|
||||
build(target, tags)
|
||||
|
||||
if goos == "darwin" {
|
||||
macosCodesign("syncthing")
|
||||
macosCodesign(target.binaryName)
|
||||
}
|
||||
tarGz(filename, files)
|
||||
|
||||
for i := range target.archiveFiles {
|
||||
target.archiveFiles[i].src = strings.Replace(target.archiveFiles[i].src, "{{binary}}", target.binaryName, 1)
|
||||
target.archiveFiles[i].dst = strings.Replace(target.archiveFiles[i].dst, "{{binary}}", target.binaryName, 1)
|
||||
target.archiveFiles[i].dst = name + "/" + target.archiveFiles[i].dst
|
||||
}
|
||||
|
||||
tarGz(filename, target.archiveFiles)
|
||||
log.Println(filename)
|
||||
}
|
||||
|
||||
func buildZip() {
|
||||
name := archiveName()
|
||||
func buildZip(target target) {
|
||||
target.binaryName += ".exe"
|
||||
|
||||
name := archiveName(target)
|
||||
filename := name + ".zip"
|
||||
|
||||
var tags []string
|
||||
if noupgrade {
|
||||
tags = []string{"noupgrade"}
|
||||
name += "-noupgrade"
|
||||
}
|
||||
build("./cmd/syncthing", tags)
|
||||
filename := name + ".zip"
|
||||
files := []archiveFile{
|
||||
{src: "README.md", dst: name + "/README.txt"},
|
||||
{src: "LICENSE", dst: name + "/LICENSE.txt"},
|
||||
{src: "AUTHORS", dst: name + "/AUTHORS.txt"},
|
||||
{src: "syncthing.exe", dst: name + "/syncthing.exe"},
|
||||
|
||||
build(target, tags)
|
||||
|
||||
for i := range target.archiveFiles {
|
||||
target.archiveFiles[i].src = strings.Replace(target.archiveFiles[i].src, "{{binary}}", target.binaryName, 1)
|
||||
target.archiveFiles[i].dst = strings.Replace(target.archiveFiles[i].dst, "{{binary}}", target.binaryName, 1)
|
||||
target.archiveFiles[i].dst = name + "/" + target.archiveFiles[i].dst
|
||||
}
|
||||
|
||||
for _, file := range listFiles("extra") {
|
||||
files = append(files, archiveFile{src: file, dst: name + "/" + filepath.Base(file)})
|
||||
}
|
||||
|
||||
zipFile(filename, files)
|
||||
zipFile(filename, target.archiveFiles)
|
||||
log.Println(filename)
|
||||
}
|
||||
|
||||
func buildDeb() {
|
||||
func buildDeb(target target) {
|
||||
os.RemoveAll("deb")
|
||||
|
||||
// "goarch" here is set to whatever the Debian packages expect. We correct
|
||||
@@ -312,32 +400,14 @@ func buildDeb() {
|
||||
goarch = "arm"
|
||||
}
|
||||
|
||||
build("./cmd/syncthing", []string{"noupgrade"})
|
||||
build(target, []string{"noupgrade"})
|
||||
|
||||
files := []archiveFile{
|
||||
{src: "README.md", dst: "deb/usr/share/doc/syncthing/README.txt", perm: 0644},
|
||||
{src: "LICENSE", dst: "deb/usr/share/doc/syncthing/LICENSE.txt", perm: 0644},
|
||||
{src: "AUTHORS", dst: "deb/usr/share/doc/syncthing/AUTHORS.txt", perm: 0644},
|
||||
{src: "syncthing", dst: "deb/usr/bin/syncthing", perm: 0755},
|
||||
{src: "man/syncthing.1", dst: "deb/usr/share/man/man1/syncthing.1", perm: 0644},
|
||||
{src: "man/syncthing-config.5", dst: "deb/usr/share/man/man5/syncthing-config.5", perm: 0644},
|
||||
{src: "man/syncthing-stignore.5", dst: "deb/usr/share/man/man5/syncthing-stignore.5", perm: 0644},
|
||||
{src: "man/syncthing-device-ids.7", dst: "deb/usr/share/man/man7/syncthing-device-ids.7", perm: 0644},
|
||||
{src: "man/syncthing-event-api.7", dst: "deb/usr/share/man/man7/syncthing-event-api.7", perm: 0644},
|
||||
{src: "man/syncthing-faq.7", dst: "deb/usr/share/man/man7/syncthing-faq.7", perm: 0644},
|
||||
{src: "man/syncthing-networking.7", dst: "deb/usr/share/man/man7/syncthing-networking.7", perm: 0644},
|
||||
{src: "man/syncthing-rest-api.7", dst: "deb/usr/share/man/man7/syncthing-rest-api.7", perm: 0644},
|
||||
{src: "man/syncthing-security.7", dst: "deb/usr/share/man/man7/syncthing-security.7", perm: 0644},
|
||||
{src: "man/syncthing-versioning.7", dst: "deb/usr/share/man/man7/syncthing-versioning.7", perm: 0644},
|
||||
{src: "etc/linux-systemd/system/syncthing@.service", dst: "deb/lib/systemd/system/syncthing@.service", perm: 0644},
|
||||
{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0644},
|
||||
for i := range target.debianFiles {
|
||||
target.debianFiles[i].src = strings.Replace(target.debianFiles[i].src, "{{binary}}", target.binaryName, 1)
|
||||
target.debianFiles[i].dst = strings.Replace(target.debianFiles[i].dst, "{{binary}}", target.binaryName, 1)
|
||||
}
|
||||
|
||||
for _, file := range listFiles("extra") {
|
||||
files = append(files, archiveFile{src: file, dst: "deb/usr/share/doc/syncthing/" + filepath.Base(file), perm: 0644})
|
||||
}
|
||||
|
||||
for _, af := range files {
|
||||
for _, af := range target.debianFiles {
|
||||
if err := copyFile(af.src, af.dst, af.perm); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -346,11 +416,14 @@ func buildDeb() {
|
||||
os.MkdirAll("deb/DEBIAN", 0755)
|
||||
|
||||
data := map[string]string{
|
||||
"name": target.name,
|
||||
"arch": debarch,
|
||||
"version": version[1:],
|
||||
"date": time.Now().Format(time.RFC1123),
|
||||
}
|
||||
for _, file := range listFiles("debian") {
|
||||
|
||||
debTemplateFiles := append(listFiles("debian/common"), listFiles("debian/"+target.name)...)
|
||||
for _, file := range debTemplateFiles {
|
||||
tpl, err := template.New(filepath.Base(file)).ParseFiles(file)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
@@ -402,21 +475,39 @@ func listFiles(dir string) []string {
|
||||
return res
|
||||
}
|
||||
|
||||
func setBuildEnv() {
|
||||
os.Setenv("GOOS", goos)
|
||||
os.Setenv("GOARCH", goarch)
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
log.Println("Warning: can't determine current dir:", err)
|
||||
log.Println("Build might not work as expected")
|
||||
}
|
||||
os.Setenv("GOPATH", fmt.Sprintf("%s%c%s", filepath.Join(wd, "Godeps", "_workspace"), os.PathListSeparator, os.Getenv("GOPATH")))
|
||||
log.Println("GOPATH=" + os.Getenv("GOPATH"))
|
||||
func rebuildAssets() {
|
||||
runPipe("lib/auto/gui.files.go", "go", "run", "script/genassets.go", "gui")
|
||||
}
|
||||
|
||||
func assets() {
|
||||
setBuildEnv()
|
||||
runPipe("lib/auto/gui.files.go", "go", "run", "script/genassets.go", "gui")
|
||||
func lazyRebuildAssets() {
|
||||
if shouldRebuildAssets() {
|
||||
rebuildAssets()
|
||||
}
|
||||
}
|
||||
|
||||
func shouldRebuildAssets() bool {
|
||||
info, err := os.Stat("lib/auto/gui.files.go")
|
||||
if err != nil {
|
||||
// If the file doesn't exist, we must rebuild it
|
||||
return true
|
||||
}
|
||||
|
||||
// Check if any of the files in gui/ are newer than the asset file. If
|
||||
// so we should rebuild it.
|
||||
currentBuild := info.ModTime()
|
||||
assetsAreNewer := false
|
||||
filepath.Walk("gui", func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if assetsAreNewer {
|
||||
return nil
|
||||
}
|
||||
assetsAreNewer = info.ModTime().After(currentBuild)
|
||||
return nil
|
||||
})
|
||||
|
||||
return assetsAreNewer
|
||||
}
|
||||
|
||||
func xdr() {
|
||||
@@ -437,24 +528,17 @@ func translate() {
|
||||
func transifex() {
|
||||
os.Chdir("gui/default/assets/lang")
|
||||
runPrint("go", "run", "../../../../script/transifexdl.go")
|
||||
os.Chdir("../../../..")
|
||||
assets()
|
||||
}
|
||||
|
||||
func deps() {
|
||||
rmr("Godeps")
|
||||
runPrint("godep", "save", "./cmd/...")
|
||||
}
|
||||
|
||||
func clean() {
|
||||
rmr("bin", "Godeps/_workspace/pkg", "Godeps/_workspace/bin")
|
||||
rmr("bin")
|
||||
rmr(filepath.Join(os.Getenv("GOPATH"), fmt.Sprintf("pkg/%s_%s/github.com/syncthing", goos, goarch)))
|
||||
}
|
||||
|
||||
func ldflags() string {
|
||||
sep := ' '
|
||||
if goVersion > 1.4 {
|
||||
sep = '='
|
||||
sep := '='
|
||||
if goVersion > 0 && goVersion < 1.5 {
|
||||
sep = ' '
|
||||
}
|
||||
|
||||
b := new(bytes.Buffer)
|
||||
@@ -506,12 +590,66 @@ func getVersion() string {
|
||||
}
|
||||
// ... then see if we have a Git tag.
|
||||
if ver, err := getGitVersion(); err == nil {
|
||||
if strings.Contains(ver, "-") {
|
||||
// The version already contains a hash and stuff. See if we can
|
||||
// find a current branch name to tack onto it as well.
|
||||
return ver + getBranchSuffix()
|
||||
}
|
||||
return ver
|
||||
}
|
||||
// This seems to be a dev build.
|
||||
return "unknown-dev"
|
||||
}
|
||||
|
||||
func getBranchSuffix() string {
|
||||
bs, err := runError("git", "branch", "-a", "--contains")
|
||||
if err != nil {
|
||||
return ""
|
||||
}
|
||||
|
||||
branches := strings.Split(string(bs), "\n")
|
||||
if len(branches) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
branch := ""
|
||||
for i, candidate := range branches {
|
||||
if strings.HasPrefix(candidate, "*") {
|
||||
// This is the current branch. Select it!
|
||||
branch = strings.TrimLeft(candidate, " \t*")
|
||||
break
|
||||
} else if i == 0 {
|
||||
// Otherwise the first branch in the list will do.
|
||||
branch = strings.TrimSpace(branch)
|
||||
}
|
||||
}
|
||||
|
||||
if branch == "" {
|
||||
return ""
|
||||
}
|
||||
|
||||
// The branch name may be on the form "remotes/origin/foo" from which we
|
||||
// just want "foo".
|
||||
parts := strings.Split(branch, "/")
|
||||
if len(parts) == 0 || len(parts[len(parts)-1]) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
branch = parts[len(parts)-1]
|
||||
if branch == "master" {
|
||||
// master builds are the default.
|
||||
return ""
|
||||
}
|
||||
|
||||
validBranchRe := regexp.MustCompile(`^[a-zA-Z0-9_.-]+$`)
|
||||
if !validBranchRe.MatchString(branch) {
|
||||
// There's some odd stuff in the branch name. Better skip it.
|
||||
return ""
|
||||
}
|
||||
|
||||
return "-" + branch
|
||||
}
|
||||
|
||||
func buildStamp() int64 {
|
||||
bs, err := runError("git", "show", "-s", "--format=%ct")
|
||||
if err != nil {
|
||||
@@ -552,8 +690,8 @@ func buildArch() string {
|
||||
return fmt.Sprintf("%s-%s", os, goarch)
|
||||
}
|
||||
|
||||
func archiveName() string {
|
||||
return fmt.Sprintf("syncthing-%s-%s", buildArch(), version)
|
||||
func archiveName(target target) string {
|
||||
return fmt.Sprintf("%s-%s-%s", target.name, buildArch(), version)
|
||||
}
|
||||
|
||||
func run(cmd string, args ...string) []byte {
|
||||
@@ -599,12 +737,6 @@ func runPipe(file, cmd string, args ...string) {
|
||||
fd.Close()
|
||||
}
|
||||
|
||||
type archiveFile struct {
|
||||
src string
|
||||
dst string
|
||||
perm os.FileMode
|
||||
}
|
||||
|
||||
func tarGz(out string, files []archiveFile) {
|
||||
fd, err := os.Create(out)
|
||||
if err != nil {
|
||||
|
||||
@@ -27,10 +27,6 @@ case "${1:-default}" in
|
||||
build "$@"
|
||||
;;
|
||||
|
||||
deps)
|
||||
build "$@"
|
||||
;;
|
||||
|
||||
assets)
|
||||
build "$@"
|
||||
;;
|
||||
@@ -106,7 +102,7 @@ case "${1:-default}" in
|
||||
fail=0
|
||||
|
||||
# For every package in the repo
|
||||
for dir in $(go list ./...) ; do
|
||||
for dir in $(go list ./lib/... ./cmd/...) ; do
|
||||
# run the tests
|
||||
GOPATH="$(pwd)/Godeps/_workspace:$GOPATH" go test -race -coverprofile=profile.out $dir
|
||||
if [ -f profile.out ] ; then
|
||||
|
||||
@@ -0,0 +1,143 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// This doesn't build on Windows due to the Rusage stuff.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
"runtime"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/rc"
|
||||
)
|
||||
|
||||
var homeDir = "h1"
|
||||
var syncthingBin = "./bin/syncthing"
|
||||
var test = "scan"
|
||||
|
||||
func main() {
|
||||
flag.StringVar(&homeDir, "home", homeDir, "Home directory location")
|
||||
flag.StringVar(&syncthingBin, "bin", syncthingBin, "Binary location")
|
||||
flag.StringVar(&test, "test", test, "Test to run")
|
||||
flag.Parse()
|
||||
|
||||
switch test {
|
||||
case "scan":
|
||||
// scan measures the resource usage required to perform the initial
|
||||
// scan, without cleaning away the database first.
|
||||
testScan()
|
||||
}
|
||||
}
|
||||
|
||||
// testScan starts a process and reports on the resource usage required to
|
||||
// perform the initial scan.
|
||||
func testScan() {
|
||||
log.Println("Starting...")
|
||||
p := rc.NewProcess("127.0.0.1:8081")
|
||||
if err := p.Start(syncthingBin, "-home", homeDir, "-no-browser"); err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
defer p.Stop()
|
||||
|
||||
wallTime := awaitScanComplete(p)
|
||||
|
||||
report(p, wallTime)
|
||||
}
|
||||
|
||||
// awaitScanComplete waits for a folder to transition idle->scanning and
|
||||
// then scanning->idle and returns the time taken for the scan.
|
||||
func awaitScanComplete(p *rc.Process) time.Duration {
|
||||
log.Println("Awaiting scan completion...")
|
||||
var t0, t1 time.Time
|
||||
lastEvent := 0
|
||||
loop:
|
||||
for {
|
||||
evs, err := p.Events(lastEvent)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
|
||||
for _, ev := range evs {
|
||||
if ev.Type == "StateChanged" {
|
||||
data := ev.Data.(map[string]interface{})
|
||||
log.Println(ev)
|
||||
|
||||
if data["to"].(string) == "scanning" {
|
||||
t0 = ev.Time
|
||||
continue
|
||||
}
|
||||
|
||||
if !t0.IsZero() && data["to"].(string) == "idle" {
|
||||
t1 = ev.Time
|
||||
break loop
|
||||
}
|
||||
}
|
||||
lastEvent = ev.ID
|
||||
}
|
||||
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
|
||||
return t1.Sub(t0)
|
||||
}
|
||||
|
||||
// report stops the given process and reports on it's resource usage in two
|
||||
// ways: human readable to stderr, and CSV to stdout.
|
||||
func report(p *rc.Process, wallTime time.Duration) {
|
||||
sv, err := p.SystemVersion()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
ss, err := p.SystemStatus()
|
||||
if err != nil {
|
||||
log.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
proc, err := p.Stop()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
rusage, ok := proc.SysUsage().(*syscall.Rusage)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
log.Println("Version:", sv.Version)
|
||||
log.Println("Alloc:", ss.Alloc/1024, "KiB")
|
||||
log.Println("Sys:", ss.Sys/1024, "KiB")
|
||||
log.Println("Goroutines:", ss.Goroutines)
|
||||
log.Println("Wall time:", wallTime)
|
||||
log.Println("Utime:", time.Duration(rusage.Utime.Nano()))
|
||||
log.Println("Stime:", time.Duration(rusage.Stime.Nano()))
|
||||
if runtime.GOOS == "darwin" {
|
||||
// Darwin reports in bytes, Linux seems to report in KiB even
|
||||
// though the manpage says otherwise.
|
||||
rusage.Maxrss /= 1024
|
||||
}
|
||||
log.Println("MaxRSS:", rusage.Maxrss, "KiB")
|
||||
|
||||
fmt.Printf("%s,%d,%d,%d,%.02f,%.02f,%.02f,%d\n",
|
||||
sv.Version,
|
||||
ss.Alloc/1024,
|
||||
ss.Sys/1024,
|
||||
ss.Goroutines,
|
||||
wallTime.Seconds(),
|
||||
time.Duration(rusage.Utime.Nano()).Seconds(),
|
||||
time.Duration(rusage.Stime.Nano()).Seconds(),
|
||||
rusage.Maxrss)
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
)
|
||||
|
||||
func main() {
|
||||
dir := flag.String("dir", "~/files", "Directory to generate into")
|
||||
files := flag.Int("files", 1000, "Number of files to create")
|
||||
maxExp := flag.Int("maxexp", 20, "Max size exponent")
|
||||
src := flag.String("src", "/dev/urandom", "Source of file data")
|
||||
flag.Parse()
|
||||
if err := generateFiles(*dir, *files, *maxExp, *src); err != nil {
|
||||
log.Println(err)
|
||||
}
|
||||
}
|
||||
|
||||
func generateFiles(dir string, files, maxexp int, srcname string) error {
|
||||
fd, err := os.Open(srcname)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for i := 0; i < files; i++ {
|
||||
n := randomName()
|
||||
|
||||
if rand.Float64() < 0.05 {
|
||||
// Some files and directories are dotfiles
|
||||
n = "." + n
|
||||
}
|
||||
|
||||
p0 := filepath.Join(dir, string(n[0]), n[0:2])
|
||||
err = os.MkdirAll(p0, 0755)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
p1 := filepath.Join(p0, n)
|
||||
|
||||
s := int64(1 << uint(rand.Intn(maxexp)))
|
||||
a := int64(128 * 1024)
|
||||
if a > s {
|
||||
a = s
|
||||
}
|
||||
s += rand.Int63n(a)
|
||||
|
||||
if err := generateOneFile(fd, p1, s); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func generateOneFile(fd io.ReadSeeker, p1 string, s int64) error {
|
||||
src := io.LimitReader(&inifiteReader{fd}, int64(s))
|
||||
dst, err := os.Create(p1)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = io.Copy(dst, src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = dst.Close()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_ = os.Chmod(p1, os.FileMode(rand.Intn(0777)|0400))
|
||||
|
||||
t := time.Now().Add(-time.Duration(rand.Intn(30*86400)) * time.Second)
|
||||
err = os.Chtimes(p1, t, t)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func randomName() string {
|
||||
var b [16]byte
|
||||
readRand(b[:])
|
||||
return fmt.Sprintf("%x", b[:])
|
||||
}
|
||||
|
||||
func readRand(bs []byte) (int, error) {
|
||||
var r uint32
|
||||
for i := range bs {
|
||||
if i%4 == 0 {
|
||||
r = uint32(rand.Int63())
|
||||
}
|
||||
bs[i] = byte(r >> uint((i%4)*8))
|
||||
}
|
||||
return len(bs), nil
|
||||
}
|
||||
|
||||
type inifiteReader struct {
|
||||
rd io.ReadSeeker
|
||||
}
|
||||
|
||||
func (i *inifiteReader) Read(bs []byte) (int, error) {
|
||||
n, err := i.rd.Read(bs)
|
||||
if err == io.EOF {
|
||||
err = nil
|
||||
i.rd.Seek(0, 0)
|
||||
}
|
||||
return n, err
|
||||
}
|
||||
@@ -1,127 +0,0 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
)
|
||||
|
||||
type addressLister struct {
|
||||
upnpService *upnpService
|
||||
cfg *config.Wrapper
|
||||
}
|
||||
|
||||
func newAddressLister(upnpService *upnpService, cfg *config.Wrapper) *addressLister {
|
||||
return &addressLister{
|
||||
upnpService: upnpService,
|
||||
cfg: cfg,
|
||||
}
|
||||
}
|
||||
|
||||
// ExternalAddresses returns a list of addresses that are our best guess for
|
||||
// where we are reachable from the outside. As a special case, we may return
|
||||
// one or more addresses with an empty IP address (0.0.0.0 or ::) and just
|
||||
// port number - this means that the outside address of a NAT gateway should
|
||||
// be substituted.
|
||||
func (e *addressLister) ExternalAddresses() []string {
|
||||
return e.addresses(false)
|
||||
}
|
||||
|
||||
// AllAddresses returns a list of addresses that are our best guess for where
|
||||
// we are reachable from the local network. Same conditions as
|
||||
// ExternalAddresses, but private IPv4 addresses are included.
|
||||
func (e *addressLister) AllAddresses() []string {
|
||||
return e.addresses(true)
|
||||
}
|
||||
|
||||
func (e *addressLister) addresses(includePrivateIPV4 bool) []string {
|
||||
var addrs []string
|
||||
|
||||
// Grab our listen addresses from the config. Unspecified ones are passed
|
||||
// on verbatim (to be interpreted by a global discovery server or local
|
||||
// discovery peer). Public addresses are passed on verbatim. Private
|
||||
// addresses are filtered.
|
||||
for _, addrStr := range e.cfg.Options().ListenAddress {
|
||||
addrURL, err := url.Parse(addrStr)
|
||||
if err != nil {
|
||||
l.Infoln("Listen address", addrStr, "is invalid:", err)
|
||||
continue
|
||||
}
|
||||
addr, err := net.ResolveTCPAddr("tcp", addrURL.Host)
|
||||
if err != nil {
|
||||
l.Infoln("Listen address", addrStr, "is invalid:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if addr.IP == nil || addr.IP.IsUnspecified() {
|
||||
// Address like 0.0.0.0:22000 or [::]:22000 or :22000; include as is.
|
||||
addrs = append(addrs, tcpAddr(addr.String()))
|
||||
} else if isPublicIPv4(addr.IP) || isPublicIPv6(addr.IP) {
|
||||
// A public address; include as is.
|
||||
addrs = append(addrs, tcpAddr(addr.String()))
|
||||
} else if includePrivateIPV4 && addr.IP.To4().IsGlobalUnicast() {
|
||||
// A private IPv4 address.
|
||||
addrs = append(addrs, tcpAddr(addr.String()))
|
||||
}
|
||||
}
|
||||
|
||||
// Get an external port mapping from the upnpService, if it has one. If so,
|
||||
// add it as another unspecified address.
|
||||
if e.upnpService != nil {
|
||||
if port := e.upnpService.ExternalPort(); port != 0 {
|
||||
addrs = append(addrs, fmt.Sprintf("tcp://:%d", port))
|
||||
}
|
||||
}
|
||||
|
||||
return addrs
|
||||
}
|
||||
|
||||
func isPublicIPv4(ip net.IP) bool {
|
||||
ip = ip.To4()
|
||||
if ip == nil {
|
||||
// Not an IPv4 address (IPv6)
|
||||
return false
|
||||
}
|
||||
|
||||
// IsGlobalUnicast below only checks that it's not link local or
|
||||
// multicast, and we want to exclude private (NAT:ed) addresses as well.
|
||||
rfc1918 := []net.IPNet{
|
||||
{IP: net.IP{10, 0, 0, 0}, Mask: net.IPMask{255, 0, 0, 0}},
|
||||
{IP: net.IP{172, 16, 0, 0}, Mask: net.IPMask{255, 240, 0, 0}},
|
||||
{IP: net.IP{192, 168, 0, 0}, Mask: net.IPMask{255, 255, 0, 0}},
|
||||
}
|
||||
for _, n := range rfc1918 {
|
||||
if n.Contains(ip) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return ip.IsGlobalUnicast()
|
||||
}
|
||||
|
||||
func isPublicIPv6(ip net.IP) bool {
|
||||
if ip.To4() != nil {
|
||||
// Not an IPv6 address (IPv4)
|
||||
// (To16() returns a v6 mapped v4 address so can't be used to check
|
||||
// that it's an actual v6 address)
|
||||
return false
|
||||
}
|
||||
|
||||
return ip.IsGlobalUnicast()
|
||||
}
|
||||
|
||||
func tcpAddr(host string) string {
|
||||
u := url.URL{
|
||||
Scheme: "tcp",
|
||||
Host: host,
|
||||
}
|
||||
return u.String()
|
||||
}
|
||||
+122
-28
@@ -32,13 +32,14 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/discover"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/relay"
|
||||
"github.com/syncthing/syncthing/lib/stats"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
"github.com/syncthing/syncthing/lib/util"
|
||||
"github.com/vitrun/qart/qr"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
@@ -50,13 +51,15 @@ var (
|
||||
|
||||
type apiService struct {
|
||||
id protocol.DeviceID
|
||||
cfg *config.Wrapper
|
||||
cfg configIntf
|
||||
httpsCertFile string
|
||||
httpsKeyFile string
|
||||
assetDir string
|
||||
themes []string
|
||||
model *model.Model
|
||||
eventSub *events.BufferedSubscription
|
||||
discoverer *discover.CachingMux
|
||||
relayService *relay.Service
|
||||
model modelIntf
|
||||
eventSub events.BufferedSubscription
|
||||
discoverer discover.CachingMux
|
||||
relayService relay.Service
|
||||
fss *folderSummaryService
|
||||
systemConfigMut sync.Mutex // serializes posts to /rest/system/config
|
||||
stop chan struct{} // signals intentional stop
|
||||
@@ -66,14 +69,57 @@ type apiService struct {
|
||||
listener net.Listener
|
||||
listenerMut sync.Mutex
|
||||
|
||||
guiErrors *logger.Recorder
|
||||
systemLog *logger.Recorder
|
||||
guiErrors logger.Recorder
|
||||
systemLog logger.Recorder
|
||||
}
|
||||
|
||||
func newAPIService(id protocol.DeviceID, cfg *config.Wrapper, assetDir string, m *model.Model, eventSub *events.BufferedSubscription, discoverer *discover.CachingMux, relayService *relay.Service, errors, systemLog *logger.Recorder) (*apiService, error) {
|
||||
type modelIntf interface {
|
||||
GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{}
|
||||
Completion(device protocol.DeviceID, folder string) float64
|
||||
Override(folder string)
|
||||
NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated, int)
|
||||
NeedSize(folder string) (nfiles int, bytes int64)
|
||||
ConnectionStats() map[string]interface{}
|
||||
DeviceStatistics() map[string]stats.DeviceStatistics
|
||||
FolderStatistics() map[string]stats.FolderStatistics
|
||||
CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool)
|
||||
CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool)
|
||||
ResetFolder(folder string)
|
||||
Availability(folder, file string) []protocol.DeviceID
|
||||
GetIgnores(folder string) ([]string, []string, error)
|
||||
SetIgnores(folder string, content []string) error
|
||||
PauseDevice(device protocol.DeviceID)
|
||||
ResumeDevice(device protocol.DeviceID)
|
||||
DelayScan(folder string, next time.Duration)
|
||||
ScanFolder(folder string) error
|
||||
ScanFolders() map[string]error
|
||||
ScanFolderSubs(folder string, subs []string) error
|
||||
BringToFront(folder, file string)
|
||||
ConnectedTo(deviceID protocol.DeviceID) bool
|
||||
GlobalSize(folder string) (nfiles, deleted int, bytes int64)
|
||||
LocalSize(folder string) (nfiles, deleted int, bytes int64)
|
||||
CurrentLocalVersion(folder string) (int64, bool)
|
||||
RemoteLocalVersion(folder string) (int64, bool)
|
||||
State(folder string) (string, time.Time, error)
|
||||
}
|
||||
|
||||
type configIntf interface {
|
||||
GUI() config.GUIConfiguration
|
||||
Raw() config.Configuration
|
||||
Options() config.OptionsConfiguration
|
||||
Replace(cfg config.Configuration) config.CommitResponse
|
||||
Subscribe(c config.Committer)
|
||||
Folders() map[string]config.FolderConfiguration
|
||||
Devices() map[protocol.DeviceID]config.DeviceConfiguration
|
||||
Save() error
|
||||
}
|
||||
|
||||
func newAPIService(id protocol.DeviceID, cfg configIntf, httpsCertFile, httpsKeyFile, assetDir string, m modelIntf, eventSub events.BufferedSubscription, discoverer discover.CachingMux, relayService relay.Service, errors, systemLog logger.Recorder) (*apiService, error) {
|
||||
service := &apiService{
|
||||
id: id,
|
||||
cfg: cfg,
|
||||
httpsCertFile: httpsCertFile,
|
||||
httpsKeyFile: httpsKeyFile,
|
||||
assetDir: assetDir,
|
||||
model: m,
|
||||
eventSub: eventSub,
|
||||
@@ -88,6 +134,7 @@ func newAPIService(id protocol.DeviceID, cfg *config.Wrapper, assetDir string, m
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
// Load themes from compiled in assets.
|
||||
for file := range auto.Assets() {
|
||||
theme := strings.Split(file, "/")[0]
|
||||
if _, ok := seen[theme]; !ok {
|
||||
@@ -95,6 +142,15 @@ func newAPIService(id protocol.DeviceID, cfg *config.Wrapper, assetDir string, m
|
||||
service.themes = append(service.themes, theme)
|
||||
}
|
||||
}
|
||||
if assetDir != "" {
|
||||
// Load any extra themes from the asset override dir.
|
||||
for _, dir := range dirNames(assetDir) {
|
||||
if _, ok := seen[dir]; !ok {
|
||||
seen[dir] = struct{}{}
|
||||
service.themes = append(service.themes, dir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var err error
|
||||
service.listener, err = service.getListener(cfg.GUI())
|
||||
@@ -102,7 +158,7 @@ func newAPIService(id protocol.DeviceID, cfg *config.Wrapper, assetDir string, m
|
||||
}
|
||||
|
||||
func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener, error) {
|
||||
cert, err := tls.LoadX509KeyPair(locations[locHTTPSCertFile], locations[locHTTPSKeyFile])
|
||||
cert, err := tls.LoadX509KeyPair(s.httpsCertFile, s.httpsKeyFile)
|
||||
if err != nil {
|
||||
l.Infoln("Loading HTTPS certificate:", err)
|
||||
l.Infoln("Creating new HTTPS certificate")
|
||||
@@ -115,7 +171,7 @@ func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener,
|
||||
name = tlsDefaultCommonName
|
||||
}
|
||||
|
||||
cert, err = tlsutil.NewCertificate(locations[locHTTPSCertFile], locations[locHTTPSKeyFile], name, httpsRSABits)
|
||||
cert, err = tlsutil.NewCertificate(s.httpsCertFile, s.httpsKeyFile, name, httpsRSABits)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -245,12 +301,9 @@ func (s *apiService) Serve() {
|
||||
|
||||
guiCfg := s.cfg.GUI()
|
||||
|
||||
// Add the CORS handling
|
||||
handler := corsMiddleware(mux)
|
||||
|
||||
// Wrap everything in CSRF protection. The /rest prefix should be
|
||||
// protected, other requests will grant cookies.
|
||||
handler = csrfMiddleware(s.id.String()[:5], "/rest", guiCfg, handler)
|
||||
handler := csrfMiddleware(s.id.String()[:5], "/rest", guiCfg, mux)
|
||||
|
||||
// Add our version and ID as a header to responses
|
||||
handler = withDetailsMiddleware(s.id, handler)
|
||||
@@ -265,6 +318,9 @@ func (s *apiService) Serve() {
|
||||
handler = redirectToHTTPSMiddleware(handler)
|
||||
}
|
||||
|
||||
// Add the CORS handling
|
||||
handler = corsMiddleware(handler)
|
||||
|
||||
handler = debugMiddleware(handler)
|
||||
|
||||
srv := http.Server{
|
||||
@@ -393,10 +449,14 @@ func corsMiddleware(next http.Handler) http.Handler {
|
||||
// when the browser initiate a POST request.
|
||||
//
|
||||
// As the OPTIONS request is unauthorized, this handler must be the first
|
||||
// of the chain.
|
||||
// of the chain (hence added at the end).
|
||||
//
|
||||
// See https://www.w3.org/TR/cors/ for details.
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
// Add a generous access-control-allow-origin header since we may be
|
||||
// redirecting REST requests over protocols
|
||||
w.Header().Add("Access-Control-Allow-Origin", "*")
|
||||
|
||||
// Process OPTIONS requests
|
||||
if r.Method == "OPTIONS" {
|
||||
// Only GET/POST Methods are supported
|
||||
@@ -536,7 +596,7 @@ func (s *apiService) getDBStatus(w http.ResponseWriter, r *http.Request) {
|
||||
sendJSON(w, folderSummary(s.cfg, s.model, folder))
|
||||
}
|
||||
|
||||
func folderSummary(cfg *config.Wrapper, m *model.Model, folder string) map[string]interface{} {
|
||||
func folderSummary(cfg configIntf, m modelIntf, folder string) map[string]interface{} {
|
||||
var res = make(map[string]interface{})
|
||||
|
||||
res["invalid"] = cfg.Folders()[folder].Invalid
|
||||
@@ -674,7 +734,7 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if curAcc := s.cfg.Options().URAccepted; to.Options.URAccepted > curAcc {
|
||||
// UR was enabled
|
||||
to.Options.URAccepted = usageReportVersion
|
||||
to.Options.URUniqueID = randomString(8)
|
||||
to.Options.URUniqueID = util.RandomString(8)
|
||||
} else if to.Options.URAccepted < curAcc {
|
||||
// UR was disabled
|
||||
to.Options.URAccepted = -1
|
||||
@@ -874,7 +934,7 @@ func (s *apiService) getDBIgnores(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
sendJSON(w, map[string][]string{
|
||||
"ignore": ignores,
|
||||
"patterns": patterns,
|
||||
"expanded": patterns,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -1129,22 +1189,33 @@ func (s embeddedStatic) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
file = "index.html"
|
||||
}
|
||||
|
||||
if s.assetDir != "" {
|
||||
p := filepath.Join(s.assetDir, filepath.FromSlash(file))
|
||||
_, err := os.Stat(p)
|
||||
if err == nil {
|
||||
http.ServeFile(w, r, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
s.mut.RLock()
|
||||
theme := s.theme
|
||||
modified := s.lastModified
|
||||
s.mut.RUnlock()
|
||||
|
||||
// Check for an override for the current theme.
|
||||
if s.assetDir != "" {
|
||||
p := filepath.Join(s.assetDir, s.theme, filepath.FromSlash(file))
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
http.ServeFile(w, r, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a compiled in asset for the current theme.
|
||||
bs, ok := s.assets[theme+"/"+file]
|
||||
if !ok {
|
||||
// Check for an overriden default asset.
|
||||
if s.assetDir != "" {
|
||||
p := filepath.Join(s.assetDir, config.DefaultTheme, filepath.FromSlash(file))
|
||||
if _, err := os.Stat(p); err == nil {
|
||||
http.ServeFile(w, r, p)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
// Check for a compiled in default asset.
|
||||
bs, ok = s.assets[config.DefaultTheme+"/"+file]
|
||||
if !ok {
|
||||
http.NotFound(w, r)
|
||||
@@ -1271,3 +1342,26 @@ func (v jsonVersionVector) MarshalJSON() ([]byte, error) {
|
||||
}
|
||||
return json.Marshal(res)
|
||||
}
|
||||
|
||||
func dirNames(dir string) []string {
|
||||
fd, err := os.Open(dir)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
fis, err := fd.Readdir(-1)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
var dirs []string
|
||||
for _, fi := range fis {
|
||||
if fi.IsDir() {
|
||||
dirs = append(dirs, filepath.Base(fi.Name()))
|
||||
}
|
||||
}
|
||||
|
||||
sort.Strings(dirs)
|
||||
return dirs
|
||||
}
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/util"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
@@ -90,7 +91,7 @@ func basicAuthAndSessionMiddleware(cookieName string, cfg config.GUIConfiguratio
|
||||
return
|
||||
}
|
||||
|
||||
sessionid := randomString(32)
|
||||
sessionid := util.RandomString(32)
|
||||
sessionsMut.Lock()
|
||||
sessions[sessionid] = true
|
||||
sessionsMut.Unlock()
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/util"
|
||||
)
|
||||
|
||||
// csrfTokens is a list of valid tokens. It is sorted so that the most
|
||||
@@ -40,8 +41,7 @@ func csrfMiddleware(unique string, prefix string, cfg config.GUIConfiguration, n
|
||||
return
|
||||
}
|
||||
|
||||
// Allow requests for anything not under the protected path prefix,
|
||||
// and set a CSRF cookie if there isn't already a valid one.
|
||||
// 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-" + unique)
|
||||
if err != nil || !validCsrfToken(cookie.Value) {
|
||||
@@ -56,6 +56,18 @@ func csrfMiddleware(unique string, prefix string, cfg config.GUIConfiguration, n
|
||||
return
|
||||
}
|
||||
|
||||
if r.Method == "GET" {
|
||||
// Allow GET requests unconditionally, but if we got the CSRF
|
||||
// token cookie do the verification anyway so we keep the
|
||||
// csrfTokens list sorted by recent usage. We don't care about the
|
||||
// outcome of the validity check.
|
||||
if cookie, err := r.Cookie("CSRF-Token-" + unique); err == nil {
|
||||
validCsrfToken(cookie.Value)
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
// Verify the CSRF token
|
||||
token := r.Header.Get("X-CSRF-Token-" + unique)
|
||||
if !validCsrfToken(token) {
|
||||
@@ -86,7 +98,7 @@ func validCsrfToken(token string) bool {
|
||||
}
|
||||
|
||||
func newCsrfToken() string {
|
||||
token := randomString(32)
|
||||
token := util.RandomString(32)
|
||||
|
||||
csrfMut.Lock()
|
||||
csrfTokens = append([]string{token}, csrfTokens...)
|
||||
|
||||
+332
-4
@@ -7,10 +7,21 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"compress/gzip"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/d4l3k/messagediff"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
@@ -48,9 +59,6 @@ func TestCSRFToken(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStopAfterBrokenConfig(t *testing.T) {
|
||||
baseDirs["config"] = "../../test/h1" // to load HTTPS keys
|
||||
expandLocations()
|
||||
|
||||
cfg := config.Configuration{
|
||||
GUI: config.GUIConfiguration{
|
||||
RawAddress: "127.0.0.1:0",
|
||||
@@ -59,7 +67,7 @@ func TestStopAfterBrokenConfig(t *testing.T) {
|
||||
}
|
||||
w := config.Wrap("/dev/null", cfg)
|
||||
|
||||
srv, err := newAPIService(protocol.LocalDeviceID, w, "", nil, nil, nil, nil, nil, nil)
|
||||
srv, err := newAPIService(protocol.LocalDeviceID, w, "../../test/h1/https-cert.pem", "../../test/h1/https-key.pem", "", nil, nil, nil, nil, nil, nil)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -89,3 +97,323 @@ func TestStopAfterBrokenConfig(t *testing.T) {
|
||||
|
||||
sup.Stop()
|
||||
}
|
||||
|
||||
func TestAssetsDir(t *testing.T) {
|
||||
// For any given request to $FILE, we should return the first found of
|
||||
// - assetsdir/$THEME/$FILE
|
||||
// - compiled in asset $THEME/$FILE
|
||||
// - assetsdir/default/$FILE
|
||||
// - compiled in asset default/$FILE
|
||||
|
||||
// The asset map contains compressed assets, so create a couple of gzip compressed assets here.
|
||||
buf := new(bytes.Buffer)
|
||||
gw := gzip.NewWriter(buf)
|
||||
gw.Write([]byte("default"))
|
||||
gw.Close()
|
||||
def := buf.Bytes()
|
||||
|
||||
buf = new(bytes.Buffer)
|
||||
gw = gzip.NewWriter(buf)
|
||||
gw.Write([]byte("foo"))
|
||||
gw.Close()
|
||||
foo := buf.Bytes()
|
||||
|
||||
e := embeddedStatic{
|
||||
theme: "foo",
|
||||
mut: sync.NewRWMutex(),
|
||||
assetDir: "testdata",
|
||||
assets: map[string][]byte{
|
||||
"foo/a": foo, // overridden in foo/a
|
||||
"foo/b": foo,
|
||||
"default/a": def, // overridden in default/a (but foo/a takes precedence)
|
||||
"default/b": def, // overridden in default/b (but foo/b takes precedence)
|
||||
"default/c": def,
|
||||
},
|
||||
}
|
||||
|
||||
s := httptest.NewServer(e)
|
||||
defer s.Close()
|
||||
|
||||
// assetsdir/foo/a exists, overrides compiled in
|
||||
expectURLToContain(t, s.URL+"/a", "overridden-foo")
|
||||
|
||||
// foo/b is compiled in, default/b is overriden, return compiled in
|
||||
expectURLToContain(t, s.URL+"/b", "foo")
|
||||
|
||||
// only exists as compiled in default/c so use that
|
||||
expectURLToContain(t, s.URL+"/c", "default")
|
||||
|
||||
// only exists as overriden default/d so use that
|
||||
expectURLToContain(t, s.URL+"/d", "overridden-default")
|
||||
}
|
||||
|
||||
func expectURLToContain(t *testing.T, url, exp string) {
|
||||
res, err := http.Get(url)
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if res.StatusCode != 200 {
|
||||
t.Errorf("Got %s instead of 200 OK", res.Status)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(res.Body)
|
||||
res.Body.Close()
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
if string(data) != exp {
|
||||
t.Errorf("Got %q instead of %q on %q", data, exp, url)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
func TestDirNames(t *testing.T) {
|
||||
names := dirNames("testdata")
|
||||
expected := []string{"default", "foo", "testfolder"}
|
||||
if diff, equal := messagediff.PrettyDiff(expected, names); !equal {
|
||||
t.Errorf("Unexpected dirNames return: %#v\n%s", names, diff)
|
||||
}
|
||||
}
|
||||
|
||||
type httpTestCase struct {
|
||||
URL string // URL to check
|
||||
Code int // Expected result code
|
||||
Type string // Expected content type
|
||||
Prefix string // Expected result prefix
|
||||
Timeout time.Duration // Defaults to a second
|
||||
}
|
||||
|
||||
func TestAPIServiceRequests(t *testing.T) {
|
||||
model := new(mockedModel)
|
||||
cfg := new(mockedConfig)
|
||||
httpsCertFile := "../../test/h1/https-cert.pem"
|
||||
httpsKeyFile := "../../test/h1/https-key.pem"
|
||||
assetDir := "../../gui"
|
||||
eventSub := new(mockedEventSub)
|
||||
discoverer := new(mockedCachingMux)
|
||||
relayService := new(mockedRelayService)
|
||||
errorLog := new(mockedLoggerRecorder)
|
||||
systemLog := new(mockedLoggerRecorder)
|
||||
|
||||
// Instantiate the API service
|
||||
svc, err := newAPIService(protocol.LocalDeviceID, cfg, httpsCertFile, httpsKeyFile, assetDir, model,
|
||||
eventSub, discoverer, relayService, errorLog, systemLog)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
_ = svc
|
||||
|
||||
// Make sure the API service is listening, and get the URL to use.
|
||||
addr := svc.listener.Addr()
|
||||
if addr == nil {
|
||||
t.Fatal("Nil listening address from API service")
|
||||
}
|
||||
tcpAddr, err := net.ResolveTCPAddr("tcp", addr.String())
|
||||
if err != nil {
|
||||
t.Fatal("Weird address from API service:", err)
|
||||
}
|
||||
baseURL := fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port)
|
||||
|
||||
// Actually start the API service
|
||||
supervisor := suture.NewSimple("API test")
|
||||
supervisor.Add(svc)
|
||||
supervisor.ServeBackground()
|
||||
|
||||
cases := []httpTestCase{
|
||||
// /rest/db
|
||||
{
|
||||
URL: "/rest/db/completion?device=" + protocol.LocalDeviceID.String() + "&folder=default",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/db/file?folder=default&file=something",
|
||||
Code: 404,
|
||||
},
|
||||
{
|
||||
URL: "/rest/db/ignores?folder=default",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/db/need?folder=default",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/db/status?folder=default",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/db/browse?folder=default",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "null",
|
||||
},
|
||||
|
||||
// /rest/stats
|
||||
{
|
||||
URL: "/rest/stats/device",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "null",
|
||||
},
|
||||
{
|
||||
URL: "/rest/stats/folder",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "null",
|
||||
},
|
||||
|
||||
// /rest/svc
|
||||
{
|
||||
URL: "/rest/svc/deviceid?id=" + protocol.LocalDeviceID.String(),
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/svc/lang",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "[",
|
||||
},
|
||||
{
|
||||
URL: "/rest/svc/report",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
Timeout: 5 * time.Second,
|
||||
},
|
||||
|
||||
// /rest/system
|
||||
{
|
||||
URL: "/rest/system/browse?current=~",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "[",
|
||||
},
|
||||
{
|
||||
URL: "/rest/system/config",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/system/config/insync",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/system/connections",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "null",
|
||||
},
|
||||
{
|
||||
URL: "/rest/system/discovery",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/system/error?since=0",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/system/ping",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/system/status",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/system/version",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/system/debug",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/system/log?since=0",
|
||||
Code: 200,
|
||||
Type: "application/json",
|
||||
Prefix: "{",
|
||||
},
|
||||
{
|
||||
URL: "/rest/system/log.txt?since=0",
|
||||
Code: 200,
|
||||
Type: "text/plain",
|
||||
Prefix: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Log("Testing", tc.URL, "...")
|
||||
testHTTPRequest(t, baseURL, tc)
|
||||
}
|
||||
}
|
||||
|
||||
// testHTTPRequest tries the given test case, comparing the result code,
|
||||
// content type, and result prefix.
|
||||
func testHTTPRequest(t *testing.T, baseURL string, tc httpTestCase) {
|
||||
timeout := time.Second
|
||||
if tc.Timeout > 0 {
|
||||
timeout = tc.Timeout
|
||||
}
|
||||
cli := &http.Client{
|
||||
Timeout: timeout,
|
||||
}
|
||||
|
||||
resp, err := cli.Get(baseURL + tc.URL)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error requesting %s: %v", tc.URL, err)
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != tc.Code {
|
||||
t.Errorf("Get on %s should have returned status code %d, not %s", tc.URL, tc.Code, resp.Status)
|
||||
return
|
||||
}
|
||||
|
||||
ct := resp.Header.Get("Content-Type")
|
||||
if !strings.HasPrefix(ct, tc.Type) {
|
||||
t.Errorf("The content type on %s should be %q, not %q", tc.URL, tc.Type, ct)
|
||||
return
|
||||
}
|
||||
|
||||
data, err := ioutil.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error reading %s: %v", tc.URL, err)
|
||||
return
|
||||
}
|
||||
|
||||
if !bytes.HasPrefix(data, []byte(tc.Prefix)) {
|
||||
t.Errorf("Returned data from %s does not have prefix %q: %s", tc.URL, tc.Prefix, data)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
@@ -48,7 +48,7 @@ var locations = map[locationEnum]string{
|
||||
locKeyFile: "${config}/key.pem",
|
||||
locHTTPSCertFile: "${config}/https-cert.pem",
|
||||
locHTTPSKeyFile: "${config}/https-key.pem",
|
||||
locDatabase: "${config}/index-v0.11.0.db",
|
||||
locDatabase: "${config}/index-v0.13.0.db",
|
||||
locLogFile: "${config}/syncthing.log", // -logfile on Windows
|
||||
locCsrfTokens: "${config}/csrftokens.txt",
|
||||
locPanicLog: "${config}/panic-${timestamp}.log",
|
||||
|
||||
+41
-47
@@ -44,20 +44,23 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/symlinks"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
"github.com/syncthing/syncthing/lib/upnp"
|
||||
"github.com/syncthing/syncthing/lib/util"
|
||||
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
var (
|
||||
Version = "unknown-dev"
|
||||
Codename = "Beryllium Bedbug"
|
||||
BuildStamp = "0"
|
||||
BuildDate time.Time
|
||||
BuildHost = "unknown"
|
||||
BuildUser = "unknown"
|
||||
IsRelease bool
|
||||
IsBeta bool
|
||||
LongVersion string
|
||||
Version = "unknown-dev"
|
||||
Codename = "Copper Cockroach"
|
||||
BuildStamp = "0"
|
||||
BuildDate time.Time
|
||||
BuildHost = "unknown"
|
||||
BuildUser = "unknown"
|
||||
IsRelease bool
|
||||
IsBeta bool
|
||||
LongVersion string
|
||||
allowedVersionExp = regexp.MustCompile(`^v\d+\.\d+\.\d+(-[a-z0-9]+)*(\.\d+)*(\+\d+-g[0-9a-f]+)?(-[^\s]+)?$`)
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -89,9 +92,8 @@ const (
|
||||
func init() {
|
||||
if Version != "unknown-dev" {
|
||||
// If not a generic dev build, version string should come from git describe
|
||||
exp := regexp.MustCompile(`^v\d+\.\d+\.\d+(-[a-z0-9]+)*(\+\d+-g[0-9a-f]+)?(-dirty)?$`)
|
||||
if !exp.MatchString(Version) {
|
||||
l.Fatalf("Invalid version string %q;\n\tdoes not match regexp %v", Version, exp)
|
||||
if !allowedVersionExp.MatchString(Version) {
|
||||
l.Fatalf("Invalid version string %q;\n\tdoes not match regexp %v", Version, allowedVersionExp)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -340,7 +342,7 @@ func main() {
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err) // exits 1
|
||||
}
|
||||
l.Okln("Upgraded from", options.upgradeTo)
|
||||
l.Infoln("Upgraded from", options.upgradeTo)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -464,14 +466,14 @@ func performUpgrade(release upgrade.Release) {
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err)
|
||||
}
|
||||
l.Okf("Upgraded to %q", release.Tag)
|
||||
l.Infof("Upgraded to %q", release.Tag)
|
||||
} else {
|
||||
l.Infoln("Attempting upgrade through running Syncthing...")
|
||||
err = upgradeViaRest()
|
||||
if err != nil {
|
||||
l.Fatalln("Upgrade:", err)
|
||||
}
|
||||
l.Okln("Syncthing upgrading")
|
||||
l.Infoln("Syncthing upgrading")
|
||||
os.Exit(exitUpgrading)
|
||||
}
|
||||
}
|
||||
@@ -558,7 +560,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
|
||||
// We reinitialize the predictable RNG with our device ID, to get a
|
||||
// sequence that is always the same but unique to this syncthing instance.
|
||||
predictableRandom.Seed(seedFromBytes(cert.Certificate[0]))
|
||||
util.PredictableRandom.Seed(util.SeedFromBytes(cert.Certificate[0]))
|
||||
|
||||
myID = protocol.NewDeviceID(cert.Certificate[0])
|
||||
l.SetPrefix(fmt.Sprintf("[%s] ", myID.String()[:5]))
|
||||
@@ -640,6 +642,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
|
||||
dbFile := locations[locDatabase]
|
||||
ldb, err := db.Open(dbFile)
|
||||
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot open database:", err, "- Is another copy of Syncthing already running?")
|
||||
}
|
||||
@@ -719,26 +722,16 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
l.Fatalln("Bad listen address:", err)
|
||||
}
|
||||
|
||||
// The externalAddr tracks our external addresses for discovery purposes.
|
||||
|
||||
var addrList *addressLister
|
||||
|
||||
// Start UPnP
|
||||
|
||||
var upnpService *upnp.Service
|
||||
if opts.UPnPEnabled {
|
||||
upnpService := newUPnPService(cfg, addr.Port)
|
||||
upnpService = upnp.NewUPnPService(cfg, addr.Port)
|
||||
mainService.Add(upnpService)
|
||||
|
||||
// The external address tracker needs to know about the UPnP service
|
||||
// so it can check for an external mapped port.
|
||||
addrList = newAddressLister(upnpService, cfg)
|
||||
} else {
|
||||
addrList = newAddressLister(nil, cfg)
|
||||
}
|
||||
|
||||
// Start relay management
|
||||
|
||||
var relayService *relay.Service
|
||||
var relayService relay.Service
|
||||
if opts.RelaysEnabled {
|
||||
relayService = relay.NewService(cfg, tlsCfg)
|
||||
mainService.Add(relayService)
|
||||
@@ -749,10 +742,15 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
cachedDiscovery := discover.NewCachingMux()
|
||||
mainService.Add(cachedDiscovery)
|
||||
|
||||
// Start connection management
|
||||
|
||||
connectionService := connections.NewConnectionService(cfg, myID, m, tlsCfg, cachedDiscovery, upnpService, relayService, bepProtocolName, tlsDefaultCommonName, lans)
|
||||
mainService.Add(connectionService)
|
||||
|
||||
if cfg.Options().GlobalAnnEnabled {
|
||||
for _, srv := range cfg.GlobalDiscoveryServers() {
|
||||
l.Infoln("Using discovery server", srv)
|
||||
gd, err := discover.NewGlobal(srv, cert, addrList, relayService)
|
||||
gd, err := discover.NewGlobal(srv, cert, connectionService, relayService)
|
||||
if err != nil {
|
||||
l.Warnln("Global discovery:", err)
|
||||
continue
|
||||
@@ -767,14 +765,14 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
|
||||
if cfg.Options().LocalAnnEnabled {
|
||||
// v4 broadcasts
|
||||
bcd, err := discover.NewLocal(myID, fmt.Sprintf(":%d", cfg.Options().LocalAnnPort), addrList, relayService)
|
||||
bcd, err := discover.NewLocal(myID, fmt.Sprintf(":%d", cfg.Options().LocalAnnPort), connectionService, relayService)
|
||||
if err != nil {
|
||||
l.Warnln("IPv4 local discovery:", err)
|
||||
} else {
|
||||
cachedDiscovery.Add(bcd, 0, 0, ipv4LocalDiscoveryPriority)
|
||||
}
|
||||
// v6 multicasts
|
||||
mcd, err := discover.NewLocal(myID, cfg.Options().LocalAnnMCAddr, addrList, relayService)
|
||||
mcd, err := discover.NewLocal(myID, cfg.Options().LocalAnnMCAddr, connectionService, relayService)
|
||||
if err != nil {
|
||||
l.Warnln("IPv6 local discovery:", err)
|
||||
} else {
|
||||
@@ -786,11 +784,6 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
|
||||
setupGUI(mainService, cfg, m, apiSub, cachedDiscovery, relayService, errors, systemLog, runtimeOptions)
|
||||
|
||||
// Start connection management
|
||||
|
||||
connectionService := connections.NewConnectionService(cfg, myID, m, tlsCfg, cachedDiscovery, relayService, bepProtocolName, tlsDefaultCommonName, lans)
|
||||
mainService.Add(connectionService)
|
||||
|
||||
if runtimeOptions.cpuProfile {
|
||||
f, err := os.Create(fmt.Sprintf("cpu-%d.pprof", os.Getpid()))
|
||||
if err != nil {
|
||||
@@ -815,7 +808,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
if opts.URUniqueID == "" {
|
||||
// Previously the ID was generated from the node ID. We now need
|
||||
// to generate a new one.
|
||||
opts.URUniqueID = randomString(8)
|
||||
opts.URUniqueID = util.RandomString(8)
|
||||
cfg.SetOptions(opts)
|
||||
cfg.Save()
|
||||
}
|
||||
@@ -851,7 +844,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
|
||||
mainService.Stop()
|
||||
|
||||
l.Okln("Exiting")
|
||||
l.Infoln("Exiting")
|
||||
|
||||
if runtimeOptions.cpuProfile {
|
||||
pprof.StopCPUProfile()
|
||||
@@ -971,7 +964,7 @@ func startAuditing(mainService *suture.Supervisor) {
|
||||
l.Infoln("Audit log in", auditFile)
|
||||
}
|
||||
|
||||
func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub *events.BufferedSubscription, discoverer *discover.CachingMux, relayService *relay.Service, errors, systemLog *logger.Recorder, runtimeOptions RuntimeOptions) {
|
||||
func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Model, apiSub events.BufferedSubscription, discoverer discover.CachingMux, relayService relay.Service, errors, systemLog logger.Recorder, runtimeOptions RuntimeOptions) {
|
||||
guiCfg := cfg.GUI()
|
||||
|
||||
if !guiCfg.Enabled {
|
||||
@@ -982,7 +975,7 @@ func setupGUI(mainService *suture.Supervisor, cfg *config.Wrapper, m *model.Mode
|
||||
l.Warnln("Insecure admin access is enabled.")
|
||||
}
|
||||
|
||||
api, err := newAPIService(myID, cfg, runtimeOptions.assetDir, m, apiSub, discoverer, relayService, errors, systemLog)
|
||||
api, err := newAPIService(myID, cfg, locations[locHTTPSCertFile], locations[locHTTPSKeyFile], runtimeOptions.assetDir, m, apiSub, discoverer, relayService, errors, systemLog)
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot start GUI:", err)
|
||||
}
|
||||
@@ -1170,12 +1163,13 @@ func autoUpgrade(cfg *config.Wrapper) {
|
||||
// suitable time after they have gone out of fashion.
|
||||
func cleanConfigDirectory() {
|
||||
patterns := map[string]time.Duration{
|
||||
"panic-*.log": 7 * 24 * time.Hour, // keep panic logs for a week
|
||||
"audit-*.log": 7 * 24 * time.Hour, // keep audit logs for a week
|
||||
"index": 14 * 24 * time.Hour, // keep old index format for two weeks
|
||||
"config.xml.v*": 30 * 24 * time.Hour, // old config versions for a month
|
||||
"*.idx.gz": 30 * 24 * time.Hour, // these should for sure no longer exist
|
||||
"backup-of-v0.8": 30 * 24 * time.Hour, // these neither
|
||||
"panic-*.log": 7 * 24 * time.Hour, // keep panic logs for a week
|
||||
"audit-*.log": 7 * 24 * time.Hour, // keep audit logs for a week
|
||||
"index": 14 * 24 * time.Hour, // keep old index format for two weeks
|
||||
"index*.converted": 14 * 24 * time.Hour, // keep old converted indexes for two weeks
|
||||
"config.xml.v*": 30 * 24 * time.Hour, // old config versions for a month
|
||||
"*.idx.gz": 30 * 24 * time.Hour, // these should for sure no longer exist
|
||||
"backup-of-v0.8": 30 * 24 * time.Hour, // these neither
|
||||
}
|
||||
|
||||
for pat, dur := range patterns {
|
||||
|
||||
@@ -175,3 +175,29 @@ func TestShortIDCheck(t *testing.T) {
|
||||
t.Error("Should have gotten an error")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAllowedVersions(t *testing.T) {
|
||||
testcases := []struct {
|
||||
ver string
|
||||
allowed bool
|
||||
}{
|
||||
{"v0.13.0", true},
|
||||
{"v0.12.11+22-gabcdef0", true},
|
||||
{"v0.13.0-beta0", true},
|
||||
{"v0.13.0-beta47", true},
|
||||
{"v0.13.0-beta47+1-gabcdef0", true},
|
||||
{"v0.13.0-beta.0", true},
|
||||
{"v0.13.0-beta.47", true},
|
||||
{"v0.13.0-beta.0+1-gabcdef0", true},
|
||||
{"v0.13.0-beta.47+1-gabcdef0", true},
|
||||
{"v0.13.0-some-weird-but-allowed-tag", true},
|
||||
{"v0.13.0-allowed.to.do.this", true},
|
||||
{"v0.13.0+not.allowed.to.do.this", false},
|
||||
}
|
||||
|
||||
for i, c := range testcases {
|
||||
if allowed := allowedVersionExp.MatchString(c.ver); allowed != c.allowed {
|
||||
t.Errorf("%d: incorrect result %v != %v for %q", i, allowed, c.allowed, c.ver)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
type mockedConfig struct{}
|
||||
|
||||
func (c *mockedConfig) GUI() config.GUIConfiguration {
|
||||
return config.GUIConfiguration{}
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Raw() config.Configuration {
|
||||
return config.Configuration{}
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Options() config.OptionsConfiguration {
|
||||
return config.OptionsConfiguration{}
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Replace(cfg config.Configuration) config.CommitResponse {
|
||||
return config.CommitResponse{}
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Subscribe(cm config.Committer) {}
|
||||
|
||||
func (c *mockedConfig) Folders() map[string]config.FolderConfiguration {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Devices() map[protocol.DeviceID]config.DeviceConfiguration {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Save() error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/discover"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
type mockedCachingMux struct{}
|
||||
|
||||
// from suture.Service
|
||||
|
||||
func (m *mockedCachingMux) Serve() {
|
||||
select {}
|
||||
}
|
||||
|
||||
func (m *mockedCachingMux) Stop() {
|
||||
}
|
||||
|
||||
// from events.Finder
|
||||
|
||||
func (m *mockedCachingMux) Lookup(deviceID protocol.DeviceID) (direct []string, relays []discover.Relay, err error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (m *mockedCachingMux) Error() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedCachingMux) String() string {
|
||||
return "mockedCachingMux"
|
||||
}
|
||||
|
||||
func (m *mockedCachingMux) Cache() map[protocol.DeviceID]discover.CacheEntry {
|
||||
return nil
|
||||
}
|
||||
|
||||
// from events.CachingMux
|
||||
|
||||
func (m *mockedCachingMux) Add(finder discover.Finder, cacheTime, negCacheTime time.Duration, priority int) {
|
||||
}
|
||||
|
||||
func (m *mockedCachingMux) ChildErrors() map[string]error {
|
||||
return nil
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/syncthing/syncthing/lib/events"
|
||||
|
||||
type mockedEventSub struct{}
|
||||
|
||||
func (s *mockedEventSub) Since(id int, into []events.Event) []events.Event {
|
||||
select {}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
)
|
||||
|
||||
type mockedLoggerRecorder struct{}
|
||||
|
||||
func (r *mockedLoggerRecorder) Since(t time.Time) []logger.Line {
|
||||
return []logger.Line{
|
||||
{
|
||||
When: time.Now(),
|
||||
Message: "Test message",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func (r *mockedLoggerRecorder) Clear() {}
|
||||
@@ -0,0 +1,115 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/stats"
|
||||
)
|
||||
|
||||
type mockedModel struct{}
|
||||
|
||||
func (m *mockedModel) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) Completion(device protocol.DeviceID, folder string) float64 {
|
||||
return 0
|
||||
}
|
||||
|
||||
func (m *mockedModel) Override(folder string) {}
|
||||
|
||||
func (m *mockedModel) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated, int) {
|
||||
return nil, nil, nil, 0
|
||||
}
|
||||
|
||||
func (m *mockedModel) NeedSize(folder string) (nfiles int, bytes int64) {
|
||||
return 0, 0
|
||||
}
|
||||
|
||||
func (m *mockedModel) ConnectionStats() map[string]interface{} {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) DeviceStatistics() map[string]stats.DeviceStatistics {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) FolderStatistics() map[string]stats.FolderStatistics {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool) {
|
||||
return protocol.FileInfo{}, false
|
||||
}
|
||||
|
||||
func (m *mockedModel) CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool) {
|
||||
return protocol.FileInfo{}, false
|
||||
}
|
||||
|
||||
func (m *mockedModel) ResetFolder(folder string) {
|
||||
}
|
||||
|
||||
func (m *mockedModel) Availability(folder, file string) []protocol.DeviceID {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) GetIgnores(folder string) ([]string, []string, error) {
|
||||
return nil, nil, nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) SetIgnores(folder string, content []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) PauseDevice(device protocol.DeviceID) {
|
||||
}
|
||||
|
||||
func (m *mockedModel) ResumeDevice(device protocol.DeviceID) {}
|
||||
|
||||
func (m *mockedModel) DelayScan(folder string, next time.Duration) {}
|
||||
|
||||
func (m *mockedModel) ScanFolder(folder string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) ScanFolders() map[string]error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) ScanFolderSubs(folder string, subs []string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) BringToFront(folder, file string) {}
|
||||
|
||||
func (m *mockedModel) ConnectedTo(deviceID protocol.DeviceID) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (m *mockedModel) GlobalSize(folder string) (nfiles, deleted int, bytes int64) {
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
func (m *mockedModel) LocalSize(folder string) (nfiles, deleted int, bytes int64) {
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
func (m *mockedModel) CurrentLocalVersion(folder string) (int64, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (m *mockedModel) RemoteLocalVersion(folder string) (int64, bool) {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
func (m *mockedModel) State(folder string) (string, time.Time, error) {
|
||||
return "", time.Time{}, nil
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"time"
|
||||
)
|
||||
|
||||
type mockedRelayService struct{}
|
||||
|
||||
// from suture.Service
|
||||
|
||||
func (s *mockedRelayService) Serve() {
|
||||
select {}
|
||||
}
|
||||
|
||||
func (s *mockedRelayService) Stop() {
|
||||
}
|
||||
|
||||
// from relay.Service
|
||||
|
||||
func (s *mockedRelayService) Accept() *tls.Conn {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockedRelayService) Relays() []string {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *mockedRelayService) RelayStatus(uri string) (time.Duration, bool) {
|
||||
return 0, false
|
||||
}
|
||||
@@ -9,9 +9,7 @@ package main
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
@@ -21,8 +19,8 @@ import (
|
||||
type folderSummaryService struct {
|
||||
*suture.Supervisor
|
||||
|
||||
cfg *config.Wrapper
|
||||
model *model.Model
|
||||
cfg configIntf
|
||||
model modelIntf
|
||||
stop chan struct{}
|
||||
immediate chan string
|
||||
|
||||
@@ -35,7 +33,7 @@ type folderSummaryService struct {
|
||||
lastEventReqMut sync.Mutex
|
||||
}
|
||||
|
||||
func newFolderSummaryService(cfg *config.Wrapper, m *model.Model) *folderSummaryService {
|
||||
func newFolderSummaryService(cfg configIntf, m modelIntf) *folderSummaryService {
|
||||
service := &folderSummaryService{
|
||||
Supervisor: suture.NewSimple("folderSummaryService"),
|
||||
cfg: cfg,
|
||||
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
overridden-default
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
overridden-default
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
overridden-default
|
||||
Vendored
+1
@@ -0,0 +1 @@
|
||||
overridden-foo
|
||||
@@ -79,7 +79,7 @@ func (m *usageReportingManager) String() string {
|
||||
|
||||
// reportData returns the data to be sent in a usage report. It's used in
|
||||
// various places, so not part of the usageReportingManager object.
|
||||
func reportData(cfg *config.Wrapper, m *model.Model) map[string]interface{} {
|
||||
func reportData(cfg configIntf, m modelIntf) map[string]interface{} {
|
||||
res := make(map[string]interface{})
|
||||
res["urVersion"] = usageReportVersion
|
||||
res["uniqueID"] = cfg.Options().URUniqueID
|
||||
|
||||
+1
-1
@@ -1,4 +1,4 @@
|
||||
syncthing ({{.version}}); urgency=medium
|
||||
{{.name}} ({{.version}}); urgency=medium
|
||||
|
||||
* Packaging of {{.version}}.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[Unit]
|
||||
Description=Syncthing - Open Source Continuous File Synchronization for %I
|
||||
Documentation=http://docs.syncthing.net/
|
||||
Documentation=man:syncthing(1)
|
||||
After=network.target
|
||||
Wants=syncthing-inotify@.service
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[Unit]
|
||||
Description=Syncthing - Open Source Continuous File Synchronization
|
||||
Documentation=http://docs.syncthing.net/
|
||||
Documentation=man:syncthing(1)
|
||||
After=network.target
|
||||
Wants=syncthing-inotify.service
|
||||
|
||||
|
||||
@@ -1,475 +0,0 @@
|
||||
/*
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
*/
|
||||
|
||||
body {
|
||||
padding-bottom: 70px;
|
||||
font-family: "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
h1, h2, h3, h4, h5 {
|
||||
font-family: "Raleway", "Helvetica Neue", Helvetica, Arial, sans-serif;
|
||||
line-height: 1.25;
|
||||
}
|
||||
|
||||
ul+h5 {
|
||||
margin-top: 1.5em;
|
||||
}
|
||||
|
||||
#content {
|
||||
margin-bottom: 50px;
|
||||
}
|
||||
|
||||
.panel-progress {
|
||||
background: #3498db;
|
||||
height: 3px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
top: 0;
|
||||
display: block;
|
||||
}
|
||||
|
||||
.panel-title {
|
||||
position: relative;
|
||||
text-overflow: ellipsis;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
identicon {
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
line-height: 1;
|
||||
margin-right: 5px;
|
||||
}
|
||||
|
||||
.identicon {
|
||||
width: 1em;
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.identicon rect {
|
||||
fill: #333;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-top: 0px;
|
||||
}
|
||||
|
||||
.checkbox input[type="checkbox"], .radio input[type="radio"] {
|
||||
float: none; /* issue #1197 */
|
||||
}
|
||||
|
||||
.popover {
|
||||
max-width: none;
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.panel-heading .fa, .modal-header .fa {
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
.panel-heading {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.text-monospace {
|
||||
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
|
||||
}
|
||||
|
||||
.table-condensed>thead>tr>th, .table-condensed>tbody>tr>th, .table-condensed>tfoot>tr>th, .table-condensed>thead>tr>td, .table-condensed>tbody>tr>td, .table-condensed>tfoot>tr>td {
|
||||
border-top: none;
|
||||
}
|
||||
|
||||
.logo {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
top: -5px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.list-no-bullet {
|
||||
list-style-type: none
|
||||
}
|
||||
|
||||
.li-column {
|
||||
display: inline-block;
|
||||
min-width: 7em;
|
||||
margin-right: 1em;
|
||||
background-color: rgb(236, 240, 241);
|
||||
border-radius: 3px;
|
||||
padding: 1px 4px;
|
||||
margin: 2px 2px;
|
||||
}
|
||||
.li-column span.data {
|
||||
margin-left: 0.5em;
|
||||
min-width: 10em;
|
||||
text-align: right;
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.ng-cloak {
|
||||
display: none !important;
|
||||
}
|
||||
|
||||
.table th {
|
||||
white-space: nowrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
.table td {
|
||||
padding-left: 20px !important;
|
||||
}
|
||||
|
||||
.table td.small-data {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
table.table-condensed {
|
||||
table-layout: fixed;
|
||||
}
|
||||
table.table-condensed td {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width:767px) {
|
||||
table.table-condensed td {
|
||||
/* for mobile phones to allow linebreaks in long repro folder/shared with
|
||||
* columns. */
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
.navbar-right {
|
||||
/* to align with panel */
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu for select language
|
||||
*/
|
||||
|
||||
@media (min-width:480px) and (max-width:649px) {
|
||||
*[language-select] > .dropdown-menu {
|
||||
width: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:650px) {
|
||||
*[language-select] > .dropdown-menu > li {
|
||||
width: 50%;
|
||||
float: left;
|
||||
}
|
||||
*[language-select] > .dropdown-menu {
|
||||
width: 440px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width:479px) {
|
||||
.dropdown-menu {
|
||||
padding-top: 55px;
|
||||
}
|
||||
|
||||
nav .dropdown-toggle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
padding-left: 0;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.navbar-nav .open .dropdown-menu > li > a {
|
||||
padding: 12px 15px 12px 25px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.panel-body .table-condensed {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
|
||||
.dl-horizontal.dl-narrow dt {
|
||||
width: 40px;
|
||||
}
|
||||
|
||||
.dl-horizontal.dl-narrow dd {
|
||||
margin-left: 60px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Progress bars with centered text
|
||||
*/
|
||||
|
||||
.progress {
|
||||
margin-bottom: 0px;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.progress span.frontal {
|
||||
text-align: center;
|
||||
position: absolute;
|
||||
display: block;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.three-columns {
|
||||
-webkit-column-count: 3;
|
||||
-moz-column-count: 3;
|
||||
column-count: 3;
|
||||
}
|
||||
|
||||
.two-columns {
|
||||
-webkit-column-count: 2;
|
||||
-moz-column-count: 2;
|
||||
column-count: 2;
|
||||
}
|
||||
|
||||
ul.three-columns li, ul.two-columns li {
|
||||
padding-left: 0.5em;
|
||||
text-indent: -0.5em;
|
||||
}
|
||||
|
||||
/** Footer nav on small devices **/
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
body {
|
||||
padding-bottom: 0;
|
||||
}
|
||||
|
||||
.navbar-fixed-bottom {
|
||||
position: static;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
|
||||
Dark theme customizations start here.
|
||||
|
||||
Author: alessandro.g89
|
||||
Source: https://userstyles.org/styles/122502/syncthing-dark
|
||||
|
||||
**/
|
||||
|
||||
body {
|
||||
color: #aaa !important;
|
||||
background-color: black !important;
|
||||
}
|
||||
|
||||
a:hover,a:focus,a.focus{
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* navbar */
|
||||
.navbar {
|
||||
background-color: #333 !important;
|
||||
border-color: #333 !important;
|
||||
border-width: 2px !important;
|
||||
}
|
||||
|
||||
.navbar-text, .dropdown>a, .dropdown-menu>li>a, .hidden-xs>a, .navbar-link {
|
||||
color: #aaa !important;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
border-color: #333 !important;
|
||||
border-width: 2px !important;
|
||||
background-color: #222 !important;
|
||||
}
|
||||
|
||||
.dropdown-menu>li>a:hover, .dropdown-menu>li>a:focus {
|
||||
color: #fff !important;
|
||||
background-color: #333 !important;
|
||||
}
|
||||
|
||||
.open>.dropdown-toggle, .dropdown-toggle:hover {
|
||||
border-color: #333 !important;
|
||||
background-color: #222 !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
background-color: #333 !important;
|
||||
height: 2px !important;
|
||||
}
|
||||
|
||||
li.hidden-xs:hover, .navbar-link:hover, .navbar-link:focus {
|
||||
outline: none !important;
|
||||
border-color: #333 !important;
|
||||
background-color: #222 !important;
|
||||
}
|
||||
|
||||
.dropdown-menu>.active>a {
|
||||
color: #fff !important;
|
||||
background-color: #217dbb !important;
|
||||
}
|
||||
|
||||
|
||||
/* main panel */
|
||||
.panel {
|
||||
background-color: #111 !important;
|
||||
border-width: 2px !important;
|
||||
}
|
||||
|
||||
.panel-default {
|
||||
border-color: #222 !important;
|
||||
}
|
||||
|
||||
.panel-default>.panel-heading {
|
||||
color: #aaa !important;
|
||||
border-color: #222 !important;
|
||||
background-color: #222 !important;
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
background-color: #111 !important;
|
||||
border-width: 0 !important;
|
||||
}
|
||||
|
||||
.table-striped>tbody>tr:nth-of-type(odd) {
|
||||
background-color: #181818 !important;
|
||||
}
|
||||
|
||||
.panel-group .panel-heading+.panel-collapse>.panel-body, .panel-group .panel-heading+.panel-collapse>.list-group {
|
||||
border-top: 1px solid #222 !important;
|
||||
}
|
||||
|
||||
.identicon>rect {
|
||||
fill: #aaa !important;
|
||||
}
|
||||
|
||||
/* buttons */
|
||||
.btn {
|
||||
border-radius: 3px !important;
|
||||
border-width: 0px !important;
|
||||
}
|
||||
|
||||
.btn:hover, .btn:focus, .btn.focus {
|
||||
outline: none !important;
|
||||
}
|
||||
.btn-default {
|
||||
color: #aaa !important;
|
||||
background-color: #333 !important;
|
||||
}
|
||||
|
||||
.btn-default:hover, .btn-default:focus, .btn-default.focus {
|
||||
color: #fff !important;
|
||||
background-color: #484848 !important;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #217dbb !important;
|
||||
}
|
||||
|
||||
.btn-primary:hover, .btn-primary:focus, .btn-primary.focus {
|
||||
background-color: #3498db !important;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: #c29d0b !important;
|
||||
}
|
||||
|
||||
.btn-warning:hover, .btn-warning:focus, .btn-warning.focus {
|
||||
background-color: #f1c40f !important;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #d62c1a !important;
|
||||
}
|
||||
|
||||
.btn-danger:hover, .btn-danger:focus, .btn-danger.focus {
|
||||
background-color: #e74c3c !important;
|
||||
}
|
||||
|
||||
|
||||
/* modal dialogs */
|
||||
.modal-header {
|
||||
border-color: #222 !important;
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-color: #666 !important;
|
||||
border-width: 2px !important;
|
||||
background-color: #111 !important;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-color: #111 !important;
|
||||
background-color: #111 !important;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #c29d0b !important;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: #d62c1a !important;
|
||||
}
|
||||
|
||||
.help-block {
|
||||
color: #aaa !important;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
color: #aaa !important;
|
||||
border-color: #444 !important;
|
||||
background-color: black !important;
|
||||
}
|
||||
|
||||
code.ng-binding{
|
||||
color: #f99 !important;
|
||||
background-color: #444 !important;
|
||||
}
|
||||
|
||||
.well, .form-control[readonly="readonly"] { /* read-only fields*/
|
||||
color: #666 !important;
|
||||
border-color: #444 !important;
|
||||
background-color: #111 !important;
|
||||
}
|
||||
|
||||
/* buttons for pagination */
|
||||
.pagination>li>a, .pagination>li>span {
|
||||
background-color: #333 !important;
|
||||
border-color: #484848 !important;
|
||||
}
|
||||
|
||||
.pagination>li>a:hover, .pagination>li>a:focus, .pagination>li>a.focus {
|
||||
background-color: #484848 !important;
|
||||
}
|
||||
|
||||
|
||||
/* progess bars */
|
||||
.progress-bar {
|
||||
background-color: #217dbb !important;
|
||||
}
|
||||
|
||||
.progress-bar-success {
|
||||
background-color: #0A8522 !important;
|
||||
}
|
||||
|
||||
.progress-bar-info {
|
||||
background-color: #9b59b6 !important;
|
||||
}
|
||||
|
||||
.progress-bar-warning {
|
||||
background-color: #c29d0b !important;
|
||||
}
|
||||
|
||||
.progress-bar-danger {
|
||||
background-color: #d62c1a !important;
|
||||
}
|
||||
@@ -0,0 +1,217 @@
|
||||
/**
|
||||
|
||||
Dark theme
|
||||
|
||||
Author: alessandro.g89
|
||||
Source: https://userstyles.org/styles/122502/syncthing-dark
|
||||
|
||||
**/
|
||||
|
||||
body {
|
||||
color: #aaa !important;
|
||||
background-color: black !important;
|
||||
}
|
||||
|
||||
a:hover,a:focus,a.focus{
|
||||
outline: none !important;
|
||||
}
|
||||
|
||||
|
||||
/* navbar */
|
||||
.navbar {
|
||||
background-color: #333 !important;
|
||||
border-color: #333 !important;
|
||||
border-width: 2px !important;
|
||||
}
|
||||
|
||||
.navbar-text, .dropdown>a, .dropdown-menu>li>a, .hidden-xs>a, .navbar-link {
|
||||
color: #aaa !important;
|
||||
}
|
||||
|
||||
.dropdown-menu {
|
||||
border-color: #333 !important;
|
||||
border-width: 2px !important;
|
||||
background-color: #222 !important;
|
||||
}
|
||||
|
||||
.dropdown-menu>li>a:hover, .dropdown-menu>li>a:focus {
|
||||
color: #fff !important;
|
||||
background-color: #333 !important;
|
||||
}
|
||||
|
||||
.open>.dropdown-toggle, .dropdown-toggle:hover {
|
||||
border-color: #333 !important;
|
||||
background-color: #222 !important;
|
||||
}
|
||||
|
||||
.divider {
|
||||
background-color: #333 !important;
|
||||
height: 2px !important;
|
||||
}
|
||||
|
||||
li.hidden-xs:hover, .navbar-link:hover, .navbar-link:focus {
|
||||
outline: none !important;
|
||||
border-color: #333 !important;
|
||||
background-color: #222 !important;
|
||||
}
|
||||
|
||||
.dropdown-menu>.active>a {
|
||||
color: #fff !important;
|
||||
background-color: #217dbb !important;
|
||||
}
|
||||
|
||||
|
||||
/* main panel */
|
||||
.panel {
|
||||
background-color: #111 !important;
|
||||
border-width: 2px !important;
|
||||
}
|
||||
|
||||
.panel-default {
|
||||
border-color: #222 !important;
|
||||
}
|
||||
|
||||
.panel-default>.panel-heading {
|
||||
color: #aaa !important;
|
||||
border-color: #222 !important;
|
||||
background-color: #222 !important;
|
||||
}
|
||||
|
||||
.panel-footer {
|
||||
background-color: #111 !important;
|
||||
border-width: 0 !important;
|
||||
}
|
||||
|
||||
.table-striped>tbody>tr:nth-of-type(odd) {
|
||||
background-color: #181818 !important;
|
||||
}
|
||||
|
||||
.panel-group .panel-heading+.panel-collapse>.panel-body, .panel-group .panel-heading+.panel-collapse>.list-group {
|
||||
border-top: 1px solid #222 !important;
|
||||
}
|
||||
|
||||
.identicon>rect {
|
||||
fill: #aaa !important;
|
||||
}
|
||||
|
||||
/* buttons */
|
||||
.btn {
|
||||
border-radius: 3px !important;
|
||||
border-width: 0px !important;
|
||||
}
|
||||
|
||||
.btn:hover, .btn:focus, .btn.focus {
|
||||
outline: none !important;
|
||||
}
|
||||
.btn-default {
|
||||
color: #aaa !important;
|
||||
background-color: #333 !important;
|
||||
}
|
||||
|
||||
.btn-default:hover, .btn-default:focus, .btn-default.focus {
|
||||
color: #fff !important;
|
||||
background-color: #484848 !important;
|
||||
}
|
||||
|
||||
.btn-primary {
|
||||
background-color: #217dbb !important;
|
||||
}
|
||||
|
||||
.btn-primary:hover, .btn-primary:focus, .btn-primary.focus {
|
||||
background-color: #3498db !important;
|
||||
}
|
||||
|
||||
.btn-warning {
|
||||
background-color: #c29d0b !important;
|
||||
}
|
||||
|
||||
.btn-warning:hover, .btn-warning:focus, .btn-warning.focus {
|
||||
background-color: #f1c40f !important;
|
||||
}
|
||||
|
||||
.btn-danger {
|
||||
background-color: #d62c1a !important;
|
||||
}
|
||||
|
||||
.btn-danger:hover, .btn-danger:focus, .btn-danger.focus {
|
||||
background-color: #e74c3c !important;
|
||||
}
|
||||
|
||||
|
||||
/* modal dialogs */
|
||||
.modal-header {
|
||||
border-color: #222 !important;
|
||||
background-color: #222;
|
||||
}
|
||||
|
||||
.modal-content {
|
||||
border-color: #666 !important;
|
||||
border-width: 2px !important;
|
||||
background-color: #111 !important;
|
||||
}
|
||||
|
||||
.modal-footer {
|
||||
border-color: #111 !important;
|
||||
background-color: #111 !important;
|
||||
}
|
||||
|
||||
.alert-warning {
|
||||
background-color: #c29d0b !important;
|
||||
}
|
||||
|
||||
.alert-danger {
|
||||
background-color: #d62c1a !important;
|
||||
}
|
||||
|
||||
.help-block {
|
||||
color: #aaa !important;
|
||||
}
|
||||
|
||||
.form-control {
|
||||
color: #aaa !important;
|
||||
border-color: #444 !important;
|
||||
background-color: black !important;
|
||||
}
|
||||
|
||||
code.ng-binding{
|
||||
color: #f99 !important;
|
||||
background-color: #444 !important;
|
||||
}
|
||||
|
||||
.well, .form-control[readonly="readonly"], .popover { /* read-only fields*/
|
||||
color: #666 !important;
|
||||
border-color: #444 !important;
|
||||
background-color: #111 !important;
|
||||
}
|
||||
|
||||
/* buttons for pagination */
|
||||
.pagination>li>a, .pagination>li>span {
|
||||
background-color: #333 !important;
|
||||
border-color: #484848 !important;
|
||||
}
|
||||
|
||||
.pagination>li>a:hover, .pagination>li>a:focus, .pagination>li>a.focus {
|
||||
background-color: #484848 !important;
|
||||
}
|
||||
|
||||
|
||||
/* progess bars */
|
||||
.progress-bar {
|
||||
background-color: #217dbb !important;
|
||||
}
|
||||
|
||||
.progress-bar-success {
|
||||
background-color: #0A8522 !important;
|
||||
}
|
||||
|
||||
.progress-bar-info {
|
||||
background-color: #9b59b6 !important;
|
||||
}
|
||||
|
||||
.progress-bar-warning {
|
||||
background-color: #c29d0b !important;
|
||||
}
|
||||
|
||||
.progress-bar-danger {
|
||||
background-color: #d62c1a !important;
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
.dev-top-bar{
|
||||
display: none;
|
||||
background-color: yellow;
|
||||
}
|
||||
|
||||
.dev-error .hasCount{
|
||||
background-color: red;
|
||||
}
|
||||
|
||||
.dev-warn .hasCount{
|
||||
background-color: yellow;
|
||||
}
|
||||
@@ -26,7 +26,6 @@ ul+h5 {
|
||||
}
|
||||
|
||||
.panel-progress {
|
||||
background: #3498db;
|
||||
height: 3px;
|
||||
left: 0;
|
||||
position: absolute;
|
||||
@@ -40,7 +39,7 @@ ul+h5 {
|
||||
overflow: hidden;
|
||||
}
|
||||
.panel-title a:hover {
|
||||
text-decoration: none;
|
||||
text-decoration: none;
|
||||
}
|
||||
|
||||
identicon {
|
||||
@@ -57,10 +56,6 @@ identicon {
|
||||
height: 1em;
|
||||
}
|
||||
|
||||
.identicon rect {
|
||||
fill: #333;
|
||||
}
|
||||
|
||||
.checkbox {
|
||||
margin-top: 0px;
|
||||
}
|
||||
@@ -74,6 +69,10 @@ identicon {
|
||||
min-width: 250px;
|
||||
}
|
||||
|
||||
.tooltip {
|
||||
word-wrap:break-word;
|
||||
}
|
||||
|
||||
.panel-heading .fa, .modal-header .fa {
|
||||
margin-right: 10px;
|
||||
}
|
||||
@@ -99,15 +98,13 @@ identicon {
|
||||
}
|
||||
|
||||
.list-no-bullet {
|
||||
list-style-type: none
|
||||
list-style-type: none;
|
||||
}
|
||||
|
||||
.li-column {
|
||||
display: inline-block;
|
||||
min-width: 7em;
|
||||
margin-right: 1em;
|
||||
background-color: rgb(236, 240, 241);
|
||||
border-radius: 3px;
|
||||
padding: 1px 4px;
|
||||
margin: 2px 2px;
|
||||
}
|
||||
@@ -143,64 +140,37 @@ table.table-condensed td {
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
@media (max-width:767px) {
|
||||
table.table-condensed td {
|
||||
/* for mobile phones to allow linebreaks in long repro folder/shared with
|
||||
* columns. */
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
table.table-condensed td.no-overflow-ellipse {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
.folder-advanced {
|
||||
padding: 1rem;
|
||||
margin-bottom: 15px;
|
||||
}
|
||||
|
||||
.folder-advanced-toggle {
|
||||
cursor: pointer;
|
||||
}
|
||||
.folder-advanced-toggle .collapse,
|
||||
.folder-advanced-toggle.collapsed .expand {
|
||||
display: inline-block;
|
||||
}
|
||||
|
||||
.folder-advanced-toggle.collapsed .collapse,
|
||||
.folder-advanced-toggle .expand{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.nav>li{
|
||||
float: left;
|
||||
}
|
||||
.navbar-right {
|
||||
/* to align with panel */
|
||||
padding-right: 15px;
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu for select language
|
||||
*/
|
||||
|
||||
@media (min-width:480px) and (max-width:649px) {
|
||||
*[language-select] > .dropdown-menu {
|
||||
width: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:650px) {
|
||||
*[language-select] > .dropdown-menu > li {
|
||||
width: 50%;
|
||||
float: left;
|
||||
}
|
||||
*[language-select] > .dropdown-menu {
|
||||
width: 440px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@media (max-width:479px) {
|
||||
.dropdown-menu {
|
||||
padding-top: 55px;
|
||||
}
|
||||
|
||||
nav .dropdown-toggle {
|
||||
font-size: 14px;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.navbar-brand {
|
||||
padding-left: 0;
|
||||
padding-top: 16px;
|
||||
}
|
||||
|
||||
.navbar-nav .open .dropdown-menu > li > a {
|
||||
padding: 12px 15px 12px 25px;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
.panel-body .table-condensed {
|
||||
margin-bottom: 0;
|
||||
}
|
||||
@@ -247,7 +217,6 @@ ul.three-columns li, ul.two-columns li {
|
||||
}
|
||||
|
||||
/** Footer nav on small devices **/
|
||||
|
||||
@media (max-width: 1199px) {
|
||||
/* Stay at the end of the page, with space reserved for the footer
|
||||
usually taking up two rows. */
|
||||
@@ -277,4 +246,59 @@ ul.three-columns li, ul.two-columns li {
|
||||
.navbar-fixed-bottom {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.nav>li {
|
||||
float:right;
|
||||
}
|
||||
|
||||
table.table-condensed td {
|
||||
/* for mobile phones to allow linebreaks in long repro folder/shared with
|
||||
* columns. */
|
||||
white-space: normal;
|
||||
}
|
||||
}
|
||||
|
||||
@media (min-width:650px) {
|
||||
*[language-select] > .dropdown-menu > li {
|
||||
width: 50%;
|
||||
float: left;
|
||||
}
|
||||
*[language-select] > .dropdown-menu {
|
||||
width: 440px;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Menu for select language
|
||||
*/
|
||||
@media (min-width:480px) and (max-width:649px) {
|
||||
*[language-select] > .dropdown-menu {
|
||||
width: 230px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width:479px) {
|
||||
.dropdown-menu {
|
||||
padding-top: 55px;
|
||||
}
|
||||
|
||||
nav .dropdown-toggle {
|
||||
font-size: 1em;
|
||||
}
|
||||
|
||||
.dropdown-toggle {
|
||||
float: left;
|
||||
}
|
||||
|
||||
.logo{
|
||||
margin:auto;
|
||||
}
|
||||
|
||||
.navbar-nav .open .dropdown-menu > li > a {
|
||||
padding: 12px 15px 12px 25px;
|
||||
}
|
||||
|
||||
.navbar-fixed-bottom li{
|
||||
width:100%;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
/*
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
*/
|
||||
|
||||
.panel-progress {
|
||||
background: #3498db;
|
||||
}
|
||||
|
||||
.identicon rect {
|
||||
fill: #333;
|
||||
}
|
||||
|
||||
.li-column {
|
||||
background-color: rgb(236, 240, 241);
|
||||
border-radius: 3px;
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
"Add": "Добави",
|
||||
"Add Device": "Добави устройство",
|
||||
"Add Folder": "Добави папка",
|
||||
"Add Remote Device": "Добави отдалечено устройство",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add new folder?": "Добави нова папка?",
|
||||
"Address": "Адрес",
|
||||
"Addresses": "Адреси",
|
||||
"Advanced": "Допълнителни",
|
||||
"Advanced Configuration": "Допълнителни настройки",
|
||||
"Advanced settings": "Допълнителни настройки",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"All Data": "Всички данни",
|
||||
"Allow Anonymous Usage Reporting?": "Разреши анонимно докладване за употребата на програмата?",
|
||||
"Alphabetic": "Азбучен ред",
|
||||
@@ -34,12 +34,12 @@
|
||||
"Connection Error": "Грешка при свързването",
|
||||
"Copied from elsewhere": "Копиране от някъде другаде",
|
||||
"Copied from original": "Копиран от оригинала",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Всички правата запазени © 2014-2016 Сътрудници:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Всички правата запазени © 2015 Сътрудници:",
|
||||
"Danger!": "Опасност!",
|
||||
"Delete": "Изтрий",
|
||||
"Deleted": "Изтрито",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Устройство \"{{name}}\" ({{device}}) на {{address}} желае да се свърже. Добави ново устройство?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "Идентификатор на устройство",
|
||||
"Device Identification": "Идентификатор на устройство",
|
||||
"Device Name": "Име на устройство",
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Промени устройство",
|
||||
"Edit Folder": "Промени папка",
|
||||
"Editing": "Променяне",
|
||||
"Enable NAT traversal": "Разреши NAT traversal",
|
||||
"Enable Relaying": "Разреши препращане",
|
||||
"Enable UPnP": "Включи UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Въведете адреси разделени със запетая (\"tcp://ip:port\", \"tcp://host:port\") или \"dynamic\", за да автоматично откриване на наличните адреси.",
|
||||
@@ -71,7 +70,7 @@
|
||||
"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 ID": "Идентификатор на папката",
|
||||
"Folder Label": "Етикет на папката",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Master": "Главна папка",
|
||||
"Folder Path": "Път до папката",
|
||||
"Folders": "Папки",
|
||||
@@ -117,7 +116,7 @@
|
||||
"OK": "ОК",
|
||||
"Off": "Изключено",
|
||||
"Oldest First": "Първо най-старите",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Допълнително разяснеие за етикета на папката. Може да бъде различно всяко устройство.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Options": "Настройки",
|
||||
"Out of Sync": "Несинхронизирано",
|
||||
"Out of Sync Items": "Несинхронизирани елементи",
|
||||
@@ -139,9 +138,9 @@
|
||||
"Relayed via": "Препратено през",
|
||||
"Relays": "Препращачи",
|
||||
"Release Notes": "Бележки по обновяването",
|
||||
"Remote Devices": "Отделечени устройства",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remove": "Премахни",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Задължителен идентификатор за тази папка. Трябва да бъде един и същ на всички устройства.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "Сканирай повторно",
|
||||
"Rescan All": "Сканирай повторно всички",
|
||||
"Rescan Interval": "Интервал за повторно сканиране",
|
||||
@@ -212,7 +211,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Ограничението на скоростта трябва да бъде положително число (0: неограничено)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Интервала на сканиране трябва да бъде не отрицателно число в секунди.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Ще бъдат спрени и автоматично синхронизирани, когато грешката бъде оправена.",
|
||||
"This Device": "Това устройство",
|
||||
"This Device": "This Device",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Това дава лесен достъп на хакери да разглеждат и променят всякакви файлове на компютъра Ви.",
|
||||
"This is a major version upgrade.": "Това е нова основна версия.",
|
||||
"Trash Can File Versioning": "Само на файловете в кошчето",
|
||||
@@ -230,7 +229,7 @@
|
||||
"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.": "Версиите биват изтривани автоматично, когато са по-стари от максималната възраст или надминават броя файлове разрешени в даден интервал.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Внимание, това е вътрешна папка на вече съществуваща папка \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"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.": "Когато добавяш нов идентификатор на папка помни, че той се използва за свързване на папките на различни устройства. Главни/малки букви са от значение и трябва да са еднакви на всички устройства.",
|
||||
"Yes": "Да",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "пълна документация",
|
||||
"items": "елемента",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} желае да сподели папка \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} желае е да сподели папка \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} желае да сподели папка \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Modificar dispositiu",
|
||||
"Edit Folder": "Modificar carpeta",
|
||||
"Editing": "Modificant",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable UPnP": "Habilitat UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introdueix adreces separades per comes (\"tcp://ip:port\", \"tcp://host:port\") o \"dinàmic\" per realitzar descobriments automàtics de l'adreça.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "documentació sencera",
|
||||
"items": "Elements",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartir la carpeta \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -1,5 +1,5 @@
|
||||
{
|
||||
"A device with that ID is already added.": "Un dispositiu amb eixa ID ja s'ha afegit.",
|
||||
"A device with that ID is already added.": "A device with that ID is already added.",
|
||||
"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",
|
||||
@@ -8,13 +8,13 @@
|
||||
"Add": "Afegir",
|
||||
"Add Device": "Afegir dispositiu",
|
||||
"Add Folder": "Afegir carpeta",
|
||||
"Add Remote Device": "Afegir Dispositiu Remot.",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add new folder?": "Afegir nova carpeta?",
|
||||
"Address": "Direcció",
|
||||
"Addresses": "Direccions",
|
||||
"Advanced": "Avançat",
|
||||
"Advanced Configuration": "Configuració avançada",
|
||||
"Advanced settings": "Ajustos avançats.",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"All Data": "Totes les dades",
|
||||
"Allow Anonymous Usage Reporting?": "Permetre informes d'ús anònim?",
|
||||
"Alphabetic": "Alfabètic",
|
||||
@@ -34,12 +34,12 @@
|
||||
"Connection Error": "Error de connexió",
|
||||
"Copied from elsewhere": "Copiat de qualsevol lloc",
|
||||
"Copied from original": "Copiat de l'original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 els següents Col·laboradors:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 els següents Col·laboradors:",
|
||||
"Danger!": "Perill!",
|
||||
"Delete": "Esborrar",
|
||||
"Deleted": "Esborrat",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Dispositiu \"{{name}}\" ({{device}} a l'adreça {{address}}) vol connectar. Afegir nou dispositiu?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "ID del dispositiu",
|
||||
"Device Identification": "Identificació del dispositiu",
|
||||
"Device Name": "Nom del dispositiu",
|
||||
@@ -55,8 +55,7 @@
|
||||
"Edit Device": "Editar dispositiu",
|
||||
"Edit Folder": "Editar carpeta",
|
||||
"Editing": "Editant",
|
||||
"Enable NAT traversal": "Permetre NAT transversal",
|
||||
"Enable Relaying": "Permetre Transmissions",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable UPnP": "Activar UPnp",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introdueix adreces separades per coma (\"tcp://ip:port\", \"tcp://host:port\") o \"dynamic\" per a realitzar el descobriment automàtic de l'adreça.",
|
||||
"Enter ignore patterns, one per line.": "Introduïr patrons a ignorar, un per línia.",
|
||||
@@ -71,7 +70,7 @@
|
||||
"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": "Carpeta",
|
||||
"Folder ID": "ID de carpeta",
|
||||
"Folder Label": "Etiqueta de la Carpeta",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Master": "Carpeta principal",
|
||||
"Folder Path": "Ruta de la carpeta",
|
||||
"Folders": "Carpetes",
|
||||
@@ -81,8 +80,8 @@
|
||||
"GUI Listen Addresses": "Direcció d'escolta de l'Interfície Gràfica d'Usuari (GUI)",
|
||||
"Generate": "Generar",
|
||||
"Global Discovery": "Descobriment global",
|
||||
"Global Discovery Server": "Servidor de Descobriment Global",
|
||||
"Global Discovery Servers": "Servidors de Descobriment Global",
|
||||
"Global Discovery Server": "Servidor de descobriment global",
|
||||
"Global Discovery Servers": "Global Discovery Servers",
|
||||
"Global State": "Estat global",
|
||||
"Help": "Ajuda",
|
||||
"Home page": "Pàgina inicial",
|
||||
@@ -117,7 +116,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "Off",
|
||||
"Oldest First": "El més vell primer",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Etiqueta descriptiva opcional per la carpeta. Pot ser diferent en cada dispositiu.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Options": "Opcions",
|
||||
"Out of Sync": "Sense sincronització",
|
||||
"Out of Sync Items": "Dispositius sense sincronitzar",
|
||||
@@ -135,13 +134,13 @@
|
||||
"Quick guide to supported patterns": "Guía ràpida de patrons suportats",
|
||||
"RAM Utilization": "Utilització de la RAM",
|
||||
"Random": "Aleatori",
|
||||
"Relay Servers": "Servidors de Transmissió",
|
||||
"Relay Servers": "Relay Servers",
|
||||
"Relayed via": "Transmitit via",
|
||||
"Relays": "Transmissions",
|
||||
"Release Notes": "Notes de la versió",
|
||||
"Remote Devices": "Dispositius Remots",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remove": "Eliminar",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Identificador necessari per la carpeta. Deu ser el mateix en tots els dispositius del cluster.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "Tornar a buscar",
|
||||
"Rescan All": "Tornar a buscar tot",
|
||||
"Rescan Interval": "Interval de nova busca",
|
||||
@@ -164,7 +163,7 @@
|
||||
"Shared With": "Compartit amb",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Identificador curt per a la carpeta. Deu ser el mateix en tots els dispositius del grup (cluster).",
|
||||
"Show ID": "Mostrar ID",
|
||||
"Show QR": "Mostrar QR",
|
||||
"Show QR": "Show QR",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Mostrat en lloc de l'ID del dispositiu en l'estat del grup (cluster). S'anunciarà als altres dispositius com el nom opcional per defecte.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Mostrat en lloc de l'ID del dispositiu en l'estat del grup (cluster). S'actualitzarà al nom que el dispositiu anuncia si es deixa buit.",
|
||||
"Shutdown": "Apagar",
|
||||
@@ -190,7 +189,7 @@
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Les estadístiques agregades estan disponibles públicament en {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració ha sigut gravada però no activada. Syncthing deu reiniciar per tal d'activar la nova configuració.",
|
||||
"The device ID cannot be blank.": "L'ID del dispositiu no pot estar buida.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "L'ID del dispositiu que hi ha que introduïr ací es pot trobar en el menú \"Accions > Mostrar ID\" en l'altre dispositiu. Els espais i les barres son opcionals (ignorats).",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).",
|
||||
"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).": "L'ID del dispositiu que hi ha que introduïr ací es pot trobar en el menú \"Editar > Mostrar ID\" en l'altre dispositiu. Els espais i les barres son opcionals (ignorats).",
|
||||
"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.": "L'informe encriptat d'ús s'envia diariament. S'utilitza per a rastrejar plataformes comuns, tamanys de carpetes i versions de l'aplicació. Si el conjunt de dades enviat a l'informe es canvia, se li demanarà a vosté l'autorització altra vegada.\n",
|
||||
"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.": "L'ID del dispositiu introduïda no pareix vàlida. Deuria ser una cadena de 52 o 56 caracters consistents en lletres i nombre, amb espais i barres opcionals.",
|
||||
@@ -212,7 +211,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "El llímit del ritme deu ser un nombre no negatiu (0: sense llímit)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "L'interval de reescaneig deu ser un nombre positiu de segons.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Es reintenta automàticament i es sincronitzaràn quant el resolga l'error.",
|
||||
"This Device": "Aquest Dispositiu",
|
||||
"This Device": "This Device",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Açò pot donar accés fàcilment als hackers per a llegir i canviar qualsevol fitxer al teu ordinador.",
|
||||
"This is a major version upgrade.": "Aquesta és una actualització important de la versió.",
|
||||
"Trash Can File Versioning": "Versionat d'arxius de la paperera",
|
||||
@@ -230,7 +229,7 @@
|
||||
"Version": "Versió",
|
||||
"Versions Path": "Ruta de les versions",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Les versions s'esborren automàticament si són més antigues que l'edat màxima o excedixen el nombre de fitxer permesos en un interval.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Perill! Aquesta ruta és un subdirectori d'una carpeta que ja existeix nomenada \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quant s'afig un nou dispositiu, hi ha que tindre en compte que aquest dispositiu deu ser afegit també en l'altre costat.",
|
||||
"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.": "Quant s'afig una nova carpeta, hi ha que tindre en compte que l'ID de la carpeta s'utilitza per a juntar les carpetes entre dispositius. Són sensibles a les majúscules i deuen coincidir exactament entre tots els dispositius.",
|
||||
"Yes": "Sí",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "Documentació completa",
|
||||
"items": "Elements",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartit la carpeta \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vol compartir la carpeta \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vol compartir la carpeta \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
"Add": "Přidat",
|
||||
"Add Device": "Přidat přístroj",
|
||||
"Add Folder": "Přidat adresář",
|
||||
"Add Remote Device": "Přidat vzdálené zařízení",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add new folder?": "Přidat nový adresář?",
|
||||
"Address": "Adresa",
|
||||
"Addresses": "Adresy",
|
||||
"Advanced": "Pokročilé",
|
||||
"Advanced Configuration": "Pokročilá nastavení",
|
||||
"Advanced settings": "Pokročilá nastavení",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"All Data": "Všechna data",
|
||||
"Allow Anonymous Usage Reporting?": "Povolit anonymní hlášení o používání?",
|
||||
"Alphabetic": "Abecedně",
|
||||
@@ -34,12 +34,12 @@
|
||||
"Connection Error": "Chyba připojení",
|
||||
"Copied from elsewhere": "Zkopírováno odjinud",
|
||||
"Copied from original": "Zkopírováno z originálu",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 následující přispěvatelé:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 následující přispěvatelé:",
|
||||
"Danger!": "Pozor!",
|
||||
"Delete": "Smazat",
|
||||
"Deleted": "Smazáno",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Zařízení \"{{name}}\" ({{device}} na {{address}}) se chce připojit. Přidat nové zařízení?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "ID přístroje",
|
||||
"Device Identification": "Identifikace přístroje",
|
||||
"Device Name": "Jméno přístroje",
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Upravit přístroj",
|
||||
"Edit Folder": "Upravit adresář",
|
||||
"Editing": "Upravuje se",
|
||||
"Enable NAT traversal": "Povolit NAT přenos",
|
||||
"Enable Relaying": "Povolit přenašeče",
|
||||
"Enable UPnP": "Povolit UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Zadejte adresy oddělené čárkou (\"tcp://ip:port\", \"tcp://host:port\") nebo \"dynamic\" pro automatické zjišťování adres.",
|
||||
@@ -71,7 +70,7 @@
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Soubory jsou chráněny před změnami na ostatních přístrojích, ale změny provedené z tohoto přístroje budou rozeslány na zbytek clusteru.",
|
||||
"Folder": "Adresář",
|
||||
"Folder ID": "ID adresáře",
|
||||
"Folder Label": "Jmenovka adresáře",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Master": "Master adresář",
|
||||
"Folder Path": "Cesta k adresáři",
|
||||
"Folders": "Adresáře",
|
||||
@@ -117,7 +116,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "Vypnuta",
|
||||
"Oldest First": "Od nejstaršího",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Volitelný popisek adresáře. Může být rozdílný na každém zařízení.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Options": "Nastavení",
|
||||
"Out of Sync": "Nesesynchronizováno",
|
||||
"Out of Sync Items": "Nesesynchronizované položky",
|
||||
@@ -139,9 +138,9 @@
|
||||
"Relayed via": "Přenášené přes",
|
||||
"Relays": "Přenašeče",
|
||||
"Release Notes": "Poznámky k vydání",
|
||||
"Remote Devices": "Vzdálená zařízení",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remove": "Odstranit",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Požadovaný identifikátor adresáře. Musí být stejný na všech zařízeních.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "Opakovat skenování",
|
||||
"Rescan All": "Opakovat skenování všech",
|
||||
"Rescan Interval": "Interval opakování skenování",
|
||||
@@ -212,7 +211,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Limit rychlosti musí být nezáporné číslo (0: bez limitu)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Interval opakování skenování musí být pozitivní číslo.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Nové pokusy o synchronizaci budou probíhat automaticky a položky budou synchronizovány jakmile bude chyba odstraněna.",
|
||||
"This Device": "Toto zařízení",
|
||||
"This Device": "This Device",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "To může útočníkům jednoduše povolit čtení a úpravy souborů na vašem přístroji. ",
|
||||
"This is a major version upgrade.": "Toto je důležitá aktualizace.",
|
||||
"Trash Can File Versioning": "Verzování souborů v koši",
|
||||
@@ -230,7 +229,7 @@
|
||||
"Version": "Verze",
|
||||
"Versions Path": "Cesta k verzím",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Verze jsou automaticky smazány, pokud jsou starší než maximální časový limit nebo překročí počet souborů povolených pro interval.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varování: tato cesta je podadresářem existujícího adresáře \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Při přidávání nového přístroje mějte na paměti, že je ho třeba také zadat na druhé straně.",
|
||||
"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.": "Při přidávání nového adresáře mějte na paměti, že jeho ID je použito ke svázání adresářů napříč přístoji. Rozlišují se malá a velká písmena a musí přesně souhlasit mezi všemi přístroji.",
|
||||
"Yes": "Ano",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "plná dokumentace",
|
||||
"items": "položky",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce sdílet adresář \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} chce sdílet adresář \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} chce sdílet adresář \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Rediger enhed",
|
||||
"Edit Folder": "Rediger mappe",
|
||||
"Editing": "Redigerer",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable UPnP": "Anvend UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Angiv kommaseparerede adresser (\"tcp://ip:port\", \"tcp://host:port\") eller \"dynamic\" for at benytte automatisk opdagelse af adressen.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "Fuld dokumentation",
|
||||
"items": "poster",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker at dele mappen \"{{folder}}\". ",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -39,7 +39,7 @@
|
||||
"Danger!": "Achtung!",
|
||||
"Delete": "Löschen",
|
||||
"Deleted": "Gelöscht",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Gerät \"{{name}}\" ({{device}} {{address}}) möchte sich verbinden. Gerät hinzufügen?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "Geräte ID",
|
||||
"Device Identification": "Geräte Identifikation",
|
||||
"Device Name": "Gerätename",
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Gerät bearbeiten",
|
||||
"Edit Folder": "Verzeichnis bearbeiten",
|
||||
"Editing": "Bearbeitet",
|
||||
"Enable NAT traversal": "NAT-Traversal aktivieren",
|
||||
"Enable Relaying": "Weiterleitung aktivieren",
|
||||
"Enable UPnP": "UPnP aktivieren",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Kommagetrennte Adressen (\"tcp://ip:port\", \"tcp://host:port\") oder \"dynamic\" eingeben, um die Adresse automatisch zu ermitteln.",
|
||||
@@ -66,7 +65,7 @@
|
||||
"File Pull Order": "Dateiübertragungsreihenfolge",
|
||||
"File Versioning": "Dateiversionierung",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Dateizugriffsrechte beim Suchen nach Veränderungen ignorieren. Bei FAT-Dateisystemen zu verwenden.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Wenn Syncthing Dateien ersetzt oder löscht, werden sie in das .stversions Verzeichnis verschoben.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Wenn Dateien von Syncthing ersetzt oder gelöscht werden sollen, werden sie vorher in den .stversions Ordner verschoben.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dateien werden, bevor Syncthing sie löscht oder ersetzt, datiert in das Verzeichnis .stversions verschoben.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dateien sind auf diesem Gerät schreibgeschützt. Auf diesem Gerät durchgeführte Veränderungen werden aber auf den Rest des Verbunds übertragen.",
|
||||
"Folder": "Verzeichnis",
|
||||
@@ -123,7 +122,7 @@
|
||||
"Out of Sync Items": "Nicht synchronisierte Objekte",
|
||||
"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 Gerät. Verzeichnis wird erzeugt, wenn es nicht existiert. Das Tilden-Zeichen (~) kann als Abkürzung benutzt werden für",
|
||||
"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 Gerät. Ordner werden erzeugt, wenn sie nicht existieren. 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 alte Dateiversionen gespeichert werden sollen (ohne Angabe wird das Verzeichnis .stversions im Verzeichnis verwendet).",
|
||||
"Pause": "Pause",
|
||||
"Paused": "Pausiert",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "Komplette Dokumentation",
|
||||
"items": "Objekte",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} möchte das Verzeichnis \"{{folder}}\" teilen.",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} möchte das Verzeichnis \"{{folderLabel}}\" ({{folder}}) teilen.",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} möchte das Verzeichnis \"{{folderLabel}}\" ({{folder}}) teilen."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} möchte das Verzeichnis \"{{folderLabel}}\" ({{folder}}) teilen."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Επεξεργασία συσκευής",
|
||||
"Edit Folder": "Επεξεργασία φακέλου",
|
||||
"Editing": "Επεξεργασία σε εξέλιξη",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Ενεργοποίηση αναμετάδοσης",
|
||||
"Enable UPnP": "Ενεργοποίηση UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "πλήρης τεκμηρίωση",
|
||||
"items": "εγγραφές",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "Η συσκευή {{device}} θέλει να μοιράσει τον φάκελο «{{folder}}».",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Edit Device",
|
||||
"Edit Folder": "Edit Folder",
|
||||
"Editing": "Editing",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable UPnP": "Enable UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "full documentation",
|
||||
"items": "items",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -39,6 +39,7 @@
|
||||
"Danger!": "Danger!",
|
||||
"Delete": "Delete",
|
||||
"Deleted": "Deleted",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "Device ID",
|
||||
"Device Identification": "Device Identification",
|
||||
"Device Name": "Device Name",
|
||||
@@ -228,6 +229,7 @@
|
||||
"Version": "Version",
|
||||
"Versions Path": "Versions Path",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "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.": "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.",
|
||||
"Yes": "Yes",
|
||||
|
||||
@@ -8,13 +8,13 @@
|
||||
"Add": "Agregar",
|
||||
"Add Device": "Agregar Dispositivo",
|
||||
"Add Folder": "Agregar Carpeta",
|
||||
"Add Remote Device": "Añadir Dispositivo Remoto",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add new folder?": "¿Agregar una carpeta nueva?",
|
||||
"Address": "Dirección",
|
||||
"Addresses": "Direcciones",
|
||||
"Advanced": "Avanzado",
|
||||
"Advanced Configuration": "Configuración Avanzada",
|
||||
"Advanced settings": "Ajustes avanzados",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"All Data": "Todos los datos",
|
||||
"Allow Anonymous Usage Reporting?": "¿Deseas permitir el envío anónimo de informes de uso?",
|
||||
"Alphabetic": "Alfabético",
|
||||
@@ -34,12 +34,12 @@
|
||||
"Connection Error": "Error de conexión",
|
||||
"Copied from elsewhere": "Copiado de otro sitio",
|
||||
"Copied from original": "Copiado del original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 los siguientes Colaboradores:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 los siguientes Colaboradores:",
|
||||
"Danger!": "¡Peligro!",
|
||||
"Delete": "Eliminar",
|
||||
"Deleted": "Eliminado",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "El dispositivo \"{{name}}\" ({{device}} en la dirección {{address}}) quiere conectarse. Añadir nuevo dispositivo?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "ID del Dispositivo",
|
||||
"Device Identification": "Identificación del Dispositivo",
|
||||
"Device Name": "Nombre del Dispositivo",
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Editar dispositivo",
|
||||
"Edit Folder": "Editar repositorio",
|
||||
"Editing": "Editando",
|
||||
"Enable NAT traversal": "Permitir NAT transversal",
|
||||
"Enable Relaying": "Habilitar Retransmisión",
|
||||
"Enable UPnP": "Habilitar UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduzca las direcciones, separadas por comas (\"tcp://ip:port\", \"tcp://host:port\"), o \"dynamic\" para llevar a cabo el descubrimiento automático de la dirección.",
|
||||
@@ -71,7 +70,7 @@
|
||||
"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": "Carpeta",
|
||||
"Folder ID": "ID de carpeta",
|
||||
"Folder Label": "Etiqueta de la Carpeta",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Master": "Carpeta principal",
|
||||
"Folder Path": "Ruta de la carpeta",
|
||||
"Folders": "Carpetas",
|
||||
@@ -117,7 +116,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "Desconectar",
|
||||
"Oldest First": "El más antiguo primero",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Etiqueta descriptiva opcional para la carpeta. Puede ser diferente en cada dispositivo.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Options": "Opciones",
|
||||
"Out of Sync": "No sincronizado",
|
||||
"Out of Sync Items": "Elementos no sincronizados",
|
||||
@@ -139,9 +138,9 @@
|
||||
"Relayed via": "Respaldada a través",
|
||||
"Relays": "Respaldos",
|
||||
"Release Notes": "Notas de la versión",
|
||||
"Remote Devices": "Dispositivos Remotos",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remove": "Eliminar",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Identificador requerido para la carpeta. Debe ser el mismo en todos los dispositivos del clúster.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "Volver a analizar",
|
||||
"Rescan All": "Volver a analizar Todo",
|
||||
"Rescan Interval": "Intervalo de análisis",
|
||||
@@ -190,7 +189,7 @@
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Las estadísticas agregadas están disponibles públicamente en {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido grabada pero no activada. Syncthing debe reiniciarse para activar la nueva configuración.",
|
||||
"The device ID cannot be blank.": "La ID del dispositivo no puede estar vacía.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "El ID del dispositivo que hay que introducir aquí se puede encontrar en el diálogo \"Acciones > Mostrar ID\" en el otro dispositivo. Los espacios y las barras son opcionales (ignorados).",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).",
|
||||
"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).": "La ID del dispositivo que hay que introducir aquí puede encontrarse en el menú \"Editar > Mostrar ID\" en el otro dispositivo. Los espacios y barras son opcionales (ignorados).",
|
||||
"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.": "El informe encriptado de uso se envía diariamente. Se usa para rastrear plataformas comunes, tamaños de carpetas y versiones de la aplicación. Si el conjunto de datos enviados en el informes se cambia, se le pedirá a usted autorización de nuevo.",
|
||||
"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.": "La ID del dispositivo introducida no parece válida. Debe ser una cadena de 52 ó 56 caracteres formada por letras y números, con espacios y guiones opcionales.",
|
||||
@@ -212,7 +211,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "El límite de velocidad debe ser un número no negativo (0: sin límite)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "El intervalo de actualización debe ser un número positivo de segundos.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Se reintentarán de forma automática y se sincronizarán cuando se resuelva el error.",
|
||||
"This Device": "Este Dispositivo",
|
||||
"This Device": "This Device",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Esto podría permitir fácilmente el acceso a hackers para leer y modificar cualquier fichero de tu equipo.",
|
||||
"This is a major version upgrade.": "Hay una actualización importante.",
|
||||
"Trash Can File Versioning": "Versionado de archivos de la papelera",
|
||||
@@ -230,7 +229,7 @@
|
||||
"Version": "Versión",
|
||||
"Versions Path": "Ruta de las versiones",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Las versiones se borran automáticamente si son más antiguas que la edad máxima o exceden el número de ficheros permitidos en un intervalo.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Peligro! Esta ruta es un subdirectorio de una carpeta ya existente llamada \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Cuando añada un nuevo dispositivo, tenga en cuenta que este debe añadirse también en el otro lado.",
|
||||
"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.": "Cuando añada una nueva carpeta, tenga en cuenta que su ID se usa para unir carpetas entre dispositivos. Son sensibles a las mayúsculas y deben coincidir exactamente entre todos los dispositivos.",
|
||||
"Yes": "Si",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "Documentación completa",
|
||||
"items": "Elementos",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir la carpeta \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} quiere compartir la carpeta \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quiere compartir la carpeta \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Editar dispositivo",
|
||||
"Edit Folder": "Editar repositorio",
|
||||
"Editing": "Editando",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable UPnP": "Permitir UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "documentación completa",
|
||||
"items": "ítems",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir repositorio \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} qiuere compartir el repositorio \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} qiuere compartir el repositorio \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Muokkaa laitetta",
|
||||
"Edit Folder": "Muokkaa kansiota",
|
||||
"Editing": "Muokkaus",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable UPnP": "Ota UPnP käyttöön",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Syötä osoitteet pilkuilla erotettuina (\"tcp://ip:portti, tcp://nimi:portti\") tai \"dynamic\" käyttääksesi osoitteen automaattista selvitystä.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "täysi dokumentaatio",
|
||||
"items": "kohteet",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} haluaa jakaa kansion \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Éditer le périphérique",
|
||||
"Edit Folder": "Éditer le répertoire",
|
||||
"Editing": "Édition",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable UPnP": "Activer l'UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses (\"tcp://ip:port\" ou \"tcp://host:port\") séparées par une virgule ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "documentation complète",
|
||||
"items": "éléments",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le dossier \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Éditer la machine",
|
||||
"Edit Folder": "Éditer le dossier",
|
||||
"Editing": "Édition",
|
||||
"Enable NAT traversal": "Activer le transfert NAT",
|
||||
"Enable Relaying": "Activer le relayage",
|
||||
"Enable UPnP": "Activer l'UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses (\"tcp://ip:port\" ou \"tcp://host:port\") séparées par une virgule ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "documentation complète",
|
||||
"items": "fichiers",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le dossier \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} veut partager le dossier \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} veut partager le dossier \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} veut partager le dossier \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
"Add": "Taheakje",
|
||||
"Add Device": "Apparaat taheakje",
|
||||
"Add Folder": "Map taheakje",
|
||||
"Add Remote Device": "Apparaat op Ofstân Taheakje",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add new folder?": "Nije map taheakje?",
|
||||
"Address": "Adres",
|
||||
"Addresses": "Adressen",
|
||||
"Advanced": "Avansearre",
|
||||
"Advanced Configuration": "Avansearre konfiguraasje",
|
||||
"Advanced settings": "Avansearre ynstellings",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"All Data": "Alle data",
|
||||
"Allow Anonymous Usage Reporting?": "Anonime brûkensrapportaazje tastean?",
|
||||
"Alphabetic": "Alfabetysk",
|
||||
@@ -34,12 +34,12 @@
|
||||
"Connection Error": "Ferbiningsflater",
|
||||
"Copied from elsewhere": "Oernommen fan earne oars",
|
||||
"Copied from original": "Oernommen fan orizjineel",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 de folgende bydragers:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 de folgende bydragers:",
|
||||
"Danger!": "Gefaar!",
|
||||
"Delete": "Fuortsmite",
|
||||
"Deleted": "Fuortsmiten",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Apparaat \"{{name}}\" {{device}} op ({{address}}) wol ferbining meitsje. Nij apparaat taheakje?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "Apparaat-ID",
|
||||
"Device Identification": "Apparaatidentifikaasje",
|
||||
"Device Name": "Apparaatnamme",
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Apparaat bewurkje",
|
||||
"Edit Folder": "Map bewurkje",
|
||||
"Editing": "Bewurkjen",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Trochjaan tastean",
|
||||
"Enable UPnP": "UPnP oansette",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Fier troch komma's skieden (\"tcp://ip:port\", \"tcp://host:port\") adressen yn of \"dynamic\" om automatyske ûntdekking fan it adres út te fieren.",
|
||||
@@ -68,10 +67,10 @@
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Bits foar triemrjochten wurde negearre yn it sykjen foar feroarings. Brûk dit op FAT-triemsystemen.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Triemen wurde ferset nei map .stversions wannear't troch Syncthing ferfangen of fuortsmiten.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Triemen wurde ferset nei in mei datum stimpele ferzjes yn in .stversions map wannear troch Syncthing ferfangen of fuortsmiten.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Triemen binne ymmún foar feroarings makke troch oare apparaten, mar feroarings makke op dit apparaat wurde nei de rest fan 'e bondel ferstjoerd.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Triemen binne ymmún foar feroarings makke troch oare apparaten, mar feroarings makke op dit apparaat wurde nei de rest ferstjoerd.",
|
||||
"Folder": "Map",
|
||||
"Folder ID": "Map-ID",
|
||||
"Folder Label": "Map-opskrift",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Master": "Map-master",
|
||||
"Folder Path": "Map-paad",
|
||||
"Folders": "Mappen",
|
||||
@@ -117,7 +116,7 @@
|
||||
"OK": "Okee",
|
||||
"Off": "Ut",
|
||||
"Oldest First": "Aldste earst",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Opsjoneel beskriuwend opskrift foar de map. Mei op ider apparaat oars wêze.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Options": "Opsjes",
|
||||
"Out of Sync": "Net syngronisearre",
|
||||
"Out of Sync Items": "Net syngronisearre items",
|
||||
@@ -139,9 +138,9 @@
|
||||
"Relayed via": "Trochjûn fia",
|
||||
"Relays": "Trochjouers",
|
||||
"Release Notes": "Utjeftenotysjes",
|
||||
"Remote Devices": "Apparaten op Ofstân",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remove": "Fuortsmite",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Ferplicht ID foar de map. Moat op alle bondelapparaten itselde wêze.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "Sken opnij",
|
||||
"Rescan All": "Sken alles opnij",
|
||||
"Rescan Interval": "Wersken ynterval",
|
||||
@@ -162,11 +161,11 @@
|
||||
"Share With Devices": "Diele mei apparaten",
|
||||
"Share this folder?": "Dizze map diele?",
|
||||
"Shared With": "Dielt mei",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Koarte ID foar de map. Moat op alle bondelapparaten itselde wêze.",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Koart opskrift foar de map. Moat op alle apparaten itselde wêze.",
|
||||
"Show ID": "ID sjen litte",
|
||||
"Show QR": "QR sjen litte",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wurd ynstee fan apparaat-ID sjen litten by de bondeltastân. Wurd nei oare apparaten advertearre as in mooglike standertnamme.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wurd yn de bondeltastân sjen litten ynstee fan apparaat-ID. Wannear't leech litten wurd, wurd it fernijt nei de namme die it apparaat útstjoert.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wurd yn de bondel-tastân sjen litten ynstee fan apparaat-ID. Wannear't leech litten wurd, wurd it fernijt nei de namme die it apparaat útstjoert.",
|
||||
"Shutdown": "Ofslute",
|
||||
"Shutdown Complete": "Ofsluten klear",
|
||||
"Simple File Versioning": "Ienfâldich triemferzjebehear",
|
||||
@@ -212,7 +211,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "It fluggenslimyt moat in posityf nûmer wêze (0: gjin limyt)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "It wersken-ynterfal moat in posityf tal fan sekonden wêze.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Sy wurde automatysk opnij probearre en sille syngronisearre wurde wannear at de flater oplost is.",
|
||||
"This Device": "Dit Apparaat",
|
||||
"This Device": "This Device",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Dit kin samar ynkringers (hackers) tagong jaan om elke triem op jo kompjûter te besjen en te feroarjen.",
|
||||
"This is a major version upgrade.": "Dit is in wichtige ferzjefernijing.",
|
||||
"Trash Can File Versioning": "Jiskefet-triemferzjebehear",
|
||||
@@ -230,7 +229,7 @@
|
||||
"Version": "Ferzje",
|
||||
"Versions Path": "Ferzjes-paad",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Ferzjes wurde automatysk fuortsmiten wannear't se âlder binne dan de maksimale âldens of wannear it tal fan triemen yn in ynterval grutter is dan tastean.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warskôging, dit paad is in ûnderlizzende triemtafel fan in besteande map \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Hâld by it taheakjen fan in nij apparaat yn de holle dat it apparaat oan de oare kant ek taheakke wurde moat. ",
|
||||
"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.": "Hâld by it taheakjen fan in nije map yn de holle dat de map-ID brûkt wurd om de mappen tusken apparaten mei-inoar te ferbinen. Se binne haadlettergefoelich en moatte oer alle apparaten eksakt oerienkomme.",
|
||||
"Yes": "Ja",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "komplete dokumintaasje",
|
||||
"items": "items",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wol map \"{{folder}}\" diele.",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wol de map \"{{folderLabel}}\" ({{folder}}) diele.",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wol map \"{{folderlabel}}\" ({{folder}}) diele."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -21,7 +21,7 @@
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Külső program kezeli a fájl verziókövetést. A fájlt el kell távolítania a szinkronizált mappából.",
|
||||
"Anonymous Usage Reporting": "Névtelen felhasználási adatok küldése",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "A bevezető eszközön beállított minden eszköz hozzá lesz adva ehhez az eszközhöz is.",
|
||||
"Automatic upgrades": "Automatikus frissítések",
|
||||
"Automatic upgrades": "Automatikus frissítés",
|
||||
"Be careful!": "Légy óvatos!",
|
||||
"Bugs": "Hibák",
|
||||
"CPU Utilization": "Processzor használat",
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Eszköz szerkesztése",
|
||||
"Edit Folder": "Mappa szerkesztése",
|
||||
"Editing": "Szerkesztés",
|
||||
"Enable NAT traversal": "NAT bejárás engedélyezése",
|
||||
"Enable Relaying": "Közvetítés engedélyezése",
|
||||
"Enable UPnP": "UPnP engedélyezése",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Vesszővel elválasztva több cím is bevihető (\"tcp://ip:port\", \"tcp://host:port\"), az automatikus felderítéshez a 'dynamic' kulcsszó használatos. ",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "teljes dokumentáció",
|
||||
"items": "elem",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} meg szeretné osztani a \"{{folder}}\" nevű mappát.",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} meg szeretné osztani \"{{folderLabel}}\" ({{folder}}) mappát.",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} szeretné megosztani \"{{folderlabel}}\" ({{folder}}) mappát."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} meg szeretné osztani \"{{folderLabel}}\" ({{folder}}) mappát."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Sunting Perangkat",
|
||||
"Edit Folder": "Sunting Folder",
|
||||
"Editing": "Menyunting",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Aktifkan Relay",
|
||||
"Enable UPnP": "Aktifkan UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Masukkan alamat, pisahkan dengan koma (\"tcp://ip:port\", \"tcp://host:port\") atau \"dynamic\" untuk menjalankan penemuan otomatis alamat tersebut.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "full documentation",
|
||||
"items": "items",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Modifica Dispositivo",
|
||||
"Edit Folder": "Modifica Cartella",
|
||||
"Editing": "Modifica di",
|
||||
"Enable NAT traversal": "Abilita NAT trasversale",
|
||||
"Enable Relaying": "Abilita relaying",
|
||||
"Enable UPnP": "Attiva UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Inserisci indirizzi separati da virgola (\"tcp://ip:porta\", \"tcp://host:porta\") oppure \"dynamic\" per effettuare il rilevamento automatico dell'indirizzo.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "documentazione completa",
|
||||
"items": "elementi",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vuole condividere la cartella \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vuole condividere la cartella \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vuole condividere la cartella \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vuole condividere la cartella \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
"Addresses": "アドレス",
|
||||
"Advanced": "高度な設定",
|
||||
"Advanced Configuration": "高度な設定",
|
||||
"Advanced settings": "高度な設定",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"All Data": "全てのデータ",
|
||||
"Allow Anonymous Usage Reporting?": "匿名で利用状況をレポートすることを許可しますか?",
|
||||
"Alphabetic": "アルファベット順",
|
||||
@@ -39,7 +39,7 @@
|
||||
"Danger!": "危険",
|
||||
"Delete": "削除",
|
||||
"Deleted": "削除",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "デバイス「{{name}}」 ({{address}} の {{device}}) が接続を求めています。新しいデバイスとして追加しますか?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "デバイスID",
|
||||
"Device Identification": "デバイス識別情報",
|
||||
"Device Name": "デバイス名",
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "デバイスの編集",
|
||||
"Edit Folder": "フォルダーの編集",
|
||||
"Editing": "編集中",
|
||||
"Enable NAT traversal": "NATトラバーサルを有効にする",
|
||||
"Enable Relaying": "中継サーバー経由の通信を有効にする",
|
||||
"Enable UPnP": "UPnPを有効にする",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "アドレスを指定する場合は「tcp://IPアドレス:ポート」または「tcp://ホスト名:ポート」をコンマで区切って入力してください。自動探索を行う場合は「dynamic」と入力してください。",
|
||||
@@ -71,7 +70,7 @@
|
||||
"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 ID": "フォルダーID",
|
||||
"Folder Label": "フォルダー名",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Master": "フォルダーのマスター",
|
||||
"Folder Path": "フォルダーパス",
|
||||
"Folders": "フォルダー",
|
||||
@@ -117,7 +116,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "オフ",
|
||||
"Oldest First": "古い順",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "分かりやすいフォルダーの名前で、設定は任意です。デバイスごとに異なってもかまいません。",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Options": "オプション",
|
||||
"Out of Sync": "未同期",
|
||||
"Out of Sync Items": "同期の必要な項目",
|
||||
@@ -141,7 +140,7 @@
|
||||
"Release Notes": "リリースノート",
|
||||
"Remote Devices": "他のデバイス",
|
||||
"Remove": "除去",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "フォルダーの識別子で、必須です。このフォルダーを共有する全てのデバイス上で同一でなくてはなりません。",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "再スキャン",
|
||||
"Rescan All": "すべて再スキャン",
|
||||
"Rescan Interval": "再スキャン間隔",
|
||||
@@ -230,7 +229,7 @@
|
||||
"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.": "古いバージョンは、最大寿命もしくは期間ごとの最大保存数を超えた場合、自動的に削除されます。",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "警告: 入力されたパスは、設定済みのフォルダー「{{otherFolder}}」のサブディレクトリです。",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"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はデバイス間でフォルダーの対応づけに使われることに注意してください。フォルダーIDは大文字と小文字が区別され、共有するすべてのデバイスの間で完全に一致しなくてはなりません。",
|
||||
"Yes": "はい",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "詳細なマニュアル",
|
||||
"items": "項目",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} がフォルダー \"{{folder}}\" を共有するよう求めています。",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} がフォルダー「{{folderLabel}}」 ({{folder}}) を共有するよう求めています。",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} がフォルダー「{{folderlabel}}」 ({{folder}}) を共有するよう求めています。"
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "기기 편집",
|
||||
"Edit Folder": "폴더 편집",
|
||||
"Editing": "편집",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable UPnP": "UPnP 활성화",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "전체 문서",
|
||||
"items": "항목",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 에서 폴더 \\\"{{folder}}\\\" 를 공유하길 원합니다.",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -12,34 +12,34 @@
|
||||
"Add new folder?": "Pridėti naują aplanką?",
|
||||
"Address": "Adresas",
|
||||
"Addresses": "Adresai",
|
||||
"Advanced": "Išplėstiniai",
|
||||
"Advanced": "Pažangus",
|
||||
"Advanced Configuration": "Išplėstinė konfigūracija",
|
||||
"Advanced settings": "Išplėstiniai nustatymai",
|
||||
"Advanced settings": "Išpėstiniai nustatymai",
|
||||
"All Data": "Visiems duomenims",
|
||||
"Allow Anonymous Usage Reporting?": "Siųsti anoniminę naudojimo ataskaitą?",
|
||||
"Allow Anonymous Usage Reporting?": "Siųsti anonimišką vartojimo ataskaitą?",
|
||||
"Alphabetic": "Abėcėlės tvarka",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Išorinė komanda apdoroja versijų valdymą. Ji turi pašalinti failą iš sinchronizuoto aplanko.",
|
||||
"Anonymous Usage Reporting": "Anoniminė naudojimo ataskaita",
|
||||
"Anonymous Usage Reporting": "Anoniminė vartojimo ataskaita",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Visi supažindintojo įrenginiai bus pridėti prie jūsų įrenginių sąrašo.",
|
||||
"Automatic upgrades": "Automatiniai atnaujinimai",
|
||||
"Be careful!": "Būkite atsargūs!",
|
||||
"Bugs": "Klaidos",
|
||||
"CPU Utilization": "Procesoriaus panaudojimas",
|
||||
"Changelog": "Pasikeitimai",
|
||||
"Clean out after": "Išvalyti po",
|
||||
"Close": "Užverti",
|
||||
"Clean out after": "Išvalyto po",
|
||||
"Close": "Uždaryti",
|
||||
"Command": "Komanda",
|
||||
"Comment, when used at the start of a line": "Komentaras naudojamas naujoje eilutėje",
|
||||
"Compression": "Kompresija",
|
||||
"Connection Error": "Susijungimo klaida",
|
||||
"Copied from elsewhere": "Nukopijuota iš kitur",
|
||||
"Copied from elsewhere": "Nukopijuota iš betkur",
|
||||
"Copied from original": "Nukopijuota iš originalo",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Autorių teisės © 2014-2016 šių bendraautorių:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Visos teisės saugomos © 2015 šių bendraautorių:",
|
||||
"Danger!": "Pavojus!",
|
||||
"Delete": "Ištrinti",
|
||||
"Deleted": "Ištrinta",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Įrenginys \"{{name}}\" ({{device}} {{address}}) nori prisijungti. Pridėti naują įrenginį?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "Įrenginio ID",
|
||||
"Device Identification": "Įrenginio identifikacija",
|
||||
"Device Name": "Įrenginio pavadinimas",
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Keisti įrenginį",
|
||||
"Edit Folder": "Keisti aplanką",
|
||||
"Editing": "Redagavimas",
|
||||
"Enable NAT traversal": "Leisti kirsti NAT",
|
||||
"Enable Relaying": "Įjungti retransliavimą",
|
||||
"Enable UPnP": "Įjungti UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Įveskite kableliais atskirtus (\"tcp://ip:prievadas\", \"tcp://serveris:prievadas\") adresus arba \"dynamic\", kad atliktumėte automatinį adresų aptikimą.",
|
||||
@@ -68,12 +67,12 @@
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Ieškant pakeitimų, į failų leidimų bitus yra nekreipiama dėmesio. Naudoti FAT failų sistemose.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Failai perkeliami į .stversions aplanką kai tampa pakeisti arba ištrinti.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Programai Syncthing pakeičiant ar ištrinant failus, jie yra perkeliami į datomis pažymėtas versijas, aplanke .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.": "Failai yra apsaugoti nuo kituose įrenginiuose atliktų pakeitimų, bet pakeitimai šiame įrenginyje bus nusiųsti kitiems įrenginiams.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Failai apsaugoti nuo pakeitimų atliktų kituose įrenginiuose, bet pakeitimai šiame įrenginyje bus nusiųsti kitiems.",
|
||||
"Folder": "Aplankas",
|
||||
"Folder ID": "Aplanko ID",
|
||||
"Folder Label": "Aplanko etiketė",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Master": "Aplanko vadovas",
|
||||
"Folder Path": "Kelias iki aplanko",
|
||||
"Folder Path": "Kelias iki apkanko",
|
||||
"Folders": "Aplankai",
|
||||
"GUI": "Valdymo skydelis",
|
||||
"GUI Authentication Password": "Valdymo skydelio slaptažodis",
|
||||
@@ -117,7 +116,7 @@
|
||||
"OK": "Gerai",
|
||||
"Off": "Netaikoma",
|
||||
"Oldest First": "Seniausi pirmiau",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Nebūtina aprašomoji aplanko etiketė. Kiekviename įrenginyje gali būti skirtinga.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Options": "Parametrai",
|
||||
"Out of Sync": "Išsisinchronizavę",
|
||||
"Out of Sync Items": "Nesutikrinta",
|
||||
@@ -141,7 +140,7 @@
|
||||
"Release Notes": "Laidos Informacija",
|
||||
"Remote Devices": "Nuotoliniai įrenginiai",
|
||||
"Remove": "Pašalinti",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Reikalaujamas aplanko identifikatorius. Privalo būti toks pats visuose įrenginiuose.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "Nuskaityti iš naujo",
|
||||
"Rescan All": "Nuskaityti visus aplankus",
|
||||
"Rescan Interval": "Pertrauka tarp nuskaitymų",
|
||||
@@ -211,7 +210,7 @@
|
||||
"The path cannot be blank.": "Kelias negali būti tuščias.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Srauto maksimalus greitis privalo būti ne neigiamas skaičius (0: nėra apribojimo)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Nuskaitymo dažnis negali būti neigiamas skaičius.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Failus bus automatiškai bandoma parsiųsti dar kartą kai išspręsite klaidas",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Failus bus automatiškai badoma parsiųsti dar kartą kai išspręsite klaidas",
|
||||
"This Device": "This Device",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Tai gali suteikti programišiams lengvą prieigą skaityti ir keisti bet kokius failus jūsų kompiuteryje.",
|
||||
"This is a major version upgrade.": "Tai yra stambus atnaujinimas.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "pilna dokumentacija",
|
||||
"items": "įrašai",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} nori dalintis aplanku \"{{folder}}\"",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} nori dalintis aplanku \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} nori dalintis aplanku \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Rediger Enhet",
|
||||
"Edit Folder": "Rediger Mappe",
|
||||
"Editing": "Redigerer",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Aktiver relésending",
|
||||
"Enable UPnP": "Aktiver UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Skriv inn kommaseparerte (\"tcp://ip:port\", \"tcp://host:port\") adresser, eller ordet \"dynamic\" for å gjøre automatisk oppslag for adressen.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "all dokumentasjon",
|
||||
"items": "elementer",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker å dele mappen \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
"Add": "Toevoegen",
|
||||
"Add Device": "Apparaat toevoegen",
|
||||
"Add Folder": "Map toevoegen",
|
||||
"Add Remote Device": "Voeg extern apparaat toe",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add new folder?": "Voeg nieuwe map toe?",
|
||||
"Address": "Adres",
|
||||
"Addresses": "Adressen",
|
||||
"Advanced": "Geavanceerd",
|
||||
"Advanced Configuration": "Geavanceerde configuratie",
|
||||
"Advanced settings": "Geavanceerde instellingen",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"All Data": "Alle gegevens",
|
||||
"Allow Anonymous Usage Reporting?": "Versturen van anonieme gebruikersstatistieken toestaan?",
|
||||
"Alphabetic": "Alfabetisch",
|
||||
@@ -34,12 +34,12 @@
|
||||
"Connection Error": "Verbindingsfout",
|
||||
"Copied from elsewhere": "Gekopieerd vanaf elders",
|
||||
"Copied from original": "Gekopieerd van het origineel",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 voor de volgende contributanten:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 de volgende Bijdragers:",
|
||||
"Danger!": "Let op!",
|
||||
"Danger!": "Gevaar!",
|
||||
"Delete": "Verwijderen",
|
||||
"Deleted": "Verwijderd",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Apparaat \"{{name}}\" ({{device}} at {{address}}) wil verbinden. Wil je dit toestaan?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "Apparaat-ID",
|
||||
"Device Identification": "Apparaat-identificatie",
|
||||
"Device Name": "Naam apparaat",
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Bewerk apparaat",
|
||||
"Edit Folder": "Bewerk map",
|
||||
"Editing": "Bezig met bewerken",
|
||||
"Enable NAT traversal": "Activeer NAT traversal",
|
||||
"Enable Relaying": "Activeer doorsturen",
|
||||
"Enable UPnP": "UPnP gebruiken",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Voer door komma's gescheiden (\"tcp://ip:port\", \"tcp://host:port\") adressen in of voer \"dynamisch\" in om automatische ontdekking van het adres uit te voeren.",
|
||||
@@ -71,7 +70,7 @@
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Bestanden zijn beschermt tegen aanpassingen die gemaakt zijn door andere apparaten, maar aanpassingen op dit apparaat worden doorgestuurd naar de rest van het cluster.",
|
||||
"Folder": "Map",
|
||||
"Folder ID": "Map-ID",
|
||||
"Folder Label": "Map label",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Master": "Hoofdmap",
|
||||
"Folder Path": "Maplocatie",
|
||||
"Folders": "Mappen",
|
||||
@@ -117,7 +116,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "Uit",
|
||||
"Oldest First": "Oudste eerst",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optioneel label met een beschrijving voor de map. Kan verschillend zijn op elk apparaat.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Options": "Opties",
|
||||
"Out of Sync": "Niet gesynchroniseerd",
|
||||
"Out of Sync Items": "Niet-gesynchroniseerde items",
|
||||
@@ -139,9 +138,9 @@
|
||||
"Relayed via": "Doorgestuurd via",
|
||||
"Relays": "Relais",
|
||||
"Release Notes": "Release notes",
|
||||
"Remote Devices": "Externe apparaten",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remove": "Verwijderen",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "De identifier voor de map is verplicht. Dit moet hetzelfde zijn op alle apparaten in het cluster. ",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "Opnieuw scannen",
|
||||
"Rescan All": "Scan alles opnieuw",
|
||||
"Rescan Interval": "Scanfrequentie",
|
||||
@@ -212,7 +211,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "De snelheidslimiet moet een positief nummer zijn (0: geen limiet)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "De scanfrequentie moet een positief getal in seconden zijn.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Het wordt automatisch opnieuw geprobeerd. Bestanden worden gesynchroniseerd als de fout is hersteld.",
|
||||
"This Device": "Dit apparaat",
|
||||
"This Device": "This Device",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Dit kan kwaadwilligen eenvoudig toegang geven tot het lezen en wijzigen van bestanden op jouw computer.",
|
||||
"This is a major version upgrade.": "Dit is een grote update.",
|
||||
"Trash Can File Versioning": "Versiebeheer bestanden prullenbak",
|
||||
@@ -230,7 +229,7 @@
|
||||
"Version": "Versie",
|
||||
"Versions Path": "Bestandspad versies",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versies worden automatisch verwijderd als deze ouder zijn dan de maximale leeftijd of als ze het maximaal aantal toegestane bestanden per interval overschrijden. ",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Let op, dit bestandspad is een submap van een bestaande map \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Wanneer een nieuw toestel wordt toegevoegd, houd er dan rekening mee dat dit toestel ook aan de andere kant moet worden toegevoegd.",
|
||||
"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.": "Houd er bij het toevoegen van nieuwe mappen rekening mee dat het map-ID gebruikt wordt om mappen tussen apparaten te verbinden. Dit ID is hoofdlettergevoelig en moet identiek zijn op andere apparaten.",
|
||||
"Yes": "Ja",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "volledige documentatie",
|
||||
"items": "objecten",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wil de map \"{{folder}}\" delen.",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wil de map \"{{folderLabel}}\" ({{folder}}) delen.",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wil de map \"{{folderlabel}}\" ({{folder}}) delen."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Rediger Eining",
|
||||
"Edit Folder": "Rediger Mappe",
|
||||
"Editing": "Redigerer",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Aktiver Reléer",
|
||||
"Enable UPnP": "Aktiver UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Skriv inn adresser med komma mellom kvar adresse (\"tcp://ip:port\", \"tcp://host:port\"), eller \"dynamic\" for å automatisk søkja opp adressa.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "all dokumentasjon",
|
||||
"items": "element",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønskjer å dela mappa \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
"Add": "Dodaj",
|
||||
"Add Device": "Dodaj urządzenie",
|
||||
"Add Folder": "Dodaj folder",
|
||||
"Add Remote Device": "Dodaj urządzenie zdalne",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add new folder?": "Dodać nowy folder?",
|
||||
"Address": "Adres",
|
||||
"Addresses": "Adresy",
|
||||
"Advanced": "Zaawansowane",
|
||||
"Advanced Configuration": "Konfiguracja zaawansowana",
|
||||
"Advanced settings": "Ustawienia zaawansowane",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"All Data": "Wszystkie dane",
|
||||
"Allow Anonymous Usage Reporting?": "Zezwalaj na anonimowe statystyki użycia?",
|
||||
"Alphabetic": "Alfabetycznie",
|
||||
@@ -34,12 +34,12 @@
|
||||
"Connection Error": "Błąd połączenia",
|
||||
"Copied from elsewhere": "Skopiowane z innego miejsca ",
|
||||
"Copied from original": "Skopiowane z oryginału",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016: ",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015: ",
|
||||
"Danger!": "Niebezpieczne!",
|
||||
"Delete": "Usuń",
|
||||
"Deleted": "Usunięto",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Urządzenie \"{{name}}\" {{device}} ({{address}}) chce się połączyć. Dodać urządzenie?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "ID urządzenia",
|
||||
"Device Identification": "Identyfikator urządzenia",
|
||||
"Device Name": "Nazwa urządzenia",
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Edytuj urządzenie",
|
||||
"Edit Folder": "Edytuj folder",
|
||||
"Editing": "Edytowanie",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Włącz przekazywanie",
|
||||
"Enable UPnP": "Włącz UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Wpisz oddzielone przecinkiem adresy (\"tcp://ip:port\", \"tcp://host:port\") lub \"dynamic\" by przeprowadzić automatyczne odnalezienie adresu.",
|
||||
@@ -71,7 +70,7 @@
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Pliki są zabezpieczone przed zmianami na innym urządzeniu, jednak zmiany w tym urządzeniu będą wysłane do reszty.",
|
||||
"Folder": "Folder",
|
||||
"Folder ID": "ID folderu",
|
||||
"Folder Label": "Etykieta folderu",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Master": "Główny folder",
|
||||
"Folder Path": "Ścieżka folderu",
|
||||
"Folders": "Foldery",
|
||||
@@ -117,7 +116,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "Wyłącz",
|
||||
"Oldest First": "Najstarsze na początku",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Opcjonalna opisowa etykieta dla folderu. Może być różna na każdym urządzeniu.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Options": "Opcje",
|
||||
"Out of Sync": "Niezsynchronizowane",
|
||||
"Out of Sync Items": "Niezsynchronizowane pliki",
|
||||
@@ -139,9 +138,9 @@
|
||||
"Relayed via": "Przekazane przez",
|
||||
"Relays": "Przekaźniki",
|
||||
"Release Notes": "Informacje o wydaniu",
|
||||
"Remote Devices": "Urządzenia zdalne",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remove": "Usuń",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Wymagany identyfikator dla folderu. Musi być taki sam dla wszystkich urządzeń.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "Skanuj ponownie",
|
||||
"Rescan All": "Skanuj wszystko ponownie",
|
||||
"Rescan Interval": "Interwał skanowania",
|
||||
@@ -212,7 +211,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Ograniczenie prędkości powinno być nieujemną liczbą całkowitą (0: brak ograniczeń)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Interwał skanowania musi być niezerową liczbą sekund.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Ponowne próby zachodzą automatycznie, synchronizacja nastąpi po usunięciu usterki.",
|
||||
"This Device": "To urządzenie",
|
||||
"This Device": "This Device",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Może to umożliwić osobom trzecim dostęp do odczytu i zmian dowolnych plików na urządzeniu.",
|
||||
"This is a major version upgrade.": "To jest ważna aktualizacja",
|
||||
"Trash Can File Versioning": "Kontrola werjsi plików w koszu",
|
||||
@@ -230,7 +229,7 @@
|
||||
"Version": "Wersja",
|
||||
"Versions Path": "Ścieżka wersji",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Wersje zostają automatycznie usunięte jeżeli są starsze niż maksymalny wiek lub przekraczają liczbę dopuszczalnych wersji.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Ostrzerzenie, ta ścieżka to podkatalog istniejącego folderu \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Gdy dodajesz nowe urządzenie, pamiętaj że urządzenie musi zostać dodane także po drugiej stronie.",
|
||||
"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.": "Przy dodawaniu nowego folderu, pamiętaj, że ID użyte jest do łączenia folderów pomiędzy urządzeniami. Wielkość liter ciągu ma znaczenie musi zgadzać się na wszystkich urządzeniach.",
|
||||
"Yes": "Tak",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "pełna dokumentacja",
|
||||
"items": "pozycji",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce udostępnić folder \"{{folder}}\"",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} chce udostępnić folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} chce udostępnić folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Editar dispositivo",
|
||||
"Edit Folder": "Editar pasta",
|
||||
"Editing": "Editando",
|
||||
"Enable NAT traversal": "Habilitar NAT",
|
||||
"Enable Relaying": "Habilitar retransmissão",
|
||||
"Enable UPnP": "Habilitar UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Insira endereços (\"tcp://ip:porta\", \"tcp://host:porta\") separados por vírgula ou \"dynamic\" para executar a descoberta automática do endereço.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "documentação completa",
|
||||
"items": "itens",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer compartilhar a pasta \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} deseja compartilhar a pasta \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quer compartilhar a pasta \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} deseja compartilhar a pasta \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
"Add": "Adicionar",
|
||||
"Add Device": "Adicionar dispositivo",
|
||||
"Add Folder": "Adicionar pasta",
|
||||
"Add Remote Device": "Adicionar dispositivo remoto",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add new folder?": "Adicionar nova pasta?",
|
||||
"Address": "Endereço",
|
||||
"Addresses": "Endereços",
|
||||
"Advanced": "Avançadas",
|
||||
"Advanced Configuration": "Configuração avançada",
|
||||
"Advanced settings": "Configurações avançadas",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"All Data": "Todos os dados",
|
||||
"Allow Anonymous Usage Reporting?": "Permitir envio de relatórios anónimos de utilização?",
|
||||
"Alphabetic": "Alfabética",
|
||||
@@ -34,12 +34,12 @@
|
||||
"Connection Error": "Erro de ligação",
|
||||
"Copied from elsewhere": "Copiado doutro sítio",
|
||||
"Copied from original": "Copiado do original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 os seguintes contribuidores:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 os seguintes contribuidores:",
|
||||
"Danger!": "Perigo!",
|
||||
"Delete": "Eliminar",
|
||||
"Deleted": "Eliminado",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "O dispositivo \"{{name}}\" ({{device}} em {{address}}) quer conectar-se. Adiciono este novo dispositivo?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "ID do dispositivo",
|
||||
"Device Identification": "Identificação do dispositivo",
|
||||
"Device Name": "Nome do dispositivo",
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Editar dispositivo",
|
||||
"Edit Folder": "Editar pasta",
|
||||
"Editing": "Editando",
|
||||
"Enable NAT traversal": "Activar travessia de NAT",
|
||||
"Enable Relaying": "Permitir retransmissão",
|
||||
"Enable UPnP": "Activar UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduza endereços separados por vírgulas (\"tcp://ip:porto\", \"tcp://máquina:porto\") ou \"dynamic\" para detectar automaticamente os endereços.",
|
||||
@@ -71,7 +70,7 @@
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Os ficheiros estão protegidos contra alterações feitas noutros dispositivos, mas alterações feitas neste dispositivo serão enviadas ao resto do grupo.",
|
||||
"Folder": "Pasta",
|
||||
"Folder ID": "ID da pasta",
|
||||
"Folder Label": "Etiqueta da pasta",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Master": "Pasta mestre",
|
||||
"Folder Path": "Caminho da pasta",
|
||||
"Folders": "Pastas",
|
||||
@@ -117,7 +116,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "Desligada",
|
||||
"Oldest First": "Primeiro os mais antigos",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Etiqueta descritiva opcional para a pasta. Pode ser diferente em cada dispositivo.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Options": "Opções",
|
||||
"Out of Sync": "Fora de sincronia",
|
||||
"Out of Sync Items": "Itens por sincronizar",
|
||||
@@ -139,9 +138,9 @@
|
||||
"Relayed via": "Retransmitido via",
|
||||
"Relays": "Retransmissores",
|
||||
"Release Notes": "Notas de lançamento",
|
||||
"Remote Devices": "Dispositivos remotos",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remove": "Remover",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Identificador obrigatório para a pasta. Tem que ser igual em todos os dispositivos do grupo.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "Verificar agora",
|
||||
"Rescan All": "Verificar todas agora",
|
||||
"Rescan Interval": "Intervalo entre verificações",
|
||||
@@ -151,7 +150,7 @@
|
||||
"Resume": "Retomar",
|
||||
"Reused": "Reutilizado",
|
||||
"Save": "Gravar",
|
||||
"Scan Time Remaining": "Tempo restante da verificação",
|
||||
"Scan Time Remaining": "Tempo restante de rastreio",
|
||||
"Scanning": "Verificando",
|
||||
"Select the devices to share this folder with.": "Seleccione os dispositivos com os quais vai partilhar esta pasta.",
|
||||
"Select the folders to share with this device.": "Seleccione as pastas a partilhar com este dispositivo.",
|
||||
@@ -212,7 +211,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "O limite de velocidade tem que ser um número que não seja negativo (0: sem limite)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "O intervalo entre verificações tem que ser um valor não negativo de segundos.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Será tentado automaticamente e os itens serão sincronizados assim que o erro seja resolvido.",
|
||||
"This Device": "Este dispositivo",
|
||||
"This Device": "This Device",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Isso facilmente dará acesso aos piratas informáticos para lerem e modificarem quaisquer ficheiros no seu computador.",
|
||||
"This is a major version upgrade.": "Esta é uma actualização para uma versão importante.",
|
||||
"Trash Can File Versioning": "Reciclagem",
|
||||
@@ -230,7 +229,7 @@
|
||||
"Version": "Versão",
|
||||
"Versions Path": "Caminho das versões",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "As versões são eliminadas automaticamente se forem mais antigas do que a idade máxima ou excederem o número máximo de ficheiros permitido num intervalo.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Aviso: Este caminho é uma subpasta da pasta \"{{otherFolder}}\" já existente.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quando adicionar um novo dispositivo, lembre-se que este dispositivo tem que ser adicionado do outro lado também.",
|
||||
"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.": "Quando adicionar uma nova pasta, lembre-se que o ID da pasta é utilizado para ligar as pastas entre dispositivos. É sensível às diferenças entre maiúsculas e minúsculas e tem que ter uma correspondência perfeita entre todos os dispositivos.",
|
||||
"Yes": "Sim",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "documentação completa",
|
||||
"items": "itens",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer partilhar a pasta \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} quer partilhar a pasta \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quer partilhar a pasta \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Изменить устройство",
|
||||
"Edit Folder": "Изменить папку",
|
||||
"Editing": "Редактирование",
|
||||
"Enable NAT traversal": "Включить NAT traversal",
|
||||
"Enable Relaying": "Включить релеи",
|
||||
"Enable UPnP": "Включить UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Введите адреса через запятую (\"tcp://ip:port\", \"tcp://host:port\") или \"dynamic\" для автоматического поиска адресов.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "полная документация",
|
||||
"items": "элементы",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хочет поделиться папкой \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} хочет поделиться папкой «{{folderLabel}}» ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} хочет поделиться папкой «{{folderlabel}}» ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} хочет поделиться папкой «{{folderLabel}}» ({{folder}})."
|
||||
}
|
||||
@@ -55,8 +55,7 @@
|
||||
"Edit Device": "Redigera enhet",
|
||||
"Edit Folder": "Redigera katalog",
|
||||
"Editing": "Redigerar",
|
||||
"Enable NAT traversal": "Aktivera NAT traversering",
|
||||
"Enable Relaying": "Aktivera reläa",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable UPnP": "Använd UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade (\"tcp://ip:port\", \"tcp://host:port\")-adresser eller ordet \"dynamic\" för att använda automatisk uppslagning.",
|
||||
"Enter ignore patterns, one per line.": "Ange filmönster, ett per rad.",
|
||||
@@ -71,7 +70,7 @@
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer skyddas från ändringar gjorda på andra enheter, men ändringar som görs på den här noden skickas till de andra klustermedlemmarna.",
|
||||
"Folder": "Katalog",
|
||||
"Folder ID": "Katalog-ID",
|
||||
"Folder Label": "Katalog etikett",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Master": "Huvudlagring",
|
||||
"Folder Path": "Sökväg",
|
||||
"Folders": "Kataloger",
|
||||
@@ -82,7 +81,7 @@
|
||||
"Generate": "Skapa",
|
||||
"Global Discovery": "Global uppslagning",
|
||||
"Global Discovery Server": "Global uppslagningsserver",
|
||||
"Global Discovery Servers": "Globala uppslagningsservrar",
|
||||
"Global Discovery Servers": "Global Discovery Servers",
|
||||
"Global State": "Global status",
|
||||
"Help": "Hjälp",
|
||||
"Home page": "Hemsida",
|
||||
@@ -117,7 +116,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "Av",
|
||||
"Oldest First": "Äldst först",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Valfri beskrivande etikett för katalogen. Kan vara olika på varje enhet.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Options": "Alternativ",
|
||||
"Out of Sync": "Osynkad",
|
||||
"Out of Sync Items": "Osynkade poster",
|
||||
@@ -128,14 +127,14 @@
|
||||
"Pause": "Paus",
|
||||
"Paused": "Pausad",
|
||||
"Please consult the release notes before performing a major upgrade.": "Läs igenom versionsnyheterna innan den stora uppgraderingen.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ställ in ett grafiskt användarautentisering och lösenord i dialogrutan Inställningar.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Please set a GUI Authentication User and Password in the Settings dialog.",
|
||||
"Please wait": "Var god vänta",
|
||||
"Preview": "Förhandsgranska",
|
||||
"Preview Usage Report": "Förhandsgranska statistik",
|
||||
"Quick guide to supported patterns": "Snabb guide till filmönster som stöds",
|
||||
"RAM Utilization": "Minnesanvändning",
|
||||
"Random": "Slumpmässig",
|
||||
"Relay Servers": "Reläservrar",
|
||||
"Relay Servers": "Relay Servers",
|
||||
"Relayed via": "Vidarbefordras via",
|
||||
"Relays": "Vidarbefordringar",
|
||||
"Release Notes": "versionsnyheter",
|
||||
@@ -213,7 +212,7 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "Förnyelseintervallet måste vara ett positivt antal sekunder",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "De omprövas automatiskt och kommer att synkroniseras när felet är löst.",
|
||||
"This Device": "Enheten",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Detta kan lätt ge hackare tillgång till att läsa och ändra några filer på datorn.",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "This can easily give hackers access to read and change any files on your computer.",
|
||||
"This is a major version upgrade.": "Det här är en stor uppgradering.",
|
||||
"Trash Can File Versioning": "Versionshantering på filer i papperskorgen",
|
||||
"Unknown": "Okänt",
|
||||
@@ -230,7 +229,7 @@
|
||||
"Version": "Version",
|
||||
"Versions Path": "Katalog för versioner",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas bort automatiskt när de är äldre än den maximala åldersgränsen eller överstiger frekvensen i sitt interval.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en underkatalog till en befintlig katalog \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "När du lägger till en ny enhet, kom ihåg att den här enheten måste läggas till på den andra enheten också.",
|
||||
"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.": "När du lägger till ny katalog, tänk på att katalog-ID:t knyter ihop katalogen mellan olika noder. De måste vara exakt desamma mellan noder och stora eller små bokstäver har betydelse.",
|
||||
"Yes": "Ja",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "fullständig dokumentation",
|
||||
"items": "poster",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela katalogen \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vill dela mappen \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela mappen \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Cihaz Düzenle",
|
||||
"Edit Folder": "Klasör Düzenle",
|
||||
"Editing": "Düzenleniyor",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable UPnP": "UPnP Etkinleştir",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Adreslerin keşfinin otomatik olarak gerçekleştirilmesi için ya adresleri virgülle ayırarak (\"tcp://ip:port\", \"tcp://host:port\") girin, ya da \"dynamic\" kelimesini girin.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "belgelendirme içeriğinin tümü",
|
||||
"items": "öğel",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} \"{{folder}}\" klasörünü paylaşmak istiyor.",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -39,7 +39,7 @@
|
||||
"Danger!": "Небезпечно!",
|
||||
"Delete": "Видалити",
|
||||
"Deleted": "Видалене",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Пристрій \"{{name}}\" ({{device}} за адресою {{address}}) намагається під’єднатися. Додати новий пристрій?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "ID пристрою",
|
||||
"Device Identification": "Ідентифікатор пристрою",
|
||||
"Device Name": "Назва пристрою",
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Налаштування пристрою",
|
||||
"Edit Folder": "Налаштування директорії",
|
||||
"Editing": "Редагування",
|
||||
"Enable NAT traversal": "Увімкнути NAT traversal",
|
||||
"Enable Relaying": "Увімкнути ретрансляцію (relaying)",
|
||||
"Enable UPnP": "Увімкнути UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Введіть розділені комою (\"tcp://ip:port\", \"tcp://host:port\") адреси або \"dynamic\" для автоматичного визначення адреси.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "повна документація",
|
||||
"items": "елементи",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хоче поділитися директорією \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} хоче поділитися директорією \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} хоче поділитися директорією \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} хоче поділитися директорією \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,6 @@
|
||||
"Edit Device": "Ch.sửa thiết bị",
|
||||
"Edit Folder": "Ch.sửa thư mục",
|
||||
"Editing": "Đang ch.sửa",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Bật chế độ ch.tiếp",
|
||||
"Enable UPnP": "Bật UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Nhập các địa chỉ ngăn cách bởi dấu phẩy (\"tcp://ip:port\", \"tcp://host:port\") hoặc \"dynamic\" để tiến hành dò tìm địa chỉ tự động.",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "tài liệu đầy đủ",
|
||||
"items": "nội dung",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} muốn chia sẻ thư mục \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} muốn chia sẻ thư mục \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} muốn chia sẻ thư mục \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"A device with that ID is already added.": "您已添加过相同 ID 的设备",
|
||||
"A negative number of days doesn't make sense.": "天数为负数没有意义。",
|
||||
"A device with that ID is already added.": "您已添加过该设备",
|
||||
"A negative number of days doesn't make sense.": "天数不能为负",
|
||||
"A new major version may not be compatible with previous versions.": "重大更新可能与之前的版本之间无法兼容",
|
||||
"API Key": "API Key",
|
||||
"About": "关于",
|
||||
@@ -13,12 +13,12 @@
|
||||
"Address": "地址",
|
||||
"Addresses": "地址列表",
|
||||
"Advanced": "高级",
|
||||
"Advanced Configuration": "高级配置",
|
||||
"Advanced Configuration": "高级设置",
|
||||
"Advanced settings": "高级设置",
|
||||
"All Data": "所有数据",
|
||||
"Allow Anonymous Usage Reporting?": "允许匿名使用报告?",
|
||||
"Alphabetic": "字母顺序",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "使用外部命令接管版本控制。该命令必须自行从同步文件夹中删除该文件。",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "使用外部命令接管版本控制。当文件在其他设备被删除时,本机的 Syncthing 会调用该外部命令,并将文件路径以参数形式传递给该命令。该命令必须自行从同步文件夹中删除该文件。",
|
||||
"Anonymous Usage Reporting": "匿名使用报告",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "在介绍人设备上被添加的其它设备,也将会被添加到本机。",
|
||||
"Automatic upgrades": "自动升级",
|
||||
@@ -34,12 +34,12 @@
|
||||
"Connection Error": "连接出错",
|
||||
"Copied from elsewhere": "从其他设备复制",
|
||||
"Copied from original": "从源复制",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 以下贡献者:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "版权所有© 2014-2016 以下贡献者:",
|
||||
"Copyright © 2015 the following Contributors:": "版权 ©2015 由下列贡献者所有:",
|
||||
"Danger!": "危险!",
|
||||
"Delete": "删除",
|
||||
"Deleted": "已删除",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "设备 \"{{name}}\"(位于 {{address}} 的 {{device}})请求连接。是否添加新设备?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "设备标识",
|
||||
"Device Identification": "设备标识",
|
||||
"Device Name": "设备名",
|
||||
@@ -55,17 +55,16 @@
|
||||
"Edit Device": "编辑设备选项",
|
||||
"Edit Folder": "编辑文件夹选项",
|
||||
"Editing": "正在编辑",
|
||||
"Enable NAT traversal": "启用 NAT 遍历",
|
||||
"Enable Relaying": "开启中继",
|
||||
"Enable UPnP": "开启UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "输入以半角逗号分隔的 (\"tcp://ip:port\", \"tcp://host:port\") 设置可用地址列表,或者输入 \"dynamic\" 表示自动寻找地址。",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "输入以半角逗号分隔的 \"tcp://IP地址:端口号\", \"tcp://主机:端口号\" 设置可用地址列表,或者输入\"dynamic\"表示自动寻找地址。",
|
||||
"Enter ignore patterns, one per line.": "请输入忽略表达式,每行一条",
|
||||
"Error": "错误",
|
||||
"External File Versioning": "外部版本控制",
|
||||
"Failed Items": "失败的项目",
|
||||
"File Pull Order": "文件拉取顺序",
|
||||
"File Versioning": "版本控制",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "当查找文件更改时,忽略文件权限位。用在 FAT文件系统上。",
|
||||
"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.": "当某个文件在其他设备被替换或删除时,本设备将会在 .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.": "在其它设备中对该文件夹内文件的修改并不会被同步到本机,但是在本机上对其的修改,则会被同步到其它设备中。",
|
||||
@@ -75,7 +74,7 @@
|
||||
"Folder Master": "主文件夹",
|
||||
"Folder Path": "文件夹路径",
|
||||
"Folders": "文件夹",
|
||||
"GUI": "图形用户界面",
|
||||
"GUI": "图形界面",
|
||||
"GUI Authentication Password": "图形管理界面密码",
|
||||
"GUI Authentication User": "图形管理界面用户名",
|
||||
"GUI Listen Addresses": "图形管理界面监听地址",
|
||||
@@ -89,7 +88,7 @@
|
||||
"Ignore": "忽略",
|
||||
"Ignore Patterns": "忽略列表",
|
||||
"Ignore Permissions": "忽略文件权限",
|
||||
"Incoming Rate Limit (KiB/s)": "下载速率限制 (KiB/s)",
|
||||
"Incoming Rate Limit (KiB/s)": "下载速率限制(KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "错误的配置可能损坏您文件夹内的内容,使得 Syncthing 无法工作。",
|
||||
"Introducer": "介绍人设备",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "对本条件取反(例如:不要排除某项)",
|
||||
@@ -104,7 +103,7 @@
|
||||
"Major Upgrade": "重大更新",
|
||||
"Maximum Age": "历史版本最长保留时间",
|
||||
"Metadata Only": "仅元数据",
|
||||
"Minimum Free Disk Space": "最低可用磁盘空间",
|
||||
"Minimum Free Disk Space": "最低空闲磁盘空间",
|
||||
"Move to top of queue": "移动到队列顶端",
|
||||
"Multi level wildcard (matches multiple directory levels)": "多级通配符(用以匹配多层文件夹)",
|
||||
"Never": "从未",
|
||||
@@ -117,7 +116,7 @@
|
||||
"OK": "确定",
|
||||
"Off": "关闭",
|
||||
"Oldest First": "旧文件优先",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "可选的文件夹说明性标签。在不同设备上可以不一致。",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "可选的文件夹表述标签。在不同设备上可以不一致。",
|
||||
"Options": "选项",
|
||||
"Out of Sync": "未同步",
|
||||
"Out of Sync Items": "未同步的项目",
|
||||
@@ -164,9 +163,9 @@
|
||||
"Shared With": "共享给",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "文件夹的别名。必须在所有设备上保持一致。",
|
||||
"Show ID": "显示设备标识",
|
||||
"Show QR": "显示 QR 码",
|
||||
"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。如果设置为空,则会使用目标设备提供的默认名称。",
|
||||
"Show QR": "显示二维码",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "在设备丛中,显示该名称,而不是设备标识。亦会作为一个可选的默认名称被发送到其他设备。",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "在设备丛中,将会显示本名称,而不是设备标识。如果设置为空,则会使用目标设备提供的默认名称。",
|
||||
"Shutdown": "关闭 Syncthing",
|
||||
"Shutdown Complete": "关闭完成",
|
||||
"Simple File Versioning": "简易版本控制",
|
||||
@@ -190,7 +189,7 @@
|
||||
"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.": "设备标识不能为空",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "在这里所需要输入的设备 ID,可以在目标设备的“操作->显示 ID”中看到。空格和横线可选(将会被忽略)。",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "在这里所需要输入的设备标识,可以在目标设备的“操作->显示设备标识”中看到。空格和横线可选(将会被忽略)。",
|
||||
"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).": "在这里所需要输入的设备标识,可以在目标设备的“选项->显示设备标识”中看到。空格和横线可选(将会被忽略)。",
|
||||
"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.": "经过加密的使用报告会每天发送。它用来跟踪统计使用本软件的平台,文件夹大小,以及本软件的版本。如果报告的内容有任何变化,本对话框会再次弹出提示您。",
|
||||
"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.": "输入的设备标识似乎无效。设备标识长度必须为52或56的字母和数字,空格和横线不算在内。",
|
||||
@@ -203,7 +202,7 @@
|
||||
"The following items could not be synchronized.": "下列项目无法被同步。",
|
||||
"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 minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "最低可用磁盘空间的数值必须介于 0-100 的正整数",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "最低空间磁盘空间的数值必须介于 0-100 的正整数",
|
||||
"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 old versions to keep, per file.": "每个文件保留的版本数量上限。",
|
||||
@@ -232,13 +231,12 @@
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "超过最长保留时间,或者不满足下列条件的历史版本,将会被删除。",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "警告, 该路径是一个已经存在文件夹\"{{otherFolder}}\"的子文件夹.",
|
||||
"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 是用以在不同设备间建立联系的。在不同设备间拥有相同 ID 的文件夹将会被同步。且文件夹 ID 区分大小写。",
|
||||
"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.": "若你添加了新文件夹,记住文件夹标识是用以在不同设备间建立联系的。在不同设备间拥有相同标识的文件夹将会被同步。且文件夹标识大小写敏感。",
|
||||
"Yes": "是",
|
||||
"You must keep at least one version.": "您必须保留至少一个版本",
|
||||
"days": "天",
|
||||
"full documentation": "完整文档",
|
||||
"items": "条目",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想将 “{{folder}}” 文件夹共享给您",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} 想要分享文件夹 \"{{folderLabel}}\" ({{folder}})。",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} 想要分享文件夹 \"{{folderLabel}}\" ({{folder}})。"
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}}想要分享文件夹\"{{folderLabel}}\" ({{folder}}).\n"
|
||||
}
|
||||
@@ -1,28 +1,28 @@
|
||||
{
|
||||
"A device with that ID is already added.": "該裝置識別碼已被新增。",
|
||||
"A device with that ID is already added.": "A device with that ID is already added.",
|
||||
"A negative number of days doesn't make sense.": "一個負的天數並不合理。",
|
||||
"A new major version may not be compatible with previous versions.": "新的主要版本可能與以前的版本不相容。",
|
||||
"A new major version may not be compatible with previous versions.": "一個新的主要版本可能與以前的版本並不相容。",
|
||||
"API Key": "API 金鑰",
|
||||
"About": "關於",
|
||||
"Actions": "操作",
|
||||
"Add": "增加",
|
||||
"Add Device": "增加裝置",
|
||||
"Add Folder": "增加資料夾",
|
||||
"Add Remote Device": "新增遠端裝置",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add new folder?": "新增資料夾?",
|
||||
"Address": "位址",
|
||||
"Addresses": "位址",
|
||||
"Advanced": "進階",
|
||||
"Advanced Configuration": "進階配置",
|
||||
"Advanced settings": "進階設定",
|
||||
"Advanced Configuration": "進階設定",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"All Data": "全部資料",
|
||||
"Allow Anonymous Usage Reporting?": "允許匿名的使用資訊回報?",
|
||||
"Allow Anonymous Usage Reporting?": "允許匿名的使用資訊回報?",
|
||||
"Alphabetic": "字母順序",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "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": "自動升級",
|
||||
"Be careful!": "請小心!",
|
||||
"Be careful!": "請小心!",
|
||||
"Bugs": "程式錯誤",
|
||||
"CPU Utilization": "CPU 使用",
|
||||
"Changelog": "更新日誌",
|
||||
@@ -33,20 +33,20 @@
|
||||
"Compression": "壓縮",
|
||||
"Connection Error": "連線錯誤",
|
||||
"Copied from elsewhere": "從別處複製",
|
||||
"Copied from original": "從原處複製",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 下列貢獻者:",
|
||||
"Copied from original": "從原來複製",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 下列貢獻者:",
|
||||
"Danger!": "危險!",
|
||||
"Danger!": "Danger!",
|
||||
"Delete": "刪除",
|
||||
"Deleted": "已刪除",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "裝置 \"{{name}}\" ({{device}} 位於 {{address}}) 想要連線。 要增加新裝置嗎?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "裝置識別碼",
|
||||
"Device Identification": "裝置識別",
|
||||
"Device Name": "裝置名稱",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "裝置 {{device}} ({{address}}) 想要連線。要新增裝置嗎?",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "裝置 {{device}} ({{address}}) 想要連線。要新增裝置嗎?",
|
||||
"Devices": "裝置",
|
||||
"Disconnected": "斷線",
|
||||
"Discovery": "探索",
|
||||
"Discovery": "Discovery",
|
||||
"Documentation": "說明文件",
|
||||
"Download Rate": "下載速率",
|
||||
"Downloaded": "已下載",
|
||||
@@ -55,8 +55,7 @@
|
||||
"Edit Device": "編輯裝置",
|
||||
"Edit Folder": "編輯資料夾",
|
||||
"Editing": "正在編輯",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "啟用中繼",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable UPnP": "啟用 UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
"Enter ignore patterns, one per line.": "輸入忽略樣式,每行一種。",
|
||||
@@ -71,26 +70,26 @@
|
||||
"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 ID": "資料夾識別碼",
|
||||
"Folder Label": "資料夾標籤",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Master": "主資料夾",
|
||||
"Folder Path": "資料夾路徑",
|
||||
"Folders": "資料夾",
|
||||
"GUI": "GUI",
|
||||
"GUI Authentication Password": "GUI 認證密碼",
|
||||
"GUI Authentication User": "GUI 使用者認證名稱",
|
||||
"GUI Authentication User": "GUI 認證使用者名稱",
|
||||
"GUI Listen Addresses": "GUI 監聽位址",
|
||||
"Generate": "產生",
|
||||
"Global Discovery": "全域探索",
|
||||
"Global Discovery Server": "全域探索伺服器",
|
||||
"Global Discovery Servers": "全域探索伺服器",
|
||||
"Global Discovery Servers": "Global Discovery Servers",
|
||||
"Global State": "全域狀態",
|
||||
"Help": "說明",
|
||||
"Home page": "首頁",
|
||||
"Ignore": "忽略",
|
||||
"Ignore Patterns": "忽略樣式",
|
||||
"Ignore Permissions": "忽略權限",
|
||||
"Incoming Rate Limit (KiB/s)": "傳入速率限制 (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "不正確的設定可能會損壞您的資料夾內容,並導致 Syncthing 不正常運作。",
|
||||
"Incoming Rate Limit (KiB/s)": "連入速率限制 (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "不正確的設定可能會損壞你的資料夾內容,並引致 Syncthing 的不正當運作。",
|
||||
"Introducer": "引入者",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "反轉給定條件 (即:不要排除)",
|
||||
"Keep Versions": "保留歷史版本數",
|
||||
@@ -98,9 +97,9 @@
|
||||
"Last File Received": "最後接收的檔案",
|
||||
"Last seen": "最後發現時間",
|
||||
"Later": "稍後",
|
||||
"Local Discovery": "本機探索",
|
||||
"Local State": "本機狀態",
|
||||
"Local State (Total)": "本機狀態 (總結)",
|
||||
"Local Discovery": "本地探索",
|
||||
"Local State": "本地狀態",
|
||||
"Local State (Total)": "本地狀態 (總結)",
|
||||
"Major Upgrade": "重大更新",
|
||||
"Maximum Age": "最長保留時間",
|
||||
"Metadata Only": "僅中繼資料",
|
||||
@@ -119,27 +118,27 @@
|
||||
"Oldest First": "最舊的優先",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Options": "選項",
|
||||
"Out of Sync": "不同步",
|
||||
"Out of Sync": "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 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 資料夾)。",
|
||||
"Pause": "暫停",
|
||||
"Paused": "暫停",
|
||||
"Please consult the release notes before performing a major upgrade.": "執行重大升級前請先參閱版本資訊。",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "請在設定對話方塊內設置 GUI 使用者認證名稱及密碼。",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Please set a GUI Authentication User and Password in the Settings dialog.",
|
||||
"Please wait": "請稍後",
|
||||
"Preview": "預覽",
|
||||
"Preview Usage Report": "預覽使用資訊報告",
|
||||
"Quick guide to supported patterns": "可支援樣式的快速指南",
|
||||
"RAM Utilization": "記憶體使用",
|
||||
"Random": "隨機",
|
||||
"Relay Servers": "中繼伺服器",
|
||||
"Relay Servers": "Relay Servers",
|
||||
"Relayed via": "中繼於",
|
||||
"Relays": "中繼點",
|
||||
"Release Notes": "版本資訊",
|
||||
"Remote Devices": "遠端裝置",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remove": "移除",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "重新掃描",
|
||||
@@ -160,11 +159,11 @@
|
||||
"Share Folder": "分享資料夾",
|
||||
"Share Folders With Device": "與裝置共享資料夾",
|
||||
"Share With Devices": "與這些裝置共享",
|
||||
"Share this folder?": "分享此資料夾?",
|
||||
"Share this folder?": "分享此資料夾?",
|
||||
"Shared With": "與誰共享",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "資料夾的簡短識別字。必須在叢集內所有的裝置上皆相同。",
|
||||
"Show ID": "顯示識別碼",
|
||||
"Show QR": "顯示 QR 碼",
|
||||
"Show QR": "Show QR",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "代替裝置識別碼顯示在叢集狀態中。這段文字將會廣播到其他的裝置作為一個可選的預設名稱。",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "代替裝置識別碼顯示在叢集狀態中。本欄若未填寫則將被更新為此裝置所廣播的名稱。",
|
||||
"Shutdown": "關閉",
|
||||
@@ -184,14 +183,14 @@
|
||||
"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 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 seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
|
||||
"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.": "裝置識別碼不能為空白。",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).",
|
||||
"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).": "要輸入在這裡的裝置識別碼,可以在其它裝置的 \"編輯 > 顯示識別碼\" 對話框找到。空白以及連接符號可不輸入 (省略)。",
|
||||
"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).": "要輸入在這裡的裝置識別碼可以在其他裝置的 \"編輯 > 顯示識別碼\" 對話框找到。空白以及連接符號可不輸入 (省略)",
|
||||
"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.": "經過加密的使用資訊報告會每天傳送。報告是用來追蹤常用的平台、資料夾的大小以及應用程式的版本。若傳送的資料集有異動,您會再次看到這個對話框。",
|
||||
"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.": "輸入的裝置識別碼似乎無效。它應該為一串包含半形英文字母及數字,並可能會含有空白或連接符號的字串,且長度為 52 或 56 個字元。",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "The first command line parameter is the folder path and the second parameter is the relative path in the folder.",
|
||||
@@ -212,7 +211,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "限制速率必須為非負的數字 (0: 不設限制)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "重新掃描間隔必須為一個非負數的秒數。",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "解決間題後,將會自動重試和同步。",
|
||||
"This Device": "本機",
|
||||
"This Device": "This Device",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "This can easily give hackers access to read and change any files on your computer.",
|
||||
"This is a major version upgrade.": "這是一個主要版本更新。",
|
||||
"Trash Can File Versioning": "Trash Can File Versioning",
|
||||
@@ -239,6 +238,5 @@
|
||||
"full documentation": "完整說明文件",
|
||||
"items": "個項目",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想要分享資料夾 \"{{folder}}\"。",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} 想要分享資料夾 \"{{folderLabel}}\" ({{folder}})。",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} 想要分享資料夾 \"{{folderlabel}}\" ({{folder}})。"
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user