Compare commits

..

76 Commits

Author SHA1 Message Date
Jakob Borg
9d348319fd Translation update 2014-10-18 20:50:40 +02:00
Jakob Borg
c55fee69de Devices added by introducer should have dynamic address (fixes #866) 2014-10-18 20:40:31 +02:00
Jakob Borg
ce31cb072b Upgrade test configs to v6 2014-10-18 20:37:15 +02:00
Jakob Borg
6b91fc9c91 Merge pull request #876 from cqcallaw/upnp
UPnP API Additions to address outstanding parts of #432
2014-10-18 19:58:56 +02:00
Caleb Callaway
e34f77ba0e Enable portmapping for individual UPnP services 2014-10-18 10:20:57 -07:00
Jakob Borg
bc3b7401a1 Merge branch 'pr/875'
* pr/875:
  Make folder path selectable in FireFox
2014-10-18 13:15:44 +02:00
Caleb Callaway
85677eaf1a UPnP API for querying of services' external IP address 2014-10-17 20:37:00 -07:00
Caleb Callaway
75d5e74059 Refinements to UPnP documentation 2014-10-17 19:47:08 -07:00
Audrius Butkevicius
c4d15b3b95 Fix blockmap hash size 2014-10-18 00:39:36 +01:00
Audrius Butkevicius
aa168ec2d6 Populate block offsets even if the blocks are not diffed 2014-10-17 23:16:29 +01:00
bigbear2nd
4ae0efe887 Make folder path selectable in FireFox
Make the folder name and the folder path selectable in FireFox, as discussed here: https://pulse-forum.ind.ie/t/how-can-the-folder-path-be-changed/1153/6

Add the assets to the commit
Add me to the contributors
Add me to the contributors in the index.html
2014-10-18 01:39:57 +09:00
Jakob Borg
86a57d8b56 Hash mismatch in general doesn't merit a warning 2014-10-17 10:33:02 +02:00
Jakob Borg
9dda7485eb Merge branch 'pr/871'
* pr/871:
  Slight increase of contrast in identicons
  Implement identicon representation for devices.

Conflicts:
	internal/auto/gui.files.go
2014-10-17 09:29:06 +02:00
Jakob Borg
8b9670add9 Add cdata 2014-10-17 09:28:45 +02:00
Jakob Borg
978aebd79c Slight increase of contrast in identicons 2014-10-17 09:26:58 +02:00
Chris Joel
ac079f0f83 Implement identicon representation for devices.
The first fifteen characters of device IDs are now used to procedurally
generate psuedo-unique avatars for their respective devices. The avatars
are represented using SVG elements that replace the icons previously
shown next to device names in the GUI.
2014-10-16 12:28:43 -07:00
Jakob Borg
e82e912151 Dependencies 2014-10-16 14:58:11 +02:00
Jakob Borg
5488ae5b89 Don't log inscrutable 'recovered: leveldb: not found' to web GUI 2014-10-16 13:45:42 +02:00
Jakob Borg
15b875b116 Merge branch 'pr/830'
* pr/830:
  Delete files and directories after pulling
  Add fetcher tests
  Track total block counts, count copier blocks
  Fix tests
  Implement block fetcher (fixes #781, fixes #3)
  Populate BlockMap
  Implement BlockMap
2014-10-16 13:33:20 +02:00
Audrius Butkevicius
dedf835aa6 Delete files and directories after pulling 2014-10-16 12:26:28 +02:00
Audrius Butkevicius
e62b9c6009 Add fetcher tests 2014-10-16 12:26:28 +02:00
Audrius Butkevicius
53da778506 Track total block counts, count copier blocks
Will eventually allow us to track progress per file
2014-10-16 12:26:28 +02:00
Audrius Butkevicius
4360b2c815 Fix tests 2014-10-16 12:26:28 +02:00
Audrius Butkevicius
1e15b1e0be Implement block fetcher (fixes #781, fixes #3) 2014-10-16 12:26:28 +02:00
Audrius Butkevicius
0bc50f7284 Populate BlockMap 2014-10-16 12:26:27 +02:00
Audrius Butkevicius
435f9113e8 Implement BlockMap 2014-10-16 12:26:27 +02:00
Jakob Borg
8818c4785b Fix debug and fmt poopoo 2014-10-16 12:23:33 +02:00
Jakob Borg
2e003e5404 Remove out of date upnp tests 2014-10-16 12:12:59 +02:00
Jakob Borg
e791a8ea07 Handle .stfolder completely in integration test 2014-10-16 12:12:59 +02:00
Jakob Borg
2fb8eb755b Add a few more debug prints 2014-10-16 12:12:54 +02:00
Jakob Borg
d031f958a9 FileInfoTruncated.String() for stindex' benefit 2014-10-16 09:26:24 +02:00
Jakob Borg
9bbadac9dc Asset rebuild 2014-10-16 09:11:23 +02:00
Jakob Borg
b012f77475 Merge pull request #848 from pluby/discovery
Simpler entry of locally discovered nodes
2014-10-16 09:11:08 +02:00
Jakob Borg
3cf36b1773 Add pluby 2014-10-16 09:09:41 +02:00
Jakob Borg
8f93c046a9 Merge pull request #775 from cqcallaw/master
UPnP cleanup and fixes for #432
2014-10-16 08:55:53 +02:00
Jakob Borg
90af68901a Add cqcallaw 2014-10-16 08:55:27 +02:00
Caleb Callaway
c17507b216 Cleanup UPnP API
This commit addresses most of the issues identified in #432:

* Support UPnP IGDs with both WANIPConnection and WANPPPConnection services

  IGDs that offer both WANIPConnection and WANPPPConnection services should
  now have port forwarding correctly configured for all services.

* Support multiple UPnP WANDevice and WANConnection descriptions

  Per Figure 1 of the InternetGatewayDevice specification
  (http://upnp.org/specs/gw/UPnP-gw-InternetGatewayDevice-v1-Device.pdf),
  an IGD may have multiple WAN devices, each with multiple WANConnection
  services

* Support for discovery of UPnP InternetGatewayDevice version 2 devices

* Support for discovery of multiple UPnP IGDs

  Consumers that cannot yet properly process multiple IGDs can simply use
  the first IGD listed in the discovery results

* Logging refinements such as friendly UPnP IGD identifiers in log messages.
2014-10-15 21:48:11 -07:00
Phill Luby
b110b7c3f7 Make cacheEntry public so that it can be marshalled to the UI. 2014-10-15 21:52:06 +01:00
Phill Luby
36431b3dcd Provide a data-list of locally discovered nodes when adding a new node. 2014-10-15 21:20:38 +01:00
Phill Luby
609294deee Set content type on discovery rest request. 2014-10-15 21:20:38 +01:00
Phill Luby
fffae9a741 Repackage discovery registry so that it can be converted to JSON.
The registry uses a non-string type as keys which is not possible in JSON.
2014-10-15 21:20:38 +01:00
Jakob Borg
598ce4bb5f Don't add newline on version string (fixes #865) 2014-10-15 18:15:40 +02:00
Jakob Borg
212f6dc9e0 Merge pull request #861 from AudriusButkevicius/int
Use relative path in integration tests
2014-10-15 18:07:51 +02:00
Jakob Borg
cd05f1c3d7 Merge pull request #860 from AudriusButkevicius/ticker
Cleanup temporaries once an hour (fixes #858)
2014-10-15 18:07:15 +02:00
Jakob Borg
d7a0691c99 Merge pull request #859 from AudriusButkevicius/markerfix
Best attempt when creating a folder marker (fixes #857)
2014-10-15 18:06:40 +02:00
Audrius Butkevicius
86346aa332 Add further nil checks (fixes #862, ref #864) 2014-10-15 15:54:55 +01:00
Audrius Butkevicius
b162b1fa34 Use relative path in integration tests 2014-10-15 14:05:25 +01:00
Audrius Butkevicius
ea9f8b0ceb Cleanup temporaries once an hour (fixes #858) 2014-10-15 10:30:10 +01:00
Audrius Butkevicius
6210b9e746 Best attempt when creating a folder marker (fixes #857) 2014-10-15 10:20:40 +01:00
Jakob Borg
a778b410b9 Only do initial scan if scanInterval==0 (fixes #856) 2014-10-15 10:51:09 +02:00
Jakob Borg
8a674c8bc3 Integration tests should take .stfolder into account when comparing dirs 2014-10-15 09:57:34 +02:00
Audrius Butkevicius
aaf625c624 Merge pull request #854 from AudriusButkevicius/nils
Revert and replace 31d95ac, 65acc7c, 87780a5
2014-10-15 08:24:19 +01:00
Jakob Borg
d4079a3273 Merge pull request #853 from AudriusButkevicius/unfinished
Keep temporaries for reuse, cleanup before pull (fixes #849, fixes #841)
2014-10-15 09:01:14 +02:00
Jakob Borg
8d94fe3346 Merge remote-tracking branch 'origin/pr/852'
* origin/pr/852:
  `-generate` flag should also create config.xml (closes #847).
2014-10-15 08:45:11 +02:00
Jakob Borg
ce510e55ae Add Nutomic 2014-10-15 08:44:58 +02:00
Audrius Butkevicius
5419ff9a71 Keep temporaries for reuse, cleanup before pull (fixes #849, fixes #841) 2014-10-14 22:00:40 +01:00
Audrius Butkevicius
ade437d625 Revert and replace 31d95ac, 65acc7c, 87780a5 2014-10-14 21:35:30 +01:00
Audrius Butkevicius
87780a5b7e Fix a missed nil (fixes #846) 2014-10-14 20:17:42 +01:00
Felix Ableitner
f6f6f261ed -generate flag should also create config.xml (closes #847). 2014-10-14 22:11:05 +03:00
Audrius Butkevicius
65acc7c9ad Merge pull request #850 from AudriusButkevicius/nil
Do not return nil pointers when loading ignores (fixes #846)
2014-10-14 19:16:20 +01:00
Audrius Butkevicius
31d95ac9e6 Do not return nil pointers when loading ignores (fixes #846)
Not sure, perhaps we should check for error, and respect that instead.
But then in the walker we'll have to check for a nil pointer anyway.
2014-10-14 16:28:43 +01:00
Jakob Borg
964d17d05a Merge pull request #842 from AudriusButkevicius/ignorecache
Cache ignore file matches
2014-10-14 12:43:21 +02:00
Audrius Butkevicius
665c5992f0 Cache ignore file matches 2014-10-14 10:30:37 +01:00
Jakob Borg
5f52e0581d Add linientMtimes workaround for Android brokenness (ref #831) 2014-10-14 08:48:43 +02:00
Jakob Borg
870e3a45ef Merge pull request #833 from AudriusButkevicius/marker
Add folder marker (fixes #762)
2014-10-14 08:23:48 +02:00
Audrius Butkevicius
a5fe4a3694 Perform tilde expansion in the config wrapper 2014-10-13 21:59:42 +01:00
Audrius Butkevicius
838670ccbc Add folder marker (fixes #762) 2014-10-13 21:54:42 +01:00
Jakob Borg
baf4cc225e Build without git 2014-10-13 20:13:42 +02:00
Jakob Borg
93ac1605bd Set version on command line when building 2014-10-13 20:10:36 +02:00
Jakob Borg
c8a68001c1 Use HTTP server read timeout (fixes #805, fixes #806) 2014-10-13 19:34:26 +02:00
Jakob Borg
244a22755c Merge branch 'pr-840'
* pr-840:
  More descriptive error if config couldn't be loaded
  Better handling of wrong config files
2014-10-13 16:02:14 +02:00
Jakob Borg
79c3ea82c7 More descriptive error if config couldn't be loaded 2014-10-13 16:01:57 +02:00
Jakob Borg
4b92960975 Add mvdan 2014-10-13 16:00:01 +02:00
Daniel Martí
ef616ff25b Better handling of wrong config files 2014-10-13 15:41:50 +02:00
Jakob Borg
7fb1a470ce Temporary workaround for panics in GUI/Usage reporting (ref #811) 2014-10-13 14:45:40 +02:00
Jakob Borg
fc6b2d9193 Ignore matcher benchmark 2014-10-12 14:54:36 +02:00
59 changed files with 2713 additions and 595 deletions

View File

@@ -5,6 +5,11 @@ Audrius Butkevicius <audrius.butkevicius@gmail.com>
Arthur Axel fREW Schmidt <frew@afoolishmanifesto.com> <frioux@gmail.com>
Ben Sidhom <bsidhom@gmail.com>
Brandon Philips <brandon@ifup.org>
Caleb Callaway <enlightened.despot@gmail.com>
Chris Joel <chris@scriptolo.gy>
Daniel Martí <mvdan@mvdan.cc>
Felix Ableitner <me@nutomic.com>
Felix Unterpaintner <bigbear2nd@gmail.com>
Gilli Sigurdsson <gilli@vx.is>
James Patterson <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
Jens Diemer <github.com@jensdiemer.de> <git@jensdiemer.de>
@@ -13,6 +18,7 @@ Lode Hoste <zillode@zillode.be>
Marcin Dziadus <dziadus.marcin@gmail.com>
Michael Tilli <pyfisch@gmail.com>
Philippe Schommers <philippe@schommers.be>
Phill Luby <phill.luby@newredo.com>
Ryan Sullivan <kayoticsully@gmail.com>
Tully Robinson <tully@tojr.org>
Veeti Paananen <veeti.paananen@rojekti.fi>

4
Godeps/Godeps.json generated
View File

@@ -30,6 +30,10 @@
"Comment": "null-15",
"Rev": "12e4b4183793ac4b061921e7980845e750679fd0"
},
{
"ImportPath": "github.com/AudriusButkevicius/lfu-go",
"Rev": "164bcecceb92fd6037f4d18a8d97b495ec6ef669"
},
{
"ImportPath": "github.com/bkaradzic/go-lz4",
"Rev": "93a831dcee242be64a9cc9803dda84af25932de7"

View File

@@ -0,0 +1,19 @@
Copyright (C) 2012 Dave Grijalva
Permission is hereby granted, free of charge, to any person obtaining a
copy of this software and associated documentation files (the "Software"),
to deal in the Software without restriction, including without limitation
the rights to use, copy, modify, merge, publish, distribute, sublicense,
and/or sell copies of the Software, and to permit persons to whom the
Software is furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS
OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL
THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.

View File

@@ -0,0 +1,19 @@
A simple LFU cache for golang. Based on the paper [An O(1) algorithm for implementing the LFU cache eviction scheme](http://dhruvbird.com/lfu.pdf).
Usage:
```go
import "github.com/dgrijalva/lfu-go"
// Make a new thing
c := lfu.New()
// Set some values
c.Set("myKey", myValue)
// Retrieve some values
myValue = c.Get("myKey")
// Evict some values
c.Evict(1)
```

View File

@@ -0,0 +1,156 @@
package lfu
import (
"container/list"
"sync"
)
type Eviction struct {
Key string
Value interface{}
}
type Cache struct {
// If len > UpperBound, cache will automatically evict
// down to LowerBound. If either value is 0, this behavior
// is disabled.
UpperBound int
LowerBound int
values map[string]*cacheEntry
freqs *list.List
len int
lock *sync.Mutex
EvictionChannel chan<- Eviction
}
type cacheEntry struct {
key string
value interface{}
freqNode *list.Element
}
type listEntry struct {
entries map[*cacheEntry]byte
freq int
}
func New() *Cache {
c := new(Cache)
c.values = make(map[string]*cacheEntry)
c.freqs = list.New()
c.lock = new(sync.Mutex)
return c
}
func (c *Cache) Get(key string) interface{} {
c.lock.Lock()
defer c.lock.Unlock()
if e, ok := c.values[key]; ok {
c.increment(e)
return e.value
}
return nil
}
func (c *Cache) Set(key string, value interface{}) {
c.lock.Lock()
defer c.lock.Unlock()
if e, ok := c.values[key]; ok {
// value already exists for key. overwrite
e.value = value
c.increment(e)
} else {
// value doesn't exist. insert
e := new(cacheEntry)
e.key = key
e.value = value
c.values[key] = e
c.increment(e)
c.len++
// bounds mgmt
if c.UpperBound > 0 && c.LowerBound > 0 {
if c.len > c.UpperBound {
c.evict(c.len - c.LowerBound)
}
}
}
}
func (c *Cache) Len() int {
c.lock.Lock()
defer c.lock.Unlock()
return c.len
}
func (c *Cache) Evict(count int) int {
c.lock.Lock()
defer c.lock.Unlock()
return c.evict(count)
}
func (c *Cache) evict(count int) int {
// No lock here so it can be called
// from within the lock (during Set)
var evicted int
for i := 0; i < count; {
if place := c.freqs.Front(); place != nil {
for entry, _ := range place.Value.(*listEntry).entries {
if i < count {
if c.EvictionChannel != nil {
c.EvictionChannel <- Eviction{
Key: entry.key,
Value: entry.value,
}
}
delete(c.values, entry.key)
c.remEntry(place, entry)
evicted++
c.len--
i++
}
}
}
}
return evicted
}
func (c *Cache) increment(e *cacheEntry) {
currentPlace := e.freqNode
var nextFreq int
var nextPlace *list.Element
if currentPlace == nil {
// new entry
nextFreq = 1
nextPlace = c.freqs.Front()
} else {
// move up
nextFreq = currentPlace.Value.(*listEntry).freq + 1
nextPlace = currentPlace.Next()
}
if nextPlace == nil || nextPlace.Value.(*listEntry).freq != nextFreq {
// create a new list entry
li := new(listEntry)
li.freq = nextFreq
li.entries = make(map[*cacheEntry]byte)
if currentPlace != nil {
nextPlace = c.freqs.InsertAfter(li, currentPlace)
} else {
nextPlace = c.freqs.PushFront(li)
}
}
e.freqNode = nextPlace
nextPlace.Value.(*listEntry).entries[e] = 1
if currentPlace != nil {
// remove from current position
c.remEntry(currentPlace, e)
}
}
func (c *Cache) remEntry(place *list.Element, entry *cacheEntry) {
entries := place.Value.(*listEntry).entries
delete(entries, entry)
if len(entries) == 0 {
c.freqs.Remove(place)
}
}

View File

@@ -0,0 +1,68 @@
package lfu
import (
"fmt"
"testing"
)
func TestLFU(t *testing.T) {
c := New()
c.Set("a", "a")
if v := c.Get("a"); v != "a" {
t.Errorf("Value was not saved: %v != 'a'", v)
}
if l := c.Len(); l != 1 {
t.Errorf("Length was not updated: %v != 1", l)
}
c.Set("b", "b")
if v := c.Get("b"); v != "b" {
t.Errorf("Value was not saved: %v != 'b'", v)
}
if l := c.Len(); l != 2 {
t.Errorf("Length was not updated: %v != 2", l)
}
c.Get("a")
evicted := c.Evict(1)
if v := c.Get("a"); v != "a" {
t.Errorf("Value was improperly evicted: %v != 'a'", v)
}
if v := c.Get("b"); v != nil {
t.Errorf("Value was not evicted: %v", v)
}
if l := c.Len(); l != 1 {
t.Errorf("Length was not updated: %v != 1", l)
}
if evicted != 1 {
t.Errorf("Number of evicted items is wrong: %v != 1", evicted)
}
}
func TestBoundsMgmt(t *testing.T) {
c := New()
c.UpperBound = 10
c.LowerBound = 5
for i := 0; i < 100; i++ {
c.Set(fmt.Sprintf("%v", i), i)
}
if c.Len() > 10 {
t.Errorf("Bounds management failed to evict properly: %v", c.Len())
}
}
func TestEviction(t *testing.T) {
ch := make(chan Eviction, 1)
c := New()
c.EvictionChannel = ch
c.Set("a", "b")
c.Evict(1)
ev := <-ch
if ev.Key != "a" || ev.Value.(string) != "b" {
t.Error("Incorrect item")
}
}

View File

@@ -35,6 +35,7 @@ import (
"runtime"
"strconv"
"strings"
"time"
)
var (
@@ -42,6 +43,7 @@ var (
goarch string
goos string
noupgrade bool
version string
)
const minGoVersion = 1.3
@@ -64,6 +66,7 @@ func main() {
flag.StringVar(&goarch, "goarch", runtime.GOARCH, "GOARCH")
flag.StringVar(&goos, "goos", runtime.GOOS, "GOOS")
flag.BoolVar(&noupgrade, "no-upgrade", false, "Disable upgrade functionality")
flag.StringVar(&version, "version", getVersion(), "Set compiled in version string")
flag.Parse()
switch goarch {
@@ -280,7 +283,7 @@ func clean() {
func ldflags() string {
var b bytes.Buffer
b.WriteString("-w")
b.WriteString(fmt.Sprintf(" -X main.Version %s", version()))
b.WriteString(fmt.Sprintf(" -X main.Version %s", version))
b.WriteString(fmt.Sprintf(" -X main.BuildStamp %d", buildStamp()))
b.WriteString(fmt.Sprintf(" -X main.BuildUser %s", buildUser()))
b.WriteString(fmt.Sprintf(" -X main.BuildHost %s", buildHost()))
@@ -298,8 +301,11 @@ func rmr(paths ...string) {
}
}
func version() string {
v := run("git", "describe", "--always", "--dirty")
func getVersion() string {
v, err := runError("git", "describe", "--always", "--dirty")
if err != nil {
return "unknown-dev"
}
v = versionRe.ReplaceAllFunc(v, func(s []byte) []byte {
s[0] = '+'
return s
@@ -308,7 +314,10 @@ func version() string {
}
func buildStamp() int64 {
bs := run("git", "show", "-s", "--format=%ct")
bs, err := runError("git", "show", "-s", "--format=%ct")
if err != nil {
return time.Now().Unix()
}
s, _ := strconv.ParseInt(string(bs), 10, 64)
return s
}
@@ -345,12 +354,11 @@ func buildArch() string {
}
func archiveName() string {
return fmt.Sprintf("syncthing-%s-%s", buildArch(), version())
return fmt.Sprintf("syncthing-%s-%s", buildArch(), version)
}
func run(cmd string, args ...string) []byte {
ecmd := exec.Command(cmd, args...)
bs, err := ecmd.CombinedOutput()
bs, err := runError(cmd, args...)
if err != nil {
log.Println(cmd, strings.Join(args, " "))
log.Println(string(bs))
@@ -359,6 +367,15 @@ func run(cmd string, args ...string) []byte {
return bytes.TrimSpace(bs)
}
func runError(cmd string, args ...string) ([]byte, error) {
ecmd := exec.Command(cmd, args...)
bs, err := ecmd.CombinedOutput()
if err != nil {
return nil, err
}
return bytes.TrimSpace(bs), nil
}
func runPrint(cmd string, args ...string) {
log.Println(cmd, strings.Join(args, " "))
ecmd := exec.Command(cmd, args...)

View File

@@ -34,6 +34,7 @@ import (
"code.google.com/p/go.crypto/bcrypt"
"github.com/syncthing/syncthing/internal/auto"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/discover"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/internal/logger"
"github.com/syncthing/syncthing/internal/model"
@@ -154,8 +155,13 @@ func startGUI(cfg config.GUIConfiguration, assetDir string, m *model.Model) erro
handler = redirectToHTTPSMiddleware(handler)
}
srv := http.Server{
Handler: handler,
ReadTimeout: 2 * time.Second,
}
go func() {
err := http.Serve(listener, handler)
err := srv.Serve(listener)
if err != nil {
panic(err)
}
@@ -446,7 +452,17 @@ func restPostDiscoveryHint(w http.ResponseWriter, r *http.Request) {
}
func restGetDiscovery(w http.ResponseWriter, r *http.Request) {
json.NewEncoder(w).Encode(discoverer.All())
w.Header().Set("Content-Type", "application/json; charset=utf-8")
registry := discoverer.All()
// Device ids can't be marshalled as keys so we need to manually
// rebuild this map using strings.
devices := make(map[string][]discover.CacheEntry, len(registry))
for device, _ := range registry {
devices[device.String()] = registry[device]
}
json.NewEncoder(w).Encode(devices)
}
func restGetReport(m *model.Model, w http.ResponseWriter, r *http.Request) {

View File

@@ -103,6 +103,7 @@ var (
stop = make(chan int)
discoverer *discover.Discoverer
externalPort int
igd *upnp.IGD
cert tls.Certificate
)
@@ -193,7 +194,7 @@ func main() {
if err != nil {
l.Fatalln("home:", err)
}
flag.StringVar(&generateDir, "generate", "", "Generate key in specified dir, then exit")
flag.StringVar(&generateDir, "generate", "", "Generate key and config in specified dir, then exit")
flag.StringVar(&guiAddress, "gui-address", guiAddress, "Override GUI address")
flag.StringVar(&guiAuthentication, "gui-authentication", guiAuthentication, "Override GUI authentication; username:password")
flag.StringVar(&guiAPIKey, "gui-apikey", guiAPIKey, "Override GUI API key")
@@ -239,17 +240,31 @@ func main() {
if err == nil {
l.Warnln("Key exists; will not overwrite.")
l.Infoln("Device ID:", protocol.NewDeviceID(cert.Certificate[0]))
return
} else {
newCertificate(dir, "")
cert, err = loadCert(dir, "")
myID = protocol.NewDeviceID(cert.Certificate[0])
if err != nil {
l.Fatalln("load cert:", err)
}
if err == nil {
l.Infoln("Device ID:", protocol.NewDeviceID(cert.Certificate[0]))
}
}
newCertificate(dir, "")
cert, err = loadCert(dir, "")
cfgFile := filepath.Join(dir, "config.xml")
if _, err := os.Stat(cfgFile); err == nil {
l.Warnln("Config exists; will not overwrite.")
return
}
var myName, _ = os.Hostname()
var newCfg = defaultConfig(myName)
var cfg = config.Wrap(cfgFile, newCfg)
err = cfg.Save()
if err != nil {
l.Fatalln("load cert:", err)
}
if err == nil {
l.Infoln("Device ID:", protocol.NewDeviceID(cert.Certificate[0]))
l.Warnln("Failed to save config", err)
}
return
}
@@ -346,13 +361,20 @@ func syncthingMain() {
// Load the configuration file, if it exists.
// If it does not, create a template.
cfg, err = config.Load(cfgFile, myID)
if err == nil {
myCfg := cfg.Devices()[myID]
if myCfg.Name == "" {
myName, _ = os.Hostname()
if info, err := os.Stat(cfgFile); err == nil {
if !info.Mode().IsRegular() {
l.Fatalln("Config file is not a file?")
}
cfg, err = config.Load(cfgFile, myID)
if err == nil {
myCfg := cfg.Devices()[myID]
if myCfg.Name == "" {
myName, _ = os.Hostname()
} else {
myName = myCfg.Name
}
} else {
myName = myCfg.Name
l.Fatalln("Configuration:", err)
}
} else {
l.Infoln("No config file; starting with empty defaults")
@@ -453,6 +475,7 @@ func syncthingMain() {
externalPort = addr.Port
// UPnP
igd = nil
if opts.UPnPEnabled {
setupUPnP()
@@ -568,17 +591,11 @@ func setupGUI(cfg *config.ConfigWrapper, m *model.Model) {
}
func sanityCheckFolders(cfg *config.ConfigWrapper, m *model.Model) {
var err error
nextFolder:
for id, folder := range cfg.Folders() {
if folder.Invalid != "" {
continue
}
folder.Path, err = osutil.ExpandTilde(folder.Path)
if err != nil {
l.Fatalln("home:", err)
}
m.AddFolder(folder)
fi, err := os.Stat(folder.Path)
@@ -591,11 +608,25 @@ nextFolder:
l.Warnf("Stopping folder %q - path does not exist, but has files in index", folder.ID)
cfg.InvalidateFolder(id, "folder path missing")
continue nextFolder
} else if !folder.HasMarker() {
l.Warnf("Stopping folder %q - path exists, but folder marker missing, check for mount issues", folder.ID)
cfg.InvalidateFolder(id, "folder marker missing")
continue nextFolder
}
} else if os.IsNotExist(err) {
// If we don't have any files in the index, and the directory
// doesn't exist, try creating it.
err = os.MkdirAll(folder.Path, 0700)
if err != nil {
l.Warnf("Stopping folder %q - %v", err)
cfg.InvalidateFolder(id, err.Error())
continue nextFolder
}
err = folder.CreateMarker()
} else if !folder.HasMarker() {
// If we don't have any files in the index, and the path does exist
// but the marker is not there, create it.
err = folder.CreateMarker()
}
if err != nil {
@@ -660,20 +691,20 @@ func setupUPnP() {
} else {
// Set up incoming port forwarding, if necessary and possible
port, _ := strconv.Atoi(portStr)
igd, err := upnp.Discover()
if err == nil {
igds := upnp.Discover()
if len(igds) > 0 {
// Configure the first discovered IGD only. This is a work-around until we have a better mechanism
// for handling multiple IGDs, which will require changes to the global discovery service
igd = igds[0]
externalPort = setupExternalPort(igd, port)
if externalPort == 0 {
l.Warnln("Failed to create UPnP port mapping")
} else {
l.Infoln("Created UPnP port mapping - external port", externalPort)
}
} else {
l.Infof("No UPnP gateway detected")
if debugNet {
l.Debugf("UPnP: %v", err)
l.Infof("Created UPnP port mapping for external port %d on UPnP device %s.", externalPort, igd.FriendlyIdentifier())
}
}
if opts.UPnPRenewal > 0 {
go renewUPnP(port)
}
@@ -684,7 +715,11 @@ func setupUPnP() {
}
func setupExternalPort(igd *upnp.IGD, port int) int {
// We seed the random number generator with the device ID to get a
if igd == nil {
return 0
}
// We seed the random number generator with the node ID to get a
// repeatable sequence of random external ports.
rnd := rand.NewSource(certSeed(cert.Certificate[0]))
for i := 0; i < 10; i++ {
@@ -702,32 +737,46 @@ func renewUPnP(port int) {
opts := cfg.Options()
time.Sleep(time.Duration(opts.UPnPRenewal) * time.Minute)
igd, err := upnp.Discover()
if err != nil {
continue
// Make sure our IGD reference isn't nil
if igd == nil {
l.Infoln("Undefined IGD during UPnP port renewal. Re-discovering...")
igds := upnp.Discover()
if len(igds) > 0 {
// Configure the first discovered IGD only. This is a work-around until we have a better mechanism
// for handling multiple IGDs, which will require changes to the global discovery service
igd = igds[0]
} else {
l.Infof("Failed to re-discover IGD during UPnP port mapping renewal.")
continue
}
}
// Just renew the same port that we already have
if externalPort != 0 {
err = igd.AddPortMapping(upnp.TCP, externalPort, port, "syncthing", opts.UPnPLease*60)
err := igd.AddPortMapping(upnp.TCP, externalPort, port, "syncthing", opts.UPnPLease*60)
if err == nil {
l.Infoln("Renewed UPnP port mapping - external port", externalPort)
continue
l.Infof("Renewed UPnP port mapping for external port %d on device %s.", externalPort, igd.FriendlyIdentifier())
} else {
l.Warnf("Error renewing UPnP port mapping for external port %d on device %s: %s", externalPort, igd.FriendlyIdentifier(), err.Error())
}
continue
}
// Something strange has happened. We didn't have an external port before?
// Or perhaps the gateway has changed?
// Retry the same port sequence from the beginning.
l.Infoln("No UPnP port mapping defined, updating...")
r := setupExternalPort(igd, port)
if r != 0 {
externalPort = r
l.Infoln("Updated UPnP port mapping - external port", externalPort)
l.Infof("Updated UPnP port mapping for external port %d on device %s.", externalPort, igd.FriendlyIdentifier())
discoverer.StopGlobal()
discoverer.StartGlobal(opts.GlobalAnnServer, uint16(r))
continue
} else {
l.Warnf("Failed to update UPnP port mapping for external port on device " + igd.FriendlyIdentifier() + ".")
}
l.Warnln("Failed to update UPnP port mapping - external port", externalPort)
}
}

View File

@@ -13,6 +13,107 @@
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package main_test
package main
// Empty test file to generate 0% coverage rather than no coverage
import (
"os"
"testing"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/files"
"github.com/syncthing/syncthing/internal/model"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/storage"
)
func TestSanityCheck(t *testing.T) {
fcfg := config.FolderConfiguration{
ID: "folder",
Path: "testdata/testfolder",
}
cfg := config.Wrap("/tmp/test", config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
})
for _, file := range []string{".stfolder", "testfolder", "testfolder/.stfolder"} {
_, err := os.Stat("testdata/" + file)
if err == nil {
t.Error("Found unexpected file")
}
}
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
// Case 1 - new folder, directory and marker created
m := model.NewModel(cfg, "device", "syncthing", "dev", db)
sanityCheckFolders(cfg, m)
if cfg.Folders()["folder"].Invalid != "" {
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
}
s, err := os.Stat("testdata/testfolder")
if err != nil || !s.IsDir() {
t.Error(err)
}
_, err = os.Stat("testdata/testfolder/.stfolder")
if err != nil {
t.Error(err)
}
os.Remove("testdata/testfolder/.stfolder")
os.Remove("testdata/testfolder/")
// Case 2 - new folder, marker created
fcfg.Path = "testdata/"
cfg = config.Wrap("/tmp/test", config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
})
m = model.NewModel(cfg, "device", "syncthing", "dev", db)
sanityCheckFolders(cfg, m)
if cfg.Folders()["folder"].Invalid != "" {
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
}
_, err = os.Stat("testdata/.stfolder")
if err != nil {
t.Error(err)
}
os.Remove("testdata/.stfolder")
// Case 3 - marker missing
set := files.NewSet("folder", db)
set.Update(protocol.LocalDeviceID, []protocol.FileInfo{
{Name: "dummyfile"},
})
m = model.NewModel(cfg, "device", "syncthing", "dev", db)
sanityCheckFolders(cfg, m)
if cfg.Folders()["folder"].Invalid != "folder marker missing" {
t.Error("Incorrect error")
}
// Case 4 - path missing
fcfg.Path = "testdata/testfolder"
cfg = config.Wrap("/tmp/test", config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
})
m = model.NewModel(cfg, "device", "syncthing", "dev", db)
sanityCheckFolders(cfg, m)
if cfg.Folders()["folder"].Invalid != "folder path missing" {
t.Error("Incorrect error")
}
}

View File

@@ -666,15 +666,21 @@ syncthing.controller('SyncthingCtrl', function ($scope, $http, $translate, $loca
};
$scope.addDevice = function () {
$scope.currentDevice = {
AddressesStr: 'dynamic',
Compression: true,
Introducer: false
};
$scope.editingExisting = false;
$scope.editingSelf = false;
$scope.deviceEditor.$setPristine();
$('#editDevice').modal();
$http.get(urlbase + '/discovery')
.success(function (registry) {
$scope.discovery = registry;
})
.then(function () {
$scope.currentDevice = {
AddressesStr: 'dynamic',
Compression: true,
Introducer: false
};
$scope.editingExisting = false;
$scope.editingSelf = false;
$scope.deviceEditor.$setPristine();
$('#editDevice').modal();
});
};
$scope.deleteDevice = function () {
@@ -1225,3 +1231,68 @@ syncthing.directive('modal', function () {
},
};
});
syncthing.directive('identicon', ['$window', function ($window) {
var svgNS = 'http://www.w3.org/2000/svg';
function Identicon (value, size) {
var svg = document.createElementNS(svgNS, 'svg');
var shouldFillRectAt = function (row, col) {
return !($window.parseInt(value.charCodeAt(row + col * size), 10) % 2);
};
var shouldMirrorRectAt = function (row, col) {
return !(size % 2 && col === middleCol)
};
var mirrorColFor = function (col) {
return size - col - 1;
};
var fillRectAt = function (row, col) {
var rect = document.createElementNS(svgNS, 'rect');
rect.setAttribute('x', (col * rectSize) + '%');
rect.setAttribute('y', (row * rectSize) + '%');
rect.setAttribute('width', rectSize + '%');
rect.setAttribute('height', rectSize + '%');
svg.appendChild(rect);
};
var rect;
var row;
var col;
var middleCol;
var rectSize;
svg.setAttribute('class', 'identicon');
size = size || 5;
rectSize = 100 / size;
middleCol = Math.ceil(size / 2) - 1;
if (value) {
value = value.toString().replace(/[\W_]/i, '');
for (row = 0; row < size; ++row) {
for (col = middleCol; col > -1; --col) {
if (shouldFillRectAt(row, col)) {
fillRectAt(row, col);
if (shouldMirrorRectAt(row, col)) {
fillRectAt(row, mirrorColFor(col));
}
}
}
}
}
return svg;
}
return {
restrict: 'E',
scope: {
value: '='
},
link: function (scope, element, attributes) {
element.append(new Identicon(scope.value));
}
}
}]);

View File

@@ -190,7 +190,7 @@
<div class="panel panel-default" ng-repeat="deviceCfg in [thisDevice()]">
<div class="panel-heading" data-toggle="collapse" href="#device-this" style="cursor: pointer">
<h3 class="panel-title">
<span class="glyphicon glyphicon-home"></span>&emsp;{{deviceName(deviceCfg)}}
<identicon data-value="deviceCfg.DeviceID"></identicon>&emsp;{{deviceName(deviceCfg)}}
</h3>
</div>
<div id="device-this" class="panel-collapse collapse in">
@@ -236,7 +236,7 @@
<div class="panel panel-{{deviceClass(deviceCfg)}}" ng-repeat="deviceCfg in otherDevices()">
<div class="panel-heading" data-toggle="collapse" data-parent="#devices" href="#device-{{$index}}" style="cursor: pointer">
<h3 class="panel-title">
<span class="glyphicon glyphicon-retweet"></span>&emsp;{{deviceName(deviceCfg)}}
<identicon data-value="deviceCfg.DeviceID"></identicon>&emsp;{{deviceName(deviceCfg)}}
<span class="pull-right hidden-xs">
<span ng-if="connections[deviceCfg.DeviceID] && completion[deviceCfg.DeviceID]._total == 100">
<span translate>Up to Date</span> (100%)
@@ -383,7 +383,10 @@
<form role="form" name="deviceEditor">
<div class="form-group" ng-class="{'has-error': deviceEditor.deviceID.$invalid && deviceEditor.deviceID.$dirty}">
<label translate for="deviceID">Device ID</label>
<input ng-if="!editingExisting" name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.DeviceID" required valid-deviceid></input>
<input ng-if="!editingExisting" name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.DeviceID" required valid-deviceid list="discovery-list" />
<datalist id="discovery-list" ng-if="!editingExisting">
<option ng-repeat="(id,address) in discovery" value="{{ id }}" />
</datalist>
<div ng-if="editingExisting" class="well well-sm text-monospace">{{currentDevice.DeviceID}}</div>
<p class="help-block">
<span translate ng-if="deviceEditor.deviceID.$valid || deviceEditor.deviceID.$pristine">The device ID to enter here can be found in the "Edit > Show ID" dialog on the other device. Spaces and dashes are optional (ignored).</span>
@@ -445,7 +448,7 @@
<div class="col-md-12">
<div class="form-group" ng-class="{'has-error': folderEditor.folderID.$invalid && folderEditor.folderID.$dirty}">
<label for="folderID"><span translate>Folder ID</span></label>
<input name="folderID" ng-disabled="editingExisting" id="folderID" class="form-control" type="text" ng-model="currentFolder.ID" required unique-folder ng-pattern="/^[a-zA-Z0-9-_.]{1,64}$/"></input>
<input name="folderID" ng-readonly="editingExisting" id="folderID" class="form-control" type="text" ng-model="currentFolder.ID" required unique-folder ng-pattern="/^[a-zA-Z0-9-_.]{1,64}$/"></input>
<p class="help-block">
<span translate ng-if="folderEditor.folderID.$valid || folderEditor.folderID.$pristine">Short identifier for the folder. Must be the same on all cluster devices.</span>
<span translate ng-if="folderEditor.folderID.$error.uniqueFolder">The folder ID must be unique.</span>
@@ -455,7 +458,7 @@
</div>
<div class="form-group" ng-class="{'has-error': folderEditor.folderPath.$invalid && folderEditor.folderPath.$dirty}">
<label translate for="folderPath">Folder Path</label>
<input name="folderPath" ng-disabled="editingExisting" id="folderPath" class="form-control" type="text" ng-model="currentFolder.Path" required></input>
<input name="folderPath" ng-readonly="editingExisting" id="folderPath" class="form-control" type="text" ng-model="currentFolder.Path" required></input>
<p class="help-block">
<span translate ng-if="folderEditor.folderPath.$valid || folderEditor.folderPath.$pristine">Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for</span> <code>{{system.tilde}}</code>.
<span translate ng-if="folderEditor.folderPath.$error.required && folderEditor.folderPath.$dirty">The folder path cannot be blank.</span>
@@ -781,18 +784,24 @@
<li>Audrius Butkevicius</li>
<li>Ben Sidhom</li>
<li>Brandon Philips</li>
<li>Gilli Sigurdsson</li>
<li>James Patterson</li>
<li>Caleb Callaway</li>
<li>Chris Joel</li>
<li>Daniel Martí</li>
<li>Felix Ableitner</li>
<li>Felix Unterpaintner</li>
</ul>
</div>
<div class="col-md-6">
<ul>
<li>Gilli Sigurdsson</li>
<li>James Patterson</li>
<li>Jens Diemer</li>
<li>Jochen Voss</li>
<li>Lode Hoste</li>
<li>Marcin Dziadus</li>
<li>Michael Tilli</li>
<li>Philippe Schommers</li>
<li>Phill Luby</li>
<li>Ryan Sullivan</li>
<li>Tully Robinson</li>
<li>Veeti Paananen</li>
@@ -815,7 +824,6 @@
</ul>
</modal>
<script src="angular/angular.min.js"></script>
<script src="angular/angular-translate.min.js"></script>
<script src="angular/angular-translate-loader.min.js"></script>

View File

@@ -38,7 +38,7 @@
"Folder Master": "Keine Veränderungen zulassen",
"Folder Path": "Verzeichnis Pfad",
"GUI Authentication Password": "Passwort für Zugang zur Benutzeroberfläche",
"GUI Authentication User": "Nutzername für Zugang zur Benutzeroberfläche.",
"GUI Authentication User": "Nutzername für Zugang zur Benutzeroberfläche",
"GUI Listen Addresses": "Adresse(n) für die Benutzeroberfläche",
"Generate": "Generieren",
"Global Discovery": "Globale Auffindung",

140
gui/lang/lang-hu.json Normal file
View File

@@ -0,0 +1,140 @@
{
"API Key": "API kulcs",
"About": "Névjegy",
"Add Device": "Eszköz hozzáadása",
"Add Folder": "Mappa hozzáadása",
"Address": "Cím",
"Addresses": "Címek",
"Allow Anonymous Usage Reporting?": "Engedélyezed a névtelen felhasználási adatok küldését?",
"Anonymous Usage Reporting": "Névtelen felhasználási adatok küldése",
"Any devices configured on an introducer device will be added to this device as well.": "Any devices configured on an introducer device will be added to this device as well.",
"Bugs": "Hibák",
"CPU Utilization": "Processzor használat",
"Close": "Bezárás",
"Comment, when used at the start of a line": "Megjegyzés, a sor elején használva",
"Compression is recommended in most setups.": "A tömörítés a a legtöbb esetben ajánlott",
"Connection Error": "Kapcsolódási hiba",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg és az alábbi Közreműködők",
"Delete": "Törlés",
"Device ID": "Eszköz azonosító",
"Device Identification": "Eszköz azonosító",
"Device Name": "Eszköz neve",
"Disconnected": "Kapcsolat bontva",
"Documentation": "Dokumentáció",
"Download Rate": "Letöltési sebesség",
"Edit": "Szerkesztés",
"Edit Device": "Eszköz szerkesztése",
"Edit Folder": "Mappa szerkesztése",
"Editing": "Szerkesztés",
"Enable UPnP": "UPnP engedélyezése",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Add meg kettősponttal elválasztva (ip:port) a címet vagy add meg a \"dynamic\" szót az a cím automatikus észleléséhez.",
"Enter ignore patterns, one per line.": "Figyelmen kívül hagyáshoz ide írhatod a mintákat, soronként egyet",
"Error": "Hiba",
"File Versioning": "Fájl verziózás",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "A fájl jogosultságok figyelmen kívül hagyása. FAT fájlrendszernél használatos.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "A fájlok időpecsételt verziói a .stversions mappában kerülnek áthelyezésre, amikor felülírásra vagy törlésre kerülnek a Sincthing által.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "A fájlok védve vannak a más eszközökön történt változásokkal szemben, de az ezen az eszközön történt változások érvényesek lesznek a többire.",
"Folder ID": "Mappa azonosító",
"Folder Master": "Központi mappa",
"Folder Path": "Mappa elérési útja",
"GUI Authentication Password": "Grafikus felület jelszava",
"GUI Authentication User": "Grafikus felület felhasználó neve ",
"GUI Listen Addresses": "Grafikus felület címe",
"Generate": "Generálás",
"Global Discovery": "Globális felfedezés",
"Global Discovery Server": "Globális felfedező szerver",
"Global State": "Globális állapot",
"Idle": "Tétlen",
"Ignore Patterns": "Figyelmen kívül hagyás",
"Ignore Permissions": "Jogosultságok figyelmen kívül hagyása",
"Incoming Rate Limit (KiB/s)": "Bejövő sebesség korlát (KIB/mp)",
"Introducer": "Introducer",
"Inversion of the given condition (i.e. do not exclude)": "A feltétel ellentéte (pl. ki nem hagyás)",
"Keep Versions": "Megtartott verziók",
"Last seen": "Utoljára látva",
"Latest Release": "Utolsó kiadás",
"Local Discovery": "Helyi felfedezés",
"Local State": "Helyi állapot",
"Maximum Age": "Maximális kor",
"Multi level wildcard (matches multiple directory levels)": "Több szintű helyettesítő karakter (több könyvtár szintre érvényesül)",
"Never": "Soha",
"No": "Nem",
"No File Versioning": "Nincs fájl verziózás",
"Notice": "Megjegyzés",
"OK": "Rendben",
"Offline": "Nincs kapcsolat",
"Online": "Kapcsolódva",
"Out Of Sync": "Nincs szinkronban",
"Outgoing Rate Limit (KiB/s)": "Kimenő sávszélesség (KiB/mp)",
"Override Changes": "Változtatások felülbírálása",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "A mappa elérési útja az eszközön. Amennyiben nem létezik, a program automatikusan létrehozza. A hullámvonal (~) a következő helyettesítésre használható: ",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Elérési út ahol a verziók tárolásra kerülnek (szabadon hagyva az alapértelmezett .stversions mappa lesz használva)",
"Please wait": "Kérlek várj",
"Preview": "Előnézet",
"Preview Usage Report": "Felhasználási adatok átnézése",
"Quick guide to supported patterns": "Rövid útmutató a használható mintákról",
"RAM Utilization": "Memória használat",
"Rescan": "Átnézés",
"Rescan Interval": "Átnézési intervallum",
"Restart": "Újraindítás",
"Restart Needed": "Újraindítás szükséges",
"Restarting": "Újraindulás",
"Save": "Mentés",
"Scanning": "Átnézés",
"Select the devices to share this folder with.": "Válaszd ki az eszközöket amelyekkel meg szeretnéd osztani a mappát",
"Settings": "Beállítások",
"Share With Devices": "Megosztás eszközökkel",
"Shared With": "Megosztva velük",
"Short identifier for the folder. Must be the same on all cluster devices.": "Rövid azonosító. Minden megosztott eszközön azonos kell legyen.",
"Show ID": "Azonosító mutatása",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Az eszköz azonosító helyett jelenik meg. A többi eszközön alapértelmezett névként használható. ",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Az eszköz azonosító helyett jelenik meg. Üresen hagyva az eszköz saját neve lesz használva ",
"Shutdown": "Leállítás",
"Simple File Versioning": "Egyszerű fájl verziózás",
"Single level wildcard (matches within a directory only)": "Egyszintű helyettesítő karakter (csak egy mappára érvényes)",
"Source Code": "Forráskód",
"Staggered File Versioning": "Többszintű fájl verziózás",
"Start Browser": "Böngésző indítása",
"Stopped": "Leállítva",
"Support / Forum": "Támogatás / Fórum",
"Sync Protocol Listen Addresses": "Szinkronizációs protokoll címe",
"Synchronization": "Szinkronizálás",
"Syncing": "Szinkronizálás",
"Syncthing has been shut down.": "Syncthing leállítva",
"Syncthing includes the following software or portions thereof:": "Syncthing a következő programokat, vagy komponenseket tartalmazza.",
"Syncthing is restarting.": "Syncthing újraindul",
"Syncthing is upgrading.": "Syncthing frissül",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "A Syncthing úgy tűnik, hogy nem működik, vagy valami probléma van az hálózati kapcsolattal. Újra próbálom...",
"The aggregated statistics are publicly available at {%url%}.": "Az összevont statisztikák nyilvánosan elérhetők a {{url}} címen.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A beállítások elmentésre kerültek, de nem lettek aktiválva. Indíts újra a Syncthing-et, hogy aktiváld őket.",
"The device ID cannot be blank.": "Az eszköz azonosító nem lehet üres.",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Az eszköz azonosító az \"Azonosító mutatása\" ablakban található az eszközökön. A szóközök és kötőjelek opcionálisak (nem lesznek figyelembe véve)",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "A titkosított felhasználási adatok naponta kerülnek küldésre. Arra használjuk őket hogy kövessük a különböző platformokat, mappa méreteket és program verziókat. Amennyiben az elküldésre kerülő adat megváltozik, ez az ablak újra megjelenik.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "A beírt eszköz azonosító nem valódi. Az azonosító 52 vagy 56 karakter hosszú, számokból és betűkből áll, opcionálisan szóközöket és kötőjeleket tartalmaz.",
"The folder ID cannot be blank.": "A mappa azonosító nem lehet üres.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "A mappa azonosító rövid (maximum 64 karakter), betűkből, számokból és a pont (.), kötőjel (-) és alulvonás (_) karakterekből állhat.",
"The folder ID must be unique.": "A mappa azonosító egyedi kell legyen",
"The folder path cannot be blank.": "Az elérési út nem lehet üres",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "A következő intervallumokat használjuk: egy régi verziót őrzünk meg az első órában minden 30 másodpercben, az első nap minden órában, az első 30 napban minden nap, egészen addig amíg el nem érjük a maximálisan megtartható verziók számát minden héten.",
"The maximum age must be a number and cannot be blank.": "A maximális kornak számnak kell lenni és nem lehet üres",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "A verziók megtartásának maximális ideje (napokban, ha 0-t adsz meg örökre megmaradnak).",
"The number of old versions to keep, per file.": "A megtartott régi verziók száma, fájlonként.",
"The number of versions must be a number and cannot be blank.": "A megtartott verziók száma nem lehet üres",
"The rescan interval must be at least 5 seconds.": "Az átnézési intervallumnak legalább 5 másodpercnek kell lennie.",
"Unknown": "Ismeretlen",
"Up to Date": "Friss",
"Upgrade To {%version%}": "Frissítés a {{version}} verzióra",
"Upgrading": "Frissítés",
"Upload Rate": "Feltöltési sebesség",
"Use Compression": "Tömörítés használata",
"Use HTTPS for GUI": "HTTPS használata a GUI-hoz",
"Version": "Verzió",
"Versions Path": "Verziók útvonala",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "A régi verziók automatikusan törlődnek, amennyiben öregebbek mint a maximum kor, vagy már több van belőlük mint az adott intervallumban megtartható maximum.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Amikor új eszközt adsz hozzá, tartsd észben hogy, ezt az eszközt is hozzá kell adni a másik oldalon.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Amikor új mappát adsz hozzá, tartsd észben hogy a mappa azonosító arra való hogy összekösd a mappákat az eszközeiden. Az azonosító kisbetű-nagybetű érzékeny és pontosan egyeznie kell az eszközökön.",
"Yes": "Igen",
"You must keep at least one version.": "Legalább egy verziót meg kell tartanod",
"full documentation": "teljes dokumentáció",
"items": "tételek"
}

View File

@@ -81,9 +81,9 @@
"Restarting": "Riavvio",
"Save": "Salva",
"Scanning": "Scansione in corso",
"Select the devices to share this folder with.": "Select the devices to share this folder with.",
"Select the devices to share this folder with.": "Seleziona i dispositivi con i quali condividere questa cartella.",
"Settings": "Impostazioni",
"Share With Devices": "Share With Devices",
"Share With Devices": "Condividi Con i Dispositivi",
"Shared With": "Condiviso Con",
"Short identifier for the folder. Must be the same on all cluster devices.": "Short identifier for the folder. Must be the same on all cluster devices.",
"Show ID": "Mostra ID",

140
gui/lang/lang-pl.json Normal file
View File

@@ -0,0 +1,140 @@
{
"API Key": "Klucz API",
"About": "O Syncthing",
"Add Device": "Dodaj urządzenie",
"Add Folder": "Dodaj folder",
"Address": "Adres",
"Addresses": "Adresy",
"Allow Anonymous Usage Reporting?": "Zezwalaj na anonimowe statystyki użycia",
"Anonymous Usage Reporting": "Anonimowe statystyki użycia",
"Any devices configured on an introducer device will be added to this device as well.": "Wszystkie urządzenia skonfigurowane na urządzeniu wprowadzającym zostaną dodane także do tego urządzenia.",
"Bugs": "Błędy",
"CPU Utilization": "Użycie CPU",
"Close": "Zamknij",
"Comment, when used at the start of a line": "Komentarz, jeżeli użyty na początku linii",
"Compression is recommended in most setups.": "Kompresja jest zalecana w większości przypadków",
"Connection Error": "Błąd połączenia",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg i następujący współautorzy:",
"Delete": "Usuń",
"Device ID": "ID urządzenia",
"Device Identification": "Identyfikator urządzenia",
"Device Name": "Nazwa urządzenia",
"Disconnected": "Rozłączony",
"Documentation": "Dokumentacja",
"Download Rate": "Prędkość pobierania",
"Edit": "Edytuj",
"Edit Device": "Edytuj urządzenie",
"Edit Folder": "Edytuj folder",
"Editing": "Edytowanie",
"Enable UPnP": "Włącz UPnP",
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "Wprowadź adresy oddzielone przecinkiem \"ip:port\", lub wpisz \"dynamic\" w celu wykrycia adresu. ",
"Enter ignore patterns, one per line.": "Wprowadz wzorce ignorowania, jeden w każdej linii.",
"Error": "Błąd",
"File Versioning": "Wersjonowanie plików",
"File permission bits are ignored when looking for changes. Use on FAT filesystems.": "Uprawnienia plików są ignorowane przy poszukiwaniu zmian. Używaj w systemie plików FAT",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.": "W momencie zmiany lub usuwania pliki są przenoszone do oznaczonych datą wersji wewnątrz folderu .stversions",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Pliki są zabezpieczone przed zmianami na innym urządzeniu, jednak zmiany w tym urządzeniu będą wysłane do reszty.",
"Folder ID": "ID folderu",
"Folder Master": "Główny folder",
"Folder Path": "Ścieżka folderu",
"GUI Authentication Password": "Hasło",
"GUI Authentication User": "Użytkownik",
"GUI Listen Addresses": "Adres nasłuchu",
"Generate": "Generuj",
"Global Discovery": "Globalne odnajdywanie",
"Global Discovery Server": "Globalny serwer rozgłoszeniowy",
"Global State": "Status globalny",
"Idle": "Bezczynny",
"Ignore Patterns": "Wzorce ignorowania",
"Ignore Permissions": "Ignoruj uprawnienia",
"Incoming Rate Limit (KiB/s)": "Ograniczenie prędkości odbierania (KiB/s)",
"Introducer": "Wprowadzający",
"Inversion of the given condition (i.e. do not exclude)": "Odwrócenie podanego wzorca (np. nie wykluczaj)",
"Keep Versions": "Zachowuj wersje",
"Last seen": "Ostatnio widziany",
"Latest Release": "Najnowsza wersja",
"Local Discovery": "Lokalne odnajdywanie",
"Local State": "Status lokalny",
"Maximum Age": "Maksymalny wiek",
"Multi level wildcard (matches multiple directory levels)": "Wieloznaczność na poziomie katalogów i plików (uwzględnia nazwy folderów i plików)",
"Never": "Nigdy",
"No": "Nie",
"No File Versioning": "Bez wersjonowania pliku",
"Notice": "Wskazówka",
"OK": "OK",
"Offline": "Rozłączony",
"Online": "Połączony",
"Out Of Sync": "Niezsynchronizowane",
"Outgoing Rate Limit (KiB/s)": "Ograniczenie prędkości wysyłania (KiB/s)",
"Override Changes": "Nadpisz zmiany",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Ścieżka do lokalnego folderu. Zostanie utworzona jeżeli nie istnieje.\nZnak tyldy (~) może zostać użyty jako skrót do",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Ścieżka gdzie będą przechowywane wersje (pozostaw puste dla domyślnego folderu .stversions)",
"Please wait": "Proszę czekać",
"Preview": "Podgląd",
"Preview Usage Report": "Podgląd raportu użycia.",
"Quick guide to supported patterns": "Krótki przewodnik po obsługiwanych wzorcach",
"RAM Utilization": "Użycie pamięci RAM",
"Rescan": "Skanuj ponownie",
"Rescan Interval": "Interwał skanowania",
"Restart": "Uruchom ponownie",
"Restart Needed": "Wymagane ponowne uruchomienie",
"Restarting": "Uruchamianie ponowne",
"Save": "Zapisz",
"Scanning": "Skanowanie",
"Select the devices to share this folder with.": "Wybierz urządzenie, któremu udostępnić folder.",
"Settings": "Ustrawienia",
"Share With Devices": "Udostępnij dla urządzenia",
"Shared With": "Współdzielony z",
"Short identifier for the folder. Must be the same on all cluster devices.": "Krótki identyfikator folderu. Musi być taki sam na wszystkich urządzeniach.",
"Show ID": "Pokaż ID",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Pokazane w statusie zamiast ID urządzenia.Zostanie wysłane do innych urządzeń jako opcjonalna domyślna nazwa.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Pokazane w statusie zamiast ID urządzenia. Zostanie zaktualizowane do nazwy urządzenia jeżeli pozostanie puste.",
"Shutdown": "Wyłącz",
"Simple File Versioning": "Proste wersjonowanie pliku",
"Single level wildcard (matches within a directory only)": "Wieloznaczność na poziomie plików (uwzględnia nazwy plików)",
"Source Code": "Kod źródłowy",
"Staggered File Versioning": "Rozbudowane wersjonowanie pliku",
"Start Browser": "Uruchom przeglądarkę",
"Stopped": "Zatrzymany",
"Support / Forum": "Wsparcie / Forum",
"Sync Protocol Listen Addresses": "Adres nasłuchu protokołu synchronizacji",
"Synchronization": "Synchronizacja",
"Syncing": "Synchronizowanie",
"Syncthing has been shut down.": "Syncthing został wyłączony",
"Syncthing includes the following software or portions thereof:": "Syncthing zawiera następujące oprogramowanie lub ich częśći:",
"Syncthing is restarting.": "Restart Syncthing",
"Syncthing is upgrading.": "Aktualizowanie Syncthing",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing wydaje się być wyłączony lub jest problem z twoim połączeniem internetowym. Próbuje ponownie...",
"The aggregated statistics are publicly available at {%url%}.": "Zebrane statystyki są publicznie dostępna pod adresem {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfiguracja została zapisana lecz nie jest aktywna. Syncthing musi zostać zrestartowany aby aktywować nową konfiguracje.",
"The device ID cannot be blank.": "ID urządzenia nie może być puste.",
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "ID urządzenia można znaleźć w \"Edytuj -> Pokaż ID\" na zdalnym urządzeniu.\nOdstępy i myślniki są opcjonalne (ignorowane)",
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Zaszyfrowane raporty użycia są wysyłane codziennie. Są one używane w celach statystycznych platform, rozmiarów katalogów i wersji programu. Jeżeli zgłaszane dane ulegną zmianie, ponownie wyświetli się ta informacja.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Wprowadzone ID urządzenia wygląda na niepoprawne. Musi zawierać 52 lub 56 znaków składających się z liter i cyfr. Odstępy i myślniki są opcjonalne.",
"The folder ID cannot be blank.": "ID folderu nie może być puste.",
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "ID folderu musi być krótkim identyfikatorem (64 lub mniej znaków) zawierać litery, cyfry, znaki kropki (.), myślnika (-) i podkreślenia (_).",
"The folder ID must be unique.": "ID folderu musi być unikalne.",
"The folder path cannot be blank.": "Ścieżka folderu nie może być pusta.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Następujący interwał jest używany: dla pierwszej godziny wersja jest zachowywana co 30 sekund, dla pierwszego dnia wersja jest zachowywana co godzinę, dla pierwszego miesiąca wersja jest zachowywana codziennie, aż do maksymalnego odstępu zapisywania wersji co tydzień.",
"The maximum age must be a number and cannot be blank.": "Maksymalny wiek musi być liczbą i nie może być pusty.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Maksymalny czas zachowania wersji (w dniach, ustaw 0 aby zachować na zawsze)",
"The number of old versions to keep, per file.": "Liczba wersji pliku do zachowania.",
"The number of versions must be a number and cannot be blank.": "Liczba wersji musi być liczbą i nie może być pusta.",
"The rescan interval must be at least 5 seconds.": "Interwał skanowania musi wynosić co najmniej 5 sekund.",
"Unknown": "Nieznany",
"Up to Date": "Aktualny",
"Upgrade To {%version%}": "Aktualizuj do {{version}}",
"Upgrading": "Aktualizowanie",
"Upload Rate": "Prędkość wysyłania",
"Use Compression": "Używaj kompresji",
"Use HTTPS for GUI": "Używaj HTTPS",
"Version": "Wersja",
"Versions Path": "Ścieżka wersji",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Wersje zostają automatycznie usunięte jeżeli są starsze niż maksymalny wiek lub przekraczają liczbę dopuszczalnych wersji.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Gdy dodajesz nowe urządzenie, pamiętaj że urządzenie musi zostać dodane także po drugiej stronie.",
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Przy dodawaniu nowego folderu, pamiętaj, że ID użyte jest do łączenia folderów pomiędzy urządzeniami. Wielkość liter ciągu ma znaczenie musi zgadzać się na wszystkich urządzeniach.",
"Yes": "Tak",
"You must keep at least one version.": "Musisz posiadać przynajmniej jedną wersję",
"full documentation": "pełna dokumentacja",
"items": "pozycji."
}

View File

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

View File

@@ -28,6 +28,43 @@ ul+h5 {
margin-top: 1.5em;
}
identicon {
display: inline-block;
position: relative;
width: 1em;
height: 1em;
line-height: 1em;
overflow: visible;
}
.identicon {
width: 30px;
height: 30px;
}
.identicon rect {
opacity: 0.85;
fill: #888;
}
.panel-heading .identicon {
display: block;
position: absolute;
top: -4px;
left: -9px;
}
.panel-heading {
position: relative;
overflow: hidden;
}
[class*="-info"] .identicon rect,
[class*="-success"] .identicon rect,
[class*="-primary"] .identicon rect {
fill: #fff;
}
.text-monospace {
font-family: Menlo, Monaco, Consolas, "Courier New", monospace;
}

View File

File diff suppressed because one or more lines are too long

View File

@@ -21,18 +21,20 @@ import (
"fmt"
"io"
"os"
"path/filepath"
"reflect"
"sort"
"strconv"
"code.google.com/p/go.crypto/bcrypt"
"github.com/syncthing/syncthing/internal/logger"
"github.com/syncthing/syncthing/internal/osutil"
"github.com/syncthing/syncthing/internal/protocol"
)
var l = logger.DefaultLogger
const CurrentVersion = 5
const CurrentVersion = 6
type Configuration struct {
Version int `xml:"version,attr"`
@@ -55,6 +57,7 @@ type FolderConfiguration struct {
RescanIntervalS int `xml:"rescanIntervalS,attr" default:"60"`
IgnorePerms bool `xml:"ignorePerms,attr"`
Versioning VersioningConfiguration `xml:"versioning"`
LenientMtimes bool `xml:"lenientMtimes"`
Invalid string `xml:"-"` // Set at runtime when there is an error, not saved
@@ -64,6 +67,28 @@ type FolderConfiguration struct {
Deprecated_Nodes []FolderDeviceConfiguration `xml:"node" json:"-"`
}
func (f *FolderConfiguration) CreateMarker() error {
if !f.HasMarker() {
marker := filepath.Join(f.Path, ".stfolder")
fd, err := os.Create(marker)
if err != nil {
return err
}
fd.Close()
osutil.HideFile(marker)
}
return nil
}
func (f *FolderConfiguration) HasMarker() bool {
_, err := os.Stat(filepath.Join(f.Path, ".stfolder"))
if err != nil {
return false
}
return true
}
func (r *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
if r.deviceIDs == nil {
for _, n := range r.Devices {
@@ -148,6 +173,7 @@ type OptionsConfiguration struct {
RestartOnWakeup bool `xml:"restartOnWakeup" default:"true"`
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" default:"12"` // 0 for off
KeepTemporariesH int `xml:"keepTemporariesH" default:"24"` // 0 for off
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" default:"true"`
Deprecated_RescanIntervalS int `xml:"rescanIntervalS,omitempty" json:"-"`
Deprecated_UREnabled bool `xml:"urEnabled,omitempty" json:"-"`
@@ -272,6 +298,11 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
convertV4V5(cfg)
}
// Upgrade to v6 configuration if appropriate
if cfg.Version == 5 {
convertV5V6(cfg)
}
// Hash old cleartext passwords
if len(cfg.GUI.Password) > 0 && cfg.GUI.Password[0] != '$' {
hash, err := bcrypt.GenerateFromPassword([]byte(cfg.GUI.Password), 0)
@@ -344,6 +375,19 @@ func ChangeRequiresRestart(from, to Configuration) bool {
return false
}
func convertV5V6(cfg *Configuration) {
// Added ".stfolder" file at folder roots to identify mount issues
// Doesn't affect the config itself, but uses config migrations to identify
// the migration point.
for _, folder := range Wrap("", *cfg).Folders() {
// Best attempt, if it fails, it fails, the user will have to fix
// it up manually, as the repo will not get started.
folder.CreateMarker()
}
cfg.Version = 6
}
func convertV4V5(cfg *Configuration) {
// Renamed a bunch of fields in the structs.
if cfg.Deprecated_Nodes == nil {

View File

@@ -16,6 +16,7 @@
package config
import (
"fmt"
"os"
"reflect"
"testing"
@@ -50,6 +51,7 @@ func TestDefaultValues(t *testing.T) {
RestartOnWakeup: true,
AutoUpgradeIntervalH: 12,
KeepTemporariesH: 24,
CacheIgnoredFiles: true,
}
cfg := New(device1)
@@ -60,17 +62,26 @@ func TestDefaultValues(t *testing.T) {
}
func TestDeviceConfig(t *testing.T) {
for i, ver := range []string{"v1", "v2", "v3", "v4", "v5"} {
wr, err := Load("testdata/"+ver+".xml", device1)
for i := 1; i <= CurrentVersion; i++ {
os.Remove("testdata/.stfolder")
wr, err := Load(fmt.Sprintf("testdata/v%d.xml", i), device1)
if err != nil {
t.Fatal(err)
}
_, err = os.Stat("testdata/.stfolder")
if i < 6 && err != nil {
t.Fatal(err)
} else if i >= 6 && err == nil {
t.Fatal("Unexpected file")
}
cfg := wr.cfg
expectedFolders := []FolderConfiguration{
{
ID: "test",
Path: "~/Sync",
Path: "testdata/",
Devices: []FolderDeviceConfiguration{{DeviceID: device1}, {DeviceID: device4}},
ReadOnly: true,
RescanIntervalS: 600,
@@ -92,8 +103,8 @@ func TestDeviceConfig(t *testing.T) {
}
expectedDeviceIDs := []protocol.DeviceID{device1, device4}
if cfg.Version != 5 {
t.Errorf("%d: Incorrect version %d != 5", i, cfg.Version)
if cfg.Version != CurrentVersion {
t.Errorf("%d: Incorrect version %d != %d", i, cfg.Version, CurrentVersion)
}
if !reflect.DeepEqual(cfg.Folders, expectedFolders) {
t.Errorf("%d: Incorrect Folders\n A: %#v\n E: %#v", i, cfg.Folders, expectedFolders)
@@ -138,6 +149,7 @@ func TestOverriddenValues(t *testing.T) {
RestartOnWakeup: false,
AutoUpgradeIntervalH: 24,
KeepTemporariesH: 48,
CacheIgnoredFiles: false,
}
cfg, err := Load("testdata/overridenvalues.xml", device1)
@@ -296,7 +308,7 @@ func TestPrepare(t *testing.T) {
}
func TestRequiresRestart(t *testing.T) {
wr, err := Load("testdata/v5.xml", device1)
wr, err := Load("testdata/v6.xml", device1)
if err != nil {
t.Fatal(err)
}

0
internal/config/testdata/.stfolder vendored Normal file
View File

View File

@@ -18,5 +18,6 @@
<restartOnWakeup>false</restartOnWakeup>
<autoUpgradeIntervalH>24</autoUpgradeIntervalH>
<keepTemporariesH>48</keepTemporariesH>
<cacheIgnoredFiles>false</cacheIgnoredFiles>
</options>
</configuration>

View File

@@ -1,5 +1,5 @@
<configuration version="1">
<repository id="test" directory="~/Sync">
<repository id="test" directory="testdata/">
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ" name="node one">
<address>a</address>
</node>

View File

@@ -1,5 +1,5 @@
<configuration version="2">
<repository id="test" directory="~/Sync" ro="true">
<repository id="test" directory="testdata/" ro="true">
<node id="P56IOI7MZJNU2IQGDREYDM2MGTMGL3BXNPQ6W5BTBBZ4TJXZWICQ"/>
<node id="AIR6LPZ7K4PTTUXQSMUUCPQ5YWOEDFIIQJUG7772YQXXR5YD6AWQ"/>
<node id="C4YBIESWDUAIGU62GOSRXCRAAJDWVE3TKCPMURZE2LH5QHAF576A"/>

View File

@@ -1,5 +1,5 @@
<configuration version="3">
<repository id="test" directory="~/Sync" ro="true" ignorePerms="false">
<repository id="test" directory="testdata/" ro="true" ignorePerms="false">
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" compression="false"></node>
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" compression="false"></node>
</repository>

View File

@@ -1,5 +1,5 @@
<configuration version="4">
<repository id="test" directory="~/Sync" ro="true" ignorePerms="false" rescanIntervalS="600">
<repository id="test" directory="testdata/" ro="true" ignorePerms="false" rescanIntervalS="600">
<node id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></node>
<node id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></node>
</repository>

View File

@@ -1,5 +1,5 @@
<configuration version="5">
<folder id="test" path="~/Sync" ro="true" ignorePerms="false" rescanIntervalS="600">
<folder id="test" path="testdata/" ro="true" ignorePerms="false" rescanIntervalS="600">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
</folder>

12
internal/config/testdata/v6.xml vendored Normal file
View File

@@ -0,0 +1,12 @@
<configuration version="6">
<folder id="test" path="testdata/" ro="true" ignorePerms="false" rescanIntervalS="600">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
</folder>
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="true">
<address>a</address>
</device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="true">
<address>b</address>
</device>
</configuration>

View File

@@ -1,5 +1,5 @@
<configuration version="2">
<repository id="test" directory="~/Sync" ro="true">
<repository id="test" directory="testdata/" ro="true">
<versioning type="simple">
<param key="foo" val="bar"/>
<param key="baz" val="quux"/>

View File

@@ -167,6 +167,12 @@ func (w *ConfigWrapper) Folders() map[string]FolderConfiguration {
if w.folderMap == nil {
w.folderMap = make(map[string]FolderConfiguration, len(w.cfg.Folders))
for _, fld := range w.cfg.Folders {
path, err := osutil.ExpandTilde(fld.Path)
if err != nil {
l.Warnln("home:", err)
continue
}
fld.Path = path
w.folderMap[fld.ID] = fld
}
}

View File

@@ -39,7 +39,7 @@ type Discoverer struct {
cacheLifetime time.Duration
broadcastBeacon beacon.Interface
multicastBeacon beacon.Interface
registry map[protocol.DeviceID][]cacheEntry
registry map[protocol.DeviceID][]CacheEntry
registryLock sync.RWMutex
extServer string
extPort uint16
@@ -51,9 +51,9 @@ type Discoverer struct {
extAnnounceOKmut sync.Mutex
}
type cacheEntry struct {
addr string
seen time.Time
type CacheEntry struct {
Address string
Seen time.Time
}
var (
@@ -68,7 +68,7 @@ func NewDiscoverer(id protocol.DeviceID, addresses []string) *Discoverer {
globalBcastIntv: 1800 * time.Second,
errorRetryIntv: 60 * time.Second,
cacheLifetime: 5 * time.Minute,
registry: make(map[protocol.DeviceID][]cacheEntry),
registry: make(map[protocol.DeviceID][]CacheEntry),
}
}
@@ -139,16 +139,16 @@ func (d *Discoverer) Lookup(device protocol.DeviceID) []string {
if len(cached) > 0 {
addrs := make([]string, len(cached))
for i := range cached {
addrs[i] = cached[i].addr
addrs[i] = cached[i].Address
}
return addrs
} else if len(d.extServer) != 0 {
addrs := d.externalLookup(device)
cached = make([]cacheEntry, len(addrs))
cached = make([]CacheEntry, len(addrs))
for i := range addrs {
cached[i] = cacheEntry{
addr: addrs[i],
seen: time.Now(),
cached[i] = CacheEntry{
Address: addrs[i],
Seen: time.Now(),
}
}
@@ -169,11 +169,11 @@ func (d *Discoverer) Hint(device string, addrs []string) {
})
}
func (d *Discoverer) All() map[protocol.DeviceID][]cacheEntry {
func (d *Discoverer) All() map[protocol.DeviceID][]CacheEntry {
d.registryLock.RLock()
devices := make(map[protocol.DeviceID][]cacheEntry, len(d.registry))
devices := make(map[protocol.DeviceID][]CacheEntry, len(d.registry))
for device, addrs := range d.registry {
addrsCopy := make([]cacheEntry, len(addrs))
addrsCopy := make([]CacheEntry, len(addrs))
copy(addrsCopy, addrs)
devices[device] = addrsCopy
}
@@ -365,14 +365,14 @@ func (d *Discoverer) registerDevice(addr net.Addr, device Device) bool {
deviceAddr = ua.String()
}
for i := range current {
if current[i].addr == deviceAddr {
current[i].seen = time.Now()
if current[i].Address == deviceAddr {
current[i].Seen = time.Now()
goto done
}
}
current = append(current, cacheEntry{
addr: deviceAddr,
seen: time.Now(),
current = append(current, CacheEntry{
Address: deviceAddr,
Seen: time.Now(),
})
done:
}
@@ -388,7 +388,7 @@ func (d *Discoverer) registerDevice(addr net.Addr, device Device) bool {
if len(current) > len(orig) {
addrs := make([]string, len(current))
for i := range current {
addrs[i] = current[i].addr
addrs[i] = current[i].Address
}
events.Default.Log(events.DeviceDiscovered, map[string]interface{}{
"device": id.String(),
@@ -468,11 +468,11 @@ func (d *Discoverer) externalLookup(device protocol.DeviceID) []string {
return addrs
}
func (d *Discoverer) filterCached(c []cacheEntry) []cacheEntry {
func (d *Discoverer) filterCached(c []CacheEntry) []CacheEntry {
for i := 0; i < len(c); {
if ago := time.Since(c[i].seen); ago > d.cacheLifetime {
if ago := time.Since(c[i].Seen); ago > d.cacheLifetime {
if debug {
l.Debugf("removing cached address %s: seen %v ago", c[i].addr, ago)
l.Debugf("removing cached address %s: seen %v ago", c[i].Address, ago)
}
c[i] = c[len(c)-1]
c = c[:len(c)-1]

201
internal/files/blockmap.go Normal file
View File

@@ -0,0 +1,201 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
// Package files provides a set type to track local/remote files with newness
// checks. We must do a certain amount of normalization in here. We will get
// fed paths with either native or wire-format separators and encodings
// depending on who calls us. We transform paths to wire-format (NFC and
// slashes) on the way to the database, and transform to native format
// (varying separator and encoding) on the way back out.
package files
import (
"bytes"
"encoding/binary"
"sort"
"sync"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/util"
)
var blockFinder *BlockFinder
type BlockMap struct {
db *leveldb.DB
folder string
}
func NewBlockMap(db *leveldb.DB, folder string) *BlockMap {
return &BlockMap{
db: db,
folder: folder,
}
}
// Add files to the block map, ignoring any deleted or invalid files.
func (m *BlockMap) Add(files []protocol.FileInfo) error {
batch := new(leveldb.Batch)
buf := make([]byte, 4)
for _, file := range files {
if file.IsDirectory() || file.IsDeleted() || file.IsInvalid() {
continue
}
for i, block := range file.Blocks {
binary.BigEndian.PutUint32(buf, uint32(i))
batch.Put(m.blockKey(block.Hash, file.Name), buf)
}
}
return m.db.Write(batch, nil)
}
// Update block map state, removing any deleted or invalid files.
func (m *BlockMap) Update(files []protocol.FileInfo) error {
batch := new(leveldb.Batch)
buf := make([]byte, 4)
for _, file := range files {
if file.IsDirectory() {
continue
}
if file.IsDeleted() || file.IsInvalid() {
for _, block := range file.Blocks {
batch.Delete(m.blockKey(block.Hash, file.Name))
}
continue
}
for i, block := range file.Blocks {
binary.BigEndian.PutUint32(buf, uint32(i))
batch.Put(m.blockKey(block.Hash, file.Name), buf)
}
}
return m.db.Write(batch, nil)
}
// Drop block map, removing all entries related to this block map from the db.
func (m *BlockMap) Drop() error {
batch := new(leveldb.Batch)
iter := m.db.NewIterator(util.BytesPrefix(m.blockKey(nil, "")[:1+64]), nil)
defer iter.Release()
for iter.Next() {
batch.Delete(iter.Key())
}
if iter.Error() != nil {
return iter.Error()
}
return m.db.Write(batch, nil)
}
func (m *BlockMap) blockKey(hash []byte, file string) []byte {
return toBlockKey(hash, m.folder, file)
}
type BlockFinder struct {
db *leveldb.DB
folders []string
mut sync.RWMutex
}
func NewBlockFinder(db *leveldb.DB, cfg *config.ConfigWrapper) *BlockFinder {
if blockFinder != nil {
return blockFinder
}
f := &BlockFinder{
db: db,
}
f.Changed(cfg.Raw())
cfg.Subscribe(f)
return f
}
// Implements config.Handler interface
func (f *BlockFinder) Changed(cfg config.Configuration) error {
folders := make([]string, len(cfg.Folders))
for i, folder := range cfg.Folders {
folders[i] = folder.ID
}
sort.Strings(folders)
f.mut.Lock()
f.folders = folders
f.mut.Unlock()
return nil
}
// An iterator function which iterates over all matching blocks for the given
// hash. The iterator function has to return either true (if they are happy with
// the block) or false to continue iterating for whatever reason.
// The iterator finally returns the result, whether or not a satisfying block
// was eventually found.
func (f *BlockFinder) Iterate(hash []byte, iterFn func(string, string, uint32) bool) bool {
f.mut.RLock()
folders := f.folders
f.mut.RUnlock()
for _, folder := range folders {
key := toBlockKey(hash, folder, "")
iter := f.db.NewIterator(util.BytesPrefix(key), nil)
defer iter.Release()
for iter.Next() && iter.Error() == nil {
folder, file := fromBlockKey(iter.Key())
index := binary.BigEndian.Uint32(iter.Value())
if iterFn(folder, nativeFilename(file), index) {
return true
}
}
}
return false
}
// m.blockKey returns a byte slice encoding the following information:
// keyTypeBlock (1 byte)
// folder (64 bytes)
// block hash (32 bytes)
// file name (variable size)
func toBlockKey(hash []byte, folder, file string) []byte {
o := make([]byte, 1+64+32+len(file))
o[0] = keyTypeBlock
copy(o[1:], []byte(folder))
copy(o[1+64:], []byte(hash))
copy(o[1+64+32:], []byte(file))
return o
}
func fromBlockKey(data []byte) (string, string) {
if len(data) < 1+64+32+1 {
panic("Incorrect key length")
}
if data[0] != keyTypeBlock {
panic("Incorrect key type")
}
file := string(data[1+64+32:])
slice := data[1 : 1+64]
izero := bytes.IndexByte(slice, 0)
if izero > -1 {
return string(slice[:izero]), file
}
return string(slice), file
}

View File

@@ -0,0 +1,235 @@
// Copyright (C) 2014 Jakob Borg and Contributors (see the CONTRIBUTORS file).
//
// This program is free software: you can redistribute it and/or modify it
// under the terms of the GNU General Public License as published by the Free
// Software Foundation, either version 3 of the License, or (at your option)
// any later version.
//
// This program is distributed in the hope that it will be useful, but WITHOUT
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
// more details.
//
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package files
import (
"testing"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/storage"
)
func genBlocks(n int) []protocol.BlockInfo {
b := make([]protocol.BlockInfo, n)
for i := range b {
h := make([]byte, 32)
for j := range h {
h[j] = byte(i + j)
}
b[i].Size = uint32(i)
b[i].Hash = h
}
return b
}
var f1, f2, f3 protocol.FileInfo
func init() {
blocks := genBlocks(30)
f1 = protocol.FileInfo{
Name: "f1",
Blocks: blocks[:10],
}
f2 = protocol.FileInfo{
Name: "f2",
Blocks: blocks[10:20],
}
f3 = protocol.FileInfo{
Name: "f3",
Blocks: blocks[20:],
}
}
func setup() (*leveldb.DB, *BlockFinder) {
// Setup
db, err := leveldb.Open(storage.NewMemStorage(), nil)
if err != nil {
panic(err)
}
wrapper := config.Wrap("", config.Configuration{})
wrapper.SetFolder(config.FolderConfiguration{
ID: "folder1",
})
wrapper.SetFolder(config.FolderConfiguration{
ID: "folder2",
})
return db, NewBlockFinder(db, wrapper)
}
func dbEmpty(db *leveldb.DB) bool {
iter := db.NewIterator(nil, nil)
defer iter.Release()
if iter.Next() {
return false
}
return true
}
func TestBlockMapAddUpdateWipe(t *testing.T) {
db, f := setup()
if !dbEmpty(db) {
t.Fatal("db not empty")
}
m := NewBlockMap(db, "folder1")
f3.Flags |= protocol.FlagDirectory
err := m.Add([]protocol.FileInfo{f1, f2, f3})
if err != nil {
t.Fatal(err)
}
f.Iterate(f1.Blocks[0].Hash, func(folder, file string, index uint32) bool {
if folder != "folder1" || file != "f1" || index != 0 {
t.Fatal("Mismatch")
}
return true
})
f.Iterate(f2.Blocks[0].Hash, func(folder, file string, index uint32) bool {
if folder != "folder1" || file != "f2" || index != 0 {
t.Fatal("Mismatch")
}
return true
})
f.Iterate(f3.Blocks[0].Hash, func(folder, file string, index uint32) bool {
t.Fatal("Unexpected block")
return true
})
f3.Flags = f1.Flags
f1.Flags |= protocol.FlagDeleted
f2.Flags |= protocol.FlagInvalid
// Should remove
err = m.Update([]protocol.FileInfo{f1, f2, f3})
if err != nil {
t.Fatal(err)
}
f.Iterate(f1.Blocks[0].Hash, func(folder, file string, index uint32) bool {
t.Fatal("Unexpected block")
return false
})
f.Iterate(f2.Blocks[0].Hash, func(folder, file string, index uint32) bool {
t.Fatal("Unexpected block")
return false
})
f.Iterate(f3.Blocks[0].Hash, func(folder, file string, index uint32) bool {
if folder != "folder1" || file != "f3" || index != 0 {
t.Fatal("Mismatch")
}
return true
})
err = m.Drop()
if err != nil {
t.Fatal(err)
}
if !dbEmpty(db) {
t.Fatal("db not empty")
}
// Should not add
err = m.Add([]protocol.FileInfo{f1, f2})
if err != nil {
t.Fatal(err)
}
if !dbEmpty(db) {
t.Fatal("db not empty")
}
f1.Flags = 0
f2.Flags = 0
f3.Flags = 0
}
func TestBlockMapFinderLookup(t *testing.T) {
db, f := setup()
m1 := NewBlockMap(db, "folder1")
m2 := NewBlockMap(db, "folder2")
err := m1.Add([]protocol.FileInfo{f1})
if err != nil {
t.Fatal(err)
}
err = m2.Add([]protocol.FileInfo{f1})
if err != nil {
t.Fatal(err)
}
counter := 0
f.Iterate(f1.Blocks[0].Hash, func(folder, file string, index uint32) bool {
counter++
switch counter {
case 1:
if folder != "folder1" || file != "f1" || index != 0 {
t.Fatal("Mismatch")
}
case 2:
if folder != "folder2" || file != "f1" || index != 0 {
t.Fatal("Mismatch")
}
default:
t.Fatal("Unexpected block")
}
return false
})
if counter != 2 {
t.Fatal("Incorrect count", counter)
}
f1.Flags |= protocol.FlagDeleted
err = m1.Update([]protocol.FileInfo{f1})
if err != nil {
t.Fatal(err)
}
counter = 0
f.Iterate(f1.Blocks[0].Hash, func(folder, file string, index uint32) bool {
counter++
switch counter {
case 1:
if folder != "folder2" || file != "f1" || index != 0 {
t.Fatal("Mismatch")
}
default:
t.Fatal("Unexpected block")
}
return false
})
if counter != 1 {
t.Fatal("Incorrect count")
}
}

View File

@@ -49,6 +49,7 @@ func clock(v uint64) uint64 {
const (
keyTypeDevice = iota
keyTypeGlobal
keyTypeBlock
)
type fileVersion struct {

View File

@@ -42,6 +42,7 @@ type Set struct {
mutex sync.Mutex
folder string
db *leveldb.DB
blockmap *BlockMap
}
func NewSet(folder string, db *leveldb.DB) *Set {
@@ -49,6 +50,7 @@ func NewSet(folder string, db *leveldb.DB) *Set {
localVersion: make(map[protocol.DeviceID]uint64),
folder: folder,
db: db,
blockmap: NewBlockMap(db, folder),
}
var deviceID protocol.DeviceID
@@ -80,6 +82,10 @@ func (s *Set) Replace(device protocol.DeviceID, fs []protocol.FileInfo) {
// Reset the local version if all files were removed.
s.localVersion[device] = 0
}
if device == protocol.LocalDeviceID {
s.blockmap.Drop()
s.blockmap.Add(fs)
}
}
func (s *Set) ReplaceWithDelete(device protocol.DeviceID, fs []protocol.FileInfo) {
@@ -92,6 +98,10 @@ func (s *Set) ReplaceWithDelete(device protocol.DeviceID, fs []protocol.FileInfo
if lv := ldbReplaceWithDelete(s.db, []byte(s.folder), device[:], fs); lv > s.localVersion[device] {
s.localVersion[device] = lv
}
if device == protocol.LocalDeviceID {
s.blockmap.Drop()
s.blockmap.Add(fs)
}
}
func (s *Set) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
@@ -104,6 +114,9 @@ func (s *Set) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
if lv := ldbUpdate(s.db, []byte(s.folder), device[:], fs); lv > s.localVersion[device] {
s.localVersion[device] = lv
}
if device == protocol.LocalDeviceID {
s.blockmap.Update(fs)
}
}
func (s *Set) WithNeed(device protocol.DeviceID, fn fileIterator) {
@@ -179,6 +192,11 @@ func ListFolders(db *leveldb.DB) []string {
// database.
func DropFolder(db *leveldb.DB, folder string) {
ldbDropFolder(db, []byte(folder))
bm := &BlockMap{
db: db,
folder: folder,
}
bm.Drop()
}
func normalizeFilenames(fs []protocol.FileInfo) {

View File

@@ -23,31 +23,94 @@ import (
"path/filepath"
"regexp"
"strings"
"sync"
"github.com/syncthing/syncthing/internal/fnmatch"
)
var caches = make(map[string]MatcherCache)
type Pattern struct {
match *regexp.Regexp
include bool
}
type Patterns []Pattern
type Matcher struct {
patterns []Pattern
oldMatches map[string]bool
func Load(file string) (Patterns, error) {
seen := make(map[string]bool)
return loadIgnoreFile(file, seen)
newMatches map[string]bool
mut sync.Mutex
}
func Parse(r io.Reader, file string) (Patterns, error) {
type MatcherCache struct {
patterns []Pattern
matches *map[string]bool
}
func Load(file string, cache bool) (*Matcher, error) {
seen := make(map[string]bool)
matcher, err := loadIgnoreFile(file, seen)
if !cache || err != nil {
return matcher, err
}
// Get the current cache object for the given file
cached, ok := caches[file]
if !ok || !patternsEqual(cached.patterns, matcher.patterns) {
// Nothing in cache or a cache mismatch, create a new cache which will
// store matches for the given set of patterns.
// Initialize oldMatches to indicate that we are interested in
// caching.
matcher.oldMatches = make(map[string]bool)
matcher.newMatches = make(map[string]bool)
caches[file] = MatcherCache{
patterns: matcher.patterns,
matches: &matcher.newMatches,
}
return matcher, nil
}
// Patterns haven't changed, so we can reuse the old matches, create a new
// matches map and update the pointer. (This prevents matches map from
// growing indefinately, as we only cache whatever we've matched in the last
// iteration, rather than through runtime history)
matcher.oldMatches = *cached.matches
matcher.newMatches = make(map[string]bool)
cached.matches = &matcher.newMatches
caches[file] = cached
return matcher, nil
}
func Parse(r io.Reader, file string) (*Matcher, error) {
seen := map[string]bool{
file: true,
}
return parseIgnoreFile(r, file, seen)
}
func (l Patterns) Match(file string) bool {
for _, pattern := range l {
func (m *Matcher) Match(file string) (result bool) {
if len(m.patterns) == 0 {
return false
}
// We have old matches map set, means we should do caching
if m.oldMatches != nil {
// Capture the result to the new matches regardless of who returns it
defer func() {
m.mut.Lock()
m.newMatches[file] = result
m.mut.Unlock()
}()
// Check perhaps we've seen this file before, and we already know
// what the outcome is going to be.
result, ok := m.oldMatches[file]
if ok {
return result
}
}
for _, pattern := range m.patterns {
if pattern.match.MatchString(file) {
return pattern.include
}
@@ -55,7 +118,7 @@ func (l Patterns) Match(file string) bool {
return false
}
func loadIgnoreFile(file string, seen map[string]bool) (Patterns, error) {
func loadIgnoreFile(file string, seen map[string]bool) (*Matcher, error) {
if seen[file] {
return nil, fmt.Errorf("Multiple include of ignore file %q", file)
}
@@ -70,8 +133,8 @@ func loadIgnoreFile(file string, seen map[string]bool) (Patterns, error) {
return parseIgnoreFile(fd, file, seen)
}
func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) (Patterns, error) {
var exps Patterns
func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) (*Matcher, error) {
var exps Matcher
addPattern := func(line string) error {
include := true
@@ -86,27 +149,27 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) (Pa
if err != nil {
return fmt.Errorf("Invalid pattern %q in ignore file", line)
}
exps = append(exps, Pattern{exp, include})
exps.patterns = append(exps.patterns, Pattern{exp, include})
} else if strings.HasPrefix(line, "**/") {
// Add the pattern as is, and without **/ so it matches in current dir
exp, err := fnmatch.Convert(line, fnmatch.FNM_PATHNAME)
if err != nil {
return fmt.Errorf("Invalid pattern %q in ignore file", line)
}
exps = append(exps, Pattern{exp, include})
exps.patterns = append(exps.patterns, Pattern{exp, include})
exp, err = fnmatch.Convert(line[3:], fnmatch.FNM_PATHNAME)
if err != nil {
return fmt.Errorf("Invalid pattern %q in ignore file", line)
}
exps = append(exps, Pattern{exp, include})
exps.patterns = append(exps.patterns, Pattern{exp, include})
} else if strings.HasPrefix(line, "#include ") {
includeFile := filepath.Join(filepath.Dir(currentFile), line[len("#include "):])
includes, err := loadIgnoreFile(includeFile, seen)
if err != nil {
return err
} else {
exps = append(exps, includes...)
exps.patterns = append(exps.patterns, includes.patterns...)
}
} else {
// Path name or pattern, add it so it matches files both in
@@ -115,13 +178,13 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) (Pa
if err != nil {
return fmt.Errorf("Invalid pattern %q in ignore file", line)
}
exps = append(exps, Pattern{exp, include})
exps.patterns = append(exps.patterns, Pattern{exp, include})
exp, err = fnmatch.Convert("**/"+line, fnmatch.FNM_PATHNAME)
if err != nil {
return fmt.Errorf("Invalid pattern %q in ignore file", line)
}
exps = append(exps, Pattern{exp, include})
exps.patterns = append(exps.patterns, Pattern{exp, include})
}
return nil
}
@@ -155,5 +218,17 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) (Pa
}
}
return exps, nil
return &exps, nil
}
func patternsEqual(a, b []Pattern) bool {
if len(a) != len(b) {
return false
}
for i := range a {
if a[i].include != b[i].include || a[i].match.String() != b[i].match.String() {
return false
}
}
return true
}

View File

@@ -13,19 +13,19 @@
// You should have received a copy of the GNU General Public License along
// with this program. If not, see <http://www.gnu.org/licenses/>.
package ignore_test
package ignore
import (
"bytes"
"io/ioutil"
"os"
"path/filepath"
"runtime"
"testing"
"github.com/syncthing/syncthing/internal/ignore"
)
func TestIgnore(t *testing.T) {
pats, err := ignore.Load("testdata/.stignore")
pats, err := Load("testdata/.stignore", true)
if err != nil {
t.Fatal(err)
}
@@ -72,7 +72,7 @@ func TestExcludes(t *testing.T) {
i*2
!ign2
`
pats, err := ignore.Parse(bytes.NewBufferString(stignore), ".stignore")
pats, err := Parse(bytes.NewBufferString(stignore), ".stignore")
if err != nil {
t.Fatal(err)
}
@@ -112,7 +112,7 @@ func TestBadPatterns(t *testing.T) {
}
for _, pat := range badPatterns {
parsed, err := ignore.Parse(bytes.NewBufferString(pat), ".stignore")
parsed, err := Parse(bytes.NewBufferString(pat), ".stignore")
if err == nil {
t.Errorf("No error for pattern %q: %v", pat, parsed)
}
@@ -120,7 +120,7 @@ func TestBadPatterns(t *testing.T) {
}
func TestCaseSensitivity(t *testing.T) {
ign, _ := ignore.Parse(bytes.NewBufferString("test"), ".stignore")
ign, _ := Parse(bytes.NewBufferString("test"), ".stignore")
match := []string{"test"}
dontMatch := []string{"foo"}
@@ -145,6 +145,144 @@ func TestCaseSensitivity(t *testing.T) {
}
}
func TestCaching(t *testing.T) {
fd1, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
fd2, err := ioutil.TempFile("", "")
if err != nil {
t.Fatal(err)
}
defer fd1.Close()
defer fd2.Close()
defer os.Remove(fd1.Name())
defer os.Remove(fd2.Name())
_, err = fd1.WriteString("/x/\n#include " + filepath.Base(fd2.Name()) + "\n")
if err != nil {
t.Fatal(err)
}
fd2.WriteString("/y/\n")
pats, err := Load(fd1.Name(), true)
if err != nil {
t.Fatal(err)
}
if pats.oldMatches == nil || len(pats.oldMatches) != 0 {
t.Fatal("Expected empty map")
}
if pats.newMatches == nil || len(pats.newMatches) != 0 {
t.Fatal("Expected empty map")
}
if len(pats.patterns) != 4 {
t.Fatal("Incorrect number of patterns loaded", len(pats.patterns), "!=", 4)
}
// Cache some outcomes
for _, letter := range []string{"a", "b", "x", "y"} {
pats.Match(letter)
}
if len(pats.newMatches) != 4 {
t.Fatal("Expected 4 cached results")
}
// Reload file, expect old outcomes to be provided
pats, err = Load(fd1.Name(), true)
if err != nil {
t.Fatal(err)
}
if len(pats.oldMatches) != 4 {
t.Fatal("Expected 4 cached results")
}
// Match less this time
for _, letter := range []string{"b", "x", "y"} {
pats.Match(letter)
}
if len(pats.newMatches) != 3 {
t.Fatal("Expected 3 cached results")
}
// Reload file, expect the new outcomes to be provided
pats, err = Load(fd1.Name(), true)
if err != nil {
t.Fatal(err)
}
if len(pats.oldMatches) != 3 {
t.Fatal("Expected 3 cached results", len(pats.oldMatches))
}
// Modify the include file, expect empty cache
fd2.WriteString("/z/\n")
pats, err = Load(fd1.Name(), true)
if err != nil {
t.Fatal(err)
}
if len(pats.oldMatches) != 0 {
t.Fatal("Expected 0 cached results")
}
// Cache some outcomes again
for _, letter := range []string{"b", "x", "y"} {
pats.Match(letter)
}
// Verify that outcomes provided on next laod
pats, err = Load(fd1.Name(), true)
if err != nil {
t.Fatal(err)
}
if len(pats.oldMatches) != 3 {
t.Fatal("Expected 3 cached results")
}
// Modify the root file, expect cache to be invalidated
fd1.WriteString("/a/\n")
pats, err = Load(fd1.Name(), true)
if err != nil {
t.Fatal(err)
}
if len(pats.oldMatches) != 0 {
t.Fatal("Expected cache invalidation")
}
// Cache some outcomes again
for _, letter := range []string{"b", "x", "y"} {
pats.Match(letter)
}
// Verify that outcomes provided on next laod
pats, err = Load(fd1.Name(), true)
if err != nil {
t.Fatal(err)
}
if len(pats.oldMatches) != 3 {
t.Fatal("Expected 3 cached results")
}
}
func TestCommentsAndBlankLines(t *testing.T) {
stignore := `
// foo
@@ -157,8 +295,83 @@ func TestCommentsAndBlankLines(t *testing.T) {
`
pats, _ := ignore.Parse(bytes.NewBufferString(stignore), ".stignore")
if len(pats) > 0 {
pats, _ := Parse(bytes.NewBufferString(stignore), ".stignore")
if len(pats.patterns) > 0 {
t.Errorf("Expected no patterns")
}
}
var result bool
func BenchmarkMatch(b *testing.B) {
stignore := `
.frog
.frog*
.frogfox
.whale
.whale/*
.dolphin
.dolphin/*
~ferret~.*
.ferret.*
flamingo.*
flamingo
*.crow
*.crow
`
pats, _ := Parse(bytes.NewBufferString(stignore), ".stignore")
b.ResetTimer()
for i := 0; i < b.N; i++ {
result = pats.Match("filename")
}
}
func BenchmarkMatchCached(b *testing.B) {
stignore := `
.frog
.frog*
.frogfox
.whale
.whale/*
.dolphin
.dolphin/*
~ferret~.*
.ferret.*
flamingo.*
flamingo
*.crow
*.crow
`
// Caches per file, hence write the patterns to a file.
fd, err := ioutil.TempFile("", "")
if err != nil {
b.Fatal(err)
}
_, err = fd.WriteString(stignore)
defer fd.Close()
defer os.Remove(fd.Name())
if err != nil {
b.Fatal(err)
}
// Load the patterns
pats, err := Load(fd.Name(), true)
if err != nil {
b.Fatal(err)
}
// Cache the outcome for "filename"
pats.Match("filename")
// This load should now load the cached outcomes as the set of patterns
// has not changed.
pats, err = Load(fd.Name(), true)
if err != nil {
b.Fatal(err)
}
b.ResetTimer()
for i := 0; i < b.N; i++ {
result = pats.Match("filename")
}
}

View File

@@ -81,8 +81,9 @@ type service interface {
}
type Model struct {
cfg *config.ConfigWrapper
db *leveldb.DB
cfg *config.ConfigWrapper
db *leveldb.DB
finder *files.BlockFinder
deviceName string
clientName string
@@ -93,7 +94,7 @@ type Model struct {
folderDevices map[string][]protocol.DeviceID // folder -> deviceIDs
deviceFolders map[protocol.DeviceID][]string // deviceID -> folders
deviceStatRefs map[protocol.DeviceID]*stats.DeviceStatisticsReference // deviceID -> statsRef
folderIgnores map[string]ignore.Patterns // folder -> list of ignore patterns
folderIgnores map[string]*ignore.Matcher // folder -> matcher object
folderRunners map[string]service // folder -> puller or scanner
fmut sync.RWMutex // protects the above
@@ -130,13 +131,14 @@ func NewModel(cfg *config.ConfigWrapper, deviceName, clientName, clientVersion s
folderDevices: make(map[string][]protocol.DeviceID),
deviceFolders: make(map[protocol.DeviceID][]string),
deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
folderIgnores: make(map[string]ignore.Patterns),
folderIgnores: make(map[string]*ignore.Matcher),
folderRunners: make(map[string]service),
folderState: make(map[string]folderState),
folderStateChanged: make(map[string]time.Time),
protoConn: make(map[protocol.DeviceID]protocol.Connection),
rawConn: make(map[protocol.DeviceID]io.Closer),
deviceVer: make(map[protocol.DeviceID]string),
finder: files.NewBlockFinder(db, cfg),
}
var timeout = 20 * 60 // seconds
@@ -167,11 +169,12 @@ func (m *Model) StartFolderRW(folder string) {
panic("cannot start already running folder " + folder)
}
p := &Puller{
folder: folder,
dir: cfg.Path,
scanIntv: time.Duration(cfg.RescanIntervalS) * time.Second,
model: m,
ignorePerms: cfg.IgnorePerms,
folder: folder,
dir: cfg.Path,
scanIntv: time.Duration(cfg.RescanIntervalS) * time.Second,
model: m,
ignorePerms: cfg.IgnorePerms,
lenientMtimes: cfg.LenientMtimes,
}
m.folderRunners[folder] = p
m.fmut.Unlock()
@@ -184,6 +187,10 @@ func (m *Model) StartFolderRW(folder string) {
p.versioner = factory(folder, cfg.Path, cfg.Versioning.Params)
}
if cfg.LenientMtimes {
l.Infof("Folder %q is running with LenientMtimes workaround. Syncing may not work properly.", folder)
}
go p.Serve()
}
@@ -266,6 +273,8 @@ func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
// Returns the completion status, in percent, for the given device and folder.
func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
defer m.leveldbPanicWorkaround()
var tot int64
m.fmut.RLock()
@@ -325,6 +334,8 @@ func sizeOfFile(f protocol.FileIntf) (files, deleted int, bytes int64) {
// GlobalSize returns the number of files, deleted files and total bytes for all
// files in the global model.
func (m *Model) GlobalSize(folder string) (files, deleted int, bytes int64) {
defer m.leveldbPanicWorkaround()
m.fmut.RLock()
defer m.fmut.RUnlock()
if rf, ok := m.folderFiles[folder]; ok {
@@ -342,6 +353,8 @@ func (m *Model) GlobalSize(folder string) (files, deleted int, bytes int64) {
// LocalSize returns the number of files, deleted files and total bytes for all
// files in the local folder.
func (m *Model) LocalSize(folder string) (files, deleted int, bytes int64) {
defer m.leveldbPanicWorkaround()
m.fmut.RLock()
defer m.fmut.RUnlock()
if rf, ok := m.folderFiles[folder]; ok {
@@ -361,6 +374,8 @@ func (m *Model) LocalSize(folder string) (files, deleted int, bytes int64) {
// NeedSize returns the number and total size of currently needed files.
func (m *Model) NeedSize(folder string) (files int, bytes int64) {
defer m.leveldbPanicWorkaround()
m.fmut.RLock()
defer m.fmut.RUnlock()
if rf, ok := m.folderFiles[folder]; ok {
@@ -380,6 +395,8 @@ func (m *Model) NeedSize(folder string) (files int, bytes int64) {
// NeedFiles returns the list of currently needed files, stopping at maxFiles
// files or maxBlocks blocks. Limits <= 0 are ignored.
func (m *Model) NeedFolderFilesLimited(folder string, maxFiles, maxBlocks int) []protocol.FileInfo {
defer m.leveldbPanicWorkaround()
m.fmut.RLock()
defer m.fmut.RUnlock()
nblocks := 0
@@ -423,7 +440,10 @@ func (m *Model) Index(deviceID protocol.DeviceID, folder string, fs []protocol.F
for i := 0; i < len(fs); {
lamport.Default.Tick(fs[i].Version)
if ignores.Match(fs[i].Name) {
if ignores != nil && ignores.Match(fs[i].Name) {
if debug {
l.Debugln("dropping update for ignored", fs[i])
}
fs[i] = fs[len(fs)-1]
fs = fs[:len(fs)-1]
} else {
@@ -464,7 +484,10 @@ func (m *Model) IndexUpdate(deviceID protocol.DeviceID, folder string, fs []prot
for i := 0; i < len(fs); {
lamport.Default.Tick(fs[i].Version)
if ignores.Match(fs[i].Name) {
if ignores != nil && ignores.Match(fs[i].Name) {
if debug {
l.Debugln("dropping update for ignored", fs[i])
}
fs[i] = fs[len(fs)-1]
fs = fs[:len(fs)-1]
} else {
@@ -538,6 +561,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
newDeviceCfg := config.DeviceConfiguration{
DeviceID: id,
Compression: true,
Addresses: []string{"dynamic"},
}
// The introducers' introducers are also our introducers.
@@ -819,7 +843,7 @@ func (m *Model) deviceWasSeen(deviceID protocol.DeviceID) {
m.deviceStatRef(deviceID).WasSeen()
}
func sendIndexes(conn protocol.Connection, folder string, fs *files.Set, ignores ignore.Patterns) {
func sendIndexes(conn protocol.Connection, folder string, fs *files.Set, ignores *ignore.Matcher) {
deviceID := conn.ID()
name := conn.Name()
var err error
@@ -844,7 +868,7 @@ func sendIndexes(conn protocol.Connection, folder string, fs *files.Set, ignores
}
}
func sendIndexTo(initial bool, minLocalVer uint64, conn protocol.Connection, folder string, fs *files.Set, ignores ignore.Patterns) (uint64, error) {
func sendIndexTo(initial bool, minLocalVer uint64, conn protocol.Connection, folder string, fs *files.Set, ignores *ignore.Matcher) (uint64, error) {
deviceID := conn.ID()
name := conn.Name()
batch := make([]protocol.FileInfo, 0, indexBatchSize)
@@ -862,7 +886,10 @@ func sendIndexTo(initial bool, minLocalVer uint64, conn protocol.Connection, fol
maxLocalVer = f.LocalVersion
}
if ignores.Match(f.Name) {
if ignores != nil && ignores.Match(f.Name) {
if debug {
l.Debugln("not sending update for ignored", f)
}
return true
}
@@ -996,13 +1023,13 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
fs, ok := m.folderFiles[folder]
dir := m.folderCfgs[folder].Path
ignores, _ := ignore.Load(filepath.Join(dir, ".stignore"))
ignores, _ := ignore.Load(filepath.Join(dir, ".stignore"), m.cfg.Options().CacheIgnoredFiles)
m.folderIgnores[folder] = ignores
w := &scanner.Walker{
Dir: dir,
Sub: sub,
Ignores: ignores,
Matcher: ignores,
BlockSize: protocol.BlockSize,
TempNamer: defTempNamer,
CurrentFiler: cFiler{m, folder},
@@ -1062,8 +1089,9 @@ func (m *Model) ScanFolderSub(folder, sub string) error {
batch = batch[:0]
}
if ignores.Match(f.Name) {
if ignores != nil && ignores.Match(f.Name) {
// File has been ignored. Set invalid bit.
l.Debugln("setting invalid bit on ignored", f)
nf := protocol.FileInfo{
Name: f.Name,
Flags: f.Flags | protocol.FlagInvalid,
@@ -1259,3 +1287,24 @@ func (m *Model) availability(folder string, file string) []protocol.DeviceID {
func (m *Model) String() string {
return fmt.Sprintf("model@%p", m)
}
func (m *Model) leveldbPanicWorkaround() {
// When an inconsistency is detected in leveldb we panic(). This is
// appropriate because it should never happen, but currently it does for
// some reason. However it only seems to trigger in the asynchronous full-
// database scans that happen due to REST and usage-reporting calls. In
// those places we defer to this workaround to catch the panic instead of
// taking down syncthing.
// This is just a band-aid and should be removed as soon as we have found
// a real root cause.
if pnc := recover(); pnc != nil {
if err, ok := pnc.(error); ok && strings.Contains(err.Error(), "leveldb") {
l.Infoln("recovered:", err)
} else {
// Any non-leveldb error is genuine and should continue panicing.
panic(err)
}
}
}

View File

@@ -390,8 +390,12 @@ func TestIgnores(t *testing.T) {
}
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
m := NewModel(nil, "device", "syncthing", "dev", db)
m.AddFolder(config.FolderConfiguration{ID: "default", Path: "testdata"})
fcfg := config.FolderConfiguration{ID: "default", Path: "testdata"}
cfg := config.Wrap("/tmp", config.Configuration{
Folders: []config.FolderConfiguration{fcfg},
})
m := NewModel(cfg, "device", "syncthing", "dev", db)
m.AddFolder(fcfg)
expected := []string{
".*",

View File

@@ -16,6 +16,7 @@
package model
import (
"bytes"
"errors"
"fmt"
"os"
@@ -23,6 +24,8 @@ import (
"sync"
"time"
"github.com/AudriusButkevicius/lfu-go"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/events"
"github.com/syncthing/syncthing/internal/osutil"
@@ -50,7 +53,7 @@ type pullBlockState struct {
}
// A copyBlocksState is passed to copy routine if the file has blocks to be
// copied from the original.
// copied.
type copyBlocksState struct {
*sharedPullerState
blocks []protocol.BlockInfo
@@ -62,13 +65,14 @@ var (
)
type Puller struct {
folder string
dir string
scanIntv time.Duration
model *Model
stop chan struct{}
versioner versioner.Versioner
ignorePerms bool
folder string
dir string
scanIntv time.Duration
model *Model
stop chan struct{}
versioner versioner.Versioner
ignorePerms bool
lenientMtimes bool
}
// Serve will run scans and pulls. It will return when Stop()ed or on a
@@ -83,10 +87,12 @@ func (p *Puller) Serve() {
pullTimer := time.NewTimer(checkPullIntv)
scanTimer := time.NewTimer(time.Millisecond) // The first scan should be done immediately.
cleanTimer := time.NewTicker(time.Hour)
defer func() {
pullTimer.Stop()
scanTimer.Stop()
cleanTimer.Stop()
// TODO: Should there be an actual FolderStopped state?
p.model.setState(p.folder, FolderIdle)
}()
@@ -160,6 +166,9 @@ loop:
curVer = lv
}
prevVer = curVer
if debug {
l.Debugln(p, "next pull in", nextPullIntv)
}
pullTimer.Reset(nextPullIntv)
break
}
@@ -170,6 +179,9 @@ loop:
// errors preventing us. Flag this with a warning and
// wait a bit longer before retrying.
l.Warnf("Folder %q isn't making progress - check logs for possible root cause. Pausing puller for %v.", p.folder, pauseIntv)
if debug {
l.Debugln(p, "next pull in", pauseIntv)
}
pullTimer.Reset(pauseIntv)
break
}
@@ -189,11 +201,20 @@ loop:
break loop
}
p.model.setState(p.folder, FolderIdle)
scanTimer.Reset(p.scanIntv)
if p.scanIntv > 0 {
if debug {
l.Debugln(p, "next rescan in", p.scanIntv)
}
scanTimer.Reset(p.scanIntv)
}
if !initialScanCompleted {
l.Infoln("Completed initial scan (rw) of folder", p.folder)
initialScanCompleted = true
}
// Clean out old temporaries
case <-cleanTimer.C:
p.clean()
}
}
}
@@ -218,24 +239,25 @@ func (p *Puller) pullerIteration(ncopiers, npullers, nfinishers int) int {
copyChan := make(chan copyBlocksState)
finisherChan := make(chan *sharedPullerState)
var wg sync.WaitGroup
var copyWg sync.WaitGroup
var pullWg sync.WaitGroup
var doneWg sync.WaitGroup
for i := 0; i < ncopiers; i++ {
wg.Add(1)
copyWg.Add(1)
go func() {
// copierRoutine finishes when copyChan is closed
p.copierRoutine(copyChan, finisherChan)
wg.Done()
p.copierRoutine(copyChan, pullChan, finisherChan)
copyWg.Done()
}()
}
for i := 0; i < npullers; i++ {
wg.Add(1)
pullWg.Add(1)
go func() {
// pullerRoutine finishes when pullChan is closed
p.pullerRoutine(pullChan, finisherChan)
wg.Done()
pullWg.Done()
}()
}
@@ -259,6 +281,9 @@ func (p *Puller) pullerIteration(ncopiers, npullers, nfinishers int) int {
// !!!
changed := 0
var deletions []protocol.FileInfo
files.WithNeed(protocol.LocalDeviceID, func(intf protocol.FileIntf) bool {
// Needed items are delivered sorted lexicographically. This isn't
@@ -280,19 +305,16 @@ func (p *Puller) pullerIteration(ncopiers, npullers, nfinishers int) int {
}
switch {
case protocol.IsDirectory(file.Flags) && protocol.IsDeleted(file.Flags):
// A deleted directory
p.deleteDir(file)
case protocol.IsDeleted(file.Flags):
// A deleted file or directory
deletions = append(deletions, file)
case protocol.IsDirectory(file.Flags):
// A new or changed directory
p.handleDir(file)
case protocol.IsDeleted(file.Flags):
// A deleted file
p.deleteFile(file)
default:
// A new or changed file. This is the only case where we do stuff
// in the background; the other three are done synchronously.
p.handleFile(file, copyChan, pullChan, finisherChan)
p.handleFile(file, copyChan, finisherChan)
}
changed++
@@ -300,18 +322,27 @@ func (p *Puller) pullerIteration(ncopiers, npullers, nfinishers int) int {
})
// Signal copy and puller routines that we are done with the in data for
// this iteration
// this iteration. Wait for them to finish.
close(copyChan)
copyWg.Wait()
close(pullChan)
pullWg.Wait()
// Wait for them to finish, then signal the finisher chan that there will
// be no more input.
wg.Wait()
// Signal the finisher chan that there will be no more input.
close(finisherChan)
// Wait for the finisherChan to finish.
doneWg.Wait()
for i := range deletions {
deletion := deletions[len(deletions)-i-1]
if deletion.IsDirectory() {
p.deleteDir(deletion)
} else {
p.deleteFile(deletion)
}
}
return changed
}
@@ -401,11 +432,15 @@ func (p *Puller) deleteFile(file protocol.FileInfo) {
// handleFile queues the copies and pulls as necessary for a single new or
// changed file.
func (p *Puller) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksState, pullChan chan<- pullBlockState, finisherChan chan<- *sharedPullerState) {
func (p *Puller) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksState, finisherChan chan<- *sharedPullerState) {
curFile := p.model.CurrentFolderFile(p.folder, file.Name)
copyBlocks, pullBlocks := scanner.BlockDiff(curFile.Blocks, file.Blocks)
if len(copyBlocks) == len(curFile.Blocks) && len(pullBlocks) == 0 {
if len(curFile.Blocks) == len(file.Blocks) {
for i := range file.Blocks {
if !bytes.Equal(curFile.Blocks[i].Hash, file.Blocks[i].Hash) {
goto FilesAreDifferent
}
}
// We are supposed to copy the entire file, and then fetch nothing. We
// are only updating metadata, so we don't actually *need* to make the
// copy.
@@ -416,11 +451,16 @@ func (p *Puller) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksSt
return
}
FilesAreDifferent:
scanner.PopulateOffsets(file.Blocks)
// Figure out the absolute filenames we need once and for all
tempName := filepath.Join(p.dir, defTempNamer.TempName(file.Name))
realName := filepath.Join(p.dir, file.Name)
var reuse bool
reused := 0
var blocks []protocol.BlockInfo
// Check for an old temporary file which might have some blocks we could
// reuse.
@@ -435,38 +475,25 @@ func (p *Puller) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksSt
existingBlocks[block.String()] = true
}
// Since the blocks are already there, we don't need to copy them
// nor we need to pull them, hence discard blocks which are already
// there, if they are exactly the same...
var newCopyBlocks []protocol.BlockInfo
for _, block := range copyBlocks {
// Since the blocks are already there, we don't need to get them.
for _, block := range file.Blocks {
_, ok := existingBlocks[block.String()]
if !ok {
newCopyBlocks = append(newCopyBlocks, block)
blocks = append(blocks, block)
}
}
var newPullBlocks []protocol.BlockInfo
for _, block := range pullBlocks {
_, ok := existingBlocks[block.String()]
if !ok {
newPullBlocks = append(newPullBlocks, block)
}
}
// If any blocks could be reused, let the sharedpullerstate know
// which flags it is expected to set on the file.
// Also update the list of work for the routines.
if len(copyBlocks) != len(newCopyBlocks) || len(pullBlocks) != len(newPullBlocks) {
reuse = true
copyBlocks = newCopyBlocks
pullBlocks = newPullBlocks
} else {
// The sharedpullerstate will know which flags to use when opening the
// temp file depending if we are reusing any blocks or not.
reused = len(file.Blocks) - len(blocks)
if reused == 0 {
// Otherwise, discard the file ourselves in order for the
// sharedpuller not to panic when it fails to exlusively create a
// file which already exists
os.Remove(tempName)
}
} else {
blocks = file.Blocks
}
s := sharedPullerState{
@@ -474,43 +501,20 @@ func (p *Puller) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksSt
folder: p.folder,
tempName: tempName,
realName: realName,
pullNeeded: len(pullBlocks),
reuse: reuse,
}
if len(copyBlocks) > 0 {
s.copyNeeded = 1
copyTotal: len(blocks),
copyNeeded: len(blocks),
reused: reused,
}
if debug {
l.Debugf("%v need file %s; copy %d, pull %d, reuse %v", p, file.Name, len(copyBlocks), len(pullBlocks), reuse)
l.Debugf("%v need file %s; copy %d, reused %v", p, file.Name, len(blocks), reused)
}
if len(copyBlocks) > 0 {
cs := copyBlocksState{
sharedPullerState: &s,
blocks: copyBlocks,
}
copyChan <- cs
}
if len(pullBlocks) > 0 {
for _, block := range pullBlocks {
ps := pullBlockState{
sharedPullerState: &s,
block: block,
}
pullChan <- ps
}
}
if len(pullBlocks) == 0 && len(copyBlocks) == 0 {
if !reuse {
panic("bug: nothing to do with file?")
}
// We have a temp file that we can reuse totally. Jump directly to the
// finisher stage.
finisherChan <- &s
cs := copyBlocksState{
sharedPullerState: &s,
blocks: blocks,
}
copyChan <- cs
}
// shortcutFile sets file mode and modification time, when that's the only
@@ -528,16 +532,24 @@ func (p *Puller) shortcutFile(file protocol.FileInfo) {
t := time.Unix(file.Modified, 0)
err := os.Chtimes(realName, t, t)
if err != nil {
l.Infof("Puller (folder %q, file %q): shortcut: %v", p.folder, file.Name, err)
return
if p.lenientMtimes {
// We accept the failure with a warning here and allow the sync to
// continue. We'll sync the new mtime back to the other devices later.
// If they have the same problem & setting, we might never get in
// sync.
l.Infof("Puller (folder %q, file %q): shortcut: %v (continuing anyway as requested)", p.folder, file.Name, err)
} else {
l.Infof("Puller (folder %q, file %q): shortcut: %v", p.folder, file.Name, err)
return
}
}
p.model.updateLocal(p.folder, file)
}
// copierRoutine reads pullerStates until the in channel closes and performs
// the relevant copy.
func (p *Puller) copierRoutine(in <-chan copyBlocksState, out chan<- *sharedPullerState) {
// copierRoutine reads copierStates until the in channel closes and performs
// the relevant copies when possible, or passes it to the puller routine.
func (p *Puller) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pullBlockState, out chan<- *sharedPullerState) {
buf := make([]byte, protocol.BlockSize)
nextFile:
@@ -549,32 +561,70 @@ nextFile:
continue nextFile
}
srcFd, err := state.sourceFile()
if err != nil {
// As above
continue nextFile
}
evictionChan := make(chan lfu.Eviction)
fdCache := lfu.New()
fdCache.UpperBound = 50
fdCache.LowerBound = 20
fdCache.EvictionChannel = evictionChan
go func() {
for item := range evictionChan {
item.Value.(*os.File).Close()
}
}()
for _, block := range state.blocks {
buf = buf[:int(block.Size)]
_, err = srcFd.ReadAt(buf, block.Offset)
if err != nil {
state.earlyClose("src read", err)
srcFd.Close()
continue nextFile
success := p.model.finder.Iterate(block.Hash, func(folder, file string, index uint32) bool {
path := filepath.Join(p.model.folderCfgs[folder].Path, file)
var fd *os.File
fdi := fdCache.Get(path)
if fdi != nil {
fd = fdi.(*os.File)
} else {
fd, err = os.Open(path)
if err != nil {
return false
}
fdCache.Set(path, fd)
}
_, err = fd.ReadAt(buf, protocol.BlockSize*int64(index))
if err != nil {
return false
}
_, err = dstFd.WriteAt(buf, block.Offset)
if err != nil {
state.earlyClose("dst write", err)
}
if file == state.file.Name {
state.copiedFromOrigin()
}
return true
})
if state.failed() != nil {
break
}
_, err = dstFd.WriteAt(buf, block.Offset)
if err != nil {
state.earlyClose("dst write", err)
srcFd.Close()
continue nextFile
if !success {
state.pullStarted()
ps := pullBlockState{
sharedPullerState: state.sharedPullerState,
block: block,
}
pullChan <- ps
} else {
state.copyDone()
}
}
srcFd.Close()
state.copyDone()
fdCache.Evict(fdCache.Len())
close(evictionChan)
out <- state.sharedPullerState
}
}
@@ -640,15 +690,13 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
// Verify the file against expected hashes
fd, err := os.Open(state.tempName)
if err != nil {
os.Remove(state.tempName)
l.Warnln("puller: final:", err)
continue
}
err = scanner.Verify(fd, protocol.BlockSize, state.file.Blocks)
fd.Close()
if err != nil {
os.Remove(state.tempName)
l.Warnln("puller: final:", state.file.Name, err)
l.Infoln("puller:", state.file.Name, err, "(file changed during pull?)")
continue
}
@@ -656,7 +704,6 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
if !p.ignorePerms {
err = os.Chmod(state.tempName, os.FileMode(state.file.Flags&0777))
if err != nil {
os.Remove(state.tempName)
l.Warnln("puller: final:", err)
continue
}
@@ -666,9 +713,16 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
t := time.Unix(state.file.Modified, 0)
err = os.Chtimes(state.tempName, t, t)
if err != nil {
os.Remove(state.tempName)
l.Warnln("puller: final:", err)
continue
if p.lenientMtimes {
// We accept the failure with a warning here and allow the sync to
// continue. We'll sync the new mtime back to the other devices later.
// If they have the same problem & setting, we might never get in
// sync.
l.Infof("Puller (folder %q, file %q): final: %v (continuing anyway as requested)", p.folder, state.file.Name, err)
} else {
l.Warnln("puller: final:", err)
continue
}
}
// If we should use versioning, let the versioner archive the old
@@ -677,7 +731,6 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
if p.versioner != nil {
err = p.versioner.Archive(state.realName)
if err != nil {
os.Remove(state.tempName)
l.Warnln("puller: final:", err)
continue
}
@@ -686,7 +739,6 @@ func (p *Puller) finisherRoutine(in <-chan *sharedPullerState) {
// Replace the original file with the new one
err = osutil.Rename(state.tempName, state.realName)
if err != nil {
os.Remove(state.tempName)
l.Warnln("puller: final:", err)
continue
}

View File

@@ -16,10 +16,13 @@
package model
import (
"os"
"path/filepath"
"testing"
"github.com/syncthing/syncthing/internal/config"
"github.com/syncthing/syncthing/internal/protocol"
"github.com/syncthing/syncthing/internal/scanner"
"github.com/syndtr/goleveldb/leveldb"
"github.com/syndtr/goleveldb/leveldb/storage"
@@ -47,7 +50,7 @@ func TestHandleFile(t *testing.T) {
// Copy: 2, 5, 8
// Pull: 1, 3, 4, 6, 7
// Create existing file, and update local index
// Create existing file
existingFile := protocol.FileInfo{
Name: "filex",
Flags: 0,
@@ -65,6 +68,7 @@ func TestHandleFile(t *testing.T) {
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
m := NewModel(config.Wrap("/tmp/test", config.Configuration{}), "device", "syncthing", "dev", db)
m.AddFolder(config.FolderConfiguration{ID: "default", Path: "testdata"})
// Update index
m.updateLocal("default", existingFile)
p := Puller{
@@ -73,34 +77,20 @@ func TestHandleFile(t *testing.T) {
model: m,
}
copyChan := make(chan copyBlocksState, 1) // Copy chan gets all blocks needed to copy in a wrapper struct
pullChan := make(chan pullBlockState, 5) // Pull chan gets blocks one by one
copyChan := make(chan copyBlocksState, 1)
p.handleFile(requiredFile, copyChan, pullChan, nil)
p.handleFile(requiredFile, copyChan, nil)
// Receive the results
toCopy := <-copyChan
toPull := []pullBlockState{<-pullChan, <-pullChan, <-pullChan, <-pullChan, <-pullChan}
select {
case <-pullChan:
t.Error("Channel not empty!")
default:
if len(toCopy.blocks) != 8 {
t.Errorf("Unexpected count of copy blocks: %d != 8", len(toCopy.blocks))
}
if len(toCopy.blocks) != 3 {
t.Errorf("Unexpected count of copy blocks: %d != 3", len(toCopy.blocks))
}
for i, eq := range []int{2, 5, 8} {
if string(toCopy.blocks[i].Hash) != string(blocks[eq].Hash) {
t.Errorf("Block mismatch: %s != %s", toCopy.blocks[i].String(), blocks[eq].String())
}
}
for i, eq := range []int{1, 3, 4, 6, 7} {
if string(toPull[i].block.Hash) != string(blocks[eq].Hash) {
t.Errorf("Block mismatch: %s != %s", toPull[i].block.String(), blocks[eq].String())
for i, block := range toCopy.blocks {
if string(block.Hash) != string(blocks[i+1].Hash) {
t.Errorf("Block mismatch: %s != %s", block.String(), blocks[i+1].String())
}
}
}
@@ -114,7 +104,7 @@ func TestHandleFileWithTemp(t *testing.T) {
// Copy: 5, 8
// Pull: 1, 6
// Create existing file, and update local index
// Create existing file
existingFile := protocol.FileInfo{
Name: "file",
Flags: 0,
@@ -132,6 +122,7 @@ func TestHandleFileWithTemp(t *testing.T) {
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
m := NewModel(config.Wrap("/tmp/test", config.Configuration{}), "device", "syncthing", "dev", db)
m.AddFolder(config.FolderConfiguration{ID: "default", Path: "testdata"})
// Update index
m.updateLocal("default", existingFile)
p := Puller{
@@ -140,34 +131,122 @@ func TestHandleFileWithTemp(t *testing.T) {
model: m,
}
copyChan := make(chan copyBlocksState, 1) // Copy chan gets all blocks needed to copy in a wrapper struct
pullChan := make(chan pullBlockState, 2) // Pull chan gets blocks one by one
copyChan := make(chan copyBlocksState, 1)
p.handleFile(requiredFile, copyChan, pullChan, nil)
p.handleFile(requiredFile, copyChan, nil)
// Receive the results
toCopy := <-copyChan
toPull := []pullBlockState{<-pullChan, <-pullChan}
select {
case <-pullChan:
t.Error("Channel not empty!")
default:
if len(toCopy.blocks) != 4 {
t.Errorf("Unexpected count of copy blocks: %d != 4", len(toCopy.blocks))
}
if len(toCopy.blocks) != 2 {
t.Errorf("Unexpected count of copy blocks: %d != 2", len(toCopy.blocks))
}
for i, eq := range []int{5, 8} {
for i, eq := range []int{1, 5, 6, 8} {
if string(toCopy.blocks[i].Hash) != string(blocks[eq].Hash) {
t.Errorf("Block mismatch: %s != %s", toCopy.blocks[i].String(), blocks[eq].String())
}
}
}
for i, eq := range []int{1, 6} {
if string(toPull[i].block.Hash) != string(blocks[eq].Hash) {
t.Errorf("Block mismatch: %s != %s", toPull[i].block.String(), blocks[eq].String())
func TestCopierFinder(t *testing.T) {
// After diff between required and existing we should:
// Copy: 1, 2, 3, 4, 6, 7, 8
// Since there is no existing file, nor a temp file
// After dropping out blocks found locally:
// Pull: 1, 5, 6, 8
tempFile := filepath.Join("testdata", defTempNamer.TempName("file2"))
err := os.Remove(tempFile)
if err != nil && !os.IsNotExist(err) {
t.Error(err)
}
// Create existing file
existingFile := protocol.FileInfo{
Name: defTempNamer.TempName("file"),
Flags: 0,
Modified: 0,
Blocks: []protocol.BlockInfo{
blocks[0], blocks[2], blocks[3], blocks[4],
blocks[0], blocks[0], blocks[7], blocks[0],
},
}
// Create target file
requiredFile := existingFile
requiredFile.Blocks = blocks[1:]
requiredFile.Name = "file2"
fcfg := config.FolderConfiguration{ID: "default", Path: "testdata"}
cfg := config.Configuration{Folders: []config.FolderConfiguration{fcfg}}
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
m := NewModel(config.Wrap("/tmp/test", cfg), "device", "syncthing", "dev", db)
m.AddFolder(fcfg)
// Update index
m.updateLocal("default", existingFile)
iterFn := func(folder, file string, index uint32) bool {
return true
}
// Verify that the blocks we say exist on file, really exist in the db.
for _, idx := range []int{2, 3, 4, 7} {
if m.finder.Iterate(blocks[idx].Hash, iterFn) == false {
t.Error("Didn't find block")
}
}
p := Puller{
folder: "default",
dir: "testdata",
model: m,
}
copyChan := make(chan copyBlocksState)
pullChan := make(chan pullBlockState, 4)
finisherChan := make(chan *sharedPullerState, 1)
// Run a single fetcher routine
go p.copierRoutine(copyChan, pullChan, finisherChan)
p.handleFile(requiredFile, copyChan, finisherChan)
pulls := []pullBlockState{<-pullChan, <-pullChan, <-pullChan, <-pullChan}
finish := <-finisherChan
select {
case <-pullChan:
t.Fatal("Finisher channel has data to be read")
case <-finisherChan:
t.Fatal("Finisher channel has data to be read")
default:
}
// Verify that the right blocks went into the pull list
for i, eq := range []int{1, 5, 6, 8} {
if string(pulls[i].block.Hash) != string(blocks[eq].Hash) {
t.Errorf("Block %d mismatch: %s != %s", eq, pulls[i].block.String(), blocks[eq].String())
}
if string(finish.file.Blocks[eq-1].Hash) != string(blocks[eq].Hash) {
t.Errorf("Block %d mismatch: %s != %s", eq, finish.file.Blocks[eq-1].String(), blocks[eq].String())
}
}
// Verify that the fetched blocks have actually been written to the temp file
blks, err := scanner.HashFile(tempFile, protocol.BlockSize)
if err != nil {
t.Log(err)
}
for _, eq := range []int{2, 3, 4, 7} {
if string(blks[eq-1].Hash) != string(blocks[eq].Hash) {
t.Errorf("Block %d mismatch: %s != %s", eq, blks[eq-1].String(), blocks[eq].String())
}
}
finish.fd.Close()
os.Remove(tempFile)
}

View File

@@ -59,6 +59,10 @@ func (s *Scanner) Serve() {
initialScanCompleted = true
}
if s.intv == 0 {
return
}
timer.Reset(s.intv)
}
}

View File

@@ -31,13 +31,16 @@ type sharedPullerState struct {
folder string
tempName string
realName string
reuse bool
reused int // Number of blocks reused from temporary file
// Mutable, must be locked for access
err error // The first error we hit
fd *os.File // The fd of the temp file
copyNeeded int // Number of copy actions we expect to happen
pullNeeded int // Number of block pulls we expect to happen
copyTotal int // Total number of copy actions for the whole job
pullTotal int // Total number of pull actions for the whole job
copyNeeded int // Number of copy actions still pending
pullNeeded int // Number of block pulls still pending
copyOrigin int // Number of blocks copied from the original file
closed bool // Set when the file has been closed
mut sync.Mutex // Protects the above
}
@@ -79,7 +82,7 @@ func (s *sharedPullerState) tempFile() (*os.File, error) {
// Attempt to create the temp file
flags := os.O_WRONLY
if !s.reuse {
if s.reused == 0 {
flags |= os.O_CREATE | os.O_EXCL
}
fd, err := os.OpenFile(s.tempName, flags, 0644)
@@ -149,7 +152,25 @@ func (s *sharedPullerState) copyDone() {
s.mut.Lock()
s.copyNeeded--
if debug {
l.Debugln("sharedPullerState", s.folder, s.file.Name, "copyNeeded ->", s.pullNeeded)
l.Debugln("sharedPullerState", s.folder, s.file.Name, "copyNeeded ->", s.copyNeeded)
}
s.mut.Unlock()
}
func (s *sharedPullerState) copiedFromOrigin() {
s.mut.Lock()
s.copyOrigin++
s.mut.Unlock()
}
func (s *sharedPullerState) pullStarted() {
s.mut.Lock()
s.copyTotal--
s.copyNeeded--
s.pullTotal++
s.pullNeeded++
if debug {
l.Debugln("sharedPullerState", s.folder, s.file.Name, "pullNeeded start ->", s.pullNeeded)
}
s.mut.Unlock()
}
@@ -158,7 +179,7 @@ func (s *sharedPullerState) pullDone() {
s.mut.Lock()
s.pullNeeded--
if debug {
l.Debugln("sharedPullerState", s.folder, s.file.Name, "pullNeeded ->", s.pullNeeded)
l.Debugln("sharedPullerState", s.folder, s.file.Name, "pullNeeded done ->", s.pullNeeded)
}
s.mut.Unlock()
}

View File

@@ -54,6 +54,10 @@ func (f FileInfo) IsInvalid() bool {
return IsInvalid(f.Flags)
}
func (f FileInfo) IsDirectory() bool {
return IsDirectory(f.Flags)
}
// Used for unmarshalling a FileInfo structure but skipping the actual block list
type FileInfoTruncated struct {
Name string // max:8192
@@ -64,6 +68,11 @@ type FileInfoTruncated struct {
NumBlocks uint32
}
func (f FileInfoTruncated) String() string {
return fmt.Sprintf("File{Name:%q, Flags:0%o, Modified:%d, Version:%d, Size:%d, NumBlocks:%d}",
f.Name, f.Flags, f.Modified, f.Version, f.Size(), f.NumBlocks)
}
// Returns a statistical guess on the size, not the exact figure
func (f FileInfoTruncated) Size() int64 {
if IsDeleted(f.Flags) || IsDirectory(f.Flags) {

View File

@@ -68,6 +68,15 @@ func Blocks(r io.Reader, blocksize int, sizehint int64) ([]protocol.BlockInfo, e
return blocks, nil
}
// Set the Offset field on each block
func PopulateOffsets(blocks []protocol.BlockInfo) {
var offset int64
for i := range blocks {
blocks[i].Offset = offset
offset += int64(blocks[i].Size)
}
}
// BlockDiff returns lists of common and missing (to transform src into tgt)
// blocks. Both block lists must have been created with the same block size.
func BlockDiff(src, tgt []protocol.BlockInfo) (have, need []protocol.BlockInfo) {
@@ -75,13 +84,6 @@ func BlockDiff(src, tgt []protocol.BlockInfo) (have, need []protocol.BlockInfo)
return nil, nil
}
// Set the Offset field on each target block
var offset int64
for i := range tgt {
tgt[i].Offset = offset
offset += int64(tgt[i].Size)
}
if len(tgt) != 0 && len(src) == 0 {
// Copy the entire file
return nil, tgt

View File

@@ -35,8 +35,8 @@ type Walker struct {
Sub string
// BlockSize controls the size of the block used when hashing.
BlockSize int
// List of patterns to ignore
Ignores ignore.Patterns
// If Matcher is not nil, it is used to identify files to ignore which were specified by the user.
Matcher *ignore.Matcher
// If TempNamer is not nil, it is used to ignore tempory files when walking.
TempNamer TempNamer
// If CurrentFiler is not nil, it is queried for the current file before rescanning.
@@ -63,7 +63,7 @@ type CurrentFiler interface {
// file system. Files are blockwise hashed.
func (w *Walker) Walk() (chan protocol.FileInfo, error) {
if debug {
l.Debugln("Walk", w.Dir, w.Sub, w.BlockSize, w.Ignores)
l.Debugln("Walk", w.Dir, w.Sub, w.BlockSize, w.Matcher)
}
err := checkDir(w.Dir)
@@ -113,7 +113,8 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
return nil
}
if sn := filepath.Base(rn); sn == ".stignore" || sn == ".stversions" || w.Ignores.Match(rn) {
if sn := filepath.Base(rn); sn == ".stignore" || sn == ".stversions" ||
sn == ".stfolder" || (w.Matcher != nil && w.Matcher.Match(rn)) {
// An ignored file
if debug {
l.Debugln("ignored:", rn)
@@ -151,7 +152,7 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
Modified: info.ModTime().Unix(),
}
if debug {
l.Debugln("dir:", f)
l.Debugln("dir:", p, f)
}
fchan <- f
return nil
@@ -175,12 +176,16 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
flags = protocol.FlagNoPermBits | 0666
}
fchan <- protocol.FileInfo{
f := protocol.FileInfo{
Name: rn,
Version: lamport.Default.Tick(0),
Flags: flags,
Modified: info.ModTime().Unix(),
}
if debug {
l.Debugln("to hash:", p, f)
}
fchan <- f
}
return nil

View File

@@ -58,7 +58,7 @@ func init() {
}
func TestWalkSub(t *testing.T) {
ignores, err := ignore.Load("testdata/.stignore")
ignores, err := ignore.Load("testdata/.stignore", false)
if err != nil {
t.Fatal(err)
}
@@ -67,7 +67,7 @@ func TestWalkSub(t *testing.T) {
Dir: "testdata",
Sub: "dir2",
BlockSize: 128 * 1024,
Ignores: ignores,
Matcher: ignores,
}
fchan, err := w.Walk()
var files []protocol.FileInfo
@@ -93,7 +93,7 @@ func TestWalkSub(t *testing.T) {
}
func TestWalk(t *testing.T) {
ignores, err := ignore.Load("testdata/.stignore")
ignores, err := ignore.Load("testdata/.stignore", false)
if err != nil {
t.Fatal(err)
}
@@ -102,7 +102,7 @@ func TestWalk(t *testing.T) {
w := Walker{
Dir: "testdata",
BlockSize: 128 * 1024,
Ignores: ignores,
Matcher: ignores,
}
fchan, err := w.Walk()

View File

@@ -1,93 +0,0 @@
<?xml version="1.0"?>
<root xmlns="urn:schemas-upnp-org:device-1-0">
<specVersion>
<major>1</major>
<minor>0</minor>
</specVersion>
<URLBase>http://192.168.1.254:8000</URLBase>
<device>
<deviceType>urn:schemas-upnp-org:device:InternetGatewayDevice:1</deviceType>
<friendlyName>Technicolor TG784n v3 (1321RAWMS)</friendlyName>
<manufacturer>Technicolor</manufacturer>
<manufacturerURL>http://www.technicolor.com
</manufacturerURL>
<modelDescription>Technicolor Internet Gateway Device</modelDescription>
<modelName>Technicolor TG</modelName>
<modelNumber>784n v3</modelNumber>
<modelURL>http://www.technicolor.com</modelURL>
<serialNumber>1321RAWMS</serialNumber>
<UDN>uuid:UPnP_Technicolor TG784n v3-1_A4-B1-E9-D8-F4-78</UDN>
<presentationURL>/</presentationURL>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:Layer3Forwarding:1</serviceType>
<serviceId>urn:upnp-org:serviceId:L3Forwarding1</serviceId>
<controlURL>/hou74cq4tw9/IGD/upnp/control/igd/layer3f</controlURL>
<eventSubURL>/hou74cq4tw9/IGD/upnp/event/igd/layer3f</eventSubURL>
<SCPDURL>/hou74cq4tw9/IGD/upnp/Layer3Forwarding.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:LANDevice:1</deviceType>
<friendlyName>LANDevice</friendlyName>
<manufacturer>Technicolor</manufacturer>
<modelName>Technicolor TG784n v3</modelName>
<serialNumber>A4-B1-E9-D8-F4-78</serialNumber>
<UDN>uuid:UPnP_Technicolor TG784n v3-1_A4-B1-E9-D8-F4-78_LD_1</UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:LANHostConfigManagement:1</serviceType>
<serviceId>urn:upnp-org:serviceId:LANHostCfg1</serviceId>
<controlURL>/hou74cq4tw9/IGD/upnp/control/igd/lanhcm_1</controlURL>
<eventSubURL></eventSubURL>
<SCPDURL>/hou74cq4tw9/IGD/upnp/LANHostConfigManagement.xml</SCPDURL>
</service>
</serviceList>
</device>
<device>
<deviceType>urn:schemas-upnp-org:device:WANDevice:1</deviceType>
<friendlyName>WANDevice</friendlyName>
<manufacturer>Technicolor</manufacturer>
<modelName>Technicolor TG784n v3</modelName>
<serialNumber>A4-B1-E9-D8-F4-78</serialNumber>
<UDN>uuid:UPnP_Technicolor TG784n v3-1_A4-B1-E9-D8-F4-78_WD_1</UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANCommonInterfaceConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANCommonIFC1</serviceId>
<controlURL>/hou74cq4tw9/IGD/upnp/control/igd/wancic_1</controlURL>
<eventSubURL>/hou74cq4tw9/IGD/upnp/event/igd/wancic_1</eventSubURL>
<SCPDURL>/hou74cq4tw9/IGD/upnp/WANCommonInterfaceConfig.xml</SCPDURL>
</service>
</serviceList>
<deviceList>
<device>
<deviceType>urn:schemas-upnp-org:device:WANConnectionDevice:1</deviceType>
<friendlyName>WANConnectionDevice</friendlyName>
<manufacturer>Technicolor</manufacturer>
<modelName>Technicolor TG784n v3</modelName>
<serialNumber>A4-B1-E9-D8-F4-78</serialNumber>
<UDN>uuid:UPnP_Technicolor TG784n v3-1_A4-B1-E9-D8-F4-78_WCD_1_1</UDN>
<serviceList>
<service>
<serviceType>urn:schemas-upnp-org:service:WANDSLLinkConfig:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANDSLLinkC1</serviceId>
<controlURL>/hou74cq4tw9/IGD/upnp/control/igd/wandsllc_1_1</controlURL>
<eventSubURL>/hou74cq4tw9/IGD/upnp/event/igd/wandsllc_1_1</eventSubURL>
<SCPDURL>/hou74cq4tw9/IGD/upnp/WANDSLLinkConfig.xml</SCPDURL>
</service>
<service>
<serviceType>urn:schemas-upnp-org:service:WANPPPConnection:1</serviceType>
<serviceId>urn:upnp-org:serviceId:WANPPPConn1</serviceId>
<controlURL>/hou74cq4tw9/IGD/upnp/control/igd/wanpppc_1_1_1</controlURL>
<eventSubURL>/hou74cq4tw9/IGD/upnp/event/igd/wanpppc_1_1_1</eventSubURL>
<SCPDURL>/hou74cq4tw9/IGD/upnp/WANPPPConnection.xml</SCPDURL>
</service>
</serviceList>
</device>
</deviceList>
</device>
</deviceList>
</device>
</root>

View File

@@ -16,7 +16,7 @@
// Adapted from https://github.com/jackpal/Taipei-Torrent/blob/dd88a8bfac6431c01d959ce3c745e74b8a911793/IGD.go
// Copyright (c) 2010 Jack Palevich (https://github.com/jackpal/Taipei-Torrent/blob/dd88a8bfac6431c01d959ce3c745e74b8a911793/LICENSE)
// Package upnp implements UPnP Internet Gateway upnpDevice port mappings
// Package upnp implements UPnP InternetGatewayDevice discovery, querying, and port mapping.
package upnp
import (
@@ -25,19 +25,29 @@ import (
"encoding/xml"
"errors"
"fmt"
"io"
"io/ioutil"
"net"
"net/http"
"net/url"
"regexp"
"strings"
"sync"
"time"
)
// A container for relevant properties of a UPnP InternetGatewayDevice.
type IGD struct {
uuid string
friendlyName string
services []IGDService
url *url.URL
localIPAddress string
}
// A container for relevant properties of a UPnP service of an IGD.
type IGDService struct {
serviceURL string
device string
ourIP string
serviceURN string
}
type Protocol string
@@ -53,180 +63,352 @@ type upnpService struct {
}
type upnpDevice struct {
DeviceType string `xml:"deviceType"`
Devices []upnpDevice `xml:"deviceList>device"`
Services []upnpService `xml:"serviceList>service"`
DeviceType string `xml:"deviceType"`
FriendlyName string `xml:"friendlyName"`
Devices []upnpDevice `xml:"deviceList>device"`
Services []upnpService `xml:"serviceList>service"`
}
type upnpRoot struct {
Device upnpDevice `xml:"device"`
}
func Discover() (*IGD, error) {
// Discover discovers UPnP InternetGatewayDevices.
// The order in which the devices appear in the result list is not deterministic.
func Discover() []*IGD {
result := make([]*IGD, 0)
l.Infoln("Starting UPnP discovery...")
timeout := 3
// Search for InternetGatewayDevice:2 devices
result = append(result, discover("urn:schemas-upnp-org:device:InternetGatewayDevice:2", timeout, result)...)
// Search for InternetGatewayDevice:1 devices
// InternetGatewayDevice:2 devices that correctly respond to the IGD:1 request as well will not be re-added to the result list
result = append(result, discover("urn:schemas-upnp-org:device:InternetGatewayDevice:1", timeout, result)...)
if len(result) > 0 && debug {
l.Debugln("UPnP discovery result:")
for _, resultDevice := range result {
l.Debugln("[" + resultDevice.uuid + "]")
for _, resultService := range resultDevice.services {
l.Debugln("* " + resultService.serviceURL)
}
}
}
suffix := "devices"
if len(result) == 1 {
suffix = "device"
}
l.Infof("UPnP discovery complete (found %d %s).", len(result), suffix)
return result
}
// Search for UPnP InternetGatewayDevices for <timeout> seconds, ignoring responses from any devices listed in knownDevices.
// The order in which the devices appear in the result list is not deterministic
func discover(deviceType string, timeout int, knownDevices []*IGD) []*IGD {
ssdp := &net.UDPAddr{IP: []byte{239, 255, 255, 250}, Port: 1900}
tpl := `M-SEARCH * HTTP/1.1
Host: 239.255.255.250:1900
St: %s
Man: "ssdp:discover"
Mx: %d
`
searchStr := fmt.Sprintf(tpl, deviceType, timeout)
search := []byte(strings.Replace(searchStr, "\n", "\r\n", -1))
if debug {
l.Debugln("Starting discovery of device type " + deviceType + "...")
}
results := make([]*IGD, 0)
resultChannel := make(chan *IGD, 8)
socket, err := net.ListenUDP("udp4", &net.UDPAddr{})
if err != nil {
return nil, err
l.Infoln(err)
return results
}
defer socket.Close()
defer socket.Close() // Make sure our socket gets closed
err = socket.SetDeadline(time.Now().Add(3 * time.Second))
err = socket.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second))
if err != nil {
return nil, err
}
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 {
return nil, err
}
resp := make([]byte, 1500)
n, _, err := socket.ReadFrom(resp)
if err != nil {
return nil, err
l.Infoln(err)
return results
}
if debug {
l.Debugln(string(resp[:n]))
l.Debugln("Sending search request for device type " + deviceType + "...")
}
reader := bufio.NewReader(bytes.NewBuffer(resp[:n]))
var resultWaitGroup sync.WaitGroup
_, err = socket.WriteTo(search, ssdp)
if err != nil {
l.Infoln(err)
return results
}
if debug {
l.Debugln("Listening for UPnP response for device type " + deviceType + "...")
}
// Listen for responses until a timeout is reached
for {
resp := make([]byte, 1500)
n, _, err := socket.ReadFrom(resp)
if err != nil {
if e, ok := err.(net.Error); !ok || !e.Timeout() {
l.Infoln(err) //legitimate error, not a timeout.
}
break
} else {
// Process results in a separate go routine so we can immediately return to listening for more responses
resultWaitGroup.Add(1)
go handleSearchResponse(deviceType, knownDevices, resp, n, resultChannel, &resultWaitGroup)
}
}
// Wait for all result handlers to finish processing, then close result channel
resultWaitGroup.Wait()
close(resultChannel)
// Collect our results from the result handlers using the result channel
for result := range resultChannel {
results = append(results, result)
}
if debug {
l.Debugln("Discovery for device type " + deviceType + " finished.")
}
return results
}
func handleSearchResponse(deviceType string, knownDevices []*IGD, resp []byte, length int, resultChannel chan<- *IGD, resultWaitGroup *sync.WaitGroup) {
defer resultWaitGroup.Done() // Signal when we've finished processing
if debug {
l.Debugln("Handling UPnP response:\n\n" + string(resp[:length]))
}
reader := bufio.NewReader(bytes.NewBuffer(resp[:length]))
request := &http.Request{}
response, err := http.ReadResponse(reader, request)
if err != nil {
return nil, err
l.Infoln(err)
return
}
if response.Header.Get("St") != "urn:schemas-upnp-org:device:InternetGatewayDevice:1" {
return nil, errors.New("no igd")
respondingDeviceType := response.Header.Get("St")
if respondingDeviceType != deviceType {
l.Infoln("Unrecognized UPnP device of type " + respondingDeviceType)
return
}
locURL := response.Header.Get("Location")
if locURL == "" {
return nil, errors.New("no location")
deviceDescriptionLocation := response.Header.Get("Location")
if deviceDescriptionLocation == "" {
l.Infoln("Invalid IGD response: no location specified.")
return
}
serviceURL, device, err := getServiceURL(locURL)
deviceDescriptionURL, err := url.Parse(deviceDescriptionLocation)
if err != nil {
return nil, err
l.Infoln("Invalid IGD location: " + err.Error())
}
// Figure out our IP number, on the network used to reach the IGD. We
// do this in a fairly roundabout way by connecting to the IGD and
deviceUSN := response.Header.Get("USN")
if deviceUSN == "" {
l.Infoln("Invalid IGD response: USN not specified.")
return
}
deviceUUID := strings.TrimLeft(strings.Split(deviceUSN, "::")[0], "uuid:")
matched, err := regexp.MatchString("[a-fA-F0-9]{8}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{4}-[a-fA-F0-9]{12}", deviceUUID)
if !matched {
l.Infoln("Invalid IGD response: invalid device UUID " + deviceUUID)
return
}
// Don't re-add devices that are already known
for _, knownDevice := range knownDevices {
if deviceUUID == knownDevice.uuid {
if debug {
l.Debugln("Ignoring known device with UUID " + deviceUUID)
}
return
}
}
response, err = http.Get(deviceDescriptionLocation)
if err != nil {
l.Infoln(err)
return
}
defer response.Body.Close()
if response.StatusCode >= 400 {
l.Infoln(errors.New(response.Status))
return
}
var upnpRoot upnpRoot
err = xml.NewDecoder(response.Body).Decode(&upnpRoot)
if err != nil {
l.Infoln(err)
return
}
services, err := getServiceDescriptions(deviceDescriptionLocation, upnpRoot.Device)
if err != nil {
l.Infoln(err)
return
}
// Figure out our IP number, on the network used to reach the IGD.
// We do this in a fairly roundabout way by connecting to the IGD and
// checking the address of the local end of the socket. I'm open to
// suggestions on a better way to do this...
ourIP, err := localIP(locURL)
localIPAddress, err := localIP(deviceDescriptionURL)
if err != nil {
return nil, err
l.Infoln(err)
return
}
igd := &IGD{
serviceURL: serviceURL,
device: device,
ourIP: ourIP,
uuid: deviceUUID,
friendlyName: upnpRoot.Device.FriendlyName,
url: deviceDescriptionURL,
services: services,
localIPAddress: localIPAddress,
}
resultChannel <- igd
if debug {
l.Debugln("Finished handling of UPnP response.")
}
return igd, nil
}
func localIP(tgt string) (string, error) {
url, err := url.Parse(tgt)
if err != nil {
return "", err
}
func localIP(url *url.URL) (string, error) {
conn, err := net.Dial("tcp", url.Host)
if err != nil {
return "", err
}
defer conn.Close()
ourIP, _, err := net.SplitHostPort(conn.LocalAddr().String())
localIPAddress, _, err := net.SplitHostPort(conn.LocalAddr().String())
if err != nil {
return "", err
}
return ourIP, nil
return localIPAddress, nil
}
func getChildDevice(d upnpDevice, deviceType string) (upnpDevice, bool) {
func getChildDevices(d upnpDevice, deviceType string) []upnpDevice {
result := make([]upnpDevice, 0)
for _, dev := range d.Devices {
if dev.DeviceType == deviceType {
return dev, true
result = append(result, dev)
}
}
return upnpDevice{}, false
return result
}
func getChildService(d upnpDevice, serviceType string) (upnpService, bool) {
func getChildServices(d upnpDevice, serviceType string) []upnpService {
result := make([]upnpService, 0)
for _, svc := range d.Services {
if svc.ServiceType == serviceType {
return svc, true
result = append(result, svc)
}
}
return upnpService{}, false
return result
}
func getServiceURL(rootURL string) (string, string, error) {
r, err := http.Get(rootURL)
if err != nil {
return "", "", err
func getServiceDescriptions(rootURL string, device upnpDevice) ([]IGDService, error) {
result := make([]IGDService, 0)
if device.DeviceType == "urn:schemas-upnp-org:device:InternetGatewayDevice:1" {
descriptions := getIGDServices(rootURL, device,
"urn:schemas-upnp-org:device:WANDevice:1",
"urn:schemas-upnp-org:device:WANConnectionDevice:1",
[]string{"urn:schemas-upnp-org:service:WANIPConnection:1", "urn:schemas-upnp-org:service:WANPPPConnection:1"})
result = append(result, descriptions...)
} else if device.DeviceType == "urn:schemas-upnp-org:device:InternetGatewayDevice:2" {
descriptions := getIGDServices(rootURL, device,
"urn:schemas-upnp-org:device:WANDevice:2",
"urn:schemas-upnp-org:device:WANConnectionDevice:2",
[]string{"urn:schemas-upnp-org:service:WANIPConnection:2", "urn:schemas-upnp-org:service:WANPPPConnection:1"})
result = append(result, descriptions...)
} else {
return result, errors.New("[" + rootURL + "] Malformed root device description: not an InternetGatewayDevice.")
}
defer r.Body.Close()
if r.StatusCode >= 400 {
return "", "", errors.New(r.Status)
if len(result) < 1 {
return result, errors.New("[" + rootURL + "] Malformed device description: no compatible service descriptions found.")
} else {
return result, nil
}
return getServiceURLReader(rootURL, r.Body)
}
func getServiceURLReader(rootURL string, r io.Reader) (string, string, error) {
var upnpRoot upnpRoot
err := xml.NewDecoder(r).Decode(&upnpRoot)
if err != nil {
return "", "", err
func getIGDServices(rootURL string, device upnpDevice, wanDeviceURN string, wanConnectionURN string, serviceURNs []string) []IGDService {
result := make([]IGDService, 0)
devices := getChildDevices(device, wanDeviceURN)
if len(devices) < 1 {
l.Infoln("[" + rootURL + "] Malformed InternetGatewayDevice description: no WANDevices specified.")
return result
}
dev := upnpRoot.Device
if dev.DeviceType != "urn:schemas-upnp-org:device:InternetGatewayDevice:1" {
return "", "", errors.New("No InternetGatewayDevice")
for _, device := range devices {
connections := getChildDevices(device, wanConnectionURN)
if len(connections) < 1 {
l.Infoln("[" + rootURL + "] Malformed " + wanDeviceURN + " description: no WANConnectionDevices specified.")
}
for _, connection := range connections {
for _, serviceURN := range serviceURNs {
services := getChildServices(connection, serviceURN)
if len(services) < 1 && debug {
l.Debugln("[" + rootURL + "] No services of type " + serviceURN + " found on connection.")
}
for _, service := range services {
if len(service.ControlURL) == 0 {
l.Infoln("[" + rootURL + "] Malformed " + service.ServiceType + " description: no control URL.")
} else {
u, _ := url.Parse(rootURL)
replaceRawPath(u, service.ControlURL)
if debug {
l.Debugln("[" + rootURL + "] Found " + service.ServiceType + " with URL " + u.String())
}
service := IGDService{serviceURL: u.String(), serviceURN: service.ServiceType}
result = append(result, service)
}
}
}
}
}
dev, ok := getChildDevice(dev, "urn:schemas-upnp-org:device:WANDevice:1")
if !ok {
return "", "", errors.New("No WANDevice")
}
dev, ok = getChildDevice(dev, "urn:schemas-upnp-org:device:WANConnectionDevice:1")
if !ok {
return "", "", errors.New("No WANConnectionDevice")
}
device := "urn:schemas-upnp-org:service:WANIPConnection:1"
svc, ok := getChildService(dev, device)
if !ok {
device = "urn:schemas-upnp-org:service:WANPPPConnection:1"
}
svc, ok = getChildService(dev, device)
if !ok {
return "", "", errors.New("No WANIPConnection nor WANPPPConnection")
}
if len(svc.ControlURL) == 0 {
return "", "", errors.New("no controlURL")
}
u, _ := url.Parse(rootURL)
replaceRawPath(u, svc.ControlURL)
return u.String(), device, nil
return result
}
func replaceRawPath(u *url.URL, rp string) {
@@ -245,17 +427,19 @@ func replaceRawPath(u *url.URL, rp string) {
u.RawQuery = q
}
func soapRequest(url, device, function, message string) error {
tpl := `<?xml version="1.0" ?>
func soapRequest(url, device, function, message string) ([]byte, error) {
tpl := ` <?xml version="1.0" ?>
<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>%s</s:Body>
</s:Envelope>
`
var resp []byte
body := fmt.Sprintf(tpl, message)
req, err := http.NewRequest("POST", url, strings.NewReader(body))
if err != nil {
return err
return resp, err
}
req.Header.Set("Content-Type", `text/xml; charset="utf-8"`)
req.Header.Set("User-Agent", "syncthing/1.0")
@@ -266,30 +450,91 @@ func soapRequest(url, device, function, message string) error {
if debug {
l.Debugln(req.Header.Get("SOAPAction"))
l.Debugln(body)
l.Debugln("SOAP Request:\n\n" + body)
}
r, err := http.DefaultClient.Do(req)
if err != nil {
return err
return resp, err
}
resp, _ = ioutil.ReadAll(r.Body)
if debug {
resp, _ := ioutil.ReadAll(r.Body)
l.Debugln(string(resp))
l.Debugln("SOAP Response:\n\n" + string(resp) + "\n")
}
r.Body.Close()
if r.StatusCode >= 400 {
return errors.New(function + ": " + r.Status)
return resp, errors.New(function + ": " + r.Status)
}
return resp, nil
}
// Add a port mapping to all relevant services on the specified InternetGatewayDevice.
// Port mapping will fail and return an error if action is fails for _any_ of the relevant services.
// For this reason, it is generally better to configure port mapping for each individual service instead.
func (n *IGD) AddPortMapping(protocol Protocol, externalPort, internalPort int, description string, timeout int) error {
for _, service := range n.services {
err := service.AddPortMapping(n.localIPAddress, protocol, externalPort, internalPort, description, timeout)
if err != nil {
return err
}
}
return nil
}
func (n *IGD) AddPortMapping(protocol Protocol, externalPort, internalPort int, description string, timeout int) error {
tpl := `<u:AddPortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
// Delete a port mapping from all relevant services on the specified InternetGatewayDevice.
// Port mapping will fail and return an error if action is fails for _any_ of the relevant services.
// For this reason, it is generally better to configure port mapping for each individual service instead.
func (n *IGD) DeletePortMapping(protocol Protocol, externalPort int) error {
for _, service := range n.services {
err := service.DeletePortMapping(protocol, externalPort)
if err != nil {
return err
}
}
return nil
}
// The InternetGatewayDevice's UUID.
func (n *IGD) UUID() string {
return n.uuid
}
// The InternetGatewayDevice's friendly name.
func (n *IGD) FriendlyName() string {
return n.friendlyName
}
// The InternetGatewayDevice's friendly identifier (friendly name + IP address).
func (n *IGD) FriendlyIdentifier() string {
return "'" + n.FriendlyName() + "' (" + strings.Split(n.URL().Host, ":")[0] + ")"
}
// The URL of the InternetGatewayDevice's root device description.
func (n *IGD) URL() *url.URL {
return n.url
}
type soapGetExternalIPAddressResponseEnvelope struct {
XMLName xml.Name
Body soapGetExternalIPAddressResponseBody `xml:"Body"`
}
type soapGetExternalIPAddressResponseBody struct {
XMLName xml.Name
GetExternalIPAddressResponse getExternalIPAddressResponse `xml:"GetExternalIPAddressResponse"`
}
type getExternalIPAddressResponse struct {
NewExternalIPAddress string `xml:"NewExternalIPAddress"`
}
// Add a port mapping to the specified IGD service.
func (s *IGDService) AddPortMapping(localIPAddress string, protocol Protocol, externalPort, internalPort int, description string, timeout int) error {
tpl := `<u:AddPortMapping xmlns:u="%s">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>%d</NewExternalPort>
<NewProtocol>%s</NewProtocol>
@@ -298,21 +543,55 @@ func (n *IGD) AddPortMapping(protocol Protocol, externalPort, internalPort int,
<NewEnabled>1</NewEnabled>
<NewPortMappingDescription>%s</NewPortMappingDescription>
<NewLeaseDuration>%d</NewLeaseDuration>
</u:AddPortMapping>
`
</u:AddPortMapping>`
body := fmt.Sprintf(tpl, s.serviceURN, externalPort, protocol, internalPort, localIPAddress, description, timeout)
body := fmt.Sprintf(tpl, externalPort, protocol, internalPort, n.ourIP, description, timeout)
return soapRequest(n.serviceURL, n.device, "AddPortMapping", body)
_, err := soapRequest(s.serviceURL, s.serviceURN, "AddPortMapping", body)
if err != nil {
return err
}
return nil
}
func (n *IGD) DeletePortMapping(protocol Protocol, externalPort int) (err error) {
tpl := `<u:DeletePortMapping xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
// Delete a port mapping from the specified IGD service.
func (s *IGDService) DeletePortMapping(protocol Protocol, externalPort int) error {
tpl := `<u:DeletePortMapping xmlns:u="%s">
<NewRemoteHost></NewRemoteHost>
<NewExternalPort>%d</NewExternalPort>
<NewProtocol>%s</NewProtocol>
</u:DeletePortMapping>
`
</u:DeletePortMapping>`
body := fmt.Sprintf(tpl, s.serviceURN, externalPort, protocol)
body := fmt.Sprintf(tpl, externalPort, protocol)
return soapRequest(n.serviceURL, n.device, "DeletePortMapping", body)
_, err := soapRequest(s.serviceURL, s.serviceURN, "DeletePortMapping", body)
if err != nil {
return err
}
return nil
}
// Query the IGD service for its external IP address.
// Returns nil if the external IP address is invalid or undefined, along with any relevant errors
func (s *IGDService) GetExternalIPAddress() (net.IP, error) {
tpl := `<u:GetExternalIPAddress xmlns:u="%s" />`
body := fmt.Sprintf(tpl, s.serviceURN)
response, err := soapRequest(s.serviceURL, s.serviceURN, "GetExternalIPAddress", body)
if err != nil {
return nil, err
}
envelope := &soapGetExternalIPAddressResponseEnvelope{}
err = xml.Unmarshal(response, envelope)
if err != nil {
return nil, err
}
result := net.ParseIP(envelope.Body.GetExternalIPAddressResponse.NewExternalIPAddress)
return result, nil
}

View File

@@ -16,17 +16,27 @@
package upnp
import (
"os"
"encoding/xml"
"testing"
)
func TestGetTechnicolorRootURL(t *testing.T) {
r, _ := os.Open("testdata/technicolor.xml")
_, action, err := getServiceURLReader("http://localhost:1234/", r)
func TestExternalIPParsing(t *testing.T) {
soap_response :=
[]byte(`<s:Envelope xmlns:s="http://schemas.xmlsoap.org/soap/envelope/" s:encodingStyle="http://schemas.xmlsoap.org/soap/encoding/">
<s:Body>
<u:GetExternalIPAddressResponse xmlns:u="urn:schemas-upnp-org:service:WANIPConnection:1">
<NewExternalIPAddress>1.2.3.4</NewExternalIPAddress>
</u:GetExternalIPAddressResponse>
</s:Body>
</s:Envelope>`)
envelope := &soapGetExternalIPAddressResponseEnvelope{}
err := xml.Unmarshal(soap_response, envelope)
if err != nil {
t.Fatal(err)
t.Error(err)
}
if action != "urn:schemas-upnp-org:service:WANPPPConnection:1" {
t.Error("Unexpected action", action)
if envelope.Body.GetExternalIPAddressResponse.NewExternalIPAddress != "1.2.3.4" {
t.Error("Parse of SOAP request failed.")
}
}

View File

@@ -324,7 +324,7 @@ func startWalker(dir string, res chan<- fileInfo, abort <-chan struct{}) {
}
rn, _ := filepath.Rel(dir, path)
if rn == "." {
if rn == "." || rn == ".stfolder" {
return nil
}

View File

@@ -1,25 +1,27 @@
<configuration version="5">
<configuration version="6">
<folder id="default" path="s1" ro="false" rescanIntervalS="10" ignorePerms="false">
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></device>
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></device>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU"></device>
<versioning></versioning>
<lenientMtimes>false</lenientMtimes>
</folder>
<folder id="s12" path="s12-1" ro="false" rescanIntervalS="10" ignorePerms="false">
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></device>
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></device>
<versioning></versioning>
<lenientMtimes>false</lenientMtimes>
</folder>
<device id="EJHMPAQ-OGCVORE-ISB4IS3-SYYVJXF-TKJGLTU-66DIQPF-GJ5D2GX-GQ3OWQK" name="s4" compression="true">
<device id="EJHMPAQ-OGCVORE-ISB4IS3-SYYVJXF-TKJGLTU-66DIQPF-GJ5D2GX-GQ3OWQK" name="s4" compression="true" introducer="false">
<address>127.0.0.1:22004</address>
</device>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="true">
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="true" introducer="false">
<address>127.0.0.1:22001</address>
</device>
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU" name="s2" compression="true">
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU" name="s2" compression="true" introducer="false">
<address>127.0.0.1:22002</address>
</device>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3" compression="true">
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3" compression="true" introducer="false">
<address>127.0.0.1:22003</address>
</device>
<gui enabled="true" tls="false">
@@ -35,7 +37,6 @@
<localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
<localAnnounceMCAddr>[ff32::5222]:21026</localAnnounceMCAddr>
<parallelRequests>16</parallelRequests>
<maxSendKbps>50000</maxSendKbps>
<maxRecvKbps>50000</maxRecvKbps>
<reconnectionIntervalS>5</reconnectionIntervalS>
@@ -45,5 +46,8 @@
<upnpRenewalMinutes>30</upnpRenewalMinutes>
<urAccepted>-1</urAccepted>
<restartOnWakeup>true</restartOnWakeup>
<autoUpgradeIntervalH>12</autoUpgradeIntervalH>
<keepTemporariesH>24</keepTemporariesH>
<cacheIgnoredFiles>true</cacheIgnoredFiles>
</options>
</configuration>

View File

@@ -1,27 +1,30 @@
<configuration version="5">
<configuration version="6">
<folder id="default" path="s2" ro="false" rescanIntervalS="15" ignorePerms="false">
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></device>
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></device>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU"></device>
<versioning></versioning>
<lenientMtimes>false</lenientMtimes>
</folder>
<folder id="s12" path="s12-2" ro="false" rescanIntervalS="15" ignorePerms="false">
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></device>
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></device>
<versioning></versioning>
<lenientMtimes>false</lenientMtimes>
</folder>
<folder id="s23" path="s23-2" ro="false" rescanIntervalS="15" ignorePerms="false">
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></device>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU"></device>
<versioning></versioning>
<lenientMtimes>false</lenientMtimes>
</folder>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="true">
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="true" introducer="false">
<address>127.0.0.1:22001</address>
</device>
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU" name="s2" compression="true">
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU" name="s2" compression="true" introducer="false">
<address>127.0.0.1:22002</address>
</device>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3" compression="true">
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3" compression="true" introducer="false">
<address>127.0.0.1:22003</address>
</device>
<gui enabled="true" tls="false">
@@ -35,7 +38,6 @@
<localAnnounceEnabled>true</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
<localAnnounceMCAddr>[ff32::5222]:21026</localAnnounceMCAddr>
<parallelRequests>16</parallelRequests>
<maxSendKbps>0</maxSendKbps>
<maxRecvKbps>0</maxRecvKbps>
<reconnectionIntervalS>5</reconnectionIntervalS>
@@ -45,5 +47,8 @@
<upnpRenewalMinutes>30</upnpRenewalMinutes>
<urAccepted>-1</urAccepted>
<restartOnWakeup>true</restartOnWakeup>
<autoUpgradeIntervalH>12</autoUpgradeIntervalH>
<keepTemporariesH>24</keepTemporariesH>
<cacheIgnoredFiles>true</cacheIgnoredFiles>
</options>
</configuration>

View File

@@ -1,8 +1,9 @@
<configuration version="5">
<configuration version="6">
<folder id="s23" path="s23-3" ro="false" rescanIntervalS="20" ignorePerms="false">
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></device>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU"></device>
<versioning></versioning>
<lenientMtimes>false</lenientMtimes>
</folder>
<folder id="default" path="s3" ro="false" rescanIntervalS="20" ignorePerms="false">
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></device>
@@ -11,14 +12,15 @@
<versioning type="simple">
<param key="keep" val="5"></param>
</versioning>
<lenientMtimes>false</lenientMtimes>
</folder>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="true">
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="true" introducer="false">
<address>127.0.0.1:22001</address>
</device>
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU" name="s2" compression="true">
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU" name="s2" compression="true" introducer="false">
<address>127.0.0.1:22002</address>
</device>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3" compression="true">
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3" compression="true" introducer="false">
<address>127.0.0.1:22003</address>
</device>
<gui enabled="true" tls="false">
@@ -32,7 +34,6 @@
<localAnnounceEnabled>false</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
<localAnnounceMCAddr>[ff32::5222]:21026</localAnnounceMCAddr>
<parallelRequests>16</parallelRequests>
<maxSendKbps>0</maxSendKbps>
<maxRecvKbps>0</maxRecvKbps>
<reconnectionIntervalS>5</reconnectionIntervalS>
@@ -42,5 +43,8 @@
<upnpRenewalMinutes>30</upnpRenewalMinutes>
<urAccepted>-1</urAccepted>
<restartOnWakeup>true</restartOnWakeup>
<autoUpgradeIntervalH>12</autoUpgradeIntervalH>
<keepTemporariesH>24</keepTemporariesH>
<cacheIgnoredFiles>true</cacheIgnoredFiles>
</options>
</configuration>

View File

@@ -1,28 +1,30 @@
<configuration version="5">
<configuration version="6">
<folder id="unique" path="s4" ro="false" rescanIntervalS="60" ignorePerms="false">
<device id="EJHMPAQ-OGCVORE-ISB4IS3-SYYVJXF-TKJGLTU-66DIQPF-GJ5D2GX-GQ3OWQK"></device>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></device>
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU"></device>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU"></device>
<versioning></versioning>
<lenientMtimes>false</lenientMtimes>
</folder>
<folder id="default" path="s4d" ro="false" rescanIntervalS="60" ignorePerms="false">
<device id="EJHMPAQ-OGCVORE-ISB4IS3-SYYVJXF-TKJGLTU-66DIQPF-GJ5D2GX-GQ3OWQK"></device>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU"></device>
<versioning></versioning>
<lenientMtimes>false</lenientMtimes>
</folder>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="true">
<device id="EJHMPAQ-OGCVORE-ISB4IS3-SYYVJXF-TKJGLTU-66DIQPF-GJ5D2GX-GQ3OWQK" name="s4" compression="true" introducer="false">
<address>dynamic</address>
</device>
<device id="I6KAH76-66SLLLB-5PFXSOA-UFJCDZC-YAOMLEK-CP2GB32-BV5RQST-3PSROAU" name="s1" compression="true" introducer="false">
<address>127.0.0.1:22001</address>
</device>
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU" name="s2" compression="true">
<device id="JMFJCXB-GZDE4BN-OCJE3VF-65GYZNU-AIVJRET-3J6HMRQ-AUQIGJO-FKNHMQU" name="s2" compression="true" introducer="false">
<address>127.0.0.1:22002</address>
</device>
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3" compression="true">
<device id="373HSRP-QLPNLIE-JYKZVQF-P4PKZ63-R2ZE6K3-YD442U2-JHBGBQG-WWXAHAU" name="s3" compression="true" introducer="false">
<address>127.0.0.1:22003</address>
</device>
<device id="EJHMPAQ-OGCVORE-ISB4IS3-SYYVJXF-TKJGLTU-66DIQPF-GJ5D2GX-GQ3OWQK" name="s4" compression="true">
<address>dynamic</address>
</device>
<gui enabled="true" tls="false">
<address>127.0.0.1:8084</address>
<apikey>abc123</apikey>
@@ -34,7 +36,6 @@
<localAnnounceEnabled>false</localAnnounceEnabled>
<localAnnouncePort>21025</localAnnouncePort>
<localAnnounceMCAddr>[ff32::5222]:21026</localAnnounceMCAddr>
<parallelRequests>16</parallelRequests>
<maxSendKbps>0</maxSendKbps>
<maxRecvKbps>0</maxRecvKbps>
<reconnectionIntervalS>10</reconnectionIntervalS>
@@ -44,5 +45,8 @@
<upnpRenewalMinutes>30</upnpRenewalMinutes>
<urAccepted>-1</urAccepted>
<restartOnWakeup>true</restartOnWakeup>
<autoUpgradeIntervalH>12</autoUpgradeIntervalH>
<keepTemporariesH>24</keepTemporariesH>
<cacheIgnoredFiles>true</cacheIgnoredFiles>
</options>
</configuration>

View File

@@ -30,7 +30,7 @@ go build json.go
start() {
echo "Starting..."
for i in 1 2 3 ; do
STTRACE=model,scanner STPROFILER=":909$i" syncthing -home "h$i" > "$i.out" 2>&1 &
STTRACE=model,scanner STPROFILER=":909$i" ../bin/syncthing -home "h$i" > "$i.out" 2>&1 &
done
}
@@ -64,7 +64,7 @@ testConvergence() {
for i in 1 2 3 12-1 12-2 23-2 23-3; do
pushd "s$i" >/dev/null
../md5r -l | sort | grep -v .stversions > ../md5-$i
../md5r -l | sort | grep -v .stversions | grep -v .stfolder > ../md5-$i
popd >/dev/null
done
@@ -112,6 +112,7 @@ alterFiles() {
set +o pipefail
find . -type f \
| grep -v timechanged \
| grep -v .stfolder \
| sort -k 1.16 \
| head -n "$todelete" \
| xargs rm -f
@@ -126,7 +127,7 @@ alterFiles() {
chmod 500 ro-test
touch "timechanged-$i"
../md5r -l | sort | grep -v .stversions > ../md5-$i
../md5r -l | sort | grep -v .stversions | grep -v .stfolder > ../md5-$i
popd >/dev/null
done
@@ -157,7 +158,7 @@ done
echo "MD5-summing..."
for i in 1 12-2 23-3 ; do
pushd "s$i" >/dev/null
../md5r -l | sort > ../md5-$i
../md5r -l | grep -v .stfolder | sort > ../md5-$i
popd >/dev/null
done

View File

@@ -26,8 +26,8 @@ go build json.go
start() {
echo "Starting..."
STTRACE=model,scanner STPROFILER=":9091" syncthing -home "f1" > 1.out 2>&1 &
STTRACE=model,scanner STPROFILER=":9092" syncthing -home "f2" > 2.out 2>&1 &
STTRACE=model,scanner STPROFILER=":9091" ../bin/syncthing -home "f1" > 1.out 2>&1 &
STTRACE=model,scanner STPROFILER=":9092" ../bin/syncthing -home "f2" > 2.out 2>&1 &
sleep 1
}

View File

@@ -30,7 +30,7 @@ go build json.go
start() {
echo "Starting..."
for i in 1 2 3 4 ; do
STTRACE=files,model,puller,versioner STPROFILER=":909$i" syncthing -home "h$i" > "$i.out" 2>&1 &
STTRACE=files,model,puller,versioner STPROFILER=":909$i" ../bin/syncthing -home "h$i" > "$i.out" 2>&1 &
done
}
@@ -43,9 +43,9 @@ stop() {
clean() {
if [[ $(uname -s) == "Linux" ]] ; then
grep -v .stversions | grep -v utf8-nfd
grep -v .stversions | grep -v .stfolder | grep -v utf8-nfd
else
grep -v .stversions
grep -v .stversions | grep -v .stfolder
fi
}