mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-03 11:29:10 -05:00
Compare commits
67 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7c6fb018ca | ||
|
|
9c5c06bf31 | ||
|
|
61e3daaead | ||
|
|
9c0fde795e | ||
|
|
ce4f565e2f | ||
|
|
5369a62fd5 | ||
|
|
b44016ff70 | ||
|
|
9f76c87880 | ||
|
|
42ae2898e1 | ||
|
|
dd649a6be4 | ||
|
|
593f098276 | ||
|
|
4a87221f16 | ||
|
|
7745ed34d3 | ||
|
|
8fe546c4a2 | ||
|
|
381f6aeaf6 | ||
|
|
9154bacced | ||
|
|
dc0dc8efb4 | ||
|
|
b062d5dd7f | ||
|
|
c519e582b5 | ||
|
|
6b9dce36bf | ||
|
|
8e0520887a | ||
|
|
cfd1fdb38e | ||
|
|
c6ba0208d0 | ||
|
|
3d055bbb79 | ||
|
|
dd971b56e5 | ||
|
|
4031f5e24b | ||
|
|
1cd7cc6869 | ||
|
|
9de2864db3 | ||
|
|
c27861cbaf | ||
|
|
c2f75d3689 | ||
|
|
5454ca1cf7 | ||
|
|
8644bf30a9 | ||
|
|
db3341a178 | ||
|
|
e2cb0219c7 | ||
|
|
217f29de76 | ||
|
|
8661afcb4f | ||
|
|
ed07fc0f2c | ||
|
|
4af3f77a9a | ||
|
|
8c4f07ef1b | ||
|
|
1a231d39a5 | ||
|
|
17e3d14272 | ||
|
|
03182c7714 | ||
|
|
963078f6ac | ||
|
|
8356b58b1d | ||
|
|
303ce02271 | ||
|
|
bcdc3ecdae | ||
|
|
b60d648e22 | ||
|
|
7bc36cbbd1 | ||
|
|
04130fcb15 | ||
|
|
52d8e4c691 | ||
|
|
ae0193b724 | ||
|
|
2e1c33206f | ||
|
|
0c642ec7cf | ||
|
|
b3ca96eeba | ||
|
|
ae0e033178 | ||
|
|
a97985b428 | ||
|
|
63c0f11458 | ||
|
|
5b0dd9d3b2 | ||
|
|
8bba82a08d | ||
|
|
51bf15728a | ||
|
|
b336b2c336 | ||
|
|
2331089854 | ||
|
|
8a5a573851 | ||
|
|
6fb05fc82a | ||
|
|
358862c7ad | ||
|
|
4235175966 | ||
|
|
cd433a4f52 |
3
.gitignore
vendored
3
.gitignore
vendored
@@ -5,3 +5,6 @@ stcli.exe
|
||||
*.tar.gz
|
||||
*.zip
|
||||
*.asc
|
||||
*.sublime*
|
||||
discosrv
|
||||
stpidx
|
||||
|
||||
@@ -4,4 +4,5 @@ Brandon Philips <brandon@ifup.org>
|
||||
James Patterson <jamespatterson@operamail.com>
|
||||
Jens Diemer <github.com@jensdiemer.de>
|
||||
Philippe Schommers <philippe@schommers.be>
|
||||
Ryan Sullivan <kayoticsully@gmail.com>
|
||||
Veeti Paananen <veeti.paananen@rojekti.fi>
|
||||
|
||||
15
Godeps/Godeps.json
generated
15
Godeps/Godeps.json
generated
@@ -1,10 +1,9 @@
|
||||
{
|
||||
"ImportPath": "github.com/calmh/syncthing",
|
||||
"GoVersion": "go1.2.1",
|
||||
"GoVersion": "go1.2.2",
|
||||
"Packages": [
|
||||
"./cmd/syncthing",
|
||||
"./cmd/assets",
|
||||
"./cmd/stcli",
|
||||
"./discover/cmd/discosrv"
|
||||
],
|
||||
"Deps": [
|
||||
@@ -49,6 +48,18 @@
|
||||
{
|
||||
"ImportPath": "github.com/juju/ratelimit",
|
||||
"Rev": "cbaa435c80a9716e086f25d409344b26c4039358"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vitrun/qart/coding",
|
||||
"Rev": "ccb109cf25f0cd24474da73b9fee4e7a3e8a8ce0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vitrun/qart/gf256",
|
||||
"Rev": "ccb109cf25f0cd24474da73b9fee4e7a3e8a8ce0"
|
||||
},
|
||||
{
|
||||
"ImportPath": "github.com/vitrun/qart/qr",
|
||||
"Rev": "ccb109cf25f0cd24474da73b9fee4e7a3e8a8ce0"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
815
Godeps/_workspace/src/github.com/vitrun/qart/coding/coding.go
generated
vendored
Normal file
815
Godeps/_workspace/src/github.com/vitrun/qart/coding/coding.go
generated
vendored
Normal file
@@ -0,0 +1,815 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package coding implements low-level QR coding details.
|
||||
package coding
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strconv"
|
||||
"strings"
|
||||
"github.com/vitrun/qart/gf256"
|
||||
)
|
||||
|
||||
// Field is the field for QR error correction.
|
||||
var Field = gf256.NewField(0x11d, 2)
|
||||
|
||||
// A Version represents a QR version.
|
||||
// The version specifies the size of the QR code:
|
||||
// a QR code with version v has 4v+17 pixels on a side.
|
||||
// Versions number from 1 to 40: the larger the version,
|
||||
// the more information the code can store.
|
||||
type Version int
|
||||
|
||||
const MinVersion = 1
|
||||
const MaxVersion = 40
|
||||
|
||||
func (v Version) String() string {
|
||||
return strconv.Itoa(int(v))
|
||||
}
|
||||
|
||||
func (v Version) sizeClass() int {
|
||||
if v <= 9 {
|
||||
return 0
|
||||
}
|
||||
if v <= 26 {
|
||||
return 1
|
||||
}
|
||||
return 2
|
||||
}
|
||||
|
||||
// DataBytes returns the number of data bytes that can be
|
||||
// stored in a QR code with the given version and level.
|
||||
func (v Version) DataBytes(l Level) int {
|
||||
vt := &vtab[v]
|
||||
lev := &vt.level[l]
|
||||
return vt.bytes - lev.nblock*lev.check
|
||||
}
|
||||
|
||||
// Encoding implements a QR data encoding scheme.
|
||||
// The implementations--Numeric, Alphanumeric, and String--specify
|
||||
// the character set and the mapping from UTF-8 to code bits.
|
||||
// The more restrictive the mode, the fewer code bits are needed.
|
||||
type Encoding interface {
|
||||
Check() error
|
||||
Bits(v Version) int
|
||||
Encode(b *Bits, v Version)
|
||||
}
|
||||
|
||||
type Bits struct {
|
||||
b []byte
|
||||
nbit int
|
||||
}
|
||||
|
||||
func (b *Bits) Reset() {
|
||||
b.b = b.b[:0]
|
||||
b.nbit = 0
|
||||
}
|
||||
|
||||
func (b *Bits) Bits() int {
|
||||
return b.nbit
|
||||
}
|
||||
|
||||
func (b *Bits) Bytes() []byte {
|
||||
if b.nbit%8 != 0 {
|
||||
panic("fractional byte")
|
||||
}
|
||||
return b.b
|
||||
}
|
||||
|
||||
func (b *Bits) Append(p []byte) {
|
||||
if b.nbit%8 != 0 {
|
||||
panic("fractional byte")
|
||||
}
|
||||
b.b = append(b.b, p...)
|
||||
b.nbit += 8 * len(p)
|
||||
}
|
||||
|
||||
func (b *Bits) Write(v uint, nbit int) {
|
||||
for nbit > 0 {
|
||||
n := nbit
|
||||
if n > 8 {
|
||||
n = 8
|
||||
}
|
||||
if b.nbit%8 == 0 {
|
||||
b.b = append(b.b, 0)
|
||||
} else {
|
||||
m := -b.nbit & 7
|
||||
if n > m {
|
||||
n = m
|
||||
}
|
||||
}
|
||||
b.nbit += n
|
||||
sh := uint(nbit - n)
|
||||
b.b[len(b.b)-1] |= uint8(v >> sh << uint(-b.nbit&7))
|
||||
v -= v >> sh << sh
|
||||
nbit -= n
|
||||
}
|
||||
}
|
||||
|
||||
// Num is the encoding for numeric data.
|
||||
// The only valid characters are the decimal digits 0 through 9.
|
||||
type Num string
|
||||
|
||||
func (s Num) String() string {
|
||||
return fmt.Sprintf("Num(%#q)", string(s))
|
||||
}
|
||||
|
||||
func (s Num) Check() error {
|
||||
for _, c := range s {
|
||||
if c < '0' || '9' < c {
|
||||
return fmt.Errorf("non-numeric string %#q", string(s))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var numLen = [3]int{10, 12, 14}
|
||||
|
||||
func (s Num) Bits(v Version) int {
|
||||
return 4 + numLen[v.sizeClass()] + (10*len(s)+2)/3
|
||||
}
|
||||
|
||||
func (s Num) Encode(b *Bits, v Version) {
|
||||
b.Write((uint)(1), 4)
|
||||
b.Write(uint(len(s)), numLen[v.sizeClass()])
|
||||
var i int
|
||||
for i = 0; i+3 <= len(s); i += 3 {
|
||||
w := uint(s[i]-'0')*100 + uint(s[i+1]-'0')*10 + uint(s[i+2]-'0')
|
||||
b.Write(w, 10)
|
||||
}
|
||||
switch len(s) - i {
|
||||
case 1:
|
||||
w := uint(s[i] - '0')
|
||||
b.Write(w, 4)
|
||||
case 2:
|
||||
w := uint(s[i]-'0')*10 + uint(s[i+1]-'0')
|
||||
b.Write(w, 7)
|
||||
}
|
||||
}
|
||||
|
||||
// Alpha is the encoding for alphanumeric data.
|
||||
// The valid characters are 0-9A-Z$%*+-./: and space.
|
||||
type Alpha string
|
||||
|
||||
const alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ $%*+-./:"
|
||||
|
||||
func (s Alpha) String() string {
|
||||
return fmt.Sprintf("Alpha(%#q)", string(s))
|
||||
}
|
||||
|
||||
func (s Alpha) Check() error {
|
||||
for _, c := range s {
|
||||
if strings.IndexRune(alphabet, c) < 0 {
|
||||
return fmt.Errorf("non-alphanumeric string %#q", string(s))
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
var alphaLen = [3]int{9, 11, 13}
|
||||
|
||||
func (s Alpha) Bits(v Version) int {
|
||||
return 4 + alphaLen[v.sizeClass()] + (11*len(s)+1)/2
|
||||
}
|
||||
|
||||
func (s Alpha) Encode(b *Bits, v Version) {
|
||||
b.Write((uint)(2), 4)
|
||||
b.Write(uint(len(s)), alphaLen[v.sizeClass()])
|
||||
var i int
|
||||
for i = 0; i+2 <= len(s); i += 2 {
|
||||
w := uint(strings.IndexRune(alphabet, rune(s[i])))*45 +
|
||||
uint(strings.IndexRune(alphabet, rune(s[i+1])))
|
||||
b.Write(w, 11)
|
||||
}
|
||||
|
||||
if i < len(s) {
|
||||
w := uint(strings.IndexRune(alphabet, rune(s[i])))
|
||||
b.Write(w, 6)
|
||||
}
|
||||
}
|
||||
|
||||
// String is the encoding for 8-bit data. All bytes are valid.
|
||||
type String string
|
||||
|
||||
func (s String) String() string {
|
||||
return fmt.Sprintf("String(%#q)", string(s))
|
||||
}
|
||||
|
||||
func (s String) Check() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
var stringLen = [3]int{8, 16, 16}
|
||||
|
||||
func (s String) Bits(v Version) int {
|
||||
return 4 + stringLen[v.sizeClass()] + 8*len(s)
|
||||
}
|
||||
|
||||
func (s String) Encode(b *Bits, v Version) {
|
||||
b.Write((uint)(4), 4)
|
||||
b.Write(uint(len(s)), stringLen[v.sizeClass()])
|
||||
for i := 0; i < len(s); i++ {
|
||||
b.Write(uint(s[i]), 8)
|
||||
}
|
||||
}
|
||||
|
||||
// A Pixel describes a single pixel in a QR code.
|
||||
type Pixel uint32
|
||||
|
||||
const (
|
||||
Black Pixel = 1 << iota
|
||||
Invert
|
||||
)
|
||||
|
||||
func (p Pixel) Offset() uint {
|
||||
return uint(p >> 6)
|
||||
}
|
||||
|
||||
func OffsetPixel(o uint) Pixel {
|
||||
return Pixel(o << 6)
|
||||
}
|
||||
|
||||
func (r PixelRole) Pixel() Pixel {
|
||||
return Pixel(r << 2)
|
||||
}
|
||||
|
||||
func (p Pixel) Role() PixelRole {
|
||||
return PixelRole(p>>2) & 15
|
||||
}
|
||||
|
||||
func (p Pixel) String() string {
|
||||
s := p.Role().String()
|
||||
if p&Black != 0 {
|
||||
s += "+black"
|
||||
}
|
||||
if p&Invert != 0 {
|
||||
s += "+invert"
|
||||
}
|
||||
s += "+" + strconv.FormatUint(uint64(p.Offset()), 10)
|
||||
return s
|
||||
}
|
||||
|
||||
// A PixelRole describes the role of a QR pixel.
|
||||
type PixelRole uint32
|
||||
|
||||
const (
|
||||
_ PixelRole = iota
|
||||
Position // position squares (large)
|
||||
Alignment // alignment squares (small)
|
||||
Timing // timing strip between position squares
|
||||
Format // format metadata
|
||||
PVersion // version pattern
|
||||
Unused // unused pixel
|
||||
Data // data bit
|
||||
Check // error correction check bit
|
||||
Extra
|
||||
)
|
||||
|
||||
var roles = []string{
|
||||
"",
|
||||
"position",
|
||||
"alignment",
|
||||
"timing",
|
||||
"format",
|
||||
"pversion",
|
||||
"unused",
|
||||
"data",
|
||||
"check",
|
||||
"extra",
|
||||
}
|
||||
|
||||
func (r PixelRole) String() string {
|
||||
if Position <= r && r <= Check {
|
||||
return roles[r]
|
||||
}
|
||||
return strconv.Itoa(int(r))
|
||||
}
|
||||
|
||||
// A Level represents a QR error correction level.
|
||||
// From least to most tolerant of errors, they are L, M, Q, H.
|
||||
type Level int
|
||||
|
||||
const (
|
||||
L Level = iota
|
||||
M
|
||||
Q
|
||||
H
|
||||
)
|
||||
|
||||
func (l Level) String() string {
|
||||
if L <= l && l <= H {
|
||||
return "LMQH"[l : l+1]
|
||||
}
|
||||
return strconv.Itoa(int(l))
|
||||
}
|
||||
|
||||
// A Code is a square pixel grid.
|
||||
type Code struct {
|
||||
Bitmap []byte // 1 is black, 0 is white
|
||||
Size int // number of pixels on a side
|
||||
Stride int // number of bytes per row
|
||||
}
|
||||
|
||||
func (c *Code) Black(x, y int) bool {
|
||||
return 0 <= x && x < c.Size && 0 <= y && y < c.Size &&
|
||||
c.Bitmap[y*c.Stride+x/8]&(1<<uint(7-x&7)) != 0
|
||||
}
|
||||
|
||||
// A Mask describes a mask that is applied to the QR
|
||||
// code to avoid QR artifacts being interpreted as
|
||||
// alignment and timing patterns (such as the squares
|
||||
// in the corners). Valid masks are integers from 0 to 7.
|
||||
type Mask int
|
||||
|
||||
// http://www.swetake.com/qr/qr5_en.html
|
||||
var mfunc = []func(int, int) bool{
|
||||
func(i, j int) bool { return (i+j)%2 == 0 },
|
||||
func(i, _ int) bool { return i%2 == 0 },
|
||||
func(_, j int) bool { return j%3 == 0 },
|
||||
func(i, j int) bool { return (i+j)%3 == 0 },
|
||||
func(i, j int) bool { return (i/2+j/3)%2 == 0 },
|
||||
func(i, j int) bool { return i*j%2+i*j%3 == 0 },
|
||||
func(i, j int) bool { return (i*j%2+i*j%3)%2 == 0 },
|
||||
func(i, j int) bool { return (i*j%3+(i+j)%2)%2 == 0 },
|
||||
}
|
||||
|
||||
func (m Mask) Invert(y, x int) bool {
|
||||
if m < 0 {
|
||||
return false
|
||||
}
|
||||
return mfunc[m](y, x)
|
||||
}
|
||||
|
||||
// A Plan describes how to construct a QR code
|
||||
// with a specific version, level, and mask.
|
||||
type Plan struct {
|
||||
Version Version
|
||||
Level Level
|
||||
Mask Mask
|
||||
|
||||
DataBytes int // number of data bytes
|
||||
CheckBytes int // number of error correcting (checksum) bytes
|
||||
Blocks int // number of data blocks
|
||||
|
||||
Pixel [][]Pixel // pixel map
|
||||
}
|
||||
|
||||
// NewPlan returns a Plan for a QR code with the given
|
||||
// version, level, and mask.
|
||||
func NewPlan(version Version, level Level, mask Mask) (*Plan, error) {
|
||||
p, err := vplan(version)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := fplan(level, mask, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := lplan(version, level, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := mplan(mask, p); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return p, nil
|
||||
}
|
||||
|
||||
func (b *Bits) Pad(n int) {
|
||||
if n < 0 {
|
||||
panic("qr: invalid pad size")
|
||||
}
|
||||
if n <= 4 {
|
||||
b.Write((uint)(0), n)
|
||||
} else {
|
||||
b.Write((uint)(0), 4)
|
||||
n -= 4
|
||||
n -= -b.Bits() & 7
|
||||
b.Write((uint)(0), -b.Bits()&7)
|
||||
pad := n / 8
|
||||
for i := 0; i < pad; i += 2 {
|
||||
b.Write((uint)(0xec), 8)
|
||||
if i+1 >= pad {
|
||||
break
|
||||
}
|
||||
b.Write((uint)(0x11), 8)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (b *Bits) AddCheckBytes(v Version, l Level) {
|
||||
nd := v.DataBytes(l)
|
||||
if b.nbit < nd*8 {
|
||||
b.Pad(nd*8 - b.nbit)
|
||||
}
|
||||
if b.nbit != nd*8 {
|
||||
panic("qr: too much data")
|
||||
}
|
||||
|
||||
dat := b.Bytes()
|
||||
vt := &vtab[v]
|
||||
lev := &vt.level[l]
|
||||
db := nd / lev.nblock
|
||||
extra := nd % lev.nblock
|
||||
chk := make([]byte, lev.check)
|
||||
rs := gf256.NewRSEncoder(Field, lev.check)
|
||||
for i := 0; i < lev.nblock; i++ {
|
||||
if i == lev.nblock-extra {
|
||||
db++
|
||||
}
|
||||
rs.ECC(dat[:db], chk)
|
||||
b.Append(chk)
|
||||
dat = dat[db:]
|
||||
}
|
||||
|
||||
if len(b.Bytes()) != vt.bytes {
|
||||
panic("qr: internal error")
|
||||
}
|
||||
}
|
||||
|
||||
func (p *Plan) Encode(text ...Encoding) (*Code, error) {
|
||||
var b Bits
|
||||
for _, t := range text {
|
||||
if err := t.Check(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
t.Encode(&b, p.Version)
|
||||
}
|
||||
if b.Bits() > p.DataBytes*8 {
|
||||
return nil, fmt.Errorf("cannot encode %d bits into %d-bit code", b.Bits(), p.DataBytes*8)
|
||||
}
|
||||
b.AddCheckBytes(p.Version, p.Level)
|
||||
bytes := b.Bytes()
|
||||
|
||||
// Now we have the checksum bytes and the data bytes.
|
||||
// Construct the actual code.
|
||||
c := &Code{Size: len(p.Pixel), Stride: (len(p.Pixel) + 7) &^ 7}
|
||||
c.Bitmap = make([]byte, c.Stride*c.Size)
|
||||
crow := c.Bitmap
|
||||
for _, row := range p.Pixel {
|
||||
for x, pix := range row {
|
||||
switch pix.Role() {
|
||||
case Data, Check:
|
||||
o := pix.Offset()
|
||||
if bytes[o/8]&(1<<uint(7-o&7)) != 0 {
|
||||
pix ^= Black
|
||||
}
|
||||
}
|
||||
if pix&Black != 0 {
|
||||
crow[x/8] |= 1 << uint(7-x&7)
|
||||
}
|
||||
}
|
||||
crow = crow[c.Stride:]
|
||||
}
|
||||
return c, nil
|
||||
}
|
||||
|
||||
// A version describes metadata associated with a version.
|
||||
type version struct {
|
||||
apos int
|
||||
astride int
|
||||
bytes int
|
||||
pattern int
|
||||
level [4]level
|
||||
}
|
||||
|
||||
type level struct {
|
||||
nblock int
|
||||
check int
|
||||
}
|
||||
|
||||
var vtab = []version{
|
||||
{},
|
||||
{100, 100, 26, 0x0, [4]level{{1, 7}, {1, 10}, {1, 13}, {1, 17}}}, // 1
|
||||
{16, 100, 44, 0x0, [4]level{{1, 10}, {1, 16}, {1, 22}, {1, 28}}}, // 2
|
||||
{20, 100, 70, 0x0, [4]level{{1, 15}, {1, 26}, {2, 18}, {2, 22}}}, // 3
|
||||
{24, 100, 100, 0x0, [4]level{{1, 20}, {2, 18}, {2, 26}, {4, 16}}}, // 4
|
||||
{28, 100, 134, 0x0, [4]level{{1, 26}, {2, 24}, {4, 18}, {4, 22}}}, // 5
|
||||
{32, 100, 172, 0x0, [4]level{{2, 18}, {4, 16}, {4, 24}, {4, 28}}}, // 6
|
||||
{20, 16, 196, 0x7c94, [4]level{{2, 20}, {4, 18}, {6, 18}, {5, 26}}}, // 7
|
||||
{22, 18, 242, 0x85bc, [4]level{{2, 24}, {4, 22}, {6, 22}, {6, 26}}}, // 8
|
||||
{24, 20, 292, 0x9a99, [4]level{{2, 30}, {5, 22}, {8, 20}, {8, 24}}}, // 9
|
||||
{26, 22, 346, 0xa4d3, [4]level{{4, 18}, {5, 26}, {8, 24}, {8, 28}}}, // 10
|
||||
{28, 24, 404, 0xbbf6, [4]level{{4, 20}, {5, 30}, {8, 28}, {11, 24}}}, // 11
|
||||
{30, 26, 466, 0xc762, [4]level{{4, 24}, {8, 22}, {10, 26}, {11, 28}}}, // 12
|
||||
{32, 28, 532, 0xd847, [4]level{{4, 26}, {9, 22}, {12, 24}, {16, 22}}}, // 13
|
||||
{24, 20, 581, 0xe60d, [4]level{{4, 30}, {9, 24}, {16, 20}, {16, 24}}}, // 14
|
||||
{24, 22, 655, 0xf928, [4]level{{6, 22}, {10, 24}, {12, 30}, {18, 24}}}, // 15
|
||||
{24, 24, 733, 0x10b78, [4]level{{6, 24}, {10, 28}, {17, 24}, {16, 30}}}, // 16
|
||||
{28, 24, 815, 0x1145d, [4]level{{6, 28}, {11, 28}, {16, 28}, {19, 28}}}, // 17
|
||||
{28, 26, 901, 0x12a17, [4]level{{6, 30}, {13, 26}, {18, 28}, {21, 28}}}, // 18
|
||||
{28, 28, 991, 0x13532, [4]level{{7, 28}, {14, 26}, {21, 26}, {25, 26}}}, // 19
|
||||
{32, 28, 1085, 0x149a6, [4]level{{8, 28}, {16, 26}, {20, 30}, {25, 28}}}, // 20
|
||||
{26, 22, 1156, 0x15683, [4]level{{8, 28}, {17, 26}, {23, 28}, {25, 30}}}, // 21
|
||||
{24, 24, 1258, 0x168c9, [4]level{{9, 28}, {17, 28}, {23, 30}, {34, 24}}}, // 22
|
||||
{28, 24, 1364, 0x177ec, [4]level{{9, 30}, {18, 28}, {25, 30}, {30, 30}}}, // 23
|
||||
{26, 26, 1474, 0x18ec4, [4]level{{10, 30}, {20, 28}, {27, 30}, {32, 30}}}, // 24
|
||||
{30, 26, 1588, 0x191e1, [4]level{{12, 26}, {21, 28}, {29, 30}, {35, 30}}}, // 25
|
||||
{28, 28, 1706, 0x1afab, [4]level{{12, 28}, {23, 28}, {34, 28}, {37, 30}}}, // 26
|
||||
{32, 28, 1828, 0x1b08e, [4]level{{12, 30}, {25, 28}, {34, 30}, {40, 30}}}, // 27
|
||||
{24, 24, 1921, 0x1cc1a, [4]level{{13, 30}, {26, 28}, {35, 30}, {42, 30}}}, // 28
|
||||
{28, 24, 2051, 0x1d33f, [4]level{{14, 30}, {28, 28}, {38, 30}, {45, 30}}}, // 29
|
||||
{24, 26, 2185, 0x1ed75, [4]level{{15, 30}, {29, 28}, {40, 30}, {48, 30}}}, // 30
|
||||
{28, 26, 2323, 0x1f250, [4]level{{16, 30}, {31, 28}, {43, 30}, {51, 30}}}, // 31
|
||||
{32, 26, 2465, 0x209d5, [4]level{{17, 30}, {33, 28}, {45, 30}, {54, 30}}}, // 32
|
||||
{28, 28, 2611, 0x216f0, [4]level{{18, 30}, {35, 28}, {48, 30}, {57, 30}}}, // 33
|
||||
{32, 28, 2761, 0x228ba, [4]level{{19, 30}, {37, 28}, {51, 30}, {60, 30}}}, // 34
|
||||
{28, 24, 2876, 0x2379f, [4]level{{19, 30}, {38, 28}, {53, 30}, {63, 30}}}, // 35
|
||||
{22, 26, 3034, 0x24b0b, [4]level{{20, 30}, {40, 28}, {56, 30}, {66, 30}}}, // 36
|
||||
{26, 26, 3196, 0x2542e, [4]level{{21, 30}, {43, 28}, {59, 30}, {70, 30}}}, // 37
|
||||
{30, 26, 3362, 0x26a64, [4]level{{22, 30}, {45, 28}, {62, 30}, {74, 30}}}, // 38
|
||||
{24, 28, 3532, 0x27541, [4]level{{24, 30}, {47, 28}, {65, 30}, {77, 30}}}, // 39
|
||||
{28, 28, 3706, 0x28c69, [4]level{{25, 30}, {49, 28}, {68, 30}, {81, 30}}}, // 40
|
||||
}
|
||||
|
||||
func grid(siz int) [][]Pixel {
|
||||
m := make([][]Pixel, siz)
|
||||
pix := make([]Pixel, siz*siz)
|
||||
for i := range m {
|
||||
m[i], pix = pix[:siz], pix[siz:]
|
||||
}
|
||||
return m
|
||||
}
|
||||
|
||||
// vplan creates a Plan for the given version.
|
||||
func vplan(v Version) (*Plan, error) {
|
||||
p := &Plan{Version: v}
|
||||
if v < 1 || v > 40 {
|
||||
return nil, fmt.Errorf("invalid QR version %d", int(v))
|
||||
}
|
||||
siz := 17 + int(v)*4
|
||||
m := grid(siz)
|
||||
p.Pixel = m
|
||||
|
||||
// Timing markers (overwritten by boxes).
|
||||
const ti = 6 // timing is in row/column 6 (counting from 0)
|
||||
for i := range m {
|
||||
p := Timing.Pixel()
|
||||
if i&1 == 0 {
|
||||
p |= Black
|
||||
}
|
||||
m[i][ti] = p
|
||||
m[ti][i] = p
|
||||
}
|
||||
|
||||
// Position boxes.
|
||||
posBox(m, 0, 0)
|
||||
posBox(m, siz-7, 0)
|
||||
posBox(m, 0, siz-7)
|
||||
|
||||
// Alignment boxes.
|
||||
info := &vtab[v]
|
||||
for x := 4; x+5 < siz; {
|
||||
for y := 4; y+5 < siz; {
|
||||
// don't overwrite timing markers
|
||||
if (x < 7 && y < 7) || (x < 7 && y+5 >= siz-7) || (x+5 >= siz-7 && y < 7) {
|
||||
} else {
|
||||
alignBox(m, x, y)
|
||||
}
|
||||
if y == 4 {
|
||||
y = info.apos
|
||||
} else {
|
||||
y += info.astride
|
||||
}
|
||||
}
|
||||
if x == 4 {
|
||||
x = info.apos
|
||||
} else {
|
||||
x += info.astride
|
||||
}
|
||||
}
|
||||
|
||||
// Version pattern.
|
||||
pat := vtab[v].pattern
|
||||
if pat != 0 {
|
||||
v := pat
|
||||
for x := 0; x < 6; x++ {
|
||||
for y := 0; y < 3; y++ {
|
||||
p := PVersion.Pixel()
|
||||
if v&1 != 0 {
|
||||
p |= Black
|
||||
}
|
||||
m[siz-11+y][x] = p
|
||||
m[x][siz-11+y] = p
|
||||
v >>= 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// One lonely black pixel
|
||||
m[siz-8][8] = Unused.Pixel() | Black
|
||||
|
||||
return p, nil
|
||||
}
|
||||
|
||||
// fplan adds the format pixels
|
||||
func fplan(l Level, m Mask, p *Plan) error {
|
||||
// Format pixels.
|
||||
fb := uint32(l^1) << 13 // level: L=01, M=00, Q=11, H=10
|
||||
fb |= uint32(m) << 10 // mask
|
||||
const formatPoly = 0x537
|
||||
rem := fb
|
||||
for i := 14; i >= 10; i-- {
|
||||
if rem&(1<<uint(i)) != 0 {
|
||||
rem ^= formatPoly << uint(i-10)
|
||||
}
|
||||
}
|
||||
fb |= rem
|
||||
invert := uint32(0x5412)
|
||||
siz := len(p.Pixel)
|
||||
for i := uint(0); i < 15; i++ {
|
||||
pix := Format.Pixel() + OffsetPixel(i)
|
||||
if (fb>>i)&1 == 1 {
|
||||
pix |= Black
|
||||
}
|
||||
if (invert>>i)&1 == 1 {
|
||||
pix ^= Invert | Black
|
||||
}
|
||||
// top left
|
||||
switch {
|
||||
case i < 6:
|
||||
p.Pixel[i][8] = pix
|
||||
case i < 8:
|
||||
p.Pixel[i+1][8] = pix
|
||||
case i < 9:
|
||||
p.Pixel[8][7] = pix
|
||||
default:
|
||||
p.Pixel[8][14-i] = pix
|
||||
}
|
||||
// bottom right
|
||||
switch {
|
||||
case i < 8:
|
||||
p.Pixel[8][siz-1-int(i)] = pix
|
||||
default:
|
||||
p.Pixel[siz-1-int(14-i)][8] = pix
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// lplan edits a version-only Plan to add information
|
||||
// about the error correction levels.
|
||||
func lplan(v Version, l Level, p *Plan) error {
|
||||
p.Level = l
|
||||
|
||||
nblock := vtab[v].level[l].nblock
|
||||
ne := vtab[v].level[l].check
|
||||
nde := (vtab[v].bytes - ne*nblock) / nblock
|
||||
extra := (vtab[v].bytes - ne*nblock) % nblock
|
||||
dataBits := (nde*nblock + extra) * 8
|
||||
checkBits := ne * nblock * 8
|
||||
|
||||
p.DataBytes = vtab[v].bytes - ne*nblock
|
||||
p.CheckBytes = ne * nblock
|
||||
p.Blocks = nblock
|
||||
|
||||
// Make data + checksum pixels.
|
||||
data := make([]Pixel, dataBits)
|
||||
for i := range data {
|
||||
data[i] = Data.Pixel() | OffsetPixel(uint(i))
|
||||
}
|
||||
check := make([]Pixel, checkBits)
|
||||
for i := range check {
|
||||
check[i] = Check.Pixel() | OffsetPixel(uint(i+dataBits))
|
||||
}
|
||||
|
||||
// Split into blocks.
|
||||
dataList := make([][]Pixel, nblock)
|
||||
checkList := make([][]Pixel, nblock)
|
||||
for i := 0; i < nblock; i++ {
|
||||
// The last few blocks have an extra data byte (8 pixels).
|
||||
nd := nde
|
||||
if i >= nblock-extra {
|
||||
nd++
|
||||
}
|
||||
dataList[i], data = data[0:nd*8], data[nd*8:]
|
||||
checkList[i], check = check[0:ne*8], check[ne*8:]
|
||||
}
|
||||
if len(data) != 0 || len(check) != 0 {
|
||||
panic("data/check math")
|
||||
}
|
||||
|
||||
// Build up bit sequence, taking first byte of each block,
|
||||
// then second byte, and so on. Then checksums.
|
||||
bits := make([]Pixel, dataBits+checkBits)
|
||||
dst := bits
|
||||
for i := 0; i < nde+1; i++ {
|
||||
for _, b := range dataList {
|
||||
if i*8 < len(b) {
|
||||
copy(dst, b[i*8:(i+1)*8])
|
||||
dst = dst[8:]
|
||||
}
|
||||
}
|
||||
}
|
||||
for i := 0; i < ne; i++ {
|
||||
for _, b := range checkList {
|
||||
if i*8 < len(b) {
|
||||
copy(dst, b[i*8:(i+1)*8])
|
||||
dst = dst[8:]
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(dst) != 0 {
|
||||
panic("dst math")
|
||||
}
|
||||
|
||||
// Sweep up pair of columns,
|
||||
// then down, assigning to right then left pixel.
|
||||
// Repeat.
|
||||
// See Figure 2 of http://www.pclviewer.com/rs2/qrtopology.htm
|
||||
siz := len(p.Pixel)
|
||||
rem := make([]Pixel, 7)
|
||||
for i := range rem {
|
||||
rem[i] = Extra.Pixel()
|
||||
}
|
||||
src := append(bits, rem...)
|
||||
for x := siz; x > 0; {
|
||||
for y := siz - 1; y >= 0; y-- {
|
||||
if p.Pixel[y][x-1].Role() == 0 {
|
||||
p.Pixel[y][x-1], src = src[0], src[1:]
|
||||
}
|
||||
if p.Pixel[y][x-2].Role() == 0 {
|
||||
p.Pixel[y][x-2], src = src[0], src[1:]
|
||||
}
|
||||
}
|
||||
x -= 2
|
||||
if x == 7 { // vertical timing strip
|
||||
x--
|
||||
}
|
||||
for y := 0; y < siz; y++ {
|
||||
if p.Pixel[y][x-1].Role() == 0 {
|
||||
p.Pixel[y][x-1], src = src[0], src[1:]
|
||||
}
|
||||
if p.Pixel[y][x-2].Role() == 0 {
|
||||
p.Pixel[y][x-2], src = src[0], src[1:]
|
||||
}
|
||||
}
|
||||
x -= 2
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// mplan edits a version+level-only Plan to add the mask.
|
||||
func mplan(m Mask, p *Plan) error {
|
||||
p.Mask = m
|
||||
for y, row := range p.Pixel {
|
||||
for x, pix := range row {
|
||||
if r := pix.Role(); (r == Data || r == Check || r == Extra) && p.Mask.Invert(y, x) {
|
||||
row[x] ^= Black | Invert
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// posBox draws a position (large) box at upper left x, y.
|
||||
func posBox(m [][]Pixel, x, y int) {
|
||||
pos := Position.Pixel()
|
||||
// box
|
||||
for dy := 0; dy < 7; dy++ {
|
||||
for dx := 0; dx < 7; dx++ {
|
||||
p := pos
|
||||
if dx == 0 || dx == 6 || dy == 0 || dy == 6 || 2 <= dx && dx <= 4 && 2 <= dy && dy <= 4 {
|
||||
p |= Black
|
||||
}
|
||||
m[y+dy][x+dx] = p
|
||||
}
|
||||
}
|
||||
// white border
|
||||
for dy := -1; dy < 8; dy++ {
|
||||
if 0 <= y+dy && y+dy < len(m) {
|
||||
if x > 0 {
|
||||
m[y+dy][x-1] = pos
|
||||
}
|
||||
if x+7 < len(m) {
|
||||
m[y+dy][x+7] = pos
|
||||
}
|
||||
}
|
||||
}
|
||||
for dx := -1; dx < 8; dx++ {
|
||||
if 0 <= x+dx && x+dx < len(m) {
|
||||
if y > 0 {
|
||||
m[y-1][x+dx] = pos
|
||||
}
|
||||
if y+7 < len(m) {
|
||||
m[y+7][x+dx] = pos
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// alignBox draw an alignment (small) box at upper left x, y.
|
||||
func alignBox(m [][]Pixel, x, y int) {
|
||||
// box
|
||||
align := Alignment.Pixel()
|
||||
for dy := 0; dy < 5; dy++ {
|
||||
for dx := 0; dx < 5; dx++ {
|
||||
p := align
|
||||
if dx == 0 || dx == 4 || dy == 0 || dy == 4 || dx == 2 && dy == 2 {
|
||||
p |= Black
|
||||
}
|
||||
m[y+dy][x+dx] = p
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
241
Godeps/_workspace/src/github.com/vitrun/qart/gf256/gf256.go
generated
vendored
Normal file
241
Godeps/_workspace/src/github.com/vitrun/qart/gf256/gf256.go
generated
vendored
Normal file
@@ -0,0 +1,241 @@
|
||||
// Copyright 2010 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// Package gf256 implements arithmetic over the Galois Field GF(256).
|
||||
package gf256
|
||||
|
||||
import "strconv"
|
||||
|
||||
// A Field represents an instance of GF(256) defined by a specific polynomial.
|
||||
type Field struct {
|
||||
log [256]byte // log[0] is unused
|
||||
exp [510]byte
|
||||
}
|
||||
|
||||
// NewField returns a new field corresponding to the polynomial poly
|
||||
// and generator α. The Reed-Solomon encoding in QR codes uses
|
||||
// polynomial 0x11d with generator 2.
|
||||
//
|
||||
// The choice of generator α only affects the Exp and Log operations.
|
||||
func NewField(poly, α int) *Field {
|
||||
if poly < 0x100 || poly >= 0x200 || reducible(poly) {
|
||||
panic("gf256: invalid polynomial: " + strconv.Itoa(poly))
|
||||
}
|
||||
|
||||
var f Field
|
||||
x := 1
|
||||
for i := 0; i < 255; i++ {
|
||||
if x == 1 && i != 0 {
|
||||
panic("gf256: invalid generator " + strconv.Itoa(α) +
|
||||
" for polynomial " + strconv.Itoa(poly))
|
||||
}
|
||||
f.exp[i] = byte(x)
|
||||
f.exp[i+255] = byte(x)
|
||||
f.log[x] = byte(i)
|
||||
x = mul(x, α, poly)
|
||||
}
|
||||
f.log[0] = 255
|
||||
for i := 0; i < 255; i++ {
|
||||
if f.log[f.exp[i]] != byte(i) {
|
||||
panic("bad log")
|
||||
}
|
||||
if f.log[f.exp[i+255]] != byte(i) {
|
||||
panic("bad log")
|
||||
}
|
||||
}
|
||||
for i := 1; i < 256; i++ {
|
||||
if f.exp[f.log[i]] != byte(i) {
|
||||
panic("bad log")
|
||||
}
|
||||
}
|
||||
|
||||
return &f
|
||||
}
|
||||
|
||||
// nbit returns the number of significant in p.
|
||||
func nbit(p int) uint {
|
||||
n := uint(0)
|
||||
for ; p > 0; p >>= 1 {
|
||||
n++
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
// polyDiv divides the polynomial p by q and returns the remainder.
|
||||
func polyDiv(p, q int) int {
|
||||
np := nbit(p)
|
||||
nq := nbit(q)
|
||||
for ; np >= nq; np-- {
|
||||
if p&(1<<(np-1)) != 0 {
|
||||
p ^= q << (np - nq)
|
||||
}
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
// mul returns the product x*y mod poly, a GF(256) multiplication.
|
||||
func mul(x, y, poly int) int {
|
||||
z := 0
|
||||
for x > 0 {
|
||||
if x&1 != 0 {
|
||||
z ^= y
|
||||
}
|
||||
x >>= 1
|
||||
y <<= 1
|
||||
if y&0x100 != 0 {
|
||||
y ^= poly
|
||||
}
|
||||
}
|
||||
return z
|
||||
}
|
||||
|
||||
// reducible reports whether p is reducible.
|
||||
func reducible(p int) bool {
|
||||
// Multiplying n-bit * n-bit produces (2n-1)-bit,
|
||||
// so if p is reducible, one of its factors must be
|
||||
// of np/2+1 bits or fewer.
|
||||
np := nbit(p)
|
||||
for q := 2; q < int(1<<(np/2+1)); q++ {
|
||||
if polyDiv(p, q) == 0 {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Add returns the sum of x and y in the field.
|
||||
func (f *Field) Add(x, y byte) byte {
|
||||
return x ^ y
|
||||
}
|
||||
|
||||
// Exp returns the the base-α exponential of e in the field.
|
||||
// If e < 0, Exp returns 0.
|
||||
func (f *Field) Exp(e int) byte {
|
||||
if e < 0 {
|
||||
return 0
|
||||
}
|
||||
return f.exp[e%255]
|
||||
}
|
||||
|
||||
// Log returns the base-α logarithm of x in the field.
|
||||
// If x == 0, Log returns -1.
|
||||
func (f *Field) Log(x byte) int {
|
||||
if x == 0 {
|
||||
return -1
|
||||
}
|
||||
return int(f.log[x])
|
||||
}
|
||||
|
||||
// Inv returns the multiplicative inverse of x in the field.
|
||||
// If x == 0, Inv returns 0.
|
||||
func (f *Field) Inv(x byte) byte {
|
||||
if x == 0 {
|
||||
return 0
|
||||
}
|
||||
return f.exp[255-f.log[x]]
|
||||
}
|
||||
|
||||
// Mul returns the product of x and y in the field.
|
||||
func (f *Field) Mul(x, y byte) byte {
|
||||
if x == 0 || y == 0 {
|
||||
return 0
|
||||
}
|
||||
return f.exp[int(f.log[x])+int(f.log[y])]
|
||||
}
|
||||
|
||||
// An RSEncoder implements Reed-Solomon encoding
|
||||
// over a given field using a given number of error correction bytes.
|
||||
type RSEncoder struct {
|
||||
f *Field
|
||||
c int
|
||||
gen []byte
|
||||
lgen []byte
|
||||
p []byte
|
||||
}
|
||||
|
||||
func (f *Field) gen(e int) (gen, lgen []byte) {
|
||||
// p = 1
|
||||
p := make([]byte, e+1)
|
||||
p[e] = 1
|
||||
|
||||
for i := 0; i < e; i++ {
|
||||
// p *= (x + Exp(i))
|
||||
// p[j] = p[j]*Exp(i) + p[j+1].
|
||||
c := f.Exp(i)
|
||||
for j := 0; j < e; j++ {
|
||||
p[j] = f.Mul(p[j], c) ^ p[j+1]
|
||||
}
|
||||
p[e] = f.Mul(p[e], c)
|
||||
}
|
||||
|
||||
// lp = log p.
|
||||
lp := make([]byte, e+1)
|
||||
for i, c := range p {
|
||||
if c == 0 {
|
||||
lp[i] = 255
|
||||
} else {
|
||||
lp[i] = byte(f.Log(c))
|
||||
}
|
||||
}
|
||||
|
||||
return p, lp
|
||||
}
|
||||
|
||||
// NewRSEncoder returns a new Reed-Solomon encoder
|
||||
// over the given field and number of error correction bytes.
|
||||
func NewRSEncoder(f *Field, c int) *RSEncoder {
|
||||
gen, lgen := f.gen(c)
|
||||
return &RSEncoder{f: f, c: c, gen: gen, lgen: lgen}
|
||||
}
|
||||
|
||||
// ECC writes to check the error correcting code bytes
|
||||
// for data using the given Reed-Solomon parameters.
|
||||
func (rs *RSEncoder) ECC(data []byte, check []byte) {
|
||||
if len(check) < rs.c {
|
||||
panic("gf256: invalid check byte length")
|
||||
}
|
||||
if rs.c == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
// The check bytes are the remainder after dividing
|
||||
// data padded with c zeros by the generator polynomial.
|
||||
|
||||
// p = data padded with c zeros.
|
||||
var p []byte
|
||||
n := len(data) + rs.c
|
||||
if len(rs.p) >= n {
|
||||
p = rs.p
|
||||
} else {
|
||||
p = make([]byte, n)
|
||||
}
|
||||
copy(p, data)
|
||||
for i := len(data); i < len(p); i++ {
|
||||
p[i] = 0
|
||||
}
|
||||
|
||||
// Divide p by gen, leaving the remainder in p[len(data):].
|
||||
// p[0] is the most significant term in p, and
|
||||
// gen[0] is the most significant term in the generator,
|
||||
// which is always 1.
|
||||
// To avoid repeated work, we store various values as
|
||||
// lv, not v, where lv = log[v].
|
||||
f := rs.f
|
||||
lgen := rs.lgen[1:]
|
||||
for i := 0; i < len(data); i++ {
|
||||
c := p[i]
|
||||
if c == 0 {
|
||||
continue
|
||||
}
|
||||
q := p[i+1:]
|
||||
exp := f.exp[f.log[c]:]
|
||||
for j, lg := range lgen {
|
||||
if lg != 255 { // lgen uses 255 for log 0
|
||||
q[j] ^= exp[lg]
|
||||
}
|
||||
}
|
||||
}
|
||||
copy(check, p[len(data):])
|
||||
rs.p = p
|
||||
}
|
||||
401
Godeps/_workspace/src/github.com/vitrun/qart/qr/png.go
generated
vendored
Normal file
401
Godeps/_workspace/src/github.com/vitrun/qart/qr/png.go
generated
vendored
Normal file
@@ -0,0 +1,401 @@
|
||||
// Copyright 2011 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
package qr
|
||||
|
||||
// PNG writer for QR codes.
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"hash"
|
||||
"hash/crc32"
|
||||
)
|
||||
|
||||
// PNG returns a PNG image displaying the code.
|
||||
//
|
||||
// PNG uses a custom encoder tailored to QR codes.
|
||||
// Its compressed size is about 2x away from optimal,
|
||||
// but it runs about 20x faster than calling png.Encode
|
||||
// on c.Image().
|
||||
func (c *Code) PNG() []byte {
|
||||
var p pngWriter
|
||||
return p.encode(c)
|
||||
}
|
||||
|
||||
type pngWriter struct {
|
||||
tmp [16]byte
|
||||
wctmp [4]byte
|
||||
buf bytes.Buffer
|
||||
zlib bitWriter
|
||||
crc hash.Hash32
|
||||
}
|
||||
|
||||
var pngHeader = []byte("\x89PNG\r\n\x1a\n")
|
||||
|
||||
func (w *pngWriter) encode(c *Code) []byte {
|
||||
scale := c.Scale
|
||||
siz := c.Size
|
||||
|
||||
w.buf.Reset()
|
||||
|
||||
// Header
|
||||
w.buf.Write(pngHeader)
|
||||
|
||||
// Header block
|
||||
binary.BigEndian.PutUint32(w.tmp[0:4], uint32((siz+8)*scale))
|
||||
binary.BigEndian.PutUint32(w.tmp[4:8], uint32((siz+8)*scale))
|
||||
w.tmp[8] = 1 // 1-bit
|
||||
w.tmp[9] = 0 // gray
|
||||
w.tmp[10] = 0
|
||||
w.tmp[11] = 0
|
||||
w.tmp[12] = 0
|
||||
w.writeChunk("IHDR", w.tmp[:13])
|
||||
|
||||
// Comment
|
||||
w.writeChunk("tEXt", comment)
|
||||
|
||||
// Data
|
||||
w.zlib.writeCode(c)
|
||||
w.writeChunk("IDAT", w.zlib.bytes.Bytes())
|
||||
|
||||
// End
|
||||
w.writeChunk("IEND", nil)
|
||||
|
||||
return w.buf.Bytes()
|
||||
}
|
||||
|
||||
var comment = []byte("Software\x00QR-PNG http://qr.swtch.com/")
|
||||
|
||||
func (w *pngWriter) writeChunk(name string, data []byte) {
|
||||
if w.crc == nil {
|
||||
w.crc = crc32.NewIEEE()
|
||||
}
|
||||
binary.BigEndian.PutUint32(w.wctmp[0:4], uint32(len(data)))
|
||||
w.buf.Write(w.wctmp[0:4])
|
||||
w.crc.Reset()
|
||||
copy(w.wctmp[0:4], name)
|
||||
w.buf.Write(w.wctmp[0:4])
|
||||
w.crc.Write(w.wctmp[0:4])
|
||||
w.buf.Write(data)
|
||||
w.crc.Write(data)
|
||||
crc := w.crc.Sum32()
|
||||
binary.BigEndian.PutUint32(w.wctmp[0:4], crc)
|
||||
w.buf.Write(w.wctmp[0:4])
|
||||
}
|
||||
|
||||
func (b *bitWriter) writeCode(c *Code) {
|
||||
const ftNone = 0
|
||||
|
||||
b.adler32.Reset()
|
||||
b.bytes.Reset()
|
||||
b.nbit = 0
|
||||
|
||||
scale := c.Scale
|
||||
siz := c.Size
|
||||
|
||||
// zlib header
|
||||
b.tmp[0] = 0x78
|
||||
b.tmp[1] = 0
|
||||
b.tmp[1] += uint8(31 - (uint16(b.tmp[0])<<8+uint16(b.tmp[1]))%31)
|
||||
b.bytes.Write(b.tmp[0:2])
|
||||
|
||||
// Start flate block.
|
||||
b.writeBits(1, 1, false) // final block
|
||||
b.writeBits(1, 2, false) // compressed, fixed Huffman tables
|
||||
|
||||
// White border.
|
||||
// First row.
|
||||
b.byte(ftNone)
|
||||
n := (scale*(siz+8) + 7) / 8
|
||||
b.byte(255)
|
||||
b.repeat(n-1, 1)
|
||||
// 4*scale rows total.
|
||||
b.repeat((4*scale-1)*(1+n), 1+n)
|
||||
|
||||
for i := 0; i < 4*scale; i++ {
|
||||
b.adler32.WriteNByte(ftNone, 1)
|
||||
b.adler32.WriteNByte(255, n)
|
||||
}
|
||||
|
||||
row := make([]byte, 1+n)
|
||||
for y := 0; y < siz; y++ {
|
||||
row[0] = ftNone
|
||||
j := 1
|
||||
var z uint8
|
||||
nz := 0
|
||||
for x := -4; x < siz+4; x++ {
|
||||
// Raw data.
|
||||
for i := 0; i < scale; i++ {
|
||||
z <<= 1
|
||||
if !c.Black(x, y) {
|
||||
z |= 1
|
||||
}
|
||||
if nz++; nz == 8 {
|
||||
row[j] = z
|
||||
j++
|
||||
nz = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
if j < len(row) {
|
||||
row[j] = z
|
||||
}
|
||||
for _, z := range row {
|
||||
b.byte(z)
|
||||
}
|
||||
|
||||
// Scale-1 copies.
|
||||
b.repeat((scale-1)*(1+n), 1+n)
|
||||
|
||||
b.adler32.WriteN(row, scale)
|
||||
}
|
||||
|
||||
// White border.
|
||||
// First row.
|
||||
b.byte(ftNone)
|
||||
b.byte(255)
|
||||
b.repeat(n-1, 1)
|
||||
// 4*scale rows total.
|
||||
b.repeat((4*scale-1)*(1+n), 1+n)
|
||||
|
||||
for i := 0; i < 4*scale; i++ {
|
||||
b.adler32.WriteNByte(ftNone, 1)
|
||||
b.adler32.WriteNByte(255, n)
|
||||
}
|
||||
|
||||
// End of block.
|
||||
b.hcode(256)
|
||||
b.flushBits()
|
||||
|
||||
// adler32
|
||||
binary.BigEndian.PutUint32(b.tmp[0:], b.adler32.Sum32())
|
||||
b.bytes.Write(b.tmp[0:4])
|
||||
}
|
||||
|
||||
// A bitWriter is a write buffer for bit-oriented data like deflate.
|
||||
type bitWriter struct {
|
||||
bytes bytes.Buffer
|
||||
bit uint32
|
||||
nbit uint
|
||||
|
||||
tmp [4]byte
|
||||
adler32 adigest
|
||||
}
|
||||
|
||||
func (b *bitWriter) writeBits(bit uint32, nbit uint, rev bool) {
|
||||
// reverse, for huffman codes
|
||||
if rev {
|
||||
br := uint32(0)
|
||||
for i := uint(0); i < nbit; i++ {
|
||||
br |= ((bit >> i) & 1) << (nbit - 1 - i)
|
||||
}
|
||||
bit = br
|
||||
}
|
||||
b.bit |= bit << b.nbit
|
||||
b.nbit += nbit
|
||||
for b.nbit >= 8 {
|
||||
b.bytes.WriteByte(byte(b.bit))
|
||||
b.bit >>= 8
|
||||
b.nbit -= 8
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bitWriter) flushBits() {
|
||||
if b.nbit > 0 {
|
||||
b.bytes.WriteByte(byte(b.bit))
|
||||
b.nbit = 0
|
||||
b.bit = 0
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bitWriter) hcode(v int) {
|
||||
/*
|
||||
Lit Value Bits Codes
|
||||
--------- ---- -----
|
||||
0 - 143 8 00110000 through
|
||||
10111111
|
||||
144 - 255 9 110010000 through
|
||||
111111111
|
||||
256 - 279 7 0000000 through
|
||||
0010111
|
||||
280 - 287 8 11000000 through
|
||||
11000111
|
||||
*/
|
||||
switch {
|
||||
case v <= 143:
|
||||
b.writeBits(uint32(v)+0x30, 8, true)
|
||||
case v <= 255:
|
||||
b.writeBits(uint32(v-144)+0x190, 9, true)
|
||||
case v <= 279:
|
||||
b.writeBits(uint32(v-256)+0, 7, true)
|
||||
case v <= 287:
|
||||
b.writeBits(uint32(v-280)+0xc0, 8, true)
|
||||
default:
|
||||
panic("invalid hcode")
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bitWriter) byte(x byte) {
|
||||
b.hcode(int(x))
|
||||
}
|
||||
|
||||
func (b *bitWriter) codex(c int, val int, nx uint) {
|
||||
b.hcode(c + val>>nx)
|
||||
b.writeBits(uint32(val)&(1<<nx-1), nx, false)
|
||||
}
|
||||
|
||||
func (b *bitWriter) repeat(n, d int) {
|
||||
for ; n >= 258+3; n -= 258 {
|
||||
b.repeat1(258, d)
|
||||
}
|
||||
if n > 258 {
|
||||
// 258 < n < 258+3
|
||||
b.repeat1(10, d)
|
||||
b.repeat1(n-10, d)
|
||||
return
|
||||
}
|
||||
if n < 3 {
|
||||
panic("invalid flate repeat")
|
||||
}
|
||||
b.repeat1(n, d)
|
||||
}
|
||||
|
||||
func (b *bitWriter) repeat1(n, d int) {
|
||||
/*
|
||||
Extra Extra Extra
|
||||
Code Bits Length(s) Code Bits Lengths Code Bits Length(s)
|
||||
---- ---- ------ ---- ---- ------- ---- ---- -------
|
||||
257 0 3 267 1 15,16 277 4 67-82
|
||||
258 0 4 268 1 17,18 278 4 83-98
|
||||
259 0 5 269 2 19-22 279 4 99-114
|
||||
260 0 6 270 2 23-26 280 4 115-130
|
||||
261 0 7 271 2 27-30 281 5 131-162
|
||||
262 0 8 272 2 31-34 282 5 163-194
|
||||
263 0 9 273 3 35-42 283 5 195-226
|
||||
264 0 10 274 3 43-50 284 5 227-257
|
||||
265 1 11,12 275 3 51-58 285 0 258
|
||||
266 1 13,14 276 3 59-66
|
||||
*/
|
||||
switch {
|
||||
case n <= 10:
|
||||
b.codex(257, n-3, 0)
|
||||
case n <= 18:
|
||||
b.codex(265, n-11, 1)
|
||||
case n <= 34:
|
||||
b.codex(269, n-19, 2)
|
||||
case n <= 66:
|
||||
b.codex(273, n-35, 3)
|
||||
case n <= 130:
|
||||
b.codex(277, n-67, 4)
|
||||
case n <= 257:
|
||||
b.codex(281, n-131, 5)
|
||||
case n == 258:
|
||||
b.hcode(285)
|
||||
default:
|
||||
panic("invalid repeat length")
|
||||
}
|
||||
|
||||
/*
|
||||
Extra Extra Extra
|
||||
Code Bits Dist Code Bits Dist Code Bits Distance
|
||||
---- ---- ---- ---- ---- ------ ---- ---- --------
|
||||
0 0 1 10 4 33-48 20 9 1025-1536
|
||||
1 0 2 11 4 49-64 21 9 1537-2048
|
||||
2 0 3 12 5 65-96 22 10 2049-3072
|
||||
3 0 4 13 5 97-128 23 10 3073-4096
|
||||
4 1 5,6 14 6 129-192 24 11 4097-6144
|
||||
5 1 7,8 15 6 193-256 25 11 6145-8192
|
||||
6 2 9-12 16 7 257-384 26 12 8193-12288
|
||||
7 2 13-16 17 7 385-512 27 12 12289-16384
|
||||
8 3 17-24 18 8 513-768 28 13 16385-24576
|
||||
9 3 25-32 19 8 769-1024 29 13 24577-32768
|
||||
*/
|
||||
if d <= 4 {
|
||||
b.writeBits(uint32(d-1), 5, true)
|
||||
} else if d <= 32768 {
|
||||
nbit := uint(16)
|
||||
for d <= 1<<(nbit-1) {
|
||||
nbit--
|
||||
}
|
||||
v := uint32(d - 1)
|
||||
v &^= 1 << (nbit - 1) // top bit is implicit
|
||||
code := uint32(2*nbit - 2) // second bit is low bit of code
|
||||
code |= v >> (nbit - 2)
|
||||
v &^= 1 << (nbit - 2)
|
||||
b.writeBits(code, 5, true)
|
||||
// rest of bits follow
|
||||
b.writeBits(uint32(v), nbit-2, false)
|
||||
} else {
|
||||
panic("invalid repeat distance")
|
||||
}
|
||||
}
|
||||
|
||||
func (b *bitWriter) run(v byte, n int) {
|
||||
if n == 0 {
|
||||
return
|
||||
}
|
||||
b.byte(v)
|
||||
if n-1 < 3 {
|
||||
for i := 0; i < n-1; i++ {
|
||||
b.byte(v)
|
||||
}
|
||||
} else {
|
||||
b.repeat(n-1, 1)
|
||||
}
|
||||
}
|
||||
|
||||
type adigest struct {
|
||||
a, b uint32
|
||||
}
|
||||
|
||||
func (d *adigest) Reset() { d.a, d.b = 1, 0 }
|
||||
|
||||
const amod = 65521
|
||||
|
||||
func aupdate(a, b uint32, pi byte, n int) (aa, bb uint32) {
|
||||
// TODO(rsc): 6g doesn't do magic multiplies for b %= amod,
|
||||
// only for b = b%amod.
|
||||
|
||||
// invariant: a, b < amod
|
||||
if pi == 0 {
|
||||
b += uint32(n%amod) * a
|
||||
b = b % amod
|
||||
return a, b
|
||||
}
|
||||
|
||||
// n times:
|
||||
// a += pi
|
||||
// b += a
|
||||
// is same as
|
||||
// b += n*a + n*(n+1)/2*pi
|
||||
// a += n*pi
|
||||
m := uint32(n)
|
||||
b += (m % amod) * a
|
||||
b = b % amod
|
||||
b += (m * (m + 1) / 2) % amod * uint32(pi)
|
||||
b = b % amod
|
||||
a += (m % amod) * uint32(pi)
|
||||
a = a % amod
|
||||
return a, b
|
||||
}
|
||||
|
||||
func afinish(a, b uint32) uint32 {
|
||||
return b<<16 | a
|
||||
}
|
||||
|
||||
func (d *adigest) WriteN(p []byte, n int) {
|
||||
for i := 0; i < n; i++ {
|
||||
for _, pi := range p {
|
||||
d.a, d.b = aupdate(d.a, d.b, pi, 1)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (d *adigest) WriteNByte(pi byte, n int) {
|
||||
d.a, d.b = aupdate(d.a, d.b, pi, n)
|
||||
}
|
||||
|
||||
func (d *adigest) Sum32() uint32 { return afinish(d.a, d.b) }
|
||||
|
||||
109
Godeps/_workspace/src/github.com/vitrun/qart/qr/qr.go
generated
vendored
Normal file
109
Godeps/_workspace/src/github.com/vitrun/qart/qr/qr.go
generated
vendored
Normal file
@@ -0,0 +1,109 @@
|
||||
package qr
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"image"
|
||||
"image/color"
|
||||
"github.com/vitrun/qart/coding"
|
||||
)
|
||||
|
||||
// A Level denotes a QR error correction level.
|
||||
// From least to most tolerant of errors, they are L, M, Q, H.
|
||||
type Level int
|
||||
|
||||
const (
|
||||
L Level = iota // 20% redundant
|
||||
M // 38% redundant
|
||||
Q // 55% redundant
|
||||
H // 65% redundant
|
||||
)
|
||||
|
||||
// Encode returns an encoding of text at the given error correction level.
|
||||
func Encode(text string, level Level) (*Code, error) {
|
||||
// Pick data encoding, smallest first.
|
||||
// We could split the string and use different encodings
|
||||
// but that seems like overkill for now.
|
||||
var enc coding.Encoding
|
||||
switch {
|
||||
case coding.Num(text).Check() == nil:
|
||||
enc = coding.Num(text)
|
||||
case coding.Alpha(text).Check() == nil:
|
||||
enc = coding.Alpha(text)
|
||||
default:
|
||||
enc = coding.String(text)
|
||||
}
|
||||
|
||||
// Pick size.
|
||||
l := coding.Level(level)
|
||||
var v coding.Version
|
||||
for v = coding.MinVersion; ; v++ {
|
||||
if v > coding.MaxVersion {
|
||||
return nil, errors.New("text too long to encode as QR")
|
||||
}
|
||||
if enc.Bits(v) <= v.DataBytes(l)*8 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Build and execute plan.
|
||||
p, err := coding.NewPlan(v, l, 0)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cc, err := p.Encode(enc)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// TODO: Pick appropriate mask.
|
||||
|
||||
return &Code{cc.Bitmap, cc.Size, cc.Stride, 8}, nil
|
||||
}
|
||||
|
||||
// A Code is a square pixel grid.
|
||||
// It implements image.Image and direct PNG encoding.
|
||||
type Code struct {
|
||||
Bitmap []byte // 1 is black, 0 is white
|
||||
Size int // number of pixels on a side
|
||||
Stride int // number of bytes per row
|
||||
Scale int // number of image pixels per QR pixel
|
||||
}
|
||||
|
||||
// Black returns true if the pixel at (x,y) is black.
|
||||
func (c *Code) Black(x, y int) bool {
|
||||
return 0 <= x && x < c.Size && 0 <= y && y < c.Size &&
|
||||
c.Bitmap[y*c.Stride+x/8]&(1<<uint(7-x&7)) != 0
|
||||
}
|
||||
|
||||
// Image returns an Image displaying the code.
|
||||
func (c *Code) Image() image.Image {
|
||||
return &codeImage{c}
|
||||
|
||||
}
|
||||
|
||||
// codeImage implements image.Image
|
||||
type codeImage struct {
|
||||
*Code
|
||||
}
|
||||
|
||||
var (
|
||||
whiteColor color.Color = color.Gray{0xFF}
|
||||
blackColor color.Color = color.Gray{0x00}
|
||||
)
|
||||
|
||||
func (c *codeImage) Bounds() image.Rectangle {
|
||||
d := (c.Size + 8) * c.Scale
|
||||
return image.Rect(0, 0, d, d)
|
||||
}
|
||||
|
||||
func (c *codeImage) At(x, y int) color.Color {
|
||||
if c.Black(x, y) {
|
||||
return blackColor
|
||||
}
|
||||
return whiteColor
|
||||
}
|
||||
|
||||
func (c *codeImage) ColorModel() color.Model {
|
||||
return color.GrayModel
|
||||
}
|
||||
|
||||
151
Godeps/_workspace/src/github.com/vitrun/qart/qr/resize.go
generated
vendored
Normal file
151
Godeps/_workspace/src/github.com/vitrun/qart/qr/resize.go
generated
vendored
Normal file
@@ -0,0 +1,151 @@
|
||||
package qr
|
||||
|
||||
import (
|
||||
"image"
|
||||
"image/color"
|
||||
)
|
||||
|
||||
// average convert the sums to averages and returns the result.
|
||||
func average(sum []uint64, w, h int, n uint64) *image.RGBA {
|
||||
ret := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
index := 4 * (y*w + x)
|
||||
pix := ret.Pix[y*ret.Stride+x*4:]
|
||||
pix[0] = uint8(sum[index+0] / n)
|
||||
pix[1] = uint8(sum[index+1] / n)
|
||||
pix[2] = uint8(sum[index+2] / n)
|
||||
pix[3] = uint8(sum[index+3] / n)
|
||||
}
|
||||
}
|
||||
return ret
|
||||
}
|
||||
|
||||
|
||||
// ResizeRGBA returns a scaled copy of the RGBA image slice r of m.
|
||||
// The returned image has width w and height h.
|
||||
func ResizeRGBA(m *image.RGBA, r image.Rectangle, w, h int) *image.RGBA {
|
||||
ww, hh := uint64(w), uint64(h)
|
||||
dx, dy := uint64(r.Dx()), uint64(r.Dy())
|
||||
// See comment in Resize.
|
||||
n, sum := dx*dy, make([]uint64, 4*w*h)
|
||||
for y := r.Min.Y; y < r.Max.Y; y++ {
|
||||
pix := m.Pix[(y-r.Min.Y)*m.Stride:]
|
||||
for x := r.Min.X; x < r.Max.X; x++ {
|
||||
// Get the source pixel.
|
||||
p := pix[(x-r.Min.X)*4:]
|
||||
r64 := uint64(p[0])
|
||||
g64 := uint64(p[1])
|
||||
b64 := uint64(p[2])
|
||||
a64 := uint64(p[3])
|
||||
// Spread the source pixel over 1 or more destination rows.
|
||||
py := uint64(y) * hh
|
||||
for remy := hh; remy > 0; {
|
||||
qy := dy - (py % dy)
|
||||
if qy > remy {
|
||||
qy = remy
|
||||
}
|
||||
// Spread the source pixel over 1 or more destination columns.
|
||||
px := uint64(x) * ww
|
||||
index := 4 * ((py/dy)*ww + (px / dx))
|
||||
for remx := ww; remx > 0; {
|
||||
qx := dx - (px % dx)
|
||||
if qx > remx {
|
||||
qx = remx
|
||||
}
|
||||
qxy := qx * qy
|
||||
sum[index+0] += r64 * qxy
|
||||
sum[index+1] += g64 * qxy
|
||||
sum[index+2] += b64 * qxy
|
||||
sum[index+3] += a64 * qxy
|
||||
index += 4
|
||||
px += qx
|
||||
remx -= qx
|
||||
}
|
||||
py += qy
|
||||
remy -= qy
|
||||
}
|
||||
}
|
||||
}
|
||||
return average(sum, w, h, (uint64)(n))
|
||||
}
|
||||
|
||||
|
||||
// ResizeNRGBA returns a scaled copy of the RGBA image slice r of m.
|
||||
// The returned image has width w and height h.
|
||||
func ResizeNRGBA(m *image.NRGBA, r image.Rectangle, w, h int) *image.RGBA {
|
||||
ww, hh := uint64(w), uint64(h)
|
||||
dx, dy := uint64(r.Dx()), uint64(r.Dy())
|
||||
// See comment in Resize.
|
||||
n, sum := dx*dy, make([]uint64, 4*w*h)
|
||||
for y := r.Min.Y; y < r.Max.Y; y++ {
|
||||
pix := m.Pix[(y-r.Min.Y)*m.Stride:]
|
||||
for x := r.Min.X; x < r.Max.X; x++ {
|
||||
// Get the source pixel.
|
||||
p := pix[(x-r.Min.X)*4:]
|
||||
r64 := uint64(p[0])
|
||||
g64 := uint64(p[1])
|
||||
b64 := uint64(p[2])
|
||||
a64 := uint64(p[3])
|
||||
r64 = (r64 * a64) / 255
|
||||
g64 = (g64 * a64) / 255
|
||||
b64 = (b64 * a64) / 255
|
||||
// Spread the source pixel over 1 or more destination rows.
|
||||
py := uint64(y) * hh
|
||||
for remy := hh; remy > 0; {
|
||||
qy := dy - (py % dy)
|
||||
if qy > remy {
|
||||
qy = remy
|
||||
}
|
||||
// Spread the source pixel over 1 or more destination columns.
|
||||
px := uint64(x) * ww
|
||||
index := 4 * ((py/dy)*ww + (px / dx))
|
||||
for remx := ww; remx > 0; {
|
||||
qx := dx - (px % dx)
|
||||
if qx > remx {
|
||||
qx = remx
|
||||
}
|
||||
qxy := qx * qy
|
||||
sum[index+0] += r64 * qxy
|
||||
sum[index+1] += g64 * qxy
|
||||
sum[index+2] += b64 * qxy
|
||||
sum[index+3] += a64 * qxy
|
||||
index += 4
|
||||
px += qx
|
||||
remx -= qx
|
||||
}
|
||||
py += qy
|
||||
remy -= qy
|
||||
}
|
||||
}
|
||||
}
|
||||
return average(sum, w, h, (uint64)(n))
|
||||
}
|
||||
|
||||
// Resample returns a resampled copy of the image slice r of m.
|
||||
// The returned image has width w and height h.
|
||||
func Resample(m image.Image, r image.Rectangle, w, h int) *image.RGBA {
|
||||
if w < 0 || h < 0 {
|
||||
return nil
|
||||
}
|
||||
if w == 0 || h == 0 || r.Dx() <= 0 || r.Dy() <= 0 {
|
||||
return image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
}
|
||||
curw, curh := r.Dx(), r.Dy()
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
// Get a source pixel.
|
||||
subx := x * curw / w
|
||||
suby := y * curh / h
|
||||
r32, g32, b32, a32 := m.At(subx, suby).RGBA()
|
||||
r := uint8(r32 >> 8)
|
||||
g := uint8(g32 >> 8)
|
||||
b := uint8(b32 >> 8)
|
||||
a := uint8(a32 >> 8)
|
||||
img.SetRGBA(x, y, color.RGBA{r, g, b, a})
|
||||
}
|
||||
}
|
||||
return img
|
||||
}
|
||||
|
||||
@@ -18,13 +18,6 @@ The two are evolving together; the protocol is not to be considered
|
||||
stable until syncthing 1.0 is released, at which point it is locked down
|
||||
for incompatible changes.
|
||||
|
||||
Syncthing does not use the BitTorrent protocol. The reasons for this are
|
||||
1) we don't know if BitTorrent Sync does either, so there's nothing to
|
||||
be compatible with, 2) BitTorrent includes a lot of functionality for
|
||||
making sure large swarms of selfish agents behave and somehow work
|
||||
towards a common goal. Here we have a much smaller swarm of cooperative
|
||||
agents and a simpler approach will suffice.
|
||||
|
||||
Getting Started
|
||||
---------------
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -102,9 +102,7 @@ func (b *Beacon) writer() {
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
if debug {
|
||||
} else if debug {
|
||||
l.Debugf("sent %d bytes to %s", len(bs), dst)
|
||||
}
|
||||
}
|
||||
|
||||
8
build.sh
8
build.sh
@@ -1,6 +1,7 @@
|
||||
#!/usr/bin/env bash
|
||||
|
||||
export COPYFILE_DISABLE=true
|
||||
export GO386=387 # Don't use SSE on 32 bit builds
|
||||
|
||||
distFiles=(README.md LICENSE) # apart from the binary itself
|
||||
version=$(git describe --always --dirty)
|
||||
@@ -88,7 +89,8 @@ case "$1" in
|
||||
;;
|
||||
|
||||
guidev)
|
||||
build -tags guidev
|
||||
echo "Syncthing is already built for GUI developments. Try:"
|
||||
echo " STGUIASSETS=~/someDir/gui syncthing"
|
||||
;;
|
||||
|
||||
test)
|
||||
@@ -112,6 +114,10 @@ case "$1" in
|
||||
test || exit 1
|
||||
assets
|
||||
|
||||
godep go build ./discover/cmd/discosrv
|
||||
godep go build ./cmd/stpidx
|
||||
godep go build ./cmd/stcli
|
||||
|
||||
for os in darwin-amd64 linux-386 linux-amd64 freebsd-amd64 windows-amd64 windows-386 ; do
|
||||
export GOOS=${os%-*}
|
||||
export GOARCH=${os#*-}
|
||||
|
||||
@@ -4,21 +4,26 @@ import (
|
||||
"bytes"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"math/rand"
|
||||
"mime"
|
||||
"net"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"crypto/tls"
|
||||
"code.google.com/p/go.crypto/bcrypt"
|
||||
"github.com/calmh/syncthing/auto"
|
||||
"github.com/calmh/syncthing/config"
|
||||
"github.com/calmh/syncthing/logger"
|
||||
"github.com/calmh/syncthing/model"
|
||||
"github.com/codegangsta/martini"
|
||||
"github.com/vitrun/qart/qr"
|
||||
)
|
||||
|
||||
type guiError struct {
|
||||
@@ -30,8 +35,7 @@ var (
|
||||
configInSync = true
|
||||
guiErrors = []guiError{}
|
||||
guiErrorsMut sync.Mutex
|
||||
static = embeddedStatic()
|
||||
staticFunc = static.(func(http.ResponseWriter, *http.Request, *log.Logger))
|
||||
static func(http.ResponseWriter, *http.Request, *log.Logger)
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -42,7 +46,7 @@ func init() {
|
||||
l.AddHandler(logger.LevelWarn, showGuiError)
|
||||
}
|
||||
|
||||
func startGUI(cfg config.GUIConfiguration, m *model.Model) error {
|
||||
func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) error {
|
||||
var listener net.Listener
|
||||
var err error
|
||||
if cfg.UseTLS {
|
||||
@@ -69,6 +73,12 @@ func startGUI(cfg config.GUIConfiguration, m *model.Model) error {
|
||||
}
|
||||
}
|
||||
|
||||
if len(assetDir) > 0 {
|
||||
static = martini.Static(assetDir).(func(http.ResponseWriter, *http.Request, *log.Logger))
|
||||
} else {
|
||||
static = embeddedStatic()
|
||||
}
|
||||
|
||||
router := martini.NewRouter()
|
||||
router.Get("/", getRoot)
|
||||
router.Get("/rest/version", restGetVersion)
|
||||
@@ -80,6 +90,7 @@ func startGUI(cfg config.GUIConfiguration, m *model.Model) error {
|
||||
router.Get("/rest/system", restGetSystem)
|
||||
router.Get("/rest/errors", restGetErrors)
|
||||
router.Get("/rest/discovery", restGetDiscovery)
|
||||
router.Get("/qr/:text", getQR)
|
||||
|
||||
router.Post("/rest/config", restPostConfig)
|
||||
router.Post("/rest/restart", restPostRestart)
|
||||
@@ -106,7 +117,7 @@ func startGUI(cfg config.GUIConfiguration, m *model.Model) error {
|
||||
|
||||
func getRoot(w http.ResponseWriter, r *http.Request) {
|
||||
r.URL.Path = "/index.html"
|
||||
staticFunc(w, r, nil)
|
||||
static(w, r, nil)
|
||||
}
|
||||
|
||||
func restMiddleware(w http.ResponseWriter, r *http.Request) {
|
||||
@@ -173,23 +184,24 @@ func restGetConfig(w http.ResponseWriter) {
|
||||
}
|
||||
|
||||
func restPostConfig(req *http.Request) {
|
||||
var prevPassHash = cfg.GUI.Password
|
||||
err := json.NewDecoder(req.Body).Decode(&cfg)
|
||||
var newCfg config.Configuration
|
||||
err := json.NewDecoder(req.Body).Decode(&newCfg)
|
||||
if err != nil {
|
||||
l.Warnln(err)
|
||||
} else {
|
||||
if cfg.GUI.Password == "" {
|
||||
if newCfg.GUI.Password == "" {
|
||||
// Leave it empty
|
||||
} else if cfg.GUI.Password != unchangedPassword {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(cfg.GUI.Password), 0)
|
||||
} else if newCfg.GUI.Password == unchangedPassword {
|
||||
newCfg.GUI.Password = cfg.GUI.Password
|
||||
} else {
|
||||
hash, err := bcrypt.GenerateFromPassword([]byte(newCfg.GUI.Password), 0)
|
||||
if err != nil {
|
||||
l.Warnln(err)
|
||||
} else {
|
||||
cfg.GUI.Password = string(hash)
|
||||
newCfg.GUI.Password = string(hash)
|
||||
}
|
||||
} else {
|
||||
cfg.GUI.Password = prevPassHash
|
||||
}
|
||||
cfg = newCfg
|
||||
saveConfig()
|
||||
configInSync = false
|
||||
}
|
||||
@@ -233,7 +245,7 @@ func restGetSystem(w http.ResponseWriter) {
|
||||
res["goroutines"] = runtime.NumGoroutine()
|
||||
res["alloc"] = m.Alloc
|
||||
res["sys"] = m.Sys
|
||||
res["tilde"] = expandTilde("~/")
|
||||
res["tilde"] = expandTilde("~")
|
||||
if cfg.Options.GlobalAnnEnabled && discoverer != nil {
|
||||
res["extAnnounceOK"] = discoverer.ExtAnnounceOK()
|
||||
}
|
||||
@@ -289,6 +301,17 @@ func restGetDiscovery(w http.ResponseWriter) {
|
||||
json.NewEncoder(w).Encode(discoverer.All())
|
||||
}
|
||||
|
||||
func getQR(w http.ResponseWriter, params martini.Params) {
|
||||
code, err := qr.Encode(params["text"], qr.M)
|
||||
if err != nil {
|
||||
http.Error(w, "Invalid", 500)
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "image/png")
|
||||
w.Write(code.PNG())
|
||||
}
|
||||
|
||||
func basic(username string, passhash string) http.HandlerFunc {
|
||||
return func(res http.ResponseWriter, req *http.Request) {
|
||||
error := func() {
|
||||
@@ -327,3 +350,29 @@ func basic(username string, passhash string) http.HandlerFunc {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func embeddedStatic() func(http.ResponseWriter, *http.Request, *log.Logger) {
|
||||
var modt = time.Now().UTC().Format(http.TimeFormat)
|
||||
|
||||
return func(res http.ResponseWriter, req *http.Request, log *log.Logger) {
|
||||
file := req.URL.Path
|
||||
|
||||
if file[0] == '/' {
|
||||
file = file[1:]
|
||||
}
|
||||
|
||||
bs, ok := auto.Assets[file]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
mtype := mime.TypeByExtension(filepath.Ext(req.URL.Path))
|
||||
if len(mtype) != 0 {
|
||||
res.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
res.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
|
||||
res.Header().Set("Last-Modified", modt)
|
||||
|
||||
res.Write(bs)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,9 +0,0 @@
|
||||
//+build guidev
|
||||
|
||||
package main
|
||||
|
||||
import "github.com/codegangsta/martini"
|
||||
|
||||
func embeddedStatic() interface{} {
|
||||
return martini.Static("gui")
|
||||
}
|
||||
@@ -1,40 +0,0 @@
|
||||
//+build !guidev
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"log"
|
||||
"mime"
|
||||
"net/http"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/calmh/syncthing/auto"
|
||||
)
|
||||
|
||||
func embeddedStatic() interface{} {
|
||||
var modt = time.Now().UTC().Format(http.TimeFormat)
|
||||
|
||||
return func(res http.ResponseWriter, req *http.Request, log *log.Logger) {
|
||||
file := req.URL.Path
|
||||
|
||||
if file[0] == '/' {
|
||||
file = file[1:]
|
||||
}
|
||||
|
||||
bs, ok := auto.Assets[file]
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
mtype := mime.TypeByExtension(filepath.Ext(req.URL.Path))
|
||||
if len(mtype) != 0 {
|
||||
res.Header().Set("Content-Type", mtype)
|
||||
}
|
||||
res.Header().Set("Content-Length", fmt.Sprintf("%d", len(bs)))
|
||||
res.Header().Set("Last-Modified", modt)
|
||||
|
||||
res.Write(bs)
|
||||
}
|
||||
}
|
||||
@@ -24,6 +24,7 @@ import (
|
||||
"github.com/calmh/syncthing/discover"
|
||||
"github.com/calmh/syncthing/logger"
|
||||
"github.com/calmh/syncthing/model"
|
||||
"github.com/calmh/syncthing/osutil"
|
||||
"github.com/calmh/syncthing/protocol"
|
||||
"github.com/calmh/syncthing/upnp"
|
||||
"github.com/juju/ratelimit"
|
||||
@@ -77,13 +78,16 @@ const (
|
||||
- "beacon" (the beacon package)
|
||||
- "discover" (the discover package)
|
||||
- "files" (the files package)
|
||||
- "net" (the main packge; connections & network messages)
|
||||
- "net" (the main package; connections & network messages)
|
||||
- "model" (the model package)
|
||||
- "scanner" (the scanner package)
|
||||
- "upnp" (the upnp package)
|
||||
- "xdr" (the xdr package)
|
||||
- "all" (all of the above)
|
||||
|
||||
STCPUPROFILE Write CPU profile to the specified file.`
|
||||
STCPUPROFILE Write CPU profile to the specified file.
|
||||
|
||||
STGUIASSETS Directory to load GUI assets from. Overrides compiled in assets.`
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -97,11 +101,6 @@ func main() {
|
||||
flag.Usage = usageFor(flag.CommandLine, usage, extraUsage)
|
||||
flag.Parse()
|
||||
|
||||
if len(os.Getenv("STRESTART")) > 0 {
|
||||
// Give the parent process time to exit and release sockets etc.
|
||||
time.Sleep(1 * time.Second)
|
||||
}
|
||||
|
||||
if showVersion {
|
||||
fmt.Println(LongVersion)
|
||||
return
|
||||
@@ -132,7 +131,12 @@ func main() {
|
||||
// continue. We don't much care if this fails at this point, we will
|
||||
// be checking that later.
|
||||
|
||||
oldDefault := expandTilde("~/.syncthing")
|
||||
var oldDefault string
|
||||
if runtime.GOOS == "windows" {
|
||||
oldDefault = filepath.Join(os.Getenv("AppData"), "Syncthing")
|
||||
} else {
|
||||
oldDefault = expandTilde("~/.syncthing")
|
||||
}
|
||||
if _, err := os.Stat(oldDefault); err == nil {
|
||||
os.MkdirAll(filepath.Dir(confDir), 0700)
|
||||
if err := os.Rename(oldDefault, confDir); err == nil {
|
||||
@@ -199,9 +203,9 @@ func main() {
|
||||
l.FatalErr(err)
|
||||
cfg.GUI.Address = fmt.Sprintf("127.0.0.1:%d", port)
|
||||
|
||||
port, err = getFreePort("", 22000)
|
||||
port, err = getFreePort("0.0.0.0", 22000)
|
||||
l.FatalErr(err)
|
||||
cfg.Options.ListenAddress = []string{fmt.Sprintf(":%d", port)}
|
||||
cfg.Options.ListenAddress = []string{fmt.Sprintf("0.0.0.0:%d", port)}
|
||||
|
||||
saveConfig()
|
||||
l.Infof("Edit %s to taste or use the GUI\n", cfgFile)
|
||||
@@ -223,6 +227,10 @@ func main() {
|
||||
}()
|
||||
}
|
||||
|
||||
if len(os.Getenv("STRESTART")) > 0 {
|
||||
waitForParentExit()
|
||||
}
|
||||
|
||||
// The TLS configuration is used for both the listening socket and outgoing
|
||||
// connections.
|
||||
|
||||
@@ -249,8 +257,8 @@ func main() {
|
||||
if repo.Invalid != "" {
|
||||
continue
|
||||
}
|
||||
dir := expandTilde(repo.Directory)
|
||||
m.AddRepo(repo.ID, dir, repo.Nodes)
|
||||
repo.Directory = expandTilde(repo.Directory)
|
||||
m.AddRepo(repo)
|
||||
}
|
||||
|
||||
// GUI
|
||||
@@ -278,7 +286,7 @@ func main() {
|
||||
}
|
||||
|
||||
l.Infof("Starting web GUI on %s://%s:%d/", proto, hostShow, addr.Port)
|
||||
err := startGUI(cfg.GUI, m)
|
||||
err := startGUI(cfg.GUI, os.Getenv("STGUIASSETS"), m)
|
||||
if err != nil {
|
||||
l.Fatalln("Cannot start GUI:", err)
|
||||
}
|
||||
@@ -359,7 +367,28 @@ func main() {
|
||||
defer pprof.StopCPUProfile()
|
||||
}
|
||||
|
||||
for _, node := range cfg.Nodes {
|
||||
if len(node.Name) > 0 {
|
||||
l.Infof("Node %s is %q at %v", node.NodeID, node.Name, node.Addresses)
|
||||
}
|
||||
}
|
||||
|
||||
<-stop
|
||||
l.Okln("Exiting")
|
||||
}
|
||||
|
||||
func waitForParentExit() {
|
||||
l.Infoln("Waiting for parent to exit...")
|
||||
// Wait for the listen address to become free, indicating that the parent has exited.
|
||||
for {
|
||||
ln, err := net.Listen("tcp", cfg.Options.ListenAddress[0])
|
||||
if err == nil {
|
||||
ln.Close()
|
||||
break
|
||||
}
|
||||
time.Sleep(250 * time.Millisecond)
|
||||
}
|
||||
l.Okln("Continuing")
|
||||
}
|
||||
|
||||
func setupUPnP() int {
|
||||
@@ -470,7 +499,7 @@ func saveConfigLoop(cfgFile string) {
|
||||
continue
|
||||
}
|
||||
|
||||
err = model.Rename(cfgFile+".tmp", cfgFile)
|
||||
err = osutil.Rename(cfgFile+".tmp", cfgFile)
|
||||
if err != nil {
|
||||
l.Warnln(err)
|
||||
}
|
||||
@@ -614,7 +643,7 @@ next:
|
||||
}
|
||||
|
||||
func discovery(extPort int) *discover.Discoverer {
|
||||
disc, err := discover.NewDiscoverer(myID, cfg.Options.ListenAddress)
|
||||
disc, err := discover.NewDiscoverer(myID, cfg.Options.ListenAddress, cfg.Options.LocalAnnPort)
|
||||
if err != nil {
|
||||
l.Warnf("No discovery possible (%v)", err)
|
||||
return nil
|
||||
@@ -647,7 +676,7 @@ func ensureDir(dir string, mode int) {
|
||||
func getDefaultConfDir() string {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
return filepath.Join(os.Getenv("AppData"), "Syncthing")
|
||||
return filepath.Join(os.Getenv("LocalAppData"), "Syncthing")
|
||||
|
||||
case "darwin":
|
||||
return expandTilde("~/Library/Application Support/Syncthing")
|
||||
@@ -662,7 +691,12 @@ func getDefaultConfDir() string {
|
||||
}
|
||||
|
||||
func expandTilde(p string) string {
|
||||
if runtime.GOOS == "windows" || !strings.HasPrefix(p, "~/") {
|
||||
if p == "~" {
|
||||
return getHomeDir()
|
||||
}
|
||||
|
||||
p = filepath.FromSlash(p)
|
||||
if !strings.HasPrefix(p, fmt.Sprintf("~%c", os.PathSeparator)) {
|
||||
return p
|
||||
}
|
||||
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
"encoding/binary"
|
||||
"encoding/pem"
|
||||
"math/big"
|
||||
mr "math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
@@ -50,7 +51,7 @@ func newCertificate(dir string, prefix string) {
|
||||
notAfter := time.Date(2049, 12, 31, 23, 59, 59, 0, time.UTC)
|
||||
|
||||
template := x509.Certificate{
|
||||
SerialNumber: new(big.Int).SetInt64(0),
|
||||
SerialNumber: new(big.Int).SetInt64(mr.Int63()),
|
||||
Subject: pkix.Name{
|
||||
CommonName: tlsName,
|
||||
},
|
||||
|
||||
@@ -1,11 +1,10 @@
|
||||
// +build !windows
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/tar"
|
||||
"compress/gzip"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
@@ -14,8 +13,10 @@ import (
|
||||
"path"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"bytes"
|
||||
"bitbucket.org/kardianos/osext"
|
||||
)
|
||||
|
||||
@@ -33,6 +34,10 @@ type githubAsset struct {
|
||||
var GoArchExtra string // "", "v5", "v6", "v7"
|
||||
|
||||
func upgrade() error {
|
||||
if runtime.GOOS == "windows" {
|
||||
return errors.New("Upgrade currently unsupported on Windows")
|
||||
}
|
||||
|
||||
path, err := osext.Executable()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -52,14 +57,15 @@ func upgrade() error {
|
||||
}
|
||||
rel := rels[0]
|
||||
|
||||
if rel.Tag > Version {
|
||||
l.Infof("Attempting upgrade to %s...", rel.Tag)
|
||||
} else if rel.Tag == Version {
|
||||
l.Okf("Already running the latest version, %s. Not upgrading.", Version)
|
||||
return nil
|
||||
} else {
|
||||
switch compareVersions(rel.Tag, Version) {
|
||||
case -1:
|
||||
l.Okf("Current version %s is newer than latest release %s. Not upgrading.", Version, rel.Tag)
|
||||
return nil
|
||||
case 0:
|
||||
l.Okf("Already running the latest version, %s. Not upgrading.", Version)
|
||||
return nil
|
||||
default:
|
||||
l.Infof("Attempting upgrade to %s...", rel.Tag)
|
||||
}
|
||||
|
||||
expectedRelease := fmt.Sprintf("syncthing-%s-%s%s-%s.", runtime.GOOS, runtime.GOARCH, GoArchExtra, rel.Tag)
|
||||
@@ -147,3 +153,18 @@ func readTarGZ(url string, dir string) (string, error) {
|
||||
|
||||
return "", fmt.Errorf("No upgrade found")
|
||||
}
|
||||
|
||||
func compareVersions(a, b string) int {
|
||||
return bytes.Compare(versionParts(a), versionParts(b))
|
||||
}
|
||||
|
||||
func versionParts(v string) []byte {
|
||||
parts := strings.Split(v, "-")
|
||||
fields := strings.Split(parts[0], ".")
|
||||
res := make([]byte, len(fields))
|
||||
for i, s := range fields {
|
||||
v, _ := strconv.Atoi(s)
|
||||
res[i] = byte(v)
|
||||
}
|
||||
return res
|
||||
}
|
||||
27
cmd/syncthing/upgrade_test.go
Normal file
27
cmd/syncthing/upgrade_test.go
Normal file
@@ -0,0 +1,27 @@
|
||||
package main
|
||||
|
||||
import "testing"
|
||||
|
||||
var testcases = []struct {
|
||||
a, b string
|
||||
r int
|
||||
}{
|
||||
{"0.1.2", "0.1.2", 0},
|
||||
{"0.1.3", "0.1.2", 1},
|
||||
{"0.1.1", "0.1.2", -1},
|
||||
{"0.3.0", "0.1.2", 1},
|
||||
{"0.0.9", "0.1.2", -1},
|
||||
{"1.1.2", "0.1.2", 1},
|
||||
{"0.1.2", "1.1.2", -1},
|
||||
{"0.1.10", "0.1.9", 1},
|
||||
{"0.10.0", "0.2.0", 1},
|
||||
{"30.10.0", "4.9.0", 1},
|
||||
}
|
||||
|
||||
func TestCompareVersions(t *testing.T) {
|
||||
for _, tc := range testcases {
|
||||
if r := compareVersions(tc.a, tc.b); r != tc.r {
|
||||
t.Errorf("compareVersions(%q, %q): %d != %d", tc.a, tc.b, r, tc.r)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// +build windows
|
||||
|
||||
package main
|
||||
|
||||
import "errors"
|
||||
|
||||
func upgrade() error {
|
||||
return errors.New("Upgrade currently unsupported on Windows")
|
||||
}
|
||||
@@ -27,12 +27,56 @@ type Configuration struct {
|
||||
}
|
||||
|
||||
type RepositoryConfiguration struct {
|
||||
ID string `xml:"id,attr"`
|
||||
Directory string `xml:"directory,attr"`
|
||||
Nodes []NodeConfiguration `xml:"node"`
|
||||
ReadOnly bool `xml:"ro,attr"`
|
||||
Invalid string `xml:"-"` // Set at runtime when there is an error, not saved
|
||||
nodeIDs []string
|
||||
ID string `xml:"id,attr"`
|
||||
Directory string `xml:"directory,attr"`
|
||||
Nodes []NodeConfiguration `xml:"node"`
|
||||
ReadOnly bool `xml:"ro,attr"`
|
||||
IgnorePerms bool `xml:"ignorePerms,attr"`
|
||||
Invalid string `xml:"-"` // Set at runtime when there is an error, not saved
|
||||
Versioning VersioningConfiguration `xml:"versioning"`
|
||||
|
||||
nodeIDs []string
|
||||
}
|
||||
|
||||
type VersioningConfiguration struct {
|
||||
Type string `xml:"type,attr"`
|
||||
Params map[string]string
|
||||
}
|
||||
|
||||
type InternalVersioningConfiguration struct {
|
||||
Type string `xml:"type,attr,omitempty"`
|
||||
Params []InternalParam `xml:"param"`
|
||||
}
|
||||
|
||||
type InternalParam struct {
|
||||
Key string `xml:"key,attr"`
|
||||
Val string `xml:"val,attr"`
|
||||
}
|
||||
|
||||
func (c *VersioningConfiguration) MarshalXML(e *xml.Encoder, start xml.StartElement) error {
|
||||
var tmp InternalVersioningConfiguration
|
||||
tmp.Type = c.Type
|
||||
for k, v := range c.Params {
|
||||
tmp.Params = append(tmp.Params, InternalParam{k, v})
|
||||
}
|
||||
|
||||
return e.EncodeElement(tmp, start)
|
||||
|
||||
}
|
||||
|
||||
func (c *VersioningConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
|
||||
var tmp InternalVersioningConfiguration
|
||||
err := d.DecodeElement(&tmp, &start)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
c.Type = tmp.Type
|
||||
c.Params = make(map[string]string, len(tmp.Params))
|
||||
for _, p := range tmp.Params {
|
||||
c.Params[p.Key] = p.Val
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (r *RepositoryConfiguration) NodeIDs() []string {
|
||||
@@ -55,6 +99,7 @@ type OptionsConfiguration struct {
|
||||
GlobalAnnServer string `xml:"globalAnnounceServer" default:"announce.syncthing.net:22025"`
|
||||
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" default:"true"`
|
||||
LocalAnnEnabled bool `xml:"localAnnounceEnabled" default:"true"`
|
||||
LocalAnnPort int `xml:"localAnnouncePort" default:"21025"`
|
||||
ParallelRequests int `xml:"parallelRequests" default:"16"`
|
||||
MaxSendKbps int `xml:"maxSendKbps"`
|
||||
RescanIntervalS int `xml:"rescanIntervalS" default:"60"`
|
||||
@@ -189,6 +234,7 @@ func Load(rd io.Reader, myID string) (Configuration, error) {
|
||||
// Strip spaces and dashes
|
||||
node.NodeID = strings.Replace(node.NodeID, "-", "", -1)
|
||||
node.NodeID = strings.Replace(node.NodeID, " ", "", -1)
|
||||
node.NodeID = strings.ToUpper(node.NodeID)
|
||||
}
|
||||
|
||||
// Check for missing, bad or duplicate repository ID:s
|
||||
@@ -198,7 +244,7 @@ func Load(rd io.Reader, myID string) (Configuration, error) {
|
||||
repo := &cfg.Repositories[i]
|
||||
|
||||
if len(repo.Directory) == 0 {
|
||||
repo.Invalid = "empty directory"
|
||||
repo.Invalid = "no directory configured"
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,7 @@ func TestDefaultValues(t *testing.T) {
|
||||
GlobalAnnServer: "announce.syncthing.net:22025",
|
||||
GlobalAnnEnabled: true,
|
||||
LocalAnnEnabled: true,
|
||||
LocalAnnPort: 21025,
|
||||
ParallelRequests: 16,
|
||||
MaxSendKbps: 0,
|
||||
RescanIntervalS: 60,
|
||||
@@ -37,10 +38,10 @@ func TestNodeConfig(t *testing.T) {
|
||||
v1data := []byte(`
|
||||
<configuration version="1">
|
||||
<repository id="test" directory="~/Sync">
|
||||
<node id="node1" name="node one">
|
||||
<node id="NODE1" name="node one">
|
||||
<address>a</address>
|
||||
</node>
|
||||
<node id="node2" name="node two">
|
||||
<node id="NODE2" name="node two">
|
||||
<address>b</address>
|
||||
</node>
|
||||
</repository>
|
||||
@@ -53,20 +54,20 @@ func TestNodeConfig(t *testing.T) {
|
||||
v2data := []byte(`
|
||||
<configuration version="2">
|
||||
<repository id="test" directory="~/Sync" ro="true">
|
||||
<node id="node1"/>
|
||||
<node id="node2"/>
|
||||
<node id="NODE1"/>
|
||||
<node id="NODE2"/>
|
||||
</repository>
|
||||
<node id="node1" name="node one">
|
||||
<node id="NODE1" name="node one">
|
||||
<address>a</address>
|
||||
</node>
|
||||
<node id="node2" name="node two">
|
||||
<node id="NODE2" name="node two">
|
||||
<address>b</address>
|
||||
</node>
|
||||
</configuration>
|
||||
`)
|
||||
|
||||
for i, data := range [][]byte{v1data, v2data} {
|
||||
cfg, err := Load(bytes.NewReader(data), "node1")
|
||||
cfg, err := Load(bytes.NewReader(data), "NODE1")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
@@ -75,23 +76,23 @@ func TestNodeConfig(t *testing.T) {
|
||||
{
|
||||
ID: "test",
|
||||
Directory: "~/Sync",
|
||||
Nodes: []NodeConfiguration{{NodeID: "node1"}, {NodeID: "node2"}},
|
||||
Nodes: []NodeConfiguration{{NodeID: "NODE1"}, {NodeID: "NODE2"}},
|
||||
ReadOnly: true,
|
||||
},
|
||||
}
|
||||
expectedNodes := []NodeConfiguration{
|
||||
{
|
||||
NodeID: "node1",
|
||||
NodeID: "NODE1",
|
||||
Name: "node one",
|
||||
Addresses: []string{"a"},
|
||||
},
|
||||
{
|
||||
NodeID: "node2",
|
||||
NodeID: "NODE2",
|
||||
Name: "node two",
|
||||
Addresses: []string{"b"},
|
||||
},
|
||||
}
|
||||
expectedNodeIDs := []string{"node1", "node2"}
|
||||
expectedNodeIDs := []string{"NODE1", "NODE2"}
|
||||
|
||||
if cfg.Version != 2 {
|
||||
t.Errorf("%d: Incorrect version %d != 2", i, cfg.Version)
|
||||
@@ -145,6 +146,7 @@ func TestOverriddenValues(t *testing.T) {
|
||||
<globalAnnounceServer>syncthing.nym.se:22025</globalAnnounceServer>
|
||||
<globalAnnounceEnabled>false</globalAnnounceEnabled>
|
||||
<localAnnounceEnabled>false</localAnnounceEnabled>
|
||||
<localAnnouncePort>42123</localAnnouncePort>
|
||||
<parallelRequests>32</parallelRequests>
|
||||
<maxSendKbps>1234</maxSendKbps>
|
||||
<rescanIntervalS>600</rescanIntervalS>
|
||||
@@ -161,6 +163,7 @@ func TestOverriddenValues(t *testing.T) {
|
||||
GlobalAnnServer: "syncthing.nym.se:22025",
|
||||
GlobalAnnEnabled: false,
|
||||
LocalAnnEnabled: false,
|
||||
LocalAnnPort: 42123,
|
||||
ParallelRequests: 32,
|
||||
MaxSendKbps: 1234,
|
||||
RescanIntervalS: 600,
|
||||
@@ -197,25 +200,25 @@ func TestNodeAddresses(t *testing.T) {
|
||||
name, _ := os.Hostname()
|
||||
expected := []NodeConfiguration{
|
||||
{
|
||||
NodeID: "n1",
|
||||
NodeID: "N1",
|
||||
Addresses: []string{"dynamic"},
|
||||
},
|
||||
{
|
||||
NodeID: "n2",
|
||||
NodeID: "N2",
|
||||
Addresses: []string{"dynamic"},
|
||||
},
|
||||
{
|
||||
NodeID: "n3",
|
||||
NodeID: "N3",
|
||||
Addresses: []string{"dynamic"},
|
||||
},
|
||||
{
|
||||
NodeID: "n4",
|
||||
NodeID: "N4",
|
||||
Name: name, // Set when auto created
|
||||
Addresses: []string{"dynamic"},
|
||||
},
|
||||
}
|
||||
|
||||
cfg, err := Load(bytes.NewReader(data), "n4")
|
||||
cfg, err := Load(bytes.NewReader(data), "N4")
|
||||
if err != nil {
|
||||
t.Error(err)
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/hex"
|
||||
"flag"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"os"
|
||||
@@ -16,18 +17,18 @@ import (
|
||||
"github.com/juju/ratelimit"
|
||||
)
|
||||
|
||||
type Node struct {
|
||||
Addresses []Address
|
||||
Updated time.Time
|
||||
type node struct {
|
||||
addresses []address
|
||||
updated time.Time
|
||||
}
|
||||
|
||||
type Address struct {
|
||||
IP []byte
|
||||
Port uint16
|
||||
type address struct {
|
||||
ip []byte
|
||||
port uint16
|
||||
}
|
||||
|
||||
var (
|
||||
nodes = make(map[string]Node)
|
||||
nodes = make(map[string]node)
|
||||
lock sync.Mutex
|
||||
queries = 0
|
||||
announces = 0
|
||||
@@ -134,7 +135,7 @@ func limit(addr *net.UDPAddr) bool {
|
||||
func handleAnnounceV2(addr *net.UDPAddr, buf []byte) {
|
||||
var pkt discover.AnnounceV2
|
||||
err := pkt.UnmarshalXDR(buf)
|
||||
if err != nil {
|
||||
if err != nil && err != io.EOF {
|
||||
log.Println("AnnounceV2 Unmarshal:", err)
|
||||
log.Println(hex.Dump(buf))
|
||||
return
|
||||
@@ -152,25 +153,25 @@ func handleAnnounceV2(addr *net.UDPAddr, buf []byte) {
|
||||
ip = addr.IP.To16()
|
||||
}
|
||||
|
||||
var addrs []Address
|
||||
for _, addr := range pkt.Addresses {
|
||||
var addrs []address
|
||||
for _, addr := range pkt.This.Addresses {
|
||||
tip := addr.IP
|
||||
if len(tip) == 0 {
|
||||
tip = ip
|
||||
}
|
||||
addrs = append(addrs, Address{
|
||||
IP: tip,
|
||||
Port: addr.Port,
|
||||
addrs = append(addrs, address{
|
||||
ip: tip,
|
||||
port: addr.Port,
|
||||
})
|
||||
}
|
||||
|
||||
node := Node{
|
||||
Addresses: addrs,
|
||||
Updated: time.Now(),
|
||||
node := node{
|
||||
addresses: addrs,
|
||||
updated: time.Now(),
|
||||
}
|
||||
|
||||
lock.Lock()
|
||||
nodes[pkt.NodeID] = node
|
||||
nodes[pkt.This.ID] = node
|
||||
lock.Unlock()
|
||||
}
|
||||
|
||||
@@ -191,19 +192,21 @@ func handleQueryV2(conn *net.UDPConn, addr *net.UDPAddr, buf []byte) {
|
||||
queries++
|
||||
lock.Unlock()
|
||||
|
||||
if ok && len(node.Addresses) > 0 {
|
||||
pkt := discover.AnnounceV2{
|
||||
Magic: discover.AnnouncementMagicV2,
|
||||
NodeID: pkt.NodeID,
|
||||
if ok && len(node.addresses) > 0 {
|
||||
ann := discover.AnnounceV2{
|
||||
Magic: discover.AnnouncementMagicV2,
|
||||
This: discover.Node{
|
||||
ID: pkt.NodeID,
|
||||
},
|
||||
}
|
||||
for _, addr := range node.Addresses {
|
||||
pkt.Addresses = append(pkt.Addresses, discover.Address{IP: addr.IP, Port: addr.Port})
|
||||
for _, addr := range node.addresses {
|
||||
ann.This.Addresses = append(ann.This.Addresses, discover.Address{IP: addr.ip, Port: addr.port})
|
||||
}
|
||||
if debug {
|
||||
log.Printf("-> %v %#v", addr, pkt)
|
||||
}
|
||||
|
||||
tb := pkt.MarshalXDR()
|
||||
tb := ann.MarshalXDR()
|
||||
_, _, err = conn.WriteMsgUDP(tb, nil, addr)
|
||||
if err != nil {
|
||||
log.Println("QueryV2 response write:", err)
|
||||
@@ -235,7 +238,7 @@ func logStats(file string, intv int) {
|
||||
|
||||
var deleted = 0
|
||||
for id, node := range nodes {
|
||||
if time.Since(node.Updated) > 60*time.Minute {
|
||||
if time.Since(node.updated) > 60*time.Minute {
|
||||
delete(nodes, id)
|
||||
deleted++
|
||||
}
|
||||
|
||||
@@ -13,10 +13,6 @@ import (
|
||||
"github.com/calmh/syncthing/buffers"
|
||||
)
|
||||
|
||||
const (
|
||||
AnnouncementPort = 21025
|
||||
)
|
||||
|
||||
type Discoverer struct {
|
||||
myID string
|
||||
listenAddrs []string
|
||||
@@ -42,8 +38,8 @@ var (
|
||||
// When we hit this many errors in succession, we stop.
|
||||
const maxErrors = 30
|
||||
|
||||
func NewDiscoverer(id string, addresses []string) (*Discoverer, error) {
|
||||
b, err := beacon.New(21025)
|
||||
func NewDiscoverer(id string, addresses []string, localPort int) (*Discoverer, error) {
|
||||
b, err := beacon.New(localPort)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -191,9 +187,8 @@ func (d *Discoverer) sendExternalAnnouncements() {
|
||||
} else {
|
||||
buf = d.announcementPkt()
|
||||
}
|
||||
var errCounter = 0
|
||||
|
||||
for errCounter < maxErrors {
|
||||
for {
|
||||
var ok bool
|
||||
|
||||
if debug {
|
||||
@@ -205,11 +200,8 @@ func (d *Discoverer) sendExternalAnnouncements() {
|
||||
if debug {
|
||||
l.Debugln("discover: warning:", err)
|
||||
}
|
||||
errCounter++
|
||||
ok = false
|
||||
} else {
|
||||
errCounter = 0
|
||||
|
||||
// Verify that the announce server responds positively for our node ID
|
||||
|
||||
time.Sleep(1 * time.Second)
|
||||
@@ -218,7 +210,6 @@ func (d *Discoverer) sendExternalAnnouncements() {
|
||||
l.Debugln("discover: external lookup check:", res)
|
||||
}
|
||||
ok = len(res) > 0
|
||||
|
||||
}
|
||||
|
||||
d.extAnnounceOKmut.Lock()
|
||||
@@ -231,7 +222,6 @@ func (d *Discoverer) sendExternalAnnouncements() {
|
||||
time.Sleep(60 * time.Second)
|
||||
}
|
||||
}
|
||||
l.Warnf("Global discovery: %v: stopping due to too many errors: %v", remote, err)
|
||||
}
|
||||
|
||||
func (d *Discoverer) recvAnnouncements() {
|
||||
|
||||
@@ -76,7 +76,7 @@ func (m *Set) ReplaceWithDelete(id uint, fs []scanner.File) {
|
||||
for _, ck := range m.remoteKey[cid.LocalID] {
|
||||
if _, ok := nf[ck.Name]; !ok {
|
||||
cf := m.files[ck].File
|
||||
if cf.Flags&protocol.FlagDeleted != protocol.FlagDeleted {
|
||||
if !protocol.IsDeleted(cf.Flags) {
|
||||
cf.Flags |= protocol.FlagDeleted
|
||||
cf.Blocks = nil
|
||||
cf.Size = 0
|
||||
@@ -193,7 +193,7 @@ func (m *Set) equals(id uint, fs []scanner.File) bool {
|
||||
curWithoutDeleted := make(map[string]key)
|
||||
for _, k := range m.remoteKey[id] {
|
||||
f := m.files[k].File
|
||||
if f.Flags&protocol.FlagDeleted == 0 {
|
||||
if !protocol.IsDeleted(f.Flags) {
|
||||
curWithoutDeleted[f.Name] = k
|
||||
}
|
||||
}
|
||||
@@ -210,6 +210,9 @@ func (m *Set) equals(id uint, fs []scanner.File) bool {
|
||||
|
||||
func (m *Set) update(cid uint, fs []scanner.File) {
|
||||
remFiles := m.remoteKey[cid]
|
||||
if remFiles == nil {
|
||||
l.Fatalln("update before replace for cid", cid)
|
||||
}
|
||||
for _, f := range fs {
|
||||
n := f.Name
|
||||
fk := keyFor(f)
|
||||
|
||||
105
gui/app.js
105
gui/app.js
@@ -30,8 +30,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
{id: 'ParallelRequests', descr: 'Max Outstanding Requests', type: 'number', restart: true},
|
||||
{id: 'MaxChangeKbps', descr: 'Max File Change Rate (KiB/s)', type: 'number', restart: true},
|
||||
|
||||
{id: 'GlobalAnnEnabled', descr: 'Global Announce', type: 'bool', restart: true},
|
||||
{id: 'LocalAnnEnabled', descr: 'Local Announce', type: 'bool', restart: true},
|
||||
{id: 'GlobalAnnEnabled', descr: 'Global Discovery', type: 'bool', restart: true},
|
||||
{id: 'LocalAnnEnabled', descr: 'Local Discovery', type: 'bool', restart: true},
|
||||
{id: 'LocalAnnPort', descr: 'Local Discovery Port', type: 'number', restart: true},
|
||||
{id: 'StartBrowser', descr: 'Start Browser', type: 'bool'},
|
||||
{id: 'UPnPEnabled', descr: 'Enable UPnP', type: 'bool'},
|
||||
];
|
||||
@@ -52,6 +53,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
if (restarting) {
|
||||
$scope.init();
|
||||
$('#restarting').modal('hide');
|
||||
$('#shutdown').modal('hide');
|
||||
restarting = false;
|
||||
}
|
||||
}
|
||||
@@ -84,9 +86,6 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
id;
|
||||
|
||||
prevDate = now;
|
||||
$scope.inbps = 0;
|
||||
$scope.outbps = 0;
|
||||
|
||||
for (id in data) {
|
||||
if (!data.hasOwnProperty(id)) {
|
||||
continue;
|
||||
@@ -98,8 +97,6 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
data[id].inbps = 0;
|
||||
data[id].outbps = 0;
|
||||
}
|
||||
$scope.inbps += data[id].inbps;
|
||||
$scope.outbps += data[id].outbps;
|
||||
}
|
||||
$scope.connections = data;
|
||||
});
|
||||
@@ -162,7 +159,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
var conn = $scope.connections[nodeCfg.NodeID];
|
||||
if (conn) {
|
||||
if (conn.Completion === 100) {
|
||||
return 'In Sync';
|
||||
return 'Up to Date';
|
||||
} else {
|
||||
return 'Syncing (' + conn.Completion + '%)';
|
||||
}
|
||||
@@ -233,6 +230,9 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
};
|
||||
|
||||
$scope.nodeName = function (nodeCfg) {
|
||||
if (typeof nodeCfg === 'undefined') {
|
||||
return "";
|
||||
}
|
||||
if (nodeCfg.Name) {
|
||||
return nodeCfg.Name;
|
||||
}
|
||||
@@ -251,26 +251,39 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
};
|
||||
|
||||
$scope.editSettings = function () {
|
||||
// Make a working copy
|
||||
$scope.config.workingOptions = angular.copy($scope.config.Options);
|
||||
$scope.config.workingGUI = angular.copy($scope.config.GUI);
|
||||
$('#settings').modal({backdrop: 'static', keyboard: true});
|
||||
}
|
||||
|
||||
$scope.saveSettings = function () {
|
||||
$scope.configInSync = false;
|
||||
$scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) { return x.trim(); });
|
||||
$http.post(urlbase + '/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
|
||||
// Make sure something changed
|
||||
var changed = ! angular.equals($scope.config.Options, $scope.config.workingOptions) ||
|
||||
! angular.equals($scope.config.GUI, $scope.config.workingGUI);
|
||||
if(changed){
|
||||
$scope.config.Options = angular.copy($scope.config.workingOptions);
|
||||
$scope.config.GUI = angular.copy($scope.config.workingGUI);
|
||||
|
||||
$scope.configInSync = false;
|
||||
$scope.config.Options.ListenAddress = $scope.config.Options.ListenStr.split(',').map(function (x) { return x.trim(); });
|
||||
$http.post(urlbase + '/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
|
||||
}
|
||||
|
||||
$('#settings').modal("hide");
|
||||
};
|
||||
|
||||
$scope.restart = function () {
|
||||
restarting = true;
|
||||
$('#restarting').modal('show');
|
||||
$('#restarting').modal({backdrop: 'static', keyboard: false});
|
||||
$http.post(urlbase + '/restart');
|
||||
$scope.configInSync = true;
|
||||
};
|
||||
|
||||
$scope.shutdown = function () {
|
||||
restarting = true;
|
||||
$http.post(urlbase + '/shutdown').success(function () {
|
||||
setTimeout($scope.refresh(), 250);
|
||||
$('#shutdown').modal({backdrop: 'static', keyboard: false});
|
||||
});
|
||||
$scope.configInSync = true;
|
||||
};
|
||||
@@ -284,6 +297,10 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
$('#editNode').modal({backdrop: 'static', keyboard: true});
|
||||
};
|
||||
|
||||
$scope.idNode = function () {
|
||||
$('#idqr').modal('show');
|
||||
};
|
||||
|
||||
$scope.addNode = function () {
|
||||
$scope.currentNode = {AddressesStr: 'dynamic'};
|
||||
$scope.editingExisting = false;
|
||||
@@ -303,7 +320,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
});
|
||||
$scope.config.Nodes = $scope.nodes;
|
||||
|
||||
for (var id in repos) {
|
||||
for (var id in $scope.repos) {
|
||||
$scope.repos[id].Nodes = $scope.repos[id].Nodes.filter(function (n) {
|
||||
return n.NodeID !== $scope.currentNode.NodeID;
|
||||
});
|
||||
@@ -319,7 +336,7 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
$scope.configInSync = false;
|
||||
$('#editNode').modal('hide');
|
||||
nodeCfg = $scope.currentNode;
|
||||
nodeCfg.NodeID = nodeCfg.NodeID.replace(/ /g, '').replace(/-/g, '').trim();
|
||||
nodeCfg.NodeID = nodeCfg.NodeID.replace(/ /g, '').replace(/-/g, '').toUpperCase().trim();
|
||||
nodeCfg.Addresses = nodeCfg.AddressesStr.split(',').map(function (x) { return x.trim(); });
|
||||
|
||||
done = false;
|
||||
@@ -388,10 +405,16 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
}
|
||||
|
||||
$scope.editRepo = function (nodeCfg) {
|
||||
$scope.currentRepo = $.extend({selectedNodes: {}}, nodeCfg);
|
||||
$scope.currentRepo = angular.copy(nodeCfg);
|
||||
$scope.currentRepo.selectedNodes = {};
|
||||
$scope.currentRepo.Nodes.forEach(function (n) {
|
||||
$scope.currentRepo.selectedNodes[n.NodeID] = true;
|
||||
});
|
||||
if ($scope.currentRepo.Versioning && $scope.currentRepo.Versioning.Type === "simple") {
|
||||
$scope.currentRepo.simpleFileVersioning = true;
|
||||
$scope.currentRepo.simpleKeep = +$scope.currentRepo.Versioning.Params.keep;
|
||||
}
|
||||
$scope.currentRepo.simpleKeep = $scope.currentRepo.simpleKeep || 5;
|
||||
$scope.editingExisting = true;
|
||||
$scope.repoEditor.$setPristine();
|
||||
$('#editRepo').modal({backdrop: 'static', keyboard: true});
|
||||
@@ -419,12 +442,34 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http) {
|
||||
}
|
||||
delete repoCfg.selectedNodes;
|
||||
|
||||
if (repoCfg.simpleFileVersioning) {
|
||||
repoCfg.Versioning = {
|
||||
'Type': 'simple',
|
||||
'Params': {
|
||||
'keep': '' + repoCfg.simpleKeep,
|
||||
}
|
||||
};
|
||||
delete repoCfg.simpleFileVersioning;
|
||||
delete repoCfg.simpleKeep;
|
||||
} else {
|
||||
delete repoCfg.Versioning;
|
||||
}
|
||||
|
||||
$scope.repos[repoCfg.ID] = repoCfg;
|
||||
$scope.config.Repositories = repoList($scope.repos);
|
||||
|
||||
$http.post(urlbase + '/config', JSON.stringify($scope.config), {headers: {'Content-Type': 'application/json'}});
|
||||
};
|
||||
|
||||
$scope.sharesRepo = function(repoCfg) {
|
||||
var names = [];
|
||||
repoCfg.Nodes.forEach(function (node) {
|
||||
names.push($scope.nodeName($scope.findNode(node.NodeID)));
|
||||
});
|
||||
names.sort();
|
||||
return names.join(", ");
|
||||
};
|
||||
|
||||
$scope.deleteRepo = function () {
|
||||
$('#editRepo').modal('hide');
|
||||
if (!$scope.editingExisting) {
|
||||
@@ -603,6 +648,12 @@ syncthing.filter('shortPath', function () {
|
||||
}
|
||||
});
|
||||
|
||||
syncthing.filter('clean', function () {
|
||||
return function (input) {
|
||||
return encodeURIComponent(input).replace(/%/g, '');
|
||||
}
|
||||
});
|
||||
|
||||
syncthing.directive('optionEditor', function () {
|
||||
return {
|
||||
restrict: 'C',
|
||||
@@ -635,3 +686,25 @@ syncthing.directive('uniqueRepo', function() {
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
syncthing.directive('validNodeid', function() {
|
||||
return {
|
||||
require: 'ngModel',
|
||||
link: function(scope, elm, attrs, ctrl) {
|
||||
ctrl.$parsers.unshift(function(viewValue) {
|
||||
if (scope.editingExisting) {
|
||||
// we shouldn't validate
|
||||
ctrl.$setValidity('validNodeid', true);
|
||||
} else {
|
||||
var cleaned = viewValue.replace(/ /g, '').replace(/-/g, '').toUpperCase().trim();
|
||||
if (cleaned.match(/^[A-Z2-7]{52}$/)) {
|
||||
ctrl.$setValidity('validNodeid', true);
|
||||
} else {
|
||||
ctrl.$setValidity('validNodeid', false);
|
||||
}
|
||||
}
|
||||
return viewValue;
|
||||
});
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
472
gui/index.html
472
gui/index.html
@@ -60,6 +60,7 @@
|
||||
}
|
||||
|
||||
.table th {
|
||||
white-space:nowrap;
|
||||
font-weight: 400;
|
||||
}
|
||||
|
||||
@@ -77,19 +78,20 @@
|
||||
<div class="container">
|
||||
<span class="navbar-brand"><img class="logo" src="st-logo-128.png" width="32" height="32" /> Syncthing<small class="hidden-xs"> <span class="text-muted">|</span> {{thisNodeName()}}</small></span>
|
||||
<ul class="nav navbar-nav navbar-right">
|
||||
<li class="dropdown">
|
||||
<li class="dropdown">
|
||||
<a href="#" class="dropdown-toggle" data-toggle="dropdown">Edit <b class="caret"></b></a>
|
||||
<ul class="dropdown-menu">
|
||||
<li><a ng-click="addRepo()"><span class="glyphicon glyphicon-hdd"></span> Add Repository</a></li>
|
||||
<li><a ng-click="addNode()"><span class="glyphicon glyphicon-retweet"></span> Add Node</a></li>
|
||||
<li><a href="" ng-click="addRepo()"><span class="glyphicon glyphicon-hdd"></span> Add Repository</a></li>
|
||||
<li><a href="" ng-click="addNode()"><span class="glyphicon glyphicon-retweet"></span> Add Node</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a ng-click="editSettings()"><span class="glyphicon glyphicon-cog"></span> Settings</a></li>
|
||||
<li><a href="" ng-click="editSettings()"><span class="glyphicon glyphicon-cog"></span> Settings</a></li>
|
||||
<li><a href="" ng-click="idNode()"><span class="glyphicon glyphicon-qrcode"></span> Show ID</a></li>
|
||||
<li class="divider"></li>
|
||||
<li><a ng-click="shutdown()"><span class="glyphicon glyphicon-off"></span> Shutdown</a></li>
|
||||
<li><a ng-click="restart()"><span class="glyphicon glyphicon-refresh"></span> Restart</a></li>
|
||||
<li><a href="" ng-click="shutdown()"><span class="glyphicon glyphicon-off"></span> Shutdown</a></li>
|
||||
<li><a href="" ng-click="restart()"><span class="glyphicon glyphicon-refresh"></span> Restart</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
@@ -119,164 +121,171 @@
|
||||
<!-- Repository list (top left) -->
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="panel-group" id="repositories">
|
||||
<div class="panel panel-{{repoClass(repo.ID)}}" ng-repeat="repo in repoList()">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#repositories" href="#repo-{{repo.ID}}">
|
||||
<span class="glyphicon glyphicon-hdd"></span> {{repo.Directory | shortPath}}
|
||||
<span class="pull-right">{{repoStatus(repo.ID)}}</span>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div id="repo-{{repo.ID}}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tag"></span> Repository ID</th>
|
||||
<td class="text-right">{{repo.ID}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-folder-open"></span> Folder</th>
|
||||
<td class="text-right">{{repo.Directory}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-comment"></span> Synchronization</th>
|
||||
<td class="text-right">{{repoStatus(repo.ID)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-globe"></span> Global Repository</th>
|
||||
<td class="text-right">{{model[repo.ID].globalFiles | alwaysNumber}} files, {{model[repo.ID].globalBytes | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-home"></span> Local Repository</th>
|
||||
<td class="text-right">{{model[repo.ID].localFiles | alwaysNumber}} files, {{model[repo.ID].localBytes | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> Out of Sync</th>
|
||||
<td class="text-right">{{model[repo.ID].needFiles | alwaysNumber}} files, {{model[repo.ID].needBytes | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-lock"></span> Master Repository</th>
|
||||
<td class="text-right">
|
||||
<span ng-if="repo.ReadOnly">Yes</span>
|
||||
<span ng-if="!repo.ReadOnly">No</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-share-alt"></span> Shared With</th>
|
||||
<td class="text-right">
|
||||
<span ng-repeat="n in repo.Nodes">
|
||||
{{nodeName(findNode(n.NodeID))}}<span ng-if="!$last">, </span>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-group" id="repositories">
|
||||
<div class="panel panel-{{repoClass(repo.ID)}}" ng-repeat="repo in repoList()">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#repositories" href="#repo-{{repo.ID | clean}}">
|
||||
<span class="glyphicon glyphicon-hdd"></span> {{repo.Directory | shortPath}}
|
||||
<span class="pull-right hidden-xs">{{repoStatus(repo.ID)}}</span>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div id="repo-{{repo.ID | clean}}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tag"></span> Repository ID</th>
|
||||
<td class="text-right">{{repo.ID}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-folder-open"></span> Folder</th>
|
||||
<td class="text-right">{{repo.Directory}}</td>
|
||||
</tr>
|
||||
<tr ng-if="model[repo.ID].invalid">
|
||||
<th><span class="glyphicon glyphicon-warning-sign"></span> Error</th>
|
||||
<td class="text-right">{{model[repo.ID].invalid}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-comment"></span> Synchronization</th>
|
||||
<td class="text-right">{{repoStatus(repo.ID)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-globe"></span> Global Repository</th>
|
||||
<td class="text-right">{{model[repo.ID].globalFiles | alwaysNumber}} files, {{model[repo.ID].globalBytes | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-home"></span> Local Repository</th>
|
||||
<td class="text-right">{{model[repo.ID].localFiles | alwaysNumber}} files, {{model[repo.ID].localBytes | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> Out of Sync</th>
|
||||
<td class="text-right">{{model[repo.ID].needFiles | alwaysNumber}} files, {{model[repo.ID].needBytes | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-lock"></span> Master Repository</th>
|
||||
<td class="text-right">
|
||||
<span ng-if="repo.ReadOnly">Yes</span>
|
||||
<span ng-if="!repo.ReadOnly">No</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-unchecked"></span> Ignore Permissions</th>
|
||||
<td class="text-right">
|
||||
<span ng-if="repo.IgnorePerms">Yes</span>
|
||||
<span ng-if="!repo.IgnorePerms">No</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-share-alt"></span> Shared With</th>
|
||||
<td class="text-right">{{sharesRepo(repo)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editRepo(repo)"><span class="glyphicon glyphicon-pencil"></span> Edit</a></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editRepo(repo)"><span class="glyphicon glyphicon-pencil"></span> Edit</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Node list (top right) -->
|
||||
|
||||
<div class="col-md-6">
|
||||
<div class="panel-group" id="nodes">
|
||||
<div class="panel panel-default" ng-repeat="nodeCfg in [thisNode()]">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#nodes" href="#node-{{nodeCfg.NodeID}}"><span class="glyphicon glyphicon-home"></span> {{nodeName(nodeCfg)}}</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div id="node-{{nodeCfg.NodeID}}" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-th"></span> RAM Utilization</th>
|
||||
<td class="text-right">{{system.sys | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tasks"></span> CPU Utilization</th>
|
||||
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> Download Rate</th>
|
||||
<td class="text-right">{{inbps | metric}}bps</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-upload"></span> Upload Rate</th>
|
||||
<td class="text-right">{{outbps | metric}}bps </td>
|
||||
</tr>
|
||||
<tr ng-if="system.extAnnounceOK != undefined">
|
||||
<th><span class="glyphicon glyphicon-bullhorn"></span> Announce Server</th>
|
||||
<td class="text-right">
|
||||
<span class="data text-success" ng-if="system.extAnnounceOK">Online</span>
|
||||
<span class="data text-danger" ng-if="!system.extAnnounceOK">Offline</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tag"></span> Version</th>
|
||||
<td class="text-right">{{version}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel-group" id="nodes">
|
||||
<div class="panel panel-default" ng-repeat="nodeCfg in [thisNode()]">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#nodes" href="#node-{{nodeCfg.NodeID | clean}}"><span class="glyphicon glyphicon-home"></span> {{nodeName(nodeCfg)}}</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div id="node-{{nodeCfg.NodeID | clean}}" class="panel-collapse collapse in">
|
||||
<div class="panel-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-th"></span> RAM Utilization</th>
|
||||
<td class="text-right">{{system.sys | binary}}B</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tasks"></span> CPU Utilization</th>
|
||||
<td class="text-right">{{system.cpuPercent | alwaysNumber | natural:1}}%</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> Download Rate</th>
|
||||
<td class="text-right">{{connections['total'].inbps | metric}}bps ({{connections['total'].InBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-upload"></span> Upload Rate</th>
|
||||
<td class="text-right">{{connections['total'].outbps | metric}}bps ({{connections['total'].OutBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr ng-if="system.extAnnounceOK != undefined">
|
||||
<th><span class="glyphicon glyphicon-bullhorn"></span> Announce Server</th>
|
||||
<td class="text-right">
|
||||
<span class="data text-success" ng-if="system.extAnnounceOK">Online</span>
|
||||
<span class="data text-danger" ng-if="!system.extAnnounceOK">Offline</span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tag"></span> Version</th>
|
||||
<td class="text-right">{{version}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span> Edit</a></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span> Edit</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="panel panel-{{nodeClass(nodeCfg)}}" ng-repeat="nodeCfg in otherNodes()">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#nodes" href="#node-{{nodeCfg.NodeID}}">
|
||||
<span class="glyphicon glyphicon-retweet"></span>
|
||||
{{nodeName(nodeCfg)}}
|
||||
<span class="pull-right">{{nodeStatus(nodeCfg)}}</span>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div id="node-{{nodeCfg.NodeID}}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-link"></span> Address</th>
|
||||
<td class="text-right">{{nodeAddr(nodeCfg)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-comment"></span> Synchronization</th>
|
||||
<td class="text-right">{{nodeStatus(nodeCfg)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> Download Rate</th>
|
||||
<td class="text-right">{{connections[nodeCfg.NodeID].inbps | metric}}bps</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-upload"></span> Upload Rate</th>
|
||||
<td class="text-right">{{connections[nodeCfg.NodeID].outbps | metric}}bps </td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tag"></span> Version</th>
|
||||
<td class="text-right">{{nodeVer(nodeCfg)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
<div class="panel panel-{{nodeClass(nodeCfg)}}" ng-repeat="nodeCfg in otherNodes()">
|
||||
<div class="panel-heading">
|
||||
<h3 class="panel-title">
|
||||
<a data-toggle="collapse" data-parent="#nodes" href="#node-{{nodeCfg.NodeID}}">
|
||||
<span class="glyphicon glyphicon-retweet"></span>
|
||||
{{nodeName(nodeCfg)}}
|
||||
<span class="pull-right hidden-xs">{{nodeStatus(nodeCfg)}}</span>
|
||||
</a>
|
||||
</h3>
|
||||
</div>
|
||||
<div id="node-{{nodeCfg.NodeID}}" class="panel-collapse collapse">
|
||||
<div class="panel-body">
|
||||
<div class="table-responsive">
|
||||
<table class="table table-condensed table-striped">
|
||||
<tbody>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-link"></span> Address</th>
|
||||
<td class="text-right">{{nodeAddr(nodeCfg)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-comment"></span> Synchronization</th>
|
||||
<td class="text-right">{{nodeStatus(nodeCfg)}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-download"></span> Download Rate</th>
|
||||
<td class="text-right">{{connections[nodeCfg.NodeID].inbps | metric}}bps ({{connections[nodeCfg.NodeID].InBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-cloud-upload"></span> Upload Rate</th>
|
||||
<td class="text-right">{{connections[nodeCfg.NodeID].outbps | metric}}bps ({{connections[nodeCfg.NodeID].OutBytesTotal | binary}}B)</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-tag"></span> Version</th>
|
||||
<td class="text-right">{{nodeVer(nodeCfg)}}</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span> Edit</a></span>
|
||||
</div>
|
||||
</div>
|
||||
<span class="pull-right"><a class="btn btn-sm btn-primary" href="" ng-click="editNode(nodeCfg)"><span class="glyphicon glyphicon-pencil"></span> Edit</a></span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div> <!-- /row -->
|
||||
|
||||
<!-- Errors -->
|
||||
@@ -353,13 +362,56 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Shutdown modal -->
|
||||
|
||||
<div id="shutdown" class="modal fade">
|
||||
<div class="modal-dialog">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header alert alert-success">
|
||||
<h4 class="modal-title">
|
||||
<span class="glyphicon glyphicon-off"></span>
|
||||
Shutdown Complete
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<p>
|
||||
Syncthing has been shut down.
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- ID modal -->
|
||||
|
||||
<div id="idqr" class="modal fade">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<h4 class="modal-title">
|
||||
<span class="glyphicon glyphicon-qrcode"></span>
|
||||
Node Identification — {{nodeName(thisNode())}}
|
||||
</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<div class="well well-sm text-monospace text-center">
|
||||
{{myID | chunkID}}
|
||||
</div>
|
||||
<img ng-if="myID" class="center-block img-thumbnail" src="qr/{{myID | chunkID}}"/>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span> Close</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Node editor modal -->
|
||||
|
||||
<div id="editNode" class="modal fade">
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 ng-show="!editingExisting" class="modal-title">Add Node</h4>
|
||||
<h4 ng-show="editingExisting" class="modal-title">Edit Node</h4>
|
||||
</div>
|
||||
@@ -367,11 +419,14 @@
|
||||
<form role="form" name="nodeEditor">
|
||||
<div class="form-group" ng-class="{'has-error': nodeEditor.nodeID.$invalid && nodeEditor.nodeID.$dirty}">
|
||||
<label for="nodeID">Node ID</label>
|
||||
<input ng-if="!editingExisting" name="nodeID" id="nodeID" class="form-control text-monospace" type="text" ng-model="currentNode.NodeID" required></input>
|
||||
<input ng-if="!editingExisting" name="nodeID" id="nodeID" class="form-control text-monospace" type="text" ng-model="currentNode.NodeID" required valid-nodeid></input>
|
||||
<div ng-if="editingExisting" class="well well-sm text-monospace">{{currentNode.NodeID | chunkID}}</div>
|
||||
<p class="help-block">
|
||||
<span ng-if="nodeEditor.nodeID.$valid || nodeEditor.nodeID.$pristine">The node ID to enter here can be found in the "Add Node" dialog on the other node. Spaces and dashes are optional (ignored).</span>
|
||||
<span ng-if="nodeEditor.nodeID.$valid || nodeEditor.nodeID.$pristine">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).
|
||||
<span ng-show="!editingExisting">When adding a new node, keep in mind that <em>this node</em> must be added on the other side too.</span>
|
||||
</span>
|
||||
<span ng-if="nodeEditor.nodeID.$error.required && nodeEditor.nodeID.$dirty">The node ID cannot be blank.</span>
|
||||
<span ng-if="nodeEditor.nodeID.$error.validNodeid && nodeEditor.nodeID.$dirty">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.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
@@ -385,10 +440,6 @@
|
||||
<p class="help-block">Enter comma separated <span class="text-monospace">ip:port</span> addresses or <span class="text-monospace">dynamic</span> to perform automatic discovery of the address.</p>
|
||||
</div>
|
||||
</form>
|
||||
<div ng-show="!editingExisting">
|
||||
When adding a new node, keep in mind that <em>this node</em> must be added on the other side too. The Node ID of this node is:
|
||||
<div class="well well-sm text-monospace">{{myID | chunkID}}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-primary" ng-click="saveNode()" ng-disabled="nodeEditor.$invalid"><span class="glyphicon glyphicon-ok"></span> Save</button>
|
||||
@@ -405,45 +456,81 @@
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 ng-show="!editingExisting" class="modal-title">Add Repository</h4>
|
||||
<h4 ng-show="editingExisting" class="modal-title">Edit Repository</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<form role="form" name="repoEditor">
|
||||
<div class="form-group" ng-class="{'has-error': repoEditor.repoID.$invalid && repoEditor.repoID.$dirty}">
|
||||
<label for="repoID">Repository ID</label>
|
||||
<input name="repoID" placeholder="documents" ng-disabled="editingExisting" id="repoID" class="form-control" type="text" ng-model="currentRepo.ID" required unique-repo></input>
|
||||
<p class="help-block">
|
||||
<span ng-if="repoEditor.repoID.$valid || repoEditor.repoID.$pristine">Short identifier for the repository. Must be the same on all cluster nodes.</span>
|
||||
<span ng-if="repoEditor.repoID.$error.uniqueRepo">The repository ID must be unique.</span>
|
||||
<span ng-if="repoEditor.repoID.$error.required && repoEditor.repoID.$dirty">The repository ID cannot be blank.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': repoEditor.repoPath.$invalid && repoEditor.repoPath.$dirty}">
|
||||
<label for="repoPath">Repository Path</label>
|
||||
<input name="repoPath" placeholder="~/Documents" id="repoPath" class="form-control" type="text" ng-model="currentRepo.Directory" required></input>
|
||||
<p class="help-block">
|
||||
<span ng-if="repoEditor.repoPath.$valid || repoEditor.repoPath.$pristine">Path to the repository on the local computer. Will be created if it does not exist. The tilde character <code>~</code> can be used as a shortcut for <code>{{system.tilde}}</code>.</span>
|
||||
<span ng-if="repoEditor.repoPath.$error.required && repoEditor.repoPath.$dirty">The repository path cannot be blank.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="currentRepo.ReadOnly"> Repository Master
|
||||
</label>
|
||||
<div class="row">
|
||||
<div class="col-md-12">
|
||||
<div class="form-group" ng-class="{'has-error': repoEditor.repoID.$invalid && repoEditor.repoID.$dirty}">
|
||||
<label for="repoID">Repository ID</label>
|
||||
<input name="repoID" placeholder="documents" ng-disabled="editingExisting" id="repoID" class="form-control" type="text" ng-model="currentRepo.ID" required unique-repo ng-pattern="/^[a-zaZ0-9-_.]{1,64}$/"></input>
|
||||
<p class="help-block">
|
||||
<span ng-if="repoEditor.repoID.$valid || repoEditor.repoID.$pristine">Short identifier for the repository. Must be the same on all cluster nodes.</span>
|
||||
<span ng-if="repoEditor.repoID.$error.uniqueRepo">The repository ID must be unique.</span>
|
||||
<span ng-if="repoEditor.repoID.$error.required && repoEditor.repoID.$dirty">The repository ID cannot be blank.</span>
|
||||
<span ng-if="repoEditor.repoID.$error.pattern && repoEditor.repoID.$dirty">The repository ID must be a short identifier (64 characters or less) consisting of letters, numbers and the the <code>-_.</code> characters only.</span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="form-group" ng-class="{'has-error': repoEditor.repoPath.$invalid && repoEditor.repoPath.$dirty}">
|
||||
<label for="repoPath">Repository Path</label>
|
||||
<input name="repoPath" placeholder="~/Documents" id="repoPath" class="form-control" type="text" ng-model="currentRepo.Directory" required></input>
|
||||
<p class="help-block">
|
||||
<span ng-if="repoEditor.repoPath.$valid || repoEditor.repoPath.$pristine">Path to the repository on the local computer. Will be created if it does not exist. The tilde character <code>~</code> can be used as a shortcut for <code>{{system.tilde}}</code>.</span>
|
||||
<span ng-if="repoEditor.repoPath.$error.required && repoEditor.repoPath.$dirty">The repository path cannot be blank.</span>
|
||||
</p>
|
||||
</div>
|
||||
</div>
|
||||
<p class="help-block">Files are protected from changes made on other nodes, but changes made on <em>this</em> node will be sent to the rest of the cluster.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="nodes">Nodes</label>
|
||||
<div class="checkbox" ng-repeat="node in otherNodes()">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="currentRepo.selectedNodes[node.NodeID]"> {{nodeName(node)}}
|
||||
</label>
|
||||
<div class="row">
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="currentRepo.ReadOnly"> Repository Master
|
||||
</label>
|
||||
</div>
|
||||
<p class="help-block">Files are protected from changes made on other nodes, but changes made on <em>this</em> node will be sent to the rest of the cluster.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="currentRepo.IgnorePerms"> Ignore Permissions
|
||||
</label>
|
||||
</div>
|
||||
<p class="help-block">File permission bits are ignored when looking for changes. Use on FAT filesystems.</p>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="nodes">Nodes</label>
|
||||
<div class="checkbox" ng-repeat="node in otherNodes()">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="currentRepo.selectedNodes[node.NodeID]"> {{nodeName(node)}}
|
||||
</label>
|
||||
</div>
|
||||
<p class="help-block">Select the nodes to share this repository with.</p>
|
||||
</div>
|
||||
</div>
|
||||
<div class="col-md-6">
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input type="checkbox" ng-model="currentRepo.simpleFileVersioning"> File Versioning
|
||||
</label>
|
||||
</div>
|
||||
<p class="help-block">Files are moved to date stamped versions in a <code>.stversions</code> folder when replaced or deleted by syncthing.</p>
|
||||
</div>
|
||||
<div class="form-group" ng-if="currentRepo.simpleFileVersioning" ng-class="{'has-error': repoEditor.simpleKeep.$invalid && repoEditor.simpleKeep.$dirty}">
|
||||
<label for="simpleKeep">Keep Versions</label>
|
||||
<input name="simpleKeep" id="simpleKeep" class="form-control" type="number" ng-model="currentRepo.simpleKeep" required min="1"></input>
|
||||
<p class="help-block">
|
||||
<span ng-if="repoEditor.simpleKeep.$valid || repoEditor.simpleKeep.$pristine">The number of old versions to keep, per file.</span>
|
||||
<span ng-if="repoEditor.simpleKeep.$error.required && repoEditor.simpleKeep.$dirty">The number of versions must be a number and cannot be blank.</span>
|
||||
<span ng-if="repoEditor.simpleKeep.$error.min && repoEditor.simpleKeep.$dirty">You must keep at least one version.</span>
|
||||
</p>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
<p class="help-block">Select the nodes to share this repository with.</p>
|
||||
</div>
|
||||
</form>
|
||||
<div ng-show="!editingExisting">
|
||||
@@ -465,7 +552,6 @@
|
||||
<div class="modal-dialog modal-lg">
|
||||
<div class="modal-content">
|
||||
<div class="modal-header">
|
||||
<button type="button" class="close" data-dismiss="modal" aria-hidden="true">×</button>
|
||||
<h4 class="modal-title"> Settings</h4>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
@@ -475,11 +561,11 @@
|
||||
<div class="form-group" ng-repeat="setting in settings">
|
||||
<div ng-if="setting.type == 'text' || setting.type == 'number'">
|
||||
<label for="{{setting.id}}">{{setting.descr}}</label>
|
||||
<input id="{{setting.id}}" class="form-control" type="{{setting.type}}" ng-model="config.Options[setting.id]"></input>
|
||||
<input id="{{setting.id}}" class="form-control" type="{{setting.type}}" ng-model="config.workingOptions[setting.id]"></input>
|
||||
</div>
|
||||
<div class="checkbox" ng-if="setting.type == 'bool'">
|
||||
<label>
|
||||
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.Options[setting.id]"></input>
|
||||
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.workingOptions[setting.id]"></input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
@@ -488,11 +574,11 @@
|
||||
<div class="form-group" ng-repeat="setting in guiSettings">
|
||||
<div ng-if="setting.type == 'text' || setting.type == 'number' || setting.type == 'password'">
|
||||
<label for="{{setting.id}}">{{setting.descr}}</label>
|
||||
<input id="{{setting.id}}" class="form-control" type="{{setting.type}}" ng-model="config.GUI[setting.id]"></input>
|
||||
<input id="{{setting.id}}" class="form-control" type="{{setting.type}}" ng-model="config.workingGUI[setting.id]"></input>
|
||||
</div>
|
||||
<div class="checkbox" ng-if="setting.type == 'bool'">
|
||||
<label>
|
||||
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.GUI[setting.id]"></input>
|
||||
{{setting.descr}} <input id="{{setting.id}}" type="checkbox" ng-model="config.workingGUI[setting.id]"></input>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -16,6 +16,7 @@ type bqBlock struct {
|
||||
file scanner.File
|
||||
block scanner.Block // get this block from the network
|
||||
copy []scanner.Block // copy these blocks from the old version of the file
|
||||
first bool
|
||||
last bool
|
||||
}
|
||||
|
||||
@@ -47,24 +48,30 @@ func (q *blockQueue) addBlock(a bqAdd) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
l := len(a.need)
|
||||
|
||||
if len(a.have) > 0 {
|
||||
// First queue a copy operation
|
||||
q.queued = append(q.queued, bqBlock{
|
||||
file: a.file,
|
||||
copy: a.have,
|
||||
file: a.file,
|
||||
copy: a.have,
|
||||
first: true,
|
||||
last: l == 0,
|
||||
})
|
||||
}
|
||||
|
||||
// Queue the needed blocks individually
|
||||
l := len(a.need)
|
||||
for i, b := range a.need {
|
||||
q.queued = append(q.queued, bqBlock{
|
||||
file: a.file,
|
||||
block: b,
|
||||
first: len(a.have) == 0 && i == 0,
|
||||
last: i == l-1,
|
||||
})
|
||||
}
|
||||
|
||||
if l == 0 {
|
||||
if len(a.need)+len(a.have) == 0 {
|
||||
// If we didn't have anything to fetch, queue an empty block with the "last" flag set to close the file.
|
||||
q.queued = append(q.queued, bqBlock{
|
||||
file: a.file,
|
||||
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
"github.com/calmh/syncthing/config"
|
||||
"github.com/calmh/syncthing/files"
|
||||
"github.com/calmh/syncthing/lamport"
|
||||
"github.com/calmh/syncthing/osutil"
|
||||
"github.com/calmh/syncthing/protocol"
|
||||
"github.com/calmh/syncthing/scanner"
|
||||
)
|
||||
@@ -43,12 +44,12 @@ type Model struct {
|
||||
clientName string
|
||||
clientVersion string
|
||||
|
||||
repoDirs map[string]string // repo -> dir
|
||||
repoFiles map[string]*files.Set // repo -> files
|
||||
repoNodes map[string][]string // repo -> nodeIDs
|
||||
nodeRepos map[string][]string // nodeID -> repos
|
||||
suppressor map[string]*suppressor // repo -> suppressor
|
||||
rmut sync.RWMutex // protects the above
|
||||
repoCfgs map[string]config.RepositoryConfiguration // repo -> cfg
|
||||
repoFiles map[string]*files.Set // repo -> files
|
||||
repoNodes map[string][]string // repo -> nodeIDs
|
||||
nodeRepos map[string][]string // nodeID -> repos
|
||||
suppressor map[string]*suppressor // repo -> suppressor
|
||||
rmut sync.RWMutex // protects the above
|
||||
|
||||
repoState map[string]repoState // repo -> state
|
||||
smut sync.RWMutex
|
||||
@@ -80,7 +81,7 @@ func NewModel(indexDir string, cfg *config.Configuration, clientName, clientVers
|
||||
cfg: cfg,
|
||||
clientName: clientName,
|
||||
clientVersion: clientVersion,
|
||||
repoDirs: make(map[string]string),
|
||||
repoCfgs: make(map[string]config.RepositoryConfiguration),
|
||||
repoFiles: make(map[string]*files.Set),
|
||||
repoNodes: make(map[string][]string),
|
||||
nodeRepos: make(map[string][]string),
|
||||
@@ -104,10 +105,10 @@ func (m *Model) StartRepoRW(repo string, threads int) {
|
||||
m.rmut.RLock()
|
||||
defer m.rmut.RUnlock()
|
||||
|
||||
if dir, ok := m.repoDirs[repo]; !ok {
|
||||
if cfg, ok := m.repoCfgs[repo]; !ok {
|
||||
panic("cannot start without repo")
|
||||
} else {
|
||||
newPuller(repo, dir, m, threads, m.cfg)
|
||||
newPuller(cfg, m, threads, m.cfg)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -149,9 +150,9 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
|
||||
|
||||
for _, repo := range m.nodeRepos[node] {
|
||||
for _, f := range m.repoFiles[repo].Global() {
|
||||
if f.Flags&protocol.FlagDeleted == 0 {
|
||||
if !protocol.IsDeleted(f.Flags) {
|
||||
size := f.Size
|
||||
if f.Flags&protocol.FlagDirectory != 0 {
|
||||
if protocol.IsDirectory(f.Flags) {
|
||||
size = zeroEntrySize
|
||||
}
|
||||
tot += size
|
||||
@@ -160,9 +161,9 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
|
||||
}
|
||||
|
||||
for _, f := range m.repoFiles[repo].Need(m.cm.Get(node)) {
|
||||
if f.Flags&protocol.FlagDeleted == 0 {
|
||||
if !protocol.IsDeleted(f.Flags) {
|
||||
size := f.Size
|
||||
if f.Flags&protocol.FlagDirectory != 0 {
|
||||
if protocol.IsDirectory(f.Flags) {
|
||||
size = zeroEntrySize
|
||||
}
|
||||
have -= size
|
||||
@@ -181,14 +182,23 @@ func (m *Model) ConnectionStats() map[string]ConnectionInfo {
|
||||
m.rmut.RUnlock()
|
||||
m.pmut.RUnlock()
|
||||
|
||||
in, out := protocol.TotalInOut()
|
||||
res["total"] = ConnectionInfo{
|
||||
Statistics: protocol.Statistics{
|
||||
At: time.Now(),
|
||||
InBytesTotal: int(in),
|
||||
OutBytesTotal: int(out),
|
||||
},
|
||||
}
|
||||
|
||||
return res
|
||||
}
|
||||
|
||||
func sizeOf(fs []scanner.File) (files, deleted int, bytes int64) {
|
||||
for _, f := range fs {
|
||||
if f.Flags&protocol.FlagDeleted == 0 {
|
||||
if !protocol.IsDeleted(f.Flags) {
|
||||
files++
|
||||
if f.Flags&protocol.FlagDirectory == 0 {
|
||||
if !protocol.IsDirectory(f.Flags) {
|
||||
bytes += f.Size
|
||||
} else {
|
||||
bytes += zeroEntrySize
|
||||
@@ -252,7 +262,7 @@ func (m *Model) Index(nodeID string, repo string, fs []protocol.FileInfo) {
|
||||
lamport.Default.Tick(f.Version)
|
||||
if debug {
|
||||
var flagComment string
|
||||
if f.Flags&protocol.FlagDeleted != 0 {
|
||||
if protocol.IsDeleted(f.Flags) {
|
||||
flagComment = " (deleted)"
|
||||
}
|
||||
l.Debugf("IDX(in): %s %q/%q m=%d f=%o%s v=%d (%d blocks)", nodeID, repo, f.Name, f.Modified, f.Flags, flagComment, f.Version, len(f.Blocks))
|
||||
@@ -265,7 +275,8 @@ func (m *Model) Index(nodeID string, repo string, fs []protocol.FileInfo) {
|
||||
if r, ok := m.repoFiles[repo]; ok {
|
||||
r.Replace(id, files)
|
||||
} else {
|
||||
l.Warnf("Index from %s for nonexistant repo %q; dropping", nodeID, repo)
|
||||
l.Warnf("Index from %s for unexpected repo %q; verify configuration", nodeID, repo)
|
||||
|
||||
}
|
||||
m.rmut.RUnlock()
|
||||
}
|
||||
@@ -283,7 +294,7 @@ func (m *Model) IndexUpdate(nodeID string, repo string, fs []protocol.FileInfo)
|
||||
lamport.Default.Tick(f.Version)
|
||||
if debug {
|
||||
var flagComment string
|
||||
if f.Flags&protocol.FlagDeleted != 0 {
|
||||
if protocol.IsDeleted(f.Flags) {
|
||||
flagComment = " (deleted)"
|
||||
}
|
||||
l.Debugf("IDXUP(in): %s %q/%q m=%d f=%o%s v=%d (%d blocks)", nodeID, repo, f.Name, f.Modified, f.Flags, flagComment, f.Version, len(f.Blocks))
|
||||
@@ -368,7 +379,7 @@ func (m *Model) Request(nodeID, repo, name string, offset int64, size int) ([]by
|
||||
}
|
||||
|
||||
lf := r.Get(cid.LocalID, name)
|
||||
if lf.Suppressed || lf.Flags&protocol.FlagDeleted != 0 {
|
||||
if lf.Suppressed || protocol.IsDeleted(lf.Flags) {
|
||||
if debug {
|
||||
l.Debugf("REQ(in): %s: %q / %q o=%d s=%d; invalid: %v", nodeID, repo, name, offset, size, lf)
|
||||
}
|
||||
@@ -386,7 +397,7 @@ func (m *Model) Request(nodeID, repo, name string, offset int64, size int) ([]by
|
||||
l.Debugf("REQ(in): %s: %q / %q o=%d s=%d", nodeID, repo, name, offset, size)
|
||||
}
|
||||
m.rmut.RLock()
|
||||
fn := filepath.Join(m.repoDirs[repo], name)
|
||||
fn := filepath.Join(m.repoCfgs[repo].Directory, name)
|
||||
m.rmut.RUnlock()
|
||||
fd, err := os.Open(fn) // XXX: Inefficient, should cache fd?
|
||||
if err != nil {
|
||||
@@ -502,7 +513,7 @@ func (m *Model) protocolIndex(repo string) []protocol.FileInfo {
|
||||
mf := fileInfoFromFile(f)
|
||||
if debug {
|
||||
var flagComment string
|
||||
if mf.Flags&protocol.FlagDeleted != 0 {
|
||||
if protocol.IsDeleted(mf.Flags) {
|
||||
flagComment = " (deleted)"
|
||||
}
|
||||
l.Debugf("IDX(out): %q/%q m=%d f=%o%s v=%d (%d blocks)", repo, mf.Name, mf.Modified, mf.Flags, flagComment, mf.Version, len(mf.Blocks))
|
||||
@@ -582,23 +593,23 @@ func (m *Model) broadcastIndexLoop() {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *Model) AddRepo(id, dir string, nodes []config.NodeConfiguration) {
|
||||
func (m *Model) AddRepo(cfg config.RepositoryConfiguration) {
|
||||
if m.started {
|
||||
panic("cannot add repo to started model")
|
||||
}
|
||||
if len(id) == 0 {
|
||||
if len(cfg.ID) == 0 {
|
||||
panic("cannot add empty repo id")
|
||||
}
|
||||
|
||||
m.rmut.Lock()
|
||||
m.repoDirs[id] = dir
|
||||
m.repoFiles[id] = files.NewSet()
|
||||
m.suppressor[id] = &suppressor{threshold: int64(m.cfg.Options.MaxChangeKbps)}
|
||||
m.repoCfgs[cfg.ID] = cfg
|
||||
m.repoFiles[cfg.ID] = files.NewSet()
|
||||
m.suppressor[cfg.ID] = &suppressor{threshold: int64(m.cfg.Options.MaxChangeKbps)}
|
||||
|
||||
m.repoNodes[id] = make([]string, len(nodes))
|
||||
for i, node := range nodes {
|
||||
m.repoNodes[id][i] = node.NodeID
|
||||
m.nodeRepos[node.NodeID] = append(m.nodeRepos[node.NodeID], id)
|
||||
m.repoNodes[cfg.ID] = make([]string, len(cfg.Nodes))
|
||||
for i, node := range cfg.Nodes {
|
||||
m.repoNodes[cfg.ID][i] = node.NodeID
|
||||
m.nodeRepos[node.NodeID] = append(m.nodeRepos[node.NodeID], cfg.ID)
|
||||
}
|
||||
|
||||
m.addedRepo = true
|
||||
@@ -607,8 +618,8 @@ func (m *Model) AddRepo(id, dir string, nodes []config.NodeConfiguration) {
|
||||
|
||||
func (m *Model) ScanRepos() {
|
||||
m.rmut.RLock()
|
||||
var repos = make([]string, 0, len(m.repoDirs))
|
||||
for repo := range m.repoDirs {
|
||||
var repos = make([]string, 0, len(m.repoCfgs))
|
||||
for repo := range m.repoCfgs {
|
||||
repos = append(repos, repo)
|
||||
}
|
||||
m.rmut.RUnlock()
|
||||
@@ -618,7 +629,10 @@ func (m *Model) ScanRepos() {
|
||||
for _, repo := range repos {
|
||||
repo := repo
|
||||
go func() {
|
||||
m.ScanRepo(repo)
|
||||
err := m.ScanRepo(repo)
|
||||
if err != nil {
|
||||
invalidateRepo(m.cfg, repo, err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
}
|
||||
@@ -627,9 +641,9 @@ func (m *Model) ScanRepos() {
|
||||
|
||||
func (m *Model) CleanRepos() {
|
||||
m.rmut.RLock()
|
||||
var dirs = make([]string, 0, len(m.repoDirs))
|
||||
for _, dir := range m.repoDirs {
|
||||
dirs = append(dirs, dir)
|
||||
var dirs = make([]string, 0, len(m.repoCfgs))
|
||||
for _, cfg := range m.repoCfgs {
|
||||
dirs = append(dirs, cfg.Directory)
|
||||
}
|
||||
m.rmut.RUnlock()
|
||||
|
||||
@@ -651,12 +665,13 @@ func (m *Model) CleanRepos() {
|
||||
func (m *Model) ScanRepo(repo string) error {
|
||||
m.rmut.RLock()
|
||||
w := &scanner.Walker{
|
||||
Dir: m.repoDirs[repo],
|
||||
Dir: m.repoCfgs[repo].Directory,
|
||||
IgnoreFile: ".stignore",
|
||||
BlockSize: scanner.StandardBlockSize,
|
||||
TempNamer: defTempNamer,
|
||||
Suppressor: m.suppressor[repo],
|
||||
CurrentFiler: cFiler{m, repo},
|
||||
IgnorePerms: m.repoCfgs[repo].IgnorePerms,
|
||||
}
|
||||
m.rmut.RUnlock()
|
||||
m.setState(repo, RepoScanning)
|
||||
@@ -671,7 +686,7 @@ func (m *Model) ScanRepo(repo string) error {
|
||||
|
||||
func (m *Model) SaveIndexes(dir string) {
|
||||
m.rmut.RLock()
|
||||
for repo := range m.repoDirs {
|
||||
for repo := range m.repoCfgs {
|
||||
fs := m.protocolIndex(repo)
|
||||
m.saveIndex(repo, dir, fs)
|
||||
}
|
||||
@@ -680,7 +695,7 @@ func (m *Model) SaveIndexes(dir string) {
|
||||
|
||||
func (m *Model) LoadIndexes(dir string) {
|
||||
m.rmut.RLock()
|
||||
for repo := range m.repoDirs {
|
||||
for repo := range m.repoCfgs {
|
||||
fs := m.loadIndex(repo, dir)
|
||||
m.SeedLocal(repo, fs)
|
||||
}
|
||||
@@ -688,7 +703,7 @@ func (m *Model) LoadIndexes(dir string) {
|
||||
}
|
||||
|
||||
func (m *Model) saveIndex(repo string, dir string, fs []protocol.FileInfo) {
|
||||
id := fmt.Sprintf("%x", sha1.Sum([]byte(m.repoDirs[repo])))
|
||||
id := fmt.Sprintf("%x", sha1.Sum([]byte(m.repoCfgs[repo].Directory)))
|
||||
name := id + ".idx.gz"
|
||||
name = filepath.Join(dir, name)
|
||||
|
||||
@@ -706,11 +721,11 @@ func (m *Model) saveIndex(repo string, dir string, fs []protocol.FileInfo) {
|
||||
gzw.Close()
|
||||
idxf.Close()
|
||||
|
||||
Rename(name+".tmp", name)
|
||||
osutil.Rename(name+".tmp", name)
|
||||
}
|
||||
|
||||
func (m *Model) loadIndex(repo string, dir string) []protocol.FileInfo {
|
||||
id := fmt.Sprintf("%x", sha1.Sum([]byte(m.repoDirs[repo])))
|
||||
id := fmt.Sprintf("%x", sha1.Sum([]byte(m.repoCfgs[repo].Directory)))
|
||||
name := id + ".idx.gz"
|
||||
name = filepath.Join(dir, name)
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ func init() {
|
||||
|
||||
func TestRequest(t *testing.T) {
|
||||
m := NewModel("/tmp", &config.Configuration{}, "syncthing", "dev")
|
||||
m.AddRepo("default", "testdata", nil)
|
||||
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
|
||||
m.ScanRepo("default")
|
||||
|
||||
bs, err := m.Request("some node", "default", "foo", 0, 6)
|
||||
@@ -85,7 +85,7 @@ func genFiles(n int) []protocol.FileInfo {
|
||||
|
||||
func BenchmarkIndex10000(b *testing.B) {
|
||||
m := NewModel("/tmp", nil, "syncthing", "dev")
|
||||
m.AddRepo("default", "testdata", nil)
|
||||
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
|
||||
m.ScanRepo("default")
|
||||
files := genFiles(10000)
|
||||
|
||||
@@ -97,7 +97,7 @@ func BenchmarkIndex10000(b *testing.B) {
|
||||
|
||||
func BenchmarkIndex00100(b *testing.B) {
|
||||
m := NewModel("/tmp", nil, "syncthing", "dev")
|
||||
m.AddRepo("default", "testdata", nil)
|
||||
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
|
||||
m.ScanRepo("default")
|
||||
files := genFiles(100)
|
||||
|
||||
@@ -109,7 +109,7 @@ func BenchmarkIndex00100(b *testing.B) {
|
||||
|
||||
func BenchmarkIndexUpdate10000f10000(b *testing.B) {
|
||||
m := NewModel("/tmp", nil, "syncthing", "dev")
|
||||
m.AddRepo("default", "testdata", nil)
|
||||
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
|
||||
m.ScanRepo("default")
|
||||
files := genFiles(10000)
|
||||
m.Index("42", "default", files)
|
||||
@@ -122,7 +122,7 @@ func BenchmarkIndexUpdate10000f10000(b *testing.B) {
|
||||
|
||||
func BenchmarkIndexUpdate10000f00100(b *testing.B) {
|
||||
m := NewModel("/tmp", nil, "syncthing", "dev")
|
||||
m.AddRepo("default", "testdata", nil)
|
||||
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
|
||||
m.ScanRepo("default")
|
||||
files := genFiles(10000)
|
||||
m.Index("42", "default", files)
|
||||
@@ -136,7 +136,7 @@ func BenchmarkIndexUpdate10000f00100(b *testing.B) {
|
||||
|
||||
func BenchmarkIndexUpdate10000f00001(b *testing.B) {
|
||||
m := NewModel("/tmp", nil, "syncthing", "dev")
|
||||
m.AddRepo("default", "testdata", nil)
|
||||
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
|
||||
m.ScanRepo("default")
|
||||
files := genFiles(10000)
|
||||
m.Index("42", "default", files)
|
||||
@@ -183,7 +183,7 @@ func (FakeConnection) Statistics() protocol.Statistics {
|
||||
|
||||
func BenchmarkRequest(b *testing.B) {
|
||||
m := NewModel("/tmp", nil, "syncthing", "dev")
|
||||
m.AddRepo("default", "testdata", nil)
|
||||
m.AddRepo(config.RepositoryConfiguration{ID: "default", Directory: "testdata"})
|
||||
m.ScanRepo("default")
|
||||
|
||||
const n = 1000
|
||||
|
||||
204
model/puller.go
204
model/puller.go
@@ -5,13 +5,16 @@ import (
|
||||
"errors"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/calmh/syncthing/buffers"
|
||||
"github.com/calmh/syncthing/cid"
|
||||
"github.com/calmh/syncthing/config"
|
||||
"github.com/calmh/syncthing/osutil"
|
||||
"github.com/calmh/syncthing/protocol"
|
||||
"github.com/calmh/syncthing/scanner"
|
||||
"github.com/calmh/syncthing/versioner"
|
||||
)
|
||||
|
||||
type requestResult struct {
|
||||
@@ -63,8 +66,7 @@ var errNoNode = errors.New("no available source node")
|
||||
|
||||
type puller struct {
|
||||
cfg *config.Configuration
|
||||
repo string
|
||||
dir string
|
||||
repoCfg config.RepositoryConfiguration
|
||||
bq *blockQueue
|
||||
model *Model
|
||||
oustandingPerNode activityMap
|
||||
@@ -72,13 +74,13 @@ type puller struct {
|
||||
requestSlots chan bool
|
||||
blocks chan bqBlock
|
||||
requestResults chan requestResult
|
||||
versioner versioner.Versioner
|
||||
}
|
||||
|
||||
func newPuller(repo, dir string, model *Model, slots int, cfg *config.Configuration) *puller {
|
||||
func newPuller(repoCfg config.RepositoryConfiguration, model *Model, slots int, cfg *config.Configuration) *puller {
|
||||
p := &puller{
|
||||
repoCfg: repoCfg,
|
||||
cfg: cfg,
|
||||
repo: repo,
|
||||
dir: dir,
|
||||
bq: newBlockQueue(),
|
||||
model: model,
|
||||
oustandingPerNode: make(activityMap),
|
||||
@@ -88,19 +90,27 @@ func newPuller(repo, dir string, model *Model, slots int, cfg *config.Configurat
|
||||
requestResults: make(chan requestResult),
|
||||
}
|
||||
|
||||
if len(repoCfg.Versioning.Type) > 0 {
|
||||
factory, ok := versioner.Factories[repoCfg.Versioning.Type]
|
||||
if !ok {
|
||||
l.Fatalf("Requested versioning type %q that does not exist", repoCfg.Versioning.Type)
|
||||
}
|
||||
p.versioner = factory(repoCfg.Versioning.Params)
|
||||
}
|
||||
|
||||
if slots > 0 {
|
||||
// Read/write
|
||||
for i := 0; i < slots; i++ {
|
||||
p.requestSlots <- true
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("starting puller; repo %q dir %q slots %d", repo, dir, slots)
|
||||
l.Debugf("starting puller; repo %q dir %q slots %d", repoCfg.ID, repoCfg.Directory, slots)
|
||||
}
|
||||
go p.run()
|
||||
} else {
|
||||
// Read only
|
||||
if debug {
|
||||
l.Debugf("starting puller; repo %q dir %q (read only)", repo, dir)
|
||||
l.Debugf("starting puller; repo %q dir %q (read only)", repoCfg.ID, repoCfg.Directory)
|
||||
}
|
||||
go p.runRO()
|
||||
}
|
||||
@@ -114,7 +124,7 @@ func (p *puller) run() {
|
||||
<-p.requestSlots
|
||||
b := p.bq.get()
|
||||
if debug {
|
||||
l.Debugf("filler: queueing %q / %q offset %d copy %d", p.repo, b.file.Name, b.block.Offset, len(b.copy))
|
||||
l.Debugf("filler: queueing %q / %q offset %d copy %d", p.repoCfg.ID, b.file.Name, b.block.Offset, len(b.copy))
|
||||
}
|
||||
p.blocks <- b
|
||||
}
|
||||
@@ -130,13 +140,13 @@ func (p *puller) run() {
|
||||
for {
|
||||
select {
|
||||
case res := <-p.requestResults:
|
||||
p.model.setState(p.repo, RepoSyncing)
|
||||
p.model.setState(p.repoCfg.ID, RepoSyncing)
|
||||
changed = true
|
||||
p.requestSlots <- true
|
||||
p.handleRequestResult(res)
|
||||
|
||||
case b := <-p.blocks:
|
||||
p.model.setState(p.repo, RepoSyncing)
|
||||
p.model.setState(p.repoCfg.ID, RepoSyncing)
|
||||
changed = true
|
||||
if p.handleBlock(b) {
|
||||
// Block was fully handled, free up the slot
|
||||
@@ -149,7 +159,7 @@ func (p *puller) run() {
|
||||
break pull
|
||||
}
|
||||
if debug {
|
||||
l.Debugf("%q: idle but have %d open files", p.repo, len(p.openFiles))
|
||||
l.Debugf("%q: idle but have %d open files", p.repoCfg.ID, len(p.openFiles))
|
||||
i := 5
|
||||
for _, f := range p.openFiles {
|
||||
l.Debugf(" %v", f)
|
||||
@@ -163,22 +173,22 @@ func (p *puller) run() {
|
||||
}
|
||||
|
||||
if changed {
|
||||
p.model.setState(p.repo, RepoCleaning)
|
||||
p.model.setState(p.repoCfg.ID, RepoCleaning)
|
||||
p.fixupDirectories()
|
||||
changed = false
|
||||
}
|
||||
|
||||
p.model.setState(p.repo, RepoIdle)
|
||||
p.model.setState(p.repoCfg.ID, RepoIdle)
|
||||
|
||||
// Do a rescan if it's time for it
|
||||
select {
|
||||
case <-walkTicker:
|
||||
if debug {
|
||||
l.Debugf("%q: time for rescan", p.repo)
|
||||
l.Debugf("%q: time for rescan", p.repoCfg.ID)
|
||||
}
|
||||
err := p.model.ScanRepo(p.repo)
|
||||
err := p.model.ScanRepo(p.repoCfg.ID)
|
||||
if err != nil {
|
||||
invalidateRepo(p.cfg, p.repo, err)
|
||||
invalidateRepo(p.cfg, p.repoCfg.ID, err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -195,11 +205,11 @@ func (p *puller) runRO() {
|
||||
|
||||
for _ = range walkTicker {
|
||||
if debug {
|
||||
l.Debugf("%q: time for rescan", p.repo)
|
||||
l.Debugf("%q: time for rescan", p.repoCfg.ID)
|
||||
}
|
||||
err := p.model.ScanRepo(p.repo)
|
||||
err := p.model.ScanRepo(p.repoCfg.ID)
|
||||
if err != nil {
|
||||
invalidateRepo(p.cfg, p.repo, err)
|
||||
invalidateRepo(p.cfg, p.repoCfg.ID, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
@@ -214,7 +224,7 @@ func (p *puller) fixupDirectories() {
|
||||
return nil
|
||||
}
|
||||
|
||||
rn, err := filepath.Rel(p.dir, path)
|
||||
rn, err := filepath.Rel(p.repoCfg.Directory, path)
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
@@ -223,7 +233,11 @@ func (p *puller) fixupDirectories() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cur := p.model.CurrentRepoFile(p.repo, rn)
|
||||
if filepath.Base(rn) == ".stversions" {
|
||||
return nil
|
||||
}
|
||||
|
||||
cur := p.model.CurrentRepoFile(p.repoCfg.ID, rn)
|
||||
if cur.Name != rn {
|
||||
// No matching dir in current list; weird
|
||||
if debug {
|
||||
@@ -232,7 +246,7 @@ func (p *puller) fixupDirectories() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if cur.Flags&protocol.FlagDeleted != 0 {
|
||||
if protocol.IsDeleted(cur.Flags) {
|
||||
if debug {
|
||||
l.Debugf("queue delete dir: %v", cur)
|
||||
}
|
||||
@@ -245,10 +259,10 @@ func (p *puller) fixupDirectories() {
|
||||
return nil
|
||||
}
|
||||
|
||||
if cur.Flags&uint32(os.ModePerm) != uint32(info.Mode()&os.ModePerm) {
|
||||
if !p.repoCfg.IgnorePerms && protocol.HasPermissionBits(cur.Flags) && !scanner.PermsEqual(cur.Flags, uint32(info.Mode())) {
|
||||
err := os.Chmod(path, os.FileMode(cur.Flags)&os.ModePerm)
|
||||
if err != nil {
|
||||
l.Warnln("Restoring folder flags: %q: %v", path, err)
|
||||
l.Warnf("Restoring folder flags: %q: %v", path, err)
|
||||
} else {
|
||||
changed++
|
||||
if debug {
|
||||
@@ -261,7 +275,10 @@ func (p *puller) fixupDirectories() {
|
||||
t := time.Unix(cur.Modified, 0)
|
||||
err := os.Chtimes(path, t, t)
|
||||
if err != nil {
|
||||
l.Warnln("Restoring folder modtime: %q: %v", path, err)
|
||||
if runtime.GOOS != "windows" {
|
||||
// https://code.google.com/p/go/issues/detail?id=8090
|
||||
l.Warnf("Restoring folder modtime: %q: %v", path, err)
|
||||
}
|
||||
} else {
|
||||
changed++
|
||||
if debug {
|
||||
@@ -276,7 +293,7 @@ func (p *puller) fixupDirectories() {
|
||||
for {
|
||||
deleteDirs = nil
|
||||
changed = 0
|
||||
filepath.Walk(p.dir, walkFn)
|
||||
filepath.Walk(p.repoCfg.Directory, walkFn)
|
||||
|
||||
var deleted = 0
|
||||
// Delete any queued directories
|
||||
@@ -286,10 +303,10 @@ func (p *puller) fixupDirectories() {
|
||||
l.Debugln("delete dir:", dir)
|
||||
}
|
||||
err := os.Remove(dir)
|
||||
if err != nil {
|
||||
l.Warnln(err)
|
||||
} else {
|
||||
if err == nil {
|
||||
deleted++
|
||||
} else if p.versioner == nil { // Failures are expected in the presence of versioning
|
||||
l.Warnln(err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -320,7 +337,7 @@ func (p *puller) handleRequestResult(res requestResult) {
|
||||
p.openFiles[f.Name] = of
|
||||
|
||||
if debug {
|
||||
l.Debugf("pull: wrote %q / %q offset %d outstanding %d done %v", p.repo, f.Name, res.offset, of.outstanding, of.done)
|
||||
l.Debugf("pull: wrote %q / %q offset %d outstanding %d done %v", p.repoCfg.ID, f.Name, res.offset, of.outstanding, of.done)
|
||||
}
|
||||
|
||||
if of.done && of.outstanding == 0 {
|
||||
@@ -336,9 +353,9 @@ func (p *puller) handleBlock(b bqBlock) bool {
|
||||
|
||||
// For directories, making sure they exist is enough.
|
||||
// Deleted directories we mark as handled and delete later.
|
||||
if f.Flags&protocol.FlagDirectory != 0 {
|
||||
if f.Flags&protocol.FlagDeleted == 0 {
|
||||
path := filepath.Join(p.dir, f.Name)
|
||||
if protocol.IsDirectory(f.Flags) {
|
||||
if !protocol.IsDeleted(f.Flags) {
|
||||
path := filepath.Join(p.repoCfg.Directory, f.Name)
|
||||
_, err := os.Stat(path)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
if debug {
|
||||
@@ -352,7 +369,30 @@ func (p *puller) handleBlock(b bqBlock) bool {
|
||||
} else if debug {
|
||||
l.Debugf("ignore delete dir: %v", f)
|
||||
}
|
||||
p.model.updateLocal(p.repo, f)
|
||||
p.model.updateLocal(p.repoCfg.ID, f)
|
||||
return true
|
||||
}
|
||||
|
||||
if len(b.copy) > 0 && len(b.copy) == len(b.file.Blocks) && b.last {
|
||||
// We are supposed to copy the entire file, and then fetch nothing.
|
||||
// We don't actually need to make the copy.
|
||||
if debug {
|
||||
l.Debugln("taking shortcut:", f)
|
||||
}
|
||||
fp := filepath.Join(p.repoCfg.Directory, f.Name)
|
||||
t := time.Unix(f.Modified, 0)
|
||||
err := os.Chtimes(fp, t, t)
|
||||
if debug && err != nil {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
if !p.repoCfg.IgnorePerms && protocol.HasPermissionBits(f.Flags) {
|
||||
err = os.Chmod(fp, os.FileMode(f.Flags&0777))
|
||||
if debug && err != nil {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
p.model.updateLocal(p.repoCfg.ID, f)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -361,12 +401,12 @@ func (p *puller) handleBlock(b bqBlock) bool {
|
||||
|
||||
if !ok {
|
||||
if debug {
|
||||
l.Debugf("pull: %q: opening file %q", p.repo, f.Name)
|
||||
l.Debugf("pull: %q: opening file %q", p.repoCfg.ID, f.Name)
|
||||
}
|
||||
|
||||
of.availability = uint64(p.model.repoFiles[p.repo].Availability(f.Name))
|
||||
of.filepath = filepath.Join(p.dir, f.Name)
|
||||
of.temp = filepath.Join(p.dir, defTempNamer.TempName(f.Name))
|
||||
of.availability = uint64(p.model.repoFiles[p.repoCfg.ID].Availability(f.Name))
|
||||
of.filepath = filepath.Join(p.repoCfg.Directory, f.Name)
|
||||
of.temp = filepath.Join(p.repoCfg.Directory, defTempNamer.TempName(f.Name))
|
||||
|
||||
dirName := filepath.Dir(of.filepath)
|
||||
_, err := os.Stat(dirName)
|
||||
@@ -374,26 +414,26 @@ func (p *puller) handleBlock(b bqBlock) bool {
|
||||
err = os.MkdirAll(dirName, 0777)
|
||||
}
|
||||
if err != nil {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repo, f.Name, err)
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
|
||||
of.file, of.err = os.Create(of.temp)
|
||||
if of.err != nil {
|
||||
if debug {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repo, f.Name, of.err)
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
|
||||
}
|
||||
if !b.last {
|
||||
p.openFiles[f.Name] = of
|
||||
}
|
||||
return true
|
||||
}
|
||||
defTempNamer.Hide(of.temp)
|
||||
osutil.HideFile(of.temp)
|
||||
}
|
||||
|
||||
if of.err != nil {
|
||||
// We have already failed this file.
|
||||
if debug {
|
||||
l.Debugf("pull: error: %q / %q has already failed: %v", p.repo, f.Name, of.err)
|
||||
l.Debugf("pull: error: %q / %q has already failed: %v", p.repoCfg.ID, f.Name, of.err)
|
||||
}
|
||||
if b.last {
|
||||
delete(p.openFiles, f.Name)
|
||||
@@ -424,14 +464,14 @@ func (p *puller) handleCopyBlock(b bqBlock) {
|
||||
of := p.openFiles[f.Name]
|
||||
|
||||
if debug {
|
||||
l.Debugf("pull: copying %d blocks for %q / %q", len(b.copy), p.repo, f.Name)
|
||||
l.Debugf("pull: copying %d blocks for %q / %q", len(b.copy), p.repoCfg.ID, f.Name)
|
||||
}
|
||||
|
||||
var exfd *os.File
|
||||
exfd, of.err = os.Open(of.filepath)
|
||||
if of.err != nil {
|
||||
if debug {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repo, f.Name, of.err)
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
|
||||
}
|
||||
of.file.Close()
|
||||
of.file = nil
|
||||
@@ -450,7 +490,7 @@ func (p *puller) handleCopyBlock(b bqBlock) {
|
||||
buffers.Put(bs)
|
||||
if of.err != nil {
|
||||
if debug {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repo, f.Name, of.err)
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, of.err)
|
||||
}
|
||||
exfd.Close()
|
||||
of.file.Close()
|
||||
@@ -493,10 +533,10 @@ func (p *puller) handleRequestBlock(b bqBlock) bool {
|
||||
|
||||
go func(node string, b bqBlock) {
|
||||
if debug {
|
||||
l.Debugf("pull: requesting %q / %q offset %d size %d from %q outstanding %d", p.repo, f.Name, b.block.Offset, b.block.Size, node, of.outstanding)
|
||||
l.Debugf("pull: requesting %q / %q offset %d size %d from %q outstanding %d", p.repoCfg.ID, f.Name, b.block.Offset, b.block.Size, node, of.outstanding)
|
||||
}
|
||||
|
||||
bs, err := p.model.requestGlobal(node, p.repo, f.Name, b.block.Offset, int(b.block.Size), nil)
|
||||
bs, err := p.model.requestGlobal(node, p.repoCfg.ID, f.Name, b.block.Offset, int(b.block.Size), nil)
|
||||
p.requestResults <- requestResult{
|
||||
node: node,
|
||||
file: f,
|
||||
@@ -520,31 +560,35 @@ func (p *puller) handleEmptyBlock(b bqBlock) {
|
||||
}
|
||||
}
|
||||
|
||||
if f.Flags&protocol.FlagDeleted != 0 {
|
||||
if protocol.IsDeleted(f.Flags) {
|
||||
if debug {
|
||||
l.Debugf("pull: delete %q", f.Name)
|
||||
}
|
||||
os.Remove(of.temp)
|
||||
os.Chmod(of.filepath, 0666)
|
||||
if err := os.Remove(of.filepath); err == nil || os.IsNotExist(err) {
|
||||
p.model.updateLocal(p.repo, f)
|
||||
if p.versioner != nil {
|
||||
if err := p.versioner.Archive(of.filepath); err == nil {
|
||||
p.model.updateLocal(p.repoCfg.ID, f)
|
||||
}
|
||||
} else if err := os.Remove(of.filepath); err == nil || os.IsNotExist(err) {
|
||||
p.model.updateLocal(p.repoCfg.ID, f)
|
||||
}
|
||||
} else {
|
||||
if debug {
|
||||
l.Debugf("pull: no blocks to fetch and nothing to copy for %q / %q", p.repo, f.Name)
|
||||
l.Debugf("pull: no blocks to fetch and nothing to copy for %q / %q", p.repoCfg.ID, f.Name)
|
||||
}
|
||||
t := time.Unix(f.Modified, 0)
|
||||
if os.Chtimes(of.temp, t, t) != nil {
|
||||
delete(p.openFiles, f.Name)
|
||||
return
|
||||
}
|
||||
if os.Chmod(of.temp, os.FileMode(f.Flags&0777)) != nil {
|
||||
if !p.repoCfg.IgnorePerms && protocol.HasPermissionBits(f.Flags) && os.Chmod(of.temp, os.FileMode(f.Flags&0777)) != nil {
|
||||
delete(p.openFiles, f.Name)
|
||||
return
|
||||
}
|
||||
defTempNamer.Show(of.temp)
|
||||
if Rename(of.temp, of.filepath) == nil {
|
||||
p.model.updateLocal(p.repo, f)
|
||||
osutil.ShowFile(of.temp)
|
||||
if osutil.Rename(of.temp, of.filepath) == nil {
|
||||
p.model.updateLocal(p.repoCfg.ID, f)
|
||||
}
|
||||
}
|
||||
delete(p.openFiles, f.Name)
|
||||
@@ -552,8 +596,8 @@ func (p *puller) handleEmptyBlock(b bqBlock) {
|
||||
|
||||
func (p *puller) queueNeededBlocks() {
|
||||
queued := 0
|
||||
for _, f := range p.model.NeedFilesRepo(p.repo) {
|
||||
lf := p.model.CurrentRepoFile(p.repo, f.Name)
|
||||
for _, f := range p.model.NeedFilesRepo(p.repoCfg.ID) {
|
||||
lf := p.model.CurrentRepoFile(p.repoCfg.ID, f.Name)
|
||||
have, need := scanner.BlockDiff(lf.Blocks, f.Blocks)
|
||||
if debug {
|
||||
l.Debugf("need:\n local: %v\n global: %v\n haveBlocks: %v\n needBlocks: %v", lf, f, have, need)
|
||||
@@ -566,13 +610,13 @@ func (p *puller) queueNeededBlocks() {
|
||||
})
|
||||
}
|
||||
if debug && queued > 0 {
|
||||
l.Debugf("%q: queued %d blocks", p.repo, queued)
|
||||
l.Debugf("%q: queued %d blocks", p.repoCfg.ID, queued)
|
||||
}
|
||||
}
|
||||
|
||||
func (p *puller) closeFile(f scanner.File) {
|
||||
if debug {
|
||||
l.Debugf("pull: closing %q / %q", p.repo, f.Name)
|
||||
l.Debugf("pull: closing %q / %q", p.repoCfg.ID, f.Name)
|
||||
}
|
||||
|
||||
of := p.openFiles[f.Name]
|
||||
@@ -584,7 +628,7 @@ func (p *puller) closeFile(f scanner.File) {
|
||||
fd, err := os.Open(of.temp)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repo, f.Name, err)
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -593,29 +637,49 @@ func (p *puller) closeFile(f scanner.File) {
|
||||
|
||||
if l0, l1 := len(hb), len(f.Blocks); l0 != l1 {
|
||||
if debug {
|
||||
l.Debugf("pull: %q / %q: nblocks %d != %d", p.repo, f.Name, l0, l1)
|
||||
l.Debugf("pull: %q / %q: nblocks %d != %d", p.repoCfg.ID, f.Name, l0, l1)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
for i := range hb {
|
||||
if bytes.Compare(hb[i].Hash, f.Blocks[i].Hash) != 0 {
|
||||
l.Debugf("pull: %q / %q: block %d hash mismatch", p.repo, f.Name, i)
|
||||
l.Debugf("pull: %q / %q: block %d hash mismatch", p.repoCfg.ID, f.Name, i)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t := time.Unix(f.Modified, 0)
|
||||
os.Chtimes(of.temp, t, t)
|
||||
os.Chmod(of.temp, os.FileMode(f.Flags&0777))
|
||||
defTempNamer.Show(of.temp)
|
||||
if debug {
|
||||
l.Debugf("pull: rename %q / %q: %q", p.repo, f.Name, of.filepath)
|
||||
err = os.Chtimes(of.temp, t, t)
|
||||
if debug && err != nil {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
if err := Rename(of.temp, of.filepath); err == nil {
|
||||
p.model.updateLocal(p.repo, f)
|
||||
if !p.repoCfg.IgnorePerms && protocol.HasPermissionBits(f.Flags) {
|
||||
err = os.Chmod(of.temp, os.FileMode(f.Flags&0777))
|
||||
if debug && err != nil {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
osutil.ShowFile(of.temp)
|
||||
|
||||
if p.versioner != nil {
|
||||
err := p.versioner.Archive(of.filepath)
|
||||
if err != nil {
|
||||
if debug {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("pull: rename %q / %q: %q", p.repoCfg.ID, f.Name, of.filepath)
|
||||
}
|
||||
if err := osutil.Rename(of.temp, of.filepath); err == nil {
|
||||
p.model.updateLocal(p.repoCfg.ID, f)
|
||||
} else {
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repo, f.Name, err)
|
||||
l.Debugf("pull: error: %q / %q: %v", p.repoCfg.ID, f.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,11 +23,3 @@ func (t tempNamer) TempName(name string) string {
|
||||
tname := fmt.Sprintf("%s.%s", t.prefix, filepath.Base(name))
|
||||
return filepath.Join(tdir, tname)
|
||||
}
|
||||
|
||||
func (t tempNamer) Hide(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (t tempNamer) Show(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -6,7 +6,6 @@ import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"syscall"
|
||||
)
|
||||
|
||||
type tempNamer struct {
|
||||
@@ -24,33 +23,3 @@ func (t tempNamer) TempName(name string) string {
|
||||
tname := fmt.Sprintf("%s.%s.tmp", t.prefix, filepath.Base(name))
|
||||
return filepath.Join(tdir, tname)
|
||||
}
|
||||
|
||||
func (t tempNamer) Hide(path string) error {
|
||||
p, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs, err := syscall.GetFileAttributes(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs |= syscall.FILE_ATTRIBUTE_HIDDEN
|
||||
return syscall.SetFileAttributes(p, attrs)
|
||||
}
|
||||
|
||||
func (t tempNamer) Show(path string) error {
|
||||
p, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs, err := syscall.GetFileAttributes(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs &^= syscall.FILE_ATTRIBUTE_HIDDEN
|
||||
return syscall.SetFileAttributes(p, attrs)
|
||||
}
|
||||
|
||||
@@ -2,26 +2,12 @@ package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/calmh/syncthing/protocol"
|
||||
"github.com/calmh/syncthing/scanner"
|
||||
)
|
||||
|
||||
func Rename(from, to string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
os.Chmod(to, 0666) // Make sure the file is user writeable
|
||||
err := os.Remove(to)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
l.Warnln(err)
|
||||
}
|
||||
}
|
||||
defer os.Remove(from) // Don't leave a dangling temp file in case of rename error
|
||||
return os.Rename(from, to)
|
||||
}
|
||||
|
||||
func fileFromFileInfo(f protocol.FileInfo) scanner.File {
|
||||
var blocks = make([]scanner.Block, len(f.Blocks))
|
||||
var offset int64
|
||||
@@ -95,17 +81,8 @@ func compareClusterConfig(local, remote protocol.ClusterConfigMessage) error {
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return ClusterConfigMismatch(fmt.Errorf("remote is missing repository %q", repo))
|
||||
}
|
||||
}
|
||||
|
||||
for repo := range rm {
|
||||
if _, ok := lm[repo]; !ok {
|
||||
return ClusterConfigMismatch(fmt.Errorf("remote has extra repository %q", repo))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -20,24 +20,6 @@ var testcases = []struct {
|
||||
remote: protocol.ClusterConfigMessage{ClientName: "c", ClientVersion: "d"},
|
||||
err: "",
|
||||
},
|
||||
{
|
||||
local: protocol.ClusterConfigMessage{
|
||||
Repositories: []protocol.Repository{
|
||||
{ID: "foo"},
|
||||
},
|
||||
},
|
||||
remote: protocol.ClusterConfigMessage{ClientName: "c", ClientVersion: "d"},
|
||||
err: `remote is missing repository "foo"`,
|
||||
},
|
||||
{
|
||||
local: protocol.ClusterConfigMessage{ClientName: "c", ClientVersion: "d"},
|
||||
remote: protocol.ClusterConfigMessage{
|
||||
Repositories: []protocol.Repository{
|
||||
{ID: "foo"},
|
||||
},
|
||||
},
|
||||
err: `remote has extra repository "foo"`,
|
||||
},
|
||||
{
|
||||
local: protocol.ClusterConfigMessage{
|
||||
Repositories: []protocol.Repository{
|
||||
@@ -53,38 +35,6 @@ var testcases = []struct {
|
||||
},
|
||||
err: "",
|
||||
},
|
||||
{
|
||||
local: protocol.ClusterConfigMessage{
|
||||
Repositories: []protocol.Repository{
|
||||
{ID: "quux"},
|
||||
{ID: "foo"},
|
||||
{ID: "bar"},
|
||||
},
|
||||
},
|
||||
remote: protocol.ClusterConfigMessage{
|
||||
Repositories: []protocol.Repository{
|
||||
{ID: "bar"},
|
||||
{ID: "quux"},
|
||||
},
|
||||
},
|
||||
err: `remote is missing repository "foo"`,
|
||||
},
|
||||
{
|
||||
local: protocol.ClusterConfigMessage{
|
||||
Repositories: []protocol.Repository{
|
||||
{ID: "quux"},
|
||||
{ID: "bar"},
|
||||
},
|
||||
},
|
||||
remote: protocol.ClusterConfigMessage{
|
||||
Repositories: []protocol.Repository{
|
||||
{ID: "bar"},
|
||||
{ID: "foo"},
|
||||
{ID: "quux"},
|
||||
},
|
||||
},
|
||||
err: `remote has extra repository "foo"`,
|
||||
},
|
||||
{
|
||||
local: protocol.ClusterConfigMessage{
|
||||
Repositories: []protocol.Repository{
|
||||
|
||||
11
osutil/hidden_unix.go
Normal file
11
osutil/hidden_unix.go
Normal file
@@ -0,0 +1,11 @@
|
||||
// +build !windows
|
||||
|
||||
package osutil
|
||||
|
||||
func HideFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func ShowFile(path string) error {
|
||||
return nil
|
||||
}
|
||||
35
osutil/hidden_windows.go
Normal file
35
osutil/hidden_windows.go
Normal file
@@ -0,0 +1,35 @@
|
||||
// +build windows
|
||||
|
||||
package osutil
|
||||
|
||||
import "syscall"
|
||||
|
||||
func HideFile(path string) error {
|
||||
p, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs, err := syscall.GetFileAttributes(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs |= syscall.FILE_ATTRIBUTE_HIDDEN
|
||||
return syscall.SetFileAttributes(p, attrs)
|
||||
}
|
||||
|
||||
func ShowFile(path string) error {
|
||||
p, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs, err := syscall.GetFileAttributes(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
attrs &^= syscall.FILE_ATTRIBUTE_HIDDEN
|
||||
return syscall.SetFileAttributes(p, attrs)
|
||||
}
|
||||
18
osutil/osutil.go
Normal file
18
osutil/osutil.go
Normal file
@@ -0,0 +1,18 @@
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func Rename(from, to string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
os.Chmod(to, 0666) // Make sure the file is user writeable
|
||||
err := os.Remove(to)
|
||||
if err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
}
|
||||
defer os.Remove(from) // Don't leave a dangling temp file in case of rename error
|
||||
return os.Rename(from, to)
|
||||
}
|
||||
@@ -388,7 +388,7 @@ The Flags field is made up of the following single bit flags:
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Reserved |I|D| Unix Perm. & Mode |
|
||||
| Reserved |P|I|D| Unix Perm. & Mode |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
- The lower 12 bits hold the common Unix permission and mode bits. An
|
||||
@@ -404,7 +404,13 @@ The Flags field is made up of the following single bit flags:
|
||||
synchronization. A peer MAY set this bit to indicate that it can
|
||||
temporarily not serve data for the file.
|
||||
|
||||
- Bit 0 through 17 are reserved for future use and SHALL be set to
|
||||
- Bit 17 ("P") is set when there is no permission information for the
|
||||
file. This is the case when it originates on a non-permission-
|
||||
supporting file system. Changes to only permission bits SHOULD be
|
||||
disregarded on files with this bit set. The permissions bits MUST be
|
||||
set to the octal value 0666.
|
||||
|
||||
- Bit 0 through 16 are reserved for future use and SHALL be set to
|
||||
zero.
|
||||
|
||||
The hash algorithm is implied by the Hash length. Currently, the hash
|
||||
|
||||
@@ -10,9 +10,15 @@ type countingReader struct {
|
||||
tot uint64
|
||||
}
|
||||
|
||||
var (
|
||||
totalIncoming uint64
|
||||
totalOutgoing uint64
|
||||
)
|
||||
|
||||
func (c *countingReader) Read(bs []byte) (int, error) {
|
||||
n, err := c.Reader.Read(bs)
|
||||
atomic.AddUint64(&c.tot, uint64(n))
|
||||
atomic.AddUint64(&totalIncoming, uint64(n))
|
||||
return n, err
|
||||
}
|
||||
|
||||
@@ -28,9 +34,14 @@ type countingWriter struct {
|
||||
func (c *countingWriter) Write(bs []byte) (int, error) {
|
||||
n, err := c.Writer.Write(bs)
|
||||
atomic.AddUint64(&c.tot, uint64(n))
|
||||
atomic.AddUint64(&totalOutgoing, uint64(n))
|
||||
return n, err
|
||||
}
|
||||
|
||||
func (c *countingWriter) Tot() uint64 {
|
||||
return atomic.LoadUint64(&c.tot)
|
||||
}
|
||||
|
||||
func TotalInOut() (uint64, uint64) {
|
||||
return atomic.LoadUint64(&totalIncoming), atomic.LoadUint64(&totalOutgoing)
|
||||
}
|
||||
|
||||
13
protocol/debug.go
Normal file
13
protocol/debug.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/calmh/syncthing/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
debug = strings.Contains(os.Getenv("STTRACE"), "protocol") || os.Getenv("STTRACE") == "all"
|
||||
l = logger.DefaultLogger
|
||||
)
|
||||
@@ -25,9 +25,10 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
FlagDeleted uint32 = 1 << 12
|
||||
FlagInvalid = 1 << 13
|
||||
FlagDirectory = 1 << 14
|
||||
FlagDeleted uint32 = 1 << 12
|
||||
FlagInvalid = 1 << 13
|
||||
FlagDirectory = 1 << 14
|
||||
FlagNoPermBits = 1 << 15
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -91,8 +92,8 @@ type asyncResult struct {
|
||||
}
|
||||
|
||||
const (
|
||||
pingTimeout = 4 * time.Minute
|
||||
pingIdleTime = 5 * time.Minute
|
||||
pingTimeout = 300 * time.Second
|
||||
pingIdleTime = 600 * time.Second
|
||||
)
|
||||
|
||||
func NewConnection(nodeID string, reader io.Reader, writer io.Writer, receiver Model) Connection {
|
||||
@@ -475,11 +476,27 @@ func (c *rawConnection) pingerLoop() {
|
||||
for {
|
||||
select {
|
||||
case <-ticker:
|
||||
if d := time.Since(c.xr.LastRead()); d < pingIdleTime {
|
||||
if debug {
|
||||
l.Debugln(c.id, "ping skipped after rd", d)
|
||||
}
|
||||
continue
|
||||
}
|
||||
if d := time.Since(c.xw.LastWrite()); d < pingIdleTime {
|
||||
if debug {
|
||||
l.Debugln(c.id, "ping skipped after wr", d)
|
||||
}
|
||||
continue
|
||||
}
|
||||
go func() {
|
||||
if debug {
|
||||
l.Debugln(c.id, "ping ->")
|
||||
}
|
||||
rc <- c.ping()
|
||||
}()
|
||||
select {
|
||||
case ok := <-rc:
|
||||
l.Debugln(c.id, "<- pong")
|
||||
if !ok {
|
||||
c.close(fmt.Errorf("ping failure"))
|
||||
}
|
||||
@@ -515,3 +532,19 @@ func (c *rawConnection) Statistics() Statistics {
|
||||
OutBytesTotal: int(c.cw.Tot()),
|
||||
}
|
||||
}
|
||||
|
||||
func IsDeleted(bits uint32) bool {
|
||||
return bits&FlagDeleted != 0
|
||||
}
|
||||
|
||||
func IsInvalid(bits uint32) bool {
|
||||
return bits&FlagInvalid != 0
|
||||
}
|
||||
|
||||
func IsDirectory(bits uint32) bool {
|
||||
return bits&FlagDirectory != 0
|
||||
}
|
||||
|
||||
func HasPermissionBits(bits uint32) bool {
|
||||
return bits&FlagNoPermBits == 0
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ type Walker struct {
|
||||
// Suppressed files will be returned with empty metadata and the Suppressed flag set.
|
||||
// Requires CurrentFiler to be set.
|
||||
Suppressor Suppressor
|
||||
// If IgnorePerms is true, changes to permission bits will not be
|
||||
// detected. Scanned files will get zero permission bits and the
|
||||
// NoPermissionBits flag set.
|
||||
IgnorePerms bool
|
||||
}
|
||||
|
||||
type TempNamer interface {
|
||||
@@ -140,15 +144,7 @@ func (w *Walker) walkAndHashFiles(res *[]File, ign map[string][]string) filepath
|
||||
return nil
|
||||
}
|
||||
|
||||
if _, sn := filepath.Split(rn); sn == w.IgnoreFile {
|
||||
// An ignore-file; these are ignored themselves
|
||||
if debug {
|
||||
l.Debugln("ignorefile:", rn)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if w.ignoreFile(ign, rn) {
|
||||
if sn := filepath.Base(rn); sn == w.IgnoreFile || sn == ".stversions" || w.ignoreFile(ign, rn) {
|
||||
// An ignored file
|
||||
if debug {
|
||||
l.Debugln("ignored:", rn)
|
||||
@@ -162,16 +158,23 @@ func (w *Walker) walkAndHashFiles(res *[]File, ign map[string][]string) filepath
|
||||
if info.Mode().IsDir() {
|
||||
if w.CurrentFiler != nil {
|
||||
cf := w.CurrentFiler.CurrentFile(rn)
|
||||
if cf.Modified == info.ModTime().Unix() && cf.Flags&protocol.FlagDirectory != 0 && permsEqual(cf.Flags, uint32(info.Mode())) {
|
||||
permUnchanged := w.IgnorePerms || !protocol.HasPermissionBits(cf.Flags) || PermsEqual(cf.Flags, uint32(info.Mode()))
|
||||
if cf.Modified == info.ModTime().Unix() && protocol.IsDirectory(cf.Flags) && permUnchanged {
|
||||
if debug {
|
||||
l.Debugln("unchanged:", cf)
|
||||
}
|
||||
*res = append(*res, cf)
|
||||
} else {
|
||||
var flags uint32 = protocol.FlagDirectory
|
||||
if w.IgnorePerms {
|
||||
flags |= protocol.FlagNoPermBits | 0777
|
||||
} else {
|
||||
flags |= uint32(info.Mode() & os.ModePerm)
|
||||
}
|
||||
f := File{
|
||||
Name: rn,
|
||||
Version: lamport.Default.Tick(0),
|
||||
Flags: uint32(info.Mode()&os.ModePerm) | protocol.FlagDirectory,
|
||||
Flags: flags,
|
||||
Modified: info.ModTime().Unix(),
|
||||
}
|
||||
if debug {
|
||||
@@ -186,7 +189,8 @@ func (w *Walker) walkAndHashFiles(res *[]File, ign map[string][]string) filepath
|
||||
if info.Mode().IsRegular() {
|
||||
if w.CurrentFiler != nil {
|
||||
cf := w.CurrentFiler.CurrentFile(rn)
|
||||
if cf.Flags&protocol.FlagDeleted == 0 && cf.Modified == info.ModTime().Unix() && 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 debug {
|
||||
l.Debugln("unchanged:", cf)
|
||||
}
|
||||
@@ -235,11 +239,16 @@ func (w *Walker) walkAndHashFiles(res *[]File, ign map[string][]string) filepath
|
||||
t1 := time.Now()
|
||||
l.Debugln("hashed:", rn, ";", len(blocks), "blocks;", info.Size(), "bytes;", int(float64(info.Size())/1024/t1.Sub(t0).Seconds()), "KB/s")
|
||||
}
|
||||
|
||||
var flags = uint32(info.Mode() & os.ModePerm)
|
||||
if w.IgnorePerms {
|
||||
flags = protocol.FlagNoPermBits | 0666
|
||||
}
|
||||
f := File{
|
||||
Name: rn,
|
||||
Version: lamport.Default.Tick(0),
|
||||
Size: info.Size(),
|
||||
Flags: uint32(info.Mode()),
|
||||
Flags: flags,
|
||||
Modified: info.ModTime().Unix(),
|
||||
Blocks: blocks,
|
||||
}
|
||||
@@ -275,15 +284,17 @@ func (w *Walker) ignoreFile(patterns map[string][]string, file string) bool {
|
||||
}
|
||||
|
||||
func checkDir(dir string) error {
|
||||
if info, err := os.Stat(dir); err != nil {
|
||||
if info, err := os.Lstat(dir); err != nil {
|
||||
return err
|
||||
} else if !info.IsDir() {
|
||||
return errors.New(dir + ": not a directory")
|
||||
} else if debug {
|
||||
l.Debugln("checkDir", dir, info)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func permsEqual(a, b uint32) bool {
|
||||
func PermsEqual(a, b uint32) bool {
|
||||
switch runtime.GOOS {
|
||||
case "windows":
|
||||
// There is only writeable and read only, represented for user, group
|
||||
|
||||
@@ -60,14 +60,14 @@ func Discover() (*IGD, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
search := []byte(`
|
||||
M-SEARCH * HTTP/1.1
|
||||
searchStr := `M-SEARCH * HTTP/1.1
|
||||
Host: 239.255.255.250:1900
|
||||
St: urn:schemas-upnp-org:device:InternetGatewayDevice:1
|
||||
Man: "ssdp:discover"
|
||||
Mx: 3
|
||||
|
||||
`)
|
||||
`
|
||||
search := []byte(strings.Replace(searchStr, "\n", "\r\n", -1))
|
||||
|
||||
_, err = socket.WriteTo(search, ssdp)
|
||||
if err != nil {
|
||||
|
||||
13
versioner/debug.go
Normal file
13
versioner/debug.go
Normal file
@@ -0,0 +1,13 @@
|
||||
package versioner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/calmh/syncthing/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
debug = strings.Contains(os.Getenv("STTRACE"), "versioner") || os.Getenv("STTRACE") == "all"
|
||||
l = logger.DefaultLogger
|
||||
)
|
||||
84
versioner/simple.go
Normal file
84
versioner/simple.go
Normal file
@@ -0,0 +1,84 @@
|
||||
package versioner
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sort"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/calmh/syncthing/osutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
// Register the constructor for this type of versioner with the name "simple"
|
||||
Factories["simple"] = NewSimple
|
||||
}
|
||||
|
||||
// The type holds our configuration
|
||||
type Simple struct {
|
||||
keep int
|
||||
}
|
||||
|
||||
// The constructor function takes a map of parameters and creates the type.
|
||||
func NewSimple(params map[string]string) Versioner {
|
||||
keep, err := strconv.Atoi(params["keep"])
|
||||
if err != nil {
|
||||
keep = 5 // A reasonable default
|
||||
}
|
||||
|
||||
s := Simple{
|
||||
keep: keep,
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugf("instantiated %#v", s)
|
||||
}
|
||||
return s
|
||||
}
|
||||
|
||||
// Move away the named file to a version archive. If this function returns
|
||||
// nil, the named file does not exist any more (has been archived).
|
||||
func (v Simple) Archive(path string) error {
|
||||
_, err := os.Stat(path)
|
||||
if err != nil && os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("archiving", path)
|
||||
}
|
||||
|
||||
file := filepath.Base(path)
|
||||
dir := filepath.Join(filepath.Dir(path), ".stversions")
|
||||
err = os.MkdirAll(dir, 0755)
|
||||
if err != nil && !os.IsExist(err) {
|
||||
return err
|
||||
} else {
|
||||
osutil.HideFile(dir)
|
||||
}
|
||||
|
||||
ver := file + "~" + time.Now().Format("20060102-150405")
|
||||
err = osutil.Rename(path, filepath.Join(dir, ver))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
versions, err := filepath.Glob(filepath.Join(dir, file+"~*"))
|
||||
if err != nil {
|
||||
l.Warnln(err)
|
||||
return nil
|
||||
}
|
||||
|
||||
if len(versions) > v.keep {
|
||||
sort.Strings(versions)
|
||||
for _, toRemove := range versions[:len(versions)-v.keep] {
|
||||
err = os.Remove(toRemove)
|
||||
if err != nil {
|
||||
l.Warnln(err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
7
versioner/versioner.go
Normal file
7
versioner/versioner.go
Normal file
@@ -0,0 +1,7 @@
|
||||
package versioner
|
||||
|
||||
type Versioner interface {
|
||||
Archive(path string) error
|
||||
}
|
||||
|
||||
var Factories = map[string]func(map[string]string) Versioner{}
|
||||
@@ -3,15 +3,17 @@ package xdr
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
var ErrElementSizeExceeded = errors.New("element size exceeded")
|
||||
|
||||
type Reader struct {
|
||||
r io.Reader
|
||||
tot int
|
||||
err error
|
||||
b [8]byte
|
||||
r io.Reader
|
||||
tot int
|
||||
err error
|
||||
b [8]byte
|
||||
last time.Time
|
||||
}
|
||||
|
||||
func NewReader(r io.Reader) *Reader {
|
||||
@@ -44,6 +46,7 @@ func (r *Reader) ReadBytesMaxInto(max int, dst []byte) []byte {
|
||||
if r.err != nil {
|
||||
return nil
|
||||
}
|
||||
r.last = time.Now()
|
||||
l := int(r.ReadUint32())
|
||||
if r.err != nil {
|
||||
return nil
|
||||
@@ -74,6 +77,7 @@ func (r *Reader) ReadUint16() uint16 {
|
||||
if r.err != nil {
|
||||
return 0
|
||||
}
|
||||
r.last = time.Now()
|
||||
_, r.err = io.ReadFull(r.r, r.b[:4])
|
||||
r.tot += 4
|
||||
v := uint16(r.b[1]) | uint16(r.b[0])<<8
|
||||
@@ -88,6 +92,7 @@ func (r *Reader) ReadUint32() uint32 {
|
||||
if r.err != nil {
|
||||
return 0
|
||||
}
|
||||
r.last = time.Now()
|
||||
n, r.err = io.ReadFull(r.r, r.b[:4])
|
||||
if n < 4 {
|
||||
return 0
|
||||
@@ -105,6 +110,7 @@ func (r *Reader) ReadUint64() uint64 {
|
||||
if r.err != nil {
|
||||
return 0
|
||||
}
|
||||
r.last = time.Now()
|
||||
n, r.err = io.ReadFull(r.r, r.b[:8])
|
||||
r.tot += n
|
||||
v := uint64(r.b[7]) | uint64(r.b[6])<<8 | uint64(r.b[5])<<16 | uint64(r.b[4])<<24 |
|
||||
@@ -122,3 +128,7 @@ func (r *Reader) Tot() int {
|
||||
func (r *Reader) Error() error {
|
||||
return r.err
|
||||
}
|
||||
|
||||
func (r *Reader) LastRead() time.Time {
|
||||
return r.last
|
||||
}
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
package xdr
|
||||
|
||||
import "io"
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
func pad(l int) int {
|
||||
d := l % 4
|
||||
@@ -13,10 +16,11 @@ func pad(l int) int {
|
||||
var padBytes = []byte{0, 0, 0}
|
||||
|
||||
type Writer struct {
|
||||
w io.Writer
|
||||
tot int
|
||||
err error
|
||||
b [8]byte
|
||||
w io.Writer
|
||||
tot int
|
||||
err error
|
||||
b [8]byte
|
||||
last time.Time
|
||||
}
|
||||
|
||||
func NewWriter(w io.Writer) *Writer {
|
||||
@@ -34,6 +38,7 @@ func (w *Writer) WriteBytes(bs []byte) (int, error) {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
w.last = time.Now()
|
||||
w.WriteUint32(uint32(len(bs)))
|
||||
if w.err != nil {
|
||||
return 0, w.err
|
||||
@@ -65,6 +70,7 @@ func (w *Writer) WriteUint16(v uint16) (int, error) {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
w.last = time.Now()
|
||||
if debug {
|
||||
dl.Debugf("wr uint16=%d", v)
|
||||
}
|
||||
@@ -85,6 +91,7 @@ func (w *Writer) WriteUint32(v uint32) (int, error) {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
w.last = time.Now()
|
||||
if debug {
|
||||
dl.Debugf("wr uint32=%d", v)
|
||||
}
|
||||
@@ -105,6 +112,7 @@ func (w *Writer) WriteUint64(v uint64) (int, error) {
|
||||
return 0, w.err
|
||||
}
|
||||
|
||||
w.last = time.Now()
|
||||
if debug {
|
||||
dl.Debugf("wr uint64=%d", v)
|
||||
}
|
||||
@@ -131,3 +139,7 @@ func (w *Writer) Tot() int {
|
||||
func (w *Writer) Error() error {
|
||||
return w.err
|
||||
}
|
||||
|
||||
func (w *Writer) LastWrite() time.Time {
|
||||
return w.last
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user