Compare commits

...

27 Commits

Author SHA1 Message Date
Simon Frei
bea6ecaf35 lib/scanner, lib/model: Actually assign version when un-ignoring (fixes #4841) (#4842)
This fixes a mistake introduced in #4750 and #4776 and is relevant to v0.14.46-rc1
2018-03-27 16:25:36 -04:00
Jakob Borg
470ef87dd5 vendor: Don't panic in FS watcher on old FreeBSD (fixes #4806) (#4809)
This adds a recover step to the notify package to avoid the panic. We
should get something like this upstreamed.
2018-03-13 13:31:26 +01:00
Simon Frei
b31bad1c4d lib/model: Remove unused shouldIgnore function (#4805) 2018-03-12 20:16:40 +01:00
Simon Frei
8b4346c3ec lib/scanner, lib/fs: Don't create file infos with abs paths (fixes #4799) (#4800) 2018-03-12 13:18:59 +01:00
Simon Frei
2bdb37d412 cmd/syncthing: More information in help about -logfile option (#4796) 2018-03-12 13:17:12 +01:00
Jakob Borg
1471c15b29 cmd/syncthing, lib/db: Be nicer about dropping deltas on upgrade (#4798)
When dropping delta index IDs due to upgrade, only drop our local one.
Previously, when dropping all of them, we would trigger a full send in
both directions on first connect after upgrade. Then the other side
would upgrade, doing the same thing. Net effect is full index data gets
sent twice in both directions.

With this change we just drop our local ID, meaning we will send our
full index on first connect after upgrade. When the other side upgrades,
they will do the same. This is a bit less cruel.
2018-03-10 11:42:01 +01:00
Jakob Borg
4b1782cf6d gui, man: Update docs & translations 2018-03-07 07:45:16 +01:00
Jakob Borg
71fab4d250 cmd/stdiscosrv: Record time of failed lookup
So that we can eventually garbage collect keys that noone is asking
about any more.
2018-03-06 16:15:29 +01:00
Jakob Borg
22ebc80329 cmd/stdiscosrv: Expose process metrics
Like this:

    $ curl -s http://localhost:9098/metrics | egrep '^syncthing_discovery_process'
    syncthing_discovery_process_cpu_seconds_total 12.92
    syncthing_discovery_process_max_fds 10240
    syncthing_discovery_process_open_fds 51
    syncthing_discovery_process_resident_memory_bytes 1.3674496e+08
    syncthing_discovery_process_start_time_seconds 1.52034731837e+09
    syncthing_discovery_process_virtual_memory_bytes 1.40324864e+08
2018-03-06 15:43:36 +01:00
Jakob Borg
74b820f287 test: Update conflict tests 2018-03-04 14:52:09 +01:00
Simon Frei
3949b750f5 gui: In remote need use index and auto-expand if only one folder (fixes #4759) (#4781) 2018-03-01 16:21:35 +01:00
Jakob Borg
a26778fa9b gui, man: Update docs & translations 2018-02-28 07:45:16 +01:00
Jakob Borg
d4b7be009c cmd/syncthing: Reset delta indexes on upgrade 2018-02-26 22:22:19 +00:00
Audrius Butkevicius
2751be57dc lib/connections: Fix relay connections when two devices use the same relay (fixes #4778) (#4779) 2018-02-25 16:12:46 +01:00
Audrius Butkevicius
8df90bb475 lib/scanner: Track modified by in symlinks (#4777) 2018-02-25 14:51:37 +01:00
Simon Frei
36251b86f7 lib/model: Mark deleted file as conflicting when un-ignoring (#4776)
This completes #4750 as a followup to #4765.
2018-02-25 13:03:55 +01:00
Jakob Borg
42cc64e2ed lib/config, lib/model: Auto adjust pullers based on desired amount of pending data (#4748)
This makes the number of pullers vary with the desired amount of outstanding requests instead of being a fixed number.
2018-02-25 10:14:02 +01:00
Simon Frei
158859a1e2 lib: Handle metadata changes for send-only folders (fixes #4616, fixes #4627) (#4750)
Unignored files are marked as conflicting while scanning, which is then resolved
in the subsequent pull. Automatically reconciles needed items on send-only
folders, if they do not actually differ except for internal metadata.
2018-02-25 09:39:00 +01:00
Simon Frei
5822222c74 lib/model: More precise deletion detection (fixes #2571, fixes #4573) (#4765) 2018-02-25 09:27:54 +01:00
Jakob Borg
e99be02055 lib/model: Don't panic if block size in index is larger than protocol block size
This doesn't happen today, but it might in the future if the block size
were increased or made variable and we were talking to a client from the
future.
2018-02-24 07:34:23 -05:00
Matic Potočnik
1901a5a9f4 all: Fix typos (#4772)
Skip-check: authors
2018-02-24 08:51:29 +01:00
Jakob Borg
a27032f09e cmd/strelaysrv: Don't patch the default HTTP client (fixes #4745) 2018-02-21 09:56:04 -05:00
Jakob Borg
5e041dca9f cmd/strelaypoolsrv: Return better error codes and messages (#4770)
The current 500 "test failed" looks and sounds like a problem in the
relay pool server, while it actually indicates a problem on the
announcing side. Instead use 400 "connection test failed" to indicate
that the request was bad and what was the test.
2018-02-21 12:53:49 +01:00
Jakob Borg
c9ec6159e8 lib/fs, vendor: s/zillode/Zillode/ 2018-02-21 08:27:33 +01:00
Jakob Borg
5b17aae1b2 cmd/syncthing: Fix help text for STRECHECKDBEVERY (fixes #4764) 2018-02-21 08:26:57 +01:00
Jakob Borg
5931231a32 gui, man: Update docs & translations 2018-02-21 07:45:16 +01:00
Simon Frei
6802505dda lib/fs: Fix MkdirAll failure due to \\?\ (fixes #4762) (#4763) 2018-02-16 15:19:20 +01:00
133 changed files with 1714 additions and 739 deletions

View File

@@ -195,6 +195,7 @@ func (s *apiSrv) handleGET(ctx context.Context, w http.ResponseWriter, req *http
if misses%notFoundMissesWriteInterval == 0 {
rec.Misses = misses
rec.Missed = time.Now().UnixNano()
rec.Addresses = nil
// rec.Seen retained from get
s.db.put(key, rec)

View File

@@ -36,6 +36,7 @@ type DatabaseRecord struct {
Addresses []DatabaseAddress `protobuf:"bytes,1,rep,name=addresses" json:"addresses"`
Misses int32 `protobuf:"varint,2,opt,name=misses,proto3" json:"misses,omitempty"`
Seen int64 `protobuf:"varint,3,opt,name=seen,proto3" json:"seen,omitempty"`
Missed int64 `protobuf:"varint,4,opt,name=missed,proto3" json:"missed,omitempty"`
}
func (m *DatabaseRecord) Reset() { *m = DatabaseRecord{} }
@@ -106,6 +107,11 @@ func (m *DatabaseRecord) MarshalTo(dAtA []byte) (int, error) {
i++
i = encodeVarintDatabase(dAtA, i, uint64(m.Seen))
}
if m.Missed != 0 {
dAtA[i] = 0x20
i++
i = encodeVarintDatabase(dAtA, i, uint64(m.Missed))
}
return i, nil
}
@@ -221,6 +227,9 @@ func (m *DatabaseRecord) Size() (n int) {
if m.Seen != 0 {
n += 1 + sovDatabase(uint64(m.Seen))
}
if m.Missed != 0 {
n += 1 + sovDatabase(uint64(m.Missed))
}
return n
}
@@ -367,6 +376,25 @@ func (m *DatabaseRecord) Unmarshal(dAtA []byte) error {
break
}
}
case 4:
if wireType != 0 {
return fmt.Errorf("proto: wrong wireType = %d for field Missed", wireType)
}
m.Missed = 0
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowDatabase
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := dAtA[iNdEx]
iNdEx++
m.Missed |= (int64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
default:
iNdEx = preIndex
skippy, err := skipDatabase(dAtA[iNdEx:])
@@ -723,21 +751,22 @@ var (
func init() { proto.RegisterFile("database.proto", fileDescriptorDatabase) }
var fileDescriptorDatabase = []byte{
// 254 bytes of a gzipped FileDescriptorProto
// 264 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x00, 0x00, 0x00, 0x02, 0xff, 0xe2, 0xe2, 0x4b, 0x49, 0x2c, 0x49,
0x4c, 0x4a, 0x2c, 0x4e, 0xd5, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0x62, 0xc9, 0x4d, 0xcc, 0xcc,
0x93, 0xd2, 0x4d, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xcf, 0x4f,
0xcf, 0xd7, 0x07, 0x4b, 0x26, 0x95, 0xa6, 0x81, 0x79, 0x60, 0x0e, 0x98, 0x05, 0xd1, 0xa4, 0x54,
0xce, 0xc5, 0xe7, 0x02, 0x35, 0x26, 0x28, 0x35, 0x39, 0xbf, 0x28, 0x45, 0xc8, 0x92, 0x8b, 0x33,
0x31, 0x25, 0xa5, 0x28, 0xb5, 0xb8, 0x38, 0xb5, 0x58, 0x82, 0x51, 0x81, 0x59, 0x83, 0xdb, 0x48,
0x54, 0x0f, 0x64, 0xb4, 0x1e, 0x4c, 0xa1, 0x23, 0x44, 0xda, 0x89, 0xe5, 0xc4, 0x3d, 0x79, 0x86,
0x20, 0x84, 0x6a, 0x21, 0x31, 0x2e, 0xb6, 0xdc, 0x4c, 0xb0, 0x3e, 0x26, 0x05, 0x46, 0x0d, 0xd6,
0x20, 0x28, 0x4f, 0x48, 0x88, 0x8b, 0xa5, 0x38, 0x35, 0x35, 0x4f, 0x82, 0x59, 0x81, 0x51, 0x83,
0x39, 0x08, 0xcc, 0x56, 0x2a, 0xe1, 0x12, 0x0c, 0x4a, 0x2d, 0xc8, 0xc9, 0x4c, 0x4e, 0x2c, 0xc9,
0xcc, 0xcf, 0x83, 0xda, 0x2d, 0xc0, 0xc5, 0x9c, 0x9d, 0x5a, 0x29, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1,
0x19, 0x04, 0x62, 0xa2, 0xba, 0x86, 0x89, 0x24, 0xd7, 0x60, 0xb3, 0xd5, 0x95, 0x8b, 0x1f, 0x4d,
0x9f, 0x90, 0x04, 0x17, 0x3b, 0x54, 0x0f, 0xd4, 0x5e, 0x18, 0x17, 0x24, 0x93, 0x5a, 0x51, 0x90,
0x59, 0x04, 0xf5, 0x0f, 0x73, 0x10, 0x8c, 0xeb, 0x24, 0x70, 0xe2, 0xa1, 0x1c, 0xc3, 0x89, 0x47,
0x72, 0x8c, 0x17, 0x1e, 0xc9, 0x31, 0x3e, 0x78, 0x24, 0xc7, 0x98, 0xc4, 0x06, 0x0e, 0x4e, 0x63,
0x40, 0x00, 0x00, 0x00, 0xff, 0xff, 0x9e, 0x45, 0x60, 0x7e, 0x95, 0x01, 0x00, 0x00,
0xcf, 0xd7, 0x07, 0x4b, 0x26, 0x95, 0xa6, 0x81, 0x79, 0x60, 0x0e, 0x98, 0x05, 0xd1, 0xa4, 0xd4,
0xcf, 0xc8, 0xc5, 0xe7, 0x02, 0x35, 0x27, 0x28, 0x35, 0x39, 0xbf, 0x28, 0x45, 0xc8, 0x92, 0x8b,
0x33, 0x31, 0x25, 0xa5, 0x28, 0xb5, 0xb8, 0x38, 0xb5, 0x58, 0x82, 0x51, 0x81, 0x59, 0x83, 0xdb,
0x48, 0x54, 0x0f, 0x64, 0xb6, 0x1e, 0x4c, 0xa1, 0x23, 0x44, 0xda, 0x89, 0xe5, 0xc4, 0x3d, 0x79,
0x86, 0x20, 0x84, 0x6a, 0x21, 0x31, 0x2e, 0xb6, 0xdc, 0x4c, 0xb0, 0x3e, 0x26, 0x05, 0x46, 0x0d,
0xd6, 0x20, 0x28, 0x4f, 0x48, 0x88, 0x8b, 0xa5, 0x38, 0x35, 0x35, 0x4f, 0x82, 0x59, 0x81, 0x51,
0x83, 0x39, 0x08, 0xcc, 0x86, 0xab, 0x4d, 0x91, 0x60, 0x01, 0x8b, 0x42, 0x79, 0x4a, 0x25, 0x5c,
0x82, 0x41, 0xa9, 0x05, 0x39, 0x99, 0xc9, 0x89, 0x25, 0x99, 0xf9, 0x79, 0x50, 0x37, 0x09, 0x70,
0x31, 0x67, 0xa7, 0x56, 0x4a, 0x30, 0x2a, 0x30, 0x6a, 0x70, 0x06, 0x81, 0x98, 0xa8, 0xae, 0x64,
0x22, 0xc9, 0x95, 0x58, 0x5c, 0xa3, 0xe4, 0xca, 0xc5, 0x8f, 0xa6, 0x4f, 0x48, 0x82, 0x8b, 0x1d,
0xaa, 0x07, 0x6a, 0x2f, 0x8c, 0x0b, 0x92, 0x49, 0xad, 0x28, 0xc8, 0x2c, 0x82, 0xfa, 0x93, 0x39,
0x08, 0xc6, 0x75, 0x12, 0x38, 0xf1, 0x50, 0x8e, 0xe1, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x8f, 0xe4,
0x18, 0x1f, 0x3c, 0x92, 0x63, 0x4c, 0x62, 0x03, 0x87, 0xb3, 0x31, 0x20, 0x00, 0x00, 0xff, 0xff,
0x6a, 0x22, 0xa2, 0x85, 0xae, 0x01, 0x00, 0x00,
}

View File

@@ -14,10 +14,13 @@ option (gogoproto.goproto_getters_all) = false;
message DatabaseRecord {
repeated DatabaseAddress addresses = 1 [(gogoproto.nullable) = false];
int32 misses = 2; // Number of lookups without hits
int32 misses = 2; // Number of lookups* without hits
int64 seen = 3; // Unix nanos, last device announce
int64 missed = 4; // Unix nanos, last* failed lookup
}
// *) Not every lookup results in a write, so may not be completely accurate
message ReplicationRecord {
string key = 1;
repeated DatabaseAddress addresses = 2 [(gogoproto.nullable) = false];

View File

@@ -154,7 +154,7 @@ func TestDatabaseGetSet(t *testing.T) {
}
if len(rec.Addresses) != 1 {
t.Log(rec.Addresses)
t.Fatal("should have one addres")
t.Fatal("should have one address")
}
if rec.Misses != 0 {
t.Log(rec.Misses)

View File

@@ -149,7 +149,7 @@ func (m replicationMultiplexer) send(key string, ps []DatabaseAddress, seen int6
}
}
// replicationListener acceptes incoming connections and reads replication
// replicationListener accepts incoming connections and reads replication
// items from them. Incoming items are applied to the KV store.
type replicationListener struct {
addr string

View File

@@ -7,6 +7,8 @@
package main
import (
"os"
"github.com/prometheus/client_golang/prometheus"
)
@@ -105,4 +107,6 @@ func init() {
replicationSendsTotal, replicationRecvsTotal,
databaseKeys, databaseStatisticsSeconds,
databaseOperations, databaseOperationSeconds)
prometheus.MustRegister(prometheus.NewProcessCollector(os.Getpid(), "syncthing_discovery"))
}

View File

@@ -90,6 +90,10 @@ var (
evictionTimers = make(map[string]*time.Timer)
)
const (
httpStatusEnhanceYourCalm = 429
)
func main() {
flag.StringVar(&listen, "listen", listen, "Listen address")
flag.StringVar(&dir, "keys", dir, "Directory where http-cert.pem and http-key.pem is stored for TLS listening")
@@ -344,7 +348,7 @@ func handlePostRequest(w http.ResponseWriter, r *http.Request) {
if debug {
log.Println("Asked to add a relay", newRelay, "which exists in permanent list")
}
http.Error(w, "Invalid request", 500)
http.Error(w, "Invalid request", http.StatusBadRequest)
return
}
}
@@ -355,7 +359,7 @@ func handlePostRequest(w http.ResponseWriter, r *http.Request) {
case requests <- request{newRelay, uri, reschan}:
result := <-reschan
if result.err != nil {
http.Error(w, result.err.Error(), 500)
http.Error(w, result.err.Error(), http.StatusBadRequest)
return
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
@@ -367,7 +371,7 @@ func handlePostRequest(w http.ResponseWriter, r *http.Request) {
if debug {
log.Println("Dropping request")
}
w.WriteHeader(429)
w.WriteHeader(httpStatusEnhanceYourCalm)
}
}
@@ -380,7 +384,7 @@ func requestProcessor() {
if debug {
log.Println("Test for relay", request.relay, "failed")
}
request.result <- result{fmt.Errorf("test failed"), 0}
request.result <- result{fmt.Errorf("connection test failed"), 0}
continue
}

View File

@@ -86,6 +86,12 @@ var (
pprofEnabled bool
)
// httpClient is the HTTP client we use for outbound requests. It has a
// timeout and may get further options set during initialization.
var httpClient = &http.Client{
Timeout: 30 * time.Second,
}
func main() {
log.SetFlags(log.Lshortfile | log.LstdFlags)
@@ -129,14 +135,14 @@ func main() {
if err != nil {
log.Fatal(err)
}
if laddr.IP != nil && !laddr.IP.IsUnspecified() {
// We bind to a specific address. Our outgoing HTTP requests should
// also come from that address.
laddr.Port = 0
transport, ok := http.DefaultTransport.(*http.Transport)
if ok {
transport.Dial = (&net.Dialer{
Timeout: 30 * time.Second,
LocalAddr: laddr,
}).Dial
boundDialer := &net.Dialer{LocalAddr: laddr}
httpClient.Transport = &http.Transport{
DialContext: boundDialer.DialContext,
}
}

View File

@@ -7,7 +7,6 @@ import (
"encoding/json"
"io/ioutil"
"log"
"net/http"
"net/url"
"time"
)
@@ -27,7 +26,7 @@ func poolHandler(pool string, uri *url.URL, mapping mapping) {
uriCopy.String(),
})
resp, err := http.Post(pool, "application/json", &b)
resp, err := httpClient.Post(pool, "application/json", &b)
if err != nil {
log.Println("Error joining pool", pool, err)
} else if resp.StatusCode == 500 {

View File

@@ -197,7 +197,7 @@ are mostly useful for developers. Use with care.
"minio" for the github.com/minio/sha256-simd implementation,
and blank (the default) for auto detection.
STDBCHECKEVERY Set to a time interval to override the default database
STRECHECKDBEVERY Set to a time interval to override the default database
check interval of 30 days (720h). The interval understands
"h", "m" and "s" abbreviations for hours minutes and seconds.
Valid values are like "720h", "30s", etc.
@@ -303,7 +303,7 @@ func parseCommandLineOptions() RuntimeOptions {
flag.BoolVar(&options.verbose, "verbose", false, "Print verbose log output")
flag.BoolVar(&options.paused, "paused", false, "Start with all devices and folders paused")
flag.BoolVar(&options.unpaused, "unpaused", false, "Start with all devices and folders unpaused")
flag.StringVar(&options.logFile, "logfile", options.logFile, "Log file name (use \"-\" for stdout)")
flag.StringVar(&options.logFile, "logfile", options.logFile, "Log file name (still always logs to stdout). Cannot be used together with -no-restart/STNORESTART environment variable.")
flag.StringVar(&options.auditFile, "auditfile", options.auditFile, "Specify audit file (use \"-\" for stdout, \"--\" for stderr)")
if runtime.GOOS == "windows" {
// Allow user to hide the console window
@@ -720,14 +720,14 @@ 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?")
}
if runtimeOptions.resetDeltaIdxs {
l.Infoln("Reinitializing delta index IDs")
ldb.DropDeltaIndexIDs()
ldb.DropLocalDeltaIndexIDs()
ldb.DropRemoteDeltaIndexIDs()
}
protectedFiles := []string{
@@ -746,28 +746,33 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
}
}
if cfg.RawCopy().OriginalVersion == 15 {
// The config version 15->16 migration is about handling ignores and
// delta indexes and requires that we drop existing indexes that
// have been incorrectly ignore filtered.
ldb.DropDeltaIndexIDs()
}
if cfg.RawCopy().OriginalVersion < 19 {
// Converts old symlink types to new in the entire database.
ldb.ConvertSymlinkTypes()
}
if cfg.RawCopy().OriginalVersion < 26 {
// Adds invalid (ignored) files to global list of files
changed := 0
for folderID, folderCfg := range folders {
changed += ldb.AddInvalidToGlobal([]byte(folderID), protocol.LocalDeviceID[:])
for _, deviceCfg := range folderCfg.Devices {
changed += ldb.AddInvalidToGlobal([]byte(folderID), deviceCfg.DeviceID[:])
}
// Grab the previously running version string from the database.
miscDB := db.NewNamespacedKV(ldb, string(db.KeyTypeMiscData))
prevVersion, _ := miscDB.String("prevVersion")
// Strip away prerelease/beta stuff and just compare the release
// numbers. 0.14.44 to 0.14.45-banana is an upgrade, 0.14.45-banana to
// 0.14.45-pineapple is not.
prevParts := strings.Split(prevVersion, "-")
curParts := strings.Split(Version, "-")
if prevParts[0] != curParts[0] {
if prevVersion != "" {
l.Infoln("Detected upgrade from", prevVersion, "to", Version)
}
l.Infof("Database update: Added %d ignored files to the global list", changed)
// Drop delta indexes in case we've changed random stuff we
// shouldn't have. We will resend our index on next connect.
ldb.DropLocalDeltaIndexIDs()
// Remember the new version.
miscDB.PutString("prevVersion", Version)
}
// Potential database transitions
ldb.UpdateSchema()
m := model.NewModel(cfg, myID, "syncthing", Version, ldb, protectedFiles)
if t := os.Getenv("STDEADLOCKTIMEOUT"); t != "" {

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Разрешени мрежи",
"Alphabetic": "Азбучен ред",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Друга команда се занимава с версиите. Тази команда трябва да премахне файла от синхронизираната папка.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Външна команда се занимава с версиите. Тази команда трябва да премахне файла от синхронизираната папка. Ако пътят до това приложение използва интервали, то той трябва да бъде заграден в кавички.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Друга команда се занимава с версиите. Тази команда трябва да премахни файла от синхронизираната папка.",
"Anonymous Usage Reporting": "Анонимен доклад",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Форматът на анонимния доклад е променен. Желаете ли да преминете към новия формат?",
@@ -33,7 +34,7 @@
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Автоматичното обновяване вече предлага избор между стабилни версии и кандидат версии.",
"Automatic upgrades": "Автоматично обновяване",
"Automatically create or share folders that this device advertises at the default path.": "Автоматично създаване или споделяне на папки, които това устройство предлага в пътя по подразбиране.",
"Available debug logging facilities:": "Available debug logging facilities:",
"Available debug logging facilities:": "Дебъгинг функционалност на разположение:",
"Be careful!": "Внимание!",
"Bugs": "Бъгове",
"CPU Utilization": "Използван процесор",
@@ -65,9 +66,9 @@
"Device that last modified the item": "Устройство, което последно промени обекта",
"Devices": "Устройства",
"Disabled": "Деактивирано",
"Disabled periodic scanning and disabled watching for changes": "Disabled periodic scanning and disabled watching for changes",
"Disabled periodic scanning and enabled watching for changes": "Disabled periodic scanning and enabled watching for changes",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:",
"Disabled periodic scanning and disabled watching for changes": "Периодичните сканирвания и наблюденията за промяна са деактивирани.",
"Disabled periodic scanning and enabled watching for changes": "Периодичните сканирвания са деактивирани , а наблюденията за промяна са активирани.",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Периодичните сканирвания са деактивирани и задаването на наблюдение за промени е неуспешно, ще опита пак след 1мин:",
"Disconnected": "Не е свързано",
"Discovered": "Открит",
"Discovery": "Откриване",
@@ -85,7 +86,7 @@
"Editing {%path%}.": "Промяна на {{path}}.",
"Enable NAT traversal": "Разреши NAT traversal",
"Enable Relaying": "Разреши препращане",
"Enabled": "Enabled",
"Enabled": "Активирано",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Въведете не отрицателно число (пр. \"2.35\") и изберете единица.\nПроцентите са като част от размера на цялото дисково пространство.",
"Enter a non-privileged port number (1024 - 65535).": "Въведете непривилегирован номер на порт (1024-65535).",
"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\", за да автоматично откриване на наличните адреси.",
@@ -93,7 +94,7 @@
"Error": "Грешка",
"External File Versioning": "Външно управление на версиите",
"Failed Items": "Неуспешни",
"Failed to setup, retrying": "Failed to setup, retrying",
"Failed to setup, retrying": "Неуспешно конфигуриране, правне на повторен опит",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Неуспешна връзка към IPv6 сървъри може да се очаква ако няма IPv6 свързаност.",
"File Pull Order": "Ред на сваляне",
"File Versioning": "Версии на файловете",
@@ -188,9 +189,9 @@
"Pause": "Пауза",
"Pause All": "Пауза на висчко",
"Paused": "На пауза",
"Periodic scanning at given interval and disabled watching for changes": "Periodic scanning at given interval and disabled watching for changes",
"Periodic scanning at given interval and enabled watching for changes": "Periodic scanning at given interval and enabled watching for changes",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:",
"Periodic scanning at given interval and disabled watching for changes": "Периодичните сканирвания на определен интервал са активирани, а наблюденията за промени са деактивирани",
"Periodic scanning at given interval and enabled watching for changes": "Периодичните сканирвания на определен интервал и наблюденията за промени са активирани",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Периодичните сканирвания са деактивирани и задаването на наблюдение за промени е неуспешно, ще опита всяка следваща минута:",
"Please consult the release notes before performing a major upgrade.": "Моля прочети бележките по обновяването преди да започнеш.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Моля задайте потребителско име и парола за потребителския интерфейс в секцията Настройки.",
"Please wait": "Моля изчакай",
@@ -213,7 +214,7 @@
"Rescan": "Сканирай",
"Rescan All": "Обнови всички",
"Rescan Interval": "Интервал за повторно сканиране",
"Rescans": "Rescans",
"Rescans": "Повторни сканирвания",
"Restart": "Рестартирай",
"Restart Needed": "Изисква се рестартиране",
"Restarting": "Рестартиране",
@@ -222,7 +223,7 @@
"Resume": "Пусни",
"Resume All": "Пускане на всичко",
"Reused": "Повторно използван",
"Running": "Running",
"Running": "Изпълнява се",
"Save": "Запази",
"Scan Time Remaining": "Оставащо време за сканиране",
"Scanning": "Сканиране",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Xarxes permeses",
"Alphabetic": "Alfabètic",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Un command extern maneja les versions. Té que eliminar el fitxer de la carpeta compartida.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Un comando extern controla el versionat. És necessari eliminar el fitxer de la carpeta sincronitzada.",
"Anonymous Usage Reporting": "Informe d'ús anònim",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymous usage report format has changed. Would you like to move to the new format?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Povolené sítě",
"Alphabetic": "Abecedně",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Verzování obstarává externí příkaz. Musí odstranit soubor ze sdíleného adresáře.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Verzování obstarává externí skript. Musí odstranit soubor ze sdíleného adresáře. Pokud cesta ke skriptu obsahuje mezeru, měla by být v uvozovkách.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Verzování obstarává externí příkaz. Musí odstranit soubor ze sdíleného adresáře.",
"Anonymous Usage Reporting": "Anonymní hlášení o používání",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Formát anonymního hlášení o používání byl změněn. Chcete přejít na nový formát?",
@@ -65,9 +66,9 @@
"Device that last modified the item": "Poslední přístroj, který modifikoval položku",
"Devices": "Přístroje",
"Disabled": "Vypnuto",
"Disabled periodic scanning and disabled watching for changes": "Disabled periodic scanning and disabled watching for changes",
"Disabled periodic scanning and enabled watching for changes": "Disabled periodic scanning and enabled watching for changes",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:",
"Disabled periodic scanning and disabled watching for changes": "Periodické skenování i sledování změn vypnuto",
"Disabled periodic scanning and enabled watching for changes": "Periodické skenování vypnuto; sledování změn zapnuto",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Periodické skenování vypnuto; nastavení sledování změn selhalo, nový pokud každou 1m:",
"Disconnected": "Odpojen",
"Discovered": "Nalezeno",
"Discovery": "Oznamování",
@@ -85,7 +86,7 @@
"Editing {%path%}.": "Editace {{path}}.",
"Enable NAT traversal": "Povolit NAT přenos",
"Enable Relaying": "Povolit přenašeče",
"Enabled": "Enabled",
"Enabled": "Zapnuto",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Zadajte kladné číslo (např. \"2.35\") a zvolte jednotku. Percenta znamenají část celkové velikosti disku.",
"Enter a non-privileged port number (1024 - 65535).": "Zadejte číslo neprivilegovaného portu (1024-65535).",
"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.",
@@ -93,7 +94,7 @@
"Error": "Chyba",
"External File Versioning": "Externí verzování souborů",
"Failed Items": "Selhalo",
"Failed to setup, retrying": "Nastavování selhalo, zkouším znova",
"Failed to setup, retrying": "Nastavování selhalo, zkouším znovu",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Je v pořádku, když připojení k IPv6 serverům selže, pokud není k dispozici IPv6 konektivita.",
"File Pull Order": "Pořadí stahování souborů",
"File Versioning": "Verzování souborů",
@@ -188,9 +189,9 @@
"Pause": "Pozastavit",
"Pause All": "Pozastavit vše",
"Paused": "Pozastaveno",
"Periodic scanning at given interval and disabled watching for changes": "Periodic scanning at given interval and disabled watching for changes",
"Periodic scanning at given interval and enabled watching for changes": "Periodic scanning at given interval and enabled watching for changes",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:",
"Periodic scanning at given interval and disabled watching for changes": "Periodické skenování podle zadaného intervalu; sledování změn vypnuto",
"Periodic scanning at given interval and enabled watching for changes": "Periodické skenování podle zadaného intervalu; sledování změn zapnuto",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodické skenování podle zadaného intervalu; nastavení sledování změn selhalo, nový pokud každou 1m:",
"Please consult the release notes before performing a major upgrade.": "Před spuštěním důležité aktualizace si nejdříve přečtěte poznámky k vydání nové verze.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Zadejte prosím přihlašovací jméno a heslo pro GUI v dialogu nastavení.",
"Please wait": "Chvíli strpení",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Tilladte netværk",
"Alphabetic": "Alfabetisk",
"An external command handles the versioning. It has to remove the file from the shared folder.": "En ekstern kommando styrer versioneringen. Den skal fjerne filen fra den delte mappe.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": " ",
"Anonymous Usage Reporting": "Anonym brugerstatistik",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymous usage report format has changed. Would you like to move to the new format?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Erlaubte Netzwerke",
"Alphabetic": "Alphabetisch",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Ein externer Befehl führt die Versionierung durch. Er muss die Datei aus dem geteilten Ordner entfernen.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ein externer Programmaufruf handhabt die Versionierung. Es muss die Datei aus dem zu synchronisierendem Ordner entfernen.",
"Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Das Format des anonymen Nutzungsberichts hat sich geändert. Möchten Sie auf das neue Format umsteigen?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Επιτρεπόμενα δίκτυα",
"Alphabetic": "Αλφαβητικά",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Μια εξωτερική εντολή χειρίζεται την τήρηση εκδόσεων και αναλαμβάνει να αφαιρέσει το αρχείο από τον συγχρονισμένο φάκελο.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Μια εξωτερική εντολή χειρίζεται την τήρηση εκδόσεων και αναλαμβάνει να αφαιρέσει το αρχείο από τον συγχρονισμένο φάκελο. Αν η διαδρομή προς την εφαρμογή περιέχει διαστήματα, πρέπει να εσωκλείεται σε εισαγωγικά. ",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Μια εξωτερική εντολή χειρίζεται την διαχείριση εκδόσεων. Χρειάζεται να αφαιρέσει το αρχείο από το φάκελο συγχρονισμένων.",
"Anonymous Usage Reporting": "Ανώνυμα στοιχεία χρήσης",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Η μορφή της αναφοράς ανώνυμων στοιχείων χρήσης έχει αλλάξει. Επιθυμείτε να μεταβείτε στη νέα μορφή;",
@@ -309,7 +310,7 @@
"Unknown": "Άγνωστο",
"Unshared": "Δε μοιράζεται",
"Unused": "Δε χρησιμοποιείται",
"Up to Date": "Ενημερωμένος",
"Up to Date": "Ενημερωμένη",
"Updated": "Ενημερωμένο",
"Upgrade": "Αναβάθμιση",
"Upgrade To {%version%}": "Αναβάθμιση στην έκδοση {{version}}",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Allowed Networks",
"Alphabetic": "Alphabetic",
"An external command handles the versioning. It has to remove the file from the shared folder.": "An external command handles the versioning. It has to remove the file from the shared folder.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"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": "Anonymous Usage Reporting",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymous usage report format has changed. Would you like to move to the new format?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Permesitaj Retoj",
"Alphabetic": "Alfabeta",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Ekstera komando manipulas la version. Ĝi devas forigi la dosieron el la partigita dosierujo.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ekstera komando manipulas la version. Ĝi devas forigi la dosieron el la sinkronigita dosierujo.",
"Anonymous Usage Reporting": "Anonima Raporto de Uzado",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymous usage report format has changed. Would you like to move to the new format?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Redes permitidas",
"Alphabetic": "Alfabético",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Un comando externo gestiona las versiones. Tiene que eliminar el fichero de la carpeta compartida.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Un comando externo controla la versión. El fichero debe ser eliminado de la carpeta sincronizada.",
"Anonymous Usage Reporting": "Informe anónimo de uso",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymous usage report format has changed. Would you like to move to the new format?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Redes permitidas",
"Alphabetic": "Alfabético",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Un comando externo gestiona las versiones. Tiene que eliminar el fichero de la carpeta compartida.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Un comando externo controla la versión. El fichero debe ser eliminado de la carpeta sincronizada.",
"Anonymous Usage Reporting": "Informe anónimo de uso",
"Anonymous usage report format has changed. Would you like to move to the new format?": "El formato del informe de uso anónimo a cambiado. ¿Desearía usar el nuevo formato?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Sare baimenduak",
"Alphabetic": "Alfabetikoa",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Kanpoko kontrolagailu batek fitxategien bertsioak kudeatzen ditu. Fitxategiak kendu behar ditu errepertorio sinkronizatuan.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Kanpoko kontrolagailu batek fitxeroen bertsioak erabiltzen ditu. Fitxeroak errepertorio sinkronizatutik desagertaraztea berari doakio.",
"Anonymous Usage Reporting": "Izenik gabeko erabiltze erreportak",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymous usage report format has changed. Would you like to move to the new format?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Réseaux autorisés",
"Alphabetic": "Alphabétique",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Une commande externe gère les versions de fichiers. Il lui incombe de supprimer les fichiers dans le répertoire synchronisé.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Une commande externe gère les versions de fichiers. Il lui incombe de supprimer les fichiers dans le répertoire synchronisé.",
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymous usage report format has changed. Would you like to move to the new format?",

View File

@@ -21,7 +21,8 @@
"Allow Anonymous Usage Reporting?": "Autoriser l'envoi de statistiques d'utilisation anonymisées ?",
"Allowed Networks": "Réseaux autorisés",
"Alphabetic": "Alphabétique",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Une commande externe gère les versions de fichiers. Il lui incombe de supprimer les fichiers du répertoire partagé. Si le chemin contient des espaces, il doit être spécifié entre guillemets.",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Une commande externe gère les versions de fichiers. Il lui incombe de supprimer les fichiers du répertoire partagé.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Une commande externe gère les versions de fichiers. Il lui incombe de supprimer les fichiers du répertoire partagé. Si le chemin contient des espaces, il doit être spécifié entre guillemets.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Une commande externe gère les versions de fichiers. Il lui incombe de supprimer les fichiers dans le répertoire synchronisé.",
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Le format du rapport anonyme d'utilisation a changé. Voulez-vous passer au nouveau format ?",
@@ -92,7 +93,7 @@
"Enter ignore patterns, one per line.": "Entrez les masques d'exclusion, un par ligne.",
"Error": "Erreur",
"External File Versioning": "Gestion externe des versions de fichiers",
"Failed Items": "Fichiers en échec",
"Failed Items": "Éléments en échec",
"Failed to setup, retrying": "Échec, nouvel essai",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "La connexion aux serveurs en IPv6 va échouer s'il n'y a pas de connectivité IPv6.",
"File Pull Order": "Ordre de récupération des fichiers",
@@ -172,7 +173,7 @@
"Normal": "Normal",
"Notice": "Notification",
"OK": "OK",
"Off": "Désactivé(e)",
"Off": "Désactivée",
"Oldest First": "Les plus anciens en premier",
"Optional descriptive label for the folder. Can be different on each device.": "Nom local, convivial et optionnel du partage, à votre guise. il peut être différent sur chaque appareil. Par notification initiale, il sera proposé tel quel aux nouveaux participants.\nAstuce : comme il est modifiable ultérieurement, pensez à indiquer un nom parlant pour les invités, puis renommez-le quand ils l'auront accepté (exemple d'un partage à deux membres où l'initiateur commence par donner son propre nom au partage, puis le renomme plus tard au nom du partenaire quand celui-ci l'a enregistré). Évitez les erreurs d'orthographe car ce nom servira aussi de base au chemin proposé en création (local et distant) et ce chemin est difficilement modifiable.",
"Options": "Options",
@@ -213,7 +214,7 @@
"Rescan": "Réanalyser",
"Rescan All": "Tout réanalyser",
"Rescan Interval": "Intervalle d'analyse",
"Rescans": "Surveillance",
"Rescans": "Analyses/Surveillance",
"Restart": "Redémarrer",
"Restart Needed": "Redémarrage nécessaire",
"Restarting": "Redémarrage en cours",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Tasteane Netwurken",
"Alphabetic": "Alfabetysk",
"An external command handles the versioning. It has to remove the file from the shared folder.": "In ekstern kommando soarget foar it ferzjebehear. It moat de triem út de dielde map fuortsmite.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "In ekstern kommando soarget foar it ferzjebehear. It moat de triem út de dielde map fuortsmite. As it paad nei de applikaasje romtes hat, moat it tusken oanheltekens sette wurden.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "In ekstern kommando soarget foar it ferzjebehear. It moat de triem út de syngronisearre map fuortsmite.",
"Anonymous Usage Reporting": "Anonym brûkensrapportaazje",
"Anonymous usage report format has changed. Would you like to move to the new format?": "It formaat fan de rapportaazje fan anonime gebrûksynformaasje is feroare. Wolle jo op dit nije formaat oerstappe?",
@@ -65,9 +66,9 @@
"Device that last modified the item": "Apparaat dat dit item it lêst oanpast hat",
"Devices": "Apparaten",
"Disabled": "Utskeakele",
"Disabled periodic scanning and disabled watching for changes": "Disabled periodic scanning and disabled watching for changes",
"Disabled periodic scanning and enabled watching for changes": "Disabled periodic scanning and enabled watching for changes",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:",
"Disabled periodic scanning and disabled watching for changes": "Periodic scanning útskeakele en feroarings wurde net mear yn'e gaten hâlden.",
"Disabled periodic scanning and enabled watching for changes": "Periodic scanning útskeakele en feroarings wurde yn'e gaten hâlden.",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Periodic scanning útskeakele en it ynskeakeljen fan it yn'e gaten hâlden fan feroarings is mislearre, wurd eltse 1m opnij besocht:",
"Disconnected": "Ferbining ferbrutsen",
"Discovered": "Untdekt",
"Discovery": "Untdekking",
@@ -85,7 +86,7 @@
"Editing {%path%}.": "{{path}} wurd bewurke.",
"Enable NAT traversal": "NAT-trochkruse ynskeakelje",
"Enable Relaying": "Trochjaan tastean",
"Enabled": "Enabled",
"Enabled": "Ynskeakele",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Fier in net-negatyf nûmer yn (bygelyks \"2.35\") en selektearje in ienheid. Percentages stean foar it part fan de totale skiifromte.",
"Enter a non-privileged port number (1024 - 65535).": "Fier in net-befoarrjochte poart-nûmer yn (1024 - 65535).",
"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.",
@@ -93,7 +94,7 @@
"Error": "Flater",
"External File Versioning": "Ekstern ferzjebehear foar triemen",
"Failed Items": "Mislearre items",
"Failed to setup, retrying": "Failed to setup, retrying",
"Failed to setup, retrying": "Ynskeakeljen mislearre, wurd no opnij besocht",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": " Mislearjen fan it ferbinen mei IPv6-tsjinners wurd ferwachte as der gjin stipe foar IPv6-ferbinings is.",
"File Pull Order": "Triemlûkfolchoarder",
"File Versioning": "Triemferzjebehear",
@@ -188,9 +189,9 @@
"Pause": "Skoftsje",
"Pause All": "Alles skoftsje",
"Paused": "Skoftet",
"Periodic scanning at given interval and disabled watching for changes": "Periodic scanning at given interval and disabled watching for changes",
"Periodic scanning at given interval and enabled watching for changes": "Periodic scanning at given interval and enabled watching for changes",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:",
"Periodic scanning at given interval and disabled watching for changes": "Periodic scanning op opjûn ynterfal en feroarings wurde net yn'e gaten hâlden.",
"Periodic scanning at given interval and enabled watching for changes": "Periodic scanning op opjûn ynterfal en feroarings wurde yn'e gaten hâlden.",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodic scanning op opjûn ynterfal en it ynskeakeljen fan it yn'e gaten hâlden fan feroarings is mislearre, wurd eltse 1m opnij besocht:",
"Please consult the release notes before performing a major upgrade.": "Foardat jo in wichtige fernijing ynstallearre, graach earst de fernijingsoantekenings lêze.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Graach foar GUI-ferifikaasje in brûkers-ID en wachtwurd ynstelle yn it ynstellingsdialooch.",
"Please wait": "In amerijke",
@@ -213,7 +214,7 @@
"Rescan": "Sken opnij",
"Rescan All": "Sken alles opnij",
"Rescan Interval": "Wersken ynterval",
"Rescans": "Rescans",
"Rescans": "Werscans",
"Restart": "Werstarte",
"Restart Needed": "Werstart nedich",
"Restarting": "Oan it werstarten",
@@ -222,7 +223,7 @@
"Resume": "Trochgean",
"Resume All": "Alles trochgean litte",
"Reused": "Opnij brûkt",
"Running": "Running",
"Running": "Rint",
"Save": "Bewarje",
"Scan Time Remaining": "Oerbleaune skentiid",
"Scanning": "Oan it skennen",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Engedélyezett hálózatok",
"Alphabetic": "ABC sorrendben",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Külső program kezeli a fájlverzió-követést. Az távolítja el a fájlt a megosztott mappából.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Külső program kezeli a fájlverzió-követést. Az távolítja el a fájlt a megosztott mappából. Ha az alkalmazás útvonala szóközöket tartalmaz, zárójelezni szükséges az útvonalat.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Külső program kezeli a fájlverzió-követést. Az távolítja el a fájlt a szinkronizált mappából.",
"Anonymous Usage Reporting": "Névtelen felhasználási adatok küldése",
"Anonymous usage report format has changed. Would you like to move to the new format?": "A névtelen használati jelentés formátuma megváltozott. Szeretnél áttérni az új formátumra?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Reti Consentite.",
"Alphabetic": "Alfabetico",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Il controllo versione è gestito da un comando esterno. Quest'ultimo deve rimuovere il file dalla cartella condivisa.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Il controllo versione è gestito da un comando esterno. Quest'ultimo deve rimuovere il file dalla cartella condivisa. Se il percorso dell'applicazione contiene spazi, deve essere indicato tra virgolette.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Il controllo versione è gestito da un comando esterno. Quest'ultimo deve rimuovere il file dalla cartella sincronizzata.",
"Anonymous Usage Reporting": "Statistiche Anonime di Utilizzo",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Il formato delle statistiche anonime di utilizzo è cambiato. Vuoi passare al nuovo formato?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "許可されているネットワーク",
"Alphabetic": "アルファベット順",
"An external command handles the versioning. It has to remove the file from the shared folder.": "外部コマンドでバージョン管理を行います。ここで指定するコマンドは、共有フォルダーからファイルを削除するものでなくてはなりません。",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "外部コマンドにバージョンを管理させます。ここで指定するコマンドは、同期フォルダーからファイルを削除するものでなくてはなりません。",
"Anonymous Usage Reporting": "匿名での使用状況レポート",
"Anonymous usage report format has changed. Would you like to move to the new format?": "匿名での使用状況レポートのフォーマットが変わりました。新形式でのレポートに移行しますか?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "허가된 네트워크",
"Alphabetic": "알파벳순",
"An external command handles the versioning. It has to remove the file from the shared folder.": "외부 커맨드가 파일 버전을 관리합니다. 공유된 폴더에서 파일을 삭제해야 합니다.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "외부 커맨드가 파일 버전을 관리합니다. 동기화된 폴더에서 파일을 삭제해야 합니다.",
"Anonymous Usage Reporting": "익명 사용 보고서",
"Anonymous usage report format has changed. Would you like to move to the new format?": "익명 사용 리포트의 형식이 변경되었습니다. 새 형식으로 이동 하시겠습니까?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Leidžiami tinklai",
"Alphabetic": "Abėcėlės tvarka",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Išorinė komanda apdoroja versijų valdymą. Ji turi pašalinti failą iš bendrinamo aplanko.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Išorinė komanda apdoroja versijų valdymą. Ji turi pašalinti failą iš bendrinamo aplanko. Jei kelyje į programą yra tarpų, jie turėtų būti imami į kabutes.",
"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 report format has changed. Would you like to move to the new format?": "Anoniminės naudojimo ataskaitos formatas pasikeitė. Ar norėtumėte pereiti prie naujojo formato?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Tillatte nettverk",
"Alphabetic": "Alfabetisk",
"An external command handles the versioning. It has to remove the file from the shared folder.": "En ekstern kommando håndterer versjonkontrollen. Den må fjerne filen fra den delte mappa.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "En ekstern kommando håndterer versjonkontrollen. Den må fjerne filen fra den synkroniserte mappa.",
"Anonymous Usage Reporting": "Anonym innsamling av brukerdata",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Det anonyme bruksrapportformatet har endret seg. Ønsker du å gå over til det nye formatet?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Toegestane netwerken",
"Alphabetic": "Alfabetisch",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Een externe opdracht regelt het versiebeheer. Dit moet het bestand verwijderen uit de gedeelde map.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Een externe opdracht regelt het versiebeheer. Dit moet het bestand verwijderen uit de gedeelde map. Als het pad naar de toepassing spaties bevat, moet dit tussen aanhalingstekens geplaatst worden.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Een extern commando regelt het versiebeheer. Dit moet het bestand verwijderen van de gesynchroniseerde map.",
"Anonymous Usage Reporting": "Anonieme gebruikersstatistieken",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Het formaat voor anonieme gebruikersrapporten is gewijzigd. Wilt u naar het nieuwe formaat overschakelen?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Dozwolone sieci",
"Alphabetic": "Alfabetycznie",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Zewnętrzna komenda odpowiedzialna za wersjonowanie. Musi usunąć plik ze współdzielonego folderu.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Zewnętrzna komenda odpowiedzialna za wersjonowanie. Musi usuwać ten plik z synchronizowanego folderu.",
"Anonymous Usage Reporting": "Anonimowe statystyki użycia",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Format anonimowego raportu zużycia uległ zmianie.\nCzy chcesz przejść na nowy format?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Redes permitidas",
"Alphabetic": "Alfabética",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Um comando externo cuida do versionamento. Ele tem que remover o arquivo da pasta compartilhada.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Um programa externo controla o versionamento. Ele tem que remover o arquivo da pasta sincronizada.",
"Anonymous Usage Reporting": "Relatórios anônimos de uso",
"Anonymous usage report format has changed. Would you like to move to the new format?": "O formato do relatório anônimo de uso mudou. Gostaria de usar o formato novo?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Redes permitidas",
"Alphabetic": "Alfabética",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Um comando externo controla as versões. Esse comando tem que remover o ficheiro da pasta partilhada.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Um comando externo controla as versões. Esse comando tem que remover o ficheiro da pasta partilhada. Se o caminho para a aplicação contiver espaços, então terá de o escrever entre aspas.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Um comando externo trata do controle de versões. Esse comando tem que remover o ficheiro da pasta sincronizada.",
"Anonymous Usage Reporting": "Enviar relatórios anónimos de utilização",
"Anonymous usage report format has changed. Would you like to move to the new format?": "O formato do relatório anónimo de utilização foi alterado. Gostaria de mudar para o novo formato?",
@@ -64,10 +65,10 @@
"Device Name": "Nome do dispositivo",
"Device that last modified the item": "Último dispositivo a modificar o item",
"Devices": "Dispositivos",
"Disabled": "Desactivado",
"Disabled periodic scanning and disabled watching for changes": "Verificação periódica desactivada e vigilância de alterações desactivada",
"Disabled periodic scanning and enabled watching for changes": "Verificação periódica desactivada e vigilância de alterações activada",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Verificação periódica desactivada e falha ao preparar a vigilância de alterações, tentando novamente a cada minuto:",
"Disabled": "Desactivada",
"Disabled periodic scanning and disabled watching for changes": "Desactivada a verificação periódica e desactivada a vigilância de alterações",
"Disabled periodic scanning and enabled watching for changes": "Desactivada a verificação periódica e desactivada a vigilância de alterações",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Desactivada a verificação periódica e falha ao preparar a vigilância de alterações, tentando novamente a cada minuto:",
"Disconnected": "Desconectado",
"Discovered": "Descoberto",
"Discovery": "Pesquisa",
@@ -158,7 +159,7 @@
"Metadata Only": "Metadados apenas",
"Minimum Free Disk Space": "Espaço livre mínimo no disco",
"Mod. Device": "Dispositivo mod.",
"Mod. Time": "Quando mod.",
"Mod. Time": "Quando foi mod.",
"Move to top of queue": "Mover para o topo da fila",
"Multi level wildcard (matches multiple directory levels)": "Caractere polivalente multi-nível (faz corresponder a vários níveis de pastas)",
"Never": "Nunca",
@@ -188,9 +189,9 @@
"Pause": "Pausar",
"Pause All": "Pausar todas",
"Paused": "Em pausa",
"Periodic scanning at given interval and disabled watching for changes": "Verificação periódica num dado intervalo e vigilância de alterações desactivada",
"Periodic scanning at given interval and enabled watching for changes": "Verificação periódica num dado intervalo e vigilância de alterações activada",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Verificação periódica num dado intervalo e falha ao preparar a vigilância de alterações, tentando novamente a cada minuto:",
"Periodic scanning at given interval and disabled watching for changes": "Verificação periódica no intervalo dado e desactivada a vigilância de alterações",
"Periodic scanning at given interval and enabled watching for changes": "Verificação periódica no intervalo dado e activada a vigilância de alterações",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Verificação periódica no intervalo dado e falha ao preparar a vigilância de alterações, tentando novamente a cada minuto:",
"Please consult the release notes before performing a major upgrade.": "Consulte as notas de lançamento antes de fazer uma actualização importante.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Por favor, defina um utilizador e senha de autenticação para a interface gráfica, nas configurações.",
"Please wait": "Aguarde",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Разрешённые сети",
"Alphabetic": "По алфавиту",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Для версионирования используется внешняя программа. Ей нужно удалить файл из общей папки.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Внешний процесс управляет версиями файлов. Процесс удалит файл из синхронизируемой папки.",
"Anonymous Usage Reporting": "Анонимный отчет об использовании",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Формат анонимных отчётов изменился. Вы хотите переключиться на новый формат?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Povolené siete",
"Alphabetic": "Abecedne",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Externý príkaz obstaráva verzie. Musí odstrániť ",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Verzie spravuje externý príkaz. Musí odstrániť súbor zo synchronizovaného adresára.",
"Anonymous Usage Reporting": "Anonymné hlásenie o používaní",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymous usage report format has changed. Would you like to move to the new format?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Tillåtna nätverk",
"Alphabetic": "Alfabetisk",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Ett externt kommando hanterar versionshanteringen. Det måste ta bort filen från den delade mappen.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "Ett externt kommando hanterar versionen. Det måste ta bort filen från den delade mappen. Om sökvägen till applikationen innehåller mellanslag bör den citeras.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ett externt kommando sköter versionshanteringen. Den behöver ta bort filen från den synkroniserade mappen.",
"Anonymous Usage Reporting": "Anonym användarstatistiksrapportering",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymt användningsrapportformat har ändrats. Vill du flytta till det nya formatet?",
@@ -65,9 +66,9 @@
"Device that last modified the item": "Enhet som senast ändrade objektet",
"Devices": "Enheter",
"Disabled": "Inaktiverad",
"Disabled periodic scanning and disabled watching for changes": "Disabled periodic scanning and disabled watching for changes",
"Disabled periodic scanning and enabled watching for changes": "Disabled periodic scanning and enabled watching for changes",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:",
"Disabled periodic scanning and disabled watching for changes": "Inaktiverad periodisk skanning och inaktiverad visning av ändringar",
"Disabled periodic scanning and enabled watching for changes": "Inaktiverad periodisk skanning och aktiverad visning av ändringar",
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Inaktiverad periodisk skanning och misslyckad att ställa in visning av ändringar, försök igen varje 1m:",
"Disconnected": "Frånkopplad",
"Discovered": "Upptäckt",
"Discovery": "Annonsering",
@@ -85,7 +86,7 @@
"Editing {%path%}.": "Redigerar {{path}}.",
"Enable NAT traversal": "Aktivera NAT traversering",
"Enable Relaying": "Aktivera vidarebefordra",
"Enabled": "Enabled",
"Enabled": "Aktiverad",
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Ange ett icke-negativt antal (t.ex., \"2.35\") och välj en enhet. Procenttalen är som en del av den totala diskstorleken.",
"Enter a non-privileged port number (1024 - 65535).": "Ange ett icke-privilegierat portnummer (1024 - 65535).",
"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.",
@@ -93,7 +94,7 @@
"Error": "Fel",
"External File Versioning": "Extern filversionshantering",
"Failed Items": "Misslyckade objekt",
"Failed to setup, retrying": "Failed to setup, retrying",
"Failed to setup, retrying": "Misslyckades med att ställa in, försök igen",
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Misslyckande med att ansluta till IPv6-servrar förväntas om ingen IPv6-anslutning finns.",
"File Pull Order": "Filhämtningsprioritering",
"File Versioning": "Filversionshantering",
@@ -188,9 +189,9 @@
"Pause": "Paus",
"Pause All": "Pausa alla",
"Paused": "Pausad",
"Periodic scanning at given interval and disabled watching for changes": "Periodic scanning at given interval and disabled watching for changes",
"Periodic scanning at given interval and enabled watching for changes": "Periodic scanning at given interval and enabled watching for changes",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:",
"Periodic scanning at given interval and disabled watching for changes": "Periodisk skanning i givet intervall och inaktiverad visning av ändringar",
"Periodic scanning at given interval and enabled watching for changes": "Periodisk skanning i givet intervall och aktiverad visning av ändringar",
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodisk skanning i givet intervall och misslyckades med att ställa in visning av ändringar, försök igen varje 1m:",
"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 grafiska gränssnittets användarautentisering och lösenord i inställningsdialogrutan.",
"Please wait": "Var god vänta",
@@ -213,7 +214,7 @@
"Rescan": "Skanna om",
"Rescan All": "Skanna om alla",
"Rescan Interval": "Återskanningsintervall",
"Rescans": "Rescans",
"Rescans": "Omskanningar",
"Restart": "Starta om",
"Restart Needed": "Omstart behövs",
"Restarting": "Startar om",
@@ -222,7 +223,7 @@
"Resume": "Återuppta",
"Resume All": "Återuppta alla",
"Reused": "Återanvänds",
"Running": "Running",
"Running": "Körs",
"Save": "Spara",
"Scan Time Remaining": "Återstående skanningstid",
"Scanning": "Skannar",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Allowed Networks",
"Alphabetic": "Alfabetik",
"An external command handles the versioning. It has to remove the file from the shared folder.": "An external command handles the versioning. It has to remove the file from the shared folder.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Sürümleme işlemini harici bir komut yürütüyor. Dosyayı eşzamanlama klasöründen kaldırmak zorunda.",
"Anonymous Usage Reporting": "Anonim Kullanım Raporlaması",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymous usage report format has changed. Would you like to move to the new format?",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "Дозволені мережі",
"Alphabetic": "За алфавітом",
"An external command handles the versioning. It has to remove the file from the shared folder.": "Зовнішня команда керування версіями. Вона має видалити файл із спільної директорії.",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Зовнішня команда керування версіями. Вона має видалити файл із директорії, що синхронізується.",
"Anonymous Usage Reporting": "Анонімна статистика використання",
"Anonymous usage report format has changed. Would you like to move to the new format?": "Змінився формат анонімного звіту про користування. Бажаєте перейти на новий формат?",

View File

@@ -22,7 +22,8 @@
"Allowed Networks": "允许的网络",
"Alphabetic": "字母顺序",
"An external command handles the versioning. It has to remove the file from the shared folder.": "使用外部命令接管版本控制。该命令必须自行从共享文件夹中删除该文件。",
"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 shared folder. If the path to the application contains spaces, it should be quoted.": "外部命令接管版本控制。它需要从共享文件夹中删除该文件。如果此应用程序的路径包含空格,应该用半角引号括起来。",
"An external command handles the versioning. It has to remove the file from the synced folder.": "外部命令接管了版本控制。它需要从同步文件夹中删除该文件。",
"Anonymous Usage Reporting": "匿名使用报告",
"Anonymous usage report format has changed. Would you like to move to the new format?": "匿名使用情况的报告格式已经变更。是否要迁移到新的格式?",
"Any devices configured on an introducer device will be added to this device as well.": "在中介设备上添加的任何“远程设备”,也会被自动添加到本机的“远程设备”列表。",

View File

@@ -22,6 +22,7 @@
"Allowed Networks": "允許的網路",
"Alphabetic": "字母順序",
"An external command handles the versioning. It has to remove the file from the shared folder.": "處理版本的外部指令。其必須從資料夾中刪除檔案。",
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "處理版本的外部指令。其必須從資料夾中刪除檔案。",
"Anonymous Usage Reporting": "匿名的使用資訊回報",
"Anonymous usage report format has changed. Would you like to move to the new format?": "匿名的使用資訊回報格式已經改變,你想要移至新格式嗎?",

View File

@@ -5,12 +5,12 @@
</div>
<div ng-if="sizeOf(remoteNeed) > 0">
<div class="panel panel-default" ng-repeat="folder in remoteNeedFolders" ng-if="remoteNeed[folder] && remoteNeed[folder].files.length > 0">
<button class="btn panel-heading" data-toggle="collapse" data-target="#remoteNeed-{{folder}}" aria-expanded="false">
<button class="btn panel-heading" data-toggle="collapse" data-target="#remoteNeed-{{$index}}" aria-expanded="false">
<h4 class="panel-title">
<span>{{folderLabel(folder)}}</span>
</h4>
</button>
<div id="remoteNeed-{{folder}}" class="panel-collapse collapse">
<div id="remoteNeed-{{$index}}" class="panel-collapse" ng-class="{collapse: sizeOf(remoteNeedFolders) > 1}">
<div class="panel-body">
<table class="table table-striped">
<thead>

View File

@@ -32,7 +32,7 @@ import (
const (
OldestHandledVersion = 10
CurrentVersion = 26
CurrentVersion = 27
MaxRescanIntervalS = 365 * 24 * 60 * 60
)
@@ -312,6 +312,9 @@ func (cfg *Configuration) clean() error {
if cfg.Version == 25 {
convertV25V26(cfg)
}
if cfg.Version == 26 {
convertV26V27(cfg)
}
// Build a list of available devices
existingDevices := make(map[protocol.DeviceID]bool)
@@ -371,6 +374,17 @@ func (cfg *Configuration) clean() error {
return nil
}
func convertV26V27(cfg *Configuration) {
for i := range cfg.Folders {
f := &cfg.Folders[i]
if f.DeprecatedPullers != 0 {
f.PullerMaxPendingKiB = 128 * f.DeprecatedPullers
f.DeprecatedPullers = 0
}
}
cfg.Version = 27
}
func convertV25V26(cfg *Configuration) {
// triggers database update
cfg.Version = 26

View File

@@ -107,7 +107,6 @@ func TestDeviceConfig(t *testing.T) {
FSWatcherEnabled: false,
FSWatcherDelayS: 10,
Copiers: 0,
Pullers: 0,
Hashers: 0,
AutoNormalize: true,
MinDiskFree: Size{1, "%"},

View File

@@ -40,7 +40,7 @@ type FolderConfiguration struct {
MinDiskFree Size `xml:"minDiskFree" json:"minDiskFree"`
Versioning VersioningConfiguration `xml:"versioning" json:"versioning"`
Copiers int `xml:"copiers" json:"copiers"` // This defines how many files are handled concurrently.
Pullers int `xml:"pullers" json:"pullers"` // Defines how many blocks are fetched at the same time, possibly between separate copier routines.
PullerMaxPendingKiB int `xml:"pullerMaxPendingKiB" json:"pullerMaxPendingKiB"`
Hashers int `xml:"hashers" json:"hashers"` // Less than one sets the value to the number of cores. These are CPU bound due to hashing.
Order PullOrder `xml:"order" json:"order"`
IgnoreDelete bool `xml:"ignoreDelete" json:"ignoreDelete"`
@@ -57,6 +57,7 @@ type FolderConfiguration struct {
DeprecatedReadOnly bool `xml:"ro,attr,omitempty" json:"-"`
DeprecatedMinDiskFreePct float64 `xml:"minDiskFreePct,omitempty" json:"-"`
DeprecatedPullers int `xml:"pullers,omitempty" json:"-"`
}
type FolderDeviceConfiguration struct {

16
lib/config/testdata/v27.xml vendored Normal file
View File

@@ -0,0 +1,16 @@
<configuration version="27">
<folder id="test" path="testdata" type="readonly" ignorePerms="false" rescanIntervalS="600" fsNotifications="false" notifyDelayS="10" autoNormalize="true">
<filesystemType>basic</filesystemType>
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
<minDiskFree unit="%">1</minDiskFree>
<maxConflicts>-1</maxConflicts>
<fsync>true</fsync>
</folder>
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="metadata">
<address>tcp://a</address>
</device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="metadata">
<address>tcp://b</address>
</device>
</configuration>

View File

@@ -341,19 +341,20 @@ func (s *Service) connect() {
l.Debugln("Reconnect loop for", deviceID, addrs)
seen = append(seen, addrs...)
dialTargets := make([]dialTarget, 0)
for _, addr := range addrs {
nextDialAt, ok := nextDial[addr]
// Use a special key that is more than just the address, as you might have two devices connected to the same relay
nextDialKey := deviceID.String() + "/" + addr
seen = append(seen, nextDialKey)
nextDialAt, ok := nextDial[nextDialKey]
if ok && initialRampup >= sleep && nextDialAt.After(now) {
l.Debugf("Not dialing %v as sleep is %v, next dial is at %s and current time is %s", addr, sleep, nextDialAt, now)
l.Debugf("Not dialing %s via %v as sleep is %v, next dial is at %s and current time is %s", deviceID, addr, sleep, nextDialAt, now)
continue
}
// If we fail at any step before actually getting the dialer
// retry in a minute
nextDial[addr] = now.Add(time.Minute)
nextDial[nextDialKey] = now.Add(time.Minute)
uri, err := url.Parse(addr)
if err != nil {
@@ -391,7 +392,7 @@ func (s *Service) connect() {
}
dialer := dialerFactory.New(s.cfg, s.tlsCfg)
nextDial[addr] = now.Add(dialer.RedialFrequency())
nextDial[nextDialKey] = now.Add(dialer.RedialFrequency())
// For LAN addresses, increase the priority so that we
// try these first.

View File

@@ -15,6 +15,8 @@ import (
"github.com/syndtr/goleveldb/leveldb/opt"
)
const dbVersion = 1
const (
KeyTypeDevice = iota
KeyTypeGlobal
@@ -26,6 +28,7 @@ const (
KeyTypeDeviceIdx
KeyTypeIndexID
KeyTypeFolderMeta
KeyTypeMiscData
)
func (l VersionList) String() string {

View File

@@ -83,6 +83,20 @@ func newDBInstance(db *leveldb.DB, location string) *Instance {
return i
}
// UpdateSchema does transitions to the current db version if necessary
func (db *Instance) UpdateSchema() {
miscDB := NewNamespacedKV(db, string(KeyTypeMiscData))
prevVersion, _ := miscDB.Int64("dbVersion")
if prevVersion == 0 {
db.updateSchema0to1()
}
if prevVersion != dbVersion {
miscDB.PutInt64("dbVersion", dbVersion)
}
}
// Committed returns the number of items committed to the database since startup
func (db *Instance) Committed() int64 {
return atomic.LoadInt64(&db.committed)
@@ -324,7 +338,7 @@ func (db *Instance) availability(folder, file []byte) []protocol.DeviceID {
return devices
}
func (db *Instance) withNeed(folder, device []byte, truncate bool, needAllInvalid bool, fn Iterator) {
func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator) {
t := db.newReadOnlyTransaction()
defer t.close()
@@ -351,12 +365,6 @@ func (db *Instance) withNeed(folder, device []byte, truncate bool, needAllInvali
if bytes.Equal(v.Device, device) {
have = true
haveFileVersion = v
// We need invalid files regardless of version when
// ignore patterns changed
if v.Invalid && needAllInvalid {
need = true
break
}
// XXX: This marks Concurrent (i.e. conflicting) changes as
// needs. Maybe we should do that, but it needs special
// handling in the puller.
@@ -531,21 +539,39 @@ func (db *Instance) checkGlobals(folder []byte, meta *metadataTracker) {
l.Debugf("db check completed for %q", folder)
}
// ConvertSymlinkTypes should be run once only on an old database. It
// changes SYMLINK_FILE and SYMLINK_DIRECTORY types to the current SYMLINK
// type (previously SYMLINK_UNKNOWN). It does this for all devices, both
// local and remote, and does not reset delta indexes. It shouldn't really
// matter what the symlink type is, but this cleans it up for a possible
// future when SYMLINK_FILE and SYMLINK_DIRECTORY are no longer understood.
func (db *Instance) ConvertSymlinkTypes() {
func (db *Instance) updateSchema0to1() {
t := db.newReadWriteTransaction()
defer t.close()
dbi := t.NewIterator(util.BytesPrefix([]byte{KeyTypeDevice}), nil)
defer dbi.Release()
conv := 0
symlinkConv := 0
changedFolders := make(map[string]struct{})
ignAdded := 0
meta := newMetadataTracker() // dummy metadata tracker
for dbi.Next() {
folder := db.deviceKeyFolder(dbi.Key())
device := db.deviceKeyDevice(dbi.Key())
name := string(db.deviceKeyName(dbi.Key()))
// Remove files with absolute path (see #4799)
if strings.HasPrefix(name, "/") {
if _, ok := changedFolders[string(folder)]; !ok {
changedFolders[string(folder)] = struct{}{}
}
t.removeFromGlobal(folder, device, nil, nil)
t.Delete(dbi.Key())
t.checkFlush()
continue
}
// Change SYMLINK_FILE and SYMLINK_DIRECTORY types to the current SYMLINK
// type (previously SYMLINK_UNKNOWN). It does this for all devices, both
// local and remote, and does not reset delta indexes. It shouldn't really
// matter what the symlink type is, but this cleans it up for a possible
// future when SYMLINK_FILE and SYMLINK_DIRECTORY are no longer understood.
var f protocol.FileInfo
if err := f.Unmarshal(dbi.Value()); err != nil {
// probably can't happen
@@ -559,99 +585,25 @@ func (db *Instance) ConvertSymlinkTypes() {
}
t.Put(dbi.Key(), bs)
t.checkFlush()
conv++
symlinkConv++
}
}
l.Infof("Updated symlink type for %d index entries", conv)
}
// AddInvalidToGlobal searches for invalid files and adds them to the global list.
// Invalid files exist in the db if they once were not ignored and subsequently
// ignored. In the new system this is still valid, but invalid files must also be
// in the global list such that they cannot be mistaken for missing files.
func (db *Instance) AddInvalidToGlobal(folder, device []byte) int {
t := db.newReadWriteTransaction()
defer t.close()
dbi := t.NewIterator(util.BytesPrefix(db.deviceKey(folder, device, nil)[:keyPrefixLen+keyFolderLen+keyDeviceLen]), nil)
defer dbi.Release()
changed := 0
for dbi.Next() {
var file protocol.FileInfo
if err := file.Unmarshal(dbi.Value()); err != nil {
// probably can't happen
continue
}
if file.Invalid {
changed++
l.Debugf("add invalid to global; folder=%q device=%v file=%q version=%v", folder, protocol.DeviceIDFromBytes(device), file.Name, file.Version)
// this is an adapted version of readWriteTransaction.updateGlobal
name := []byte(file.Name)
gk := t.db.globalKey(folder, name)
var fl VersionList
if svl, err := t.Get(gk, nil); err == nil {
fl.Unmarshal(svl) // skip error, range handles success case
}
nv := FileVersion{
Device: device,
Version: file.Version,
Invalid: file.Invalid,
}
inserted := false
// Find a position in the list to insert this file. The file at the front
// of the list is the newer, the "global".
insert:
for i := range fl.Versions {
switch fl.Versions[i].Version.Compare(file.Version) {
case protocol.Equal:
// Invalid files should go after a valid file of equal version
if nv.Invalid {
continue insert
}
fallthrough
case protocol.Lesser:
// The version at this point in the list is equal to or lesser
// ("older") than us. We insert ourselves in front of it.
fl.Versions = insertVersion(fl.Versions, i, nv)
inserted = true
break insert
case protocol.ConcurrentLesser, protocol.ConcurrentGreater:
// The version at this point is in conflict with us. We must pull
// the actual file metadata to determine who wins. If we win, we
// insert ourselves in front of the loser here. (The "Lesser" and
// "Greater" in the condition above is just based on the device
// IDs in the version vector, which is not the only thing we use
// to determine the winner.)
//
// A surprise missing file entry here is counted as a win for us.
of, ok := t.getFile(folder, fl.Versions[i].Device, name)
if !ok || file.WinsConflict(of) {
fl.Versions = insertVersion(fl.Versions, i, nv)
inserted = true
break insert
}
// Add invalid files to global list
if f.Invalid {
if t.updateGlobal(folder, device, f, meta) {
if _, ok := changedFolders[string(folder)]; !ok {
changedFolders[string(folder)] = struct{}{}
}
ignAdded++
}
if !inserted {
// We didn't find a position for an insert above, so append to the end.
fl.Versions = append(fl.Versions, nv)
}
t.Put(gk, mustMarshal(&fl))
}
}
return changed
for folder := range changedFolders {
db.dropFolderMeta([]byte(folder))
}
l.Infof("Updated symlink type for %d index entries and added %d invalid files to global list", symlinkConv, ignAdded)
}
// deviceKey returns a byte slice encoding the following information:
@@ -751,6 +703,15 @@ func (db *Instance) indexIDKey(device, folder []byte) []byte {
return k
}
func (db *Instance) indexIDDevice(key []byte) []byte {
device, ok := db.deviceIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen:]))
if !ok {
// uuh ...
return nil
}
return device
}
func (db *Instance) mtimesKey(folder []byte) []byte {
prefix := make([]byte, 5) // key type + 4 bytes folder idx number
prefix[0] = KeyTypeVirtualMtime
@@ -765,10 +726,33 @@ func (db *Instance) folderMetaKey(folder []byte) []byte {
return prefix
}
// DropDeltaIndexIDs removes all index IDs from the database. This will
// cause a full index transmission on the next connection.
func (db *Instance) DropDeltaIndexIDs() {
db.dropPrefix([]byte{KeyTypeIndexID})
// DropLocalDeltaIndexIDs removes all index IDs for the local device ID from
// the database. This will cause a full index transmission on the next
// connection.
func (db *Instance) DropLocalDeltaIndexIDs() {
db.dropDeltaIndexIDs(true)
}
// DropRemoteDeltaIndexIDs removes all index IDs for the other devices than
// the local one from the database. This will cause them to send us a full
// index on the next connection.
func (db *Instance) DropRemoteDeltaIndexIDs() {
db.dropDeltaIndexIDs(false)
}
func (db *Instance) dropDeltaIndexIDs(local bool) {
t := db.newReadWriteTransaction()
defer t.close()
dbi := t.NewIterator(util.BytesPrefix([]byte{KeyTypeIndexID}), nil)
defer dbi.Release()
for dbi.Next() {
device := db.indexIDDevice(dbi.Key())
if bytes.Equal(device, protocol.LocalDeviceID[:]) == local {
t.Delete(dbi.Key())
}
}
}
func (db *Instance) dropMtimes(folder []byte) {

View File

@@ -9,6 +9,8 @@ package db
import (
"bytes"
"testing"
"github.com/syncthing/syncthing/lib/protocol"
)
func TestDeviceKey(t *testing.T) {
@@ -62,3 +64,91 @@ func TestGlobalKey(t *testing.T) {
t.Error("should not have been found")
}
}
func TestDropIndexIDs(t *testing.T) {
db := OpenMemory()
d1 := []byte("device67890123456789012345678901")
d2 := []byte("device12345678901234567890123456")
// Set some index IDs
db.setIndexID(protocol.LocalDeviceID[:], []byte("foo"), 1)
db.setIndexID(protocol.LocalDeviceID[:], []byte("bar"), 2)
db.setIndexID(d1, []byte("foo"), 3)
db.setIndexID(d1, []byte("bar"), 4)
db.setIndexID(d2, []byte("foo"), 5)
db.setIndexID(d2, []byte("bar"), 6)
// Verify them
if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 1 {
t.Fatal("fail local 1")
}
if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 2 {
t.Fatal("fail local 2")
}
if db.getIndexID(d1, []byte("foo")) != 3 {
t.Fatal("fail remote 1")
}
if db.getIndexID(d1, []byte("bar")) != 4 {
t.Fatal("fail remote 2")
}
if db.getIndexID(d2, []byte("foo")) != 5 {
t.Fatal("fail remote 3")
}
if db.getIndexID(d2, []byte("bar")) != 6 {
t.Fatal("fail remote 4")
}
// Drop the local ones, verify only they got dropped
db.DropLocalDeltaIndexIDs()
if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 0 {
t.Fatal("fail local 1")
}
if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 0 {
t.Fatal("fail local 2")
}
if db.getIndexID(d1, []byte("foo")) != 3 {
t.Fatal("fail remote 1")
}
if db.getIndexID(d1, []byte("bar")) != 4 {
t.Fatal("fail remote 2")
}
if db.getIndexID(d2, []byte("foo")) != 5 {
t.Fatal("fail remote 3")
}
if db.getIndexID(d2, []byte("bar")) != 6 {
t.Fatal("fail remote 4")
}
// Set local ones again
db.setIndexID(protocol.LocalDeviceID[:], []byte("foo"), 1)
db.setIndexID(protocol.LocalDeviceID[:], []byte("bar"), 2)
// Drop the remote ones, verify only they got dropped
db.DropRemoteDeltaIndexIDs()
if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 1 {
t.Fatal("fail local 1")
}
if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 2 {
t.Fatal("fail local 2")
}
if db.getIndexID(d1, []byte("foo")) != 0 {
t.Fatal("fail remote 1")
}
if db.getIndexID(d1, []byte("bar")) != 0 {
t.Fatal("fail remote 2")
}
if db.getIndexID(d2, []byte("foo")) != 0 {
t.Fatal("fail remote 3")
}
if db.getIndexID(d2, []byte("bar")) != 0 {
t.Fatal("fail remote 4")
}
}

View File

@@ -164,24 +164,12 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
func (s *FileSet) WithNeed(device protocol.DeviceID, fn Iterator) {
l.Debugf("%s WithNeed(%v)", s.folder, device)
s.db.withNeed([]byte(s.folder), device[:], false, false, nativeFileIterator(fn))
s.db.withNeed([]byte(s.folder), device[:], false, nativeFileIterator(fn))
}
func (s *FileSet) WithNeedTruncated(device protocol.DeviceID, fn Iterator) {
l.Debugf("%s WithNeedTruncated(%v)", s.folder, device)
s.db.withNeed([]byte(s.folder), device[:], true, false, nativeFileIterator(fn))
}
// WithNeedOrInvalid considers all invalid files as needed, regardless of their version
// (e.g. for pulling when ignore patterns changed)
func (s *FileSet) WithNeedOrInvalid(device protocol.DeviceID, fn Iterator) {
l.Debugf("%s WithNeedExcludingInvalid(%v)", s.folder, device)
s.db.withNeed([]byte(s.folder), device[:], false, true, nativeFileIterator(fn))
}
func (s *FileSet) WithNeedOrInvalidTruncated(device protocol.DeviceID, fn Iterator) {
l.Debugf("%s WithNeedExcludingInvalidTruncated(%v)", s.folder, device)
s.db.withNeed([]byte(s.folder), device[:], true, true, nativeFileIterator(fn))
s.db.withNeed([]byte(s.folder), device[:], true, nativeFileIterator(fn))
}
func (s *FileSet) WithHave(device protocol.DeviceID, fn Iterator) {

View File

@@ -88,28 +88,10 @@ func (f *BasicFilesystem) rooted(rel string) (string, error) {
expectedPrefix += pathSep
}
// The relative path should be clean from internal dotdots and similar
// funkyness.
rel = filepath.FromSlash(rel)
if filepath.Clean(rel) != rel {
return "", ErrInvalidFilename
}
// It is not acceptable to attempt to traverse upwards.
switch rel {
case "..", pathSep:
return "", ErrNotRelative
}
if strings.HasPrefix(rel, ".."+pathSep) {
return "", ErrNotRelative
}
if strings.HasPrefix(rel, pathSep+pathSep) {
// The relative path may pretend to be an absolute path within the
// root, but the double path separator on Windows implies something
// else. It would get cleaned by the Join below, but it's out of
// spec anyway.
return "", ErrNotRelative
var err error
rel, err = Canonicalize(rel)
if err != nil {
return "", err
}
// The supposedly correct path is the one filepath.Join will return, as
@@ -154,6 +136,19 @@ func (f *BasicFilesystem) Mkdir(name string, perm FileMode) error {
return os.Mkdir(name, os.FileMode(perm))
}
// MkdirAll creates a directory named path, along with any necessary parents,
// and returns nil, or else returns an error.
// The permission bits perm are used for all directories that MkdirAll creates.
// If path is already a directory, MkdirAll does nothing and returns nil.
func (f *BasicFilesystem) MkdirAll(path string, perm FileMode) error {
path, err := f.rooted(path)
if err != nil {
return err
}
return f.mkdirAll(path, os.FileMode(perm))
}
func (f *BasicFilesystem) Lstat(name string) (FileInfo, error) {
name, err := f.rooted(name)
if err != nil {

View File

@@ -364,17 +364,17 @@ func TestRooted(t *testing.T) {
{"baz/foo/", "bar/baz", "baz/foo/bar/baz", true},
{"baz/foo/", "/bar/baz", "baz/foo/bar/baz", true},
// Not escape attempts, but oddly formatted relative paths. Disallowed.
{"foo", "./bar", "", false},
{"baz/foo", "./bar", "", false},
{"foo", "./bar/baz", "", false},
{"baz/foo", "./bar/baz", "", false},
{"baz/foo", "bar/../baz", "", false},
{"baz/foo", "/bar/../baz", "", false},
{"baz/foo", "./bar/../baz", "", false},
{"baz/foo", "bar/../baz", "", false},
{"baz/foo", "/bar/../baz", "", false},
{"baz/foo", "./bar/../baz", "", false},
// Not escape attempts, but oddly formatted relative paths.
{"foo", "", "foo/", true},
{"foo", "/", "foo/", true},
{"foo", "/..", "foo/", true},
{"foo", "./bar", "foo/bar", true},
{"baz/foo", "./bar", "baz/foo/bar", true},
{"foo", "./bar/baz", "foo/bar/baz", true},
{"baz/foo", "./bar/baz", "baz/foo/bar/baz", true},
{"baz/foo", "bar/../baz", "baz/foo/baz", true},
{"baz/foo", "/bar/../baz", "baz/foo/baz", true},
{"baz/foo", "./bar/../baz", "baz/foo/baz", true},
// Results in an allowed path, but does it by probing. Disallowed.
{"foo", "../foo", "", false},
@@ -385,10 +385,7 @@ func TestRooted(t *testing.T) {
{"baz/foo", "bar/../../../baz/foo/bar", "", false},
// Escape attempts.
{"foo", "", "", false},
{"foo", "/", "", false},
{"foo", "..", "", false},
{"foo", "/..", "", false},
{"foo", "../", "", false},
{"foo", "../bar", "", false},
{"foo", "../foobar", "", false},
@@ -413,8 +410,8 @@ func TestRooted(t *testing.T) {
{"/", "/foo", "/foo", true},
{"/", "../foo", "", false},
{"/", "..", "", false},
{"/", "/", "", false},
{"/", "", "", false},
{"/", "/", "/", true},
{"/", "", "/", true},
// special case for filesystems to be able to MkdirAll('.') for example
{"/", ".", "/", true},
@@ -427,11 +424,11 @@ func TestRooted(t *testing.T) {
{`c:\`, `\foo`, `c:\foo`, true},
{`\\?\c:\`, `\foo`, `\\?\c:\foo`, true},
{`c:\`, `\\foo`, ``, false},
{`c:\`, ``, ``, false},
{`c:\`, `\`, ``, false},
{`c:\`, ``, `c:\`, true},
{`c:\`, `\`, `c:\`, true},
{`\\?\c:\`, `\\foo`, ``, false},
{`\\?\c:\`, ``, ``, false},
{`\\?\c:\`, `\`, ``, false},
{`\\?\c:\`, ``, `\\?\c:\`, true},
{`\\?\c:\`, `\`, `\\?\c:\`, true},
// makes no sense, but will be treated simply as a bad filename
{`c:\foo`, `d:\bar`, `c:\foo\d:\bar`, true},

View File

@@ -30,12 +30,8 @@ func (f *BasicFilesystem) ReadSymlink(name string) (string, error) {
return os.Readlink(name)
}
func (f *BasicFilesystem) MkdirAll(name string, perm FileMode) error {
name, err := f.rooted(name)
if err != nil {
return err
}
return os.MkdirAll(name, os.FileMode(perm))
func (f *BasicFilesystem) mkdirAll(path string, perm os.FileMode) error {
return os.MkdirAll(path, perm)
}
// Unhide is a noop on unix, as unhiding files requires renaming them.

View File

@@ -13,7 +13,7 @@ import (
"errors"
"path/filepath"
"github.com/zillode/notify"
"github.com/Zillode/notify"
)
// Notify does not block on sending to channel, so the channel must be buffered.

View File

@@ -8,7 +8,7 @@
package fs
import "github.com/zillode/notify"
import "github.com/Zillode/notify"
const (
subEventMask = notify.Create | notify.FileModified | notify.FileRenameFrom | notify.FileDelete | notify.FileRenameTo

View File

@@ -8,7 +8,7 @@
package fs
import "github.com/zillode/notify"
import "github.com/Zillode/notify"
const (
subEventMask = notify.InCreate | notify.InMovedTo | notify.InDelete | notify.InDeleteSelf | notify.InModify | notify.InMovedFrom | notify.InMoveSelf

View File

@@ -8,7 +8,7 @@
package fs
import "github.com/zillode/notify"
import "github.com/Zillode/notify"
const (
subEventMask = notify.NoteDelete | notify.NoteWrite | notify.NoteRename

View File

@@ -12,7 +12,7 @@
package fs
import "github.com/zillode/notify"
import "github.com/Zillode/notify"
const (
subEventMask = notify.All

View File

@@ -8,7 +8,7 @@
package fs
import "github.com/zillode/notify"
import "github.com/Zillode/notify"
const (
subEventMask = notify.FileNotifyChangeFileName | notify.FileNotifyChangeDirName | notify.FileNotifyChangeSize | notify.FileNotifyChangeCreation

View File

@@ -18,7 +18,7 @@ import (
"testing"
"time"
"github.com/zillode/notify"
"github.com/Zillode/notify"
)
func TestMain(m *testing.M) {

View File

@@ -32,19 +32,6 @@ func (BasicFilesystem) CreateSymlink(target, name string) error {
return errNotSupported
}
// MkdirAll creates a directory named path, along with any necessary parents,
// and returns nil, or else returns an error.
// The permission bits perm are used for all directories that MkdirAll creates.
// If path is already a directory, MkdirAll does nothing and returns nil.
func (f *BasicFilesystem) MkdirAll(path string, perm FileMode) error {
path, err := f.rooted(path)
if err != nil {
return err
}
return f.mkdirAll(path, os.FileMode(perm))
}
// Required due to https://github.com/golang/go/issues/10900
func (f *BasicFilesystem) mkdirAll(path string, perm os.FileMode) error {
// Fast path: if we can tell whether path is a directory or file, stop with success or error.
@@ -75,7 +62,7 @@ func (f *BasicFilesystem) mkdirAll(path string, perm os.FileMode) error {
// Create parent
parent := path[0 : j-1]
if parent != filepath.VolumeName(parent) {
err = os.MkdirAll(parent, perm)
err = f.mkdirAll(parent, perm)
if err != nil {
return err
}

View File

@@ -198,3 +198,39 @@ func IsInternal(file string) bool {
}
return false
}
// Canonicalize checks that the file path is valid and returns it in the "canonical" form:
// - /foo/bar -> foo/bar
// - / -> "."
func Canonicalize(file string) (string, error) {
pathSep := string(PathSeparator)
if strings.HasPrefix(file, pathSep+pathSep) {
// The relative path may pretend to be an absolute path within
// the root, but the double path separator on Windows implies
// something else and is out of spec.
return "", ErrNotRelative
}
// The relative path should be clean from internal dotdots and similar
// funkyness.
file = filepath.Clean(file)
// It is not acceptable to attempt to traverse upwards.
switch file {
case "..":
return "", ErrNotRelative
}
if strings.HasPrefix(file, ".."+pathSep) {
return "", ErrNotRelative
}
if strings.HasPrefix(file, pathSep) {
if file == pathSep {
return ".", nil
}
return file[1:], nil
}
return file, nil
}

View File

@@ -41,3 +41,60 @@ func TestIsInternal(t *testing.T) {
}
}
}
func TestCanonicalize(t *testing.T) {
type testcase struct {
path string
expected string
ok bool
}
cases := []testcase{
// Valid cases
{"/bar", "bar", true},
{"/bar/baz", "bar/baz", true},
{"bar", "bar", true},
{"bar/baz", "bar/baz", true},
// Not escape attempts, but oddly formatted relative paths
{"", ".", true},
{"/", ".", true},
{"/..", ".", true},
{"./bar", "bar", true},
{"./bar/baz", "bar/baz", true},
{"bar/../baz", "baz", true},
{"/bar/../baz", "baz", true},
{"./bar/../baz", "baz", true},
// Results in an allowed path, but does it by probing. Disallowed.
{"../foo", "", false},
{"../foo/bar", "", false},
{"../foo/bar", "", false},
{"../../baz/foo/bar", "", false},
{"bar/../../foo/bar", "", false},
{"bar/../../../baz/foo/bar", "", false},
// Escape attempts.
{"..", "", false},
{"../", "", false},
{"../bar", "", false},
{"../foobar", "", false},
{"bar/../../quux/baz", "", false},
}
for _, tc := range cases {
res, err := Canonicalize(tc.path)
if tc.ok {
if err != nil {
t.Errorf("Unexpected error for Canonicalize(%q): %v", tc.path, err)
continue
}
exp := filepath.FromSlash(tc.expected)
if res != exp {
t.Errorf("Unexpected result for Canonicalize(%q): %q != expected %q", tc.path, res, exp)
}
} else if err == nil {
t.Errorf("Unexpected pass for Canonicalize(%q) => %q", tc.path, res)
continue
}
}
}

View File

@@ -23,7 +23,7 @@ func TestUnicodeLowercase(t *testing.T) {
{"ὈΔΥΣΣΕΎΣ", "ὀδυσσεύσ"},
// German ß doesn't really have an upper case variant, and we
// shouldn't mess things up when lower casing it either. We don't
// attempt to make ß equivalant to "ss".
// attempt to make ß equivalent to "ss".
{"Reichwaldstraße", "reichwaldstraße"},
// The Turks do their thing with the Is.... Like the Greek example
// we pick just the one canonicalized "i" although you can argue

View File

@@ -38,7 +38,12 @@ func NewWalkFilesystem(next Filesystem) Filesystem {
// walk recursively descends path, calling walkFn.
func (f *walkFilesystem) walk(path string, info FileInfo, walkFn WalkFunc) error {
err := walkFn(path, info, nil)
path, err := Canonicalize(path)
if err != nil {
return err
}
err = walkFn(path, info, nil)
if err != nil {
if info.IsDir() && err == SkipDir {
return nil

View File

@@ -13,6 +13,7 @@ import (
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/ignore"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/watchaggregator"
)
@@ -23,16 +24,21 @@ type folder struct {
stateTracker
config.FolderConfiguration
model *Model
shortID protocol.ShortID
ctx context.Context
cancel context.CancelFunc
scan folderScanner
model *Model
ctx context.Context
cancel context.CancelFunc
initialScanFinished chan struct{}
watchCancel context.CancelFunc
watchChan chan []string
restartWatchChan chan struct{}
watchErr error
watchErrMut sync.Mutex
pullScheduled chan struct{}
watchCancel context.CancelFunc
watchChan chan []string
restartWatchChan chan struct{}
watchErr error
watchErrMut sync.Mutex
}
func newFolder(model *Model, cfg config.FolderConfiguration) folder {
@@ -42,14 +48,19 @@ func newFolder(model *Model, cfg config.FolderConfiguration) folder {
stateTracker: newStateTracker(cfg.ID),
FolderConfiguration: cfg,
model: model,
shortID: model.shortID,
ctx: ctx,
cancel: cancel,
scan: newFolderScanner(cfg),
ctx: ctx,
cancel: cancel,
model: model,
initialScanFinished: make(chan struct{}),
watchCancel: func() {},
watchErr: errWatchNotStarted,
watchErrMut: sync.NewMutex(),
pullScheduled: make(chan struct{}, 1), // This needs to be 1-buffered so that we queue a pull if we're busy when it comes.
watchCancel: func() {},
watchErr: errWatchNotStarted,
watchErrMut: sync.NewMutex(),
}
}
@@ -65,7 +76,16 @@ func (f *folder) IgnoresUpdated() {
}
}
func (f *folder) SchedulePull() {}
func (f *folder) SchedulePull() {
select {
case f.pullScheduled <- struct{}{}:
default:
// We might be busy doing a pull and thus not reading from this
// channel. The channel is 1-buffered, so one notification will be
// queued to ensure we recheck after the pull, but beyond that we must
// make sure to not block index receiving.
}
}
func (f *folder) Jobs() ([]string, []string) {
return nil, nil

View File

@@ -1405,7 +1405,7 @@ func (cf cFiler) CurrentFile(file string) (protocol.FileInfo, bool) {
return cf.m.CurrentFolderFile(cf.r, file)
}
// Connection returns the current connection for device, and a boolean wether a connection was found.
// Connection returns the current connection for device, and a boolean whether a connection was found.
func (m *Model) Connection(deviceID protocol.DeviceID) (connections.Connection, bool) {
m.pmut.RLock()
cn, ok := m.conn[deviceID]
@@ -2044,33 +2044,40 @@ func (m *Model) internalScanFolderSubdirs(ctx context.Context, folder string, su
batchSizeBytes += nf.ProtoSize()
changes++
case f.IsInvalid() && !ignores.Match(f.Name).IsIgnored():
// Successfully scanned items are already un-ignored during
// the scan, so check whether it is deleted.
fallthrough
case !f.IsInvalid() && !f.IsDeleted():
// The file is valid and not deleted. Lets check if it's
// still here.
if _, err := mtimefs.Lstat(f.Name); err != nil {
// We don't specifically verify that the error is
// fs.IsNotExist because there is a corner case when a
// directory is suddenly transformed into a file. When that
// happens, files that were in the directory (that is now a
// file) are deleted but will return a confusing error ("not a
// directory") when we try to Lstat() them.
nf := protocol.FileInfo{
Name: f.Name,
Type: f.Type,
Size: 0,
ModifiedS: f.ModifiedS,
ModifiedNs: f.ModifiedNs,
ModifiedBy: m.id.Short(),
Deleted: true,
Version: f.Version.Update(m.shortID),
}
batch = append(batch, nf)
batchSizeBytes += nf.ProtoSize()
changes++
// Simply stating it wont do as there are tons of corner
// cases (e.g. parent dir->simlink, missing permissions)
if !osutil.IsDeleted(mtimefs, f.Name) {
return true
}
nf := protocol.FileInfo{
Name: f.Name,
Type: f.Type,
Size: 0,
ModifiedS: f.ModifiedS,
ModifiedNs: f.ModifiedNs,
ModifiedBy: m.id.Short(),
Deleted: true,
Version: f.Version.Update(m.shortID),
}
// We do not want to override the global version
// with the deleted file. Keeping only our local
// counter makes sure we are in conflict with any
// other existing versions, which will be resolved
// by the normal pulling mechanisms.
if f.IsInvalid() {
nf.Version = nf.Version.DropOthers(m.shortID)
}
batch = append(batch, nf)
batchSizeBytes += nf.ProtoSize()
changes++
}
return true
})
@@ -2777,7 +2784,12 @@ func unifySubs(dirs []string, exists func(dir string) bool) []string {
}
prev := "./" // Anything that can't be parent of a clean path
for i := 0; i < len(dirs); {
dir := filepath.Clean(dirs[i])
dir, err := fs.Canonicalize(dirs[i])
if err != nil {
l.Debugf("Skipping %v for scan: %s", dirs[i], err)
dirs = append(dirs[:i], dirs[i+1:]...)
continue
}
if dir == prev || strings.HasPrefix(dir, prev+string(fs.PathSeparator)) {
dirs = append(dirs[:i], dirs[i+1:]...)
continue
@@ -2811,22 +2823,6 @@ func makeForgetUpdate(files []protocol.FileInfo) []protocol.FileDownloadProgress
return updates
}
// shouldIgnore returns true when a file should be excluded from processing
func shouldIgnore(file db.FileIntf, matcher *ignore.Matcher, ignoreDelete bool) bool {
switch {
case ignoreDelete && file.IsDeleted():
// ignoreDelete first because it's a very cheap test so a win if it
// succeeds, and we might in the long run accumulate quite a few
// deleted files.
return true
case matcher.ShouldIgnore(file.FileName()):
return true
}
return false
}
// folderDeviceSet is a set of (folder, deviceID) pairs
type folderDeviceSet map[string]map[protocol.DeviceID]struct{}

View File

@@ -26,7 +26,6 @@ import (
"github.com/d4l3k/messagediff"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/ignore"
"github.com/syncthing/syncthing/lib/osutil"
@@ -50,6 +49,7 @@ func init() {
defaultFolderConfig = config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, "testdata")
defaultFolderConfig.Devices = []config.FolderDeviceConfiguration{{DeviceID: device1}}
_defaultConfig := config.Configuration{
Version: config.CurrentVersion,
Folders: []config.FolderConfiguration{defaultFolderConfig},
Devices: []config.DeviceConfiguration{config.NewDeviceConfiguration(device1, "device1")},
Options: config.OptionsConfiguration{
@@ -2159,6 +2159,12 @@ func unifySubsCases() []unifySubsCase {
[]string{"foo"},
nil,
},
{
// 10. absolute path
[]string{"/foo"},
[]string{"foo"},
[]string{"foo"},
},
}
if runtime.GOOS == "windows" {
@@ -2808,6 +2814,186 @@ func TestNoRequestsFromPausedDevices(t *testing.T) {
}
}
// TestIssue2571 tests replacing a directory with content with a symlink
func TestIssue2571(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Scanning symlinks isn't supported on windows")
}
err := defaultFs.MkdirAll("replaceDir", 0755)
if err != nil {
t.Fatal(err)
}
defer func() {
defaultFs.RemoveAll("replaceDir")
}()
testFs := fs.NewFilesystem(fs.FilesystemTypeBasic, filepath.Join(defaultFs.URI(), "replaceDir"))
for _, dir := range []string{"toLink", "linkTarget"} {
err := testFs.MkdirAll(dir, 0775)
if err != nil {
t.Fatal(err)
}
fd, err := testFs.Create(filepath.Join(dir, "a"))
if err != nil {
t.Fatal(err)
}
fd.Close()
}
dbi := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "syncthing", "dev", dbi, nil)
m.AddFolder(defaultFolderConfig)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
m.ScanFolder("default")
if err = testFs.RemoveAll("toLink"); err != nil {
t.Fatal(err)
}
if err := osutil.DebugSymlinkForTestsOnly(filepath.Join(testFs.URI(), "linkTarget"), filepath.Join(testFs.URI(), "toLink")); err != nil {
t.Fatal(err)
}
m.ScanFolder("default")
if dir, ok := m.CurrentFolderFile("default", filepath.Join("replaceDir", "toLink")); !ok {
t.Fatalf("Dir missing in db")
} else if !dir.IsSymlink() {
t.Errorf("Dir wasn't changed to symlink")
}
if file, ok := m.CurrentFolderFile("default", filepath.Join("replaceDir", "toLink", "a")); !ok {
t.Fatalf("File missing in db")
} else if !file.Deleted {
t.Errorf("File below symlink has not been marked as deleted")
}
}
// TestIssue4573 tests that contents of an unavailable dir aren't marked deleted
func TestIssue4573(t *testing.T) {
if runtime.GOOS == "windows" {
t.Skip("Can't make the dir inaccessible on windows")
}
err := defaultFs.MkdirAll("inaccessible", 0755)
if err != nil {
t.Fatal(err)
}
defer func() {
defaultFs.Chmod("inaccessible", 0777)
defaultFs.RemoveAll("inaccessible")
}()
file := filepath.Join("inaccessible", "a")
fd, err := defaultFs.Create(file)
if err != nil {
t.Fatal(err)
}
fd.Close()
dbi := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "syncthing", "dev", dbi, nil)
m.AddFolder(defaultFolderConfig)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
m.ScanFolder("default")
err = defaultFs.Chmod("inaccessible", 0000)
if err != nil {
t.Fatal(err)
}
m.ScanFolder("default")
if file, ok := m.CurrentFolderFile("default", file); !ok {
t.Fatalf("File missing in db")
} else if file.Deleted {
t.Errorf("Inaccessible file has been marked as deleted.")
}
}
// TestInternalScan checks whether various fs operations are correctly represented
// in the db after scanning.
func TestInternalScan(t *testing.T) {
err := defaultFs.MkdirAll("internalScan", 0755)
if err != nil {
t.Fatal(err)
}
defer func() {
defaultFs.RemoveAll("internalScan")
}()
testFs := fs.NewFilesystem(fs.FilesystemTypeBasic, filepath.Join(defaultFs.URI(), "internalScan"))
testCases := map[string]func(protocol.FileInfo) bool{
"removeDir": func(f protocol.FileInfo) bool {
return !f.Deleted
},
"dirToFile": func(f protocol.FileInfo) bool {
return f.Deleted || f.IsDirectory()
},
}
baseDirs := []string{"dirToFile", "removeDir"}
for _, dir := range baseDirs {
sub := filepath.Join(dir, "subDir")
for _, dir := range []string{dir, sub} {
err := testFs.MkdirAll(dir, 0775)
if err != nil {
t.Fatalf("%v: %v", dir, err)
}
}
testCases[sub] = func(f protocol.FileInfo) bool {
return !f.Deleted
}
for _, dir := range []string{dir, sub} {
file := filepath.Join(dir, "a")
fd, err := testFs.Create(file)
if err != nil {
t.Fatal(err)
}
fd.Close()
testCases[file] = func(f protocol.FileInfo) bool {
return !f.Deleted
}
}
}
dbi := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "syncthing", "dev", dbi, nil)
m.AddFolder(defaultFolderConfig)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
m.ScanFolder("default")
for _, dir := range baseDirs {
if err = testFs.RemoveAll(dir); err != nil {
t.Fatal(err)
}
}
fd, err := testFs.Create("dirToFile")
if err != nil {
t.Fatal(err)
}
fd.Close()
m.ScanFolder("default")
for path, cond := range testCases {
if f, ok := m.CurrentFolderFile("default", filepath.Join("internalScan", path)); !ok {
t.Fatalf("%v missing in db", path)
} else if cond(f) {
t.Errorf("Incorrect db entry for %v", path)
}
}
}
func TestCustomMarkerName(t *testing.T) {
ldb := db.OpenMemory()
set := db.NewFileSet("default", defaultFs, ldb)
@@ -3262,86 +3448,6 @@ func TestPausedFolders(t *testing.T) {
}
}
func TestPullInvalid(t *testing.T) {
if runtime.GOOS != "windows" {
t.Skip("Windows only")
}
tmpDir, err := ioutil.TempDir(".", "_model-")
if err != nil {
panic("Failed to create temporary testing dir")
}
defer os.RemoveAll(tmpDir)
cfg := defaultConfig.RawCopy()
cfg.Folders[0] = config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, tmpDir)
cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{{DeviceID: device1}}
w := config.Wrap("/tmp/cfg", cfg)
db := db.OpenMemory()
m := NewModel(w, protocol.LocalDeviceID, "syncthing", "dev", db, nil)
m.AddFolder(cfg.Folders[0])
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
m.ScanFolder("default")
if err := m.SetIgnores("default", []string{"*:ignored"}); err != nil {
panic(err)
}
ign := "invalid:ignored"
del := "invalid:deleted"
var version protocol.Vector
version = version.Update(device1.Short())
m.IndexUpdate(device1, "default", []protocol.FileInfo{
{
Name: ign,
Size: 1234,
Type: protocol.FileInfoTypeFile,
Version: version,
},
{
Name: del,
Size: 1234,
Type: protocol.FileInfoTypeFile,
Version: version,
Deleted: true,
},
})
sub := events.Default.Subscribe(events.FolderErrors)
defer events.Default.Unsubscribe(sub)
timeout := time.NewTimer(5 * time.Second)
for {
select {
case ev := <-sub.C():
t.Fatalf("Errors while pulling: %v", ev)
case <-timeout.C:
t.Fatalf("File wasn't added to index until timeout")
default:
}
file, ok := m.CurrentFolderFile("default", ign)
if !ok {
time.Sleep(100 * time.Millisecond)
continue
}
if !file.Invalid {
t.Error("Ignored file isn't marked as invalid")
}
if file, ok = m.CurrentFolderFile("default", del); ok {
t.Error("Deleted invalid file was added to index")
}
return
}
}
func addFakeConn(m *Model, dev protocol.DeviceID) *fakeConnection {
fc := &fakeConnection{id: dev, model: m}
m.AddConnection(fc, protocol.HelloResult{})

View File

@@ -8,6 +8,7 @@ package model
import (
"bytes"
"errors"
"io/ioutil"
"os"
"path/filepath"
@@ -18,7 +19,9 @@ import (
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/ignore"
"github.com/syncthing/syncthing/lib/protocol"
)
@@ -26,9 +29,9 @@ func TestRequestSimple(t *testing.T) {
// Verify that the model performs a request and creates a file based on
// an incoming index update.
m, fc, tmpFolder := setupModelWithConnection()
m, fc, tmpDir := setupModelWithConnection()
defer m.Stop()
defer os.RemoveAll(tmpFolder)
defer os.RemoveAll(tmpDir)
// We listen for incoming index updates and trigger when we see one for
// the expected test file.
@@ -51,13 +54,8 @@ func TestRequestSimple(t *testing.T) {
<-done
// Verify the contents
bs, err := ioutil.ReadFile(filepath.Join(tmpFolder, "testfile"))
if err != nil {
if err := equalContents(filepath.Join(tmpDir, "testfile"), contents); err != nil {
t.Error("File did not sync correctly:", err)
return
}
if !bytes.Equal(bs, contents) {
t.Error("File did not sync correctly: incorrect data")
}
}
@@ -69,9 +67,9 @@ func TestSymlinkTraversalRead(t *testing.T) {
return
}
m, fc, tmpFolder := setupModelWithConnection()
m, fc, tmpDir := setupModelWithConnection()
defer m.Stop()
defer os.RemoveAll(tmpFolder)
defer os.RemoveAll(tmpDir)
// We listen for incoming index updates and trigger when we see one for
// the expected test file.
@@ -109,9 +107,9 @@ func TestSymlinkTraversalWrite(t *testing.T) {
return
}
m, fc, tmpFolder := setupModelWithConnection()
m, fc, tmpDir := setupModelWithConnection()
defer m.Stop()
defer os.RemoveAll(tmpFolder)
defer os.RemoveAll(tmpDir)
// We listen for incoming index updates and trigger when we see one for
// the expected names.
@@ -169,9 +167,9 @@ func TestSymlinkTraversalWrite(t *testing.T) {
func TestRequestCreateTmpSymlink(t *testing.T) {
// Test that an update for a temporary file is invalidated
m, fc, tmpFolder := setupModelWithConnection()
m, fc, tmpDir := setupModelWithConnection()
defer m.Stop()
defer os.RemoveAll(tmpFolder)
defer os.RemoveAll(tmpDir)
// We listen for incoming index updates and trigger when we see one for
// the expected test file.
@@ -211,12 +209,12 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
// Sets up a folder with trashcan versioning and tries to use a
// deleted symlink to escape
tmpFolder, err := ioutil.TempDir(".", "_request-")
tmpDir, err := ioutil.TempDir(".", "_request-")
if err != nil {
panic("Failed to create temporary testing dir")
}
cfg := defaultConfig.RawCopy()
cfg.Folders[0] = config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, tmpFolder)
cfg.Folders[0] = config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, tmpDir)
cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2},
@@ -233,7 +231,7 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
m.StartFolder("default")
defer m.Stop()
defer os.RemoveAll(tmpFolder)
defer os.RemoveAll(tmpDir)
fc := addFakeConn(m, device2)
fc.folder = "default"
@@ -286,17 +284,200 @@ func TestRequestVersioningSymlinkAttack(t *testing.T) {
}
}
func setupModelWithConnection() (*Model, *fakeConnection, string) {
tmpFolder, err := ioutil.TempDir(".", "_request-")
if err != nil {
panic("Failed to create temporary testing dir")
}
func TestPullInvalidIgnoredSO(t *testing.T) {
pullInvalidIgnored(t, config.FolderTypeSendOnly)
}
func TestPullInvalidIgnoredSR(t *testing.T) {
pullInvalidIgnored(t, config.FolderTypeSendReceive)
}
// This test checks that (un-)ignored/invalid/deleted files are treated as expected.
func pullInvalidIgnored(t *testing.T, ft config.FolderType) {
t.Helper()
tmpDir := createTmpDir()
defer os.RemoveAll(tmpDir)
cfg := defaultConfig.RawCopy()
cfg.Folders[0] = config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, tmpFolder)
cfg.Devices = append(cfg.Devices, config.NewDeviceConfiguration(device2, "device2"))
cfg.Folders[0] = config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, tmpDir)
cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2},
}
cfg.Folders[0].Type = ft
m, fc := setupModelWithConnectionManual(cfg)
defer m.Stop()
// Reach in and update the ignore matcher to one that always does
// reloads when asked to, instead of checking file mtimes. This is
// because we might be changing the files on disk often enough that the
// mtimes will be unreliable to determine change status.
m.fmut.Lock()
m.folderIgnores["default"] = ignore.New(cfg.Folders[0].Filesystem(), ignore.WithChangeDetector(newAlwaysChanged()))
m.fmut.Unlock()
if err := m.SetIgnores("default", []string{"*ignored*"}); err != nil {
panic(err)
}
contents := []byte("test file contents\n")
otherContents := []byte("other test file contents\n")
invIgn := "invalid:ignored"
invDel := "invalid:deleted"
ign := "ignoredNonExisting"
ignExisting := "ignoredExisting"
fc.addFile(invIgn, 0644, protocol.FileInfoTypeFile, contents)
fc.addFile(invDel, 0644, protocol.FileInfoTypeFile, contents)
fc.deleteFile(invDel)
fc.addFile(ign, 0644, protocol.FileInfoTypeFile, contents)
fc.addFile(ignExisting, 0644, protocol.FileInfoTypeFile, contents)
if err := ioutil.WriteFile(filepath.Join(tmpDir, ignExisting), otherContents, 0644); err != nil {
panic(err)
}
done := make(chan struct{})
fc.mut.Lock()
fc.indexFn = func(folder string, fs []protocol.FileInfo) {
expected := map[string]struct{}{invIgn: {}, ign: {}, ignExisting: {}}
for _, f := range fs {
if _, ok := expected[f.Name]; !ok {
t.Fatalf("Unexpected file %v was added to index", f.Name)
}
if !f.Invalid {
t.Errorf("File %v wasn't marked as invalid", f.Name)
}
delete(expected, f.Name)
}
for name := range expected {
t.Errorf("File %v wasn't added to index", name)
}
done <- struct{}{}
}
fc.mut.Unlock()
sub := events.Default.Subscribe(events.FolderErrors)
defer events.Default.Unsubscribe(sub)
fc.sendIndexUpdate()
timeout := time.NewTimer(5 * time.Second)
select {
case ev := <-sub.C():
t.Fatalf("Errors while pulling: %v", ev)
case <-timeout.C:
t.Fatalf("timed out before index was received")
case <-done:
return
}
fc.mut.Lock()
fc.indexFn = func(folder string, fs []protocol.FileInfo) {
expected := map[string]struct{}{ign: {}, ignExisting: {}}
for _, f := range fs {
if _, ok := expected[f.Name]; !ok {
t.Fatalf("Unexpected file %v was updated in index", f.Name)
}
if f.Invalid {
t.Errorf("File %v is still marked as invalid", f.Name)
}
// The unignored files should only have a local version,
// to mark them as in conflict with any other existing versions.
ev := protocol.Vector{}.Update(device1.Short())
if v := f.Version; !v.Equal(ev) {
t.Errorf("File %v has version %v, expected %v", f.Name, v, ev)
}
if f.Name == ign {
if !f.Deleted {
t.Errorf("File %v was not marked as deleted", f.Name)
}
} else if f.Deleted {
t.Errorf("File %v is marked as deleted", f.Name)
}
delete(expected, f.Name)
}
for name := range expected {
t.Errorf("File %v wasn't updated in index", name)
}
done <- struct{}{}
}
// Make sure pulling doesn't interfere, as index updates are racy and
// thus we cannot distinguish between scan and pull results.
fc.requestFn = func(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
return nil, nil
}
fc.mut.Unlock()
if err := m.SetIgnores("default", []string{"*:ignored*"}); err != nil {
panic(err)
}
timeout = time.NewTimer(5 * time.Second)
select {
case <-timeout.C:
t.Fatalf("timed out before index was received")
case <-done:
return
}
}
func TestIssue4841(t *testing.T) {
m, fc, tmpDir := setupModelWithConnection()
defer m.Stop()
defer os.RemoveAll(tmpDir)
received := make(chan protocol.FileInfo)
fc.mut.Lock()
fc.indexFn = func(folder string, fs []protocol.FileInfo) {
if len(fs) != 1 {
t.Fatalf("Sent index with %d files, should be 1", len(fs))
}
if fs[0].Name != "foo" {
t.Fatalf(`Sent index with file %v, should be "foo"`, fs[0].Name)
}
received <- fs[0]
return
}
fc.mut.Unlock()
// Setup file from remote that was ignored locally
m.updateLocals(defaultFolderConfig.ID, []protocol.FileInfo{{
Name: "foo",
Type: protocol.FileInfoTypeFile,
Invalid: true,
Version: protocol.Vector{}.Update(device2.Short()),
}})
<-received
// Scan without ignore patterns with "foo" not existing locally
if err := m.ScanFolder("default"); err != nil {
t.Fatal("Failed scanning:", err)
}
f := <-received
if expected := (protocol.Vector{}.Update(device1.Short())); !f.Version.Equal(expected) {
t.Errorf("Got Version == %v, expected %v", f.Version, expected)
}
}
func setupModelWithConnection() (*Model, *fakeConnection, string) {
tmpDir := createTmpDir()
cfg := defaultConfig.RawCopy()
cfg.Devices = append(cfg.Devices, config.NewDeviceConfiguration(device2, "device2"))
cfg.Folders[0] = config.NewFolderConfiguration(protocol.LocalDeviceID, "default", "default", fs.FilesystemTypeBasic, tmpDir)
cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2},
}
m, fc := setupModelWithConnectionManual(cfg)
return m, fc, tmpDir
}
func setupModelWithConnectionManual(cfg config.Configuration) (*Model, *fakeConnection) {
w := config.Wrap("/tmp/cfg", cfg)
db := db.OpenMemory()
@@ -308,5 +489,24 @@ func setupModelWithConnection() (*Model, *fakeConnection, string) {
fc := addFakeConn(m, device2)
fc.folder = "default"
return m, fc, tmpFolder
m.ScanFolder("default")
return m, fc
}
func createTmpDir() string {
tmpDir, err := ioutil.TempDir(".", "_request-")
if err != nil {
panic("Failed to create temporary testing dir")
}
return tmpDir
}
func equalContents(path string, contents []byte) error {
if bs, err := ioutil.ReadFile(path); err != nil {
return err
} else if !bytes.Equal(bs, contents) {
return errors.New("incorrect data")
}
return nil
}

View File

@@ -10,7 +10,9 @@ import (
"fmt"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/protocol"
"github.com/syncthing/syncthing/lib/versioner"
)
@@ -43,6 +45,9 @@ func (f *sendOnlyFolder) Serve() {
case <-f.ctx.Done():
return
case <-f.pullScheduled:
f.pull()
case <-f.restartWatchChan:
f.restartWatch()
@@ -70,3 +75,62 @@ func (f *sendOnlyFolder) String() string {
func (f *sendOnlyFolder) PullErrors() []FileError {
return nil
}
// pull checks need for files that only differ by metadata (no changes on disk)
func (f *sendOnlyFolder) pull() {
select {
case <-f.initialScanFinished:
default:
// Once the initial scan finished, a pull will be scheduled
return
}
f.model.fmut.RLock()
folderFiles := f.model.folderFiles[f.folderID]
ignores := f.model.folderIgnores[f.folderID]
f.model.fmut.RUnlock()
batch := make([]protocol.FileInfo, 0, maxBatchSizeFiles)
batchSizeBytes := 0
folderFiles.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
if len(batch) == maxBatchSizeFiles || batchSizeBytes > maxBatchSizeBytes {
f.model.updateLocalsFromPulling(f.folderID, batch)
batch = batch[:0]
batchSizeBytes = 0
}
if ignores.ShouldIgnore(intf.FileName()) {
file := intf.(protocol.FileInfo)
file.Invalidate(f.shortID)
batch = append(batch, file)
batchSizeBytes += file.ProtoSize()
l.Debugln(f, "Handling ignored file", file)
return true
}
curFile, ok := f.model.CurrentFolderFile(f.folderID, intf.FileName())
if !ok {
if intf.IsDeleted() {
panic("Should never get a deleted file as needed when we don't have it")
}
return true
}
file := intf.(protocol.FileInfo)
if !file.IsEquivalent(curFile, f.IgnorePerms, false) {
return true
}
file.Version = file.Version.Merge(curFile.Version)
batch = append(batch, file)
batchSizeBytes += file.ProtoSize()
l.Debugln(f, "Merging versions of identical file", file)
return true
})
if len(batch) > 0 {
f.model.updateLocalsFromPulling(f.folderID, batch)
}
}

View File

@@ -15,6 +15,7 @@ import (
"runtime"
"sort"
"strings"
stdsync "sync"
"time"
"github.com/syncthing/syncthing/lib/config"
@@ -78,9 +79,10 @@ const (
)
const (
defaultCopiers = 2
defaultPullers = 64
defaultPullerPause = 60 * time.Second
defaultCopiers = 2
defaultPullerPause = 60 * time.Second
defaultPullerPendingKiB = 8192 // must be larger than block size
maxPullerIterations = 3
)
@@ -96,8 +98,7 @@ type sendReceiveFolder struct {
versioner versioner.Versioner
pause time.Duration
queue *jobQueue
pullScheduled chan struct{}
queue *jobQueue
errors map[string]string // path -> error string
errorsMut sync.Mutex
@@ -110,33 +111,28 @@ func newSendReceiveFolder(model *Model, cfg config.FolderConfiguration, ver vers
fs: fs,
versioner: ver,
queue: newJobQueue(),
pullScheduled: make(chan struct{}, 1), // This needs to be 1-buffered so that we queue a pull if we're busy when it comes.
queue: newJobQueue(),
errorsMut: sync.NewMutex(),
}
f.configureCopiersAndPullers()
return f
}
func (f *sendReceiveFolder) configureCopiersAndPullers() {
if f.Copiers == 0 {
f.Copiers = defaultCopiers
}
if f.Pullers == 0 {
f.Pullers = defaultPullers
// If the configured max amount of pending data is zero, we use the
// default. If it's configured to something non-zero but less than the
// protocol block size we adjust it upwards accordingly.
if f.PullerMaxPendingKiB == 0 {
f.PullerMaxPendingKiB = defaultPullerPendingKiB
}
if blockSizeKiB := protocol.BlockSize / 1024; f.PullerMaxPendingKiB < blockSizeKiB {
f.PullerMaxPendingKiB = blockSizeKiB
}
f.pause = f.basePause()
}
// Helper function to check whether either the ignorePerm flag has been
// set on the local host or the FlagNoPermBits has been set on the file/dir
// which is being pulled.
func (f *sendReceiveFolder) ignorePermissions(file protocol.FileInfo) bool {
return f.IgnorePerms || file.NoPermissions
return f
}
// Serve will run scans and pulls. It will return when Stop()ed or on a
@@ -326,7 +322,7 @@ func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher, ignoresChan
doneWg := sync.NewWaitGroup()
updateWg := sync.NewWaitGroup()
l.Debugln(f, "c", f.Copiers, "p", f.Pullers)
l.Debugln(f, "copiers:", f.Copiers, "pullerPendingKiB:", f.PullerMaxPendingKiB)
updateWg.Add(1)
go func() {
@@ -344,14 +340,12 @@ func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher, ignoresChan
}()
}
for i := 0; i < f.Pullers; i++ {
pullWg.Add(1)
go func() {
// pullerRoutine finishes when pullChan is closed
f.pullerRoutine(pullChan, finisherChan)
pullWg.Done()
}()
}
pullWg.Add(1)
go func() {
// pullerRoutine finishes when pullChan is closed
f.pullerRoutine(pullChan, finisherChan)
pullWg.Done()
}()
doneWg.Add(1)
// finisherRoutine finishes when finisherChan is closed
@@ -371,14 +365,7 @@ func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher, ignoresChan
// Regular files to pull goes into the file queue, everything else
// (directories, symlinks and deletes) goes into the "process directly"
// pile.
// Don't iterate over invalid/ignored files unless ignores have changed
iterate := folderFiles.WithNeed
if ignoresChanged {
iterate = folderFiles.WithNeedOrInvalid
}
iterate(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
folderFiles.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
if f.IgnoreDelete && intf.IsDeleted() {
l.Debugln(f, "ignore file deletion (config)", intf.FileName())
return true
@@ -388,7 +375,7 @@ func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher, ignoresChan
switch {
case ignores.ShouldIgnore(file.Name):
file.Invalidate(f.model.id.Short())
file.Invalidate(f.shortID)
l.Debugln(f, "Handling ignored file", file)
dbUpdateChan <- dbUpdateJob{file, dbUpdateInvalidate}
@@ -416,7 +403,7 @@ func (f *sendReceiveFolder) pullerIteration(ignores *ignore.Matcher, ignoresChan
l.Debugln(f, "Needed file is unavailable", file)
case runtime.GOOS == "windows" && file.IsSymlink():
file.Invalidate(f.model.id.Short())
file.Invalidate(f.shortID)
l.Debugln(f, "Invalidating symlink (unsupported)", file.Name)
dbUpdateChan <- dbUpdateJob{file, dbUpdateInvalidate}
@@ -562,7 +549,7 @@ nextFile:
// we can just do a rename instead.
key := string(fi.Blocks[0].Hash)
for i, candidate := range buckets[key] {
if blocksEqual(candidate.Blocks, fi.Blocks) {
if protocol.BlocksEqual(candidate.Blocks, fi.Blocks) {
// Remove the candidate from the bucket
lidx := len(buckets[key]) - 1
buckets[key][i] = buckets[key][lidx]
@@ -617,21 +604,6 @@ nextFile:
return changed
}
// blocksEqual returns whether two slices of blocks are exactly the same hash
// and index pair wise.
func blocksEqual(src, tgt []protocol.BlockInfo) bool {
if len(tgt) != len(src) {
return false
}
for i, sblk := range src {
if !bytes.Equal(sblk.Hash, tgt[i].Hash) {
return false
}
}
return true
}
// handleDir creates or updates the given directory
func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, dbUpdateChan chan<- dbUpdateJob) {
// Used in the defer closure below, updated by the function body. Take
@@ -656,7 +628,7 @@ func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, dbUpdateChan chan<
}()
mode := fs.FileMode(file.Permissions & 0777)
if f.ignorePermissions(file) {
if f.IgnorePerms || file.NoPermissions {
mode = 0777
}
@@ -685,7 +657,7 @@ func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, dbUpdateChan chan<
// not MkdirAll because the parent should already exist.
mkdir := func(path string) error {
err = f.fs.Mkdir(path, mode)
if err != nil || f.ignorePermissions(file) {
if err != nil || f.IgnorePerms || file.NoPermissions {
return err
}
@@ -716,7 +688,7 @@ func (f *sendReceiveFolder) handleDir(file protocol.FileInfo, dbUpdateChan chan<
// The directory already exists, so we just correct the mode bits. (We
// don't handle modification times on directories, because that sucks...)
// It's OK to change mode bits on stuff within non-writable directories.
if f.ignorePermissions(file) {
if f.IgnorePerms || file.NoPermissions {
dbUpdateChan <- dbUpdateJob{file, dbUpdateHandleDir}
} else if err := f.fs.Chmod(file.Name, mode|(fs.FileMode(info.Mode())&retainBits)); err == nil {
dbUpdateChan <- dbUpdateJob{file, dbUpdateHandleDir}
@@ -1107,7 +1079,7 @@ func (f *sendReceiveFolder) handleFile(file protocol.FileInfo, copyChan chan<- c
updated: time.Now(),
available: reused,
availableUpdated: time.Now(),
ignorePerms: f.ignorePermissions(file),
ignorePerms: f.IgnorePerms || file.NoPermissions,
hasCurFile: hasCurFile,
curFile: curFile,
mut: sync.NewRWMutex(),
@@ -1167,7 +1139,7 @@ func populateOffsets(blocks []protocol.BlockInfo) {
// shortcutFile sets file mode and modification time, when that's the only
// thing that has changed.
func (f *sendReceiveFolder) shortcutFile(file protocol.FileInfo) error {
if !f.ignorePermissions(file) {
if !f.IgnorePerms && !file.NoPermissions {
if err := f.fs.Chmod(file.Name, fs.FileMode(file.Permissions&0777)); err != nil {
f.newError("shortcut chmod", file.Name, err)
return err
@@ -1260,7 +1232,11 @@ func (f *sendReceiveFolder) copierRoutine(in <-chan copyBlocksState, pullChan ch
continue
}
buf = buf[:int(block.Size)]
if s := int(block.Size); s > cap(buf) {
buf = make([]byte, s)
} else {
buf = buf[:s]
}
found, err := weakHashFinder.Iterate(block.WeakHash, buf, func(offset int64) bool {
if _, err := verifyBuffer(buf, block); err != nil {
@@ -1367,81 +1343,104 @@ func verifyBuffer(buf []byte, block protocol.BlockInfo) ([]byte, error) {
}
func (f *sendReceiveFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *sharedPullerState) {
requestLimiter := newByteSemaphore(f.PullerMaxPendingKiB * 1024)
wg := sync.NewWaitGroup()
for state := range in {
if state.failed() != nil {
out <- state.sharedPullerState
continue
}
// Get an fd to the temporary file. Technically we don't need it until
// after fetching the block, but if we run into an error here there is
// no point in issuing the request to the network.
fd, err := state.tempFile()
if err != nil {
out <- state.sharedPullerState
continue
}
// The requestLimiter limits how many pending block requests we have
// ongoing at any given time, based on the size of the blocks
// themselves.
if !f.DisableSparseFiles && state.reused == 0 && state.block.IsEmpty() {
// There is no need to request a block of all zeroes. Pretend we
// requested it and handled it correctly.
state.pullDone(state.block)
out <- state.sharedPullerState
continue
}
state := state
bytes := int(state.block.Size)
var lastError error
candidates := f.model.Availability(f.folderID, state.file.Name, state.file.Version, state.block)
for {
// Select the least busy device to pull the block from. If we found no
// feasible device at all, fail the block (and in the long run, the
// file).
selected, found := activity.leastBusy(candidates)
if !found {
if lastError != nil {
state.fail("pull", lastError)
} else {
state.fail("pull", errNoDevice)
}
break
}
requestLimiter.take(bytes)
wg.Add(1)
candidates = removeAvailability(candidates, selected)
go func() {
defer wg.Done()
defer requestLimiter.give(bytes)
// Fetch the block, while marking the selected device as in use so that
// leastBusy can select another device when someone else asks.
activity.using(selected)
buf, lastError := f.model.requestGlobal(selected.ID, f.folderID, state.file.Name, state.block.Offset, int(state.block.Size), state.block.Hash, selected.FromTemporary)
activity.done(selected)
f.pullBlock(state, out)
}()
}
wg.Wait()
}
func (f *sendReceiveFolder) pullBlock(state pullBlockState, out chan<- *sharedPullerState) {
// Get an fd to the temporary file. Technically we don't need it until
// after fetching the block, but if we run into an error here there is
// no point in issuing the request to the network.
fd, err := state.tempFile()
if err != nil {
out <- state.sharedPullerState
return
}
if !f.DisableSparseFiles && state.reused == 0 && state.block.IsEmpty() {
// There is no need to request a block of all zeroes. Pretend we
// requested it and handled it correctly.
state.pullDone(state.block)
out <- state.sharedPullerState
return
}
var lastError error
candidates := f.model.Availability(f.folderID, state.file.Name, state.file.Version, state.block)
for {
// Select the least busy device to pull the block from. If we found no
// feasible device at all, fail the block (and in the long run, the
// file).
selected, found := activity.leastBusy(candidates)
if !found {
if lastError != nil {
l.Debugln("request:", f.folderID, state.file.Name, state.block.Offset, state.block.Size, "returned error:", lastError)
continue
}
// Verify that the received block matches the desired hash, if not
// try pulling it from another device.
_, lastError = verifyBuffer(buf, state.block)
if lastError != nil {
l.Debugln("request:", f.folderID, state.file.Name, state.block.Offset, state.block.Size, "hash mismatch")
continue
}
// Save the block data we got from the cluster
_, err = fd.WriteAt(buf, state.block.Offset)
if err != nil {
state.fail("save", err)
state.fail("pull", lastError)
} else {
state.pullDone(state.block)
state.fail("pull", errNoDevice)
}
break
}
out <- state.sharedPullerState
candidates = removeAvailability(candidates, selected)
// Fetch the block, while marking the selected device as in use so that
// leastBusy can select another device when someone else asks.
activity.using(selected)
buf, lastError := f.model.requestGlobal(selected.ID, f.folderID, state.file.Name, state.block.Offset, int(state.block.Size), state.block.Hash, selected.FromTemporary)
activity.done(selected)
if lastError != nil {
l.Debugln("request:", f.folderID, state.file.Name, state.block.Offset, state.block.Size, "returned error:", lastError)
continue
}
// Verify that the received block matches the desired hash, if not
// try pulling it from another device.
_, lastError = verifyBuffer(buf, state.block)
if lastError != nil {
l.Debugln("request:", f.folderID, state.file.Name, state.block.Offset, state.block.Size, "hash mismatch")
continue
}
// Save the block data we got from the cluster
_, err = fd.WriteAt(buf, state.block.Offset)
if err != nil {
state.fail("save", err)
} else {
state.pullDone(state.block)
}
break
}
out <- state.sharedPullerState
}
func (f *sendReceiveFolder) performFinish(ignores *ignore.Matcher, state *sharedPullerState, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) error {
// Set the correct permission bits on the new file
if !f.ignorePermissions(state.file) {
if !f.IgnorePerms && !state.file.NoPermissions {
if err := f.fs.Chmod(state.tempName, fs.FileMode(state.file.Permissions&0777)); err != nil {
return err
}
@@ -1486,7 +1485,7 @@ func (f *sendReceiveFolder) performFinish(ignores *ignore.Matcher, state *shared
case stat.IsDir():
// Dirs only have perm, no modetime/size
if !f.ignorePermissions(state.curFile) && state.curFile.HasPermissionBits() && !scanner.PermsEqual(state.curFile.Permissions, curMode) {
if !f.IgnorePerms && !state.curFile.NoPermissions && state.curFile.HasPermissionBits() && !protocol.PermsEqual(state.curFile.Permissions, curMode) {
l.Debugln("file permission modified but not rescanned; not finishing:", state.curFile.Name)
changed = true
}
@@ -1718,7 +1717,7 @@ func (f *sendReceiveFolder) inConflict(current, replacement protocol.Vector) boo
// Obvious case
return true
}
if replacement.Counter(f.model.shortID) > current.Counter(f.model.shortID) {
if replacement.Counter(f.shortID) > current.Counter(f.shortID) {
// The replacement file contains a higher version for ourselves than
// what we have. This isn't supposed to be possible, since it's only
// we who can increment that counter. We take it as a sign that
@@ -1821,11 +1820,6 @@ func (f *sendReceiveFolder) basePause() time.Duration {
return time.Duration(f.PullerPauseS) * time.Second
}
func (f *sendReceiveFolder) IgnoresUpdated() {
f.folder.IgnoresUpdated()
f.SchedulePull()
}
// deleteDir attempts to delete a directory. It checks for files/dirs inside
// the directory and removes them if possible or returns an error if it fails
func (f *sendReceiveFolder) deleteDir(dir string, ignores *ignore.Matcher, scanChan chan<- string) error {
@@ -1931,3 +1925,41 @@ func componentCount(name string) int {
}
return count
}
type byteSemaphore struct {
max int
available int
mut stdsync.Mutex
cond *stdsync.Cond
}
func newByteSemaphore(max int) *byteSemaphore {
s := byteSemaphore{
max: max,
available: max,
}
s.cond = stdsync.NewCond(&s.mut)
return &s
}
func (s *byteSemaphore) take(bytes int) {
if bytes > s.max {
panic("bug: more than max bytes will never be available")
}
s.mut.Lock()
for bytes > s.available {
s.cond.Wait()
}
s.available -= bytes
s.mut.Unlock()
}
func (s *byteSemaphore) give(bytes int) {
s.mut.Lock()
if s.available+bytes > s.max {
panic("bug: can never give more than max")
}
s.available += bytes
s.cond.Broadcast()
s.mut.Unlock()
}

View File

@@ -17,6 +17,7 @@ import (
"testing"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/ignore"
@@ -107,6 +108,9 @@ func setUpSendReceiveFolder(model *Model) *sendReceiveFolder {
model: model,
initialScanFinished: make(chan struct{}),
ctx: context.TODO(),
FolderConfiguration: config.FolderConfiguration{
PullerMaxPendingKiB: defaultPullerPendingKiB,
},
},
fs: fs.NewMtimeFS(fs.NewFilesystem(fs.FilesystemTypeBasic, "testdata"), db.NewNamespacedKV(model.db, "mtime")),

View File

@@ -153,3 +153,14 @@ func init() {
func IsWindowsExecutable(path string) bool {
return execExts[strings.ToLower(filepath.Ext(path))]
}
func IsDeleted(ffs fs.Filesystem, name string) bool {
if _, err := ffs.Lstat(name); fs.IsNotExist(err) {
return true
}
switch TraversesSymlink(ffs, filepath.Dir(name)).(type) {
case *NotADirectoryError, *TraversesSymlinkError:
return true
}
return false
}

View File

@@ -26,7 +26,7 @@ func DebugSymlinkForTestsOnly(oldname, newname string) error {
// '/' does not work in link's content
oldname = filepath.FromSlash(oldname)
// need the exact location of the oldname when its relative to determine if its a directory
// need the exact location of the oldname when it's relative to determine if it's a directory
destpath := oldname
if !filepath.IsAbs(oldname) {
destpath = filepath.Dir(newname) + `\` + oldname
@@ -75,7 +75,7 @@ func fixLongPath(path string) string {
// minus 12)." Since MAX_PATH is 260, 260 - 12 = 248.
//
// The MSDN docs appear to say that a normal path that is 248 bytes long
// will work; empirically the path must be less then 248 bytes long.
// will work; empirically the path must be less than 248 bytes long.
if len(path) < 248 {
// Don't fix. (This is how Go 1.7 and earlier worked,
// not automatically generating the \\?\ form)

View File

@@ -10,6 +10,7 @@ import (
"encoding/binary"
"errors"
"fmt"
"runtime"
"time"
"github.com/syncthing/syncthing/lib/rand"
@@ -122,6 +123,74 @@ func (f FileInfo) WinsConflict(other FileInfo) bool {
return f.Version.Compare(other.Version) == ConcurrentGreater
}
func (f FileInfo) IsEmpty() bool {
return f.Version.Counters == nil
}
// IsEquivalent checks that the two file infos represent the same actual file content,
// i.e. it does purposely not check only selected (see below) struct members.
// Permissions (config) and blocks (scanning) can be excluded from the comparison.
// Any file info is not "equivalent", if it has different
// - type
// - deleted flag
// - invalid flag
// - permissions, unless they are ignored
// A file is not "equivalent", if it has different
// - modification time
// - size
// - blocks, unless there are no blocks to compare (scanning)
// A symlink is not "equivalent", if it has different
// - target
// A directory does not have anything specific to check.
func (f FileInfo) IsEquivalent(other FileInfo, ignorePerms bool, ignoreBlocks bool) bool {
if f.Name != other.Name || f.Type != other.Type || f.Deleted != other.Deleted || f.Invalid != other.Invalid {
return false
}
if !ignorePerms && !f.NoPermissions && !other.NoPermissions && !PermsEqual(f.Permissions, other.Permissions) {
return false
}
switch f.Type {
case FileInfoTypeFile:
return f.Size == other.Size && f.ModTime().Equal(other.ModTime()) && (ignoreBlocks || BlocksEqual(f.Blocks, other.Blocks))
case FileInfoTypeSymlink:
return f.SymlinkTarget == other.SymlinkTarget
case FileInfoTypeDirectory:
return true
}
return false
}
func PermsEqual(a, b uint32) bool {
switch runtime.GOOS {
case "windows":
// There is only writeable and read only, represented for user, group
// and other equally. We only compare against user.
return a&0600 == b&0600
default:
// All bits count
return a&0777 == b&0777
}
}
// BlocksEqual returns whether two slices of blocks are exactly the same hash
// and index pair wise.
func BlocksEqual(a, b []BlockInfo) bool {
if len(b) != len(a) {
return false
}
for i, sblk := range a {
if !bytes.Equal(sblk.Hash, b[i].Hash) {
return false
}
}
return true
}
func (f *FileInfo) Invalidate(invalidatedBy ShortID) {
f.Invalid = true
f.ModifiedBy = invalidatedBy

View File

@@ -109,6 +109,18 @@ func (v Vector) Counter(id ShortID) uint64 {
return 0
}
// DropOthers removes all counters, keeping only the one with given id. If there
// is no such counter, an empty Vector is returned.
func (v Vector) DropOthers(id ShortID) Vector {
for i, c := range v.Counters {
if c.ID == id {
v.Counters = v.Counters[i : i+1]
return v
}
}
return Vector{}
}
// Ordering represents the relationship between two Vectors.
type Ordering int

View File

@@ -285,25 +285,7 @@ func (w *walker) walkRegular(ctx context.Context, relPath string, info fs.FileIn
curMode |= 0111
}
// A file is "unchanged", if it
// - exists
// - has the same permissions as previously, unless we are ignoring permissions
// - was not marked deleted (since it apparently exists now)
// - had the same modification time as it has now
// - was not a directory previously (since it's a file now)
// - was not a symlink (since it's a file now)
// - was not invalid (since it looks valid now)
// - has the same size as previously
cf, ok := w.CurrentFiler.CurrentFile(relPath)
permUnchanged := w.IgnorePerms || !cf.HasPermissionBits() || PermsEqual(cf.Permissions, curMode)
if ok && permUnchanged && !cf.IsDeleted() && cf.ModTime().Equal(info.ModTime()) && !cf.IsDirectory() &&
!cf.IsSymlink() && !cf.IsInvalid() && cf.Size == info.Size() {
return nil
}
if ok {
l.Debugln("rescan:", cf, info.ModTime().Unix(), info.Mode()&fs.ModePerm)
}
f := protocol.FileInfo{
Name: relPath,
@@ -316,6 +298,21 @@ func (w *walker) walkRegular(ctx context.Context, relPath string, info fs.FileIn
ModifiedBy: w.ShortID,
Size: info.Size(),
}
if ok {
if cf.IsEquivalent(f, w.IgnorePerms, true) {
return nil
}
if cf.Invalid {
// We do not want to override the global version with the file we
// currently have. Keeping only our local counter makes sure we are in
// conflict with any other existing versions, which will be resolved by
// the normal pulling mechanisms.
f.Version = f.Version.DropOthers(w.ShortID)
}
l.Debugln("rescan:", cf, info.ModTime().Unix(), info.Mode()&fs.ModePerm)
}
l.Debugln("to hash:", relPath, f)
select {
@@ -328,18 +325,7 @@ func (w *walker) walkRegular(ctx context.Context, relPath string, info fs.FileIn
}
func (w *walker) walkDir(ctx context.Context, relPath string, info fs.FileInfo, dchan chan protocol.FileInfo) error {
// A directory is "unchanged", if it
// - exists
// - has the same permissions as previously, unless we are ignoring permissions
// - was not marked deleted (since it apparently exists now)
// - was a directory previously (not a file or something else)
// - was not a symlink (since it's a directory now)
// - was not invalid (since it looks valid now)
cf, ok := w.CurrentFiler.CurrentFile(relPath)
permUnchanged := w.IgnorePerms || !cf.HasPermissionBits() || PermsEqual(cf.Permissions, uint32(info.Mode()))
if ok && permUnchanged && !cf.IsDeleted() && cf.IsDirectory() && !cf.IsSymlink() && !cf.IsInvalid() {
return nil
}
f := protocol.FileInfo{
Name: relPath,
@@ -351,6 +337,20 @@ func (w *walker) walkDir(ctx context.Context, relPath string, info fs.FileInfo,
ModifiedNs: int32(info.ModTime().Nanosecond()),
ModifiedBy: w.ShortID,
}
if ok {
if cf.IsEquivalent(f, w.IgnorePerms, true) {
return nil
}
if cf.Invalid {
// We do not want to override the global version with the file we
// currently have. Keeping only our local counter makes sure we are in
// conflict with any other existing versions, which will be resolved by
// the normal pulling mechanisms.
f.Version = f.Version.DropOthers(w.ShortID)
}
}
l.Debugln("dir:", relPath, f)
select {
@@ -382,16 +382,7 @@ func (w *walker) walkSymlink(ctx context.Context, relPath string, dchan chan pro
return nil
}
// A symlink is "unchanged", if
// - it exists
// - it wasn't deleted (because it isn't now)
// - it was a symlink
// - it wasn't invalid
// - the target was the same
cf, ok := w.CurrentFiler.CurrentFile(relPath)
if ok && !cf.IsDeleted() && cf.IsSymlink() && !cf.IsInvalid() && cf.SymlinkTarget == target {
return nil
}
f := protocol.FileInfo{
Name: relPath,
@@ -399,6 +390,20 @@ func (w *walker) walkSymlink(ctx context.Context, relPath string, dchan chan pro
Version: cf.Version.Update(w.ShortID),
NoPermissions: true, // Symlinks don't have permissions of their own
SymlinkTarget: target,
ModifiedBy: w.ShortID,
}
if ok {
if cf.IsEquivalent(f, w.IgnorePerms, true) {
return nil
}
if cf.Invalid {
// We do not want to override the global version with the file we
// currently have. Keeping only our local counter makes sure we are in
// conflict with any other existing versions, which will be resolved by
// the normal pulling mechanisms.
f.Version = f.Version.DropOthers(w.ShortID)
}
}
l.Debugln("symlink changedb:", relPath, f)
@@ -475,18 +480,6 @@ func (w *walker) normalizePath(path string, info fs.FileInfo) (normPath string,
return normPath, false
}
func PermsEqual(a, b uint32) bool {
switch runtime.GOOS {
case "windows":
// There is only writeable and read only, represented for user, group
// and other equally. We only compare against user.
return a&0600 == b&0600
default:
// All bits count
return a&0777 == b&0777
}
}
// A byteCounter gets bytes added to it via Update() and then provides the
// Total() and one minute moving average Rate() in bytes per second.
type byteCounter struct {

View File

@@ -498,6 +498,76 @@ func TestStopWalk(t *testing.T) {
}
}
func TestIssue4799(t *testing.T) {
tmp, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmp)
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, tmp)
fd, err := fs.Create("foo")
if err != nil {
t.Fatal(err)
}
fd.Close()
files, err := walkDir(fs, "/foo")
if err != nil {
t.Fatal(err)
}
if len(files) != 1 || files[0].Name != "foo" {
t.Error(`Received unexpected file infos when walking "/foo"`, files)
}
}
func TestIssue4841(t *testing.T) {
tmp, err := ioutil.TempDir("", "")
if err != nil {
t.Fatal(err)
}
defer os.RemoveAll(tmp)
fs := fs.NewFilesystem(fs.FilesystemTypeBasic, tmp)
fd, err := fs.Create("foo")
if err != nil {
panic(err)
}
fd.Close()
fchan := Walk(context.TODO(), Config{
Filesystem: fs,
Subs: nil,
BlockSize: 128 * 1024,
AutoNormalize: true,
Hashers: 2,
CurrentFiler: fakeCurrentFiler{
"foo": {
Name: "foo",
Type: protocol.FileInfoTypeFile,
Invalid: true,
Version: protocol.Vector{}.Update(1),
},
},
ShortID: protocol.LocalDeviceID.Short(),
})
var files []protocol.FileInfo
for f := range fchan {
files = append(files, f)
}
sort.Sort(fileList(files))
if len(files) != 1 {
t.Fatalf("Expected 1 file, got %d: %v", len(files), files)
}
if expected := (protocol.Vector{}.Update(protocol.LocalDeviceID.Short())); !files[0].Version.Equal(expected) {
t.Fatalf("Expected Version == %v, got %v", expected, files[0].Version)
}
}
// Verify returns nil or an error describing the mismatch between the block
// list and actual reader contents
func verify(r io.Reader, blocksize int, blocks []protocol.BlockInfo) error {
@@ -529,3 +599,10 @@ func verify(r io.Reader, blocksize int, blocks []protocol.BlockInfo) error {
return nil
}
type fakeCurrentFiler map[string]protocol.FileInfo
func (fcf fakeCurrentFiler) CurrentFile(name string) (protocol.FileInfo, bool) {
f, ok := fcf[name]
return f, ok
}

View File

@@ -122,7 +122,7 @@ func TestSelectedReleaseMacOS(t *testing.T) {
t.Skip("macOS only")
}
// The alterantives that we expect should work
// The alternatives that we expect should work
assetNames := []string{
"syncthing-macos-amd64-v0.14.47.tar.gz",
"syncthing-macosx-amd64-v0.14.47.tar.gz",

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "STDISCOSRV" "1" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "STDISCOSRV" "1" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
stdiscosrv \- Syncthing Discovery Server
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "STRELAYSRV" "1" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "STRELAYSRV" "1" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
strelaysrv \- Syncthing Relay Server
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-BEP" "7" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-BEP" "7" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-bep \- Block Exchange Protocol v1
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-CONFIG" "5" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-CONFIG" "5" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-config \- Syncthing Configuration
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-DEVICE-IDS" "7" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-DEVICE-IDS" "7" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-device-ids \- Understanding Device IDs
.
@@ -40,11 +40,12 @@ the public key in use.
To understand device IDs we need to look at the underlying mechanisms. At first
startup, Syncthing will create a public/private keypair.
.sp
Currently this is a 3072 bit RSA key. The keys are saved in the form of the
private key (\fBkey.pem\fP) and a self signed certificate (\fBcert.pem\fP). The self
signing part doesnt actually add any security or functionality as far as
Syncthing is concerned but it enables the use of the keys in a standard TLS
exchange.
Currently this is a 384 bit ECDSA key (3072 bit RSA prior to v0.12.5,
which is what is used as an example in this article). The keys are saved in
the form of the private key (\fBkey.pem\fP) and a self signed certificate
(\fBcert.pem\fP). The self signing part doesnt actually add any security or
functionality as far as Syncthing is concerned but it enables the use of the
keys in a standard TLS exchange.
.sp
The typical certificate will look something like this, inspected with
\fBopenssl x509\fP:

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-EVENT-API" "7" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-EVENT-API" "7" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-event-api \- Event API
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-FAQ" "7" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-FAQ" "7" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-faq \- Frequently Asked Questions
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-GLOBALDISCO" "7" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-GLOBALDISCO" "7" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-globaldisco \- Global Discovery Protocol v3
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-LOCALDISCO" "7" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-LOCALDISCO" "7" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-localdisco \- Local Discovery Protocol v4
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-NETWORKING" "7" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-NETWORKING" "7" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-networking \- Firewall Setup
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-RELAY" "7" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-RELAY" "7" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-relay \- Relay Protocol v1
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-REST-API" "7" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-REST-API" "7" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-rest-api \- REST API
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-SECURITY" "7" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-SECURITY" "7" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-security \- Security Principles
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-STIGNORE" "5" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-STIGNORE" "5" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-stignore \- Prevent files from being synchronized to other nodes
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING-VERSIONING" "7" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING-VERSIONING" "7" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing-versioning \- Keep automatic backups of deleted files by other nodes
.

View File

@@ -1,6 +1,6 @@
.\" Man page generated from reStructuredText.
.
.TH "SYNCTHING" "1" "Feb 11, 2018" "v0.14" "Syncthing"
.TH "SYNCTHING" "1" "Mar 04, 2018" "v0.14" "Syncthing"
.SH NAME
syncthing \- Syncthing
.
@@ -48,7 +48,7 @@ syncthing [\-audit] [\-auditfile=<file|\-|\-\->] [\-browser\-only] [device\-id]
.UNINDENT
.SH DESCRIPTION
.sp
Syncthing is an application that lets you synchronize your files across multiple
Syncthing lets you synchronize your files bidirectionally across multiple
devices. This means the creation, modification or deletion of files on one
machine will automatically be replicated to your other devices. We believe your
data is your data alone and you deserve to choose where it is stored. Therefore
@@ -99,8 +99,8 @@ Set destination filename for logging (use \fB"\-"\fP for stdout, which is the de
.INDENT 0.0
.TP
.B \-logflags=<flags>
Select information in log line prefix, default 2. The \fB\-logflags\fP value is
a sum of the following:
Select information in log line prefix. The \fB\-logflags\fP value is a sum of
the following:
.INDENT 7.0
.IP \(bu 2
1: Date
@@ -146,9 +146,11 @@ Start with all devices and folders paused.
.INDENT 0.0
.TP
.B \-reset\-database
Reset the database, forcing a full rescan and resync.
Create \fI\&.stfolder\fP folders in each sync folder if they do not already exist.
\fBCaution\fP: Ensure that all sync folders which are mountpoints are already mounted. Inconsistent versions may result if the mountpoint is later mounted and contains older versions.
Reset the database, forcing a full rescan and resync. Create \fI\&.stfolder\fP
folders in each sync folder if they do not already exist. \fBCaution\fP:
Ensure that all sync folders which are mountpoints are already mounted.
Inconsistent versions may result if the mountpoint is later mounted and
contains older versions.
.UNINDENT
.INDENT 0.0
.TP
@@ -235,33 +237,30 @@ If you start Syncthing from within service managers like systemd or supervisor,
path expansion may not be supported.
.INDENT 0.0
.TP
.B STNODEFAULTFOLDER
Dont create a default folder when starting for the first time. This
variable will be ignored anytime after the first run.
.TP
.B STGUIASSETS
Directory to load GUI assets from. Overrides compiled in assets.
.TP
.B STTRACE
Used to increase the debugging verbosity in specific or all facilities, generally mapping to a Go package. Enabling any of these also enables microsecond timestamps, file names plus line numbers. Enter a comma\-separated string of facilities to trace. \fBsyncthing \-help\fP always outputs an up\-to\-date list. The valid facility strings
are:
Used to increase the debugging verbosity in specific or all facilities,
generally mapping to a Go package. Enabling any of these also enables
microsecond timestamps, file names plus line numbers. Enter a
comma\-separated string of facilities to trace. \fBsyncthing \-help\fP always
outputs an up\-to\-date list. The valid facility strings are:
.INDENT 7.0
.TP
.B Main and operational facilities:
.INDENT 7.0
.TP
.B main
Main package.
.TP
.B model
The root hub; the largest chunk of the system. File pulling, index transmission and requests for chunks.
.TP
.B config
Configuration loading and saving.
.TP
.B db
The database layer.
.TP
.B main
Main package.
.TP
.B model
The root hub; the largest chunk of the system. File pulling, index
transmission and requests for chunks.
.TP
.B scanner
File change detection and hashing.
.TP
@@ -273,7 +272,8 @@ File versioning.
.INDENT 7.0
.TP
.B beacon
Multicast and broadcast discovery packets.
Multicast and broadcast UDP discovery packets: Selected interfaces
and addresses.
.TP
.B connections
Connection handling.
@@ -282,13 +282,8 @@ Connection handling.
Dialing connections.
.TP
.B discover
Remote device discovery requests, replies and registration of devices.
.TP
.B relay
Relay interaction.
.TP
.B protocol
The BEP protocol.
Remote device discovery requests, replies and registration of
devices.
.TP
.B nat
NAT discovery and port mapping.
@@ -296,6 +291,12 @@ NAT discovery and port mapping.
.B pmp
NAT\-PMP discovery and port mapping.
.TP
.B protocol
The BEP protocol.
.TP
.B relay
Relay interaction (\fBstrelaysrv\fP).
.TP
.B upnp
UPnP discovery and port mapping.
.UNINDENT
@@ -303,6 +304,9 @@ UPnP discovery and port mapping.
.B Other facilities:
.INDENT 7.0
.TP
.B fs
Filesystem access.
.TP
.B events
Event generation and logging.
.TP
@@ -321,32 +325,19 @@ Mutexes. Used for debugging race conditions and deadlocks.
.B upgrade
Binary upgrades.
.TP
.B walkfs
Filesystem access while walking.
.TP
.B all
All of the above.
.UNINDENT
.UNINDENT
.TP
.B STPROFILER
Set to a listen address such as “127.0.0.1:9090” to start the profiler with
HTTP access.
.TP
.B STCPUPROFILE
Write a CPU profile to cpu\-$pid.pprof on exit.
.TP
.B STHEAPPROFILE
Write heap profiles to \fBheap\-$pid\-$timestamp.pprof\fP each time heap usage
increases.
.TP
.B STBLOCKPROFILE
Write block profiles to \fBblock\-$pid\-$timestamp.pprof\fP every 20 seconds.
.TP
.B STPERFSTATS
Write running performance statistics to \fBperf\-$pid.csv\fP\&. Not supported on
Windows.
.TP
.B STDEADLOCK
Used for debugging internal deadlocks. Use only under direction of a
developer.
.B STCPUPROFILE
Write a CPU profile to \fBcpu\-$pid.pprof\fP on exit.
.TP
.B STDEADLOCKTIMEOUT
Used for debugging internal deadlocks; sets debug sensitivity. Use only
@@ -356,15 +347,46 @@ under direction of a developer.
Used for debugging internal deadlocks; sets debug sensitivity. Use only
under direction of a developer.
.TP
.B STGUIASSETS
Directory to load GUI assets from. Overrides compiled in assets. Useful for
developing webgui, commonly use \fBSTGUIASSETS=gui bin/syncthing\fP\&.
.TP
.B STHASHING
Specify which hashing package to use. Defaults to automatic based on
performance. Specify “minio” (compatibility) or “standard” for the default
Go implementation.
.TP
.B STHEAPPROFILE
Write heap profiles to \fBheap\-$pid\-$timestamp.pprof\fP each time heap usage
increases.
.TP
.B STNODEFAULTFOLDER
Dont create a default folder when starting for the first time. This
variable will be ignored anytime after the first run.
.TP
.B STNORESTART
Equivalent to the \-no\-restart argument. Disable the Syncthing monitor process which handles restarts for some configuration changes, upgrades, crashes and also log file writing (stdout is still written).
Equivalent to the \fB\-no\-restart\fP flag. Disable the Syncthing monitor
process which handles restarts for some configuration changes, upgrades,
crashes and also log file writing (stdout is still written).
.TP
.B STNOUPGRADE
Disable automatic upgrades.
.TP
.B STHASHING
Specify which hashing package to use. Defaults to automatic based on
performance. Specify “minio” (compatibility) or “standard” for the default Go implementation.
.B STPROFILER
Set to a listen address such as “127.0.0.1:9090” to start the profiler with
HTTP access, which then can be reached at
\fI\%http://localhost:9090/debug/pprof\fP\&. See \fBgo tool pprof\fP for more
information.
.TP
.B STPERFSTATS
Write running performance statistics to \fBperf\-$pid.csv\fP\&. Not supported on
Windows.
.TP
.B STRECHECKDBEVERY
Time before folder statistics (file, dir, … counts) are recalculated from
scratch. The given duration must be parseable by GOs time.ParseDuration. If
missing or not parseable, the default value of 1 month is used. To force
recalculation on every startup, set it to \fB0\fP\&.
.TP
.B GOMAXPROCS
Set the maximum number of CPU cores to use. Defaults to all available CPU

View File

@@ -127,10 +127,9 @@ func TestConflictsDefault(t *testing.T) {
}
rc.AwaitSync("default", sender, receiver)
// The conflict is expected on the s2 side due to how we calculate which
// file is the winner (based on device ID)
// Expect one conflict file, created on either side.
files, err := filepath.Glob("s2/*sync-conflict*")
files, err := filepath.Glob("s?/*sync-conflict*")
if err != nil {
t.Fatal(err)
}
@@ -252,19 +251,26 @@ func TestConflictsInitialMerge(t *testing.T) {
rc.AwaitSync("default", sender, receiver)
// Do it once more so the conflict copies propagate to both sides.
sender.Rescan("default")
receiver.Rescan("default")
rc.AwaitSync("default", sender, receiver)
checkedStop(t, sender)
checkedStop(t, receiver)
log.Println("Verifying...")
// s1 should have three-four files (there's a conflict from s2 which may or may not have synced yet)
// s1 should have four files (there's a conflict)
files, err := filepath.Glob("s1/file*")
if err != nil {
t.Fatal(err)
}
if len(files) < 3 || len(files) > 4 {
t.Errorf("Expected 3-4 files in s1 instead of %d", len(files))
if len(files) != 4 {
t.Errorf("Expected 4 files in s1 instead of %d", len(files))
}
// s2 should have four files (there's a conflict)
@@ -345,7 +351,7 @@ func TestConflictsIndexReset(t *testing.T) {
t.Errorf("Expected 3 files in s1 instead of %d", len(files))
}
// s2 should have three
// s2 should have three files
files, err = filepath.Glob("s2/file*")
if err != nil {
@@ -440,3 +446,88 @@ func TestConflictsIndexReset(t *testing.T) {
t.Errorf("Expected 2 'file2' files in s2 instead of %d", len(files))
}
}
func TestConflictsSameContent(t *testing.T) {
log.Println("Cleaning...")
err := removeAll("s1", "s2", "h1/index*", "h2/index*")
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("s1", 0755)
if err != nil {
t.Fatal(err)
}
err = os.Mkdir("s2", 0755)
if err != nil {
t.Fatal(err)
}
// Two files on s1
err = ioutil.WriteFile("s1/file1", []byte("hello\n"), 0644)
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile("s1/file2", []byte("hello\n"), 0644)
if err != nil {
t.Fatal(err)
}
// Two files on s2, content differs in file1 only, timestamps differ on both.
err = ioutil.WriteFile("s2/file1", []byte("goodbye\n"), 0644)
if err != nil {
t.Fatal(err)
}
err = ioutil.WriteFile("s2/file2", []byte("hello\n"), 0644)
if err != nil {
t.Fatal(err)
}
ts := time.Now().Add(-time.Hour)
os.Chtimes("s2/file1", ts, ts)
os.Chtimes("s2/file2", ts, ts)
// Let them sync
sender := startInstance(t, 1)
defer checkedStop(t, sender)
receiver := startInstance(t, 2)
defer checkedStop(t, receiver)
sender.ResumeAll()
receiver.ResumeAll()
log.Println("Syncing...")
rc.AwaitSync("default", sender, receiver)
// Let conflict copies propagate
sender.Rescan("default")
receiver.Rescan("default")
rc.AwaitSync("default", sender, receiver)
log.Println("Verifying...")
// s1 should have three files
files, err := filepath.Glob("s1/file*")
if err != nil {
t.Fatal(err)
}
if len(files) != 3 {
t.Errorf("Expected 3 files in s1 instead of %d", len(files))
}
// s2 should have three files
files, err = filepath.Glob("s2/file*")
if err != nil {
t.Fatal(err)
}
if len(files) != 3 {
t.Errorf("Expected 3 files in s2 instead of %d", len(files))
}
}

View File

@@ -8,7 +8,6 @@
<minDiskFree unit="%">1</minDiskFree>
<versioning></versioning>
<copiers>1</copiers>
<pullers>16</pullers>
<hashers>0</hashers>
<order>random</order>
<ignoreDelete>false</ignoreDelete>
@@ -28,7 +27,6 @@
<minDiskFree unit="%">1</minDiskFree>
<versioning></versioning>
<copiers>1</copiers>
<pullers>16</pullers>
<hashers>0</hashers>
<order>random</order>
<ignoreDelete>false</ignoreDelete>

View File

@@ -7,7 +7,6 @@
<minDiskFree unit="%">1</minDiskFree>
<versioning></versioning>
<copiers>1</copiers>
<pullers>16</pullers>
<hashers>0</hashers>
<order>random</order>
<ignoreDelete>false</ignoreDelete>
@@ -27,7 +26,6 @@
<minDiskFree unit="%">1</minDiskFree>
<versioning></versioning>
<copiers>1</copiers>
<pullers>16</pullers>
<hashers>0</hashers>
<order>random</order>
<ignoreDelete>false</ignoreDelete>
@@ -47,7 +45,6 @@
<minDiskFree unit="%">1</minDiskFree>
<versioning></versioning>
<copiers>1</copiers>
<pullers>16</pullers>
<hashers>0</hashers>
<order>random</order>
<ignoreDelete>false</ignoreDelete>

View File

@@ -9,7 +9,6 @@
<param key="keep" val="5"></param>
</versioning>
<copiers>1</copiers>
<pullers>16</pullers>
<hashers>0</hashers>
<order>random</order>
<ignoreDelete>false</ignoreDelete>
@@ -29,7 +28,6 @@
<minDiskFree unit="%">1</minDiskFree>
<versioning></versioning>
<copiers>1</copiers>
<pullers>16</pullers>
<hashers>0</hashers>
<order>random</order>
<ignoreDelete>false</ignoreDelete>

Some files were not shown because too many files have changed in this diff Show More