Compare commits

...

29 Commits

Author SHA1 Message Date
Jakob Borg
4ff6cd9105 Asset update 2014-07-29 13:29:19 +02:00
Jakob Borg
96c17d8292 Translation update 2014-07-29 13:26:49 +02:00
Jakob Borg
bc6faaffc4 Add debug hook for completion, for integration tests 2014-07-29 13:01:27 +02:00
Jakob Borg
51e9839237 Handle UI in restart/shutdown 2014-07-29 11:59:11 +02:00
Jakob Borg
6115631746 Fix status updates for remote nodes 2014-07-29 11:54:00 +02:00
Jakob Borg
ee005fbc8e Generate events on scanning updates 2014-07-29 11:53:45 +02:00
Jakob Borg
e27d42935c Use event interface for GUI (fixes #383) 2014-07-29 11:06:52 +02:00
Jakob Borg
9c99d65716 Build on 32 bit archs (ref #446) 2014-07-28 15:25:34 +02:00
Jakob Borg
5b9469eed3 Might want to keep English as a valid language... 2014-07-28 15:17:43 +02:00
Jakob Borg
6805ac915b Ugly hack to automatically update translations. 2014-07-28 15:14:02 +02:00
Jakob Borg
7148cf99f7 Fix tests, again 2014-07-28 13:11:09 +02:00
Jakob Borg
67a3fb8bf2 Compression as a user option (fixes #446) 2014-07-28 12:44:46 +02:00
Jakob Borg
933b61f99f Fix protocol tests 2014-07-28 12:16:15 +02:00
Jakob Borg
6c5c14f35f Refactor compression support, now at message level. 2014-07-28 11:31:22 +02:00
Jakob Borg
6a441d5013 Merge pull request #445 from AudriusButkevicius/dupes
Fixes and improvements
2014-07-28 10:55:38 +02:00
Audrius Butkevicius
6b46465c77 Avoid resorting multiple times 2014-07-28 00:21:22 +01:00
Audrius Butkevicius
75388caeed Prevent duplicate nodes in repos 2014-07-28 00:15:16 +01:00
Audrius Butkevicius
2546930a1a Fix in-place removal 2014-07-28 00:08:15 +01:00
Jakob Borg
135e29a3bb Don't FATAL if a repo dir cannot be created (fixes #443) 2014-07-27 14:31:15 +02:00
Jakob Borg
3b65a58f59 Translation, language detection 2014-07-26 22:56:12 +02:00
Jakob Borg
49cb931572 Minor refactoring: extract variable... 2014-07-26 21:28:32 +02:00
Jakob Borg
b7176d2204 Implement reception of Close message 2014-07-26 21:27:55 +02:00
Jakob Borg
5bf7d372f6 Genfiles use actual source data 2014-07-26 13:06:57 +02:00
Jakob Borg
073775e461 Build Solaris again 2014-07-25 15:26:23 +02:00
Jakob Borg
fbf8f3dc68 Add LZ4 compression 2014-07-25 15:16:23 +02:00
Jakob Borg
e8c8cc550b Don't use 100% doing nothing 2014-07-25 14:59:56 +02:00
Jakob Borg
87c3790fa8 Debug events module 2014-07-25 14:50:14 +02:00
Jakob Borg
0d9dcb2f4f Remove file count and size limits in protocol 2014-07-25 09:01:54 +02:00
Jakob Borg
6188185b22 Beta versions *should* upgrade to other beta version (ref #436) 2014-07-24 14:23:25 +02:00
56 changed files with 15943 additions and 536 deletions

8
Godeps/Godeps.json generated
View File

@@ -1,6 +1,6 @@
{
"ImportPath": "github.com/calmh/syncthing",
"GoVersion": "devel +6bdbf9086c00 Thu Jul 17 17:02:46 2014 +1000",
"GoVersion": "go1.3",
"Packages": [
"./cmd/..."
],
@@ -40,6 +40,10 @@
"Comment": "null-15",
"Rev": "12e4b4183793ac4b061921e7980845e750679fd0"
},
{
"ImportPath": "github.com/bkaradzic/go-lz4",
"Rev": "77e2ba877bde9da31213bec75dbbe197fa507c21"
},
{
"ImportPath": "github.com/golang/groupcache/lru",
"Rev": "8b25adc0f62632c810997cb38c21111a3f256bf4"
@@ -50,7 +54,7 @@
},
{
"ImportPath": "github.com/syndtr/goleveldb/leveldb",
"Rev": "ba4481e4cb1d45f586e32be2ab663f173b08b207"
"Rev": "c5955912e3287376475731c5bc59c79a5a799105"
},
{
"ImportPath": "github.com/vitrun/qart/coding",

View File

@@ -0,0 +1 @@
/lz4-example/lz4-example

View File

@@ -0,0 +1,7 @@
language: go
go:
- 1.1
- 1.2
- 1.3
- tip

View File

@@ -0,0 +1,24 @@
Copyright 2011-2012 Branimir Karadzic. All rights reserved.
Copyright 2013 Damian Gryski. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,71 @@
go-lz4
======
go-lz4 is port of LZ4 lossless compression algorithm to Go. The original C code
is located at:
https://code.google.com/p/lz4/
Status
------
[![Build Status](https://secure.travis-ci.org/bkaradzic/go-lz4.png)](http://travis-ci.org/bkaradzic/go-lz4)
[![GoDoc](https://godoc.org/github.com/bkaradzic/go-lz4?status.png)](https://godoc.org/github.com/bkaradzic/go-lz4)
Usage
-----
go get github.com/bkaradzic/go-lz4
import "github.com/bkaradzic/go-lz4"
The package name is `lz4`
Notes
-----
* go-lz4 saves a uint32 with the original uncompressed length at the beginning
of the encoded buffer. They may get in the way of interoperability with
other implementations.
Contributors
------------
Damian Gryski ([@dgryski](https://github.com/dgryski))
Dustin Sallings ([@dustin](https://github.com/dustin))
Contact
-------
[@bkaradzic](https://twitter.com/bkaradzic)
http://www.stuckingeometry.com
Project page
https://github.com/bkaradzic/go-lz4
License
-------
Copyright 2011-2012 Branimir Karadzic. All rights reserved.
Copyright 2013 Damian Gryski. All rights reserved.
Redistribution and use in source and binary forms, with or without modification,
are permitted provided that the following conditions are met:
1. Redistributions of source code must retain the above copyright notice, this
list of conditions and the following disclaimer.
2. Redistributions in binary form must reproduce the above copyright notice,
this list of conditions and the following disclaimer in the documentation
and/or other materials provided with the distribution.
THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR
IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
THE POSSIBILITY OF SUCH DAMAGE.

View File

@@ -0,0 +1,74 @@
package main
import (
"math/rand"
"github.com/bkaradzic/go-lz4"
// lz4's API matches snappy's, so we can easily see how it performs
// lz4 "code.google.com/p/snappy-go/snappy"
)
var input = `
ADVENTURE I. A SCANDAL IN BOHEMIA
I.
To Sherlock Holmes she is always THE woman. I have seldom heard
him mention her under any other name. In his eyes she eclipses
and predominates the whole of her sex. It was not that he felt
any emotion akin to love for Irene Adler. All emotions, and that
one particularly, were abhorrent to his cold, precise but
admirably balanced mind. He was, I take it, the most perfect
reasoning and observing machine that the world has seen, but as a
lover he would have placed himself in a false position. He never
spoke of the softer passions, save with a gibe and a sneer. They
were admirable things for the observer--excellent for drawing the
veil from men's motives and actions. But for the trained reasoner
to admit such intrusions into his own delicate and finely
adjusted temperament was to introduce a distracting factor which
might throw a doubt upon all his mental results. Grit in a
sensitive instrument, or a crack in one of his own high-power
lenses, would not be more disturbing than a strong emotion in a
nature such as his. And yet there was but one woman to him, and
that woman was the late Irene Adler, of dubious and questionable
memory.
I had seen little of Holmes lately. My marriage had drifted us
away from each other. My own complete happiness, and the
home-centred interests which rise up around the man who first
finds himself master of his own establishment, were sufficient to
absorb all my attention, while Holmes, who loathed every form of
society with his whole Bohemian soul, remained in our lodgings in
Baker Street, buried among his old books, and alternating from
week to week between cocaine and ambition, the drowsiness of the
drug, and the fierce energy of his own keen nature. He was still,
as ever, deeply attracted by the study of crime, and occupied his
immense faculties and extraordinary powers of observation in
following out those clues, and clearing up those mysteries which
had been abandoned as hopeless by the official police. From time
to time I heard some vague account of his doings: of his summons
to Odessa in the case of the Trepoff murder, of his clearing up
of the singular tragedy of the Atkinson brothers at Trincomalee,
and finally of the mission which he had accomplished so
delicately and successfully for the reigning family of Holland.
Beyond these signs of his activity, however, which I merely
shared with all the readers of the daily press, I knew little of
my former friend and companion.
`
func main() {
compressed, _ := lz4.Encode(nil, []byte(input))
modified := make([]byte, len(compressed))
for {
copy(modified, compressed)
for i := 0; i < 100; i++ {
modified[rand.Intn(len(compressed)-4)+4] = byte(rand.Intn(256))
}
lz4.Decode(nil, modified)
}
}

View File

@@ -0,0 +1,86 @@
/*
* Copyright 2011 Branimir Karadzic. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
package main
import (
"flag"
"fmt"
"io/ioutil"
"log"
"os"
"runtime/pprof"
lz4 "github.com/bkaradzic/go-lz4"
)
var (
decompress = flag.Bool("d", false, "decompress")
)
func main() {
var optCPUProfile = flag.String("cpuprofile", "", "profile")
flag.Parse()
if *optCPUProfile != "" {
f, err := os.Create(*optCPUProfile)
if err != nil {
log.Fatal(err)
}
pprof.StartCPUProfile(f)
defer pprof.StopCPUProfile()
}
args := flag.Args()
var data []byte
if len(args) < 2 {
fmt.Print("Usage: lz4 [-d] <input> <output>\n")
os.Exit(1)
}
input, err := os.OpenFile(args[0], os.O_RDONLY, 0644)
if err != nil {
fmt.Printf("Failed to open input file %s\n", args[0])
os.Exit(1)
}
defer input.Close()
if *decompress {
data, _ = ioutil.ReadAll(input)
data, _ = lz4.Decode(nil, data)
} else {
data, _ = ioutil.ReadAll(input)
data, _ = lz4.Encode(nil, data)
}
err = ioutil.WriteFile(args[1], data, 0644)
if err != nil {
fmt.Printf("Failed to open output file %s\n", args[1])
os.Exit(1)
}
}

View File

@@ -0,0 +1,63 @@
package lz4
import (
"bytes"
"io/ioutil"
"testing"
)
var testfile, _ = ioutil.ReadFile("testdata/pg1661.txt")
func roundtrip(t *testing.T, input []byte) {
dst, err := Encode(nil, input)
if err != nil {
t.Errorf("got error during compression: %s", err)
}
output, err := Decode(nil, dst)
if err != nil {
t.Errorf("got error during decompress: %s", err)
}
if !bytes.Equal(output, input) {
t.Errorf("roundtrip failed")
}
}
func TestEmpty(t *testing.T) {
roundtrip(t, nil)
}
func TestLengths(t *testing.T) {
for i := 0; i < 1024; i++ {
roundtrip(t, testfile[:i])
}
for i := 1024; i < 4096; i += 23 {
roundtrip(t, testfile[:i])
}
}
func TestWords(t *testing.T) {
roundtrip(t, testfile)
}
func BenchmarkLZ4Encode(b *testing.B) {
for i := 0; i < b.N; i++ {
Encode(nil, testfile)
}
}
func BenchmarkLZ4Decode(b *testing.B) {
var compressed, _ = Encode(nil, testfile)
b.ResetTimer()
for i := 0; i < b.N; i++ {
Decode(nil, compressed)
}
}

View File

@@ -0,0 +1,194 @@
/*
* Copyright 2011-2012 Branimir Karadzic. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
package lz4
import (
"encoding/binary"
"errors"
"io"
)
var (
// ErrCorrupt indicates the input was corrupt
ErrCorrupt = errors.New("corrupt input")
)
const (
mlBits = 4
mlMask = (1 << mlBits) - 1
runBits = 8 - mlBits
runMask = (1 << runBits) - 1
)
type decoder struct {
src []byte
dst []byte
spos uint32
dpos uint32
ref uint32
}
func (d *decoder) readByte() (uint8, error) {
if int(d.spos) == len(d.src) {
return 0, io.EOF
}
b := d.src[d.spos]
d.spos++
return b, nil
}
func (d *decoder) getLen() (uint32, error) {
length := uint32(0)
ln, err := d.readByte()
if err != nil {
return 0, ErrCorrupt
}
for ln == 255 {
length += 255
ln, err = d.readByte()
if err != nil {
return 0, ErrCorrupt
}
}
length += uint32(ln)
return length, nil
}
func (d *decoder) cp(length, decr uint32) {
if int(d.ref+length) < int(d.dpos) {
copy(d.dst[d.dpos:], d.dst[d.ref:d.ref+length])
} else {
for ii := uint32(0); ii < length; ii++ {
d.dst[d.dpos+ii] = d.dst[d.ref+ii]
}
}
d.dpos += length
d.ref += length - decr
}
func (d *decoder) finish(err error) error {
if err == io.EOF {
return nil
}
return err
}
// Decode returns the decoded form of src. The returned slice may be a
// subslice of dst if it was large enough to hold the entire decoded block.
func Decode(dst, src []byte) ([]byte, error) {
if len(src) < 4 {
return nil, ErrCorrupt
}
uncompressedLen := binary.LittleEndian.Uint32(src)
if uncompressedLen == 0 {
return nil, nil
}
if uncompressedLen > MaxInputSize {
return nil, ErrTooLarge
}
if dst == nil || len(dst) < int(uncompressedLen) {
dst = make([]byte, uncompressedLen)
}
d := decoder{src: src, dst: dst[:uncompressedLen], spos: 4}
decr := []uint32{0, 3, 2, 3}
for {
code, err := d.readByte()
if err != nil {
return d.dst, d.finish(err)
}
length := uint32(code >> mlBits)
if length == runMask {
ln, err := d.getLen()
if err != nil {
return nil, ErrCorrupt
}
length += ln
}
if int(d.spos+length) > len(d.src) {
return nil, ErrCorrupt
}
for ii := uint32(0); ii < length; ii++ {
d.dst[d.dpos+ii] = d.src[d.spos+ii]
}
d.spos += length
d.dpos += length
if int(d.spos) == len(d.src) {
return d.dst, nil
}
if int(d.spos+2) >= len(d.src) {
return nil, ErrCorrupt
}
back := uint32(d.src[d.spos]) | uint32(d.src[d.spos+1])<<8
if back > d.dpos {
return nil, ErrCorrupt
}
d.spos += 2
d.ref = d.dpos - back
length = uint32(code & mlMask)
if length == mlMask {
ln, err := d.getLen()
if err != nil {
return nil, ErrCorrupt
}
length += ln
}
literal := d.dpos - d.ref
if literal < 4 {
d.cp(4, decr[literal])
} else {
length += 4
}
if d.dpos+length > uncompressedLen {
return nil, ErrCorrupt
}
d.cp(length, 0)
}
}

View File

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,188 @@
/*
* Copyright 2011-2012 Branimir Karadzic. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification,
* are permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this
* list of conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice,
* this list of conditions and the following disclaimer in the documentation
* and/or other materials provided with the distribution.
*
* THIS SOFTWARE IS PROVIDED BY COPYRIGHT HOLDER ``AS IS'' AND ANY EXPRESS OR
* IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT
* SHALL COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT,
* INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR
* PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY,
* WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
* OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
* THE POSSIBILITY OF SUCH DAMAGE.
*/
package lz4
import "encoding/binary"
import "errors"
const (
minMatch = 4
hashLog = 17
hashTableSize = 1 << hashLog
hashShift = (minMatch * 8) - hashLog
incompressible uint32 = 128
uninitHash = 0x88888888
// MaxInputSize is the largest buffer than can be compressed in a single block
MaxInputSize = 0x7E000000
)
var (
// ErrTooLarge indicates the input buffer was too large
ErrTooLarge = errors.New("input too large")
)
type encoder struct {
src []byte
dst []byte
hashTable []uint32
pos uint32
anchor uint32
dpos uint32
}
// CompressBound returns the maximum length of a lz4 block, given it's uncompressed length
func CompressBound(isize int) int {
if isize > MaxInputSize {
return 0
}
return isize + ((isize) / 255) + 16 + 4
}
func (e *encoder) writeLiterals(length, mlLen, pos uint32) {
ln := length
var code byte
if ln > runMask-1 {
code = runMask
} else {
code = byte(ln)
}
if mlLen > mlMask-1 {
e.dst[e.dpos] = (code << mlBits) + byte(mlMask)
} else {
e.dst[e.dpos] = (code << mlBits) + byte(mlLen)
}
e.dpos++
if code == runMask {
ln -= runMask
for ; ln > 254; ln -= 255 {
e.dst[e.dpos] = 255
e.dpos++
}
e.dst[e.dpos] = byte(ln)
e.dpos++
}
for ii := uint32(0); ii < length; ii++ {
e.dst[e.dpos+ii] = e.src[pos+ii]
}
e.dpos += length
}
// Encode returns the encoded form of src. The returned array may be a
// sub-slice of dst if it was large enough to hold the entire output.
func Encode(dst, src []byte) ([]byte, error) {
if len(src) >= MaxInputSize {
return nil, ErrTooLarge
}
if n := CompressBound(len(src)); len(dst) < n {
dst = make([]byte, n)
}
e := encoder{src: src, dst: dst, hashTable: make([]uint32, hashTableSize)}
binary.LittleEndian.PutUint32(dst, uint32(len(src)))
e.dpos = 4
var (
step uint32 = 1
limit = incompressible
)
for {
if int(e.pos)+4 >= len(e.src) {
e.writeLiterals(uint32(len(e.src))-e.anchor, 0, e.anchor)
return e.dst[:e.dpos], nil
}
sequence := uint32(e.src[e.pos+3])<<24 | uint32(e.src[e.pos+2])<<16 | uint32(e.src[e.pos+1])<<8 | uint32(e.src[e.pos+0])
hash := (sequence * 2654435761) >> hashShift
ref := e.hashTable[hash] + uninitHash
e.hashTable[hash] = e.pos - uninitHash
if ((e.pos-ref)>>16) != 0 || uint32(e.src[ref+3])<<24|uint32(e.src[ref+2])<<16|uint32(e.src[ref+1])<<8|uint32(e.src[ref+0]) != sequence {
if e.pos-e.anchor > limit {
limit <<= 1
step += 1 + (step >> 2)
}
e.pos += step
continue
}
if step > 1 {
e.hashTable[hash] = ref - uninitHash
e.pos -= step - 1
step = 1
continue
}
limit = incompressible
ln := e.pos - e.anchor
back := e.pos - ref
anchor := e.anchor
e.pos += minMatch
ref += minMatch
e.anchor = e.pos
for int(e.pos) < len(e.src) && e.src[e.pos] == e.src[ref] {
e.pos++
ref++
}
mlLen := e.pos - e.anchor
e.writeLiterals(ln, mlLen, anchor)
e.dst[e.dpos] = uint8(back)
e.dst[e.dpos+1] = uint8(back >> 8)
e.dpos += 2
if mlLen > mlMask-1 {
mlLen -= mlMask
for mlLen > 254 {
mlLen -= 255
e.dst[e.dpos] = 255
e.dpos++
}
e.dst[e.dpos] = byte(mlLen)
e.dpos++
}
e.anchor = e.pos
}
}

View File

@@ -0,0 +1,68 @@
// Copyright (c) 2012, Suryandaru Triandana <syndtr@gmail.com>
// All rights reserved.
//
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// +build solaris
package storage
import (
"os"
"syscall"
)
type unixFileLock struct {
f *os.File
}
func (fl *unixFileLock) release() error {
if err := setFileLock(fl.f, false); err != nil {
return err
}
return fl.f.Close()
}
func newFileLock(path string) (fl fileLock, err error) {
f, err := os.OpenFile(path, os.O_RDWR|os.O_CREATE, 0644)
if err != nil {
return
}
err = setFileLock(f, true)
if err != nil {
f.Close()
return
}
fl = &unixFileLock{f: f}
return
}
func setFileLock(f *os.File, lock bool) error {
flock := syscall.Flock_t{
Type: syscall.F_UNLCK,
Start: 0,
Len: 0,
Whence: 1,
}
if lock {
flock.Type = syscall.F_WRLCK
}
return syscall.FcntlFlock(f.Fd(), syscall.F_SETLK, &flock)
}
func rename(oldpath, newpath string) error {
return os.Rename(oldpath, newpath)
}
func syncDir(name string) error {
f, err := os.Open(name)
if err != nil {
return err
}
defer f.Close()
if err := f.Sync(); err != nil {
return err
}
return nil
}

View File

@@ -4,7 +4,7 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
// +build darwin freebsd linux netbsd openbsd
// +build darwin dragonfly freebsd linux netbsd openbsd
package storage

View File

File diff suppressed because one or more lines are too long

View File

@@ -102,6 +102,12 @@ xdr() {
done
}
transifex() {
pushd gui
go run ../cmd/transifexdl/main.go
popd
}
case "$1" in
"")
shift
@@ -143,7 +149,7 @@ case "$1" in
test || exit 1
assets
for os in darwin-amd64 linux-386 linux-amd64 freebsd-amd64 windows-amd64 windows-386 ; do
for os in darwin-amd64 linux-386 linux-amd64 freebsd-amd64 windows-amd64 windows-386 solaris-amd64 ; do
export GOOS=${os%-*}
export GOARCH=${os#*-}
@@ -208,6 +214,10 @@ case "$1" in
xdr
;;
transifex)
transifex
;;
*)
echo "Unknown build parameter $1"
;;

View File

@@ -47,7 +47,7 @@ var (
static func(http.ResponseWriter, *http.Request, *log.Logger)
apiKey string
modt = time.Now().UTC().Format(http.TimeFormat)
eventSub = events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents), 1000)
eventSub *events.BufferedSubscription
)
const (
@@ -56,6 +56,8 @@ const (
func init() {
l.AddHandler(logger.LevelWarn, showGuiError)
sub := events.Default.Subscribe(^events.EventType(events.ItemStarted | events.ItemCompleted))
eventSub = events.NewBufferedSubscription(sub, 1000)
}
func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
@@ -92,31 +94,36 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
// The GET handlers
getRestMux := http.NewServeMux()
getRestMux.HandleFunc("/rest/version", restGetVersion)
getRestMux.HandleFunc("/rest/completion", withModel(m, restGetCompletion))
getRestMux.HandleFunc("/rest/config", restGetConfig)
getRestMux.HandleFunc("/rest/config/sync", restGetConfigInSync)
getRestMux.HandleFunc("/rest/connections", withModel(m, restGetConnections))
getRestMux.HandleFunc("/rest/discovery", restGetDiscovery)
getRestMux.HandleFunc("/rest/errors", restGetErrors)
getRestMux.HandleFunc("/rest/events", restGetEvents)
getRestMux.HandleFunc("/rest/lang", restGetLang)
getRestMux.HandleFunc("/rest/model", withModel(m, restGetModel))
getRestMux.HandleFunc("/rest/model/version", withModel(m, restGetModelVersion))
getRestMux.HandleFunc("/rest/need", withModel(m, restGetNeed))
getRestMux.HandleFunc("/rest/connections", withModel(m, restGetConnections))
getRestMux.HandleFunc("/rest/config", restGetConfig)
getRestMux.HandleFunc("/rest/config/sync", restGetConfigInSync)
getRestMux.HandleFunc("/rest/system", restGetSystem)
getRestMux.HandleFunc("/rest/errors", restGetErrors)
getRestMux.HandleFunc("/rest/discovery", restGetDiscovery)
getRestMux.HandleFunc("/rest/report", withModel(m, restGetReport))
getRestMux.HandleFunc("/rest/events", restGetEvents)
getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
getRestMux.HandleFunc("/rest/nodeid", restGetNodeID)
getRestMux.HandleFunc("/rest/report", withModel(m, restGetReport))
getRestMux.HandleFunc("/rest/system", restGetSystem)
getRestMux.HandleFunc("/rest/upgrade", restGetUpgrade)
getRestMux.HandleFunc("/rest/version", restGetVersion)
// Debug endpoints, not for general use
getRestMux.HandleFunc("/rest/debug/peerCompletion", withModel(m, restGetPeerCompletion))
// The POST handlers
postRestMux := http.NewServeMux()
postRestMux.HandleFunc("/rest/config", withModel(m, restPostConfig))
postRestMux.HandleFunc("/rest/restart", restPostRestart)
postRestMux.HandleFunc("/rest/reset", restPostReset)
postRestMux.HandleFunc("/rest/shutdown", restPostShutdown)
postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint)
postRestMux.HandleFunc("/rest/error", restPostError)
postRestMux.HandleFunc("/rest/error/clear", restClearErrors)
postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint)
postRestMux.HandleFunc("/rest/model/override", withModel(m, restPostOverride))
postRestMux.HandleFunc("/rest/reset", restPostReset)
postRestMux.HandleFunc("/rest/restart", restPostRestart)
postRestMux.HandleFunc("/rest/shutdown", restPostShutdown)
postRestMux.HandleFunc("/rest/upgrade", restPostUpgrade)
// A handler that splits requests between the two above and disables
@@ -174,6 +181,25 @@ func restGetVersion(w http.ResponseWriter, r *http.Request) {
w.Write([]byte(Version))
}
func restGetCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var repo = qs.Get("repo")
var nodeStr = qs.Get("node")
node, err := protocol.NodeIDFromString(nodeStr)
if err != nil {
http.Error(w, err.Error(), 500)
return
}
res := map[string]float64{
"completion": m.Completion(node, repo),
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(res)
}
func restGetModelVersion(m *model.Model, w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query()
var repo = qs.Get("repo")
@@ -287,6 +313,7 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
nm := newCfg.NodeMap()
for k := range om {
if _, ok := nm[k]; !ok {
// A node was removed and another added
configInSync = false
break
}
@@ -421,11 +448,18 @@ func restGetReport(m *model.Model, w http.ResponseWriter, r *http.Request) {
func restGetEvents(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query()
ts := qs.Get("since")
since, _ := strconv.Atoi(ts)
sinceStr := qs.Get("since")
limitStr := qs.Get("limit")
since, _ := strconv.Atoi(sinceStr)
limit, _ := strconv.Atoi(limitStr)
evs := eventSub.Since(since, nil)
if 0 < limit && limit < len(evs) {
evs = evs[len(evs)-limit:]
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(eventSub.Since(since, nil))
json.NewEncoder(w).Encode(evs)
}
func restGetUpgrade(w http.ResponseWriter, r *http.Request) {
@@ -459,6 +493,17 @@ func restGetNodeID(w http.ResponseWriter, r *http.Request) {
}
}
func restGetLang(w http.ResponseWriter, r *http.Request) {
lang := r.Header.Get("Accept-Language")
var langs []string
for _, l := range strings.Split(lang, ",") {
if len(l) >= 2 {
langs = append(langs, l[:2])
}
}
json.NewEncoder(w).Encode(langs)
}
func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
err := upgrade()
if err != nil {
@@ -483,6 +528,31 @@ func getQR(w http.ResponseWriter, r *http.Request) {
w.Write(code.PNG())
}
func restGetPeerCompletion(m *model.Model, w http.ResponseWriter, r *http.Request) {
tot := map[string]float64{}
count := map[string]float64{}
for _, repo := range cfg.Repositories {
for _, node := range repo.NodeIDs() {
nodeStr := node.String()
if m.ConnectedTo(node) {
tot[nodeStr] += m.Completion(node, repo.ID)
} else {
tot[nodeStr] = 0
}
count[nodeStr]++
}
}
comp := map[string]int{}
for node := range tot {
comp[node] = int(tot[node] / count[node])
}
w.Header().Set("Content-Type", "application/json; charset=utf-8")
json.NewEncoder(w).Encode(comp)
}
func basicAuthMiddleware(username string, passhash string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if validAPIKey(r.Header.Get("X-API-Key")) {

View File

@@ -107,6 +107,7 @@ The following enviroment variables are interpreted by syncthing:
facility strings:
- "beacon" (the beacon package)
- "discover" (the discover package)
- "events" (the events package)
- "files" (the files package)
- "net" (the main package; connections & network messages)
- "model" (the model package)
@@ -307,20 +308,29 @@ nextRepo:
repo.Directory = expandTilde(repo.Directory)
// Safety check. If the cached index contains files but the repository
// doesn't exist, we have a problem. We would assume that all files
// have been deleted which might not be the case, so abort instead.
id := fmt.Sprintf("%x", sha1.Sum([]byte(repo.Directory)))
idxFile := filepath.Join(confDir, id+".idx.gz")
if _, err := os.Stat(idxFile); err == nil {
if fi, err := os.Stat(repo.Directory); err != nil || !fi.IsDir() {
fi, err := os.Stat(repo.Directory)
if m.LocalVersion(repo.ID) > 0 {
// Safety check. If the cached index contains files but the
// repository doesn't exist, we have a problem. We would assume
// that all files have been deleted which might not be the case,
// so mark it as invalid instead.
if err != nil || !fi.IsDir() {
cfg.Repositories[i].Invalid = "repo directory missing"
continue nextRepo
}
} else if os.IsNotExist(err) {
// If we don't have ny files in the index, and the directory does
// exist, try creating it.
err = os.MkdirAll(repo.Directory, 0700)
}
if err != nil {
// If there was another error or we could not create the
// directory, the repository is invalid.
cfg.Repositories[i].Invalid = err.Error()
continue nextRepo
}
ensureDir(repo.Directory, -1)
m.AddRepo(repo)
}
@@ -644,7 +654,7 @@ next:
wr = &limitedWriter{conn, rateBucket}
}
name := fmt.Sprintf("%s-%s", conn.LocalAddr(), conn.RemoteAddr())
protoConn := protocol.NewConnection(remoteID, conn, wr, m, name)
protoConn := protocol.NewConnection(remoteID, conn, wr, m, name, nodeCfg.Compression)
l.Infof("Established secure connection to %s at %s", remoteID, name)
if debugNet {

View File

@@ -93,13 +93,24 @@ func currentRelease() (githubRelease, error) {
json.NewDecoder(resp.Body).Decode(&rels)
resp.Body.Close()
for _, rel := range rels {
if !rel.Prerelease {
return rel, nil
if strings.Contains(Version, "-beta") {
// We are a beta version. Use whatever we can find that is newer-or-equal than current.
for _, rel := range rels {
if compareVersions(rel.Tag, Version) >= 0 {
return rel, nil
}
}
// We found nothing. Return the latest release and let the next layer decide.
return rels[0], nil
} else {
// We are a regular release. Only consider non-prerelease versions for upgrade.
for _, rel := range rels {
if !rel.Prerelease {
return rel, nil
}
}
return githubRelease{}, errors.New("no suitable release found")
}
return githubRelease{}, errors.New("no suitable release found")
}
func readTarGZ(url string, dir string) (string, error) {

91
cmd/transifexdl/main.go Normal file
View File

@@ -0,0 +1,91 @@
package main
import (
"encoding/json"
"log"
"net/http"
"os"
"sort"
)
type stat struct {
Translated int `json:"translated_entities"`
Untranslated int `json:"untranslated_entities"`
}
type translation struct {
Content string
}
func main() {
log.SetFlags(log.Lshortfile)
if u, p := userPass(); u == "" || p == "" {
log.Fatal("Need environment variables TRANSIFEX_USER and TRANSIFEX_PASS")
}
resp := req("https://www.transifex.com/api/2/project/syncthing/resource/gui/stats")
var stats map[string]stat
err := json.NewDecoder(resp.Body).Decode(&stats)
if err != nil {
log.Fatal(err)
}
resp.Body.Close()
var langs []string
for code, stat := range stats {
shortCode := code[:2]
if pct := 100 * stat.Translated / (stat.Translated + stat.Untranslated); pct < 95 {
log.Printf("Skipping language %q (too low completion ratio %d%%)", shortCode, pct)
os.Remove("lang-" + shortCode + ".json")
continue
}
langs = append(langs, shortCode)
if shortCode == "en" {
continue
}
log.Printf("Updating language %q", shortCode)
resp := req("https://www.transifex.com/api/2/project/syncthing/resource/gui/translation/" + code)
var t translation
err := json.NewDecoder(resp.Body).Decode(&t)
if err != nil {
log.Fatal(err)
}
resp.Body.Close()
fd, err := os.Create("lang-" + shortCode + ".json")
if err != nil {
log.Fatal(err)
}
fd.WriteString(t.Content)
fd.Close()
}
sort.Strings(langs)
json.NewEncoder(os.Stdout).Encode(langs)
}
func userPass() (string, string) {
user := os.Getenv("TRANSIFEX_USER")
pass := os.Getenv("TRANSIFEX_PASS")
return user, pass
}
func req(url string) *http.Response {
user, pass := userPass()
req, err := http.NewRequest("GET", url, nil)
if err != nil {
log.Fatal(err)
}
req.SetBasicAuth(user, pass)
resp, err := http.DefaultClient.Do(req)
if err != nil {
log.Fatal(err)
}
return resp
}

View File

@@ -93,9 +93,10 @@ func (r *RepositoryConfiguration) NodeIDs() []protocol.NodeID {
}
type NodeConfiguration struct {
NodeID protocol.NodeID `xml:"id,attr"`
Name string `xml:"name,attr,omitempty"`
Addresses []string `xml:"address,omitempty"`
NodeID protocol.NodeID `xml:"id,attr"`
Name string `xml:"name,attr,omitempty"`
Addresses []string `xml:"address,omitempty"`
Compression bool `xml:"compression,attr"`
}
type OptionsConfiguration struct {
@@ -313,10 +314,14 @@ func Load(rd io.Reader, myID protocol.NodeID) (Configuration, error) {
// Ensure this node is present in all relevant places
// Ensure that any loose nodes are not present in the wrong places
// Ensure that there are no duplicate nodes
cfg.Nodes = ensureNodePresent(cfg.Nodes, myID)
sort.Sort(NodeConfigurationList(cfg.Nodes))
for i := range cfg.Repositories {
cfg.Repositories[i].Nodes = ensureNodePresent(cfg.Repositories[i].Nodes, myID)
cfg.Repositories[i].Nodes = ensureExistingNodes(cfg.Repositories[i].Nodes, existingNodes)
cfg.Repositories[i].Nodes = ensureNoDuplicates(cfg.Repositories[i].Nodes)
sort.Sort(NodeConfigurationList(cfg.Repositories[i].Nodes))
}
// An empty address list is equivalent to a single "dynamic" entry
@@ -381,40 +386,50 @@ func (l NodeConfigurationList) Len() int {
}
func ensureNodePresent(nodes []NodeConfiguration, myID protocol.NodeID) []NodeConfiguration {
var myIDExists bool
for _, node := range nodes {
if node.NodeID.Equals(myID) {
myIDExists = true
break
return nodes
}
}
if !myIDExists {
name, _ := os.Hostname()
nodes = append(nodes, NodeConfiguration{
NodeID: myID,
Name: name,
})
}
sort.Sort(NodeConfigurationList(nodes))
name, _ := os.Hostname()
nodes = append(nodes, NodeConfiguration{
NodeID: myID,
Name: name,
})
return nodes
}
func ensureExistingNodes(nodes []NodeConfiguration, existingNodes map[protocol.NodeID]bool) []NodeConfiguration {
count := len(nodes)
i := 0
for _, node := range nodes {
if _, ok := existingNodes[node.NodeID]; !ok {
last := len(nodes) - 1
nodes[i] = nodes[last]
nodes = nodes[:last]
} else {
i++
loop:
for i < count {
if _, ok := existingNodes[nodes[i].NodeID]; !ok {
nodes[i] = nodes[count-1]
count--
continue loop
}
i++
}
sort.Sort(NodeConfigurationList(nodes))
return nodes
return nodes[0:count]
}
func ensureNoDuplicates(nodes []NodeConfiguration) []NodeConfiguration {
count := len(nodes)
i := 0
seenNodes := make(map[protocol.NodeID]bool)
loop:
for i < count {
id := nodes[i].NodeID
if _, ok := seenNodes[id]; ok {
nodes[i] = nodes[count-1]
count--
continue loop
}
seenNodes[id] = true
i++
}
return nodes[0:count]
}

View File

@@ -59,6 +59,12 @@ func TestNodeConfig(t *testing.T) {
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
<address>b</address>
</node>
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
<address>a</address>
</node>
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
<address>b</address>
</node>
</repository>
<options>
<readOnly>true</readOnly>
@@ -69,9 +75,12 @@ func TestNodeConfig(t *testing.T) {
v2data := []byte(`
<configuration version="2">
<repository id="test" directory="~/Sync" ro="true">
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
<node id="C4YBIESWDUAIGU62GOSRXCRAAJDWVE3TKCPMURZE2LH5QHAF576A"/>
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
<node id="C4YBIESWDUAIGU62GOSRXCRAAJDWVE3TKCPMURZE2LH5QHAF576A"/>
</repository>
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
<address>a</address>

17
events/debug.go Normal file
View File

@@ -0,0 +1,17 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package events
import (
"os"
"strings"
"github.com/calmh/syncthing/logger"
)
var (
debug = strings.Contains(os.Getenv("STTRACE"), "events") || os.Getenv("STTRACE") == "all"
dl = logger.DefaultLogger
)

View File

@@ -1,3 +1,7 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
// Package events provides event subscription and polling functionality.
package events
@@ -93,6 +97,9 @@ func NewLogger() *Logger {
func (l *Logger) Log(t EventType, data interface{}) {
l.mutex.Lock()
if debug {
dl.Debugln("log", l.nextId, t.String(), data)
}
e := Event{
ID: l.nextId,
Time: time.Now(),
@@ -114,6 +121,9 @@ func (l *Logger) Log(t EventType, data interface{}) {
func (l *Logger) Subscribe(mask EventType) *Subscription {
l.mutex.Lock()
if debug {
dl.Debugln("subscribe", mask)
}
s := &Subscription{
mask: mask,
id: l.nextId,
@@ -127,6 +137,9 @@ func (l *Logger) Subscribe(mask EventType) *Subscription {
func (l *Logger) Unsubscribe(s *Subscription) {
l.mutex.Lock()
if debug {
dl.Debugln("unsubsribe")
}
delete(l.subs, s.id)
close(s.events)
l.mutex.Unlock()
@@ -136,6 +149,10 @@ func (s *Subscription) Poll(timeout time.Duration) (Event, error) {
s.mutex.Lock()
defer s.mutex.Unlock()
if debug {
dl.Debugln("poll", timeout)
}
to := time.After(timeout)
select {
case e, ok := <-s.events:

View File

@@ -1,3 +1,7 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
// All rights reserved. Use of this source code is governed by an MIT-style
// license that can be found in the LICENSE file.
package events_test
import (

View File

@@ -632,9 +632,6 @@ func ldbWithNeed(db *leveldb.DB, repo, node []byte, fn fileIterator) {
if need || !have {
name := globalKeyName(dbi.Key())
if debug {
l.Debugf("need repo=%q node=%v name=%q need=%v have=%v haveV=%d globalV=%d", repo, protocol.NodeIDFromBytes(node), name, need, have, haveVersion, vl.versions[0].version)
}
fk := nodeKey(repo, vl.versions[0].node, name)
bs, err := snap.Get(fk, nil)
if err != nil {
@@ -652,6 +649,10 @@ func ldbWithNeed(db *leveldb.DB, repo, node []byte, fn fileIterator) {
continue
}
if debug {
l.Debugf("need repo=%q node=%v name=%q need=%v have=%v haveV=%d globalV=%d", repo, protocol.NodeIDFromBytes(node), name, need, have, haveVersion, vl.versions[0].version)
}
if cont := fn(gf); !cont {
return
}

View File

@@ -85,7 +85,7 @@ func (s *Set) Update(node protocol.NodeID, fs []protocol.FileInfo) {
func (s *Set) WithNeed(node protocol.NodeID, fn fileIterator) {
if debug {
l.Debugf("%s Need(%v)", s.repo, node)
l.Debugf("%s WithNeed(%v)", s.repo, node)
}
ldbWithNeed(s.db, []byte(s.repo), node[:], fn)
}

View File

@@ -9,6 +9,7 @@
var syncthing = angular.module('syncthing', ['pascalprecht.translate']);
var urlbase = 'rest';
var validLangs = ["de","en","es","fr","pt","sv"];
syncthing.config(function ($httpProvider, $translateProvider) {
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
@@ -18,7 +19,51 @@ syncthing.config(function ($httpProvider, $translateProvider) {
prefix: 'lang-',
suffix: '.json'
});
$translateProvider.preferredLanguage('en');
});
syncthing.controller('EventCtrl', function ($scope, $http) {
$scope.lastEvent = null;
var online = false;
var lastID = 0;
var successFn = function (data) {
if (!online) {
$scope.$emit('UIOnline');
online = true;
}
if (lastID > 0) {
data.forEach(function (event) {
console.log("event", event.id, event.type, event.data);
$scope.$emit(event.type, event);
});
};
$scope.lastEvent = data[data.length - 1];
lastID = $scope.lastEvent.id;
setTimeout(function () {
$http.get(urlbase + '/events?since=' + lastID)
.success(successFn)
.error(errorFn);
}, 500);
};
var errorFn = function (data) {
if (online) {
$scope.$emit('UIOffline');
online = false;
}
setTimeout(function () {
$http.get(urlbase + '/events?limit=1')
.success(successFn)
.error(errorFn);
}, 1000);
};
$http.get(urlbase + '/events?limit=1')
.success(successFn)
.error(errorFn);
});
syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $location) {
@@ -26,20 +71,32 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
var getOK = true;
var restarting = false;
$scope.connections = {};
$scope.completion = {};
$scope.config = {};
$scope.configInSync = true;
$scope.connections = {};
$scope.errors = [];
$scope.model = {};
$scope.myID = '';
$scope.nodes = [];
$scope.configInSync = true;
$scope.protocolChanged = false;
$scope.errors = [];
$scope.seenError = '';
$scope.model = {};
$scope.repos = {};
$scope.reportData = {};
$scope.reportPreview = false;
$scope.repos = {};
$scope.seenError = '';
$scope.upgradeInfo = {};
$http.get(urlbase+"/lang").success(function (langs) {
var lang;
for (var i = 0; i < langs.length; i++) {
lang = langs[i];
if (validLangs.indexOf(lang) >= 0) {
$translate.use(lang);
break;
}
}
})
$scope.$on("$locationChangeSuccess", function () {
var lang = $location.search().lang;
if (lang) {
@@ -60,53 +117,136 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
'touch': 'asterisk',
}
function getSucceeded() {
if (!getOK) {
$scope.init();
$('#networkError').modal('hide');
getOK = true;
}
if (restarting) {
$scope.init();
$('#restarting').modal('hide');
$('#shutdown').modal('hide');
restarting = false;
}
}
$scope.$on('UIOnline', function (event, arg) {
console.log('UIOnline');
$scope.init();
restarting = false;
$('#networkError').modal('hide');
$('#restarting').modal('hide');
$('#shutdown').modal('hide');
});
function getFailed() {
if (restarting) {
return;
}
if (getOK) {
$scope.$on('UIOffline', function (event, arg) {
console.log('UIOffline');
if (!restarting) {
$('#networkError').modal({backdrop: 'static', keyboard: false});
getOK = false;
}
});
$scope.$on('StateChanged', function (event, arg) {
var data = arg.data;
if ($scope.model[data.repo]) {
$scope.model[data.repo].state = data.to;
}
});
$scope.$on('LocalIndexUpdated', function (event, arg) {
var data = arg.data;
refreshRepo(data.repo);
// Update completion status for all nodes that we share this repo with.
$scope.repos[data.repo].Nodes.forEach(function (nodeCfg) {
refreshCompletion(nodeCfg.NodeID, data.repo);
});
});
$scope.$on('RemoteIndexUpdated', function (event, arg) {
var data = arg.data;
refreshRepo(data.repo);
refreshCompletion(data.node, data.repo);
});
$scope.$on('NodeDisconnected', function (event, arg) {
delete $scope.connections[arg.data.id];
});
$scope.$on('NodeConnected', function (event, arg) {
if (!$scope.connections[arg.data.id]) {
$scope.connections[arg.data.id] = {
inbps: 0,
outbps: 0,
InBytesTotal: 0,
OutBytesTotal: 0,
Address: arg.data.addr,
};
}
});
$scope.$on('ConfigLoaded', function (event) {
if ($scope.config.Options.URAccepted == 0) {
// If usage reporting has been neither accepted nor declined,
// we want to ask the user to make a choice. But we don't want
// to bug them during initial setup, so we set a cookie with
// the time of the first visit. When that cookie is present
// and the time is more than four hours ago, we ask the
// question.
var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
if (!firstVisit) {
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30*24*3600;
} else {
if (+firstVisit < Date.now() - 4*3600*1000){
$('#ur').modal({backdrop: 'static', keyboard: false});
}
}
}
})
var debouncedFuncs = {};
function refreshRepo(repo) {
var key = "refreshRepo" + repo;
if (!debouncedFuncs[key]) {
debouncedFuncs[key] = debounce(function () {
$http.get(urlbase + '/model?repo=' + encodeURIComponent(repo)).success(function (data) {
$scope.model[repo] = data;
console.log("refreshRepo", repo, data);
});
}, 1000, true);
}
debouncedFuncs[key]();
}
$scope.refresh = function () {
function refreshSystem() {
$http.get(urlbase + '/system').success(function (data) {
getSucceeded();
$scope.myID = data.myID;
$scope.system = data;
}).error(function () {
getFailed();
console.log("refreshSystem", data);
});
Object.keys($scope.repos).forEach(function (id) {
if (typeof $scope.model[id] === 'undefined') {
// Never fetched before
$http.get(urlbase + '/model?repo=' + encodeURIComponent(id)).success(function (data) {
$scope.model[id] = data;
});
} else {
$http.get(urlbase + '/model/version?repo=' + encodeURIComponent(id)).success(function (data) {
if (data.version > $scope.model[id].version) {
$http.get(urlbase + '/model?repo=' + encodeURIComponent(id)).success(function (data) {
$scope.model[id] = data;
});
}
function refreshCompletion(node, repo) {
if (node === $scope.myID) {
return
}
var key = "refreshCompletion" + node + repo;
if (!debouncedFuncs[key]) {
debouncedFuncs[key] = debounce(function () {
$http.get(urlbase + '/completion?node=' + node + '&repo=' + encodeURIComponent(repo)).success(function (data) {
if (!$scope.completion[node]) {
$scope.completion[node] = {};
}
$scope.completion[node][repo] = data.completion;
var tot = 0, cnt = 0;
for (var cmp in $scope.completion[node]) {
if (cmp === "_total") {
continue;
}
tot += $scope.completion[node][cmp];
cnt += 1;
}
$scope.completion[node]._total = tot / cnt;
console.log("refreshCompletion", node, repo, $scope.completion[node]);
});
}
});
}, 1000, true);
}
debouncedFuncs[key]();
}
function refreshConnectionStats() {
$http.get(urlbase + '/connections').success(function (data) {
var now = Date.now(),
td = (now - prevDate) / 1000,
@@ -126,29 +266,83 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
}
}
$scope.connections = data;
console.log("refreshConnections", data);
});
}
function refreshErrors() {
$http.get(urlbase + '/errors').success(function (data) {
$scope.errors = data;
console.log("refreshErrors", data);
});
}
function refreshConfig() {
$http.get(urlbase + '/config').success(function (data) {
var hasConfig = !isEmptyObject($scope.config);
$scope.config = data;
$scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(', ');
$scope.nodes = $scope.config.Nodes;
$scope.nodes.sort(nodeCompare);
$scope.repos = repoMap($scope.config.Repositories);
Object.keys($scope.repos).forEach(function (repo) {
refreshRepo(repo);
$scope.repos[repo].Nodes.forEach(function (nodeCfg) {
refreshCompletion(nodeCfg.NodeID, repo);
});
});
if (!hasConfig) {
$scope.$emit('ConfigLoaded');
}
console.log("refreshConfig", data);
});
$http.get(urlbase + '/config/sync').success(function (data) {
$scope.configInSync = data.configInSync;
});
}
$scope.init = function() {
refreshSystem();
refreshConfig();
refreshConnectionStats();
$http.get(urlbase + '/version').success(function (data) {
$scope.version = data;
});
$http.get(urlbase + '/report').success(function (data) {
$scope.reportData = data;
});
$http.get(urlbase + '/upgrade').success(function (data) {
$scope.upgradeInfo = data;
}).error(function () {
$scope.upgradeInfo = {};
});
};
$scope.refresh = function () {
refreshSystem();
refreshConnectionStats();
refreshErrors();
};
$scope.repoStatus = function (repo) {
if (typeof $scope.model[repo] === 'undefined') {
return 'Unknown';
return 'unknown';
}
if ($scope.model[repo].invalid !== '') {
return 'Stopped';
return 'stopped';
}
var state = '' + $scope.model[repo].state;
state = state[0].toUpperCase() + state.substr(1);
if (state == "Syncing" || state == "Idle") {
state += " (" + $scope.syncPercentage(repo) + "%)";
}
return state;
return '' + $scope.model[repo].state;
};
$scope.repoClass = function (repo) {
@@ -182,23 +376,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
return Math.floor(pct);
};
$scope.nodeStatus = function (nodeCfg) {
var conn = $scope.connections[nodeCfg.NodeID];
if (conn) {
if (conn.Completion === 100) {
return 'Up to Date';
} else {
return 'Syncing (' + conn.Completion + '%)';
}
}
return 'Disconnected';
};
$scope.nodeIcon = function (nodeCfg) {
var conn = $scope.connections[nodeCfg.NodeID];
if (conn) {
if (conn.Completion === 100) {
if ($scope.connections[nodeCfg.NodeID]) {
if ($scope.completion[nodeCfg.NodeID] && $scope.completion[nodeCfg.NodeID]._total === 100) {
return 'ok';
} else {
return 'refresh';
@@ -209,9 +389,8 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
};
$scope.nodeClass = function (nodeCfg) {
var conn = $scope.connections[nodeCfg.NodeID];
if (conn) {
if (conn.Completion === 100) {
if ($scope.connections[nodeCfg.NodeID]) {
if ($scope.completion[nodeCfg.NodeID] && $scope.completion[nodeCfg.NodeID]._total === 100) {
return 'success';
} else {
return 'primary';
@@ -381,7 +560,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
};
$scope.addNode = function () {
$scope.currentNode = {AddressesStr: 'dynamic'};
$scope.currentNode = {AddressesStr: 'dynamic', Compression: true};
$scope.editingExisting = false;
$scope.editingSelf = false;
$scope.nodeEditor.$setPristine();
@@ -561,60 +740,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
cfg.APIKey = randomString(30, 32);
};
$scope.init = function() {
$http.get(urlbase + '/version').success(function (data) {
$scope.version = data;
});
$http.get(urlbase + '/system').success(function (data) {
$scope.system = data;
$scope.myID = data.myID;
});
$http.get(urlbase + '/config').success(function (data) {
$scope.config = data;
$scope.config.Options.ListenStr = $scope.config.Options.ListenAddress.join(', ');
$scope.nodes = $scope.config.Nodes;
$scope.nodes.sort(nodeCompare);
$scope.repos = repoMap($scope.config.Repositories);
$scope.refresh();
if ($scope.config.Options.URAccepted == 0) {
// If usage reporting has been neither accepted nor declined,
// we want to ask the user to make a choice. But we don't want
// to bug them during initial setup, so we set a cookie with
// the time of the first visit. When that cookie is present
// and the time is more than four hours ago, we ask the
// question.
var firstVisit = document.cookie.replace(/(?:(?:^|.*;\s*)firstVisit\s*\=\s*([^;]*).*$)|^.*$/, "$1");
if (!firstVisit) {
document.cookie = "firstVisit=" + Date.now() + ";max-age=" + 30*24*3600;
} else {
if (+firstVisit < Date.now() - 4*3600*1000){
$('#ur').modal({backdrop: 'static', keyboard: false});
}
}
}
});
$http.get(urlbase + '/config/sync').success(function (data) {
$scope.configInSync = data.configInSync;
});
$http.get(urlbase + '/report').success(function (data) {
$scope.reportData = data;
});
$http.get(urlbase + '/upgrade').success(function (data) {
$scope.upgradeInfo = data;
}).error(function () {
$scope.upgradeInfo = {};
});
};
$scope.acceptUR = function () {
$scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
@@ -726,6 +852,48 @@ function randomString(len, bits)
return outStr.toLowerCase();
}
function isEmptyObject(obj) {
var name;
for (name in obj) {
return false;
}
return true;
}
function debounce(func, wait) {
var timeout, args, context, timestamp, result, again;
var later = function() {
var last = Date.now() - timestamp;
if (last < wait) {
timeout = setTimeout(later, wait - last);
} else {
timeout = null;
if (again) {
result = func.apply(context, args);
context = args = null;
again = false;
}
}
};
return function() {
context = this;
args = arguments;
timestamp = Date.now();
var callNow = !timeout;
if (!timeout) {
timeout = setTimeout(later, wait);
result = func.apply(context, args);
context = args = null;
} else {
again = true;
}
return result;
};
}
syncthing.filter('natural', function () {
return function (input, valid) {
return input.toFixed(decimals(input, valid));

View File

@@ -89,6 +89,7 @@
</head>
<body>
<div ng-controller="EventCtrl"></div>
<!-- Top bar -->
@@ -153,7 +154,19 @@
<h3 class="panel-title">
<a data-toggle="collapse" data-parent="#repositories" href="#repo-{{$index}}">
<span class="glyphicon glyphicon-hdd"></span> {{repo.Directory | shortPath}}
<span class="pull-right hidden-xs">{{repoStatus(repo.ID)}}</span>
<span class="pull-right hidden-xs" ng-switch="repoStatus(repo.ID)">
<span translate ng-switch-when="unknown">Unknown</span>
<span translate ng-switch-when="stopped">Stopped</span>
<span translate ng-switch-when="scanning">Scanning</span>
<span ng-switch-when="syncing">
<span translate>Syncing</span>
({{syncPercentage(repo.ID)}}%)
</span>
<span ng-switch-when="idle">
<span translate>Idle</span>
({{syncPercentage(repo.ID)}}%)
</span>
</span>
</a>
</h3>
</div>
@@ -174,10 +187,6 @@
<th><span class="glyphicon glyphicon-warning-sign"></span>&emsp;<span translate>Error</span></th>
<td class="text-right">{{model[repo.ID].invalid}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-comment"></span>&emsp;<span translate>Synchronization</span></th>
<td class="text-right">{{repoStatus(repo.ID)}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-globe"></span>&emsp;<span translate>Global Repository</span></th>
<td class="text-right">{{model[repo.ID].globalFiles | alwaysNumber}} <span translate>items</span>, {{model[repo.ID].globalBytes | binary}}B</td>
@@ -280,7 +289,15 @@
<a data-toggle="collapse" data-parent="#nodes" href="#node-{{$index}}">
<span class="glyphicon glyphicon-retweet"></span>
{{nodeName(nodeCfg)}}
<span class="pull-right hidden-xs">{{nodeStatus(nodeCfg)}}</span>
<span class="pull-right hidden-xs">
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total == 100">
<span translate>Up to Date</span> (100%)
</span>
<span ng-if="connections[nodeCfg.NodeID] && completion[nodeCfg.NodeID]._total < 100">
<span translate>Syncing</span> ({{completion[nodeCfg.NodeID]._total | number:0}}%)
</span>
<span translate ng-if="!connections[nodeCfg.NodeID]">Disconnected</span>
</span>
</a>
</h3>
</div>
@@ -295,7 +312,12 @@
</tr>
<tr>
<th><span class="glyphicon glyphicon-comment"></span>&emsp;<span translate>Synchronization</span></th>
<td class="text-right">{{nodeStatus(nodeCfg)}}</td>
<td class="text-right">{{completion[nodeCfg.NodeID]._total | alwaysNumber | number:0}}%</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-compressed"></span>&emsp;<span translate>Use Compression</span></th>
<td translate ng-if="nodeCfg.Compression" class="text-right">Yes</td>
<td translate ng-if="!nodeCfg.Compression" class="text-right">No</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;<span translate>Download Rate</span></th>
@@ -412,6 +434,13 @@
<input placeholder="dynamic" ng-disabled="currentNode.NodeID == myID" id="addresses" class="form-control" type="text" ng-model="currentNode.AddressesStr"></input>
<p translate class="help-block">Enter comma separated "ip:port" addresses or "dynamic" to perform automatic discovery of the address.</p>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentNode.Compression"> <span translate>Use Compression</span>
</label>
</div>
</div>
</form>
</div>
<div class="modal-footer">
@@ -686,6 +715,7 @@
<li>Aaron Bieber</li>
<li>Andrew Dunham</li>
<li>Arthur Axel fREW Schmidt</li>
<li>Audrius Butkevicius</li>
<li>Ben Sidhom</li>
<li>Brandon Philips</li>
</ul>

112
gui/lang-de.json Normal file
View File

@@ -0,0 +1,112 @@
{
"API Key": "API-Schlüssel",
"About": "Über",
"Add Node": "Knoten hinzufügen",
"Add Repository": "Verzeichnis hinzufügen",
"Address": "Adresse",
"Addresses": "Adressen",
"Allow Anonymous Usage Reporting?": "Übertragung von anonymen Nutzungsstatistiken erlauben?",
"Announce Server": "Ankündigungs-Server",
"Anonymous Usage Reporting": "Anonymer Nutzungsbericht",
"Bugs": "Fehler",
"CPU Utilization": "Prozessorauslastung",
"Close": "Schließen",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg und folgende Unterstützer:",
"Delete": "Löschen",
"Disconnected": "Verbindung getrennt",
"Documentation": "Dokumentation",
"Download Rate": "Downloadgeschwindigkeit",
"Edit": "Bearbeiten",
"Edit Node": "Knoten bearbeiten",
"Edit Repository": "Verzeichnis ändern",
"Enable UPnP": "Aktiviere UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Trage durch ein Komma getrennte \"IP:Port\" Adressen oder \"dynamic\" ein um automatische Adresserkennung durchzuführen.",
"Error": "Fehler",
"File Versioning": "Dateiversionierung",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Dateizugriffsrechte beim Suchen nach Veränderungen ignorieren. Bei FAT-Dateisystemen verwenden.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Dateien werden beim Löschen oder Ersetzen als datierte Versionen in einen .stversions -Ordner verschoben.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Dateien sind vor Veränderung durch andere Knoten geschützt, auf diesem Knoten durchgeführte Veränderungen werden aber auf den Rest des Netzwerks übertragen.",
"Folder": "Ordner",
"GUI Authentication Password": "Passwort für Zugang zur Benutzeroberfläche",
"GUI Authentication User": "Nutzername für Zugang zur Benutzeroberfläche.",
"GUI Listen Addresses": "Adresse(n) für die Benutzeroberfläche",
"Generate": "Generiere",
"Global Discovery": "Globale Auffindung",
"Global Repository": "Globales Verzeichnis",
"Idle": "Untätig",
"Ignore Permissions": "Berechtigungen ignorieren",
"Keep Versions": "Versionen erhalten",
"Latest Release": "Letzte Veröffentlichung",
"Local Discovery": "Lokale Auffindung",
"Local Discovery Port": "Lokaler Aufindungsport",
"Local Repository": "Lokales Verzeichnis",
"Master Repo": "Veränderungen zugelassen",
"Max File Change Rate (KiB/s)": "Maximale Datenänderungsrate (KiB/s)",
"Max Outstanding Requests": "Max. ausstehende Anfragen",
"No": "Nein",
"Node ID": "Knoten-ID",
"Node Name": "Knoten-Name",
"Notice": "Benachrichtigung",
"OK": "Ok",
"Offline": "Offline",
"Online": "Online",
"Out Of Sync": "Nicht synchronisiert",
"Outgoing Rate Limit (KiB/s)": "Ausgehendes Datenratelimit (KiB/s)",
"Override Changes": "Änderungen überschreiben",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Pfad zum Verzeichnis auf dem lokalen Rechner. Wird erzeugt, wenn es nicht existiert. Das Tilden-Zeichen (~) kann als Abkürzung benutzt werden für",
"Please wait": "Bitte warten",
"Preview Usage Report": "Vorschau des Nutzungsberichts",
"RAM Utilization": "Verwendeter Arbeitsspeicher",
"Reconnect Interval (s)": "Wiederverbindungsintervall (s)",
"Repository ID": "Verzeichnis-ID",
"Repository Master": "Keine Veränderungen zulassen",
"Repository Path": "Pfad zum Verzeichnis",
"Rescan Interval (s)": "Suchintervall (s)",
"Restart": "Neustart",
"Restart Needed": "Neustart notwendig",
"Save": "Speichern",
"Scanning": "Überprüfe",
"Select the nodes to share this repository with.": "Wähle die Knoten aus, mit denen du dieses Verzeichnis teilen willst.",
"Settings": "Einstellungen",
"Share With Nodes": "Teile mit diesen Knoten",
"Shared With": "Geteilt mit",
"Short identifier for the repository. Must be the same on all cluster nodes.": "Kurze ID für das Verzeichnis. Muss auf allen Verbunds-Knoten gleich sein.",
"Show ID": "ID anzeigen",
"Shown instead of Node ID in the cluster status.": "Wird anstatt der Knoten-ID im Verbunds-Status angezeigt.",
"Shutdown": "Herunterfahren",
"Source Code": "Quellcode",
"Start Browser": "Starte Browser",
"Stopped": "Gestoppt",
"Support / Forum": "Support / Forum",
"Sync Protocol Listen Addresses": "Adresse(n) für das Synchronisierungsprotokoll",
"Synchronization": "Synchronisierung",
"Syncing": "Synchronisiere",
"Syncthing has been shut down.": "Syncthing wurde heruntergefahren.",
"Syncthing includes the following software or portions thereof:": "Syncthing enthält die folgende Software oder Teile davon:",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing scheint nicht erreichbar zu sein oder es gibt ein Problem mit Ihrer Internetverbindung. Versuche erneut...",
"The aggregated statistics are publicly available at {{url}}": "Die aggregierten Statistiken sind öffentlich verfügbar unter {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Die Konfiguration wurde gespeichert, aber nicht aktiviert. Syncthing muss neugestartet werden um die neue Konfiguration zu aktivieren.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Der verschlüsselte Benutzungsbericht wird täglich gesendet. Er wird benutzt um Statistiken über verwendete Betriebssysteme, Verzeichnis-Größen und Programm-Versionen zu erstellen. Sobald der Bericht in Zukunft weitere Daten erfasst, wird dir dieses Fenster erneut angezeigt.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Die eingegebene Knoten-ID scheint nicht gültig zu sein. Sie sollte eine 52 Stellen lange Zeichenkette aus Buchstaben und Zahlen sein. Leerzeichen und Striche sind optional (werden ignoriert).",
"The node ID cannot be blank.": "Die Knoten-ID darf nicht leer sein.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Die hier einzutragende Knoten-ID kann im \"Bearbeiten > Zeige ID\"-Dialog auf dem anderen Knoten gefunden werden. Leerzeichen und Striche sind optional (werden ignoriert).",
"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 sein und darf nicht leer sein.",
"The repository ID cannot be blank.": "Die Verzeichnis-ID darf nicht leer sein.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Die Verzeichnis-ID muss eine kurze Kennung (64 Zeichen oder weniger) sein. Sie kann aus Buchstaben, Zahlen und den Punkt- (.), Strich- (-), und Unterstrich- (_) Zeichen bestehen.",
"The repository ID must be unique.": "Die Verzeichnis-ID muss eindeutig sein.",
"The repository path cannot be blank.": "Der Verzeichnis-Pfad kann nicht leer sein",
"Unknown": "Unbekannt",
"Up to Date": "Aktuell",
"Upgrade To {%version%}": "Upgrade auf {{version}}",
"Upload Rate": "Uploadgeschwindigkeit",
"Usage": "Benutzung",
"Use Compression": "Benutze Komprimierung",
"Use HTTPS for GUI": "Benutze HTTPS für Benutzeroberfläche",
"Version": "Version",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Beachte beim Hinzufügen eines neuen Knotens, dass dieser Knoten auch auf der Gegenseite hinzugefügt werden muss.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Beim Hinzufügen eines neuen Verzeichnisses, beachte dass die Verzeichnis-ID dazu verwendet wird, Verzeichnisse zwischen Knoten zu verbinden. Die ID muss also auf allen Knoten gleich sein, Groß- und Kleinschreibung muss dabei beachtet werden.",
"Yes": "Ja",
"You must keep at least one version.": "Du musst mindestens eine Version behalten.",
"items": "Einträge"
}

View File

@@ -13,6 +13,7 @@
"Close": "Close",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:",
"Delete": "Delete",
"Disconnected": "Disconnected",
"Documentation": "Documentation",
"Download Rate": "Download Rate",
"Edit": "Edit",
@@ -32,6 +33,7 @@
"Generate": "Generate",
"Global Discovery": "Global Discovery",
"Global Repository": "Global Repository",
"Idle": "Idle",
"Ignore Permissions": "Ignore Permissions",
"Keep Versions": "Keep Versions",
"Latest Release": "Latest Release",
@@ -63,6 +65,7 @@
"Restart": "Restart",
"Restart Needed": "Restart Needed",
"Save": "Save",
"Scanning": "Scanning",
"Select the nodes to share this repository with.": "Select the nodes to share this repository with.",
"Settings": "Settings",
"Share With Nodes": "Share With Nodes",
@@ -73,9 +76,11 @@
"Shutdown": "Shutdown",
"Source Code": "Source Code",
"Start Browser": "Start Browser",
"Stopped": "Stopped",
"Support / Forum": "Support / Forum",
"Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses",
"Synchronization": "Synchronization",
"Syncing": "Syncing",
"Syncthing has been shut down.": "Syncthing has been shut down.",
"Syncthing includes the following software or portions thereof:": "Syncthing includes the following software or portions thereof:",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…",
@@ -91,9 +96,12 @@
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.",
"The repository ID must be unique.": "The repository ID must be unique.",
"The repository path cannot be blank.": "The repository path cannot be blank.",
"Unknown": "Unknown",
"Up to Date": "Up to Date",
"Upgrade To {%version%}": "Upgrade To {{version}}",
"Upload Rate": "Upload Rate",
"Usage": "Usage",
"Use Compression": "Use Compression",
"Use HTTPS for GUI": "Use HTTPS for GUI",
"Version": "Version",
"When adding a new node, keep in mind that this node must be added on the other side too.": "When adding a new node, keep in mind that this node must be added on the other side too.",
@@ -101,4 +109,4 @@
"Yes": "Yes",
"You must keep at least one version.": "You must keep at least one version.",
"items": "items"
}
}

112
gui/lang-es.json Normal file
View File

@@ -0,0 +1,112 @@
{
"API Key": "Clave API",
"About": "Acerca de",
"Add Node": "Añadir nodo",
"Add Repository": "Añadir repositorio",
"Address": "Dirección",
"Addresses": "Direcciones",
"Allow Anonymous Usage Reporting?": "Permitir reporte anónimo de uso?",
"Announce Server": "Anunciar servidor",
"Anonymous Usage Reporting": "Reporte anónimo de uso",
"Bugs": "Errores",
"CPU Utilization": "Uso de la CPU",
"Close": "Cerrar",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Derechos de autor © 2014 Jakob Borg y los siguientes colaboradores:",
"Delete": "Suprimir",
"Disconnected": "Desconectado",
"Documentation": "Documentación",
"Download Rate": "Tasa de descarga",
"Edit": "Editar",
"Edit Node": "Editar nodo",
"Edit Repository": "Editar repositorio",
"Enable UPnP": "Permitir UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Ingrese las direcciones \"ip:port\" separadas por coma o \"dynamic\" para descubrir automáticamente las direcciones.",
"Error": "Error",
"File Versioning": "Control de versiones",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Los permisos de archivo son ignorados al buscar cambios. Usar en sistemas de archivos FAT.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Los archivos son movidos al directorio .stversions y renombrados a versiones marcadas por fecha cuando son reemplazados o eliminados por Syncthing,",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Los archivos están protegidos de cambios en otros nodos, pero los cambios realizados en este nodo serán enviados al resto de los nodos.",
"Folder": "Carpeta",
"GUI Authentication Password": "Contraseña de autenticación de la GUI",
"GUI Authentication User": "Usuario de la GUI",
"GUI Listen Addresses": "Direcciones de escucha para la GUI.",
"Generate": "Generar",
"Global Discovery": "Búsqueda en internet",
"Global Repository": "Repositorio global",
"Idle": "Inactivo",
"Ignore Permissions": "Ignorar permisos",
"Keep Versions": "Conservar versiones",
"Latest Release": "Última versión",
"Local Discovery": "Búsqueda en red local",
"Local Discovery Port": "Puerto de búsqueda de red local",
"Local Repository": "Repositorio local.",
"Master Repo": "Repositorio maestro",
"Max File Change Rate (KiB/s)": "Tasa máxima de cambios (KiB/s)",
"Max Outstanding Requests": "Cantidad máxima de peticiones pendientes",
"No": "No",
"Node ID": "Nodo ID",
"Node Name": "Nodo nombre",
"Notice": "Aviso",
"OK": "OK",
"Offline": "Fuera de linea",
"Online": "En linea",
"Out Of Sync": "Fuera de sincronización",
"Outgoing Rate Limit (KiB/s)": "Tasa máxima de envío (KiB/s)",
"Override Changes": "Reemplazar los cambios",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Ruta del repositorio en el equipo local. La carpeta sera creada si no existe. El carácter tilde (~) puede ser utilizado como atajo de ",
"Please wait": "Aguarde por favor",
"Preview Usage Report": "Ver reporte de uso",
"RAM Utilization": "Utilización de RAM",
"Reconnect Interval (s)": "Intervalo de reconexión (s)",
"Repository ID": "ID de repositorio",
"Repository Master": "Repositorio maestro",
"Repository Path": "Ruta del repositorio",
"Rescan Interval (s)": "Intervalo de reescaneo (s)",
"Restart": "Reiniciar",
"Restart Needed": "Es necesario reiniciar",
"Save": "Guardar",
"Scanning": "Actualización",
"Select the nodes to share this repository with.": "Seleccione los nodos con los cuales compartir el repositorio.",
"Settings": "Configuración",
"Share With Nodes": "Compartir con los nodos",
"Shared With": "Compartido con",
"Short identifier for the repository. Must be the same on all cluster nodes.": "Identificador corto para el repositorio. Debe ser el mismo en todos los nodos del clúster.",
"Show ID": "Mostrar ID",
"Shown instead of Node ID in the cluster status.": "Mostrar en lugar de ID de nodo en estado de cluster.",
"Shutdown": "Apagar",
"Source Code": "Código fuente",
"Start Browser": "Iniciar navegador",
"Stopped": "Parado",
"Support / Forum": "Soporte / Foro",
"Sync Protocol Listen Addresses": "Dirección de escucha del protocolo de sincronización",
"Synchronization": "Sincronización",
"Syncing": "Sincronización",
"Syncthing has been shut down.": "La sincronización esta apagada",
"Syncthing includes the following software or portions thereof:": "Syncthing incluye los siguientes softwares o partes de ellos:",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece estar apagado, o hay un problema con su conexión de Internet. Reintentando...",
"The aggregated statistics are publicly available at {{url}}": "Las estadísticas acumuladas están públicamente disponibles en {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido guardada pero no activada.\nSyncthing debe reiniciarse para activar la nueva configuración.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "El reporte de uso se envía encriptado diariamente. Se utiliza para hacer un seguimiento de plataformas comunes, tamaño de repositorios y versión de aplicaciones. Si el conjunto de datos cambia sera notificado mediante este dialogo nuevamente.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "El ID de nodo ingresado no es valido. Debe ser una cadena de al menos 52 caracteres consistente en letras y números, con espacios y guiones opcionales.",
"The node ID cannot be blank.": "El ID de nodo no puede estar vacío.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "El ID de nodo a ingresar aquí puede verse en la opción de menú \"Edición > Mostrar ID\" del otro nodo. Espacios y guiones son opcionales (ignorados).",
"The number of old versions to keep, per file.": "El numero de versiones anteriores a conservar, por archivo.",
"The number of versions must be a number and cannot be blank.": "El número de versiones debe ser un número y no puede estar vacío.",
"The repository ID cannot be blank.": "El ID de repositorio no puede estar vacio.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "El ID de repositorio debe ser una cadena corta (64 caracteres o menos) consistente solamente en letras, números, punto (.), guion (-) y guion bajo (_).",
"The repository ID must be unique.": "El ID de repositorio debe ser único.",
"The repository path cannot be blank.": "La ruta del repositorio no puede estar vacía.",
"Unknown": "Desconocido",
"Up to Date": "Actualizado",
"Upgrade To {%version%}": "Actualizar a {{version}}",
"Upload Rate": "Tasa de subida",
"Usage": "Utilización",
"Use Compression": "Use Compression",
"Use HTTPS for GUI": "Usar HTTPS para la GUI",
"Version": "Versión",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Al agregar un nuevo nodo, recuerde que este nodo debe ser agregado en el otro lado también.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Al agregar un nuevo repositorio, tenga en mente que el ID de repositorio se utiliza para ligar los repositorios entre nodos. Distingue mayúsculas y minúsculas y debe ser exactamente igual en todos los nodos.",
"Yes": "Si",
"You must keep at least one version.": "Debe mantener al menos una versión",
"items": "items"
}

112
gui/lang-fr.json Normal file
View File

@@ -0,0 +1,112 @@
{
"API Key": "Clé API",
"About": "À propos",
"Add Node": "Ajouter un nœud",
"Add Repository": "Ajouter un répertoire",
"Address": "Adresse",
"Addresses": "Adresses",
"Allow Anonymous Usage Reporting?": "Autoriser le rapport anonyme de statistiques d'utilisation?",
"Announce Server": "Serveur d'annonce",
"Anonymous Usage Reporting": "Rapport anonyme de statistiques d'utilisation",
"Bugs": "Bugs",
"CPU Utilization": "Utilisation du CPU",
"Close": "Fermer",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg et les contributeurs suivants:",
"Delete": "Supprimer",
"Disconnected": "Déconnecté",
"Documentation": "Documentation",
"Download Rate": "Débit Download",
"Edit": "Éditer",
"Edit Node": "Éditer le nœud",
"Edit Repository": "Éditer le répertoire",
"Enable UPnP": "Activer l'UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Entrer les adresses \"ip:port\" séparées par une virgule ou \"dynamic\" afin d'activer la recherche automatique de l'adresse.",
"Error": "Erreur",
"File Versioning": "Versions de fichier",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Les permissions de fichier sont ignorées lors de la recherche de changements. À utiliser sur les systèmes de fichiers en FAT.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Les fichiers sont datés et déplacés dans le dossier .stversions lors de leurs remplacements ou suppressions par syncthing.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Les fichiers sont protégés des changements réalisés sur les autres nœuds, mais les changements réalisés sur ce nœud seront transférés au reste du cluster.",
"Folder": "Dossier",
"GUI Authentication Password": "Mot de passe d'authentification GUI",
"GUI Authentication User": "Utilistateur autorisé GUI",
"GUI Listen Addresses": "Adresse du GUI",
"Generate": "Générer",
"Global Discovery": "Recherche globale",
"Global Repository": "Répertoire global",
"Idle": "Au repos",
"Ignore Permissions": "Ignorer les permissions",
"Keep Versions": "Conserver les versions",
"Latest Release": "Dernière version",
"Local Discovery": "Recherche locale",
"Local Discovery Port": "Port de recherche locale",
"Local Repository": "Dossier local",
"Master Repo": "Dossier maître",
"Max File Change Rate (KiB/s)": "Débit maximum de changement de fichier (KiB/s)",
"Max Outstanding Requests": "Nombre maximum de demandes conccurentes de blocs de fichier",
"No": "Non",
"Node ID": "ID du nœud",
"Node Name": "Nom du nœud",
"Notice": "Notification",
"OK": "OK",
"Offline": "Offline",
"Online": "Online",
"Out Of Sync": "Non synchronisé",
"Outgoing Rate Limit (KiB/s)": "Limite du débit sortant (KiB/s)",
"Override Changes": "Écraser les changements",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Chemin du répertoire sur l'ordinateur local. Il sera créé si il n'existe pas. Le caractère tilde (~) peut être utilisé comme raccourci vers",
"Please wait": "Merci de patienter",
"Preview Usage Report": "Aperçu du rapport de statistiques d'utilisation",
"RAM Utilization": "Utilisation de la RAM",
"Reconnect Interval (s)": "Intervalle de reconnexion (s)",
"Repository ID": "ID du répertoire",
"Repository Master": "Répertoire maître",
"Repository Path": "Chemin du répertoire",
"Rescan Interval (s)": "Intervalle de rescan (s)",
"Restart": "Redémarrer",
"Restart Needed": "Redémarrage nécessaire",
"Save": "Sauver",
"Scanning": "Scanning",
"Select the nodes to share this repository with.": "Sélectionner les nœuds qui partageront ce répertoire.",
"Settings": "Configuration",
"Share With Nodes": "Partager avec les nœuds",
"Shared With": "Partagé avec",
"Short identifier for the repository. Must be the same on all cluster nodes.": "Identifiant court pour le répertoire. Il doit être le même sur l'ensemble des nœuds du cluster.",
"Show ID": "Montrer l'ID",
"Shown instead of Node ID in the cluster status.": "Affiché à la place de l'ID du nœud au sein du cluster.",
"Shutdown": "Éteindre",
"Source Code": "Code source",
"Start Browser": "Démarrer le navigateur web",
"Stopped": "Arrêté",
"Support / Forum": "Aide / Forum",
"Sync Protocol Listen Addresses": "Adresse du protocole de synchronisation",
"Synchronization": "Synchronisation",
"Syncing": "En cours de synchronisation",
"Syncthing has been shut down.": "Syncthing a été éteint.",
"Syncthing includes the following software or portions thereof:": "Syncthing inclut les logiciels, ou portion de ceux-ci, suivants:",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être éteint, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
"The aggregated statistics are publicly available at {{url}}": "Les statistiques agrégées sont disponibles publiquement à l'adresse {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été sauvée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Le rapport d'utilisation chiffré est envoyé quotidiennement. Il sert à répertorier les plateformes utilisées, la taille des répertoires et les versions de l'application. Si le jeu de données rapportées devait être changé, il vous sera demandé de le valider de nouveau via ce dialogue.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "L'ID du nœud ne semble pas être valide. Il devrait ressembler à une chaine de 52 caractères comprenant lettres et chiffres, avec des espaces et des traits d'union optionnels.",
"The node ID cannot be blank.": "L'ID du nœud ne peut être vide.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "L'ID du nœud à insérer peut être trouvé à travers le menu \"Éditer > Montrer l'ID\" des autres nœuds. Les espaces et les traits d'union sont optionnels (ils seront ignorés).",
"The number of old versions to keep, per file.": "Le nombre d'anciennes versions à garder, par fichier.",
"The number of versions must be a number and cannot be blank.": "Le nombre de version doit être un nombre et ne peut pas être vide.",
"The repository ID cannot be blank.": "L'ID du répertoire ne peut être vide.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "L'ID du répertoire doit un identifiant court (64 caractères ou moins) comprenant des lettres, nombres, points (.), trait d'union (-) et tiret bas (_).",
"The repository ID must be unique.": "L'ID du répertoire doit être unique.",
"The repository path cannot be blank.": "Le chemin du répertoire ne peut pas être vide.",
"Unknown": "Inconnu",
"Up to Date": "Synchronistation à jour",
"Upgrade To {%version%}": "Upgrader vers {{version}}",
"Upload Rate": "Débit Upload",
"Usage": "Utilisation",
"Use Compression": "Utiliser la compression",
"Use HTTPS for GUI": "Utiliser l'HTTPS pour le GUI",
"Version": "Version",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Lorsqu'un nœud est ajouté, gardez à l'esprit que ce nœud doit aussi être ajouté de l'autre coté.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Lorsqu'un nouveau répertoire est ajouté, gardez à l'esprit que l'ID du répertoire est utilisé pour lier les répertoires à travers les nœuds. Ils sont sensibles à la case et doivent être semblables à travers tous les nœuds.",
"Yes": "Oui",
"You must keep at least one version.": "Vous devez garder au minimum une version.",
"items": "éléments"
}

112
gui/lang-pt.json Normal file
View File

@@ -0,0 +1,112 @@
{
"API Key": "Chave API",
"About": "Acerca de",
"Add Node": "Adicionar Nó",
"Add Repository": "Adicionar Repositório",
"Address": "Endereço",
"Addresses": "Endereços",
"Allow Anonymous Usage Reporting?": "Permitir Envio de Relatórios Anónimos?",
"Announce Server": "Servidor de anúncios",
"Anonymous Usage Reporting": "Envio de Relatórios Anónimos",
"Bugs": "Erros",
"CPU Utilization": "Utilização da CPU",
"Close": "Fechar",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Direitos Reservados © 2014 Jakob Borg e os seguintes Contribuidores:",
"Delete": "Apagar",
"Disconnected": "Desconectado",
"Documentation": "Documentação",
"Download Rate": "Taxa de transferência",
"Edit": "Editar",
"Edit Node": "Editar Nó",
"Edit Repository": "Editar Repositório",
"Enable UPnP": "Ativar UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Introduza endereços \"ip:porto\" separados por virgulas ou \"dynamic\" para o descobrimento automático do endereço.",
"Error": "Erro",
"File Versioning": "Gestão de versões",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "As permissões do ficheiro são ignoradas na pesquisa por mudanças. Utilize nos sistemas de ficheiros FAT.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Os ficheiros são movidos para versões carimbadas com o tempo numa pasta .stversions quando substituídos ou apagados por syncthing.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Os ficheiros são protegidos de mudanças feitas em outros nós, mas alterações feitas neste nó serão enviadas para o resto do agrupamento.",
"Folder": "Pasta",
"GUI Authentication Password": "Senha de Autenticação GUI",
"GUI Authentication User": "Utilizador de autenticação GUI",
"GUI Listen Addresses": "Endereço de escuta GUI",
"Generate": "Gerar",
"Global Discovery": "Descoberta Global",
"Global Repository": "Repositório Global",
"Idle": "Em espera",
"Ignore Permissions": "Ignorar Permissões",
"Keep Versions": "Manter Versões",
"Latest Release": "Última versão",
"Local Discovery": "Descoberta Local",
"Local Discovery Port": "Porto de Descoberta Local",
"Local Repository": "Repositório local",
"Master Repo": "Repositório Mestre",
"Max File Change Rate (KiB/s)": "Taxa máxima de troca de ficheiros (KiB/s)",
"Max Outstanding Requests": "Número máximo de pedidos pendentes",
"No": "Não",
"Node ID": "ID do Nó",
"Node Name": "Nome do Nó",
"Notice": "Nota",
"OK": "OK",
"Offline": "Desconectado",
"Online": "Conectado",
"Out Of Sync": "Não sincronizado",
"Outgoing Rate Limit (KiB/s)": "Limite da taxa de envio (KiB/s)",
"Override Changes": "Sobrepor Mudanças",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Caminho para o repositório no computador local. Será criado se não existir. O carácter (~) pode ser utilizado como atalho para",
"Please wait": "Aguarde",
"Preview Usage Report": "Visualização de Relatório",
"RAM Utilization": "Utilização da RAM",
"Reconnect Interval (s)": "Intervalo de reestabelecimento de ligação (s)",
"Repository ID": "ID do Repositório",
"Repository Master": "Repositório Mestre",
"Repository Path": "Caminho do Repositório",
"Rescan Interval (s)": "Intervalo de monitorização (s)",
"Restart": "Reiniciar",
"Restart Needed": "É preciso reiniciar",
"Save": "Gravar",
"Scanning": "Varrendo",
"Select the nodes to share this repository with.": "Selecione os nós com quais partilhar este repositório.",
"Settings": "Configurações",
"Share With Nodes": "Partilhar com Nós",
"Shared With": "Partilhado Com",
"Short identifier for the repository. Must be the same on all cluster nodes.": "Identificador curto para o repositório. Tem que ser igual em todos os nós do agrupamento.",
"Show ID": "Mostrar ID",
"Shown instead of Node ID in the cluster status.": "Mostrado ao invés do ID do Nó no estado do agrupamento.",
"Shutdown": "Desligar",
"Source Code": "Código Fonte",
"Start Browser": "Iniciar Navegador",
"Stopped": "Parado",
"Support / Forum": "Suporte / Fórum",
"Sync Protocol Listen Addresses": "Endereços de escuta do protocolo de sincronização",
"Synchronization": "Sincronização",
"Syncing": "Sincronizando",
"Syncthing has been shut down.": "Syncthing foi desligado.",
"Syncthing includes the following software or portions thereof:": "Syncthing inclui as seguintes aplicacoes ou partes delas:",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece estar em baixo, ou existe um problema com a sua ligação Internet. A tentar novamente...",
"The aggregated statistics are publicly available at {{url}}": "As estatísticas agrupadas estão disponíveis publicamente em {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A configuração foi gravada mas não activada. Syncthing tem que reiniciar para activar a nova configuração.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "O relatório de utilização cifrado é enviado diariamente. É utilizado para seguir plataformas comuns, tamanhos de repositórios e versões da aplicação. Se o conjunto de dados do relatório for alterado, será notificado novamente através desta janela.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "O ID do nó indicado não parece ser válido. Deveria conter uma palavra de 52 caracteres constituída por letras e números, com espaços e traços opcionais. ",
"The node ID cannot be blank.": "O ID do nó não pode ser vazio.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "O ID do nó a introduzir pode ser encontrado no diálogo \"Editar > Mostrar ID\" no outro nó. Espaços e traços são opcionais (ignorados).",
"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 ser vazio.",
"The repository ID cannot be blank.": "O ID do repositório não pode ser vazio.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "O ID do repositório tem que ser um identificador curto (64 carateres ou menos) e consiste em letras, números e os carateres ponto (.), traço (-) e (_).",
"The repository ID must be unique.": "O ID do repositório tem que ser único.",
"The repository path cannot be blank.": "O caminho do repositório não pode ser vazio.",
"Unknown": "Desconhecido",
"Up to Date": "Actualizado",
"Upgrade To {%version%}": "Atualizar para {{version}}",
"Upload Rate": "Taxa de envio",
"Usage": "Utilização",
"Use Compression": "Usar Compressão",
"Use HTTPS for GUI": "Utilizar HTTPS para GUI",
"Version": "Versão",
"When adding a new node, keep in mind that this node must be added on the other side too.": "Quando adicionar um novo nó, lembre-se que este nó tem que ser adicionado do outro lado também.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "Quando adicionar um novo repositório, lembre-se que o ID do Repositório é utilizado para juntar os repositórios entre nós. São sensíveis às maiúsculas e minúsculas e tem que corresponder exactamente entre todos os nós.",
"Yes": "Sim",
"You must keep at least one version.": "Tem que manter pelo menos uma versão.",
"items": "itens"
}

112
gui/lang-sv.json Normal file
View File

@@ -0,0 +1,112 @@
{
"API Key": "API-nyckel",
"About": "Om",
"Add Node": "Lägg till nod",
"Add Repository": "Lägg till lagring",
"Address": "Adress",
"Addresses": "Adresser",
"Allow Anonymous Usage Reporting?": "Tillåt anonym användarstatistik?",
"Announce Server": "Uppslagningsserver",
"Anonymous Usage Reporting": "Anonym användarstatistik",
"Bugs": "Buggar",
"CPU Utilization": "CPU-utnyttjande",
"Close": "Stäng",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg och de följande medarbetarna:",
"Delete": "Radera",
"Disconnected": "Ej ansluten",
"Documentation": "Dokumentation",
"Download Rate": "Nerladdningshastighet",
"Edit": "Redigera",
"Edit Node": "Redigera nod",
"Edit Repository": "Redigera lagring",
"Enable UPnP": "Använd UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade \"ip:port\"-adresser eller ordet \"dynamic\" för att använda automatisk uppslagning.",
"Error": "Fel",
"File Versioning": "Versionshantering",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Filers rättighetsbitar tas inte hänsyn till vid synkronisering. Använd på FAT-filsystem.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "Filer flyttas till datumstämplade versioner i en .stversions-katalog när de blir uppdaterade eller raderade av syncthing.",
"Files are protected from changes made on other nodes, but changes made on this node will be sent to the rest of the cluster.": "Filer skyddas från ändringar gjorda på andra noder, men ändringar som görs på den här noden skickas till de andra klustermedlemmarna.",
"Folder": "Katalog",
"GUI Authentication Password": "GUI-lösenord",
"GUI Authentication User": "GUI-användare",
"GUI Listen Addresses": "GUI-address",
"Generate": "Skapa",
"Global Discovery": "Global uppslagning",
"Global Repository": "Global lagring",
"Idle": "Vilande",
"Ignore Permissions": "Ignorera filrättigheter",
"Keep Versions": "Behåll versioner",
"Latest Release": "Senaste version",
"Local Discovery": "Lokal uppslagning",
"Local Discovery Port": "Lokal uppslagningsport",
"Local Repository": "Lokal lagring",
"Master Repo": "Huvudlagring",
"Max File Change Rate (KiB/s)": "Högsta ändringshastighet (KiB/s)",
"Max Outstanding Requests": "Paralellitet",
"No": "Nej",
"Node ID": "Nod-ID",
"Node Name": "Nodnamn",
"Notice": "OBS",
"OK": "OK",
"Offline": "Ej tillgänglig",
"Online": "Tillgänglig",
"Out Of Sync": "Ur synk",
"Outgoing Rate Limit (KiB/s)": "Utgående hastighetsbegränsning (KiB/s)",
"Override Changes": "Skriv över ändringar",
"Path to the repository on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Sökväg till katalogen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
"Please wait": "Var god vänta",
"Preview Usage Report": "Förhandsgranska statistik",
"RAM Utilization": "Minnesutnyttjande",
"Reconnect Interval (s)": "Anslutningsintervall (s)",
"Repository ID": "Lagrings-ID",
"Repository Master": "Huvudlagring",
"Repository Path": "Lagringskatalog",
"Rescan Interval (s)": "Förnyelseintervall (s)",
"Restart": "Starta om",
"Restart Needed": "Omstart behövs",
"Save": "Spara",
"Scanning": "Uppdaterar",
"Select the nodes to share this repository with.": "Välj vilka noder förvaringen ska delas med.",
"Settings": "Inställningar",
"Share With Nodes": "Dela med noder",
"Shared With": "Delat med",
"Short identifier for the repository. Must be the same on all cluster nodes.": "Kort identifieringssträng för förvaringen. Måste vara samma på alla noder i klustern.",
"Show ID": "Visa ID",
"Shown instead of Node ID in the cluster status.": "Visas i stället för nod-ID",
"Shutdown": "Stäng av",
"Source Code": "Kälkod",
"Start Browser": "Starta browser",
"Stopped": "Stoppad",
"Support / Forum": "Support / Forum",
"Sync Protocol Listen Addresses": "Address för inkommande anslutningar",
"Synchronization": "Synkronisation",
"Syncing": "Synkroniserar",
"Syncthing has been shut down.": "Syncthing har stängts ner.",
"Syncthing includes the following software or portions thereof:": "Syncthing innehåller de följande mjukvarupaketen eller delar därav:",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar avstängd, eller så är där ett problem med din Internetanslutning. Försöker igen...",
"The aggregated statistics are publicly available at {{url}}": "Aggregerad statistik finns publikt tillgänglig på {{url}}",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
"The encrypted usage report is sent daily. It is used to track common platforms, repo sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Den krypterade användarstatistiken skickas dagligen. Den används för att spåra vanliga plattformar, lagringsstorlekar och versioner. Om datan som rapporteras ändras så kommer du att bli tillfrågad igen.",
"The entered node ID does not look valid. It should be a 52 character string consisting of letters and numbers, with spaces and dashes being optional.": "Det inmatade nod-ID:t verkar inte korrekt. Det ska vara en 52 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
"The node ID cannot be blank.": "Nod-ID:t kan inte vara blankt.",
"The node ID to enter here can be found in the \"Edit > Show ID\" dialog on the other node. Spaces and dashes are optional (ignored).": "Nod-ID:t som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra noden. Mellanrum och bindestreck är valfria (ignoreras).",
"The number of old versions to keep, per file.": "Antalet gamla versioner som ska behållas, per fil.",
"The number of versions must be a number and cannot be blank.": "Antalet versioner måste vara ett nummer och kan inte lämnas blankt.",
"The repository ID cannot be blank.": "Lagrings-ID kan inte vara blankt.",
"The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the dot (.), dash (-) and underscode (_) characters only.": "Lagrings-ID måste vara en kort identifierar (64 tecken eller mindre), bestående av endast bokstäver, siffror, punkt (.), bindestreck (-) och understräck (_).",
"The repository ID must be unique.": "Lagrings-ID måste vara unikt.",
"The repository path cannot be blank.": "Lagrings-ID kan inte lämnas blankt.",
"Unknown": "Okänt",
"Up to Date": "Helt uppdaterad",
"Upgrade To {%version%}": "Uppgradera till {{version}}",
"Upload Rate": "Uppladdningshastighet",
"Usage": "Användande",
"Use Compression": "Använd komprimering",
"Use HTTPS for GUI": "Använd HTTPS för GUI",
"Version": "Version",
"When adding a new node, keep in mind that this node must be added on the other side too.": "När du lägger till en ny nod, kom ihåg att den här noden måste läggas till på den andra noden också.",
"When adding a new repository, keep in mind that the Repository ID is used to tie repositories together between nodes. They are case sensitive and must match exactly between all nodes.": "När du lägger till ny lagring, tänk på att lagrings-ID knyter ihop lagringen mellan olika noder. De måste vara exakt desamma mellan noder, och stora eller små bokstäver har betydelse.",
"Yes": "Ja",
"You must keep at least one version.": "Du måste behålla åtminstone en version.",
"items": "poster"
}

View File

@@ -21,7 +21,7 @@
<localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
<parallelRequests>16</parallelRequests>
<maxSendKbps>1000</maxSendKbps>
<maxSendKbps>500</maxSendKbps>
<rescanIntervalS>10</rescanIntervalS>
<reconnectionIntervalS>5</reconnectionIntervalS>
<maxChangeKbps>10000</maxChangeKbps>

View File

@@ -10,7 +10,8 @@ import (
"crypto/rand"
"flag"
"fmt"
"io/ioutil"
"io"
"log"
mr "math/rand"
"os"
"path/filepath"
@@ -26,29 +27,73 @@ func name() string {
func main() {
var files int
var maxexp int
var srcname string
flag.IntVar(&files, "files", 1000, "Number of files")
flag.IntVar(&maxexp, "maxexp", 20, "Maximum file size (max = 2^n + 128*1024 B)")
flag.StringVar(&srcname, "src", "/usr/share/dict/words", "Source material")
flag.Parse()
fd, err := os.Open(srcname)
if err != nil {
log.Fatal(err)
}
for i := 0; i < files; i++ {
n := name()
p0 := filepath.Join(string(n[0]), n[0:2])
os.MkdirAll(p0, 0755)
err = os.MkdirAll(p0, 0755)
if err != nil {
log.Fatal(err)
}
s := 1 << uint(mr.Intn(maxexp))
a := 128 * 1024
if a > s {
a = s
}
s += mr.Intn(a)
b := make([]byte, s)
rand.Reader.Read(b)
p1 := filepath.Join(p0, n)
ioutil.WriteFile(p1, b, 0644)
os.Chmod(p1, os.FileMode(mr.Intn(0777)|0400))
src := io.LimitReader(&inifiteReader{fd}, int64(s))
p1 := filepath.Join(p0, n)
dst, err := os.Create(p1)
if err != nil {
log.Fatal(err)
}
_, err = io.Copy(dst, src)
if err != nil {
log.Fatal(err)
}
err = dst.Close()
if err != nil {
log.Fatal(err)
}
err = os.Chmod(p1, os.FileMode(mr.Intn(0777)|0400))
if err != nil {
log.Fatal(err)
}
t := time.Now().Add(-time.Duration(mr.Intn(30*86400)) * time.Second)
os.Chtimes(p1, t, t)
err = os.Chtimes(p1, t, t)
if err != nil {
log.Fatal(err)
}
}
}
type inifiteReader struct {
rd io.ReadSeeker
}
func (i *inifiteReader) Read(bs []byte) (int, error) {
n, err := i.rd.Read(bs)
if err == io.EOF {
err = nil
i.rd.Seek(0, 0)
}
return n, err
}

View File

@@ -1,36 +1,36 @@
<configuration version="2">
<repository id="default" directory="s1" ro="false" ignorePerms="false">
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA"></node>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
<node id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></node>
<node id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></node>
<node id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU"></node>
<versioning></versioning>
</repository>
<repository id="s12" directory="s12-1" ro="false" ignorePerms="false">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node>
<node id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></node>
<node id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></node>
<versioning></versioning>
</repository>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1">
<node id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="true">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2">
<node id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU" name="s2">
<address>127.0.0.1:22002</address>
</node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3">
<node id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3">
<address>127.0.0.1:22003</address>
</node>
<node id="EJHMPAQOGCVORISB4IS3SYYVJXTKJGLTU66DIQPGJ5D2GXGQ3OWQ" name="s4">
<node id="EJHMPAQ-OGCVORE-ISB4IS3-SYYVJXF-TKJGLTU-66DIQPF-GJ5D2GX-GQ3OWQK" name="s4">
<address>127.0.0.1:22004</address>
</node>
<gui enabled="true" tls="false">
<address>127.0.0.1:8081</address>
<apikey>abc123</apikey>
<user>testuser</user>
<password>testpass</password>
<password>$2a$10$7tKL5uvLDGn5s2VLPM2yWOK/II45az0mTel8hxAUJDRQN1Tk2QYwu</password>
<apikey>abc123</apikey>
</gui>
<options>
<listenAddress>127.0.0.1:22001</listenAddress>
<globalAnnounceServer>announce.syncthing.net:22025</globalAnnounceServer>
<globalAnnounceServer>announce.syncthing.net:22026</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
@@ -41,5 +41,6 @@
<maxChangeKbps>10000</maxChangeKbps>
<startBrowser>false</startBrowser>
<upnpEnabled>true</upnpEnabled>
<urAccepted>-1</urAccepted>
</options>
</configuration>

View File

@@ -18,10 +18,10 @@
<versioning></versioning>
<syncorder></syncorder>
</repository>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1" compression="true">
<address>127.0.0.1:22001</address>
</node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2">
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2" compression="true">
<address>127.0.0.1:22002</address>
</node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3">

View File

@@ -31,9 +31,9 @@ stop() {
testConvergence() {
while true ; do
sleep 5
s1comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8082/rest/connections" | ./json "$id1/Completion")
s2comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8083/rest/connections" | ./json "$id2/Completion")
s3comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8081/rest/connections" | ./json "$id3/Completion")
s1comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8082/rest/debug/peerCompletion" | ./json "$id1")
s2comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8083/rest/debug/peerCompletion" | ./json "$id2")
s3comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8081/rest/debug/peerCompletion" | ./json "$id3")
s1comp=${s1comp:-0}
s2comp=${s2comp:-0}
s3comp=${s3comp:-0}

View File

@@ -46,8 +46,8 @@ setup() {
testConvergence() {
while true ; do
sleep 5
s1comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8082/rest/connections" | ./json "$id1/Completion")
s2comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8081/rest/connections" | ./json "$id2/Completion")
s1comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8082/rest/debug/peerCompletion" | ./json "$id1")
s2comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8081/rest/debug/peerCompletion" | ./json "$id2")
s1comp=${s1comp:-0}
s2comp=${s2comp:-0}
tot=$(($s1comp + $s2comp))

View File

@@ -40,9 +40,9 @@ clean() {
testConvergence() {
while true ; do
sleep 5
s1comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8082/rest/connections" | ./json "$id1/Completion")
s2comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8083/rest/connections" | ./json "$id2/Completion")
s3comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8081/rest/connections" | ./json "$id3/Completion")
s1comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8082/rest/debug/peerCompletion" | ./json "$id1")
s2comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8083/rest/debug/peerCompletion" | ./json "$id2")
s3comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8081/rest/debug/peerCompletion" | ./json "$id3")
s1comp=${s1comp:-0}
s2comp=${s2comp:-0}
s3comp=${s3comp:-0}

View File

@@ -43,7 +43,7 @@ testConvergence() {
while true ; do
sleep 5
comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8081/rest/connections" | ./json "$id2/Completion")
comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8081/rest/debug/peerCompletion" | ./json "$id2")
comp=${comp:-0}
echo $comp / 100

View File

@@ -157,7 +157,6 @@ type ConnectionInfo struct {
protocol.Statistics
Address string
ClientVersion string
Completion int
}
// ConnectionStats returns a map with connection statistics for each connected node.
@@ -179,43 +178,6 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
ci.Address = nc.RemoteAddr().String()
}
var tot int64
var have int64
for _, repo := range m.nodeRepos[node] {
m.repoFiles[repo].WithGlobal(func(f protocol.FileInfo) bool {
if !protocol.IsDeleted(f.Flags) {
var size int64
if protocol.IsDirectory(f.Flags) {
size = zeroEntrySize
} else {
size = f.Size()
}
tot += size
have += size
}
return true
})
m.repoFiles[repo].WithNeed(node, func(f protocol.FileInfo) bool {
if !protocol.IsDeleted(f.Flags) {
var size int64
if protocol.IsDirectory(f.Flags) {
size = zeroEntrySize
} else {
size = f.Size()
}
have -= size
}
return true
})
}
ci.Completion = 100
if tot != 0 {
ci.Completion = int(100 * have / tot)
}
res[node.String()] = ci
}
@@ -234,6 +196,39 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
return res
}
// Returns the completion status, in percent, for the given node and repo.
func (m *Model) Completion(node protocol.NodeID, repo string) float64 {
var tot int64
m.repoFiles[repo].WithGlobal(func(f protocol.FileInfo) bool {
if !protocol.IsDeleted(f.Flags) {
var size int64
if protocol.IsDirectory(f.Flags) {
size = zeroEntrySize
} else {
size = f.Size()
}
tot += size
}
return true
})
var need int64
m.repoFiles[repo].WithNeed(node, func(f protocol.FileInfo) bool {
if !protocol.IsDeleted(f.Flags) {
var size int64
if protocol.IsDirectory(f.Flags) {
size = zeroEntrySize
} else {
size = f.Size()
}
need += size
}
return true
})
return 100 * (1 - float64(need)/float64(tot))
}
func sizeOf(fs []protocol.FileInfo) (files, deleted int, bytes int64) {
for _, f := range fs {
fs, de, by := sizeOfFile(f)
@@ -586,9 +581,9 @@ func sendIndexes(conn protocol.Connection, repo string, fs *files.Set) {
for err == nil {
if !initial {
time.Sleep(5 * time.Second)
}
if fs.LocalVersion(protocol.LocalNodeID) <= minLocalVer {
continue
if fs.LocalVersion(protocol.LocalNodeID) <= minLocalVer {
continue
}
}
batch := make([]protocol.FileInfo, 0, indexBatchSize)
@@ -769,6 +764,13 @@ func (m *Model) ScanRepo(repo string) error {
batchSize := 100
batch := make([]protocol.FileInfo, 0, 00)
for f := range fchan {
events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{
"repo": repo,
"name": f.Name,
"modified": time.Unix(f.Modified, 0),
"flags": fmt.Sprintf("0%o", f.Flags),
"size": f.Size(),
})
if len(batch) == batchSize {
fs.Update(protocol.LocalNodeID, batch)
batch = batch[:0]
@@ -792,6 +794,13 @@ func (m *Model) ScanRepo(repo string) error {
f.Flags |= protocol.FlagDeleted
f.Version = lamport.Default.Tick(f.Version)
f.LocalVersion = 0
events.Default.Log(events.LocalIndexUpdated, map[string]interface{}{
"repo": repo,
"name": f.Name,
"modified": time.Unix(f.Modified, 0),
"flags": fmt.Sprintf("0%o", f.Flags),
"size": f.Size(),
})
batch = append(batch, f)
}
}

View File

@@ -64,13 +64,15 @@ Messages
--------
Every message starts with one 32 bit word indicating the message
version, type and ID. The header is in network byte order, i.e. big
endian.
version, type and ID, followed by the length of the message. The header
is in network byte order, i.e. big endian.
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Ver | Message ID | Type | Reserved |
| Ver | Message ID | Type | Reserved |C|
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
For BEP v1 the Version field is set to zero. Future versions with
@@ -92,10 +94,28 @@ The Type field indicates the type of data following the message header
and is one of the integers defined below. A message of an unknown type
is a protocol error and MUST result in the connection being terminated.
All data following the message header MUST be in XDR (RFC 1014)
encoding. All fields shorter than 32 bits and all variable length data
MUST be padded to a multiple of 32 bits. The actual data types in use by
BEP, in XDR naming convention, are the following:
The Compression bit "C" indicates the compression used for the message.
For C=1:
* The Length field contains the length, in bytes, of the
compressed message data.
* The message data is compressed using the LZ4 format and algorithm
described in https://code.google.com/p/lz4/.
For C=0:
* The Length field contains the length, in bytes, of the
uncompressed message data.
* The message is not compressed.
All data within the the message (post decompression, if compression is
in use) MUST be in XDR (RFC 1014) encoding. All fields shorter than 32
bits and all variable length data MUST be padded to a multiple of 32
bits. The actual data types in use by BEP, in XDR naming convention, are
the following:
- (unsigned) int -- (unsigned) 32 bit integer
- (unsigned) hyper -- (unsigned) 64 bit integer
@@ -561,6 +581,33 @@ firewalls and NAT gateways. The Ping message has no contents.
The Pong message is sent in response to a Ping. The Pong message has no
contents, but copies the Message ID from the Ping.
### Close (Type = 7)
The Close message MAY be sent to indicate that the connection will be
torn down due to an error condition. A Close message MUST NOT be
followed by further messages.
#### Graphical Representation
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length of Reason |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Reason (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
#### Fields
The Reason field contains a human description of the error condition,
suitable for consumption by a human.
struct CloseMessage {
string Reason<1024>;
}
Sharing Modes
-------------

View File

@@ -7,11 +7,13 @@ package protocol
import (
"io"
"sync/atomic"
"time"
)
type countingReader struct {
io.Reader
tot uint64
tot uint64 // bytes
last int64 // unix nanos
}
var (
@@ -23,6 +25,7 @@ func (c *countingReader) Read(bs []byte) (int, error) {
n, err := c.Reader.Read(bs)
atomic.AddUint64(&c.tot, uint64(n))
atomic.AddUint64(&totalIncoming, uint64(n))
atomic.StoreInt64(&c.last, time.Now().UnixNano())
return n, err
}
@@ -30,15 +33,21 @@ func (c *countingReader) Tot() uint64 {
return atomic.LoadUint64(&c.tot)
}
func (c *countingReader) Last() time.Time {
return time.Unix(0, atomic.LoadInt64(&c.last))
}
type countingWriter struct {
io.Writer
tot uint64
tot uint64 // bytes
last int64 // unix nanos
}
func (c *countingWriter) Write(bs []byte) (int, error) {
n, err := c.Writer.Write(bs)
atomic.AddUint64(&c.tot, uint64(n))
atomic.AddUint64(&totalOutgoing, uint64(n))
atomic.StoreInt64(&c.last, time.Now().UnixNano())
return n, err
}
@@ -46,6 +55,10 @@ func (c *countingWriter) Tot() uint64 {
return atomic.LoadUint64(&c.tot)
}
func (c *countingWriter) Last() time.Time {
return time.Unix(0, atomic.LoadInt64(&c.last))
}
func TotalInOut() (uint64, uint64) {
return atomic.LoadUint64(&totalIncoming), atomic.LoadUint64(&totalOutgoing)
}

View File

@@ -7,9 +7,10 @@ package protocol
import "github.com/calmh/syncthing/xdr"
type header struct {
version int
msgID int
msgType int
version int
msgID int
msgType int
compression bool
}
func (h header) encodeXDR(xw *xdr.Writer) (int, error) {
@@ -24,15 +25,21 @@ func (h *header) decodeXDR(xr *xdr.Reader) error {
}
func encodeHeader(h header) uint32 {
var isComp uint32
if h.compression {
isComp = 1 << 0 // the zeroth bit is the compression bit
}
return uint32(h.version&0xf)<<28 +
uint32(h.msgID&0xfff)<<16 +
uint32(h.msgType&0xff)<<8
uint32(h.msgType&0xff)<<8 +
isComp
}
func decodeHeader(u uint32) header {
return header{
version: int(u>>28) & 0xf,
msgID: int(u>>16) & 0xfff,
msgType: int(u>>8) & 0xff,
version: int(u>>28) & 0xf,
msgID: int(u>>16) & 0xfff,
msgType: int(u>>8) & 0xff,
compression: u&1 == 1,
}
}

119
protocol/lz4stream.go Normal file
View File

@@ -0,0 +1,119 @@
package protocol
import (
"bytes"
"encoding/binary"
"errors"
"io"
"sync"
lz4 "github.com/bkaradzic/go-lz4"
)
const lz4Magic = 0x5e63b278
type lz4Writer struct {
wr io.Writer
mut sync.Mutex
buf []byte
}
func newLZ4Writer(w io.Writer) *lz4Writer {
return &lz4Writer{wr: w}
}
func (w *lz4Writer) Write(bs []byte) (int, error) {
w.mut.Lock()
defer w.mut.Unlock()
var err error
w.buf, err = lz4.Encode(w.buf[:cap(w.buf)], bs)
if err != nil {
return 0, err
}
var hdr [8]byte
binary.BigEndian.PutUint32(hdr[0:], lz4Magic)
binary.BigEndian.PutUint32(hdr[4:], uint32(len(w.buf)))
_, err = w.wr.Write(hdr[:])
if err != nil {
return 0, err
}
_, err = w.wr.Write(w.buf)
if err != nil {
return 0, err
}
if debug {
l.Debugf("lz4 write; %d / %d bytes", len(bs), 8+len(w.buf))
}
return len(bs), nil
}
type lz4Reader struct {
rd io.Reader
mut sync.Mutex
buf []byte
ebuf []byte
obuf *bytes.Buffer
ibytes uint64
obytes uint64
}
func newLZ4Reader(r io.Reader) *lz4Reader {
return &lz4Reader{rd: r}
}
func (r *lz4Reader) Read(bs []byte) (int, error) {
r.mut.Lock()
defer r.mut.Unlock()
if r.obuf == nil {
r.obuf = bytes.NewBuffer(nil)
}
if r.obuf.Len() == 0 {
if err := r.moreBits(); err != nil {
return 0, err
}
}
n, err := r.obuf.Read(bs)
if debug {
l.Debugf("lz4 read; %d bytes", n)
}
return n, err
}
func (r *lz4Reader) moreBits() error {
var hdr [8]byte
_, err := io.ReadFull(r.rd, hdr[:])
if binary.BigEndian.Uint32(hdr[0:]) != lz4Magic {
return errors.New("bad magic")
}
ln := int(binary.BigEndian.Uint32(hdr[4:]))
if len(r.buf) < ln {
r.buf = make([]byte, int(ln))
} else {
r.buf = r.buf[:ln]
}
_, err = io.ReadFull(r.rd, r.buf)
if err != nil {
return err
}
r.ebuf, err = lz4.Decode(r.ebuf[:cap(r.ebuf)], r.buf)
if err != nil {
return err
}
if debug {
l.Debugf("lz4 moreBits: %d / %d bytes", ln+8, len(r.ebuf))
}
_, err = r.obuf.Write(r.ebuf)
return err
}

View File

@@ -0,0 +1,60 @@
package protocol
import (
"bytes"
"crypto/rand"
"io"
"testing"
)
var toWrite = [][]byte{
[]byte("this is a short byte string that should pass through somewhat compressed this is a short byte string that should pass through somewhat compressed this is a short byte string that should pass through somewhat compressed this is a short byte string that should pass through somewhat compressed this is a short byte string that should pass through somewhat compressed this is a short byte string that should pass through somewhat compressed"),
[]byte("this is another short byte string that should pass through uncompressed"),
[]byte{0, 1, 2, 3, 4, 5},
}
func TestLZ4Stream(t *testing.T) {
tb := make([]byte, 128*1024)
rand.Reader.Read(tb)
toWrite = append(toWrite, tb)
tb = make([]byte, 512*1024)
rand.Reader.Read(tb)
toWrite = append(toWrite, tb)
toWrite = append(toWrite, toWrite[0])
toWrite = append(toWrite, toWrite[1])
rd, wr := io.Pipe()
lz4r := newLZ4Reader(rd)
lz4w := newLZ4Writer(wr)
go func() {
for i := 0; i < 5; i++ {
for _, bs := range toWrite {
n, err := lz4w.Write(bs)
if err != nil {
t.Error(err)
}
if n != len(bs) {
t.Errorf("weird write length; %d != %d", n, len(bs))
}
}
}
}()
buf := make([]byte, 512*1024)
for i := 0; i < 5; i++ {
for _, bs := range toWrite {
n, err := lz4r.Read(buf)
if err != nil {
t.Fatal(err)
}
if n != len(bs) {
t.Errorf("Unexpected len %d != %d", n, len(bs))
}
if bytes.Compare(bs, buf[:n]) != 0 {
t.Error("Unexpected data")
}
}
}
}

View File

@@ -7,8 +7,8 @@ package protocol
import "fmt"
type IndexMessage struct {
Repository string // max:64
Files []FileInfo // max:10000000
Repository string // max:64
Files []FileInfo
}
type FileInfo struct {
@@ -17,7 +17,7 @@ type FileInfo struct {
Modified int64
Version uint64
LocalVersion uint64
Blocks []BlockInfo // max:1000000
Blocks []BlockInfo
}
func (f FileInfo) String() string {
@@ -49,6 +49,10 @@ type RequestMessage struct {
Size uint32
}
type ResponseMessage struct {
Data []byte
}
type ClusterConfigMessage struct {
ClientName string // max:64
ClientVersion string // max:64
@@ -71,3 +75,9 @@ type Option struct {
Key string // max:64
Value string // max:1024
}
type CloseMessage struct {
Reason string // max:1024
}
type EmptyMessage struct{}

View File

@@ -38,7 +38,7 @@ IndexMessage Structure:
struct IndexMessage {
string Repository<64>;
FileInfo Files<10000000>;
FileInfo Files<>;
}
*/
@@ -64,9 +64,6 @@ func (o IndexMessage) encodeXDR(xw *xdr.Writer) (int, error) {
return xw.Tot(), xdr.ErrElementSizeExceeded
}
xw.WriteString(o.Repository)
if len(o.Files) > 10000000 {
return xw.Tot(), xdr.ErrElementSizeExceeded
}
xw.WriteUint32(uint32(len(o.Files)))
for i := range o.Files {
o.Files[i].encodeXDR(xw)
@@ -88,9 +85,6 @@ func (o *IndexMessage) UnmarshalXDR(bs []byte) error {
func (o *IndexMessage) decodeXDR(xr *xdr.Reader) error {
o.Repository = xr.ReadStringMax(64)
_FilesSize := int(xr.ReadUint32())
if _FilesSize > 10000000 {
return xdr.ErrElementSizeExceeded
}
o.Files = make([]FileInfo, _FilesSize)
for i := range o.Files {
(&o.Files[i]).decodeXDR(xr)
@@ -139,7 +133,7 @@ struct FileInfo {
hyper Modified;
unsigned hyper Version;
unsigned hyper LocalVersion;
BlockInfo Blocks<1000000>;
BlockInfo Blocks<>;
}
*/
@@ -169,9 +163,6 @@ func (o FileInfo) encodeXDR(xw *xdr.Writer) (int, error) {
xw.WriteUint64(uint64(o.Modified))
xw.WriteUint64(o.Version)
xw.WriteUint64(o.LocalVersion)
if len(o.Blocks) > 1000000 {
return xw.Tot(), xdr.ErrElementSizeExceeded
}
xw.WriteUint32(uint32(len(o.Blocks)))
for i := range o.Blocks {
o.Blocks[i].encodeXDR(xw)
@@ -197,9 +188,6 @@ func (o *FileInfo) decodeXDR(xr *xdr.Reader) error {
o.Version = xr.ReadUint64()
o.LocalVersion = xr.ReadUint64()
_BlocksSize := int(xr.ReadUint32())
if _BlocksSize > 1000000 {
return xdr.ErrElementSizeExceeded
}
o.Blocks = make([]BlockInfo, _BlocksSize)
for i := range o.Blocks {
(&o.Blocks[i]).decodeXDR(xr)
@@ -360,6 +348,64 @@ func (o *RequestMessage) decodeXDR(xr *xdr.Reader) error {
/*
ResponseMessage Structure:
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length of Data |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Data (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct ResponseMessage {
opaque Data<>;
}
*/
func (o ResponseMessage) EncodeXDR(w io.Writer) (int, error) {
var xw = xdr.NewWriter(w)
return o.encodeXDR(xw)
}
func (o ResponseMessage) MarshalXDR() []byte {
return o.AppendXDR(make([]byte, 0, 128))
}
func (o ResponseMessage) AppendXDR(bs []byte) []byte {
var aw = xdr.AppendWriter(bs)
var xw = xdr.NewWriter(&aw)
o.encodeXDR(xw)
return []byte(aw)
}
func (o ResponseMessage) encodeXDR(xw *xdr.Writer) (int, error) {
xw.WriteBytes(o.Data)
return xw.Tot(), xw.Error()
}
func (o *ResponseMessage) DecodeXDR(r io.Reader) error {
xr := xdr.NewReader(r)
return o.decodeXDR(xr)
}
func (o *ResponseMessage) UnmarshalXDR(bs []byte) error {
var br = bytes.NewReader(bs)
var xr = xdr.NewReader(br)
return o.decodeXDR(xr)
}
func (o *ResponseMessage) decodeXDR(xr *xdr.Reader) error {
o.Data = xr.ReadBytes()
return xr.Error()
}
/*
ClusterConfigMessage Structure:
0 1 2 3
@@ -703,3 +749,113 @@ func (o *Option) decodeXDR(xr *xdr.Reader) error {
o.Value = xr.ReadStringMax(1024)
return xr.Error()
}
/*
CloseMessage Structure:
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
| Length of Reason |
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
/ /
\ Reason (variable length) \
/ /
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct CloseMessage {
string Reason<1024>;
}
*/
func (o CloseMessage) EncodeXDR(w io.Writer) (int, error) {
var xw = xdr.NewWriter(w)
return o.encodeXDR(xw)
}
func (o CloseMessage) MarshalXDR() []byte {
return o.AppendXDR(make([]byte, 0, 128))
}
func (o CloseMessage) AppendXDR(bs []byte) []byte {
var aw = xdr.AppendWriter(bs)
var xw = xdr.NewWriter(&aw)
o.encodeXDR(xw)
return []byte(aw)
}
func (o CloseMessage) encodeXDR(xw *xdr.Writer) (int, error) {
if len(o.Reason) > 1024 {
return xw.Tot(), xdr.ErrElementSizeExceeded
}
xw.WriteString(o.Reason)
return xw.Tot(), xw.Error()
}
func (o *CloseMessage) DecodeXDR(r io.Reader) error {
xr := xdr.NewReader(r)
return o.decodeXDR(xr)
}
func (o *CloseMessage) UnmarshalXDR(bs []byte) error {
var br = bytes.NewReader(bs)
var xr = xdr.NewReader(br)
return o.decodeXDR(xr)
}
func (o *CloseMessage) decodeXDR(xr *xdr.Reader) error {
o.Reason = xr.ReadStringMax(1024)
return xr.Error()
}
/*
EmptyMessage Structure:
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
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
struct EmptyMessage {
}
*/
func (o EmptyMessage) EncodeXDR(w io.Writer) (int, error) {
var xw = xdr.NewWriter(w)
return o.encodeXDR(xw)
}
func (o EmptyMessage) MarshalXDR() []byte {
return o.AppendXDR(make([]byte, 0, 128))
}
func (o EmptyMessage) AppendXDR(bs []byte) []byte {
var aw = xdr.AppendWriter(bs)
var xw = xdr.NewWriter(&aw)
o.encodeXDR(xw)
return []byte(aw)
}
func (o EmptyMessage) encodeXDR(xw *xdr.Writer) (int, error) {
return xw.Tot(), xw.Error()
}
func (o *EmptyMessage) DecodeXDR(r io.Reader) error {
xr := xdr.NewReader(r)
return o.decodeXDR(xr)
}
func (o *EmptyMessage) UnmarshalXDR(bs []byte) error {
var br = bytes.NewReader(bs)
var xr = xdr.NewReader(br)
return o.decodeXDR(xr)
}
func (o *EmptyMessage) decodeXDR(xr *xdr.Reader) error {
return xr.Error()
}

View File

@@ -6,16 +6,20 @@ package protocol
import (
"bufio"
"encoding/binary"
"encoding/hex"
"errors"
"fmt"
"io"
"sync"
"time"
"github.com/calmh/syncthing/xdr"
lz4 "github.com/bkaradzic/go-lz4"
)
const BlockSize = 128 * 1024
const (
BlockSize = 128 * 1024
)
const (
messageTypeClusterConfig = 0
@@ -25,6 +29,7 @@ const (
messageTypePing = 4
messageTypePong = 5
messageTypeIndexUpdate = 6
messageTypeClose = 7
)
const (
@@ -81,21 +86,24 @@ type rawConnection struct {
state int
cr *countingReader
xr *xdr.Reader
cw *countingWriter
wb *bufio.Writer
xw *xdr.Writer
awaiting []chan asyncResult
awaiting [4096]chan asyncResult
awaitingMut sync.Mutex
idxMut sync.Mutex // ensures serialization of Index calls
nextID chan int
outbox chan []encodable
outbox chan hdrMsg
closed chan struct{}
once sync.Once
compressionThreshold int // compress messages larger than this many bytes
rdbuf0 []byte // used & reused by readMessage
rdbuf1 []byte // used & reused by readMessage
}
type asyncResult struct {
@@ -103,32 +111,39 @@ type asyncResult struct {
err error
}
type hdrMsg struct {
hdr header
msg encodable
}
type encodable interface {
AppendXDR([]byte) []byte
}
const (
pingTimeout = 30 * time.Second
pingIdleTime = 60 * time.Second
)
func NewConnection(nodeID NodeID, reader io.Reader, writer io.Writer, receiver Model, name string) Connection {
func NewConnection(nodeID NodeID, reader io.Reader, writer io.Writer, receiver Model, name string, compress bool) Connection {
cr := &countingReader{Reader: reader}
cw := &countingWriter{Writer: writer}
rb := bufio.NewReader(cr)
wb := bufio.NewWriterSize(cw, 65536)
compThres := 1<<31 - 1 // compression disabled
if compress {
compThres = 128 // compress messages that are 128 bytes long or larger
}
c := rawConnection{
id: nodeID,
name: name,
receiver: nativeModel{receiver},
state: stateInitial,
cr: cr,
xr: xdr.NewReader(rb),
cw: cw,
wb: wb,
xw: xdr.NewWriter(wb),
awaiting: make([]chan asyncResult, 0x1000),
outbox: make(chan []encodable),
nextID: make(chan int),
closed: make(chan struct{}),
id: nodeID,
name: name,
receiver: nativeModel{receiver},
state: stateInitial,
cr: cr,
cw: cw,
outbox: make(chan hdrMsg),
nextID: make(chan int),
closed: make(chan struct{}),
compressionThreshold: compThres,
}
go c.readerLoop()
@@ -155,7 +170,7 @@ func (c *rawConnection) Index(repo string, idx []FileInfo) error {
default:
}
c.idxMut.Lock()
c.send(header{0, -1, messageTypeIndex}, IndexMessage{repo, idx})
c.send(-1, messageTypeIndex, IndexMessage{repo, idx})
c.idxMut.Unlock()
return nil
}
@@ -168,7 +183,7 @@ func (c *rawConnection) IndexUpdate(repo string, idx []FileInfo) error {
default:
}
c.idxMut.Lock()
c.send(header{0, -1, messageTypeIndexUpdate}, IndexMessage{repo, idx})
c.send(-1, messageTypeIndexUpdate, IndexMessage{repo, idx})
c.idxMut.Unlock()
return nil
}
@@ -190,8 +205,7 @@ func (c *rawConnection) Request(repo string, name string, offset int64, size int
c.awaiting[id] = rc
c.awaitingMut.Unlock()
ok := c.send(header{0, id, messageTypeRequest},
RequestMessage{repo, name, uint64(offset), uint32(size)})
ok := c.send(id, messageTypeRequest, RequestMessage{repo, name, uint64(offset), uint32(size)})
if !ok {
return nil, ErrClosed
}
@@ -205,7 +219,7 @@ func (c *rawConnection) Request(repo string, name string, offset int64, size int
// ClusterConfig send the cluster configuration message to the peer and returns any error
func (c *rawConnection) ClusterConfig(config ClusterConfigMessage) {
c.send(header{0, -1, messageTypeClusterConfig}, config)
c.send(-1, messageTypeClusterConfig, config)
}
func (c *rawConnection) ping() bool {
@@ -221,7 +235,7 @@ func (c *rawConnection) ping() bool {
c.awaiting[id] = rc
c.awaitingMut.Unlock()
ok := c.send(header{0, id, messageTypePing})
ok := c.send(id, messageTypePing, nil)
if !ok {
return false
}
@@ -242,169 +256,207 @@ func (c *rawConnection) readerLoop() (err error) {
default:
}
var hdr header
hdr.decodeXDR(c.xr)
if err := c.xr.Error(); err != nil {
hdr, msg, err := c.readMessage()
if err != nil {
return err
}
if hdr.version != 0 {
return fmt.Errorf("protocol error: %s: unknown message version %#x", c.id, hdr.version)
}
switch hdr.msgType {
case messageTypeIndex:
if c.state < stateCCRcvd {
return fmt.Errorf("protocol error: index message in state %d", c.state)
}
if err := c.handleIndex(); err != nil {
return err
}
c.handleIndex(msg.(IndexMessage))
c.state = stateIdxRcvd
case messageTypeIndexUpdate:
if c.state < stateIdxRcvd {
return fmt.Errorf("protocol error: index update message in state %d", c.state)
}
if err := c.handleIndexUpdate(); err != nil {
return err
}
c.handleIndexUpdate(msg.(IndexMessage))
case messageTypeRequest:
if c.state < stateIdxRcvd {
return fmt.Errorf("protocol error: request message in state %d", c.state)
}
if err := c.handleRequest(hdr); err != nil {
return err
}
// Requests are handled asynchronously
go c.handleRequest(hdr.msgID, msg.(RequestMessage))
case messageTypeResponse:
if c.state < stateIdxRcvd {
return fmt.Errorf("protocol error: response message in state %d", c.state)
}
if err := c.handleResponse(hdr); err != nil {
return err
}
c.handleResponse(hdr.msgID, msg.(ResponseMessage))
case messageTypePing:
c.send(header{0, hdr.msgID, messageTypePong})
c.send(hdr.msgID, messageTypePong, EmptyMessage{})
case messageTypePong:
c.handlePong(hdr)
c.handlePong(hdr.msgID)
case messageTypeClusterConfig:
if c.state != stateInitial {
return fmt.Errorf("protocol error: cluster config message in state %d", c.state)
}
if err := c.handleClusterConfig(); err != nil {
return err
}
go c.receiver.ClusterConfig(c.id, msg.(ClusterConfigMessage))
c.state = stateCCRcvd
case messageTypeClose:
return errors.New(msg.(CloseMessage).Reason)
default:
return fmt.Errorf("protocol error: %s: unknown message type %#x", c.id, hdr.msgType)
}
}
}
func (c *rawConnection) handleIndex() error {
var im IndexMessage
im.decodeXDR(c.xr)
if err := c.xr.Error(); err != nil {
return err
func (c *rawConnection) readMessage() (hdr header, msg encodable, err error) {
if cap(c.rdbuf0) < 8 {
c.rdbuf0 = make([]byte, 8)
} else {
if debug {
l.Debugf("Index(%v, %v, %d files)", c.id, im.Repository, len(im.Files))
}
c.receiver.Index(c.id, im.Repository, im.Files)
c.rdbuf0 = c.rdbuf0[:8]
}
_, err = io.ReadFull(c.cr, c.rdbuf0)
if err != nil {
return
}
return nil
}
func (c *rawConnection) handleIndexUpdate() error {
var im IndexMessage
im.decodeXDR(c.xr)
if err := c.xr.Error(); err != nil {
return err
hdr = decodeHeader(binary.BigEndian.Uint32(c.rdbuf0[0:4]))
msglen := int(binary.BigEndian.Uint32(c.rdbuf0[4:8]))
if debug {
l.Debugf("read header %v (msglen=%d)", hdr, msglen)
}
if cap(c.rdbuf0) < msglen {
c.rdbuf0 = make([]byte, msglen)
} else {
if debug {
l.Debugf("queueing IndexUpdate(%v, %v, %d files)", c.id, im.Repository, len(im.Files))
c.rdbuf0 = c.rdbuf0[:msglen]
}
_, err = io.ReadFull(c.cr, c.rdbuf0)
if err != nil {
return
}
if debug {
l.Debugf("read %d bytes", len(c.rdbuf0))
}
msgBuf := c.rdbuf0
if hdr.compression {
c.rdbuf1 = c.rdbuf1[:cap(c.rdbuf1)]
c.rdbuf1, err = lz4.Decode(c.rdbuf1, c.rdbuf0)
if err != nil {
return
}
msgBuf = c.rdbuf1
if debug {
l.Debugf("decompressed to %d bytes", len(msgBuf))
}
c.receiver.IndexUpdate(c.id, im.Repository, im.Files)
}
return nil
if debug {
if len(msgBuf) > 1024 {
l.Debugf("message data:\n%s", hex.Dump(msgBuf[:1024]))
} else {
l.Debugf("message data:\n%s", hex.Dump(msgBuf))
}
}
switch hdr.msgType {
case messageTypeIndex, messageTypeIndexUpdate:
var idx IndexMessage
err = idx.UnmarshalXDR(msgBuf)
msg = idx
case messageTypeRequest:
var req RequestMessage
err = req.UnmarshalXDR(msgBuf)
msg = req
case messageTypeResponse:
var resp ResponseMessage
err = resp.UnmarshalXDR(msgBuf)
msg = resp
case messageTypePing, messageTypePong:
msg = EmptyMessage{}
case messageTypeClusterConfig:
var cc ClusterConfigMessage
err = cc.UnmarshalXDR(msgBuf)
msg = cc
case messageTypeClose:
var cm CloseMessage
err = cm.UnmarshalXDR(msgBuf)
msg = cm
default:
err = fmt.Errorf("protocol error: %s: unknown message type %#x", c.id, hdr.msgType)
}
return
}
func (c *rawConnection) handleRequest(hdr header) error {
var req RequestMessage
req.decodeXDR(c.xr)
if err := c.xr.Error(); err != nil {
return err
func (c *rawConnection) handleIndex(im IndexMessage) {
if debug {
l.Debugf("Index(%v, %v, %d files)", c.id, im.Repository, len(im.Files))
}
go c.processRequest(hdr.msgID, req)
return nil
c.receiver.Index(c.id, im.Repository, im.Files)
}
func (c *rawConnection) handleResponse(hdr header) error {
data := c.xr.ReadBytesMax(256 * 1024) // Sufficiently larger than max expected block size
if err := c.xr.Error(); err != nil {
return err
func (c *rawConnection) handleIndexUpdate(im IndexMessage) {
if debug {
l.Debugf("queueing IndexUpdate(%v, %v, %d files)", c.id, im.Repository, len(im.Files))
}
c.receiver.IndexUpdate(c.id, im.Repository, im.Files)
}
func (c *rawConnection) handleRequest(msgID int, req RequestMessage) {
data, _ := c.receiver.Request(c.id, req.Repository, req.Name, int64(req.Offset), int(req.Size))
c.send(msgID, messageTypeResponse, ResponseMessage{data})
}
func (c *rawConnection) handleResponse(msgID int, resp ResponseMessage) {
c.awaitingMut.Lock()
if rc := c.awaiting[hdr.msgID]; rc != nil {
c.awaiting[hdr.msgID] = nil
rc <- asyncResult{data, nil}
if rc := c.awaiting[msgID]; rc != nil {
c.awaiting[msgID] = nil
rc <- asyncResult{resp.Data, nil}
close(rc)
}
c.awaitingMut.Unlock()
return nil
}
func (c *rawConnection) handlePong(hdr header) {
func (c *rawConnection) handlePong(msgID int) {
c.awaitingMut.Lock()
if rc := c.awaiting[hdr.msgID]; rc != nil {
c.awaiting[hdr.msgID] = nil
if rc := c.awaiting[msgID]; rc != nil {
c.awaiting[msgID] = nil
rc <- asyncResult{}
close(rc)
}
c.awaitingMut.Unlock()
}
func (c *rawConnection) handleClusterConfig() error {
var cm ClusterConfigMessage
cm.decodeXDR(c.xr)
if err := c.xr.Error(); err != nil {
return err
} else {
go c.receiver.ClusterConfig(c.id, cm)
}
return nil
}
type encodable interface {
encodeXDR(*xdr.Writer) (int, error)
}
type encodableBytes []byte
func (e encodableBytes) encodeXDR(xw *xdr.Writer) (int, error) {
return xw.WriteBytes(e)
}
func (c *rawConnection) send(h header, es ...encodable) bool {
if h.msgID < 0 {
func (c *rawConnection) send(msgID int, msgType int, msg encodable) bool {
if msgID < 0 {
select {
case id := <-c.nextID:
h.msgID = id
msgID = id
case <-c.closed:
return false
}
}
msg := append([]encodable{h}, es...)
hdr := header{
version: 0,
msgID: msgID,
msgType: msgType,
}
select {
case c.outbox <- msg:
case c.outbox <- hdrMsg{hdr, msg}:
return true
case <-c.closed:
return false
@@ -412,13 +464,71 @@ func (c *rawConnection) send(h header, es ...encodable) bool {
}
func (c *rawConnection) writerLoop() {
var msgBuf = make([]byte, 8) // buffer for wire format message, kept and reused
var uncBuf []byte // buffer for uncompressed message, kept and reused
for {
var tempBuf []byte
var err error
select {
case es := <-c.outbox:
for _, e := range es {
e.encodeXDR(c.xw)
case hm := <-c.outbox:
if hm.msg != nil {
// Uncompressed message in uncBuf
uncBuf = hm.msg.AppendXDR(uncBuf[:0])
if len(uncBuf) >= c.compressionThreshold {
// Use compression for large messages
hm.hdr.compression = true
// Make sure we have enough space for the compressed message plus header in msgBug
msgBuf = msgBuf[:cap(msgBuf)]
if maxLen := lz4.CompressBound(len(uncBuf)) + 8; maxLen > len(msgBuf) {
msgBuf = make([]byte, maxLen)
}
// Compressed is written to msgBuf, we keep tb for the length only
tempBuf, err = lz4.Encode(msgBuf[8:], uncBuf)
binary.BigEndian.PutUint32(msgBuf[4:8], uint32(len(tempBuf)))
msgBuf = msgBuf[0 : len(tempBuf)+8]
if debug {
l.Debugf("write compressed message; %v (len=%d)", hm.hdr, len(tempBuf))
}
} else {
// No point in compressing very short messages
hm.hdr.compression = false
msgBuf = msgBuf[:cap(msgBuf)]
if l := len(uncBuf) + 8; l > len(msgBuf) {
msgBuf = make([]byte, l)
}
binary.BigEndian.PutUint32(msgBuf[4:8], uint32(len(uncBuf)))
msgBuf = msgBuf[0 : len(uncBuf)+8]
copy(msgBuf[8:], uncBuf)
if debug {
l.Debugf("write uncompressed message; %v (len=%d)", hm.hdr, len(uncBuf))
}
}
} else {
if debug {
l.Debugf("write empty message; %v", hm.hdr)
}
binary.BigEndian.PutUint32(msgBuf[4:8], 0)
msgBuf = msgBuf[:8]
}
if err := c.flush(); err != nil {
binary.BigEndian.PutUint32(msgBuf[0:4], encodeHeader(hm.hdr))
if err == nil {
var n int
n, err = c.cw.Write(msgBuf)
if debug {
l.Debugf("wrote %d bytes on the wire", n)
}
}
if err != nil {
c.close(err)
return
}
@@ -428,20 +538,6 @@ func (c *rawConnection) writerLoop() {
}
}
type flusher interface {
Flush() error
}
func (c *rawConnection) flush() error {
if err := c.xw.Error(); err != nil {
return err
}
if err := c.wb.Flush(); err != nil {
return err
}
return nil
}
func (c *rawConnection) close(err error) {
c.once.Do(func() {
close(c.closed)
@@ -477,13 +573,13 @@ func (c *rawConnection) pingerLoop() {
for {
select {
case <-ticker:
if d := time.Since(c.xr.LastRead()); d < pingIdleTime {
if d := time.Since(c.cr.Last()); d < pingIdleTime {
if debug {
l.Debugln(c.id, "ping skipped after rd", d)
}
continue
}
if d := time.Since(c.xw.LastWrite()); d < pingIdleTime {
if d := time.Since(c.cw.Last()); d < pingIdleTime {
if debug {
l.Debugln(c.id, "ping skipped after wr", d)
}
@@ -515,12 +611,6 @@ func (c *rawConnection) pingerLoop() {
}
}
func (c *rawConnection) processRequest(msgID int, req RequestMessage) {
data, _ := c.receiver.Request(c.id, req.Repository, req.Name, int64(req.Offset), int(req.Size))
c.send(header{0, msgID, messageTypeResponse}, encodableBytes(data))
}
type Statistics struct {
At time.Time
InBytesTotal uint64

View File

@@ -9,6 +9,8 @@ import (
"io"
"testing"
"testing/quick"
"github.com/calmh/syncthing/xdr"
)
var (
@@ -21,7 +23,7 @@ func TestHeaderFunctions(t *testing.T) {
ver = int(uint(ver) % 16)
id = int(uint(id) % 4096)
typ = int(uint(typ) % 256)
h0 := header{ver, id, typ}
h0 := header{version: ver, msgID: id, msgType: typ}
h1 := decodeHeader(encodeHeader(h0))
return h0 == h1
}
@@ -35,21 +37,21 @@ func TestHeaderLayout(t *testing.T) {
// Version are the first four bits
e = 0xf0000000
a = encodeHeader(header{0xf, 0, 0})
a = encodeHeader(header{version: 0xf})
if a != e {
t.Errorf("Header layout incorrect; %08x != %08x", a, e)
}
// Message ID are the following 12 bits
e = 0x0fff0000
a = encodeHeader(header{0, 0xfff, 0})
a = encodeHeader(header{msgID: 0xfff})
if a != e {
t.Errorf("Header layout incorrect; %08x != %08x", a, e)
}
// Type are the last 8 bits before reserved
e = 0x0000ff00
a = encodeHeader(header{0, 0, 0xff})
a = encodeHeader(header{msgType: 0xff})
if a != e {
t.Errorf("Header layout incorrect; %08x != %08x", a, e)
}
@@ -59,8 +61,8 @@ func TestPing(t *testing.T) {
ar, aw := io.Pipe()
br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, nil, "name").(wireFormatConnection).next.(*rawConnection)
c1 := NewConnection(c1ID, br, aw, nil, "name").(wireFormatConnection).next.(*rawConnection)
c0 := NewConnection(c0ID, ar, bw, nil, "name", true).(wireFormatConnection).next.(*rawConnection)
c1 := NewConnection(c1ID, br, aw, nil, "name", true).(wireFormatConnection).next.(*rawConnection)
if ok := c0.ping(); !ok {
t.Error("c0 ping failed")
@@ -73,8 +75,8 @@ func TestPing(t *testing.T) {
func TestPingErr(t *testing.T) {
e := errors.New("something broke")
for i := 0; i < 12; i++ {
for j := 0; j < 12; j++ {
for i := 0; i < 16; i++ {
for j := 0; j < 16; j++ {
m0 := newTestModel()
m1 := newTestModel()
@@ -83,13 +85,13 @@ func TestPingErr(t *testing.T) {
eaw := &ErrPipe{PipeWriter: *aw, max: i, err: e}
ebw := &ErrPipe{PipeWriter: *bw, max: j, err: e}
c0 := NewConnection(c0ID, ar, ebw, m0, "name").(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, eaw, m1, "name")
c0 := NewConnection(c0ID, ar, ebw, m0, "name", true).(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, eaw, m1, "name", true)
res := c0.ping()
if (i < 4 || j < 4) && res {
if (i < 8 || j < 8) && res {
t.Errorf("Unexpected ping success; i=%d, j=%d", i, j)
} else if (i >= 8 && j >= 8) && !res {
} else if (i >= 12 && j >= 12) && !res {
t.Errorf("Unexpected ping fail; i=%d, j=%d", i, j)
}
}
@@ -159,15 +161,16 @@ func TestVersionErr(t *testing.T) {
ar, aw := io.Pipe()
br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, m0, "name").(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, aw, m1, "name")
c0 := NewConnection(c0ID, ar, bw, m0, "name", true).(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, aw, m1, "name", true)
c0.xw.WriteUint32(encodeHeader(header{
w := xdr.NewWriter(c0.cw)
w.WriteUint32(encodeHeader(header{
version: 2,
msgID: 0,
msgType: 0,
}))
c0.flush()
w.WriteUint32(0)
if !m1.isClosed() {
t.Error("Connection should close due to unknown version")
@@ -181,15 +184,16 @@ func TestTypeErr(t *testing.T) {
ar, aw := io.Pipe()
br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, m0, "name").(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, aw, m1, "name")
c0 := NewConnection(c0ID, ar, bw, m0, "name", true).(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, aw, m1, "name", true)
c0.xw.WriteUint32(encodeHeader(header{
w := xdr.NewWriter(c0.cw)
w.WriteUint32(encodeHeader(header{
version: 0,
msgID: 0,
msgType: 42,
}))
c0.flush()
w.WriteUint32(0)
if !m1.isClosed() {
t.Error("Connection should close due to unknown message type")
@@ -203,8 +207,8 @@ func TestClose(t *testing.T) {
ar, aw := io.Pipe()
br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, m0, "name").(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, aw, m1, "name")
c0 := NewConnection(c0ID, ar, bw, m0, "name", true).(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, aw, m1, "name", true)
c0.close(nil)

View File

@@ -14,6 +14,8 @@ import (
const StandardBlockSize = 128 * 1024
var sha256OfNothing = []uint8{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55}
// Blocks returns the blockwise hash of the reader.
func Blocks(r io.Reader, blocksize int) ([]protocol.BlockInfo, error) {
var blocks []protocol.BlockInfo
@@ -44,7 +46,7 @@ func Blocks(r io.Reader, blocksize int) ([]protocol.BlockInfo, error) {
blocks = append(blocks, protocol.BlockInfo{
Offset: 0,
Size: 0,
Hash: []uint8{0xe3, 0xb0, 0xc4, 0x42, 0x98, 0xfc, 0x1c, 0x14, 0x9a, 0xfb, 0xf4, 0xc8, 0x99, 0x6f, 0xb9, 0x24, 0x27, 0xae, 0x41, 0xe4, 0x64, 0x9b, 0x93, 0x4c, 0xa4, 0x95, 0x99, 0x1b, 0x78, 0x52, 0xb8, 0x55},
Hash: sha256OfNothing,
})
}

View File

@@ -167,9 +167,6 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo, ign map[string][
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 {
if debug {
l.Debugln("unchanged:", cf)
}
return nil
}
}
@@ -198,9 +195,6 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo, ign map[string][
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 {
if debug {
l.Debugln("unchanged:", cf)
}
return nil
}

View File

@@ -7,18 +7,15 @@ package xdr
import (
"errors"
"io"
"time"
)
var ErrElementSizeExceeded = errors.New("element size exceeded")
type Reader struct {
r io.Reader
tot int
err error
b [8]byte
sb []byte
last time.Time
r io.Reader
err error
b [8]byte
sb []byte
}
func NewReader(r io.Reader) *Reader {
@@ -63,8 +60,6 @@ func (r *Reader) ReadBytesMaxInto(max int, dst []byte) []byte {
if r.err != nil {
return nil
}
r.last = time.Now()
s := r.tot
l := int(r.ReadUint32())
if r.err != nil {
@@ -85,17 +80,16 @@ func (r *Reader) ReadBytesMaxInto(max int, dst []byte) []byte {
n, r.err = io.ReadFull(r.r, dst)
if r.err != nil {
if debug {
dl.Debugf("@0x%x: rd bytes (%d): %v", s, len(dst), r.err)
dl.Debugf("rd bytes (%d): %v", len(dst), r.err)
}
return nil
}
r.tot += n
if debug {
if n > maxDebugBytes {
dl.Debugf("@0x%x: rd bytes (%d): %x...", s, len(dst), dst[:maxDebugBytes])
dl.Debugf("rd bytes (%d): %x...", len(dst), dst[:maxDebugBytes])
} else {
dl.Debugf("@0x%x: rd bytes (%d): %x", s, len(dst), dst)
dl.Debugf("rd bytes (%d): %x", len(dst), dst)
}
}
return dst[:l]
@@ -113,15 +107,11 @@ func (r *Reader) ReadUint32() uint32 {
if r.err != nil {
return 0
}
r.last = time.Now()
s := r.tot
var n int
n, r.err = io.ReadFull(r.r, r.b[:4])
r.tot += n
_, r.err = io.ReadFull(r.r, r.b[:4])
if r.err != nil {
if debug {
dl.Debugf("@0x%x: rd uint32: %v", r.tot, r.err)
dl.Debugf("rd uint32: %v", r.err)
}
return 0
}
@@ -129,7 +119,7 @@ func (r *Reader) ReadUint32() uint32 {
v := uint32(r.b[3]) | uint32(r.b[2])<<8 | uint32(r.b[1])<<16 | uint32(r.b[0])<<24
if debug {
dl.Debugf("@0x%x: rd uint32=%d (0x%08x)", s, v, v)
dl.Debugf("rd uint32=%d (0x%08x)", v, v)
}
return v
}
@@ -138,15 +128,11 @@ func (r *Reader) ReadUint64() uint64 {
if r.err != nil {
return 0
}
r.last = time.Now()
s := r.tot
var n int
n, r.err = io.ReadFull(r.r, r.b[:8])
r.tot += n
_, r.err = io.ReadFull(r.r, r.b[:8])
if r.err != nil {
if debug {
dl.Debugf("@0x%x: rd uint64: %v", r.tot, r.err)
dl.Debugf("rd uint64: %v", r.err)
}
return 0
}
@@ -155,19 +141,23 @@ func (r *Reader) ReadUint64() uint64 {
uint64(r.b[3])<<32 | uint64(r.b[2])<<40 | uint64(r.b[1])<<48 | uint64(r.b[0])<<56
if debug {
dl.Debugf("@0x%x: rd uint64=%d (0x%016x)", s, v, v)
dl.Debugf("rd uint64=%d (0x%016x)", v, v)
}
return v
}
func (r *Reader) Tot() int {
return r.tot
type XDRError struct {
op string
err error
}
func (e XDRError) Error() string {
return "xdr " + e.op + ": " + e.err.Error()
}
func (r *Reader) Error() error {
return r.err
}
func (r *Reader) LastRead() time.Time {
return r.last
if r.err == nil {
return nil
}
return XDRError{"read", r.err}
}

View File

@@ -4,10 +4,7 @@
package xdr
import (
"io"
"time"
)
import "io"
func pad(l int) int {
d := l % 4
@@ -20,11 +17,10 @@ func pad(l int) int {
var padBytes = []byte{0, 0, 0}
type Writer struct {
w io.Writer
tot int
err error
b [8]byte
last time.Time
w io.Writer
tot int
err error
b [8]byte
}
type AppendWriter []byte
@@ -49,7 +45,6 @@ func (w *Writer) WriteBytes(bs []byte) (int, error) {
return 0, w.err
}
w.last = time.Now()
w.WriteUint32(uint32(len(bs)))
if w.err != nil {
return 0, w.err
@@ -93,7 +88,6 @@ func (w *Writer) WriteUint32(v uint32) (int, error) {
return 0, w.err
}
w.last = time.Now()
if debug {
dl.Debugf("wr uint32=%d", v)
}
@@ -114,7 +108,6 @@ func (w *Writer) WriteUint64(v uint64) (int, error) {
return 0, w.err
}
w.last = time.Now()
if debug {
dl.Debugf("wr uint64=%d", v)
}
@@ -139,9 +132,8 @@ func (w *Writer) Tot() int {
}
func (w *Writer) Error() error {
return w.err
}
func (w *Writer) LastWrite() time.Time {
return w.last
if w.err == nil {
return nil
}
return XDRError{"write", w.err}
}