Compare commits

...

83 Commits

Author SHA1 Message Date
Jakob Borg
722b81c6f0 gui, man: Update docs & translations 2016-12-21 19:46:28 +01:00
Jakob Borg
f0efa2b974 lib/model: Allow empty subdirs in scan request (fixes #3829)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3833
2016-12-21 19:45:38 +01:00
Jakob Borg
0437f6dd66 lib/model: Don't send symlinks to old devices that can't handle them (fixes #3802) 2016-12-17 12:39:22 +01:00
Jakob Borg
11b35d650d lib/model: Accept scan requests of paths ending in slash (fixes #3804)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3805
2016-12-17 12:39:22 +01:00
Jakob Borg
b279e261a1 gui: Avoid pause between event polls (ref #3527)
We lose important events due to the frequency of ItemStarted /
ItemFinished events.
2016-12-17 12:39:22 +01:00
Jakob Borg
1ef75be1c6 gui, man: Update docs & translations 2016-12-13 11:29:40 +01:00
Jakob Borg
3582783972 lib/model, lib/osutil: Verify target directory before pulling / requesting
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3798
2016-12-13 10:24:10 +00:00
Jakob Borg
5070d52f2f lib/model: Add benchmark for model.Request() 2016-12-09 23:14:56 +01:00
Jakob Borg
7b07ed6580 lib/model, lib/protocol, lib/scanner: Include symlink target in index, pull symlinks synchronously
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3792
2016-12-09 18:02:18 +00:00
Han Boetes
f6a2b6252a gui: Tweak wording (fixes #3769)
Skip-check: authors

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3796
2016-12-09 17:16:29 +00:00
Jakob Borg
a9b03de99a gui, lib/db: Correct space accounting of symlinks, for "out of sync" status 2016-12-09 10:38:36 +01:00
Jakob Borg
a7f7058636 vendor: Update github.com/gobwas/glob (bugfix) 2016-12-07 09:25:58 +01:00
Lars K.W. Gohlke
8ce9b026e9 lib/model: Minor cleanups
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3765
2016-12-06 08:54:04 +00:00
Audrius Butkevicius
0dcf2f1bc8 cmd/strelaysrv: Use legacy dial (fixes #3753)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3784
2016-12-02 22:45:08 +00:00
Audrius Butkevicius
99922feb3b gui: Disable device removal when we know it will be reintroduced
Skip-check: pr-build-windows

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3762
2016-12-02 21:07:02 +00:00
Jakob Borg
2fd1dca905 lib/connections: Fix odd logging, forgot to call function 2016-12-02 12:56:14 +01:00
Jakob Borg
e3cf718998 lib/ignore: Add central check for internal files, used in scanning, pulling and requests
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3779
2016-12-01 14:00:11 +00:00
Stefan Kuntz
7157917a16 etc: Updated ufw firewall application preset with default GUI port
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3774
2016-12-01 12:36:15 +00:00
Jakob Borg
3266aae1c3 lib/protocol: Apply input filtering on file names
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3775
2016-12-01 12:35:32 +00:00
Jakob Borg
63194a37f6 lib/model: Double check results in filepath.Join where needed
Wherever we have untrusted relative paths, make sure they are not
escaping their folder root.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3776
2016-12-01 12:35:11 +00:00
Unrud
cabe94552a lib/model: Prevent collisions in the progressemitter registry
Using filepath.Join can cause collisions. The folder ID could be something
like ".." or "../..".

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3778
2016-12-01 12:34:20 +00:00
Jakob Borg
48a229a0cd lib/model: Temp names from all platforms should be recognized as such
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3777
LGTM: AudriusButkevicius
2016-11-30 21:23:24 +00:00
Jakob Borg
e4db86836b lib/model: Locking in the request test 2016-11-30 13:11:06 +01:00
Jakob Borg
913a85c571 lib/model: Add simple file syncing test
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3772
2016-11-30 09:32:28 +00:00
Jakob Borg
ed4f6fc4b3 lib/connections, lib/model: Connection service should expose a single interface
Makes testing easier, which we'll need

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3771
2016-11-30 07:54:20 +00:00
Jakob Borg
9da422f1c5 gui, man: Update docs & translations 2016-11-29 11:56:02 +01:00
Stefan Tatschner
ab1739ba34 cmd/syncthing: Trigger usage message on extra CLI parameters
fixes: #3690

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3763
2016-11-27 11:21:05 +00:00
Jakob Borg
fc1430aa92 lib/fs: The interface and basicfs
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3748
2016-11-24 12:07:14 +00:00
Jakob Borg
3cde608eda lib/db: Fix ineffassign lint issue 2016-11-24 12:08:44 +01:00
Jakob Borg
2898552f4b build: Setup should insteall deadcode metalinter 2016-11-24 12:05:04 +01:00
Jakob Borg
911c148c71 build: Improve setup, add metalint ineffasign 2016-11-24 11:33:43 +01:00
Jakob Borg
91568a173a lib/model: Remove ineffectual assignment in test 2016-11-24 11:33:27 +01:00
Jakob Borg
e57f5499a1 lib/sync: Remove unused struct field 2016-11-24 11:30:55 +01:00
Jakob Borg
9abb7b71a9 lib/osutil: Fix lint warning on error formatting (fixes #3760) 2016-11-24 11:20:51 +01:00
Jakob Borg
724c354d62 cmd/stdiscosrv: Fix lint warning on Context keys (fixes #3760) 2016-11-24 11:20:51 +01:00
Jakob Borg
c44779094d build: Setup should download latest version of linters etc 2016-11-24 11:20:51 +01:00
Wulf Weich
eeedab4091 gui: bottom nav always behind dropdown (fixes #3758)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3759
2016-11-23 17:03:43 +00:00
Jakob Borg
8559e20237 lib/osutil: Don't chmod in atomic file creation (fixes #2472)
Instead, trust (and test) that the temp file has appropriate permissions
from the start. The only place where this changes our behavior is for
ignores which go from 0644 to 0600. I'm OK with that.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3756
2016-11-23 14:06:08 +00:00
Jakob Borg
26730eb083 lib/model: Fix test that relies on ignore reloading 2016-11-23 14:42:29 +01:00
Simon Frei
4160ce674d model: consistently use cfg when referring to config instance and not package
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3755
2016-11-22 23:14:20 +00:00
Jakob Borg
2dbeea21c4 lib/ignore: Don't slow down tests by sleeping 2016-11-22 22:44:04 +01:00
Jakob Borg
a2b8485a89 lib/ignore: Fast reload of unchanged ignores (fixes #3394)
This changes the "seen" map that we're anyway keeping around to track
the modtimes of loaded files instead. When doing a Load() we check that
1) the file we are loading is in the modtime set, and 2) that none of
the files in the modtime set have changed modtimes. If that's the case
we do a quick return without parsing anything or clearing the cache.

This required adding two one seconds sleeps in the tests to make sure
the modtimes were updated when we expect cache reloads, because I'm on a
crappy filesystem with one second timestamp granularity. That also
proves it works...

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3754
2016-11-22 21:30:45 +00:00
Jakob Borg
5bb74ee61c gui, man: Update docs & translations 2016-11-22 09:32:57 +01:00
Jakob Borg
462fde5e7d cmd/syncthing: Make the default folder default again
The current way is quite confusing for new users - we create a default
folder, but it's not usable with the default folder created somewhere
else. Instead, when setting up for the first time with two devices, the
default folder must be removed and recreated on one of them. This comes
up on IRC and the forum now and then.

I think this matches expectactions better.

Another alternative would be to remove it entirely (not create a default
folder), but then we should also add some guidance in the UI on how to
proceed.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3751
2016-11-22 08:18:43 +00:00
Unrud
f1e83a57cd lib/osutil: Remove unnecessary fsync in Copy()
Fsyncing the file has a small performance penalty and seems unnecessary. The
file will be fsynced anyway, when the changes are commited to the database.

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3749
2016-11-22 07:59:54 +00:00
Jakob Borg
cc9a9fb390 lib/model, lib/protocol: Add Folder.Description() for logging (ref #3741) 2016-11-22 08:36:14 +01:00
Jakob Borg
8fbcceb742 authors: Add further Unrud address 2016-11-22 08:14:22 +01:00
Roman Zaynetdinov
d3a251e6d9 lib/model: Log folder IDs and labels (fixes #3724)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3741
2016-11-21 20:09:18 +01:00
Jakob Borg
be80b26c18 authors: Add zaynetro 2016-11-21 20:08:31 +01:00
Unrud
1574b7d834 lib/model: Add fsync of files and directories, option to disable (fixes #3711) 2016-11-21 18:09:51 +01:00
Jakob Borg
51e10e344d Add Unrud 2016-11-21 17:59:44 +01:00
kwhite17
0d55d8c5b0 gui: Convert URLs in warning messages to HTML links (fixes #3241)
Skip-check: metalint (annoying timeout)

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3747
2016-11-21 08:27:44 +00:00
Jakob Borg
1392589d36 authors: Add kwhite17 2016-11-21 09:12:03 +01:00
Jakob Borg
548a324256 lib/connections: Slow down failing listeners
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3745
2016-11-19 12:37:14 +00:00
Jakob Borg
a8a0bc356a lib/model: Minor cleanup to not fondle cfg.Raw things in handleDeintroductions
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3739
2016-11-17 08:56:55 +00:00
Jakob Borg
faee1d5a8d lib/model: Fix locking around introduction handling (fixes #3737)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3738
2016-11-17 08:50:24 +00:00
Jakob Borg
3088dac33b lib/model: Clean up generateClusterConfig, fix spurious test failure by sorting 2016-11-17 07:45:45 +01:00
Jakob Borg
2641062c17 gui, man: Update docs & translations 2016-11-15 07:23:48 +01:00
Jakob Borg
95c738ea28 lib/protocol: Serialize the all zeroes device ID to the empty string
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3734
2016-11-15 06:22:36 +00:00
Jakob Borg
562d2f67a6 snapcraft: Point home and config dir towards non-versioned snap home (fixes #3730) 2016-11-14 19:06:05 +01:00
Ben Schulz
ba6aff4a1b gui: Use icons and tooltips for folder size info (fixes #3710)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3731
2016-11-13 13:56:07 +00:00
Audrius Butkevicius
bb23e3940e cmd/strelaysrv: Use listen address for outgoing HTTP requests (fixes #3682) 2016-11-13 09:32:05 +01:00
Audrius Butkevicius
94e4370c7e cmd/strelaysrv: Outbox will get GCed (fixes #3718) 2016-11-13 09:32:05 +01:00
Audrius Butkevicius
38d28c3f4a lib/relay: Close invitation channel in all error cases (fixes #3726) 2016-11-13 09:32:05 +01:00
Audrius Butkevicius
f60b424d70 lib/config: Raw() -> RawCopy() 2016-11-13 09:29:35 +01:00
Audrius Butkevicius
a1a91d5ef4 lib/model: Introducer can remove stuff it introduced (fixes #1015)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3522
2016-11-13 09:29:33 +01:00
Jakob Borg
bfb48b5dde jenkins: Clean should remove old snaps 2016-11-12 10:08:13 +01:00
Jakob Borg
2860813a8e build: Set snap grade to "stable" for releases 2016-11-12 09:47:57 +01:00
Jakob Borg
72538e350d build: Snap versions should not have initial "v"
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3728
2016-11-12 08:36:19 +00:00
Jakob Borg
59f3d1445f Revert "lib/model: Introducer can remove stuff it introduced (fixes #1015)"
This reverts commit 0b88cf1d03.
2016-11-12 08:38:29 +01:00
Audrius Butkevicius
0b88cf1d03 lib/model: Introducer can remove stuff it introduced (fixes #1015)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3522
2016-11-11 15:54:25 +01:00
Audrius Butkevicius
56e2ba29d0 lib/config: Subscribers get a copy of the config
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3722
2016-11-11 14:52:23 +00:00
Jakob Borg
6ec9b84674 test: Fix test config 2016-11-09 09:02:55 +08:00
Leo Arias
afd15392b1 build: Build snaps for ARM
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3717
2016-11-09 00:52:33 +00:00
Jakob Borg
ae4cc94a9d lib/model: Fix locking order in Availability() (fixes #3634)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3714
2016-11-08 06:38:50 +00:00
Jakob Borg
3f9b75b7b3 Revert "lib/model: Introducer can remove stuff it introduced (fixes #1015)"
This reverts commit ec2b097313.
2016-11-08 14:27:32 +08:00
Audrius Butkevicius
ec2b097313 lib/model: Introducer can remove stuff it introduced (fixes #1015)
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3522
2016-11-08 00:40:48 +08:00
Audrius Butkevicius
caaab462bc lib/sync: Fix broken build 2016-11-05 02:31:52 +00:00
Audrius Butkevicius
da413b823b lib/sync: Add option for sasha-s/go-deadlock 2016-11-05 02:24:53 +00:00
Audrius Butkevicius
14937e7dd2 build: Fix proto builder on Windows 2016-11-03 22:06:51 +00:00
Audrius Butkevicius
3418497f3d lib/sync: Log everything... 2016-11-03 21:33:33 +00:00
Stefan Kuntz
e408f1061a etc: Added ufw firewall application preset
GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3703
2016-11-03 15:46:25 +00:00
佛跳墙
c08fe4e2c5 gui: Remove erroneous right parenthesis
Skip-check: authors

GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/3699
2016-11-02 13:03:57 +00:00
147 changed files with 5823 additions and 1304 deletions

View File

@@ -61,6 +61,7 @@ Karol Różycki (krozycki) <rozycki.karol@gmail.com>
Kelong Cong (kc1212) <kc04bc@gmx.com> <kc1212@users.noreply.github.com>
Ken'ichi Kamada (kamadak) <kamada@nanohz.org>
Kevin Allen (ironmig) <kma1660@gmail.com>
Kevin White, Jr. (kwhite17) <kevinwhite1710@gmail.com>
Lars K.W. Gohlke (lkwg82) <lkwg82@gmx.de>
Laurent Etiemble (letiemble) <laurent.etiemble@gmail.com> <laurent.etiemble@monobjc.net>
Leo Arias (elopio) <yo@elopio.net>
@@ -82,6 +83,7 @@ Peter Hoeg (peterhoeg) <peter@speartail.com>
Philippe Schommers (filoozoom) <philippe@schommers.be>
Phill Luby (pluby) <phill.luby@newredo.com>
Piotr Bejda (piobpl) <piotrb10@gmail.com>
Roman Zaynetdinov (zaynetro) <romanznet@gmail.com>
Ryan Sullivan (KayoticSully) <kayoticsully@gmail.com>
Scott Klupfel (kluppy) <kluppy@going2blue.com>
Sergey Mishin (ralder) <ralder@yandex.ru>
@@ -94,6 +96,7 @@ Tobias Nygren (tnn2) <tnn@nygren.pp.se>
Tomas Cerveny (kozec) <kozec@kozec.com>
Tully Robinson (tojrobinson) <tully@tojr.org>
Tyler Brazier (tylerbrazier) <tyler@tylerbrazier.com>
Unrud (Unrud) <unrud@openaliasbox.org> <Unrud@users.noreply.github.com>
Veeti Paananen (veeti) <veeti.paananen@rojekti.fi>
Victor Buinsky (buinsky) <vix_booja@tut.by>
Vil Brekin (Vilbrekin) <vilbrekin@gmail.com>

4
NICKS
View File

@@ -63,6 +63,7 @@ kozec <kozec@kozec.com>
kralo <max.schulze@online.de>
kralo <kralo@users.noreply.github.com>
krozycki <rozycki.karol@gmail.com>
kwhite17 <kevinwhite1710@gmail.com>
letiemble <laurent.etiemble@gmail.com>
letiemble <laurent.etiemble@monobjc.net>
lkwg82 <lkwg82@gmx.de>
@@ -109,6 +110,8 @@ tnn2 <tnn@nygren.pp.se>
tojrobinson <tully@tojr.org>
tpng <benny.tpng@gmail.com>
tylerbrazier <tyler@tylerbrazier.com>
Unrud <unrud@openaliasbox.org>
Unrud <Unrud@users.noreply.github.com>
uok <ueomkail@gmail.com>
uok <uok@users.noreply.github.com>
veeti <veeti.paananen@rojekti.fi>
@@ -118,5 +121,6 @@ WSGCSysadmin <e.meitner@willystreet.coop>
wweich <wweich@users.noreply.github.com>
wweich <wweich@gmx.de>
xduugu <cedric@gmx.ca>
zaynetro <romanznet@gmail.com>
Zillode <zillode@zillode.be>
zukoo <fxgsell@gmail.com>

View File

@@ -95,6 +95,7 @@ var targets = map[string]target{
{src: "etc/linux-systemd/system/syncthing@.service", dst: "deb/lib/systemd/system/syncthing@.service", perm: 0644},
{src: "etc/linux-systemd/system/syncthing-resume.service", dst: "deb/lib/systemd/system/syncthing-resume.service", perm: 0644},
{src: "etc/linux-systemd/user/syncthing.service", dst: "deb/usr/lib/systemd/user/syncthing.service", perm: 0644},
{src: "etc/firewall-ufw/syncthing", dst: "deb/etc/ufw/applications.d/syncthing", perm: 0644},
},
},
"stdiscosrv": {
@@ -297,6 +298,7 @@ func runCommand(cmd string, target target) {
ok := gometalinter("deadcode", dirs, "test/util.go")
ok = gometalinter("structcheck", dirs) && ok
ok = gometalinter("varcheck", dirs) && ok
ok = gometalinter("ineffassign", dirs) && ok
if !ok {
os.Exit(1)
}
@@ -355,14 +357,23 @@ func checkRequiredGoVersion() (float64, bool) {
}
func setup() {
runPrint("go", "get", "-v", "github.com/golang/lint/golint")
runPrint("go", "get", "-v", "golang.org/x/tools/cmd/cover")
runPrint("go", "get", "-v", "golang.org/x/net/html")
runPrint("go", "get", "-v", "github.com/FiloSottile/gvt")
runPrint("go", "get", "-v", "github.com/axw/gocov/gocov")
runPrint("go", "get", "-v", "github.com/AlekSi/gocov-xml")
runPrint("go", "get", "-v", "github.com/alecthomas/gometalinter")
runPrint("go", "get", "-v", "github.com/mitchellh/go-wordwrap")
packages := []string{
"github.com/alecthomas/gometalinter",
"github.com/AlekSi/gocov-xml",
"github.com/axw/gocov/gocov",
"github.com/FiloSottile/gvt",
"github.com/golang/lint/golint",
"github.com/gordonklaus/ineffassign",
"github.com/mitchellh/go-wordwrap",
"github.com/opennota/check/cmd/...",
"github.com/tsenart/deadcode",
"golang.org/x/net/html",
"golang.org/x/tools/cmd/cover",
}
for _, pkg := range packages {
fmt.Println(pkg)
runPrint("go", "get", "-u", pkg)
}
}
func test(pkgs ...string) {
@@ -525,6 +536,8 @@ func buildDeb(target target) {
}
func buildSnap(target target) {
os.RemoveAll("snap")
tmpl, err := template.ParseFiles("snapcraft.yaml.template")
if err != nil {
log.Fatal(err)
@@ -534,7 +547,27 @@ func buildSnap(target target) {
if err != nil {
log.Fatal(err)
}
err = tmpl.Execute(f, map[string]string{"Version": version})
snaparch := goarch
if snaparch == "armhf" {
goarch = "arm"
}
snapver := version
if strings.HasPrefix(snapver, "v") {
snapver = snapver[1:]
}
snapgrade := "devel"
if matched, _ := regexp.MatchString(`^\d+\.\d+\.\d+$`, snapver); matched {
snapgrade = "stable"
}
err = tmpl.Execute(f, map[string]string{
"Version": snapver,
"Architecture": snaparch,
"Grade": snapgrade,
})
if err != nil {
log.Fatal(err)
}
runPrint("snapcraft", "clean")
build(target, []string{"noupgrade"})
runPrint("snapcraft")

View File

@@ -62,6 +62,10 @@ func (i requestID) String() string {
return fmt.Sprintf("%016x", int64(i))
}
type contextKey int
const idKey contextKey = iota
func negCacheFor(lastSeen time.Time) int {
since := time.Since(lastSeen).Seconds()
if since >= maxDeviceAge {
@@ -132,7 +136,7 @@ var topCtx = context.Background()
func (s *querysrv) handler(w http.ResponseWriter, req *http.Request) {
reqID := requestID(rand.Int63())
ctx := context.WithValue(topCtx, "id", reqID)
ctx := context.WithValue(topCtx, idKey, reqID)
if debug {
log.Println(reqID, req.Method, req.URL)
@@ -186,7 +190,7 @@ func (s *querysrv) handler(w http.ResponseWriter, req *http.Request) {
}
func (s *querysrv) handleGET(ctx context.Context, w http.ResponseWriter, req *http.Request) {
reqID := ctx.Value("id").(requestID)
reqID := ctx.Value(idKey).(requestID)
deviceID, err := protocol.DeviceIDFromString(req.URL.Query().Get("device"))
if err != nil {
@@ -238,7 +242,7 @@ func (s *querysrv) handleGET(ctx context.Context, w http.ResponseWriter, req *ht
}
func (s *querysrv) handlePOST(ctx context.Context, remoteIP net.IP, w http.ResponseWriter, req *http.Request) {
reqID := ctx.Value("id").(requestID)
reqID := ctx.Value(idKey).(requestID)
rawCert := certificateBytes(req)
if rawCert == nil {
@@ -299,7 +303,7 @@ func (s *querysrv) Stop() {
}
func (s *querysrv) handleAnnounce(ctx context.Context, remote net.IP, deviceID protocol.DeviceID, addresses []string) (userErr, internalErr error) {
reqID := ctx.Value("id").(requestID)
reqID := ctx.Value(idKey).(requestID)
tx, err := s.db.Begin()
if err != nil {
@@ -383,7 +387,7 @@ func (s *querysrv) limit(remote net.IP) bool {
}
func (s *querysrv) updateDevice(ctx context.Context, tx *sql.Tx, device protocol.DeviceID) error {
reqID := ctx.Value("id").(requestID)
reqID := ctx.Value(idKey).(requestID)
t0 := time.Now()
res, err := tx.Stmt(s.prep["updateDevice"]).Exec(device.String())
if err != nil {

View File

@@ -172,7 +172,7 @@ func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config) {
if debug {
log.Println("Sent invitation from", id, "to", requestedPeer)
}
default:
case <-time.After(time.Second):
if debug {
log.Println("Could not send invitation from", id, "to", requestedPeer, "as peer disconnected")
}
@@ -204,7 +204,6 @@ func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config) {
if debug {
log.Printf("Closing connection %s: %s", id, err)
}
close(outbox)
// Potentially closing a second time.
conn.Close()
@@ -260,10 +259,6 @@ func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config) {
conn.Close()
case msg := <-outbox:
if msg == nil {
conn.Close()
return
}
if debug {
log.Printf("Sending message %T to %s", msg, id)
}

View File

@@ -8,6 +8,7 @@ import (
"fmt"
"log"
"net"
"net/http"
"net/url"
"os"
"os/signal"
@@ -120,6 +121,21 @@ func main() {
log.Fatal(err)
}
laddr, err := net.ResolveTCPAddr(proto, listen)
if err != nil {
log.Fatal(err)
}
if laddr.IP != nil && !laddr.IP.IsUnspecified() {
laddr.Port = 0
transport, ok := http.DefaultTransport.(*http.Transport)
if ok {
transport.Dial = (&net.Dialer{
Timeout: 30 * time.Second,
LocalAddr: laddr,
}).Dial
}
}
log.Println(LongVersion)
maxDescriptors, err := osutil.MaximizeOpenFileLimit()

View File

@@ -99,7 +99,7 @@ type modelIntf interface {
type configIntf interface {
GUI() config.GUIConfiguration
Raw() config.Configuration
RawCopy() config.Configuration
Options() config.OptionsConfiguration
Replace(cfg config.Configuration) error
Subscribe(c config.Committer)
@@ -736,7 +736,7 @@ func (s *apiService) getDBFile(w http.ResponseWriter, r *http.Request) {
}
func (s *apiService) getSystemConfig(w http.ResponseWriter, r *http.Request) {
sendJSON(w, s.cfg.Raw())
sendJSON(w, s.cfg.RawCopy())
}
func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {

View File

@@ -116,7 +116,7 @@ func saveCsrfTokens() {
// nothing relevant we can do about them anyway...
name := locations[locCsrfTokens]
f, err := osutil.CreateAtomic(name, 0600)
f, err := osutil.CreateAtomic(name)
if err != nil {
return
}

View File

@@ -507,6 +507,9 @@ func TestCSRFRequired(t *testing.T) {
cfg := new(mockedConfig)
cfg.gui.APIKey = testAPIKey
baseURL, err := startHTTP(cfg)
if err != nil {
t.Fatal("Unexpected error from getting base URL:", err)
}
cli := &http.Client{
Timeout: time.Second,

View File

@@ -47,6 +47,8 @@ import (
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/thejerf/suture"
_ "net/http/pprof" // Need to import this to support STPROFILER.
)
var (
@@ -276,6 +278,11 @@ func parseCommandLineOptions() RuntimeOptions {
flag.Usage = usageFor(flag.CommandLine, usage, longUsage)
flag.Parse()
if len(flag.Args()) > 0 {
flag.Usage()
os.Exit(2)
}
return options
}
@@ -672,7 +679,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
}
}
if cfg.Raw().OriginalVersion == 15 {
if cfg.RawCopy().OriginalVersion == 15 {
// The config version 15->16 migration is about handling ignores and
// delta indexes and requires that we drop existing indexes that
// have been incorrectly ignore filtered.
@@ -871,7 +878,7 @@ func loadOrCreateConfig() *config.Wrapper {
l.Fatalln("Config:", err)
}
if cfg.Raw().OriginalVersion != config.CurrentVersion {
if cfg.RawCopy().OriginalVersion != config.CurrentVersion {
err = archiveAndSaveConfig(cfg)
if err != nil {
l.Fatalln("Config archive:", err)
@@ -883,7 +890,7 @@ func loadOrCreateConfig() *config.Wrapper {
func archiveAndSaveConfig(cfg *config.Wrapper) error {
// Copy the existing config to an archive copy
archivePath := cfg.ConfigPath() + fmt.Sprintf(".v%d", cfg.Raw().OriginalVersion)
archivePath := cfg.ConfigPath() + fmt.Sprintf(".v%d", cfg.RawCopy().OriginalVersion)
l.Infoln("Archiving a copy of old config file format at:", archivePath)
if err := copyFile(cfg.ConfigPath(), archivePath); err != nil {
return err
@@ -953,9 +960,8 @@ func defaultConfig(myName string) config.Configuration {
if !noDefaultFolder {
l.Infoln("Default folder created and/or linked to new config")
folderID := strings.ToLower(rand.String(5) + "-" + rand.String(5))
defaultFolder = config.NewFolderConfiguration(folderID, locations[locDefFolder])
defaultFolder.Label = "Default Folder (" + folderID + ")"
defaultFolder = config.NewFolderConfiguration("default", locations[locDefFolder])
defaultFolder.Label = "Default Folder"
defaultFolder.RescanIntervalS = 60
defaultFolder.MinDiskFreePct = 1
defaultFolder.Devices = []config.FolderDeviceConfiguration{{DeviceID: myID}}

View File

@@ -23,7 +23,7 @@ func (c *mockedConfig) ListenAddresses() []string {
return nil
}
func (c *mockedConfig) Raw() config.Configuration {
func (c *mockedConfig) RawCopy() config.Configuration {
return config.Configuration{}
}

View File

@@ -45,7 +45,7 @@ func newUsageReportingManager(cfg *config.Wrapper, m *model.Model) *usageReporti
}
// Start UR if it's enabled.
mgr.CommitConfiguration(config.Configuration{}, cfg.Raw())
mgr.CommitConfiguration(config.Configuration{}, cfg.RawCopy())
// Listen to future config changes so that we can start and stop as
// appropriate.

View File

@@ -0,0 +1,31 @@
Uncomplicated FireWall application preset
===================
Installation
-----------
**Please note:** When you installed syncthing using the official deb package, you can skip the copying.
Copy the file `syncthing` to your ufw applications directory usually located at `/etc/ufw/applications.d/` (root permissions required).
In a terminal run
```
sudo ufw app update syncthing
sudo ufw app update syncthing-gui
```
to load the presets.
To allow the syncthing ports, run
```
sudo ufw allow syncthing
```
If you want to access the web gui from anywhere (not only from localhost), you can also allow the gui port.
This is step is **not** necessary for a "normal" installation!
```
sudo ufw allow syncthing-gui
```
Verification
----------
You can verify the opened ports by running
```
sudo ufw status verbose
```

View File

@@ -0,0 +1,9 @@
[syncthing]
title=Syncthing
description=Syncthing file synchronisation
ports=22000/tcp|21027/udp
[syncthing-gui]
title=Syncthing-GUI
description=Syncthing web gui
ports=8384/tcp

View File

@@ -249,6 +249,10 @@ ul.three-columns li, ul.two-columns li {
text-indent: -0.5em;
}
.navbar-fixed-bottom {
z-index: 980;
}
/** Footer nav on small devices **/
@media (max-width: 1199px) {
/* Stay at the end of the page, with space reserved for the footer

View File

@@ -52,6 +52,8 @@
"Downloaded": "Изтеглен",
"Downloading": "Изтегляне",
"Edit": "Промени",
"Edit Device": "Промяна на устройството",
"Edit Folder": "Промяна на папката",
"Editing": "Променяне",
"Enable NAT traversal": "Разреши NAT traversal",
"Enable Relaying": "Разреши препращане",
@@ -95,6 +97,7 @@
"Last Scan": "Последно сканиран",
"Last seen": "Последно видяно",
"Later": "По-късно",
"Latest Change": "Последна промяна",
"Listeners": "Синхронизиращи устройства",
"Local Discovery": "Локално откриване",
"Local State": "Локално състояние",
@@ -153,6 +156,8 @@
"Scanning": "Сканиране",
"Select the devices to share this folder with.": "Изберете устройствата, с които да споделите папката.",
"Select the folders to share with this device.": "Изберете папките за споделяне с това устройство.",
"Send & Receive": "Изпращане & получаване",
"Send Only": "Само изпращане",
"Settings": "Настройки",
"Share": "Сподели",
"Share Folder": "Сподели папка",

View File

@@ -52,6 +52,8 @@
"Downloaded": "Descarregat",
"Downloading": "Descarregant",
"Edit": "Editar",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Modificant",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
@@ -95,6 +97,7 @@
"Last Scan": "Last Scan",
"Last seen": "Vist per última vegada",
"Later": "Després",
"Latest Change": "Latest Change",
"Listeners": "Listeners",
"Local Discovery": "Descobriment Local",
"Local State": "Estat local",
@@ -153,6 +156,8 @@
"Scanning": "Escanejant",
"Select the devices to share this folder with.": "Selecciona els dispositius en els quals compartir aquesta carpeta.",
"Select the folders to share with this device.": "Selecciona la carpeta per a compartir en aquest dispositiu.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "Preferències",
"Share": "Compartir",
"Share Folder": "Compartir carpeta",

View File

@@ -52,6 +52,8 @@
"Downloaded": "Descarregat",
"Downloading": "Descarregant",
"Edit": "Editar",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Editant",
"Enable NAT traversal": "Permetre NAT transversal",
"Enable Relaying": "Permetre Transmissions",
@@ -95,6 +97,7 @@
"Last Scan": "Últim escaneig",
"Last seen": "Vist per última vegada",
"Later": "Més tard",
"Latest Change": "Latest Change",
"Listeners": "Escoltants",
"Local Discovery": "Descobriment local",
"Local State": "Estat local",
@@ -153,6 +156,8 @@
"Scanning": "Rastrejant",
"Select the devices to share this folder with.": "Selecciona els dispositius amb els que compartir aquesta carpeta.",
"Select the folders to share with this device.": "Selecciona les carpetes per a compartir amb aquest dispositiu.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "Ajustos",
"Share": "Compartir",
"Share Folder": "Compartir carpeta",

View File

@@ -52,6 +52,8 @@
"Downloaded": "Staženo",
"Downloading": "Stahuji",
"Edit": "Upravit",
"Edit Device": "Upravit zařízení",
"Edit Folder": "Upravit adresář",
"Editing": "Upravuje se",
"Enable NAT traversal": "Povolit NAT přenos",
"Enable Relaying": "Povolit přenašeče",
@@ -95,6 +97,7 @@
"Last Scan": "Poslední sken",
"Last seen": "Naposledy spatřen",
"Later": "Později",
"Latest Change": "Poslední změna",
"Listeners": "Naslouchající",
"Local Discovery": "Místní oznamování",
"Local State": "Místní status",
@@ -153,6 +156,8 @@
"Scanning": "Skenování",
"Select the devices to share this folder with.": "Vybrat přístroje, se kterými sdílet tento adresář.",
"Select the folders to share with this device.": "Vybrat adresáře sdílené s tímto přístrojem.",
"Send & Receive": "Odeslat a přijmout",
"Send Only": "Pouze odeslat",
"Settings": "Nastavení",
"Share": "Sdílet",
"Share Folder": "Sdílet adresář",

View File

@@ -52,6 +52,8 @@
"Downloaded": "Downloadet",
"Downloading": "Downloader",
"Edit": "Rediger",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Redigerer",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
@@ -95,6 +97,7 @@
"Last Scan": "Last Scan",
"Last seen": "Sidst set",
"Later": "Senere",
"Latest Change": "Latest Change",
"Listeners": "Listeners",
"Local Discovery": "Lokal opslag",
"Local State": "Lokal tilstand",
@@ -153,6 +156,8 @@
"Scanning": "Opdaterer",
"Select the devices to share this folder with.": "Vælg hvilke enheder du vil dele denne mappe med",
"Select the folders to share with this device.": "Vælg hvilke mapper du vil dele med denne enhed.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "Indstillinger",
"Share": "Del",
"Share Folder": "Delt mappe",

View File

@@ -1,14 +1,14 @@
{
"A device with that ID is already added.": "Ein Gerät mit dieser ID existiert bereits.",
"A device with that ID is already added.": "Ein Gerät mit dieser Kennung ist bereits hinzugefügt.",
"A negative number of days doesn't make sense.": "Eine negative Anzahl von Tagen ergibt keinen Sinn.",
"A new major version may not be compatible with previous versions.": "Die neue Hauptversion ist evtl. nicht mit vorherigen Versionen kompatibel.",
"API Key": "API-Schlüssel",
"About": "Über Syncthing",
"About": "Über",
"Actions": "Aktionen",
"Add": "Hinzufügen",
"Add Device": "Gerät hinzufügen",
"Add Folder": "Verzeichnis hinzufügen",
"Add Remote Device": "Remote-Gerät hinzufügen",
"Add Remote Device": "Fern-Gerät hinzufügen",
"Add new folder?": "Neues Verzeichnis hinzufügen?",
"Address": "Adresse",
"Addresses": "Adressen",
@@ -31,7 +31,7 @@
"Command": "Befehl",
"Comment, when used at the start of a line": "Kommentar, wenn am Anfang der Zeile benutzt.",
"Compression": "Komprimierung",
"Configured": "Configured",
"Configured": "Konfiguriert",
"Connection Error": "Verbindungsfehler",
"Connection Type": "Verbindungstyp",
"Copied from elsewhere": "Von anderer Quelle kopiert",
@@ -40,18 +40,20 @@
"Danger!": "Achtung!",
"Deleted": "Gelöscht",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Gerät \"{{name}}\" ({{device}} {{address}}) möchte sich verbinden. Gerät hinzufügen?",
"Device ID": "Geräte ID",
"Device Identification": "Geräte Identifikation",
"Device ID": "Gerätekennung",
"Device Identification": "Geräteidentifikation",
"Device Name": "Gerätename",
"Devices": "Geräte",
"Disconnected": "Getrennt",
"Discovered": "Discovered",
"Discovered": "Ermittelt",
"Discovery": "Gerätesuche",
"Documentation": "Dokumentation",
"Download Rate": "Download",
"Downloaded": "Heruntergeladen",
"Downloading": "Lädt herunter",
"Edit": "Bearbeiten",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Bearbeitet",
"Enable NAT traversal": "NAT-Durchdringung aktivieren",
"Enable Relaying": "Weiterleitung aktivieren",
@@ -67,7 +69,7 @@
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dateien werden, bevor Syncthing sie löscht oder ersetzt, datiert in das Verzeichnis .stversions verschoben.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dateien sind auf diesem Gerät schreibgeschützt. Auf diesem Gerät durchgeführte Veränderungen werden aber auf den Rest des Verbunds übertragen.",
"Folder": "Verzeichnis",
"Folder ID": "Verzeichnis ID",
"Folder ID": "Verzeichniskennung",
"Folder Label": "Verzeichnisbezeichnung",
"Folder Path": "Verzeichnispfad",
"Folder Type": "Verzeichnistyp",
@@ -95,6 +97,7 @@
"Last Scan": "Letzter Scan",
"Last seen": "Zuletzt online",
"Later": "Später",
"Latest Change": "Letzte Änderung",
"Listeners": "Zuhörer",
"Local Discovery": "Lokale Gerätesuche",
"Local State": "Lokaler Status",
@@ -135,11 +138,11 @@
"Quick guide to supported patterns": "Schnellanleitung zu den unterstützten Mustern",
"RAM Utilization": "RAM Auslastung",
"Random": "Zufall",
"Reduced by ignore patterns": "Reduced by ignore patterns",
"Reduced by ignore patterns": "Durch Ignoriermuster reduziert",
"Release Notes": "Veröffentlichungsnotizen",
"Remote Devices": "Remote-Geräte",
"Remote Devices": "Fern-Geräte",
"Remove": "Entfernen",
"Required identifier for the folder. Must be the same on all cluster devices.": "Erforderliche ID für das Verzeichnis. Muss auf allen Verbund-Geräten gleich sein.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Erforderlicher Bezeichner für das Verzeichnis. Muss auf allen Verbund-Geräten gleich sein.",
"Rescan": "Neu scannen",
"Rescan All": "Alle neu scannen",
"Rescan Interval": "Scanintervall",
@@ -153,6 +156,8 @@
"Scanning": "Scannen",
"Select the devices to share this folder with.": "Wähle die Geräte aus, mit denen Du dieses Verzeichnis teilen willst.",
"Select the folders to share with this device.": "Wähle die Verzeichnisse aus, die du mit diesem Gerät teilen möchtest",
"Send & Receive": "Senden & empfangen",
"Send Only": "Nur senden",
"Settings": "Einstellungen",
"Share": "Teilen",
"Share Folder": "Teile Verzeichnis",
@@ -160,9 +165,9 @@
"Share With Devices": "Teile mit diesen Geräten",
"Share this folder?": "Dieses Verzeichnis teilen?",
"Shared With": "Geteilt mit",
"Show ID": "ID anzeigen",
"Show ID": "Kennung anzeigen",
"Show QR": "Zeige QR Code",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wird anstatt der Geräte ID angezeigt. Wird als optionaler Gerätename an die anderen Clients im Cluster weitergegeben.",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wird anstatt der Gerätekennung im Verbund-Status angezeigt. Wird als optionaler Standardname an andere Geräte bekannt gegeben.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wird auf diesem Gerät als Gerätename angezeigt und an die anderen Geräte im Geräte-Verbund weitergegeben. Wenn kein Gerätename anegegeben wird, wird der Name des entfernten Gerätes genommen.",
"Shutdown": "Herunterfahren",
"Shutdown Complete": "Vollständig Heruntergefahren",
@@ -186,13 +191,13 @@
"The Syncthing admin interface is configured to allow remote access without a password.": "Die Syncthing-Oberfläche erlaubt mit den jetzigen Einstellungen einen Zugriff ohne Passwort.",
"The aggregated statistics are publicly available at the URL below.": "Die gesammelten Statistiken sind öffentlich unter der nachfolgenden URL verfügbar.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Die Konfiguration wurde gespeichert, aber noch nicht aktiviert. Syncthing muss neugestartet werden, um die neue Konfiguration zu übernehmen.",
"The device ID cannot be blank.": "Die Geräte ID darf nicht leer sein.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Die hier einzutragende Geräte ID kann im \"Aktionen > Zeige ID\"-Dialog auf dem anderen Gerät gefunden werden. Leerzeichen und Bindestriche sind optional (werden ignoriert).",
"The device ID cannot be blank.": "Die Gerätekennung darf nicht leer sein.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Die hier einzutragende Gerätekennung kann im Dialog \"Aktionen > Kennung anzeigen\" auf dem anderen Gerät gefunden werden. Leerzeichen und Bindestriche sind optional (werden ignoriert).",
"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.": "Der verschlüsselte Nutzungsbericht wird täglich gesendet. Er wird verwendet, um Statistiken über verwendete Betriebssysteme, Verzeichnis-Größen und Programm-Versionen zu erstellen. Sollte der Bericht in Zukunft weitere Daten erfassen, wird dieses Fenster erneut angezeigt.",
"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.": "Die eingegebene Geräte ID scheint nicht gültig zu sein. Es sollte eine 52 oder 56 stellige Zeichenkette aus Buchstaben und Nummern sein. Leerzeichen und Bindestriche sind optional.",
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Die eingegebene Gerätekennung scheint nicht gültig zu sein. Es sollte eine 52 oder 56 stellige Zeichenkette aus Buchstaben und Nummern sein. Leerzeichen und Bindestriche sind optional.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Der erste Kommandozeilenparameter ist der Verzeichnis-Pfad und der zweite Parameter ist der relative Pfad in diesem Verzeichnis.",
"The folder ID cannot be blank.": "Die Verzeichnis ID darf nicht leer sein.",
"The folder ID must be unique.": "Die Verzeichnis ID darf nur einmal existieren.",
"The folder ID cannot be blank.": "Die Verzeichniskennung darf nicht leer sein.",
"The folder ID must be unique.": "Die Verzeichniskennung muss eindeutig sein.",
"The folder path cannot be blank.": "Der Verzeichnispfad darf nicht leer sein.",
"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.": "Es wird in folgenden Abständen versioniert: In der ersten Stunde wird alle 30 Sekunden eine Version behalten, am ersten Tag eine jede Stunde, in den ersten 30 Tagen eine jeden Tag. Danach wird bis zum angegebenen Höchstalter eine Version pro Woche behalten.",
"The following items could not be synchronized.": "Die folgenden Objekte konnten nicht synchronisiert werden.",
@@ -227,12 +232,12 @@
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Alte Dateiversionen werden automatisch gelöscht, wenn sie älter als das angegebene Höchstalter sind oder die angegebene Höchstzahl an Dateien erreicht ist.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warnung, dieser Pfad ist ein Unterverzeichnis des existierenden Verzeichnisses \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Beachte beim Hinzufügen eines neuen Gerätes, dass dieses Gerät auch auf den anderen Geräten hinzugefügt werden muss.",
"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.": "Beachte bitte beim Hinzufügen eines neuen Verzeichnisses, dass die Verzeichnis ID dazu verwendet wird, Verzeichnisse zwischen Geräten zu verbinden. Die ID muss also auf allen Geräten gleich sein, die Groß- und Kleinschreibung muss dabei beachtet werden.",
"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.": "Beachte bitte beim Hinzufügen eines neuen Verzeichnisses, dass die Verzeichniskennung dazu verwendet wird, Verzeichnisse zwischen Geräten zu verbinden. Die Kennung muss also auf allen Geräten gleich sein, die Groß- und Kleinschreibung muss dabei beachtet werden.",
"Yes": "Ja",
"You must keep at least one version.": "Du musst mindestens eine Version behalten.",
"days": "Tage",
"directories": "directories",
"files": "files",
"directories": "Verzeichnisse",
"files": "Dateien",
"full documentation": "Komplette Dokumentation",
"items": "Objekte",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} möchte das Verzeichnis \"{{folder}}\" teilen.",

View File

@@ -36,7 +36,7 @@
"Connection Type": "Τύπος Σύνδεσης",
"Copied from elsewhere": "Έχει αντιγραφεί από κάπου αλλού",
"Copied from original": "Έχει αντιγραφεί από το πρωτότυπο",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 οι παρακάτω Συνεισφέροντες:",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 για τους παρακάτω συνεισφέροντες:",
"Danger!": "Προσοχή!",
"Deleted": "Διαγραμμένα",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Η συσκευή \"{{name}}\" ({{device}} στη διεύθυνση {{address}}) επιθυμεί να συνδεθεί. Προσθήκη της νέας συσκευής;",
@@ -52,6 +52,8 @@
"Downloaded": "Έχει ληφθεί",
"Downloading": "Λήψη",
"Edit": "Επεξεργασία",
"Edit Device": "Επεξεργασία συσκευής",
"Edit Folder": "Επεξεργασία φακέλου",
"Editing": "Επεξεργασία σε εξέλιξη",
"Enable NAT traversal": "Ενεργοποίηση διάσχισης NAT",
"Enable Relaying": "Ενεργοποίηση αναμετάδοσης",
@@ -86,7 +88,7 @@
"Ignore Patterns": "Πρότυπο για αγνόηση",
"Ignore Permissions": "Αγνόησε τα δικαιώματα",
"Incoming Rate Limit (KiB/s)": "Περιορισμός ταχύτητας λήψης (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Με μια εσφαλμένη ρύθμιση μπορεί προκαληθεί ζημιά στα περιεχόμενα του φακέλου και το Syncthing μπορεί να σταματήσει να λειτουργεί.",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Με μια εσφαλμένη ρύθμιση μπορεί να προκληθεί ζημιά στα περιεχόμενα των φακέλων και το Syncthing ενδέχεται να σταματήσει να λειτουργεί.",
"Introducer": "Βασικός κόμβος",
"Inversion of the given condition (i.e. do not exclude)": "Αντιστροφή της δοσμένης συνθήκης (π.χ. να μην εξαιρείς) ",
"Keep Versions": "Διατήρηση εκδόσεων",
@@ -95,6 +97,7 @@
"Last Scan": "Τελευταία Σάρωση",
"Last seen": "Τελευταία φορά συνδεδεμένος",
"Later": "Αργότερα",
"Latest Change": "Τελευταία αλλαγή",
"Listeners": "Ακροατές",
"Local Discovery": "Τοπική ανεύρεση",
"Local State": "Τοπική κατάσταση",
@@ -146,13 +149,15 @@
"Restart": "Επανεκκίνηση",
"Restart Needed": "Απαιτείται επανεκκίνηση",
"Restarting": "Επανεκκίνηση",
"Resume": "Συνέχιση",
"Resume": "Συνέχεια",
"Reused": "Χρησιμοποιήθηκε ξανά",
"Save": "Αποθήκευση",
"Scan Time Remaining": "Εναπομείναντας χρόνος για τον έλεγχο ",
"Scanning": "Έλεγχος για αλλαγές",
"Select the devices to share this folder with.": "Διάλεξε τις συσκευές προς τις οποίες θα διαμοιράζεται αυτός ο φάκελος.",
"Select the folders to share with this device.": "Διάλεξε ποιοι φάκελοι θα διαμοιράζονται προς αυτή τη συσκευή.",
"Send & Receive": "Αποστολή και λήψη",
"Send Only": "Μόνο αποστολή",
"Settings": "Ρυθμίσεις",
"Share": "Διαμοίραση",
"Share Folder": "Διαμοίραση φακέλου",

View File

@@ -52,6 +52,8 @@
"Downloaded": "Downloaded",
"Downloading": "Downloading",
"Edit": "Edit",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Editing",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
@@ -95,6 +97,7 @@
"Last Scan": "Last Scan",
"Last seen": "Last seen",
"Later": "Later",
"Latest Change": "Latest Change",
"Listeners": "Listeners",
"Local Discovery": "Local Discovery",
"Local State": "Local State",
@@ -153,6 +156,8 @@
"Scanning": "Scanning",
"Select the devices to share this folder with.": "Select the devices to share this folder with.",
"Select the folders to share with this device.": "Select the folders to share with this device.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "Settings",
"Share": "Share",
"Share Folder": "Share Folder",

View File

@@ -52,6 +52,8 @@
"Downloaded": "Descargado",
"Downloading": "Descargando",
"Edit": "Editar",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Editando",
"Enable NAT traversal": "Permitir NAT transversal",
"Enable Relaying": "Habilitar Retransmisión",
@@ -95,6 +97,7 @@
"Last Scan": "Último escaneo",
"Last seen": "Visto por última vez",
"Later": "Más tarde",
"Latest Change": "Latest Change",
"Listeners": "Oyentes",
"Local Discovery": "Descubrimiento local",
"Local State": "Estado local",
@@ -153,6 +156,8 @@
"Scanning": "Analizando",
"Select the devices to share this folder with.": "Selecciona los dispositivos con los que compartir esta carpeta.",
"Select the folders to share with this device.": "Selecciona las carpetas para compartir con este dispositivo.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "Ajustes",
"Share": "Compartir",
"Share Folder": "Compartir carpeta",

View File

@@ -52,6 +52,8 @@
"Downloaded": "Descargado",
"Downloading": "Descargando",
"Edit": "Editar",
"Edit Device": "Cambiando dispositivo",
"Edit Folder": "Cambiando repositorio",
"Editing": "Editando",
"Enable NAT traversal": "Habilitar NAT trasversal",
"Enable Relaying": "Habilitar Retransmisión",
@@ -95,6 +97,7 @@
"Last Scan": "Último escaneo",
"Last seen": "Visto por ultima vez",
"Later": "Más tarde",
"Latest Change": "Último cambio",
"Listeners": "Receptor",
"Local Discovery": "Búsqueda en red local",
"Local State": "Estado local",
@@ -135,7 +138,7 @@
"Quick guide to supported patterns": "Guía rápida sobre los patrones soportados",
"RAM Utilization": "Utilización de RAM",
"Random": "Aleatorio",
"Reduced by ignore patterns": "Restringido por patrones de exclusión",
"Reduced by ignore patterns": "(Restringido por patrones de exclusión)",
"Release Notes": "Notas de lanzamiento",
"Remote Devices": "Otros dispositivos",
"Remove": "Eliminar",
@@ -153,6 +156,8 @@
"Scanning": "Actualización",
"Select the devices to share this folder with.": "Seleccione los dispositivos con los cuales compartir este repositorio.",
"Select the folders to share with this device.": "Seleccione los repositorios para compartir con este dispositivo.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "Configuración",
"Share": "Compartir",
"Share Folder": "Compartir repositorio",

View File

@@ -0,0 +1,245 @@
{
"A device with that ID is already added.": "Id horrekin beste talde bat gehitu da.",
"A negative number of days doesn't make sense.": "Hemen 0 edo zenbaki positiboa ,mesedez.",
"A new major version may not be compatible with previous versions.": "Aldaketa garrantzitsuak dituen bertsio berria beharbada ez da bateragarria izango bertsio zaharragoekin.",
"API Key": "API giltza",
"About": "Egoki",
"Actions": "Egintzak",
"Add": "Gaineratu",
"Add Device": "Gaineratu makina",
"Add Folder": "Gaineratu partekatze",
"Add Remote Device": "Gaineratu makinan izan",
"Add new folder?": "Gaineratu hau partekatze ?",
"Address": "Helbide",
"Addresses": "Helbidek",
"Advanced": "Aditu",
"Advanced Configuration": "Konfigurazio aintzinatua",
"Advanced settings": "Ezarpen aurreratuak",
"All Data": "Datu guziak",
"Allow Anonymous Usage Reporting?": "Izenik gabeko erabiltze erreportak baimendu",
"Alphabetic": "Alfabetikoa",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Kanpoko kontrolagailu batek bertsioak erabiltzen ditu. Fitxeroak errepertorio sinkronizatutik desagertaraztea berari doakio.",
"Anonymous Usage Reporting": "Izenik gabeko erabiltze erreportak",
"Any devices configured on an introducer device will be added to this device as well.": "Sarrarazle batean sartua izanen edozein tresna dena huntan ere izanen da.",
"Automatic upgrades": "Aktualizatze automatikoak",
"Be careful!": "Kasu!",
"Bugs": "Akatsek",
"CPU Utilization": "Prozesadore erabiltze",
"Changelog": "Bertsio historia",
"Clean out after": "Garbi …. epearen ondotik",
"Close": "Ezeztatu",
"Command": "Kontrolagailua",
"Comment, when used at the start of a line": "Komentarioa, lerro baten hastean delarik",
"Compression": "Trinkotze",
"Configured": "Konfiguratua",
"Connection Error": "Konexio hutsa",
"Connection Type": "Konexion mota",
"Copied from elsewhere": "Beste nunbaitik kopiatua",
"Copied from original": "Kopiatua jatorrizkoatik",
"Copyright © 2014-2016 the following Contributors:": "Copyright 2014-2016, ekarle hauk:",
"Danger!": "Lanjer !",
"Deleted": "Ezeztatu",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Makina \"{{name}}\" ({{device}} izan {{address}}) konektatu nahi du. Gaineratu berri makina ?",
"Device ID": "Makina ID",
"Device Identification": "Tresnaren identifikazioa",
"Device Name": "Makina izen",
"Devices": "Makinak",
"Disconnected": "Desloturik",
"Discovered": "Ziloa",
"Discovery": "Aurkikuntza",
"Documentation": "Dokumentazio",
"Download Rate": "Deskargatze emari",
"Downloaded": "Telekargatua",
"Downloading": "Deskargatze",
"Edit": "Aldatu",
"Edit Device": "Aldaketa tresna",
"Edit Folder": "Aldaketa partekatze",
"Editing": "Aldaketa",
"Enable NAT traversal": "Ahalbidetu NAT",
"Enable Relaying": "Ahalbidetu lekua hartu",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": " (\"tcp://ip:port\" ou \"tcp://nom:port\") zuzenbideak sar, krakotx batez separatuak edo bestenaz \"dynamic\", zuzenbidearen xekatze automatikoa aktibatzeko\nTu peux traduire nom et port dans la parenthèse, c'est pas des variables du programme, juste du texte explicatif",
"Enter ignore patterns, one per line.": "Ezkluzio filtroak sar, lerro batean bakar bat",
"Error": "Huts",
"External File Versioning": "Fitxero bertsioen kanpoko kudeaketa",
"Failed Items": "Fitxategiken huts",
"File Pull Order": "Fitxategiak irekitzeko agindua",
"File Versioning": "Artxiboak babesteko metodoa",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Aldaketa bilaketetan fitxero baimenen bitak ez dira kontuan hartuko. Fitxero FAT sistimetan erabilia.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": ".stbersioak azpi karpetan lekutuko dira fitxeroak, Syncthing-ek aldatu edo ezeztatuko dituelarik. Beren helbide errelatiboak hor berean berriz sortuak izanen dira, behar balin bada",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": ".stbersioak azpi karpetan lekutuko eta ordu-markatuko dira fitxeroak, Syncthing-ek aldatu edo ezeztatuko dituelarik. Beren helbide errelatiboak hor berean berriz sortuak izanen dira, behar balin bada",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Beste tresnetan eginak izanen diren aldaketetatik zainduak izanen dira fitxeroak; haatik, tresna huntan egindako aldaketak besteeri hedatuak izanen dira",
"Folder": "Partekatze",
"Folder ID": "Partekatze ID",
"Folder Label": "Partekatze izengoiti",
"Folder Path": "Partekatze bidexka",
"Folder Type": "Partekatze mota",
"Folders": "Partekatzek",
"GUI": "Interfaze grafiko",
"GUI Authentication Password": "Interfaze grafiko pasahitz",
"GUI Authentication User": "Interfaze grafiko erabiltzaile",
"GUI Listen Addresses": "Interfaze grafiko helbide",
"Generate": "Sortu",
"Global Discovery": "Aurkikuntza oso",
"Global Discovery Servers": "Aurkikuntza oso zerbitzarik",
"Global State": "Oso egoera",
"Help": "Aiuta",
"Home page": "Errezibitze",
"Ignore": "Baztertu",
"Ignore Patterns": "Bazterketak arauk",
"Ignore Permissions": "Baztertu baimenek",
"Incoming Rate Limit (KiB/s)": "Deskargatze emari gehieneko (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Langer !!!!",
"Introducer": "Abiarazle",
"Inversion of the given condition (i.e. do not exclude)": "Emana izan den baldintza alderantziz eman (i.e ez baztertu)",
"Keep Versions": "Gorde bertsioak",
"Largest First": "Handienak lehenik",
"Last File Received": " Fitxategi azken eskuratu",
"Last Scan": "Azterketa azken",
"Last seen": "Azken agerraldia",
"Later": "Berantago",
"Latest Change": "Asken aldaketa",
"Listeners": "Entzungailuak",
"Local Discovery": "Lekuko aurkikuntza",
"Local State": "Lekuko egoera",
"Local State (Total)": "Lekuko egoera (Oso)",
"Major Upgrade": "Nagusi eguneratu",
"Master": "Nagusi",
"Maximum Age": "Goren adin",
"Metadata Only": "Metadatuak bakarrik",
"Minimum Free Disk Space": "Diskoan leku libre gutxieneko",
"Move to top of queue": "Igurikatze zerrenda bururat lekuz alda",
"Multi level wildcard (matches multiple directory levels)": "Hein askorentzako jokerra (errepertorio eta azpi errepertorioeri dagokiona)",
"Never": "Sekulan",
"New Device": "Berria makina",
"New Folder": "Berri partekatze",
"Newest First": "Gehien gertatu berri lehen",
"No": "Ez",
"No File Versioning": "Ez babestu",
"Normal": "Normal",
"Notice": "Jakinaraztea",
"OK": "Ados",
"Off": "Desgaitu",
"Oldest First": "Gehien zahar lehen",
"Optional descriptive label for the folder. Can be different on each device.": "Partikatzearen izen hautuzkoa eta atsegina, zure gisa. Tresna bakotxean desberdina izaiten ahal da.",
"Options": "Hautuk",
"Out of Sync": "Ez sinkronizatua",
"Out of Sync Items": "Ez sinkronizatu elementuak",
"Outgoing Rate Limit (KiB/s)": "Bidaltze emari gehieneko (KiB/s)",
"Override Changes": "Aldaketak desegin",
"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": "Bertako tresnaren karpetari buruzko bidea. Ez balin bada, asmatu beharko da bat. Vous pouvez entrer un chemin absolu (p.ex \"/home/moi/Sync/Exemple\") ou relatif à celui du programme (p.ex \"..\\Partages\\Exemple\" - utile pour installation portable). Tildea (~, edo ~+Espazioa Windows XP+Azerty-n) erabil litzateke bide motz gisa",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Kopiak kontserbatzeko bidea (hutsa utzezazu, .stversioen karpetaren bide ohituan).",
"Pause": "Pausa",
"Paused": "Geldirik",
"Please consult the release notes before performing a major upgrade.": "Aktualizatze garrantzitsu bat egin baino lehen, bertsioaren oharrak begira itzazu.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Konfigurazio leihoan asma itzazu erabiltzale izen bat eta sartzeko giltza bat",
"Please wait": "Oraino pazientzia pixka bat har ezazu",
"Preview": "Aurrebista",
"Preview Usage Report": "Erabiltze aurrebista",
"Quick guide to supported patterns": "Filtro ez onartuen txostena",
"RAM Utilization": "RAM erabiltze",
"Random": "Aleatorio",
"Reduced by ignore patterns": "(Mugatu baztertzek patroikez)",
"Release Notes": "Bertsioen notak",
"Remote Devices": "Besteak makinak",
"Remove": "Kendu",
"Required identifier for the folder. Must be the same on all cluster devices.": "Partikatzearen erabilzaile izena. Diren tresna guzietan berdin berdina izan behar du",
"Rescan": "Berreskanea",
"Rescan All": "Berreskanea guzia",
"Rescan Interval": "Arte berreskanea",
"Restart": "Berriz abiatu",
"Restart Needed": "Berriz piztea beharrezkoa",
"Restarting": "Berriz piztea martxan",
"Resume": "Berriz hasi",
"Reused": "Berriz erabilia",
"Save": "Begiratu",
"Scan Time Remaining": "Gelditzen den denbora azterketa",
"Scanning": "Etengabeko azterketa",
"Select the devices to share this folder with.": "Honekin sinkronizatua:",
"Select the folders to share with this device.": "Tresna hunek erabiltzen dituen banaketak hauta",
"Send & Receive": "Igorri & Artu",
"Send Only": "Bakarrik igorri",
"Settings": "Egokitzek",
"Share": "Banatu",
"Share Folder": "Banatu",
"Share Folders With Device": "Partekatu makinakekin",
"Share With Devices": "Partekatuekin makinak",
"Share this folder?": "Banatze hau onartzen duzu?",
"Shared With": "Partekatuekin",
"Show ID": "Erakutsi ene ID",
"Show QR": "Erakutsi QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Tresnaren ID-aren ordez erakutsia, taldearen egoeran. Beste tresneri erakutsia izanen da, izen erabilgarria bezala",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Tresnaren ID-aren ordez erakutsia, taldearen egoeran. Hutsa utzia balin bada, urrun den tresnak proposatu izenarekin aktualizatua izanen da",
"Shutdown": "Utzi",
"Shutdown Complete": "Gelditua!",
"Simple File Versioning": "Bertsioen segitze sinplifikatuak",
"Single level wildcard (matches within a directory only)": "Hein bakar bateko jokerra (karpetaren barnean bakarrik dagokiona)",
"Smallest First": "Tipienak lehenik",
"Source Code": "Iturri kode",
"Staggered File Versioning": "Bertsio eskaleratuak",
"Start Browser": "Web nabigatzailea pitz",
"Statistics": "Statistikak",
"Stopped": "Gelditua!",
"Support": "Foroa",
"Sync Protocol Listen Addresses": "Sinkronizatu protokoloaren entzun zuzenbideak",
"Syncing": "Sinkronizazio joaira",
"Syncthing has been shut down.": "Syncthing gelditua izan da",
"Syncthing includes the following software or portions thereof:": "Syncthing-ek programa hauk integratzen ditu (edo programa hauetatik datozten elementuak):",
"Syncthing is restarting.": "Syncthing berriz pizten ari",
"Syncthing is upgrading.": "Syncthing aktualizatzen ari da",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Iduri luke Syncthing gelditua dela, edo bestenaz arrazo bat bada interneten konekzioarekin. Berriz entsea zaitez…",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Iduri luke Syncthing arazo bat duela zure eskaeraren tratatzeko. Otoi, horria freska ezazu edo bestenaz Syncthing berriz pitz arazoak segitzen badu.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administrazio interfazea pentsatua da urrundikako irisbideak sekretu hitz gabe onartzeko !!!",
"The aggregated statistics are publicly available at the URL below.": "Estadistikak zuzen bide honetan publikoki ikusgarri dira",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurazioa grabatua izan da bainan ez aktibatua. Syncthing berriz piztu behar da konfigurazio berriaren berriz aktibatzeko.",
"The device ID cannot be blank.": "Tresnaren ID-a ez da hutsa izaiten ahal.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Tresnaren ID-a atxemaiten ahal da \"Ekintza> Tresna urrunduaren \"ID-a erakuts\" menuan. Espazio eta gioiak ez dira beharrezkoak.",
"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.": "Erabileraren zifratu txostena egun guziz igorria da. Erabili diren plataformak, banaketeen neurriak eta aplikazioaren bertsioen zerendatzeko balio du.",
"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.": "Sartu den tresnaren ID-ak iduri du ez duela balio. 52 edo 56-ko ezaugarriko kadena baten itxura behar luke, hizkiak, zifrak eta baita ere tarte edo gioiez egina",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Agingailu lerroaren lehen parametroa banatua den karpetaren bidea da, eta bigarrena karpetan den errelatibo bidea",
"The folder ID cannot be blank.": "banatzearen ID-a ez da hutsa izaiten ahal",
"The folder ID must be unique.": "Banatzearen ID-ak bakarra izan behar du",
"The folder path cannot be blank.": "Karpetari buruzko bidea ez da hutsa izaiten ahal",
"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.": "Hunako tarteak erabiliak dira: oren bateko denboraldian bertsio bat kontserbatua da 30 segundu guziz. Egun batekoan,bertsio bat egunero. Handik harat, adinaren mugetan egonez, bertsio bat astero.",
"The following items could not be synchronized.": "Ondoko fitxero hauk ez dira sinkronizatuak ahal izan",
"The maximum age must be a number and cannot be blank.": "Gehieneko adinak zenbaki bat behar du izan eta ez da hutsa izaiten ahal.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Bertsio baten kontserbatzeko epe haundiena (egunez behar du izan. Jar ezazu zerotan bertsioak betirako atxikitzeko)",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Diskoaren ehuneko espazioa hutsak zenbaki positibo bat behar du izan, 0 eta 100-en artekoa (100 barne)",
"The number of days must be a number and cannot be blank.": "Egunen kopuruak numerikoa izan behar du eta ez da hutsa izaiten ahal",
"The number of days to keep files in the trash can. Zero means forever.": "Zikin ontziko elgar hizketen kontserbatzeko egun kopurua. Beti 0 erran nahi du.",
"The number of old versions to keep, per file.": "Atxikitzeko diren lehenagoko bertsio kopurua,fitxero bakotxarentzat",
"The number of versions must be a number and cannot be blank.": "Bertsio kopuruak numerikoa behar du izan eta ez da hutsa izaiten ahal",
"The path cannot be blank.": "Bidea ez da hutsa izaiten ahal",
"The rate limit must be a non-negative number (0: no limit)": "Ixuriaren neurria ez da negatiboa izaiten ahal (0 = mugarik gabekoa)",
"The rescan interval must be a non-negative number of seconds.": "Ikerketaren tartea ez da segundu kopuru negatiboa izaiten ahal",
"They are retried automatically and will be synced when the error is resolved.": "Akatsa zuzendua izanen delarik, automatikoki berriz entseatuak et sinkronizatuak izanen dira",
"This Device": "Makina hau",
"This can easily give hackers access to read and change any files on your computer.": "Hunek errexki irakurtzen eta aldatzen uzten ahal du zure ordenagailuko edozein fitxero, nahiz eta sartu denak ez haizu!",
"This is a major version upgrade.": "Aktualizatze garrantzitsu bat da",
"Trash Can File Versioning": "Zakarrontzia",
"Unknown": "Ez ezaguna",
"Unshared": "Partekatu ez den",
"Unused": "Ez baliatua",
"Up to Date": "Egun",
"Updated": "Berritu",
"Upgrade": "Aktualizatu",
"Upgrade To {%version%}": "Egunetaratzea {{version}}-i buruz",
"Upgrading": "Syncthing-en egunetaratzea",
"Upload Rate": "Bidaltze emari",
"Uptime": "Denbora ibiltze",
"Use HTTPS for GUI": "HTTPS-a erabil GUI-arentzat",
"Version": "Bertsio",
"Versions Path": "Bertsioen egon tokia",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Bertsioak automatikoki ezeztatuak izanen dira, kontserbatzeko iraupen denbora pasatua badute edo bitartean onartua den kopurua gainditua balin bada",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Kasu emazu, \"{{otherFolder}}\" banaketa azpi-karpetaren bidea da. Arazoak emaiten ahal ditu, fitxero batzuen ezeztatze edo duplikatzeak batez ere.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Tresna bat gehitzen duzularik, gogoan atxik ezazu zurea bestaldean gehitu behar dela ere",
"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.": "Tresna bat gehitzen delarik, gogoan atxik ezazu bere IDa erabilia dela errepertorioak lotzeko tresnen bitartez. ID-a hautskorra da eta banatze huntan parte hartzen duten tresna guzietan berdina izanen da",
"Yes": "Bai",
"You must keep at least one version.": "Bertsio bat bederen behar duzu atxiki",
"days": "Egunak",
"directories": "karpetak",
"files": "fitxategik",
"full documentation": "Dokumentazio osoa",
"items": "Elementuak",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} banaketa \"{{folder}}\" gomitatzen zaitu.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} \"{{folderlabel}}\" ({{folder}}) gomitatzen zaitu."
}

View File

@@ -52,6 +52,8 @@
"Downloaded": "Ladattu",
"Downloading": "Ladataan",
"Edit": "Muokkaa",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Muokkaus",
"Enable NAT traversal": "Aktivoi osoitteenmuunnoksen kierto",
"Enable Relaying": "Aktivoi yhteyden välitys",
@@ -95,6 +97,7 @@
"Last Scan": "Viimeisin skannaus",
"Last seen": "Nähty viimeksi",
"Later": "Myöhemmin",
"Latest Change": "Latest Change",
"Listeners": "Kuuntelijat",
"Local Discovery": "Paikallinen etsintä",
"Local State": "Paikallinen tila",
@@ -153,6 +156,8 @@
"Scanning": "Skannataan",
"Select the devices to share this folder with.": "Valitse laitteet, joiden kanssa tämä kansio jaetaan.",
"Select the folders to share with this device.": "Valitse kansiot jaettavaksi tämän laitteen kanssa.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "Asetukset",
"Share": "Jaa",
"Share Folder": "Jaa kansio",

View File

@@ -9,7 +9,7 @@
"Add Device": "Ajouter l'appareil",
"Add Folder": "Ajouter un partage",
"Add Remote Device": "Ajouter un appareil",
"Add new folder?": "Ajouter un nouveau partage ?",
"Add new folder?": "Ajouter ce partage ?",
"Address": "Adresse",
"Addresses": "Adresses",
"Advanced": "Avancé",
@@ -50,8 +50,10 @@
"Documentation": "Documentation",
"Download Rate": "Débit de réception",
"Downloaded": "Téléchargé",
"Downloading": "En cours de téléchargement",
"Downloading": "Téléchargement",
"Edit": "Modifier",
"Edit Device": "Modifier l'appareil",
"Edit Folder": "Modifier le partage",
"Editing": "Modifications",
"Enable NAT traversal": "Activer transfert d'adresses NAT",
"Enable Relaying": "Activer le relayage",
@@ -91,10 +93,11 @@
"Inversion of the given condition (i.e. do not exclude)": "Inverser la condition donnée (i.e. ne pas exclure)",
"Keep Versions": "Combien de versions conserver",
"Largest First": "Les plus volumineux d'abord",
"Last File Received": "Dernière mise à jour",
"Last File Received": "Dernier changement",
"Last Scan": "Dernière analyse",
"Last seen": "Dernière apparition",
"Later": "Plus tard",
"Latest Change": "Dernier changement",
"Listeners": "Systèmes à l'écoute",
"Local Discovery": "Découverte locale",
"Local State": "État local",
@@ -123,7 +126,7 @@
"Out of Sync Items": "Éléments non synchronisés",
"Outgoing Rate Limit (KiB/s)": "Limite du débit sortant (KiB/s)",
"Override Changes": "Écraser les changements",
"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": "Chemin vers le répertoire dans l'appareil local. Il sera créé s'il n'existe pas. Le caractère tilde (~, ou ~+Espace sous Windows+Azerty) peut être utilisé comme raccourci vers",
"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": "Chemin vers le répertoire dans l'appareil local. Il sera créé s'il n'existe pas. Vous pouvez entrer un chemin absolu (p.ex \"/home/moi/Sync/Exemple\") ou relatif à celui du programme (p.ex \"..\\Partages\\Exemple\" - utile pour installation portable). Le caractère tilde (~, ou ~+Espace sous Windows XP+Azerty) peut être utilisé comme raccourci vers",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Chemin où les versions doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire)",
"Pause": "Pause",
"Paused": "En pause",
@@ -135,7 +138,7 @@
"Quick guide to supported patterns": "Guide rapide des masques supportés",
"RAM Utilization": "Utilisation de la RAM",
"Random": "Aléatoire",
"Reduced by ignore patterns": "Restreint par les masques d'exclusion",
"Reduced by ignore patterns": "(Limité par des masques d'exclusion)",
"Release Notes": "Notes de version",
"Remote Devices": "Autres appareils",
"Remove": "Enlever",
@@ -153,6 +156,8 @@
"Scanning": "Analyse en cours",
"Select the devices to share this folder with.": "Synchroniser avec :",
"Select the folders to share with this device.": "Sélectionner les partages auxquels participe cet appareil.",
"Send & Receive": "Envoi & réception",
"Send Only": "Envoi seulement",
"Settings": "Configuration",
"Share": "Partager",
"Share Folder": "Partager",
@@ -171,7 +176,7 @@
"Smallest First": "Les plus petits d'abord",
"Source Code": "Code source",
"Staggered File Versioning": "Versions échelonnées",
"Start Browser": "Démarrer le navigateur web",
"Start Browser": "Lancer le navigateur web",
"Statistics": "Statistiques",
"Stopped": "Arrêté",
"Support": "Forum",
@@ -235,6 +240,6 @@
"files": "fichiers",
"full documentation": "documentation complète",
"items": "éléments",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vous invite au partage \"{{folderLabel}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vous invite au partage \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -9,7 +9,7 @@
"Add Device": "Ajouter l'appareil",
"Add Folder": "Ajouter un partage",
"Add Remote Device": "Ajouter un appareil",
"Add new folder?": "Ajouter un nouveau partage ?",
"Add new folder?": "Ajouter ce partage ?",
"Address": "Adresse",
"Addresses": "Adresses",
"Advanced": "Avancé",
@@ -50,8 +50,10 @@
"Documentation": "Documentation",
"Download Rate": "Vitesse de réception",
"Downloaded": "Téléchargé",
"Downloading": "En cours de téléchargement",
"Downloading": "Téléchargement",
"Edit": "Modifier",
"Edit Device": "Modifier l'appareil",
"Edit Folder": "Modifier le partage",
"Editing": "Modifications",
"Enable NAT traversal": "Activer la translation d'adresses (NAT)",
"Enable Relaying": "Relayage possible",
@@ -91,10 +93,11 @@
"Inversion of the given condition (i.e. do not exclude)": "Inverser la condition donnée (i.e. ne pas exclure)",
"Keep Versions": "Combien de versions conserver",
"Largest First": "Les plus volumineux d'abord",
"Last File Received": "Dernière mise à jour",
"Last File Received": "Dernier changement",
"Last Scan": "Dernière analyse",
"Last seen": "Dernière apparition",
"Later": "Plus tard",
"Latest Change": "Dernier changement",
"Listeners": "Systèmes en écoute",
"Local Discovery": "Découverte locale",
"Local State": "État local",
@@ -123,7 +126,7 @@
"Out of Sync Items": "Éléments non synchronisés",
"Outgoing Rate Limit (KiB/s)": "Limite du débit d'émission (Ko/s)",
"Override Changes": "Écraser les changements",
"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": "Chemin vers le répertoire dans l'appareil local. Il sera créé s'il n'existe pas. Le caractère tilde (~, ou ~+Espace sous Windows+Azerty) peut être utilisé comme raccourci vers",
"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": "Chemin vers le répertoire dans l'appareil local. Il sera créé s'il n'existe pas. Vous pouvez entrer un chemin absolu (p.ex \"/home/moi/Sync/Exemple\") ou relatif à celui du programme (p.ex \"..\\Partages\\Exemple\" - utile pour installation portable). Le caractère tilde (~, ou ~+Espace sous Windows XP+Azerty) peut être utilisé comme raccourci vers",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Chemin où les copies doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire)",
"Pause": "Pause",
"Paused": "En pause",
@@ -132,10 +135,10 @@
"Please wait": "Merci de patienter",
"Preview": "Aperçu",
"Preview Usage Report": "Aperçu du rapport de statistiques d'utilisation",
"Quick guide to supported patterns": "Guide rapide des masques supportés",
"Quick guide to supported patterns": "Filtro ez onartuen txostena",
"RAM Utilization": "Utilisation de la RAM",
"Random": "Aléatoire",
"Reduced by ignore patterns": "Restreint par les masques d'exclusion",
"Reduced by ignore patterns": "(Limité par des masques d'exclusion)",
"Release Notes": "Notes de version",
"Remote Devices": "Autres appareils",
"Remove": "Enlever",
@@ -153,6 +156,8 @@
"Scanning": "Analyse en cours",
"Select the devices to share this folder with.": "Synchroniser avec :",
"Select the folders to share with this device.": "Sélectionner les partages auxquels participe cet appareil.",
"Send & Receive": "Envoyer & recevoir",
"Send Only": "Envoyer seulement",
"Settings": "Configuration",
"Share": "Partager",
"Share Folder": "Partager",
@@ -171,7 +176,7 @@
"Smallest First": "Les plus petits d'abord",
"Source Code": "Code source",
"Staggered File Versioning": "Versions échelonnées",
"Start Browser": "Démarrer le navigateur web",
"Start Browser": "Lancer le navigateur web",
"Statistics": "Statistiques",
"Stopped": "Arrêté",
"Support": "Forum",
@@ -235,6 +240,6 @@
"files": "Fichiers",
"full documentation": "documentation complète",
"items": "éléments",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vous invite au partage \"{{folderLabel}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vous invite au partage \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderlabel}}\" ({{folder}})."
}

View File

@@ -31,7 +31,7 @@
"Command": "Kommando",
"Comment, when used at the start of a line": "Kommentaar, wannear as brûkt by it begjin fan in rige",
"Compression": "Kompresje",
"Configured": "Configured",
"Configured": "Konfigureart",
"Connection Error": "Ferbiningsflater",
"Connection Type": "Ferbiningstype",
"Copied from elsewhere": "Oernommen fan earne oars",
@@ -45,13 +45,15 @@
"Device Name": "Apparaatnamme",
"Devices": "Apparaten",
"Disconnected": "Ferbining ferbrutsen",
"Discovered": "Discovered",
"Discovered": "Untdekt",
"Discovery": "Untdekking",
"Documentation": "Dokumintaasje",
"Download Rate": "Ynlaadfluggens",
"Downloaded": "Ynladen",
"Downloading": "Oan it ynladen",
"Edit": "Bewurkje",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Bewurkjen",
"Enable NAT traversal": "NAT-trochkruse ynskeakelje",
"Enable Relaying": "Trochjaan tastean",
@@ -95,6 +97,7 @@
"Last Scan": "Lêst Skent",
"Last seen": "Lêst sjoen",
"Later": "Letter",
"Latest Change": "Latest Change",
"Listeners": "Harkers",
"Local Discovery": "Lokale ûntdekking",
"Local State": "Lokale tastân",
@@ -135,7 +138,7 @@
"Quick guide to supported patterns": "Fluch-paadwizer foar stipe patroanen",
"RAM Utilization": "RAM-brûken",
"Random": "Willekeurich",
"Reduced by ignore patterns": "Reduced by ignore patterns",
"Reduced by ignore patterns": "Ferlytse troch negear-patroanen",
"Release Notes": "Utjeftenotysjes",
"Remote Devices": "Apparaten op Ofstân",
"Remove": "Fuortsmite",
@@ -153,6 +156,8 @@
"Scanning": "Oan it skennen",
"Select the devices to share this folder with.": "Sykje de apparaten út om dizze map mei te dielen.",
"Select the folders to share with this device.": "Sykje de mappen út om mei dit apparaat te dielen.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "Ynstellings",
"Share": "Diele",
"Share Folder": "Map diele",
@@ -231,8 +236,8 @@
"Yes": "Ja",
"You must keep at least one version.": "Jo moatte minstens ien ferzje bewarje.",
"days": "dagen",
"directories": "directories",
"files": "files",
"directories": "triemtafels",
"files": "triemmen",
"full documentation": "komplete dokumintaasje",
"items": "items",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wol map \"{{folder}}\" diele.",

View File

@@ -52,6 +52,8 @@
"Downloaded": "Letöltve",
"Downloading": "Letöltés",
"Edit": "Szerkesztés",
"Edit Device": "Eszköz szerkesztése",
"Edit Folder": "Mappa szerkesztése",
"Editing": "Szerkesztés",
"Enable NAT traversal": "NAT bejárás engedélyezése",
"Enable Relaying": "Közvetítés engedélyezése",
@@ -95,6 +97,7 @@
"Last Scan": "Utolsó vizsgálat",
"Last seen": "Utoljára látva",
"Later": "Később",
"Latest Change": "Utolsó módosítás",
"Listeners": "Kapcsolatok",
"Local Discovery": "Helyi felfedezés",
"Local State": "Helyi állapot",
@@ -153,6 +156,8 @@
"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",
"Select the folders to share with this device.": "Válaszd ki a mappákat, amiket meg szeretnél osztani ezzel az eszközzel.",
"Send & Receive": "Küldés és fogadás",
"Send Only": "Csak küldés",
"Settings": "Beállítások",
"Share": "Megosztás",
"Share Folder": "Mappa megosztása",

View File

@@ -52,6 +52,8 @@
"Downloaded": "Terunduh",
"Downloading": "Mengunduh",
"Edit": "Sunting",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Menyunting",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Aktifkan Relay",
@@ -95,6 +97,7 @@
"Last Scan": "Last Scan",
"Last seen": "Last seen",
"Later": "Later",
"Latest Change": "Latest Change",
"Listeners": "Listeners",
"Local Discovery": "Local Discovery",
"Local State": "Local State",
@@ -153,6 +156,8 @@
"Scanning": "Scanning",
"Select the devices to share this folder with.": "Select the devices to share this folder with.",
"Select the folders to share with this device.": "Select the folders to share with this device.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "Settings",
"Share": "Share",
"Share Folder": "Share Folder",

View File

@@ -52,6 +52,8 @@
"Downloaded": "Scaricato",
"Downloading": "Scaricamento in corso",
"Edit": "Modifica",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Modifica di",
"Enable NAT traversal": "Abilita NAT traversal",
"Enable Relaying": "Abilita Reindirizzamento",
@@ -92,10 +94,11 @@
"Keep Versions": "Versioni Mantenute",
"Largest First": "Prima il più grande",
"Last File Received": "Ultimo File Ricevuto",
"Last Scan": "Ultima scansione",
"Last Scan": "Ultima Scansione",
"Last seen": "Ultima connessione",
"Later": "Più Tardi",
"Listeners": "In ascolto",
"Latest Change": "Ultima Modifica",
"Listeners": "In Ascolto",
"Local Discovery": "Individuazione Locale",
"Local State": "Stato Locale",
"Local State (Total)": "Stato Locale (Totale)",
@@ -153,6 +156,8 @@
"Scanning": "Scansione in corso",
"Select the devices to share this folder with.": "Seleziona i dispositivi con i quali condividere questa cartella.",
"Select the folders to share with this device.": "Seleziona le cartelle da condividere con questo dispositivo.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "Impostazioni",
"Share": "Condividi",
"Share Folder": "Condividi la Cartella",
@@ -220,7 +225,7 @@
"Upgrade To {%version%}": "Aggiorna alla {{version}}",
"Upgrading": "Aggiornamento",
"Upload Rate": "Velocità Upload",
"Uptime": "Tempo di funzionamento",
"Uptime": "Tempo di Funzionamento",
"Use HTTPS for GUI": "Utilizza HTTPS per l'interfaccia grafica",
"Version": "Versione",
"Versions Path": "Percorso Cartella Versioni",

View File

@@ -31,7 +31,7 @@
"Command": "コマンド",
"Comment, when used at the start of a line": "行頭で使用された場合、コメント行",
"Compression": "圧縮",
"Configured": "Configured",
"Configured": "設定値",
"Connection Error": "接続エラー",
"Connection Type": "接続種別",
"Copied from elsewhere": "別ファイルからコピー済",
@@ -45,13 +45,15 @@
"Device Name": "デバイス名",
"Devices": "デバイス",
"Disconnected": "切断中",
"Discovered": "Discovered",
"Discovered": "探索結果",
"Discovery": "探索サーバー",
"Documentation": "マニュアル",
"Download Rate": "ダウンロード速度",
"Downloaded": "ダウンロード済",
"Downloading": "ダウンロード中",
"Edit": "編集",
"Edit Device": "デバイスの編集",
"Edit Folder": "フォルダーの編集",
"Editing": "編集中",
"Enable NAT traversal": "NATトラバーサルを有効にする",
"Enable Relaying": "中継サーバー経由の通信を有効にする",
@@ -95,6 +97,7 @@
"Last Scan": "最終スキャン時刻",
"Last seen": "最終接続日時",
"Later": "後で設定",
"Latest Change": "最終更新内容",
"Listeners": "待ち受けポート",
"Local Discovery": "LAN内で探索",
"Local State": "ローカル状態",
@@ -135,7 +138,7 @@
"Quick guide to supported patterns": "サポートされているパターンの簡易ガイド",
"RAM Utilization": "メモリ使用量",
"Random": "ランダム",
"Reduced by ignore patterns": "Reduced by ignore patterns",
"Reduced by ignore patterns": "無視パターン該当分を除く",
"Release Notes": "リリースノート",
"Remote Devices": "他のデバイス",
"Remove": "除去",
@@ -153,6 +156,8 @@
"Scanning": "スキャン中",
"Select the devices to share this folder with.": "このフォルダーを共有するデバイスを選択してください。",
"Select the folders to share with this device.": "このデバイスと共有するフォルダーを選択してください。",
"Send & Receive": "送受信",
"Send Only": "送信のみ",
"Settings": "設定",
"Share": "共有",
"Share Folder": "フォルダーを共有する",
@@ -231,8 +236,8 @@
"Yes": "はい",
"You must keep at least one version.": "少なくとも一つのバージョンを保存してください。",
"days": "日",
"directories": "directories",
"files": "files",
"directories": "ディレクトリ",
"files": "ファイル",
"full documentation": "詳細なマニュアル",
"items": "項目",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} がフォルダー \"{{folder}}\" を共有するよう求めています。",

View File

@@ -31,7 +31,7 @@
"Command": "커맨드",
"Comment, when used at the start of a line": "명령행에서 시작을 할수 있어요.",
"Compression": "압축",
"Configured": "Configured",
"Configured": "설정됨",
"Connection Error": "연결 에러",
"Connection Type": "연결 종류",
"Copied from elsewhere": "다른 곳에서 복사됨",
@@ -45,13 +45,15 @@
"Device Name": "기기 이름",
"Devices": "기기",
"Disconnected": "연결 끊김",
"Discovered": "Discovered",
"Discovered": "탐색됨",
"Discovery": "탐색",
"Documentation": "문서",
"Download Rate": "다운로드 속도",
"Downloaded": "다운로드됨",
"Downloading": "다운로드 중",
"Edit": "편집",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "편집",
"Enable NAT traversal": "NAT traversal 활성화",
"Enable Relaying": "Relaying 활성화",
@@ -95,6 +97,7 @@
"Last Scan": "마지막 탐색",
"Last seen": "마지막 접속",
"Later": "나중에",
"Latest Change": "최신 변경",
"Listeners": "수신자",
"Local Discovery": "로컬 노드 검색",
"Local State": "로컬 상태",
@@ -153,6 +156,8 @@
"Scanning": "탐색중",
"Select the devices to share this folder with.": "이 폴더를 공유할 장치를 선택합니다.",
"Select the folders to share with this device.": "이 장치와 공유할 폴더를 선택합니다.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "설정",
"Share": "공유",
"Share Folder": "폴더 공유",
@@ -231,8 +236,8 @@
"Yes": "예",
"You must keep at least one version.": "최소 한 개의 버전은 유지해야 합니다.",
"days": "일",
"directories": "directories",
"files": "files",
"directories": "디렉토리",
"files": "파일",
"full documentation": "전체 문서",
"items": "항목",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 에서 폴더 \\\"{{folder}}\\\" 를 공유하길 원합니다.",

View File

@@ -31,7 +31,7 @@
"Command": "Komanda",
"Comment, when used at the start of a line": "Komentaras naudojamas naujoje eilutėje",
"Compression": "Kompresija",
"Configured": "Configured",
"Configured": "Sukonfigūruotas",
"Connection Error": "Susijungimo klaida",
"Connection Type": "Ryšio tipas",
"Copied from elsewhere": "Nukopijuota iš kitur",
@@ -45,13 +45,15 @@
"Device Name": "Įrenginio pavadinimas",
"Devices": "Įrenginiai",
"Disconnected": "Atsijungęs",
"Discovered": "Discovered",
"Discovered": "Atrastas",
"Discovery": "Lokacija",
"Documentation": "Aprašymas",
"Download Rate": "Parsisiuntimo greitis",
"Downloaded": "Parsisiųstas",
"Downloading": "Siunčiama",
"Edit": "Redaguoti",
"Edit Device": "Redaguoti įrenginį",
"Edit Folder": "Redaguoti aplanką",
"Editing": "Redagavimas",
"Enable NAT traversal": "Leisti kirsti NAT",
"Enable Relaying": "Įjungti retransliavimą",
@@ -82,7 +84,7 @@
"Global State": "Visuotinė būsena",
"Help": "Pagalba",
"Home page": "Pagrindinis puslapis",
"Ignore": "Ignoruoti",
"Ignore": "Nepaisyti",
"Ignore Patterns": "Nepaisyti šablonų",
"Ignore Permissions": "Nepaisyti failų prieigos leidimų",
"Incoming Rate Limit (KiB/s)": "Įeinančio srauto maksimalus greitis (KiB/s)",
@@ -95,6 +97,7 @@
"Last Scan": "Paskutinis nuskaitymas",
"Last seen": "Paskutinį kartą matytas",
"Later": "Vėliau",
"Latest Change": "Paskutinis pakeitimas",
"Listeners": "Klausytojai",
"Local Discovery": "Vietinis matomumas",
"Local State": "Vietinė būsena",
@@ -135,7 +138,7 @@
"Quick guide to supported patterns": "Trumpas leistinų šablonų vadovas",
"RAM Utilization": "Atminties naudojimas",
"Random": "Atsitiktinė",
"Reduced by ignore patterns": "Reduced by ignore patterns",
"Reduced by ignore patterns": "Sumažinta pagal nepaisomus šablonus",
"Release Notes": "Laidos Informacija",
"Remote Devices": "Nuotoliniai įrenginiai",
"Remove": "Pašalinti",
@@ -153,6 +156,8 @@
"Scanning": "Skenuojama",
"Select the devices to share this folder with.": "Pasirinkite įrenginius, su kuriais dalinsitės šį aplanką.",
"Select the folders to share with this device.": "Pasirinkite aplankus kuriais norite dalintis su šiuo įrenginiu.",
"Send & Receive": "Siųsti ir gauti",
"Send Only": "Tik siųsti",
"Settings": "Nustatymai",
"Share": "Dalintis",
"Share Folder": "Dalintis aplanku",
@@ -231,8 +236,8 @@
"Yes": "Taip",
"You must keep at least one version.": "Būtina saugoti bent vieną versiją.",
"days": "dienos",
"directories": "directories",
"files": "files",
"directories": "papkės",
"files": "failai",
"full documentation": "pilna dokumentacija",
"items": "įrašai",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} nori dalintis aplanku \"{{folder}}\"",

View File

@@ -6,20 +6,20 @@
"About": "Om",
"Actions": "Handlinger",
"Add": "Legg til",
"Add Device": "Legg til Enhet",
"Add Folder": "Legg til Mappe",
"Add Device": "Legg til enhet",
"Add Folder": "Legg til mappe",
"Add Remote Device": "Legg til ekstern enhet",
"Add new folder?": "Legg til ny mappe?",
"Address": "Adresse",
"Addresses": "Adresser",
"Advanced": "Avansert",
"Advanced Configuration": "Avanserte Innstillinger",
"Advanced Configuration": "Avanserte innstillinger",
"Advanced settings": "Avanserte innstillinger ",
"All Data": "Alle data",
"Allow Anonymous Usage Reporting?": "Tillat Anonym Innsamling Av Brukerdata?",
"Allow Anonymous Usage Reporting?": "Tillat anonym innsamling av brukerdata?",
"Alphabetic": "Alfabetisk",
"An external command handles the versioning. It has to remove the file from the synced folder.": "En ekstern kommando håndterer versjonkontrollen. Den må fjerne filen fra den synkroniserte katalogen.",
"Anonymous Usage Reporting": "Anonym Innsamling Av Brukerdata",
"Anonymous Usage Reporting": "Anonym innsamling av brukerdata",
"Any devices configured on an introducer device will be added to this device as well.": "Enheter konfigurert på en introduksjonsenhet vil også bli lagt til denne enheten.",
"Automatic upgrades": "Automatiske oppdateringer",
"Be careful!": "Vær forsiktig!",
@@ -31,7 +31,7 @@
"Command": "Kommando",
"Comment, when used at the start of a line": "Kommentar, når det blir brukt i starten av en linje.",
"Compression": "Komprimering",
"Configured": "Configured",
"Configured": "Konfigurert",
"Connection Error": "Tilkoblingsfeil",
"Connection Type": "Tilkoblingstype",
"Copied from elsewhere": "Kopiert fra et annet sted",
@@ -42,23 +42,25 @@
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} på {{address}}) ønsker å koble til. Legge til ny enhet?",
"Device ID": "Enhets ID",
"Device Identification": "Enhetskjennemerke",
"Device Name": "Navn på Enhet",
"Device Name": "Navn på enhet",
"Devices": "Enheter",
"Disconnected": "Frakoblet",
"Discovered": "Discovered",
"Discovered": "Oppdaget",
"Discovery": "Oppslag",
"Documentation": "Dokumentasjon",
"Download Rate": "Nedlastingsrate",
"Downloaded": "Lastet ned",
"Downloading": "Laster ned",
"Edit": "Rediger",
"Edit Device": "Rediger enhet",
"Edit Folder": "Rediger katalog",
"Editing": "Redigerer",
"Enable NAT traversal": "Slå på NAT traversering",
"Enable Relaying": "Aktiver relésending",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Skriv inn kommaseparerte (\"tcp://ip:port\", \"tcp://host:port\") adresser, eller ordet \"dynamic\" for å gjøre automatisk oppslag for adressen.",
"Enter ignore patterns, one per line.": "Skriv inn mønster som skal utelates, ett per linje.",
"Error": "Feilmelding",
"External File Versioning": "Ekstern Versjonskontroll",
"External File Versioning": "Ekstern versjonskontroll",
"Failed Items": "Elementsynkronisering som har feilet",
"File Pull Order": "Filenes Henterekkefølge",
"File Versioning": "Versjonskontroll",
@@ -79,39 +81,40 @@
"Generate": "Generer",
"Global Discovery": "Globalt oppslag",
"Global Discovery Servers": "Globale oppslagstjenere",
"Global State": "Global Tilstand",
"Global State": "Global tilstand",
"Help": "Hjelp",
"Home page": "Hjemmeside",
"Ignore": "Ignorer",
"Ignore Patterns": "Utelatelsesmønster",
"Ignore Permissions": "Ignorer Tilgangsbit",
"Incoming Rate Limit (KiB/s)": "Innkommende Hastighetsbegrensning (KiB/s)",
"Incoming Rate Limit (KiB/s)": "Innkommende hastighetsbegrensning (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Feilaktige innstillinger kan skade innholdet i dine delte kataloger og hindre Syncthing i å fungere.",
"Introducer": "Introduktør",
"Inversion of the given condition (i.e. do not exclude)": "Invers av den gitte tilstanden (t.d. ikke ekskluder)",
"Keep Versions": "Behold Versjoner",
"Keep Versions": "Behold versjoner",
"Largest First": "Største fil",
"Last File Received": "Siste mottatte fil",
"Last Scan": "Siste gjennomsøking",
"Last seen": "Sist sett",
"Later": "Senere",
"Latest Change": "Sist endret",
"Listeners": "Lyttere",
"Local Discovery": "Lokalt oppslag",
"Local State": "Lokal Tilstand",
"Local State (Total)": "Lokal Tilstand (Total)",
"Major Upgrade": "Hovedoppgradering",
"Master": "Hoved",
"Maximum Age": "Maksimal Levetid",
"Local State": "Lokal tilstand",
"Local State (Total)": "Lokal tilstand (Total)",
"Major Upgrade": "Storoppgradering",
"Master": "Styrende",
"Maximum Age": "Maksimal levetid",
"Metadata Only": "Kun metadata",
"Minimum Free Disk Space": "Nødvendig ledig diskplass",
"Move to top of queue": "Flytt fremst i køen",
"Multi level wildcard (matches multiple directory levels)": "Multinivåsøk (søker på flere mappenivå)",
"Never": "Aldri",
"New Device": "Ny Enhet",
"New Folder": "Ny Mappe",
"New Device": "Ny enhet",
"New Folder": "Ny mappe",
"Newest First": "Den nyeste først",
"No": "Nei",
"No File Versioning": "Ingen Versjonskontroll",
"No File Versioning": "Ingen versjonskontroll",
"Normal": "Normal",
"Notice": "Merknader",
"OK": "OK",
@@ -120,22 +123,22 @@
"Optional descriptive label for the folder. Can be different on each device.": "Valgfri merkelapp på katalogen. Denne kan være ulik på forskjellige enheter",
"Options": "Valg",
"Out of Sync": "Ikke synkronisert",
"Out of Sync Items": "Ikke Synkroniserte Element",
"Outgoing Rate Limit (KiB/s)": "Utgående Hastighetsbegrensning (KiB/s)",
"Override Changes": "Overstyr Endringer",
"Out of Sync Items": "Ikke synkroniserte element",
"Outgoing Rate Limit (KiB/s)": "Utgående hastighetsbegrensning (KiB/s)",
"Override Changes": "Overstyr endringer",
"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": "Plasseringen av mappen på datamaskinen. Denne vil bli opprettet dersom den ikke finnes. Krøllstrektegnet (~) kan brukes som forkortelse for",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Plasseringen for lagrede versjoner (la denne være tom for å bruke standard .stversions-mappen i mappen).",
"Pause": "Oppholde",
"Paused": "Oppholdt",
"Please consult the release notes before performing a major upgrade.": "Se \"release notes\" før en hovedoppgradering utføres.",
"Please consult the release notes before performing a major upgrade.": "Se \"release notes\" før en storoppgradering utføres.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Vennligst angi bruker og passord for GUI-autentisering i innstillingsvinduet.",
"Please wait": "Vennligst vent",
"Preview": "Forhåndsvisning",
"Preview Usage Report": "Forhåndsvisning Av Datainnsamling",
"Preview Usage Report": "Forhåndsvisning av datainnsamling",
"Quick guide to supported patterns": "Kjapp innføring i godkjente mønster",
"RAM Utilization": "RAM-utnyttelse",
"Random": "Tilfeldig",
"Reduced by ignore patterns": "Reduced by ignore patterns",
"Reduced by ignore patterns": "Reduser med utelatelsesmønster",
"Release Notes": "Utgivelsesnotat",
"Remote Devices": "Andre enheter",
"Remove": "Fjern",
@@ -144,8 +147,8 @@
"Rescan All": "Gjennomsøk alt på nytt",
"Rescan Interval": "Intervall for gjennomsøking",
"Restart": "Omstart",
"Restart Needed": "Omstart Kreves",
"Restarting": "Starter På Ny",
"Restart Needed": "Omstart kreves",
"Restarting": "Starter på nytt",
"Resume": "Gjenoppta",
"Reused": "Gjenbrukt",
"Save": "Lagre",
@@ -153,29 +156,31 @@
"Scanning": "Gjennomsøker",
"Select the devices to share this folder with.": "Velg enhetene du vil dele denne mappen med.",
"Select the folders to share with this device.": "Velg hvilke mapper som skal deles med denne enheten.",
"Send & Receive": "Sende og motta",
"Send Only": "Bare sende",
"Settings": "Innstillinger",
"Share": "Del",
"Share Folder": "Del Mappe",
"Share Folders With Device": "Del Mapper Med Enhet",
"Share With Devices": "Del Med Enheter",
"Share Folder": "Del mappe",
"Share Folders With Device": "Del mapper med enhet",
"Share With Devices": "Del med enheter",
"Share this folder?": "Dele denne mappen?",
"Shared With": "Delt med",
"Show ID": "Vis ID",
"Show QR": "Vis QR kode",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vis i stedet for Eining ID i gruppestatus. Vil bli kringkastet til andre enheter som et valgfritt standardnavn.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vist i stedet for Mappe-ID i gruppestatus. Vil bli oppdatert til navnet enheten kringkaster dersom tomt.",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Vis i stedet for enhets ID i gruppestatus. Vil bli kringkastet til andre enheter som et valgfritt standardnavn.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Vist i stedet for mappe ID i gruppestatus. Vil bli oppdatert til navnet enheten kringkaster dersom tomt.",
"Shutdown": "Avslutt",
"Shutdown Complete": "Avslutning fullført",
"Simple File Versioning": "Enkel Versjonskontroll",
"Simple File Versioning": "Enkel versjonskontroll",
"Single level wildcard (matches within a directory only)": "Enkeltnivåsøk (søker kun i en mappe)",
"Smallest First": "Den minste først",
"Source Code": "Kildekode",
"Staggered File Versioning": "Forskjøvet Versjonskontroll",
"Start Browser": "Start Nettleser",
"Start Browser": "Start nettleser",
"Statistics": "Statistikk",
"Stopped": "Stoppa",
"Support": "Brukerstøtte",
"Sync Protocol Listen Addresses": "Lytteadresse For Synkroniseringsprotokoll",
"Sync Protocol Listen Addresses": "Lytteadresse for synkroniseringsprotokoll",
"Syncing": "Synkroniserer",
"Syncthing has been shut down.": "Syncthing har blitt slått av.",
"Syncthing includes the following software or portions thereof:": "Syncthing inkluderer helt eller delvis følgende programvare:",
@@ -209,21 +214,21 @@
"They are retried automatically and will be synced when the error is resolved.": "Disse hentes automatisk og vil synkroniseres når feilen er blitt utbedret.",
"This Device": "Denne enheten",
"This can easily give hackers access to read and change any files on your computer.": "Dette kan lett gi hackere tilgang til å lese og endre alle filer på datamaskinen din.",
"This is a major version upgrade.": "Dette er en hovedoppgradering",
"Trash Can File Versioning": "Papirkurv Versjonskontroll",
"This is a major version upgrade.": "Dette er en storoppgradering",
"Trash Can File Versioning": "Papirkurv versjonskontroll",
"Unknown": "Ukjent",
"Unshared": "Ikke delt",
"Unused": "Ikke i bruk",
"Up to Date": "Oppdatert",
"Updated": "Oppdatert",
"Upgrade": "Oppgradere",
"Upgrade To {%version%}": "Oppgrader Til {{version}}",
"Upgrade To {%version%}": "Oppgrader til {{version}}",
"Upgrading": "Oppgraderer",
"Upload Rate": "Opplastingsrate",
"Uptime": "Oppetid",
"Use HTTPS for GUI": "Bruk HTTPS for GUI",
"Version": "Versjon",
"Versions Path": "Plassering Av Versjoner",
"Versions Path": "Plassering av versjoner",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versjoner blir automatisk slettet når maksimal levetid er nådd eller når antall filer er oversteget.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Advarsel, denne stien er en underkatalog i en eksisterende katalog \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Merk at når en ny enhet blir lagt til må denne også legges til på andre siden.",
@@ -231,8 +236,8 @@
"Yes": "Ja",
"You must keep at least one version.": "Du må beholde minst én versjon",
"days": "dager",
"directories": "directories",
"files": "files",
"directories": "kataloger",
"files": "filer",
"full documentation": "all dokumentasjon",
"items": "elementer",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker å dele mappen \"{{folder}}\".",

View File

@@ -31,7 +31,7 @@
"Command": "Commando",
"Comment, when used at the start of a line": "Reageer indien gebruikt aan het begin van een lijn.",
"Compression": "Compressie",
"Configured": "Configured",
"Configured": "Geconfigureerd",
"Connection Error": "Verbindingsfout",
"Connection Type": "Soort verbinding",
"Copied from elsewhere": "Gekopieerd vanaf elders",
@@ -45,13 +45,15 @@
"Device Name": "Naam apparaat",
"Devices": "Apparaten",
"Disconnected": "Niet verbonden",
"Discovered": "Discovered",
"Discovered": "Ontdekt",
"Discovery": "Zoeken",
"Documentation": "Documentatie",
"Download Rate": "Downloadsnelheid",
"Downloaded": "Gedownload",
"Downloading": "Bezig met downloaden",
"Edit": "Bewerk",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Bezig met bewerken",
"Enable NAT traversal": "Activeer NAT traversal",
"Enable Relaying": "Activeer doorsturen",
@@ -95,6 +97,7 @@
"Last Scan": "Laatste scan",
"Last seen": "Laatst gezien op",
"Later": "Later",
"Latest Change": "Meest recente wijziging",
"Listeners": "Luisteraars",
"Local Discovery": "Lokaal zoeken",
"Local State": "Lokale status",
@@ -135,7 +138,7 @@
"Quick guide to supported patterns": "Snelgids voor ondersteunde patronen",
"RAM Utilization": "Geheugengebruik",
"Random": "Willekeurig",
"Reduced by ignore patterns": "Reduced by ignore patterns",
"Reduced by ignore patterns": "Verminderd door negeerpatronen",
"Release Notes": "Release notes",
"Remote Devices": "Externe apparaten",
"Remove": "Verwijderen",
@@ -153,6 +156,8 @@
"Scanning": "Aan het zoeken",
"Select the devices to share this folder with.": "Selecteer de apparaten om deze map mee te delen.",
"Select the folders to share with this device.": "Selecteer de mappen om met dit apparaat te delen.",
"Send & Receive": "Verzenden & Ontvangen",
"Send Only": "Alleen verzenden",
"Settings": "Instellingen",
"Share": "Delen",
"Share Folder": "Deel map",
@@ -231,8 +236,8 @@
"Yes": "Ja",
"You must keep at least one version.": "Minstens 1 versie moet bewaard blijven.",
"days": "dagen",
"directories": "directories",
"files": "files",
"directories": "Mappen",
"files": "Bestanden",
"full documentation": "volledige documentatie",
"items": "objecten",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wil de map \"{{folder}}\" delen.",

View File

@@ -1,6 +1,6 @@
{
"A device with that ID is already added.": "Ein eining med den ID er allerede lagt til.",
"A negative number of days doesn't make sense.": "Eit negativt tal dagar har ikkje meining.",
"A device with that ID is already added.": "Ei eining med den ID-en er allereie lagt til.",
"A negative number of days doesn't make sense.": "Eit negativt tal dagar gir ikkje meining.",
"A new major version may not be compatible with previous versions.": "Ein ny hovudversjon er ikkje nødvendigvis kompatibel med eldre versjonar. ",
"API Key": "API-nøkkel",
"About": "Om",
@@ -8,17 +8,17 @@
"Add": "Legg til",
"Add Device": "Legg Til Eining",
"Add Folder": "Legg Til Mappe",
"Add Remote Device": "Legg Til Ekstern Eining",
"Add Remote Device": "Legg til ekstern eining",
"Add new folder?": "Leggja til ny mappe?",
"Address": "Adresse",
"Addresses": "Adresser",
"Advanced": "Avansert",
"Advanced Configuration": "Avansert konfigurasjon",
"Advanced settings": "Avansert innstillingar",
"Advanced settings": "Avanserte innstillingar",
"All Data": "Alle data",
"Allow Anonymous Usage Reporting?": "Tillata anonymisert bruksrapportering?",
"Alphabetic": "Alfabetisk",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ein ekstern kommando håndterer filutgåver. Den må sørge for at fila blir fjerna frå den synkroniserte mappa.",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ein ekstern kommando handterer filutgåver. Den må sørga for at fila blir fjerna frå den synkroniserte mappa.",
"Anonymous Usage Reporting": "Anonymisert bruksrapportering",
"Any devices configured on an introducer device will be added to this device as well.": "Einingar konfigurert på ei introduksjonseining vil òg verta lagt til denne eininga.",
"Automatic upgrades": "Automatiske oppdateringar",
@@ -31,7 +31,7 @@
"Command": "Kommando",
"Comment, when used at the start of a line": "Kommentar, når brukt i starten av linja",
"Compression": "Komprimering",
"Configured": "Configured",
"Configured": "Konfigurert",
"Connection Error": "Tilkoplingsfeil",
"Connection Type": "Tilkoplingstype",
"Copied from elsewhere": "Kopiert frå ein annan stad",
@@ -39,36 +39,38 @@
"Copyright © 2014-2016 the following Contributors:": "Opphavsrett © 2014-2016 for følgjande bidragsyterar:",
"Danger!": "Fare!",
"Deleted": "Sletta",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Eininga \"{{name}}\" {{device}} ({{address}}) vil kopla seg til. Vil du leggja ho til?",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Eininga «{{name}}» {{device}} ({{address}}) vil kopla seg til. Vil du leggja ho til?",
"Device ID": "Eining ID",
"Device Identification": "Einingskjennemerke",
"Device Name": "Namn På Eining",
"Devices": "Einingar",
"Disconnected": "Fråkopla",
"Discovered": "Discovered",
"Discovered": "Oppdaga",
"Discovery": "Oppdaging",
"Documentation": "Dokumentasjon",
"Download Rate": "Nedlastingsfart",
"Downloaded": "Lasta ned",
"Downloading": "Lastar ned",
"Edit": "Rediger",
"Edit Device": "Rediger enhet",
"Edit Folder": "Rediger katalog",
"Editing": "Redigerer",
"Enable NAT traversal": "Slå på NAT-gjennomgang",
"Enable Relaying": "Aktiver Reléer",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Skriv inn adresser med komma mellom kvar adresse (\"tcp://ip:port\", \"tcp://host:port\"), eller \"dynamic\" for å automatisk søkja opp adressa.",
"Enable Relaying": "Aktiver relé",
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Skriv inn adresser med komma mellom kvar adresse («tcp://ip:port», «tcp://host:port»), eller «dynamic» for å automatisk søkja opp adressa.",
"Enter ignore patterns, one per line.": "Skriv inn mønster som skal utelatast, eitt per linje.",
"Error": "Feilmelding",
"External File Versioning": "Ekstern filutgåvehandtering",
"Failed Items": "Feilande element",
"File Pull Order": "Henterekkefølge for filer",
"File Versioning": "Filutgåvekontroll",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Informasjon om filrettar vert ignorert når det blir leita etter endringar. Bruk på FAT filsystem. ",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Informasjon om filrettar vert ignorert når det blir leita etter endringar. Bruk på FAT-filsystem. ",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer som Syncthing slettar eller skriv over vert flytta til katalogen .stversions.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer som Syncthing oppdaterar eller sletter vert flytta til ein datostempla versjon i .stversions-katalogen.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer som Syncthing oppdaterer eller slettar vert flytta til ein datostempla versjon i .stversions-katalogen.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer er beskytta mot endringar gjort på andre einingar, men endringar gjort på denne eininga vert sende til resten av klyngja.",
"Folder": "Mappe",
"Folder ID": "Mappe ID",
"Folder Label": "Merkelapp for Mappe",
"Folder Label": "Merkelapp for mappa",
"Folder Path": "Mappeplassering",
"Folder Type": "Mappetype",
"Folders": "Mapper",
@@ -78,7 +80,7 @@
"GUI Listen Addresses": "GUI Lytteadresse",
"Generate": "Generer",
"Global Discovery": "Global søking",
"Global Discovery Servers": "Globalsøkjetenarar",
"Global Discovery Servers": "Globale søkjetenarar",
"Global State": "Global Tilstand",
"Help": "Hjelp",
"Home page": "Heimeside",
@@ -86,15 +88,16 @@
"Ignore Patterns": "Utelatingsmønster",
"Ignore Permissions": "Ignorer tilgangar",
"Incoming Rate Limit (KiB/s)": "Innkomande hastigheitsgrense (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Feil innstillingar kan skade innhaldet i dine delte katalogar og hindre Syncthing i å fungere.",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Feil innstillingar kan skada innhaldet i dine delte katalogar og hindra Syncthing i å fungera.",
"Introducer": "Introduktør",
"Inversion of the given condition (i.e. do not exclude)": "Det motsette av den gitte tilstanden (dvs. ekskluder ikkje)",
"Keep Versions": "Behald Versjonar",
"Largest First": "Største fyrst",
"Last File Received": "Siste mottatte fila",
"Last Scan": "Siste Skanning",
"Last Scan": "Siste skanning",
"Last seen": "Sist sett",
"Later": "Seinare",
"Latest Change": "Siste endringar",
"Listeners": "Lyttarar",
"Local Discovery": "Lokal oppdaging",
"Local State": "Lokal Tilstand",
@@ -111,35 +114,35 @@
"New Folder": "Ny mappe",
"Newest First": "Nyaste fyrst",
"No": "Nei",
"No File Versioning": "Ingen filutgåvehandtering",
"No File Versioning": "Inga filutgåvehandtering",
"Normal": "Normal",
"Notice": "Merknad",
"OK": "OK",
"Off": "Av",
"Oldest First": "Elste fyrst",
"Optional descriptive label for the folder. Can be different on each device.": "Valfri merkelapp på katalogen. Denne kan være ulik på andre enheter.",
"Oldest First": "Eldste fyrst",
"Optional descriptive label for the folder. Can be different on each device.": "Valfri merkelapp på katalogen. Denne kan vera ulik på andre einingar.",
"Options": "Val",
"Out of Sync": "Ikkje synkronisert",
"Out of Sync Items": "Ikkje-synkroniserte element",
"Outgoing Rate Limit (KiB/s)": "Utgåande hastigheitsgrense (KiB/s)",
"Override Changes": "Overstyr endringar",
"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": "Plasseringa av mappa på datamaskinen. Vert oppretta om ho ikkje finst. Krøllstrekteiknet (~) kan brukast som forkorting for",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Plasseringa for lagra versjonar (la denne vera tom for å bruka standard .stversions-mappa i mappa).",
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Plasseringa for lagra versjonar (la denne vera tom for å bruka standardmappa .stversions i mappa).",
"Pause": "Stans",
"Paused": "Stansa",
"Please consult the release notes before performing a major upgrade.": "Sjå \"release notes\" før ei hovudoppgradering vert utført.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ver vennleg å set ein GUI brukar og passord i Innstillingar-dialogen.",
"Please consult the release notes before performing a major upgrade.": "Sjå utgjevingsmerknadene før ei hovudoppgradering vert utført.",
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ver vennleg å laga ein GUI-brukar og eit passord i Innstillingar-dialogen.",
"Please wait": "Gjer vel og vent",
"Preview": "Førehandsvisning",
"Preview Usage Report": "Førehandsvis bruksrapporten",
"Quick guide to supported patterns": "Kjapp innføring i godkjente mønster",
"RAM Utilization": "Minnebruk",
"Random": "Tilfeldig",
"Reduced by ignore patterns": "Reduced by ignore patterns",
"Release Notes": "Utgivingsnotat",
"Remote Devices": "Eksterne Einingar",
"Reduced by ignore patterns": "Reduser med utelatelsesmønster",
"Release Notes": "Utgjevingsnotat",
"Remote Devices": "Eksterne einingar",
"Remove": "Fjern",
"Required identifier for the folder. Must be the same on all cluster devices.": "Påkrevd identifikator for katalogen. Denne må være lik på alle einingar i samme klynge.",
"Required identifier for the folder. Must be the same on all cluster devices.": "Påkravd identifikator for katalogen. Denne må vera lik på alle einingane i same klynge.",
"Rescan": "Skann På Ny",
"Rescan All": "Skann alle på nytt",
"Rescan Interval": "Skanneintervall",
@@ -149,10 +152,12 @@
"Resume": "Start",
"Reused": "Gjenbrukt",
"Save": "Lagre",
"Scan Time Remaining": "Gjenståande Skannetid",
"Scan Time Remaining": "Gjenståande skannetid",
"Scanning": "Skannar",
"Select the devices to share this folder with.": "Vel einingane du vil dela denne mappa med.",
"Select the folders to share with this device.": "Vel mappene du vil dela med denne eininga.",
"Send & Receive": "Sende og motta",
"Send Only": "Bare sende",
"Settings": "Innstillingar",
"Share": "Del",
"Share Folder": "Del mappe",
@@ -182,35 +187,35 @@
"Syncthing is restarting.": "Syncthing startar på ny.",
"Syncthing is upgrading.": "Syncthing oppgraderer.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ut til å vera nede, eller så er det eit problem med nettilkoplinga di. Prøvar på ny …",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ser ut til å ha støtt på eit problem under behandling av din førespurnad. Vær vennleg å oppfrisk nettlesaren eller start Syncthing på nytt om problemet vedvarer.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing sitt administreringsgrensesnitt er sett opp til å tillate ekstern tilgang uten passord.",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ser ut til å ha støtt på eit problem under behandling av førespurnaden din. Ver vennleg å oppfriska nettlesaren eller starta Syncthing på nytt om problemet varer ved.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Administreringsgrensesnittet til Syncthing er sett opp til å tillata ekstern tilgang utan passord.",
"The aggregated statistics are publicly available at the URL below.": "Samla statistikk er opent tilgjengeleg på URL-en nedanfor.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Instillingane har blitt lagra men ikkje aktivert. Syncthing må starta på ny for å aktivera dei nye instillingane.",
"The device ID cannot be blank.": "Eining ID kan ikkje vera tom.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Einings-IDen som skal oppgis her kan hentast fram via \"Rediger > Vis ID\"-dialogboksen på den andre eininga. Mellomrom og bindestrek er valfritt (blir ignorert).",
"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.": "Den krypterte bruksrapporten vert send dagleg. Han vert nytta til å spora vanlege plattformer, mappestorleikar og programutgåvene. Om datasettet endrar seg, vil dette meldingsvindauget dukka opp att og du vil verta beden om å godkjenna det.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Einings-ID-en som skal oppgjevast her kan hentast fram via menyvalet «Handlingar > Vis ID» på den andre eininga. Mellomrom og bindestrek er valfritt (blir ignorert).",
"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.": "Den krypterte bruksrapporten vert send dagleg. Han vert nytta til å spora vanlege plattformer, mappestorleikar og programutgåvene. Om datasettet endrar seg, vil dette meldingsvindauget dukka opp att, og du vil verta beden om å godkjenna det.",
"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.": "Einings-ID-en er ikkje gyldig. Han må vera på 52 eller 56 teikn og vera samansett av bokstavar og tal med valfrie mellomrom og bindestrekar.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Den første kommandolinjeparameteren er mappebana og den andre syner den relative bana i mappa.",
"The folder ID cannot be blank.": "Mappe ID kan ikkje vera tom.",
"The folder ID must be unique.": "Mappe ID må vera unik.",
"The folder path cannot be blank.": "Mappeplasseringa kan ikkje vera tom.",
"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.": "Desse intervalla vert nytta: den fyrste timen vert ei utgåve lagra kvart 30. sekund, den fyrste dagen vert ei utgåve lagra kvar time, dei fyrste 30 dagane vert ei utgåve lagra kvar dag, og inntil høgaste alderen vert ei utgåve lagra kvar veke.",
"The following items could not be synchronized.": "Fyljande filer kunne ikkje synkroniserast.",
"The following items could not be synchronized.": "Fylgjande filer kunne ikkje synkroniserast.",
"The maximum age must be a number and cannot be blank.": "Maksimal levetid må vera eit tal og kan ikkje vera tomt.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Høgaste tidsrom å behalda ei utgåve (i dagar, set til 0 for å behalda versjonane for alltid).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Nødvendig ledig diskplass må vere eit tal mellom 0 og 100.",
"The number of days must be a number and cannot be blank.": "Tal på dagar må ver eit tal og kan ikkje vera tomt.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Høgaste tidsrom å spara på ei utgåve (i dagar; set til 0 for å spara på versjonane for alltid).",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Nødvendig ledig diskplass må vera eit tal mellom 0 og 100.",
"The number of days must be a number and cannot be blank.": "Tal på dagar må vera eit tal, og kan ikkje vera tomt.",
"The number of days to keep files in the trash can. Zero means forever.": "Antall dagar å behalda filer i søppelkorga. Null betyr for alltid.",
"The number of old versions to keep, per file.": "Tal på gamle versjonar ein skal behalda, per fil.",
"The number of versions must be a number and cannot be blank.": "Tal på versjonar må vera eit tal og kan ikkje vera tomt.",
"The path cannot be blank.": "Bana kan ikkje vera tom.",
"The rate limit must be a non-negative number (0: no limit)": "Hastigheitsgrensa må ver eit positivt tall (0: ingen grensa)",
"The rate limit must be a non-negative number (0: no limit)": "Hastigheitsgrensa må vera eit positivt tall (0: inga grense)",
"The rescan interval must be a non-negative number of seconds.": "Talet på sekund i skanneintervallet kan ikkje vera negativt.",
"They are retried automatically and will be synced when the error is resolved.": "Desse vil bli prøvd på nytt automatisk og vil bli synkronisert når feilen har blitt utbetra.",
"This Device": "Denne Eininga",
"This can easily give hackers access to read and change any files on your computer.": "Dette kan lett gje dataekspertar tilgang til å lese og endre vilkårlege filer på denne maskina.",
"This Device": "Denne eininga",
"This can easily give hackers access to read and change any files on your computer.": "Dette kan lett gje datasnokar tilgang til å lesa og endra vilkårlege filer på denne maskina.",
"This is a major version upgrade.": "Dette er ei hovudoppgradering",
"Trash Can File Versioning": "Papirkorg filutgåvehandtering",
"Trash Can File Versioning": "Papirkorg-filutgåvehandtering",
"Unknown": "Ukjent",
"Unshared": "Ikkje delt",
"Unused": "Ubrukt",
@@ -225,16 +230,16 @@
"Version": "Versjon",
"Versions Path": "Utgåvebane",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Utgåver vert automatisk sletta når maksimal levetid er nådd eller når det høgaste tillate talet på filer innan eit intervall vert overskride.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Åtvaring, denne banen er ei undermappe av den eksisterande mappa \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Hugs at når ei ny eining vert lagt til må ho òg leggjast til på andre sida.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Åtvaring, denne bana er ei undermappe av den eksisterande mappa «{{otherFolder}}».",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Hugs at når ei ny eining vert lagt til, må ho òg leggjast til på andre sida.",
"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.": "Hugs at når ei ny mappe vert lagt til, vert mappe-ID-en brukt til å binda saman mappene mellom einingane. Det er skilnad på store og små bokstavar, så ID-ane må vera identiske på alle einingane.",
"Yes": "Ja",
"You must keep at least one version.": "Du må behalda minst ein versjon.",
"days": "dagar",
"directories": "directories",
"files": "files",
"directories": "katalogar",
"files": "filer",
"full documentation": "all dokumentasjon",
"items": "element",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønskjer å dela mappa \"{{folder}}\".",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} ønskjer å dela mappa \"{{folderLabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} ønskjer å dela mappa «{{folderLabel}}» ({{folder}})."
}

View File

@@ -52,6 +52,8 @@
"Downloaded": "Pobrane",
"Downloading": "Pobieranie",
"Edit": "Edytuj",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Edytowanie",
"Enable NAT traversal": "Włącz trawersowanie NAT",
"Enable Relaying": "Włącz przekazywanie",
@@ -95,6 +97,7 @@
"Last Scan": "Czas ostatniego skanu",
"Last seen": "Ostatnio widziany",
"Later": "Później",
"Latest Change": "Latest Change",
"Listeners": "Nasłuchujący",
"Local Discovery": "Lokalne odnajdywanie",
"Local State": "Status lokalny",
@@ -135,7 +138,7 @@
"Quick guide to supported patterns": "Krótki przewodnik po obsługiwanych wzorcach",
"RAM Utilization": "Użycie pamięci RAM",
"Random": "Losowo",
"Reduced by ignore patterns": "Reduced by ignore patterns",
"Reduced by ignore patterns": "Ograniczono przez wzorce ignorowania",
"Release Notes": "Informacje o wydaniu",
"Remote Devices": "Urządzenia zdalne",
"Remove": "Usuń",
@@ -153,6 +156,8 @@
"Scanning": "Skanowanie",
"Select the devices to share this folder with.": "Wybierz urządzenie, któremu udostępnić folder.",
"Select the folders to share with this device.": "Wybierz foldery do współdzielenia z tym urządzeniem.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "Ustawienia",
"Share": "Udostępnij",
"Share Folder": "Udostępnij folder",
@@ -231,8 +236,8 @@
"Yes": "Tak",
"You must keep at least one version.": "Musisz posiadać przynajmniej jedną wersję",
"days": "dni",
"directories": "directories",
"files": "files",
"directories": "katalogi",
"files": "pliki",
"full documentation": "pełna dokumentacja",
"items": "pozycji",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce udostępnić folder \"{{folder}}\"",

View File

@@ -31,7 +31,7 @@
"Command": "Comando",
"Comment, when used at the start of a line": "Comentário, se usado no início de uma linha",
"Compression": "Compressão",
"Configured": "Configured",
"Configured": "Configurado",
"Connection Error": "Erro de conexão",
"Connection Type": "Tipo da conexão",
"Copied from elsewhere": "Copiado de outro lugar",
@@ -45,13 +45,15 @@
"Device Name": "Nome do dispositivo",
"Devices": "Dispositivos",
"Disconnected": "Desconectado",
"Discovered": "Discovered",
"Discovered": "Descoberto",
"Discovery": "Descoberta",
"Documentation": "Documentação",
"Download Rate": "Velocidade de recepção",
"Downloaded": "Recebido",
"Downloading": "Recebendo",
"Edit": "Editar",
"Edit Device": "Editar dispositivo",
"Edit Folder": "Editar pasta",
"Editing": "Editando",
"Enable NAT traversal": "Habilitar NAT",
"Enable Relaying": "Habilitar retransmissão",
@@ -95,6 +97,7 @@
"Last Scan": "Última verificação",
"Last seen": "Visto por último em",
"Later": "Depois",
"Latest Change": "Última mudança",
"Listeners": "Escutadores",
"Local Discovery": "Descoberta local",
"Local State": "Estado local",
@@ -135,7 +138,7 @@
"Quick guide to supported patterns": "Guia rápido dos padrões suportados",
"RAM Utilization": "Uso de RAM",
"Random": "Aleatória",
"Reduced by ignore patterns": "Reduced by ignore patterns",
"Reduced by ignore patterns": "Reduzido por filtros",
"Release Notes": "Notas de lançamento",
"Remote Devices": "Dispositivos remotos",
"Remove": "Remover",
@@ -153,6 +156,8 @@
"Scanning": "Verificando",
"Select the devices to share this folder with.": "Selecione os dispositivos com os quais esta pasta será compartilhada.",
"Select the folders to share with this device.": "Selecione as pastas a serem compartilhadas com este dispositivo.",
"Send & Receive": "Enviar e receber",
"Send Only": "Somente enviar",
"Settings": "Configurações",
"Share": "Compartilhar",
"Share Folder": "Compartilhar pasta",
@@ -231,8 +236,8 @@
"Yes": "Sim",
"You must keep at least one version.": "Você deve manter pelo menos uma versão.",
"days": "dias",
"directories": "directories",
"files": "files",
"directories": "direrios",
"files": "arquivos",
"full documentation": "documentação completa",
"items": "itens",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer compartilhar a pasta \"{{folder}}\".",

View File

@@ -31,7 +31,7 @@
"Command": "Comando",
"Comment, when used at the start of a line": "Comentário, quando usado no início de uma linha",
"Compression": "Compressão",
"Configured": "Configured",
"Configured": "Configurado",
"Connection Error": "Erro de ligação",
"Connection Type": "Tipo de ligação",
"Copied from elsewhere": "Copiado doutro sítio",
@@ -45,13 +45,15 @@
"Device Name": "Nome do dispositivo",
"Devices": "Dispositivos",
"Disconnected": "Desconectado",
"Discovered": "Discovered",
"Discovered": "Descoberto",
"Discovery": "Pesquisa",
"Documentation": "Documentação",
"Download Rate": "Velocidade de recepção",
"Downloaded": "Recebido",
"Downloading": "Recebendo",
"Edit": "Editar",
"Edit Device": "Editar dispositivo",
"Edit Folder": "Editar pasta",
"Editing": "Editando",
"Enable NAT traversal": "Activar travessia de NAT",
"Enable Relaying": "Permitir retransmissão",
@@ -72,7 +74,7 @@
"Folder Path": "Caminho da pasta",
"Folder Type": "Tipo de pasta",
"Folders": "Pastas",
"GUI": "GUI",
"GUI": "Interface gráfica",
"GUI Authentication Password": "Senha da autenticação na interface gráfica",
"GUI Authentication User": "Utilizador da autenticação na interface gráfica",
"GUI Listen Addresses": "Endereço de escuta da interface gráfica",
@@ -95,6 +97,7 @@
"Last Scan": "Última verificação",
"Last seen": "Última vez que foi verificado",
"Later": "Mais tarde",
"Latest Change": "Última alteração",
"Listeners": "Auscultadores",
"Local Discovery": "Pesquisa local",
"Local State": "Estado local",
@@ -135,7 +138,7 @@
"Quick guide to supported patterns": "Guia rápido dos padrões suportados",
"RAM Utilization": "Utilização da RAM",
"Random": "Aleatória",
"Reduced by ignore patterns": "Reduced by ignore patterns",
"Reduced by ignore patterns": "Reduzido pelos padrões de exclusão",
"Release Notes": "Notas de lançamento",
"Remote Devices": "Dispositivos remotos",
"Remove": "Remover",
@@ -153,6 +156,8 @@
"Scanning": "Verificando",
"Select the devices to share this folder with.": "Seleccione os dispositivos com os quais vai partilhar esta pasta.",
"Select the folders to share with this device.": "Seleccione as pastas a partilhar com este dispositivo.",
"Send & Receive": "Enviar e receber",
"Send Only": "Enviar apenas",
"Settings": "Configurações",
"Share": "Partilhar",
"Share Folder": "Partilhar pasta",
@@ -231,8 +236,8 @@
"Yes": "Sim",
"You must keep at least one version.": "Tem que manter pelo menos uma versão.",
"days": "dias",
"directories": "directories",
"files": "files",
"directories": "pastas",
"files": "ficheiros",
"full documentation": "documentação completa",
"items": "itens",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer partilhar a pasta \"{{folder}}\".",

View File

@@ -31,7 +31,7 @@
"Command": "Команда",
"Comment, when used at the start of a line": "Комментарий, если используется в начале строки",
"Compression": "Сжатие",
"Configured": "Configured",
"Configured": "Сконфигурировано",
"Connection Error": "Ошибка подключения",
"Connection Type": "Тип соединения",
"Copied from elsewhere": "Скопировано из другого места",
@@ -45,13 +45,15 @@
"Device Name": "Имя устройства",
"Devices": "Устройства",
"Disconnected": "Нет соединения",
"Discovered": "Discovered",
"Discovered": "Обнаружено",
"Discovery": "Обнаружение",
"Documentation": "Документация",
"Download Rate": "Скорость загрузки",
"Downloaded": "Загружено",
"Downloading": "Загрузка",
"Edit": "Редактировать",
"Edit Device": "Редактирование устройства",
"Edit Folder": "Редактирование папки",
"Editing": "Редактирование",
"Enable NAT traversal": "Включить NAT traversal",
"Enable Relaying": "Включить релеи",
@@ -95,6 +97,7 @@
"Last Scan": "Последнее сканирование",
"Last seen": "Был доступен",
"Later": "Позже",
"Latest Change": "Последнее изменение",
"Listeners": "Прослушиватель",
"Local Discovery": "Локальное обнаружение",
"Local State": "Локальное состояние",
@@ -135,7 +138,7 @@
"Quick guide to supported patterns": "Краткое руководство по поддерживаемым шаблонам",
"RAM Utilization": "Использование памяти",
"Random": "Случайно",
"Reduced by ignore patterns": "Reduced by ignore patterns",
"Reduced by ignore patterns": "Уменьшено шаблонами игнорирования",
"Release Notes": "Примечания к выпуску",
"Remote Devices": "Удалённые устройства",
"Remove": "Удалить",
@@ -153,6 +156,8 @@
"Scanning": "Сканирование",
"Select the devices to share this folder with.": "Выберите устройства, для которых будет доступна эта папка.",
"Select the folders to share with this device.": "Выберите папки, которые будут доступны этому устройству.",
"Send & Receive": "Отправить и получить",
"Send Only": "Только отправить",
"Settings": "Настройки",
"Share": "Предоставить доступ",
"Share Folder": "Предоставить доступ к папке",
@@ -231,8 +236,8 @@
"Yes": "Да",
"You must keep at least one version.": "Вы должны хранить как минимум одну версию.",
"days": "дней",
"directories": "directories",
"files": "files",
"directories": "папок",
"files": "файлов",
"full documentation": "полная документация",
"items": "элементы",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хочет поделиться папкой «{{folder}}».",

View File

@@ -1,5 +1,5 @@
{
"A device with that ID is already added.": "En enhet med det ID:t är redan tillagt.",
"A device with that ID is already added.": "En enhet med det ID är redan tillagt.",
"A negative number of days doesn't make sense.": "Ett negativt antal dagar är inte rimligt.",
"A new major version may not be compatible with previous versions.": "En ny huvudversion kan eventuellt vara inkompatibel med tidigare versioner.",
"API Key": "API-nyckel",
@@ -24,7 +24,7 @@
"Automatic upgrades": "Automatiska uppgraderingar",
"Be careful!": "Var aktsam!",
"Bugs": "Buggar",
"CPU Utilization": "CPU-användning",
"CPU Utilization": "CPU användning",
"Changelog": "Ändringslogg",
"Clean out after": "Rensa efteråt",
"Close": "Stäng",
@@ -40,9 +40,9 @@
"Danger!": "Fara!",
"Deleted": "Borttaget",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} på {{address}}) vill ansluta. Lägg till ny enhet?",
"Device ID": "Enhets ID",
"Device Identification": "Enhets identifikation",
"Device Name": "Enhets namn",
"Device ID": "Enhet-ID",
"Device Identification": "Enhetsidentifikation",
"Device Name": "Enhetsnamn",
"Devices": "Enheter",
"Disconnected": "Frånkopplad",
"Discovered": "Upptäckt",
@@ -52,6 +52,8 @@
"Downloaded": "Hämtat",
"Downloading": "Hämtar",
"Edit": "Redigera",
"Edit Device": "Redigera enhet",
"Edit Folder": "Redigera katalog",
"Editing": "Redigerar",
"Enable NAT traversal": "Aktivera NAT traversering",
"Enable Relaying": "Aktivera reläa",
@@ -62,9 +64,9 @@
"Failed Items": "Misslyckade objekt",
"File Pull Order": "Filhämtningsprioritering",
"File Versioning": "Filversionshantering",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Filrättigheter ignoreras vid sökning efter förändringar. Används på FAT-filsystem.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till katalogen .stversions om de ersätts eller raderas av Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till datummärkta versioner i en .stversions-mapp när de ersatts eller raderats av Syncthing.",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Filrättigheter ignoreras under sökning efter förändringar. Används på FAT-filsystem.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till .stversions katalogen när de ersätts eller raderas av Syncthing.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Filer flyttas till datummärkta versioner i en .stversions mapp när de ersätts eller raderas av Syncthing.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer skyddas från ändringar gjorda på andra enheter, men ändringar som görs på den här noden skickas till de andra klustermedlemmarna.",
"Folder": "Katalog",
"Folder ID": "Katalog-ID",
@@ -95,6 +97,7 @@
"Last Scan": "Senaste skanning",
"Last seen": "Senast sedd",
"Later": "Senare",
"Latest Change": "Senast ändrad",
"Listeners": "Lyssnare",
"Local Discovery": "Lokal annonsering",
"Local State": "Lokal status",
@@ -133,7 +136,7 @@
"Preview": "Förhandsgranska",
"Preview Usage Report": "Förhandsgranska statistik",
"Quick guide to supported patterns": "Snabb handledning till mönster som stöds",
"RAM Utilization": "RAM-användning",
"RAM Utilization": "RAM användning",
"Random": "Slumpmässig",
"Reduced by ignore patterns": "Minskas med ignorera mönster",
"Release Notes": "Versionsanteckningar",
@@ -153,6 +156,8 @@
"Scanning": "Skannar",
"Select the devices to share this folder with.": "Ange enheterna som den här katalogen ska delas med.",
"Select the folders to share with this device.": "Välj kataloger att dela med den här enheten.",
"Send & Receive": "Skicka & ta emot",
"Send Only": "Skicka endast",
"Settings": "Inställningar",
"Share": "Dela",
"Share Folder": "Dela katalog",
@@ -162,8 +167,8 @@
"Shared With": "Delad med",
"Show ID": "Visa ID",
"Show QR": "Visa QR",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas i stället för enhets ID i samlingsstatusen. Skickas till andra enheter som namn på denna enhet.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas i stället för enhets ID i samlingsstatusen. Sätts till namnet på den andra enheten vid första anslutning om det lämnas tomt.",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas istället för enhet-ID i klusterstatusen. Skickas till andra enheter som ett alternativt förvalt namn.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas i stället för enhet-ID i klusterstatusen. Kommer att uppdateras till namnet enheten annonserar om det lämnas tomt.",
"Shutdown": "Stäng av",
"Shutdown Complete": "Avstängning klar",
"Simple File Versioning": "Enkel filversionshantering",
@@ -181,18 +186,18 @@
"Syncthing includes the following software or portions thereof:": "Syncthing innehåller följande mjukvarupaket eller delar av dem:",
"Syncthing is restarting.": "Syncthing startar om.",
"Syncthing is upgrading.": "Syncthing uppgraderas.",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar avstängd, eller finns det problem med din Internetanslutning. Försöker igen...",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing verkar avstängd eller så är det problem med din Internetanslutning. Försöker igen...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing verkar ha drabbats av ett problem med behandlingen av din begäran. Uppdatera sidan eller starta om Syncthing om problemet kvarstår.",
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administratör gränssnittet är konfigurerat för att tillåta fjärrtillträde utan ett lösenord.",
"The aggregated statistics are publicly available at the URL below.": "Den aggregerade statistiken är offentligt tillgängliga på webbadressen nedan.",
"The aggregated statistics are publicly available at the URL below.": "Den aggregerade statistiken är offentligt tillgänglig på webbadressen nedan.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
"The device ID cannot be blank.": "Enhets ID:t kan inte vara tomt.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhets ID:t som behövs här kan du hitta i \"Åtgärder > Visa ID\"-dialogrutan på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
"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.": "Den krypterade användarstatistiken skickas dagligen. Den används för att spåra vanliga plattformar, katalogstorlekar och versioner. Om datan som rapporteras ändras så kommer du att bli tillfrågad igen.",
"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.": "Det inmatade enhets ID:t verkar inte korrekt. Det ska vara en 52 eller 56 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
"The device ID cannot be blank.": "Enhet-ID kan inte vara tomt.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhet-ID som behövs här kan du hitta i \"Åtgärder > Visa ID\"-dialogrutan på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
"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.": "Den krypterade användarstatistiken skickas dagligen. Den används för att spåra vanliga plattformar, katalogstorlekar och versioner. Om datat som rapporteras ändras så kommer du att bli tillfrågad igen.",
"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.": "Det inmatade enhet-ID verkar inte vara korrekt. Det ska vara en 52 eller 56 teckensträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Den första kommandoparametern är sökvägen till mappen och den andra parametern är den relativa sökvägen i mappen.",
"The folder ID cannot be blank.": "Katalogens ID får inte vara tomt.",
"The folder ID must be unique.": "Katalogens ID måste vara unikt.",
"The folder ID cannot be blank.": "Katalog-ID får inte vara tomt.",
"The folder ID must be unique.": "Katalog-ID måste vara unikt.",
"The folder path cannot be blank.": "Katalogsökvägen kan inte vara tom.",
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "De följande intervallen används: varje 30 sekunder under den första timmen; varje timme under den första dagen; varje dag för de första 30 dagarna; varje vecka tills den maximala åldersgränsen uppnås.",
"The following items could not be synchronized.": "Följande objekt kunde inte synkroniseras.",
@@ -215,7 +220,7 @@
"Unshared": "Inte delad",
"Unused": "Oanvänd",
"Up to Date": "Uppdaterad",
"Updated": "Uppdaterad",
"Updated": "Uppdaterade",
"Upgrade": "Uppgradering",
"Upgrade To {%version%}": "Uppgradera till {{version}}",
"Upgrading": "Uppgraderar",
@@ -223,11 +228,11 @@
"Uptime": "Drifttid",
"Use HTTPS for GUI": "Använd HTTPS för GUI",
"Version": "Version",
"Versions Path": "Versioner sökväg",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas bort automatiskt när de är äldre än den maximala åldersgränsen eller överstiger frekvensen i sitt interval.",
"Versions Path": "Sökväg för versioner",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas bort automatiskt när de är äldre än den maximala åldersgränsen eller överstiger frekvensen i intervallet.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en underkatalog till en befintlig katalog \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "När du lägger till en ny enhet, kom ihåg att den här enheten måste läggas till på den andra enheten också.",
"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.": "När du lägger till ny katalog, tänk på att katalogens ID knyter ihop katalogen mellan olika noder. De måste vara exakt desamma mellan noder och stora eller små bokstäver har betydelse.",
"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.": "När du lägger till ny katalog, tänk på att katalog-ID knyter ihop kataloger mellan olika enheter. De skiftlägeskänsliga och måste matcha precis mellan alla enheter.",
"Yes": "Ja",
"You must keep at least one version.": "Du måste behålla åtminstone en version.",
"days": "dagar",

View File

@@ -1,28 +1,28 @@
{
"A device with that ID is already added.": "Bu ID'yi taşıyan cihaz zaten eklendi.",
"A device with that ID is already added.": "Bu ID'yi taşıyan aygıt zaten eklendi.",
"A negative number of days doesn't make sense.": "Eksi gün sayısı mantıklı bir ifade değil.",
"A new major version may not be compatible with previous versions.": "Yeni birincil sürümler önceki sürümlerle uyumlu olmayabilir.",
"A new major version may not be compatible with previous versions.": "Yeni ana sürüm önceki sürümlerle uyumlu olmayabilir.",
"API Key": "API Anahtarı",
"About": "Hakkında",
"Actions": "Eylemler",
"Add": "Ekle",
"Add Device": "Cihaz Ekle",
"Add Device": "Aygıt Ekle",
"Add Folder": "Klasör Ekle",
"Add Remote Device": "Add Remote Device",
"Add Remote Device": "Uzak Aygıt Ekle",
"Add new folder?": "Yeni klasör ekle?",
"Address": "Adres",
"Addresses": "Adresler",
"Advanced": "Gelişmiş Düzey",
"Advanced Configuration": "Gelişmiş Yapılandırma",
"Advanced settings": "Advanced settings",
"All Data": "Bütün Veriler",
"Advanced settings": "Gelişmiş ayarlar",
"All Data": "Tüm Veriler",
"Allow Anonymous Usage Reporting?": "Anonim kullanımın raporlanmasına izin veriyor musun ?",
"Alphabetic": "Alfabetik",
"An external command handles the versioning. It has to remove the file from the synced folder.": "Sürümlendirme işlemini harici bir komut yürütüyor. Dosyayı eşzamanlama klasöründen kaldırmak zorunda.",
"Anonymous Usage Reporting": "Anonim Kullanım Raporlama",
"Any devices configured on an introducer device will be added to this device as well.": "Tanıtıcı bir cihazda yapılandırılan cihazlar bu cihaza da eklenecektir.",
"Automatic upgrades": "Otomatik güncellemeler",
"Be careful!": "Dikkatli Ol!",
"Be careful!": "Dikkatli ol!",
"Bugs": "Hatalar",
"CPU Utilization": "İşlemci Kullanımı",
"Changelog": "Değişim Günlüğü",
@@ -33,25 +33,27 @@
"Compression": "Sıkıştırma",
"Configured": "Configured",
"Connection Error": "Bağlantı hatası",
"Connection Type": "Connection Type",
"Connection Type": "Bağlantı Türü",
"Copied from elsewhere": "Başka bir yerden kopyalanmış",
"Copied from original": "Aslından kopyalanmış",
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
"Danger!": "Tehlike!",
"Deleted": "Silindi",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
"Device ID": "Cihaz ID",
"Device Identification": "Cihaz Kimliği",
"Device Name": "Cihaz Adı",
"Devices": "Cihazlar",
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "\"{{name}}\" aygıtı ({{address}} adresindeki {{device}}) bağlanmak istiyor. Yeni aygıtı ekle?",
"Device ID": "Aygıt ID",
"Device Identification": "Aygıt Kimliği",
"Device Name": "Aygıt Adı",
"Devices": "Aygıtlar",
"Disconnected": "Bağlantı Kesik",
"Discovered": "Discovered",
"Discovery": "Discovery",
"Discovered": "Keşfedildi",
"Discovery": "Keşif",
"Documentation": "Belgeleme",
"Download Rate": "İndirme Hızı",
"Downloaded": "İndirilmiş",
"Downloading": "İndiriliyor",
"Edit": "Düzenle",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Düzenleniyor",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Enable Relaying",
@@ -59,68 +61,69 @@
"Enter ignore patterns, one per line.": "Yoksayılacak/ihmal edilecek kalıp dizilerini her satıra bir tane olacak şekilde girin.",
"Error": "Hata",
"External File Versioning": "Harici Dosya Sürümlendirme",
"Failed Items": "Başarısız olunan Öğeler",
"File Pull Order": "File Pull Order",
"Failed Items": "Başarısız Olunan Ögeler",
"File Pull Order": "Dosya Koyma Düzeni",
"File Versioning": "Dosya Sürümlendirme",
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Değişimleri yoklarken dosya izin bilgilerini ihmal et. FAT dosya sistemlerinde kullanın.",
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Dosyalar Syncthing tarafından yeri değiştirildiğinde ya da silindiğinde .stversions klasörüne taşınır.",
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dosyalar Syncthing tarafından yeri değiştirildiğinde ya da silindiğinde, tarih damgalı sürümleri .stversions klasörüne taşınır.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dosyalar diğer cihazlarda yapılan değişikliklerden korunur, ancak bu cihazdaki değişiklikler kümedeki diğer cihazlara gönderilir.",
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dosyalar diğer aygıtlarda yapılan değişikliklerden korunur, ancak bu aygıttaki değişiklikler kümedeki diğer aygıtlara gönderilir.",
"Folder": "Klasör",
"Folder ID": "Klasör ID",
"Folder Label": "Folder Label",
"Folder Label": "Klasör Etiketi",
"Folder Path": "Klasör Yolu",
"Folder Type": "Folder Type",
"Folder Type": "Klasör Türü",
"Folders": "Klasörler",
"GUI": "GUI / Kullanıcı Grafik Arayüzü",
"GUI": "GUI / Grafiksel Kullanıcı Arayüzü",
"GUI Authentication Password": "GUI Kimlik Doğrulaması için Kullanıcı Parolası",
"GUI Authentication User": "GUI Kimlik Doğrulaması için Kullanıcı Adı",
"GUI Listen Addresses": "GUI Dinleme/Bağlantı Adresleri",
"Generate": "Oluştur",
"Global Discovery": "Küresel Discovery",
"Global Discovery Servers": "Global Discovery Servers",
"Global Discovery Servers": "Küresel Keşif Sunucuları",
"Global State": "Küresel Durum",
"Help": "Yardım",
"Home page": "Ana Sayfa",
"Home page": "Ana sayfa",
"Ignore": "Yoksay",
"Ignore Patterns": "Kalıpları Yoksay",
"Ignore Permissions": "İzinleri yoksay",
"Incoming Rate Limit (KiB/s)": "İndirme Oranı Limiti (KiB/s)",
"Incoming Rate Limit (KiB/s)": "İndirme Oranı Sınırı (KiB/s)",
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Yanlış yapılandırma klasör içeriğine zarar verebilir ve Syncthing'i çalışamaz hale getirebilir.",
"Introducer": "Tanıtıcı",
"Inversion of the given condition (i.e. do not exclude)": "Verilen koşulun ters çevirilmesi (örneğin: yok sayma)",
"Keep Versions": "Sürümleri Tut",
"Largest First": "En büyük olan önce",
"Last File Received": "Alınan Son Dosya",
"Last Scan": "Last Scan",
"Last seen": "Son Görülen",
"Last Scan": "Son Tarama",
"Last seen": "Son görülme",
"Later": "Sonra",
"Listeners": "Listeners",
"Latest Change": "Latest Change",
"Listeners": "Dinleyiciler",
"Local Discovery": "Yerel Discovery",
"Local State": "Yerel Durum",
"Local State (Total)": "Yerel Durum (Toplamı)",
"Major Upgrade": "Birincil Yükseltme",
"Master": "Master",
"Master": "Ana",
"Maximum Age": "Azami Süre",
"Metadata Only": "Sadece Üstveri",
"Metadata Only": "Yalnızca Üstveri",
"Minimum Free Disk Space": "En Az Boş Disk Alanı",
"Move to top of queue": "Kuyruğun başına taşı",
"Multi level wildcard (matches multiple directory levels)": "Çoklu düzey wildcard (çok sayıda dizin düzeyinde eşleşme)",
"Never": "Asla",
"New Device": "Yeni Cihaz",
"New Device": "Yeni Aygıt",
"New Folder": "Yeni Klasör",
"Newest First": "En yeni olan önce",
"No": "Hayır",
"No File Versioning": "Dosya Sürümlendirmesi Yok",
"Normal": "Normal",
"Normal": "Olağan",
"Notice": "Uyarı",
"OK": "Tamam",
"Off": "Kapalı",
"Oldest First": "En eski olan önce",
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
"Optional descriptive label for the folder. Can be different on each device.": "Klasör için isteğe bağlııklayıcı etiket. Her aygıtta başka olabilir.",
"Options": "Seçenekler",
"Out of Sync": "Eşzamanlama Dışı",
"Out of Sync Items": "Eşzamanlama dışında kalan Öğeler",
"Out of Sync Items": "Eşzamanlama Dışında Kalan Ögeler",
"Outgoing Rate Limit (KiB/s)": "Yükleme hız sınırı (KB/sn)",
"Override Changes": "Değişiklikleri Geçersiz kıl",
"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": "Yerel bilgisayardaki klasöre ulaşım yolu. Klasör yoksa yaratılacak. Tilde (~) karakterinin kısayol olarak kullanılabileceği yol",
@@ -137,7 +140,7 @@
"Random": "Rastgele",
"Reduced by ignore patterns": "Reduced by ignore patterns",
"Release Notes": "Sürüm Notları",
"Remote Devices": "Remote Devices",
"Remote Devices": "Uzak Aygıtlar",
"Remove": "Kaldır",
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
"Rescan": "Tekrar Tara",
@@ -151,23 +154,25 @@
"Save": "Kaydet",
"Scan Time Remaining": "Kalan Tarama Zamanı",
"Scanning": "Taranıyor",
"Select the devices to share this folder with.": "Bu klasörü paylaşacağın cihazları seç.",
"Select the folders to share with this device.": "Bu cihazla paylaşılacak klasörleri seç.",
"Select the devices to share this folder with.": "Bu klasörü paylaşacağın aygıtları seç.",
"Select the folders to share with this device.": "Bu aygıtla paylaşılacak klasörleri seç.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "Ayarlar",
"Share": "Paylaş",
"Share Folder": "Paylaşım Klasörü",
"Share Folders With Device": "Klasörü Cihazla Paylaş",
"Share With Devices": "Cihazlar İle Paylaş",
"Share this folder?": "Bu klasörü paylaşmak istiyor musun?",
"Share Folders With Device": "Klasörü Aygıtla Paylaş",
"Share With Devices": "Aygıtlar İle Paylaş",
"Share this folder?": "Bu klasörü paylaş?",
"Shared With": "Paylaşılan düğümler",
"Show ID": "ID Göster",
"Show QR": "QR Göster",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Küme durumunda Cihaz ID yerine bunu göster. Varsayılan isim isteğe bağlı olarak diğer cihazlara ilan edilecektir.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Küme durumunda Cihaz ID yerine bunu göster. Eğer düğüm ismi boş bırakılırsa düğüm ismi güncellenip ilan edilecektir.",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Küme durumunda Aygıt ID yerine bunu göster. Varsayılan ad isteğe bağlı olarak diğer aygıtlara ilan edilecektir.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Küme durumunda Aygıt ID yerine bunu göster. Eğer düğüm adı boş bırakılırsa düğüm adı güncellenip ilan edilecektir.",
"Shutdown": "Kapat",
"Shutdown Complete": "Kapatma İşlemi Tamamlandı",
"Simple File Versioning": "Basit Dosya Sürümlendirme",
"Single level wildcard (matches within a directory only)": "Tekli düzey wildcard (sadece bir dizin içinde eşleşme)",
"Single level wildcard (matches within a directory only)": "Tekli düzey wildcard (yalnızca bir dizin içinde eşleşme)",
"Smallest First": "En küçük olan önce",
"Source Code": "Kaynak Kodu",
"Staggered File Versioning": "Aşamalı Dosya Sürümlendirme",
@@ -186,16 +191,16 @@
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing yönetici arayüzü parolasız olarak uzaktan erişime izin verilecek şekilde yapılandırıldı.",
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Yapılandırma kaydedildi ancak etkinleştirilmedi. Etkinleştirmek için Syncthing yeniden başlatılmalı.",
"The device ID cannot be blank.": "Cihaz ID boş olamaz.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Buraya girilecek olan aygıt ID'si diğer cihazlarda, \"Eylemler > ID Göster\" penceresinde bulunabilir. Boşluklar ve çizgiler isteğe bağlıdır (yoksayılmış).",
"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.": "Şifrelenmiş kullanım bilgisi günlük olarak gönderilir. Platform, klasör büyüklüğü ve uygulama sürümü hakkında bilgi toplanır. Toplanan bilgi çeşidi değişecek olursa, sizden tekrar onay istenecek.",
"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.": "Girilen cihaz ID'si geçerli gibi gözükmüyor. 52 ya da 56 karakter uzunluğunda, harf ve rakamlardan oluşmalı. Boşlukların ve kısa çizgilerin olup olmaması önemli değildir.",
"The device ID cannot be blank.": "Aygıt ID boş olamaz.",
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Buraya girilecek olan aygıt ID'si diğer aygıtlarda, \"Eylemler > ID Göster\" penceresinde bulunabilir. Boşluklar ve çizgiler isteğe bağlıdır (yoksayılmış).",
"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.": "Şifrelenmiş kullanım bilgisi günlük olarak gönderilir. Platform, klasör büyüklüğü ve uygulama sürümü hakkında bilgi toplanır. Toplanan bilgi türü değişecek olursa, sizden yeniden onay istenecek.",
"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.": "Girilen aygıt ID'si geçerli gibi gözükmüyor. 52 ya da 56 karakter uzunluğunda, harf ve rakamlardan oluşmalı. Boşlukların ve kısa çizgilerin olup olmaması önemli değildir.",
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "İlk komut satırı parametresi klasör yoludur; ikinci parametre ise klasördeki göreceli yoldur. ",
"The folder ID cannot be blank.": "Klasör ID boş olamaz.",
"The folder ID must be unique.": "Klasör ID benzersiz olmalıdır.",
"The folder path cannot be blank.": "Klasör dizini boş bırakılamaz.",
"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.": "Kullanılan zaman aralıkları: ilk bir saat zarfında her 30 saniyede bir, ilk gün zarfında saatte bir, ilk 30 gün zarfında her gün, azami süreye kadar geçen zamanda ise her hafta yeni bir sürüm değeri oluşturulur/tutulur.",
"The following items could not be synchronized.": "Aşağıdaki öğelerin eşzamanlama işlemi gerçekleştirilemedi.",
"The following items could not be synchronized.": "Aşağıdaki ögelerin eşzamanlama işlemi gerçekleştirilemedi.",
"The maximum age must be a number and cannot be blank.": "Azami süre tanımı boş bırakılmamalı ve bir sayı olarak tanımlanmalıdır.",
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Bir sürümün tutulması için belirlenen azami süre (sürümleri sürekli olarak tutabilmek için 0 değeri atayın)",
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "En az boş disk alanı yüzde olarak 0 ve 100 (dahil) arasında kalan pozitif bir sayıyla tanımlanmalıdır.",
@@ -203,11 +208,11 @@
"The number of days to keep files in the trash can. Zero means forever.": "Dosyaları çöp kutusunda tutma süresini tanımlayan gün sayısı. Sıfır devamlı/sürekli anlamına gelir.",
"The number of old versions to keep, per file.": "Dosya başına saklanacak/tutulacak eski sürüm sayısı.",
"The number of versions must be a number and cannot be blank.": "Sürümlerin sayısı sayı olmalı ve boş bırakılamaz.",
"The path cannot be blank.": "Dizin yolu boş bırakılamaz.",
"The path cannot be blank.": "Yol boş bırakılamaz.",
"The rate limit must be a non-negative number (0: no limit)": "Hız sınırı pozitif bir sayı olmalıdır. (0: sınırsız)",
"The rescan interval must be a non-negative number of seconds.": "Tarama zaman aralığı, saniye cinsinden negatif olmayan bir sayı olmalıdır.",
"They are retried automatically and will be synced when the error is resolved.": "Otomatik olarak yeniden deneniyor; hata giderildiğinde eşzamanlama gerçekleştirilecek.",
"This Device": "This Device",
"They are retried automatically and will be synced when the error is resolved.": "Kendiliğinden yeniden deneniyor; hata giderildiğinde eşzamanlama gerçekleştirilecek.",
"This Device": "Bu Aygıt",
"This can easily give hackers access to read and change any files on your computer.": "Hacker'ların bilgisayarındaki dosyaları okuma ve değiştirme yetkisine kolayca erişebilmelerini sağlayabilir.",
"This is a major version upgrade.": "Birincil sürüm yükseltmesidir.",
"Trash Can File Versioning": "Çöp Kutusu Dosya Sürümleme",
@@ -225,16 +230,16 @@
"Version": "Sürüm",
"Versions Path": "Sürüm Dizin Yolu",
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Sürümler, tanımlı azami süre veya belirlenen zaman aralığı için izin verilen dosya sayısıılmışsa kendiliğinden silinir.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Yeni bir cihaz eklendiğinde, bu cihazın karşı tarafa da eklenmesi gerektiğini unutmayın.",
"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.": "Yeni bir klasör eklendiğinde, Klasör ID'nin klasörleri cihazlar arasında bağlantılandırmak için kullanıldığını unutmayın. Klasör ID'ler büyük - küçük harf duyarlıdır ve bütün cihazlarda tamı tamına eşleşmelidir.",
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Uyarı, bu yol var olan bir klasörün \"{{otherFolder}}\" alt klasörüdür.",
"When adding a new device, keep in mind that this device must be added on the other side too.": "Yeni bir aygıt eklendiğinde, bu aygıtın karşı tarafa da eklenmesi gerektiğini unutmayın.",
"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.": "Yeni bir klasör eklendiğinde, Klasör ID'nin klasörleri aygıtlar arasında bağlantılandırmak için kullanıldığını unutmayın. Klasör ID'ler büyük - küçük harf duyarlıdır ve tüm aygıtlarda tamı tamına eşleşmelidir.",
"Yes": "Evet",
"You must keep at least one version.": "En az bir sürümü tutmalısınız.",
"days": "günler",
"directories": "directories",
"files": "files",
"days": "gün",
"directories": "dizin",
"files": "dosya",
"full documentation": "belgelendirme içeriğinin tümü",
"items": "öğel",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} \"{{folder}}\" klasörünü paylaşmak istiyor.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}}, \"{{folderlabel}}\" ({{folder}}) klasörünü paylaşmak istiyor."
}

View File

@@ -52,6 +52,8 @@
"Downloaded": "Завантажено",
"Downloading": "Завантаження",
"Edit": "Редагувати",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Редагування",
"Enable NAT traversal": "Увімкнути NAT traversal",
"Enable Relaying": "Увімкнути ретрансляцію (relaying)",
@@ -95,6 +97,7 @@
"Last Scan": "Останнє сканування",
"Last seen": "З’являвся останній раз",
"Later": "Пізніше",
"Latest Change": "Latest Change",
"Listeners": "Приймачі (TCP & Relay)",
"Local Discovery": "Локальне виявлення (LAN)",
"Local State": "Локальний статус",
@@ -153,6 +156,8 @@
"Scanning": "Сканування",
"Select the devices to share this folder with.": "Оберіть пристрої, які матимуть доступ до цієї директорії.",
"Select the folders to share with this device.": "Оберіть директорії до яких матиме доступ цей пристрій.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "Налаштування",
"Share": "Розповсюдити ",
"Share Folder": "Розповсюдити каталог",

View File

@@ -52,6 +52,8 @@
"Downloaded": "Đã tải xuống",
"Downloading": "Đang tải xuống",
"Edit": "Chỉnh sửa",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "Đang ch.sửa",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "Bật chế độ ch.tiếp",
@@ -95,6 +97,7 @@
"Last Scan": "Last Scan",
"Last seen": "Thấy lần cuối",
"Later": "Để sau",
"Latest Change": "Latest Change",
"Listeners": "Listeners",
"Local Discovery": "Dò tìm cục bộ",
"Local State": "Tr.thái cục bộ",
@@ -153,6 +156,8 @@
"Scanning": "Đang quét",
"Select the devices to share this folder with.": "Chọn các thiết bị để chia sẻ thư mục này.",
"Select the folders to share with this device.": "Chọn các thư mục để chia sẻ với thiết bị này.",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "Cài đặt",
"Share": "Chia sẻ",
"Share Folder": "Chia sẻ th.mục",

View File

@@ -52,6 +52,8 @@
"Downloaded": "已下载",
"Downloading": "下载中",
"Edit": "选项",
"Edit Device": "编辑设备",
"Edit Folder": "编辑文件夹",
"Editing": "正在编辑",
"Enable NAT traversal": "启用 NAT 遍历",
"Enable Relaying": "开启中继",
@@ -78,7 +80,7 @@
"GUI Listen Addresses": "图形管理界面监听地址",
"Generate": "生成",
"Global Discovery": "在互联网上寻找设备\n",
"Global Discovery Servers": "全发现服务器",
"Global Discovery Servers": "全发现服务器",
"Global State": "全局状态",
"Help": "帮助",
"Home page": "主页",
@@ -95,6 +97,7 @@
"Last Scan": "最后扫描",
"Last seen": "最后可见",
"Later": "稍后",
"Latest Change": "最后更改",
"Listeners": "侦听程序",
"Local Discovery": "在局域网上寻找设备",
"Local State": "本地状态",
@@ -153,6 +156,8 @@
"Scanning": "扫描中",
"Select the devices to share this folder with.": "选择将本文件夹共享给哪些设备",
"Select the folders to share with this device.": "选择与该设备共享的文件夹。",
"Send & Receive": "发送与接收",
"Send Only": "仅发送",
"Settings": "设置",
"Share": "共享",
"Share Folder": "共享文件夹",

View File

@@ -52,6 +52,8 @@
"Downloaded": "已下載",
"Downloading": "正在下載",
"Edit": "編輯",
"Edit Device": "Edit Device",
"Edit Folder": "Edit Folder",
"Editing": "正在編輯",
"Enable NAT traversal": "Enable NAT traversal",
"Enable Relaying": "啟用中繼",
@@ -95,6 +97,7 @@
"Last Scan": "Last Scan",
"Last seen": "最後發現時間",
"Later": "稍後",
"Latest Change": "Latest Change",
"Listeners": "Listeners",
"Local Discovery": "本機探索",
"Local State": "本機狀態",
@@ -153,6 +156,8 @@
"Scanning": "正在掃描",
"Select the devices to share this folder with.": "選擇要共享這個資料夾的裝置。",
"Select the folders to share with this device.": "選擇要共享這個資料夾的裝置。",
"Send & Receive": "Send & Receive",
"Send Only": "Send Only",
"Settings": "設定",
"Share": "分享",
"Share Folder": "分享資料夾",

View File

@@ -1 +1 @@
var langPrettyprint = {"bg":"Bulgarian","ca":"Catalan","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","id":"Indonesian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ru":"Russian","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","vi":"Vietnamese","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
var langPrettyprint = {"bg":"Bulgarian","ca":"Catalan","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","eu":"Basque","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","id":"Indonesian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","nn":"Norwegian Nynorsk","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ru":"Russian","sv":"Swedish","tr":"Turkish","uk":"Ukrainian","vi":"Vietnamese","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}

View File

@@ -1 +1 @@
var validLangs = ["bg","ca","ca@valencia","cs","da","de","el","en","en-GB","es","es-ES","fi","fr","fr-CA","fy","hu","id","it","ja","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ru","sv","tr","uk","vi","zh-CN","zh-TW"]
var validLangs = ["bg","ca","ca@valencia","cs","da","de","el","en","en-GB","es","es-ES","eu","fi","fr","fr-CA","fy","hu","id","it","ja","ko-KR","lt","nb","nl","nn","pl","pt-BR","pt-PT","ru","sv","tr","uk","vi","zh-CN","zh-TW"]

View File

@@ -236,7 +236,10 @@
</h3>
</div>
<div class="panel-body">
<p ng-repeat="err in errorList()"><small>{{err.when | date:"yyyy-MM-dd HH:mm:ss"}}:</small> {{friendlyDevices(err.message)}}</p>
<p ng-repeat="err in errorList()">
<small>{{err.when | date:"yyyy-MM-dd HH:mm:ss"}}:</small>
<span ng-bind-html="friendlyDevices(err.message) | linky: '_blank'"></span>
</p>
</div>
<div class="panel-footer">
<button type="button" class="btn btn-sm btn-default pull-right" ng-click="clearErrors()">
@@ -311,18 +314,22 @@
<tr>
<th><span class="fa fa-fw fa-globe"></span>&nbsp;<span translate>Global State</span></th>
<td class="text-right">
{{model[folder.id].globalFiles | alwaysNumber}} <span translate>files</span>,
{{model[folder.id].globalDirectories | alwaysNumber}} <span translate>directories</span>,
~{{model[folder.id].globalBytes | binary}}B
<span tooltip data-original-title="{{model[folder.id].globalFiles | alwaysNumber}} {{'files' | translate}}, {{model[folder.id].globalDirectories | alwaysNumber}} {{'directories' | translate}}, ~{{model[folder.id].globalBytes | binary}}B">
<span class="fa fa-files-o"></span>&nbsp;{{model[folder.id].globalFiles | alwaysNumber}}&ensp;
<span class="fa fa-folder-o"></span>&nbsp;{{model[folder.id].globalDirectories | alwaysNumber}}&ensp;
<span class="fa fa-hdd-o"></span>&nbsp;~{{model[folder.id].globalBytes | binary}}B
</span>
</td>
</tr>
<tr>
<th><span class="fa fa-fw fa-home"></span>&nbsp;<span translate>Local State</span></th>
<td class="text-right">
{{model[folder.id].localFiles | alwaysNumber}} <span translate>files</span>,
{{model[folder.id].localDirectories | alwaysNumber}} <span translate>directories</span>,
~{{model[folder.id].localBytes | binary}}B
<span ng-if="model[folder.id].ignorePatterns"><br/><i><small translate class="text-muted">Reduced by ignore patterns</small></i></span>
<span tooltip data-original-title="{{model[folder.id].localFiles | alwaysNumber}} {{'files' | translate}}, {{model[folder.id].localDirectories | alwaysNumber}} {{'directories' | translate}}, ~{{model[folder.id].localBytes | binary}}B">
<span class="fa fa-files-o"></span>&nbsp;{{model[folder.id].localFiles | alwaysNumber}}&ensp;
<span class="fa fa-folder-o"></span>&nbsp;{{model[folder.id].localDirectories | alwaysNumber}}&ensp;
<span class="fa fa-hdd-o"></span>&nbsp;~{{model[folder.id].localBytes | binary}}B
<span ng-if="model[folder.id].ignorePatterns"><br/><i><small translate class="text-muted">Reduced by ignore patterns</small></i></span>
</span>
</td>
</tr>
<tr ng-if="neededItems(folder.id) > 0">
@@ -337,7 +344,7 @@
<span tooltip data-original-title="{{scanRate(folder.id) | binary}}B/s">~ {{scanRemaining(folder.id)}}</span>
</td>
</tr>
<tr ng-if="hasFailedFiles(folder.id))">
<tr ng-if="hasFailedFiles(folder.id)">
<th><span class="fa fa-fw fa-exclamation-circle"></span>&nbsp;<span translate>Failed Items</span></th>
<!-- Show the number of failed items as a link to bring up the list. -->
<td class="text-right">
@@ -393,7 +400,7 @@
</td>
</tr>
<tr ng-if="folder.type != 'readonly' && folderStats[folder.id].lastFile && folderStats[folder.id].lastFile.filename">
<th><span class="fa fa-fw fa-exchange"></span>&nbsp;<span translate>Last File Received</span></th>
<th><span class="fa fa-fw fa-exchange"></span>&nbsp;<span translate>Latest Change</span></th>
<td class="text-right">
<span tooltip data-original-title="{{folderStats[folder.id].lastFile.filename}} @ {{folderStats[folder.id].lastFile.at | date:'yyyy-MM-dd HH:mm:ss'}}">
<span translate ng-if="!folderStats[folder.id].lastFile.deleted">Updated</span>
@@ -654,6 +661,7 @@
<!-- vendor scripts -->
<script src="vendor/jquery/jquery-2.2.2.js"></script>
<script src="vendor/angular/angular.js"></script>
<script src="vendor/angular/angular-sanitize.js"></script>
<script src="vendor/angular/angular-translate.js"></script>
<script src="vendor/angular/angular-translate-loader-static-files.js"></script>
<script src="vendor/angular/angular-dirPagination.js"></script>

View File

@@ -10,7 +10,7 @@
var syncthing = angular.module('syncthing', [
'angularUtils.directives.dirPagination',
'pascalprecht.translate',
'pascalprecht.translate', 'ngSanitize',
'syncthing.core'
]);

View File

@@ -12,7 +12,7 @@
<p translate>Copyright &copy; 2014-2016 the following Contributors:</p>
<div class="row">
<div class="col-md-12" id="contributor-list">
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Alexandre Viau, Andrew Dunham, Andrey D, Antoine Lamielle, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Laurent Etiemble, Leo Arias, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Simon Frei, Stefan Kuntz, Tim Abell, Tim Howes, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Xavier O., Yannic A.
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Alexandre Viau, Andrew Dunham, Andrey D, Antoine Lamielle, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin White, Jr., Laurent Etiemble, Leo Arias, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Roman Zaynetdinov, Scott Klupfel, Simon Frei, Stefan Kuntz, Tim Abell, Tim Howes, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Unrud, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Xavier O., Yannic A.
</div>
</div>
<hr/>

View File

@@ -33,11 +33,9 @@ angular.module('syncthing.core')
lastID = lastEvent.id;
}
$timeout(function () {
$http.get(urlbase + '/events?since=' + lastID)
.success(successFn)
.error(errorFn);
}, 500, false);
$http.get(urlbase + '/events?since=' + lastID)
.success(successFn)
.error(errorFn);
}
function errorFn (dummy) {

View File

@@ -654,7 +654,7 @@ angular.module('syncthing.core')
if (state === 'error') {
return 'stopped'; // legacy, the state is called "stopped" in the GUI
}
if (state === 'idle' && $scope.model[folderCfg.id].needFiles + $scope.model[folderCfg.id].needDeletes > 0) {
if (state === 'idle' && $scope.neededItems(folderCfg.id) > 0) {
return 'outofsync';
}
if (state === 'scanning') {
@@ -1071,6 +1071,13 @@ angular.module('syncthing.core')
$scope.editDevice = function (deviceCfg) {
$scope.currentDevice = $.extend({}, deviceCfg);
$scope.editingExisting = true;
$scope.willBeReintroducedBy = undefined;
if (deviceCfg.introducedBy) {
var introducerDevice = $scope.findDevice(deviceCfg.introducedBy);
if (introducerDevice && introducerDevice.introducer) {
$scope.willBeReintroducedBy = $scope.deviceName(introducerDevice);
}
}
$scope.currentDevice._addressesStr = deviceCfg.addresses.join(', ');
$scope.currentDevice.selectedFolders = {};
$scope.deviceFolders($scope.currentDevice).forEach(function (folder) {
@@ -1318,6 +1325,7 @@ angular.module('syncthing.core')
rescanIntervalS: 60,
minDiskFreePct: 1,
maxConflicts: 10,
fsync: true,
order: "random",
fileVersioningSelector: "none",
trashcanClean: 0,
@@ -1345,6 +1353,7 @@ angular.module('syncthing.core')
rescanIntervalS: 60,
minDiskFreePct: 1,
maxConflicts: 10,
fsync: true,
order: "random",
fileVersioningSelector: "none",
trashcanClean: 0,

View File

@@ -75,8 +75,13 @@
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
<span class="fa fa-times"></span>&nbsp;<span translate>Close</span>
</button>
<button type="button" class="btn btn-warning pull-left btn-sm" ng-click="deleteDevice()" ng-if="editingExisting">
<span class="fa fa-minus-circle"></span>&nbsp;<span translate>Remove</span>
</button>
<div ng-if="editingExisting" class="pull-left">
<button type="button" class="btn btn-warning btn-sm disabled" ng-if="willBeReintroducedBy" tooltip data-original-title="This device will be reintroduced by {{ willBeReintroducedBy }}">
<span class="fa fa-minus-circle"></span>&nbsp;<span translate>Remove</span>
</button>
<button type="button" class="btn btn-warning btn-sm" ng-click="deleteDevice()" ng-if="!willBeReintroducedBy">
<span class="fa fa-minus-circle"></span>&nbsp;<span translate>Remove</span>
</button>
</div>
</div>
</modal>

View File

@@ -0,0 +1,647 @@
/**
* @license AngularJS v1.2.27
* (c) 2010-2014 Google, Inc. http://angularjs.org
* License: MIT
*/
(function(window, angular, undefined) {'use strict';
var $sanitizeMinErr = angular.$$minErr('$sanitize');
/**
* @ngdoc module
* @name ngSanitize
* @description
*
* # ngSanitize
*
* The `ngSanitize` module provides functionality to sanitize HTML.
*
*
* <div doc-module-components="ngSanitize"></div>
*
* See {@link ngSanitize.$sanitize `$sanitize`} for usage.
*/
/*
* HTML Parser By Misko Hevery (misko@hevery.com)
* based on: HTML Parser By John Resig (ejohn.org)
* Original code by Erik Arvidsson, Mozilla Public License
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
*
* // Use like so:
* htmlParser(htmlString, {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* });
*
*/
/**
* @ngdoc service
* @name $sanitize
* @kind function
*
* @description
* The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
* then serialized back to properly escaped html string. This means that no unsafe input can make
* it into the returned string, however, since our parser is more strict than a typical browser
* parser, it's possible that some obscure input, which would be recognized as valid HTML by a
* browser, won't make it through the sanitizer.
* The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
* `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
*
* @param {string} html Html input.
* @returns {string} Sanitized html.
*
* @example
<example module="sanitizeExample" deps="angular-sanitize.js">
<file name="index.html">
<script>
angular.module('sanitizeExample', ['ngSanitize'])
.controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
$scope.snippet =
'<p style="color:blue">an html\n' +
'<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
'snippet</p>';
$scope.deliberatelyTrustDangerousSnippet = function() {
return $sce.trustAsHtml($scope.snippet);
};
}]);
</script>
<div ng-controller="ExampleController">
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
<table>
<tr>
<td>Directive</td>
<td>How</td>
<td>Source</td>
<td>Rendered</td>
</tr>
<tr id="bind-html-with-sanitize">
<td>ng-bind-html</td>
<td>Automatically uses $sanitize</td>
<td><pre>&lt;div ng-bind-html="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng-bind-html="snippet"></div></td>
</tr>
<tr id="bind-html-with-trust">
<td>ng-bind-html</td>
<td>Bypass $sanitize by explicitly trusting the dangerous value</td>
<td>
<pre>&lt;div ng-bind-html="deliberatelyTrustDangerousSnippet()"&gt;
&lt;/div&gt;</pre>
</td>
<td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
</tr>
<tr id="bind-default">
<td>ng-bind</td>
<td>Automatically escapes</td>
<td><pre>&lt;div ng-bind="snippet"&gt;<br/>&lt;/div&gt;</pre></td>
<td><div ng-bind="snippet"></div></td>
</tr>
</table>
</div>
</file>
<file name="protractor.js" type="protractor">
it('should sanitize the html snippet by default', function() {
expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
});
it('should inline raw snippet if bound to a trusted value', function() {
expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
toBe("<p style=\"color:blue\">an html\n" +
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
"snippet</p>");
});
it('should escape snippet without any filter', function() {
expect(element(by.css('#bind-default div')).getInnerHtml()).
toBe("&lt;p style=\"color:blue\"&gt;an html\n" +
"&lt;em onmouseover=\"this.textContent='PWN3D!'\"&gt;click here&lt;/em&gt;\n" +
"snippet&lt;/p&gt;");
});
it('should update', function() {
element(by.model('snippet')).clear();
element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
toBe('new <b>text</b>');
expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
'new <b onclick="alert(1)">text</b>');
expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
"new &lt;b onclick=\"alert(1)\"&gt;text&lt;/b&gt;");
});
</file>
</example>
*/
function $SanitizeProvider() {
this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
return function(html) {
var buf = [];
htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
return !/^unsafe/.test($$sanitizeUri(uri, isImage));
}));
return buf.join('');
};
}];
}
function sanitizeText(chars) {
var buf = [];
var writer = htmlSanitizeWriter(buf, angular.noop);
writer.chars(chars);
return buf.join('');
}
// Regular Expressions for parsing tags and attributes
var START_TAG_REGEXP =
/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,
END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/,
ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
BEGIN_TAG_REGEXP = /^</,
BEGING_END_TAGE_REGEXP = /^<\//,
COMMENT_REGEXP = /<!--(.*?)-->/g,
DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
// Match everything outside of normal chars and " (quote character)
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
// Good source of info about elements and attributes
// http://dev.w3.org/html5/spec/Overview.html#semantics
// http://simon.html5.org/html-elements
// Safe Void Elements - HTML5
// http://dev.w3.org/html5/spec/Overview.html#void-elements
var voidElements = makeMap("area,br,col,hr,img,wbr");
// Elements that you can, intentionally, leave open (and which close themselves)
// http://dev.w3.org/html5/spec/Overview.html#optional-tags
var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
optionalEndTagInlineElements = makeMap("rp,rt"),
optionalEndTagElements = angular.extend({},
optionalEndTagInlineElements,
optionalEndTagBlockElements);
// Safe Block Elements - HTML5
var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
"aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
"h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
// Inline Elements - HTML5
var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
"bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
"samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
// Special Elements (can contain anything)
var specialElements = makeMap("script,style");
var validElements = angular.extend({},
voidElements,
blockElements,
inlineElements,
optionalEndTagElements);
//Attributes that have href and hence need to be sanitized
var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap");
var validAttrs = angular.extend({}, uriAttrs, makeMap(
'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+
'scope,scrolling,shape,size,span,start,summary,target,title,type,'+
'valign,value,vspace,width'));
function makeMap(str) {
var obj = {}, items = str.split(','), i;
for (i = 0; i < items.length; i++) obj[items[i]] = true;
return obj;
}
/**
* @example
* htmlParser(htmlString, {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* });
*
* @param {string} html string
* @param {object} handler
*/
function htmlParser( html, handler ) {
if (typeof html !== 'string') {
if (html === null || typeof html === 'undefined') {
html = '';
} else {
html = '' + html;
}
}
var index, chars, match, stack = [], last = html, text;
stack.last = function() { return stack[ stack.length - 1 ]; };
while ( html ) {
text = '';
chars = true;
// Make sure we're not in a script or style element
if ( !stack.last() || !specialElements[ stack.last() ] ) {
// Comment
if ( html.indexOf("<!--") === 0 ) {
// comments containing -- are not allowed unless they terminate the comment
index = html.indexOf("--", 4);
if ( index >= 0 && html.lastIndexOf("-->", index) === index) {
if (handler.comment) handler.comment( html.substring( 4, index ) );
html = html.substring( index + 3 );
chars = false;
}
// DOCTYPE
} else if ( DOCTYPE_REGEXP.test(html) ) {
match = html.match( DOCTYPE_REGEXP );
if ( match ) {
html = html.replace( match[0], '');
chars = false;
}
// end tag
} else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
match = html.match( END_TAG_REGEXP );
if ( match ) {
html = html.substring( match[0].length );
match[0].replace( END_TAG_REGEXP, parseEndTag );
chars = false;
}
// start tag
} else if ( BEGIN_TAG_REGEXP.test(html) ) {
match = html.match( START_TAG_REGEXP );
if ( match ) {
// We only have a valid start-tag if there is a '>'.
if ( match[4] ) {
html = html.substring( match[0].length );
match[0].replace( START_TAG_REGEXP, parseStartTag );
}
chars = false;
} else {
// no ending tag found --- this piece should be encoded as an entity.
text += '<';
html = html.substring(1);
}
}
if ( chars ) {
index = html.indexOf("<");
text += index < 0 ? html : html.substring( 0, index );
html = index < 0 ? "" : html.substring( index );
if (handler.chars) handler.chars( decodeEntities(text) );
}
} else {
html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
function(all, text){
text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
if (handler.chars) handler.chars( decodeEntities(text) );
return "";
});
parseEndTag( "", stack.last() );
}
if ( html == last ) {
throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
"of html: {0}", html);
}
last = html;
}
// Clean up any remaining tags
parseEndTag();
function parseStartTag( tag, tagName, rest, unary ) {
tagName = angular.lowercase(tagName);
if ( blockElements[ tagName ] ) {
while ( stack.last() && inlineElements[ stack.last() ] ) {
parseEndTag( "", stack.last() );
}
}
if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) {
parseEndTag( "", tagName );
}
unary = voidElements[ tagName ] || !!unary;
if ( !unary )
stack.push( tagName );
var attrs = {};
rest.replace(ATTR_REGEXP,
function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
var value = doubleQuotedValue
|| singleQuotedValue
|| unquotedValue
|| '';
attrs[name] = decodeEntities(value);
});
if (handler.start) handler.start( tagName, attrs, unary );
}
function parseEndTag( tag, tagName ) {
var pos = 0, i;
tagName = angular.lowercase(tagName);
if ( tagName )
// Find the closest opened tag of the same type
for ( pos = stack.length - 1; pos >= 0; pos-- )
if ( stack[ pos ] == tagName )
break;
if ( pos >= 0 ) {
// Close all the open elements, up the stack
for ( i = stack.length - 1; i >= pos; i-- )
if (handler.end) handler.end( stack[ i ] );
// Remove the open elements from the stack
stack.length = pos;
}
}
}
var hiddenPre=document.createElement("pre");
var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/;
/**
* decodes all entities into regular string
* @param value
* @returns {string} A string with decoded entities.
*/
function decodeEntities(value) {
if (!value) { return ''; }
// Note: IE8 does not preserve spaces at the start/end of innerHTML
// so we must capture them and reattach them afterward
var parts = spaceRe.exec(value);
var spaceBefore = parts[1];
var spaceAfter = parts[3];
var content = parts[2];
if (content) {
hiddenPre.innerHTML=content.replace(/</g,"&lt;");
// innerText depends on styling as it doesn't display hidden elements.
// Therefore, it's better to use textContent not to cause unnecessary
// reflows. However, IE<9 don't support textContent so the innerText
// fallback is necessary.
content = 'textContent' in hiddenPre ?
hiddenPre.textContent : hiddenPre.innerText;
}
return spaceBefore + content + spaceAfter;
}
/**
* Escapes all potentially dangerous characters, so that the
* resulting string can be safely inserted into attribute or
* element text.
* @param value
* @returns {string} escaped text
*/
function encodeEntities(value) {
return value.
replace(/&/g, '&amp;').
replace(SURROGATE_PAIR_REGEXP, function (value) {
var hi = value.charCodeAt(0);
var low = value.charCodeAt(1);
return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
}).
replace(NON_ALPHANUMERIC_REGEXP, function(value){
return '&#' + value.charCodeAt(0) + ';';
}).
replace(/</g, '&lt;').
replace(/>/g, '&gt;');
}
/**
* create an HTML/XML writer which writes to buffer
* @param {Array} buf use buf.jain('') to get out sanitized html string
* @returns {object} in the form of {
* start: function(tag, attrs, unary) {},
* end: function(tag) {},
* chars: function(text) {},
* comment: function(text) {}
* }
*/
function htmlSanitizeWriter(buf, uriValidator){
var ignore = false;
var out = angular.bind(buf, buf.push);
return {
start: function(tag, attrs, unary){
tag = angular.lowercase(tag);
if (!ignore && specialElements[tag]) {
ignore = tag;
}
if (!ignore && validElements[tag] === true) {
out('<');
out(tag);
angular.forEach(attrs, function(value, key){
var lkey=angular.lowercase(key);
var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
if (validAttrs[lkey] === true &&
(uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
out(' ');
out(key);
out('="');
out(encodeEntities(value));
out('"');
}
});
out(unary ? '/>' : '>');
}
},
end: function(tag){
tag = angular.lowercase(tag);
if (!ignore && validElements[tag] === true) {
out('</');
out(tag);
out('>');
}
if (tag == ignore) {
ignore = false;
}
},
chars: function(chars){
if (!ignore) {
out(encodeEntities(chars));
}
}
};
}
// define ngSanitize module and register $sanitize service
angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
/* global sanitizeText: false */
/**
* @ngdoc filter
* @name linky
* @kind function
*
* @description
* Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
* plain email address links.
*
* Requires the {@link ngSanitize `ngSanitize`} module to be installed.
*
* @param {string} text Input text.
* @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
* @returns {string} Html-linkified text.
*
* @usage
<span ng-bind-html="linky_expression | linky"></span>
*
* @example
<example module="linkyExample" deps="angular-sanitize.js">
<file name="index.html">
<script>
angular.module('linkyExample', ['ngSanitize'])
.controller('ExampleController', ['$scope', function($scope) {
$scope.snippet =
'Pretty text with some links:\n'+
'http://angularjs.org/,\n'+
'mailto:us@somewhere.org,\n'+
'another@somewhere.org,\n'+
'and one more: ftp://127.0.0.1/.';
$scope.snippetWithTarget = 'http://angularjs.org/';
}]);
</script>
<div ng-controller="ExampleController">
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
<table>
<tr>
<td>Filter</td>
<td>Source</td>
<td>Rendered</td>
</tr>
<tr id="linky-filter">
<td>linky filter</td>
<td>
<pre>&lt;div ng-bind-html="snippet | linky"&gt;<br>&lt;/div&gt;</pre>
</td>
<td>
<div ng-bind-html="snippet | linky"></div>
</td>
</tr>
<tr id="linky-target">
<td>linky target</td>
<td>
<pre>&lt;div ng-bind-html="snippetWithTarget | linky:'_blank'"&gt;<br>&lt;/div&gt;</pre>
</td>
<td>
<div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
</td>
</tr>
<tr id="escaped-html">
<td>no filter</td>
<td><pre>&lt;div ng-bind="snippet"&gt;<br>&lt;/div&gt;</pre></td>
<td><div ng-bind="snippet"></div></td>
</tr>
</table>
</file>
<file name="protractor.js" type="protractor">
it('should linkify the snippet with urls', function() {
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
'another@somewhere.org, and one more: ftp://127.0.0.1/.');
expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
});
it('should not linkify snippet without the linky filter', function() {
expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
'another@somewhere.org, and one more: ftp://127.0.0.1/.');
expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
});
it('should update', function() {
element(by.model('snippet')).clear();
element(by.model('snippet')).sendKeys('new http://link.');
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
toBe('new http://link.');
expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
.toBe('new http://link.');
});
it('should work with the target property', function() {
expect(element(by.id('linky-target')).
element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
toBe('http://angularjs.org/');
expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
});
</file>
</example>
*/
angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
var LINKY_URL_REGEXP =
/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"]/,
MAILTO_REGEXP = /^mailto:/;
return function(text, target) {
if (!text) return text;
var match;
var raw = text;
var html = [];
var url;
var i;
while ((match = raw.match(LINKY_URL_REGEXP))) {
// We can not end in these as they are sometimes found at the end of the sentence
url = match[0];
// if we did not match ftp/http/mailto then assume mailto
if (match[2] == match[3]) url = 'mailto:' + url;
i = match.index;
addText(raw.substr(0, i));
addLink(url, match[0].replace(MAILTO_REGEXP, ''));
raw = raw.substring(i + match[0].length);
}
addText(raw);
return $sanitize(html.join(''));
function addText(text) {
if (!text) {
return;
}
html.push(sanitizeText(text));
}
function addLink(url, text) {
html.push('<a ');
if (angular.isDefined(target)) {
html.push('target="');
html.push(target);
html.push('" ');
}
html.push('href="');
html.push(url);
html.push('">');
addText(text);
html.push('</a>');
}
};
}]);
})(window, window.angular);

View File

@@ -51,6 +51,7 @@ go run build.go -goarch armhf deb
mv *.deb "$WORKSPACE"
go run build.go -goarch amd64 snap
go run build.go -goarch armhf snap
go run build.go -goarch arm64 snap
mv *.snap "$WORKSPACE"

View File

@@ -20,7 +20,7 @@ function init {
export GOPATH=$(pwd)
export WORKSPACE="${WORKSPACE:-$GOPATH}"
go version
rm -f *.tar.gz *.zip *.deb
rm -f *.tar.gz *.zip *.deb *.snap
cd src/github.com/syncthing/syncthing
version=$(go run build.go version)

View File

@@ -43,7 +43,7 @@ func (validationError) String() string {
func TestReplaceCommit(t *testing.T) {
w := Wrap("/dev/null", Configuration{Version: 0})
if w.Raw().Version != 0 {
if w.RawCopy().Version != 0 {
t.Fatal("Config incorrect")
}
@@ -57,7 +57,7 @@ func TestReplaceCommit(t *testing.T) {
if w.RequiresRestart() {
t.Fatal("Should not require restart")
}
if w.Raw().Version != CurrentVersion {
if w.RawCopy().Version != CurrentVersion {
t.Fatal("Config should have changed")
}
@@ -76,7 +76,7 @@ func TestReplaceCommit(t *testing.T) {
if !w.RequiresRestart() {
t.Fatal("Should require restart")
}
if w.Raw().Version != CurrentVersion {
if w.RawCopy().Version != CurrentVersion {
t.Fatal("Config should have changed")
}
@@ -92,7 +92,7 @@ func TestReplaceCommit(t *testing.T) {
if !w.RequiresRestart() {
t.Fatal("Should still require restart")
}
if w.Raw().Version != CurrentVersion {
if w.RawCopy().Version != CurrentVersion {
t.Fatal("Config should not have changed")
}
}

View File

@@ -26,7 +26,7 @@ import (
const (
OldestHandledVersion = 10
CurrentVersion = 16
CurrentVersion = 17
MaxRescanIntervalS = 365 * 24 * 60 * 60
)
@@ -254,6 +254,9 @@ func (cfg *Configuration) clean() error {
if cfg.Version == 15 {
convertV15V16(cfg)
}
if cfg.Version == 16 {
convertV16V17(cfg)
}
// Build a list of available devices
existingDevices := make(map[protocol.DeviceID]bool)
@@ -327,6 +330,14 @@ func convertV15V16(cfg *Configuration) {
cfg.Version = 16
}
func convertV16V17(cfg *Configuration) {
for i := range cfg.Folders {
cfg.Folders[i].Fsync = true
}
cfg.Version = 17
}
func convertV13V14(cfg *Configuration) {
// Not using the ignore cache is the new default. Disable it on existing
// configurations.

View File

@@ -104,6 +104,7 @@ func TestDeviceConfig(t *testing.T) {
AutoNormalize: true,
MinDiskFreePct: 1,
MaxConflicts: -1,
Fsync: true,
Versioning: VersioningConfiguration{
Params: map[string]string{},
},
@@ -456,7 +457,7 @@ func TestNewSaveLoad(t *testing.T) {
t.Error(err)
}
if diff, equal := messagediff.PrettyDiff(cfg.Raw(), cfg2.Raw()); !equal {
if diff, equal := messagediff.PrettyDiff(cfg.RawCopy(), cfg2.RawCopy()); !equal {
t.Errorf("Configs are not equal. Diff:\n%s", diff)
}
@@ -482,7 +483,7 @@ func TestCopy(t *testing.T) {
if err != nil {
t.Fatal(err)
}
cfg := wrapper.Raw()
cfg := wrapper.RawCopy()
bsOrig, err := json.MarshalIndent(cfg, "", " ")
if err != nil {
@@ -548,7 +549,7 @@ func TestPullOrder(t *testing.T) {
// Serialize and deserialize again to verify it survives the transformation
buf := new(bytes.Buffer)
cfg := wrapper.Raw()
cfg := wrapper.RawCopy()
cfg.WriteXML(buf)
t.Logf("%s", buf.Bytes())
@@ -611,7 +612,7 @@ func TestDuplicateDevices(t *testing.T) {
t.Fatal(err)
}
if l := len(wrapper.Raw().Devices); l != 3 {
if l := len(wrapper.RawCopy().Devices); l != 3 {
t.Errorf("Incorrect number of devices, %d != 3", l)
}
@@ -755,7 +756,7 @@ func TestSharesRemovedOnDeviceRemoval(t *testing.T) {
t.Errorf("Failed: %s", err)
}
raw := wrapper.Raw()
raw := wrapper.RawCopy()
raw.Devices = raw.Devices[:len(raw.Devices)-1]
if len(raw.Folders[0].Devices) <= len(raw.Devices) {
@@ -767,7 +768,7 @@ func TestSharesRemovedOnDeviceRemoval(t *testing.T) {
t.Errorf("Failed: %s", err)
}
raw = wrapper.Raw()
raw = wrapper.RawCopy()
if len(raw.Folders[0].Devices) > len(raw.Devices) {
t.Error("Unexpected extra device")
}

View File

@@ -9,12 +9,14 @@ package config
import "github.com/syncthing/syncthing/lib/protocol"
type DeviceConfiguration struct {
DeviceID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
Name string `xml:"name,attr,omitempty" json:"name"`
Addresses []string `xml:"address,omitempty" json:"addresses"`
Compression protocol.Compression `xml:"compression,attr" json:"compression"`
CertName string `xml:"certName,attr,omitempty" json:"certName"`
Introducer bool `xml:"introducer,attr" json:"introducer"`
DeviceID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
Name string `xml:"name,attr,omitempty" json:"name"`
Addresses []string `xml:"address,omitempty" json:"addresses"`
Compression protocol.Compression `xml:"compression,attr" json:"compression"`
CertName string `xml:"certName,attr,omitempty" json:"certName"`
Introducer bool `xml:"introducer,attr" json:"introducer"`
SkipIntroductionRemovals bool `xml:"skipIntroductionRemovals,attr" json:"skipIntroductionRemovals"`
IntroducedBy protocol.DeviceID `xml:"introducedBy,attr" json:"introducedBy"`
}
func NewDeviceConfiguration(id protocol.DeviceID, name string) DeviceConfiguration {

View File

@@ -7,6 +7,7 @@
package config
import (
"fmt"
"os"
"path/filepath"
"runtime"
@@ -38,6 +39,7 @@ type FolderConfiguration struct {
MaxConflicts int `xml:"maxConflicts" json:"maxConflicts"`
DisableSparseFiles bool `xml:"disableSparseFiles" json:"disableSparseFiles"`
DisableTempIndexes bool `xml:"disableTempIndexes" json:"disableTempIndexes"`
Fsync bool `xml:"fsync" json:"fsync"`
cachedPath string
@@ -45,7 +47,8 @@ type FolderConfiguration struct {
}
type FolderDeviceConfiguration struct {
DeviceID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
DeviceID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
IntroducedBy protocol.DeviceID `xml:"introducedBy,attr" json:"introducedBy"`
}
func NewFolderConfiguration(id, path string) FolderConfiguration {
@@ -84,6 +87,9 @@ func (f *FolderConfiguration) CreateMarker() error {
return err
}
fd.Close()
if err := osutil.SyncDir(filepath.Dir(marker)); err != nil {
l.Infof("fsync %q failed: %v", filepath.Dir(marker), err)
}
osutil.HideFile(marker)
}
@@ -98,6 +104,10 @@ func (f *FolderConfiguration) HasMarker() bool {
return true
}
func (f FolderConfiguration) Description() string {
return fmt.Sprintf("%q (%s)", f.Label, f.ID)
}
func (f *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
deviceIDs := make([]protocol.DeviceID, len(f.Devices))
for i, n := range f.Devices {

15
lib/config/testdata/v17.xml vendored Normal file
View File

@@ -0,0 +1,15 @@
<configuration version="17">
<folder id="test" path="testdata" type="readonly" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
<minDiskFreePct>1</minDiskFreePct>
<maxConflicts>-1</maxConflicts>
<fsync>true</fsync>
</folder>
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="metadata">
<address>tcp://a</address>
</device>
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="metadata">
<address>tcp://b</address>
</device>
</configuration>

View File

@@ -120,9 +120,11 @@ func (w *Wrapper) Unsubscribe(c Committer) {
w.mut.Unlock()
}
// Raw returns the currently wrapped Configuration object.
func (w *Wrapper) Raw() Configuration {
return w.cfg
// RawCopy returns a copy of the currently wrapped Configuration object.
func (w *Wrapper) RawCopy() Configuration {
w.mut.Lock()
defer w.mut.Unlock()
return w.cfg.Copy()
}
// Replace swaps the current configuration object for the given one.
@@ -159,7 +161,7 @@ func (w *Wrapper) replaceLocked(to Configuration) error {
func (w *Wrapper) notifyListeners(from, to Configuration) {
for _, sub := range w.subs {
go w.notifyListener(sub, from, to)
go w.notifyListener(sub, from.Copy(), to.Copy())
}
}
@@ -207,6 +209,27 @@ func (w *Wrapper) SetDevice(dev DeviceConfiguration) error {
return w.replaceLocked(newCfg)
}
// RemoveDevice removes the device from the configuration
func (w *Wrapper) RemoveDevice(id protocol.DeviceID) error {
w.mut.Lock()
defer w.mut.Unlock()
newCfg := w.cfg.Copy()
removed := false
for i := range newCfg.Devices {
if newCfg.Devices[i].DeviceID == id {
newCfg.Devices = append(newCfg.Devices[:i], newCfg.Devices[i+1:]...)
removed = true
break
}
}
if !removed {
return nil
}
return w.replaceLocked(newCfg)
}
// Folders returns a map of folders. Folder structures should not be changed,
// other than for the purpose of updating via SetFolder().
func (w *Wrapper) Folders() map[string]FolderConfiguration {
@@ -302,7 +325,7 @@ func (w *Wrapper) Device(id protocol.DeviceID) (DeviceConfiguration, bool) {
// Save writes the configuration to disk, and generates a ConfigSaved event.
func (w *Wrapper) Save() error {
fd, err := osutil.CreateAtomic(w.path, 0600)
fd, err := osutil.CreateAtomic(w.path)
if err != nil {
l.Debugln("CreateAtomic:", err)
return err

View File

@@ -28,21 +28,21 @@ type relayDialer struct {
tlsCfg *tls.Config
}
func (d *relayDialer) Dial(id protocol.DeviceID, uri *url.URL) (IntermediateConnection, error) {
func (d *relayDialer) Dial(id protocol.DeviceID, uri *url.URL) (internalConn, error) {
inv, err := client.GetInvitationFromRelay(uri, id, d.tlsCfg.Certificates, 10*time.Second)
if err != nil {
return IntermediateConnection{}, err
return internalConn{}, err
}
conn, err := client.JoinSession(inv)
if err != nil {
return IntermediateConnection{}, err
return internalConn{}, err
}
err = dialer.SetTCPOptions(conn)
if err != nil {
conn.Close()
return IntermediateConnection{}, err
return internalConn{}, err
}
var tc *tls.Conn
@@ -55,10 +55,10 @@ func (d *relayDialer) Dial(id protocol.DeviceID, uri *url.URL) (IntermediateConn
err = tlsTimedHandshake(tc)
if err != nil {
tc.Close()
return IntermediateConnection{}, err
return internalConn{}, err
}
return IntermediateConnection{tc, "Relay (Client)", relayPriority}, nil
return internalConn{tc, connTypeRelayClient, relayPriority}, nil
}
func (relayDialer) Priority() int {

View File

@@ -30,7 +30,7 @@ type relayListener struct {
uri *url.URL
tlsCfg *tls.Config
conns chan IntermediateConnection
conns chan internalConn
factory listenerFactory
err error
@@ -44,6 +44,7 @@ func (t *relayListener) Serve() {
t.mut.Unlock()
clnt, err := client.NewClient(t.uri, t.tlsCfg.Certificates, nil, 10*time.Second)
invitations := clnt.Invitations()
if err != nil {
t.mut.Lock()
t.err = err
@@ -62,7 +63,7 @@ func (t *relayListener) Serve() {
for {
select {
case inv, ok := <-t.client.Invitations():
case inv, ok := <-invitations:
if !ok {
return
}
@@ -92,7 +93,7 @@ func (t *relayListener) Serve() {
continue
}
t.conns <- IntermediateConnection{tc, "Relay (Server)", relayPriority}
t.conns <- internalConn{tc, connTypeRelayServer, relayPriority}
// Poor mans notifier that informs the connection service that the
// relay URI has changed. This can only happen when we connect to a
@@ -166,7 +167,7 @@ func (t *relayListener) String() string {
type relayListenerFactory struct{}
func (f *relayListenerFactory) New(uri *url.URL, cfg *config.Wrapper, tlsCfg *tls.Config, conns chan IntermediateConnection, natService *nat.Service) genericListener {
func (f *relayListenerFactory) New(uri *url.URL, cfg *config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
return &relayListener{
uri: uri,
tlsCfg: tlsCfg,

View File

@@ -50,7 +50,7 @@ type Service struct {
model Model
tlsCfg *tls.Config
discoverer discover.Finder
conns chan IntermediateConnection
conns chan internalConn
bepProtocolName string
tlsDefaultCommonName string
lans []*net.IPNet
@@ -59,25 +59,30 @@ type Service struct {
natService *nat.Service
natServiceToken *suture.ServiceToken
listenersMut sync.RWMutex
listeners map[string]genericListener
listenerTokens map[string]suture.ServiceToken
listenersMut sync.RWMutex
listeners map[string]genericListener
listenerTokens map[string]suture.ServiceToken
listenerSupervisor *suture.Supervisor
curConMut sync.Mutex
currentConnection map[protocol.DeviceID]Connection
currentConnection map[protocol.DeviceID]completeConn
}
func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder,
bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) *Service {
service := &Service{
Supervisor: suture.NewSimple("connections.Service"),
Supervisor: suture.New("connections.Service", suture.Spec{
Log: func(line string) {
l.Infoln(line)
},
}),
cfg: cfg,
myID: myID,
model: mdl,
tlsCfg: tlsCfg,
discoverer: discoverer,
conns: make(chan IntermediateConnection),
conns: make(chan internalConn),
bepProtocolName: bepProtocolName,
tlsDefaultCommonName: tlsDefaultCommonName,
lans: lans,
@@ -87,8 +92,20 @@ func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *
listeners: make(map[string]genericListener),
listenerTokens: make(map[string]suture.ServiceToken),
// A listener can fail twice, rapidly. Any more than that and it
// will be put on suspension for ten minutes. Restarts and changes
// due to config are done by removing and adding services, so are
// not subject to these limitations.
listenerSupervisor: suture.New("c.S.listenerSupervisor", suture.Spec{
Log: func(line string) {
l.Infoln(line)
},
FailureThreshold: 2,
FailureBackoff: 600 * time.Second,
}),
curConMut: sync.NewMutex(),
currentConnection: make(map[protocol.DeviceID]Connection),
currentConnection: make(map[protocol.DeviceID]completeConn),
}
cfg.Subscribe(service)
@@ -111,8 +128,9 @@ func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *
service.Add(serviceFunc(service.connect))
service.Add(serviceFunc(service.handle))
service.Add(service.listenerSupervisor)
raw := cfg.Raw()
raw := cfg.RawCopy()
// Actually starts the listeners and NAT service
service.CommitConfiguration(raw, raw)
@@ -186,7 +204,7 @@ next:
// The Model will return an error for devices that we don't want to
// have a connection with for whatever reason, for example unknown devices.
if err := s.model.OnHello(remoteID, c.RemoteAddr(), hello); err != nil {
l.Infof("Connection from %s at %s (%s) rejected: %v", remoteID, c.RemoteAddr(), c.Type, err)
l.Infof("Connection from %s at %s (%s) rejected: %v", remoteID, c.RemoteAddr(), c.Type(), err)
c.Close()
continue
}
@@ -200,7 +218,7 @@ next:
priorityKnown := ok && connected
// Lower priority is better, just like nice etc.
if priorityKnown && ct.Priority > c.Priority {
if priorityKnown && ct.internalConn.priority > c.priority {
l.Debugln("Switching connections", remoteID)
} else if connected {
// We should not already be connected to the other party. TODO: This
@@ -250,9 +268,9 @@ next:
rd = NewReadLimiter(c, s.readRateLimit)
}
name := fmt.Sprintf("%s-%s (%s)", c.LocalAddr(), c.RemoteAddr(), c.Type)
name := fmt.Sprintf("%s-%s (%s)", c.LocalAddr(), c.RemoteAddr(), c.Type())
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
modelConn := Connection{c, protoConn}
modelConn := completeConn{c, protoConn}
l.Infof("Established secure connection to %s at %s", remoteID, name)
l.Debugf("cipher suite: %04X in lan: %t", c.ConnectionState().CipherSuite, !limit)
@@ -276,7 +294,7 @@ func (s *Service) connect() {
var sleep time.Duration
for {
cfg := s.cfg.Raw()
cfg := s.cfg.RawCopy()
bestDialerPrio := 1<<31 - 1 // worse prio won't build on 32 bit
for _, df := range dialers {
@@ -311,7 +329,7 @@ func (s *Service) connect() {
s.curConMut.Unlock()
priorityKnown := ok && connected
if priorityKnown && ct.Priority == bestDialerPrio {
if priorityKnown && ct.internalConn.priority == bestDialerPrio {
// Things are already as good as they can get.
continue
}
@@ -359,8 +377,8 @@ func (s *Service) connect() {
continue
}
if priorityKnown && dialerFactory.Priority() >= ct.Priority {
l.Debugf("Not dialing using %s as priority is less than current connection (%d >= %d)", dialerFactory, dialerFactory.Priority(), ct.Priority)
if priorityKnown && dialerFactory.Priority() >= ct.internalConn.priority {
l.Debugf("Not dialing using %s as priority is less than current connection (%d >= %d)", dialerFactory, dialerFactory.Priority(), ct.internalConn.priority)
continue
}
@@ -417,7 +435,7 @@ func (s *Service) createListener(factory listenerFactory, uri *url.URL) bool {
listener := factory.New(uri, s.cfg, s.tlsCfg, s.conns, s.natService)
listener.OnAddressesChanged(s.logListenAddressesChangedEvent)
s.listeners[uri.String()] = listener
s.listenerTokens[uri.String()] = s.Add(listener)
s.listenerTokens[uri.String()] = s.listenerSupervisor.Add(listener)
return true
}
@@ -481,7 +499,7 @@ func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
for addr, listener := range s.listeners {
if _, ok := seen[addr]; !ok || !listener.Factory().Enabled(to) {
l.Debugln("Stopping listener", addr)
s.Remove(s.listenerTokens[addr])
s.listenerSupervisor.Remove(s.listenerTokens[addr])
delete(s.listenerTokens, addr)
delete(s.listeners, addr)
}

View File

@@ -9,6 +9,7 @@ package connections
import (
"crypto/tls"
"fmt"
"io"
"net"
"net/url"
"time"
@@ -18,19 +19,61 @@ import (
"github.com/syncthing/syncthing/lib/protocol"
)
type IntermediateConnection struct {
*tls.Conn
Type string
Priority int
// Connection is what we expose to the outside. It is a protocol.Connection
// that can be closed and has some metadata.
type Connection interface {
protocol.Connection
io.Closer
Type() string
RemoteAddr() net.Addr
}
type Connection struct {
IntermediateConnection
// completeConn is the aggregation of an internalConn and the
// protocol.Connection running on top of it. It implements the Connection
// interface.
type completeConn struct {
internalConn
protocol.Connection
}
func (c Connection) String() string {
return fmt.Sprintf("%s-%s/%s", c.LocalAddr(), c.RemoteAddr(), c.Type)
// internalConn is the raw TLS connection plus some metadata on where it
// came from (type, priority).
type internalConn struct {
*tls.Conn
connType connType
priority int
}
type connType int
const (
connTypeRelayClient connType = iota
connTypeRelayServer
connTypeTCPClient
connTypeTCPServer
)
func (t connType) String() string {
switch t {
case connTypeRelayClient:
return "relay-client"
case connTypeRelayServer:
return "relay-server"
case connTypeTCPClient:
return "tcp-client"
case connTypeTCPServer:
return "tcp-server"
default:
return "unknown-type"
}
}
func (c internalConn) Type() string {
return c.connType.String()
}
func (c internalConn) String() string {
return fmt.Sprintf("%s-%s/%s", c.LocalAddr(), c.RemoteAddr(), c.connType.String())
}
type dialerFactory interface {
@@ -41,12 +84,12 @@ type dialerFactory interface {
}
type genericDialer interface {
Dial(protocol.DeviceID, *url.URL) (IntermediateConnection, error)
Dial(protocol.DeviceID, *url.URL) (internalConn, error)
RedialFrequency() time.Duration
}
type listenerFactory interface {
New(*url.URL, *config.Wrapper, *tls.Config, chan IntermediateConnection, *nat.Service) genericListener
New(*url.URL, *config.Wrapper, *tls.Config, chan internalConn, *nat.Service) genericListener
Enabled(config.Configuration) bool
}

View File

@@ -30,23 +30,23 @@ type tcpDialer struct {
tlsCfg *tls.Config
}
func (d *tcpDialer) Dial(id protocol.DeviceID, uri *url.URL) (IntermediateConnection, error) {
func (d *tcpDialer) Dial(id protocol.DeviceID, uri *url.URL) (internalConn, error) {
uri = fixupPort(uri)
conn, err := dialer.DialTimeout(uri.Scheme, uri.Host, 10*time.Second)
if err != nil {
l.Debugln(err)
return IntermediateConnection{}, err
return internalConn{}, err
}
tc := tls.Client(conn, d.tlsCfg)
err = tlsTimedHandshake(tc)
if err != nil {
tc.Close()
return IntermediateConnection{}, err
return internalConn{}, err
}
return IntermediateConnection{tc, "TCP (Client)", tcpPriority}, nil
return internalConn{tc, connTypeTCPClient, tcpPriority}, nil
}
func (d *tcpDialer) RedialFrequency() time.Duration {

View File

@@ -32,7 +32,7 @@ type tcpListener struct {
uri *url.URL
tlsCfg *tls.Config
stop chan struct{}
conns chan IntermediateConnection
conns chan internalConn
factory listenerFactory
natService *nat.Service
@@ -115,7 +115,7 @@ func (t *tcpListener) Serve() {
continue
}
t.conns <- IntermediateConnection{tc, "TCP (Server)", tcpPriority}
t.conns <- internalConn{tc, connTypeTCPServer, tcpPriority}
}
}
@@ -173,7 +173,7 @@ func (t *tcpListener) Factory() listenerFactory {
type tcpListenerFactory struct{}
func (f *tcpListenerFactory) New(uri *url.URL, cfg *config.Wrapper, tlsCfg *tls.Config, conns chan IntermediateConnection, natService *nat.Service) genericListener {
func (f *tcpListenerFactory) New(uri *url.URL, cfg *config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
return &tcpListener{
uri: fixupPort(uri),
tlsCfg: tlsCfg,

View File

@@ -128,7 +128,7 @@ func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.
Version: file.Version,
}
insertedAt := -1
var insertedAt int
// Find a position in the list to insert this file. The file at the front
// of the list is the newer, the "global".
for i := range fl.Versions {

View File

@@ -73,7 +73,7 @@ func (s *sizeTracker) addFile(f FileIntf) {
switch {
case f.IsDeleted():
s.Deleted++
case f.IsDirectory():
case f.IsDirectory() && !f.IsSymlink():
s.Directories++
case f.IsSymlink():
s.Symlinks++
@@ -93,7 +93,7 @@ func (s *sizeTracker) removeFile(f FileIntf) {
switch {
case f.IsDeleted():
s.Deleted--
case f.IsDirectory():
case f.IsDirectory() && !f.IsSymlink():
s.Directories--
case f.IsSymlink():
s.Symlinks--

View File

@@ -5,7 +5,7 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:generate go run ../../script/protofmt.go structs.proto
//go:generate protoc --proto_path=../../../../../:../../../../gogo/protobuf/protobuf:. --gogofast_out=. structs.proto
//go:generate protoc -I ../../../../../ -I ../../../../gogo/protobuf/protobuf -I . --gogofast_out=. structs.proto
package db
@@ -50,7 +50,7 @@ func (f FileInfoTruncated) FileSize() int64 {
if f.Deleted {
return 0
}
if f.IsDirectory() {
if f.IsDirectory() || f.IsSymlink() {
return protocol.SyntheticDirectorySize
}
return f.Size

View File

@@ -63,6 +63,7 @@ type FileInfoTruncated struct {
NoPermissions bool `protobuf:"varint,8,opt,name=no_permissions,json=noPermissions,proto3" json:"no_permissions,omitempty"`
Version protocol.Vector `protobuf:"bytes,9,opt,name=version" json:"version"`
Sequence int64 `protobuf:"varint,10,opt,name=sequence,proto3" json:"sequence,omitempty"`
SymlinkTarget string `protobuf:"bytes,17,opt,name=symlink_target,json=symlinkTarget,proto3" json:"symlink_target,omitempty"`
}
func (m *FileInfoTruncated) Reset() { *m = FileInfoTruncated{} }
@@ -225,6 +226,14 @@ func (m *FileInfoTruncated) MarshalTo(data []byte) (int, error) {
i++
i = encodeVarintStructs(data, i, uint64(m.ModifiedNs))
}
if len(m.SymlinkTarget) > 0 {
data[i] = 0x8a
i++
data[i] = 0x1
i++
i = encodeVarintStructs(data, i, uint64(len(m.SymlinkTarget)))
i += copy(data[i:], m.SymlinkTarget)
}
return i, nil
}
@@ -315,6 +324,10 @@ func (m *FileInfoTruncated) ProtoSize() (n int) {
if m.ModifiedNs != 0 {
n += 1 + sovStructs(uint64(m.ModifiedNs))
}
l = len(m.SymlinkTarget)
if l > 0 {
n += 2 + l + sovStructs(uint64(l))
}
return n
}
@@ -785,6 +798,35 @@ func (m *FileInfoTruncated) Unmarshal(data []byte) error {
break
}
}
case 17:
if wireType != 2 {
return fmt.Errorf("proto: wrong wireType = %d for field SymlinkTarget", wireType)
}
var stringLen uint64
for shift := uint(0); ; shift += 7 {
if shift >= 64 {
return ErrIntOverflowStructs
}
if iNdEx >= l {
return io.ErrUnexpectedEOF
}
b := data[iNdEx]
iNdEx++
stringLen |= (uint64(b) & 0x7F) << shift
if b < 0x80 {
break
}
}
intStringLen := int(stringLen)
if intStringLen < 0 {
return ErrInvalidLengthStructs
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.SymlinkTarget = string(data[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipStructs(data[iNdEx:])
@@ -912,32 +954,33 @@ var (
)
var fileDescriptorStructs = []byte{
// 419 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x51, 0xcd, 0xaa, 0xd3, 0x40,
0x18, 0x4d, 0xda, 0xdc, 0x36, 0xfd, 0x62, 0xaf, 0x3a, 0xc8, 0x25, 0x14, 0x4c, 0x2f, 0x05, 0x41,
0x04, 0x53, 0xbd, 0xe2, 0xc6, 0x65, 0x17, 0x05, 0x41, 0x44, 0x46, 0xa9, 0xcb, 0xd2, 0x64, 0xa6,
0xe9, 0x40, 0x32, 0x13, 0x33, 0x93, 0x42, 0x7d, 0x12, 0x97, 0x7d, 0x9c, 0x2e, 0x7d, 0x02, 0xd1,
0xfa, 0x12, 0x2e, 0x9d, 0x4e, 0x7e, 0xcc, 0xd2, 0x45, 0xe0, 0x3b, 0x73, 0xce, 0xf9, 0xce, 0x99,
0x0c, 0x8c, 0xa5, 0x2a, 0xca, 0x58, 0xc9, 0x30, 0x2f, 0x84, 0x12, 0xa8, 0x47, 0xa2, 0xc9, 0xf3,
0x84, 0xa9, 0x5d, 0x19, 0x85, 0xb1, 0xc8, 0xe6, 0x89, 0x48, 0xc4, 0xdc, 0x50, 0x51, 0xb9, 0x35,
0xc8, 0x00, 0x33, 0x55, 0x96, 0xc9, 0xeb, 0x8e, 0x5c, 0x1e, 0x78, 0xac, 0x76, 0x8c, 0x27, 0x9d,
0x29, 0x65, 0x51, 0xb5, 0x21, 0x16, 0xe9, 0x3c, 0xa2, 0x79, 0x65, 0x9b, 0x7d, 0x06, 0x6f, 0xc9,
0x52, 0xba, 0xa2, 0x85, 0x64, 0x82, 0xa3, 0x17, 0x30, 0xdc, 0x57, 0xa3, 0x6f, 0xdf, 0xda, 0x4f,
0xbd, 0xbb, 0x07, 0x61, 0x63, 0x0a, 0x57, 0x34, 0x56, 0xa2, 0x58, 0x38, 0xa7, 0x1f, 0x53, 0x0b,
0x37, 0x32, 0x74, 0x03, 0x03, 0x42, 0xf7, 0x2c, 0xa6, 0x7e, 0x4f, 0x1b, 0xee, 0xe1, 0x1a, 0xcd,
0x96, 0xe0, 0xd5, 0x4b, 0xdf, 0x31, 0xa9, 0xd0, 0x4b, 0x70, 0x6b, 0x87, 0xd4, 0x9b, 0xfb, 0x7a,
0xf3, 0xfd, 0x90, 0x44, 0x61, 0x27, 0xbb, 0x5e, 0xdc, 0xca, 0xde, 0x38, 0xdf, 0x8e, 0x53, 0x6b,
0xf6, 0xa7, 0x07, 0x0f, 0x2f, 0xaa, 0xb7, 0x7c, 0x2b, 0x3e, 0x15, 0x25, 0x8f, 0x37, 0x8a, 0x12,
0x84, 0xc0, 0xe1, 0x9b, 0x8c, 0x9a, 0x92, 0x23, 0x6c, 0x66, 0xf4, 0x0c, 0x1c, 0x75, 0xc8, 0xab,
0x1e, 0xd7, 0x77, 0x37, 0xff, 0x8a, 0xb7, 0x76, 0xcd, 0x62, 0xa3, 0xb9, 0xf8, 0x25, 0xfb, 0x4a,
0xfd, 0xbe, 0xd6, 0xf6, 0xb1, 0x99, 0xd1, 0x2d, 0x78, 0x39, 0x2d, 0x32, 0x26, 0xab, 0x96, 0x8e,
0xa6, 0xc6, 0xb8, 0x7b, 0x84, 0x1e, 0x03, 0x64, 0x82, 0xb0, 0x2d, 0xa3, 0x64, 0x2d, 0xfd, 0x2b,
0xe3, 0x1d, 0x35, 0x27, 0x1f, 0x91, 0x0f, 0x43, 0x42, 0x53, 0xaa, 0xfb, 0xf9, 0x03, 0xcd, 0xb9,
0xb8, 0x81, 0x17, 0x86, 0xf1, 0xfd, 0x26, 0x65, 0xc4, 0x1f, 0x56, 0x4c, 0x0d, 0xd1, 0x13, 0xb8,
0xe6, 0x62, 0xdd, 0xcd, 0x75, 0x8d, 0x60, 0xcc, 0xc5, 0x87, 0x4e, 0x72, 0xe7, 0x5d, 0x46, 0xff,
0xf7, 0x2e, 0x13, 0x70, 0x25, 0xfd, 0x52, 0x52, 0xae, 0x5f, 0x06, 0x4c, 0xd3, 0x16, 0xa3, 0x29,
0x78, 0xed, 0x3d, 0x74, 0xa2, 0xa7, 0xe9, 0x2b, 0xdc, 0x5e, 0xed, 0x7d, 0xfd, 0xeb, 0x17, 0x8f,
0x4e, 0xbf, 0x02, 0xeb, 0x74, 0x0e, 0xec, 0xef, 0xfa, 0xfb, 0x79, 0x0e, 0xac, 0xe3, 0xef, 0xc0,
0x8e, 0x06, 0x26, 0xf8, 0xd5, 0xdf, 0x00, 0x00, 0x00, 0xff, 0xff, 0x2a, 0xae, 0x24, 0x77, 0xb3,
0x02, 0x00, 0x00,
// 442 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x51, 0x5d, 0x8b, 0xd3, 0x40,
0x14, 0x6d, 0xb7, 0xd9, 0x7e, 0xdc, 0xd8, 0xd5, 0x1d, 0x64, 0x19, 0x0a, 0xb6, 0x4b, 0x41, 0x10,
0xc1, 0x54, 0x57, 0x7c, 0xf1, 0x71, 0x1f, 0x16, 0x04, 0x11, 0x19, 0x97, 0xf5, 0xb1, 0x34, 0x99,
0xdb, 0xec, 0x60, 0x32, 0x53, 0x33, 0x93, 0x42, 0xfd, 0x25, 0xbe, 0xb9, 0x3f, 0xa7, 0x8f, 0xfe,
0x02, 0xd1, 0xfa, 0x47, 0x9c, 0xce, 0xa4, 0x31, 0x8f, 0xfb, 0x10, 0xb8, 0xe7, 0x9e, 0x73, 0xee,
0x3d, 0x93, 0x0b, 0x43, 0x6d, 0x8a, 0x32, 0x31, 0x3a, 0x5a, 0x15, 0xca, 0x28, 0x72, 0xc4, 0xe3,
0xd1, 0x8b, 0x54, 0x98, 0xdb, 0x32, 0x8e, 0x12, 0x95, 0xcf, 0x52, 0x95, 0xaa, 0x99, 0xa3, 0xe2,
0x72, 0xe9, 0x90, 0x03, 0xae, 0xf2, 0x96, 0xd1, 0x9b, 0x86, 0x5c, 0x6f, 0x64, 0x62, 0x6e, 0x85,
0x4c, 0x1b, 0x55, 0x26, 0x62, 0x3f, 0x21, 0x51, 0xd9, 0x2c, 0xc6, 0x95, 0xb7, 0x4d, 0x3f, 0x43,
0x78, 0x25, 0x32, 0xbc, 0xc1, 0x42, 0x0b, 0x25, 0xc9, 0x4b, 0xe8, 0xad, 0x7d, 0x49, 0xdb, 0xe7,
0xed, 0x67, 0xe1, 0xc5, 0xa3, 0xe8, 0x60, 0x8a, 0x6e, 0x30, 0x31, 0xaa, 0xb8, 0x0c, 0xb6, 0xbf,
0x26, 0x2d, 0x76, 0x90, 0x91, 0x33, 0xe8, 0x72, 0x5c, 0x8b, 0x04, 0xe9, 0x91, 0x35, 0x3c, 0x60,
0x15, 0x9a, 0x5e, 0x41, 0x58, 0x0d, 0x7d, 0x2f, 0xb4, 0x21, 0xaf, 0xa0, 0x5f, 0x39, 0xb4, 0x9d,
0xdc, 0xb1, 0x93, 0x1f, 0x46, 0x3c, 0x8e, 0x1a, 0xbb, 0xab, 0xc1, 0xb5, 0xec, 0x6d, 0xf0, 0xfd,
0x6e, 0xd2, 0x9a, 0xfe, 0xe8, 0xc0, 0xe9, 0x5e, 0xf5, 0x4e, 0x2e, 0xd5, 0x75, 0x51, 0xca, 0x64,
0x61, 0x90, 0x13, 0x02, 0x81, 0x5c, 0xe4, 0xe8, 0x42, 0x0e, 0x98, 0xab, 0xc9, 0x73, 0x08, 0xcc,
0x66, 0xe5, 0x73, 0x9c, 0x5c, 0x9c, 0xfd, 0x0f, 0x5e, 0xdb, 0x2d, 0xcb, 0x9c, 0x66, 0xef, 0xd7,
0xe2, 0x1b, 0xd2, 0x8e, 0xd5, 0x76, 0x98, 0xab, 0xc9, 0x39, 0x84, 0x2b, 0x2c, 0x72, 0xa1, 0x7d,
0xca, 0xc0, 0x52, 0x43, 0xd6, 0x6c, 0x91, 0x27, 0x00, 0xb9, 0xe2, 0x62, 0x29, 0x90, 0xcf, 0x35,
0x3d, 0x76, 0xde, 0xc1, 0xa1, 0xf3, 0x89, 0x50, 0xe8, 0x71, 0xcc, 0xd0, 0xe6, 0xa3, 0x5d, 0xcb,
0xf5, 0xd9, 0x01, 0xee, 0x19, 0x21, 0xd7, 0x8b, 0x4c, 0x70, 0xda, 0xf3, 0x4c, 0x05, 0xc9, 0x53,
0x38, 0x91, 0x6a, 0xde, 0xdc, 0xdb, 0x77, 0x82, 0xa1, 0x54, 0x1f, 0x1b, 0x9b, 0x1b, 0x77, 0x19,
0xdc, 0xef, 0x2e, 0x23, 0xe8, 0x6b, 0xfc, 0x5a, 0xa2, 0xb4, 0x97, 0x01, 0x97, 0xb4, 0xc6, 0x64,
0x02, 0x61, 0xfd, 0x0e, 0xbb, 0x31, 0xb4, 0xf4, 0x31, 0xab, 0x9f, 0xf6, 0x41, 0xef, 0x53, 0xe9,
0x4d, 0x9e, 0x09, 0xf9, 0x65, 0x6e, 0x16, 0x45, 0x8a, 0x86, 0x9e, 0xba, 0x1f, 0x3d, 0xac, 0xba,
0xd7, 0xae, 0xe9, 0x2f, 0x74, 0xf9, 0x78, 0xfb, 0x67, 0xdc, 0xda, 0xee, 0xc6, 0xed, 0x9f, 0xf6,
0xfb, 0xbd, 0x1b, 0xb7, 0xee, 0xfe, 0x8e, 0xdb, 0x71, 0xd7, 0xe5, 0x7b, 0xfd, 0x2f, 0x00, 0x00,
0xff, 0xff, 0xb1, 0x2f, 0x12, 0xb6, 0xda, 0x02, 0x00, 0x00,
}

View File

@@ -33,4 +33,5 @@ message FileInfoTruncated {
bool no_permissions = 8;
protocol.Vector version = 9 [(gogoproto.nullable) = false];
int64 sequence = 10;
string symlink_target = 17;
}

View File

@@ -5,7 +5,7 @@
// You can obtain one at http://mozilla.org/MPL/2.0/.
//go:generate go run ../../script/protofmt.go local.proto
//go:generate protoc --proto_path=../../../../../:../../../../gogo/protobuf/protobuf:. --gogofast_out=. local.proto
//go:generate protoc -I ../../../../../ -I ../../../../gogo/protobuf/protobuf -I . --gogofast_out=. local.proto
package discover

96
lib/fs/basicfs.go Normal file
View File

@@ -0,0 +1,96 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package fs
import (
"os"
"time"
)
// The BasicFilesystem implements all aspects by delegating to package os.
type BasicFilesystem struct {
}
func NewBasicFilesystem() *BasicFilesystem {
return new(BasicFilesystem)
}
func (f *BasicFilesystem) Chmod(name string, mode FileMode) error {
return os.Chmod(name, os.FileMode(mode))
}
func (f *BasicFilesystem) Chtimes(name string, atime time.Time, mtime time.Time) error {
return os.Chtimes(name, atime, mtime)
}
func (f *BasicFilesystem) Mkdir(name string, perm FileMode) error {
return os.Mkdir(name, os.FileMode(perm))
}
func (f *BasicFilesystem) Lstat(name string) (FileInfo, error) {
fi, err := os.Lstat(name)
if err != nil {
return nil, err
}
return fsFileInfo{fi}, err
}
func (f *BasicFilesystem) Remove(name string) error {
return os.Remove(name)
}
func (f *BasicFilesystem) Rename(oldpath, newpath string) error {
return os.Rename(oldpath, newpath)
}
func (f *BasicFilesystem) Stat(name string) (FileInfo, error) {
fi, err := os.Stat(name)
if err != nil {
return nil, err
}
return fsFileInfo{fi}, err
}
func (f *BasicFilesystem) DirNames(name string) ([]string, error) {
fd, err := os.OpenFile(name, os.O_RDONLY, 0777)
if err != nil {
return nil, err
}
defer fd.Close()
names, err := fd.Readdirnames(-1)
if err != nil {
return nil, err
}
return names, nil
}
func (f *BasicFilesystem) Open(name string) (File, error) {
return os.Open(name)
}
func (f *BasicFilesystem) Create(name string) (File, error) {
return os.Create(name)
}
// fsFileInfo implements the fs.FileInfo interface on top of an os.FileInfo.
type fsFileInfo struct {
os.FileInfo
}
func (e fsFileInfo) Mode() FileMode {
return FileMode(e.FileInfo.Mode())
}
func (e fsFileInfo) IsRegular() bool {
return e.FileInfo.Mode().IsRegular()
}
func (e fsFileInfo) IsSymlink() bool {
return e.FileInfo.Mode()&os.ModeSymlink == os.ModeSymlink
}

View File

@@ -0,0 +1,43 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// +build !windows
package fs
import "os"
var symlinksSupported = true
func DisableSymlinks() {
symlinksSupported = false
}
func (BasicFilesystem) SymlinksSupported() bool {
return symlinksSupported
}
func (BasicFilesystem) CreateSymlink(name, target string, _ LinkTargetType) error {
return os.Symlink(target, name)
}
func (BasicFilesystem) ChangeSymlinkType(_ string, _ LinkTargetType) error {
return nil
}
func (BasicFilesystem) ReadSymlink(path string) (string, LinkTargetType, error) {
tt := LinkTargetUnknown
if stat, err := os.Stat(path); err == nil {
if stat.IsDir() {
tt = LinkTargetDirectory
} else {
tt = LinkTargetFile
}
}
path, err := os.Readlink(path)
return path, tt, err
}

View File

@@ -0,0 +1,195 @@
// Copyright (C) 2014 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
// +build windows
package fs
import (
"os"
"path/filepath"
"github.com/syncthing/syncthing/lib/osutil"
"syscall"
"unicode/utf16"
"unsafe"
)
const (
win32FsctlGetReparsePoint = 0x900a8
win32FileFlagOpenReparsePoint = 0x00200000
win32SymbolicLinkFlagDirectory = 0x1
)
var (
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
procDeviceIoControl = modkernel32.NewProc("DeviceIoControl")
procCreateSymbolicLink = modkernel32.NewProc("CreateSymbolicLinkW")
symlinksSupported = false
)
func init() {
defer func() {
if err := recover(); err != nil {
// Ensure that the supported flag is disabled when we hit an
// error, even though it should already be. Also, silently swallow
// the error since it's fine for a system not to support symlinks.
symlinksSupported = false
}
}()
// Needs administrator privileges.
// Let's check that everything works.
// This could be done more officially:
// http://stackoverflow.com/questions/2094663/determine-if-windows-process-has-privilege-to-create-symbolic-link
// But I don't want to define 10 more structs just to look this up.
base := os.TempDir()
path := filepath.Join(base, "symlinktest")
defer os.Remove(path)
err := DefaultFilesystem.CreateSymlink(path, base, LinkTargetDirectory)
if err != nil {
return
}
stat, err := osutil.Lstat(path)
if err != nil || stat.Mode()&os.ModeSymlink == 0 {
return
}
target, tt, err := DefaultFilesystem.ReadSymlink(path)
if err != nil || osutil.NativeFilename(target) != base || tt != LinkTargetDirectory {
return
}
symlinksSupported = true
}
func DisableSymlinks() {
symlinksSupported = false
}
func (BasicFilesystem) SymlinksSupported() bool {
return symlinksSupported
}
func (BasicFilesystem) ReadSymlink(path string) (string, LinkTargetType, error) {
ptr, err := syscall.UTF16PtrFromString(path)
if err != nil {
return "", LinkTargetUnknown, err
}
handle, err := syscall.CreateFile(ptr, 0, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS|win32FileFlagOpenReparsePoint, 0)
if err != nil || handle == syscall.InvalidHandle {
return "", LinkTargetUnknown, err
}
defer syscall.Close(handle)
var ret uint16
var data reparseData
r1, _, err := syscall.Syscall9(procDeviceIoControl.Addr(), 8, uintptr(handle), win32FsctlGetReparsePoint, 0, 0, uintptr(unsafe.Pointer(&data)), unsafe.Sizeof(data), uintptr(unsafe.Pointer(&ret)), 0, 0)
if r1 == 0 {
return "", LinkTargetUnknown, err
}
tt := LinkTargetUnknown
if attr, err := syscall.GetFileAttributes(ptr); err == nil {
if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
tt = LinkTargetDirectory
} else {
tt = LinkTargetFile
}
}
return osutil.NormalizedFilename(data.printName()), tt, nil
}
func (BasicFilesystem) CreateSymlink(path, target string, tt LinkTargetType) error {
srcp, err := syscall.UTF16PtrFromString(path)
if err != nil {
return err
}
trgp, err := syscall.UTF16PtrFromString(osutil.NativeFilename(target))
if err != nil {
return err
}
// Sadly for Windows we need to specify the type of the symlink,
// whether it's a directory symlink or a file symlink.
// If the flags doesn't reveal the target type, try to evaluate it
// ourselves, and worst case default to the symlink pointing to a file.
mode := 0
if tt == LinkTargetUnknown {
path := target
if !filepath.IsAbs(target) {
path = filepath.Join(filepath.Dir(path), target)
}
stat, err := os.Stat(path)
if err == nil && stat.IsDir() {
mode = win32SymbolicLinkFlagDirectory
}
} else if tt == LinkTargetDirectory {
mode = win32SymbolicLinkFlagDirectory
}
r0, _, err := syscall.Syscall(procCreateSymbolicLink.Addr(), 3, uintptr(unsafe.Pointer(srcp)), uintptr(unsafe.Pointer(trgp)), uintptr(mode))
if r0 == 1 {
return nil
}
return err
}
func (fs BasicFilesystem) ChangeSymlinkType(path string, tt LinkTargetType) error {
target, existingTargetType, err := fs.ReadSymlink(path)
if err != nil {
return err
}
// If it's the same type, nothing to do.
if tt == existingTargetType {
return nil
}
// If the actual type is unknown, but the new type is file, nothing to do
if existingTargetType == LinkTargetUnknown && tt != LinkTargetDirectory {
return nil
}
return osutil.InWritableDir(func(path string) error {
// It should be a symlink as well hence no need to change permissions on
// the file.
os.Remove(path)
return fs.CreateSymlink(path, target, tt)
}, path)
}
type reparseData struct {
reparseTag uint32
reparseDataLength uint16
reserved uint16
substitueNameOffset uint16
substitueNameLength uint16
printNameOffset uint16
printNameLength uint16
flags uint32
// substituteName - 264 widechars max = 528 bytes
// printName - 260 widechars max = 520 bytes
// = 1048 bytes total
buffer [1048 / 2]uint16
}
func (r *reparseData) printName() string {
// offset and length are in bytes but we're indexing a []uint16
offset := r.printNameOffset / 2
length := r.printNameLength / 2
return string(utf16.Decode(r.buffer[offset : offset+length]))
}
func (r *reparseData) substituteName() string {
// offset and length are in bytes but we're indexing a []uint16
offset := r.substitueNameOffset / 2
length := r.substitueNameLength / 2
return string(utf16.Decode(r.buffer[offset : offset+length]))
}

81
lib/fs/basicfs_walk.go Normal file
View File

@@ -0,0 +1,81 @@
// Copyright 2009 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.
// This part copied directly from golang.org/src/path/filepath/path.go (Go
// 1.6) and lightly modified to be methods on BasicFilesystem.
// In our Walk() all paths given to a WalkFunc() are relative to the
// filesystem root.
package fs
import "path/filepath"
// WalkFunc is the type of the function called for each file or directory
// visited by Walk. The path argument contains the argument to Walk as a
// prefix; that is, if Walk is called with "dir", which is a directory
// containing the file "a", the walk function will be called with argument
// "dir/a". The info argument is the FileInfo for the named path.
//
// If there was a problem walking to the file or directory named by path, the
// incoming error will describe the problem and the function can decide how
// to handle that error (and Walk will not descend into that directory). If
// an error is returned, processing stops. The sole exception is when the function
// returns the special value SkipDir. If the function returns SkipDir when invoked
// on a directory, Walk skips the directory's contents entirely.
// If the function returns SkipDir when invoked on a non-directory file,
// Walk skips the remaining files in the containing directory.
type WalkFunc func(path string, info FileInfo, err error) error
// walk recursively descends path, calling walkFn.
func (f *BasicFilesystem) walk(path string, info FileInfo, walkFn WalkFunc) error {
err := walkFn(path, info, nil)
if err != nil {
if info.IsDir() && err == SkipDir {
return nil
}
return err
}
if !info.IsDir() {
return nil
}
names, err := f.DirNames(path)
if err != nil {
return walkFn(path, info, err)
}
for _, name := range names {
filename := filepath.Join(path, name)
fileInfo, err := f.Lstat(filename)
if err != nil {
if err := walkFn(filename, fileInfo, err); err != nil && err != SkipDir {
return err
}
} else {
err = f.walk(filename, fileInfo, walkFn)
if err != nil {
if !fileInfo.IsDir() || err != SkipDir {
return err
}
}
}
}
return nil
}
// Walk walks the file tree rooted at root, calling walkFn for each file or
// directory in the tree, including root. All errors that arise visiting files
// and directories are filtered by walkFn. The files are walked in lexical
// order, which makes the output deterministic but means that for very
// large directories Walk can be inefficient.
// Walk does not follow symbolic links.
func (f *BasicFilesystem) Walk(root string, walkFn WalkFunc) error {
info, err := f.Lstat(root)
if err != nil {
return walkFn(root, nil, err)
}
return f.walk(root, info, walkFn)
}

77
lib/fs/filesystem.go Normal file
View File

@@ -0,0 +1,77 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package fs
import (
"errors"
"io"
"time"
)
type LinkTargetType int
const (
LinkTargetFile LinkTargetType = iota
LinkTargetDirectory
LinkTargetUnknown
)
// The Filesystem interface abstracts access to the file system.
type Filesystem interface {
ChangeSymlinkType(name string, tt LinkTargetType) error
Chmod(name string, mode FileMode) error
Chtimes(name string, atime time.Time, mtime time.Time) error
Create(name string) (File, error)
CreateSymlink(name, target string, tt LinkTargetType) error
DirNames(name string) ([]string, error)
Lstat(name string) (FileInfo, error)
Mkdir(name string, perm FileMode) error
Open(name string) (File, error)
ReadSymlink(name string) (string, LinkTargetType, error)
Remove(name string) error
Rename(oldname, newname string) error
Stat(name string) (FileInfo, error)
SymlinksSupported() bool
Walk(root string, walkFn WalkFunc) error
}
// The File interface abstracts access to a regular file, being a somewhat
// smaller interface than os.File
type File interface {
io.Reader
io.WriterAt
io.Closer
Truncate(size int64) error
}
// The FileInfo interface is almost the same as os.FileInfo, but with the
// Sys method removed (as we don't want to expose whatever is underlying)
// and with a couple of convenience methods added.
type FileInfo interface {
// Standard things present in os.FileInfo
Name() string
Mode() FileMode
Size() int64
ModTime() time.Time
IsDir() bool
// Extensions
IsRegular() bool
IsSymlink() bool
}
// FileMode is similar to os.FileMode
type FileMode uint32
// DefaultFilesystem is the fallback to use when nothing explicitly has
// been passed.
var DefaultFilesystem Filesystem = new(BasicFilesystem)
// SkipDir is used as a return value from WalkFuncs to indicate that
// the directory named in the call is to be skipped. It is not returned
// as an error by any function.
var errSkipDir = errors.New("skip this directory")
var SkipDir = errSkipDir // silences the lint warning...

View File

@@ -69,6 +69,7 @@ type Matcher struct {
matches *cache
curHash string
stop chan struct{}
modtimes map[string]time.Time
mut sync.Mutex
}
@@ -85,25 +86,41 @@ func New(withCache bool) *Matcher {
}
func (m *Matcher) Load(file string) error {
// No locking, Parse() does the locking
m.mut.Lock()
defer m.mut.Unlock()
if m.patternsUnchanged(file) {
return nil
}
fd, err := os.Open(file)
if err != nil {
// We do a parse with empty patterns to clear out the hash, cache etc.
m.Parse(&bytes.Buffer{}, file)
m.parseLocked(&bytes.Buffer{}, file)
return err
}
defer fd.Close()
return m.Parse(fd, file)
info, err := fd.Stat()
if err != nil {
m.parseLocked(&bytes.Buffer{}, file)
return err
}
m.modtimes = map[string]time.Time{
file: info.ModTime(),
}
return m.parseLocked(fd, file)
}
func (m *Matcher) Parse(r io.Reader, file string) error {
m.mut.Lock()
defer m.mut.Unlock()
return m.parseLocked(r, file)
}
seen := map[string]bool{file: true}
patterns, err := parseIgnoreFile(r, file, seen)
func (m *Matcher) parseLocked(r io.Reader, file string) error {
patterns, err := parseIgnoreFile(r, file, m.modtimes)
// Error is saved and returned at the end. We process the patterns
// (possibly blank) anyway.
@@ -122,6 +139,26 @@ func (m *Matcher) Parse(r io.Reader, file string) error {
return err
}
// patternsUnchanged returns true if none of the files making up the loaded
// patterns have changed since last check.
func (m *Matcher) patternsUnchanged(file string) bool {
if _, ok := m.modtimes[file]; !ok {
return false
}
for filename, modtime := range m.modtimes {
info, err := os.Stat(filename)
if err != nil {
return false
}
if !info.ModTime().Equal(modtime) {
return false
}
}
return true
}
func (m *Matcher) Match(file string) (result Result) {
if m == nil {
return resultNotMatched
@@ -221,11 +258,10 @@ func hashPatterns(patterns []Pattern) string {
return fmt.Sprintf("%x", h.Sum(nil))
}
func loadIgnoreFile(file string, seen map[string]bool) ([]Pattern, error) {
if seen[file] {
func loadIgnoreFile(file string, modtimes map[string]time.Time) ([]Pattern, error) {
if _, ok := modtimes[file]; ok {
return nil, fmt.Errorf("Multiple include of ignore file %q", file)
}
seen[file] = true
fd, err := os.Open(file)
if err != nil {
@@ -233,10 +269,16 @@ func loadIgnoreFile(file string, seen map[string]bool) ([]Pattern, error) {
}
defer fd.Close()
return parseIgnoreFile(fd, file, seen)
info, err := fd.Stat()
if err != nil {
return nil, err
}
modtimes[file] = info.ModTime()
return parseIgnoreFile(fd, file, modtimes)
}
func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]Pattern, error) {
func parseIgnoreFile(fd io.Reader, currentFile string, modtimes map[string]time.Time) ([]Pattern, error) {
var patterns []Pattern
defaultResult := resultInclude
@@ -302,7 +344,7 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
} else if strings.HasPrefix(line, "#include ") {
includeRel := line[len("#include "):]
includeFile := filepath.Join(filepath.Dir(currentFile), includeRel)
includes, err := loadIgnoreFile(includeFile, seen)
includes, err := loadIgnoreFile(includeFile, modtimes)
if err != nil {
return fmt.Errorf("include of %q: %v", includeRel, err)
}
@@ -359,3 +401,20 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
return patterns, nil
}
// IsInternal returns true if the file, as a path relative to the folder
// root, represents an internal file that should always be ignored. The file
// path must be clean (i.e., in canonical shortest form).
func IsInternal(file string) bool {
internals := []string{".stfolder", ".stignore", ".stversions"}
pathSep := string(os.PathSeparator)
for _, internal := range internals {
if file == internal {
return true
}
if strings.HasPrefix(file, internal+pathSep) {
return true
}
}
return false
}

View File

@@ -14,6 +14,7 @@ import (
"path/filepath"
"runtime"
"testing"
"time"
)
func TestIgnore(t *testing.T) {
@@ -276,9 +277,13 @@ func TestCaching(t *testing.T) {
t.Fatal("Expected 4 cached results")
}
// Modify the include file, expect empty cache
// Modify the include file, expect empty cache. Ensure the timestamp on
// the file changes.
fd2.WriteString("/z/\n")
fd2.Sync()
fakeTime := time.Now().Add(5 * time.Second)
os.Chtimes(fd2.Name(), fakeTime, fakeTime)
err = pats.Load(fd1.Name())
if err != nil {
@@ -308,6 +313,9 @@ func TestCaching(t *testing.T) {
// Modify the root file, expect cache to be invalidated
fd1.WriteString("/a/\n")
fd1.Sync()
fakeTime = time.Now().Add(5 * time.Second)
os.Chtimes(fd1.Name(), fakeTime, fakeTime)
err = pats.Load(fd1.Name())
if err != nil {
@@ -484,6 +492,9 @@ func TestCacheReload(t *testing.T) {
if err != nil {
t.Fatal(err)
}
fd.Sync()
fakeTime := time.Now().Add(5 * time.Second)
os.Chtimes(fd.Name(), fakeTime, fakeTime)
err = pats.Load(fd.Name())
if err != nil {
@@ -801,3 +812,34 @@ func TestGobwasGlobIssue18(t *testing.T) {
}
}
}
func TestIsInternal(t *testing.T) {
cases := []struct {
file string
internal bool
}{
{".stfolder", true},
{".stignore", true},
{".stversions", true},
{".stfolder/foo", true},
{".stignore/foo", true},
{".stversions/foo", true},
{".stfolderfoo", false},
{".stignorefoo", false},
{".stversionsfoo", false},
{"foo.stfolder", false},
{"foo.stignore", false},
{"foo.stversions", false},
{"foo/.stfolder", false},
{"foo/.stignore", false},
{"foo/.stversions", false},
}
for _, tc := range cases {
res := IsInternal(filepath.FromSlash(tc.file))
if res != tc.internal {
t.Errorf("Unexpected result: IsInteral(%q): %v should be %v", tc.file, res, tc.internal)
}
}
}

View File

@@ -35,6 +35,7 @@ import (
"github.com/syncthing/syncthing/lib/stats"
"github.com/syncthing/syncthing/lib/symlinks"
"github.com/syncthing/syncthing/lib/sync"
"github.com/syncthing/syncthing/lib/upgrade"
"github.com/syncthing/syncthing/lib/versioner"
"github.com/thejerf/suture"
)
@@ -83,7 +84,7 @@ type Model struct {
folderCfgs map[string]config.FolderConfiguration // folder -> cfg
folderFiles map[string]*db.FileSet // folder -> files
folderDevices map[string][]protocol.DeviceID // folder -> deviceIDs
folderDevices folderDeviceSet // folder -> deviceIDs
deviceFolders map[protocol.DeviceID][]string // deviceID -> folders
deviceStatRefs map[protocol.DeviceID]*stats.DeviceStatisticsReference // deviceID -> statsRef
folderIgnores map[string]*ignore.Matcher // folder -> matcher object
@@ -119,6 +120,8 @@ var (
errDeviceUnknown = errors.New("unknown device")
errDevicePaused = errors.New("device is paused")
errDeviceIgnored = errors.New("device is ignored")
errNotRelative = errors.New("not a relative path")
errNotDir = errors.New("parent is not a directory")
)
// NewModel creates and starts a new model. The model starts in read-only mode,
@@ -144,7 +147,7 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName,
clientVersion: clientVersion,
folderCfgs: make(map[string]config.FolderConfiguration),
folderFiles: make(map[string]*db.FileSet),
folderDevices: make(map[string][]protocol.DeviceID),
folderDevices: make(folderDeviceSet),
deviceFolders: make(map[protocol.DeviceID][]string),
deviceStatRefs: make(map[protocol.DeviceID]*stats.DeviceStatisticsReference),
folderIgnores: make(map[string]*ignore.Matcher),
@@ -189,12 +192,12 @@ func (m *Model) StartFolder(folder string) {
func (m *Model) startFolderLocked(folder string) config.FolderType {
cfg, ok := m.folderCfgs[folder]
if !ok {
panic("cannot start nonexistent folder " + folder)
panic("cannot start nonexistent folder " + cfg.Description())
}
_, ok = m.folderRunners[folder]
if ok {
panic("cannot start already running folder " + folder)
panic("cannot start already running folder " + cfg.Description())
}
folderFactory, ok := folderFactories[cfg.Type]
@@ -303,9 +306,8 @@ func (m *Model) addFolderLocked(cfg config.FolderConfiguration) {
m.folderCfgs[cfg.ID] = cfg
m.folderFiles[cfg.ID] = db.NewFileSet(cfg.ID, m.db)
m.folderDevices[cfg.ID] = make([]protocol.DeviceID, len(cfg.Devices))
for i, device := range cfg.Devices {
m.folderDevices[cfg.ID][i] = device.DeviceID
for _, device := range cfg.Devices {
m.folderDevices.set(device.DeviceID, cfg.ID)
m.deviceFolders[device.DeviceID] = append(m.deviceFolders[device.DeviceID], cfg.ID)
}
@@ -335,7 +337,7 @@ func (m *Model) tearDownFolderLocked(folder string) {
}
// Close connections to affected devices
for _, dev := range m.folderDevices[folder] {
for dev := range m.folderDevices[folder] {
if conn, ok := m.conn[dev]; ok {
closeRawConn(conn)
}
@@ -413,7 +415,7 @@ func (m *Model) ConnectionStats() map[string]interface{} {
Paused: m.devicePaused[device],
}
if conn, ok := m.conn[device]; ok {
ci.Type = conn.Type
ci.Type = conn.Type()
ci.Connected = ok
ci.Statistics = conn.Statistics()
if addr := conn.RemoteAddr(); addr != nil {
@@ -443,7 +445,7 @@ func (m *Model) ConnectionStats() map[string]interface{} {
// DeviceStatistics returns statistics about each device
func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
var res = make(map[string]stats.DeviceStatistics)
res := make(map[string]stats.DeviceStatistics)
for id := range m.cfg.Devices() {
res[id.String()] = m.deviceStatRef(id).GetStatistics()
}
@@ -452,7 +454,7 @@ func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
// FolderStatistics returns statistics about each folder
func (m *Model) FolderStatistics() map[string]stats.FolderStatistics {
var res = make(map[string]stats.FolderStatistics)
res := make(map[string]stats.FolderStatistics)
for id := range m.cfg.Folders() {
res[id] = m.folderStatRef(id).GetStatistics()
}
@@ -543,7 +545,6 @@ func addSizeOfFile(s *db.Counts, f db.FileIntf) {
s.Files++
}
s.Bytes += f.FileSize()
return
}
// GlobalSize returns the number of files, deleted files and total bytes for all
@@ -578,7 +579,7 @@ func (m *Model) NeedSize(folder string) db.Counts {
ignores := m.folderIgnores[folder]
cfg := m.folderCfgs[folder]
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
if shouldIgnore(f, ignores, cfg.IgnoreDelete) {
if shouldIgnore(f, ignores, cfg.IgnoreDelete, defTempNamer) {
return true
}
@@ -642,7 +643,7 @@ func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
ignores := m.folderIgnores[folder]
cfg := m.folderCfgs[folder]
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
if shouldIgnore(f, ignores, cfg.IgnoreDelete) {
if shouldIgnore(f, ignores, cfg.IgnoreDelete, defTempNamer) {
return true
}
@@ -679,16 +680,16 @@ func (m *Model) Index(deviceID protocol.DeviceID, folder string, fs []protocol.F
runner := m.folderRunners[folder]
m.fmut.RUnlock()
if !ok {
l.Fatalf("Index for nonexistent folder %q", folder)
}
if runner != nil {
// Runner may legitimately not be set if this is the "cleanup" Index
// message at startup.
defer runner.IndexUpdated()
}
if !ok {
l.Fatalf("Index for nonexistent folder %q", folder)
}
m.pmut.RLock()
m.deviceDownloads[deviceID].Update(folder, makeForgetUpdate(fs))
m.pmut.RUnlock()
@@ -740,8 +741,9 @@ func (m *Model) IndexUpdate(deviceID protocol.DeviceID, folder string, fs []prot
func (m *Model) folderSharedWith(folder string, deviceID protocol.DeviceID) bool {
m.fmut.RLock()
defer m.fmut.RUnlock()
return m.folderSharedWithLocked(folder, deviceID)
shared := m.folderSharedWithLocked(folder, deviceID)
m.fmut.RUnlock()
return shared
}
func (m *Model) folderSharedWithLocked(folder string, deviceID protocol.DeviceID) bool {
@@ -763,6 +765,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
m.pmut.RLock()
conn, ok := m.conn[deviceID]
hello := m.helloMessages[deviceID]
m.pmut.RUnlock()
if !ok {
panic("bug: ClusterConfig called on closed or nonexistent connection")
@@ -770,6 +773,14 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
dbLocation := filepath.Dir(m.db.Location())
// See issue #3802 - in short, we can't send modern symlink entries to older
// clients.
dropSymlinks := false
if hello.ClientName == m.clientName && upgrade.CompareVersions(hello.ClientVersion, "v0.14.14") < 0 {
l.Warnln("Not sending symlinks to old client", deviceID, "- please upgrade to v0.14.14 or newer")
dropSymlinks = true
}
m.fmut.Lock()
for _, folder := range cm.Folders {
if !m.folderSharedWithLocked(folder.ID, deviceID) {
@@ -778,7 +789,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
"folderLabel": folder.Label,
"device": deviceID.String(),
})
l.Infof("Unexpected folder ID %q sent from device %q; ensure that the folder exists and that this device is selected under \"Share With\" in the folder configuration.", folder.ID, deviceID)
l.Infof("Unexpected folder %s sent from device %q; ensure that the folder exists and that this device is selected under \"Share With\" in the folder configuration.", folder.Description(), deviceID)
continue
}
if !folder.DisableTempIndexes {
@@ -807,19 +818,19 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
// the IndexID, or something else weird has
// happened. We send a full index to reset the
// situation.
l.Infof("Device %v folder %q is delta index compatible, but seems out of sync with reality", deviceID, folder.ID)
l.Infof("Device %v folder %s is delta index compatible, but seems out of sync with reality", deviceID, folder.Description())
startSequence = 0
continue
}
l.Debugf("Device %v folder %q is delta index compatible (mlv=%d)", deviceID, folder.ID, dev.MaxSequence)
l.Debugf("Device %v folder %s is delta index compatible (mlv=%d)", deviceID, folder.Description(), dev.MaxSequence)
startSequence = dev.MaxSequence
} else if dev.IndexID != 0 {
// They say they've seen an index ID from us, but it's
// not the right one. Either they are confused or we
// must have reset our database since last talking to
// them. We'll start with a full index transfer.
l.Infof("Device %v folder %q has mismatching index ID for us (%v != %v)", deviceID, folder.ID, dev.IndexID, myIndexID)
l.Infof("Device %v folder %s has mismatching index ID for us (%v != %v)", deviceID, folder.Description(), dev.IndexID, myIndexID)
startSequence = 0
}
} else if dev.ID == deviceID && dev.IndexID != 0 {
@@ -841,7 +852,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
// will probably send us a full index. We drop any
// information we have and remember this new index ID
// instead.
l.Infof("Device %v folder %q has a new index ID (%v)", deviceID, folder.ID, dev.IndexID)
l.Infof("Device %v folder %s has a new index ID (%v)", deviceID, folder.Description(), dev.IndexID)
fs.Replace(deviceID, nil)
fs.SetIndexID(deviceID, dev.IndexID)
} else {
@@ -856,9 +867,8 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
}
}
go sendIndexes(conn, folder.ID, fs, m.folderIgnores[folder.ID], startSequence, dbLocation)
go sendIndexes(conn, folder.ID, fs, m.folderIgnores[folder.ID], startSequence, dbLocation, dropSymlinks)
}
m.fmut.Unlock()
// This breaks if we send multiple CM messages during the same connection.
if len(tempIndexFolders) > 0 {
@@ -872,78 +882,167 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
}
}
var changed bool
var changed = false
if deviceCfg := m.cfg.Devices()[deviceID]; deviceCfg.Introducer {
foldersDevices, introduced := m.handleIntroductions(deviceCfg, cm)
if introduced {
changed = true
}
// If permitted, check if the introducer has unshare devices/folders with
// some of the devices/folders that we know were introduced to us by him.
if !deviceCfg.SkipIntroductionRemovals && m.handleDeintroductions(deviceCfg, cm, foldersDevices) {
changed = true
}
}
m.fmut.Unlock()
if m.cfg.Devices()[deviceID].Introducer {
// This device is an introducer. Go through the announced lists of folders
// and devices and add what we are missing.
if changed {
if err := m.cfg.Save(); err != nil {
l.Warnln("Failed to save config", err)
}
}
}
for _, folder := range cm.Folders {
if _, ok := m.folderDevices[folder.ID]; !ok {
continue
// handleIntroductions handles adding devices/shares that are shared by an introducer device
func (m *Model) handleIntroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig) (folderDeviceSet, bool) {
// This device is an introducer. Go through the announced lists of folders
// and devices and add what we are missing, remove what we have extra that
// has been introducer by the introducer.
changed := false
foldersDevices := make(folderDeviceSet)
for _, folder := range cm.Folders {
// We don't have this folder, skip.
if _, ok := m.folderDevices[folder.ID]; !ok {
continue
}
// Adds devices which we do not have, but the introducer has
// for the folders that we have in common. Also, shares folders
// with devices that we have in common, yet are currently not sharing
// the folder.
nextDevice:
for _, device := range folder.Devices {
foldersDevices.set(device.ID, folder.ID)
if _, ok := m.cfg.Devices()[device.ID]; !ok {
// The device is currently unknown. Add it to the config.
m.introduceDevice(device, introducerCfg)
changed = true
}
nextDevice:
for _, device := range folder.Devices {
if _, ok := m.cfg.Devices()[device.ID]; !ok {
// The device is currently unknown. Add it to the config.
addresses := []string{"dynamic"}
for _, addr := range device.Addresses {
if addr != "dynamic" {
addresses = append(addresses, addr)
}
}
l.Infof("Adding device %v to config (vouched for by introducer %v)", device.ID, deviceID)
newDeviceCfg := config.DeviceConfiguration{
DeviceID: device.ID,
Name: device.Name,
Compression: m.cfg.Devices()[deviceID].Compression,
Addresses: addresses,
CertName: device.CertName,
}
// The introducers' introducers are also our introducers.
if device.Introducer {
l.Infof("Device %v is now also an introducer", device.ID)
newDeviceCfg.Introducer = true
}
m.cfg.SetDevice(newDeviceCfg)
changed = true
for _, er := range m.deviceFolders[device.ID] {
if er == folder.ID {
// We already share the folder with this device, so
// nothing to do.
continue nextDevice
}
}
for _, er := range m.deviceFolders[device.ID] {
if er == folder.ID {
// We already share the folder with this device, so
// nothing to do.
continue nextDevice
}
// We don't yet share this folder with this device. Add the device
// to sharing list of the folder.
m.introduceDeviceToFolder(device, folder, introducerCfg)
changed = true
}
}
return foldersDevices, changed
}
// handleIntroductions handles removals of devices/shares that are removed by an introducer device
func (m *Model) handleDeintroductions(introducerCfg config.DeviceConfiguration, cm protocol.ClusterConfig, foldersDevices folderDeviceSet) bool {
changed := false
foldersIntroducedByOthers := make(folderDeviceSet)
// Check if we should unshare some folders, if the introducer has unshared them.
for _, folderCfg := range m.cfg.Folders() {
folderChanged := false
for i := 0; i < len(folderCfg.Devices); i++ {
if folderCfg.Devices[i].IntroducedBy == introducerCfg.DeviceID {
if !foldersDevices.has(folderCfg.Devices[i].DeviceID, folderCfg.ID) {
// We could not find that folder shared on the
// introducer with the device that was introduced to us.
// We should follow and unshare aswell.
l.Infof("Unsharing folder %s with %v as introducer %v no longer shares the folder with that device", folderCfg.Description(), folderCfg.Devices[i].DeviceID, folderCfg.Devices[i].IntroducedBy)
folderCfg.Devices = append(folderCfg.Devices[:i], folderCfg.Devices[i+1:]...)
i--
folderChanged = true
}
} else {
foldersIntroducedByOthers.set(folderCfg.Devices[i].DeviceID, folderCfg.ID)
}
}
// We don't yet share this folder with this device. Add the device
// to sharing list of the folder.
l.Infof("Adding device %v to share %q (vouched for by introducer %v)", device.ID, folder.ID, deviceID)
m.deviceFolders[device.ID] = append(m.deviceFolders[device.ID], folder.ID)
m.folderDevices[folder.ID] = append(m.folderDevices[folder.ID], device.ID)
folderCfg := m.cfg.Folders()[folder.ID]
folderCfg.Devices = append(folderCfg.Devices, config.FolderDeviceConfiguration{
DeviceID: device.ID,
})
m.cfg.SetFolder(folderCfg)
// We've modified the folder, hence update it.
if folderChanged {
m.cfg.SetFolder(folderCfg)
changed = true
}
}
// Check if we should remove some devices, if the introducer no longer
// shares any folder with them. Yet do not remove if we share other
// folders that haven't been introduced by the introducer.
for _, device := range m.cfg.Devices() {
if device.IntroducedBy == introducerCfg.DeviceID {
if !foldersDevices.hasDevice(device.DeviceID) {
if foldersIntroducedByOthers.hasDevice(device.DeviceID) {
l.Infof("Would have removed %v as %v no longer shares any folders, yet there are other folders that are shared with this device that haven't been introduced by this introducer.", device.DeviceID, device.IntroducedBy)
continue
}
// The introducer no longer shares any folder with the
// device, remove the device.
l.Infof("Removing device %v as introducer %v no longer shares any folders with that device", device.DeviceID, device.IntroducedBy)
m.cfg.RemoveDevice(device.DeviceID)
changed = true
}
}
}
if changed {
m.cfg.Save()
return changed
}
func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.DeviceConfiguration) {
addresses := []string{"dynamic"}
for _, addr := range device.Addresses {
if addr != "dynamic" {
addresses = append(addresses, addr)
}
}
l.Infof("Adding device %v to config (vouched for by introducer %v)", device.ID, introducerCfg.DeviceID)
newDeviceCfg := config.DeviceConfiguration{
DeviceID: device.ID,
Name: device.Name,
Compression: introducerCfg.Compression,
Addresses: addresses,
CertName: device.CertName,
IntroducedBy: introducerCfg.DeviceID,
}
// The introducers' introducers are also our introducers.
if device.Introducer {
l.Infof("Device %v is now also an introducer", device.ID)
newDeviceCfg.Introducer = true
newDeviceCfg.SkipIntroductionRemovals = device.SkipIntroductionRemovals
}
m.cfg.SetDevice(newDeviceCfg)
}
func (m *Model) introduceDeviceToFolder(device protocol.Device, folder protocol.Folder, introducerCfg config.DeviceConfiguration) {
l.Infof("Sharing folder %s with %v (vouched for by introducer %v)", folder.Description(), device.ID, introducerCfg.DeviceID)
m.deviceFolders[device.ID] = append(m.deviceFolders[device.ID], folder.ID)
m.folderDevices.set(device.ID, folder.ID)
folderCfg := m.cfg.Folders()[folder.ID]
folderCfg.Devices = append(folderCfg.Devices, config.FolderDeviceConfiguration{
DeviceID: device.ID,
IntroducedBy: introducerCfg.DeviceID,
})
m.cfg.SetFolder(folderCfg)
}
// Closed is called when a connection has been closed
@@ -1004,60 +1103,43 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
folderIgnores := m.folderIgnores[folder]
m.fmut.RUnlock()
// filepath.Join() returns a filepath.Clean()ed path, which (quoting the
// docs for clarity here):
//
// Clean returns the shortest path name equivalent to path by purely lexical
// processing. It applies the following rules iteratively until no further
// processing can be done:
//
// 1. Replace multiple Separator elements with a single one.
// 2. Eliminate each . path name element (the current directory).
// 3. Eliminate each inner .. path name element (the parent directory)
// along with the non-.. element that precedes it.
// 4. Eliminate .. elements that begin a rooted path:
// that is, replace "/.." by "/" at the beginning of a path,
// assuming Separator is '/'.
fn := filepath.Join(folderPath, name)
if !strings.HasPrefix(fn, folderPath) {
fn, err := rootedJoinedPath(folderPath, name)
if err != nil {
// Request tries to escape!
l.Debugf("%v Invalid REQ(in) tries to escape: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
return protocol.ErrInvalid
}
if folderIgnores != nil {
// "rn" becomes the relative name of the file within the folder. This is
// different than the original "name" parameter in that it's been
// cleaned from any possible funny business.
if rn, err := filepath.Rel(folderPath, fn); err != nil {
return err
} else if folderIgnores.Match(rn).IsIgnored() {
l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
return protocol.ErrNoSuchFile
}
// Having passed the rootedJoinedPath check above, we know "name" is
// acceptable relative to "folderPath" and in canonical form, so we can
// trust it.
if ignore.IsInternal(name) {
l.Debugf("%v REQ(in) for internal file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
return protocol.ErrNoSuchFile
}
if info, err := osutil.Lstat(fn); err == nil && info.Mode()&os.ModeSymlink != 0 {
target, _, err := symlinks.Read(fn)
if err != nil {
l.Debugln("symlinks.Read:", err)
if os.IsNotExist(err) {
return protocol.ErrNoSuchFile
}
return protocol.ErrGeneric
}
if _, err := strings.NewReader(target).ReadAt(buf, offset); err != nil {
l.Debugln("symlink.Reader.ReadAt", err)
return protocol.ErrGeneric
}
return nil
if folderIgnores.Match(name).IsIgnored() {
l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
return protocol.ErrNoSuchFile
}
if !osutil.IsDir(folderPath, filepath.Dir(name)) {
l.Debugf("%v REQ(in) for file not in dir: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
return protocol.ErrNoSuchFile
}
// Only check temp files if the flag is set, and if we are set to advertise
// the temp indexes.
if fromTemporary && !folderCfg.DisableTempIndexes {
tempFn := filepath.Join(folderPath, defTempNamer.TempName(name))
if info, err := osutil.Lstat(tempFn); err != nil || !info.Mode().IsRegular() {
// Reject reads for anything that doesn't exist or is something
// other than a regular file.
return protocol.ErrNoSuchFile
}
if err := readOffsetIntoBuf(tempFn, offset, buf); err == nil {
return nil
}
@@ -1065,7 +1147,13 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
// file has finished downloading.
}
err := readOffsetIntoBuf(fn, offset, buf)
if info, err := osutil.Lstat(fn); err != nil || !info.Mode().IsRegular() {
// Reject reads for anything that doesn't exist or is something
// other than a regular file.
return protocol.ErrNoSuchFile
}
err = readOffsetIntoBuf(fn, offset, buf)
if os.IsNotExist(err) {
return protocol.ErrNoSuchFile
} else if err != nil {
@@ -1081,8 +1169,7 @@ func (m *Model) CurrentFolderFile(folder string, file string) (protocol.FileInfo
if !ok {
return protocol.FileInfo{}, false
}
f, ok := fs.Get(protocol.LocalDeviceID, file)
return f, ok
return fs.Get(protocol.LocalDeviceID, file)
}
func (m *Model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool) {
@@ -1092,8 +1179,7 @@ func (m *Model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo
if !ok {
return protocol.FileInfo{}, false
}
f, ok := fs.GetGlobal(file)
return f, ok
return fs.GetGlobal(file)
}
type cFiler struct {
@@ -1161,7 +1247,7 @@ func (m *Model) SetIgnores(folder string, content []string) error {
path := filepath.Join(cfg.Path(), ".stignore")
fd, err := osutil.CreateAtomic(path, 0644)
fd, err := osutil.CreateAtomic(path)
if err != nil {
l.Warnln("Saving .stignore:", err)
return err
@@ -1247,7 +1333,7 @@ func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloR
"deviceName": hello.DeviceName,
"clientName": hello.ClientName,
"clientVersion": hello.ClientVersion,
"type": conn.Type,
"type": conn.Type(),
}
addr := conn.RemoteAddr()
@@ -1358,7 +1444,7 @@ func (m *Model) receivedFile(folder string, file protocol.FileInfo) {
m.folderStatRef(folder).ReceivedFile(file.Name, file.IsDeleted())
}
func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, startSequence int64, dbLocation string) {
func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, startSequence int64, dbLocation string, dropSymlinks bool) {
deviceID := conn.ID()
name := conn.Name()
var err error
@@ -1366,7 +1452,7 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
l.Debugf("sendIndexes for %s-%s/%q starting (slv=%d)", deviceID, name, folder, startSequence)
defer l.Debugf("sendIndexes for %s-%s/%q exiting: %v", deviceID, name, folder, err)
minSequence, err := sendIndexTo(startSequence, conn, folder, fs, ignores, dbLocation)
minSequence, err := sendIndexTo(startSequence, conn, folder, fs, ignores, dbLocation, dropSymlinks)
// Subscribe to LocalIndexUpdated (we have new information to send) and
// DeviceDisconnected (it might be us who disconnected, so we should
@@ -1389,7 +1475,7 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
continue
}
minSequence, err = sendIndexTo(minSequence, conn, folder, fs, ignores, dbLocation)
minSequence, err = sendIndexTo(minSequence, conn, folder, fs, ignores, dbLocation, dropSymlinks)
// Wait a short amount of time before entering the next loop. If there
// are continuous changes happening to the local index, this gives us
@@ -1398,7 +1484,7 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
}
}
func sendIndexTo(minSequence int64, conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, dbLocation string) (int64, error) {
func sendIndexTo(minSequence int64, conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, dbLocation string, dropSymlinks bool) (int64, error) {
deviceID := conn.ID()
name := conn.Name()
batch := make([]protocol.FileInfo, 0, indexBatchSize)
@@ -1420,6 +1506,14 @@ func sendIndexTo(minSequence int64, conn protocol.Connection, folder string, fs
maxSequence = f.Sequence
}
if dropSymlinks && f.IsSymlink() {
// Do not send index entries with symlinks to clients that can't
// handle it. Fixes issue #3802. Once both sides are upgraded, a
// rescan (i.e., change) of the symlink is required for it to
// sync again, due to delta indexes.
return true
}
sorter.Append(f)
return true
})
@@ -1469,7 +1563,6 @@ func (m *Model) updateLocalsFromScanning(folder string, fs []protocol.FileInfo)
m.fmut.RLock()
folderCfg := m.folderCfgs[folder]
m.fmut.RUnlock()
// Fire the LocalChangeDetected event to notify listeners about local updates.
m.localChangeDetected(folderCfg, fs)
}
@@ -1612,9 +1705,22 @@ func (m *Model) ScanFolderSubdirs(folder string, subs []string) error {
}
func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error {
for i, sub := range subDirs {
sub = osutil.NativeFilename(sub)
if p := filepath.Clean(filepath.Join(folder, sub)); !strings.HasPrefix(p, folder) {
for i := 0; i < len(subDirs); i++ {
sub := osutil.NativeFilename(subDirs[i])
if sub == "" {
// A blank subdirs means to scan the entire folder. We can trim
// the subDirs list and go on our way.
subDirs = nil
break
}
// We test each path by joining with "root". What we join with is
// not relevant, we just want the dotdot escape detection here. For
// historical reasons we may get paths that end in a slash. We
// remove that first to allow the rootedJoinedPath to pass.
sub = strings.TrimRight(sub, string(os.PathSeparator))
if _, err := rootedJoinedPath("root", sub); err != nil {
return errors.New("invalid subpath")
}
subDirs[i] = sub
@@ -1649,14 +1755,14 @@ func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error
if err := m.CheckFolderHealth(folder); err != nil {
runner.setError(err)
l.Infof("Stopping folder %s due to error: %s", folder, err)
l.Infof("Stopping folder %s due to error: %s", folderCfg.Description(), err)
return err
}
if err := ignores.Load(filepath.Join(folderCfg.Path(), ".stignore")); err != nil && !os.IsNotExist(err) {
err = fmt.Errorf("loading ignores: %v", err)
runner.setError(err)
l.Infof("Stopping folder %s due to error: %s", folder, err)
l.Infof("Stopping folder %s due to error: %s", folderCfg.Description(), err)
return err
}
@@ -1713,7 +1819,7 @@ func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error
for f := range fchan {
if len(batch) == batchSizeFiles || blocksHandled > batchSizeBlocks {
if err := m.CheckFolderHealth(folder); err != nil {
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folder, err)
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folderCfg.Description(), err)
return err
}
m.updateLocalsFromScanning(folder, batch)
@@ -1725,7 +1831,7 @@ func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error
}
if err := m.CheckFolderHealth(folder); err != nil {
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folder, err)
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folderCfg.Description(), err)
return err
} else if len(batch) > 0 {
m.updateLocalsFromScanning(folder, batch)
@@ -1801,13 +1907,13 @@ func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error
})
if iterError != nil {
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folder, iterError)
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folderCfg.Description(), iterError)
return iterError
}
}
if err := m.CheckFolderHealth(folder); err != nil {
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folder, err)
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folderCfg.Description(), err)
return err
} else if len(batch) > 0 {
m.updateLocalsFromScanning(folder, batch)
@@ -1863,7 +1969,12 @@ func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
var message protocol.ClusterConfig
m.fmut.RLock()
for _, folder := range m.deviceFolders[device] {
// The list of folders in the message is sorted, so we always get the
// same order.
folders := m.deviceFolders[device]
sort.Strings(folders)
for _, folder := range folders {
folderCfg := m.cfg.Folders()[folder]
fs := m.folderFiles[folder]
@@ -1876,12 +1987,8 @@ func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
DisableTempIndexes: folderCfg.DisableTempIndexes,
}
for _, device := range m.folderDevices[folder] {
// DeviceID is a value type, but with an underlying array. Copy it
// so we don't grab aliases to the same array later on in device[:]
device := device
// TODO: Set read only bit when relevant, and when we have per device
// access controls.
// Devices are sorted, so we always get the same order.
for _, device := range m.folderDevices.sortedDevices(folder) {
deviceCfg := m.cfg.Devices()[device]
var indexID protocol.IndexID
@@ -1999,8 +2106,8 @@ func (m *Model) RemoteSequence(folder string) (int64, bool) {
}
var ver int64
for _, n := range m.folderDevices[folder] {
ver += fs.Sequence(n)
for device := range m.folderDevices[folder] {
ver += fs.Sequence(device)
}
return ver, true
@@ -2070,15 +2177,18 @@ func (m *Model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly
}
func (m *Model) Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []Availability {
// Acquire this lock first, as the value returned from foldersFiles can
// get heavily modified on Close()
// The slightly unusual locking sequence here is because we need to hold
// pmut for the duration (as the value returned from foldersFiles can
// get heavily modified on Close()), but also must acquire fmut before
// pmut. (The locks can be *released* in any order.)
m.fmut.RLock()
m.pmut.RLock()
defer m.pmut.RUnlock()
m.fmut.RLock()
fs, ok := m.folderFiles[folder]
devices := m.folderDevices[folder]
m.fmut.RUnlock()
if !ok {
return nil
}
@@ -2091,7 +2201,7 @@ func (m *Model) Availability(folder, file string, version protocol.Vector, block
}
}
for _, device := range devices {
for device := range devices {
if m.deviceDownloads[device].Has(folder, file, version, int32(block.Offset/protocol.BlockSize)) {
availabilities = append(availabilities, Availability{ID: device, FromTemporary: true})
}
@@ -2201,9 +2311,9 @@ func (m *Model) runnerExchangeError(folder config.FolderConfiguration, err error
if err != nil {
if oldErr != nil && oldErr.Error() != err.Error() {
l.Infof("Folder %q error changed: %q -> %q", folder.ID, oldErr, err)
l.Infof("Folder %s error changed: %q -> %q", folder.Description(), oldErr, err)
} else if oldErr == nil {
l.Warnf("Stopping folder %q - %v", folder.ID, err)
l.Warnf("Stopping folder %s - %v", folder.Description(), err)
}
if runnerExists {
runner.setError(err)
@@ -2405,7 +2515,7 @@ func unifySubs(dirs []string, exists func(dir string) bool) []string {
func trimUntilParentKnown(dirs []string, exists func(dir string) bool) []string {
var subs []string
for _, sub := range dirs {
for sub != "" && sub != ".stfolder" && sub != ".stignore" {
for sub != "" && !ignore.IsInternal(sub) {
sub = filepath.Clean(sub)
parent := filepath.Dir(sub)
if parent == "." || exists(parent) {
@@ -2459,9 +2569,7 @@ func makeForgetUpdate(files []protocol.FileInfo) []protocol.FileDownloadProgress
}
// shouldIgnore returns true when a file should be excluded from processing
func shouldIgnore(file db.FileIntf, matcher *ignore.Matcher, ignoreDelete bool) bool {
// We check things in a certain order here...
func shouldIgnore(file db.FileIntf, matcher *ignore.Matcher, ignoreDelete bool, tmpNamer tempNamer) bool {
switch {
case ignoreDelete && file.IsDeleted():
// ignoreDelete first because it's a very cheap test so a win if it
@@ -2469,11 +2577,108 @@ func shouldIgnore(file db.FileIntf, matcher *ignore.Matcher, ignoreDelete bool)
// deleted files.
return true
case tmpNamer.IsTemporary(file.FileName()):
return true
case ignore.IsInternal(file.FileName()):
return true
case matcher.Match(file.FileName()).IsIgnored():
// ignore patterns second because ignoring them is a valid way to
// silence warnings about them being invalid and so on.
return true
}
return false
}
// folderDeviceSet is a set of (folder, deviceID) pairs
type folderDeviceSet map[string]map[protocol.DeviceID]struct{}
// set adds the (dev, folder) pair to the set
func (s folderDeviceSet) set(dev protocol.DeviceID, folder string) {
devs, ok := s[folder]
if !ok {
devs = make(map[protocol.DeviceID]struct{})
s[folder] = devs
}
devs[dev] = struct{}{}
}
// has returns true if the (dev, folder) pair is in the set
func (s folderDeviceSet) has(dev protocol.DeviceID, folder string) bool {
_, ok := s[folder][dev]
return ok
}
// hasDevice returns true if the device is set on any folder
func (s folderDeviceSet) hasDevice(dev protocol.DeviceID) bool {
for _, devices := range s {
if _, ok := devices[dev]; ok {
return true
}
}
return false
}
// sortedDevices returns the list of devices for a given folder, sorted
func (s folderDeviceSet) sortedDevices(folder string) []protocol.DeviceID {
devs := make([]protocol.DeviceID, 0, len(s[folder]))
for dev := range s[folder] {
devs = append(devs, dev)
}
sort.Sort(protocol.DeviceIDs(devs))
return devs
}
// rootedJoinedPath takes a root and a supposedly relative path inside that
// root and returns the joined path. An error is returned if the joined path
// is not in fact inside the root.
func rootedJoinedPath(root, rel string) (string, error) {
// The root must not be empty.
if root == "" {
return "", errInvalidFilename
}
pathSep := string(os.PathSeparator)
// The expected prefix for the resulting path is the root, with a path
// separator at the end.
expectedPrefix := filepath.FromSlash(root)
if !strings.HasSuffix(expectedPrefix, pathSep) {
expectedPrefix += pathSep
}
// The relative path should be clean from internal dotdots and similar
// funkyness.
rel = filepath.FromSlash(rel)
if filepath.Clean(rel) != rel {
return "", errInvalidFilename
}
// It is not acceptable to attempt to traverse upwards or refer to the
// root itself.
switch rel {
case ".", "..", pathSep:
return "", errNotRelative
}
if strings.HasPrefix(rel, ".."+pathSep) {
return "", errNotRelative
}
if strings.HasPrefix(rel, pathSep+pathSep) {
// The relative path may pretend to be an absolute path within the
// root, but the double path separator on Windows implies something
// else. It would get cleaned by the Join below, but it's out of
// spec anyway.
return "", errNotRelative
}
// The supposedly correct path is the one filepath.Join will return, as
// it does cleaning and so on. Check that one first to make sure no
// obvious escape attempts have been made.
joined := filepath.Join(root, rel)
if !strings.HasPrefix(joined, expectedPrefix) {
return "", errNotRelative
}
return joined, nil
}

View File

@@ -8,7 +8,6 @@ package model
import (
"bytes"
"crypto/tls"
"encoding/json"
"fmt"
"io/ioutil"
@@ -18,17 +17,18 @@ import (
"path/filepath"
"runtime"
"strconv"
"sync"
"testing"
"time"
"github.com/d4l3k/messagediff"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/connections"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/ignore"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/protocol"
srand "github.com/syncthing/syncthing/lib/rand"
"github.com/syncthing/syncthing/lib/scanner"
)
var device1, device2 protocol.DeviceID
@@ -218,65 +218,142 @@ type downloadProgressMessage struct {
updates []protocol.FileDownloadProgressUpdate
}
type FakeConnection struct {
type fakeConnection struct {
id protocol.DeviceID
requestData []byte
downloadProgressMessages []downloadProgressMessage
closed bool
files []protocol.FileInfo
fileData map[string][]byte
folder string
model *Model
indexFn func(string, []protocol.FileInfo)
requestFn func(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error)
mut sync.Mutex
}
func (FakeConnection) Close() error {
func (f *fakeConnection) Close() error {
f.mut.Lock()
defer f.mut.Unlock()
f.closed = true
return nil
}
func (f FakeConnection) Start() {
func (f *fakeConnection) Start() {
}
func (f FakeConnection) ID() protocol.DeviceID {
func (f *fakeConnection) ID() protocol.DeviceID {
return f.id
}
func (f FakeConnection) Name() string {
func (f *fakeConnection) Name() string {
return ""
}
func (f FakeConnection) Option(string) string {
func (f *fakeConnection) Option(string) string {
return ""
}
func (FakeConnection) Index(string, []protocol.FileInfo) error {
func (f *fakeConnection) Index(folder string, fs []protocol.FileInfo) error {
f.mut.Lock()
defer f.mut.Unlock()
if f.indexFn != nil {
f.indexFn(folder, fs)
}
return nil
}
func (FakeConnection) IndexUpdate(string, []protocol.FileInfo) error {
func (f *fakeConnection) IndexUpdate(folder string, fs []protocol.FileInfo) error {
f.mut.Lock()
defer f.mut.Unlock()
if f.indexFn != nil {
f.indexFn(folder, fs)
}
return nil
}
func (f FakeConnection) Request(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
return f.requestData, nil
func (f *fakeConnection) Request(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
f.mut.Lock()
defer f.mut.Unlock()
if f.requestFn != nil {
return f.requestFn(folder, name, offset, size, hash, fromTemporary)
}
return f.fileData[name], nil
}
func (FakeConnection) ClusterConfig(protocol.ClusterConfig) {}
func (f *fakeConnection) ClusterConfig(protocol.ClusterConfig) {}
func (FakeConnection) Ping() bool {
return true
func (f *fakeConnection) Ping() bool {
f.mut.Lock()
defer f.mut.Unlock()
return f.closed
}
func (FakeConnection) Closed() bool {
return false
func (f *fakeConnection) Closed() bool {
f.mut.Lock()
defer f.mut.Unlock()
return f.closed
}
func (FakeConnection) Statistics() protocol.Statistics {
func (f *fakeConnection) Statistics() protocol.Statistics {
return protocol.Statistics{}
}
func (f *FakeConnection) DownloadProgress(folder string, updates []protocol.FileDownloadProgressUpdate) {
func (f *fakeConnection) RemoteAddr() net.Addr {
return &fakeAddr{}
}
func (f *fakeConnection) Type() string {
return "fake"
}
func (f *fakeConnection) DownloadProgress(folder string, updates []protocol.FileDownloadProgressUpdate) {
f.downloadProgressMessages = append(f.downloadProgressMessages, downloadProgressMessage{
folder: folder,
updates: updates,
})
}
func BenchmarkRequest(b *testing.B) {
func (f *fakeConnection) addFile(name string, flags uint32, ftype protocol.FileInfoType, data []byte) {
f.mut.Lock()
defer f.mut.Unlock()
blocks, _ := scanner.Blocks(bytes.NewReader(data), protocol.BlockSize, int64(len(data)), nil)
var version protocol.Vector
version = version.Update(f.id.Short())
if ftype == protocol.FileInfoTypeFile || ftype == protocol.FileInfoTypeDirectory {
f.files = append(f.files, protocol.FileInfo{
Name: name,
Type: ftype,
Size: int64(len(data)),
ModifiedS: time.Now().Unix(),
Permissions: flags,
Version: version,
Sequence: time.Now().UnixNano(),
Blocks: blocks,
})
} else {
// Symlink
f.files = append(f.files, protocol.FileInfo{
Name: name,
Type: ftype,
Version: version,
Sequence: time.Now().UnixNano(),
SymlinkTarget: string(data),
})
}
if f.fileData == nil {
f.fileData = make(map[string][]byte)
}
f.fileData[name] = data
}
func (f *fakeConnection) sendIndexUpdate() {
f.model.IndexUpdate(f.id, f.folder, f.files)
}
func BenchmarkRequestOut(b *testing.B) {
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
@@ -287,18 +364,11 @@ func BenchmarkRequest(b *testing.B) {
const n = 1000
files := genFiles(n)
fc := &FakeConnection{
id: device1,
requestData: []byte("some data to return"),
fc := &fakeConnection{id: device1}
for _, f := range files {
fc.addFile(f.Name, 0644, protocol.FileInfoTypeFile, []byte("some data to return"))
}
m.AddConnection(connections.Connection{
IntermediateConnection: connections.IntermediateConnection{
Conn: tls.Client(&fakeConn{}, nil),
Type: "foo",
Priority: 10,
},
Connection: fc,
}, protocol.HelloResult{})
m.AddConnection(fc, protocol.HelloResult{})
m.Index(device1, "default", files)
b.ResetTimer()
@@ -313,6 +383,32 @@ func BenchmarkRequest(b *testing.B) {
}
}
func BenchmarkRequestInSingleFile(b *testing.B) {
db := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
m.AddFolder(defaultFolderConfig)
m.ServeBackground()
defer m.Stop()
m.ScanFolder("default")
buf := make([]byte, 128<<10)
rand.Read(buf)
os.RemoveAll("testdata/request")
defer os.RemoveAll("testdata/request")
os.MkdirAll("testdata/request/for/a/file/in/a/couple/of/dirs", 0755)
ioutil.WriteFile("testdata/request/for/a/file/in/a/couple/of/dirs/128k", buf, 0644)
b.ResetTimer()
for i := 0; i < b.N; i++ {
if err := m.Request(device1, "default", "request/for/a/file/in/a/couple/of/dirs/128k", 0, nil, false, buf); err != nil {
b.Error(err)
}
}
b.SetBytes(128 << 10)
}
func TestDeviceRename(t *testing.T) {
hello := protocol.HelloResult{
ClientName: "syncthing",
@@ -335,17 +431,7 @@ func TestDeviceRename(t *testing.T) {
t.Errorf("Device already has a name")
}
conn := connections.Connection{
IntermediateConnection: connections.IntermediateConnection{
Conn: tls.Client(&fakeConn{}, nil),
Type: "foo",
Priority: 10,
},
Connection: &FakeConnection{
id: device1,
requestData: []byte("some data to return"),
},
}
conn := &fakeConnection{id: device1}
m.AddConnection(conn, hello)
@@ -445,13 +531,13 @@ func TestClusterConfig(t *testing.T) {
t.Errorf("Incorrect number of devices %d != 2", l)
}
if id := r.Devices[0].ID; id != device1 {
t.Errorf("Incorrect device ID %x != %x", id, device1)
t.Errorf("Incorrect device ID %s != %s", id, device1)
}
if !r.Devices[0].Introducer {
t.Error("Device1 should be flagged as Introducer")
}
if id := r.Devices[1].ID; id != device2 {
t.Errorf("Incorrect device ID %x != %x", id, device2)
t.Errorf("Incorrect device ID %s != %s", id, device2)
}
if r.Devices[1].Introducer {
t.Error("Device2 should not be flagged as Introducer")
@@ -465,19 +551,382 @@ func TestClusterConfig(t *testing.T) {
t.Errorf("Incorrect number of devices %d != 2", l)
}
if id := r.Devices[0].ID; id != device1 {
t.Errorf("Incorrect device ID %x != %x", id, device1)
t.Errorf("Incorrect device ID %s != %s", id, device1)
}
if !r.Devices[0].Introducer {
t.Error("Device1 should be flagged as Introducer")
}
if id := r.Devices[1].ID; id != device2 {
t.Errorf("Incorrect device ID %x != %x", id, device2)
t.Errorf("Incorrect device ID %s != %s", id, device2)
}
if r.Devices[1].Introducer {
t.Error("Device2 should not be flagged as Introducer")
}
}
func TestIntroducer(t *testing.T) {
var introducedByAnyone protocol.DeviceID
// LocalDeviceID is a magic value meaning don't check introducer
contains := func(cfg config.FolderConfiguration, id, introducedBy protocol.DeviceID) bool {
for _, dev := range cfg.Devices {
if dev.DeviceID.Equals(id) {
if introducedBy.Equals(introducedByAnyone) {
return true
}
return introducedBy.Equals(introducedBy)
}
}
return false
}
newState := func(cfg config.Configuration) (*config.Wrapper, *Model) {
db := db.OpenMemory()
wcfg := config.Wrap("/tmp/test", cfg)
m := NewModel(wcfg, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
for _, folder := range cfg.Folders {
m.AddFolder(folder)
}
m.ServeBackground()
m.AddConnection(&fakeConnection{id: device1}, protocol.HelloResult{})
return wcfg, m
}
wcfg, m := newState(config.Configuration{
Devices: []config.DeviceConfiguration{
{
DeviceID: device1,
Introducer: true,
},
},
Folders: []config.FolderConfiguration{
{
ID: "folder1",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
},
},
{
ID: "folder2",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
},
},
},
})
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: "folder1",
Devices: []protocol.Device{
{
ID: device2,
Introducer: true,
SkipIntroductionRemovals: true,
},
},
},
},
})
if newDev, ok := wcfg.Device(device2); !ok || !newDev.Introducer || !newDev.SkipIntroductionRemovals {
t.Error("devie 2 missing or wrong flags")
}
if !contains(wcfg.Folders()["folder1"], device2, device1) {
t.Error("expected folder 1 to have device2 introduced by device 1")
}
wcfg, m = newState(config.Configuration{
Devices: []config.DeviceConfiguration{
{
DeviceID: device1,
Introducer: true,
},
{
DeviceID: device2,
IntroducedBy: device1,
},
},
Folders: []config.FolderConfiguration{
{
ID: "folder1",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2, IntroducedBy: device1},
},
},
{
ID: "folder2",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
},
},
},
})
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: "folder2",
Devices: []protocol.Device{
{
ID: device2,
Introducer: true,
SkipIntroductionRemovals: true,
},
},
},
},
})
// Should not get introducer, as it's already unset, and it's an existing device.
if newDev, ok := wcfg.Device(device2); !ok || newDev.Introducer || newDev.SkipIntroductionRemovals {
t.Error("device 2 missing or changed flags")
}
if contains(wcfg.Folders()["folder1"], device2, introducedByAnyone) {
t.Error("expected device 2 to be removed from folder 1")
}
if !contains(wcfg.Folders()["folder2"], device2, device1) {
t.Error("expected device 2 to be added to folder 2")
}
wcfg, m = newState(config.Configuration{
Devices: []config.DeviceConfiguration{
{
DeviceID: device1,
Introducer: true,
},
{
DeviceID: device2,
IntroducedBy: device1,
},
},
Folders: []config.FolderConfiguration{
{
ID: "folder1",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2, IntroducedBy: device1},
},
},
{
ID: "folder2",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2, IntroducedBy: device1},
},
},
},
})
m.ClusterConfig(device1, protocol.ClusterConfig{})
if _, ok := wcfg.Device(device2); ok {
t.Error("device 2 should have been removed")
}
if contains(wcfg.Folders()["folder1"], device2, introducedByAnyone) {
t.Error("expected device 2 to be removed from folder 1")
}
if contains(wcfg.Folders()["folder2"], device2, introducedByAnyone) {
t.Error("expected device 2 to be removed from folder 2")
}
// Two cases when removals should not happen
// 1. Introducer flag no longer set on device
wcfg, m = newState(config.Configuration{
Devices: []config.DeviceConfiguration{
{
DeviceID: device1,
Introducer: false,
},
{
DeviceID: device2,
IntroducedBy: device1,
},
},
Folders: []config.FolderConfiguration{
{
ID: "folder1",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2, IntroducedBy: device1},
},
},
{
ID: "folder2",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2, IntroducedBy: device1},
},
},
},
})
m.ClusterConfig(device1, protocol.ClusterConfig{})
if _, ok := wcfg.Device(device2); !ok {
t.Error("device 2 should not have been removed")
}
if !contains(wcfg.Folders()["folder1"], device2, device1) {
t.Error("expected device 2 not to be removed from folder 1")
}
if !contains(wcfg.Folders()["folder2"], device2, device1) {
t.Error("expected device 2 not to be removed from folder 2")
}
// 2. SkipIntroductionRemovals is set
wcfg, m = newState(config.Configuration{
Devices: []config.DeviceConfiguration{
{
DeviceID: device1,
Introducer: true,
SkipIntroductionRemovals: true,
},
{
DeviceID: device2,
IntroducedBy: device1,
},
},
Folders: []config.FolderConfiguration{
{
ID: "folder1",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2, IntroducedBy: device1},
},
},
{
ID: "folder2",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
},
},
},
})
m.ClusterConfig(device1, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: "folder2",
Devices: []protocol.Device{
{
ID: device2,
Introducer: true,
SkipIntroductionRemovals: true,
},
},
},
},
})
if _, ok := wcfg.Device(device2); !ok {
t.Error("device 2 should not have been removed")
}
if !contains(wcfg.Folders()["folder1"], device2, device1) {
t.Error("expected device 2 not to be removed from folder 1")
}
if !contains(wcfg.Folders()["folder2"], device2, device1) {
t.Error("expected device 2 not to be added to folder 2")
}
// Test device not being removed as it's shared without an introducer.
wcfg, m = newState(config.Configuration{
Devices: []config.DeviceConfiguration{
{
DeviceID: device1,
Introducer: true,
},
{
DeviceID: device2,
IntroducedBy: device1,
},
},
Folders: []config.FolderConfiguration{
{
ID: "folder1",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2, IntroducedBy: device1},
},
},
{
ID: "folder2",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2},
},
},
},
})
m.ClusterConfig(device1, protocol.ClusterConfig{})
if _, ok := wcfg.Device(device2); !ok {
t.Error("device 2 should not have been removed")
}
if contains(wcfg.Folders()["folder1"], device2, introducedByAnyone) {
t.Error("expected device 2 to be removed from folder 1")
}
if !contains(wcfg.Folders()["folder2"], device2, introducedByAnyone) {
t.Error("expected device 2 not to be removed from folder 2")
}
// Test device not being removed as it's shared by a different introducer.
wcfg, m = newState(config.Configuration{
Devices: []config.DeviceConfiguration{
{
DeviceID: device1,
Introducer: true,
},
{
DeviceID: device2,
IntroducedBy: device1,
},
},
Folders: []config.FolderConfiguration{
{
ID: "folder1",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2, IntroducedBy: device1},
},
},
{
ID: "folder2",
Devices: []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2, IntroducedBy: protocol.LocalDeviceID},
},
},
},
})
m.ClusterConfig(device1, protocol.ClusterConfig{})
if _, ok := wcfg.Device(device2); !ok {
t.Error("device 2 should not have been removed")
}
if contains(wcfg.Folders()["folder1"], device2, introducedByAnyone) {
t.Error("expected device 2 to be removed from folder 1")
}
if !contains(wcfg.Folders()["folder2"], device2, introducedByAnyone) {
t.Error("expected device 2 not to be removed from folder 2")
}
}
func TestIgnores(t *testing.T) {
arrEqual := func(a, b []string) bool {
if len(a) != len(b) {
@@ -551,7 +1000,7 @@ func TestIgnores(t *testing.T) {
t.Errorf("Incorrect ignores: %v != %v", ignores, expected)
}
ignores, _, err = m.GetIgnores("doesnotexist")
_, _, err = m.GetIgnores("doesnotexist")
if err == nil {
t.Error("No error")
}
@@ -1403,6 +1852,8 @@ func TestScanNoDatabaseWrite(t *testing.T) {
}
defer m.SetIgnores("default", curIgn)
m.SetIgnores("default", nil)
fakeTime := time.Now().Add(5 * time.Second)
os.Chtimes("testdata/.stignore", fakeTime, fakeTime)
// Scan the folder twice. The second scan should be a no-op database wise
@@ -1419,6 +1870,8 @@ func TestScanNoDatabaseWrite(t *testing.T) {
// Ignore a file we know exists. It'll be updated in the database.
m.SetIgnores("default", []string{"foo"})
fakeTime = time.Now().Add(10 * time.Second)
os.Chtimes("testdata/.stignore", fakeTime, fakeTime)
m.ScanFolder("default")
c2 := db.Committed()
@@ -1530,34 +1983,14 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
wcfg := config.Wrap("/tmp/test", cfg)
d2c := &fakeConn{}
m := NewModel(wcfg, protocol.LocalDeviceID, "device", "syncthing", "dev", dbi, nil)
m.AddFolder(fcfg)
m.StartFolder(fcfg.ID)
m.ServeBackground()
conn1 := connections.Connection{
IntermediateConnection: connections.IntermediateConnection{
Conn: tls.Client(&fakeConn{}, nil),
Type: "foo",
Priority: 10,
},
Connection: &FakeConnection{
id: device1,
},
}
conn1 := &fakeConnection{id: device1}
m.AddConnection(conn1, protocol.HelloResult{})
conn2 := connections.Connection{
IntermediateConnection: connections.IntermediateConnection{
Conn: tls.Client(d2c, nil),
Type: "foo",
Priority: 10,
},
Connection: &FakeConnection{
id: device2,
},
}
conn2 := &fakeConnection{id: device2}
m.AddConnection(conn2, protocol.HelloResult{})
m.ClusterConfig(device1, protocol.ClusterConfig{
@@ -1590,7 +2023,7 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
t.Error("not shared with device2")
}
if d2c.closed {
if conn2.Closed() {
t.Error("conn already closed")
}
@@ -1610,7 +2043,7 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
t.Error("shared with device2")
}
if !d2c.closed {
if !conn2.Closed() {
t.Error("connection not closed")
}
@@ -1623,7 +2056,7 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
t.Error("folder missing?")
}
for _, id := range fdevs {
for id := range fdevs {
if id == device2 {
t.Error("still there")
}
@@ -1733,20 +2166,186 @@ func TestIssue3496(t *testing.T) {
}
}
func addFakeConn(m *Model, dev protocol.DeviceID) {
conn1 := connections.Connection{
IntermediateConnection: connections.IntermediateConnection{
Conn: tls.Client(&fakeConn{}, nil),
Type: "foo",
Priority: 10,
},
Connection: &FakeConnection{
id: dev,
},
}
m.AddConnection(conn1, protocol.HelloResult{})
func TestIssue3804(t *testing.T) {
dbi := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", dbi, nil)
m.AddFolder(defaultFolderConfig)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
m.ClusterConfig(device1, protocol.ClusterConfig{
// Subdirs ending in slash should be accepted
if err := m.ScanFolderSubdirs("default", []string{"baz/", "foo"}); err != nil {
t.Error("Unexpected error:", err)
}
}
func TestIssue3829(t *testing.T) {
dbi := db.OpenMemory()
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", dbi, nil)
m.AddFolder(defaultFolderConfig)
m.StartFolder("default")
m.ServeBackground()
defer m.Stop()
// Empty subdirs should be accepted
if err := m.ScanFolderSubdirs("default", []string{""}); err != nil {
t.Error("Unexpected error:", err)
}
}
func TestRootedJoinedPath(t *testing.T) {
type testcase struct {
root string
rel string
joined string
ok bool
}
cases := []testcase{
// Valid cases
{"foo", "bar", "foo/bar", true},
{"foo", "/bar", "foo/bar", true},
{"foo/", "bar", "foo/bar", true},
{"foo/", "/bar", "foo/bar", true},
{"baz/foo", "bar", "baz/foo/bar", true},
{"baz/foo", "/bar", "baz/foo/bar", true},
{"baz/foo/", "bar", "baz/foo/bar", true},
{"baz/foo/", "/bar", "baz/foo/bar", true},
{"foo", "bar/baz", "foo/bar/baz", true},
{"foo", "/bar/baz", "foo/bar/baz", true},
{"foo/", "bar/baz", "foo/bar/baz", true},
{"foo/", "/bar/baz", "foo/bar/baz", true},
{"baz/foo", "bar/baz", "baz/foo/bar/baz", true},
{"baz/foo", "/bar/baz", "baz/foo/bar/baz", true},
{"baz/foo/", "bar/baz", "baz/foo/bar/baz", true},
{"baz/foo/", "/bar/baz", "baz/foo/bar/baz", true},
// Not escape attempts, but oddly formatted relative paths. Disallowed.
{"foo", "./bar", "", false},
{"baz/foo", "./bar", "", false},
{"foo", "./bar/baz", "", false},
{"baz/foo", "./bar/baz", "", false},
{"baz/foo", "bar/../baz", "", false},
{"baz/foo", "/bar/../baz", "", false},
{"baz/foo", "./bar/../baz", "", false},
{"baz/foo", "bar/../baz", "", false},
{"baz/foo", "/bar/../baz", "", false},
{"baz/foo", "./bar/../baz", "", false},
// Results in an allowed path, but does it by probing. Disallowed.
{"foo", "../foo", "", false},
{"foo", "../foo/bar", "", false},
{"baz/foo", "../foo/bar", "", false},
{"baz/foo", "../../baz/foo/bar", "", false},
{"baz/foo", "bar/../../foo/bar", "", false},
{"baz/foo", "bar/../../../baz/foo/bar", "", false},
// Escape attempts.
{"foo", "", "", false},
{"foo", "/", "", false},
{"foo", "..", "", false},
{"foo", "/..", "", false},
{"foo", "../", "", false},
{"foo", "../bar", "", false},
{"foo", "../foobar", "", false},
{"foo/", "../bar", "", false},
{"foo/", "../foobar", "", false},
{"baz/foo", "../bar", "", false},
{"baz/foo", "../foobar", "", false},
{"baz/foo/", "../bar", "", false},
{"baz/foo/", "../foobar", "", false},
{"baz/foo/", "bar/../../quux/baz", "", false},
// Empty root is a misconfiguration.
{"", "/foo", "", false},
{"", "foo", "", false},
{"", ".", "", false},
{"", "..", "", false},
{"", "/", "", false},
{"", "", "", false},
// Root=/ is valid, and things should be verified as usual.
{"/", "foo", "/foo", true},
{"/", "/foo", "/foo", true},
{"/", "../foo", "", false},
{"/", ".", "", false},
{"/", "..", "", false},
{"/", "/", "", false},
{"/", "", "", false},
}
if runtime.GOOS == "windows" {
extraCases := []testcase{
{`c:\`, `foo`, `c:\foo`, true},
{`\\?\c:\`, `foo`, `\\?\c:\foo`, true},
{`c:\`, `\foo`, `c:\foo`, true},
{`\\?\c:\`, `\foo`, `\\?\c:\foo`, true},
{`c:\`, `\\foo`, ``, false},
{`c:\`, ``, ``, false},
{`c:\`, `.`, ``, false},
{`c:\`, `\`, ``, false},
{`\\?\c:\`, `\\foo`, ``, false},
{`\\?\c:\`, ``, ``, false},
{`\\?\c:\`, `.`, ``, false},
{`\\?\c:\`, `\`, ``, false},
// makes no sense, but will be treated simply as a bad filename
{`c:\foo`, `d:\bar`, `c:\foo\d:\bar`, true},
}
for _, tc := range cases {
// Add case where root is backslashed, rel is forward slashed
extraCases = append(extraCases, testcase{
root: filepath.FromSlash(tc.root),
rel: tc.rel,
joined: tc.joined,
ok: tc.ok,
})
// and the opposite
extraCases = append(extraCases, testcase{
root: tc.root,
rel: filepath.FromSlash(tc.rel),
joined: tc.joined,
ok: tc.ok,
})
// and both backslashed
extraCases = append(extraCases, testcase{
root: filepath.FromSlash(tc.root),
rel: filepath.FromSlash(tc.rel),
joined: tc.joined,
ok: tc.ok,
})
}
cases = append(cases, extraCases...)
}
for _, tc := range cases {
res, err := rootedJoinedPath(tc.root, tc.rel)
if tc.ok {
if err != nil {
t.Errorf("Unexpected error for rootedJoinedPath(%q, %q): %v", tc.root, tc.rel, err)
continue
}
exp := filepath.FromSlash(tc.joined)
if res != exp {
t.Errorf("Unexpected result for rootedJoinedPath(%q, %q): %q != expected %q", tc.root, tc.rel, res, exp)
}
} else if err == nil {
t.Errorf("Unexpected pass for rootedJoinedPath(%q, %q) => %q", tc.root, tc.rel, res)
continue
}
}
}
func addFakeConn(m *Model, dev protocol.DeviceID) *fakeConnection {
fc := &fakeConnection{id: dev, model: m}
m.AddConnection(fc, protocol.HelloResult{})
m.ClusterConfig(dev, protocol.ClusterConfig{
Folders: []protocol.Folder{
{
ID: "default",
@@ -1757,6 +2356,8 @@ func addFakeConn(m *Model, dev protocol.DeviceID) {
},
},
})
return fc
}
type fakeAddr struct{}
@@ -1768,40 +2369,3 @@ func (fakeAddr) Network() string {
func (fakeAddr) String() string {
return "address"
}
type fakeConn struct {
closed bool
}
func (c *fakeConn) Close() error {
c.closed = true
return nil
}
func (fakeConn) LocalAddr() net.Addr {
return &fakeAddr{}
}
func (fakeConn) RemoteAddr() net.Addr {
return &fakeAddr{}
}
func (fakeConn) Read([]byte) (int, error) {
return 0, nil
}
func (fakeConn) Write([]byte) (int, error) {
return 0, nil
}
func (fakeConn) SetDeadline(time.Time) error {
return nil
}
func (fakeConn) SetReadDeadline(time.Time) error {
return nil
}
func (fakeConn) SetWriteDeadline(time.Time) error {
return nil
}

View File

@@ -8,7 +8,6 @@ package model
import (
"fmt"
"path/filepath"
"time"
"github.com/syncthing/syncthing/lib/config"
@@ -42,7 +41,7 @@ func NewProgressEmitter(cfg *config.Wrapper) *ProgressEmitter {
mut: sync.NewMutex(),
}
t.CommitConfiguration(config.Configuration{}, cfg.Raw())
t.CommitConfiguration(config.Configuration{}, cfg.RawCopy())
cfg.Subscribe(t)
return t
@@ -213,7 +212,9 @@ func (t *ProgressEmitter) Register(s *sharedPullerState) {
if len(t.registry) == 0 {
t.timer.Reset(t.interval)
}
t.registry[filepath.Join(s.folder, s.file.Name)] = s
// Separate the folder ID (arbitrary string) and and the file name by "//"
// because it never appears in a valid file name.
t.registry[s.folder+"//"+s.file.Name] = s
}
// Deregister a puller which will stop broadcasting pullers state.
@@ -223,7 +224,7 @@ func (t *ProgressEmitter) Deregister(s *sharedPullerState) {
l.Debugln("progress emitter: deregistering", s.folder, s.file.Name)
delete(t.registry, filepath.Join(s.folder, s.file.Name))
delete(t.registry, s.folder+"//"+s.file.Name)
}
// BytesCompleted returns the number of bytes completed in the given folder.

View File

@@ -107,7 +107,7 @@ func TestSendDownloadProgressMessages(t *testing.T) {
TempIndexMinBlocks: 10,
})
fc := &FakeConnection{}
fc := &fakeConnection{}
p := NewProgressEmitter(c)
p.temporaryIndexSubscribe(fc, []string{"folder", "folder2"})

227
lib/model/requests_test.go Normal file
View File

@@ -0,0 +1,227 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package model
import (
"bytes"
"io/ioutil"
"os"
"runtime"
"strings"
"testing"
"time"
"github.com/syncthing/syncthing/lib/config"
"github.com/syncthing/syncthing/lib/db"
"github.com/syncthing/syncthing/lib/protocol"
)
func TestRequestSimple(t *testing.T) {
// Verify that the model performs a request and creates a file based on
// an incoming index update.
defer os.RemoveAll("_tmpfolder")
m, fc := setupModelWithConnection()
defer m.Stop()
// We listen for incoming index updates and trigger when we see one for
// the expected test file.
done := make(chan struct{})
fc.mut.Lock()
fc.indexFn = func(folder string, fs []protocol.FileInfo) {
for _, f := range fs {
if f.Name == "testfile" {
close(done)
return
}
}
}
fc.mut.Unlock()
// Send an update for the test file, wait for it to sync and be reported back.
contents := []byte("test file contents\n")
fc.addFile("testfile", 0644, protocol.FileInfoTypeFile, contents)
fc.sendIndexUpdate()
<-done
// Verify the contents
bs, err := ioutil.ReadFile("_tmpfolder/testfile")
if err != nil {
t.Error("File did not sync correctly:", err)
return
}
if !bytes.Equal(bs, contents) {
t.Error("File did not sync correctly: incorrect data")
}
}
func TestSymlinkTraversalRead(t *testing.T) {
// Verify that a symlink can not be traversed for reading.
if runtime.GOOS == "windows" {
t.Skip("no symlink support on CI")
return
}
defer os.RemoveAll("_tmpfolder")
m, fc := setupModelWithConnection()
defer m.Stop()
// We listen for incoming index updates and trigger when we see one for
// the expected test file.
done := make(chan struct{})
fc.mut.Lock()
fc.indexFn = func(folder string, fs []protocol.FileInfo) {
for _, f := range fs {
if f.Name == "symlink" {
close(done)
return
}
}
}
fc.mut.Unlock()
// Send an update for the symlink, wait for it to sync and be reported back.
contents := []byte("..")
fc.addFile("symlink", 0644, protocol.FileInfoTypeSymlinkDirectory, contents)
fc.sendIndexUpdate()
<-done
// Request a file by traversing the symlink
buf := make([]byte, 10)
err := m.Request(device1, "default", "symlink/requests_test.go", 0, nil, false, buf)
if err == nil || !bytes.Equal(buf, make([]byte, 10)) {
t.Error("Managed to traverse symlink")
}
}
func TestSymlinkTraversalWrite(t *testing.T) {
// Verify that a symlink can not be traversed for writing.
if runtime.GOOS == "windows" {
t.Skip("no symlink support on CI")
return
}
defer os.RemoveAll("_tmpfolder")
m, fc := setupModelWithConnection()
defer m.Stop()
// We listen for incoming index updates and trigger when we see one for
// the expected names.
done := make(chan struct{}, 1)
badReq := make(chan string, 1)
badIdx := make(chan string, 1)
fc.mut.Lock()
fc.indexFn = func(folder string, fs []protocol.FileInfo) {
for _, f := range fs {
if f.Name == "symlink" {
done <- struct{}{}
return
}
if strings.HasPrefix(f.Name, "symlink") {
badIdx <- f.Name
return
}
}
}
fc.requestFn = func(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
if name != "symlink" && strings.HasPrefix(name, "symlink") {
badReq <- name
}
return fc.fileData[name], nil
}
fc.mut.Unlock()
// Send an update for the symlink, wait for it to sync and be reported back.
contents := []byte("..")
fc.addFile("symlink", 0644, protocol.FileInfoTypeSymlinkDirectory, contents)
fc.sendIndexUpdate()
<-done
// Send an update for things behind the symlink, wait for requests for
// blocks for any of them to come back, or index entries. Hopefully none
// of that should happen.
contents = []byte("testdata testdata\n")
fc.addFile("symlink/testfile", 0644, protocol.FileInfoTypeFile, contents)
fc.addFile("symlink/testdir", 0644, protocol.FileInfoTypeDirectory, contents)
fc.addFile("symlink/testsyml", 0644, protocol.FileInfoTypeSymlinkFile, contents)
fc.sendIndexUpdate()
select {
case name := <-badReq:
t.Fatal("Should not have requested the data for", name)
case name := <-badIdx:
t.Fatal("Should not have sent the index entry for", name)
case <-time.After(3 * time.Second):
// Unfortunately not much else to trigger on here. The puller sleep
// interval is 1s so if we didn't get any requests within two
// iterations we should be fine.
}
}
func TestRequestCreateTmpSymlink(t *testing.T) {
// Verify that the model performs a request and creates a file based on
// an incoming index update.
defer os.RemoveAll("_tmpfolder")
m, fc := setupModelWithConnection()
defer m.Stop()
// We listen for incoming index updates and trigger when we see one for
// the expected test file.
badIdx := make(chan string)
fc.mut.Lock()
fc.indexFn = func(folder string, fs []protocol.FileInfo) {
for _, f := range fs {
if f.Name == ".syncthing.testlink.tmp" {
badIdx <- f.Name
return
}
}
}
fc.mut.Unlock()
// Send an update for the test file, wait for it to sync and be reported back.
fc.addFile(".syncthing.testlink.tmp", 0644, protocol.FileInfoTypeSymlinkDirectory, []byte(".."))
fc.sendIndexUpdate()
select {
case name := <-badIdx:
t.Fatal("Should not have sent the index entry for", name)
case <-time.After(3 * time.Second):
// Unfortunately not much else to trigger on here. The puller sleep
// interval is 1s so if we didn't get any requests within two
// iterations we should be fine.
}
}
func setupModelWithConnection() (*Model, *fakeConnection) {
cfg := defaultConfig.RawCopy()
cfg.Folders[0] = config.NewFolderConfiguration("default", "_tmpfolder")
cfg.Folders[0].PullerSleepS = 1
cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{
{DeviceID: device1},
{DeviceID: device2},
}
w := config.Wrap("/tmp/cfg", cfg)
db := db.OpenMemory()
m := NewModel(w, device1, "device", "syncthing", "dev", db, nil)
m.AddFolder(cfg.Folders[0])
m.ServeBackground()
m.StartFolder("default")
fc := addFakeConn(m, device2)
fc.folder = "default"
return m, fc
}

View File

@@ -22,11 +22,11 @@ type roFolder struct {
folder
}
func newROFolder(model *Model, config config.FolderConfiguration, _ versioner.Versioner, _ *fs.MtimeFS) service {
func newROFolder(model *Model, cfg config.FolderConfiguration, _ versioner.Versioner, _ *fs.MtimeFS) service {
return &roFolder{
folder: folder{
stateTracker: newStateTracker(config.ID),
scan: newFolderScanner(config),
stateTracker: newStateTracker(cfg.ID),
scan: newFolderScanner(cfg),
stop: make(chan struct{}),
model: model,
},

View File

@@ -9,7 +9,6 @@ package model
import (
"errors"
"fmt"
"io/ioutil"
"math/rand"
"os"
"path/filepath"
@@ -63,6 +62,7 @@ const (
dbUpdateHandleFile
dbUpdateDeleteFile
dbUpdateShortcutFile
dbUpdateHandleSymlink
)
const (
@@ -91,6 +91,7 @@ type rwFolder struct {
allowSparse bool
checkFreeSpace bool
ignoreDelete bool
fsync bool
copiers int
pullers int
@@ -126,6 +127,7 @@ func newRWFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Ver
allowSparse: !cfg.DisableSparseFiles,
checkFreeSpace: cfg.MinDiskFreePct != 0,
ignoreDelete: cfg.IgnoreDelete,
fsync: cfg.Fsync,
queue: newJobQueue(),
pullTimer: time.NewTimer(time.Second),
@@ -141,7 +143,7 @@ func newRWFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Ver
return f
}
func (f *rwFolder) configureCopiersAndPullers(config config.FolderConfiguration) {
func (f *rwFolder) configureCopiersAndPullers(cfg config.FolderConfiguration) {
if f.copiers == 0 {
f.copiers = defaultCopiers
}
@@ -149,16 +151,16 @@ func (f *rwFolder) configureCopiersAndPullers(config config.FolderConfiguration)
f.pullers = defaultPullers
}
if config.PullerPauseS == 0 {
if cfg.PullerPauseS == 0 {
f.pause = defaultPullerPause
} else {
f.pause = time.Duration(config.PullerPauseS) * time.Second
f.pause = time.Duration(cfg.PullerPauseS) * time.Second
}
if config.PullerSleepS == 0 {
if cfg.PullerSleepS == 0 {
f.sleep = defaultPullerSleep
} else {
f.sleep = time.Duration(config.PullerSleepS) * time.Second
f.sleep = time.Duration(cfg.PullerSleepS) * time.Second
}
}
@@ -380,23 +382,81 @@ func (f *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
folderFiles := f.model.folderFiles[f.folderID]
f.model.fmut.RUnlock()
// !!!
// WithNeed takes a database snapshot (by necessity). By the time we've
// handled a bunch of files it might have become out of date and we might
// be attempting to sync with an old version of a file...
// !!!
changed := 0
var processDirectly []protocol.FileInfo
// Iterate the list of items that we need and sort them into piles.
// Regular files to pull goes into the file queue, everything else
// (directories, symlinks and deletes) goes into the "process directly"
// pile.
folderFiles.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
if shouldIgnore(intf, ignores, f.ignoreDelete, defTempNamer) {
return true
}
if err := fileValid(intf); err != nil {
// The file isn't valid so we can't process it. Pretend that we
// tried and set the error for the file.
f.newError(intf.FileName(), err)
changed++
return true
}
file := intf.(protocol.FileInfo)
switch {
case file.IsDeleted():
processDirectly = append(processDirectly, file)
changed++
case file.Type == protocol.FileInfoTypeFile:
// Queue files for processing after directories and symlinks, if
// it has availability.
devices := folderFiles.Availability(file.Name)
for _, dev := range devices {
if f.model.ConnectedTo(dev) {
f.queue.Push(file.Name, file.Size, file.ModTime())
changed++
break
}
}
default:
// Directories, symlinks
processDirectly = append(processDirectly, file)
changed++
}
return true
})
// Sort the "process directly" pile by number of path components. This
// ensures that we handle parents before children.
sort.Sort(byComponentCount(processDirectly))
// Process the list.
fileDeletions := map[string]protocol.FileInfo{}
dirDeletions := []protocol.FileInfo{}
buckets := map[string][]protocol.FileInfo{}
handleFile := func(fi protocol.FileInfo) bool {
for _, fi := range processDirectly {
// Verify that the thing we are handling lives inside a directory,
// and not a symlink or empty space.
if !osutil.IsDir(f.dir, filepath.Dir(fi.Name)) {
f.newError(fi.Name, errNotDir)
continue
}
switch {
case fi.IsDeleted():
// A deleted file, directory or symlink
if fi.IsDirectory() {
// Perform directory deletions at the end, as we may have
// files to delete inside them before we get to that point.
dirDeletions = append(dirDeletions, fi)
} else {
fileDeletions[fi.Name] = fi
@@ -411,55 +471,22 @@ func (f *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
buckets[key] = append(buckets[key], df)
}
}
case fi.IsDirectory() && !fi.IsSymlink():
// A new or changed directory
l.Debugln("Creating directory", fi.Name)
l.Debugln("Handling directory", fi.Name)
f.handleDir(fi)
case fi.IsSymlink():
l.Debugln("Handling symlink", fi.Name)
f.handleSymlink(fi)
default:
return false
l.Warnln(fi)
panic("unhandleable item type, can't happen")
}
return true
}
folderFiles.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
// Needed items are delivered sorted lexicographically. We'll handle
// directories as they come along, so parents before children. Files
// are queued and the order may be changed later.
if shouldIgnore(intf, ignores, f.ignoreDelete) {
return true
}
if err := fileValid(intf); err != nil {
// The file isn't valid so we can't process it. Pretend that we
// tried and set the error for the file.
f.newError(intf.FileName(), err)
changed++
return true
}
file := intf.(protocol.FileInfo)
l.Debugln(f, "handling", file.Name)
if !handleFile(file) {
// A new or changed file or symlink. This is the only case where
// we do stuff concurrently in the background. We only queue
// files where we are connected to at least one device that has
// the file.
devices := folderFiles.Availability(file.Name)
for _, dev := range devices {
if f.model.ConnectedTo(dev) {
f.queue.Push(file.Name, file.Size, file.ModTime())
changed++
break
}
}
}
return true
})
// Reorder the file queue according to configuration
// Now do the file queue. Reorder it according to configuration.
switch f.order {
case config.OrderRandom:
@@ -476,7 +503,7 @@ func (f *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
f.queue.SortNewestFirst()
}
// Process the file queue
// Process the file queue.
nextFile:
for {
@@ -499,38 +526,45 @@ nextFile:
continue
}
// Handles races where an index update arrives changing what the file
// is between queueing and retrieving it from the queue, effectively
// changing how the file should be handled.
if handleFile(fi) {
if fi.IsDeleted() || fi.Type != protocol.FileInfoTypeFile {
// The item has changed type or status in the index while we
// were processing directories above.
f.queue.Done(fileName)
continue
}
if !fi.IsSymlink() {
key := string(fi.Blocks[0].Hash)
for i, candidate := range buckets[key] {
if scanner.BlocksEqual(candidate.Blocks, fi.Blocks) {
// Remove the candidate from the bucket
lidx := len(buckets[key]) - 1
buckets[key][i] = buckets[key][lidx]
buckets[key] = buckets[key][:lidx]
// Verify that the thing we are handling lives inside a directory,
// and not a symlink or empty space.
if !osutil.IsDir(f.dir, filepath.Dir(fi.Name)) {
f.newError(fi.Name, errNotDir)
continue
}
// candidate is our current state of the file, where as the
// desired state with the delete bit set is in the deletion
// map.
desired := fileDeletions[candidate.Name]
// Remove the pending deletion (as we perform it by renaming)
delete(fileDeletions, candidate.Name)
// Check our list of files to be removed for a match, in which case
// we can just do a rename instead.
key := string(fi.Blocks[0].Hash)
for i, candidate := range buckets[key] {
if scanner.BlocksEqual(candidate.Blocks, fi.Blocks) {
// Remove the candidate from the bucket
lidx := len(buckets[key]) - 1
buckets[key][i] = buckets[key][lidx]
buckets[key] = buckets[key][:lidx]
f.renameFile(desired, fi)
// candidate is our current state of the file, where as the
// desired state with the delete bit set is in the deletion
// map.
desired := fileDeletions[candidate.Name]
// Remove the pending deletion (as we perform it by renaming)
delete(fileDeletions, candidate.Name)
f.queue.Done(fileName)
continue nextFile
}
f.renameFile(desired, fi)
f.queue.Done(fileName)
continue nextFile
}
}
// Not a rename or a symlink, deal with it.
// Handle the file normally, by coping and pulling, etc.
f.handleFile(fi, copyChan, finisherChan)
}
@@ -567,7 +601,10 @@ nextFile:
// handleDir creates or updates the given directory
func (f *rwFolder) handleDir(file protocol.FileInfo) {
// Used in the defer closure below, updated by the function body. Take
// care not declare another err.
var err error
events.Default.Log(events.ItemStarted, map[string]string{
"folder": f.folderID,
"item": file.Name,
@@ -585,7 +622,11 @@ func (f *rwFolder) handleDir(file protocol.FileInfo) {
})
}()
realName := filepath.Join(f.dir, file.Name)
realName, err := rootedJoinedPath(f.dir, file.Name)
if err != nil {
f.newError(file.Name, err)
return
}
mode := os.FileMode(file.Permissions & 0777)
if f.ignorePermissions(file) {
mode = 0777
@@ -660,15 +701,93 @@ func (f *rwFolder) handleDir(file protocol.FileInfo) {
}
}
// handleSymlink creates or updates the given symlink
func (f *rwFolder) handleSymlink(file protocol.FileInfo) {
// Used in the defer closure below, updated by the function body. Take
// care not declare another err.
var err error
events.Default.Log(events.ItemStarted, map[string]string{
"folder": f.folderID,
"item": file.Name,
"type": "symlink",
"action": "update",
})
defer func() {
events.Default.Log(events.ItemFinished, map[string]interface{}{
"folder": f.folderID,
"item": file.Name,
"error": events.Error(err),
"type": "symlink",
"action": "update",
})
}()
realName, err := rootedJoinedPath(f.dir, file.Name)
if err != nil {
f.newError(file.Name, err)
return
}
if shouldDebug() {
curFile, _ := f.model.CurrentFolderFile(f.folderID, file.Name)
l.Debugf("need symlink\n\t%v\n\t%v", file, curFile)
}
if len(file.SymlinkTarget) == 0 {
// Index entry from a Syncthing predating the support for including
// the link target in the index entry. We log this as an error.
err = errors.New("incompatible symlink entry; rescan with newer Syncthing on source")
l.Infof("Puller (folder %q, dir %q): %v", f.folderID, file.Name, err)
f.newError(file.Name, err)
return
}
if _, err = f.mtimeFS.Lstat(realName); err == nil {
// There is already something under that name. Remove it to replace
// with the symlink. This also handles the "change symlink type"
// path.
err = osutil.InWritableDir(os.Remove, realName)
if err != nil {
l.Infof("Puller (folder %q, dir %q): %v", f.folderID, file.Name, err)
f.newError(file.Name, err)
return
}
}
tt := symlinks.TargetFile
if file.IsDirectory() {
tt = symlinks.TargetDirectory
}
// We declare a function that acts on only the path name, so
// we can pass it to InWritableDir.
createLink := func(path string) error {
return symlinks.Create(path, file.SymlinkTarget, tt)
}
if err = osutil.InWritableDir(createLink, realName); err == nil {
f.dbUpdates <- dbUpdateJob{file, dbUpdateHandleSymlink}
} else {
l.Infof("Puller (folder %q, dir %q): %v", f.folderID, file.Name, err)
f.newError(file.Name, err)
}
}
// deleteDir attempts to delete the given directory
func (f *rwFolder) deleteDir(file protocol.FileInfo, matcher *ignore.Matcher) {
// Used in the defer closure below, updated by the function body. Take
// care not declare another err.
var err error
events.Default.Log(events.ItemStarted, map[string]string{
"folder": f.folderID,
"item": file.Name,
"type": "dir",
"action": "delete",
})
defer func() {
events.Default.Log(events.ItemFinished, map[string]interface{}{
"folder": f.folderID,
@@ -679,7 +798,12 @@ func (f *rwFolder) deleteDir(file protocol.FileInfo, matcher *ignore.Matcher) {
})
}()
realName := filepath.Join(f.dir, file.Name)
realName, err := rootedJoinedPath(f.dir, file.Name)
if err != nil {
f.newError(file.Name, err)
return
}
// Delete any temporary files lying around in the directory
dir, _ := os.Open(realName)
if dir != nil {
@@ -711,13 +835,17 @@ func (f *rwFolder) deleteDir(file protocol.FileInfo, matcher *ignore.Matcher) {
// deleteFile attempts to delete the given file
func (f *rwFolder) deleteFile(file protocol.FileInfo) {
// Used in the defer closure below, updated by the function body. Take
// care not declare another err.
var err error
events.Default.Log(events.ItemStarted, map[string]string{
"folder": f.folderID,
"item": file.Name,
"type": "file",
"action": "delete",
})
defer func() {
events.Default.Log(events.ItemFinished, map[string]interface{}{
"folder": f.folderID,
@@ -728,7 +856,11 @@ func (f *rwFolder) deleteFile(file protocol.FileInfo) {
})
}()
realName := filepath.Join(f.dir, file.Name)
realName, err := rootedJoinedPath(f.dir, file.Name)
if err != nil {
f.newError(file.Name, err)
return
}
cur, ok := f.model.CurrentFolderFile(f.folderID, file.Name)
if ok && f.inConflict(cur.Version, file.Version) {
@@ -761,7 +893,10 @@ func (f *rwFolder) deleteFile(file protocol.FileInfo) {
// renameFile attempts to rename an existing file to a destination
// and set the right attributes on it.
func (f *rwFolder) renameFile(source, target protocol.FileInfo) {
// Used in the defer closure below, updated by the function body. Take
// care not declare another err.
var err error
events.Default.Log(events.ItemStarted, map[string]string{
"folder": f.folderID,
"item": source.Name,
@@ -774,6 +909,7 @@ func (f *rwFolder) renameFile(source, target protocol.FileInfo) {
"type": "file",
"action": "update",
})
defer func() {
events.Default.Log(events.ItemFinished, map[string]interface{}{
"folder": f.folderID,
@@ -793,8 +929,16 @@ func (f *rwFolder) renameFile(source, target protocol.FileInfo) {
l.Debugln(f, "taking rename shortcut", source.Name, "->", target.Name)
from := filepath.Join(f.dir, source.Name)
to := filepath.Join(f.dir, target.Name)
from, err := rootedJoinedPath(f.dir, source.Name)
if err != nil {
f.newError(source.Name, err)
return
}
to, err := rootedJoinedPath(f.dir, target.Name)
if err != nil {
f.newError(target.Name, err)
return
}
if f.versioner != nil {
err = osutil.Copy(from, to)
@@ -890,13 +1034,7 @@ func (f *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
f.queue.Done(file.Name)
var err error
if file.IsSymlink() {
err = f.shortcutSymlink(file)
} else {
err = f.shortcutFile(file)
}
err := f.shortcutFile(file)
events.Default.Log(events.ItemFinished, map[string]interface{}{
"folder": f.folderID,
"item": file.Name,
@@ -916,8 +1054,16 @@ func (f *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
}
// Figure out the absolute filenames we need once and for all
tempName := filepath.Join(f.dir, defTempNamer.TempName(file.Name))
realName := filepath.Join(f.dir, file.Name)
tempName, err := rootedJoinedPath(f.dir, defTempNamer.TempName(file.Name))
if err != nil {
f.newError(file.Name, err)
return
}
realName, err := rootedJoinedPath(f.dir, file.Name)
if err != nil {
f.newError(file.Name, err)
return
}
if hasCurFile && !curFile.IsDirectory() && !curFile.IsSymlink() {
// Check that the file on disk is what we expect it to be according to
@@ -1035,7 +1181,11 @@ func (f *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
// shortcutFile sets file mode and modification time, when that's the only
// thing that has changed.
func (f *rwFolder) shortcutFile(file protocol.FileInfo) error {
realName := filepath.Join(f.dir, file.Name)
realName, err := rootedJoinedPath(f.dir, file.Name)
if err != nil {
f.newError(file.Name, err)
return err
}
if !f.ignorePermissions(file) {
if err := os.Chmod(realName, os.FileMode(file.Permissions&0777)); err != nil {
l.Infof("Puller (folder %q, file %q): shortcut: chmod: %v", f.folderID, file.Name, err)
@@ -1055,20 +1205,6 @@ func (f *rwFolder) shortcutFile(file protocol.FileInfo) error {
return nil
}
// shortcutSymlink changes the symlinks type if necessary.
func (f *rwFolder) shortcutSymlink(file protocol.FileInfo) (err error) {
tt := symlinks.TargetFile
if file.IsDirectory() {
tt = symlinks.TargetDirectory
}
err = symlinks.ChangeType(filepath.Join(f.dir, file.Name), tt)
if err != nil {
l.Infof("Puller (folder %q, file %q): symlink shortcut: %v", f.folderID, file.Name, err)
f.newError(file.Name, err)
}
return
}
// copierRoutine reads copierStates until the in channel closes and performs
// the relevant copies when possible, or passes it to the puller routine.
func (f *rwFolder) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pullBlockState, out chan<- *sharedPullerState) {
@@ -1110,7 +1246,11 @@ func (f *rwFolder) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pull
buf = buf[:int(block.Size)]
found := f.model.finder.Iterate(folders, block.Hash, func(folder, file string, index int32) bool {
fd, err := os.Open(filepath.Join(folderRoots[folder], file))
inFile, err := rootedJoinedPath(folderRoots[folder], file)
if err != nil {
return false
}
fd, err := os.Open(inFile)
if err != nil {
return false
}
@@ -1294,27 +1434,6 @@ func (f *rwFolder) performFinish(state *sharedPullerState) error {
// Set the correct timestamp on the new file
f.mtimeFS.Chtimes(state.realName, state.file.ModTime(), state.file.ModTime()) // never fails
// If it's a symlink, the target of the symlink is inside the file.
if state.file.IsSymlink() {
content, err := ioutil.ReadFile(state.realName)
if err != nil {
return err
}
// Remove the file, and replace it with a symlink.
err = osutil.InWritableDir(func(path string) error {
os.Remove(path)
tt := symlinks.TargetFile
if state.file.IsDirectory() {
tt = symlinks.TargetDirectory
}
return symlinks.Create(path, string(content), tt)
}, state.realName)
if err != nil {
return err
}
}
// Record the updated file in the index
f.dbUpdates <- dbUpdateJob{state.file, dbUpdateHandleFile}
return nil
@@ -1372,12 +1491,47 @@ func (f *rwFolder) dbUpdaterRoutine() {
tick := time.NewTicker(maxBatchTime)
defer tick.Stop()
var changedFiles []string
var changedDirs []string
if f.fsync {
changedFiles = make([]string, 0, maxBatchSize)
changedDirs = make([]string, 0, maxBatchSize)
}
syncFilesOnce := func(files []string, syncFn func(string) error) {
sort.Strings(files)
var lastFile string
for _, file := range files {
if lastFile == file {
continue
}
lastFile = file
if err := syncFn(file); err != nil {
l.Infof("fsync %q failed: %v", file, err)
}
}
}
handleBatch := func() {
found := false
var lastFile protocol.FileInfo
for _, job := range batch {
files = append(files, job.file)
if f.fsync {
// collect changed files and dirs
switch job.jobType {
case dbUpdateHandleFile, dbUpdateShortcutFile:
changedFiles = append(changedFiles, filepath.Join(f.dir, job.file.Name))
case dbUpdateHandleDir:
changedDirs = append(changedDirs, filepath.Join(f.dir, job.file.Name))
case dbUpdateHandleSymlink:
// fsyncing symlinks is only supported by MacOS, ignore
}
if job.jobType != dbUpdateShortcutFile {
changedDirs = append(changedDirs, filepath.Dir(filepath.Join(f.dir, job.file.Name)))
}
}
if job.file.IsInvalid() || (job.file.IsDirectory() && !job.file.IsSymlink()) {
continue
}
@@ -1390,6 +1544,14 @@ func (f *rwFolder) dbUpdaterRoutine() {
lastFile = job.file
}
if f.fsync {
// sync files and dirs to disk
syncFilesOnce(changedFiles, osutil.SyncFile)
changedFiles = changedFiles[:0]
syncFilesOnce(changedDirs, osutil.SyncDir)
changedDirs = changedDirs[:0]
}
// All updates to file/folder objects that originated remotely
// (across the network) use this call to updateLocals
f.model.updateLocalsFromPulling(f.folderID, files)
@@ -1590,3 +1752,29 @@ func windowsInvalidFilename(name string) bool {
// The path must not contain any disallowed characters
return strings.ContainsAny(name, windowsDisallowedCharacters)
}
// byComponentCount sorts by the number of path components in Name, that is
// "x/y" sorts before "foo/bar/baz".
type byComponentCount []protocol.FileInfo
func (l byComponentCount) Len() int {
return len(l)
}
func (l byComponentCount) Less(a, b int) bool {
return componentCount(l[a].Name) < componentCount(l[b].Name)
}
func (l byComponentCount) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
func componentCount(name string) int {
count := 0
for _, codepoint := range name {
if codepoint == os.PathSeparator {
count++
}
}
return count
}

View File

@@ -19,8 +19,10 @@ import (
)
func init() {
// We do this to make sure that the temp file required for the tests does
// not get removed during the tests.
// We do this to make sure that the temp file required for the tests
// does not get removed during the tests. Also set the prefix so it's
// found correctly regardless of platform.
defTempNamer.prefix = windowsTempPrefix
future := time.Now().Add(time.Hour)
err := os.Chtimes(filepath.Join("testdata", defTempNamer.TempName("file")), future, future)
if err != nil {

View File

@@ -15,26 +15,41 @@ import (
)
type tempNamer struct {
prefix string
prefix string
recognize []string
}
const (
windowsTempPrefix = "~syncthing~"
unixTempPrefix = ".syncthing."
)
var defTempNamer tempNamer
// Real filesystems usually handle 255 bytes. encfs has varying and
// confusing file name limits. We take a safe way out and switch to hashing
// quite early.
const maxFilenameLength = 160 - len(".syncthing.") - len(".tmp")
const maxFilenameLength = 160 - len(unixTempPrefix) - len(".tmp")
func init() {
if runtime.GOOS == "windows" {
defTempNamer = tempNamer{"~syncthing~"}
defTempNamer = tempNamer{windowsTempPrefix, []string{unixTempPrefix, windowsTempPrefix}}
} else {
defTempNamer = tempNamer{".syncthing."}
defTempNamer = tempNamer{unixTempPrefix, []string{unixTempPrefix, windowsTempPrefix}}
}
}
// IsTemporary is true if the file name has the temporary prefix. Regardless
// of the normally used prefix, the standard Windows and Unix temp prefixes
// are always recognized as temp files.
func (t tempNamer) IsTemporary(name string) bool {
return strings.HasPrefix(filepath.Base(name), t.prefix)
name = filepath.Base(name)
for _, prefix := range t.recognize {
if strings.HasPrefix(name, prefix) {
return true
}
}
return false
}
func (t tempNamer) TempName(name string) string {

View File

Binary file not shown.

View File

@@ -12,8 +12,8 @@ import (
"time"
)
type Holder interface {
Holder() (string, int)
type Holdable interface {
Holders() string
}
func newDeadlockDetector(timeout time.Duration) *deadlockDetector {
@@ -49,9 +49,8 @@ func (d *deadlockDetector) Watch(name string, mut sync.Locker) {
if r := <-ok; !r {
msg := fmt.Sprintf("deadlock detected at %s", name)
for otherName, otherMut := range d.lockers {
if otherHolder, ok := otherMut.(Holder); ok {
holder, goid := otherHolder.Holder()
msg += fmt.Sprintf("\n %s = current holder: %s at routine %d", otherName, holder, goid)
if otherHolder, ok := otherMut.(Holdable); ok {
msg += "\n===" + otherName + "===\n" + otherHolder.Holders()
}
}
panic(msg)

View File

@@ -29,23 +29,19 @@ type AtomicWriter struct {
err error
}
// CreateAtomic is like os.Create with a FileMode, except a temporary file
// name is used instead of the given name.
func CreateAtomic(path string, mode os.FileMode) (*AtomicWriter, error) {
// CreateAtomic is like os.Create, except a temporary file name is used
// instead of the given name. The file is created with secure (0600)
// permissions.
func CreateAtomic(path string) (*AtomicWriter, error) {
// The security of this depends on the tempfile having secure
// permissions, 0600, from the beginning. This is what ioutil.TempFile
// does. We have a test that verifies that that is the case, should this
// ever change in the standard library in the future.
fd, err := ioutil.TempFile(filepath.Dir(path), TempPrefix)
if err != nil {
return nil, err
}
// chmod fails on Android so don't even try
if runtime.GOOS != "android" {
if err := os.Chmod(fd.Name(), mode); err != nil {
fd.Close()
os.Remove(fd.Name())
return nil, err
}
}
w := &AtomicWriter{
path: path,
next: fd,
@@ -77,6 +73,11 @@ func (w *AtomicWriter) Close() error {
// Try to not leave temp file around, but ignore error.
defer os.Remove(w.next.Name())
if err := w.next.Sync(); err != nil {
w.err = err
return err
}
if err := w.next.Close(); err != nil {
w.err = err
return err
@@ -97,6 +98,8 @@ func (w *AtomicWriter) Close() error {
return err
}
SyncDir(filepath.Dir(w.next.Name()))
// Set w.err to return appropriately for any future operations.
w.err = ErrClosed

View File

@@ -21,7 +21,7 @@ func TestCreateAtomicCreate(t *testing.T) {
t.Fatal(err)
}
w, err := CreateAtomic("testdata/file", 0644)
w, err := CreateAtomic("testdata/file")
if err != nil {
t.Fatal(err)
}
@@ -63,7 +63,7 @@ func TestCreateAtomicReplace(t *testing.T) {
t.Fatal(err)
}
w, err := CreateAtomic("testdata/file", 0644)
w, err := CreateAtomic("testdata/file")
if err != nil {
t.Fatal(err)
}

View File

@@ -0,0 +1,44 @@
// Copyright (C) 2016 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
//+build !windows
// (No syscall.Umask or the equivalent on Windows)
package osutil
import (
"io/ioutil"
"os"
"syscall"
"testing"
)
func TestTempFilePermissions(t *testing.T) {
// Set a zero umask, so any files created will have the permission bits
// asked for in the create call and nothing less.
oldMask := syscall.Umask(0)
defer syscall.Umask(oldMask)
fd, err := ioutil.TempFile("", "test")
if err != nil {
t.Fatal(err)
}
info, err := fd.Stat()
if err != nil {
t.Fatal(err)
}
defer os.Remove(fd.Name())
defer fd.Close()
// The temp file should have 0600 permissions at the most, or we have a
// security problem in CreateAtomic.
t.Logf("Got 0%03o", info.Mode())
if info.Mode()&^0600 != 0 {
t.Errorf("Permission 0%03o is too generous", info.Mode())
}
}

Some files were not shown because too many files have changed in this diff Show More