Compare commits

...

39 Commits

Author SHA1 Message Date
Jakob Borg
9a549a853b Update goleveldb 2014-11-24 11:57:31 +01:00
Jakob Borg
2dad769a00 Only run Go based integration tests in Docker 2014-11-24 11:49:49 +01:00
Jakob Borg
0ceb14dbf6 Merge pull request #1013 from syncthing/timestamp-before-ext
Use file~timestamp.ext for version (fixes #1010)
2014-11-24 11:44:56 +01:00
Jakob Borg
bab1e26d9b Use source data for genfiles that is guaranteed to exist 2014-11-24 11:37:00 +01:00
Jakob Borg
9a91cc232c Use file~timestamp.ext for version (fixes #1010) 2014-11-24 11:02:14 +01:00
Jakob Borg
5a46cf1d48 Be a little more generous with HTTP timeouts 2014-11-24 10:16:47 +01:00
Jakob Borg
f1e241940b Translation update 2014-11-24 10:10:01 +01:00
Jakob Borg
47b344ba12 Merge pull request #1006 from AudriusButkevicius/defaults
Populate correct defaults
2014-11-24 08:18:31 +01:00
Jakob Borg
afbb06a72f Tests may dirty workspace 2014-11-23 23:10:08 +01:00
Jakob Borg
e336cd463f Tests may take longer than 60 seconds to complete 2014-11-23 23:10:07 +01:00
Jakob Borg
3a8315971e Run integration tests under Docker 2014-11-23 22:31:07 +01:00
Jakob Borg
4ccfa98771 Correct command in README 2014-11-23 22:01:07 +01:00
Jakob Borg
1db120bf06 Improve docker image and build 2014-11-23 21:46:18 +01:00
Audrius Butkevicius
262cf63956 Populate correct defaults 2014-11-23 18:45:45 +00:00
Jakob Borg
fe2ae4c6c3 Merge pull request #997 from syncthing/lig
Minor fixes
2014-11-23 11:35:19 +01:00
Jakob Borg
16d9944dbb Merge pull request #1002 from AudriusButkevicius/routine-cfg
Make copiers, pullers and finishers configurable
2014-11-23 11:29:58 +01:00
Jakob Borg
e9956cc71e Merge pull request #1003 from AudriusButkevicius/needtrim
Use custom structure for /need calls (fixes #1001)
2014-11-23 11:28:38 +01:00
Audrius Butkevicius
59a85c1d75 Use custom structure for /need calls (fixes #1001)
Also, remove trimming by number of blocks as this no longer affects the size
of the response.
2014-11-23 00:52:48 +00:00
Audrius Butkevicius
4427149a38 Make copiers, pullers and finishers configurable
Compliments #999
2014-11-23 00:02:12 +00:00
Audrius Butkevicius
20dee618ea Populate ignores upon adding a folder (fixes #996) 2014-11-22 02:22:09 +00:00
Audrius Butkevicius
37ebbb53be Replace directories/links with files (fixes #580) 2014-11-22 02:22:03 +00:00
Jakob Borg
ba019efaf1 Use a docker container for full builds 2014-11-21 06:48:24 +01:00
Jakob Borg
ce948fc512 Don't leave read only dir around, fails clean 2014-11-20 23:34:14 +01:00
Jakob Borg
2cd9e7fb55 Merge pull request #953 from syncthing/symlink
Symlink support
2014-11-20 16:34:12 +01:00
Jakob Borg
1e2d151684 Copyright notice update 2014-11-20 16:33:16 +01:00
Jakob Borg
ce5651f5fa Integration tests for symlinks 2014-11-20 16:32:01 +01:00
Audrius Butkevicius
20ba0bf4ed Update PROTOCOL.md 2014-11-20 16:32:01 +01:00
Audrius Butkevicius
c325ffd0f8 Add symlink support (fixes #873) 2014-11-20 16:32:00 +01:00
Audrius Butkevicius
6e88d9688b Implement symlinks package 2014-11-20 16:32:00 +01:00
Audrius Butkevicius
bf898f10fb Add symlink support at the protocol level 2014-11-20 16:32:00 +01:00
Audrius Butkevicius
c891999e1d Move filename conversion into osutil 2014-11-20 16:32:00 +01:00
Audrius Butkevicius
938e287501 Code smell 2014-11-20 16:32:00 +01:00
Jakob Borg
edcfc32b1a Add integration test (disabled) for file->dir and dir->file replacement (ref #580) 2014-11-20 16:23:58 +01:00
Jakob Borg
904b211d98 Merge pull request #990 from bigbear2nd/master
Add directory separator to autocomplete. Fixes #984
2014-11-20 16:09:57 +01:00
bigbear2nd
af08567f24 Add directory separator to autocomplete. Fixes #984 2014-11-20 00:26:06 +09:00
Jakob Borg
75ef658962 Correct file mode bits 2014-11-19 07:39:01 +04:00
Jakob Borg
fe2dd79838 Clean up global discovery timer handing 2014-11-19 01:03:43 +04:00
Jakob Borg
bbe7e6525d Finalize s/CONTRIBUTORS/AUTHORS/ 2014-11-18 18:13:19 +04:00
Jakob Borg
ef20df719c Remove redundant style section 2014-11-18 17:18:10 +04:00
79 changed files with 2130 additions and 434 deletions

View File

@@ -70,8 +70,8 @@ International License. You retain the copyright to code you have
written.
When accepting your first contribution, the maintainer of the project
will ensure that you are added to the CONTRIBUTORS file. You are welcome
to add yourself as a separate commit in your first pull request.
will ensure that you are added to the AUTHORS file. You are welcome to
add yourself as a separate commit in your first pull request.
## Building
@@ -101,12 +101,6 @@ signed by GPG key BCE524C7.
Yes please!
## Style
- `go fmt`
- Unix line breaks
## Documentation
[Over here!](http://discourse.syncthing.net/category/documentation)

4
Godeps/Godeps.json generated
View File

@@ -1,6 +1,6 @@
{
"ImportPath": "github.com/syncthing/syncthing",
"GoVersion": "go1.3.3",
"GoVersion": "go1.4rc1",
"Packages": [
"./cmd/..."
],
@@ -51,7 +51,7 @@
},
{
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
"Rev": "d8d1d2a5cc2d34c950dffa2f554525415d59f737"
"Rev": "97e257099d2ab9578151ba85e2641e2cd14d3ca8"
},
{
"ImportPath": "github.com/syndtr/gosnappy/snappy",

View File

@@ -14,6 +14,7 @@ import (
"testing"
"github.com/syndtr/goleveldb/leveldb/cache"
"github.com/syndtr/goleveldb/leveldb/filter"
"github.com/syndtr/goleveldb/leveldb/opt"
"github.com/syndtr/goleveldb/leveldb/storage"
)
@@ -96,21 +97,22 @@ func (h *dbCorruptHarness) deleteRand(n, max int, rnd *rand.Rand) {
}
}
func (h *dbCorruptHarness) corrupt(ft storage.FileType, offset, n int) {
func (h *dbCorruptHarness) corrupt(ft storage.FileType, fi, offset, n int) {
p := &h.dbHarness
t := p.t
var file storage.File
ff, _ := p.stor.GetFiles(ft)
for _, f := range ff {
if file == nil || f.Num() > file.Num() {
file = f
}
sff := files(ff)
sff.sort()
if fi < 0 {
fi = len(sff) - 1
}
if file == nil {
t.Fatalf("no such file with type %q", ft)
if fi >= len(sff) {
t.Fatalf("no such file with type %q with index %d", ft, fi)
}
file := sff[fi]
r, err := file.Open()
if err != nil {
t.Fatal("cannot open file: ", err)
@@ -225,8 +227,8 @@ func TestCorruptDB_Journal(t *testing.T) {
h.build(100)
h.check(100, 100)
h.closeDB()
h.corrupt(storage.TypeJournal, 19, 1)
h.corrupt(storage.TypeJournal, 32*1024+1000, 1)
h.corrupt(storage.TypeJournal, -1, 19, 1)
h.corrupt(storage.TypeJournal, -1, 32*1024+1000, 1)
h.openDB()
h.check(36, 36)
@@ -242,7 +244,7 @@ func TestCorruptDB_Table(t *testing.T) {
h.compactRangeAt(0, "", "")
h.compactRangeAt(1, "", "")
h.closeDB()
h.corrupt(storage.TypeTable, 100, 1)
h.corrupt(storage.TypeTable, -1, 100, 1)
h.openDB()
h.check(99, 99)
@@ -256,7 +258,7 @@ func TestCorruptDB_TableIndex(t *testing.T) {
h.build(10000)
h.compactMem()
h.closeDB()
h.corrupt(storage.TypeTable, -2000, 500)
h.corrupt(storage.TypeTable, -1, -2000, 500)
h.openDB()
h.check(5000, 9999)
@@ -355,7 +357,7 @@ func TestCorruptDB_CorruptedManifest(t *testing.T) {
h.compactMem()
h.compactRange("", "")
h.closeDB()
h.corrupt(storage.TypeManifest, 0, 1000)
h.corrupt(storage.TypeManifest, -1, 0, 1000)
h.openAssert(false)
h.recover()
@@ -370,7 +372,7 @@ func TestCorruptDB_CompactionInputError(t *testing.T) {
h.build(10)
h.compactMem()
h.closeDB()
h.corrupt(storage.TypeTable, 100, 1)
h.corrupt(storage.TypeTable, -1, 100, 1)
h.openDB()
h.check(9, 9)
@@ -387,7 +389,7 @@ func TestCorruptDB_UnrelatedKeys(t *testing.T) {
h.build(10)
h.compactMem()
h.closeDB()
h.corrupt(storage.TypeTable, 100, 1)
h.corrupt(storage.TypeTable, -1, 100, 1)
h.openDB()
h.put(string(tkey(1000)), string(tval(1000, ctValSize)))
@@ -470,3 +472,31 @@ func TestCorruptDB_MissingTableFiles(t *testing.T) {
h.close()
}
func TestCorruptDB_RecoverTable(t *testing.T) {
h := newDbCorruptHarnessWopt(t, &opt.Options{
WriteBuffer: 112 * opt.KiB,
CompactionTableSize: 90 * opt.KiB,
Filter: filter.NewBloomFilter(10),
})
h.build(1000)
h.compactMem()
h.compactRangeAt(0, "", "")
h.compactRangeAt(1, "", "")
seq := h.db.seq
h.closeDB()
h.corrupt(storage.TypeTable, 0, 1000, 1)
h.corrupt(storage.TypeTable, 3, 10000, 1)
// Corrupted filter shouldn't affect recovery.
h.corrupt(storage.TypeTable, 3, 113888, 10)
h.corrupt(storage.TypeTable, -1, 20000, 1)
h.recover()
if h.db.seq != seq {
t.Errorf("invalid seq, want=%d got=%d", seq, h.db.seq)
}
h.check(985, 985)
h.close()
}

View File

@@ -269,7 +269,7 @@ func recoverTable(s *session, o *opt.Options) error {
tableFiles.sort()
var (
mSeq uint64
maxSeq uint64
recoveredKey, goodKey, corruptedKey, corruptedBlock, droppedTable int
// We will drop corrupted table.
@@ -324,7 +324,12 @@ func recoverTable(s *session, o *opt.Options) error {
if err != nil {
return err
}
defer reader.Close()
var closed bool
defer func() {
if !closed {
reader.Close()
}
}()
// Get file size.
size, err := reader.Seek(0, 2)
@@ -392,14 +397,15 @@ func recoverTable(s *session, o *opt.Options) error {
if err != nil {
return err
}
closed = true
reader.Close()
if err := file.Replace(tmp); err != nil {
return err
}
size = newSize
}
if tSeq > mSeq {
mSeq = tSeq
if tSeq > maxSeq {
maxSeq = tSeq
}
recoveredKey += tgoodKey
// Add table to level 0.
@@ -426,11 +432,11 @@ func recoverTable(s *session, o *opt.Options) error {
}
}
s.logf("table@recovery recovered F·%d N·%d Gk·%d Ck·%d Q·%d", len(tableFiles), recoveredKey, goodKey, corruptedKey, mSeq)
s.logf("table@recovery recovered F·%d N·%d Gk·%d Ck·%d Q·%d", len(tableFiles), recoveredKey, goodKey, corruptedKey, maxSeq)
}
// Set sequence number.
rec.setSeqNum(mSeq + 1)
rec.setSeqNum(maxSeq)
// Create new manifest.
if err := s.create(); err != nil {
@@ -625,7 +631,7 @@ func (db *DB) get(key []byte, seq uint64, ro *opt.ReadOptions) (value []byte, er
}
v := db.s.version()
value, cSched, err := v.get(ikey, ro)
value, cSched, err := v.get(ikey, ro, false)
v.release()
if cSched {
// Trigger table compaction.
@@ -634,8 +640,51 @@ func (db *DB) get(key []byte, seq uint64, ro *opt.ReadOptions) (value []byte, er
return
}
func (db *DB) has(key []byte, seq uint64, ro *opt.ReadOptions) (ret bool, err error) {
ikey := newIkey(key, seq, ktSeek)
em, fm := db.getMems()
for _, m := range [...]*memDB{em, fm} {
if m == nil {
continue
}
defer m.decref()
mk, _, me := m.mdb.Find(ikey)
if me == nil {
ukey, _, kt, kerr := parseIkey(mk)
if kerr != nil {
// Shouldn't have had happen.
panic(kerr)
}
if db.s.icmp.uCompare(ukey, key) == 0 {
if kt == ktDel {
return false, nil
}
return true, nil
}
} else if me != ErrNotFound {
return false, me
}
}
v := db.s.version()
_, cSched, err := v.get(ikey, ro, true)
v.release()
if cSched {
// Trigger table compaction.
db.compSendTrigger(db.tcompCmdC)
}
if err == nil {
ret = true
} else if err == ErrNotFound {
err = nil
}
return
}
// Get gets the value for the given key. It returns ErrNotFound if the
// DB does not contain the key.
// DB does not contains the key.
//
// The returned slice is its own copy, it is safe to modify the contents
// of the returned slice.
@@ -651,6 +700,20 @@ func (db *DB) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error) {
return db.get(key, se.seq, ro)
}
// Has returns true if the DB does contains the given key.
//
// It is safe to modify the contents of the argument after Get returns.
func (db *DB) Has(key []byte, ro *opt.ReadOptions) (ret bool, err error) {
err = db.ok()
if err != nil {
return
}
se := db.acquireSnapshot()
defer db.releaseSnapshot(se)
return db.has(key, se.seq, ro)
}
// NewIterator returns an iterator for the latest snapshot of the
// uderlying DB.
// The returned iterator is not goroutine-safe, but it is safe to use

View File

@@ -90,7 +90,7 @@ func (db *DB) newSnapshot() *Snapshot {
}
// Get gets the value for the given key. It returns ErrNotFound if
// the DB does not contain the key.
// the DB does not contains the key.
//
// The caller should not modify the contents of the returned slice, but
// it is safe to modify the contents of the argument after Get returns.
@@ -108,6 +108,23 @@ func (snap *Snapshot) Get(key []byte, ro *opt.ReadOptions) (value []byte, err er
return snap.db.get(key, snap.elem.seq, ro)
}
// Has returns true if the DB does contains the given key.
//
// It is safe to modify the contents of the argument after Get returns.
func (snap *Snapshot) Has(key []byte, ro *opt.ReadOptions) (ret bool, err error) {
err = snap.db.ok()
if err != nil {
return
}
snap.mu.RLock()
defer snap.mu.RUnlock()
if snap.released {
err = ErrSnapshotReleased
return
}
return snap.db.has(key, snap.elem.seq, ro)
}
// NewIterator returns an iterator for the snapshot of the uderlying DB.
// The returned iterator is not goroutine-safe, but it is safe to use
// multiple iterators concurrently, with each in a dedicated goroutine.

View File

@@ -530,7 +530,7 @@ func Test_FieldsAligned(t *testing.T) {
testAligned(t, "session.stSeqNum", unsafe.Offsetof(p2.stSeqNum))
}
func TestDb_Locking(t *testing.T) {
func TestDB_Locking(t *testing.T) {
h := newDbHarness(t)
defer h.stor.Close()
h.openAssert(false)
@@ -538,7 +538,7 @@ func TestDb_Locking(t *testing.T) {
h.openAssert(true)
}
func TestDb_Empty(t *testing.T) {
func TestDB_Empty(t *testing.T) {
trun(t, func(h *dbHarness) {
h.get("foo", false)
@@ -547,7 +547,7 @@ func TestDb_Empty(t *testing.T) {
})
}
func TestDb_ReadWrite(t *testing.T) {
func TestDB_ReadWrite(t *testing.T) {
trun(t, func(h *dbHarness) {
h.put("foo", "v1")
h.getVal("foo", "v1")
@@ -562,7 +562,7 @@ func TestDb_ReadWrite(t *testing.T) {
})
}
func TestDb_PutDeleteGet(t *testing.T) {
func TestDB_PutDeleteGet(t *testing.T) {
trun(t, func(h *dbHarness) {
h.put("foo", "v1")
h.getVal("foo", "v1")
@@ -576,7 +576,7 @@ func TestDb_PutDeleteGet(t *testing.T) {
})
}
func TestDb_EmptyBatch(t *testing.T) {
func TestDB_EmptyBatch(t *testing.T) {
h := newDbHarness(t)
defer h.close()
@@ -588,7 +588,7 @@ func TestDb_EmptyBatch(t *testing.T) {
h.get("foo", false)
}
func TestDb_GetFromFrozen(t *testing.T) {
func TestDB_GetFromFrozen(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{WriteBuffer: 100100})
defer h.close()
@@ -614,7 +614,7 @@ func TestDb_GetFromFrozen(t *testing.T) {
h.get("k2", true)
}
func TestDb_GetFromTable(t *testing.T) {
func TestDB_GetFromTable(t *testing.T) {
trun(t, func(h *dbHarness) {
h.put("foo", "v1")
h.compactMem()
@@ -622,7 +622,7 @@ func TestDb_GetFromTable(t *testing.T) {
})
}
func TestDb_GetSnapshot(t *testing.T) {
func TestDB_GetSnapshot(t *testing.T) {
trun(t, func(h *dbHarness) {
bar := strings.Repeat("b", 200)
h.put("foo", "v1")
@@ -656,7 +656,7 @@ func TestDb_GetSnapshot(t *testing.T) {
})
}
func TestDb_GetLevel0Ordering(t *testing.T) {
func TestDB_GetLevel0Ordering(t *testing.T) {
trun(t, func(h *dbHarness) {
for i := 0; i < 4; i++ {
h.put("bar", fmt.Sprintf("b%d", i))
@@ -679,7 +679,7 @@ func TestDb_GetLevel0Ordering(t *testing.T) {
})
}
func TestDb_GetOrderedByLevels(t *testing.T) {
func TestDB_GetOrderedByLevels(t *testing.T) {
trun(t, func(h *dbHarness) {
h.put("foo", "v1")
h.compactMem()
@@ -691,7 +691,7 @@ func TestDb_GetOrderedByLevels(t *testing.T) {
})
}
func TestDb_GetPicksCorrectFile(t *testing.T) {
func TestDB_GetPicksCorrectFile(t *testing.T) {
trun(t, func(h *dbHarness) {
// Arrange to have multiple files in a non-level-0 level.
h.put("a", "va")
@@ -715,7 +715,7 @@ func TestDb_GetPicksCorrectFile(t *testing.T) {
})
}
func TestDb_GetEncountersEmptyLevel(t *testing.T) {
func TestDB_GetEncountersEmptyLevel(t *testing.T) {
trun(t, func(h *dbHarness) {
// Arrange for the following to happen:
// * sstable A in level 0
@@ -770,7 +770,7 @@ func TestDb_GetEncountersEmptyLevel(t *testing.T) {
})
}
func TestDb_IterMultiWithDelete(t *testing.T) {
func TestDB_IterMultiWithDelete(t *testing.T) {
trun(t, func(h *dbHarness) {
h.put("a", "va")
h.put("b", "vb")
@@ -796,7 +796,7 @@ func TestDb_IterMultiWithDelete(t *testing.T) {
})
}
func TestDb_IteratorPinsRef(t *testing.T) {
func TestDB_IteratorPinsRef(t *testing.T) {
h := newDbHarness(t)
defer h.close()
@@ -820,7 +820,7 @@ func TestDb_IteratorPinsRef(t *testing.T) {
iter.Release()
}
func TestDb_Recover(t *testing.T) {
func TestDB_Recover(t *testing.T) {
trun(t, func(h *dbHarness) {
h.put("foo", "v1")
h.put("baz", "v5")
@@ -842,7 +842,7 @@ func TestDb_Recover(t *testing.T) {
})
}
func TestDb_RecoverWithEmptyJournal(t *testing.T) {
func TestDB_RecoverWithEmptyJournal(t *testing.T) {
trun(t, func(h *dbHarness) {
h.put("foo", "v1")
h.put("foo", "v2")
@@ -856,7 +856,7 @@ func TestDb_RecoverWithEmptyJournal(t *testing.T) {
})
}
func TestDb_RecoverDuringMemtableCompaction(t *testing.T) {
func TestDB_RecoverDuringMemtableCompaction(t *testing.T) {
truno(t, &opt.Options{WriteBuffer: 1000000}, func(h *dbHarness) {
h.stor.DelaySync(storage.TypeTable)
@@ -872,7 +872,7 @@ func TestDb_RecoverDuringMemtableCompaction(t *testing.T) {
})
}
func TestDb_MinorCompactionsHappen(t *testing.T) {
func TestDB_MinorCompactionsHappen(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{WriteBuffer: 10000})
defer h.close()
@@ -896,7 +896,7 @@ func TestDb_MinorCompactionsHappen(t *testing.T) {
}
}
func TestDb_RecoverWithLargeJournal(t *testing.T) {
func TestDB_RecoverWithLargeJournal(t *testing.T) {
h := newDbHarness(t)
defer h.close()
@@ -921,7 +921,7 @@ func TestDb_RecoverWithLargeJournal(t *testing.T) {
v.release()
}
func TestDb_CompactionsGenerateMultipleFiles(t *testing.T) {
func TestDB_CompactionsGenerateMultipleFiles(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{
WriteBuffer: 10000000,
Compression: opt.NoCompression,
@@ -959,7 +959,7 @@ func TestDb_CompactionsGenerateMultipleFiles(t *testing.T) {
}
}
func TestDb_RepeatedWritesToSameKey(t *testing.T) {
func TestDB_RepeatedWritesToSameKey(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{WriteBuffer: 100000})
defer h.close()
@@ -975,7 +975,7 @@ func TestDb_RepeatedWritesToSameKey(t *testing.T) {
}
}
func TestDb_RepeatedWritesToSameKeyAfterReopen(t *testing.T) {
func TestDB_RepeatedWritesToSameKeyAfterReopen(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{WriteBuffer: 100000})
defer h.close()
@@ -993,7 +993,7 @@ func TestDb_RepeatedWritesToSameKeyAfterReopen(t *testing.T) {
}
}
func TestDb_SparseMerge(t *testing.T) {
func TestDB_SparseMerge(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{Compression: opt.NoCompression})
defer h.close()
@@ -1031,7 +1031,7 @@ func TestDb_SparseMerge(t *testing.T) {
h.maxNextLevelOverlappingBytes(20 * 1048576)
}
func TestDb_SizeOf(t *testing.T) {
func TestDB_SizeOf(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{
Compression: opt.NoCompression,
WriteBuffer: 10000000,
@@ -1081,7 +1081,7 @@ func TestDb_SizeOf(t *testing.T) {
}
}
func TestDb_SizeOf_MixOfSmallAndLarge(t *testing.T) {
func TestDB_SizeOf_MixOfSmallAndLarge(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{Compression: opt.NoCompression})
defer h.close()
@@ -1119,7 +1119,7 @@ func TestDb_SizeOf_MixOfSmallAndLarge(t *testing.T) {
}
}
func TestDb_Snapshot(t *testing.T) {
func TestDB_Snapshot(t *testing.T) {
trun(t, func(h *dbHarness) {
h.put("foo", "v1")
s1 := h.getSnapshot()
@@ -1148,7 +1148,7 @@ func TestDb_Snapshot(t *testing.T) {
})
}
func TestDb_SnapshotList(t *testing.T) {
func TestDB_SnapshotList(t *testing.T) {
db := &DB{snapsList: list.New()}
e0a := db.acquireSnapshot()
e0b := db.acquireSnapshot()
@@ -1186,7 +1186,7 @@ func TestDb_SnapshotList(t *testing.T) {
}
}
func TestDb_HiddenValuesAreRemoved(t *testing.T) {
func TestDB_HiddenValuesAreRemoved(t *testing.T) {
trun(t, func(h *dbHarness) {
s := h.db.s
@@ -1229,7 +1229,7 @@ func TestDb_HiddenValuesAreRemoved(t *testing.T) {
})
}
func TestDb_DeletionMarkers2(t *testing.T) {
func TestDB_DeletionMarkers2(t *testing.T) {
h := newDbHarness(t)
defer h.close()
s := h.db.s
@@ -1270,7 +1270,7 @@ func TestDb_DeletionMarkers2(t *testing.T) {
h.allEntriesFor("foo", "[ ]")
}
func TestDb_CompactionTableOpenError(t *testing.T) {
func TestDB_CompactionTableOpenError(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{CachedOpenFiles: -1})
defer h.close()
@@ -1305,7 +1305,7 @@ func TestDb_CompactionTableOpenError(t *testing.T) {
}
}
func TestDb_OverlapInLevel0(t *testing.T) {
func TestDB_OverlapInLevel0(t *testing.T) {
trun(t, func(h *dbHarness) {
if h.o.GetMaxMemCompationLevel() != 2 {
t.Fatal("fix test to reflect the config")
@@ -1348,7 +1348,7 @@ func TestDb_OverlapInLevel0(t *testing.T) {
})
}
func TestDb_L0_CompactionBug_Issue44_a(t *testing.T) {
func TestDB_L0_CompactionBug_Issue44_a(t *testing.T) {
h := newDbHarness(t)
defer h.close()
@@ -1368,7 +1368,7 @@ func TestDb_L0_CompactionBug_Issue44_a(t *testing.T) {
h.getKeyVal("(a->v)")
}
func TestDb_L0_CompactionBug_Issue44_b(t *testing.T) {
func TestDB_L0_CompactionBug_Issue44_b(t *testing.T) {
h := newDbHarness(t)
defer h.close()
@@ -1397,7 +1397,7 @@ func TestDb_L0_CompactionBug_Issue44_b(t *testing.T) {
h.getKeyVal("(->)(c->cv)")
}
func TestDb_SingleEntryMemCompaction(t *testing.T) {
func TestDB_SingleEntryMemCompaction(t *testing.T) {
trun(t, func(h *dbHarness) {
for i := 0; i < 10; i++ {
h.put("big", strings.Repeat("v", opt.DefaultWriteBuffer))
@@ -1414,7 +1414,7 @@ func TestDb_SingleEntryMemCompaction(t *testing.T) {
})
}
func TestDb_ManifestWriteError(t *testing.T) {
func TestDB_ManifestWriteError(t *testing.T) {
for i := 0; i < 2; i++ {
func() {
h := newDbHarness(t)
@@ -1464,7 +1464,7 @@ func assertErr(t *testing.T, err error, wanterr bool) {
}
}
func TestDb_ClosedIsClosed(t *testing.T) {
func TestDB_ClosedIsClosed(t *testing.T) {
h := newDbHarness(t)
db := h.db
@@ -1559,7 +1559,7 @@ func (p numberComparer) Compare(a, b []byte) int {
func (numberComparer) Separator(dst, a, b []byte) []byte { return nil }
func (numberComparer) Successor(dst, b []byte) []byte { return nil }
func TestDb_CustomComparer(t *testing.T) {
func TestDB_CustomComparer(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{
Comparer: numberComparer{},
WriteBuffer: 1000,
@@ -1589,7 +1589,7 @@ func TestDb_CustomComparer(t *testing.T) {
}
}
func TestDb_ManualCompaction(t *testing.T) {
func TestDB_ManualCompaction(t *testing.T) {
h := newDbHarness(t)
defer h.close()
@@ -1627,7 +1627,7 @@ func TestDb_ManualCompaction(t *testing.T) {
h.tablesPerLevel("0,0,1")
}
func TestDb_BloomFilter(t *testing.T) {
func TestDB_BloomFilter(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{
BlockCache: opt.NoCache,
Filter: filter.NewBloomFilter(10),
@@ -1680,7 +1680,7 @@ func TestDb_BloomFilter(t *testing.T) {
h.stor.ReleaseSync(storage.TypeTable)
}
func TestDb_Concurrent(t *testing.T) {
func TestDB_Concurrent(t *testing.T) {
const n, secs, maxkey = 4, 2, 1000
runtime.GOMAXPROCS(n)
@@ -1745,7 +1745,7 @@ func TestDb_Concurrent(t *testing.T) {
runtime.GOMAXPROCS(1)
}
func TestDb_Concurrent2(t *testing.T) {
func TestDB_Concurrent2(t *testing.T) {
const n, n2 = 4, 4000
runtime.GOMAXPROCS(n*2 + 2)
@@ -1816,7 +1816,7 @@ func TestDb_Concurrent2(t *testing.T) {
runtime.GOMAXPROCS(1)
}
func TestDb_CreateReopenDbOnFile(t *testing.T) {
func TestDB_CreateReopenDbOnFile(t *testing.T) {
dbpath := filepath.Join(os.TempDir(), fmt.Sprintf("goleveldbtestCreateReopenDbOnFile-%d", os.Getuid()))
if err := os.RemoveAll(dbpath); err != nil {
t.Fatal("cannot remove old db: ", err)
@@ -1844,7 +1844,7 @@ func TestDb_CreateReopenDbOnFile(t *testing.T) {
}
}
func TestDb_CreateReopenDbOnFile2(t *testing.T) {
func TestDB_CreateReopenDbOnFile2(t *testing.T) {
dbpath := filepath.Join(os.TempDir(), fmt.Sprintf("goleveldbtestCreateReopenDbOnFile2-%d", os.Getuid()))
if err := os.RemoveAll(dbpath); err != nil {
t.Fatal("cannot remove old db: ", err)
@@ -1865,7 +1865,7 @@ func TestDb_CreateReopenDbOnFile2(t *testing.T) {
}
}
func TestDb_DeletionMarkersOnMemdb(t *testing.T) {
func TestDB_DeletionMarkersOnMemdb(t *testing.T) {
h := newDbHarness(t)
defer h.close()
@@ -1876,7 +1876,7 @@ func TestDb_DeletionMarkersOnMemdb(t *testing.T) {
h.getKeyVal("")
}
func TestDb_LeveldbIssue178(t *testing.T) {
func TestDB_LeveldbIssue178(t *testing.T) {
nKeys := (opt.DefaultCompactionTableSize / 30) * 5
key1 := func(i int) string {
return fmt.Sprintf("my_key_%d", i)
@@ -1919,7 +1919,7 @@ func TestDb_LeveldbIssue178(t *testing.T) {
h.assertNumKeys(nKeys)
}
func TestDb_LeveldbIssue200(t *testing.T) {
func TestDB_LeveldbIssue200(t *testing.T) {
h := newDbHarness(t)
defer h.close()
@@ -1946,7 +1946,7 @@ func TestDb_LeveldbIssue200(t *testing.T) {
assertBytes(t, []byte("5"), iter.Key())
}
func TestDb_GoleveldbIssue74(t *testing.T) {
func TestDB_GoleveldbIssue74(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{
WriteBuffer: 1 * opt.MiB,
})
@@ -2044,7 +2044,7 @@ func TestDb_GoleveldbIssue74(t *testing.T) {
wg.Wait()
}
func TestDb_GetProperties(t *testing.T) {
func TestDB_GetProperties(t *testing.T) {
h := newDbHarness(t)
defer h.close()
@@ -2064,7 +2064,7 @@ func TestDb_GetProperties(t *testing.T) {
}
}
func TestDb_GoleveldbIssue72and83(t *testing.T) {
func TestDB_GoleveldbIssue72and83(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{
WriteBuffer: 1 * opt.MiB,
CachedOpenFiles: 3,
@@ -2077,12 +2077,13 @@ func TestDb_GoleveldbIssue72and83(t *testing.T) {
randomData := func(prefix byte, i int) []byte {
data := make([]byte, 1+4+32+64+32)
_, err := crand.Reader.Read(data[1 : len(data)-4])
_, err := crand.Reader.Read(data[1 : len(data)-8])
if err != nil {
panic(err)
}
data[0] = prefix
binary.LittleEndian.PutUint32(data[len(data)-4:], uint32(i))
binary.LittleEndian.PutUint32(data[len(data)-8:], uint32(i))
binary.LittleEndian.PutUint32(data[len(data)-4:], util.NewCRC(data[:len(data)-4]).Value())
return data
}
@@ -2131,12 +2132,22 @@ func TestDb_GoleveldbIssue72and83(t *testing.T) {
continue
}
iter := snap.NewIterator(util.BytesPrefix([]byte{1}), nil)
writei := int(snap.elem.seq/(n*2) - 1)
writei := int(seq/(n*2) - 1)
var k int
for ; iter.Next(); k++ {
k1 := iter.Key()
k2 := iter.Value()
kwritei := int(binary.LittleEndian.Uint32(k2[len(k2)-4:]))
k1checksum0 := binary.LittleEndian.Uint32(k1[len(k1)-4:])
k1checksum1 := util.NewCRC(k1[:len(k1)-4]).Value()
if k1checksum0 != k1checksum1 {
t.Fatalf("READER0 #%d.%d W#%d invalid K1 checksum: %#x != %#x", i, k, k1checksum0, k1checksum0)
}
k2checksum0 := binary.LittleEndian.Uint32(k2[len(k2)-4:])
k2checksum1 := util.NewCRC(k2[:len(k2)-4]).Value()
if k2checksum0 != k2checksum1 {
t.Fatalf("READER0 #%d.%d W#%d invalid K2 checksum: %#x != %#x", i, k, k2checksum0, k2checksum1)
}
kwritei := int(binary.LittleEndian.Uint32(k2[len(k2)-8:]))
if writei != kwritei {
t.Fatalf("READER0 #%d.%d W#%d invalid write iteration num: %d", i, k, writei, kwritei)
}
@@ -2186,7 +2197,7 @@ func TestDb_GoleveldbIssue72and83(t *testing.T) {
wg.Wait()
}
func TestDb_TransientError(t *testing.T) {
func TestDB_TransientError(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{
WriteBuffer: 128 * opt.KiB,
CachedOpenFiles: 3,
@@ -2299,7 +2310,7 @@ func TestDb_TransientError(t *testing.T) {
wg.Wait()
}
func TestDb_UkeyShouldntHopAcrossTable(t *testing.T) {
func TestDB_UkeyShouldntHopAcrossTable(t *testing.T) {
h := newDbHarnessWopt(t, &opt.Options{
WriteBuffer: 112 * opt.KiB,
CompactionTableSize: 90 * opt.KiB,
@@ -2388,7 +2399,7 @@ func TestDb_UkeyShouldntHopAcrossTable(t *testing.T) {
wg.Wait()
}
func TestDb_TableCompactionBuilder(t *testing.T) {
func TestDB_TableCompactionBuilder(t *testing.T) {
stor := newTestStorage(t)
defer stor.Close()

View File

@@ -19,11 +19,12 @@ var _ = testutil.Defer(func() {
o := &opt.Options{
BlockCache: opt.NoCache,
BlockRestartInterval: 5,
BlockSize: 50,
BlockSize: 80,
Compression: opt.NoCompression,
CachedOpenFiles: -1,
Strict: opt.StrictAll,
WriteBuffer: 1000,
CompactionTableSize: 2000,
}
Describe("write test", func() {

View File

@@ -3,15 +3,9 @@ package iterator_test
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/syndtr/goleveldb/leveldb/testutil"
)
func TestIterator(t *testing.T) {
testutil.RunDefer()
RegisterFailHandler(Fail)
RunSpecs(t, "Iterator Suite")
testutil.RunSuite(t, "Iterator Suite")
}

View File

@@ -3,18 +3,9 @@ package leveldb
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/syndtr/goleveldb/leveldb/testutil"
)
func TestLeveldb(t *testing.T) {
testutil.RunDefer()
RegisterFailHandler(Fail)
RunSpecs(t, "Leveldb Suite")
RegisterTestingT(t)
testutil.RunDefer("teardown")
func TestLevelDB(t *testing.T) {
testutil.RunSuite(t, "LevelDB Suite")
}

View File

@@ -3,15 +3,9 @@ package memdb
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/syndtr/goleveldb/leveldb/testutil"
)
func TestMemdb(t *testing.T) {
testutil.RunDefer()
RegisterFailHandler(Fail)
RunSpecs(t, "Memdb Suite")
func TestMemDB(t *testing.T) {
testutil.RunSuite(t, "MemDB Suite")
}

View File

@@ -114,7 +114,7 @@ const (
StrictOverride
// StrictAll enables all strict flags.
StrictAll = StrictManifest | StrictJournalChecksum | StrictJournal | StrictBlockChecksum | StrictCompaction | StrictReader
StrictAll = StrictManifest | StrictJournalChecksum | StrictJournal | StrictBlockChecksum | StrictCompaction | StrictReader | StrictRecovery
// DefaultStrict is the default strict flags. Specify any strict flags
// will override default strict flags as whole (i.e. not OR'ed).
@@ -136,9 +136,14 @@ type Options struct {
// BlockCache provides per-block caching for LevelDB. Specify NoCache to
// disable block caching.
//
// By default LevelDB will create LRU-cache with capacity of 8MiB.
// By default LevelDB will create LRU-cache with capacity of BlockCacheSize.
BlockCache cache.Cache
// BlockCacheSize defines the capacity of the default 'block cache'.
//
// The default value is 8MiB.
BlockCacheSize int
// BlockRestartInterval is the number of keys between restart points for
// delta encoding of keys.
//
@@ -322,6 +327,13 @@ func (o *Options) GetBlockCache() cache.Cache {
return o.BlockCache
}
func (o *Options) GetBlockCacheSize() int {
if o == nil || o.BlockCacheSize <= 0 {
return DefaultBlockCacheSize
}
return o.BlockCacheSize
}
func (o *Options) GetBlockRestartInterval() int {
if o == nil || o.BlockRestartInterval <= 0 {
return DefaultBlockRestartInterval

View File

@@ -17,6 +17,9 @@ func dupOptions(o *opt.Options) *opt.Options {
if o != nil {
*newo = *o
}
if newo.Strict == 0 {
newo.Strict = opt.DefaultStrict
}
return newo
}
@@ -32,7 +35,7 @@ func (s *session) setOptions(o *opt.Options) {
// Block cache.
switch o.GetBlockCache() {
case nil:
no.BlockCache = cache.NewLRUCache(opt.DefaultBlockCacheSize)
no.BlockCache = cache.NewLRUCache(o.GetBlockCacheSize())
case opt.NoCache:
no.BlockCache = nil
}

View File

@@ -233,6 +233,23 @@ func (tf tsFile) Create() (w storage.Writer, err error) {
return
}
func (tf tsFile) Replace(newfile storage.File) (err error) {
ts := tf.ts
ts.mu.Lock()
defer ts.mu.Unlock()
err = tf.checkOpen("replace")
if err != nil {
return
}
err = tf.File.Replace(newfile.(tsFile).File)
if err != nil {
ts.t.Errorf("E: cannot replace file, num=%d type=%v: %v", tf.Num(), tf.Type(), err)
} else {
ts.t.Logf("I: file replace, num=%d type=%v", tf.Num(), tf.Type())
}
return
}
func (tf tsFile) Remove() (err error) {
ts := tf.ts
ts.mu.Lock()
@@ -492,6 +509,10 @@ func newTestStorage(t *testing.T) *testStorage {
}
f.Close()
}
if t.Failed() {
t.Logf("testing failed, test DB preserved at %s", path)
return nil
}
if tsKeepFS {
return nil
}

View File

@@ -373,7 +373,17 @@ func (t *tOps) find(f *tFile, key []byte, ro *opt.ReadOptions) (rkey, rvalue []b
return nil, nil, err
}
defer ch.Release()
return ch.Value().(*table.Reader).Find(key, ro)
return ch.Value().(*table.Reader).Find(key, true, ro)
}
// Finds key that is greater than or equal to the given key.
func (t *tOps) findKey(f *tFile, key []byte, ro *opt.ReadOptions) (rkey []byte, err error) {
ch, err := t.open(f)
if err != nil {
return nil, err
}
defer ch.Release()
return ch.Value().(*table.Reader).FindKey(key, true, ro)
}
// Returns approximate offset of the given key.

View File

@@ -122,6 +122,7 @@ var _ = testutil.Defer(func() {
}
testutil.DoIteratorTesting(&t)
iter.Release()
done <- true
}
}

View File

@@ -50,13 +50,6 @@ func max(x, y int) int {
return y
}
func verifyBlockChecksum(data []byte) bool {
n := len(data) - 4
checksum0 := binary.LittleEndian.Uint32(data[n:])
checksum1 := util.NewCRC(data[:n]).Value()
return checksum0 == checksum1
}
type block struct {
bpool *util.BufferPool
bh blockHandle
@@ -525,21 +518,24 @@ type Reader struct {
filter filter.Filter
verifyChecksum bool
dataEnd int64
indexBH, filterBH blockHandle
indexBlock *block
filterBlock *filterBlock
dataEnd int64
metaBH, indexBH, filterBH blockHandle
indexBlock *block
filterBlock *filterBlock
}
func (r *Reader) blockKind(bh blockHandle) string {
switch bh.offset {
case r.metaBH.offset:
return "meta-block"
case r.indexBH.offset:
return "index-block"
case r.filterBH.offset:
return "filter-block"
default:
return "data-block"
if r.filterBH.length > 0 {
return "filter-block"
}
}
return "data-block"
}
func (r *Reader) newErrCorrupted(pos, size int64, kind, reason string) error {
@@ -565,10 +561,17 @@ func (r *Reader) readRawBlock(bh blockHandle, verifyChecksum bool) ([]byte, erro
if _, err := r.reader.ReadAt(data, int64(bh.offset)); err != nil && err != io.EOF {
return nil, err
}
if verifyChecksum && !verifyBlockChecksum(data) {
r.bpool.Put(data)
return nil, r.newErrCorruptedBH(bh, "checksum mismatch")
if verifyChecksum {
n := bh.length + 1
checksum0 := binary.LittleEndian.Uint32(data[n:])
checksum1 := util.NewCRC(data[:n]).Value()
if checksum0 != checksum1 {
r.bpool.Put(data)
return nil, r.newErrCorruptedBH(bh, fmt.Sprintf("checksum mismatch, want=%#x got=%#x", checksum0, checksum1))
}
}
switch data[bh.length] {
case blockTypeNoCompression:
data = data[:bh.length]
@@ -798,13 +801,7 @@ func (r *Reader) NewIterator(slice *util.Range, ro *opt.ReadOptions) iterator.It
return iterator.NewIndexedIterator(index, opt.GetStrict(r.o, ro, opt.StrictReader))
}
// Find finds key/value pair whose key is greater than or equal to the
// given key. It returns ErrNotFound if the table doesn't contain
// such pair.
//
// The caller should not modify the contents of the returned slice, but
// it is safe to modify the contents of the argument after Find returns.
func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err error) {
func (r *Reader) find(key []byte, filtered bool, ro *opt.ReadOptions, noValue bool) (rkey, value []byte, err error) {
r.mu.RLock()
defer r.mu.RUnlock()
@@ -833,14 +830,17 @@ func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err
r.err = r.newErrCorruptedBH(r.indexBH, "bad data block handle")
return
}
if r.filter != nil {
filterBlock, rel, ferr := r.getFilterBlock(true)
if filtered && r.filter != nil {
filterBlock, frel, ferr := r.getFilterBlock(true)
if ferr == nil {
if !filterBlock.contains(r.filter, dataBH.offset, key) {
rel.Release()
frel.Release()
return nil, nil, ErrNotFound
}
rel.Release()
frel.Release()
} else if !errors.IsCorrupted(ferr) {
err = ferr
return
}
}
data := r.getDataIter(dataBH, nil, r.verifyChecksum, !ro.GetDontFillCache())
@@ -854,21 +854,52 @@ func (r *Reader) Find(key []byte, ro *opt.ReadOptions) (rkey, value []byte, err
}
// Don't use block buffer, no need to copy the buffer.
rkey = data.Key()
if r.bpool == nil {
value = data.Value()
} else {
// Use block buffer, and since the buffer will be recycled, the buffer
// need to be copied.
value = append([]byte{}, data.Value()...)
if !noValue {
if r.bpool == nil {
value = data.Value()
} else {
// Use block buffer, and since the buffer will be recycled, the buffer
// need to be copied.
value = append([]byte{}, data.Value()...)
}
}
return
}
// Find finds key/value pair whose key is greater than or equal to the
// given key. It returns ErrNotFound if the table doesn't contain
// such pair.
// If filtered is true then the nearest 'block' will be checked against
// 'filter data' (if present) and will immediately return ErrNotFound if
// 'filter data' indicates that such pair doesn't exist.
//
// The caller may modify the contents of the returned slice as it is its
// own copy.
// It is safe to modify the contents of the argument after Find returns.
func (r *Reader) Find(key []byte, filtered bool, ro *opt.ReadOptions) (rkey, value []byte, err error) {
return r.find(key, filtered, ro, false)
}
// Find finds key that is greater than or equal to the given key.
// It returns ErrNotFound if the table doesn't contain such key.
// If filtered is true then the nearest 'block' will be checked against
// 'filter data' (if present) and will immediately return ErrNotFound if
// 'filter data' indicates that such key doesn't exist.
//
// The caller may modify the contents of the returned slice as it is its
// own copy.
// It is safe to modify the contents of the argument after Find returns.
func (r *Reader) FindKey(key []byte, filtered bool, ro *opt.ReadOptions) (rkey []byte, err error) {
rkey, _, err = r.find(key, filtered, ro, true)
return
}
// Get gets the value for the given key. It returns errors.ErrNotFound
// if the table does not contain the key.
//
// The caller should not modify the contents of the returned slice, but
// it is safe to modify the contents of the argument after Get returns.
// The caller may modify the contents of the returned slice as it is its
// own copy.
// It is safe to modify the contents of the argument after Find returns.
func (r *Reader) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error) {
r.mu.RLock()
defer r.mu.RUnlock()
@@ -878,7 +909,7 @@ func (r *Reader) Get(key []byte, ro *opt.ReadOptions) (value []byte, err error)
return
}
rkey, value, err := r.Find(key, ro)
rkey, value, err := r.find(key, false, ro, false)
if err == nil && r.cmp.Compare(rkey, key) != 0 {
value = nil
err = ErrNotFound
@@ -950,6 +981,10 @@ func (r *Reader) Release() {
//
// The returned table reader instance is goroutine-safe.
func NewReader(f io.ReaderAt, size int64, fi *storage.FileInfo, cache cache.Namespace, bpool *util.BufferPool, o *opt.Options) (*Reader, error) {
if f == nil {
return nil, errors.New("leveldb/table: nil file")
}
r := &Reader{
fi: fi,
reader: f,
@@ -959,13 +994,12 @@ func NewReader(f io.ReaderAt, size int64, fi *storage.FileInfo, cache cache.Name
cmp: o.GetComparer(),
verifyChecksum: o.GetStrict(opt.StrictBlockChecksum),
}
if f == nil {
return nil, errors.New("leveldb/table: nil file")
}
if size < footerLen {
r.err = r.newErrCorrupted(0, size, "table", "too small")
return r, nil
}
footerPos := size - footerLen
var footer [footerLen]byte
if _, err := r.reader.ReadAt(footer[:], footerPos); err != nil && err != io.EOF {
@@ -975,20 +1009,24 @@ func NewReader(f io.ReaderAt, size int64, fi *storage.FileInfo, cache cache.Name
r.err = r.newErrCorrupted(footerPos, footerLen, "table-footer", "bad magic number")
return r, nil
}
var n int
// Decode the metaindex block handle.
metaBH, n := decodeBlockHandle(footer[:])
r.metaBH, n = decodeBlockHandle(footer[:])
if n == 0 {
r.err = r.newErrCorrupted(footerPos, footerLen, "table-footer", "bad metaindex block handle")
return r, nil
}
// Decode the index block handle.
r.indexBH, n = decodeBlockHandle(footer[n:])
if n == 0 {
r.err = r.newErrCorrupted(footerPos, footerLen, "table-footer", "bad index block handle")
return r, nil
}
// Read metaindex block.
metaBlock, err := r.readBlock(metaBH, true)
metaBlock, err := r.readBlock(r.metaBH, true)
if err != nil {
if errors.IsCorrupted(err) {
r.err = err
@@ -997,9 +1035,12 @@ func NewReader(f io.ReaderAt, size int64, fi *storage.FileInfo, cache cache.Name
return nil, err
}
}
// Set data end.
r.dataEnd = int64(metaBH.offset)
metaIter := r.newBlockIter(metaBlock, nil, nil, false)
r.dataEnd = int64(r.metaBH.offset)
// Read metaindex.
metaIter := r.newBlockIter(metaBlock, nil, nil, true)
for metaIter.Next() {
key := string(metaIter.Key())
if !strings.HasPrefix(key, "filter.") {
@@ -1044,13 +1085,12 @@ func NewReader(f io.ReaderAt, size int64, fi *storage.FileInfo, cache cache.Name
if r.filter != nil {
r.filterBlock, err = r.readFilterBlock(r.filterBH)
if err != nil {
if !errors.IsCorrupted(r.err) {
if !errors.IsCorrupted(err) {
return nil, err
}
// Don't use filter then.
r.filter = nil
r.filterBH = blockHandle{}
}
}
}

View File

@@ -3,15 +3,9 @@ package table
import (
"testing"
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
"github.com/syndtr/goleveldb/leveldb/testutil"
)
func TestTable(t *testing.T) {
testutil.RunDefer()
RegisterFailHandler(Fail)
RunSpecs(t, "Table Suite")
testutil.RunSuite(t, "Table Suite")
}

View File

@@ -23,7 +23,7 @@ type tableWrapper struct {
}
func (t tableWrapper) TestFind(key []byte) (rkey, rvalue []byte, err error) {
return t.Reader.Find(key, nil)
return t.Reader.Find(key, false, nil)
}
func (t tableWrapper) TestGet(key []byte) (value []byte, err error) {

View File

@@ -35,6 +35,10 @@ type Get interface {
TestGet(key []byte) (value []byte, err error)
}
type Has interface {
TestHas(key []byte) (ret bool, err error)
}
type NewIterator interface {
TestNewIterator(slice *util.Range) iterator.Iterator
}
@@ -213,5 +217,6 @@ func DoDBTesting(t *DBTesting) {
}
DoIteratorTesting(&it)
iter.Release()
}
}

View File

@@ -0,0 +1,21 @@
package testutil
import (
. "github.com/onsi/ginkgo"
. "github.com/onsi/gomega"
)
func RunSuite(t GinkgoTestingT, name string) {
RunDefer()
SynchronizedBeforeSuite(func() []byte {
RunDefer("setup")
return nil
}, func(data []byte) {})
SynchronizedAfterSuite(func() {
RunDefer("teardown")
}, func() {})
RegisterFailHandler(Fail)
RunSpecs(t, name)
}

View File

@@ -26,9 +26,11 @@ func KeyValueTesting(rnd *rand.Rand, kv KeyValue, p DB, setup func(KeyValue) DB,
BeforeEach(func() {
p = setup(kv)
})
AfterEach(func() {
teardown(p)
})
if teardown != nil {
AfterEach(func() {
teardown(p)
})
}
}
It("Should find all keys with Find", func() {
@@ -84,6 +86,26 @@ func KeyValueTesting(rnd *rand.Rand, kv KeyValue, p DB, setup func(KeyValue) DB,
}
})
It("Should only find present key with Has", func() {
if db, ok := p.(Has); ok {
ShuffledIndex(nil, kv.Len(), 1, func(i int) {
key_, key, _ := kv.IndexInexact(i)
// Using exact key.
ret, err := db.TestHas(key)
Expect(err).ShouldNot(HaveOccurred(), "Error for key %q", key)
Expect(ret).Should(BeTrue(), "False for key %q", key)
// Using inexact key.
if len(key_) > 0 {
ret, err = db.TestHas(key_)
Expect(err).ShouldNot(HaveOccurred(), "Error for key %q", key_)
Expect(ret).ShouldNot(BeTrue(), "True for key %q", key)
}
})
}
})
TestIter := func(r *util.Range, _kv KeyValue) {
if db, ok := p.(NewIterator); ok {
iter := db.TestNewIterator(r)
@@ -95,6 +117,7 @@ func KeyValueTesting(rnd *rand.Rand, kv KeyValue, p DB, setup func(KeyValue) DB,
}
DoIteratorTesting(&t)
iter.Release()
}
}
@@ -103,7 +126,7 @@ func KeyValueTesting(rnd *rand.Rand, kv KeyValue, p DB, setup func(KeyValue) DB,
done <- true
}, 3.0)
RandomIndex(rnd, kv.Len(), kv.Len(), func(i int) {
RandomIndex(rnd, kv.Len(), Min(kv.Len(), 50), func(i int) {
type slice struct {
r *util.Range
start, limit int
@@ -121,7 +144,7 @@ func KeyValueTesting(rnd *rand.Rand, kv KeyValue, p DB, setup func(KeyValue) DB,
}
})
RandomRange(rnd, kv.Len(), kv.Len(), func(start, limit int) {
RandomRange(rnd, kv.Len(), Min(kv.Len(), 50), func(start, limit int) {
It(fmt.Sprintf("Should iterates and seeks correctly of a slice %d .. %d", start, limit), func(done Done) {
r := kv.Range(start, limit)
TestIter(&r, kv.Slice(start, limit))
@@ -134,10 +157,22 @@ func AllKeyValueTesting(rnd *rand.Rand, body, setup func(KeyValue) DB, teardown
Test := func(kv *KeyValue) func() {
return func() {
var p DB
if setup != nil {
Defer("setup", func() {
p = setup(*kv)
})
}
if teardown != nil {
Defer("teardown", func() {
teardown(p)
})
}
if body != nil {
p = body(*kv)
}
KeyValueTesting(rnd, *kv, p, setup, teardown)
KeyValueTesting(rnd, *kv, p, func(KeyValue) DB {
return p
}, nil)
}
}
@@ -148,4 +183,5 @@ func AllKeyValueTesting(rnd *rand.Rand, body, setup func(KeyValue) DB, teardown
Describe("with big value", Test(KeyValue_BigValue()))
Describe("with special key", Test(KeyValue_SpecialKey()))
Describe("with multiple key/value", Test(KeyValue_MultipleKeyValue()))
Describe("with generated key/value", Test(KeyValue_Generate(nil, 120, 1, 50, 10, 120)))
}

View File

@@ -397,6 +397,7 @@ func (s *Storage) logI(format string, args ...interface{}) {
func (s *Storage) Log(str string) {
s.log(1, "Log: "+str)
s.Storage.Log(str)
}
func (s *Storage) Lock() (r util.Releaser, err error) {

View File

@@ -155,3 +155,17 @@ func RandomRange(rnd *rand.Rand, n, round int, fn func(start, limit int)) {
}
return
}
func Max(x, y int) int {
if x > y {
return x
}
return y
}
func Min(x, y int) int {
if x < y {
return x
}
return y
}

View File

@@ -34,6 +34,10 @@ func (t *testingDB) TestGet(key []byte) (value []byte, err error) {
return t.Get(key, t.ro)
}
func (t *testingDB) TestHas(key []byte) (ret bool, err error) {
return t.Has(key, t.ro)
}
func (t *testingDB) TestNewIterator(slice *util.Range) iterator.Iterator {
return t.NewIterator(slice, t.ro)
}

View File

@@ -114,7 +114,7 @@ func (v *version) walkOverlapping(ikey iKey, f func(level int, t *tFile) bool, l
}
}
func (v *version) get(ikey iKey, ro *opt.ReadOptions) (value []byte, tcomp bool, err error) {
func (v *version) get(ikey iKey, ro *opt.ReadOptions, noValue bool) (value []byte, tcomp bool, err error) {
ukey := ikey.ukey()
var (
@@ -142,7 +142,15 @@ func (v *version) get(ikey iKey, ro *opt.ReadOptions) (value []byte, tcomp bool,
}
}
fikey, fval, ferr := v.s.tops.find(t, ikey, ro)
var (
fikey, fval []byte
ferr error
)
if noValue {
fikey, ferr = v.s.tops.findKey(t, ikey, ro)
} else {
fikey, fval, ferr = v.s.tops.find(t, ikey, ro)
}
switch ferr {
case nil:
case ErrNotFound:

View File

@@ -2,6 +2,8 @@
set -euo pipefail
IFS=$'\n\t'
DOCKERIMGV=1.3.3-3
case "${1:-default}" in
default)
go run build.go
@@ -71,7 +73,7 @@ case "${1:-default}" in
;;
test-cov)
ulimit -t 60 &>/dev/null || true
ulimit -t 600 &>/dev/null || true
ulimit -d 512000 &>/dev/null || true
ulimit -m 512000 &>/dev/null || true
@@ -102,6 +104,33 @@ case "${1:-default}" in
fi
;;
docker-init)
docker build -q -t syncthing/build:$DOCKERIMGV docker
;;
docker-all)
docker run --rm -h syncthing-builder -u $(id -u) -t \
-v $(pwd):/go/src/github.com/syncthing/syncthing \
-w /go/src/github.com/syncthing/syncthing \
syncthing/build:$DOCKERIMGV \
sh -c './build.sh clean && ./build.sh all && STTRACE=all ./build.sh test-cov'
;;
docker-test)
docker run --rm -h syncthing-builder -u $(id -u) -t \
-v $(pwd):/tmp/syncthing \
syncthing/build:$DOCKERIMGV \
sh -euxc 'mkdir -p /go/src/github.com/syncthing \
&& cd /go/src/github.com/syncthing \
&& cp -r /tmp/syncthing syncthing \
&& cd syncthing \
&& ./build.sh clean \
&& ./build.sh \
&& export GOPATH=$(pwd)/Godeps/_workspace:$GOPATH \
&& cd test \
&& go test -tags integration -v'
;;
*)
echo "Unknown build command $1"
;;

View File

@@ -1,6 +1,6 @@
#!/bin/bash
missing-contribs() {
missing-authors() {
for email in $(git log --format=%ae master | sort | uniq) ; do
grep -q "$email" AUTHORS || echo $email
done
@@ -16,8 +16,8 @@ no-docs-typos() {
grep -v f1120d7aa936c0658429edef0037792520b46334
}
print-missing-contribs() {
for email in $(missing-contribs) ; do
print-missing-authors() {
for email in $(missing-authors) ; do
git log --author="$email" --format="%H %ae %s" | no-docs-typos
done
}
@@ -31,8 +31,8 @@ print-line-blame() {
git blame --line-porcelain $f | grep author-mail
done | sort | uniq -c | sort -n
}
echo Author emails missing in CONTRIBUTORS:
print-missing-contribs
echo Author emails missing in AUTHORS file:
print-missing-authors
echo
echo Files missing copyright notice:

View File

@@ -291,10 +291,23 @@ func restGetNeed(m *model.Model, w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var folder = qs.Get("folder")
files := m.NeedFolderFilesLimited(folder, 100, 2500) // max 100 files or 2500 blocks
files := m.NeedFolderFilesLimited(folder, 100) // max 100 files
// Convert the struct to a more loose structure, and inject the size.
output := make([]map[string]interface{}, 0, len(files))
for _, file := range files {
output = append(output, map[string]interface{}{
"Name": file.Name,
"Flags": file.Flags,
"Modified": file.Modified,
"Version": file.Version,
"LocalVersion": file.LocalVersion,
"NumBlocks": file.NumBlocks,
"Size": protocol.BlocksToSize(file.NumBlocks),
})
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(files)
json.NewEncoder(w).Encode(output)
}
func restGetConnections(m *model.Model, w http.ResponseWriter, r *http.Request) {
@@ -658,7 +671,7 @@ func restGetAutocompleteDirectory(w http.ResponseWriter, r *http.Request) {
for _, subdirectory := range subdirectories {
info, err := os.Stat(subdirectory)
if err == nil && info.IsDir() {
ret = append(ret, subdirectory)
ret = append(ret, subdirectory+pathSeparator)
if len(ret) > 9 {
break
}

0
cmd/syncthing/gui_auth.go Executable file → Normal file
View File

0
cmd/syncthing/gui_windows.go Executable file → Normal file
View File

56
docker/Dockerfile Normal file
View File

@@ -0,0 +1,56 @@
FROM debian:squeeze
MAINTAINER Jakob Borg <jakob@nym.se>
ENV GOLANG_VERSION 1.3.3
# SCMs for "go get", gcc for cgo
RUN apt-get update && apt-get install -y \
ca-certificates curl gcc libc6-dev make \
bzr git mercurial unzip \
--no-install-recommends \
&& apt-get clean \
&& rm -rf /var/lib/apt/lists/*
# Get the binary dist of Go to be able to bootstrap gonative.
RUN curl -sSL https://golang.org/dl/go${GOLANG_VERSION}.linux-amd64.tar.gz \
| tar -v -C /usr/local -xz
ENV PATH /usr/local/go/bin:$PATH
RUN mkdir /go
ENV GOPATH /go
ENV PATH /go/bin:$PATH
WORKDIR /go
# Use gonative to install native Go for most arch/OS combos
RUN go get github.com/calmh/gonative \
&& cd /usr/local \
&& rm -rf go \
&& gonative -version $GOLANG_VERSION
# Rebuild the special and missing versions
RUN bash -xec '\
cd /usr/local/go/src; \
for platform in linux/386 freebsd/386 windows/386 linux/arm openbsd/amd64 openbsd/386; do \
GOOS=${platform%/*} \
GOARCH=${platform##*/} \
GOARM=5 \
GO386=387 \
CGO_ENABLED=0 \
./make.bash --no-clean 2>&1; \
done \
&& ./make.bash --no-clean \
'
# Install packages needed for test coverage
RUN go get github.com/tools/godep \
&& go get code.google.com/p/go.tools/cmd/cover \
&& go get github.com/axw/gocov/gocov \
&& go get github.com/AlekSi/gocov-xml
# Random build users needs to be able to create stuff in /go
RUN chmod -R 777 /go/bin /go/pkg /go/src

29
docker/README.md Normal file
View File

@@ -0,0 +1,29 @@
Docker Build
============
Official builds are produced using a Docker image specified by the
Dockerfile in this directory. The following commands exactly reproduce
the official build process.
Create an image called `syncthing/build` with the build environment.
```
./build.sh docker-init
```
> This is a Debian based image containing the latest stable version of
> Go set up for cross compilation. The cross compilation uses the
> dynamically linked standard libraries and SSE instructions for amd64
> builds, but static linking and minimal instruction set for the 386 and
> arm builds. The command should be run in the main repo directory, as a
> user with permission to perform Docker operations.
Build the full set of supported binaries.
```
./build.sh docker-all
```
> This uses a temporary container with the image from above and a volume
> mapped to the directory containing the source. Tests are run and
> binary packages created.

View File

@@ -8,7 +8,7 @@
"Allow Anonymous Usage Reporting?": "Povolit anonymní hlášení o používání?",
"Anonymous Usage Reporting": "Anonymní hlášení o používání",
"Any devices configured on an introducer device will be added to this device as well.": "Jakékoliv přístroje nakonfigurované na zavaděči budou přidány na tento přístroj také.",
"Automatic upgrades": "Automatic upgrades",
"Automatic upgrades": "Automatic upgrade",
"Bugs": "Chyby",
"CPU Utilization": "Využití CPU",
"Close": "Zavřít",
@@ -121,7 +121,7 @@
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maximální doba pro zachování verze (dny, zapsáním hodnoty 0 bude ponecháno navždy).",
"The number of old versions to keep, per file.": "Počet starších verzí k zachování pro každý soubor.",
"The number of versions must be a number and cannot be blank.": "Počet verzí musí být číslo a nemůže být prázdné.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"The rescan interval must be a non-negative number of seconds.": "Interval pro opakování skenování musí být pozitivní číslo.",
"The rescan interval must be at least 5 seconds.": "Interval opakování skenování musí být delší než 5 sekund.",
"Unknown": "Neznámý",
"Up to Date": "Aktuální",

View File

@@ -8,7 +8,7 @@
"Allow Anonymous Usage Reporting?": "Übertragung von anonymen Nutzungsstatistiken erlauben?",
"Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
"Any devices configured on an introducer device will be added to this device as well.": "Alle Geräte, die beim Verteiler eingetragen sind, werden auch bei diesem Gerät eingetragen",
"Automatic upgrades": "Automatic upgrades",
"Automatic upgrades": "Automatisches Upgrade",
"Bugs": "Fehler",
"CPU Utilization": "Prozessorauslastung",
"Close": "Schließen",
@@ -121,7 +121,7 @@
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Die längste Zeit, die alte Versionen vorgehalten werden (in Tagen, 0 bedeutet, alte Versionen für immer zu behalten).",
"The number of old versions to keep, per file.": "Anzahl der alten Versionen, die von jeder Datei gespeichert werden sollen.",
"The number of versions must be a number and cannot be blank.": "Die Anzahl von Versionen muss eine Zahl und darf nicht leer sein.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"The rescan interval must be a non-negative number of seconds.": "Das Suchintervall muss eine nicht negative Anzahl von Sekunden sein.",
"The rescan interval must be at least 5 seconds.": "Das Suchintervall muss mindestens 5 Sekunden betragen.",
"Unknown": "Unbekannt",
"Up to Date": "Aktuell",

View File

@@ -7,8 +7,8 @@
"Addresses": "Címek",
"Allow Anonymous Usage Reporting?": "Engedélyezed a névtelen felhasználási adatok küldését?",
"Anonymous Usage Reporting": "Névtelen felhasználási adatok küldése",
"Any devices configured on an introducer device will be added to this device as well.": "Any devices configured on an introducer device will be added to this device as well.",
"Automatic upgrades": "Automatic upgrades",
"Any devices configured on an introducer device will be added to this device as well.": "Minden eszköz ami a bevezető eszközön lett beállítva hozzá lesz adva ehhez az eszközhöz is.",
"Automatic upgrades": "Automatikus frissítés",
"Bugs": "Hibák",
"CPU Utilization": "Processzor használat",
"Close": "Bezárás",
@@ -49,7 +49,7 @@
"Ignore Patterns": "Figyelmen kívül hagyás",
"Ignore Permissions": "Jogosultságok figyelmen kívül hagyása",
"Incoming Rate Limit (KiB/s)": "Bejövő sebesség korlát (KIB/mp)",
"Introducer": "Introducer",
"Introducer": "Bevezető",
"Inversion of the given condition (i.e. do not exclude)": "A feltétel ellentéte (pl. ki nem hagyás)",
"Keep Versions": "Megtartott verziók",
"Last seen": "Utoljára látva",
@@ -121,7 +121,7 @@
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "A verziók megtartásának maximális ideje (napokban, ha 0-t adsz meg örökre megmaradnak).",
"The number of old versions to keep, per file.": "A megtartott régi verziók száma, fájlonként.",
"The number of versions must be a number and cannot be blank.": "A megtartott verziók száma nem lehet üres",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"The rescan interval must be a non-negative number of seconds.": "Az átnézési intervallum nullánál nagyobb másodperc érték kell legyen",
"The rescan interval must be at least 5 seconds.": "Az átnézési intervallumnak legalább 5 másodpercnek kell lennie.",
"Unknown": "Ismeretlen",
"Up to Date": "Friss",

View File

@@ -7,8 +7,8 @@
"Addresses": "Adresai",
"Allow Anonymous Usage Reporting?": "Siųsti anonimišką vartojimo ataskaitą?",
"Anonymous Usage Reporting": "Anoniminė vartojimo ataskaita",
"Any devices configured on an introducer device will be added to this device as well.": "Any devices configured on an introducer device will be added to this device as well.",
"Automatic upgrades": "Automatic upgrades",
"Any devices configured on an introducer device will be added to this device as well.": "Visi supažindintojo įrenginiai bus pridėti prie jūsų įrenginių sąrašo.",
"Automatic upgrades": "Automatiniai atnaujinimai",
"Bugs": "Klaidos",
"CPU Utilization": "Procesoriaus panaudojimas",
"Close": "Uždaryti",
@@ -36,7 +36,7 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Syncthing programa talpina senesnes versijas .stversions aplanke.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Failai apsaugoti nuo pakeitimų atliktų kituose įrenginiuose, bet pakeitimai šiame įrenginyje bus nusiųsti kitiems.",
"Folder ID": "Aplanko ID",
"Folder Master": "Kelias iki aplanko",
"Folder Master": "Aplanko vadovas",
"Folder Path": "Kelias iki apkanko",
"GUI Authentication Password": "Valdymo skydelio slaptažodis",
"GUI Authentication User": "Valdymo skydelio vartotojo vardas",
@@ -49,7 +49,7 @@
"Ignore Patterns": "Nepaisyti šablonų",
"Ignore Permissions": "Nepaisyti failų prieigos leidimų",
"Incoming Rate Limit (KiB/s)": "Įeinančio srauto maksimalus greitis (KiB/s)",
"Introducer": "Introducer",
"Introducer": "Supažindintojas",
"Inversion of the given condition (i.e. do not exclude)": "Apversti sąlygas (pvz.: nenustoti naudoti)",
"Keep Versions": "Saugojamų versijų kiekis",
"Last seen": "Paskutinį kartą matytas",
@@ -88,8 +88,8 @@
"Shared With": "Dalinamasi su",
"Short identifier for the folder. Must be the same on all cluster devices.": "Trumpas aplanko identifikatorius. Privalo būti toks pat visuose įrenginiuose.",
"Show ID": "Rodyti ID",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Grupės būsenoje rodomas vietoje įrenginio vardo. Kiti įrenginiai matys kaip pasirinktinį vardą.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Grupės būsenoje rodomas vietoje įrenginio vardo. Bus atnaujintas į įrenginio vardą jei nieko neįrašysite.",
"Shutdown": "Išjungti",
"Simple File Versioning": "Supaprastintas versijų valdymas",
"Single level wildcard (matches within a directory only)": "Vieno lygio pakaitos (atitinka tik vieną direktorijos lygį)",
@@ -111,9 +111,9 @@
"The device ID cannot be blank.": "Įrenginio ID negali būti tuščias.",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Įrenginio ID, kurį čia reikia įvesti, gali būti rastas „Keisti > Rodyti vardą“ dialoge kitame įrenginyje. Tarpai ir brūkšneliai nebūtini (ignoruojami).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Kas dieną siunčiama šifruota naudojimo ataskaita. Ji naudojama sekti, kokios platformos naudojamos, aplankų dydžius ir programų versijas. Jei siunčiamų duomenų tipas pasikeis, šis dialogas bus parodytas iš naujo.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Įvestas neteisingas įrenginio ID. Turi būti 52 ar 56 simbolių eilutė su raidėmis ir skaičiais kuriuos galima atskirti tarpu arba brūkšneliu.",
"The folder ID cannot be blank.": "Aplanko ID negali būti tuščias.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Aplanko vardas negali būti ilgesnis nei 64 simboliai. Galima naudoti tik raides ir skaičius bet tašką (.), brūkšnelį (-) ir pabraukimą (_).",
"The folder ID must be unique.": "Aplanko ID turi būti unikalus.",
"The folder path cannot be blank.": "Kelias iki aplanko negali būti tuščias.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Šie pertraukų nustatymai naudojami: pirmą valandą versijos laikomos 30 sekundžių, pirmą dieną versijos laikomos valandą, pirmas 30 dienų versijos laikomos parą, kol nebus viršytas nustatytas maksimalus amžius.",
@@ -121,7 +121,7 @@
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksimalus laikas kurį bus saugojama versija (dienomis, nustatykite 0 norėdami saugoti amžinai).",
"The number of old versions to keep, per file.": "Kiek failo versijų saugoti.",
"The number of versions must be a number and cannot be blank.": "Versijų skaičius turi būti skaitmuo ir negali būti tuščias laukelis.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"The rescan interval must be a non-negative number of seconds.": "Nuskaitymo dažnis negali būti neigiamas skaičius.",
"The rescan interval must be at least 5 seconds.": "Nuskaityti galima nedažniau nei kas 5 sekundes.",
"Unknown": "Nežinoma",
"Up to Date": "Atnaujinta",
@@ -134,7 +134,7 @@
"Versions Path": "Kelias iki versijos",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versijos ištrinamos jei senesnės už nustatyta maksimalų amžių arba jei viršytas maksimalus failų skaičius per nustatytą laiko tarpą.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Pridėdami įrenginį, turėkite omeny, kad šis įrenginys taip pat turi būti pridėtas kitoje pusėje.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Kai įvedate naują aplanką neužmirškite, kad jis bus naudojamas visuose įrenginiuose. Svarbu visur įvesti visiškai tokį pat aplanko vardą neužmirštant apie didžiąsias ir mažąsias raides.",
"Yes": "Taip",
"You must keep at least one version.": "Būtina saugoti bent vieną versiją.",
"full documentation": "pilna dokumentacija",

View File

@@ -5,10 +5,10 @@
"Add Folder": "Legg Til Mappe",
"Address": "Adresse",
"Addresses": "Adresser",
"Allow Anonymous Usage Reporting?": "Tillat Anonym Datainnsamling?",
"Anonymous Usage Reporting": "Anonym Datainnsamling",
"Allow Anonymous Usage Reporting?": "Tillat Anonym Innsamling Av Brukerdata?",
"Anonymous Usage Reporting": "Anonym Innsamling Av Brukerdata",
"Any devices configured on an introducer device will be added to this device as well.": "Enheter konfigurert på en introduksjonsenhet vil også bli lagt til denne enheten.",
"Automatic upgrades": "Automatic upgrades",
"Automatic upgrades": "Automatiske oppdateringer",
"Bugs": "Programfeil",
"CPU Utilization": "CPU-utnyttelse",
"Close": "Lukk",
@@ -121,7 +121,7 @@
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksimal tid å beholde en versjon (i dager, sett til 0 for å beholde versjoner på ubegrenset tid).",
"The number of old versions to keep, per file.": "Antall gamle versjoner å beholde, per fil.",
"The number of versions must be a number and cannot be blank.": "Antall versjoner må være et tall og kan ikke være tomt.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"The rescan interval must be a non-negative number of seconds.": "Antall sekund i skanneintervallet kan ikke være negativt.",
"The rescan interval must be at least 5 seconds.": "Skanneintervallet må være minst 5 sekund.",
"Unknown": "Ukjent",
"Up to Date": "Oppdatert",

142
gui/lang/lang-nl.json Normal file
View File

@@ -0,0 +1,142 @@
{
"API Key": "API-sleutel",
"About": "Over",
"Add Device": "Toestel toevoegen",
"Add Folder": "Folder toevoegen",
"Address": "Adres",
"Addresses": "Adressen",
"Allow Anonymous Usage Reporting?": "Bijhouden van anonieme gebruikers statistieken toestaan?",
"Anonymous Usage Reporting": "Bijhouden anonieme gebruikers statistieken",
"Any devices configured on an introducer device will be added to this device as well.": "Toestellen geconfigureerd op een introductie toestel zullen ook aan dit toestel worden toegevoegd.",
"Automatic upgrades": "Automatisch bijwerken",
"Bugs": "Fouten",
"CPU Utilization": "CPU Gebruik",
"Close": "Sluiten",
"Comment, when used at the start of a line": "Commentaar, indien gebruikt aan het begin van de lijn",
"Compression is recommended in most setups.": "Gegevenscompressie is aan te raden in de meeste situaties.",
"Connection Error": "Verbindingsfout",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg en de onderstaande bijdragers:",
"Delete": "Verwijderen",
"Device ID": "Toestel ID",
"Device Identification": "Toestel identificatie",
"Device Name": "Naam toestel",
"Disconnected": "Niet Verbonden",
"Documentation": "Documentatie",
"Download Rate": "Downloadsnelheid",
"Edit": "Bewerk",
"Edit Device": "Toestel aanpassen",
"Edit Folder": "Folder aanpassen",
"Editing": "Bezig met aanpassen",
"Enable UPnP": "UPnP aanzetten",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Geef, gescheiden door komma's, \"ip:port\" adressen of \"dynamic\" voor het automatische vinden van de addressen.",
"Enter ignore patterns, one per line.": "Geef te negeren patronen, één per regel.",
"Error": "Fout",
"File Versioning": "Versiebeheer",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Bestands permissiebits worden genegeerd wanneer naar veranderingen wordt gekeken. Gebruik dit op FAT bestandsystemen",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Bestanden worden naar de .stversions map verplaatst met een tijdsaanduiding, wanneer ze aangepast of verwijderd worden door syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Bestanden zijn beschermt tegen aanpassingen gemaakt door andere toestellen maar aanpassingen op dit toestel worden doorgestuurd naar de rest van de cluster.",
"Folder ID": "Folder ID",
"Folder Master": "Hoofdfolder",
"Folder Path": "Locatie folder",
"GUI Authentication Password": "GUI Authentificatie Wachtwoord",
"GUI Authentication User": "GUI Authentificatie Gebruikersnaam",
"GUI Listen Addresses": "GUI Inkomend adres",
"Generate": "Genereer",
"Global Discovery": "Globaal zoeken",
"Global Discovery Server": "Globale zoekserver",
"Global State": "Globale status",
"Idle": "Inactief",
"Ignore Patterns": "Te negeren patronen",
"Ignore Permissions": "Rechten negeren",
"Incoming Rate Limit (KiB/s)": "Download snelheidslimiet (KiB/s)",
"Introducer": "Introductietoestel",
"Inversion of the given condition (i.e. do not exclude)": "Inversie van de gegeven voorwaarde (bv. niet uitsluiten)",
"Keep Versions": "Versies behouden",
"Last seen": "Laatst gezien op",
"Latest Release": "Laatste uitgave",
"Local Discovery": "Lokaal zoeken",
"Local State": "Lokale status",
"Maximum Age": "Maximum leeftijd",
"Multi level wildcard (matches multiple directory levels)": "Wildcard op meerder niveaus (toepasbaar op meerdere niveaus van folders)",
"Never": "Nooit",
"No": "Nee",
"No File Versioning": "Geen versiebeheer",
"Notice": "Notificatie",
"OK": "OK",
"Offline": "Offline",
"Online": "Online",
"Out Of Sync": "Niet gesynchroniseerd",
"Outgoing Rate Limit (KiB/s)": "Uitgaande snelheidslimiet (KiB/s)",
"Override Changes": "Veranderingen overschrijven",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Locatie van de folder op de lokale computer. Zal aangemaakt worden wanneer deze niet bestaat. De tilde (~) kan gebruikt in plaats van",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Locatie waar de versies opgeslagen moeten worden (leeg laten voor de standaard .stversions subfolder).",
"Please wait": "Even geduld",
"Preview": "Voorbeeld",
"Preview Usage Report": "Bekijk gebruiksstatistieken",
"Quick guide to supported patterns": "Snelgids voor ondersteunde patronen",
"RAM Utilization": "RAM gebruik",
"Rescan": "Opnieuw scannen",
"Rescan Interval": "Scanfrequentie",
"Restart": "Herstart",
"Restart Needed": "Herstart nodig",
"Restarting": "Herstarten",
"Save": "Bewaar",
"Scanning": "Aan het zoeken",
"Select the devices to share this folder with.": "Selecteer de toestellen om deze folder mee te delen.",
"Settings": "Instellingen",
"Share With Devices": "Delen met toestellen",
"Shared With": "Gedeeld met",
"Short identifier for the folder. Must be the same on all cluster devices.": "Korte aanduiding voor deze folder. Moet dezelfde zijn op alle toestellen in de cluster.",
"Show ID": "Toon ID",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wordt getoond in plaats van de toestel ID in de cluster staat. Wordt doorgegeven aan andere toestellen as een bijkomende standaard toestelnaam.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wordt getoond in plaats van de toestel ID in de cluster staat. Wanneer leeg wordt deze aangepast met de naam aangekondigd door het toestel.",
"Shutdown": "Sluit af",
"Simple File Versioning": "Eenvoudig versiebeheer",
"Single level wildcard (matches within a directory only)": "Wildcard op enkel niveau (toepasbaar binnen een enkele folder)",
"Source Code": "Broncode",
"Staggered File Versioning": "Gelaagd versiebeheer",
"Start Browser": "Start browser",
"Stopped": "Gestopt",
"Support / Forum": "Support / Forum",
"Sync Protocol Listen Addresses": "Synchronisatie protocol luister adres",
"Synchronization": "Synchronisatie",
"Syncing": "Aan het synchroniseren",
"Syncthing has been shut down.": "Syncthing is afgesloten",
"Syncthing includes the following software or portions thereof:": "De volgende software of delen daarvan zijn onderdeel van syncthing:",
"Syncthing is restarting.": "Syncthing is aan het herstarten.",
"Syncthing is upgrading.": "Syncthing is aan het upgraden.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing lijkt afgesloten te zijn, of er is een verbindingsprobleem met het internet. Nieuwe poging....",
"The aggregated statistics are publicly available at {%url%}.": "The verzamelde statistieken zijn publiek beschikbaar op {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "De configuratie is opslagen maar nog niet actief. Syncthing moet opnieuw opgestart worden om de nieuwe configuratie te activeren.",
"The device ID cannot be blank.": "Het toestel ID mag niet leeg zijn.",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Het verwachte toestel ID kan teruggevonden worden in het \"Aanpassen > Toon ID\" scherm op het andere toestel. Spaties en streepjes zijn facultatief (worden genegeerd).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Het versleutelde gebruiksrapport wordt dagelijks opgestuurd en wordt gebruikt om de verschillende platformen, folder groottes en versies op te volgen. Als de reeks gegevens wijzigt zal opnieuw toestemming gevraagd worden.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Dit toestel ID lijkt ongeldig. Het toestel ID bestaat uit 52 of 56 letters en nummers met facultatieve spaties en streepjes.",
"The folder ID cannot be blank.": "De folder ID mag niet leeg zijn.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "De folder ID mag maximaal 64 tekens lang zijn en bestaat enkel uit letters, nummers, punten (.), streepjes (-) en onderstrepingstekens (_).",
"The folder ID must be unique.": "De folder ID moet uniek zijn.",
"The folder path cannot be blank.": "De folder locatie mag niet leeg zijn.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De volgende intervallen worden gebruikt: het eerste uur worden versies iedere 30 seconden bewaard, de eerste dag worden versies ieder uur bewaard, de eerste 30 dagen worden versies iedere dag bewaard, tot de maximale leeftijd worden versies iedere week bewaard.",
"The maximum age must be a number and cannot be blank.": "De maximum leeftijd moet uit cijfers bestaan en mag niet leeggelaten worden.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "De maximale tijdsduur om een versie te bewaren (in dagen, gebruik 0 om versies voor altijd te bewaren).",
"The number of old versions to keep, per file.": "Het aantal versies dat bewaard moet worden per file.",
"The number of versions must be a number and cannot be blank.": "Het aantal nummers moet een getal zijn en mag niet leeg blijven.",
"The rescan interval must be a non-negative number of seconds.": "De scanfrequentie moet een positief getal in seconden zijn.",
"The rescan interval must be at least 5 seconds.": "De scanfrequentie moet minimaal 5 seconden zijn.",
"Unknown": "Onbekend",
"Up to Date": "Gesynchroniseerd",
"Upgrade To {%version%}": "Upgrade naar {{version}}",
"Upgrading": "Bezig met upgrade",
"Upload Rate": "Upload snelheid",
"Use Compression": "Compressie gebruiken",
"Use HTTPS for GUI": "Gebruik HTTPS voor de GUI",
"Version": "Versie",
"Versions Path": "Locatie versies",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versies worden automatisch verwijderd als deze ouder zijn dan de maximale leeftijd of als ze het maximaal aantal toegestane bestanden per interval overschrijden. ",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Onthoud dat een toegevoegd toestel ook aan de andere kant moet worden toegevoegd.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Onthoud, bij het toevoegen van een folder, dat de folder ID gebruikt wordt om folders tussen toestellen te verbinden. Ze zijn hoofdletter gevoelig en moeten exact hetzelfde zijn op de andere toestellen.",
"Yes": "Ja",
"You must keep at least one version.": "Minstens 1 versie moet bewaard blijven.",
"full documentation": "volledige documentatie",
"items": "objecten"
}

View File

@@ -5,10 +5,10 @@
"Add Folder": "Legg Til Mappe",
"Address": "Adresse",
"Addresses": "Adresser",
"Allow Anonymous Usage Reporting?": "Tillat Anonym Datainnsamling?",
"Anonymous Usage Reporting": "Tillat Anonym Datainnsamling",
"Allow Anonymous Usage Reporting?": "Tillat Anonym Innsamling Av Brukardata?",
"Anonymous Usage Reporting": "Tillat Anonym Innsamling Av Brukardata",
"Any devices configured on an introducer device will be added to this device as well.": "Einingar konfigurert på ein introduksjonseining vil òg verte lagt til denne eininga.",
"Automatic upgrades": "Automatic upgrades",
"Automatic upgrades": "Automatiske oppdateringar",
"Bugs": "Programfeil",
"CPU Utilization": "CPU-utnytting",
"Close": "Lukk",
@@ -121,7 +121,7 @@
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksimalt tidsrom å behalda ein versjon (i dagar, set til 0 for å behalda versjonar for ubegrensa tid.)",
"The number of old versions to keep, per file.": "Tal på gamle versjonar ein skal behalda, per fil.",
"The number of versions must be a number and cannot be blank.": "Tal på versjonar må vera eit tal og kan ikkje vera tomt.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"The rescan interval must be a non-negative number of seconds.": "Talet på sekund i skanneintervallet kan ikkje vera negativt.",
"The rescan interval must be at least 5 seconds.": "Skanneintervallet må vera minst 5 sekund.",
"Unknown": "Ukjent",
"Up to Date": "Oppdatert",

View File

@@ -8,7 +8,7 @@
"Allow Anonymous Usage Reporting?": "Permitir envio de relatórios anónimos de utilização?",
"Anonymous Usage Reporting": "Enviar relatórios anónimos de utilização",
"Any devices configured on an introducer device will be added to this device as well.": "Quaisquer dispositivos configurados num dispositivo apresentador serão também adicionados a este dispositivo.",
"Automatic upgrades": "Automatic upgrades",
"Automatic upgrades": "Actualizações automáticas",
"Bugs": "Erros",
"CPU Utilization": "Utilização da CPU",
"Close": "Fechar",
@@ -121,7 +121,7 @@
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Tempo máximo para manter uma versão (em dias, use 0 para manter a versão para sempre).",
"The number of old versions to keep, per file.": "O número de versões antigas a manter, por ficheiro.",
"The number of versions must be a number and cannot be blank.": "O número de versões tem que ser um número e não pode estar vazio.",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"The rescan interval must be a non-negative number of seconds.": "O intervalo entre verificações tem que ser um valor não negativo de segundos.",
"The rescan interval must be at least 5 seconds.": "O intervalo entre verificações tem que ser pelo menos de 5 segundos.",
"Unknown": "Desconhecido",
"Up to Date": "Actualizado",

142
gui/lang/lang-ru.json Normal file
View File

@@ -0,0 +1,142 @@
{
"API Key": "Ключ API",
"About": "О программе",
"Add Device": "Добавить устройство",
"Add Folder": "Добавить папку",
"Address": "Адрес",
"Addresses": "Адреса",
"Allow Anonymous Usage Reporting?": "Разрешить сбор анонимной статистики использования?",
"Anonymous Usage Reporting": "Анонимная статистика использования",
"Any devices configured on an introducer device will be added to this device as well.": "Все устройства, подключённые к устройству-рекомендателю, будут добавлены к текущему устройству.",
"Automatic upgrades": "Автообновление",
"Bugs": "Ошибки",
"CPU Utilization": "Загрузка ЦПУ",
"Close": "Закрыть",
"Comment, when used at the start of a line": "Комментарий, если используется в начале строки",
"Compression is recommended in most setups.": "Сжатие рекомендуется в большинстве случаев.",
"Connection Error": "Ошибка подключения",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Все права защищены © 2014 Jakob Borg и следующие участники:",
"Delete": "Удалить",
"Device ID": "ID устройства",
"Device Identification": "Идентификация устройства",
"Device Name": "Имя устройства",
"Disconnected": "Нет соединения",
"Documentation": "Документация",
"Download Rate": "Скорость загрузки",
"Edit": "Изменить",
"Edit Device": "Изменить устройство",
"Edit Folder": "Изменение папки",
"Editing": "Редактирование",
"Enable UPnP": "Включить UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": " Введите пары \"IP:PORT\" разделённые запятыми, или слово \"dynamic\" для автоматического обнаружения адреса.",
"Enter ignore patterns, one per line.": "Введите шаблон игнорирования, по-одному на строку.",
"Error": "Ошибка",
"File Versioning": "Управление версиями",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Права доступа к файлам будут игнорироваться. Используйте на файловых системах типа FAT.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Версии файлов с временнОй меткой перемещаются в директорию .stversions, если они удалены или перемещены в процессе синхронизации.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Файлы защищены от изменений сделанных на других устройствах, но изменения сделанные на этом устройстве будут отправлены всему кластеру.",
"Folder ID": "ID папки",
"Folder Master": "Папка-оригинал",
"Folder Path": "Путь к папке",
"GUI Authentication Password": "Пароль для доступа к панели управления",
"GUI Authentication User": "Имя пользователя для доступа к панели управления",
"GUI Listen Addresses": "Адрес панели управления",
"Generate": "Сгенерировать",
"Global Discovery": "Глобальное обнаружение",
"Global Discovery Server": "Сервер глобального обнаружения",
"Global State": "Глобальное состояние",
"Idle": "Бездействует",
"Ignore Patterns": "Шаблоны игнорирования",
"Ignore Permissions": "Игнорировать файловые права доступа",
"Incoming Rate Limit (KiB/s)": "Ограничение входящего потока (Кбит/сек)",
"Introducer": "Рекомендатель",
"Inversion of the given condition (i.e. do not exclude)": "Инвертировать текущее условие (например, исключить)",
"Keep Versions": "Количество хранимых версий",
"Last seen": "Был доступен",
"Latest Release": "Последняя версия",
"Local Discovery": "Локальное обнаружение",
"Local State": "Локальное состояние",
"Maximum Age": "Максимальный срок",
"Multi level wildcard (matches multiple directory levels)": "Многоуровневая маска (поиск совпадений во всех подпапках)",
"Never": "Никогда",
"No": "Нет",
"No File Versioning": "Без управления версиями файлов",
"Notice": "Внимание",
"OK": "ОК",
"Offline": "Оффлайн",
"Online": "Онлайн",
"Out Of Sync": "Не синхронизировано",
"Outgoing Rate Limit (KiB/s)": "Предел скорости отдачи (KiB/s)",
"Override Changes": "Перезаписать изменения",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Путь к папке на локальном компьютере. Если её не существует, то она будет создана. Тильда (~) может использоваться как сокращение для",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Путь, где должны храниться версии (оставьте пустым по-умолчанию для папки .stversions в папке).",
"Please wait": "Пожалуйста, подождите",
"Preview": "Предварительный просмотр",
"Preview Usage Report": "Посмотреть отчёт об использовании",
"Quick guide to supported patterns": "Краткое руководство по поддерживаемым шаблонам",
"RAM Utilization": "Использование ОЗУ",
"Rescan": "Пересканирование",
"Rescan Interval": "Интервал пересканирования",
"Restart": "Перезапуск",
"Restart Needed": "Требуется перезапуск",
"Restarting": "Перезапуск",
"Save": "Сохранить",
"Scanning": "Сканирование",
"Select the devices to share this folder with.": "Выберите устройства, для которых будет доступна эта папка.",
"Settings": "Настройки",
"Share With Devices": "Предоставить доступ устройствам",
"Shared With": "Доступ предоставлен",
"Short identifier for the folder. Must be the same on all cluster devices.": "Короткий идентификатор папки. Должен быть одинаковым на всех устройствах кластера.",
"Show ID": "Показать ID",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Отображается вместо ID устройства в статусе группы. Будет разослан другим устройствам в качестве имени по умолчанию.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Отображается вместо ID устройства в статусе группы. Если поле не заполнено, то будет установлено имя, передаваемое этим устройством.",
"Shutdown": "Выключить",
"Simple File Versioning": "Простое управление версиями файлов",
"Single level wildcard (matches within a directory only)": "Одноуровневая маска (поиск совпадений только внутри папки)",
"Source Code": "Исходный код",
"Staggered File Versioning": "Ступенчатое управление версиями файлов",
"Start Browser": "Открыть браузер",
"Stopped": "Остановлено",
"Support / Forum": "Поддержка / Форум",
"Sync Protocol Listen Addresses": "Адрес протокола синхронизации",
"Synchronization": "Синхронизация",
"Syncing": "Синхронизация",
"Syncthing has been shut down.": "Syncthing выключен.",
"Syncthing includes the following software or portions thereof:": "Syncthing включает в себя следующее ПО или его части:",
"Syncthing is restarting.": "Перезапуск Syncthing",
"Syncthing is upgrading.": "Обновление Syncthing ",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Кажется, Syncthing не запущен или есть проблемы с подключением к Интернету. Переподключаюсь...",
"The aggregated statistics are publicly available at {%url%}.": "Суммарная статистика общедоступна на {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Конфигурация была сохранена но не активирована. Для активации новой конфигурации необходимо рестартовать Syncthing.",
"The device ID cannot be blank.": "ID устройства не может быть пустым.",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Идентификатор устройства для ввода здесь, может быть найден в диалоге \"Редактирование > Показать ID\" на другом устройстве. Пробелы и тире не обязательны (игнорируются).",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Зашифрованный отчет об использовании отправляется ежедневно. Это используется для отслеживания общих платформ, размеров папок и версий приложения. Если отчетные данные изменятся, вам будет снова показано это диалоговое окно.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Введённое ID устройства не валидное. Оно должно состоять из букв и цифр, может включать пробелы и дефисы, его длина должна быть от 52 до 56 символов, ",
"The folder ID cannot be blank.": "ID папки не может быть пустым.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "ID папки должен быть коротким (не более 64 символов), должен состоять только из букв, цифр, точек (.), дефисов (-) или подчёркиваний (_).",
"The folder ID must be unique.": "ID папки должен быть уникальным.",
"The folder path cannot be blank.": "Путь к папке не должен быть пустым.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Используются следующие интервалы: в первый час версия меняется каждые 30 секунд, в первый день - каждый час, первые 30 дней - каждый день, после, до максимального срока - каждую неделю.",
"The maximum age must be a number and cannot be blank.": "Максимальный срок должен быть числом и не может быть пустым.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максимальный срок хранения версии (в днях, 0 значит вечное хранение).",
"The number of old versions to keep, per file.": "Количество хранимых версий файла.",
"The number of versions must be a number and cannot be blank.": "Количество версий должно быть числом и не может быть пустым.",
"The rescan interval must be a non-negative number of seconds.": "Интервал пересканирования должен быть неотрицательным количеством секунд.",
"The rescan interval must be at least 5 seconds.": "Интервал пересканирования должен быть хотя бы 5 секунд.",
"Unknown": "Неизвестно",
"Up to Date": "Обновлено",
"Upgrade To {%version%}": "Обновить до {{version}}",
"Upgrading": "Обновление",
"Upload Rate": "Скорость отдачи",
"Use Compression": "Использовать сжатие",
"Use HTTPS for GUI": "Использовать HTTPS для панели управления",
"Version": "Версия",
"Versions Path": "Путь к версиям",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Версии удаляются автоматически, если они существуют дольше максимального срока или превышают разрешённое количество файлов за интервал.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Когда добавляете устройство, помните о том, что это же устройство должно быть добавлено и другой стороной.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Когда добавляете новую папку, помните, что ID папки используются для того, чтобы связывать папки между всеми устройствами. Они чувствительны к регистру и должны совпадать на всех используемых устройствах.",
"Yes": "Да",
"You must keep at least one version.": "Вы должны хранить как минимум одну версию.",
"full documentation": "полная документация",
"items": "элементы"
}

View File

@@ -8,7 +8,7 @@
"Allow Anonymous Usage Reporting?": "允许匿名使用报告?",
"Anonymous Usage Reporting": "匿名使用报告",
"Any devices configured on an introducer device will be added to this device as well.": "在介绍人设备上被添加的其它设备,也将会被添加到本机。",
"Automatic upgrades": "Automatic upgrades",
"Automatic upgrades": "自动升级",
"Bugs": "Bug汇报",
"CPU Utilization": "CPU使用率",
"Close": "关闭",
@@ -121,7 +121,7 @@
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "历史版本保留的最长天数0为永久保存",
"The number of old versions to keep, per file.": "每个文件保留的版本数量上限。",
"The number of versions must be a number and cannot be blank.": "保留版本数量必须为数字,且不能为空。",
"The rescan interval must be a non-negative number of seconds.": "The rescan interval must be a non-negative number of seconds.",
"The rescan interval must be a non-negative number of seconds.": "扫描间隔单位为秒,且不能为负数。",
"The rescan interval must be at least 5 seconds.": "扫描间隔必须至少为5秒。",
"Unknown": "未知",
"Up to Date": "同步完成",

View File

@@ -1 +1 @@
var validLangs = ["be","bg","cs","de","en","fr","hu","it","lt","nb","nn","pl","pt-PT","sv","zh-CN","zh-TW"]
var validLangs = ["be","bg","cs","de","en","fr","hu","it","lt","nb","nl","nn","pl","pt-PT","ru","sv","zh-CN","zh-TW"]

View File

File diff suppressed because one or more lines are too long

View File

@@ -58,6 +58,9 @@ type FolderConfiguration struct {
IgnorePerms bool `xml:"ignorePerms,attr"`
Versioning VersioningConfiguration `xml:"versioning"`
LenientMtimes bool `xml:"lenientMtimes"`
Copiers int `xml:"copiers" default:"1"` // This defines how many files are handled concurrently.
Pullers int `xml:"pullers" default:"16"` // Defines how many blocks are fetched at the same time, possibly between separate copier routines.
Finishers int `xml:"finishers" default:"1"` // Most of the time, should be equal to the number of copiers. These are CPU bound due to hashing.
Invalid string `xml:"-"` // Set at runtime when there is an error, not saved
@@ -332,10 +335,20 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
sort.Sort(DeviceConfigurationList(cfg.Devices))
// Ensure that any loose devices are not present in the wrong places
// Ensure that there are no duplicate devices
// Ensure that puller settings are sane
for i := range cfg.Folders {
cfg.Folders[i].Devices = ensureDevicePresent(cfg.Folders[i].Devices, myID)
cfg.Folders[i].Devices = ensureExistingDevices(cfg.Folders[i].Devices, existingDevices)
cfg.Folders[i].Devices = ensureNoDuplicates(cfg.Folders[i].Devices)
if cfg.Folders[i].Copiers == 0 {
cfg.Folders[i].Copiers = 1
}
if cfg.Folders[i].Pullers == 0 {
cfg.Folders[i].Pullers = 16
}
if cfg.Folders[i].Finishers == 0 {
cfg.Folders[i].Finishers = 1
}
sort.Sort(FolderDeviceConfigurationList(cfg.Folders[i].Devices))
}

View File

@@ -85,6 +85,9 @@ func TestDeviceConfig(t *testing.T) {
Devices: []FolderDeviceConfiguration{{DeviceID: device1}, {DeviceID: device4}},
ReadOnly: true,
RescanIntervalS: 600,
Copiers: 1,
Pullers: 16,
Finishers: 1,
},
}
expectedDevices := []DeviceConfiguration{

View File

@@ -258,64 +258,47 @@ func (d *Discoverer) sendExternalAnnouncements() {
buf = d.announcementPkt()
}
var bcastTick = time.Tick(d.globalBcastIntv)
var errTick <-chan time.Time
sendOneAnnouncement := func() {
var ok bool
if debug {
l.Debugf("discover: send announcement -> %v\n%s", remote, hex.Dump(buf))
}
_, err := conn.WriteTo(buf, remote)
if err != nil {
if debug {
l.Debugln("discover: warning:", err)
}
ok = false
} else {
// Verify that the announce server responds positively for our device ID
time.Sleep(1 * time.Second)
res := d.externalLookup(d.myID)
if debug {
l.Debugln("discover: external lookup check:", res)
}
ok = len(res) > 0
}
d.extAnnounceOKmut.Lock()
d.extAnnounceOK = ok
d.extAnnounceOKmut.Unlock()
if ok {
errTick = nil
} else if errTick != nil {
errTick = time.Tick(d.errorRetryIntv)
}
}
// Announce once, immediately
sendOneAnnouncement()
loop:
nextAnnouncement := time.NewTimer(0)
for {
select {
case <-d.stopGlobal:
break loop
return
case <-errTick:
sendOneAnnouncement()
case <-nextAnnouncement.C:
var ok bool
case <-bcastTick:
sendOneAnnouncement()
if debug {
l.Debugf("discover: send announcement -> %v\n%s", remote, hex.Dump(buf))
}
_, err := conn.WriteTo(buf, remote)
if err != nil {
if debug {
l.Debugln("discover: warning:", err)
}
ok = false
} else {
// Verify that the announce server responds positively for our device ID
time.Sleep(1 * time.Second)
res := d.externalLookup(d.myID)
if debug {
l.Debugln("discover: external lookup check:", res)
}
ok = len(res) > 0
}
d.extAnnounceOKmut.Lock()
d.extAnnounceOK = ok
d.extAnnounceOKmut.Unlock()
if ok {
nextAnnouncement.Reset(d.globalBcastIntv)
} else {
nextAnnouncement.Reset(d.errorRetryIntv)
}
}
}
if debug {
l.Debugln("discover: stopping global")
}
}
func (d *Discoverer) recvAnnouncements(b beacon.Interface) {

View File

@@ -29,6 +29,7 @@ import (
"sync"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syndtr/goleveldb/leveldb"
@@ -171,7 +172,7 @@ func (f *BlockFinder) Iterate(hash []byte, iterFn func(string, string, uint32) b
for iter.Next() && iter.Error() == nil {
folder, file := fromBlockKey(iter.Key())
index := binary.BigEndian.Uint32(iter.Value())
if iterFn(folder, nativeFilename(file), index) {
if iterFn(folder, osutil.NativeFilename(file), index) {
return true
}
}

View File

@@ -25,6 +25,7 @@ import (
"sync"
"github.com/syncthing/syncthing/internal/lamport"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syndtr/goleveldb/leveldb"
)
@@ -174,19 +175,19 @@ func (s *Set) WithGlobalTruncated(fn fileIterator) {
}
func (s *Set) Get(device protocol.DeviceID, file string) protocol.FileInfo {
f := ldbGet(s.db, []byte(s.folder), device[:], []byte(normalizedFilename(file)))
f.Name = nativeFilename(f.Name)
f := ldbGet(s.db, []byte(s.folder), device[:], []byte(osutil.NormalizedFilename(file)))
f.Name = osutil.NativeFilename(f.Name)
return f
}
func (s *Set) GetGlobal(file string) protocol.FileInfo {
f := ldbGetGlobal(s.db, []byte(s.folder), []byte(normalizedFilename(file)))
f.Name = nativeFilename(f.Name)
f := ldbGetGlobal(s.db, []byte(s.folder), []byte(osutil.NormalizedFilename(file)))
f.Name = osutil.NativeFilename(f.Name)
return f
}
func (s *Set) Availability(file string) []protocol.DeviceID {
return ldbAvailability(s.db, []byte(s.folder), []byte(normalizedFilename(file)))
return ldbAvailability(s.db, []byte(s.folder), []byte(osutil.NormalizedFilename(file)))
}
func (s *Set) LocalVersion(device protocol.DeviceID) uint64 {
@@ -213,7 +214,7 @@ func DropFolder(db *leveldb.DB, folder string) {
func normalizeFilenames(fs []protocol.FileInfo) {
for i := range fs {
fs[i].Name = normalizedFilename(fs[i].Name)
fs[i].Name = osutil.NormalizedFilename(fs[i].Name)
}
}
@@ -221,10 +222,10 @@ func nativeFileIterator(fn fileIterator) fileIterator {
return func(fi protocol.FileIntf) bool {
switch f := fi.(type) {
case protocol.FileInfo:
f.Name = nativeFilename(f.Name)
f.Name = osutil.NativeFilename(f.Name)
return fn(f)
case protocol.FileInfoTruncated:
f.Name = nativeFilename(f.Name)
f.Name = osutil.NativeFilename(f.Name)
return fn(f)
default:
panic("unknown interface type")

View File

@@ -39,6 +39,7 @@ import (
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/scanner"
"github.com/syncthing/syncthing/internal/stats"
"github.com/syncthing/syncthing/internal/symlinks"
"github.com/syncthing/syncthing/internal/versioner"
"github.com/syndtr/goleveldb/leveldb"
)
@@ -114,6 +115,8 @@ type Model struct {
var (
ErrNoSuchFile = errors.New("no such file")
ErrInvalid = errors.New("file is invalid")
SymlinkWarning = sync.Once{}
)
// NewModel creates and starts a new model. The model starts in read-only mode,
@@ -175,6 +178,9 @@ func (m *Model) StartFolderRW(folder string) {
model: m,
ignorePerms: cfg.IgnorePerms,
lenientMtimes: cfg.LenientMtimes,
copiers: cfg.Copiers,
pullers: cfg.Pullers,
finishers: cfg.Finishers,
}
m.folderRunners[folder] = p
m.fmut.Unlock()
@@ -393,20 +399,17 @@ func (m *Model) NeedSize(folder string) (files int, bytes int64) {
}
// NeedFiles returns the list of currently needed files, stopping at maxFiles
// files or maxBlocks blocks. Limits <= 0 are ignored.
func (m *Model) NeedFolderFilesLimited(folder string, maxFiles, maxBlocks int) []protocol.FileInfo {
// files. Limit <= 0 is ignored.
func (m *Model) NeedFolderFilesLimited(folder string, maxFiles int) []protocol.FileInfoTruncated {
defer m.leveldbPanicWorkaround()
m.fmut.RLock()
defer m.fmut.RUnlock()
nblocks := 0
if rf, ok := m.folderFiles[folder]; ok {
fs := make([]protocol.FileInfo, 0, maxFiles)
rf.WithNeed(protocol.LocalDeviceID, func(f protocol.FileIntf) bool {
fi := f.(protocol.FileInfo)
fs = append(fs, fi)
nblocks += len(fi.Blocks)
return (maxFiles <= 0 || len(fs) < maxFiles) && (maxBlocks <= 0 || nblocks < maxBlocks)
fs := make([]protocol.FileInfoTruncated, 0, maxFiles)
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f protocol.FileIntf) bool {
fs = append(fs, f.(protocol.FileInfoTruncated))
return maxFiles <= 0 || len(fs) < maxFiles
})
return fs
}
@@ -440,9 +443,9 @@ func (m *Model) Index(deviceID protocol.DeviceID, folder string, fs []protocol.F
for i := 0; i < len(fs); {
lamport.Default.Tick(fs[i].Version)
if ignores != nil && ignores.Match(fs[i].Name) {
if (ignores != nil && ignores.Match(fs[i].Name)) || symlinkInvalid(fs[i].IsSymlink()) {
if debug {
l.Debugln("dropping update for ignored", fs[i])
l.Debugln("dropping update for ignored/unsupported symlink", fs[i])
}
fs[i] = fs[len(fs)-1]
fs = fs[:len(fs)-1]
@@ -484,9 +487,9 @@ func (m *Model) IndexUpdate(deviceID protocol.DeviceID, folder string, fs []prot
for i := 0; i < len(fs); {
lamport.Default.Tick(fs[i].Version)
if ignores != nil && ignores.Match(fs[i].Name) {
if (ignores != nil && ignores.Match(fs[i].Name)) || symlinkInvalid(fs[i].IsSymlink()) {
if debug {
l.Debugln("dropping update for ignored", fs[i])
l.Debugln("dropping update for ignored/unsupported symlink", fs[i])
}
fs[i] = fs[len(fs)-1]
fs = fs[:len(fs)-1]
@@ -655,7 +658,7 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
}
lf := r.Get(protocol.LocalDeviceID, name)
if protocol.IsInvalid(lf.Flags) || protocol.IsDeleted(lf.Flags) {
if lf.IsInvalid() || lf.IsDeleted() {
if debug {
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d; invalid: %v", m, deviceID, folder, name, offset, size, lf)
}
@@ -675,14 +678,26 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
m.fmut.RLock()
fn := filepath.Join(m.folderCfgs[folder].Path, name)
m.fmut.RUnlock()
fd, err := os.Open(fn) // XXX: Inefficient, should cache fd?
if err != nil {
return nil, err
var reader io.ReaderAt
var err error
if lf.IsSymlink() {
target, _, err := symlinks.Read(fn)
if err != nil {
return nil, err
}
reader = strings.NewReader(target)
} else {
reader, err = os.Open(fn) // XXX: Inefficient, should cache fd?
if err != nil {
return nil, err
}
defer reader.(*os.File).Close()
}
defer fd.Close()
buf := make([]byte, size)
_, err = fd.ReadAt(buf, offset)
_, err = reader.ReadAt(buf, offset)
if err != nil {
return nil, err
}
@@ -892,9 +907,9 @@ func sendIndexTo(initial bool, minLocalVer uint64, conn protocol.Connection, fol
maxLocalVer = f.LocalVersion
}
if ignores != nil && ignores.Match(f.Name) {
if (ignores != nil && ignores.Match(f.Name)) || symlinkInvalid(f.IsSymlink()) {
if debug {
l.Debugln("not sending update for ignored", f)
l.Debugln("not sending update for ignored/unsupported symlink", f)
}
return true
}
@@ -989,6 +1004,9 @@ func (m *Model) AddFolder(cfg config.FolderConfiguration) {
m.deviceFolders[device.DeviceID] = append(m.deviceFolders[device.DeviceID], cfg.ID)
}
ignores, _ := ignore.Load(filepath.Join(cfg.Path, ".stignore"), m.cfg.Options().CacheIgnoredFiles)
m.folderIgnores[cfg.ID] = ignores
m.addedFolder = true
m.fmut.Unlock()
}
@@ -1085,7 +1103,7 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
}
seenPrefix = true
if !protocol.IsDeleted(f.Flags) {
if !f.IsDeleted() {
if f.IsInvalid() {
return true
}
@@ -1095,8 +1113,8 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
batch = batch[:0]
}
if ignores != nil && ignores.Match(f.Name) {
// File has been ignored. Set invalid bit.
if (ignores != nil && ignores.Match(f.Name)) || symlinkInvalid(f.IsSymlink()) {
// File has been ignored or an unsupported symlink. Set invalid bit.
l.Debugln("setting invalid bit on ignored", f)
nf := protocol.FileInfo{
Name: f.Name,
@@ -1112,7 +1130,7 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
"size": f.Size(),
})
batch = append(batch, nf)
} else if _, err := os.Stat(filepath.Join(dir, f.Name)); err != nil && os.IsNotExist(err) {
} else if _, err := os.Lstat(filepath.Join(dir, f.Name)); err != nil && os.IsNotExist(err) {
// File has been deleted
nf := protocol.FileInfo{
Name: f.Name,
@@ -1326,3 +1344,13 @@ func (m *Model) leveldbPanicWorkaround() {
}
}
}
func symlinkInvalid(isLink bool) bool {
if !symlinks.Supported && isLink {
SymlinkWarning.Do(func() {
l.Warnln("Symlinks are unsupported as they require Administrator priviledges. This might cause your folder to appear out of sync.")
})
return true
}
return false
}

View File

@@ -20,6 +20,7 @@ import (
"crypto/sha256"
"errors"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"sync"
@@ -32,18 +33,16 @@ import (
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/scanner"
"github.com/syncthing/syncthing/internal/symlinks"
"github.com/syncthing/syncthing/internal/versioner"
)
// TODO: Stop on errors
const (
copiersPerFolder = 1
pullersPerFolder = 16
finishersPerFolder = 2
pauseIntv = 60 * time.Second
nextPullIntv = 10 * time.Second
checkPullIntv = 1 * time.Second
pauseIntv = 60 * time.Second
nextPullIntv = 10 * time.Second
checkPullIntv = 1 * time.Second
)
// A pullBlockState is passed to the puller routine for each block that needs
@@ -74,6 +73,9 @@ type Puller struct {
versioner versioner.Versioner
ignorePerms bool
lenientMtimes bool
copiers int
pullers int
finishers int
}
// Serve will run scans and pulls. It will return when Stop()ed or on a
@@ -151,7 +153,7 @@ loop:
checksum = true
}
changed := p.pullerIteration(copiersPerFolder, pullersPerFolder, finishersPerFolder, checksum)
changed := p.pullerIteration(checksum)
if debug {
l.Debugln(p, "changed", changed)
}
@@ -238,11 +240,8 @@ func (p *Puller) String() string {
// pullerIteration runs a single puller iteration for the given folder and
// returns the number items that should have been synced (even those that
// might have failed). One puller iteration handles all files currently
// flagged as needed in the folder. The specified number of copier, puller and
// finisher routines are used. It's seldom efficient to use more than one
// copier routine, while multiple pullers are essential and multiple finishers
// may be useful (they are primarily CPU bound due to hashing).
func (p *Puller) pullerIteration(ncopiers, npullers, nfinishers int, checksum bool) int {
// flagged as needed in the folder.
func (p *Puller) pullerIteration(checksum bool) int {
pullChan := make(chan pullBlockState)
copyChan := make(chan copyBlocksState)
finisherChan := make(chan *sharedPullerState)
@@ -251,7 +250,11 @@ func (p *Puller) pullerIteration(ncopiers, npullers, nfinishers int, checksum bo
var pullWg sync.WaitGroup
var doneWg sync.WaitGroup
for i := 0; i < ncopiers; i++ {
if debug {
l.Debugln(p, "c", p.copiers, "p", p.pullers, "f", p.finishers)
}
for i := 0; i < p.copiers; i++ {
copyWg.Add(1)
go func() {
// copierRoutine finishes when copyChan is closed
@@ -260,7 +263,7 @@ func (p *Puller) pullerIteration(ncopiers, npullers, nfinishers int, checksum bo
}()
}
for i := 0; i < npullers; i++ {
for i := 0; i < p.pullers; i++ {
pullWg.Add(1)
go func() {
// pullerRoutine finishes when pullChan is closed
@@ -269,7 +272,7 @@ func (p *Puller) pullerIteration(ncopiers, npullers, nfinishers int, checksum bo
}()
}
for i := 0; i < nfinishers; i++ {
for i := 0; i < p.finishers; i++ {
doneWg.Add(1)
// finisherRoutine finishes when finisherChan is closed
go func() {
@@ -313,15 +316,16 @@ func (p *Puller) pullerIteration(ncopiers, npullers, nfinishers int, checksum bo
}
switch {
case protocol.IsDeleted(file.Flags):
// A deleted file or directory
case file.IsDeleted():
// A deleted file, directory or symlink
deletions = append(deletions, file)
case protocol.IsDirectory(file.Flags):
case file.IsDirectory() && !file.IsSymlink():
// A new or changed directory
p.handleDir(file)
default:
// A new or changed file. This is the only case where we do stuff
// in the background; the other three are done synchronously.
// A new or changed file or symlink. This is the only case where we
// do stuff in the background; the other three are done
// synchronously.
p.handleFile(file, copyChan, finisherChan)
}
@@ -367,12 +371,13 @@ func (p *Puller) handleDir(file protocol.FileInfo) {
l.Debugf("need dir\n\t%v\n\t%v", file, curFile)
}
info, err := os.Stat(realName)
info, err := os.Lstat(realName)
isLink, _ := symlinks.IsSymlink(realName)
switch {
// There is already something under that name, but it's a file.
// Most likely a file is getting replaced with a directory.
// Remove the file and fall through to directory creation.
case err == nil && !info.IsDir():
// There is already something under that name, but it's a file/link.
// Most likely a file/link is getting replaced with a directory.
// Remove the file/link and fall through to directory creation.
case isLink || (err == nil && !info.IsDir()):
err = osutil.InWritableDir(os.Remove, realName)
if err != nil {
l.Infof("Puller (folder %q, dir %q): %v", p.folder, file.Name, err)
@@ -459,24 +464,21 @@ func (p *Puller) deleteFile(file protocol.FileInfo) {
func (p *Puller) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksState, finisherChan chan<- *sharedPullerState) {
curFile := p.model.CurrentFolderFile(p.folder, file.Name)
if len(curFile.Blocks) == len(file.Blocks) {
for i := range file.Blocks {
if !bytes.Equal(curFile.Blocks[i].Hash, file.Blocks[i].Hash) {
goto FilesAreDifferent
}
}
if len(curFile.Blocks) == len(file.Blocks) && scanner.BlocksEqual(curFile.Blocks, file.Blocks) {
// We are supposed to copy the entire file, and then fetch nothing. We
// are only updating metadata, so we don't actually *need* to make the
// copy.
if debug {
l.Debugln(p, "taking shortcut on", file.Name)
}
p.shortcutFile(file)
if file.IsSymlink() {
p.shortcutSymlink(curFile, file)
} else {
p.shortcutFile(file)
}
return
}
FilesAreDifferent:
scanner.PopulateOffsets(file.Blocks)
// Figure out the absolute filenames we need once and for all
@@ -571,6 +573,17 @@ func (p *Puller) shortcutFile(file protocol.FileInfo) {
p.model.updateLocal(p.folder, file)
}
// shortcutSymlink changes the symlinks type if necessery.
func (p *Puller) shortcutSymlink(curFile, file protocol.FileInfo) {
err := symlinks.ChangeType(filepath.Join(p.dir, file.Name), file.Flags)
if err != nil {
l.Infof("Puller (folder %q, file %q): symlink shortcut: %v", p.folder, file.Name, err)
return
}
p.model.updateLocal(p.folder, file)
}
// copierRoutine reads copierStates until the in channel closes and performs
// the relevant copies when possible, or passes it to the puller routine.
func (p *Puller) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pullBlockState, out chan<- *sharedPullerState, checksum bool) {
@@ -784,13 +797,39 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
}
}
// Replace the original file with the new one
// If the target path is a symlink or a directory, we cannot copy
// over it, hence remove it before proceeding.
stat, err := os.Lstat(state.realName)
isLink, _ := symlinks.IsSymlink(state.realName)
if isLink || (err == nil && stat.IsDir()) {
osutil.InWritableDir(os.Remove, state.realName)
}
// Replace the original content with the new one
err = osutil.Rename(state.tempName, state.realName)
if err != nil {
l.Warnln("puller: final:", err)
continue
}
// If it's a symlink, the target of the symlink is inside the file.
if state.file.IsSymlink() {
content, err := ioutil.ReadFile(state.realName)
if err != nil {
l.Warnln("puller: final: reading symlink:", err)
continue
}
// Remove the file, and replace it with a symlink.
err = osutil.InWritableDir(func(path string) error {
os.Remove(path)
return symlinks.Create(path, string(content), state.file.Flags)
}, state.realName)
if err != nil {
l.Warnln("puller: final: creating symlink:", err)
continue
}
}
// Record the updated file in the index
p.model.updateLocal(p.folder, state.file)
}

View File

@@ -17,7 +17,6 @@ package model
import (
"os"
"path/filepath"
"testing"
)
@@ -68,13 +67,17 @@ func TestSourceFileBad(t *testing.T) {
// Test creating temporary file inside read-only directory
func TestReadOnlyDir(t *testing.T) {
// Create a read only directory, clean it up afterwards.
os.Mkdir("testdata/read_only_dir", 0555)
defer func() {
os.Chmod("testdata/read_only_dir", 0755)
os.RemoveAll("testdata/read_only_dir")
}()
s := sharedPullerState{
tempName: "testdata/read_only_dir/.temp_name",
}
// Ensure dir is read-only (git doesn't store full permissions)
os.Chmod(filepath.Dir(s.tempName), 0555)
fd, err := s.tempFile()
if err != nil {
t.Fatal(err)

View File

View File

@@ -13,14 +13,14 @@
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package files
package osutil
import "code.google.com/p/go.text/unicode/norm"
func normalizedFilename(s string) string {
func NormalizedFilename(s string) string {
return norm.NFC.String(s)
}
func nativeFilename(s string) string {
func NativeFilename(s string) string {
return norm.NFD.String(s)
}

View File

@@ -15,14 +15,14 @@
// +build !windows,!darwin
package files
package osutil
import "code.google.com/p/go.text/unicode/norm"
func normalizedFilename(s string) string {
func NormalizedFilename(s string) string {
return norm.NFC.String(s)
}
func nativeFilename(s string) string {
func NativeFilename(s string) string {
return s
}

View File

@@ -13,7 +13,7 @@
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package files
package osutil
import (
"path/filepath"
@@ -21,10 +21,10 @@ import (
"code.google.com/p/go.text/unicode/norm"
)
func normalizedFilename(s string) string {
func NormalizedFilename(s string) string {
return norm.NFC.String(filepath.ToSlash(s))
}
func nativeFilename(s string) string {
func NativeFilename(s string) string {
return filepath.FromSlash(s)
}

View File

@@ -37,7 +37,7 @@ func (f FileInfo) String() string {
}
func (f FileInfo) Size() (bytes int64) {
if IsDeleted(f.Flags) || IsDirectory(f.Flags) {
if f.IsDeleted() || f.IsDirectory() {
return 128
}
for _, b := range f.Blocks {
@@ -47,15 +47,23 @@ func (f FileInfo) Size() (bytes int64) {
}
func (f FileInfo) IsDeleted() bool {
return IsDeleted(f.Flags)
return f.Flags&FlagDeleted != 0
}
func (f FileInfo) IsInvalid() bool {
return IsInvalid(f.Flags)
return f.Flags&FlagInvalid != 0
}
func (f FileInfo) IsDirectory() bool {
return IsDirectory(f.Flags)
return f.Flags&FlagDirectory != 0
}
func (f FileInfo) IsSymlink() bool {
return f.Flags&FlagSymlink != 0
}
func (f FileInfo) HasPermissionBits() bool {
return f.Flags&FlagNoPermBits == 0
}
// Used for unmarshalling a FileInfo structure but skipping the actual block list
@@ -73,30 +81,48 @@ func (f FileInfoTruncated) String() string {
f.Name, f.Flags, f.Modified, f.Version, f.Size(), f.NumBlocks)
}
func BlocksToSize(num uint32) int64 {
if num < 2 {
return BlockSize / 2
}
return int64(num-1)*BlockSize + BlockSize/2
}
// Returns a statistical guess on the size, not the exact figure
func (f FileInfoTruncated) Size() int64 {
if IsDeleted(f.Flags) || IsDirectory(f.Flags) {
if f.IsDeleted() || f.IsDirectory() {
return 128
}
if f.NumBlocks < 2 {
return BlockSize / 2
} else {
return int64(f.NumBlocks-1)*BlockSize + BlockSize/2
}
return BlocksToSize(f.NumBlocks)
}
func (f FileInfoTruncated) IsDeleted() bool {
return IsDeleted(f.Flags)
return f.Flags&FlagDeleted != 0
}
func (f FileInfoTruncated) IsInvalid() bool {
return IsInvalid(f.Flags)
return f.Flags&FlagInvalid != 0
}
func (f FileInfoTruncated) IsDirectory() bool {
return f.Flags&FlagDirectory != 0
}
func (f FileInfoTruncated) IsSymlink() bool {
return f.Flags&FlagSymlink != 0
}
func (f FileInfoTruncated) HasPermissionBits() bool {
return f.Flags&FlagNoPermBits == 0
}
type FileIntf interface {
Size() int64
IsDeleted() bool
IsInvalid() bool
IsDirectory() bool
IsSymlink() bool
HasPermissionBits() bool
}
type BlockInfo struct {

View File

@@ -49,10 +49,14 @@ const (
)
const (
FlagDeleted uint32 = 1 << 12
FlagInvalid = 1 << 13
FlagDirectory = 1 << 14
FlagNoPermBits = 1 << 15
FlagDeleted uint32 = 1 << 12
FlagInvalid = 1 << 13
FlagDirectory = 1 << 14
FlagNoPermBits = 1 << 15
FlagSymlink = 1 << 16
FlagSymlinkMissingTarget = 1 << 17
SymlinkTypeMask = FlagDirectory | FlagSymlinkMissingTarget
)
const (
@@ -637,19 +641,3 @@ func (c *rawConnection) Statistics() Statistics {
OutBytesTotal: c.cw.Tot(),
}
}
func IsDeleted(bits uint32) bool {
return bits&FlagDeleted != 0
}
func IsInvalid(bits uint32) bool {
return bits&FlagInvalid != 0
}
func IsDirectory(bits uint32) bool {
return bits&FlagDirectory != 0
}
func HasPermissionBits(bits uint32) bool {
return bits&FlagNoPermBits == 0
}

View File

@@ -68,7 +68,7 @@ func HashFile(path string, blockSize int) ([]protocol.BlockInfo, error) {
func hashFiles(dir string, blockSize int, outbox, inbox chan protocol.FileInfo) {
for f := range inbox {
if protocol.IsDirectory(f.Flags) || protocol.IsDeleted(f.Flags) {
if f.IsDirectory() || f.IsDeleted() || f.IsSymlink() {
outbox <- f
continue
}

View File

@@ -129,3 +129,18 @@ func Verify(r io.Reader, blocksize int, blocks []protocol.BlockInfo) error {
return nil
}
// BlockEqual 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
}

View File

@@ -27,6 +27,7 @@ import (
"github.com/syncthing/syncthing/internal/ignore"
"github.com/syncthing/syncthing/internal/lamport"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/symlinks"
)
type Walker struct {
@@ -131,11 +132,75 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
return nil
}
// We must perform this check, as symlinks on Windows are always
// .IsRegular or .IsDir unlike on Unix.
// Index wise symlinks are always files, regardless of what the target
// is, because symlinks carry their target path as their content.
isSymlink, _ := symlinks.IsSymlink(p)
if isSymlink {
var rval error
// If the target is a directory, do NOT descend down there.
// This will cause files to get tracked, and removing the symlink
// will as a result remove files in their real location.
// But do not SkipDir if the target is not a directory, as it will
// stop scanning the current directory.
if info.IsDir() {
rval = filepath.SkipDir
}
// We always rehash symlinks as they have no modtime or
// permissions.
// We check if they point to the old target by checking that
// their existing blocks match with the blocks in the index.
// If we don't have a filer or don't support symlinks, skip.
if w.CurrentFiler == nil || !symlinks.Supported {
return rval
}
target, flags, err := symlinks.Read(p)
flags = flags & protocol.SymlinkTypeMask
if err != nil {
if debug {
l.Debugln("readlink error:", p, err)
}
return rval
}
blocks, err := Blocks(strings.NewReader(target), w.BlockSize, 0)
if err != nil {
if debug {
l.Debugln("hash link error:", p, err)
}
return rval
}
cf := w.CurrentFiler.CurrentFile(rn)
if !cf.IsDeleted() && cf.IsSymlink() && SymlinkTypeEqual(flags, cf.Flags) && BlocksEqual(cf.Blocks, blocks) {
return rval
}
f := protocol.FileInfo{
Name: rn,
Version: lamport.Default.Tick(0),
Flags: protocol.FlagSymlink | flags | protocol.FlagNoPermBits | 0666,
Modified: 0,
Blocks: blocks,
}
if debug {
l.Debugln("symlink to hash:", p, f)
}
fchan <- f
return rval
}
if info.Mode().IsDir() {
if w.CurrentFiler != nil {
cf := w.CurrentFiler.CurrentFile(rn)
permUnchanged := w.IgnorePerms || !protocol.HasPermissionBits(cf.Flags) || PermsEqual(cf.Flags, uint32(info.Mode()))
if !protocol.IsDeleted(cf.Flags) && protocol.IsDirectory(cf.Flags) && permUnchanged {
permUnchanged := w.IgnorePerms || !cf.HasPermissionBits() || PermsEqual(cf.Flags, uint32(info.Mode()))
if !cf.IsDeleted() && cf.IsDirectory() && permUnchanged {
return nil
}
}
@@ -162,8 +227,8 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
if info.Mode().IsRegular() {
if w.CurrentFiler != nil {
cf := w.CurrentFiler.CurrentFile(rn)
permUnchanged := w.IgnorePerms || !protocol.HasPermissionBits(cf.Flags) || PermsEqual(cf.Flags, uint32(info.Mode()))
if !protocol.IsDeleted(cf.Flags) && cf.Modified == info.ModTime().Unix() && permUnchanged {
permUnchanged := w.IgnorePerms || !cf.HasPermissionBits() || PermsEqual(cf.Flags, uint32(info.Mode()))
if !cf.IsDeleted() && cf.Modified == info.ModTime().Unix() && permUnchanged {
return nil
}
@@ -215,3 +280,19 @@ func PermsEqual(a, b uint32) bool {
return a&0777 == b&0777
}
}
// If the target is missing, Unix never knows what type of symlink it is
// and Windows always knows even if there is no target.
// Which means that without this special check a Unix node would be fighting
// with a Windows node about whether or not the target is known.
// Basically, if you don't know and someone else knows, just accept it.
// The fact that you don't know means you are on Unix, and on Unix you don't
// really care what the target type is. The moment you do know, and if something
// doesn't match, that will propogate throught the cluster.
func SymlinkTypeEqual(disk, index uint32) bool {
if disk&protocol.FlagSymlinkMissingTarget != 0 && index&protocol.FlagSymlinkMissingTarget == 0 {
return true
}
return disk&protocol.SymlinkTypeMask == index&protocol.SymlinkTypeMask
}

0
internal/stats/debug.go Executable file → Normal file
View File

0
internal/stats/device.go Executable file → Normal file
View File

0
internal/stats/leveldb.go Executable file → Normal file
View File

View File

@@ -0,0 +1,58 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
// +build !windows
package symlinks
import (
"os"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol"
)
var (
Supported = true
)
func Read(path string) (string, uint32, error) {
var mode uint32
stat, err := os.Stat(path)
if err != nil {
mode = protocol.FlagSymlinkMissingTarget
} else if stat.IsDir() {
mode = protocol.FlagDirectory
}
path, err = os.Readlink(path)
return osutil.NormalizedFilename(path), mode, err
}
func IsSymlink(path string) (bool, error) {
lstat, err := os.Lstat(path)
if err != nil {
return false, err
}
return lstat.Mode()&os.ModeSymlink != 0, nil
}
func Create(source, target string, flags uint32) error {
return os.Symlink(osutil.NativeFilename(target), source)
}
func ChangeType(path string, flags uint32) error {
return nil
}

View File

@@ -0,0 +1,203 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
// +build windows
package symlinks
import (
"os"
"path/filepath"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol"
"syscall"
"unicode/utf16"
"unsafe"
)
const (
FSCTL_GET_REPARSE_POINT = 0x900a8
FILE_FLAG_OPEN_REPARSE_POINT = 0x00200000
FILE_ATTRIBUTE_REPARSE_POINT = 0x400
IO_REPARSE_TAG_SYMLINK = 0xA000000C
SYMBOLIC_LINK_FLAG_DIRECTORY = 0x1
)
var (
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
procDeviceIoControl = modkernel32.NewProc("DeviceIoControl")
procCreateSymbolicLink = modkernel32.NewProc("CreateSymbolicLinkW")
Supported = false
)
func init() {
// Needs administrator priviledges.
// Let's check that everything works.
// This could be done more officially:
// http://stackoverflow.com/questions/2094663/determine-if-windows-process-has-privilege-to-create-symbolic-link
// But I don't want to define 10 more structs just to look this up.
base := os.TempDir()
path := filepath.Join(base, "symlinktest")
defer os.Remove(path)
err := Create(path, base, protocol.FlagDirectory)
if err != nil {
return
}
isLink, err := IsSymlink(path)
if err != nil || !isLink {
return
}
target, flags, err := Read(path)
if err != nil || osutil.NativeFilename(target) != base || flags&protocol.FlagDirectory == 0 {
return
}
Supported = true
}
type reparseData struct {
reparseTag uint32
reparseDataLength uint16
reserved uint16
substitueNameOffset uint16
substitueNameLength uint16
printNameOffset uint16
printNameLength uint16
flags uint32
// substituteName - 264 widechars max = 528 bytes
// printName - 260 widechars max = 520 bytes
// = 1048 bytes total
buffer [1048]uint16
}
func (r *reparseData) PrintName() string {
// No clue why the offset and length is doubled...
offset := r.printNameOffset / 2
length := r.printNameLength / 2
return string(utf16.Decode(r.buffer[offset : offset+length]))
}
func (r *reparseData) SubstituteName() string {
// No clue why the offset and length is doubled...
offset := r.substitueNameOffset / 2
length := r.substitueNameLength / 2
return string(utf16.Decode(r.buffer[offset : offset+length]))
}
func Read(path string) (string, uint32, error) {
ptr, err := syscall.UTF16PtrFromString(path)
if err != nil {
return "", protocol.FlagSymlinkMissingTarget, err
}
handle, err := syscall.CreateFile(ptr, 0, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS|FILE_FLAG_OPEN_REPARSE_POINT, 0)
if err != nil || handle == syscall.InvalidHandle {
return "", protocol.FlagSymlinkMissingTarget, err
}
defer syscall.Close(handle)
var ret uint16
var data reparseData
r1, _, err := syscall.Syscall9(procDeviceIoControl.Addr(), 8, uintptr(handle), FSCTL_GET_REPARSE_POINT, 0, 0, uintptr(unsafe.Pointer(&data)), unsafe.Sizeof(data), uintptr(unsafe.Pointer(&ret)), 0, 0)
if r1 == 0 {
return "", protocol.FlagSymlinkMissingTarget, err
}
var flags uint32 = 0
attr, err := syscall.GetFileAttributes(ptr)
if err != nil {
flags = protocol.FlagSymlinkMissingTarget
} else if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
flags = protocol.FlagDirectory
}
return osutil.NormalizedFilename(data.PrintName()), flags, nil
}
func IsSymlink(path string) (bool, error) {
ptr, err := syscall.UTF16PtrFromString(path)
if err != nil {
return false, err
}
attr, err := syscall.GetFileAttributes(ptr)
if err != nil {
return false, err
}
return attr&FILE_ATTRIBUTE_REPARSE_POINT != 0, nil
}
func Create(source, target string, flags uint32) error {
srcp, err := syscall.UTF16PtrFromString(source)
if err != nil {
return err
}
trgp, err := syscall.UTF16PtrFromString(osutil.NativeFilename(target))
if err != nil {
return err
}
// Sadly for Windows we need to specify the type of the symlink,
// whether it's a directory symlink or a file symlink.
// If the flags doesn't reveal the target type, try to evaluate it
// ourselves, and worst case default to the symlink pointing to a file.
mode := 0
if flags&protocol.FlagSymlinkMissingTarget != 0 {
path := target
if !filepath.IsAbs(target) {
path = filepath.Join(filepath.Dir(source), target)
}
stat, err := os.Stat(path)
if err == nil && stat.IsDir() {
mode = SYMBOLIC_LINK_FLAG_DIRECTORY
}
} else if flags&protocol.FlagDirectory != 0 {
mode = SYMBOLIC_LINK_FLAG_DIRECTORY
}
r0, _, err := syscall.Syscall(procCreateSymbolicLink.Addr(), 3, uintptr(unsafe.Pointer(srcp)), uintptr(unsafe.Pointer(trgp)), uintptr(mode))
if r0 == 1 {
return nil
}
return err
}
func ChangeType(path string, flags uint32) error {
target, cflags, err := Read(path)
if err != nil {
return err
}
// If it's the same type, nothing to do.
if cflags&protocol.SymlinkTypeMask == flags&protocol.SymlinkTypeMask {
return nil
}
// If the actual type is unknown, but the new type is file, nothing to do
if cflags&protocol.FlagSymlinkMissingTarget != 0 && flags&protocol.FlagDirectory == 0 {
return nil
}
return osutil.InWritableDir(func(path string) error {
// It should be a symlink as well hence no need to change permissions on
// the file.
os.Remove(path)
return Create(path, target, flags)
}, path)
}

0
internal/upgrade/upgrade_windows.go Executable file → Normal file
View File

View File

@@ -98,7 +98,7 @@ func (v Simple) Archive(filePath string) error {
return err
}
ver := file + "~" + fileInfo.ModTime().Format("20060102-150405")
ver := taggedFilename(file, fileInfo.ModTime().Format(TimeFormat))
dst := filepath.Join(dir, ver)
if debug {
l.Debugln("moving to", dst)
@@ -108,12 +108,24 @@ func (v Simple) Archive(filePath string) error {
return err
}
versions, err := filepath.Glob(filepath.Join(dir, file+"~[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]"))
// Glob according to the new file~timestamp.ext pattern.
newVersions, err := filepath.Glob(filepath.Join(dir, taggedFilename(file, TimeGlob)))
if err != nil {
l.Warnln("globbing:", err)
return nil
}
// Also according to the old file.ext~timestamp pattern.
oldVersions, err := filepath.Glob(filepath.Join(dir, file+"~"+TimeGlob))
if err != nil {
l.Warnln("globbing:", err)
return nil
}
// Use all the found filenames. "~" sorts after "." so all old pattern
// files will be deleted before any new, which is as it should be.
versions := append(oldVersions, newVersions...)
if len(versions) > v.keep {
sort.Strings(versions)
for _, toRemove := range versions[:len(versions)-v.keep] {

View File

@@ -56,17 +56,6 @@ func isFile(path string) bool {
return fileInfo.Mode().IsRegular()
}
const TimeLayout = "20060102-150405"
func versionExt(path string) string {
pathSplit := strings.Split(path, "~")
if len(pathSplit) > 1 {
return pathSplit[len(pathSplit)-1]
} else {
return ""
}
}
// Rename versions with old version format
func (v Staggered) renameOld() {
err := filepath.Walk(v.versionsPath, func(path string, f os.FileInfo, err error) error {
@@ -79,7 +68,7 @@ func (v Staggered) renameOld() {
l.Infoln("Renaming file", path, "from old to new version format")
versiondate := time.Unix(versionUnix, 0)
name := path[:len(path)-len(filepath.Ext(path))]
err = osutil.Rename(path, name+"~"+versiondate.Format(TimeLayout))
err = osutil.Rename(path, taggedFilename(name, versiondate.Format(TimeFormat)))
if err != nil {
l.Infoln("Error renaming to new format", err)
}
@@ -187,7 +176,7 @@ func (v Staggered) clean() {
filesPerDir[dir]++
}
case mode.IsRegular():
extension := versionExt(path)
extension := filenameTag(path)
dir := filepath.Dir(path)
name := path[:len(path)-len(extension)-1]
@@ -240,7 +229,7 @@ func (v Staggered) expire(versions []string) {
firstFile := true
for _, file := range versions {
if isFile(file) {
versionTime, err := time.Parse(TimeLayout, versionExt(file))
versionTime, err := time.Parse(TimeFormat, filenameTag(file))
if err != nil {
l.Infof("Versioner: file name %q is invalid: %v", file, err)
continue
@@ -342,7 +331,7 @@ func (v Staggered) Archive(filePath string) error {
return err
}
ver := file + "~" + fileInfo.ModTime().Format(TimeLayout)
ver := taggedFilename(file, fileInfo.ModTime().Format(TimeFormat))
dst := filepath.Join(dir, ver)
if debug {
l.Debugln("moving to", dst)
@@ -352,12 +341,23 @@ func (v Staggered) Archive(filePath string) error {
return err
}
versions, err := filepath.Glob(filepath.Join(dir, file+"~[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]"))
// Glob according to the new file~timestamp.ext pattern.
newVersions, err := filepath.Glob(filepath.Join(dir, taggedFilename(file, TimeGlob)))
if err != nil {
l.Warnln("Versioner: error finding versions for", file, err)
l.Warnln("globbing:", err)
return nil
}
// Also according to the old file.ext~timestamp pattern.
oldVersions, err := filepath.Glob(filepath.Join(dir, file+"~"+TimeGlob))
if err != nil {
l.Warnln("globbing:", err)
return nil
}
// Use all the found filenames.
versions := append(oldVersions, newVersions...)
sort.Strings(versions)
v.expire(versions)

View File

@@ -0,0 +1,42 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package versioner
import (
"path/filepath"
"regexp"
)
// Inserts ~tag just before the extension of the filename.
func taggedFilename(name, tag string) string {
dir, file := filepath.Dir(name), filepath.Base(name)
ext := filepath.Ext(file)
withoutExt := file[:len(file)-len(ext)]
return filepath.Join(dir, withoutExt+"~"+tag+ext)
}
var tagExp = regexp.MustCompile(`~([^~.]+)(?:\.[^.]+)?$`)
// Returns the tag from a filename, whether at the end or middle.
func filenameTag(path string) string {
match := tagExp.FindStringSubmatch(path)
// match is []string{"whole match", "submatch"} when successfull
if len(match) != 2 {
return ""
}
return match[1]
}

View File

@@ -22,3 +22,8 @@ type Versioner interface {
}
var Factories = map[string]func(folderID string, folderDir string, params map[string]string) Versioner{}
const (
TimeFormat = "20060102-150405"
TimeGlob = "[0-9][0-9][0-9][0-9][0-9][0-9][0-9][0-9]-[0-9][0-9][0-9][0-9][0-9][0-9]" // glob pattern matching TimeFormat
)

View File

@@ -13,6 +13,34 @@
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package versioner_test
package versioner
// Empty test file to generate 0% coverage rather than no coverage
import "testing"
func TestTaggedFilename(t *testing.T) {
cases := [][3]string{
{"foo/bar.baz", "tag", "foo/bar~tag.baz"},
{"bar.baz", "tag", "bar~tag.baz"},
{"bar", "tag", "bar~tag"},
// Parsing test only
{"", "tag-only", "foo/bar.baz~tag-only"},
{"", "tag-only", "bar.baz~tag-only"},
}
for _, tc := range cases {
if tc[0] != "" {
// Test tagger
tf := taggedFilename(tc[0], tc[1])
if tf != tc[2] {
t.Errorf("%s != %s", tf, tc[2])
}
}
// Test parser
tag := filenameTag(tc[2])
if tag != tc[1] {
t.Errorf("%s != %s", tag, tc[1])
}
}
}

View File

@@ -439,7 +439,7 @@ The Flags field is made up of the following single bit flags:
0 1 2 3
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Reserved |P|I|D| Unix Perm. & Mode |
| Reserved |U|S|P|I|D| Unix Perm. & Mode |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
- The lower 12 bits hold the common Unix permission and mode bits. An
@@ -461,7 +461,16 @@ The Flags field is made up of the following single bit flags:
disregarded on files with this bit set. The permissions bits MUST be
set to the octal value 0666.
- Bit 0 through 16 are reserved for future use and SHALL be set to
- Bit 16 ("S") is set when the file is a symbolic link. The block list
SHALL be of one or more blocks since the target of the symlink is
stored within the blocks of the file.
- Bit 15 ("U") is set when the symbolic links target does not exist.
On systems where symbolic links have types, this bit being means
that the default file symlink SHALL be used. If this bit is unset
bit 19 will decide the type of symlink to be created.
- Bit 0 through 14 are reserved for future use and SHALL be set to
zero.
The hash algorithm is implied by the Hash length. Currently, the hash

View File

@@ -30,6 +30,8 @@ import (
"os/exec"
"path/filepath"
"time"
"github.com/syncthing/syncthing/internal/symlinks"
)
func init() {
@@ -355,7 +357,22 @@ func startWalker(dir string, res chan<- fileInfo, abort <-chan struct{}) {
}
var f fileInfo
if info.IsDir() {
if ok, err := symlinks.IsSymlink(path); err == nil && ok {
f = fileInfo{
name: rn,
mode: os.ModeSymlink,
}
tgt, _, err := symlinks.Read(path)
if err != nil {
return err
}
h := md5.New()
h.Write([]byte(tgt))
hash := h.Sum(nil)
copy(f.hash[:], hash)
} else if info.IsDir() {
f = fileInfo{
name: rn,
mode: info.Mode(),

181
test/filetype_test.go Normal file
View File

@@ -0,0 +1,181 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
// +build integration
// This currently fails; it should be fixed
package integration_test
import (
"log"
"os"
"strings"
"testing"
"time"
)
func TestFiletypeChange(t *testing.T) {
log.Println("Cleaning...")
err := removeAll("s1", "s2", "h1/index", "h2/index")
if err != nil {
t.Fatal(err)
}
log.Println("Generating files...")
err = generateFiles("s1", 100, 20, "../bin/syncthing")
if err != nil {
t.Fatal(err)
}
// A file that we will replace with a directory later
fd, err := os.Create("s1/fileToReplace")
if err != nil {
t.Fatal(err)
}
fd.Close()
// A directory that we will replace with a file later
err = os.Mkdir("s1/dirToReplace", 0755)
if err != nil {
t.Fatal(err)
}
// Verify that the files and directories sync to the other side
log.Println("Syncing...")
sender := syncthingProcess{ // id1
log: "1.out",
argv: []string{"-home", "h1"},
port: 8081,
apiKey: apiKey,
}
err = sender.start()
if err != nil {
t.Fatal(err)
}
receiver := syncthingProcess{ // id2
log: "2.out",
argv: []string{"-home", "h2"},
port: 8082,
apiKey: apiKey,
}
err = receiver.start()
if err != nil {
sender.stop()
t.Fatal(err)
}
for {
comp, err := sender.peerCompletion()
if err != nil {
if strings.Contains(err.Error(), "use of closed network connection") {
time.Sleep(time.Second)
continue
}
sender.stop()
receiver.stop()
t.Fatal(err)
}
curComp := comp[id2]
if curComp == 100 {
sender.stop()
receiver.stop()
break
}
time.Sleep(time.Second)
}
sender.stop()
receiver.stop()
log.Println("Comparing directories...")
err = compareDirectories("s1", "s2")
if err != nil {
t.Fatal(err)
}
log.Println("Making some changes...")
// Replace file with directory
os.RemoveAll("s1/fileToReplace")
err = os.Mkdir("s1/fileToReplace", 0755)
if err != nil {
t.Fatal(err)
}
// Replace directory with file
os.RemoveAll("s1/dirToReplace")
fd, err = os.Create("s1/dirToReplace")
if err != nil {
t.Fatal(err)
}
fd.Close()
// Sync these changes and recheck
log.Println("Syncing...")
err = sender.start()
if err != nil {
t.Fatal(err)
}
err = receiver.start()
if err != nil {
sender.stop()
t.Fatal(err)
}
for {
comp, err := sender.peerCompletion()
if err != nil {
if strings.Contains(err.Error(), "use of closed network connection") {
time.Sleep(time.Second)
continue
}
sender.stop()
receiver.stop()
t.Fatal(err)
}
curComp := comp[id2]
if curComp == 100 {
sender.stop()
receiver.stop()
break
}
time.Sleep(time.Second)
}
sender.stop()
receiver.stop()
log.Println("Comparing directories...")
err = compareDirectories("s1", "s2")
if err != nil {
t.Fatal(err)
}
}

View File

@@ -53,12 +53,12 @@ func TestStressHTTP(t *testing.T) {
tr := &http.Transport{
TLSClientConfig: tc,
DisableKeepAlives: true,
ResponseHeaderTimeout: time.Second,
TLSHandshakeTimeout: time.Second,
ResponseHeaderTimeout: 10 * time.Second,
TLSHandshakeTimeout: 10 * time.Second,
}
client := &http.Client{
Transport: tr,
Timeout: 2 * time.Second,
Timeout: 10 * time.Second,
}
var wg sync.WaitGroup
t0 := time.Now()

274
test/symlink_test.go Normal file
View File

@@ -0,0 +1,274 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
// +build integration
package integration_test
import (
"log"
"os"
"strings"
"testing"
"time"
"github.com/syncthing/syncthing/internal/symlinks"
)
func TestSymlinks(t *testing.T) {
log.Println("Cleaning...")
err := removeAll("s1", "s2", "h1/index", "h2/index")
if err != nil {
t.Fatal(err)
}
log.Println("Generating files...")
err = generateFiles("s1", 100, 20, "../bin/syncthing")
if err != nil {
t.Fatal(err)
}
// A file that we will replace with a symlink later
fd, err := os.Create("s1/fileToReplace")
if err != nil {
t.Fatal(err)
}
fd.Close()
// A directory that we will replace with a symlink later
err = os.Mkdir("s1/dirToReplace", 0755)
if err != nil {
t.Fatal(err)
}
// A file and a symlink to that file
fd, err = os.Create("s1/file")
if err != nil {
t.Fatal(err)
}
fd.Close()
err = symlinks.Create("s1/fileLink", "file", 0)
if err != nil {
log.Fatal(err)
}
// A directory and a symlink to that directory
err = os.Mkdir("s1/dir", 0755)
if err != nil {
t.Fatal(err)
}
err = symlinks.Create("s1/dirLink", "dir", 0)
if err != nil {
log.Fatal(err)
}
// A link to something in the repo that does not exist
err = symlinks.Create("s1/noneLink", "does/not/exist", 0)
if err != nil {
log.Fatal(err)
}
// A link we will replace with a file later
err = symlinks.Create("s1/repFileLink", "does/not/exist", 0)
if err != nil {
log.Fatal(err)
}
// A link we will replace with a directory later
err = symlinks.Create("s1/repDirLink", "does/not/exist", 0)
if err != nil {
log.Fatal(err)
}
// Verify that the files and symlinks sync to the other side
log.Println("Syncing...")
sender := syncthingProcess{ // id1
log: "1.out",
argv: []string{"-home", "h1"},
port: 8081,
apiKey: apiKey,
}
err = sender.start()
if err != nil {
t.Fatal(err)
}
receiver := syncthingProcess{ // id2
log: "2.out",
argv: []string{"-home", "h2"},
port: 8082,
apiKey: apiKey,
}
err = receiver.start()
if err != nil {
sender.stop()
t.Fatal(err)
}
for {
comp, err := sender.peerCompletion()
if err != nil {
if strings.Contains(err.Error(), "use of closed network connection") {
time.Sleep(time.Second)
continue
}
sender.stop()
receiver.stop()
t.Fatal(err)
}
curComp := comp[id2]
if curComp == 100 {
sender.stop()
receiver.stop()
break
}
time.Sleep(time.Second)
}
sender.stop()
receiver.stop()
log.Println("Comparing directories...")
err = compareDirectories("s1", "s2")
if err != nil {
t.Fatal(err)
}
log.Println("Making some changes...")
// Remove one symlink
err = os.Remove("s1/fileLink")
if err != nil {
log.Fatal(err)
}
// Change the target of another
err = os.Remove("s1/dirLink")
if err != nil {
log.Fatal(err)
}
err = symlinks.Create("s1/dirLink", "file", 0)
if err != nil {
log.Fatal(err)
}
// Replace one with a file
err = os.Remove("s1/repFileLink")
if err != nil {
log.Fatal(err)
}
fd, err = os.Create("s1/repFileLink")
if err != nil {
log.Fatal(err)
}
fd.Close()
// Replace one with a directory
err = os.Remove("s1/repDirLink")
if err != nil {
log.Fatal(err)
}
err = os.Mkdir("s1/repDirLink", 0755)
if err != nil {
log.Fatal(err)
}
// Replace a file with a symlink
err = os.Remove("s1/fileToReplace")
if err != nil {
log.Fatal(err)
}
err = symlinks.Create("s1/fileToReplace", "somewhere/non/existent", 0)
if err != nil {
log.Fatal(err)
}
// Replace a directory with a symlink
err = os.RemoveAll("s1/dirToReplace")
if err != nil {
log.Fatal(err)
}
err = symlinks.Create("s1/dirToReplace", "somewhere/non/existent", 0)
if err != nil {
log.Fatal(err)
}
// Sync these changes and recheck
log.Println("Syncing...")
err = sender.start()
if err != nil {
t.Fatal(err)
}
err = receiver.start()
if err != nil {
sender.stop()
t.Fatal(err)
}
for {
comp, err := sender.peerCompletion()
if err != nil {
if strings.Contains(err.Error(), "use of closed network connection") {
time.Sleep(time.Second)
continue
}
sender.stop()
receiver.stop()
t.Fatal(err)
}
curComp := comp[id2]
if curComp == 100 {
sender.stop()
receiver.stop()
break
}
time.Sleep(time.Second)
}
sender.stop()
receiver.stop()
log.Println("Comparing directories...")
err = compareDirectories("s1", "s2")
if err != nil {
t.Fatal(err)
}
}

View File

@@ -121,7 +121,7 @@ alterFiles() {
# Create some new files and alter existing ones
echo " $i: random nonoverlapping"
../genfiles -maxexp 22 -files 200
../genfiles -maxexp 22 -files 200 -src ../genfiles
echo " $i: new files in ro directory"
uuidgen > ro-test/$(uuidgen)
chmod 500 ro-test
@@ -146,7 +146,7 @@ echo "Setting up files..."
for i in 1 12-2 23-3; do
pushd "s$i" >/dev/null
echo " $i: random nonoverlapping"
../genfiles -maxexp 22 -files 400
../genfiles -maxexp 22 -files 400 -src ../genfiles
echo " $i: ro directory"
mkdir ro-test
uuidgen > ro-test/$(uuidgen)

View File

@@ -115,7 +115,7 @@ alterFiles() {
pushd "s$i" >/dev/null
echo " $i: random nonoverlapping"
../genfiles -maxexp 22 -files 200
../genfiles -maxexp 22 -files 200 -src ../genfiles
echo " $i: append to large file"
dd if=large-$i bs=1024k count=4 >> large-$i 2>/dev/null
../md5r -l > ../md5-tmp
@@ -135,7 +135,7 @@ for i in 1 2 3 12-1 12-2 23-2 23-3; do
mkdir "s$i"
pushd "s$i" >/dev/null
echo " $i: random nonoverlapping"
../genfiles -maxexp 22 -files 200
../genfiles -maxexp 22 -files 200 -src ../genfiles
echo " $i: empty file"
touch "empty-$i"
echo " $i: large file"