Compare commits

...

57 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
125 changed files with 3835 additions and 1068 deletions
+3
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
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>
+18 -8
View File
@@ -298,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)
}
@@ -356,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) {
+9 -5
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 {
+2 -2
View File
@@ -129,10 +129,10 @@ func main() {
laddr.Port = 0
transport, ok := http.DefaultTransport.(*http.Transport)
if ok {
transport.DialContext = (&net.Dialer{
transport.Dial = (&net.Dialer{
Timeout: 30 * time.Second,
LocalAddr: laddr,
}).DialContext
}).Dial
}
}
+1 -1
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
}
+3
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,
+7 -3
View File
@@ -278,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
}
@@ -955,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}}
+13 -4
View File
@@ -4,19 +4,28 @@ 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).
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 preset.
to load the presets.
To allow the syncthing ports, run
```
sudo ufw allow syncthing
```
You can then verify the opened ports
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
```
+5
View File
@@ -2,3 +2,8 @@
title=Syncthing
description=Syncthing file synchronisation
ports=22000/tcp|21027/udp
[syncthing-gui]
title=Syncthing-GUI
description=Syncthing web gui
ports=8384/tcp
+4
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
+5
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": "Сподели папка",
+5
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",
@@ -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",
+5
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ář",
+5
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",
+22 -17
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",
@@ -40,8 +40,8 @@
"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",
@@ -52,6 +52,8 @@
"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": "Beschränkt durch Ignoriermuster",
"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,7 +232,7 @@
"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",
+6 -1
View File
@@ -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": "Τοπική κατάσταση",
@@ -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": "Διαμοίραση φακέλου",
+5
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",
+5
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",
+5
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",
@@ -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",
+10 -5
View File
@@ -9,7 +9,7 @@
"Add Device": "Gaineratu makina",
"Add Folder": "Gaineratu partekatze",
"Add Remote Device": "Gaineratu makinan izan",
"Add new folder?": "Gaineratu berri partekatze ?",
"Add new folder?": "Gaineratu hau partekatze ?",
"Address": "Helbide",
"Addresses": "Helbidek",
"Advanced": "Aditu",
@@ -50,8 +50,10 @@
"Documentation": "Dokumentazio",
"Download Rate": "Deskargatze emari",
"Downloaded": "Telekargatua",
"Downloading": "Deskargatua",
"Downloading": "Deskargatze",
"Edit": "Aldatu",
"Edit Device": "Aldaketa tresna",
"Edit Folder": "Aldaketa partekatze",
"Editing": "Aldaketa",
"Enable NAT traversal": "Ahalbidetu NAT",
"Enable Relaying": "Ahalbidetu lekua hartu",
@@ -95,6 +97,7 @@
"Last Scan": "Azterketa azken",
"Last seen": "Azken agerraldia",
"Later": "Berantago",
"Latest Change": "Asken aldaketa",
"Listeners": "Entzungailuak",
"Local Discovery": "Lekuko aurkikuntza",
"Local State": "Lekuko egoera",
@@ -123,7 +126,7 @@
"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. Tildea (~, edo ~+Espazioa Windows XP+Azerty-n) erabil litzateke bide motz gisa.",
"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",
@@ -153,6 +156,8 @@
"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",
@@ -235,6 +240,6 @@
"files": "fitxategik",
"full documentation": "Dokumentazio osoa",
"items": "Elementuak",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} banaketa \"{{folderLabel}}\" gomitatzen zaitu.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} \"{{folderLabel}}\" ({{folder}}) gomitatzen zaitu."
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} banaketa \"{{folder}}\" gomitatzen zaitu.",
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} \"{{folderlabel}}\" ({{folder}}) gomitatzen zaitu."
}
+5
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",
+11 -6
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 XP+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",
@@ -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",
@@ -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}})."
}
+11 -6
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 XP+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",
@@ -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",
@@ -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}})."
}
+5
View File
@@ -52,6 +52,8 @@
"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",
@@ -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",
+5
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",
+5
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",
+8 -3
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",
+10 -5
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}}\" を共有するよう求めています。",
+5
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": "최신 변경",
"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": "폴더 공유",
+7 -2
View File
@@ -52,6 +52,8 @@
"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",
+47 -42
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}}\".",
+10 -5
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.",
+49 -44
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}})."
}
+8 -3
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}}\"",
+10 -5
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}}\".",
+5
View File
@@ -52,6 +52,8 @@
"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",
@@ -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",
@@ -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",
+10 -5
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}}».",
+13 -8
View File
@@ -41,8 +41,8 @@
"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": "Enhet-ID",
"Device Identification": "Enhet identifikation",
"Device Name": "Enhets namn",
"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",
@@ -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",
@@ -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",
@@ -181,14 +186,14 @@
"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.": "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 datan som rapporteras ändras så kommer du att bli tillfrågad igen.",
"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.": "Katalog-ID får inte vara tomt.",
@@ -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,8 +228,8 @@
"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 katalog-ID knyter ihop kataloger mellan olika enheter. De skiftlägeskänsliga och måste matcha precis mellan alla enheter.",
+62 -57
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ı açı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ı aşı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."
}
+5
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": "Розповсюдити каталог",
+5
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",
+5
View File
@@ -52,6 +52,8 @@
"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": "在局域网上寻找设备",
"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": "共享文件夹",
+5
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": "分享資料夾",
+6 -2
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()">
@@ -397,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>
@@ -658,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>
+1 -1
View File
@@ -10,7 +10,7 @@
var syncthing = angular.module('syncthing', [
'angularUtils.directives.dirPagination',
'pascalprecht.translate',
'pascalprecht.translate', 'ngSanitize',
'syncthing.core'
]);
@@ -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/>
+3 -5
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) {
@@ -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,
@@ -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>
+647
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);
+12 -1
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.
+1
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{},
},
+9
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
@@ -85,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)
}
@@ -99,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
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>
+22 -1
View File
@@ -209,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 {
@@ -304,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
+6 -6
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 {
+3 -3
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
@@ -93,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
@@ -167,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,
+35 -17
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,6 +128,7 @@ 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.RawCopy()
// Actually starts the listeners and NAT service
@@ -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)
@@ -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)
}
+53 -10
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
}
+4 -4
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 {
+3 -3
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,
+1 -1
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 {
+2 -2
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--
+1 -1
View File
@@ -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
+71 -28
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{
// 425 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x51, 0x4f, 0x8b, 0xd3, 0x40,
0x1c, 0xed, 0xb4, 0xd9, 0xfe, 0xf9, 0xc5, 0xae, 0x3a, 0xc8, 0x32, 0x14, 0x4c, 0x43, 0x41, 0x08,
0x82, 0xa9, 0x56, 0xbc, 0x78, 0xdc, 0xc3, 0x82, 0x20, 0x22, 0x51, 0xd6, 0xe3, 0xd2, 0xcc, 0xfc,
0xda, 0x1d, 0x48, 0x67, 0x62, 0x66, 0x52, 0xa8, 0x9f, 0xc4, 0xe3, 0x7e, 0x9c, 0x1e, 0xfd, 0x04,
0xa2, 0xf5, 0x4b, 0x78, 0x94, 0x4e, 0xd2, 0x98, 0xe3, 0xde, 0xde, 0x9b, 0xdf, 0x7b, 0xbf, 0xf7,
0x66, 0x06, 0xc6, 0xc6, 0x16, 0x25, 0xb7, 0x26, 0xce, 0x0b, 0x6d, 0x35, 0xed, 0x8a, 0x74, 0xf2,
0x62, 0x2d, 0xed, 0x6d, 0x99, 0xc6, 0x5c, 0x6f, 0xe6, 0x6b, 0xbd, 0xd6, 0x73, 0x37, 0x4a, 0xcb,
0x95, 0x63, 0x8e, 0x38, 0x54, 0x59, 0x26, 0x6f, 0x5a, 0x72, 0xb3, 0x53, 0xdc, 0xde, 0x4a, 0xb5,
0x6e, 0xa1, 0x4c, 0xa6, 0xd5, 0x06, 0xae, 0xb3, 0x79, 0x8a, 0x79, 0x65, 0x9b, 0x7d, 0x01, 0xff,
0x4a, 0x66, 0x78, 0x8d, 0x85, 0x91, 0x5a, 0xd1, 0x97, 0x30, 0xd8, 0x56, 0x90, 0x91, 0x90, 0x44,
0xfe, 0xe2, 0x51, 0x7c, 0x32, 0xc5, 0xd7, 0xc8, 0xad, 0x2e, 0x2e, 0xbd, 0xfd, 0xcf, 0x69, 0x27,
0x39, 0xc9, 0xe8, 0x05, 0xf4, 0x05, 0x6e, 0x25, 0x47, 0xd6, 0x0d, 0x49, 0xf4, 0x20, 0xa9, 0xd9,
0xec, 0x0a, 0xfc, 0x7a, 0xe9, 0x7b, 0x69, 0x2c, 0x7d, 0x05, 0xc3, 0xda, 0x61, 0x18, 0x09, 0x7b,
0x91, 0xbf, 0x78, 0x18, 0x8b, 0x34, 0x6e, 0x65, 0xd7, 0x8b, 0x1b, 0xd9, 0x5b, 0xef, 0xfb, 0xdd,
0xb4, 0x33, 0xfb, 0xdb, 0x85, 0xc7, 0x47, 0xd5, 0x3b, 0xb5, 0xd2, 0x9f, 0x8b, 0x52, 0xf1, 0xa5,
0x45, 0x41, 0x29, 0x78, 0x6a, 0xb9, 0x41, 0x57, 0x72, 0x94, 0x38, 0x4c, 0x9f, 0x83, 0x67, 0x77,
0x79, 0xd5, 0xe3, 0x7c, 0x71, 0xf1, 0xbf, 0x78, 0x63, 0xdf, 0xe5, 0x98, 0x38, 0xcd, 0xd1, 0x6f,
0xe4, 0x37, 0x64, 0xbd, 0x90, 0x44, 0xbd, 0xc4, 0x61, 0x1a, 0x82, 0x9f, 0x63, 0xb1, 0x91, 0xa6,
0x6a, 0xe9, 0x85, 0x24, 0x1a, 0x27, 0xed, 0x23, 0xfa, 0x14, 0x60, 0xa3, 0x85, 0x5c, 0x49, 0x14,
0x37, 0x86, 0x9d, 0x39, 0xef, 0xe8, 0x74, 0xf2, 0x89, 0x32, 0x18, 0x08, 0xcc, 0xd0, 0xa2, 0x60,
0xfd, 0x90, 0x44, 0xc3, 0xe4, 0x44, 0x8f, 0x13, 0xa9, 0xb6, 0xcb, 0x4c, 0x0a, 0x36, 0xa8, 0x26,
0x35, 0xa5, 0xcf, 0xe0, 0x5c, 0xe9, 0x9b, 0x76, 0xee, 0xd0, 0x09, 0xc6, 0x4a, 0x7f, 0x6c, 0x25,
0xb7, 0xfe, 0x65, 0x74, 0xbf, 0x7f, 0x99, 0xc0, 0xd0, 0xe0, 0xd7, 0x12, 0x15, 0x47, 0x06, 0xae,
0x69, 0xc3, 0xe9, 0x14, 0xfc, 0xe6, 0x1e, 0xca, 0x30, 0x3f, 0x24, 0xd1, 0x59, 0xd2, 0x5c, 0xed,
0x43, 0xfd, 0xf4, 0x97, 0x4f, 0xf6, 0xbf, 0x83, 0xce, 0xfe, 0x10, 0x90, 0x1f, 0x87, 0x80, 0xfc,
0x3a, 0x04, 0x9d, 0xbb, 0x3f, 0x01, 0x49, 0xfb, 0x2e, 0xf8, 0xf5, 0xbf, 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,
}
+1
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;
}
+16 -17
View File
@@ -382,21 +382,20 @@ var (
)
var fileDescriptorLocal = []byte{
// 241 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8e, 0x4f, 0x4e, 0x84, 0x30,
0x14, 0xc6, 0x29, 0x24, 0x66, 0xa6, 0x63, 0x5c, 0x10, 0x17, 0xc4, 0x98, 0x42, 0x5c, 0xb1, 0x11,
0x16, 0x7a, 0x01, 0x09, 0x9b, 0x6e, 0xb9, 0x80, 0x81, 0xb6, 0x32, 0x2f, 0xc1, 0x3e, 0x43, 0x61,
0x12, 0x6f, 0xe3, 0x05, 0xbc, 0x07, 0x4b, 0xd7, 0x2e, 0x1a, 0xad, 0x17, 0x31, 0xe9, 0x68, 0x86,
0xdd, 0xf7, 0xfd, 0xf2, 0x7b, 0x7f, 0xe8, 0x6e, 0x40, 0xd1, 0x0e, 0xc5, 0xcb, 0x88, 0x13, 0xc6,
0x1b, 0x09, 0x46, 0xe0, 0x41, 0x8d, 0x57, 0xb7, 0x3d, 0x4c, 0xfb, 0xb9, 0x2b, 0x04, 0x3e, 0x97,
0x3d, 0xf6, 0x58, 0x7a, 0xa1, 0x9b, 0x9f, 0x7c, 0xf3, 0xc5, 0xa7, 0xe3, 0xe0, 0xcd, 0x3b, 0xa1,
0x9b, 0x07, 0xad, 0x71, 0xd6, 0x42, 0xc5, 0x0d, 0x0d, 0x41, 0x26, 0x24, 0x23, 0xf9, 0x79, 0x55,
0x2d, 0x36, 0x0d, 0x3e, 0x6d, 0x7a, 0xbf, 0xda, 0x67, 0x5e, 0xb5, 0x98, 0xf6, 0xa0, 0xfb, 0x55,
0x1a, 0xa0, 0x3b, 0x9e, 0x10, 0x38, 0x14, 0xb5, 0x3a, 0x80, 0x50, 0xbc, 0x76, 0x36, 0x0d, 0x79,
0xdd, 0x84, 0x20, 0xe3, 0x6b, 0xba, 0x6d, 0xa5, 0x1c, 0x95, 0x31, 0xca, 0x24, 0x61, 0x16, 0xe5,
0xdb, 0xe6, 0x04, 0xe2, 0x92, 0xee, 0x40, 0x9b, 0xa9, 0xd5, 0x42, 0x3d, 0x82, 0x4c, 0xa2, 0x8c,
0xe4, 0x51, 0x75, 0xe1, 0x6c, 0x4a, 0xf9, 0x1f, 0xe6, 0x75, 0x43, 0xff, 0x15, 0x2e, 0xab, 0xcb,
0xe5, 0x9b, 0x05, 0x8b, 0x63, 0xe4, 0xc3, 0x31, 0xf2, 0xe5, 0x58, 0xf0, 0xf6, 0xc3, 0x48, 0x77,
0xe6, 0x3f, 0xb8, 0xfb, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xa4, 0x46, 0x4f, 0x13, 0x14, 0x01, 0x00,
0x00,
// 235 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0xce, 0xc9, 0x4f, 0x4e,
0xcc, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x48, 0xc9, 0x2c, 0x4e, 0xce, 0x2f, 0x4b,
0x2d, 0x92, 0xd2, 0x4d, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xcf,
0x4f, 0xcf, 0xd7, 0x07, 0x2b, 0x48, 0x2a, 0x4d, 0x03, 0xf3, 0xc0, 0x1c, 0x30, 0x0b, 0xa2, 0x51,
0x69, 0x2d, 0x23, 0x17, 0x87, 0x63, 0x5e, 0x5e, 0x7e, 0x69, 0x5e, 0x72, 0xaa, 0x50, 0x10, 0x17,
0x53, 0x66, 0x8a, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x8f, 0x93, 0xd3, 0x89, 0x7b, 0xf2, 0x0c, 0xb7,
0xee, 0xc9, 0x9b, 0x20, 0x99, 0x57, 0x5c, 0x99, 0x97, 0x5c, 0x92, 0x91, 0x99, 0x97, 0x8e, 0xc4,
0xca, 0xc9, 0x4c, 0x82, 0x58, 0x91, 0x9c, 0x9f, 0xa3, 0xe7, 0x92, 0x5a, 0x96, 0x99, 0x9c, 0xea,
0xe9, 0xf2, 0xe8, 0x9e, 0x3c, 0x93, 0xa7, 0x4b, 0x10, 0xd0, 0x34, 0x21, 0x19, 0x2e, 0xce, 0xc4,
0x94, 0x94, 0xa2, 0xd4, 0xe2, 0xe2, 0xd4, 0x62, 0x09, 0x26, 0x05, 0x66, 0x0d, 0xce, 0x20, 0x84,
0x80, 0x90, 0x3e, 0x17, 0x77, 0x66, 0x5e, 0x71, 0x49, 0x22, 0xd0, 0xf6, 0x78, 0xa0, 0xd5, 0xcc,
0x40, 0xab, 0x99, 0x9d, 0xf8, 0x80, 0xda, 0xb9, 0x3c, 0xa1, 0xc2, 0x40, 0x63, 0xb8, 0x60, 0x4a,
0x3c, 0x53, 0x9c, 0x44, 0x4e, 0x3c, 0x94, 0x63, 0x38, 0xf1, 0x48, 0x8e, 0xf1, 0x02, 0x10, 0x3f,
0x78, 0x24, 0xc7, 0xb0, 0xe0, 0xb1, 0x1c, 0x63, 0x12, 0x1b, 0xd8, 0x05, 0xc6, 0x80, 0x00, 0x00,
0x00, 0xff, 0xff, 0xa4, 0x46, 0x4f, 0x13, 0x14, 0x01, 0x00, 0x00,
}
+96
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
}
+43
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
}
+195
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
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
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...
+71 -12
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
}
+43 -1
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)
}
}
}
+201 -123
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"
)
@@ -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,
@@ -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]
@@ -412,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 {
@@ -442,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()
}
@@ -451,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()
}
@@ -542,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
@@ -577,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
}
@@ -641,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
}
@@ -678,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()
@@ -739,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 {
@@ -762,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")
@@ -769,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) {
@@ -777,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 {
@@ -806,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 {
@@ -840,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 {
@@ -855,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 {
@@ -883,6 +894,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
changed = true
}
}
m.fmut.Unlock()
if changed {
if err := m.cfg.Save(); err != nil {
@@ -949,9 +961,10 @@ func (m *Model) handleDeintroductions(introducerCfg config.DeviceConfiguration,
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 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 %q with %v as introducer %v no longer shares the folder with that device", folderCfg.ID, folderCfg.Devices[i].DeviceID, folderCfg.Devices[i].IntroducedBy)
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
@@ -968,36 +981,26 @@ func (m *Model) handleDeintroductions(introducerCfg config.DeviceConfiguration,
}
}
// 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.
raw := m.cfg.RawCopy()
deviceChanged := false
for i := 0; i < len(raw.Devices); i++ {
if raw.Devices[i].IntroducedBy == introducerCfg.DeviceID {
if !foldersDevices.hasDevice(raw.Devices[i].DeviceID) {
if foldersIntroducedByOthers.hasDevice(raw.Devices[i].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.", raw.Devices[i].DeviceID, raw.Devices[i].IntroducedBy)
// 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", raw.Devices[i].DeviceID, raw.Devices[i].IntroducedBy)
raw.Devices = append(raw.Devices[:i], raw.Devices[i+1:]...)
i--
deviceChanged = true
// 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
}
}
}
// We've removed a device, replace the config.
if deviceChanged {
if err := m.cfg.Replace(raw); err != nil {
l.Warnln("Failed to save config", err)
}
changed = true
}
return changed
}
func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.DeviceConfiguration) {
@@ -1029,7 +1032,7 @@ func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.Dev
}
func (m *Model) introduceDeviceToFolder(device protocol.Device, folder protocol.Folder, introducerCfg config.DeviceConfiguration) {
l.Infof("Sharing folder %q with %v (vouched for by introducer %v)", folder.ID, device.ID, introducerCfg.DeviceID)
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)
@@ -1100,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
}
@@ -1161,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 {
@@ -1177,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) {
@@ -1188,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 {
@@ -1257,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
@@ -1343,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()
@@ -1454,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
@@ -1462,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
@@ -1485,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
@@ -1494,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)
@@ -1516,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
})
@@ -1707,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
@@ -1744,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
}
@@ -1808,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)
@@ -1820,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)
@@ -1896,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)
@@ -1958,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]
@@ -1971,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
@@ -2299,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)
@@ -2503,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) {
@@ -2557,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
@@ -2567,9 +2577,13 @@ 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
}
@@ -2604,3 +2618,67 @@ func (s folderDeviceSet) hasDevice(dev protocol.DeviceID) bool {
}
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
}
+324 -130
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,13 +551,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")
@@ -504,14 +590,7 @@ func TestIntroducer(t *testing.T) {
m.AddFolder(folder)
}
m.ServeBackground()
m.AddConnection(connections.Connection{
IntermediateConnection: connections.IntermediateConnection{
Conn: tls.Client(&fakeConn{}, nil),
},
Connection: &FakeConnection{
id: device1,
},
}, protocol.HelloResult{})
m.AddConnection(&fakeConnection{id: device1}, protocol.HelloResult{})
return wcfg, m
}
@@ -921,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")
}
@@ -1773,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
@@ -1789,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()
@@ -1900,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{
@@ -1960,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")
}
@@ -1980,7 +2043,7 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
t.Error("shared with device2")
}
if !d2c.closed {
if !conn2.Closed() {
t.Error("connection not closed")
}
@@ -2103,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",
@@ -2127,6 +2356,8 @@ func addFakeConn(m *Model, dev protocol.DeviceID) {
},
},
})
return fc
}
type fakeAddr struct{}
@@ -2138,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
}
+4 -3
View File
@@ -8,7 +8,6 @@ package model
import (
"fmt"
"path/filepath"
"time"
"github.com/syncthing/syncthing/lib/config"
@@ -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.
+1 -1
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
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
}
+3 -3
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,
},
+319 -131
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
}
+4 -2
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 {
+20 -5
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 {
Binary file not shown.
+15 -12
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
+2 -2
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)
}
+44
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())
}
}
+49
View File
@@ -0,0 +1,49 @@
// 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 osutil
import (
"os"
"path/filepath"
"strings"
)
// IsDir returns true if base and every path component of name up to and
// including filepath.Join(base, name) is a directory (and not a symlink or
// similar). Base and name must both be clean and name must be relative to
// base.
func IsDir(base, name string) bool {
path := base
info, err := Lstat(path)
if err != nil {
return false
}
if !info.IsDir() {
return false
}
if name == "." {
// The result of calling IsDir("some/where", filepath.Dir("foo"))
return true
}
parts := strings.Split(name, string(os.PathSeparator))
for _, part := range parts {
path = filepath.Join(path, part)
info, err := Lstat(path)
if err != nil {
return false
}
if info.Mode()&os.ModeSymlink != 0 {
return false
}
if !info.IsDir() {
return false
}
}
return true
}
+75
View File
@@ -0,0 +1,75 @@
// 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 osutil_test
import (
"os"
"testing"
"github.com/syncthing/syncthing/lib/osutil"
"github.com/syncthing/syncthing/lib/symlinks"
)
func TestIsDir(t *testing.T) {
if !symlinks.Supported {
t.Skip("pointless test")
return
}
os.RemoveAll("testdata")
defer os.RemoveAll("testdata")
os.MkdirAll("testdata/a/b/c", 0755)
symlinks.Create("testdata/a/l", "b", symlinks.TargetDirectory)
// a/l -> b, so a/l/c should resolve by normal stat
info, err := osutil.Lstat("testdata/a/l/c")
if err != nil {
t.Fatal("unexpected error", err)
}
if !info.IsDir() {
t.Fatal("error in setup, a/l/c should be a directory")
}
cases := []struct {
name string
isDir bool
}{
// Exist
{".", true},
{"a", true},
{"a/b", true},
{"a/b/c", true},
// Don't exist
{"x", false},
{"a/x", false},
{"a/b/x", false},
{"a/x/c", false},
// Symlink or behind symlink
{"a/l", false},
{"a/l/c", false},
}
for _, tc := range cases {
if res := osutil.IsDir("testdata", tc.name); res != tc.isDir {
t.Errorf("IsDir(%q) = %v, should be %v", tc.name, res, tc.isDir)
}
}
}
var isDirResult bool
func BenchmarkIsDir(b *testing.B) {
os.RemoveAll("testdata")
defer os.RemoveAll("testdata")
os.MkdirAll("testdata/a/b/c", 0755)
for i := 0; i < b.N; i++ {
isDirResult = osutil.IsDir("testdata", "a/b/c")
}
b.ReportAllocs()
}
+3 -6
View File
@@ -20,7 +20,7 @@ import (
"github.com/syncthing/syncthing/lib/sync"
)
var ErrNoHome = errors.New("No home directory found - set $HOME (or the platform equivalent).")
var errNoHome = errors.New("no home directory found - set $HOME (or the platform equivalent)")
// Try to keep this entire operation atomic-like. We shouldn't be doing this
// often enough that there is any contention on this lock.
@@ -123,7 +123,7 @@ func getHomeDir() (string, error) {
}
if home == "" {
return "", ErrNoHome
return "", errNoHome
}
return home, nil
@@ -172,10 +172,7 @@ func copyFileContents(src, dst string) (err error) {
err = cerr
}
}()
if _, err = io.Copy(out, in); err != nil {
return
}
err = out.Sync()
_, err = io.Copy(out, in)
return
}
+37
View File
@@ -0,0 +1,37 @@
// 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 osutil
import (
"os"
"runtime"
)
func SyncFile(path string) error {
flag := 0
if runtime.GOOS == "windows" {
flag = os.O_WRONLY
}
fd, err := os.OpenFile(path, flag, 0)
if err != nil {
return err
}
defer fd.Close()
// MacOS and Windows do not flush the disk cache
if err := fd.Sync(); err != nil {
return err
}
return nil
}
func SyncDir(path string) error {
if runtime.GOOS == "windows" {
// not supported by Windows
return nil
}
return SyncFile(path)
}
+147 -107
View File
@@ -303,7 +303,8 @@ type FileInfo struct {
NoPermissions bool `protobuf:"varint,8,opt,name=no_permissions,json=noPermissions,proto3" json:"no_permissions,omitempty"`
Version Vector `protobuf:"bytes,9,opt,name=version" json:"version"`
Sequence int64 `protobuf:"varint,10,opt,name=sequence,proto3" json:"sequence,omitempty"`
Blocks []BlockInfo `protobuf:"bytes,16,rep,name=Blocks,json=blocks" json:"Blocks"`
Blocks []BlockInfo `protobuf:"bytes,16,rep,name=Blocks" json:"Blocks"`
SymlinkTarget string `protobuf:"bytes,17,opt,name=symlink_target,json=symlinkTarget,proto3" json:"symlink_target,omitempty"`
}
func (m *FileInfo) Reset() { *m = FileInfo{} }
@@ -870,6 +871,14 @@ func (m *FileInfo) MarshalTo(data []byte) (int, error) {
i += n
}
}
if len(m.SymlinkTarget) > 0 {
data[i] = 0x8a
i++
data[i] = 0x1
i++
i = encodeVarintBep(data, i, uint64(len(m.SymlinkTarget)))
i += copy(data[i:], m.SymlinkTarget)
}
return i, nil
}
@@ -1394,6 +1403,10 @@ func (m *FileInfo) ProtoSize() (n int) {
n += 2 + l + sovBep(uint64(l))
}
}
l = len(m.SymlinkTarget)
if l > 0 {
n += 2 + l + sovBep(uint64(l))
}
return n
}
@@ -2850,6 +2863,35 @@ func (m *FileInfo) Unmarshal(data []byte) error {
return err
}
iNdEx = postIndex
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 ErrIntOverflowBep
}
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 ErrInvalidLengthBep
}
postIndex := iNdEx + intStringLen
if postIndex > l {
return io.ErrUnexpectedEOF
}
m.SymlinkTarget = string(data[iNdEx:postIndex])
iNdEx = postIndex
default:
iNdEx = preIndex
skippy, err := skipBep(data[iNdEx:])
@@ -3987,110 +4029,108 @@ var (
)
var fileDescriptorBep = []byte{
// 1665 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xac, 0x56, 0x4f, 0x73, 0xdb, 0xc6,
0x15, 0x17, 0x48, 0xf0, 0xdf, 0x23, 0xa5, 0x40, 0x6b, 0x59, 0x41, 0x61, 0x85, 0x42, 0xe0, 0xb8,
0x55, 0x34, 0x8d, 0xe2, 0xc6, 0x69, 0x33, 0xd3, 0x69, 0x3b, 0x43, 0x91, 0x90, 0xcc, 0x09, 0x0d,
0x32, 0x4b, 0xca, 0xae, 0x73, 0x28, 0x06, 0x24, 0x96, 0x14, 0xc6, 0x20, 0x96, 0x05, 0x40, 0xd9,
0xea, 0x47, 0x60, 0xbf, 0x40, 0x2f, 0x9c, 0xc9, 0xf4, 0xd6, 0x7b, 0x3f, 0x84, 0x8f, 0x99, 0x1c,
0x7b, 0xf0, 0x34, 0xea, 0xa5, 0xc7, 0x5e, 0x7a, 0xef, 0x60, 0x17, 0x00, 0x41, 0xfd, 0xe9, 0xe4,
0x90, 0x13, 0x77, 0xdf, 0xfb, 0xed, 0xdb, 0x7d, 0xbf, 0xf7, 0x7e, 0x8f, 0x80, 0xca, 0x90, 0xcc,
0x8e, 0x66, 0x3e, 0x0d, 0x29, 0x2a, 0xb3, 0x9f, 0x11, 0x75, 0x95, 0x4f, 0x26, 0x4e, 0x78, 0x3e,
0x1f, 0x1e, 0x8d, 0xe8, 0xf4, 0xd3, 0x09, 0x9d, 0xd0, 0x4f, 0x99, 0x67, 0x38, 0x1f, 0xb3, 0x1d,
0xdb, 0xb0, 0x15, 0x3f, 0xa8, 0xcd, 0xa0, 0xf0, 0x94, 0xb8, 0x2e, 0x45, 0xfb, 0x50, 0xb5, 0xc9,
0x85, 0x33, 0x22, 0xa6, 0x67, 0x4d, 0x89, 0x2c, 0xa8, 0xc2, 0x41, 0x05, 0x03, 0x37, 0x19, 0xd6,
0x94, 0x44, 0x80, 0x91, 0xeb, 0x10, 0x2f, 0xe4, 0x80, 0x1c, 0x07, 0x70, 0x13, 0x03, 0x3c, 0x82,
0xad, 0x18, 0x70, 0x41, 0xfc, 0xc0, 0xa1, 0x9e, 0x9c, 0x67, 0x98, 0x4d, 0x6e, 0x7d, 0xce, 0x8d,
0x5a, 0x00, 0xc5, 0xa7, 0xc4, 0xb2, 0x89, 0x8f, 0x3e, 0x06, 0x31, 0xbc, 0x9c, 0xf1, 0xbb, 0xb6,
0x3e, 0xbb, 0x7f, 0x94, 0xe4, 0x70, 0xf4, 0x8c, 0x04, 0x81, 0x35, 0x21, 0x83, 0xcb, 0x19, 0xc1,
0x0c, 0x82, 0x7e, 0x07, 0xd5, 0x11, 0x9d, 0xce, 0x7c, 0x12, 0xb0, 0xc0, 0x39, 0x76, 0x62, 0xef,
0xc6, 0x89, 0xe6, 0x0a, 0x83, 0xb3, 0x07, 0xb4, 0x06, 0x6c, 0x36, 0xdd, 0x79, 0x10, 0x12, 0xbf,
0x49, 0xbd, 0xb1, 0x33, 0x41, 0x8f, 0xa1, 0x34, 0xa6, 0xae, 0x4d, 0xfc, 0x40, 0x16, 0xd4, 0xfc,
0x41, 0xf5, 0x33, 0x69, 0x15, 0xec, 0x84, 0x39, 0x8e, 0xc5, 0xb7, 0xef, 0xf6, 0x37, 0x70, 0x02,
0xd3, 0xfe, 0x9c, 0x83, 0x22, 0xf7, 0xa0, 0x5d, 0xc8, 0x39, 0x36, 0xa7, 0xe8, 0xb8, 0x78, 0xf5,
0x6e, 0x3f, 0xd7, 0x6e, 0xe1, 0x9c, 0x63, 0xa3, 0x1d, 0x28, 0xb8, 0xd6, 0x90, 0xb8, 0x31, 0x39,
0x7c, 0x83, 0x1e, 0x40, 0xc5, 0x27, 0x96, 0x6d, 0x52, 0xcf, 0xbd, 0x64, 0x94, 0x94, 0x71, 0x39,
0x32, 0x74, 0x3d, 0xf7, 0x12, 0x7d, 0x02, 0xc8, 0x99, 0x78, 0xd4, 0x27, 0xe6, 0x8c, 0xf8, 0x53,
0x87, 0xbd, 0x36, 0x90, 0x45, 0x86, 0xda, 0xe6, 0x9e, 0xde, 0xca, 0x81, 0x1e, 0xc2, 0x66, 0x0c,
0xb7, 0x89, 0x4b, 0x42, 0x22, 0x17, 0x18, 0xb2, 0xc6, 0x8d, 0x2d, 0x66, 0x43, 0x8f, 0x61, 0xc7,
0x76, 0x02, 0x6b, 0xe8, 0x12, 0x33, 0x24, 0xd3, 0x99, 0xe9, 0x78, 0x36, 0x79, 0x43, 0x02, 0xb9,
0xc8, 0xb0, 0x28, 0xf6, 0x0d, 0xc8, 0x74, 0xd6, 0xe6, 0x9e, 0x88, 0x0d, 0x5e, 0xe9, 0x40, 0x96,
0xae, 0xb3, 0xd1, 0x62, 0x8e, 0x84, 0x8d, 0x18, 0xa6, 0xfd, 0x27, 0x07, 0x45, 0xee, 0x41, 0x3f,
0x4d, 0xd9, 0xa8, 0x1d, 0xef, 0x46, 0xa8, 0x7f, 0xbc, 0xdb, 0x2f, 0x73, 0x5f, 0xbb, 0x95, 0x61,
0x07, 0x81, 0x98, 0xe9, 0x1c, 0xb6, 0x46, 0x7b, 0x50, 0xb1, 0x6c, 0x3b, 0xaa, 0x12, 0x09, 0xe4,
0xbc, 0x9a, 0x3f, 0xa8, 0xe0, 0x95, 0x01, 0x7d, 0xb1, 0x5e, 0x75, 0xf1, 0x7a, 0x9f, 0xdc, 0x55,
0xee, 0x88, 0xf2, 0x11, 0xf1, 0xe3, 0x4e, 0x2d, 0xb0, 0xfb, 0xca, 0x91, 0x81, 0xf5, 0xe9, 0x87,
0x50, 0x9b, 0x5a, 0x6f, 0xcc, 0x80, 0xfc, 0x71, 0x4e, 0xbc, 0x11, 0x61, 0xb4, 0xe4, 0x71, 0x75,
0x6a, 0xbd, 0xe9, 0xc7, 0x26, 0x54, 0x07, 0x70, 0xbc, 0xd0, 0xa7, 0xf6, 0x7c, 0x44, 0x7c, 0xb9,
0xc4, 0x78, 0xcb, 0x58, 0xd0, 0x2f, 0xa1, 0xcc, 0x48, 0x35, 0x1d, 0x5b, 0x2e, 0xab, 0xc2, 0x81,
0x78, 0xac, 0xc4, 0x89, 0x97, 0x18, 0xa5, 0x2c, 0xef, 0x64, 0x89, 0x4b, 0x0c, 0xdb, 0xb6, 0xd1,
0x6f, 0x40, 0x09, 0x5e, 0x39, 0x51, 0x41, 0x78, 0xa4, 0xd0, 0xa1, 0x9e, 0xe9, 0x93, 0x29, 0xbd,
0xb0, 0xdc, 0x40, 0xae, 0xb0, 0x6b, 0xe4, 0x08, 0xd1, 0xce, 0x00, 0x70, 0xec, 0xd7, 0xba, 0x50,
0x60, 0x11, 0xd1, 0x2e, 0x14, 0x79, 0x53, 0xc6, 0x2a, 0x8d, 0x77, 0xe8, 0x08, 0x0a, 0x63, 0xc7,
0x25, 0x81, 0x9c, 0x63, 0x35, 0x44, 0x99, 0x8e, 0x76, 0x5c, 0xd2, 0xf6, 0xc6, 0x34, 0xae, 0x22,
0x87, 0x69, 0x67, 0x50, 0x65, 0x01, 0xcf, 0x66, 0xb6, 0x15, 0x92, 0x1f, 0x2d, 0xec, 0x5f, 0xf3,
0x50, 0x4e, 0x3c, 0x69, 0xd1, 0x85, 0x4c, 0xd1, 0x0f, 0x63, 0xdd, 0x73, 0x15, 0xef, 0xde, 0x8c,
0x97, 0x11, 0x3e, 0x02, 0x31, 0x70, 0xfe, 0x44, 0x98, 0x6e, 0xf2, 0x98, 0xad, 0x91, 0x0a, 0xd5,
0xeb, 0x62, 0xd9, 0xc4, 0x59, 0x13, 0xfa, 0x00, 0x60, 0x4a, 0x6d, 0x67, 0xec, 0x10, 0xdb, 0x0c,
0x58, 0x03, 0xe4, 0x71, 0x25, 0xb1, 0xf4, 0x91, 0x1c, 0xb5, 0x7b, 0x24, 0x15, 0x3b, 0xd6, 0x44,
0xb2, 0x8d, 0x3c, 0x8e, 0x77, 0x61, 0xb9, 0x8e, 0x1d, 0x57, 0x3d, 0xd9, 0x46, 0xd3, 0xcd, 0xa3,
0x6b, 0x22, 0x2d, 0x33, 0xc0, 0xa6, 0x47, 0xb3, 0x02, 0x7d, 0x0c, 0xa5, 0x64, 0xfa, 0x45, 0xf5,
0x5c, 0x53, 0xd2, 0x73, 0x32, 0x0a, 0x69, 0x3a, 0x57, 0x62, 0x18, 0x52, 0xa0, 0x9c, 0xb6, 0x22,
0xb0, 0x97, 0xa6, 0xfb, 0x68, 0xe6, 0xa6, 0x79, 0x78, 0x81, 0x5c, 0x55, 0x85, 0x83, 0x02, 0x4e,
0x53, 0x33, 0x02, 0xf4, 0x0b, 0x28, 0x1e, 0xbb, 0x74, 0xf4, 0x2a, 0xd1, 0xed, 0xbd, 0xd5, 0x6d,
0xcc, 0x9e, 0xa9, 0x4e, 0x71, 0xc8, 0x80, 0xbf, 0x16, 0xff, 0xf2, 0xcd, 0xfe, 0x86, 0xf6, 0x15,
0x54, 0x52, 0x40, 0x54, 0x79, 0x3a, 0x1e, 0x07, 0x24, 0x64, 0x65, 0xca, 0xe3, 0x78, 0x97, 0x92,
0x9f, 0x63, 0xf7, 0x72, 0xf2, 0x11, 0x88, 0xe7, 0x56, 0x70, 0xce, 0x0a, 0x52, 0xc3, 0x6c, 0x1d,
0x87, 0xfc, 0x2d, 0x14, 0x79, 0x86, 0xe8, 0x09, 0x94, 0x47, 0x74, 0xee, 0x85, 0xab, 0xe9, 0xba,
0x9d, 0x15, 0x2d, 0xf3, 0xc4, 0xaf, 0x4a, 0x81, 0xda, 0x09, 0x94, 0x62, 0x17, 0x7a, 0x94, 0x4e,
0x14, 0xf1, 0xf8, 0x7e, 0x22, 0xac, 0xfe, 0x39, 0xf5, 0xc3, 0xb5, 0x81, 0xb2, 0x03, 0x85, 0x0b,
0xcb, 0x9d, 0xf3, 0xf7, 0x89, 0x98, 0x6f, 0xb4, 0xbf, 0x0b, 0x50, 0xc2, 0x11, 0x81, 0x41, 0x98,
0x19, 0xd4, 0x85, 0xb5, 0x41, 0xbd, 0x6a, 0xf5, 0xdc, 0x5a, 0xab, 0x27, 0xdd, 0x9a, 0xcf, 0x74,
0xeb, 0x8a, 0x1c, 0xf1, 0x56, 0x72, 0x0a, 0xb7, 0x90, 0x53, 0x5c, 0x91, 0x13, 0x35, 0xce, 0xd8,
0xa7, 0x53, 0x36, 0x8a, 0xa9, 0x6f, 0xf9, 0x97, 0x71, 0x67, 0x6d, 0x46, 0xd6, 0x41, 0x62, 0xd4,
0x4c, 0x28, 0x63, 0x12, 0xcc, 0xa8, 0x17, 0x90, 0x3b, 0x9f, 0x8d, 0x40, 0xb4, 0xad, 0xd0, 0x62,
0x8f, 0xae, 0x61, 0xb6, 0x46, 0x3f, 0x03, 0x71, 0x44, 0x6d, 0xfe, 0xe4, 0xad, 0x6c, 0xfd, 0x75,
0xdf, 0xa7, 0x7e, 0x93, 0xda, 0x04, 0x33, 0x80, 0x36, 0x03, 0xa9, 0x45, 0x5f, 0x7b, 0x2e, 0xb5,
0xec, 0x9e, 0x4f, 0x27, 0xd1, 0xa8, 0xbc, 0x53, 0xf2, 0x2d, 0x28, 0xcd, 0xd9, 0x50, 0x48, 0x44,
0xff, 0xd1, 0xba, 0x48, 0xaf, 0x07, 0xe2, 0x13, 0x24, 0xe9, 0xec, 0xf8, 0xa8, 0xf6, 0x9d, 0x00,
0xca, 0xdd, 0x68, 0xd4, 0x86, 0x2a, 0x47, 0x9a, 0x99, 0xaf, 0x80, 0x83, 0x1f, 0x72, 0x11, 0x9b,
0x0f, 0x30, 0x4f, 0xd7, 0xb7, 0xfe, 0xb5, 0x64, 0x94, 0x98, 0xff, 0x61, 0x4a, 0x7c, 0x08, 0x9b,
0x4c, 0x23, 0xe9, 0x1f, 0xa6, 0xa8, 0xe6, 0x0f, 0x0a, 0xb8, 0x36, 0xe4, 0x42, 0x61, 0x36, 0xad,
0x08, 0x62, 0xcf, 0xf1, 0x26, 0xda, 0x3e, 0x14, 0x9a, 0x2e, 0x65, 0xc5, 0x2a, 0xfa, 0xc4, 0x0a,
0xa8, 0x97, 0x70, 0xc8, 0x77, 0x87, 0xdf, 0xe5, 0xa0, 0x9a, 0xf9, 0x90, 0x41, 0x8f, 0x61, 0xab,
0xd9, 0x39, 0xeb, 0x0f, 0x74, 0x6c, 0x36, 0xbb, 0xc6, 0x49, 0xfb, 0x54, 0xda, 0x50, 0xf6, 0x16,
0x4b, 0x55, 0x9e, 0xae, 0x40, 0xeb, 0xdf, 0x28, 0xfb, 0x50, 0x68, 0x1b, 0x2d, 0xfd, 0xf7, 0x92,
0xa0, 0xec, 0x2c, 0x96, 0xaa, 0x94, 0x01, 0xf2, 0x3f, 0x82, 0x9f, 0x43, 0x8d, 0x01, 0xcc, 0xb3,
0x5e, 0xab, 0x31, 0xd0, 0xa5, 0x9c, 0xa2, 0x2c, 0x96, 0xea, 0xee, 0x75, 0x5c, 0xcc, 0xf7, 0x43,
0x28, 0x61, 0xfd, 0xab, 0x33, 0xbd, 0x3f, 0x90, 0xf2, 0xca, 0xee, 0x62, 0xa9, 0xa2, 0x0c, 0x30,
0x51, 0xcc, 0x23, 0x28, 0x63, 0xbd, 0xdf, 0xeb, 0x1a, 0x7d, 0x5d, 0x12, 0x95, 0xf7, 0x17, 0x4b,
0xf5, 0xde, 0x1a, 0x2a, 0xee, 0xd0, 0x5f, 0xc1, 0x76, 0xab, 0xfb, 0xc2, 0xe8, 0x74, 0x1b, 0x2d,
0xb3, 0x87, 0xbb, 0xa7, 0x58, 0xef, 0xf7, 0xa5, 0x82, 0xb2, 0xbf, 0x58, 0xaa, 0x0f, 0x32, 0xf8,
0x1b, 0x0d, 0xf7, 0x01, 0x88, 0xbd, 0xb6, 0x71, 0x2a, 0x15, 0x95, 0x7b, 0x8b, 0xa5, 0xfa, 0x5e,
0x06, 0x1a, 0x91, 0x1a, 0x65, 0xdc, 0xec, 0x74, 0xfb, 0xba, 0x54, 0xba, 0x91, 0x31, 0x23, 0xfb,
0xf0, 0x0f, 0x80, 0x6e, 0x7e, 0xea, 0xa1, 0x8f, 0x40, 0x34, 0xba, 0x86, 0x2e, 0x6d, 0xf0, 0xfc,
0x6f, 0x22, 0x0c, 0xea, 0x11, 0xa4, 0x41, 0xbe, 0xf3, 0xf5, 0xe7, 0x92, 0xa0, 0xfc, 0x64, 0xb1,
0x54, 0xef, 0xdf, 0x04, 0x75, 0xbe, 0xfe, 0xfc, 0x90, 0x42, 0x35, 0x1b, 0x58, 0x83, 0xf2, 0x33,
0x7d, 0xd0, 0x68, 0x35, 0x06, 0x0d, 0x69, 0x83, 0x3f, 0x29, 0x71, 0x3f, 0x23, 0xa1, 0xc5, 0x04,
0xb8, 0x07, 0x05, 0x43, 0x7f, 0xae, 0x63, 0x49, 0x50, 0xb6, 0x17, 0x4b, 0x75, 0x33, 0x01, 0x18,
0xe4, 0x82, 0xf8, 0xa8, 0x0e, 0xc5, 0x46, 0xe7, 0x45, 0xe3, 0x65, 0x5f, 0xca, 0x29, 0x68, 0xb1,
0x54, 0xb7, 0x12, 0x77, 0xc3, 0x7d, 0x6d, 0x5d, 0x06, 0x87, 0xff, 0x15, 0xa0, 0x96, 0xfd, 0xdb,
0x43, 0x75, 0x10, 0x4f, 0xda, 0x1d, 0x3d, 0xb9, 0x2e, 0xeb, 0x8b, 0xd6, 0xe8, 0x00, 0x2a, 0xad,
0x36, 0xd6, 0x9b, 0x83, 0x2e, 0x7e, 0x99, 0xe4, 0x92, 0x05, 0xb5, 0x1c, 0x9f, 0x35, 0x77, 0xf4,
0x69, 0x59, 0xeb, 0xbf, 0x7c, 0xd6, 0x69, 0x1b, 0x5f, 0x9a, 0x2c, 0x62, 0x4e, 0x79, 0xb0, 0x58,
0xaa, 0xef, 0x67, 0xc1, 0xfd, 0xcb, 0xa9, 0xeb, 0x78, 0xaf, 0x58, 0xe0, 0x2f, 0x60, 0x3b, 0x81,
0xaf, 0x2e, 0xc8, 0x2b, 0xea, 0x62, 0xa9, 0xee, 0xdd, 0x72, 0x66, 0x75, 0xcf, 0x13, 0x78, 0x2f,
0x39, 0x78, 0x66, 0x7c, 0x69, 0x74, 0x5f, 0x18, 0x92, 0xa8, 0xd4, 0x17, 0x4b, 0x55, 0xb9, 0xe5,
0xd8, 0x99, 0xf7, 0xca, 0xa3, 0xaf, 0xbd, 0xc3, 0xbf, 0x09, 0x50, 0x49, 0x27, 0x54, 0xc4, 0xb3,
0xd1, 0x35, 0x75, 0x8c, 0xbb, 0x38, 0x49, 0x3c, 0x75, 0x1a, 0x94, 0x2d, 0xd1, 0x87, 0x50, 0x3a,
0xd5, 0x0d, 0x1d, 0xb7, 0x9b, 0x89, 0x1e, 0x52, 0xc8, 0x29, 0xf1, 0x88, 0xef, 0x8c, 0xd0, 0xc7,
0x50, 0x33, 0xba, 0x66, 0xff, 0xac, 0xf9, 0x34, 0xc9, 0x98, 0x35, 0x70, 0x26, 0x54, 0x7f, 0x3e,
0x3a, 0x67, 0xd9, 0x1e, 0x46, 0xd2, 0x79, 0xde, 0xe8, 0xb4, 0x5b, 0x1c, 0x9a, 0x57, 0xe4, 0xc5,
0x52, 0xdd, 0x49, 0xa1, 0x6d, 0xfe, 0xb7, 0x1f, 0x61, 0x0f, 0x6d, 0xa8, 0xff, 0xff, 0x59, 0x84,
0x54, 0x28, 0x36, 0x7a, 0x3d, 0xdd, 0x68, 0x25, 0xaf, 0x5f, 0xf9, 0x1a, 0xb3, 0x19, 0xf1, 0xec,
0x08, 0x71, 0xd2, 0xc5, 0xa7, 0xfa, 0x20, 0x79, 0xfc, 0x0a, 0x71, 0x42, 0xfd, 0x09, 0x09, 0x8f,
0xf7, 0xde, 0x7e, 0x5f, 0xdf, 0xf8, 0xf6, 0xfb, 0xfa, 0xc6, 0xdb, 0xab, 0xba, 0xf0, 0xed, 0x55,
0x5d, 0xf8, 0xe7, 0x55, 0x7d, 0xe3, 0xdf, 0x57, 0x75, 0xe1, 0x9b, 0x7f, 0xd5, 0x85, 0x61, 0x91,
0xcd, 0xae, 0x27, 0xff, 0x0b, 0x00, 0x00, 0xff, 0xff, 0x64, 0x17, 0x1e, 0x19, 0xf4, 0x0d, 0x00,
0x00,
// 1645 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xac, 0x57, 0x4f, 0x73, 0xdb, 0x5a,
0x15, 0x8f, 0x6d, 0xf9, 0xdf, 0xb5, 0x93, 0xe7, 0xdc, 0xa6, 0x79, 0x46, 0xcd, 0x4b, 0x82, 0xde,
0x2b, 0x04, 0x0f, 0x4d, 0xa1, 0x05, 0x3a, 0xc3, 0x00, 0x33, 0x8e, 0xad, 0xa4, 0x9a, 0x3a, 0xb2,
0x2b, 0xdb, 0x29, 0x65, 0x81, 0x46, 0xb6, 0xae, 0x1d, 0x4d, 0x64, 0x5d, 0x23, 0xc9, 0x6d, 0xc3,
0x47, 0x80, 0x2f, 0xc0, 0x86, 0x99, 0x6e, 0xd9, 0xf3, 0x21, 0xca, 0xae, 0xd3, 0x25, 0x8b, 0x0e,
0x94, 0x0d, 0x4b, 0x36, 0xec, 0x39, 0xf7, 0x5e, 0x49, 0x96, 0xf3, 0x87, 0xe9, 0xe2, 0x2d, 0x32,
0xd6, 0x3d, 0xe7, 0x77, 0xcf, 0xb9, 0xe7, 0x77, 0xfe, 0xdc, 0x1b, 0x54, 0x1e, 0x91, 0xf9, 0xe1,
0xdc, 0xa7, 0x21, 0xc5, 0x25, 0xfe, 0x33, 0xa6, 0xae, 0xfc, 0x60, 0xea, 0x84, 0xe7, 0x8b, 0xd1,
0xe1, 0x98, 0xce, 0x1e, 0x4e, 0xe9, 0x94, 0x3e, 0xe4, 0x9a, 0xd1, 0x62, 0xc2, 0x57, 0x7c, 0xc1,
0xbf, 0xc4, 0x46, 0x65, 0x8e, 0xf2, 0x4f, 0x89, 0xeb, 0x52, 0xbc, 0x87, 0x2a, 0x36, 0x79, 0xe5,
0x8c, 0x89, 0xe9, 0x59, 0x33, 0x52, 0xcf, 0xec, 0x67, 0x0e, 0xca, 0x06, 0x12, 0x22, 0x1d, 0x24,
0x0c, 0x30, 0x76, 0x1d, 0xe2, 0x85, 0x02, 0x90, 0x15, 0x00, 0x21, 0xe2, 0x80, 0xfb, 0x68, 0x23,
0x02, 0xbc, 0x22, 0x7e, 0xe0, 0x50, 0xaf, 0x9e, 0xe3, 0x98, 0x75, 0x21, 0x3d, 0x13, 0x42, 0x25,
0x40, 0x85, 0xa7, 0xc4, 0xb2, 0x89, 0x8f, 0x7f, 0x80, 0xa4, 0xf0, 0x72, 0x2e, 0x7c, 0x6d, 0x3c,
0xba, 0x7b, 0x18, 0xc7, 0x70, 0x78, 0x4a, 0x82, 0xc0, 0x9a, 0x92, 0x01, 0x28, 0x0d, 0x0e, 0xc1,
0xbf, 0x02, 0xe7, 0x74, 0x36, 0xf7, 0x41, 0xc1, 0x0c, 0x67, 0xf9, 0x8e, 0x9d, 0x6b, 0x3b, 0x5a,
0x4b, 0x8c, 0x91, 0xde, 0xa0, 0x34, 0xd1, 0x7a, 0xcb, 0x5d, 0x04, 0x21, 0xf1, 0x5b, 0xd4, 0x9b,
0x38, 0x53, 0xfc, 0x23, 0x54, 0x9c, 0x50, 0x17, 0x4e, 0x11, 0x80, 0xfb, 0xdc, 0x41, 0xe5, 0x51,
0x6d, 0x69, 0xec, 0x98, 0x2b, 0x8e, 0xa4, 0x77, 0x1f, 0xf7, 0xd6, 0x8c, 0x18, 0xa6, 0xfc, 0x31,
0x8b, 0x0a, 0x42, 0x83, 0xb7, 0x51, 0xd6, 0xb1, 0x05, 0x45, 0x47, 0x85, 0x4f, 0x1f, 0xf7, 0xb2,
0x5a, 0xdb, 0x00, 0x09, 0xde, 0x42, 0x79, 0xd7, 0x1a, 0x11, 0x37, 0x22, 0x47, 0x2c, 0xf0, 0x3d,
0x54, 0xf6, 0x21, 0x60, 0x93, 0x7a, 0xee, 0x25, 0xa7, 0xa4, 0x64, 0x94, 0x98, 0xa0, 0x0b, 0x6b,
0xfc, 0x00, 0x61, 0x67, 0xea, 0x51, 0x9f, 0x98, 0x73, 0xe2, 0xcf, 0x1c, 0x7e, 0xda, 0xa0, 0x2e,
0x71, 0xd4, 0xa6, 0xd0, 0xf4, 0x96, 0x0a, 0xfc, 0x35, 0x5a, 0x8f, 0xe0, 0x36, 0x71, 0x49, 0x48,
0xea, 0x79, 0x8e, 0xac, 0x0a, 0x61, 0x9b, 0xcb, 0x20, 0xb6, 0x2d, 0xdb, 0x09, 0xac, 0x91, 0x4b,
0xcc, 0x90, 0xcc, 0xe6, 0xa6, 0xe3, 0xd9, 0xe4, 0x0d, 0x09, 0xea, 0x05, 0x8e, 0xc5, 0x91, 0x6e,
0x00, 0x2a, 0x4d, 0x68, 0x18, 0x1b, 0x22, 0xd3, 0x41, 0xbd, 0x76, 0x95, 0x8d, 0x36, 0x57, 0xc4,
0x6c, 0x44, 0x30, 0xe5, 0x3f, 0xc0, 0x86, 0xd0, 0xe0, 0xef, 0x25, 0x6c, 0x54, 0x8f, 0xb6, 0x19,
0xea, 0xef, 0x1f, 0xf7, 0x4a, 0x42, 0xa7, 0xb5, 0x53, 0xec, 0x60, 0x24, 0xa5, 0x2a, 0x87, 0x7f,
0xe3, 0x1d, 0x54, 0xb6, 0x6c, 0x9b, 0x65, 0x09, 0x5c, 0xe7, 0xc0, 0x75, 0xd9, 0x58, 0x0a, 0xf0,
0x93, 0xd5, 0xac, 0x4b, 0x57, 0xeb, 0xe4, 0xb6, 0x74, 0x33, 0xca, 0xc7, 0xc4, 0x8f, 0x2a, 0x35,
0xcf, 0xfd, 0x95, 0x98, 0x80, 0xd7, 0xe9, 0x77, 0x51, 0x75, 0x66, 0xbd, 0x31, 0x03, 0xf2, 0xbb,
0x05, 0xf1, 0xc6, 0x84, 0xd3, 0x92, 0x33, 0x2a, 0x20, 0xeb, 0x47, 0x22, 0xbc, 0x8b, 0x90, 0xe3,
0x85, 0x3e, 0xb5, 0x17, 0xb0, 0xab, 0x5e, 0xe4, 0xbc, 0xa5, 0x24, 0xf8, 0xa7, 0xa8, 0xc4, 0x49,
0x35, 0x21, 0xf0, 0x12, 0x68, 0xa5, 0x23, 0x39, 0x0a, 0xbc, 0xc8, 0x29, 0xe5, 0x71, 0xc7, 0x9f,
0x46, 0x91, 0x63, 0x35, 0x1b, 0xff, 0x02, 0xc9, 0xc1, 0x85, 0xc3, 0x12, 0x22, 0x2c, 0x85, 0x70,
0x56, 0xd3, 0x27, 0x33, 0xfa, 0xca, 0x72, 0x83, 0x7a, 0x99, 0xbb, 0xa9, 0x33, 0x84, 0x96, 0x02,
0x18, 0x91, 0x5e, 0xe9, 0xa2, 0x3c, 0xb7, 0x08, 0xe5, 0x57, 0x10, 0x45, 0x19, 0x75, 0x69, 0xb4,
0xc2, 0x87, 0x28, 0x3f, 0x71, 0x5c, 0x20, 0x32, 0xcb, 0x73, 0x88, 0x53, 0x15, 0x0d, 0x62, 0xcd,
0x9b, 0xd0, 0x28, 0x8b, 0x02, 0xa6, 0x0c, 0x51, 0x85, 0x1b, 0x1c, 0xce, 0x6d, 0x0b, 0xca, 0xe6,
0xdb, 0x32, 0xfb, 0xb7, 0x1c, 0x2a, 0xc5, 0x9a, 0x24, 0xe9, 0x99, 0x54, 0xd2, 0x1b, 0x51, 0xdf,
0x8b, 0x2e, 0xde, 0xbe, 0x6e, 0x2f, 0xd5, 0xf8, 0xb0, 0x3f, 0x70, 0x7e, 0x4f, 0x78, 0xdf, 0xe4,
0x0c, 0xfe, 0x8d, 0xf7, 0x51, 0xe5, 0x6a, 0xb3, 0xac, 0x1b, 0x69, 0x11, 0xfe, 0x0a, 0xa1, 0x19,
0xb5, 0x9d, 0x89, 0x43, 0x6c, 0x33, 0xe0, 0x05, 0x90, 0x33, 0xca, 0xb1, 0xa4, 0x8f, 0xeb, 0xac,
0xdc, 0x59, 0xab, 0xd8, 0x51, 0x4f, 0xc4, 0x4b, 0xa6, 0x71, 0x3c, 0x60, 0x1b, 0xf2, 0x2a, 0xb2,
0x1e, 0x2f, 0xd9, 0x74, 0xf3, 0xe8, 0x4a, 0x93, 0x96, 0x38, 0x60, 0xdd, 0xa3, 0xe9, 0x06, 0x85,
0x4e, 0x8a, 0xa7, 0x1f, 0xcb, 0xe7, 0x4a, 0x27, 0x9d, 0x91, 0x71, 0x48, 0x93, 0xb9, 0x12, 0xc1,
0xb0, 0x8c, 0x4a, 0x49, 0x29, 0x22, 0x7e, 0xd2, 0x64, 0xcd, 0x66, 0x6e, 0x12, 0x07, 0x78, 0xac,
0x80, 0x3a, 0x6f, 0x24, 0xa1, 0xe9, 0x01, 0xfe, 0x31, 0x2a, 0x1c, 0xb9, 0x74, 0x7c, 0x11, 0xf7,
0xed, 0x9d, 0xa5, 0x37, 0x2e, 0x4f, 0x65, 0x27, 0x02, 0xb2, 0x40, 0x82, 0xcb, 0x99, 0xeb, 0x78,
0x17, 0x66, 0x68, 0xf9, 0x53, 0x12, 0xd6, 0x37, 0xc5, 0x98, 0x8e, 0xa4, 0x03, 0x2e, 0xfc, 0xb9,
0xf4, 0xa7, 0xb7, 0x7b, 0x6b, 0xca, 0x73, 0x54, 0x4e, 0xec, 0xb0, 0x02, 0xa1, 0x93, 0x49, 0x00,
0x3b, 0x32, 0xfc, 0x9c, 0xd1, 0x2a, 0xc9, 0x51, 0x96, 0x1f, 0x4f, 0xe4, 0x08, 0x64, 0xe7, 0x56,
0x70, 0xce, 0xf3, 0x56, 0x35, 0xf8, 0x77, 0x64, 0xf2, 0x97, 0xa8, 0x20, 0x88, 0xc0, 0x8f, 0x51,
0x69, 0x4c, 0x17, 0x5e, 0xb8, 0x1c, 0xc2, 0x9b, 0xe9, 0xde, 0xe6, 0x9a, 0xe8, 0xf0, 0x09, 0x50,
0x39, 0x46, 0xc5, 0x48, 0x05, 0x91, 0xc4, 0x83, 0x47, 0x3a, 0xba, 0x1b, 0xf7, 0x5f, 0xff, 0x9c,
0xfa, 0xe1, 0xca, 0xdc, 0x81, 0xa9, 0x0c, 0x29, 0x5c, 0x88, 0xf3, 0x49, 0x86, 0x58, 0x28, 0x7f,
0xcd, 0xa0, 0xa2, 0xc1, 0x78, 0x0e, 0xc2, 0xd4, 0x3c, 0xcf, 0xaf, 0xcc, 0xf3, 0x65, 0x47, 0x64,
0x57, 0x3a, 0x22, 0x2e, 0xea, 0x5c, 0xaa, 0xa8, 0x97, 0xe4, 0x48, 0x37, 0x92, 0x93, 0xbf, 0x81,
0x9c, 0xc2, 0x92, 0x1c, 0x96, 0x96, 0x89, 0x4f, 0x67, 0x7c, 0x62, 0x53, 0xdf, 0xf2, 0x2f, 0xa3,
0x02, 0x5c, 0x67, 0xd2, 0x41, 0x2c, 0x54, 0x4c, 0x54, 0x32, 0x48, 0x30, 0x87, 0x52, 0x23, 0xb7,
0x1e, 0x1b, 0xcc, 0x43, 0x43, 0x5b, 0xfc, 0xd0, 0x60, 0x9e, 0x7d, 0xe3, 0xef, 0x23, 0x69, 0x4c,
0x6d, 0x71, 0xe4, 0x8d, 0x74, 0x99, 0xa8, 0xbe, 0x4f, 0xe1, 0x52, 0xb4, 0xa1, 0xe1, 0x18, 0x00,
0x1e, 0x04, 0xb5, 0x36, 0x7d, 0xed, 0xb9, 0xd4, 0xb2, 0x7b, 0x3e, 0x9d, 0xb2, 0x89, 0x7a, 0xeb,
0x64, 0x68, 0xa3, 0xe2, 0x82, 0xcf, 0x8e, 0x78, 0x36, 0x7c, 0xb3, 0xda, 0xcb, 0x57, 0x0d, 0x89,
0x41, 0x13, 0x37, 0x40, 0xb4, 0x55, 0xf9, 0x90, 0x41, 0xf2, 0xed, 0x68, 0xac, 0xa1, 0x8a, 0x40,
0x9a, 0xa9, 0xc7, 0xc2, 0xc1, 0xe7, 0x38, 0xe2, 0x63, 0x04, 0x2d, 0x92, 0xef, 0x1b, 0x6f, 0xa0,
0x54, 0xc3, 0xe6, 0x3e, 0xaf, 0x61, 0xe1, 0x0e, 0x1e, 0xb1, 0x9e, 0x48, 0xee, 0x55, 0x09, 0x62,
0xcf, 0x1b, 0xd5, 0x91, 0x68, 0x14, 0x2e, 0x53, 0x0a, 0x48, 0xea, 0x39, 0xde, 0x54, 0xd9, 0x43,
0xf9, 0x96, 0x4b, 0x79, 0xb2, 0x0a, 0x70, 0xe9, 0x07, 0xe0, 0x26, 0xe2, 0x50, 0xac, 0x1a, 0x1f,
0xb2, 0xa8, 0x92, 0x7a, 0xef, 0xc0, 0x79, 0x36, 0x5a, 0x9d, 0x61, 0x7f, 0xa0, 0x1a, 0x66, 0xab,
0xab, 0x1f, 0x6b, 0x27, 0xb5, 0x35, 0x79, 0xe7, 0x0f, 0x7f, 0xde, 0xaf, 0xcf, 0x96, 0xa0, 0xd5,
0xa7, 0x0c, 0xb8, 0xd0, 0xf4, 0xb6, 0xfa, 0xeb, 0x5a, 0x46, 0xde, 0x02, 0x60, 0x2d, 0x05, 0x14,
0xf7, 0xc5, 0x0f, 0x51, 0x95, 0x03, 0xcc, 0x61, 0xaf, 0xdd, 0x1c, 0xa8, 0xb5, 0xac, 0x2c, 0x03,
0x6e, 0xfb, 0x2a, 0x2e, 0xe2, 0xfb, 0x6b, 0xe8, 0x0b, 0xf5, 0xf9, 0x50, 0xed, 0x0f, 0x6a, 0x39,
0x79, 0x1b, 0x80, 0x38, 0x05, 0x8c, 0x3b, 0xe6, 0x3e, 0x94, 0xa1, 0xda, 0xef, 0x75, 0xf5, 0xbe,
0x5a, 0x93, 0xe4, 0x2f, 0x01, 0x75, 0x67, 0x05, 0x15, 0x55, 0xe8, 0xcf, 0xd0, 0x66, 0xbb, 0xfb,
0x42, 0xef, 0x74, 0x9b, 0x6d, 0xb3, 0x67, 0x74, 0x4f, 0x60, 0x4f, 0xbf, 0x96, 0x97, 0xf7, 0x00,
0x7f, 0x2f, 0x85, 0xbf, 0x56, 0x70, 0x5f, 0x01, 0x7b, 0x9a, 0x7e, 0x52, 0x2b, 0xc8, 0x77, 0x00,
0xfa, 0x45, 0x0a, 0xca, 0x48, 0x65, 0x11, 0xb7, 0x3a, 0x5d, 0x70, 0x5d, 0xbc, 0x16, 0x31, 0x27,
0xbb, 0xf1, 0x5b, 0x84, 0xaf, 0xbf, 0x08, 0xf1, 0x37, 0x48, 0xd2, 0xbb, 0xba, 0x0a, 0x84, 0xf2,
0xf8, 0xaf, 0x23, 0x74, 0xea, 0x11, 0xac, 0xa0, 0x5c, 0xe7, 0x37, 0x3f, 0x01, 0x32, 0xbf, 0x03,
0xa0, 0xbb, 0xd7, 0x41, 0xa0, 0x6c, 0x50, 0x54, 0x49, 0x1b, 0x56, 0x50, 0xe9, 0x54, 0x1d, 0x34,
0x81, 0xdc, 0x26, 0x18, 0xe7, 0x47, 0x8a, 0xd5, 0xa7, 0x24, 0xb4, 0x78, 0x03, 0xee, 0xa0, 0xbc,
0xae, 0x9e, 0xa9, 0x06, 0x18, 0xde, 0x04, 0xc0, 0x7a, 0x0c, 0xd0, 0x09, 0xd4, 0x15, 0x3c, 0x38,
0x0a, 0xcd, 0xce, 0x8b, 0xe6, 0xcb, 0x3e, 0x24, 0x07, 0x83, 0x7a, 0x23, 0x56, 0x37, 0xdd, 0xd7,
0xd6, 0x65, 0xd0, 0xf8, 0x6f, 0x06, 0x55, 0xd3, 0xb7, 0x23, 0x6c, 0x90, 0x8e, 0xb5, 0x8e, 0x1a,
0xbb, 0x4b, 0xeb, 0xd8, 0x37, 0x3e, 0x40, 0xe5, 0xb6, 0x66, 0xa8, 0xad, 0x41, 0xd7, 0x78, 0x19,
0xc7, 0x92, 0x06, 0xb5, 0x1d, 0x9f, 0x17, 0x37, 0x7b, 0x81, 0x56, 0xfb, 0x2f, 0x4f, 0x3b, 0x9a,
0xfe, 0xcc, 0xe4, 0x16, 0xb3, 0xf2, 0x3d, 0x00, 0x7f, 0x99, 0x06, 0xf7, 0xc5, 0xcd, 0xc0, 0x0d,
0x3f, 0x41, 0x9b, 0x31, 0x7c, 0xe9, 0x20, 0x27, 0xef, 0xc3, 0x9e, 0x9d, 0x1b, 0xf6, 0x2c, 0xfd,
0x3c, 0x46, 0x5f, 0xc4, 0x1b, 0x87, 0xfa, 0x33, 0x1d, 0xca, 0x02, 0x2a, 0x67, 0x17, 0xb6, 0xc9,
0x37, 0x6c, 0x1b, 0x7a, 0x17, 0x1e, 0x14, 0x45, 0xe3, 0x2f, 0x19, 0x54, 0x4e, 0x26, 0x14, 0xe3,
0x59, 0xef, 0x9a, 0xaa, 0x61, 0x74, 0x8d, 0x38, 0xf0, 0x44, 0xa9, 0x53, 0xfe, 0x09, 0xaf, 0xbb,
0xe2, 0x89, 0xaa, 0xab, 0x86, 0xd6, 0x8a, 0xfb, 0x21, 0x81, 0x9c, 0x10, 0x8f, 0xf8, 0xce, 0x18,
0xfe, 0xef, 0xa8, 0x82, 0x99, 0xfe, 0xb0, 0xf5, 0x34, 0x8e, 0x98, 0x17, 0x70, 0xca, 0x54, 0x7f,
0x31, 0x3e, 0xe7, 0xd1, 0x36, 0x58, 0xeb, 0x9c, 0x35, 0x3b, 0x5a, 0x5b, 0x40, 0x73, 0x72, 0x1d,
0xa0, 0x5b, 0x09, 0x54, 0x13, 0xaf, 0x03, 0x86, 0x6d, 0xd8, 0x68, 0xf7, 0xff, 0xcf, 0x22, 0x78,
0xb8, 0x14, 0x9a, 0xbd, 0x9e, 0xaa, 0xb7, 0xe3, 0xd3, 0x2f, 0x75, 0xcd, 0xf9, 0x9c, 0x78, 0x36,
0x43, 0x1c, 0x77, 0x8d, 0x13, 0x75, 0x10, 0x1f, 0x7e, 0x89, 0x38, 0xa6, 0xec, 0x5e, 0x3e, 0xda,
0x79, 0xf7, 0xcf, 0xdd, 0xb5, 0xf7, 0xf0, 0xf7, 0xee, 0xd3, 0x6e, 0xe6, 0x3d, 0xfc, 0xfd, 0xe3,
0xd3, 0xee, 0xda, 0xbf, 0xe1, 0xf7, 0xed, 0xbf, 0x76, 0x33, 0xa3, 0x02, 0x9f, 0x5d, 0x8f, 0xff,
0x17, 0x00, 0x00, 0xff, 0xff, 0x20, 0x74, 0xd7, 0x8f, 0x1b, 0x0e, 0x00, 0x00,
}
+2 -1
View File
@@ -106,7 +106,8 @@ message FileInfo {
Vector version = 9 [(gogoproto.nullable) = false];
int64 sequence = 10;
repeated BlockInfo Blocks = 16 [(gogoproto.nullable) = false];
repeated BlockInfo Blocks = 16 [(gogoproto.nullable) = false];
string symlink_target = 17;
}
enum FileInfoType {
+19 -3
View File
@@ -30,8 +30,19 @@ func (m Hello) Magic() uint32 {
}
func (f FileInfo) String() string {
return fmt.Sprintf("File{Name:%q, Type:%v, Sequence:%d, Permissions:0%o, ModTime:%v, Version:%v, Length:%d, Deleted:%v, Invalid:%v, NoPermissions:%v, Blocks:%v}",
f.Name, f.Type, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.Size, f.Deleted, f.Invalid, f.NoPermissions, f.Blocks)
switch f.Type {
case FileInfoTypeDirectory:
return fmt.Sprintf("Directory{Name:%q, Sequence:%d, Permissions:0%o, ModTime:%v, Version:%v, Deleted:%v, Invalid:%v, NoPermissions:%v}",
f.Name, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.Deleted, f.Invalid, f.NoPermissions)
case FileInfoTypeFile:
return fmt.Sprintf("File{Name:%q, Sequence:%d, Permissions:0%o, ModTime:%v, Version:%v, Length:%d, Deleted:%v, Invalid:%v, NoPermissions:%v, Blocks:%v}",
f.Name, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.Size, f.Deleted, f.Invalid, f.NoPermissions, f.Blocks)
case FileInfoTypeSymlinkDirectory, FileInfoTypeSymlinkFile, FileInfoTypeSymlinkUnknown:
return fmt.Sprintf("Symlink{Name:%q, Type:%v, Sequence:%d, Version:%v, Deleted:%v, Invalid:%v, NoPermissions:%v, SymlinkTarget:%q}",
f.Name, f.Type, f.Sequence, f.Version, f.Deleted, f.Invalid, f.NoPermissions, f.SymlinkTarget)
default:
panic("mystery file type detected")
}
}
func (f FileInfo) IsDeleted() bool {
@@ -63,7 +74,7 @@ func (f FileInfo) FileSize() int64 {
if f.Deleted {
return 0
}
if f.IsDirectory() {
if f.IsDirectory() || f.IsSymlink() {
return SyntheticDirectorySize
}
return f.Size
@@ -134,3 +145,8 @@ func (i *IndexID) Unmarshal(bs []byte) error {
func NewIndexID() IndexID {
return IndexID(rand.Int64())
}
func (f Folder) Description() string {
// used by logging stuff
return fmt.Sprintf("%q (%s)", f.Label, f.ID)
}
+15
View File
@@ -203,3 +203,18 @@ func untypeoify(s string) string {
s = strings.Replace(s, "8", "B", -1)
return s
}
// DeviceIDs is a sortable slice of DeviceID
type DeviceIDs []DeviceID
func (l DeviceIDs) Len() int {
return len(l)
}
func (l DeviceIDs) Less(a, b int) bool {
return l[a].Compare(l[b]) == -1
}
func (l DeviceIDs) Swap(a, b int) {
l[a], l[b] = l[b], l[a]
}
+8 -8
View File
@@ -427,16 +427,16 @@ var (
)
var fileDescriptorDeviceidTest = []byte{
// 176 bytes of a gzipped FileDescriptorProto
// 171 bytes of a gzipped FileDescriptorProto
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0x4e, 0x49, 0x2d, 0xcb,
0x4c, 0x4e, 0xcd, 0x4c, 0x89, 0x2f, 0x49, 0x2d, 0x2e, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17,
0xe2, 0x00, 0x53, 0xc9, 0xf9, 0x39, 0x52, 0xba, 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9,
0xf9, 0xb9, 0xfa, 0xe9, 0xf9, 0xe9, 0xf9, 0xfa, 0x60, 0x99, 0xa4, 0xd2, 0x34, 0x30, 0x0f, 0xcc,
0x01, 0xb3, 0x20, 0x1a, 0x95, 0x54, 0xb9, 0xf8, 0x43, 0x52, 0x8b, 0x4b, 0xfc, 0x73, 0x52, 0x5c,
0xc0, 0xc6, 0x7a, 0xba, 0x08, 0x09, 0x71, 0xb1, 0x80, 0x4c, 0x96, 0x60, 0x54, 0x60, 0xd4, 0xe0,
0x09, 0x02, 0xb3, 0x95, 0xcc, 0x21, 0xca, 0xfc, 0x52, 0xcb, 0xe1, 0xca, 0x54, 0x90, 0x95, 0x39,
0x09, 0x9c, 0xb8, 0x27, 0xcf, 0x70, 0xeb, 0x9e, 0x3c, 0x07, 0x4c, 0x1e, 0xa2, 0xd1, 0x49, 0xe6,
0xc4, 0x43, 0x39, 0x86, 0x0b, 0x0f, 0xe5, 0x18, 0x4e, 0x3c, 0x92, 0x63, 0xbc, 0xf0, 0x48, 0x8e,
0xf1, 0xc1, 0x23, 0x39, 0x86, 0x17, 0x8f, 0xe4, 0x18, 0x16, 0x3c, 0x96, 0x63, 0x4c, 0x62, 0x03,
0x3b, 0xc2, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x35, 0x9c, 0x00, 0x78, 0xd4, 0x00, 0x00, 0x00,
0x01, 0xb3, 0x20, 0x1a, 0x95, 0x54, 0xb9, 0xf8, 0x43, 0x80, 0xc6, 0xf8, 0xe7, 0xa4, 0xb8, 0x80,
0x8d, 0xf5, 0x74, 0x11, 0x12, 0xe2, 0x62, 0x01, 0x99, 0x2c, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x13,
0x04, 0x66, 0x2b, 0x99, 0x43, 0x94, 0xf9, 0xa5, 0x96, 0xc3, 0x95, 0xa9, 0x20, 0x2b, 0x73, 0x12,
0x38, 0x71, 0x4f, 0x9e, 0xe1, 0xd6, 0x3d, 0x79, 0x0e, 0x98, 0x3c, 0x44, 0xa3, 0x93, 0xcc, 0x89,
0x87, 0x72, 0x0c, 0x17, 0x80, 0xf8, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x40, 0xfc, 0xe0, 0x91, 0x1c,
0xc3, 0x0b, 0x20, 0x5e, 0xf0, 0x58, 0x8e, 0x31, 0x89, 0x0d, 0xec, 0x08, 0x63, 0x40, 0x00, 0x00,
0x00, 0xff, 0xff, 0x35, 0x9c, 0x00, 0x78, 0xd4, 0x00, 0x00, 0x00,
}
+39 -4
View File
@@ -6,29 +6,64 @@ package protocol
// Windows uses backslashes as file separator
import "path/filepath"
import (
"path/filepath"
"strings"
)
type nativeModel struct {
Model
}
func (m nativeModel) Index(deviceID DeviceID, folder string, files []FileInfo) {
fixupFiles(folder, files)
files = fixupFiles(files)
m.Model.Index(deviceID, folder, files)
}
func (m nativeModel) IndexUpdate(deviceID DeviceID, folder string, files []FileInfo) {
fixupFiles(folder, files)
files = fixupFiles(files)
m.Model.IndexUpdate(deviceID, folder, files)
}
func (m nativeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, fromTemporary bool, buf []byte) error {
if strings.Contains(name, `\`) {
l.Warnln("Dropping request for %s, contains invalid path separator", name)
return ErrNoSuchFile
}
name = filepath.FromSlash(name)
return m.Model.Request(deviceID, folder, name, offset, hash, fromTemporary, buf)
}
func fixupFiles(folder string, files []FileInfo) {
func fixupFiles(files []FileInfo) []FileInfo {
var out []FileInfo
for i := range files {
if strings.Contains(files[i].Name, `\`) {
l.Warnln("Dropping index entry for %s, contains invalid path separator", files[i].Name)
if out == nil {
// Most incoming updates won't contain anything invalid, so
// we delay the allocation and copy to output slice until we
// really need to do it, then copy all the so-far valid
// files to it.
out = make([]FileInfo, i, len(files)-1)
copy(out, files)
}
continue
}
// Fixup the path separators
files[i].Name = filepath.FromSlash(files[i].Name)
if out != nil {
out = append(out, files[i])
}
}
if out != nil {
// We did some filtering
return out
}
// Unchanged
return files
}
+29
View File
@@ -0,0 +1,29 @@
// Copyright (C) 2016 The Protocol Authors.
package protocol
import "testing"
import "reflect"
func TestFixupFiles(t *testing.T) {
files := []FileInfo{
{Name: "foo/bar"},
{Name: `foo\bar`},
{Name: "foo/baz"},
{Name: "foo/quux"},
{Name: `foo\fail`},
}
// Filenames should be slash converted, except files which already have
// backslashes in them which are instead filtered out.
expected := []FileInfo{
{Name: `foo\bar`},
{Name: `foo\baz`},
{Name: `foo\quux`},
}
fixed := fixupFiles(files)
if !reflect.DeepEqual(fixed, expected) {
t.Errorf("Got %v, expected %v", fixed, expected)
}
}

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