Compare 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 15941 additions and 534 deletions
+6 -2
View File
@@ -1,6 +1,6 @@
{ {
"ImportPath": "github.com/calmh/syncthing", "ImportPath": "github.com/calmh/syncthing",
"GoVersion": "devel +6bdbf9086c00 Thu Jul 17 17:02:46 2014 +1000", "GoVersion": "go1.3",
"Packages": [ "Packages": [
"./cmd/..." "./cmd/..."
], ],
@@ -40,6 +40,10 @@
"Comment": "null-15", "Comment": "null-15",
"Rev": "12e4b4183793ac4b061921e7980845e750679fd0" "Rev": "12e4b4183793ac4b061921e7980845e750679fd0"
}, },
{
"ImportPath": "github.com/bkaradzic/go-lz4",
"Rev": "77e2ba877bde9da31213bec75dbbe197fa507c21"
},
{ {
"ImportPath": "github.com/golang/groupcache/lru", "ImportPath": "github.com/golang/groupcache/lru",
"Rev": "8b25adc0f62632c810997cb38c21111a3f256bf4" "Rev": "8b25adc0f62632c810997cb38c21111a3f256bf4"
@@ -50,7 +54,7 @@
}, },
{ {
"ImportPath": "github.com/syndtr/goleveldb/leveldb", "ImportPath": "github.com/syndtr/goleveldb/leveldb",
"Rev": "ba4481e4cb1d45f586e32be2ab663f173b08b207" "Rev": "c5955912e3287376475731c5bc59c79a5a799105"
}, },
{ {
"ImportPath": "github.com/vitrun/qart/coding", "ImportPath": "github.com/vitrun/qart/coding",
@@ -0,0 +1 @@
/lz4-example/lz4-example
+7
View File
@@ -0,0 +1,7 @@
language: go
go:
- 1.1
- 1.2
- 1.3
- tip
+24
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.
+71
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.
+74
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)
}
}
+86
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)
}
}
+63
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)
}
}
+194
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)
}
}
File diff suppressed because it is too large Load Diff
+188
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
}
}
@@ -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
}
@@ -4,7 +4,7 @@
// Use of this source code is governed by a BSD-style license that can be // Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file. // found in the LICENSE file.
// +build darwin freebsd linux netbsd openbsd // +build darwin dragonfly freebsd linux netbsd openbsd
package storage package storage
+20 -5
View File
File diff suppressed because one or more lines are too long
+11 -1
View File
@@ -102,6 +102,12 @@ xdr() {
done done
} }
transifex() {
pushd gui
go run ../cmd/transifexdl/main.go
popd
}
case "$1" in case "$1" in
"") "")
shift shift
@@ -143,7 +149,7 @@ case "$1" in
test || exit 1 test || exit 1
assets 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 GOOS=${os%-*}
export GOARCH=${os#*-} export GOARCH=${os#*-}
@@ -208,6 +214,10 @@ case "$1" in
xdr xdr
;; ;;
transifex)
transifex
;;
*) *)
echo "Unknown build parameter $1" echo "Unknown build parameter $1"
;; ;;
+88 -18
View File
@@ -47,7 +47,7 @@ var (
static func(http.ResponseWriter, *http.Request, *log.Logger) static func(http.ResponseWriter, *http.Request, *log.Logger)
apiKey string apiKey string
modt = time.Now().UTC().Format(http.TimeFormat) modt = time.Now().UTC().Format(http.TimeFormat)
eventSub = events.NewBufferedSubscription(events.Default.Subscribe(events.AllEvents), 1000) eventSub *events.BufferedSubscription
) )
const ( const (
@@ -56,6 +56,8 @@ const (
func init() { func init() {
l.AddHandler(logger.LevelWarn, showGuiError) 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 { 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 // The GET handlers
getRestMux := http.NewServeMux() 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", withModel(m, restGetModel))
getRestMux.HandleFunc("/rest/model/version", withModel(m, restGetModelVersion)) getRestMux.HandleFunc("/rest/model/version", withModel(m, restGetModelVersion))
getRestMux.HandleFunc("/rest/need", withModel(m, restGetNeed)) 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/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 // The POST handlers
postRestMux := http.NewServeMux() postRestMux := http.NewServeMux()
postRestMux.HandleFunc("/rest/config", withModel(m, restPostConfig)) postRestMux.HandleFunc("/rest/config", withModel(m, restPostConfig))
postRestMux.HandleFunc("/rest/restart", restPostRestart) postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint)
postRestMux.HandleFunc("/rest/reset", restPostReset)
postRestMux.HandleFunc("/rest/shutdown", restPostShutdown)
postRestMux.HandleFunc("/rest/error", restPostError) postRestMux.HandleFunc("/rest/error", restPostError)
postRestMux.HandleFunc("/rest/error/clear", restClearErrors) postRestMux.HandleFunc("/rest/error/clear", restClearErrors)
postRestMux.HandleFunc("/rest/discovery/hint", restPostDiscoveryHint)
postRestMux.HandleFunc("/rest/model/override", withModel(m, restPostOverride)) 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) postRestMux.HandleFunc("/rest/upgrade", restPostUpgrade)
// A handler that splits requests between the two above and disables // 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)) 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) { func restGetModelVersion(m *model.Model, w http.ResponseWriter, r *http.Request) {
var qs = r.URL.Query() var qs = r.URL.Query()
var repo = qs.Get("repo") var repo = qs.Get("repo")
@@ -287,6 +313,7 @@ func restPostConfig(m *model.Model, w http.ResponseWriter, r *http.Request) {
nm := newCfg.NodeMap() nm := newCfg.NodeMap()
for k := range om { for k := range om {
if _, ok := nm[k]; !ok { if _, ok := nm[k]; !ok {
// A node was removed and another added
configInSync = false configInSync = false
break break
} }
@@ -421,11 +448,18 @@ func restGetReport(m *model.Model, w http.ResponseWriter, r *http.Request) {
func restGetEvents(w http.ResponseWriter, r *http.Request) { func restGetEvents(w http.ResponseWriter, r *http.Request) {
qs := r.URL.Query() qs := r.URL.Query()
ts := qs.Get("since") sinceStr := qs.Get("since")
since, _ := strconv.Atoi(ts) 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") 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) { 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) { func restPostUpgrade(w http.ResponseWriter, r *http.Request) {
err := upgrade() err := upgrade()
if err != nil { if err != nil {
@@ -483,6 +528,31 @@ func getQR(w http.ResponseWriter, r *http.Request) {
w.Write(code.PNG()) 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 { func basicAuthMiddleware(username string, passhash string, next http.Handler) http.Handler {
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if validAPIKey(r.Header.Get("X-API-Key")) { if validAPIKey(r.Header.Get("X-API-Key")) {
+20 -10
View File
@@ -107,6 +107,7 @@ The following enviroment variables are interpreted by syncthing:
facility strings: facility strings:
- "beacon" (the beacon package) - "beacon" (the beacon package)
- "discover" (the discover package) - "discover" (the discover package)
- "events" (the events package)
- "files" (the files package) - "files" (the files package)
- "net" (the main package; connections & network messages) - "net" (the main package; connections & network messages)
- "model" (the model package) - "model" (the model package)
@@ -307,20 +308,29 @@ nextRepo:
repo.Directory = expandTilde(repo.Directory) repo.Directory = expandTilde(repo.Directory)
// Safety check. If the cached index contains files but the repository fi, err := os.Stat(repo.Directory)
// doesn't exist, we have a problem. We would assume that all files if m.LocalVersion(repo.ID) > 0 {
// have been deleted which might not be the case, so abort instead. // Safety check. If the cached index contains files but the
// repository doesn't exist, we have a problem. We would assume
id := fmt.Sprintf("%x", sha1.Sum([]byte(repo.Directory))) // that all files have been deleted which might not be the case,
idxFile := filepath.Join(confDir, id+".idx.gz") // so mark it as invalid instead.
if _, err := os.Stat(idxFile); err == nil { if err != nil || !fi.IsDir() {
if fi, err := os.Stat(repo.Directory); err != nil || !fi.IsDir() {
cfg.Repositories[i].Invalid = "repo directory missing" cfg.Repositories[i].Invalid = "repo directory missing"
continue nextRepo 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) m.AddRepo(repo)
} }
@@ -644,7 +654,7 @@ next:
wr = &limitedWriter{conn, rateBucket} wr = &limitedWriter{conn, rateBucket}
} }
name := fmt.Sprintf("%s-%s", conn.LocalAddr(), conn.RemoteAddr()) 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) l.Infof("Established secure connection to %s at %s", remoteID, name)
if debugNet { if debugNet {
+16 -5
View File
@@ -93,13 +93,24 @@ func currentRelease() (githubRelease, error) {
json.NewDecoder(resp.Body).Decode(&rels) json.NewDecoder(resp.Body).Decode(&rels)
resp.Body.Close() resp.Body.Close()
for _, rel := range rels { if strings.Contains(Version, "-beta") {
if !rel.Prerelease { // We are a beta version. Use whatever we can find that is newer-or-equal than current.
return rel, nil 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) { func readTarGZ(url string, dir string) (string, error) {
+91
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
}
+40 -25
View File
@@ -93,9 +93,10 @@ func (r *RepositoryConfiguration) NodeIDs() []protocol.NodeID {
} }
type NodeConfiguration struct { type NodeConfiguration struct {
NodeID protocol.NodeID `xml:"id,attr"` NodeID protocol.NodeID `xml:"id,attr"`
Name string `xml:"name,attr,omitempty"` Name string `xml:"name,attr,omitempty"`
Addresses []string `xml:"address,omitempty"` Addresses []string `xml:"address,omitempty"`
Compression bool `xml:"compression,attr"`
} }
type OptionsConfiguration struct { 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 this node is present in all relevant places
// Ensure that any loose nodes are not present in the wrong 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) cfg.Nodes = ensureNodePresent(cfg.Nodes, myID)
sort.Sort(NodeConfigurationList(cfg.Nodes))
for i := range cfg.Repositories { for i := range cfg.Repositories {
cfg.Repositories[i].Nodes = ensureNodePresent(cfg.Repositories[i].Nodes, myID) cfg.Repositories[i].Nodes = ensureNodePresent(cfg.Repositories[i].Nodes, myID)
cfg.Repositories[i].Nodes = ensureExistingNodes(cfg.Repositories[i].Nodes, existingNodes) 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 // 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 { func ensureNodePresent(nodes []NodeConfiguration, myID protocol.NodeID) []NodeConfiguration {
var myIDExists bool
for _, node := range nodes { for _, node := range nodes {
if node.NodeID.Equals(myID) { if node.NodeID.Equals(myID) {
myIDExists = true return nodes
break
} }
} }
if !myIDExists { name, _ := os.Hostname()
name, _ := os.Hostname() nodes = append(nodes, NodeConfiguration{
nodes = append(nodes, NodeConfiguration{ NodeID: myID,
NodeID: myID, Name: name,
Name: name, })
})
}
sort.Sort(NodeConfigurationList(nodes))
return nodes return nodes
} }
func ensureExistingNodes(nodes []NodeConfiguration, existingNodes map[protocol.NodeID]bool) []NodeConfiguration { func ensureExistingNodes(nodes []NodeConfiguration, existingNodes map[protocol.NodeID]bool) []NodeConfiguration {
count := len(nodes)
i := 0 i := 0
for _, node := range nodes { loop:
if _, ok := existingNodes[node.NodeID]; !ok { for i < count {
last := len(nodes) - 1 if _, ok := existingNodes[nodes[i].NodeID]; !ok {
nodes[i] = nodes[last] nodes[i] = nodes[count-1]
nodes = nodes[:last] count--
} else { continue loop
i++
} }
i++
} }
return nodes[0:count]
}
sort.Sort(NodeConfigurationList(nodes)) func ensureNoDuplicates(nodes []NodeConfiguration) []NodeConfiguration {
count := len(nodes)
return 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]
} }
+9
View File
@@ -59,6 +59,12 @@ func TestNodeConfig(t *testing.T) {
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two"> <node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
<address>b</address> <address>b</address>
</node> </node>
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
<address>a</address>
</node>
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ" name="node two">
<address>b</address>
</node>
</repository> </repository>
<options> <options>
<readOnly>true</readOnly> <readOnly>true</readOnly>
@@ -69,9 +75,12 @@ func TestNodeConfig(t *testing.T) {
v2data := []byte(` v2data := []byte(`
<configuration version="2"> <configuration version="2">
<repository id="test" directory="~/Sync" ro="true"> <repository id="test" directory="~/Sync" ro="true">
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/> <node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
<node id="C4YBIESWDUAIGU62GOSRXCRAAJDWVE3TKCPMURZE2LH5QHAF576A"/> <node id="C4YBIESWDUAIGU62GOSRXCRAAJDWVE3TKCPMURZE2LH5QHAF576A"/>
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/> <node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
<node id="C4YBIESWDUAIGU62GOSRXCRAAJDWVE3TKCPMURZE2LH5QHAF576A"/>
</repository> </repository>
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one"> <node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
<address>a</address> <address>a</address>
+17
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
)
+17
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 provides event subscription and polling functionality.
package events package events
@@ -93,6 +97,9 @@ func NewLogger() *Logger {
func (l *Logger) Log(t EventType, data interface{}) { func (l *Logger) Log(t EventType, data interface{}) {
l.mutex.Lock() l.mutex.Lock()
if debug {
dl.Debugln("log", l.nextId, t.String(), data)
}
e := Event{ e := Event{
ID: l.nextId, ID: l.nextId,
Time: time.Now(), Time: time.Now(),
@@ -114,6 +121,9 @@ func (l *Logger) Log(t EventType, data interface{}) {
func (l *Logger) Subscribe(mask EventType) *Subscription { func (l *Logger) Subscribe(mask EventType) *Subscription {
l.mutex.Lock() l.mutex.Lock()
if debug {
dl.Debugln("subscribe", mask)
}
s := &Subscription{ s := &Subscription{
mask: mask, mask: mask,
id: l.nextId, id: l.nextId,
@@ -127,6 +137,9 @@ func (l *Logger) Subscribe(mask EventType) *Subscription {
func (l *Logger) Unsubscribe(s *Subscription) { func (l *Logger) Unsubscribe(s *Subscription) {
l.mutex.Lock() l.mutex.Lock()
if debug {
dl.Debugln("unsubsribe")
}
delete(l.subs, s.id) delete(l.subs, s.id)
close(s.events) close(s.events)
l.mutex.Unlock() l.mutex.Unlock()
@@ -136,6 +149,10 @@ func (s *Subscription) Poll(timeout time.Duration) (Event, error) {
s.mutex.Lock() s.mutex.Lock()
defer s.mutex.Unlock() defer s.mutex.Unlock()
if debug {
dl.Debugln("poll", timeout)
}
to := time.After(timeout) to := time.After(timeout)
select { select {
case e, ok := <-s.events: case e, ok := <-s.events:
+4
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 package events_test
import ( import (
+4 -3
View File
@@ -632,9 +632,6 @@ func ldbWithNeed(db *leveldb.DB, repo, node []byte, fn fileIterator) {
if need || !have { if need || !have {
name := globalKeyName(dbi.Key()) 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) fk := nodeKey(repo, vl.versions[0].node, name)
bs, err := snap.Get(fk, nil) bs, err := snap.Get(fk, nil)
if err != nil { if err != nil {
@@ -652,6 +649,10 @@ func ldbWithNeed(db *leveldb.DB, repo, node []byte, fn fileIterator) {
continue 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 { if cont := fn(gf); !cont {
return return
} }
+1 -1
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) { func (s *Set) WithNeed(node protocol.NodeID, fn fileIterator) {
if debug { 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) ldbWithNeed(s.db, []byte(s.repo), node[:], fn)
} }
+295 -127
View File
@@ -9,6 +9,7 @@
var syncthing = angular.module('syncthing', ['pascalprecht.translate']); var syncthing = angular.module('syncthing', ['pascalprecht.translate']);
var urlbase = 'rest'; var urlbase = 'rest';
var validLangs = ["de","en","es","fr","pt","sv"];
syncthing.config(function ($httpProvider, $translateProvider) { syncthing.config(function ($httpProvider, $translateProvider) {
$httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token'; $httpProvider.defaults.xsrfHeaderName = 'X-CSRF-Token';
@@ -18,7 +19,51 @@ syncthing.config(function ($httpProvider, $translateProvider) {
prefix: 'lang-', prefix: 'lang-',
suffix: '.json' 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) { syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $location) {
@@ -26,20 +71,32 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
var getOK = true; var getOK = true;
var restarting = false; var restarting = false;
$scope.connections = {}; $scope.completion = {};
$scope.config = {}; $scope.config = {};
$scope.configInSync = true;
$scope.connections = {};
$scope.errors = [];
$scope.model = {};
$scope.myID = ''; $scope.myID = '';
$scope.nodes = []; $scope.nodes = [];
$scope.configInSync = true;
$scope.protocolChanged = false; $scope.protocolChanged = false;
$scope.errors = [];
$scope.seenError = '';
$scope.model = {};
$scope.repos = {};
$scope.reportData = {}; $scope.reportData = {};
$scope.reportPreview = false; $scope.reportPreview = false;
$scope.repos = {};
$scope.seenError = '';
$scope.upgradeInfo = {}; $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 () { $scope.$on("$locationChangeSuccess", function () {
var lang = $location.search().lang; var lang = $location.search().lang;
if (lang) { if (lang) {
@@ -60,53 +117,136 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
'touch': 'asterisk', 'touch': 'asterisk',
} }
function getSucceeded() { $scope.$on('UIOnline', function (event, arg) {
if (!getOK) { console.log('UIOnline');
$scope.init(); $scope.init();
$('#networkError').modal('hide'); restarting = false;
getOK = true; $('#networkError').modal('hide');
} $('#restarting').modal('hide');
if (restarting) { $('#shutdown').modal('hide');
$scope.init(); });
$('#restarting').modal('hide');
$('#shutdown').modal('hide');
restarting = false;
}
}
function getFailed() { $scope.$on('UIOffline', function (event, arg) {
if (restarting) { console.log('UIOffline');
return; if (!restarting) {
}
if (getOK) {
$('#networkError').modal({backdrop: 'static', keyboard: false}); $('#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) { $http.get(urlbase + '/system').success(function (data) {
getSucceeded(); $scope.myID = data.myID;
$scope.system = data; $scope.system = data;
}).error(function () { console.log("refreshSystem", data);
getFailed();
}); });
Object.keys($scope.repos).forEach(function (id) { }
if (typeof $scope.model[id] === 'undefined') {
// Never fetched before function refreshCompletion(node, repo) {
$http.get(urlbase + '/model?repo=' + encodeURIComponent(id)).success(function (data) { if (node === $scope.myID) {
$scope.model[id] = data; return
}); }
} else {
$http.get(urlbase + '/model/version?repo=' + encodeURIComponent(id)).success(function (data) { var key = "refreshCompletion" + node + repo;
if (data.version > $scope.model[id].version) { if (!debouncedFuncs[key]) {
$http.get(urlbase + '/model?repo=' + encodeURIComponent(id)).success(function (data) { debouncedFuncs[key] = debounce(function () {
$scope.model[id] = data; $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) { $http.get(urlbase + '/connections').success(function (data) {
var now = Date.now(), var now = Date.now(),
td = (now - prevDate) / 1000, td = (now - prevDate) / 1000,
@@ -126,29 +266,83 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
} }
} }
$scope.connections = data; $scope.connections = data;
console.log("refreshConnections", data);
}); });
}
function refreshErrors() {
$http.get(urlbase + '/errors').success(function (data) { $http.get(urlbase + '/errors').success(function (data) {
$scope.errors = 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) { $scope.repoStatus = function (repo) {
if (typeof $scope.model[repo] === 'undefined') { if (typeof $scope.model[repo] === 'undefined') {
return 'Unknown'; return 'unknown';
} }
if ($scope.model[repo].invalid !== '') { if ($scope.model[repo].invalid !== '') {
return 'Stopped'; return 'stopped';
} }
var state = '' + $scope.model[repo].state; return '' + $scope.model[repo].state;
state = state[0].toUpperCase() + state.substr(1);
if (state == "Syncing" || state == "Idle") {
state += " (" + $scope.syncPercentage(repo) + "%)";
}
return state;
}; };
$scope.repoClass = function (repo) { $scope.repoClass = function (repo) {
@@ -182,23 +376,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
return Math.floor(pct); 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) { $scope.nodeIcon = function (nodeCfg) {
var conn = $scope.connections[nodeCfg.NodeID]; if ($scope.connections[nodeCfg.NodeID]) {
if (conn) { if ($scope.completion[nodeCfg.NodeID] && $scope.completion[nodeCfg.NodeID]._total === 100) {
if (conn.Completion === 100) {
return 'ok'; return 'ok';
} else { } else {
return 'refresh'; return 'refresh';
@@ -209,9 +389,8 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
}; };
$scope.nodeClass = function (nodeCfg) { $scope.nodeClass = function (nodeCfg) {
var conn = $scope.connections[nodeCfg.NodeID]; if ($scope.connections[nodeCfg.NodeID]) {
if (conn) { if ($scope.completion[nodeCfg.NodeID] && $scope.completion[nodeCfg.NodeID]._total === 100) {
if (conn.Completion === 100) {
return 'success'; return 'success';
} else { } else {
return 'primary'; return 'primary';
@@ -381,7 +560,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
}; };
$scope.addNode = function () { $scope.addNode = function () {
$scope.currentNode = {AddressesStr: 'dynamic'}; $scope.currentNode = {AddressesStr: 'dynamic', Compression: true};
$scope.editingExisting = false; $scope.editingExisting = false;
$scope.editingSelf = false; $scope.editingSelf = false;
$scope.nodeEditor.$setPristine(); $scope.nodeEditor.$setPristine();
@@ -561,60 +740,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
cfg.APIKey = randomString(30, 32); 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.acceptUR = function () {
$scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version $scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
@@ -726,6 +852,48 @@ function randomString(len, bits)
return outStr.toLowerCase(); 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 () { syncthing.filter('natural', function () {
return function (input, valid) { return function (input, valid) {
return input.toFixed(decimals(input, valid)); return input.toFixed(decimals(input, valid));
+37 -7
View File
@@ -89,6 +89,7 @@
</head> </head>
<body> <body>
<div ng-controller="EventCtrl"></div>
<!-- Top bar --> <!-- Top bar -->
@@ -153,7 +154,19 @@
<h3 class="panel-title"> <h3 class="panel-title">
<a data-toggle="collapse" data-parent="#repositories" href="#repo-{{$index}}"> <a data-toggle="collapse" data-parent="#repositories" href="#repo-{{$index}}">
<span class="glyphicon glyphicon-hdd"></span> {{repo.Directory | shortPath}} <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> </a>
</h3> </h3>
</div> </div>
@@ -174,10 +187,6 @@
<th><span class="glyphicon glyphicon-warning-sign"></span>&emsp;<span translate>Error</span></th> <th><span class="glyphicon glyphicon-warning-sign"></span>&emsp;<span translate>Error</span></th>
<td class="text-right">{{model[repo.ID].invalid}}</td> <td class="text-right">{{model[repo.ID].invalid}}</td>
</tr> </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> <tr>
<th><span class="glyphicon glyphicon-globe"></span>&emsp;<span translate>Global Repository</span></th> <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> <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}}"> <a data-toggle="collapse" data-parent="#nodes" href="#node-{{$index}}">
<span class="glyphicon glyphicon-retweet"></span> <span class="glyphicon glyphicon-retweet"></span>
{{nodeName(nodeCfg)}} {{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> </a>
</h3> </h3>
</div> </div>
@@ -295,7 +312,12 @@
</tr> </tr>
<tr> <tr>
<th><span class="glyphicon glyphicon-comment"></span>&emsp;<span translate>Synchronization</span></th> <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>
<tr> <tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;<span translate>Download Rate</span></th> <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> <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> <p translate class="help-block">Enter comma separated "ip:port" addresses or "dynamic" to perform automatic discovery of the address.</p>
</div> </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> </form>
</div> </div>
<div class="modal-footer"> <div class="modal-footer">
@@ -686,6 +715,7 @@
<li>Aaron Bieber</li> <li>Aaron Bieber</li>
<li>Andrew Dunham</li> <li>Andrew Dunham</li>
<li>Arthur Axel fREW Schmidt</li> <li>Arthur Axel fREW Schmidt</li>
<li>Audrius Butkevicius</li>
<li>Ben Sidhom</li> <li>Ben Sidhom</li>
<li>Brandon Philips</li> <li>Brandon Philips</li>
</ul> </ul>
+112
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"
}
+9 -1
View File
@@ -13,6 +13,7 @@
"Close": "Close", "Close": "Close",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:", "Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg and the following Contributors:",
"Delete": "Delete", "Delete": "Delete",
"Disconnected": "Disconnected",
"Documentation": "Documentation", "Documentation": "Documentation",
"Download Rate": "Download Rate", "Download Rate": "Download Rate",
"Edit": "Edit", "Edit": "Edit",
@@ -32,6 +33,7 @@
"Generate": "Generate", "Generate": "Generate",
"Global Discovery": "Global Discovery", "Global Discovery": "Global Discovery",
"Global Repository": "Global Repository", "Global Repository": "Global Repository",
"Idle": "Idle",
"Ignore Permissions": "Ignore Permissions", "Ignore Permissions": "Ignore Permissions",
"Keep Versions": "Keep Versions", "Keep Versions": "Keep Versions",
"Latest Release": "Latest Release", "Latest Release": "Latest Release",
@@ -63,6 +65,7 @@
"Restart": "Restart", "Restart": "Restart",
"Restart Needed": "Restart Needed", "Restart Needed": "Restart Needed",
"Save": "Save", "Save": "Save",
"Scanning": "Scanning",
"Select the nodes to share this repository with.": "Select the nodes to share this repository with.", "Select the nodes to share this repository with.": "Select the nodes to share this repository with.",
"Settings": "Settings", "Settings": "Settings",
"Share With Nodes": "Share With Nodes", "Share With Nodes": "Share With Nodes",
@@ -73,9 +76,11 @@
"Shutdown": "Shutdown", "Shutdown": "Shutdown",
"Source Code": "Source Code", "Source Code": "Source Code",
"Start Browser": "Start Browser", "Start Browser": "Start Browser",
"Stopped": "Stopped",
"Support / Forum": "Support / Forum", "Support / Forum": "Support / Forum",
"Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses", "Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses",
"Synchronization": "Synchronization", "Synchronization": "Synchronization",
"Syncing": "Syncing",
"Syncthing has been shut down.": "Syncthing has been shut down.", "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 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…", "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 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 ID must be unique.": "The repository ID must be unique.",
"The repository path cannot be blank.": "The repository path cannot be blank.", "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}}", "Upgrade To {%version%}": "Upgrade To {{version}}",
"Upload Rate": "Upload Rate", "Upload Rate": "Upload Rate",
"Usage": "Usage", "Usage": "Usage",
"Use Compression": "Use Compression",
"Use HTTPS for GUI": "Use HTTPS for GUI", "Use HTTPS for GUI": "Use HTTPS for GUI",
"Version": "Version", "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.", "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", "Yes": "Yes",
"You must keep at least one version.": "You must keep at least one version.", "You must keep at least one version.": "You must keep at least one version.",
"items": "items" "items": "items"
} }
+112
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
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
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
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"
}
+1 -1
View File
@@ -21,7 +21,7 @@
<localAnnounceEnabled>true</localAnnounceEnabled> <localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort> <localAnnouncePort>21025</localAnnouncePort>
<parallelRequests>16</parallelRequests> <parallelRequests>16</parallelRequests>
<maxSendKbps>1000</maxSendKbps> <maxSendKbps>500</maxSendKbps>
<rescanIntervalS>10</rescanIntervalS> <rescanIntervalS>10</rescanIntervalS>
<reconnectionIntervalS>5</reconnectionIntervalS> <reconnectionIntervalS>5</reconnectionIntervalS>
<maxChangeKbps>10000</maxChangeKbps> <maxChangeKbps>10000</maxChangeKbps>
+53 -8
View File
@@ -10,7 +10,8 @@ import (
"crypto/rand" "crypto/rand"
"flag" "flag"
"fmt" "fmt"
"io/ioutil" "io"
"log"
mr "math/rand" mr "math/rand"
"os" "os"
"path/filepath" "path/filepath"
@@ -26,29 +27,73 @@ func name() string {
func main() { func main() {
var files int var files int
var maxexp int var maxexp int
var srcname string
flag.IntVar(&files, "files", 1000, "Number of files") flag.IntVar(&files, "files", 1000, "Number of files")
flag.IntVar(&maxexp, "maxexp", 20, "Maximum file size (max = 2^n + 128*1024 B)") 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() flag.Parse()
fd, err := os.Open(srcname)
if err != nil {
log.Fatal(err)
}
for i := 0; i < files; i++ { for i := 0; i < files; i++ {
n := name() n := name()
p0 := filepath.Join(string(n[0]), n[0:2]) 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)) s := 1 << uint(mr.Intn(maxexp))
a := 128 * 1024 a := 128 * 1024
if a > s { if a > s {
a = s a = s
} }
s += mr.Intn(a) 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) 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
}
+13 -12
View File
@@ -1,36 +1,36 @@
<configuration version="2"> <configuration version="2">
<repository id="default" directory="s1" ro="false" ignorePerms="false"> <repository id="default" directory="s1" ro="false" ignorePerms="false">
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA"></node> <node id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></node>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node> <node id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node> <node id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU"></node>
<versioning></versioning> <versioning></versioning>
</repository> </repository>
<repository id="s12" directory="s12-1" ro="false" ignorePerms="false"> <repository id="s12" directory="s12-1" ro="false" ignorePerms="false">
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA"></node> <node id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ"></node> <node id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></node>
<versioning></versioning> <versioning></versioning>
</repository> </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> <address>127.0.0.1:22001</address>
</node> </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> <address>127.0.0.1:22002</address>
</node> </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> <address>127.0.0.1:22003</address>
</node> </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> <address>127.0.0.1:22004</address>
</node> </node>
<gui enabled="true" tls="false"> <gui enabled="true" tls="false">
<address>127.0.0.1:8081</address> <address>127.0.0.1:8081</address>
<apikey>abc123</apikey>
<user>testuser</user> <user>testuser</user>
<password>testpass</password> <password>$2a$10$7tKL5uvLDGn5s2VLPM2yWOK/II45az0mTel8hxAUJDRQN1Tk2QYwu</password>
<apikey>abc123</apikey>
</gui> </gui>
<options> <options>
<listenAddress>127.0.0.1:22001</listenAddress> <listenAddress>127.0.0.1:22001</listenAddress>
<globalAnnounceServer>announce.syncthing.net:22025</globalAnnounceServer> <globalAnnounceServer>announce.syncthing.net:22026</globalAnnounceServer>
<globalAnnounceEnabled>false</globalAnnounceEnabled> <globalAnnounceEnabled>false</globalAnnounceEnabled>
<localAnnounceEnabled>true</localAnnounceEnabled> <localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort> <localAnnouncePort>21025</localAnnouncePort>
@@ -41,5 +41,6 @@
<maxChangeKbps>10000</maxChangeKbps> <maxChangeKbps>10000</maxChangeKbps>
<startBrowser>false</startBrowser> <startBrowser>false</startBrowser>
<upnpEnabled>true</upnpEnabled> <upnpEnabled>true</upnpEnabled>
<urAccepted>-1</urAccepted>
</options> </options>
</configuration> </configuration>
+2 -2
View File
@@ -18,10 +18,10 @@
<versioning></versioning> <versioning></versioning>
<syncorder></syncorder> <syncorder></syncorder>
</repository> </repository>
<node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1"> <node id="I6KAH7666SLLL5PFXSOAUFJCDZYAOMLEKCP2GB3BV5RQST3PSROA" name="s1" compression="true">
<address>127.0.0.1:22001</address> <address>127.0.0.1:22001</address>
</node> </node>
<node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2"> <node id="JMFJCXBGZDE4BOCJE3VF65GYZNAIVJRET3J6HMRAUQIGJOFKNHMQ" name="s2" compression="true">
<address>127.0.0.1:22002</address> <address>127.0.0.1:22002</address>
</node> </node>
<node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3"> <node id="373HSRPQLPNLIJYKZVQFP4PKZ6R2ZE6K3YD442UJHBGBQGWWXAHA" name="s3">
+3 -3
View File
@@ -31,9 +31,9 @@ stop() {
testConvergence() { testConvergence() {
while true ; do while true ; do
sleep 5 sleep 5
s1comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8082/rest/connections" | ./json "$id1/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/connections" | ./json "$id2/Completion") 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/connections" | ./json "$id3/Completion") s3comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8081/rest/debug/peerCompletion" | ./json "$id3")
s1comp=${s1comp:-0} s1comp=${s1comp:-0}
s2comp=${s2comp:-0} s2comp=${s2comp:-0}
s3comp=${s3comp:-0} s3comp=${s3comp:-0}
+2 -2
View File
@@ -46,8 +46,8 @@ setup() {
testConvergence() { testConvergence() {
while true ; do while true ; do
sleep 5 sleep 5
s1comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8082/rest/connections" | ./json "$id1/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/connections" | ./json "$id2/Completion") s2comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8081/rest/debug/peerCompletion" | ./json "$id2")
s1comp=${s1comp:-0} s1comp=${s1comp:-0}
s2comp=${s2comp:-0} s2comp=${s2comp:-0}
tot=$(($s1comp + $s2comp)) tot=$(($s1comp + $s2comp))
+3 -3
View File
@@ -40,9 +40,9 @@ clean() {
testConvergence() { testConvergence() {
while true ; do while true ; do
sleep 5 sleep 5
s1comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8082/rest/connections" | ./json "$id1/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/connections" | ./json "$id2/Completion") 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/connections" | ./json "$id3/Completion") s3comp=$(curl -HX-API-Key:abc123 -s "http://localhost:8081/rest/debug/peerCompletion" | ./json "$id3")
s1comp=${s1comp:-0} s1comp=${s1comp:-0}
s2comp=${s2comp:-0} s2comp=${s2comp:-0}
s3comp=${s3comp:-0} s3comp=${s3comp:-0}
+1 -1
View File
@@ -43,7 +43,7 @@ testConvergence() {
while true ; do while true ; do
sleep 5 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} comp=${comp:-0}
echo $comp / 100 echo $comp / 100
+50 -41
View File
@@ -157,7 +157,6 @@ type ConnectionInfo struct {
protocol.Statistics protocol.Statistics
Address string Address string
ClientVersion string ClientVersion string
Completion int
} }
// ConnectionStats returns a map with connection statistics for each connected node. // 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() 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 res[node.String()] = ci
} }
@@ -234,6 +196,39 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
return res 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) { func sizeOf(fs []protocol.FileInfo) (files, deleted int, bytes int64) {
for _, f := range fs { for _, f := range fs {
fs, de, by := sizeOfFile(f) fs, de, by := sizeOfFile(f)
@@ -586,9 +581,9 @@ func sendIndexes(conn protocol.Connection, repo string, fs *files.Set) {
for err == nil { for err == nil {
if !initial { if !initial {
time.Sleep(5 * time.Second) time.Sleep(5 * time.Second)
} if fs.LocalVersion(protocol.LocalNodeID) <= minLocalVer {
if fs.LocalVersion(protocol.LocalNodeID) <= minLocalVer { continue
continue }
} }
batch := make([]protocol.FileInfo, 0, indexBatchSize) batch := make([]protocol.FileInfo, 0, indexBatchSize)
@@ -769,6 +764,13 @@ func (m *Model) ScanRepo(repo string) error {
batchSize := 100 batchSize := 100
batch := make([]protocol.FileInfo, 0, 00) batch := make([]protocol.FileInfo, 0, 00)
for f := range fchan { 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 { if len(batch) == batchSize {
fs.Update(protocol.LocalNodeID, batch) fs.Update(protocol.LocalNodeID, batch)
batch = batch[:0] batch = batch[:0]
@@ -792,6 +794,13 @@ func (m *Model) ScanRepo(repo string) error {
f.Flags |= protocol.FlagDeleted f.Flags |= protocol.FlagDeleted
f.Version = lamport.Default.Tick(f.Version) f.Version = lamport.Default.Tick(f.Version)
f.LocalVersion = 0 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) batch = append(batch, f)
} }
} }
+54 -7
View File
@@ -64,13 +64,15 @@ Messages
-------- --------
Every message starts with one 32 bit word indicating the message 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 version, type and ID, followed by the length of the message. The header
endian. is in network byte order, i.e. big endian.
0 1 2 3 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 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 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 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. is a protocol error and MUST result in the connection being terminated.
All data following the message header MUST be in XDR (RFC 1014) The Compression bit "C" indicates the compression used for the message.
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 For C=1:
BEP, in XDR naming convention, are the following:
* 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) int -- (unsigned) 32 bit integer
- (unsigned) hyper -- (unsigned) 64 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 The Pong message is sent in response to a Ping. The Pong message has no
contents, but copies the Message ID from the Ping. 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 Sharing Modes
------------- -------------
+15 -2
View File
@@ -7,11 +7,13 @@ package protocol
import ( import (
"io" "io"
"sync/atomic" "sync/atomic"
"time"
) )
type countingReader struct { type countingReader struct {
io.Reader io.Reader
tot uint64 tot uint64 // bytes
last int64 // unix nanos
} }
var ( var (
@@ -23,6 +25,7 @@ func (c *countingReader) Read(bs []byte) (int, error) {
n, err := c.Reader.Read(bs) n, err := c.Reader.Read(bs)
atomic.AddUint64(&c.tot, uint64(n)) atomic.AddUint64(&c.tot, uint64(n))
atomic.AddUint64(&totalIncoming, uint64(n)) atomic.AddUint64(&totalIncoming, uint64(n))
atomic.StoreInt64(&c.last, time.Now().UnixNano())
return n, err return n, err
} }
@@ -30,15 +33,21 @@ func (c *countingReader) Tot() uint64 {
return atomic.LoadUint64(&c.tot) return atomic.LoadUint64(&c.tot)
} }
func (c *countingReader) Last() time.Time {
return time.Unix(0, atomic.LoadInt64(&c.last))
}
type countingWriter struct { type countingWriter struct {
io.Writer io.Writer
tot uint64 tot uint64 // bytes
last int64 // unix nanos
} }
func (c *countingWriter) Write(bs []byte) (int, error) { func (c *countingWriter) Write(bs []byte) (int, error) {
n, err := c.Writer.Write(bs) n, err := c.Writer.Write(bs)
atomic.AddUint64(&c.tot, uint64(n)) atomic.AddUint64(&c.tot, uint64(n))
atomic.AddUint64(&totalOutgoing, uint64(n)) atomic.AddUint64(&totalOutgoing, uint64(n))
atomic.StoreInt64(&c.last, time.Now().UnixNano())
return n, err return n, err
} }
@@ -46,6 +55,10 @@ func (c *countingWriter) Tot() uint64 {
return atomic.LoadUint64(&c.tot) return atomic.LoadUint64(&c.tot)
} }
func (c *countingWriter) Last() time.Time {
return time.Unix(0, atomic.LoadInt64(&c.last))
}
func TotalInOut() (uint64, uint64) { func TotalInOut() (uint64, uint64) {
return atomic.LoadUint64(&totalIncoming), atomic.LoadUint64(&totalOutgoing) return atomic.LoadUint64(&totalIncoming), atomic.LoadUint64(&totalOutgoing)
} }
+14 -7
View File
@@ -7,9 +7,10 @@ package protocol
import "github.com/calmh/syncthing/xdr" import "github.com/calmh/syncthing/xdr"
type header struct { type header struct {
version int version int
msgID int msgID int
msgType int msgType int
compression bool
} }
func (h header) encodeXDR(xw *xdr.Writer) (int, error) { 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 { 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 + return uint32(h.version&0xf)<<28 +
uint32(h.msgID&0xfff)<<16 + uint32(h.msgID&0xfff)<<16 +
uint32(h.msgType&0xff)<<8 uint32(h.msgType&0xff)<<8 +
isComp
} }
func decodeHeader(u uint32) header { func decodeHeader(u uint32) header {
return header{ return header{
version: int(u>>28) & 0xf, version: int(u>>28) & 0xf,
msgID: int(u>>16) & 0xfff, msgID: int(u>>16) & 0xfff,
msgType: int(u>>8) & 0xff, msgType: int(u>>8) & 0xff,
compression: u&1 == 1,
} }
} }
+119
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
}
+60
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")
}
}
}
}
+13 -3
View File
@@ -7,8 +7,8 @@ package protocol
import "fmt" import "fmt"
type IndexMessage struct { type IndexMessage struct {
Repository string // max:64 Repository string // max:64
Files []FileInfo // max:10000000 Files []FileInfo
} }
type FileInfo struct { type FileInfo struct {
@@ -17,7 +17,7 @@ type FileInfo struct {
Modified int64 Modified int64
Version uint64 Version uint64
LocalVersion uint64 LocalVersion uint64
Blocks []BlockInfo // max:1000000 Blocks []BlockInfo
} }
func (f FileInfo) String() string { func (f FileInfo) String() string {
@@ -49,6 +49,10 @@ type RequestMessage struct {
Size uint32 Size uint32
} }
type ResponseMessage struct {
Data []byte
}
type ClusterConfigMessage struct { type ClusterConfigMessage struct {
ClientName string // max:64 ClientName string // max:64
ClientVersion string // max:64 ClientVersion string // max:64
@@ -71,3 +75,9 @@ type Option struct {
Key string // max:64 Key string // max:64
Value string // max:1024 Value string // max:1024
} }
type CloseMessage struct {
Reason string // max:1024
}
type EmptyMessage struct{}
+170 -14
View File
@@ -38,7 +38,7 @@ IndexMessage Structure:
struct IndexMessage { struct IndexMessage {
string Repository<64>; 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 return xw.Tot(), xdr.ErrElementSizeExceeded
} }
xw.WriteString(o.Repository) xw.WriteString(o.Repository)
if len(o.Files) > 10000000 {
return xw.Tot(), xdr.ErrElementSizeExceeded
}
xw.WriteUint32(uint32(len(o.Files))) xw.WriteUint32(uint32(len(o.Files)))
for i := range o.Files { for i := range o.Files {
o.Files[i].encodeXDR(xw) o.Files[i].encodeXDR(xw)
@@ -88,9 +85,6 @@ func (o *IndexMessage) UnmarshalXDR(bs []byte) error {
func (o *IndexMessage) decodeXDR(xr *xdr.Reader) error { func (o *IndexMessage) decodeXDR(xr *xdr.Reader) error {
o.Repository = xr.ReadStringMax(64) o.Repository = xr.ReadStringMax(64)
_FilesSize := int(xr.ReadUint32()) _FilesSize := int(xr.ReadUint32())
if _FilesSize > 10000000 {
return xdr.ErrElementSizeExceeded
}
o.Files = make([]FileInfo, _FilesSize) o.Files = make([]FileInfo, _FilesSize)
for i := range o.Files { for i := range o.Files {
(&o.Files[i]).decodeXDR(xr) (&o.Files[i]).decodeXDR(xr)
@@ -139,7 +133,7 @@ struct FileInfo {
hyper Modified; hyper Modified;
unsigned hyper Version; unsigned hyper Version;
unsigned hyper LocalVersion; 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(uint64(o.Modified))
xw.WriteUint64(o.Version) xw.WriteUint64(o.Version)
xw.WriteUint64(o.LocalVersion) xw.WriteUint64(o.LocalVersion)
if len(o.Blocks) > 1000000 {
return xw.Tot(), xdr.ErrElementSizeExceeded
}
xw.WriteUint32(uint32(len(o.Blocks))) xw.WriteUint32(uint32(len(o.Blocks)))
for i := range o.Blocks { for i := range o.Blocks {
o.Blocks[i].encodeXDR(xw) o.Blocks[i].encodeXDR(xw)
@@ -197,9 +188,6 @@ func (o *FileInfo) decodeXDR(xr *xdr.Reader) error {
o.Version = xr.ReadUint64() o.Version = xr.ReadUint64()
o.LocalVersion = xr.ReadUint64() o.LocalVersion = xr.ReadUint64()
_BlocksSize := int(xr.ReadUint32()) _BlocksSize := int(xr.ReadUint32())
if _BlocksSize > 1000000 {
return xdr.ErrElementSizeExceeded
}
o.Blocks = make([]BlockInfo, _BlocksSize) o.Blocks = make([]BlockInfo, _BlocksSize)
for i := range o.Blocks { for i := range o.Blocks {
(&o.Blocks[i]).decodeXDR(xr) (&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: ClusterConfigMessage Structure:
0 1 2 3 0 1 2 3
@@ -703,3 +749,113 @@ func (o *Option) decodeXDR(xr *xdr.Reader) error {
o.Value = xr.ReadStringMax(1024) o.Value = xr.ReadStringMax(1024)
return xr.Error() 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()
}
+232 -142
View File
@@ -6,16 +6,20 @@ package protocol
import ( import (
"bufio" "bufio"
"encoding/binary"
"encoding/hex"
"errors" "errors"
"fmt" "fmt"
"io" "io"
"sync" "sync"
"time" "time"
"github.com/calmh/syncthing/xdr" lz4 "github.com/bkaradzic/go-lz4"
) )
const BlockSize = 128 * 1024 const (
BlockSize = 128 * 1024
)
const ( const (
messageTypeClusterConfig = 0 messageTypeClusterConfig = 0
@@ -25,6 +29,7 @@ const (
messageTypePing = 4 messageTypePing = 4
messageTypePong = 5 messageTypePong = 5
messageTypeIndexUpdate = 6 messageTypeIndexUpdate = 6
messageTypeClose = 7
) )
const ( const (
@@ -81,21 +86,24 @@ type rawConnection struct {
state int state int
cr *countingReader cr *countingReader
xr *xdr.Reader
cw *countingWriter cw *countingWriter
wb *bufio.Writer wb *bufio.Writer
xw *xdr.Writer
awaiting []chan asyncResult awaiting [4096]chan asyncResult
awaitingMut sync.Mutex awaitingMut sync.Mutex
idxMut sync.Mutex // ensures serialization of Index calls idxMut sync.Mutex // ensures serialization of Index calls
nextID chan int nextID chan int
outbox chan []encodable outbox chan hdrMsg
closed chan struct{} closed chan struct{}
once sync.Once 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 { type asyncResult struct {
@@ -103,32 +111,39 @@ type asyncResult struct {
err error err error
} }
type hdrMsg struct {
hdr header
msg encodable
}
type encodable interface {
AppendXDR([]byte) []byte
}
const ( const (
pingTimeout = 30 * time.Second pingTimeout = 30 * time.Second
pingIdleTime = 60 * 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} cr := &countingReader{Reader: reader}
cw := &countingWriter{Writer: writer} cw := &countingWriter{Writer: writer}
rb := bufio.NewReader(cr) compThres := 1<<31 - 1 // compression disabled
wb := bufio.NewWriterSize(cw, 65536) if compress {
compThres = 128 // compress messages that are 128 bytes long or larger
}
c := rawConnection{ c := rawConnection{
id: nodeID, id: nodeID,
name: name, name: name,
receiver: nativeModel{receiver}, receiver: nativeModel{receiver},
state: stateInitial, state: stateInitial,
cr: cr, cr: cr,
xr: xdr.NewReader(rb), cw: cw,
cw: cw, outbox: make(chan hdrMsg),
wb: wb, nextID: make(chan int),
xw: xdr.NewWriter(wb), closed: make(chan struct{}),
awaiting: make([]chan asyncResult, 0x1000), compressionThreshold: compThres,
outbox: make(chan []encodable),
nextID: make(chan int),
closed: make(chan struct{}),
} }
go c.readerLoop() go c.readerLoop()
@@ -155,7 +170,7 @@ func (c *rawConnection) Index(repo string, idx []FileInfo) error {
default: default:
} }
c.idxMut.Lock() c.idxMut.Lock()
c.send(header{0, -1, messageTypeIndex}, IndexMessage{repo, idx}) c.send(-1, messageTypeIndex, IndexMessage{repo, idx})
c.idxMut.Unlock() c.idxMut.Unlock()
return nil return nil
} }
@@ -168,7 +183,7 @@ func (c *rawConnection) IndexUpdate(repo string, idx []FileInfo) error {
default: default:
} }
c.idxMut.Lock() c.idxMut.Lock()
c.send(header{0, -1, messageTypeIndexUpdate}, IndexMessage{repo, idx}) c.send(-1, messageTypeIndexUpdate, IndexMessage{repo, idx})
c.idxMut.Unlock() c.idxMut.Unlock()
return nil return nil
} }
@@ -190,8 +205,7 @@ func (c *rawConnection) Request(repo string, name string, offset int64, size int
c.awaiting[id] = rc c.awaiting[id] = rc
c.awaitingMut.Unlock() c.awaitingMut.Unlock()
ok := c.send(header{0, id, messageTypeRequest}, ok := c.send(id, messageTypeRequest, RequestMessage{repo, name, uint64(offset), uint32(size)})
RequestMessage{repo, name, uint64(offset), uint32(size)})
if !ok { if !ok {
return nil, ErrClosed 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 // ClusterConfig send the cluster configuration message to the peer and returns any error
func (c *rawConnection) ClusterConfig(config ClusterConfigMessage) { func (c *rawConnection) ClusterConfig(config ClusterConfigMessage) {
c.send(header{0, -1, messageTypeClusterConfig}, config) c.send(-1, messageTypeClusterConfig, config)
} }
func (c *rawConnection) ping() bool { func (c *rawConnection) ping() bool {
@@ -221,7 +235,7 @@ func (c *rawConnection) ping() bool {
c.awaiting[id] = rc c.awaiting[id] = rc
c.awaitingMut.Unlock() c.awaitingMut.Unlock()
ok := c.send(header{0, id, messageTypePing}) ok := c.send(id, messageTypePing, nil)
if !ok { if !ok {
return false return false
} }
@@ -242,169 +256,207 @@ func (c *rawConnection) readerLoop() (err error) {
default: default:
} }
var hdr header hdr, msg, err := c.readMessage()
hdr.decodeXDR(c.xr) if err != nil {
if err := c.xr.Error(); err != nil {
return err return err
} }
if hdr.version != 0 {
return fmt.Errorf("protocol error: %s: unknown message version %#x", c.id, hdr.version)
}
switch hdr.msgType { switch hdr.msgType {
case messageTypeIndex: case messageTypeIndex:
if c.state < stateCCRcvd { if c.state < stateCCRcvd {
return fmt.Errorf("protocol error: index message in state %d", c.state) return fmt.Errorf("protocol error: index message in state %d", c.state)
} }
if err := c.handleIndex(); err != nil { c.handleIndex(msg.(IndexMessage))
return err
}
c.state = stateIdxRcvd c.state = stateIdxRcvd
case messageTypeIndexUpdate: case messageTypeIndexUpdate:
if c.state < stateIdxRcvd { if c.state < stateIdxRcvd {
return fmt.Errorf("protocol error: index update message in state %d", c.state) return fmt.Errorf("protocol error: index update message in state %d", c.state)
} }
if err := c.handleIndexUpdate(); err != nil { c.handleIndexUpdate(msg.(IndexMessage))
return err
}
case messageTypeRequest: case messageTypeRequest:
if c.state < stateIdxRcvd { if c.state < stateIdxRcvd {
return fmt.Errorf("protocol error: request message in state %d", c.state) return fmt.Errorf("protocol error: request message in state %d", c.state)
} }
if err := c.handleRequest(hdr); err != nil { // Requests are handled asynchronously
return err go c.handleRequest(hdr.msgID, msg.(RequestMessage))
}
case messageTypeResponse: case messageTypeResponse:
if c.state < stateIdxRcvd { if c.state < stateIdxRcvd {
return fmt.Errorf("protocol error: response message in state %d", c.state) return fmt.Errorf("protocol error: response message in state %d", c.state)
} }
if err := c.handleResponse(hdr); err != nil { c.handleResponse(hdr.msgID, msg.(ResponseMessage))
return err
}
case messageTypePing: case messageTypePing:
c.send(header{0, hdr.msgID, messageTypePong}) c.send(hdr.msgID, messageTypePong, EmptyMessage{})
case messageTypePong: case messageTypePong:
c.handlePong(hdr) c.handlePong(hdr.msgID)
case messageTypeClusterConfig: case messageTypeClusterConfig:
if c.state != stateInitial { if c.state != stateInitial {
return fmt.Errorf("protocol error: cluster config message in state %d", c.state) return fmt.Errorf("protocol error: cluster config message in state %d", c.state)
} }
if err := c.handleClusterConfig(); err != nil { go c.receiver.ClusterConfig(c.id, msg.(ClusterConfigMessage))
return err
}
c.state = stateCCRcvd c.state = stateCCRcvd
case messageTypeClose:
return errors.New(msg.(CloseMessage).Reason)
default: default:
return fmt.Errorf("protocol error: %s: unknown message type %#x", c.id, hdr.msgType) return fmt.Errorf("protocol error: %s: unknown message type %#x", c.id, hdr.msgType)
} }
} }
} }
func (c *rawConnection) handleIndex() error { func (c *rawConnection) readMessage() (hdr header, msg encodable, err error) {
var im IndexMessage if cap(c.rdbuf0) < 8 {
im.decodeXDR(c.xr) c.rdbuf0 = make([]byte, 8)
if err := c.xr.Error(); err != nil {
return err
} else { } else {
if debug { c.rdbuf0 = c.rdbuf0[:8]
l.Debugf("Index(%v, %v, %d files)", c.id, im.Repository, len(im.Files)) }
} _, err = io.ReadFull(c.cr, c.rdbuf0)
c.receiver.Index(c.id, im.Repository, im.Files) if err != nil {
return
} }
return nil
}
func (c *rawConnection) handleIndexUpdate() error { hdr = decodeHeader(binary.BigEndian.Uint32(c.rdbuf0[0:4]))
var im IndexMessage msglen := int(binary.BigEndian.Uint32(c.rdbuf0[4:8]))
im.decodeXDR(c.xr)
if err := c.xr.Error(); err != nil { if debug {
return err l.Debugf("read header %v (msglen=%d)", hdr, msglen)
}
if cap(c.rdbuf0) < msglen {
c.rdbuf0 = make([]byte, msglen)
} else { } else {
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 { if debug {
l.Debugf("queueing IndexUpdate(%v, %v, %d files)", c.id, im.Repository, len(im.Files)) 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 { func (c *rawConnection) handleIndex(im IndexMessage) {
var req RequestMessage if debug {
req.decodeXDR(c.xr) l.Debugf("Index(%v, %v, %d files)", c.id, im.Repository, len(im.Files))
if err := c.xr.Error(); err != nil {
return err
} }
go c.processRequest(hdr.msgID, req) c.receiver.Index(c.id, im.Repository, im.Files)
return nil
} }
func (c *rawConnection) handleResponse(hdr header) error { func (c *rawConnection) handleIndexUpdate(im IndexMessage) {
data := c.xr.ReadBytesMax(256 * 1024) // Sufficiently larger than max expected block size if debug {
l.Debugf("queueing IndexUpdate(%v, %v, %d files)", c.id, im.Repository, len(im.Files))
if err := c.xr.Error(); err != nil {
return err
} }
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() c.awaitingMut.Lock()
if rc := c.awaiting[hdr.msgID]; rc != nil { if rc := c.awaiting[msgID]; rc != nil {
c.awaiting[hdr.msgID] = nil c.awaiting[msgID] = nil
rc <- asyncResult{data, nil} rc <- asyncResult{resp.Data, nil}
close(rc) close(rc)
} }
c.awaitingMut.Unlock() c.awaitingMut.Unlock()
return nil
} }
func (c *rawConnection) handlePong(hdr header) { func (c *rawConnection) handlePong(msgID int) {
c.awaitingMut.Lock() c.awaitingMut.Lock()
if rc := c.awaiting[hdr.msgID]; rc != nil { if rc := c.awaiting[msgID]; rc != nil {
c.awaiting[hdr.msgID] = nil c.awaiting[msgID] = nil
rc <- asyncResult{} rc <- asyncResult{}
close(rc) close(rc)
} }
c.awaitingMut.Unlock() c.awaitingMut.Unlock()
} }
func (c *rawConnection) handleClusterConfig() error { func (c *rawConnection) send(msgID int, msgType int, msg encodable) bool {
var cm ClusterConfigMessage if msgID < 0 {
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 {
select { select {
case id := <-c.nextID: case id := <-c.nextID:
h.msgID = id msgID = id
case <-c.closed: case <-c.closed:
return false return false
} }
} }
msg := append([]encodable{h}, es...)
hdr := header{
version: 0,
msgID: msgID,
msgType: msgType,
}
select { select {
case c.outbox <- msg: case c.outbox <- hdrMsg{hdr, msg}:
return true return true
case <-c.closed: case <-c.closed:
return false return false
@@ -412,13 +464,71 @@ func (c *rawConnection) send(h header, es ...encodable) bool {
} }
func (c *rawConnection) writerLoop() { 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 { for {
var tempBuf []byte
var err error
select { select {
case es := <-c.outbox: case hm := <-c.outbox:
for _, e := range es { if hm.msg != nil {
e.encodeXDR(c.xw) // 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) c.close(err)
return 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) { func (c *rawConnection) close(err error) {
c.once.Do(func() { c.once.Do(func() {
close(c.closed) close(c.closed)
@@ -477,13 +573,13 @@ func (c *rawConnection) pingerLoop() {
for { for {
select { select {
case <-ticker: case <-ticker:
if d := time.Since(c.xr.LastRead()); d < pingIdleTime { if d := time.Since(c.cr.Last()); d < pingIdleTime {
if debug { if debug {
l.Debugln(c.id, "ping skipped after rd", d) l.Debugln(c.id, "ping skipped after rd", d)
} }
continue continue
} }
if d := time.Since(c.xw.LastWrite()); d < pingIdleTime { if d := time.Since(c.cw.Last()); d < pingIdleTime {
if debug { if debug {
l.Debugln(c.id, "ping skipped after wr", d) 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 { type Statistics struct {
At time.Time At time.Time
InBytesTotal uint64 InBytesTotal uint64
+26 -22
View File
@@ -9,6 +9,8 @@ import (
"io" "io"
"testing" "testing"
"testing/quick" "testing/quick"
"github.com/calmh/syncthing/xdr"
) )
var ( var (
@@ -21,7 +23,7 @@ func TestHeaderFunctions(t *testing.T) {
ver = int(uint(ver) % 16) ver = int(uint(ver) % 16)
id = int(uint(id) % 4096) id = int(uint(id) % 4096)
typ = int(uint(typ) % 256) typ = int(uint(typ) % 256)
h0 := header{ver, id, typ} h0 := header{version: ver, msgID: id, msgType: typ}
h1 := decodeHeader(encodeHeader(h0)) h1 := decodeHeader(encodeHeader(h0))
return h0 == h1 return h0 == h1
} }
@@ -35,21 +37,21 @@ func TestHeaderLayout(t *testing.T) {
// Version are the first four bits // Version are the first four bits
e = 0xf0000000 e = 0xf0000000
a = encodeHeader(header{0xf, 0, 0}) a = encodeHeader(header{version: 0xf})
if a != e { if a != e {
t.Errorf("Header layout incorrect; %08x != %08x", a, e) t.Errorf("Header layout incorrect; %08x != %08x", a, e)
} }
// Message ID are the following 12 bits // Message ID are the following 12 bits
e = 0x0fff0000 e = 0x0fff0000
a = encodeHeader(header{0, 0xfff, 0}) a = encodeHeader(header{msgID: 0xfff})
if a != e { if a != e {
t.Errorf("Header layout incorrect; %08x != %08x", a, e) t.Errorf("Header layout incorrect; %08x != %08x", a, e)
} }
// Type are the last 8 bits before reserved // Type are the last 8 bits before reserved
e = 0x0000ff00 e = 0x0000ff00
a = encodeHeader(header{0, 0, 0xff}) a = encodeHeader(header{msgType: 0xff})
if a != e { if a != e {
t.Errorf("Header layout incorrect; %08x != %08x", a, e) t.Errorf("Header layout incorrect; %08x != %08x", a, e)
} }
@@ -59,8 +61,8 @@ func TestPing(t *testing.T) {
ar, aw := io.Pipe() ar, aw := io.Pipe()
br, bw := io.Pipe() br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, nil, "name").(wireFormatConnection).next.(*rawConnection) c0 := NewConnection(c0ID, ar, bw, nil, "name", true).(wireFormatConnection).next.(*rawConnection)
c1 := NewConnection(c1ID, br, aw, nil, "name").(wireFormatConnection).next.(*rawConnection) c1 := NewConnection(c1ID, br, aw, nil, "name", true).(wireFormatConnection).next.(*rawConnection)
if ok := c0.ping(); !ok { if ok := c0.ping(); !ok {
t.Error("c0 ping failed") t.Error("c0 ping failed")
@@ -73,8 +75,8 @@ func TestPing(t *testing.T) {
func TestPingErr(t *testing.T) { func TestPingErr(t *testing.T) {
e := errors.New("something broke") e := errors.New("something broke")
for i := 0; i < 12; i++ { for i := 0; i < 16; i++ {
for j := 0; j < 12; j++ { for j := 0; j < 16; j++ {
m0 := newTestModel() m0 := newTestModel()
m1 := newTestModel() m1 := newTestModel()
@@ -83,13 +85,13 @@ func TestPingErr(t *testing.T) {
eaw := &ErrPipe{PipeWriter: *aw, max: i, err: e} eaw := &ErrPipe{PipeWriter: *aw, max: i, err: e}
ebw := &ErrPipe{PipeWriter: *bw, max: j, err: e} ebw := &ErrPipe{PipeWriter: *bw, max: j, err: e}
c0 := NewConnection(c0ID, ar, ebw, m0, "name").(wireFormatConnection).next.(*rawConnection) c0 := NewConnection(c0ID, ar, ebw, m0, "name", true).(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, eaw, m1, "name") NewConnection(c1ID, br, eaw, m1, "name", true)
res := c0.ping() 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) 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) t.Errorf("Unexpected ping fail; i=%d, j=%d", i, j)
} }
} }
@@ -159,15 +161,16 @@ func TestVersionErr(t *testing.T) {
ar, aw := io.Pipe() ar, aw := io.Pipe()
br, bw := io.Pipe() br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, m0, "name").(wireFormatConnection).next.(*rawConnection) c0 := NewConnection(c0ID, ar, bw, m0, "name", true).(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, aw, m1, "name") NewConnection(c1ID, br, aw, m1, "name", true)
c0.xw.WriteUint32(encodeHeader(header{ w := xdr.NewWriter(c0.cw)
w.WriteUint32(encodeHeader(header{
version: 2, version: 2,
msgID: 0, msgID: 0,
msgType: 0, msgType: 0,
})) }))
c0.flush() w.WriteUint32(0)
if !m1.isClosed() { if !m1.isClosed() {
t.Error("Connection should close due to unknown version") t.Error("Connection should close due to unknown version")
@@ -181,15 +184,16 @@ func TestTypeErr(t *testing.T) {
ar, aw := io.Pipe() ar, aw := io.Pipe()
br, bw := io.Pipe() br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, m0, "name").(wireFormatConnection).next.(*rawConnection) c0 := NewConnection(c0ID, ar, bw, m0, "name", true).(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, aw, m1, "name") NewConnection(c1ID, br, aw, m1, "name", true)
c0.xw.WriteUint32(encodeHeader(header{ w := xdr.NewWriter(c0.cw)
w.WriteUint32(encodeHeader(header{
version: 0, version: 0,
msgID: 0, msgID: 0,
msgType: 42, msgType: 42,
})) }))
c0.flush() w.WriteUint32(0)
if !m1.isClosed() { if !m1.isClosed() {
t.Error("Connection should close due to unknown message type") t.Error("Connection should close due to unknown message type")
@@ -203,8 +207,8 @@ func TestClose(t *testing.T) {
ar, aw := io.Pipe() ar, aw := io.Pipe()
br, bw := io.Pipe() br, bw := io.Pipe()
c0 := NewConnection(c0ID, ar, bw, m0, "name").(wireFormatConnection).next.(*rawConnection) c0 := NewConnection(c0ID, ar, bw, m0, "name", true).(wireFormatConnection).next.(*rawConnection)
NewConnection(c1ID, br, aw, m1, "name") NewConnection(c1ID, br, aw, m1, "name", true)
c0.close(nil) c0.close(nil)
+3 -1
View File
@@ -14,6 +14,8 @@ import (
const StandardBlockSize = 128 * 1024 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. // Blocks returns the blockwise hash of the reader.
func Blocks(r io.Reader, blocksize int) ([]protocol.BlockInfo, error) { func Blocks(r io.Reader, blocksize int) ([]protocol.BlockInfo, error) {
var blocks []protocol.BlockInfo var blocks []protocol.BlockInfo
@@ -44,7 +46,7 @@ func Blocks(r io.Reader, blocksize int) ([]protocol.BlockInfo, error) {
blocks = append(blocks, protocol.BlockInfo{ blocks = append(blocks, protocol.BlockInfo{
Offset: 0, Offset: 0,
Size: 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,
}) })
} }
-6
View File
@@ -167,9 +167,6 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo, ign map[string][
cf := w.CurrentFiler.CurrentFile(rn) cf := w.CurrentFiler.CurrentFile(rn)
permUnchanged := w.IgnorePerms || !protocol.HasPermissionBits(cf.Flags) || PermsEqual(cf.Flags, uint32(info.Mode())) permUnchanged := w.IgnorePerms || !protocol.HasPermissionBits(cf.Flags) || PermsEqual(cf.Flags, uint32(info.Mode()))
if !protocol.IsDeleted(cf.Flags) && protocol.IsDirectory(cf.Flags) && permUnchanged { if !protocol.IsDeleted(cf.Flags) && protocol.IsDirectory(cf.Flags) && permUnchanged {
if debug {
l.Debugln("unchanged:", cf)
}
return nil return nil
} }
} }
@@ -198,9 +195,6 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo, ign map[string][
cf := w.CurrentFiler.CurrentFile(rn) cf := w.CurrentFiler.CurrentFile(rn)
permUnchanged := w.IgnorePerms || !protocol.HasPermissionBits(cf.Flags) || PermsEqual(cf.Flags, uint32(info.Mode())) 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 !protocol.IsDeleted(cf.Flags) && cf.Modified == info.ModTime().Unix() && permUnchanged {
if debug {
l.Debugln("unchanged:", cf)
}
return nil return nil
} }
+24 -34
View File
@@ -7,18 +7,15 @@ package xdr
import ( import (
"errors" "errors"
"io" "io"
"time"
) )
var ErrElementSizeExceeded = errors.New("element size exceeded") var ErrElementSizeExceeded = errors.New("element size exceeded")
type Reader struct { type Reader struct {
r io.Reader r io.Reader
tot int err error
err error b [8]byte
b [8]byte sb []byte
sb []byte
last time.Time
} }
func NewReader(r io.Reader) *Reader { func NewReader(r io.Reader) *Reader {
@@ -63,8 +60,6 @@ func (r *Reader) ReadBytesMaxInto(max int, dst []byte) []byte {
if r.err != nil { if r.err != nil {
return nil return nil
} }
r.last = time.Now()
s := r.tot
l := int(r.ReadUint32()) l := int(r.ReadUint32())
if r.err != nil { 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) n, r.err = io.ReadFull(r.r, dst)
if r.err != nil { if r.err != nil {
if debug { 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 return nil
} }
r.tot += n
if debug { if debug {
if n > maxDebugBytes { 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 { } 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] return dst[:l]
@@ -113,15 +107,11 @@ func (r *Reader) ReadUint32() uint32 {
if r.err != nil { if r.err != nil {
return 0 return 0
} }
r.last = time.Now()
s := r.tot
var n int _, r.err = io.ReadFull(r.r, r.b[:4])
n, r.err = io.ReadFull(r.r, r.b[:4])
r.tot += n
if r.err != nil { if r.err != nil {
if debug { if debug {
dl.Debugf("@0x%x: rd uint32: %v", r.tot, r.err) dl.Debugf("rd uint32: %v", r.err)
} }
return 0 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 v := uint32(r.b[3]) | uint32(r.b[2])<<8 | uint32(r.b[1])<<16 | uint32(r.b[0])<<24
if debug { 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 return v
} }
@@ -138,15 +128,11 @@ func (r *Reader) ReadUint64() uint64 {
if r.err != nil { if r.err != nil {
return 0 return 0
} }
r.last = time.Now()
s := r.tot
var n int _, r.err = io.ReadFull(r.r, r.b[:8])
n, r.err = io.ReadFull(r.r, r.b[:8])
r.tot += n
if r.err != nil { if r.err != nil {
if debug { if debug {
dl.Debugf("@0x%x: rd uint64: %v", r.tot, r.err) dl.Debugf("rd uint64: %v", r.err)
} }
return 0 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 uint64(r.b[3])<<32 | uint64(r.b[2])<<40 | uint64(r.b[1])<<48 | uint64(r.b[0])<<56
if debug { 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 return v
} }
func (r *Reader) Tot() int { type XDRError struct {
return r.tot op string
err error
}
func (e XDRError) Error() string {
return "xdr " + e.op + ": " + e.err.Error()
} }
func (r *Reader) Error() error { func (r *Reader) Error() error {
return r.err if r.err == nil {
} return nil
}
func (r *Reader) LastRead() time.Time { return XDRError{"read", r.err}
return r.last
} }
+9 -17
View File
@@ -4,10 +4,7 @@
package xdr package xdr
import ( import "io"
"io"
"time"
)
func pad(l int) int { func pad(l int) int {
d := l % 4 d := l % 4
@@ -20,11 +17,10 @@ func pad(l int) int {
var padBytes = []byte{0, 0, 0} var padBytes = []byte{0, 0, 0}
type Writer struct { type Writer struct {
w io.Writer w io.Writer
tot int tot int
err error err error
b [8]byte b [8]byte
last time.Time
} }
type AppendWriter []byte type AppendWriter []byte
@@ -49,7 +45,6 @@ func (w *Writer) WriteBytes(bs []byte) (int, error) {
return 0, w.err return 0, w.err
} }
w.last = time.Now()
w.WriteUint32(uint32(len(bs))) w.WriteUint32(uint32(len(bs)))
if w.err != nil { if w.err != nil {
return 0, w.err return 0, w.err
@@ -93,7 +88,6 @@ func (w *Writer) WriteUint32(v uint32) (int, error) {
return 0, w.err return 0, w.err
} }
w.last = time.Now()
if debug { if debug {
dl.Debugf("wr uint32=%d", v) dl.Debugf("wr uint32=%d", v)
} }
@@ -114,7 +108,6 @@ func (w *Writer) WriteUint64(v uint64) (int, error) {
return 0, w.err return 0, w.err
} }
w.last = time.Now()
if debug { if debug {
dl.Debugf("wr uint64=%d", v) dl.Debugf("wr uint64=%d", v)
} }
@@ -139,9 +132,8 @@ func (w *Writer) Tot() int {
} }
func (w *Writer) Error() error { func (w *Writer) Error() error {
return w.err if w.err == nil {
} return nil
}
func (w *Writer) LastWrite() time.Time { return XDRError{"write", w.err}
return w.last
} }