mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-16 01:38:56 -05:00
Compare commits
27 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bea6ecaf35 | ||
|
|
470ef87dd5 | ||
|
|
b31bad1c4d | ||
|
|
8b4346c3ec | ||
|
|
2bdb37d412 | ||
|
|
1471c15b29 | ||
|
|
4b1782cf6d | ||
|
|
71fab4d250 | ||
|
|
22ebc80329 | ||
|
|
74b820f287 | ||
|
|
3949b750f5 | ||
|
|
a26778fa9b | ||
|
|
d4b7be009c | ||
|
|
2751be57dc | ||
|
|
8df90bb475 | ||
|
|
36251b86f7 | ||
|
|
42cc64e2ed | ||
|
|
158859a1e2 | ||
|
|
5822222c74 | ||
|
|
e99be02055 | ||
|
|
1901a5a9f4 | ||
|
|
a27032f09e | ||
|
|
5e041dca9f | ||
|
|
c9ec6159e8 | ||
|
|
5b17aae1b2 | ||
|
|
5931231a32 | ||
|
|
6802505dda |
@@ -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)
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
|
||||
@@ -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];
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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"))
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 != "" {
|
||||
|
||||
@@ -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": "Сканиране",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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í",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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}}",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?": "匿名での使用状況レポートのフォーマットが変わりました。新形式でのレポートに移行しますか?",
|
||||
|
||||
@@ -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?": "익명 사용 리포트의 형식이 변경되었습니다. 새 형식으로 이동 하시겠습니까?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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?": "Формат анонимных отчётов изменился. Вы хотите переключиться на новый формат?",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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?",
|
||||
|
||||
@@ -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?": "Змінився формат анонімного звіту про користування. Бажаєте перейти на новий формат?",
|
||||
|
||||
@@ -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.": "在中介设备上添加的任何“远程设备”,也会被自动添加到本机的“远程设备”列表。",
|
||||
|
||||
@@ -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?": "匿名的使用資訊回報格式已經改變,你想要移至新格式嗎?",
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -107,7 +107,6 @@ func TestDeviceConfig(t *testing.T) {
|
||||
FSWatcherEnabled: false,
|
||||
FSWatcherDelayS: 10,
|
||||
Copiers: 0,
|
||||
Pullers: 0,
|
||||
Hashers: 0,
|
||||
AutoNormalize: true,
|
||||
MinDiskFree: Size{1, "%"},
|
||||
|
||||
@@ -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
16
lib/config/testdata/v27.xml
vendored
Normal 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>
|
||||
@@ -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.
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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},
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
|
||||
package fs
|
||||
|
||||
import "github.com/zillode/notify"
|
||||
import "github.com/Zillode/notify"
|
||||
|
||||
const (
|
||||
subEventMask = notify.NoteDelete | notify.NoteWrite | notify.NoteRename
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
|
||||
package fs
|
||||
|
||||
import "github.com/zillode/notify"
|
||||
import "github.com/Zillode/notify"
|
||||
|
||||
const (
|
||||
subEventMask = notify.All
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/zillode/notify"
|
||||
"github.com/Zillode/notify"
|
||||
)
|
||||
|
||||
func TestMain(m *testing.M) {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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{}
|
||||
|
||||
|
||||
@@ -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{})
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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()
|
||||
}
|
||||
|
||||
@@ -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")),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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
|
||||
.
|
||||
|
||||
@@ -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
|
||||
.
|
||||
|
||||
@@ -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
|
||||
.
|
||||
|
||||
@@ -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
|
||||
.
|
||||
|
||||
@@ -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 doesn’t 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 doesn’t 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:
|
||||
|
||||
@@ -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
|
||||
.
|
||||
|
||||
@@ -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
|
||||
.
|
||||
|
||||
@@ -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
|
||||
.
|
||||
|
||||
@@ -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
|
||||
.
|
||||
|
||||
@@ -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
|
||||
.
|
||||
|
||||
@@ -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
|
||||
.
|
||||
|
||||
@@ -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
|
||||
.
|
||||
|
||||
@@ -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
|
||||
.
|
||||
|
||||
@@ -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
|
||||
.
|
||||
|
||||
@@ -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
|
||||
.
|
||||
|
||||
126
man/syncthing.1
126
man/syncthing.1
@@ -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
|
||||
Don’t 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
|
||||
Don’t 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 GO’s 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
|
||||
|
||||
@@ -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))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
@@ -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
Reference in New Issue
Block a user