mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-16 09:48:51 -05:00
Compare commits
42 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3bc918ff78 | ||
|
|
3e50edf46f | ||
|
|
1b10607def | ||
|
|
9d7a811e72 | ||
|
|
4f7d43a6dd | ||
|
|
83675e7a1d | ||
|
|
34fee05a1d | ||
|
|
d10773c311 | ||
|
|
caa2356409 | ||
|
|
523ac45456 | ||
|
|
b50d57b7fd | ||
|
|
666314a9d9 | ||
|
|
8e645ab782 | ||
|
|
dfc1101307 | ||
|
|
f12ca95af2 | ||
|
|
f528923d1e | ||
|
|
c9d6366d75 | ||
|
|
714a47ffb0 | ||
|
|
86a22b8086 | ||
|
|
a4d27282ae | ||
|
|
2e425c4386 | ||
|
|
d27463268d | ||
|
|
3d74ff97af | ||
|
|
675846ac1e | ||
|
|
c2b0d309fb | ||
|
|
cb0950b3fd | ||
|
|
03d0f0dc34 | ||
|
|
91c3218a0c | ||
|
|
2ced65b9e7 | ||
|
|
92405ad1a6 | ||
|
|
5a69e85e80 | ||
|
|
4f034a01ed | ||
|
|
d7bc0659e4 | ||
|
|
4f4781d254 | ||
|
|
6a87aac84f | ||
|
|
797a999585 | ||
|
|
272fb3b444 | ||
|
|
60eb9088ff | ||
|
|
c8652222ef | ||
|
|
84494edab4 | ||
|
|
b19885fdb3 | ||
|
|
e39dafb584 |
2
AUTHORS
2
AUTHORS
@@ -30,6 +30,7 @@ Antony Male (canton7) <antony.male@gmail.com>
|
||||
Aranjedeath <Aranjedeath@users.noreply.github.com>
|
||||
Arthur Axel fREW Schmidt (frioux) <frew@afoolishmanifesto.com> <frioux@gmail.com>
|
||||
Audrius Butkevicius (AudriusButkevicius) <audrius.butkevicius@gmail.com>
|
||||
BAHADIR YILMAZ <bahadiryilmaz32@gmail.com>
|
||||
Bart De Vries (mogwa1) <devriesb@gmail.com>
|
||||
Ben Curthoys (bencurthoys) <ben@bencurthoys.com>
|
||||
Ben Schulz (uok) <ueomkail@gmail.com> <uok@users.noreply.github.com>
|
||||
@@ -139,6 +140,7 @@ Pascal Jungblut (pascalj) <github@pascalj.com> <mail@pascal-jungblut.com>
|
||||
Pawel Palenica (qepasa) <pawelpalenica11@gmail.com>
|
||||
Paweł Rozlach <vespian@users.noreply.github.com>
|
||||
perewa <cavalcante.ten@gmail.com>
|
||||
Peter Badida <KeyWeeUsr@users.noreply.github.com>
|
||||
Peter Dave Hello <hsu@peterdavehello.org>
|
||||
Peter Hoeg (peterhoeg) <peter@speartail.com>
|
||||
Peter Marquardt (wwwutz) <wwwutz@gmail.com> <wwwutz@googlemail.com>
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func dump(ldb *db.Instance) {
|
||||
func dump(ldb *db.Lowlevel) {
|
||||
it := ldb.NewIterator(nil, nil)
|
||||
for it.Next() {
|
||||
key := it.Key()
|
||||
|
||||
@@ -37,7 +37,7 @@ func (h *ElementHeap) Pop() interface{} {
|
||||
return x
|
||||
}
|
||||
|
||||
func dumpsize(ldb *db.Instance) {
|
||||
func dumpsize(ldb *db.Lowlevel) {
|
||||
h := &ElementHeap{}
|
||||
heap.Init(h)
|
||||
|
||||
|
||||
@@ -7,9 +7,11 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"net"
|
||||
"net/http"
|
||||
@@ -206,7 +208,13 @@ func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener,
|
||||
},
|
||||
}
|
||||
|
||||
rawListener, err := net.Listen("tcp", guiCfg.Address())
|
||||
if guiCfg.Network() == "unix" {
|
||||
// When listening on a UNIX socket we should unlink before bind,
|
||||
// lest we get a "bind: address already in use". We don't
|
||||
// particularly care if this succeeds or not.
|
||||
os.Remove(guiCfg.Address())
|
||||
}
|
||||
rawListener, err := net.Listen(guiCfg.Network(), guiCfg.Address())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@@ -317,6 +325,7 @@ func (s *apiService) Serve() {
|
||||
debugMux.HandleFunc("/rest/debug/httpmetrics", s.getSystemHTTPMetrics)
|
||||
debugMux.HandleFunc("/rest/debug/cpuprof", s.getCPUProf) // duration
|
||||
debugMux.HandleFunc("/rest/debug/heapprof", s.getHeapProf)
|
||||
debugMux.HandleFunc("/rest/debug/support", s.getSupportBundle)
|
||||
getRestMux.Handle("/rest/debug/", s.whenDebugging(debugMux))
|
||||
|
||||
// A handler that splits requests between the two above and disables
|
||||
@@ -420,6 +429,9 @@ func (s *apiService) String() string {
|
||||
}
|
||||
|
||||
func (s *apiService) VerifyConfiguration(from, to config.Configuration) error {
|
||||
if to.GUI.Network() != "tcp" {
|
||||
return nil
|
||||
}
|
||||
_, err := net.ResolveTCPAddr("tcp", to.GUI.Address())
|
||||
return err
|
||||
}
|
||||
@@ -592,7 +604,7 @@ func (s *apiService) whenDebugging(h http.Handler) http.Handler {
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Debugging disabled", http.StatusBadRequest)
|
||||
http.Error(w, "Debugging disabled", http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -886,12 +898,15 @@ func (s *apiService) postSystemConfig(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
// Activate and save
|
||||
// Activate and save. Wait for the configuration to become active before
|
||||
// completing the request.
|
||||
|
||||
if _, err := s.cfg.Replace(to); err != nil {
|
||||
if wg, err := s.cfg.Replace(to); err != nil {
|
||||
l.Warnln("Replacing config:", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
} else {
|
||||
wg.Wait()
|
||||
}
|
||||
|
||||
if err := s.cfg.Save(); err != nil {
|
||||
@@ -980,6 +995,7 @@ func (s *apiService) getSystemStatus(w http.ResponseWriter, r *http.Request) {
|
||||
res["urVersionMax"] = usageReportVersion
|
||||
res["uptime"] = int(time.Since(startTime).Seconds())
|
||||
res["startTime"] = startTime
|
||||
res["guiAddressOverridden"] = s.cfg.GUI().IsOverridden()
|
||||
|
||||
sendJSON(w, res)
|
||||
}
|
||||
@@ -1024,6 +1040,111 @@ func (s *apiService) getSystemLogTxt(w http.ResponseWriter, r *http.Request) {
|
||||
}
|
||||
}
|
||||
|
||||
type fileEntry struct {
|
||||
name string
|
||||
data []byte
|
||||
}
|
||||
|
||||
func (s *apiService) getSupportBundle(w http.ResponseWriter, r *http.Request) {
|
||||
var files []fileEntry
|
||||
|
||||
// Redacted configuration as a JSON
|
||||
if jsonConfig, err := json.MarshalIndent(getRedactedConfig(s), "", " "); err == nil {
|
||||
files = append(files, fileEntry{name: "config.json.txt", data: jsonConfig})
|
||||
} else {
|
||||
l.Warnln("Support bundle: failed to create config.json:", err)
|
||||
}
|
||||
|
||||
// Log as a text
|
||||
var buflog bytes.Buffer
|
||||
for _, line := range s.systemLog.Since(time.Time{}) {
|
||||
fmt.Fprintf(&buflog, "%s: %s\n", line.When.Format(time.RFC3339), line.Message)
|
||||
}
|
||||
files = append(files, fileEntry{name: "log-inmemory.txt", data: buflog.Bytes()})
|
||||
|
||||
// Errors as a JSON
|
||||
if errs := s.guiErrors.Since(time.Time{}); len(errs) > 0 {
|
||||
if jsonError, err := json.MarshalIndent(errs, "", " "); err != nil {
|
||||
files = append(files, fileEntry{name: "errors.json.txt", data: jsonError})
|
||||
} else {
|
||||
l.Warnln("Support bundle: failed to create errors.json:", err)
|
||||
}
|
||||
}
|
||||
|
||||
// Panic files
|
||||
if panicFiles, err := filepath.Glob(filepath.Join(baseDirs["config"], "panic*")); err == nil {
|
||||
for _, f := range panicFiles {
|
||||
if panicFile, err := ioutil.ReadFile(f); err != nil {
|
||||
l.Warnf("Support bundle: failed to load %s: %s", filepath.Base(f), err)
|
||||
} else {
|
||||
files = append(files, fileEntry{name: filepath.Base(f), data: panicFile})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Archived log (default on Windows)
|
||||
if logFile, err := ioutil.ReadFile(locations[locLogFile]); err == nil {
|
||||
files = append(files, fileEntry{name: "log-ondisk.txt", data: logFile})
|
||||
}
|
||||
|
||||
// Version and platform information as a JSON
|
||||
if versionPlatform, err := json.MarshalIndent(map[string]string{
|
||||
"now": time.Now().Format(time.RFC3339),
|
||||
"version": Version,
|
||||
"codename": Codename,
|
||||
"longVersion": LongVersion,
|
||||
"os": runtime.GOOS,
|
||||
"arch": runtime.GOARCH,
|
||||
}, "", " "); err == nil {
|
||||
files = append(files, fileEntry{name: "version-platform.json.txt", data: versionPlatform})
|
||||
} else {
|
||||
l.Warnln("Failed to create versionPlatform.json: ", err)
|
||||
}
|
||||
|
||||
// Report Data as a JSON
|
||||
if usageReportingData, err := json.MarshalIndent(reportData(s.cfg, s.model, s.connectionsService, usageReportVersion, true), "", " "); err != nil {
|
||||
l.Warnln("Support bundle: failed to create versionPlatform.json:", err)
|
||||
} else {
|
||||
files = append(files, fileEntry{name: "usage-reporting.json.txt", data: usageReportingData})
|
||||
}
|
||||
|
||||
// Heap and CPU Proofs as a pprof extension
|
||||
var heapBuffer, cpuBuffer bytes.Buffer
|
||||
filename := fmt.Sprintf("syncthing-heap-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, Version, time.Now().Format("150405")) // hhmmss
|
||||
runtime.GC()
|
||||
pprof.WriteHeapProfile(&heapBuffer)
|
||||
files = append(files, fileEntry{name: filename, data: heapBuffer.Bytes()})
|
||||
|
||||
const duration = 4 * time.Second
|
||||
filename = fmt.Sprintf("syncthing-cpu-%s-%s-%s-%s.pprof", runtime.GOOS, runtime.GOARCH, Version, time.Now().Format("150405")) // hhmmss
|
||||
pprof.StartCPUProfile(&cpuBuffer)
|
||||
time.Sleep(duration)
|
||||
pprof.StopCPUProfile()
|
||||
files = append(files, fileEntry{name: filename, data: cpuBuffer.Bytes()})
|
||||
|
||||
// Add buffer files to buffer zip
|
||||
var zipFilesBuffer bytes.Buffer
|
||||
if err := writeZip(&zipFilesBuffer, files); err != nil {
|
||||
l.Warnln("Support bundle: failed to create support bundle zip:", err)
|
||||
http.Error(w, err.Error(), http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// Set zip file name and path
|
||||
zipFileName := fmt.Sprintf("support-bundle-%s-%s.zip", s.id.Short().String(), time.Now().Format("2006-01-02T150405"))
|
||||
zipFilePath := filepath.Join(baseDirs["config"], zipFileName)
|
||||
|
||||
// Write buffer zip to local zip file (back up)
|
||||
if err := ioutil.WriteFile(zipFilePath, zipFilesBuffer.Bytes(), 0600); err != nil {
|
||||
l.Warnln("Support bundle: support bundle zip could not be created:", err)
|
||||
}
|
||||
|
||||
// Serve the buffer zip to client for download
|
||||
w.Header().Set("Content-Type", "application/zip")
|
||||
w.Header().Set("Content-Disposition", "attachment; filename="+zipFileName)
|
||||
io.Copy(w, &zipFilesBuffer)
|
||||
}
|
||||
|
||||
func (s *apiService) getSystemHTTPMetrics(w http.ResponseWriter, r *http.Request) {
|
||||
stats := make(map[string]interface{})
|
||||
metrics.Each(func(name string, intf interface{}) {
|
||||
|
||||
@@ -7,12 +7,18 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"testing"
|
||||
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
)
|
||||
|
||||
var passwordHashBytes []byte
|
||||
|
||||
func init() {
|
||||
passwordHashBytes, _ = bcrypt.GenerateFromPassword([]byte("pass"), 0)
|
||||
}
|
||||
|
||||
func TestStaticAuthOK(t *testing.T) {
|
||||
passwordHashBytes, _ := bcrypt.GenerateFromPassword([]byte("pass"), 14)
|
||||
ok := authStatic("user", "pass", "user", string(passwordHashBytes))
|
||||
if !ok {
|
||||
t.Fatalf("should pass auth")
|
||||
@@ -20,7 +26,6 @@ func TestStaticAuthOK(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestSimpleAuthUsernameFail(t *testing.T) {
|
||||
passwordHashBytes, _ := bcrypt.GenerateFromPassword([]byte("pass"), 14)
|
||||
ok := authStatic("userWRONG", "pass", "user", string(passwordHashBytes))
|
||||
if ok {
|
||||
t.Fatalf("should fail auth")
|
||||
@@ -28,8 +33,7 @@ func TestSimpleAuthUsernameFail(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestStaticAuthPasswordFail(t *testing.T) {
|
||||
passwordHashBytes, _ := bcrypt.GenerateFromPassword([]byte("passWRONG"), 14)
|
||||
ok := authStatic("user", "pass", "user", string(passwordHashBytes))
|
||||
ok := authStatic("user", "passWRONG", "user", string(passwordHashBytes))
|
||||
if ok {
|
||||
t.Fatalf("should fail auth")
|
||||
}
|
||||
|
||||
@@ -252,6 +252,7 @@ type RuntimeOptions struct {
|
||||
cpuProfile bool
|
||||
stRestarting bool
|
||||
logFlags int
|
||||
showHelp bool
|
||||
}
|
||||
|
||||
func defaultRuntimeOptions() RuntimeOptions {
|
||||
@@ -295,6 +296,7 @@ func parseCommandLineOptions() RuntimeOptions {
|
||||
flag.BoolVar(&options.doUpgrade, "upgrade", false, "Perform upgrade")
|
||||
flag.BoolVar(&options.doUpgradeCheck, "upgrade-check", false, "Check for available upgrade")
|
||||
flag.BoolVar(&options.showVersion, "version", false, "Show version")
|
||||
flag.BoolVar(&options.showHelp, "help", false, "Show this help")
|
||||
flag.BoolVar(&options.showPaths, "paths", false, "Show configuration paths")
|
||||
flag.BoolVar(&options.showDeviceId, "device-id", false, "Show the device ID")
|
||||
flag.StringVar(&options.upgradeTo, "upgrade-to", options.upgradeTo, "Force upgrade directly from specified URL")
|
||||
@@ -381,6 +383,11 @@ func main() {
|
||||
return
|
||||
}
|
||||
|
||||
if options.showHelp {
|
||||
flag.Usage()
|
||||
return
|
||||
}
|
||||
|
||||
if options.showPaths {
|
||||
showPaths(options)
|
||||
return
|
||||
@@ -705,11 +712,13 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
if err != nil {
|
||||
l.Fatalln("Error opening database:", err)
|
||||
}
|
||||
if err := db.UpdateSchema(ldb); err != nil {
|
||||
l.Fatalln("Database schema:", err)
|
||||
}
|
||||
|
||||
if runtimeOptions.resetDeltaIdxs {
|
||||
l.Infoln("Reinitializing delta index IDs")
|
||||
ldb.DropLocalDeltaIndexIDs()
|
||||
ldb.DropRemoteDeltaIndexIDs()
|
||||
db.DropDeltaIndexIDs(ldb)
|
||||
}
|
||||
|
||||
protectedFiles := []string{
|
||||
@@ -730,7 +739,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
|
||||
// Grab the previously running version string from the database.
|
||||
|
||||
miscDB := db.NewNamespacedKV(ldb, string(db.KeyTypeMiscData))
|
||||
miscDB := db.NewMiscDataNamespace(ldb)
|
||||
prevVersion, _ := miscDB.String("prevVersion")
|
||||
|
||||
// Strip away prerelease/beta stuff and just compare the release
|
||||
@@ -746,7 +755,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
|
||||
// Drop delta indexes in case we've changed random stuff we
|
||||
// shouldn't have. We will resend our index on next connect.
|
||||
ldb.DropLocalDeltaIndexIDs()
|
||||
db.DropDeltaIndexIDs(ldb)
|
||||
|
||||
// Remember the new version.
|
||||
miscDB.PutString("prevVersion", Version)
|
||||
@@ -1254,6 +1263,7 @@ func cleanConfigDirectory() {
|
||||
"*.idx.gz": 30 * 24 * time.Hour, // these should for sure no longer exist
|
||||
"backup-of-v0.8": 30 * 24 * time.Hour, // these neither
|
||||
"tmp-index-sorter.*": time.Minute, // these should never exist on startup
|
||||
"support-bundle-*": 30 * 24 * time.Hour, // keep old support bundle zip or folder for a month
|
||||
}
|
||||
|
||||
for pat, dur := range patterns {
|
||||
|
||||
@@ -39,7 +39,7 @@ func (c *mockedConfig) Options() config.OptionsConfiguration {
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Replace(cfg config.Configuration) (config.Waiter, error) {
|
||||
return nil, nil
|
||||
return noopWaiter{}, nil
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Subscribe(cm config.Committer) {}
|
||||
@@ -53,11 +53,11 @@ func (c *mockedConfig) Devices() map[protocol.DeviceID]config.DeviceConfiguratio
|
||||
}
|
||||
|
||||
func (c *mockedConfig) SetDevice(config.DeviceConfiguration) (config.Waiter, error) {
|
||||
return nil, nil
|
||||
return noopWaiter{}, nil
|
||||
}
|
||||
|
||||
func (c *mockedConfig) SetDevices([]config.DeviceConfiguration) (config.Waiter, error) {
|
||||
return nil, nil
|
||||
return noopWaiter{}, nil
|
||||
}
|
||||
|
||||
func (c *mockedConfig) Save() error {
|
||||
@@ -67,3 +67,7 @@ func (c *mockedConfig) Save() error {
|
||||
func (c *mockedConfig) RequiresRestart() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
type noopWaiter struct{}
|
||||
|
||||
func (noopWaiter) Wait() {}
|
||||
|
||||
47
cmd/syncthing/support_bundle.go
Normal file
47
cmd/syncthing/support_bundle.go
Normal file
@@ -0,0 +1,47 @@
|
||||
// Copyright (C) 2018 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
"archive/zip"
|
||||
"io"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
)
|
||||
|
||||
// getRedactedConfig redacting some parts of config
|
||||
func getRedactedConfig(s *apiService) config.Configuration {
|
||||
rawConf := s.cfg.RawCopy()
|
||||
rawConf.GUI.APIKey = "REDACTED"
|
||||
if rawConf.GUI.Password != "" {
|
||||
rawConf.GUI.Password = "REDACTED"
|
||||
}
|
||||
if rawConf.GUI.User != "" {
|
||||
rawConf.GUI.User = "REDACTED"
|
||||
}
|
||||
return rawConf
|
||||
}
|
||||
|
||||
// writeZip writes a zip file containing the given entries
|
||||
func writeZip(writer io.Writer, files []fileEntry) error {
|
||||
zipWriter := zip.NewWriter(writer)
|
||||
defer zipWriter.Close()
|
||||
|
||||
for _, file := range files {
|
||||
zipFile, err := zipWriter.Create(file.name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, err = zipFile.Write(file.data)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
return zipWriter.Close()
|
||||
}
|
||||
@@ -107,8 +107,8 @@
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Битовете за права за достъп ще бъдат игнорирани, когато се проверява за промени. Ползвайте за файлови системи тип FAT.",
|
||||
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Файловете биват преместени в .stversions папка, когато са заменен или изтрити от Syncthing.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Файловете биват преместени в .stversions папка, когато са заменен или изтрити от Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Когато syncthing замени или изтрие файл той се премества в .stversions и преименува с набавени дата и час.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Когато syncthing замени или изтрие файл той се премества в .stversions и преименува с набавени дата и час.",
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Когато syncthing замени или изтрие файл той се премества в .stversions и преименува с добавяне на дата и час.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Когато syncthing замени или изтрие файл той се премества в .stversions и преименува с набавяне на дата и час.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Защитава файловете от промени направени на други устройства, но промените направени на това устройство ще бъдат синхронизирани с останалите устройства.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.",
|
||||
"Filesystem Notifications": "Известия на системата",
|
||||
@@ -140,8 +140,8 @@
|
||||
"Ignore": "Игнорирай",
|
||||
"Ignore Patterns": "Шаблони за игнориране",
|
||||
"Ignore Permissions": "Игнорирай правата за достъп",
|
||||
"Ignored Devices": "Ignored Devices",
|
||||
"Ignored Folders": "Ignored Folders",
|
||||
"Ignored Devices": "Игнорирани устройства",
|
||||
"Ignored Folders": "Игнорирани папки",
|
||||
"Ignored at": "Ignored at",
|
||||
"Incoming Rate Limit (KiB/s)": "Лимит на скоростта за сваляне (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Неправилни настройки могат да повредят файловете и да попречат на синхронизирането.",
|
||||
@@ -198,7 +198,7 @@
|
||||
"Path": "Път",
|
||||
"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": "Път до папката на това устройство. Ако не съществува ще бъде създадена. Символът тилда (~) може да бъде използван като заместител на",
|
||||
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Пътя, където нови автоматично приети папки ще бъдат създадени, както и пътят, който потребителският интерфейс ще предлага при добавяне на нови папки. ТСимволът тилда (~) ще се превърне в {{tilde}}.",
|
||||
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Пътят, където версиите да бъдат складирани(оставете празно за папката .stversions).",
|
||||
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Пътят, където версиите да бъдат складирани (оставете празно за папката .stversions).",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Пътят, където версиите да бъдат складирани(остави празно за папката .stversions).",
|
||||
"Pause": "Пауза",
|
||||
"Pause All": "Пауза на висчко",
|
||||
@@ -218,7 +218,7 @@
|
||||
"Quick guide to supported patterns": "Бърз наръчник към поддържаните шаблони",
|
||||
"RAM Utilization": "Използван RAM",
|
||||
"Random": "Произволен",
|
||||
"Receive Only": "Receive Only",
|
||||
"Receive Only": "Само получаване",
|
||||
"Recent Changes": "Последни промени",
|
||||
"Reduced by ignore patterns": "Намалено посредством шаблон за игнориране",
|
||||
"Release Notes": "Бележки по обновяването",
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Статистика",
|
||||
"Stopped": "Не се синхронизира",
|
||||
"Support": "Помощ",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Адрес за слушане на синхронизиращия протокол",
|
||||
"Syncing": "Синхронизиране",
|
||||
"Syncthing has been shut down.": "Syncthing е спрян.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing изглежда не е включен, или има проблем с интерент връзката. Повторен опит...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing има проблем при обработването на заявката. Моля, презаредете браузъра или рестартирайте Syncthing ако проблемът продължи.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Администраторския панел на Syncthing е настроен да приема дистанционни връзки без парола.",
|
||||
"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.": "Конфигурацията е запазена, но не е активирана. Syncthing трябва да рестартира, за да се активира новата конфигурация.",
|
||||
@@ -306,7 +308,7 @@
|
||||
"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.": "Използва се следния интервал: за първия час се пази версия всеки 30 секунди, за първия ден се пази версия всеки час, за първите 30 дена се пази версия всеки ден, до максимума се пази една версия всяка седмица.",
|
||||
"The following items could not be synchronized.": "Следните не могат да бъдат синхронизирани.",
|
||||
"The maximum age must be a number and cannot be blank.": "Максималната възраст трябва да е число и не може д ае празна.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максималното време да се пазят весрсии (в дни, сложи 0, за да пазиш версии завинаги).",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Максималното време за пазене на версии (в дни, задайте 0 за да пазите всяка версия завинаги).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "Минималното свободно дисково пространство в проценти трябва да е между 0 и 100 (включително).",
|
||||
"The number of days must be a number and cannot be blank.": "Броят дни трябва да бъде число и не може да бъде празно.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Броят дни за запазване на файловете в кошчето. Нула значи завинаги.",
|
||||
@@ -349,17 +351,17 @@
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Внимание, това е вътрешна папка на вече съществуваща папка \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Предупреждение, този път е под-директория на съществуващата папка \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Предупреждение: Ако използвате външна програма за наблюдение като {{syncthingInotify}}, трябва да я деактивирате.",
|
||||
"Watch for Changes": "Следене за промени",
|
||||
"Watching for Changes": "Следи за промени",
|
||||
"Watch for Changes": "Следи за промени",
|
||||
"Watching for Changes": "Следене за промени",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Когато добавяш ново устройство помни, че твоето устройство също трябва да бъде добавено от другата страна.",
|
||||
"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.": "Когато добавяш нов идентификатор на папка помни, че той се използва за свързване на папките на различни устройства. Главни/малки букви са от значение и трябва да са еднакви на всички устройства.",
|
||||
"Yes": "Да",
|
||||
"You can also select one of these nearby devices:": "Също така може да изберете едно от следните устройтва намиращи се наблизо:",
|
||||
"You can change your choice at any time in the Settings dialog.": "Може да промените решението си по всяко време в прозореца Настройки.",
|
||||
"You can read more about the two release channels at the link below.": "Може да научите допълнително за двата канала на версии, следвайки връзката по-долу.",
|
||||
"You have no ignored devices.": "You have no ignored devices.",
|
||||
"You have no ignored folders.": "You have no ignored folders.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
|
||||
"You have no ignored devices.": "Няма игнорирани устройства.",
|
||||
"You have no ignored folders.": "Няма игнорирани папки.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Има незапазени промени. Наистина ли желаете да ги отмените?",
|
||||
"You must keep at least one version.": "Трябва да пазиш поне една версия.",
|
||||
"days": "дни",
|
||||
"directories": "директории",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Estadístiques",
|
||||
"Stopped": "Parat",
|
||||
"Support": "Suport",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Direccions d'escolta del protocol de sincronització",
|
||||
"Syncing": "Sincronitzant",
|
||||
"Syncthing has been shut down.": "Syncthing s'ha apagat",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing pareix apagat o hi ha un problema amb la connexió a Internet. Tornant a intentar...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing pareix que té un problema processant la seua sol·licitud. Per favor, refresque la pàgina o reinicie Syncthing si el problema persistix.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interfície d'administració de Syncthing està configurat per a permetre l'accés remot sense una contrasenya.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Les estadístiques agregades estàn disponibles en la URL que figura a continuació.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuració ha sigut gravada però no activada. Syncthing deu reiniciar per tal d'activar la nova configuració.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistiky",
|
||||
"Stopped": "Pozastaveno",
|
||||
"Support": "Podpora",
|
||||
"Support Bundle": "Balík podpory",
|
||||
"Sync Protocol Listen Addresses": "Adresa naslouchání synchronizačního protokolu",
|
||||
"Syncing": "Synchronizuje se",
|
||||
"Syncthing has been shut down.": "Syncthing byl vypnut.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing se zdá být nefunkční, nebo je problém s připojením k Internetu. Opakuji...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing má nejspíše problém s provedením vašeho požadavku. Pokud problém přetrvává, obnovte stránku v prohlížeči nebo restartujte Syncthing.",
|
||||
"Take me back": "Jít zpět",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Adresa v GUI je potlačena parametry při spuštění. Dokud potlačení trvá, zdejší změny nemají efekt.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "V nastavení aplikace Syncthing je povoleno vzdálené připojení k administrátorskému rozhraní bez zadání hesla.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Souhrnné statistiky jsou veřejně dostupné na níže uvedené URL.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurace byla uložena, ale není aktivována. Pro aktivaci nové konfigurace je třeba restartovat Syncthing.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistikker",
|
||||
"Stopped": "Stoppet",
|
||||
"Support": "Støt",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Lytteadresser for synkroniseringsprotokol",
|
||||
"Syncing": "Synkroniserer",
|
||||
"Syncthing has been shut down.": "Syncthing er lukket ned.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ud til at være stoppet eller oplever problemer med din internetforbindelse. Prøver igen…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Det ser ud til, at Syncthing har problemer med at udføre opgaven. Prøv at genindlæse siden eller genstarte Synching, hvis problemet vedbliver.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing-administationsfladen er sat op til at kunne fjernstyres uden adgangskode.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Den indsamlede statistik er offentligt tilgængelig på den nedenstående URL.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen er gemt, men ikke aktiveret. Syncthing skal genstarte for at aktivere den nye konfiguration.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistiken",
|
||||
"Stopped": "Gestoppt",
|
||||
"Support": "Support",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Adresse(n) für das Synchronisierungsprotokoll",
|
||||
"Syncing": "Synchronisiere",
|
||||
"Syncthing has been shut down.": "Syncthing wurde heruntergefahren.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing scheint nicht erreichbar zu sein oder es gibt ein Problem mit Deiner Internetverbindung. Versuche erneut...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing scheint ein Problem mit der Verarbeitung Deiner Eingabe zu haben. Bitte lade die Seite neu oder führe einen Neustart durch, falls das Problem weiterhin besteht.",
|
||||
"Take me back": "Führe mich zurück",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Die GUI-Adresse wird durch Startoptionen überschrieben. Hier vorgenommene Änderungen werden nicht wirksam, solange die Überschreibung besteht.",
|
||||
"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.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Στατιστικά",
|
||||
"Stopped": "Απενεργοποιημένο",
|
||||
"Support": "Υποστήριξη",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Διευθύνσεις για το πρωτόκολλο συγχρονισμού",
|
||||
"Syncing": "Συγχρονίζω",
|
||||
"Syncthing has been shut down.": "Το Syncthing έχει απενεργοποιηθεί.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Το Syncthing φαίνεται πως είναι απενεργοποιημένο ή υπάρχει πρόβλημα στη σύνδεσή σου στο διαδίκτυο. Προσπαθώ πάλι…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Το Syncthing φαίνεται να αντιμετωπίζει ένα πρόβλημα με την επεξεργασία του αιτήματός σου. Παρακαλούμε, αν το πρόβλημα συνεχίζει, ανανέωσε την σελίδα ή επανεκκίνησε το Syncthing.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Η διεπαφή διαχείρισης του Syncthing είναι ρυθμισμένη να επιτρέπει την πρόσβαση χωρίς κωδικό.",
|
||||
"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.": "Οι ρυθμίσεις έχουν αποθηκευτεί αλλά δεν έχουν ενεργοποιηθεί. Πρέπει να επανεκκινήσεις το Syncthing για να ισχύσουν οι νέες ρυθμίσεις.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistics",
|
||||
"Stopped": "Stopped",
|
||||
"Support": "Support",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses",
|
||||
"Syncing": "Syncing",
|
||||
"Syncthing has been shut down.": "Syncthing has been shut down.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
|
||||
"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.": "The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistics",
|
||||
"Stopped": "Stopped",
|
||||
"Support": "Support",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Sync Protocol Listen Addresses",
|
||||
"Syncing": "Syncing",
|
||||
"Syncthing has been shut down.": "Syncthing has been shut down.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
|
||||
"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.": "The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.",
|
||||
|
||||
@@ -1,371 +0,0 @@
|
||||
{
|
||||
"A device with that ID is already added.": "Aparato kies ID estis jam aldonita.",
|
||||
"A negative number of days doesn't make sense.": "Negativa numero de tagoj sen senco.",
|
||||
"A new major version may not be compatible with previous versions.": "Nova ĉefa versio eble ne kongruanta kun antaŭaj versioj.",
|
||||
"API Key": "API Ŝlosilo",
|
||||
"About": "Pri",
|
||||
"Action": "Ago",
|
||||
"Actions": "Agoj",
|
||||
"Add": "Aldoni",
|
||||
"Add Device": "Aldonu Aparaton",
|
||||
"Add Folder": "Aldonu Dosierujon",
|
||||
"Add Remote Device": "Aldonu Foran Aparaton",
|
||||
"Add devices from the introducer to our device list, for mutually shared folders.": "Add devices from the introducer to our device list, for mutually shared folders.",
|
||||
"Add new folder?": "Aldoni novan dosierujon?",
|
||||
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.",
|
||||
"Address": "Adreso",
|
||||
"Addresses": "Adresoj",
|
||||
"Advanced": "Altnivela",
|
||||
"Advanced Configuration": "Altnivela Konfiguro",
|
||||
"Advanced settings": "Altnivelaj agordoj",
|
||||
"All Data": "Ĉiuj Datumoj",
|
||||
"Allow Anonymous Usage Reporting?": "Permesi Anoniman Raporton de Uzado?",
|
||||
"Allowed Networks": "Permesitaj Retoj",
|
||||
"Alphabetic": "Alfabeta",
|
||||
"An external command handles the versioning. It has to remove the file from the shared folder.": "Ekstera komando manipulas la version. Ĝi devas forigi la dosieron el la partigita dosierujo.",
|
||||
"An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.": "An external command handles the versioning. It has to remove the file from the shared folder. If the path to the application contains spaces, it should be quoted.",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Ekstera komando manipulas la version. Ĝi devas forigi la dosieron el la sinkronigita dosierujo.",
|
||||
"Anonymous Usage Reporting": "Anonima Raporto de Uzado",
|
||||
"Anonymous usage report format has changed. Would you like to move to the new format?": "Anonymous usage report format has changed. Would you like to move to the new format?",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Ajna aparatoj agorditaj sur enkondukanto aparato estos ankaŭ aldonita al ĉi tiu aparato.",
|
||||
"Are you sure you want to remove device {%name%}?": "Are you sure you want to remove device {{name}}?",
|
||||
"Are you sure you want to remove folder {%label%}?": "Are you sure you want to remove folder {{label}}?",
|
||||
"Are you sure you want to restore {%count%} files?": "Are you sure you want to restore {{count}} files?",
|
||||
"Auto Accept": "Auto Accept",
|
||||
"Automatic upgrade now offers the choice between stable releases and release candidates.": "Aŭtomata ĝisdatigo nun proponas la elekton inter stabilaj eldonoj kaj kandidataj eldonoj.",
|
||||
"Automatic upgrades": "Aŭtomataj ĝisdatigoj",
|
||||
"Automatically create or share folders that this device advertises at the default path.": "Automatically create or share folders that this device advertises at the default path.",
|
||||
"Available debug logging facilities:": "Available debug logging facilities:",
|
||||
"Be careful!": "Atentu!",
|
||||
"Bugs": "Bugoj",
|
||||
"CPU Utilization": "Ĉefprocesoro Uzo",
|
||||
"Changelog": "Ŝanĝoprotokolo",
|
||||
"Clean out after": "Purigi poste",
|
||||
"Click to see discovery failures": "Klaku por vidi malsukcesajn malkovrojn",
|
||||
"Close": "Fermi",
|
||||
"Command": "Komando",
|
||||
"Comment, when used at the start of a line": "Komento, kiam uzita ĉe la komenco de lineo",
|
||||
"Compression": "Densigo",
|
||||
"Configured": "Agordita",
|
||||
"Connection Error": "Eraro de Konekto",
|
||||
"Connection Type": "Tipo de Konekto",
|
||||
"Connections": "Connections",
|
||||
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.",
|
||||
"Copied from elsewhere": "Kopiita el aliloke",
|
||||
"Copied from original": "Kopiita el la originalo",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Kopirajto © 2014-2016 por la sekvantaj Kontribuantoj:",
|
||||
"Copyright © 2014-2017 the following Contributors:": "Kopirajto © 2014-2017 por la sekvantaj Kontribuantoj:",
|
||||
"Creating ignore patterns, overwriting an existing file at {%path%}.": "Kreante ignori skemojn, anstataŭige ekzistantan dosieron ĉe {{path}}.",
|
||||
"Danger!": "Danĝero!",
|
||||
"Debugging Facilities": "Debugging Facilities",
|
||||
"Default Folder Path": "Default Folder Path",
|
||||
"Deleted": "Forigita",
|
||||
"Device": "Aparato",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Aparato \"{{name}}\" ({{device}} ĉe {{address}}) volas konekti. Aldoni la novan aparaton?",
|
||||
"Device ID": "Aparato ID",
|
||||
"Device Identification": "Identigo de Aparato",
|
||||
"Device Name": "Nomo de Aparato",
|
||||
"Device rate limits": "Device rate limits",
|
||||
"Device that last modified the item": "Device that last modified the item",
|
||||
"Devices": "Aparatoj",
|
||||
"Disabled": "Disabled",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Disabled periodic scanning and disabled watching for changes",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Disabled periodic scanning and enabled watching for changes",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:",
|
||||
"Discard": "Discard",
|
||||
"Disconnected": "Malkonektita",
|
||||
"Discovered": "Malkovrita",
|
||||
"Discovery": "Malkovro",
|
||||
"Discovery Failures": "Malkovritaj Fiaskoj",
|
||||
"Do not restore": "Do not restore",
|
||||
"Do not restore all": "Do not restore all",
|
||||
"Do you want to enable watching for changes for all your folders?": "Do you want to enable watching for changes for all your folders?",
|
||||
"Documentation": "Dokumentado",
|
||||
"Download Rate": "Elŝutado rapida",
|
||||
"Downloaded": "Elŝutita",
|
||||
"Downloading": "Elŝutado",
|
||||
"Edit": "Redakti",
|
||||
"Edit Device": "Redakti Aparaton",
|
||||
"Edit Folder": "Redakti Dosierujon",
|
||||
"Editing": "Redaktado",
|
||||
"Editing {%path%}.": "Redaktado {{path}}.",
|
||||
"Enable NAT traversal": "Ŝaltu trairan NAT",
|
||||
"Enable Relaying": "Ŝaltu Relajsadon",
|
||||
"Enabled": "Enabled",
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.",
|
||||
"Enter a non-privileged port number (1024 - 65535).": "Enter a non-privileged port number (1024 - 65535).",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enigi adresojn dividitajn per komoj (\"tcp://ip:port\", \"tcp://host:port\") aŭ \"dynamic\" por elfari aŭtomatan malkovradon de la adreso.",
|
||||
"Enter ignore patterns, one per line.": "Entajpu ignori skemojn, unu po linio.",
|
||||
"Error": "Eraro",
|
||||
"External File Versioning": "Ekstera Versionado de Dosiero",
|
||||
"Failed Items": "Malsukcesaj Eroj",
|
||||
"Failed to load ignore patterns": "Failed to load ignore patterns",
|
||||
"Failed to setup, retrying": "Failed to setup, retrying",
|
||||
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Malsukceso por konekti al IPv6 serviloj atendante se ekzistas neniu IPv6 konektebleco.",
|
||||
"File Pull Order": "Ordo por Tiri Dosieron",
|
||||
"File Versioning": "Versionado de Dosieroj",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Permesoj bitaj de dosieroj estas ignorita dum la serĉado por ŝanĝoj. Uzi en FAT dosiersistemoj.",
|
||||
"Files are moved to .stversions directory when replaced or deleted by Syncthing.": "Dosieroj estas movigitaj al .stversiaj dosierujoj kiam anstataŭigitaj aŭ forigitaj en Syncthing.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Dosieroj estas movigitaj al .stversions dosierujo kiam anstataŭigitaj aŭ forigitaj en Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Dosieroj estas movigitaj al date stampitaj versioj en .stversiaj dosierujo kiam ili estas anstataŭigitaj aŭ forigitaj en Syncthing.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dosieroj estas movigitaj al datostampitaj versioj en .stversions dosierujoj kiam ili estas anstataŭigitaj aŭ forigitaj en Syncthing.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dosieroj estas protektataj kontraŭ ŝanĝoj faritaj en aliaj aparatoj, sed ŝanĝoj faritaj en ĉi tiu aparato estos senditaj al cetera parto de la grupo.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.",
|
||||
"Filesystem Notifications": "Filesystem Notifications",
|
||||
"Filesystem Watcher Errors": "Filesystem Watcher Errors",
|
||||
"Filter by date": "Filter by date",
|
||||
"Filter by name": "Filter by name",
|
||||
"Folder": "Dosierujo",
|
||||
"Folder ID": "Dosieruja ID",
|
||||
"Folder Label": "Dosieruja Etikedo",
|
||||
"Folder Path": "Dosieruja Vojo",
|
||||
"Folder Type": "Dosieruja Tipo",
|
||||
"Folders": "Dosierujoj",
|
||||
"For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.": "For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.",
|
||||
"Full Rescan Interval (s)": "Full Rescan Interval (s)",
|
||||
"GUI": "Grafika Interfaco",
|
||||
"GUI Authentication Password": "Pasvorta Aŭtentigo en Grafika Interfaco",
|
||||
"GUI Authentication User": "Uzanta Aŭtentigo en Grafika Interfaco",
|
||||
"GUI Listen Address": "GUI Listen Address",
|
||||
"GUI Listen Addresses": "Aŭskultado de Adresoj en Grafika Interfaco",
|
||||
"GUI Theme": "Etoso de Grafika Interfaco",
|
||||
"General": "General",
|
||||
"Generate": "Generi",
|
||||
"Global Changes": "Kompletaj Ŝanĝoj",
|
||||
"Global Discovery": "Kompleta Malkovro",
|
||||
"Global Discovery Servers": "Kompleta Malkovrado de Serviloj",
|
||||
"Global State": "Kompleta Stato",
|
||||
"Help": "Helpo",
|
||||
"Home page": "Hejma paĝo",
|
||||
"Ignore": "Ignoru",
|
||||
"Ignore Patterns": "Ignori Ŝablonojn",
|
||||
"Ignore Permissions": "Ignori Permesojn",
|
||||
"Ignored Devices": "Ignored Devices",
|
||||
"Ignored Folders": "Ignored Folders",
|
||||
"Ignored at": "Ignored at",
|
||||
"Incoming Rate Limit (KiB/s)": "Alvenanta Limo Rapideco (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Erara agordo povas difekti viajn dosierujajn enhavojn kaj senefikigi Syncthing-n.",
|
||||
"Introduced By": "Enkondukita Per",
|
||||
"Introducer": "Enkondukita",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inversigo de la donita kondiĉo (i.e. ne ekskludas)",
|
||||
"Keep Versions": "Konservi Versiojn.",
|
||||
"Largest First": "Plej Granda Unue",
|
||||
"Last File Received": "Lasta Dosiero Ricevita",
|
||||
"Last Scan": "Lasta Skano",
|
||||
"Last seen": "Lasta vidita",
|
||||
"Later": "Poste",
|
||||
"Latest Change": "Lasta Ŝanĝo",
|
||||
"Learn more": "Lerni pli",
|
||||
"Listeners": "Aŭskultantoj",
|
||||
"Loading data...": "Loading data...",
|
||||
"Loading...": "Loading...",
|
||||
"Local Discovery": "Loka Malkovro",
|
||||
"Local State": "Loka Stato",
|
||||
"Local State (Total)": "Loka Stato (Tuta)",
|
||||
"Log": "Log",
|
||||
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Log tailing paused. Scroll to bottom continue.",
|
||||
"Logs": "Logs",
|
||||
"Major Upgrade": "Ĉefa Ĝisdatigo",
|
||||
"Mass actions": "Mass actions",
|
||||
"Master": "Ĉefa",
|
||||
"Maximum Age": "Maksimuma Aĝo",
|
||||
"Metadata Only": "Nur Metadatumoj",
|
||||
"Minimum Free Disk Space": "Minimuma Libera Diskospaco",
|
||||
"Mod. Device": "Mod. Device",
|
||||
"Mod. Time": "Mod. Time",
|
||||
"Move to top of queue": "Movi supren en la atendovico",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Multi nivelo ĵokero (egalas multoblajn dosierujaj niveloj)",
|
||||
"Never": "Neniam",
|
||||
"New Device": "Nova Aparato",
|
||||
"New Folder": "Dova Dosierujo",
|
||||
"Newest First": "Plejnova Unue",
|
||||
"No": "Ne",
|
||||
"No File Versioning": "Sen Dosiera Versionado",
|
||||
"No files will be deleted as a result of this operation.": "No files will be deleted as a result of this operation.",
|
||||
"No upgrades": "Sen ĝisdatigoj",
|
||||
"Normal": "Normala",
|
||||
"Notice": "Avizo",
|
||||
"OK": "Bone",
|
||||
"Off": "For",
|
||||
"Oldest First": "Malnova Unue",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Laŭvola priskriba etikedo por la dosierujo. Povas esti malsama en ĉiu aparato.",
|
||||
"Options": "Opcioj",
|
||||
"Out of Sync": "Elsinkronigita",
|
||||
"Out of Sync Items": "Elsinkronigitaj Eroj",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limo de Eliranta Rapideco (KiB/s)",
|
||||
"Override Changes": "Transpasi Ŝanĝojn",
|
||||
"Path": "Vojo",
|
||||
"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": "Vojo de la dosierujo en la loka komputilo. Kreiĝos se ne ekzistas. La tilda signo (~) povas esti uzata kiel mallongigilo por",
|
||||
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {{tilde}}.",
|
||||
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Vojo kies versioj devus esti stokitaj (lasi malplena por la defaŭlta .stversiaj dosierujoj en la dividita dosierujo).",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Vojo kies vesioj estas konservitaj (lasi malplena por la defaŭlta .stversiaj dosierujoj en la dosierujo).",
|
||||
"Pause": "Paŭzu",
|
||||
"Pause All": "Paŭzu Ĉion",
|
||||
"Paused": "Paŭzita",
|
||||
"Pending changes": "Pending changes",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Periodic scanning at given interval and disabled watching for changes",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Periodic scanning at given interval and enabled watching for changes",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:",
|
||||
"Permissions": "Permissions",
|
||||
"Please consult the release notes before performing a major upgrade.": "Bonvolu konsulti la eldonitajn notojn antaŭ elfari ĉefan ĝisdatigon.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Bonvolu agordi GUI Authentication Uzanto kaj Pasvorto en la agordoj dialogo.",
|
||||
"Please wait": "Bonvolu atendi",
|
||||
"Prefix indicating that the file can be deleted if preventing directory removal": "Prefix indicating that the file can be deleted if preventing directory removal",
|
||||
"Prefix indicating that the pattern should be matched without case sensitivity": "Prefix indicating that the pattern should be matched without case sensitivity",
|
||||
"Preview": "Antaŭrigardo",
|
||||
"Preview Usage Report": "Antaŭrigardo Uzada Raporto",
|
||||
"Quick guide to supported patterns": "Rapida gvidisto al subtenata ŝablonoj",
|
||||
"RAM Utilization": "RAM Utiligado",
|
||||
"Random": "Hazarda",
|
||||
"Receive Only": "Receive Only",
|
||||
"Recent Changes": "Recent Changes",
|
||||
"Reduced by ignore patterns": "Reduktita per ignorado de la ŝablonoj",
|
||||
"Release Notes": "Eldonitaj Notoj",
|
||||
"Release candidates contain the latest features and fixes. They are similar to the traditional bi-weekly Syncthing releases.": "Kandidataj eldonoj enhavas la lastajn trajtojn kaj korektojn. Ili estas similaj al la tradiciaj dusemajnaj Syncthing ĵetoj.",
|
||||
"Remote Devices": "Foraj Aparatoj",
|
||||
"Remove": "Forigu",
|
||||
"Remove Device": "Remove Device",
|
||||
"Remove Folder": "Remove Folder",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Nepra identigilo por la dosierujo. Devas esti la sama en ĉiuj aparatoj de la grupo.",
|
||||
"Rescan": "Reskanu",
|
||||
"Rescan All": "Reskanu Ĉion",
|
||||
"Rescan Interval": "Reskana Intervalo",
|
||||
"Rescans": "Rescans",
|
||||
"Restart": "Restartu",
|
||||
"Restart Needed": "Restarto Bezonata",
|
||||
"Restarting": "Restartado",
|
||||
"Restore": "Restore",
|
||||
"Restore Versions": "Restore Versions",
|
||||
"Resume": "Daŭrigu",
|
||||
"Resume All": "Daŭrigu Ĉion",
|
||||
"Reused": "Reuzita",
|
||||
"Revert Local Changes": "Revert Local Changes",
|
||||
"Running": "Running",
|
||||
"Save": "Konservu",
|
||||
"Scan Time Remaining": "Skanada Restanta Tempo",
|
||||
"Scanning": "Skanado",
|
||||
"See external versioner help for supported templated command line parameters.": "See external versioner help for supported templated command line parameters.",
|
||||
"See external versioning help for supported templated command line parameters.": "See external versioning help for supported templated command line parameters.",
|
||||
"Select a version": "Select a version",
|
||||
"Select latest version": "Select latest version",
|
||||
"Select oldest version": "Select oldest version",
|
||||
"Select the devices to share this folder with.": "Elekti la aparatojn por komunigi ĉi tiun dosierujon.",
|
||||
"Select the folders to share with this device.": "Elekti la dosierujojn por komunigi kun ĉi tiu aparato.",
|
||||
"Send & Receive": "Sendi kaj Ricevi",
|
||||
"Send Only": "Nur Sendi",
|
||||
"Settings": "Agordoj",
|
||||
"Share": "Komunigu",
|
||||
"Share Folder": "Komunigu Dosierujon",
|
||||
"Share Folders With Device": "Dosierujoj Komunigitaj Kun Aparato",
|
||||
"Share With Devices": "Komunigu Kun Aparatoj",
|
||||
"Share this folder?": "Komunigi ĉi tiun dosierujon?",
|
||||
"Shared With": "Komunigita Kun",
|
||||
"Sharing": "Sharing",
|
||||
"Show ID": "Montru ID",
|
||||
"Show QR": "Montru QR",
|
||||
"Show diff with previous version": "Show diff with previous version",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Montrita anstataŭ ID de Aparato en la statuso de la grupo. Estos anoncita al aliaj aparatoj kiel laŭvola defaŭlta nomo.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Montri anstataŭ ID de Aparato en la statuso de la grupo. Estos ĝisdatigita al la nomo de la aparato sciigante se ĝi estas lasita malplena.",
|
||||
"Shutdown": "Sistemfermu",
|
||||
"Shutdown Complete": "Sistemfermu tute",
|
||||
"Simple File Versioning": "Simpla Versionado de Dosieroj",
|
||||
"Single level wildcard (matches within a directory only)": "Ununura nivelo ĵokero (egalas nur ene de dosierujo)",
|
||||
"Size": "Size",
|
||||
"Smallest First": "Plej Malgranda Unue",
|
||||
"Some items could not be restored:": "Some items could not be restored:",
|
||||
"Source Code": "Fontkodo",
|
||||
"Stable releases and release candidates": "Stabilaj eldonoj kaj kandidataj eldonoj",
|
||||
"Stable releases are delayed by about two weeks. During this time they go through testing as release candidates.": "Stabilaj eldonoj prokrastas je ĉirkaŭ du semjanoj. Dum tiu tempo ili estos testataj kiel kandidataj eldonoj.",
|
||||
"Stable releases only": "Nur stabilaj eldonoj",
|
||||
"Staggered File Versioning": "Gradigitaj Dosiera versionado",
|
||||
"Start Browser": "Startu Retumilon",
|
||||
"Statistics": "Statistikoj",
|
||||
"Stopped": "Haltita",
|
||||
"Support": "Subteno",
|
||||
"Sync Protocol Listen Addresses": "Sync Protokolo de Aŭskultado de Adresoj",
|
||||
"Syncing": "Elsinkronado",
|
||||
"Syncthing has been shut down.": "Syncthing estis malŝaltita.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing inkluzivas la jenajn programarojn aŭ porciojn ĝiajn:",
|
||||
"Syncthing is restarting.": "Syncthing estas restartanta.",
|
||||
"Syncthing is upgrading.": "Syncthing estas ĝisdatigita.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ŝajnas nefunkcii, aŭ estas problemo kun via retkonekto. Reprovado...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ŝajnas renkonti problemon kun la traktado de via peto. Bonvolu refreŝigi la paĝon aŭ restarti Syncthing se la problemo daŭras.",
|
||||
"Take me back": "Take me back",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "La administra interfaco de Syncthing estas agordita por permesi foran atingon sen pasvorto.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "La agregita statistikoj estas publike disponebla ĉe la URL malsupre.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La agordo estis registrita sed ne aktivigita. Syncthing devas restarti por aktivigi la novan agordon.",
|
||||
"The device ID cannot be blank.": "La aparato ID ne povas esti malplena.",
|
||||
"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).": "La aparato ID por eniri ĉi tie estas trovebla per \"Agoj > Montru ID\" dialogo en la alia aparato. Interspacoj kaj streketoj estas opcio (ignorigita).",
|
||||
"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.": "La ĉifrita raporto de uzado estas sendata ĉiutage. Ĝi estas uzata por sekvi komunajn platformojn, dosierujajn grandojn kaj aplikaĵajn versiojn. Se la raporto datumaro ŝanĝis, vi estos avertata per ĉi tiu dialogo denove.",
|
||||
"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.": "La enigita aparato ID ne ŝajnas valida. Ĝi devas esti signoĉeno el 52 aŭ 56 karaktroj longa enhavanta leterojn kaj nombrojn, kun interspacoj kaj streketoj opciaj.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "La unua komandlinia parametro estas la vojo de la dosierujo kaj la dua parametro estas la relativa vojo en la dosierujo.",
|
||||
"The folder ID cannot be blank.": "La dosierujo ID ne povas esti malplena.",
|
||||
"The folder ID must be unique.": "La dosierujo ID devas esti unika.",
|
||||
"The folder path cannot be blank.": "La vojo de la dosierujo ne povas esti malplena.",
|
||||
"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.": "La jenaj intervaloj estas uzataj: dum la unua horo version restas dum ĉiuj 30 sekundoj, dum la unua tago versio restas konservita dum ĉiu horo, dum la unuaj 30 tagoj versio estas konservita dum ĉiu tago, ĝis la maksimume aĝa versio restas konservita dum ĉiu semajno.",
|
||||
"The following items could not be synchronized.": "La sekvantaj elementoj ne povas esti sinkronigataj.",
|
||||
"The maximum age must be a number and cannot be blank.": "La maksimuma aĝo devas esti nombro kaj ne povas esti malplena.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "La maksimuma tempo por konservi version (en tagoj, agordi je 0 por konservi versiojn eterne).",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "La minimuma libera procento de diskospaco devas esti pozitiva nombro inter 0 kaj 100 (inkluziva).",
|
||||
"The number of days must be a number and cannot be blank.": "La nombro da tagoj devas esti nombro kaj ne povas esti malplena.",
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "La nombro da tagoj por konservi dosierojn en la rubujo. Nulo signifas eterne.",
|
||||
"The number of old versions to keep, per file.": "La nombro da malnovaj versioj por konservi, po ĉiu dosiero.",
|
||||
"The number of versions must be a number and cannot be blank.": "La nombro da versioj devas esti nombro kaj ne povas esti malplena.",
|
||||
"The path cannot be blank.": "La vojo ne povas esti malplena.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "La rapideca limo devas esti pozitiva nombro (0: senlimo)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "La intervalo de reskano devas esti pozitiva nombro da sekundoj.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Ili estas reprovitaj aŭtomate kaj estos sinkronigitaj kiam la eraro estas solvita.",
|
||||
"This Device": "Ĉi tiu Aparato",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Ĉi tio povas facile doni al kodumuloj atingon por legi kaj ŝanĝi ajnajn dosierojn en via komputilo.",
|
||||
"This is a major version upgrade.": "Ĉi tio estas ĉefversio ĝisdatigita.",
|
||||
"This setting controls the free space required on the home (i.e., index database) disk.": "This setting controls the free space required on the home (i.e., index database) disk.",
|
||||
"Time": "Tempo",
|
||||
"Time the item was last modified": "Time the item was last modified",
|
||||
"Trash Can File Versioning": "Rubujo ebligas Dosieran Versionadon",
|
||||
"Type": "Tipo",
|
||||
"Unavailable": "Unavailable",
|
||||
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
|
||||
"Undecided (will prompt)": "Undecided (will prompt)",
|
||||
"Unignore": "Unignore",
|
||||
"Unknown": "Nekonata",
|
||||
"Unshared": "Nekomunigita",
|
||||
"Unused": "Neuzita",
|
||||
"Up to Date": "Ĝisdata",
|
||||
"Updated": "Ĝisdatigita",
|
||||
"Upgrade": "Altgradigo",
|
||||
"Upgrade To {%version%}": "Altgradigi Al {{version}}",
|
||||
"Upgrading": "Altgradigata",
|
||||
"Upload Rate": "Alŝutorapideco",
|
||||
"Uptime": "Daŭro de funkciado",
|
||||
"Usage reporting is always enabled for candidate releases.": "Uzada raportado ĉiam ŝaltita por kandidataj ĵetoj.",
|
||||
"Use HTTPS for GUI": "Uzi HTTPS por grafika interfaco.",
|
||||
"Version": "Versio",
|
||||
"Versions": "Versions",
|
||||
"Versions Path": "Vojo de Versioj",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioj estas aŭtomate forigita se ili estas pli malnovaj ol la maksimuma aĝo aŭ superas la nombron da dosieroj permesita en intervalo.",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolder%}\".": "Averto, ĉi tiu vojo estas parenta dosierujo de ekzistanta dosierujo \"{{otherFolder}}\".",
|
||||
"Warning, this path is a parent directory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Averto, ĉi tiu vojo estas parenta dosierujo de ekzistanta dosierujo \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Averto, ĉi tiu vojo estas subdosierujo de ekzistanta dosierujo \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolderLabel%}\" ({%otherFolder%}).": "Averto, ĉi tiu vojo estas subdosierujo de ekzistanta dosierujo \"{{otherFolderLabel}}\" ({{otherFolder}}).",
|
||||
"Warning: If you are using an external watcher like {%syncthingInotify%}, you should make sure it is deactivated.": "Warning: If you are using an external watcher like {{syncthingInotify}}, you should make sure it is deactivated.",
|
||||
"Watch for Changes": "Watch for Changes",
|
||||
"Watching for Changes": "Watching for Changes",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Dum la aldonado de nova aparato, memoru ke ĉi tiu aparato devas esti aldonita en la alia flanko ankaŭ.",
|
||||
"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.": "Dum la aldonado de nova dosierujo, memoru ke la Dosieruja ID estas uzita por ligi la dosierujojn kune inter aparatoj. Ili estas literfakodistingaj kaj devas kongrui precize inter ĉiuj aparatoj.",
|
||||
"Yes": "Jes",
|
||||
"You can also select one of these nearby devices:": "You can also select one of these nearby devices:",
|
||||
"You can change your choice at any time in the Settings dialog.": "Vi povas ŝanĝi vian elekton iam ajn en la Agorda dialogo.",
|
||||
"You can read more about the two release channels at the link below.": "Vi povas legi plu pri la du eldonkanaloj per la malsupra ligilo.",
|
||||
"You have no ignored devices.": "You have no ignored devices.",
|
||||
"You have no ignored folders.": "You have no ignored folders.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
|
||||
"You must keep at least one version.": "Vi devas konservi almenaŭ unu version.",
|
||||
"days": "tagoj",
|
||||
"directories": "dosierujoj",
|
||||
"files": "dosieroj",
|
||||
"full documentation": "tuta dokumentado",
|
||||
"items": "elementoj",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} volas komunigi dosierujon \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} volas komunigi dosierujon \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Estadísticas",
|
||||
"Stopped": "Detenido",
|
||||
"Support": "Forum",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Direcciones de escucha del protocolo de sincronización",
|
||||
"Syncing": "Sincronizando",
|
||||
"Syncthing has been shut down.": "Syncthing se ha detenido.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece no estar activo o hay un problema con tu conexión de internet. Reintentando...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing tiene problemas para procesar tu solicitud. Por favor, actualiza la página o reinicia Syncthing si el problema persiste.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "El panel de administración de Syncthing está configurado para permitir el acceso remoto sin contraseña.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Las estadísticas agragadas están disponibles públicamente en la URL de abajo.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido grabada pero no activada. Syncthing debe reiniciarse para activar la nueva configuración.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Estadísticas",
|
||||
"Stopped": "Detenido",
|
||||
"Support": "Forum",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Direcciones de escucha del protocolo de sincronización",
|
||||
"Syncing": "Sincronizando",
|
||||
"Syncthing has been shut down.": "Syncthing se ha detenido.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing parece no estar activo o hay un problema con tu conexión de internet. Reintentando...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing tiene problemas para procesar tu solicitud. Por favor, actualiza la página o reinicia Syncthing si el problema persiste.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "El panel de administración de Syncthing está configurado para permitir el acceso remoto sin contraseña.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Las estadísticas agragadas están disponibles públicamente en la URL de abajo.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuración ha sido grabada pero no activada. Syncthing debe reiniciarse para activar la nueva configuración.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Estatistikak",
|
||||
"Stopped": "Gelditua!",
|
||||
"Support": "Foroa",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Sinkronizatu protokoloaren entzun zuzenbideak",
|
||||
"Syncing": "Sinkronizazioa martxan",
|
||||
"Syncthing has been shut down.": "Syncthing gelditua izan da",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Iduri luke Syncthing gelditua dela, edo bestenaz arrazo bat bada interneten konekzioarekin. Berriz entsea zaitez…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Iduri luke Syncthing-ek arazo bat duela zure eskaera tratatzeko. Otoi, orrialdea freska ezazu edo bestenaz, arazoak segitzen badu, Syncthing berriz pitz ezazu .",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing-en administrazio interfazea pentsatua da urrundikako helbideak pasahitzik gabe onartzeko !",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Estadistikak zuzen bide honetan publikoki ikusgarriak dira",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurazioa grabatua izan da bainan ez aktibatua. Syncthing berriz piztu behar da konfigurazio berria berriz aktibatzeko.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Tilastot",
|
||||
"Stopped": "Pysäytetty",
|
||||
"Support": "Tuki",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Synkronointiprotokollan kuunteluosoite",
|
||||
"Syncing": "Synkronoidaan",
|
||||
"Syncthing has been shut down.": "Syncthing on sammutettu.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing näyttää olevan alhaalla tai internetyhteydessä on ongelma. Yritetään uudelleen...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing ei pysty käsittelemään pyyntöäsi. Ole hyvä ja päivitä sivu tai käynnistä Syncthing uudelleen, jos ongelma jatkuu.",
|
||||
"Take me back": "Takaisin",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthingin hallintakäyttöliittymä on asetettu sallimaan ulkoiset yhteydet ilman salasanaa.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Koostetut tilastot ovat julkisesti saatavilla alla olevassa osoitteessa.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Asetukset on tallennettu, mutta niitä ei ole otettu käyttöön. Syncthingin täytyy käynnistyä uudelleen, jotta uudet asetukset saadaan käyttöön.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistiques",
|
||||
"Stopped": "Arrêté",
|
||||
"Support": "Forum",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Adresses d'écoute du protocole de synchronisation",
|
||||
"Syncing": "En cours de synchronisation",
|
||||
"Syncthing has been shut down.": "Syncthing a été arrêté.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être arrêté, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing semble avoir un problème pour traiter votre demande. S'il vous plaît, rafraîchissez la page ou redémarrez Syncthing si le problème persiste.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interface d'administration de Syncthing est configuré pour accepter l'accès distant sans mot de passe !",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Les statistiques agrégées sont publiquement disponibles à l'adresse ci-dessous.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été enregistrée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
|
||||
|
||||
@@ -110,7 +110,7 @@
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Quand ils sont remplacés ou supprimés par Syncthing, les fichiers sont déplacés et horodatés vers le sous-répertoire .stversions dans une arborescence relative identique à celle de l'original.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Quand ils sont remplacés ou supprimés par Syncthing, les fichiers sont déplacés et horodatés vers le sous-répertoire .stversions dans une arborescence relative identique à celle de l'original.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Les fichiers sont protégés des changements réalisés sur les autres appareils, mais les changements réalisés sur celui-ci seront transférés aux autres.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Les fichiers sont synchronisés à partir du cluster, mais les modifications apportées localement ne seront pas envoyées aux autres appareils.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Les fichiers sont synchronisés à partir des autres participants, mais les modifications apportées localement ne leur seront pas envoyées.",
|
||||
"Filesystem Notifications": "Notifications du système de fichiers",
|
||||
"Filesystem Watcher Errors": "Erreurs de la surveillance du système de fichiers",
|
||||
"Filter by date": "Filtrer par date",
|
||||
@@ -164,7 +164,7 @@
|
||||
"Local State (Total)": "État local (Total)",
|
||||
"Log": "Journal",
|
||||
"Log tailing paused. Click here to continue.": "La file d'attente du journal est en pause. Cliquer ici pour continuer.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Le fil du journal est en pause. Faites défiler vers le bas pour continuer.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Le défilement du journal est en pause. Faites défiler vers le bas pour continuer.",
|
||||
"Logs": "Journaux",
|
||||
"Major Upgrade": "Mise à jour majeure",
|
||||
"Mass actions": "Actions multiples",
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistiques",
|
||||
"Stopped": "Arrêté",
|
||||
"Support": "Forum",
|
||||
"Support Bundle": "Kit d'assistance",
|
||||
"Sync Protocol Listen Addresses": "Adresses d'écoute du protocole de synchronisation",
|
||||
"Syncing": "Synchronisation en cours",
|
||||
"Syncthing has been shut down.": "Syncthing a été arrêté.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing semble être arrêté, ou il y a un problème avec votre connexion Internet. Nouvelle tentative ...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing semble avoir un problème pour traiter votre demande. S'il vous plaît, rafraîchissez la page ou redémarrez Syncthing si le problème persiste.",
|
||||
"Take me back": "Reprends moi",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "L'adresse de l'interface graphique est remplacée par une ou des options de lancement. Les modifications apportées ici ne seront pas effectives tant que ces options seront utilisées.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interface d'administration de Syncthing est paramétrée pour autoriser les accès à distance sans mot de passe !!!",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Les statistiques agrégées sont disponibles publiquement à l'adresse ci-dessous.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configuration a été enregistrée mais pas activée. Syncthing doit redémarrer afin d'activer la nouvelle configuration.",
|
||||
@@ -359,7 +361,7 @@
|
||||
"You can read more about the two release channels at the link below.": "Vous pouvez en savoir plus sur les deux canaux de distribution via le lien ci-dessous.",
|
||||
"You have no ignored devices.": "Vous n'avez aucun appareil ignoré.",
|
||||
"You have no ignored folders.": "Vous n'avez aucun partage ignoré.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Vous avez des changements non enregistrés. Voulez-vous vraiment les ignorer ?",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Vous avez des réglages non enregistrés. Voulez-vous vraiment les ignorer ?",
|
||||
"You must keep at least one version.": "Vous devez garder au minimum une version.",
|
||||
"days": "Jours",
|
||||
"directories": "répertoires",
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"Disabled periodic scanning and disabled watching for changes": "Periodic scanning útskeakele en feroarings wurde net mear yn'e gaten hâlden.",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Periodic scanning útskeakele en feroarings wurde yn'e gaten hâlden.",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Periodic scanning útskeakele en it ynskeakeljen fan it yn'e gaten hâlden fan feroarings is mislearre, wurd eltse 1m opnij besocht:",
|
||||
"Discard": "Discard",
|
||||
"Discard": "Fuortsmite\n",
|
||||
"Disconnected": "Ferbining ferbrutsen",
|
||||
"Discovered": "Untdekt",
|
||||
"Discovery": "Untdekking",
|
||||
@@ -110,7 +110,7 @@
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Triemen wurde ferset nei mei datum stimpele ferzjes yn in .stversions map wannear troch Syncthing ferfangen of fuortsmiten.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Triemen wurde ferset nei in mei datum stimpele ferzjes yn in .stversions map wannear troch Syncthing ferfangen of fuortsmiten.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Triemen binne ymmún foar feroarings makke troch oare apparaten, mar feroarings makke op dit apparaat wurde nei de rest fan 'e bondel ferstjoerd.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Triemen binne syngronisearre fanôf de kluster, mar feroarings dy't lokaal makke binne, wurde net nei oare apparaten ferstjoert.",
|
||||
"Filesystem Notifications": "Meldingen fan Triemsysteem",
|
||||
"Filesystem Watcher Errors": "Triemsysteemsjoggerflaters",
|
||||
"Filter by date": "Op datum filterje",
|
||||
@@ -140,9 +140,9 @@
|
||||
"Ignore": "Negearje",
|
||||
"Ignore Patterns": "Negear-patroanen",
|
||||
"Ignore Permissions": "Negear-rjochten",
|
||||
"Ignored Devices": "Ignored Devices",
|
||||
"Ignored Folders": "Ignored Folders",
|
||||
"Ignored at": "Ignored at",
|
||||
"Ignored Devices": "Negearde Apparaten",
|
||||
"Ignored Folders": "Negearde Mappen",
|
||||
"Ignored at": "Negeard op ",
|
||||
"Incoming Rate Limit (KiB/s)": "Downloadfluggenslimyt (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Ferkearde konfiguraasje kin de ynhâld fan jo mappen skeine en Syncthing unbrûkber meitsje.",
|
||||
"Introduced By": "Yntrodusearre Troch",
|
||||
@@ -203,7 +203,7 @@
|
||||
"Pause": "Skoftsje",
|
||||
"Pause All": "Alles skoftsje",
|
||||
"Paused": "Skoftet",
|
||||
"Pending changes": "Pending changes",
|
||||
"Pending changes": "Wachtet op feroarings",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Periodic scanning op opjûn ynterfal en feroarings wurde net yn'e gaten hâlden.",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Periodic scanning op opjûn ynterfal en feroarings wurde yn'e gaten hâlden.",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Periodic scanning op opjûn ynterfal en it ynskeakeljen fan it yn'e gaten hâlden fan feroarings is mislearre, wurd eltse 1m opnij besocht:",
|
||||
@@ -218,7 +218,7 @@
|
||||
"Quick guide to supported patterns": "Fluch-paadwizer foar stipe patroanen",
|
||||
"RAM Utilization": "RAM-brûken",
|
||||
"Random": "Willekeurich",
|
||||
"Receive Only": "Receive Only",
|
||||
"Receive Only": "Allinnich Untfange",
|
||||
"Recent Changes": "Resinte Feroarings",
|
||||
"Reduced by ignore patterns": "Ferlytse troch negear-patroanen",
|
||||
"Release Notes": "Utjeftenotysjes",
|
||||
@@ -240,7 +240,7 @@
|
||||
"Resume": "Trochgean",
|
||||
"Resume All": "Alles trochgean litte",
|
||||
"Reused": "Opnij brûkt",
|
||||
"Revert Local Changes": "Revert Local Changes",
|
||||
"Revert Local Changes": "Lokale Feroarings Weromsette",
|
||||
"Running": "Rint",
|
||||
"Save": "Bewarje",
|
||||
"Scan Time Remaining": "Oerbleaune skentiid",
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistiken",
|
||||
"Stopped": "Stoppe",
|
||||
"Support": "Help (Forum)",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Sync-protokolharkadressen",
|
||||
"Syncing": "Oan it Syncen",
|
||||
"Syncthing has been shut down.": "Syncthing is útsetten",
|
||||
@@ -291,7 +292,8 @@
|
||||
"Syncthing is upgrading.": "Syncthing is oan it fernijen.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "It liket dêrop dat Syncthing op dit stuit net rint, of der is in swierrichheid mei jo ynternetferbining. Wurd no opnij besocht...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "It liket dêrop dat Syncthing swierrichheden ûnderfynt mei it ferwurkjen fan jo fersyk. Graach de stee ferfarskje of Syncthing werstarte as it probleem der bliuwt.",
|
||||
"Take me back": "Take me back",
|
||||
"Take me back": "Bring my werom",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "De Syncthing haadbrûker-ynterfaasje is sa ynstelt dat tagong fan ôfstân sûnder wachtwurd tastean is.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "De fersammele statistiken binnen yn it publyk beskikber fia ûndersteande keppeling.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "De konfiguraasje is bewarre mar noch net aktivearre. Syncthing moat werstarte om de nije konfiguraasje te aktivearren.",
|
||||
@@ -327,7 +329,7 @@
|
||||
"Unavailable": "Net beskikber",
|
||||
"Unavailable/Disabled by administrator or maintainer": "Net beskikber/Utsetten troch administrator of ûnderhâlder",
|
||||
"Undecided (will prompt)": "Noch net beslist (wurd noch frege)",
|
||||
"Unignore": "Unignore",
|
||||
"Unignore": "Net mear negeare",
|
||||
"Unknown": "Unbekend",
|
||||
"Unshared": "Net dielt",
|
||||
"Unused": "Net brûkt",
|
||||
@@ -357,9 +359,9 @@
|
||||
"You can also select one of these nearby devices:": "Jo kinne ek ien fan dizze tichtbye apparaten selektearje:",
|
||||
"You can change your choice at any time in the Settings dialog.": "Jo kinne jo kar op elk stuit oanpasse yn it Ynstellingsdialooch.",
|
||||
"You can read more about the two release channels at the link below.": "Jo kinne mear lêze oer de twa útjeftekanalen fia de ûndersteande link.",
|
||||
"You have no ignored devices.": "You have no ignored devices.",
|
||||
"You have no ignored folders.": "You have no ignored folders.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
|
||||
"You have no ignored devices.": "Jo hawwe gjin negearde apparaten.",
|
||||
"You have no ignored folders.": "Jo hawwe gjin negearde mappen.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Jo hawwe noch net-opsleine feroarings. Wolle jo dizze echt fuortsmite?",
|
||||
"You must keep at least one version.": "Jo moatte minstens ien ferzje bewarje.",
|
||||
"days": "dagen",
|
||||
"directories": "triemtafels",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statisztika",
|
||||
"Stopped": "Leállítva",
|
||||
"Support": "Támogatás",
|
||||
"Support Bundle": "Támogatási csomag",
|
||||
"Sync Protocol Listen Addresses": "Szinkronizációs protokoll címe",
|
||||
"Syncing": "Szinkronizálás",
|
||||
"Syncthing has been shut down.": "Syncthing leállítva",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Úgy tűnik, hogy a Syncthing nem működik, vagy valami probléma van a hálózati kapcsolattal. Újra próbálom...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Úgy tűnik, hogy a Syncthing problémába ütközött a kérés feldolgozása során. Ha a probléma továbbra is fennáll, akkor frissíteni kell az oldalt, vagy újra kell indítani a Syncthinget.",
|
||||
"Take me back": "Vissza",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "A grafikus felület címét egy indítási beállítás felülírta. Az itt történő módosítás hatástalan marad, amíg ez a felülírás érvényben van.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "A Syncthing adminisztrációs felületének távoli elérése be van kapcsolva jelszó nélkül.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Az összesített statisztikák elérhetők az alábbi címen.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A beállítások elmentésre kerültek, de nem lettek aktiválva. Újra kell indítani a Syncthing-et az aktiválásukhoz.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistiche",
|
||||
"Stopped": "Fermato",
|
||||
"Support": "Supporto",
|
||||
"Support Bundle": "Pacchetto di supporto",
|
||||
"Sync Protocol Listen Addresses": "Indirizzi del Protocollo di Sincronizzazione",
|
||||
"Syncing": "Sincronizzazione in corso",
|
||||
"Syncthing has been shut down.": "Syncthing è stato arrestato.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing sembra inattivo, oppure c'è un problema con la tua connessione a Internet. Nuovo tentativo…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Sembra che Syncthing abbia problemi nell'elaborazione della tua richiesta. Aggiorna la pagina o riavvia Syncthing se il problema persiste.",
|
||||
"Take me back": "Portami indietro",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "L'indirizzo della GUI è sovrascritto dalle opzioni di avvio. Le modifiche qui non avranno effetto finché queste opzioni sono impostate.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "L'interfaccia di amministrazione di Syncthing è configurata in modo da permettere l'accesso senza password.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Le statistiche aggregate sono disponibili pubblicamente all'URL seguente.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configurazione è stata salvata ma non attivata. Syncthing deve essere riavviato per attivare la nuova configurazione.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "統計情報",
|
||||
"Stopped": "停止中",
|
||||
"Support": "サポート",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "同期プロトコルの待ち受けアドレス",
|
||||
"Syncing": "同期中",
|
||||
"Syncthing has been shut down.": "Syncthingをシャットダウンしました。",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthingが落ちているか、インターネット接続に問題があります。リトライ中です…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "リクエストの処理に問題があるようです。問題が継続する場合、ページを更新するかSyncthingを再起動してください。",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthingの管理画面が、パスワードなしで外部からアクセスできるように設定されています。",
|
||||
"The aggregated statistics are publicly available at the URL below.": "集計結果は以下のURLで公開されています。",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "設定が保存されましたが、まだ有効になっていません。新しい設定を有効にするにはSyncthingを再起動してください。",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "통계",
|
||||
"Stopped": "중지됨",
|
||||
"Support": "지원",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "동기화 프로토콜 수신 주소",
|
||||
"Syncing": "동기화 중",
|
||||
"Syncthing has been shut down.": "Syncthing이 종료되었습니다.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing이 중지되었거나 인터넷 연결에 문제가 있는 것 같습니다. 재시도 중입니다...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing에서 요청을 처리하는 중에 문제가 발생했습니다. 계속 문제가 발생하면 페이지를 다시 불러오거나 Syncthing을 재시작해 보세요.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing 관리자 인터페이스가 암호 없이 원격 접속이 허가되도록 설정되었습니다.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "수집된 통계는 아래 URL에서 공개적으로 볼 수 있습니다.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "설정이 저장되었지만 활성화되지 않았습니다. 설정을 활성화 하려면 Syncthing을 다시 시작하세요.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistika",
|
||||
"Stopped": "Sustabdyta",
|
||||
"Support": "Pagalba",
|
||||
"Support Bundle": "Palaikymo paketas",
|
||||
"Sync Protocol Listen Addresses": "Sutapatinimo taisyklių adresas",
|
||||
"Syncing": "Sutapatinama",
|
||||
"Syncthing has been shut down.": "Syncthing išjungtas",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing išjungta arba problemos su Interneto ryšių. Bandoma iš naujo...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Atrodo, kad Syncthing, vykdydamas jūsų užklausą, susidūrė su problemomis. Prašome iš naujo įkelti puslapį, arba jei problema išlieka, iš naujo paleisti Syncthing.",
|
||||
"Take me back": "Sugrąžinkite mane",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Valdymo skydelio adresas yra nustelbiamas paleidimo parametrų. Čia esantys pakeitimai neįsigalios tol, kol yra nustelbimas.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing administratoriaus sąsaja yra sukonfigūruota taip, kad be slaptažodžio leistų nuotolinę prieigą.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Naudojimosi ataskaitą galite peržiūrėti žemiau nurodytu URL adresu.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Nauji nustatymai išsaugoti, bet neaktyvuoti. Perleiskite Syncthing programą iš naujo norėdami įgalinti naujus nustatymus.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistikk",
|
||||
"Stopped": "Stoppet",
|
||||
"Support": "Brukerstøtte",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Lytteadresse for synkroniseringsprotokoll",
|
||||
"Syncing": "Synkroniserer",
|
||||
"Syncthing has been shut down.": "Syncthing har blitt slått av.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing ser ut til å være nede, eller så er det et problem med nettforbindelsen din. Prøver 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å et problem under behandling av din forespørsel. Gjenoppfrisk nettleseren eller start Syncthing på nytt dersom problemet vedvarer.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Grensesnittet for administrering av Syncthing er satt til å tillate ekstern tilgang uten et passord.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Innsamlet statistikk er åpent tilgjengelig via nettadressen angitt nedenfor.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Innstillingene har blitt lagret men ikke aktivert. Syncthing må starte på ny for å aktivere de nye innstillingene.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistieken",
|
||||
"Stopped": "Gestopt",
|
||||
"Support": "Ondersteuning",
|
||||
"Support Bundle": "Ondersteuningspakket",
|
||||
"Sync Protocol Listen Addresses": "Luisteradressen synchronisatieprotocol",
|
||||
"Syncing": "Synchroniseren",
|
||||
"Syncthing has been shut down.": "Syncthing werd afgesloten.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing lijkt gestopt te zijn, of er is een probleem met uw internetverbinding. Opnieuw proberen...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing lijkt een probleem te ondervinden met het verwerken van uw verzoek. Vernieuw de pagina of start Syncthing opnieuw als het probleem zich blijft voordoen. ",
|
||||
"Take me back": "Neem me terug",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Het GUI-adres wordt overschreven door opstart-opties. Wijzigingen hier zullen geen effect hebben terwijl de overschrijving van kracht is.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "De beheerdersinterface van Syncthing is ingesteld om externe toegang zonder wachtwoord toe te staan.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "De verzamelde statistieken zijn publiek beschikbaar op de onderstaande URL.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "De configuratie is opslagen maar nog niet ingeschakeld. Syncthing moet opnieuw gestart worden om de nieuwe configuratie in te schakelen.",
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
"Disabled periodic scanning and disabled watching for changes": "Wyłączono okresowe skanowanie i wyłączono obserwowanie zmian",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Wyłączono okresowe skanowanie i włączono obserwowanie zmian",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Wyłączono okresowe skanowanie i nie udało się skonfigurować obserwowania zmian, powtórzę co minutę:",
|
||||
"Discard": "Discard",
|
||||
"Discard": "Odrzuć",
|
||||
"Disconnected": "Rozłączony",
|
||||
"Discovered": "Odkryte",
|
||||
"Discovery": "Odnajdywanie",
|
||||
@@ -140,8 +140,8 @@
|
||||
"Ignore": "Ignoruj",
|
||||
"Ignore Patterns": "Wzorce ignorowania",
|
||||
"Ignore Permissions": "Ignoruj uprawnienia",
|
||||
"Ignored Devices": "Ignored Devices",
|
||||
"Ignored Folders": "Ignored Folders",
|
||||
"Ignored Devices": "Ignorowane urządzenia",
|
||||
"Ignored Folders": "Ignorowane katalogi",
|
||||
"Ignored at": "Ignored at",
|
||||
"Incoming Rate Limit (KiB/s)": "Ograniczenie prędkości odbierania (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Niepoprawna konfiguracja może uszkodzić zawartośc Twojego folderu i uczynić Syncthing niedziałającym.",
|
||||
@@ -203,7 +203,7 @@
|
||||
"Pause": "Zatrzymaj",
|
||||
"Pause All": "Zatrzymaj wszystkie",
|
||||
"Paused": "Zatrzymany",
|
||||
"Pending changes": "Pending changes",
|
||||
"Pending changes": "Oczekujące zmiany",
|
||||
"Periodic scanning at given interval and disabled watching for changes": "Okresowe skanowanie w podanym przedziale czasu i wyłączone obserwowanie zmian",
|
||||
"Periodic scanning at given interval and enabled watching for changes": "Okresowe skanowanie w podanym przedziale czasu i włączone obserwowanie zmian",
|
||||
"Periodic scanning at given interval and failed setting up watching for changes, retrying every 1m:": "Okresowe skanowanie w podanym przedziale czasu i nieudane konfigurowanie obserwowania zmian, ponowna próba co minutę:",
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statystyki",
|
||||
"Stopped": "Zatrzymany",
|
||||
"Support": "Wsparcie",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Adres nasłuchu protokołu synchronizacji",
|
||||
"Syncing": "Synchronizowanie",
|
||||
"Syncthing has been shut down.": "Syncthing został wyłączony",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing wydaje się być wyłączony lub jest problem z twoim połączeniem internetowym. Próbuje ponownie...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing nie może przetworzyć twojego zapytania. Proszę przeładuj stronę lub zrestartuj Syncthing, jeśli problem pozostanie.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Interfejs administracyjny Syncthing jest skonfigurowany w sposób pozwalający na zdalny dostęp bez hasła.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Zebrane statystyki są publicznie dostępne pod poniższym linkiem.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfiguracja została zapisana lecz nie jest aktywna. Syncthing musi zostać zrestartowany aby aktywować nową konfiguracje.",
|
||||
@@ -327,7 +329,7 @@
|
||||
"Unavailable": "Niedostępne",
|
||||
"Unavailable/Disabled by administrator or maintainer": "Niedostępne/Wyłączone przez administratora lub opiekuna",
|
||||
"Undecided (will prompt)": "Jeszcze nie zdecydowałem (przypomnij później)",
|
||||
"Unignore": "Unignore",
|
||||
"Unignore": "Wyłączyć ignorowanie",
|
||||
"Unknown": "Nieznany",
|
||||
"Unshared": "Nieudostępnione",
|
||||
"Unused": "Nieużywane",
|
||||
@@ -357,9 +359,9 @@
|
||||
"You can also select one of these nearby devices:": "Możesz również wybrać jedno z pobliskich urządzeń:",
|
||||
"You can change your choice at any time in the Settings dialog.": "Możesz zmienić swój wybór w dowolnej chwili w Ustawieniach",
|
||||
"You can read more about the two release channels at the link below.": "Możesz więcej poczytać na temat obydwu wydań klikając poniższy link.",
|
||||
"You have no ignored devices.": "You have no ignored devices.",
|
||||
"You have no ignored folders.": "You have no ignored folders.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
|
||||
"You have no ignored devices.": "Nie posiadasz zignorowanych urządzeń.",
|
||||
"You have no ignored folders.": "Nie posiadasz zignorowanych katalogów.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Masz niezapisane zmiany. Czy naprawdę chcesz je odrzucić?",
|
||||
"You must keep at least one version.": "Musisz posiadać przynajmniej jedną wersję",
|
||||
"days": "dni",
|
||||
"directories": "katalogi",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Estatísticas",
|
||||
"Stopped": "Parado",
|
||||
"Support": "Suporte",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Endereços de escuta do protocolo de sincronização",
|
||||
"Syncing": "Sincronizando",
|
||||
"Syncthing has been shut down.": "O Syncthing foi desligado.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Parece que o Syncthing está desligado ou há um problema com a sua conexão de internet. Tentando novamente...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Parece que o Syncthing está tendo problemas no processamento da requisição. Por favor, atualize a página ou reinicie o Syncthing caso o problema persista.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "A interface de administração do Syncthing está configurada para permitir acesso remoto sem uma senha.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "As estatísticas agregadas estão disponíveis no endereço abaixo.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A configuração foi salva mas ainda não foi ativada. O Syncthing precisa ser reiniciado para a ativação da nova configuração.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Estatísticas",
|
||||
"Stopped": "Parado",
|
||||
"Support": "Suporte",
|
||||
"Support Bundle": "Pacote de suporte",
|
||||
"Sync Protocol Listen Addresses": "Endereços de escuta do protocolo de sincronização",
|
||||
"Syncing": "A Sincronizar",
|
||||
"Syncthing has been shut down.": "O Syncthing foi desligado.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "O Syncthing parece estar em baixo, ou então existe um problema com a sua ligação à Internet. Tentando novamente...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "O Syncthing parece estar com problemas em processar o seu pedido. Tente recarregar a página ou reiniciar o Syncthing, se o problema persistir.",
|
||||
"Take me back": "Retornar",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "O endereço da interface gráfica é substituído pelas opções de arranque. Alterações feitas aqui não terão efeito enquanto a substituição estiver activa.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "A interface de administração do Syncthing está configurada para permitir o acesso remoto sem pedir senha.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "As estatísticas agregadas estão publicamente disponíveis no URL abaixo.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "A configuração foi gravada mas não activada. O Syncthing tem que reiniciar para activar a nova configuração.",
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
"Add Remote Device": "Добавить удалённое устройство",
|
||||
"Add devices from the introducer to our device list, for mutually shared folders.": "Add devices from the introducer to our device list, for mutually shared folders.",
|
||||
"Add new folder?": "Добавить новую папку?",
|
||||
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.",
|
||||
"Additionally the full rescan interval will be increased (times 60, i.e. new default of 1h). You can also configure it manually for every folder later after choosing No.": "Также будет увеличен интервал полного сканирования (в 60 раз, т.е. новое значение по умолчанию 1 час). Вы можете вручную настроить интервал для каждой папки, выбрав \"Нет\".",
|
||||
"Address": "Адрес",
|
||||
"Addresses": "Адреса",
|
||||
"Advanced": "Дополнительно",
|
||||
@@ -50,7 +50,7 @@
|
||||
"Connection Error": "Ошибка подключения",
|
||||
"Connection Type": "Тип соединения",
|
||||
"Connections": "Подключения",
|
||||
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.",
|
||||
"Continuously watching for changes is now available within Syncthing. This will detect changes on disk and issue a scan on only the modified paths. The benefits are that changes are propagated quicker and that less full scans are required.": "В Syncthing теперь доступно непрерывное отслеживание изменений на диске, что позволяет выполнять сканирование только изменившихся путей. Благодаря этому полное сканирование выполняется реже, а изменения распространяются быстрее.",
|
||||
"Copied from elsewhere": "Скопировано из другого места",
|
||||
"Copied from original": "Скопировано с оригинала",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Авторские права © 2014–2016 принадлежат:",
|
||||
@@ -69,8 +69,8 @@
|
||||
"Device that last modified the item": "Устройство, последним изменившее объект",
|
||||
"Devices": "Устройства",
|
||||
"Disabled": "Отключено",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Disabled periodic scanning and disabled watching for changes",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Disabled periodic scanning and enabled watching for changes",
|
||||
"Disabled periodic scanning and disabled watching for changes": "Периодическое сканирование и отслеживание изменений отключено",
|
||||
"Disabled periodic scanning and enabled watching for changes": "Периодическое сканирование отключено, отслеживание изменений включено",
|
||||
"Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:": "Disabled periodic scanning and failed setting up watching for changes, retrying every 1m:",
|
||||
"Discard": "Discard",
|
||||
"Disconnected": "Нет соединения",
|
||||
@@ -78,7 +78,7 @@
|
||||
"Discovery": "Обнаружение",
|
||||
"Discovery Failures": "Ошибки обнаружения",
|
||||
"Do not restore": "Не восстанавливать",
|
||||
"Do not restore all": "Do not restore all",
|
||||
"Do not restore all": "Не восстанавливать все",
|
||||
"Do you want to enable watching for changes for all your folders?": "Хотите включить слежение за изменениями для всех своих папок?",
|
||||
"Documentation": "Документация",
|
||||
"Download Rate": "Скорость загрузки",
|
||||
@@ -92,14 +92,14 @@
|
||||
"Enable NAT traversal": "Включить NAT traversal",
|
||||
"Enable Relaying": "Включить релеи",
|
||||
"Enabled": "Включено",
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.",
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Введите положительное значение (например, \"2.35\") и выберите единицу измерения, либо процент от общей ёмкости диска.",
|
||||
"Enter a non-privileged port number (1024 - 65535).": "Введите непривилегированный порт (1024—65535).",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Введите через запятую («tcp://ip:port», «tcp://host:port») адреса, либо «dynamic», чтобы выполнить автоматическое обнаружение адреса.",
|
||||
"Enter ignore patterns, one per line.": "Введите шаблоны игнорирования, по одному на строку.",
|
||||
"Error": "Ошибка",
|
||||
"External File Versioning": "Внешний контроль версий файлов",
|
||||
"Failed Items": "Сбои",
|
||||
"Failed to load ignore patterns": "Failed to load ignore patterns",
|
||||
"Failed to load ignore patterns": "Не удалось загрузить шаблоны игнорирования",
|
||||
"Failed to setup, retrying": "Не удалось настроить, пробуем ещё",
|
||||
"Failure to connect to IPv6 servers is expected if there is no IPv6 connectivity.": "Если нет IPv6-соединений, при подключении к IPv6-серверам произойдёт ошибка.",
|
||||
"File Pull Order": "Порядок получения файлов",
|
||||
@@ -110,7 +110,7 @@
|
||||
"Files are moved to date stamped versions in a .stversions directory when replaced or deleted by Syncthing.": "Когда Syncthing изменяет или удаляет файлы, их версии с таймштампами помещаются в папку .stversions",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Файлы с временнОй меткой версии помещаются в папку .stversions при их замене или удалении Syncthing.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Файлы защищены от изменений сделанных на других устройствах, но изменения сделанные на этом устройстве будут отправлены всему кластеру.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.",
|
||||
"Files are synchronized from the cluster, but any changes made locally will not be sent to other devices.": "Файлы синхронизируются из группы, но изменения, сделанные на этом устройстве, не будут отправлены на другие устройства группы.",
|
||||
"Filesystem Notifications": "Уведомления файловой системы",
|
||||
"Filesystem Watcher Errors": "Filesystem Watcher Errors",
|
||||
"Filter by date": "Отфильтровать по дате",
|
||||
@@ -122,7 +122,7 @@
|
||||
"Folder Type": "Тип папки",
|
||||
"Folders": "Папки",
|
||||
"For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.": "For the following folders an error occurred while starting to watch for changes. It will be retried every minute, so the errors might go away soon. If they persist, try to fix the underlying issue and ask for help if you can't.",
|
||||
"Full Rescan Interval (s)": "Full Rescan Interval (s)",
|
||||
"Full Rescan Interval (s)": "Интервал полного сканирования (в секундах)",
|
||||
"GUI": "Интерфейс",
|
||||
"GUI Authentication Password": "Пароль для доступа к панели управления",
|
||||
"GUI Authentication User": "Имя пользователя для доступа к панели управления",
|
||||
@@ -140,12 +140,12 @@
|
||||
"Ignore": "Игнорировать",
|
||||
"Ignore Patterns": "Шаблоны игнорирования",
|
||||
"Ignore Permissions": "Игнорировать файловые права доступа",
|
||||
"Ignored Devices": "Ignored Devices",
|
||||
"Ignored Folders": "Ignored Folders",
|
||||
"Ignored at": "Ignored at",
|
||||
"Ignored Devices": "Игнорируемые устройства",
|
||||
"Ignored Folders": "Игнорируемые папки",
|
||||
"Ignored at": "Добавлено",
|
||||
"Incoming Rate Limit (KiB/s)": "Ограничение входящей скорости (КиБ/с)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Неправильные настройки могут повредить содержимое папок и сделать Syncthing неработоспособным.",
|
||||
"Introduced By": "Introduced By",
|
||||
"Introduced By": "Рекомендовано",
|
||||
"Introducer": "Рекомендатель",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Инвертировать текущее условие (например, исключить)",
|
||||
"Keep Versions": "Количество хранимых версий",
|
||||
@@ -163,8 +163,8 @@
|
||||
"Local State": "Локальное состояние",
|
||||
"Local State (Total)": "Локальное состояние (всего)",
|
||||
"Log": "Журнал",
|
||||
"Log tailing paused. Click here to continue.": "Log tailing paused. Click here to continue.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Log tailing paused. Scroll to bottom continue.",
|
||||
"Log tailing paused. Click here to continue.": "Вывод журнала приостановлен. Чтобы продолжить, нажмите здесь.",
|
||||
"Log tailing paused. Scroll to bottom continue.": "Вывод журнала приостановлен. Чтобы продолжить, прокрутите журнал до конца.",
|
||||
"Logs": "Журналы",
|
||||
"Major Upgrade": "Обновление основной версии",
|
||||
"Mass actions": "Массовые действия",
|
||||
@@ -197,7 +197,7 @@
|
||||
"Override Changes": "Перезаписать изменения",
|
||||
"Path": "Путь",
|
||||
"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": "Путь к папке на локальном компьютере. Если её не существует, то она будет создана. Тильда (~) может использоваться как сокращение для",
|
||||
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {{tilde}}.",
|
||||
"Path where new auto accepted folders will be created, as well as the default suggested path when adding new folders via the UI. Tilde character (~) expands to {%tilde%}.": "Путь, по которому создаются автоматически принятые папки (также предлагаемый по умолчанию путь при создании новых папок в интерфейсе). Символ тильды (~) разворачивается в {{tilde}}.",
|
||||
"Path where versions should be stored (leave empty for the default .stversions directory in the shared folder).": "Путь, в котором нужно хранить версии (оставьте пустым для папки по умолчанию .stversions внутри общей папки).",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Путь, где должны храниться версии (оставьте пустым, чтобы использовать папку по умолчанию .stversions внутри папки).",
|
||||
"Pause": "Пауза",
|
||||
@@ -218,7 +218,7 @@
|
||||
"Quick guide to supported patterns": "Краткое руководство по поддерживаемым шаблонам",
|
||||
"RAM Utilization": "Использование памяти",
|
||||
"Random": "Случайно",
|
||||
"Receive Only": "Receive Only",
|
||||
"Receive Only": "Только получить",
|
||||
"Recent Changes": "Последние изменения",
|
||||
"Reduced by ignore patterns": "Уменьшено шаблонами игнорирования",
|
||||
"Release Notes": "Примечания к выпуску",
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Статистика",
|
||||
"Stopped": "Остановлено",
|
||||
"Support": "Поддержка",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Адрес протокола синхронизации",
|
||||
"Syncing": "Синхронизация",
|
||||
"Syncthing has been shut down.": "Syncthing был выключен.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Кажется, Syncthing не запущен или есть проблемы с подключением к Интернету. Переподключаюсь...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing столкнулся с проблемой при обработке Вашего запроса. Пожалуйста, обновите страницу или перезапустите Syncthing если проблема повторится.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "Эти изменения не вступят в силу, пока адрес панели управления переопределён в настройках запуска.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Административный интерфейс Syncthing настроен для предоставления удаленного доступа без пароля.",
|
||||
"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.": "Конфигурация была сохранена, но не активирована. Syncthing должен быть перезапущен для применения новой конфигурации.",
|
||||
@@ -327,7 +329,7 @@
|
||||
"Unavailable": "Недоступно",
|
||||
"Unavailable/Disabled by administrator or maintainer": "Unavailable/Disabled by administrator or maintainer",
|
||||
"Undecided (will prompt)": "Undecided (will prompt)",
|
||||
"Unignore": "Unignore",
|
||||
"Unignore": "Не игнорировать",
|
||||
"Unknown": "Неизвестно",
|
||||
"Unshared": "Необщедоступно",
|
||||
"Unused": "Не используется",
|
||||
@@ -357,9 +359,9 @@
|
||||
"You can also select one of these nearby devices:": "Вы можете выбрать из этих устройств рядом:",
|
||||
"You can change your choice at any time in the Settings dialog.": "Выбор можно изменить в любой момент в диалоге настроек.",
|
||||
"You can read more about the two release channels at the link below.": "О двух каналах выпусков можно почитать подробнее по нижеприведённой ссылке.",
|
||||
"You have no ignored devices.": "You have no ignored devices.",
|
||||
"You have no ignored folders.": "You have no ignored folders.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "You have unsaved changes. Do you really want to discard them?",
|
||||
"You have no ignored devices.": "У вас нет игнорируемых устройств.",
|
||||
"You have no ignored folders.": "У вас нет игнорируемых папок.",
|
||||
"You have unsaved changes. Do you really want to discard them?": "Есть несохранённые изменения. Вы действительно хотите отменить их?",
|
||||
"You must keep at least one version.": "Вы должны хранить как минимум одну версию.",
|
||||
"days": "дней",
|
||||
"directories": "папок",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Štatistika",
|
||||
"Stopped": "Zastavené",
|
||||
"Support": "Podpora",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Adresa načúvania synchronizačného protokolu",
|
||||
"Syncing": "Synchronizácia",
|
||||
"Syncthing has been shut down.": "Syncthing bol vypnutý.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing se zdá byť nefunkčný, alebo je problém s internetovým pripojením. Opakujem...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "The Syncthing admin interface is configured to allow remote access without a password.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "Súhrnné štatistiky sú verejne dostupné na uvedenej URL.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.",
|
||||
|
||||
@@ -95,7 +95,7 @@
|
||||
"Enter a non-negative number (e.g., \"2.35\") and select a unit. Percentages are as part of the total disk size.": "Ange ett icke-negativt antal (t.ex., \"2.35\") och välj en enhet. Procenttalen är som en del av den totala diskstorleken.",
|
||||
"Enter a non-privileged port number (1024 - 65535).": "Ange ett icke-privilegierat portnummer (1024 - 65535).",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Ange kommaseparerade (\"tcp://ip:port\", \"tcp://host:port\")-adresser eller ordet \"dynamic\" för att använda automatisk uppslagning.",
|
||||
"Enter ignore patterns, one per line.": "Ange ignorera mönster, en per rad.",
|
||||
"Enter ignore patterns, one per line.": "Ange mönster att ignorera, en per rad.",
|
||||
"Error": "Fel",
|
||||
"External File Versioning": "Extern filversionshantering",
|
||||
"Failed Items": "Misslyckade objekt",
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Statistik",
|
||||
"Stopped": "Stoppad",
|
||||
"Support": "Support",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Synkroniseringsprotokollets lyssnaradresser",
|
||||
"Syncing": "Synkroniserar",
|
||||
"Syncthing has been shut down.": "Syncthing har stängts.",
|
||||
@@ -292,6 +293,7 @@
|
||||
"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.",
|
||||
"Take me back": "Ta mig tillbaka",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"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ä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.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "Статистика",
|
||||
"Stopped": "Зупинено",
|
||||
"Support": "Підтримка",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "Адреса і вхідний порт протоколу синхронізації",
|
||||
"Syncing": "Синхронізація",
|
||||
"Syncthing has been shut down.": "Syncthing вимкнено (закрито).",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Схоже на те, що Syncthing закритий, або виникла проблема із Інтернет-з’єднанням. Проводиться повторна спроба з’єднання…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Схоже на те, що Syncthing стикнувся з проблемою оброблюючи ваш запит. Будь ласка перезавантажте сторінку в браузері або перезапустіть Syncthing.",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Інтерфейс адміністрування Syncthing налаштовано на дозвіл віддаленого доступу без пароля.",
|
||||
"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.": "Конфігурацію збережено, але не активовано. Необхідно перезапустити Syncthing для того, щоби активувати нову конфігурацію.",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "统计",
|
||||
"Stopped": "已停止",
|
||||
"Support": "支持",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "协议监听地址",
|
||||
"Syncing": "同步中",
|
||||
"Syncthing has been shut down.": "Syncthing 已关闭。",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎关闭了,或者您的网络连接存在故障。重试中…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing 在处理您的请求时似乎遇到了问题。如果问题持续,请刷新页面,或重启 Syncthing。",
|
||||
"Take me back": "带我回去",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "GUI 地址已被启动选项覆盖。当覆盖存在时,此处的更改就不会生效。",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "当前配置允许在不使用密码的情况下远程访问 Syncthing 管理界面。",
|
||||
"The aggregated statistics are publicly available at the URL below.": "全局统计数据公布于以下 URL。",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "设置已经保存,但是还未生效。Syncthing 需要重启以启用新的设置。",
|
||||
|
||||
@@ -283,6 +283,7 @@
|
||||
"Statistics": "統計",
|
||||
"Stopped": "已停止",
|
||||
"Support": "支援",
|
||||
"Support Bundle": "Support Bundle",
|
||||
"Sync Protocol Listen Addresses": "同步通訊協定監聽位址",
|
||||
"Syncing": "正在同步",
|
||||
"Syncthing has been shut down.": "Syncthing 已經關閉。",
|
||||
@@ -292,6 +293,7 @@
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎離線了,或者您的網際網路連線出現問題。正在重試...",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing 在處理您的請求時似乎遇到了問題。請重新整理本頁面,若問題持續發生,請重新啟動 Syncthing。",
|
||||
"Take me back": "Take me back",
|
||||
"The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.": "The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.",
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing 管理介面被設定允許無密碼的遠端存取。",
|
||||
"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.": "組態已經儲存但尚未啟用。Syncthing 必須重新啟動以便啟用新的組態。",
|
||||
|
||||
@@ -1 +1 @@
|
||||
var langPrettyprint = {"bg":"Bulgarian","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","eo":"Esperanto","es":"Spanish","es-ES":"Spanish (Spain)","eu":"Basque","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ru":"Russian","sk":"Slovak","sv":"Swedish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
|
||||
var langPrettyprint = {"bg":"Bulgarian","ca@valencia":"Catalan (Valencian)","cs":"Czech","da":"Danish","de":"German","el":"Greek","en":"English","en-GB":"English (United Kingdom)","es":"Spanish","es-ES":"Spanish (Spain)","eu":"Basque","fi":"Finnish","fr":"French","fr-CA":"French (Canada)","fy":"Western Frisian","hu":"Hungarian","it":"Italian","ja":"Japanese","ko-KR":"Korean (Korea)","lt":"Lithuanian","nb":"Norwegian Bokmål","nl":"Dutch","pl":"Polish","pt-BR":"Portuguese (Brazil)","pt-PT":"Portuguese (Portugal)","ru":"Russian","sk":"Slovak","sv":"Swedish","uk":"Ukrainian","zh-CN":"Chinese (China)","zh-TW":"Chinese (Taiwan)"}
|
||||
|
||||
@@ -1 +1 @@
|
||||
var validLangs = ["bg","ca@valencia","cs","da","de","el","en","en-GB","eo","es","es-ES","eu","fi","fr","fr-CA","fy","hu","it","ja","ko-KR","lt","nb","nl","pl","pt-BR","pt-PT","ru","sk","sv","uk","zh-CN","zh-TW"]
|
||||
var validLangs = ["bg","ca@valencia","cs","da","de","el","en","en-GB","es","es-ES","eu","fi","fr","fr-CA","fy","hu","it","ja","ko-KR","lt","nb","nl","pl","pt-BR","pt-PT","ru","sk","sv","uk","zh-CN","zh-TW"]
|
||||
|
||||
@@ -21,7 +21,8 @@
|
||||
<link href="vendor/bootstrap/css/bootstrap.css" rel="stylesheet"/>
|
||||
<link href="vendor/daterangepicker/daterangepicker.css" rel="stylesheet"/>
|
||||
<link href="assets/font/raleway.css" rel="stylesheet"/>
|
||||
<link href="vendor/font-awesome/css/fontawesome-all.css" rel="stylesheet"/>
|
||||
<link href="vendor/fork-awesome/css/fork-awesome.css" rel="stylesheet"/>
|
||||
<link href="vendor/fork-awesome/css/v5-compat.css" rel="stylesheet"/>
|
||||
<link href="assets/css/overrides.css" rel="stylesheet"/>
|
||||
<link href="assets/css/theme.css" rel="stylesheet"/>
|
||||
<link href="vendor/fancytree/css/ui.fancytree.css" rel="stylesheet"/>
|
||||
@@ -70,7 +71,7 @@
|
||||
<li><a href="" data-toggle="modal" data-target="#idqr" ng-click="currentDevice=thisDevice()"><span class="fas fa-fw fa-qrcode"></span> <span translate>Show ID</span></a></li>
|
||||
<li class="divider" aria-hidden="true"></li>
|
||||
<li><a href="" ng-click="shutdown()"><span class="fas fa-fw fa-power-off"></span> <span translate>Shutdown</span></a></li>
|
||||
<li><a href="" ng-click="restart()"><span class="fas fa-fw fa-sync"></span> <span translate>Restart</span></a></li>
|
||||
<li><a href="" ng-click="restart()"><span class="fas fa-fw fa-refresh"></span> <span translate>Restart</span></a></li>
|
||||
<li class="divider" aria-hidden="true"></li>
|
||||
<li class="visible-xs">
|
||||
<a href="https://docs.syncthing.net/intro/gui.html" target="_blank">
|
||||
@@ -81,6 +82,8 @@
|
||||
<li class="divider" aria-hidden="true"></li>
|
||||
<li><a href="" ng-click="advanced()"><span class="fas fa-fw fa-cogs"></span> <span translate>Advanced</span></a></li>
|
||||
<li><a href="" ng-click="logging.show()"><span class="far fa-fw fa-file-alt"></span> <span translate>Logs</span></a></li>
|
||||
<li class="divider" aria-hidden="true" ng-if="config.gui.debugging"></li>
|
||||
<li><a href="/rest/debug/support" target="_blank" ng-if="config.gui.debugging"><span class="fa fa-user-md"></span> <span translate>Support Bundle</span></a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
@@ -137,7 +140,7 @@
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<button type="button" class="btn btn-sm btn-default pull-right" ng-click="restart()">
|
||||
<span class="fas fa-sync"></span> <span translate>Restart</span>
|
||||
<span class="fas fa-refresh"></span> <span translate>Restart</span>
|
||||
</button>
|
||||
<div class="clearfix"></div>
|
||||
</div>
|
||||
@@ -402,7 +405,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="fas fa-fw fa-sync"></span> <span translate>Rescans</span></th>
|
||||
<th><span class="fas fa-fw fa-refresh"></span> <span translate>Rescans</span></th>
|
||||
<td class="text-right">
|
||||
<div ng-if="folder.rescanIntervalS > 0">
|
||||
<span ng-if="!folder.fsWatcherEnabled" tooltip data-original-title="{{'Periodic scanning at given interval and disabled watching for changes' | translate}}">
|
||||
@@ -496,7 +499,7 @@
|
||||
<span class="fas fa-undo"></span> <span translate>Versions</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="rescanFolder(folder.id)" ng-disabled="['idle', 'stopped', 'unshared', 'outofsync'].indexOf(folderStatus(folder)) < 0">
|
||||
<span class="fas fa-sync"></span> <span translate>Rescan</span>
|
||||
<span class="fas fa-refresh"></span> <span translate>Rescan</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="editFolder(folder)">
|
||||
<span class="fas fa-pencil-alt"></span> <span translate>Edit</span>
|
||||
@@ -515,7 +518,7 @@
|
||||
<span class="fas fa-play"></span> <span translate>Resume All</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="rescanAllFolders()">
|
||||
<span class="fas fa-sync"></span> <span translate>Rescan All</span>
|
||||
<span class="fas fa-refresh"></span> <span translate>Rescan All</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-sm btn-default" ng-click="addFolder()">
|
||||
<span class="fas fa-plus"></span> <span translate>Add Folder</span>
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<p translate>Copyright © 2014-2017 the following Contributors:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-12" id="contributor-list">
|
||||
Jakob Borg, Audrius Butkevicius, Simon Frei, Alexander Graf, Alexandre Viau, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Nate Morrison, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Wulf Weich, Aaron Bieber, Adam Piggott, Adel Qalieh, Alessandro G., Andrew Dunham, Andrew Rabert, Andrey D, Antoine Lamielle, Aranjedeath, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benedikt Morbach, Benny Ng, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Chris Tonkinson, Colin Kennedy, Dale Visser, Daniel Bergmann, Daniel Martí, Darshil Chanpura, David Rimmer, Denis A., Dennis Wilson, Dmitry Saveliev, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Graham Miln, Han Boetes, Harrison Jones, Heiko Zuerker, Iain Barnett, Ian Johnson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jaya Chithra, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonathan Cross, Jose Manuel Delicado, Karol Różycki, Keith Turner, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin White, Jr., Kurt Fitzner, Laurent Arnoud, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mark Pulford, Mateusz Naściszewski, Matic Potočnik, Matt Burke, Matteo Ruina, Max Schulze, MaximAL, Maxime Thirouin, Michael Jephcote, Michael Tilli, Mike Boone, MikeLund, Nicholas Rishel, Nicolas Braud-Santoni, Niels Peter Roest, Nils Jakobi, NoLooseEnds, Oyebanji Jacob Mayowa, Pascal Jungblut, Pawel Palenica, Paweł Rozlach, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phil Davis, Phill Luby, Pier Paolo Ramon, Piotr Bejda, Pramodh KP, Richard Hartmann, Robert Carosi, Roman Zaynetdinov, Ross Smith II, Sacheendra Talluri, Scott Klupfel, Sly_tom_cat, Stefan Kuntz, Suhas Gundimeda, Taylor Khan, Thomas Hipp, Tim Abell, Tim Howes, Tobias Nygren, Tobias Tom, Tomas Cerveny, Tommy Thorn, Tully Robinson, Tyler Brazier, Unrud, Veeti Paananen, Victor Buinsky, Vil Brekin, Vladimir Rusinov, William A. Kennington III, Xavier O., Yannic A., andresvia, andyleap, chucic, derekriemer, janost, jaseg, klemens, marco-m, perewa, rubenbe, wangguoliang, xjtdy888, 佛跳墙
|
||||
Jakob Borg, Audrius Butkevicius, Simon Frei, Alexander Graf, Alexandre Viau, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Nate Morrison, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Wulf Weich, Aaron Bieber, Adam Piggott, Adel Qalieh, Alessandro G., Andrew Dunham, Andrew Rabert, Andrey D, Antoine Lamielle, Aranjedeath, Arthur Axel fREW Schmidt, BAHADIR YILMAZ, Bart De Vries, Ben Curthoys, Ben Shepherd, Ben Sidhom, Benedikt Heine, Benedikt Morbach, Benny Ng, Boris Rybalkin, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Chris Tonkinson, Colin Kennedy, Dale Visser, Daniel Bergmann, Daniel Martí, Darshil Chanpura, David Rimmer, Denis A., Dennis Wilson, Dmitry Saveliev, Dominik Heidler, Elias Jarlebring, Elliot Huffman, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Graham Miln, Han Boetes, Harrison Jones, Heiko Zuerker, Iain Barnett, Ian Johnson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jaya Chithra, Jens Diemer, Jerry Jacobs, Jochen Voss, Johan Andersson, Johan Vromans, John Rinehart, Jonathan Cross, Jose Manuel Delicado, Karol Różycki, Keith Turner, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin White, Jr., Kurt Fitzner, Laurent Arnoud, Laurent Etiemble, Leo Arias, Liu Siyuan, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mark Pulford, Mateusz Naściszewski, Matic Potočnik, Matt Burke, Matteo Ruina, Max Schulze, MaximAL, Maxime Thirouin, Michael Jephcote, Michael Tilli, Mike Boone, MikeLund, Nicholas Rishel, Nicolas Braud-Santoni, Niels Peter Roest, Nils Jakobi, NoLooseEnds, Oyebanji Jacob Mayowa, Pascal Jungblut, Pawel Palenica, Paweł Rozlach, Peter Badida, Peter Dave Hello, Peter Hoeg, Peter Marquardt, Phil Davis, Phill Luby, Pier Paolo Ramon, Piotr Bejda, Pramodh KP, Richard Hartmann, Robert Carosi, Roman Zaynetdinov, Ross Smith II, Sacheendra Talluri, Scott Klupfel, Sly_tom_cat, Stefan Kuntz, Suhas Gundimeda, Taylor Khan, Thomas Hipp, Tim Abell, Tim Howes, Tobias Nygren, Tobias Tom, Tomas Cerveny, Tommy Thorn, Tully Robinson, Tyler Brazier, Unrud, Veeti Paananen, Victor Buinsky, Vil Brekin, Vladimir Rusinov, William A. Kennington III, Xavier O., Yannic A., andresvia, andyleap, chucic, derekriemer, janost, jaseg, klemens, marco-m, perewa, rubenbe, wangguoliang, xjtdy888, 佛跳墙
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
@@ -392,6 +392,7 @@ angular.module('syncthing.core')
|
||||
$scope.openNoAuth = guiCfg.address.substr(0, 4) !== "127."
|
||||
&& guiCfg.address.substr(0, 6) !== "[::1]:"
|
||||
&& (!guiCfg.user || !guiCfg.password)
|
||||
&& guiCfg.authMode !== 'ldap'
|
||||
&& !guiCfg.insecureAdminAccess;
|
||||
|
||||
if (!hasConfig) {
|
||||
@@ -1275,11 +1276,17 @@ angular.module('syncthing.core')
|
||||
}
|
||||
|
||||
// Apply new settings locally
|
||||
$scope.thisDevice().name = $scope.tmpOptions.deviceName;
|
||||
$scope.thisDeviceIn($scope.tmpDevices).name = $scope.tmpOptions.deviceName;
|
||||
$scope.config.options = angular.copy($scope.tmpOptions);
|
||||
$scope.config.gui = angular.copy($scope.tmpGUI);
|
||||
$scope.config.remoteIgnoredDevices = angular.copy($scope.tmpRemoteIgnoredDevices);
|
||||
$scope.config.devices = angular.copy($scope.tmpDevices);
|
||||
// $scope.devices is updated by updateLocalConfig based on
|
||||
// the config changed event, but settingsModified will look
|
||||
// at it before that and conclude that the settings are
|
||||
// modified (even though we just saved) unless we update
|
||||
// here as well...
|
||||
$scope.devices = $scope.config.devices;
|
||||
|
||||
['listenAddresses', 'globalAnnounceServers'].forEach(function (key) {
|
||||
$scope.config.options[key] = $scope.config.options["_" + key + "Str"].split(/[ ,]+/).map(function (x) {
|
||||
@@ -1508,8 +1515,12 @@ angular.module('syncthing.core')
|
||||
};
|
||||
|
||||
$scope.thisDevice = function () {
|
||||
for (var i = 0; i < $scope.devices.length; i++) {
|
||||
var n = $scope.devices[i];
|
||||
return $scope.thisDeviceIn($scope.devices);
|
||||
}
|
||||
|
||||
$scope.thisDeviceIn = function (l) {
|
||||
for (var i = 0; i < l.length; i++) {
|
||||
var n = l[i];
|
||||
if (n.deviceID === $scope.myID) {
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -111,7 +111,11 @@
|
||||
<div id="settings-gui" class="tab-pane">
|
||||
<div class="form-group" ng-class="{'has-error': settingsEditor.Address.$invalid && settingsEditor.Address.$dirty}">
|
||||
<label translate for="Address">GUI Listen Address</label> <a href="https://docs.syncthing.net/users/guilisten.html" target="_blank"><span class="fas fa-question-circle"></span> <span translate>Help</span></a>
|
||||
<input id="Address" name="Address" class="form-control" type="text" ng-model="tmpGUI.address" ng-pattern="/.*:0*((102[4-9])|(10[3-9][0-9])|(1[1-9][0-9][0-9])|([2-9][0-9][0-9][0-9])|([1-6]\d{4}))$/"/>
|
||||
<p class="text-warning" ng-show="system.guiAddressOverridden">
|
||||
<span class="fas fa-exclamation-triangle"></span>
|
||||
<span translate>The GUI address is overridden by startup options. Changes here will not take effect while the override is in place.</span>
|
||||
</p>
|
||||
<input id="Address" name="Address" class="form-control" type="text" ng-model="tmpGUI.address" ng-pattern="/^(/.*)|(.*:0*((102[4-9])|(10[3-9][0-9])|(1[1-9][0-9][0-9])|([2-9][0-9][0-9][0-9])|([1-6]\d{4})))$/"/>
|
||||
<p class="help-block" ng-show="settingsEditor.Address.$invalid" translate>
|
||||
Enter a non-privileged port number (1024 - 65535).
|
||||
</p>
|
||||
|
||||
@@ -42,4 +42,9 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="modal-footer">
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fas fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
3203
gui/default/vendor/font-awesome/css/fontawesome-all.css
vendored
3203
gui/default/vendor/font-awesome/css/fontawesome-all.css
vendored
File diff suppressed because it is too large
Load Diff
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 586 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 102 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
|
Before Width: | Height: | Size: 477 KiB |
Binary file not shown.
Binary file not shown.
Binary file not shown.
2494
gui/default/vendor/fork-awesome/css/fork-awesome.css
vendored
Normal file
2494
gui/default/vendor/fork-awesome/css/fork-awesome.css
vendored
Normal file
File diff suppressed because it is too large
Load Diff
446
gui/default/vendor/fork-awesome/css/v5-compat.css
vendored
Normal file
446
gui/default/vendor/fork-awesome/css/v5-compat.css
vendored
Normal file
@@ -0,0 +1,446 @@
|
||||
/*!
|
||||
Fork Awesome 1.1.2
|
||||
License - http://forkawesome.github.io/Fork-Awesome/license
|
||||
|
||||
Copyright 2018 Dave Gandy & Fork Awesome
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||
*/
|
||||
.fas,
|
||||
.fab,
|
||||
.far {
|
||||
display: inline-block;
|
||||
font: normal normal normal 14px/1 ForkAwesome;
|
||||
font-size: inherit;
|
||||
text-rendering: auto;
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
.fas.fa-chart-area:before {
|
||||
content: "\f1fe";
|
||||
}
|
||||
.fas.fa-arrows-alt:before {
|
||||
content: "\f047";
|
||||
}
|
||||
.fas.fa-expand-arrows-alt:before {
|
||||
content: "\f0b2";
|
||||
}
|
||||
.fas.fa-arrows-alt-h:before {
|
||||
content: "\f07e";
|
||||
}
|
||||
.fas.fa-arrows-alt-v:before {
|
||||
content: "\f07d";
|
||||
}
|
||||
.fas.fa-calendar-alt:before {
|
||||
content: "\f073";
|
||||
}
|
||||
.fas.fa-circle-notch:before {
|
||||
content: "\f1ce";
|
||||
}
|
||||
.fas.fa-cloud-download-alt:before {
|
||||
content: "\f0ed";
|
||||
}
|
||||
.fas.fa-cloud-upload-alt:before {
|
||||
content: "\f0ee";
|
||||
}
|
||||
.fas.fa-credit-card:before {
|
||||
content: "\f283";
|
||||
}
|
||||
.fas.fa-dollar-sign:before {
|
||||
content: "\f155";
|
||||
}
|
||||
.fas.fa-euro-sign:before {
|
||||
content: "\f153";
|
||||
}
|
||||
.fas.fa-exchange-alt:before {
|
||||
content: "\f0ec";
|
||||
}
|
||||
.fas.fa-external-link-alt:before {
|
||||
content: "\f08e";
|
||||
}
|
||||
.fas.fa-external-link-square-alt:before {
|
||||
content: "\f14c";
|
||||
}
|
||||
.fas.fa-eye-dropper:before {
|
||||
content: "\f1fb";
|
||||
}
|
||||
.fas.fa-pound-sign:before {
|
||||
content: "\f154";
|
||||
}
|
||||
.fas.fa-glass-martini:before {
|
||||
content: "\f000";
|
||||
}
|
||||
.fas.fa-shekel-sign:before {
|
||||
content: "\f20b";
|
||||
}
|
||||
.fas.fa-rupee-sign:before {
|
||||
content: "\f156";
|
||||
}
|
||||
.fas.fa-won-sign:before {
|
||||
content: "\f159";
|
||||
}
|
||||
.fas.fa-level-down-alt:before {
|
||||
content: "\f149";
|
||||
}
|
||||
.fas.fa-level-up-alt:before {
|
||||
content: "\f148";
|
||||
}
|
||||
.fas.fa-chart-line:before {
|
||||
content: "\f201";
|
||||
}
|
||||
.fas.fa-long-arrow-alt-down:before {
|
||||
content: "\f175";
|
||||
}
|
||||
.fas.fa-long-arrow-alt-left:before {
|
||||
content: "\f177";
|
||||
}
|
||||
.fas.fa-long-arrow-alt-right:before {
|
||||
content: "\f178";
|
||||
}
|
||||
.fas.fa-long-arrow-alt-up:before {
|
||||
content: "\f176";
|
||||
}
|
||||
.fas.fa-map-marker-alt:before {
|
||||
content: "\f041";
|
||||
}
|
||||
.fas.fa-mobile-alt:before {
|
||||
content: "\f10b";
|
||||
}
|
||||
.fas.fa-pencil-alt:before {
|
||||
content: "\f040";
|
||||
}
|
||||
.fas.fa-pen-square:before {
|
||||
content: "\f14b";
|
||||
}
|
||||
.fas.fa-chart-pie:before {
|
||||
content: "\f200";
|
||||
}
|
||||
.fas.fa-yen-sign:before {
|
||||
content: "\f157";
|
||||
}
|
||||
.fas.fa-ruble-sign:before {
|
||||
content: "\f158";
|
||||
}
|
||||
.fas.fa-shield-alt:before {
|
||||
content: "\f132";
|
||||
}
|
||||
.fas.fa-sign-in-alt:before {
|
||||
content: "\f090";
|
||||
}
|
||||
.fas.fa-sign-out-alt:before {
|
||||
content: "\f08b";
|
||||
}
|
||||
.fas.fa-sliders-h:before {
|
||||
content: "\f1de";
|
||||
}
|
||||
.fas.fa-tablet-alt:before {
|
||||
content: "\f10a";
|
||||
}
|
||||
.fas.fa-tachometer-alt:before {
|
||||
content: "\f0e4";
|
||||
}
|
||||
.fas.fa-thumbtack:before {
|
||||
content: "\f08d";
|
||||
}
|
||||
.fas.fa-ticket-alt:before {
|
||||
content: "\f145";
|
||||
}
|
||||
.fas.fa-trash-alt:before {
|
||||
content: "\f1f8";
|
||||
}
|
||||
.fas.fa-lira-sign:before {
|
||||
content: "\f195";
|
||||
}
|
||||
.fab.fa-linkedin-in:before {
|
||||
content: "\fe01";
|
||||
}
|
||||
.fab.fa-linkedin:before {
|
||||
content: "\f08c";
|
||||
}
|
||||
.far.fa-address-book:before {
|
||||
content: "\f2ba";
|
||||
}
|
||||
.far.fa-address-card:before {
|
||||
content: "\f2bc";
|
||||
}
|
||||
.far.fa-arrow-alt-circle-down:before {
|
||||
content: "\f01a";
|
||||
}
|
||||
.far.fa-arrow-alt-circle-left:before {
|
||||
content: "\f190";
|
||||
}
|
||||
.far.fa-arrow-alt-circle-right:before {
|
||||
content: "\f18e";
|
||||
}
|
||||
.far.fa-arrow-alt-circle-up:before {
|
||||
content: "\f01b";
|
||||
}
|
||||
.far.fa-bell:before {
|
||||
content: "\f0f3";
|
||||
}
|
||||
.far.fa-bell-slash:before {
|
||||
content: "\f1f7";
|
||||
}
|
||||
.far.fa-bookmark:before {
|
||||
content: "\f097";
|
||||
}
|
||||
.far.fa-building:before {
|
||||
content: "\f0f7";
|
||||
}
|
||||
.far.fa-calendar-check:before {
|
||||
content: "\f274";
|
||||
}
|
||||
.far.fa-calendar-minus:before {
|
||||
content: "\f272";
|
||||
}
|
||||
.far.fa-calendar:before {
|
||||
content: "\f133";
|
||||
}
|
||||
.far.fa-calendar-plus:before {
|
||||
content: "\f271";
|
||||
}
|
||||
.far.fa-calendar-times:before {
|
||||
content: "\f273";
|
||||
}
|
||||
.far.fa-caret-square-down:before {
|
||||
content: "\f150";
|
||||
}
|
||||
.far.fa-caret-square-left:before {
|
||||
content: "\f191";
|
||||
}
|
||||
.far.fa-caret-square-right:before {
|
||||
content: "\f152";
|
||||
}
|
||||
.far.fa-caret-square-up:before {
|
||||
content: "\f151";
|
||||
}
|
||||
.far.fa-check-circle:before {
|
||||
content: "\f05d";
|
||||
}
|
||||
.far.fa-check-square:before {
|
||||
content: "\f046";
|
||||
}
|
||||
.far.fa-circle:before {
|
||||
content: "\f10c";
|
||||
}
|
||||
.far.fa-clock:before {
|
||||
content: "\f017";
|
||||
}
|
||||
.far.fa-comment:before {
|
||||
content: "\f0e5";
|
||||
}
|
||||
.far.fa-comment-dots:before {
|
||||
content: "\f27b";
|
||||
}
|
||||
.far.fa-comments:before {
|
||||
content: "\f0e6";
|
||||
}
|
||||
.far.fa-dot-circle:before {
|
||||
content: "\f192";
|
||||
}
|
||||
.far.fa-id-card:before {
|
||||
content: "\f2c3";
|
||||
}
|
||||
.far.fa-envelope:before {
|
||||
content: "\f003";
|
||||
}
|
||||
.far.fa-envelope-open:before {
|
||||
content: "\f2b7";
|
||||
}
|
||||
.far.fa-file-archive:before {
|
||||
content: "\f1c6";
|
||||
}
|
||||
.far.fa-file-audio:before {
|
||||
content: "\f1c7";
|
||||
}
|
||||
.far.fa-file-code:before {
|
||||
content: "\f1c9";
|
||||
}
|
||||
.far.fa-file-excel:before {
|
||||
content: "\f1c3";
|
||||
}
|
||||
.far.fa-file-image:before {
|
||||
content: "\f1c5";
|
||||
}
|
||||
.far.fa-file-video:before {
|
||||
content: "\f1c8";
|
||||
}
|
||||
.far.fa-copy:before,
|
||||
.far.fa-file:before {
|
||||
content: "\f016";
|
||||
}
|
||||
.far.fa-file-pdf:before {
|
||||
content: "\f1c1";
|
||||
}
|
||||
.far.fa-file-powerpoint:before {
|
||||
content: "\f1c4";
|
||||
}
|
||||
.far.fa-file-alt:before {
|
||||
content: "\f0f6";
|
||||
}
|
||||
.far.fa-file-word:before {
|
||||
content: "\f1c2";
|
||||
}
|
||||
.far.fa-flag:before {
|
||||
content: "\f11d";
|
||||
}
|
||||
.far.fa-save:before {
|
||||
content: "\f0c7";
|
||||
}
|
||||
.far.fa-folder:before {
|
||||
content: "\f114";
|
||||
}
|
||||
.far.fa-folder-open:before {
|
||||
content: "\f115";
|
||||
}
|
||||
.far.fa-frown:before {
|
||||
content: "\f119";
|
||||
}
|
||||
.far.fa-futbol:before {
|
||||
content: "\f1e3";
|
||||
}
|
||||
.far.fa-hand-rock:before {
|
||||
content: "\f255";
|
||||
}
|
||||
.far.fa-hand-lizard:before {
|
||||
content: "\f258";
|
||||
}
|
||||
.far.fa-hand-point-down:before {
|
||||
content: "\f0a7";
|
||||
}
|
||||
.far.fa-hand-point-left:before {
|
||||
content: "\f0a5";
|
||||
}
|
||||
.far.fa-hand-point-right:before {
|
||||
content: "\f0a4";
|
||||
}
|
||||
.far.fa-hand-point-up:before {
|
||||
content: "\f0a6";
|
||||
}
|
||||
.far.fa-hand-paper:before {
|
||||
content: "\256";
|
||||
}
|
||||
.far.fa-hand-pointer:before {
|
||||
content: "\f25a";
|
||||
}
|
||||
.far.fa-hand-scissors:before {
|
||||
content: "\f257";
|
||||
}
|
||||
.far.fa-hand-spock:before {
|
||||
content: "\f259";
|
||||
}
|
||||
.far.fa-handshake:before {
|
||||
content: "\f2b5";
|
||||
}
|
||||
.far.fa-hdd:before {
|
||||
content: "\f0a0";
|
||||
}
|
||||
.far.fa-heart:before {
|
||||
content: "\f08a";
|
||||
}
|
||||
.far.fa-hospital:before {
|
||||
content: "\f0f8";
|
||||
}
|
||||
.far.fa-hourglass:before {
|
||||
content: "\f250";
|
||||
}
|
||||
.far.fa-id-card:before {
|
||||
content: "\f2c3";
|
||||
}
|
||||
.far.fa-keyboard:before {
|
||||
content: "\f11c";
|
||||
}
|
||||
.far.fa-lemon:before {
|
||||
content: "\f094";
|
||||
}
|
||||
.far.fa-lightbulb:before {
|
||||
content: "\f0eb";
|
||||
}
|
||||
.far.fa-meh:before {
|
||||
content: "\f11a";
|
||||
}
|
||||
.far.fa-minus-square:before {
|
||||
content: "\f147";
|
||||
}
|
||||
.far.fa-money-bill-alt:before {
|
||||
content: "\f0d6";
|
||||
}
|
||||
.far.fa-moon:before {
|
||||
content: "\f186";
|
||||
}
|
||||
.far.fa-newspaper:before {
|
||||
content: "\f1ea";
|
||||
}
|
||||
.far.fa-paper-plane:before {
|
||||
content: "\f1d9";
|
||||
}
|
||||
.far.fa-pause-circle:before {
|
||||
content: "\f28c";
|
||||
}
|
||||
.far.fa-edit:before {
|
||||
content: "\f044";
|
||||
}
|
||||
.far.fa-image:before {
|
||||
content: "\f03e";
|
||||
}
|
||||
.far.fa-play-circle:before {
|
||||
content: "\f01d";
|
||||
}
|
||||
.far.fa-plus-square:before {
|
||||
content: "\f196";
|
||||
}
|
||||
.far.fa-question-circle:before {
|
||||
content: "\f92c";
|
||||
}
|
||||
.far.fa-share-square:before {
|
||||
content: "\f045";
|
||||
}
|
||||
.far.fa-smile:before {
|
||||
content: "\f118";
|
||||
}
|
||||
.far.fa-snowflake:before {
|
||||
content: "\f2dc";
|
||||
}
|
||||
.far.fa-futbol:before {
|
||||
content: "\f1e3";
|
||||
}
|
||||
.far.fa-star-half:before {
|
||||
content: "\f089";
|
||||
}
|
||||
.far.fa-star:before {
|
||||
content: "\f006";
|
||||
}
|
||||
.far.fa-sticky-note:before {
|
||||
content: "\f24a";
|
||||
}
|
||||
.far.fa-stop-circle:before {
|
||||
content: "\f28e";
|
||||
}
|
||||
.far.fa-sun:before {
|
||||
content: "\f185";
|
||||
}
|
||||
.far.fa-thumbs-down:before {
|
||||
content: "\f088";
|
||||
}
|
||||
.far.fa-thumbs-up:before {
|
||||
content: "\f087";
|
||||
}
|
||||
.far.fa-times-circle:before {
|
||||
content: "\f05c";
|
||||
}
|
||||
.far.fa-window-close:before {
|
||||
content: "\f2d4";
|
||||
}
|
||||
.far.fa-trash-alt:before {
|
||||
content: "\f014";
|
||||
}
|
||||
.far.fa-user-circle:before {
|
||||
content: "\f2be";
|
||||
}
|
||||
.far.fa-user:before {
|
||||
content: "\f2c0";
|
||||
}
|
||||
BIN
gui/default/vendor/fork-awesome/fonts/forkawesome-webfont.eot
vendored
Normal file
BIN
gui/default/vendor/fork-awesome/fonts/forkawesome-webfont.eot
vendored
Normal file
Binary file not shown.
BIN
gui/default/vendor/fork-awesome/fonts/forkawesome-webfont.svg
vendored
Normal file
BIN
gui/default/vendor/fork-awesome/fonts/forkawesome-webfont.svg
vendored
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 453 KiB |
BIN
gui/default/vendor/fork-awesome/fonts/forkawesome-webfont.ttf
vendored
Normal file
BIN
gui/default/vendor/fork-awesome/fonts/forkawesome-webfont.ttf
vendored
Normal file
Binary file not shown.
BIN
gui/default/vendor/fork-awesome/fonts/forkawesome-webfont.woff
vendored
Normal file
BIN
gui/default/vendor/fork-awesome/fonts/forkawesome-webfont.woff
vendored
Normal file
Binary file not shown.
BIN
gui/default/vendor/fork-awesome/fonts/forkawesome-webfont.woff2
vendored
Normal file
BIN
gui/default/vendor/fork-awesome/fonts/forkawesome-webfont.woff2
vendored
Normal file
Binary file not shown.
@@ -31,6 +31,10 @@ func (c GUIConfiguration) IsAuthEnabled() bool {
|
||||
return c.AuthMode == AuthModeLDAP || (len(c.User) > 0 && len(c.Password) > 0)
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) IsOverridden() bool {
|
||||
return os.Getenv("STGUIADDRESS") != ""
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) Address() string {
|
||||
if override := os.Getenv("STGUIADDRESS"); override != "" {
|
||||
// This value may be of the form "scheme://address:port" or just
|
||||
@@ -43,6 +47,9 @@ func (c GUIConfiguration) Address() string {
|
||||
if err != nil {
|
||||
return override
|
||||
}
|
||||
if strings.HasPrefix(url.Scheme, "unix") {
|
||||
return url.Path
|
||||
}
|
||||
return url.Host
|
||||
}
|
||||
|
||||
@@ -52,14 +59,39 @@ func (c GUIConfiguration) Address() string {
|
||||
return c.RawAddress
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) Network() string {
|
||||
if override := os.Getenv("STGUIADDRESS"); strings.Contains(override, "/") {
|
||||
url, err := url.Parse(override)
|
||||
if err != nil {
|
||||
return "tcp"
|
||||
}
|
||||
if strings.HasPrefix(url.Scheme, "unix") {
|
||||
return "unix"
|
||||
}
|
||||
}
|
||||
if strings.HasPrefix(c.RawAddress, "/") {
|
||||
return "unix"
|
||||
}
|
||||
return "tcp"
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) UseTLS() bool {
|
||||
if override := os.Getenv("STGUIADDRESS"); override != "" && strings.HasPrefix(override, "http") {
|
||||
return strings.HasPrefix(override, "https:")
|
||||
if override := os.Getenv("STGUIADDRESS"); override != "" {
|
||||
if strings.HasPrefix(override, "http") {
|
||||
return strings.HasPrefix(override, "https:")
|
||||
}
|
||||
if strings.HasPrefix(override, "unix") {
|
||||
return strings.HasPrefix(override, "unixs:")
|
||||
}
|
||||
}
|
||||
return c.RawUseTLS
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) URL() string {
|
||||
if strings.HasPrefix(c.RawAddress, "/") {
|
||||
return "unix://" + c.RawAddress
|
||||
}
|
||||
|
||||
u := url.URL{
|
||||
Scheme: "http",
|
||||
Host: c.Address(),
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"sync/atomic"
|
||||
@@ -499,8 +500,11 @@ func (w *Wrapper) AddOrUpdatePendingFolder(id, label string, device protocol.Dev
|
||||
// CheckHomeFreeSpace returns nil if the home disk has the required amount of
|
||||
// free space, or if home disk free space checking is disabled.
|
||||
func (w *Wrapper) CheckHomeFreeSpace() error {
|
||||
if usage, err := fs.NewFilesystem(fs.FilesystemTypeBasic, filepath.Dir(w.ConfigPath())).Usage("."); err == nil {
|
||||
return checkFreeSpace(w.Options().MinHomeDiskFree, usage)
|
||||
path := filepath.Dir(w.ConfigPath())
|
||||
if usage, err := fs.NewFilesystem(fs.FilesystemTypeBasic, path).Usage("."); err == nil {
|
||||
if err = checkFreeSpace(w.Options().MinHomeDiskFree, usage); err != nil {
|
||||
return fmt.Errorf("insufficient space on home disk (%v): %v", path, err)
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -134,36 +134,38 @@ func (lim *limiter) CommitConfiguration(from, to config.Configuration) bool {
|
||||
return true
|
||||
}
|
||||
|
||||
limited := false
|
||||
sendLimitStr := "is unlimited"
|
||||
recvLimitStr := "is unlimited"
|
||||
|
||||
// The rate variables are in KiB/s in the config (despite the camel casing
|
||||
// of the name). We multiply by 1024 to get bytes/s.
|
||||
if to.Options.MaxRecvKbps <= 0 {
|
||||
lim.read.SetLimit(rate.Inf)
|
||||
} else {
|
||||
lim.read.SetLimit(1024 * rate.Limit(to.Options.MaxRecvKbps))
|
||||
recvLimitStr = fmt.Sprintf("limit is %d KiB/s", to.Options.MaxRecvKbps)
|
||||
limited = true
|
||||
}
|
||||
|
||||
if to.Options.MaxSendKbps <= 0 {
|
||||
lim.write.SetLimit(rate.Inf)
|
||||
} else {
|
||||
lim.write.SetLimit(1024 * rate.Limit(to.Options.MaxSendKbps))
|
||||
sendLimitStr = fmt.Sprintf("limit is %d KiB/s", to.Options.MaxSendKbps)
|
||||
limited = true
|
||||
}
|
||||
|
||||
lim.limitsLAN.set(to.Options.LimitBandwidthInLan)
|
||||
|
||||
sendLimitStr := "is unlimited"
|
||||
recvLimitStr := "is unlimited"
|
||||
if to.Options.MaxSendKbps > 0 {
|
||||
sendLimitStr = fmt.Sprintf("limit is %d KiB/s", to.Options.MaxSendKbps)
|
||||
}
|
||||
if to.Options.MaxRecvKbps > 0 {
|
||||
recvLimitStr = fmt.Sprintf("limit is %d KiB/s", to.Options.MaxRecvKbps)
|
||||
}
|
||||
l.Infof("Overall send rate %s, receive rate %s", sendLimitStr, recvLimitStr)
|
||||
|
||||
if to.Options.LimitBandwidthInLan {
|
||||
l.Infoln("Rate limits apply to LAN connections")
|
||||
} else {
|
||||
l.Infoln("Rate limits do not apply to LAN connections")
|
||||
if limited {
|
||||
if to.Options.LimitBandwidthInLan {
|
||||
l.Infoln("Rate limits apply to LAN connections")
|
||||
} else {
|
||||
l.Infoln("Rate limits do not apply to LAN connections")
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
|
||||
@@ -45,7 +45,7 @@ func lazyInitBenchFileSet() {
|
||||
replace(benchS, protocol.LocalDeviceID, firstHalf)
|
||||
}
|
||||
|
||||
func tempDB() (*db.Instance, string) {
|
||||
func tempDB() (*db.Lowlevel, string) {
|
||||
dir, err := ioutil.TempDir("", "syncthing")
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
@@ -22,14 +22,14 @@ var blockFinder *BlockFinder
|
||||
const maxBatchSize = 1000
|
||||
|
||||
type BlockMap struct {
|
||||
db *Instance
|
||||
db *Lowlevel
|
||||
folder uint32
|
||||
}
|
||||
|
||||
func NewBlockMap(db *Instance, folder uint32) *BlockMap {
|
||||
func NewBlockMap(db *Lowlevel, folder string) *BlockMap {
|
||||
return &BlockMap{
|
||||
db: db,
|
||||
folder: folder,
|
||||
folder: db.folderIdx.ID([]byte(folder)),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -139,10 +139,10 @@ func (m *BlockMap) blockKeyInto(o, hash []byte, file string) []byte {
|
||||
}
|
||||
|
||||
type BlockFinder struct {
|
||||
db *Instance
|
||||
db *Lowlevel
|
||||
}
|
||||
|
||||
func NewBlockFinder(db *Instance) *BlockFinder {
|
||||
func NewBlockFinder(db *Lowlevel) *BlockFinder {
|
||||
if blockFinder != nil {
|
||||
return blockFinder
|
||||
}
|
||||
|
||||
@@ -48,14 +48,14 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
func setup() (*Instance, *BlockFinder) {
|
||||
func setup() (*Lowlevel, *BlockFinder) {
|
||||
// Setup
|
||||
|
||||
db := OpenMemory()
|
||||
return db, NewBlockFinder(db)
|
||||
}
|
||||
|
||||
func dbEmpty(db *Instance) bool {
|
||||
func dbEmpty(db *Lowlevel) bool {
|
||||
iter := db.NewIterator(util.BytesPrefix([]byte{KeyTypeBlock}), nil)
|
||||
defer iter.Release()
|
||||
return !iter.Next()
|
||||
@@ -68,7 +68,7 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
|
||||
t.Fatal("db not empty")
|
||||
}
|
||||
|
||||
m := NewBlockMap(db, db.folderIdx.ID([]byte("folder1")))
|
||||
m := NewBlockMap(db, "folder1")
|
||||
|
||||
f3.Type = protocol.FileInfoTypeDirectory
|
||||
|
||||
@@ -152,8 +152,8 @@ func TestBlockMapAddUpdateWipe(t *testing.T) {
|
||||
func TestBlockFinderLookup(t *testing.T) {
|
||||
db, f := setup()
|
||||
|
||||
m1 := NewBlockMap(db, db.folderIdx.ID([]byte("folder1")))
|
||||
m2 := NewBlockMap(db, db.folderIdx.ID([]byte("folder2")))
|
||||
m1 := NewBlockMap(db, "folder1")
|
||||
m2 := NewBlockMap(db, "folder2")
|
||||
|
||||
err := m1.Add([]protocol.FileInfo{f1})
|
||||
if err != nil {
|
||||
|
||||
@@ -7,160 +7,20 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/fs"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func TestDeviceKey(t *testing.T) {
|
||||
fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
|
||||
dev := []byte("device67890123456789012345678901")
|
||||
name := []byte("name")
|
||||
|
||||
db := OpenMemory()
|
||||
db.folderIdx.ID(fld)
|
||||
db.deviceIdx.ID(dev)
|
||||
|
||||
key := db.deviceKey(fld, dev, name)
|
||||
|
||||
fld2 := db.deviceKeyFolder(key)
|
||||
if !bytes.Equal(fld2, fld) {
|
||||
t.Errorf("wrong folder %q != %q", fld2, fld)
|
||||
}
|
||||
dev2 := db.deviceKeyDevice(key)
|
||||
if !bytes.Equal(dev2, dev) {
|
||||
t.Errorf("wrong device %q != %q", dev2, dev)
|
||||
}
|
||||
name2 := db.deviceKeyName(key)
|
||||
if !bytes.Equal(name2, name) {
|
||||
t.Errorf("wrong name %q != %q", name2, name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalKey(t *testing.T) {
|
||||
fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
|
||||
name := []byte("name")
|
||||
|
||||
db := OpenMemory()
|
||||
db.folderIdx.ID(fld)
|
||||
|
||||
key := db.globalKey(fld, name)
|
||||
|
||||
fld2, ok := db.globalKeyFolder(key)
|
||||
if !ok {
|
||||
t.Error("should have been found")
|
||||
}
|
||||
if !bytes.Equal(fld2, fld) {
|
||||
t.Errorf("wrong folder %q != %q", fld2, fld)
|
||||
}
|
||||
name2 := db.globalKeyName(key)
|
||||
if !bytes.Equal(name2, name) {
|
||||
t.Errorf("wrong name %q != %q", name2, name)
|
||||
}
|
||||
|
||||
_, ok = db.globalKeyFolder([]byte{1, 2, 3, 4, 5})
|
||||
if ok {
|
||||
t.Error("should not have been found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestDropIndexIDs(t *testing.T) {
|
||||
db := OpenMemory()
|
||||
|
||||
d1 := []byte("device67890123456789012345678901")
|
||||
d2 := []byte("device12345678901234567890123456")
|
||||
|
||||
// Set some index IDs
|
||||
|
||||
db.setIndexID(protocol.LocalDeviceID[:], []byte("foo"), 1)
|
||||
db.setIndexID(protocol.LocalDeviceID[:], []byte("bar"), 2)
|
||||
db.setIndexID(d1, []byte("foo"), 3)
|
||||
db.setIndexID(d1, []byte("bar"), 4)
|
||||
db.setIndexID(d2, []byte("foo"), 5)
|
||||
db.setIndexID(d2, []byte("bar"), 6)
|
||||
|
||||
// Verify them
|
||||
|
||||
if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 1 {
|
||||
t.Fatal("fail local 1")
|
||||
}
|
||||
if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 2 {
|
||||
t.Fatal("fail local 2")
|
||||
}
|
||||
if db.getIndexID(d1, []byte("foo")) != 3 {
|
||||
t.Fatal("fail remote 1")
|
||||
}
|
||||
if db.getIndexID(d1, []byte("bar")) != 4 {
|
||||
t.Fatal("fail remote 2")
|
||||
}
|
||||
if db.getIndexID(d2, []byte("foo")) != 5 {
|
||||
t.Fatal("fail remote 3")
|
||||
}
|
||||
if db.getIndexID(d2, []byte("bar")) != 6 {
|
||||
t.Fatal("fail remote 4")
|
||||
}
|
||||
|
||||
// Drop the local ones, verify only they got dropped
|
||||
|
||||
db.DropLocalDeltaIndexIDs()
|
||||
|
||||
if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 0 {
|
||||
t.Fatal("fail local 1")
|
||||
}
|
||||
if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 0 {
|
||||
t.Fatal("fail local 2")
|
||||
}
|
||||
if db.getIndexID(d1, []byte("foo")) != 3 {
|
||||
t.Fatal("fail remote 1")
|
||||
}
|
||||
if db.getIndexID(d1, []byte("bar")) != 4 {
|
||||
t.Fatal("fail remote 2")
|
||||
}
|
||||
if db.getIndexID(d2, []byte("foo")) != 5 {
|
||||
t.Fatal("fail remote 3")
|
||||
}
|
||||
if db.getIndexID(d2, []byte("bar")) != 6 {
|
||||
t.Fatal("fail remote 4")
|
||||
}
|
||||
|
||||
// Set local ones again
|
||||
|
||||
db.setIndexID(protocol.LocalDeviceID[:], []byte("foo"), 1)
|
||||
db.setIndexID(protocol.LocalDeviceID[:], []byte("bar"), 2)
|
||||
|
||||
// Drop the remote ones, verify only they got dropped
|
||||
|
||||
db.DropRemoteDeltaIndexIDs()
|
||||
|
||||
if db.getIndexID(protocol.LocalDeviceID[:], []byte("foo")) != 1 {
|
||||
t.Fatal("fail local 1")
|
||||
}
|
||||
if db.getIndexID(protocol.LocalDeviceID[:], []byte("bar")) != 2 {
|
||||
t.Fatal("fail local 2")
|
||||
}
|
||||
if db.getIndexID(d1, []byte("foo")) != 0 {
|
||||
t.Fatal("fail remote 1")
|
||||
}
|
||||
if db.getIndexID(d1, []byte("bar")) != 0 {
|
||||
t.Fatal("fail remote 2")
|
||||
}
|
||||
if db.getIndexID(d2, []byte("foo")) != 0 {
|
||||
t.Fatal("fail remote 3")
|
||||
}
|
||||
if db.getIndexID(d2, []byte("bar")) != 0 {
|
||||
t.Fatal("fail remote 4")
|
||||
}
|
||||
}
|
||||
|
||||
func TestIgnoredFiles(t *testing.T) {
|
||||
ldb, err := openJSONS("testdata/v0.14.48-ignoredfiles.db.jsons")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, _ := newDBInstance(ldb, "<memory>")
|
||||
db := NewLowlevel(ldb, "<memory>")
|
||||
UpdateSchema(db)
|
||||
|
||||
fs := NewFileSet("test", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), db)
|
||||
|
||||
// The contents of the database are like this:
|
||||
@@ -281,21 +141,23 @@ func TestUpdate0to3(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
db, _ := newDBInstance(ldb, "<memory>")
|
||||
|
||||
db := newInstance(NewLowlevel(ldb, "<memory>"))
|
||||
updater := schemaUpdater{db}
|
||||
|
||||
folder := []byte(update0to3Folder)
|
||||
|
||||
db.updateSchema0to1()
|
||||
updater.updateSchema0to1()
|
||||
|
||||
if _, ok := db.getFile(db.deviceKey(folder, protocol.LocalDeviceID[:], []byte(slashPrefixed))); ok {
|
||||
if _, ok := db.getFile(db.keyer.GenerateDeviceFileKey(nil, folder, protocol.LocalDeviceID[:], []byte(slashPrefixed))); ok {
|
||||
t.Error("File prefixed by '/' was not removed during transition to schema 1")
|
||||
}
|
||||
|
||||
if _, err := db.Get(db.globalKey(folder, []byte(invalid)), nil); err != nil {
|
||||
if _, err := db.Get(db.keyer.GenerateGlobalVersionKey(nil, folder, []byte(invalid)), nil); err != nil {
|
||||
t.Error("Invalid file wasn't added to global list")
|
||||
}
|
||||
|
||||
db.updateSchema1to2()
|
||||
updater.updateSchema1to2()
|
||||
|
||||
found := false
|
||||
db.withHaveSequence(folder, 0, func(fi FileIntf) bool {
|
||||
@@ -316,7 +178,7 @@ func TestUpdate0to3(t *testing.T) {
|
||||
t.Error("Local file wasn't added to sequence bucket", err)
|
||||
}
|
||||
|
||||
db.updateSchema2to3()
|
||||
updater.updateSchema2to3()
|
||||
|
||||
need := map[string]protocol.FileInfo{
|
||||
haveUpdate0to3[remoteDevice0][0].Name: haveUpdate0to3[remoteDevice0][0],
|
||||
@@ -341,22 +203,17 @@ func TestUpdate0to3(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestDowngrade(t *testing.T) {
|
||||
loc := "testdata/downgrade.db"
|
||||
db, err := Open(loc)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
db.Close()
|
||||
os.RemoveAll(loc)
|
||||
}()
|
||||
db := OpenMemory()
|
||||
UpdateSchema(db) // sets the min version etc
|
||||
|
||||
miscDB := NewNamespacedKV(db, string(KeyTypeMiscData))
|
||||
// Bump the database version to something newer than we actually support
|
||||
miscDB := NewMiscDataNamespace(db)
|
||||
miscDB.PutInt64("dbVersion", dbVersion+1)
|
||||
l.Infoln(dbVersion)
|
||||
|
||||
db.Close()
|
||||
db, err = Open(loc)
|
||||
// Pretend we just opened the DB and attempt to update it again
|
||||
err := UpdateSchema(db)
|
||||
|
||||
if err, ok := err.(databaseDowngradeError); !ok {
|
||||
t.Fatal("Expected error due to database downgrade, got", err)
|
||||
} else if err.minSyncthingVersion != dbMinSyncthingVersion {
|
||||
596
lib/db/instance.go
Normal file
596
lib/db/instance.go
Normal file
@@ -0,0 +1,596 @@
|
||||
// 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
type deletionHandler func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator)
|
||||
|
||||
type instance struct {
|
||||
*Lowlevel
|
||||
keyer keyer
|
||||
}
|
||||
|
||||
func newInstance(ll *Lowlevel) *instance {
|
||||
return &instance{
|
||||
Lowlevel: ll,
|
||||
keyer: newDefaultKeyer(ll.folderIdx, ll.deviceIdx),
|
||||
}
|
||||
}
|
||||
|
||||
func (db *instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, meta *metadataTracker) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
var fk []byte
|
||||
var gk []byte
|
||||
for _, f := range fs {
|
||||
name := []byte(f.Name)
|
||||
fk = db.keyer.GenerateDeviceFileKey(fk, folder, device, name)
|
||||
|
||||
// Get and unmarshal the file entry. If it doesn't exist or can't be
|
||||
// unmarshalled we'll add it as a new entry.
|
||||
bs, err := t.Get(fk, nil)
|
||||
var ef FileInfoTruncated
|
||||
if err == nil {
|
||||
err = ef.Unmarshal(bs)
|
||||
}
|
||||
|
||||
// Local flags or the invalid bit might change without the version
|
||||
// being bumped. The IsInvalid() method handles both.
|
||||
if err == nil && ef.Version.Equal(f.Version) && ef.IsInvalid() == f.IsInvalid() {
|
||||
continue
|
||||
}
|
||||
|
||||
devID := protocol.DeviceIDFromBytes(device)
|
||||
if err == nil {
|
||||
meta.removeFile(devID, ef)
|
||||
}
|
||||
meta.addFile(devID, f)
|
||||
|
||||
t.insertFile(fk, folder, device, f)
|
||||
|
||||
gk = db.keyer.GenerateGlobalVersionKey(gk, folder, name)
|
||||
t.updateGlobal(gk, folder, device, f, meta)
|
||||
|
||||
// Write out and reuse the batch every few records, to avoid the batch
|
||||
// growing too large and thus allocating unnecessarily much memory.
|
||||
t.checkFlush()
|
||||
}
|
||||
}
|
||||
|
||||
func (db *instance) addSequences(folder []byte, fs []protocol.FileInfo) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
var sk []byte
|
||||
var dk []byte
|
||||
for _, f := range fs {
|
||||
sk = db.keyer.GenerateSequenceKey(sk, folder, f.Sequence)
|
||||
dk = db.keyer.GenerateDeviceFileKey(dk, folder, protocol.LocalDeviceID[:], []byte(f.Name))
|
||||
t.Put(sk, dk)
|
||||
l.Debugf("adding sequence; folder=%q sequence=%v %v", folder, f.Sequence, f.Name)
|
||||
t.checkFlush()
|
||||
}
|
||||
}
|
||||
|
||||
func (db *instance) removeSequences(folder []byte, fs []protocol.FileInfo) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
var sk []byte
|
||||
for _, f := range fs {
|
||||
t.Delete(db.keyer.GenerateSequenceKey(sk, folder, f.Sequence))
|
||||
l.Debugf("removing sequence; folder=%q sequence=%v %v", folder, f.Sequence, f.Name)
|
||||
t.checkFlush()
|
||||
}
|
||||
}
|
||||
|
||||
func (db *instance) withHave(folder, device, prefix []byte, truncate bool, fn Iterator) {
|
||||
if len(prefix) > 0 {
|
||||
unslashedPrefix := prefix
|
||||
if bytes.HasSuffix(prefix, []byte{'/'}) {
|
||||
unslashedPrefix = unslashedPrefix[:len(unslashedPrefix)-1]
|
||||
} else {
|
||||
prefix = append(prefix, '/')
|
||||
}
|
||||
|
||||
if f, ok := db.getFileTrunc(db.keyer.GenerateDeviceFileKey(nil, folder, device, unslashedPrefix), true); ok && !fn(f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.keyer.GenerateDeviceFileKey(nil, folder, device, prefix)), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
for dbi.Next() {
|
||||
name := db.keyer.NameFromDeviceFileKey(dbi.Key())
|
||||
if len(prefix) > 0 && !bytes.HasPrefix(name, prefix) {
|
||||
return
|
||||
}
|
||||
|
||||
// The iterator function may keep a reference to the unmarshalled
|
||||
// struct, which in turn references the buffer it was unmarshalled
|
||||
// from. dbi.Value() just returns an internal slice that it reuses, so
|
||||
// we need to copy it.
|
||||
f, err := unmarshalTrunc(append([]byte{}, dbi.Value()...), truncate)
|
||||
if err != nil {
|
||||
l.Debugln("unmarshal error:", err)
|
||||
continue
|
||||
}
|
||||
if !fn(f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *instance) withHaveSequence(folder []byte, startSeq int64, fn Iterator) {
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(&util.Range{Start: db.keyer.GenerateSequenceKey(nil, folder, startSeq), Limit: db.keyer.GenerateSequenceKey(nil, folder, maxInt64)}, nil)
|
||||
defer dbi.Release()
|
||||
|
||||
for dbi.Next() {
|
||||
f, ok := db.getFile(dbi.Value())
|
||||
if !ok {
|
||||
l.Debugln("missing file for sequence number", db.keyer.SequenceFromSequenceKey(dbi.Key()))
|
||||
continue
|
||||
}
|
||||
|
||||
if shouldDebug() {
|
||||
if seq := db.keyer.SequenceFromSequenceKey(dbi.Key()); f.Sequence != seq {
|
||||
panic(fmt.Sprintf("sequence index corruption (folder %v, file %v): sequence %d != expected %d", string(folder), f.Name, f.Sequence, seq))
|
||||
}
|
||||
}
|
||||
if !fn(f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *instance) withAllFolderTruncated(folder []byte, fn func(device []byte, f FileInfoTruncated) bool) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.keyer.GenerateDeviceFileKey(nil, folder, nil, nil).WithoutNameAndDevice()), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var gk []byte
|
||||
|
||||
for dbi.Next() {
|
||||
device, ok := db.keyer.DeviceFromDeviceFileKey(dbi.Key())
|
||||
if !ok {
|
||||
// Not having the device in the index is bad. Clear it.
|
||||
t.Delete(dbi.Key())
|
||||
t.checkFlush()
|
||||
continue
|
||||
}
|
||||
var f FileInfoTruncated
|
||||
// The iterator function may keep a reference to the unmarshalled
|
||||
// struct, which in turn references the buffer it was unmarshalled
|
||||
// from. dbi.Value() just returns an internal slice that it reuses, so
|
||||
// we need to copy it.
|
||||
err := f.Unmarshal(append([]byte{}, dbi.Value()...))
|
||||
if err != nil {
|
||||
l.Debugln("unmarshal error:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
switch f.Name {
|
||||
case "", ".", "..", "/": // A few obviously invalid filenames
|
||||
l.Infof("Dropping invalid filename %q from database", f.Name)
|
||||
name := []byte(f.Name)
|
||||
gk = db.keyer.GenerateGlobalVersionKey(gk, folder, name)
|
||||
t.removeFromGlobal(gk, folder, device, name, nil)
|
||||
t.Delete(dbi.Key())
|
||||
t.checkFlush()
|
||||
continue
|
||||
}
|
||||
|
||||
if !fn(device, f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *instance) getFile(key []byte) (protocol.FileInfo, bool) {
|
||||
if f, ok := db.getFileTrunc(key, false); ok {
|
||||
return f.(protocol.FileInfo), true
|
||||
}
|
||||
return protocol.FileInfo{}, false
|
||||
}
|
||||
|
||||
func (db *instance) getFileTrunc(key []byte, trunc bool) (FileIntf, bool) {
|
||||
bs, err := db.Get(key, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
return nil, false
|
||||
}
|
||||
if err != nil {
|
||||
l.Debugln("surprise error:", err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
f, err := unmarshalTrunc(bs, trunc)
|
||||
if err != nil {
|
||||
l.Debugln("unmarshal error:", err)
|
||||
return nil, false
|
||||
}
|
||||
return f, true
|
||||
}
|
||||
|
||||
func (db *instance) getGlobal(folder, file []byte, truncate bool) (FileIntf, bool) {
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
_, _, f, ok := db.getGlobalInto(t, nil, nil, folder, file, truncate)
|
||||
return f, ok
|
||||
}
|
||||
|
||||
func (db *instance) getGlobalInto(t readOnlyTransaction, gk, dk, folder, file []byte, truncate bool) ([]byte, []byte, FileIntf, bool) {
|
||||
gk = db.keyer.GenerateGlobalVersionKey(gk, folder, file)
|
||||
|
||||
bs, err := t.Get(gk, nil)
|
||||
if err != nil {
|
||||
return gk, dk, nil, false
|
||||
}
|
||||
|
||||
vl, ok := unmarshalVersionList(bs)
|
||||
if !ok {
|
||||
return gk, dk, nil, false
|
||||
}
|
||||
|
||||
dk = db.keyer.GenerateDeviceFileKey(dk, folder, vl.Versions[0].Device, file)
|
||||
if fi, ok := db.getFileTrunc(dk, truncate); ok {
|
||||
return gk, dk, fi, true
|
||||
}
|
||||
|
||||
return gk, dk, nil, false
|
||||
}
|
||||
|
||||
func (db *instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator) {
|
||||
if len(prefix) > 0 {
|
||||
unslashedPrefix := prefix
|
||||
if bytes.HasSuffix(prefix, []byte{'/'}) {
|
||||
unslashedPrefix = unslashedPrefix[:len(unslashedPrefix)-1]
|
||||
} else {
|
||||
prefix = append(prefix, '/')
|
||||
}
|
||||
|
||||
if f, ok := db.getGlobal(folder, unslashedPrefix, truncate); ok && !fn(f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.keyer.GenerateGlobalVersionKey(nil, folder, prefix)), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var fk []byte
|
||||
for dbi.Next() {
|
||||
name := db.keyer.NameFromGlobalVersionKey(dbi.Key())
|
||||
if len(prefix) > 0 && !bytes.HasPrefix(name, prefix) {
|
||||
return
|
||||
}
|
||||
|
||||
vl, ok := unmarshalVersionList(dbi.Value())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
fk = db.keyer.GenerateDeviceFileKey(fk, folder, vl.Versions[0].Device, name)
|
||||
|
||||
f, ok := db.getFileTrunc(fk, truncate)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if !fn(f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *instance) availability(folder, file []byte) []protocol.DeviceID {
|
||||
k := db.keyer.GenerateGlobalVersionKey(nil, folder, file)
|
||||
bs, err := db.Get(k, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
l.Debugln("surprise error:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
vl, ok := unmarshalVersionList(bs)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var devices []protocol.DeviceID
|
||||
for _, v := range vl.Versions {
|
||||
if !v.Version.Equal(vl.Versions[0].Version) {
|
||||
break
|
||||
}
|
||||
if v.Invalid {
|
||||
continue
|
||||
}
|
||||
n := protocol.DeviceIDFromBytes(v.Device)
|
||||
devices = append(devices, n)
|
||||
}
|
||||
|
||||
return devices
|
||||
}
|
||||
|
||||
func (db *instance) withNeed(folder, device []byte, truncate bool, fn Iterator) {
|
||||
if bytes.Equal(device, protocol.LocalDeviceID[:]) {
|
||||
db.withNeedLocal(folder, truncate, fn)
|
||||
return
|
||||
}
|
||||
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.keyer.GenerateGlobalVersionKey(nil, folder, nil).WithoutName()), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var fk []byte
|
||||
for dbi.Next() {
|
||||
vl, ok := unmarshalVersionList(dbi.Value())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
haveFV, have := vl.Get(device)
|
||||
// XXX: This marks Concurrent (i.e. conflicting) changes as
|
||||
// needs. Maybe we should do that, but it needs special
|
||||
// handling in the puller.
|
||||
if have && haveFV.Version.GreaterEqual(vl.Versions[0].Version) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := db.keyer.NameFromGlobalVersionKey(dbi.Key())
|
||||
needVersion := vl.Versions[0].Version
|
||||
needDevice := protocol.DeviceIDFromBytes(vl.Versions[0].Device)
|
||||
|
||||
for i := range vl.Versions {
|
||||
if !vl.Versions[i].Version.Equal(needVersion) {
|
||||
// We haven't found a valid copy of the file with the needed version.
|
||||
break
|
||||
}
|
||||
|
||||
if vl.Versions[i].Invalid {
|
||||
// The file is marked invalid, don't use it.
|
||||
continue
|
||||
}
|
||||
|
||||
fk = db.keyer.GenerateDeviceFileKey(fk, folder, vl.Versions[i].Device, name)
|
||||
bs, err := t.Get(fk, nil)
|
||||
if err != nil {
|
||||
l.Debugln("surprise error:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
gf, err := unmarshalTrunc(bs, truncate)
|
||||
if err != nil {
|
||||
l.Debugln("unmarshal error:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if gf.IsDeleted() && !have {
|
||||
// We don't need deleted files that we don't have
|
||||
break
|
||||
}
|
||||
|
||||
l.Debugf("need folder=%q device=%v name=%q have=%v invalid=%v haveV=%v globalV=%v globalDev=%v", folder, protocol.DeviceIDFromBytes(device), name, have, haveFV.Invalid, haveFV.Version, needVersion, needDevice)
|
||||
|
||||
if !fn(gf) {
|
||||
return
|
||||
}
|
||||
|
||||
// This file is handled, no need to look further in the version list
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *instance) withNeedLocal(folder []byte, truncate bool, fn Iterator) {
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.keyer.GenerateNeedFileKey(nil, folder, nil).WithoutName()), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var dk []byte
|
||||
var gk []byte
|
||||
var f FileIntf
|
||||
var ok bool
|
||||
for dbi.Next() {
|
||||
gk, dk, f, ok = db.getGlobalInto(t, gk, dk, folder, db.keyer.NameFromGlobalVersionKey(dbi.Key()), truncate)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !fn(f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *instance) dropFolder(folder []byte) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
for _, key := range [][]byte{
|
||||
// Remove all items related to the given folder from the device->file bucket
|
||||
db.keyer.GenerateDeviceFileKey(nil, folder, nil, nil).WithoutNameAndDevice(),
|
||||
// Remove all sequences related to the folder
|
||||
db.keyer.GenerateSequenceKey(nil, []byte(folder), 0).WithoutSequence(),
|
||||
// Remove all items related to the given folder from the global bucket
|
||||
db.keyer.GenerateGlobalVersionKey(nil, folder, nil).WithoutName(),
|
||||
// Remove all needs related to the folder
|
||||
db.keyer.GenerateNeedFileKey(nil, folder, nil).WithoutName(),
|
||||
} {
|
||||
t.deleteKeyPrefix(key)
|
||||
}
|
||||
}
|
||||
|
||||
func (db *instance) dropDeviceFolder(device, folder []byte, meta *metadataTracker) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.keyer.GenerateDeviceFileKey(nil, folder, device, nil)), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var gk []byte
|
||||
|
||||
for dbi.Next() {
|
||||
key := dbi.Key()
|
||||
name := db.keyer.NameFromDeviceFileKey(key)
|
||||
gk = db.keyer.GenerateGlobalVersionKey(gk, folder, name)
|
||||
t.removeFromGlobal(gk, folder, device, name, meta)
|
||||
t.Delete(key)
|
||||
t.checkFlush()
|
||||
}
|
||||
}
|
||||
|
||||
func (db *instance) checkGlobals(folder []byte, meta *metadataTracker) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.keyer.GenerateGlobalVersionKey(nil, folder, nil).WithoutName()), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var fk []byte
|
||||
for dbi.Next() {
|
||||
vl, ok := unmarshalVersionList(dbi.Value())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check the global version list for consistency. An issue in previous
|
||||
// versions of goleveldb could result in reordered writes so that
|
||||
// there are global entries pointing to no longer existing files. Here
|
||||
// we find those and clear them out.
|
||||
|
||||
name := db.keyer.NameFromGlobalVersionKey(dbi.Key())
|
||||
var newVL VersionList
|
||||
for i, version := range vl.Versions {
|
||||
fk = db.keyer.GenerateDeviceFileKey(fk, folder, version.Device, name)
|
||||
_, err := t.Get(fk, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
l.Debugln("surprise error:", err)
|
||||
return
|
||||
}
|
||||
newVL.Versions = append(newVL.Versions, version)
|
||||
|
||||
if i == 0 {
|
||||
if fi, ok := db.getFile(fk); ok {
|
||||
meta.addFile(protocol.GlobalDeviceID, fi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(newVL.Versions) != len(vl.Versions) {
|
||||
t.Put(dbi.Key(), mustMarshal(&newVL))
|
||||
t.checkFlush()
|
||||
}
|
||||
}
|
||||
l.Debugf("db check completed for %q", folder)
|
||||
}
|
||||
|
||||
func (db *instance) getIndexID(device, folder []byte) protocol.IndexID {
|
||||
key := db.keyer.GenerateIndexIDKey(nil, device, folder)
|
||||
cur, err := db.Get(key, nil)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
var id protocol.IndexID
|
||||
if err := id.Unmarshal(cur); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (db *instance) setIndexID(device, folder []byte, id protocol.IndexID) {
|
||||
key := db.keyer.GenerateIndexIDKey(nil, device, folder)
|
||||
bs, _ := id.Marshal() // marshalling can't fail
|
||||
if err := db.Put(key, bs, nil); err != nil {
|
||||
panic("storing index ID: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (db *instance) dropMtimes(folder []byte) {
|
||||
db.dropPrefix(db.keyer.GenerateMtimesKey(nil, folder))
|
||||
}
|
||||
|
||||
func (db *instance) dropFolderMeta(folder []byte) {
|
||||
db.dropPrefix(db.keyer.GenerateFolderMetaKey(nil, folder))
|
||||
}
|
||||
|
||||
func (db *instance) dropPrefix(prefix []byte) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(prefix), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
for dbi.Next() {
|
||||
t.Delete(dbi.Key())
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalTrunc(bs []byte, truncate bool) (FileIntf, error) {
|
||||
if truncate {
|
||||
var tf FileInfoTruncated
|
||||
err := tf.Unmarshal(bs)
|
||||
return tf, err
|
||||
}
|
||||
|
||||
var tf protocol.FileInfo
|
||||
err := tf.Unmarshal(bs)
|
||||
return tf, err
|
||||
}
|
||||
|
||||
func unmarshalVersionList(data []byte) (VersionList, bool) {
|
||||
var vl VersionList
|
||||
if err := vl.Unmarshal(data); err != nil {
|
||||
l.Debugln("unmarshal error:", err)
|
||||
return VersionList{}, false
|
||||
}
|
||||
if len(vl.Versions) == 0 {
|
||||
l.Debugln("empty version list")
|
||||
return VersionList{}, false
|
||||
}
|
||||
return vl, true
|
||||
}
|
||||
|
||||
type errorSuggestion struct {
|
||||
inner error
|
||||
suggestion string
|
||||
}
|
||||
|
||||
func (e errorSuggestion) Error() string {
|
||||
return fmt.Sprintf("%s (%s)", e.inner.Error(), e.suggestion)
|
||||
}
|
||||
202
lib/db/keyer.go
Normal file
202
lib/db/keyer.go
Normal file
@@ -0,0 +1,202 @@
|
||||
// Copyright (C) 2018 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
)
|
||||
|
||||
const (
|
||||
keyPrefixLen = 1
|
||||
keyFolderLen = 4 // indexed
|
||||
keyDeviceLen = 4 // indexed
|
||||
keySequenceLen = 8
|
||||
keyHashLen = 32
|
||||
|
||||
maxInt64 int64 = 1<<63 - 1
|
||||
)
|
||||
|
||||
const (
|
||||
KeyTypeDevice = 0
|
||||
KeyTypeGlobal = 1
|
||||
KeyTypeBlock = 2
|
||||
KeyTypeDeviceStatistic = 3
|
||||
KeyTypeFolderStatistic = 4
|
||||
KeyTypeVirtualMtime = 5
|
||||
KeyTypeFolderIdx = 6
|
||||
KeyTypeDeviceIdx = 7
|
||||
KeyTypeIndexID = 8
|
||||
KeyTypeFolderMeta = 9
|
||||
KeyTypeMiscData = 10
|
||||
KeyTypeSequence = 11
|
||||
KeyTypeNeed = 12
|
||||
)
|
||||
|
||||
type keyer interface {
|
||||
// device file key stuff
|
||||
GenerateDeviceFileKey(key, folder, device, name []byte) deviceFileKey
|
||||
NameFromDeviceFileKey(key []byte) []byte
|
||||
DeviceFromDeviceFileKey(key []byte) ([]byte, bool)
|
||||
FolderFromDeviceFileKey(key []byte) ([]byte, bool)
|
||||
|
||||
// global version key stuff
|
||||
GenerateGlobalVersionKey(key, folder, name []byte) globalVersionKey
|
||||
NameFromGlobalVersionKey(key []byte) []byte
|
||||
FolderFromGlobalVersionKey(key []byte) ([]byte, bool)
|
||||
|
||||
// file need index
|
||||
GenerateNeedFileKey(key, folder, name []byte) needFileKey
|
||||
|
||||
// file sequence index
|
||||
GenerateSequenceKey(key, folder []byte, seq int64) sequenceKey
|
||||
SequenceFromSequenceKey(key []byte) int64
|
||||
|
||||
// index IDs
|
||||
GenerateIndexIDKey(key, device, folder []byte) indexIDKey
|
||||
DeviceFromIndexIDKey(key []byte) ([]byte, bool)
|
||||
|
||||
// Mtimes
|
||||
GenerateMtimesKey(key, folder []byte) mtimesKey
|
||||
|
||||
// Folder metadata
|
||||
GenerateFolderMetaKey(key, folder []byte) folderMetaKey
|
||||
}
|
||||
|
||||
// defaultKeyer implements our key scheme. It needs folder and device
|
||||
// indexes.
|
||||
type defaultKeyer struct {
|
||||
folderIdx *smallIndex
|
||||
deviceIdx *smallIndex
|
||||
}
|
||||
|
||||
func newDefaultKeyer(folderIdx, deviceIdx *smallIndex) defaultKeyer {
|
||||
return defaultKeyer{
|
||||
folderIdx: folderIdx,
|
||||
deviceIdx: deviceIdx,
|
||||
}
|
||||
}
|
||||
|
||||
type deviceFileKey []byte
|
||||
|
||||
func (k deviceFileKey) WithoutNameAndDevice() []byte {
|
||||
return k[:keyPrefixLen+keyFolderLen]
|
||||
}
|
||||
|
||||
func (k defaultKeyer) GenerateDeviceFileKey(key, folder, device, name []byte) deviceFileKey {
|
||||
key = resize(key, keyPrefixLen+keyFolderLen+keyDeviceLen+len(name))
|
||||
key[0] = KeyTypeDevice
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], k.folderIdx.ID(folder))
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen+keyFolderLen:], k.deviceIdx.ID(device))
|
||||
copy(key[keyPrefixLen+keyFolderLen+keyDeviceLen:], name)
|
||||
return key
|
||||
}
|
||||
|
||||
func (k defaultKeyer) NameFromDeviceFileKey(key []byte) []byte {
|
||||
return key[keyPrefixLen+keyFolderLen+keyDeviceLen:]
|
||||
}
|
||||
|
||||
func (k defaultKeyer) DeviceFromDeviceFileKey(key []byte) ([]byte, bool) {
|
||||
return k.deviceIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen+keyFolderLen:]))
|
||||
}
|
||||
|
||||
func (k defaultKeyer) FolderFromDeviceFileKey(key []byte) ([]byte, bool) {
|
||||
return k.folderIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen:]))
|
||||
}
|
||||
|
||||
type globalVersionKey []byte
|
||||
|
||||
func (k globalVersionKey) WithoutName() []byte {
|
||||
return k[:keyPrefixLen+keyFolderLen]
|
||||
}
|
||||
|
||||
func (k defaultKeyer) GenerateGlobalVersionKey(key, folder, name []byte) globalVersionKey {
|
||||
key = resize(key, keyPrefixLen+keyFolderLen+len(name))
|
||||
key[0] = KeyTypeGlobal
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], k.folderIdx.ID(folder))
|
||||
copy(key[keyPrefixLen+keyFolderLen:], name)
|
||||
return key
|
||||
}
|
||||
|
||||
func (k defaultKeyer) NameFromGlobalVersionKey(key []byte) []byte {
|
||||
return key[keyPrefixLen+keyFolderLen:]
|
||||
}
|
||||
|
||||
func (k defaultKeyer) FolderFromGlobalVersionKey(key []byte) ([]byte, bool) {
|
||||
return k.folderIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen:]))
|
||||
}
|
||||
|
||||
type needFileKey []byte
|
||||
|
||||
func (k needFileKey) WithoutName() []byte {
|
||||
return k[:keyPrefixLen+keyFolderLen]
|
||||
}
|
||||
|
||||
func (k defaultKeyer) GenerateNeedFileKey(key, folder, name []byte) needFileKey {
|
||||
key = resize(key, keyPrefixLen+keyFolderLen+len(name))
|
||||
key[0] = KeyTypeNeed
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], k.folderIdx.ID(folder))
|
||||
copy(key[keyPrefixLen+keyFolderLen:], name)
|
||||
return key
|
||||
}
|
||||
|
||||
type sequenceKey []byte
|
||||
|
||||
func (k sequenceKey) WithoutSequence() []byte {
|
||||
return k[:keyPrefixLen+keyFolderLen]
|
||||
}
|
||||
|
||||
func (k defaultKeyer) GenerateSequenceKey(key, folder []byte, seq int64) sequenceKey {
|
||||
key = resize(key, keyPrefixLen+keyFolderLen+keySequenceLen)
|
||||
key[0] = KeyTypeSequence
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], k.folderIdx.ID(folder))
|
||||
binary.BigEndian.PutUint64(key[keyPrefixLen+keyFolderLen:], uint64(seq))
|
||||
return key
|
||||
}
|
||||
|
||||
func (k defaultKeyer) SequenceFromSequenceKey(key []byte) int64 {
|
||||
return int64(binary.BigEndian.Uint64(key[keyPrefixLen+keyFolderLen:]))
|
||||
}
|
||||
|
||||
type indexIDKey []byte
|
||||
|
||||
func (k defaultKeyer) GenerateIndexIDKey(key, device, folder []byte) indexIDKey {
|
||||
key = resize(key, keyPrefixLen+keyDeviceLen+keyFolderLen)
|
||||
key[0] = KeyTypeIndexID
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], k.deviceIdx.ID(device))
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen+keyDeviceLen:], k.folderIdx.ID(folder))
|
||||
return key
|
||||
}
|
||||
|
||||
func (k defaultKeyer) DeviceFromIndexIDKey(key []byte) ([]byte, bool) {
|
||||
return k.deviceIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen:]))
|
||||
}
|
||||
|
||||
type mtimesKey []byte
|
||||
|
||||
func (k defaultKeyer) GenerateMtimesKey(key, folder []byte) mtimesKey {
|
||||
key = resize(key, keyPrefixLen+keyFolderLen)
|
||||
key[0] = KeyTypeVirtualMtime
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], k.folderIdx.ID(folder))
|
||||
return key
|
||||
}
|
||||
|
||||
type folderMetaKey []byte
|
||||
|
||||
func (k defaultKeyer) GenerateFolderMetaKey(key, folder []byte) folderMetaKey {
|
||||
key = resize(key, keyPrefixLen+keyFolderLen)
|
||||
key[0] = KeyTypeFolderMeta
|
||||
binary.BigEndian.PutUint32(key[keyPrefixLen:], k.folderIdx.ID(folder))
|
||||
return key
|
||||
}
|
||||
|
||||
// resize returns a byte slice of the specified size, reusing bs if possible
|
||||
func resize(bs []byte, size int) []byte {
|
||||
if cap(bs) < size {
|
||||
return make([]byte, size)
|
||||
}
|
||||
return bs[:size]
|
||||
}
|
||||
80
lib/db/keyer_test.go
Normal file
80
lib/db/keyer_test.go
Normal file
@@ -0,0 +1,80 @@
|
||||
// Copyright (C) 2018 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDeviceKey(t *testing.T) {
|
||||
fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
|
||||
dev := []byte("device67890123456789012345678901")
|
||||
name := []byte("name")
|
||||
|
||||
db := newInstance(OpenMemory())
|
||||
|
||||
key := db.keyer.GenerateDeviceFileKey(nil, fld, dev, name)
|
||||
|
||||
fld2, ok := db.keyer.FolderFromDeviceFileKey(key)
|
||||
if !ok {
|
||||
t.Fatal("unexpectedly not found")
|
||||
}
|
||||
if !bytes.Equal(fld2, fld) {
|
||||
t.Errorf("wrong folder %q != %q", fld2, fld)
|
||||
}
|
||||
dev2, ok := db.keyer.DeviceFromDeviceFileKey(key)
|
||||
if !ok {
|
||||
t.Fatal("unexpectedly not found")
|
||||
}
|
||||
if !bytes.Equal(dev2, dev) {
|
||||
t.Errorf("wrong device %q != %q", dev2, dev)
|
||||
}
|
||||
name2 := db.keyer.NameFromDeviceFileKey(key)
|
||||
if !bytes.Equal(name2, name) {
|
||||
t.Errorf("wrong name %q != %q", name2, name)
|
||||
}
|
||||
}
|
||||
|
||||
func TestGlobalKey(t *testing.T) {
|
||||
fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
|
||||
name := []byte("name")
|
||||
|
||||
db := newInstance(OpenMemory())
|
||||
|
||||
key := db.keyer.GenerateGlobalVersionKey(nil, fld, name)
|
||||
|
||||
fld2, ok := db.keyer.FolderFromGlobalVersionKey(key)
|
||||
if !ok {
|
||||
t.Error("should have been found")
|
||||
}
|
||||
if !bytes.Equal(fld2, fld) {
|
||||
t.Errorf("wrong folder %q != %q", fld2, fld)
|
||||
}
|
||||
name2 := db.keyer.NameFromGlobalVersionKey(key)
|
||||
if !bytes.Equal(name2, name) {
|
||||
t.Errorf("wrong name %q != %q", name2, name)
|
||||
}
|
||||
|
||||
_, ok = db.keyer.FolderFromGlobalVersionKey([]byte{1, 2, 3, 4, 5})
|
||||
if ok {
|
||||
t.Error("should not have been found")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSequenceKey(t *testing.T) {
|
||||
fld := []byte("folder6789012345678901234567890123456789012345678901234567890123")
|
||||
|
||||
db := newInstance(OpenMemory())
|
||||
|
||||
const seq = 1234567890
|
||||
key := db.keyer.GenerateSequenceKey(nil, fld, seq)
|
||||
outSeq := db.keyer.SequenceFromSequenceKey(key)
|
||||
if outSeq != seq {
|
||||
t.Errorf("sequence number mangled, %d != %d", outSeq, seq)
|
||||
}
|
||||
}
|
||||
@@ -1,134 +0,0 @@
|
||||
// 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
const (
|
||||
KeyTypeDevice = iota
|
||||
KeyTypeGlobal
|
||||
KeyTypeBlock
|
||||
KeyTypeDeviceStatistic
|
||||
KeyTypeFolderStatistic
|
||||
KeyTypeVirtualMtime
|
||||
KeyTypeFolderIdx
|
||||
KeyTypeDeviceIdx
|
||||
KeyTypeIndexID
|
||||
KeyTypeFolderMeta
|
||||
KeyTypeMiscData
|
||||
KeyTypeSequence
|
||||
KeyTypeNeed
|
||||
)
|
||||
|
||||
func (vl VersionList) String() string {
|
||||
var b bytes.Buffer
|
||||
var id protocol.DeviceID
|
||||
b.WriteString("{")
|
||||
for i, v := range vl.Versions {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
copy(id[:], v.Device)
|
||||
fmt.Fprintf(&b, "{%v, %v}", v.Version, id)
|
||||
}
|
||||
b.WriteString("}")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// update brings the VersionList up to date with file. It returns the updated
|
||||
// VersionList, a potentially removed old FileVersion and its index, as well as
|
||||
// the index where the new FileVersion was inserted.
|
||||
func (vl VersionList) update(folder, device []byte, file protocol.FileInfo, db *Instance) (_ VersionList, removedFV FileVersion, removedAt int, insertedAt int) {
|
||||
removedAt, insertedAt = -1, -1
|
||||
for i, v := range vl.Versions {
|
||||
if bytes.Equal(v.Device, device) {
|
||||
removedAt = i
|
||||
removedFV = v
|
||||
vl.Versions = append(vl.Versions[:i], vl.Versions[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
nv := FileVersion{
|
||||
Device: device,
|
||||
Version: file.Version,
|
||||
Invalid: file.IsInvalid(),
|
||||
}
|
||||
for i, v := range vl.Versions {
|
||||
switch v.Version.Compare(file.Version) {
|
||||
case protocol.Equal:
|
||||
if nv.Invalid {
|
||||
continue
|
||||
}
|
||||
fallthrough
|
||||
|
||||
case protocol.Lesser:
|
||||
// The version at this point in the list is equal to or lesser
|
||||
// ("older") than us. We insert ourselves in front of it.
|
||||
vl = vl.insertAt(i, nv)
|
||||
return vl, removedFV, removedAt, i
|
||||
|
||||
case protocol.ConcurrentLesser, protocol.ConcurrentGreater:
|
||||
// The version at this point is in conflict with us. We must pull
|
||||
// the actual file metadata to determine who wins. If we win, we
|
||||
// insert ourselves in front of the loser here. (The "Lesser" and
|
||||
// "Greater" in the condition above is just based on the device
|
||||
// IDs in the version vector, which is not the only thing we use
|
||||
// to determine the winner.)
|
||||
//
|
||||
// A surprise missing file entry here is counted as a win for us.
|
||||
if of, ok := db.getFile(db.deviceKey(folder, v.Device, []byte(file.Name))); !ok || file.WinsConflict(of) {
|
||||
vl = vl.insertAt(i, nv)
|
||||
return vl, removedFV, removedAt, i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't find a position for an insert above, so append to the end.
|
||||
vl.Versions = append(vl.Versions, nv)
|
||||
|
||||
return vl, removedFV, removedAt, len(vl.Versions) - 1
|
||||
}
|
||||
|
||||
func (vl VersionList) insertAt(i int, v FileVersion) VersionList {
|
||||
vl.Versions = append(vl.Versions, FileVersion{})
|
||||
copy(vl.Versions[i+1:], vl.Versions[i:])
|
||||
vl.Versions[i] = v
|
||||
return vl
|
||||
}
|
||||
|
||||
func (vl VersionList) Get(device []byte) (FileVersion, bool) {
|
||||
for _, v := range vl.Versions {
|
||||
if bytes.Equal(v.Device, device) {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
|
||||
return FileVersion{}, false
|
||||
}
|
||||
|
||||
type fileList []protocol.FileInfo
|
||||
|
||||
func (fl fileList) Len() int {
|
||||
return len(fl)
|
||||
}
|
||||
|
||||
func (fl fileList) Swap(a, b int) {
|
||||
fl[a], fl[b] = fl[b], fl[a]
|
||||
}
|
||||
|
||||
func (fl fileList) Less(a, b int) bool {
|
||||
return fl[a].Name < fl[b].Name
|
||||
}
|
||||
|
||||
// Flush batches to disk when they contain this many records.
|
||||
const batchFlushSize = 64
|
||||
@@ -1,956 +0,0 @@
|
||||
// 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/binary"
|
||||
"fmt"
|
||||
"os"
|
||||
"sort"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb/iterator"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
type deletionHandler func(t readWriteTransaction, folder, device, name []byte, dbi iterator.Iterator)
|
||||
|
||||
type Instance struct {
|
||||
committed int64 // this must be the first attribute in the struct to ensure 64 bit alignment on 32 bit plaforms
|
||||
*leveldb.DB
|
||||
location string
|
||||
folderIdx *smallIndex
|
||||
deviceIdx *smallIndex
|
||||
}
|
||||
|
||||
const (
|
||||
keyPrefixLen = 1
|
||||
keyFolderLen = 4 // indexed
|
||||
keyDeviceLen = 4 // indexed
|
||||
keySequenceLen = 8
|
||||
keyHashLen = 32
|
||||
|
||||
maxInt64 int64 = 1<<63 - 1
|
||||
)
|
||||
|
||||
func Open(file string) (*Instance, error) {
|
||||
opts := &opt.Options{
|
||||
OpenFilesCacheCapacity: 100,
|
||||
WriteBuffer: 4 << 20,
|
||||
}
|
||||
|
||||
db, err := leveldb.OpenFile(file, opts)
|
||||
if leveldbIsCorrupted(err) {
|
||||
db, err = leveldb.RecoverFile(file, opts)
|
||||
}
|
||||
if leveldbIsCorrupted(err) {
|
||||
// The database is corrupted, and we've tried to recover it but it
|
||||
// didn't work. At this point there isn't much to do beyond dropping
|
||||
// the database and reindexing...
|
||||
l.Infoln("Database corruption detected, unable to recover. Reinitializing...")
|
||||
if err := os.RemoveAll(file); err != nil {
|
||||
return nil, errorSuggestion{err, "failed to delete corrupted database"}
|
||||
}
|
||||
db, err = leveldb.OpenFile(file, opts)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errorSuggestion{err, "is another instance of Syncthing running?"}
|
||||
}
|
||||
|
||||
return newDBInstance(db, file)
|
||||
}
|
||||
|
||||
func OpenMemory() *Instance {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
ldb, _ := newDBInstance(db, "<memory>")
|
||||
return ldb
|
||||
}
|
||||
|
||||
func newDBInstance(db *leveldb.DB, location string) (*Instance, error) {
|
||||
i := &Instance{
|
||||
DB: db,
|
||||
location: location,
|
||||
}
|
||||
i.folderIdx = newSmallIndex(i, []byte{KeyTypeFolderIdx})
|
||||
i.deviceIdx = newSmallIndex(i, []byte{KeyTypeDeviceIdx})
|
||||
err := i.updateSchema()
|
||||
return i, err
|
||||
}
|
||||
|
||||
// Committed returns the number of items committed to the database since startup
|
||||
func (db *Instance) Committed() int64 {
|
||||
return atomic.LoadInt64(&db.committed)
|
||||
}
|
||||
|
||||
// Location returns the filesystem path where the database is stored
|
||||
func (db *Instance) Location() string {
|
||||
return db.location
|
||||
}
|
||||
|
||||
func (db *Instance) updateFiles(folder, device []byte, fs []protocol.FileInfo, meta *metadataTracker) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
var fk []byte
|
||||
var gk []byte
|
||||
for _, f := range fs {
|
||||
name := []byte(f.Name)
|
||||
fk = db.deviceKeyInto(fk, folder, device, name)
|
||||
|
||||
// Get and unmarshal the file entry. If it doesn't exist or can't be
|
||||
// unmarshalled we'll add it as a new entry.
|
||||
bs, err := t.Get(fk, nil)
|
||||
var ef FileInfoTruncated
|
||||
if err == nil {
|
||||
err = ef.Unmarshal(bs)
|
||||
}
|
||||
|
||||
// Local flags or the invalid bit might change without the version
|
||||
// being bumped. The IsInvalid() method handles both.
|
||||
if err == nil && ef.Version.Equal(f.Version) && ef.IsInvalid() == f.IsInvalid() {
|
||||
continue
|
||||
}
|
||||
|
||||
devID := protocol.DeviceIDFromBytes(device)
|
||||
if err == nil {
|
||||
meta.removeFile(devID, ef)
|
||||
}
|
||||
meta.addFile(devID, f)
|
||||
|
||||
t.insertFile(fk, folder, device, f)
|
||||
|
||||
gk = db.globalKeyInto(gk, folder, name)
|
||||
t.updateGlobal(gk, folder, device, f, meta)
|
||||
|
||||
// Write out and reuse the batch every few records, to avoid the batch
|
||||
// growing too large and thus allocating unnecessarily much memory.
|
||||
t.checkFlush()
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) addSequences(folder []byte, fs []protocol.FileInfo) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
var sk []byte
|
||||
var dk []byte
|
||||
for _, f := range fs {
|
||||
sk = db.sequenceKeyInto(sk, folder, f.Sequence)
|
||||
dk = db.deviceKeyInto(dk, folder, protocol.LocalDeviceID[:], []byte(f.Name))
|
||||
t.Put(sk, dk)
|
||||
l.Debugf("adding sequence; folder=%q sequence=%v %v", folder, f.Sequence, f.Name)
|
||||
t.checkFlush()
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) removeSequences(folder []byte, fs []protocol.FileInfo) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
var sk []byte
|
||||
for _, f := range fs {
|
||||
t.Delete(db.sequenceKeyInto(sk, folder, f.Sequence))
|
||||
l.Debugf("removing sequence; folder=%q sequence=%v %v", folder, f.Sequence, f.Name)
|
||||
t.checkFlush()
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) withHave(folder, device, prefix []byte, truncate bool, fn Iterator) {
|
||||
if len(prefix) > 0 {
|
||||
unslashedPrefix := prefix
|
||||
if bytes.HasSuffix(prefix, []byte{'/'}) {
|
||||
unslashedPrefix = unslashedPrefix[:len(unslashedPrefix)-1]
|
||||
} else {
|
||||
prefix = append(prefix, '/')
|
||||
}
|
||||
|
||||
if f, ok := db.getFileTrunc(db.deviceKey(folder, device, unslashedPrefix), true); ok && !fn(f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.deviceKey(folder, device, prefix)[:keyPrefixLen+keyFolderLen+keyDeviceLen+len(prefix)]), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
for dbi.Next() {
|
||||
name := db.deviceKeyName(dbi.Key())
|
||||
if len(prefix) > 0 && !bytes.HasPrefix(name, prefix) {
|
||||
return
|
||||
}
|
||||
|
||||
// The iterator function may keep a reference to the unmarshalled
|
||||
// struct, which in turn references the buffer it was unmarshalled
|
||||
// from. dbi.Value() just returns an internal slice that it reuses, so
|
||||
// we need to copy it.
|
||||
f, err := unmarshalTrunc(append([]byte{}, dbi.Value()...), truncate)
|
||||
if err != nil {
|
||||
l.Debugln("unmarshal error:", err)
|
||||
continue
|
||||
}
|
||||
if !fn(f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) withHaveSequence(folder []byte, startSeq int64, fn Iterator) {
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(&util.Range{Start: db.sequenceKey(folder, startSeq), Limit: db.sequenceKey(folder, maxInt64)}, nil)
|
||||
defer dbi.Release()
|
||||
|
||||
for dbi.Next() {
|
||||
f, ok := db.getFile(dbi.Value())
|
||||
if !ok {
|
||||
l.Debugln("missing file for sequence number", db.sequenceKeySequence(dbi.Key()))
|
||||
continue
|
||||
}
|
||||
|
||||
if shouldDebug() {
|
||||
key := dbi.Key()
|
||||
seq := int64(binary.BigEndian.Uint64(key[keyPrefixLen+keyFolderLen:]))
|
||||
if f.Sequence != seq {
|
||||
panic(fmt.Sprintf("sequence index corruption, file sequence %d != expected %d", f.Sequence, seq))
|
||||
}
|
||||
}
|
||||
if !fn(f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) withAllFolderTruncated(folder []byte, fn func(device []byte, f FileInfoTruncated) bool) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.deviceKey(folder, nil, nil)[:keyPrefixLen+keyFolderLen]), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var gk []byte
|
||||
|
||||
for dbi.Next() {
|
||||
device := db.deviceKeyDevice(dbi.Key())
|
||||
var f FileInfoTruncated
|
||||
// The iterator function may keep a reference to the unmarshalled
|
||||
// struct, which in turn references the buffer it was unmarshalled
|
||||
// from. dbi.Value() just returns an internal slice that it reuses, so
|
||||
// we need to copy it.
|
||||
err := f.Unmarshal(append([]byte{}, dbi.Value()...))
|
||||
if err != nil {
|
||||
l.Debugln("unmarshal error:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
switch f.Name {
|
||||
case "", ".", "..", "/": // A few obviously invalid filenames
|
||||
l.Infof("Dropping invalid filename %q from database", f.Name)
|
||||
name := []byte(f.Name)
|
||||
gk = db.globalKeyInto(gk, folder, name)
|
||||
t.removeFromGlobal(gk, folder, device, name, nil)
|
||||
t.Delete(dbi.Key())
|
||||
t.checkFlush()
|
||||
continue
|
||||
}
|
||||
|
||||
if !fn(device, f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) getFile(key []byte) (protocol.FileInfo, bool) {
|
||||
if f, ok := db.getFileTrunc(key, false); ok {
|
||||
return f.(protocol.FileInfo), true
|
||||
}
|
||||
return protocol.FileInfo{}, false
|
||||
}
|
||||
|
||||
func (db *Instance) getFileTrunc(key []byte, trunc bool) (FileIntf, bool) {
|
||||
bs, err := db.Get(key, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
return nil, false
|
||||
}
|
||||
if err != nil {
|
||||
l.Debugln("surprise error:", err)
|
||||
return nil, false
|
||||
}
|
||||
|
||||
f, err := unmarshalTrunc(bs, trunc)
|
||||
if err != nil {
|
||||
l.Debugln("unmarshal error:", err)
|
||||
return nil, false
|
||||
}
|
||||
return f, true
|
||||
}
|
||||
|
||||
func (db *Instance) getGlobal(folder, file []byte, truncate bool) (FileIntf, bool) {
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
_, _, f, ok := db.getGlobalInto(t, nil, nil, folder, file, truncate)
|
||||
return f, ok
|
||||
}
|
||||
|
||||
func (db *Instance) getGlobalInto(t readOnlyTransaction, gk, dk, folder, file []byte, truncate bool) ([]byte, []byte, FileIntf, bool) {
|
||||
gk = db.globalKeyInto(gk, folder, file)
|
||||
|
||||
bs, err := t.Get(gk, nil)
|
||||
if err != nil {
|
||||
return gk, dk, nil, false
|
||||
}
|
||||
|
||||
vl, ok := unmarshalVersionList(bs)
|
||||
if !ok {
|
||||
return gk, dk, nil, false
|
||||
}
|
||||
|
||||
dk = db.deviceKeyInto(dk, folder, vl.Versions[0].Device, file)
|
||||
if fi, ok := db.getFileTrunc(dk, truncate); ok {
|
||||
return gk, dk, fi, true
|
||||
}
|
||||
|
||||
return gk, dk, nil, false
|
||||
}
|
||||
|
||||
func (db *Instance) withGlobal(folder, prefix []byte, truncate bool, fn Iterator) {
|
||||
if len(prefix) > 0 {
|
||||
unslashedPrefix := prefix
|
||||
if bytes.HasSuffix(prefix, []byte{'/'}) {
|
||||
unslashedPrefix = unslashedPrefix[:len(unslashedPrefix)-1]
|
||||
} else {
|
||||
prefix = append(prefix, '/')
|
||||
}
|
||||
|
||||
if f, ok := db.getGlobal(folder, unslashedPrefix, truncate); ok && !fn(f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.globalKey(folder, prefix)), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var fk []byte
|
||||
for dbi.Next() {
|
||||
name := db.globalKeyName(dbi.Key())
|
||||
if len(prefix) > 0 && !bytes.HasPrefix(name, prefix) {
|
||||
return
|
||||
}
|
||||
|
||||
vl, ok := unmarshalVersionList(dbi.Value())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
fk = db.deviceKeyInto(fk, folder, vl.Versions[0].Device, name)
|
||||
|
||||
f, ok := db.getFileTrunc(fk, truncate)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
if !fn(f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) availability(folder, file []byte) []protocol.DeviceID {
|
||||
k := db.globalKey(folder, file)
|
||||
bs, err := db.Get(k, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
return nil
|
||||
}
|
||||
if err != nil {
|
||||
l.Debugln("surprise error:", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
vl, ok := unmarshalVersionList(bs)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
var devices []protocol.DeviceID
|
||||
for _, v := range vl.Versions {
|
||||
if !v.Version.Equal(vl.Versions[0].Version) {
|
||||
break
|
||||
}
|
||||
if v.Invalid {
|
||||
continue
|
||||
}
|
||||
n := protocol.DeviceIDFromBytes(v.Device)
|
||||
devices = append(devices, n)
|
||||
}
|
||||
|
||||
return devices
|
||||
}
|
||||
|
||||
func (db *Instance) withNeed(folder, device []byte, truncate bool, fn Iterator) {
|
||||
if bytes.Equal(device, protocol.LocalDeviceID[:]) {
|
||||
db.withNeedLocal(folder, truncate, fn)
|
||||
return
|
||||
}
|
||||
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.globalKey(folder, nil)[:keyPrefixLen+keyFolderLen]), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var fk []byte
|
||||
for dbi.Next() {
|
||||
vl, ok := unmarshalVersionList(dbi.Value())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
haveFV, have := vl.Get(device)
|
||||
// XXX: This marks Concurrent (i.e. conflicting) changes as
|
||||
// needs. Maybe we should do that, but it needs special
|
||||
// handling in the puller.
|
||||
if have && haveFV.Version.GreaterEqual(vl.Versions[0].Version) {
|
||||
continue
|
||||
}
|
||||
|
||||
name := db.globalKeyName(dbi.Key())
|
||||
needVersion := vl.Versions[0].Version
|
||||
needDevice := protocol.DeviceIDFromBytes(vl.Versions[0].Device)
|
||||
|
||||
for i := range vl.Versions {
|
||||
if !vl.Versions[i].Version.Equal(needVersion) {
|
||||
// We haven't found a valid copy of the file with the needed version.
|
||||
break
|
||||
}
|
||||
|
||||
if vl.Versions[i].Invalid {
|
||||
// The file is marked invalid, don't use it.
|
||||
continue
|
||||
}
|
||||
|
||||
fk = db.deviceKeyInto(fk, folder, vl.Versions[i].Device, name)
|
||||
bs, err := t.Get(fk, nil)
|
||||
if err != nil {
|
||||
l.Debugln("surprise error:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
gf, err := unmarshalTrunc(bs, truncate)
|
||||
if err != nil {
|
||||
l.Debugln("unmarshal error:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
if gf.IsDeleted() && !have {
|
||||
// We don't need deleted files that we don't have
|
||||
break
|
||||
}
|
||||
|
||||
l.Debugf("need folder=%q device=%v name=%q have=%v invalid=%v haveV=%v globalV=%v globalDev=%v", folder, protocol.DeviceIDFromBytes(device), name, have, haveFV.Invalid, haveFV.Version, needVersion, needDevice)
|
||||
|
||||
if !fn(gf) {
|
||||
return
|
||||
}
|
||||
|
||||
// This file is handled, no need to look further in the version list
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) withNeedLocal(folder []byte, truncate bool, fn Iterator) {
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.needKey(folder, nil)[:keyPrefixLen+keyFolderLen]), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var dk []byte
|
||||
var gk []byte
|
||||
var f FileIntf
|
||||
var ok bool
|
||||
for dbi.Next() {
|
||||
gk, dk, f, ok = db.getGlobalInto(t, gk, dk, folder, db.globalKeyName(dbi.Key()), truncate)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if !fn(f) {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) ListFolders() []string {
|
||||
t := db.newReadOnlyTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix([]byte{KeyTypeGlobal}), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
folderExists := make(map[string]bool)
|
||||
for dbi.Next() {
|
||||
folder, ok := db.globalKeyFolder(dbi.Key())
|
||||
if ok && !folderExists[string(folder)] {
|
||||
folderExists[string(folder)] = true
|
||||
}
|
||||
}
|
||||
|
||||
folders := make([]string, 0, len(folderExists))
|
||||
for k := range folderExists {
|
||||
folders = append(folders, k)
|
||||
}
|
||||
|
||||
sort.Strings(folders)
|
||||
return folders
|
||||
}
|
||||
|
||||
func (db *Instance) dropFolder(folder []byte) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
for _, key := range [][]byte{
|
||||
// Remove all items related to the given folder from the device->file bucket
|
||||
db.deviceKey(folder, nil, nil)[:keyPrefixLen+keyFolderLen],
|
||||
// Remove all sequences related to the folder
|
||||
db.sequenceKey([]byte(folder), 0)[:keyPrefixLen+keyFolderLen],
|
||||
// Remove all items related to the given folder from the global bucket
|
||||
db.globalKey(folder, nil)[:keyPrefixLen+keyFolderLen],
|
||||
// Remove all needs related to the folder
|
||||
db.needKey(folder, nil)[:keyPrefixLen+keyFolderLen],
|
||||
} {
|
||||
t.deleteKeyPrefix(key)
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) dropDeviceFolder(device, folder []byte, meta *metadataTracker) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.deviceKey(folder, device, nil)), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var gk []byte
|
||||
|
||||
for dbi.Next() {
|
||||
key := dbi.Key()
|
||||
name := db.deviceKeyName(key)
|
||||
gk = db.globalKeyInto(gk, folder, name)
|
||||
t.removeFromGlobal(gk, folder, device, name, meta)
|
||||
t.Delete(key)
|
||||
t.checkFlush()
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) checkGlobals(folder []byte, meta *metadataTracker) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(db.globalKey(folder, nil)[:keyPrefixLen+keyFolderLen]), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
var fk []byte
|
||||
for dbi.Next() {
|
||||
vl, ok := unmarshalVersionList(dbi.Value())
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Check the global version list for consistency. An issue in previous
|
||||
// versions of goleveldb could result in reordered writes so that
|
||||
// there are global entries pointing to no longer existing files. Here
|
||||
// we find those and clear them out.
|
||||
|
||||
name := db.globalKeyName(dbi.Key())
|
||||
var newVL VersionList
|
||||
for i, version := range vl.Versions {
|
||||
fk = db.deviceKeyInto(fk, folder, version.Device, name)
|
||||
_, err := t.Get(fk, nil)
|
||||
if err == leveldb.ErrNotFound {
|
||||
continue
|
||||
}
|
||||
if err != nil {
|
||||
l.Debugln("surprise error:", err)
|
||||
return
|
||||
}
|
||||
newVL.Versions = append(newVL.Versions, version)
|
||||
|
||||
if i == 0 {
|
||||
if fi, ok := db.getFile(fk); ok {
|
||||
meta.addFile(protocol.GlobalDeviceID, fi)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if len(newVL.Versions) != len(vl.Versions) {
|
||||
t.Put(dbi.Key(), mustMarshal(&newVL))
|
||||
t.checkFlush()
|
||||
}
|
||||
}
|
||||
l.Debugf("db check completed for %q", folder)
|
||||
}
|
||||
|
||||
// deviceKey returns a byte slice encoding the following information:
|
||||
// keyTypeDevice (1 byte)
|
||||
// folder (4 bytes)
|
||||
// device (4 bytes)
|
||||
// name (variable size)
|
||||
func (db *Instance) deviceKey(folder, device, file []byte) []byte {
|
||||
return db.deviceKeyInto(nil, folder, device, file)
|
||||
}
|
||||
|
||||
func (db *Instance) deviceKeyInto(k, folder, device, file []byte) []byte {
|
||||
reqLen := keyPrefixLen + keyFolderLen + keyDeviceLen + len(file)
|
||||
k = resize(k, reqLen)
|
||||
k[0] = KeyTypeDevice
|
||||
binary.BigEndian.PutUint32(k[keyPrefixLen:], db.folderIdx.ID(folder))
|
||||
binary.BigEndian.PutUint32(k[keyPrefixLen+keyFolderLen:], db.deviceIdx.ID(device))
|
||||
copy(k[keyPrefixLen+keyFolderLen+keyDeviceLen:], file)
|
||||
return k
|
||||
}
|
||||
|
||||
// deviceKeyName returns the device ID from the key
|
||||
func (db *Instance) deviceKeyName(key []byte) []byte {
|
||||
return key[keyPrefixLen+keyFolderLen+keyDeviceLen:]
|
||||
}
|
||||
|
||||
// deviceKeyFolder returns the folder name from the key
|
||||
func (db *Instance) deviceKeyFolder(key []byte) []byte {
|
||||
folder, ok := db.folderIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen:]))
|
||||
if !ok {
|
||||
panic("bug: lookup of nonexistent folder ID")
|
||||
}
|
||||
return folder
|
||||
}
|
||||
|
||||
// deviceKeyDevice returns the device ID from the key
|
||||
func (db *Instance) deviceKeyDevice(key []byte) []byte {
|
||||
device, ok := db.deviceIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen+keyFolderLen:]))
|
||||
if !ok {
|
||||
panic("bug: lookup of nonexistent device ID")
|
||||
}
|
||||
return device
|
||||
}
|
||||
|
||||
// globalKey returns a byte slice encoding the following information:
|
||||
// keyTypeGlobal (1 byte)
|
||||
// folder (4 bytes)
|
||||
// name (variable size)
|
||||
func (db *Instance) globalKey(folder, file []byte) []byte {
|
||||
return db.globalKeyInto(nil, folder, file)
|
||||
}
|
||||
|
||||
func (db *Instance) globalKeyInto(gk, folder, file []byte) []byte {
|
||||
reqLen := keyPrefixLen + keyFolderLen + len(file)
|
||||
gk = resize(gk, reqLen)
|
||||
gk[0] = KeyTypeGlobal
|
||||
binary.BigEndian.PutUint32(gk[keyPrefixLen:], db.folderIdx.ID(folder))
|
||||
copy(gk[keyPrefixLen+keyFolderLen:], file)
|
||||
return gk
|
||||
}
|
||||
|
||||
// globalKeyName returns the filename from the key
|
||||
func (db *Instance) globalKeyName(key []byte) []byte {
|
||||
return key[keyPrefixLen+keyFolderLen:]
|
||||
}
|
||||
|
||||
// globalKeyFolder returns the folder name from the key
|
||||
func (db *Instance) globalKeyFolder(key []byte) ([]byte, bool) {
|
||||
return db.folderIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen:]))
|
||||
}
|
||||
|
||||
// needKey is a globalKey with a different prefix
|
||||
func (db *Instance) needKey(folder, file []byte) []byte {
|
||||
return db.needKeyInto(nil, folder, file)
|
||||
}
|
||||
|
||||
func (db *Instance) needKeyInto(k, folder, file []byte) []byte {
|
||||
k = db.globalKeyInto(k, folder, file)
|
||||
k[0] = KeyTypeNeed
|
||||
return k
|
||||
}
|
||||
|
||||
// sequenceKey returns a byte slice encoding the following information:
|
||||
// KeyTypeSequence (1 byte)
|
||||
// folder (4 bytes)
|
||||
// sequence number (8 bytes)
|
||||
func (db *Instance) sequenceKey(folder []byte, seq int64) []byte {
|
||||
return db.sequenceKeyInto(nil, folder, seq)
|
||||
}
|
||||
|
||||
func (db *Instance) sequenceKeyInto(k []byte, folder []byte, seq int64) []byte {
|
||||
reqLen := keyPrefixLen + keyFolderLen + keySequenceLen
|
||||
k = resize(k, reqLen)
|
||||
k[0] = KeyTypeSequence
|
||||
binary.BigEndian.PutUint32(k[keyPrefixLen:], db.folderIdx.ID(folder))
|
||||
binary.BigEndian.PutUint64(k[keyPrefixLen+keyFolderLen:], uint64(seq))
|
||||
return k
|
||||
}
|
||||
|
||||
// sequenceKeySequence returns the sequence number from the key
|
||||
func (db *Instance) sequenceKeySequence(key []byte) int64 {
|
||||
return int64(binary.BigEndian.Uint64(key[keyPrefixLen+keyFolderLen:]))
|
||||
}
|
||||
|
||||
func (db *Instance) getIndexID(device, folder []byte) protocol.IndexID {
|
||||
key := db.indexIDKey(device, folder)
|
||||
cur, err := db.Get(key, nil)
|
||||
if err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
var id protocol.IndexID
|
||||
if err := id.Unmarshal(cur); err != nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
return id
|
||||
}
|
||||
|
||||
func (db *Instance) setIndexID(device, folder []byte, id protocol.IndexID) {
|
||||
key := db.indexIDKey(device, folder)
|
||||
bs, _ := id.Marshal() // marshalling can't fail
|
||||
if err := db.Put(key, bs, nil); err != nil {
|
||||
panic("storing index ID: " + err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) indexIDKey(device, folder []byte) []byte {
|
||||
k := make([]byte, keyPrefixLen+keyDeviceLen+keyFolderLen)
|
||||
k[0] = KeyTypeIndexID
|
||||
binary.BigEndian.PutUint32(k[keyPrefixLen:], db.deviceIdx.ID(device))
|
||||
binary.BigEndian.PutUint32(k[keyPrefixLen+keyDeviceLen:], db.folderIdx.ID(folder))
|
||||
return k
|
||||
}
|
||||
|
||||
func (db *Instance) indexIDDevice(key []byte) []byte {
|
||||
device, ok := db.deviceIdx.Val(binary.BigEndian.Uint32(key[keyPrefixLen:]))
|
||||
if !ok {
|
||||
// uuh ...
|
||||
return nil
|
||||
}
|
||||
return device
|
||||
}
|
||||
|
||||
func (db *Instance) mtimesKey(folder []byte) []byte {
|
||||
prefix := make([]byte, 5) // key type + 4 bytes folder idx number
|
||||
prefix[0] = KeyTypeVirtualMtime
|
||||
binary.BigEndian.PutUint32(prefix[1:], db.folderIdx.ID(folder))
|
||||
return prefix
|
||||
}
|
||||
|
||||
func (db *Instance) folderMetaKey(folder []byte) []byte {
|
||||
prefix := make([]byte, 5) // key type + 4 bytes folder idx number
|
||||
prefix[0] = KeyTypeFolderMeta
|
||||
binary.BigEndian.PutUint32(prefix[1:], db.folderIdx.ID(folder))
|
||||
return prefix
|
||||
}
|
||||
|
||||
// DropLocalDeltaIndexIDs removes all index IDs for the local device ID from
|
||||
// the database. This will cause a full index transmission on the next
|
||||
// connection.
|
||||
func (db *Instance) DropLocalDeltaIndexIDs() {
|
||||
db.dropDeltaIndexIDs(true)
|
||||
}
|
||||
|
||||
// DropRemoteDeltaIndexIDs removes all index IDs for the other devices than
|
||||
// the local one from the database. This will cause them to send us a full
|
||||
// index on the next connection.
|
||||
func (db *Instance) DropRemoteDeltaIndexIDs() {
|
||||
db.dropDeltaIndexIDs(false)
|
||||
}
|
||||
|
||||
func (db *Instance) dropDeltaIndexIDs(local bool) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix([]byte{KeyTypeIndexID}), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
for dbi.Next() {
|
||||
device := db.indexIDDevice(dbi.Key())
|
||||
if bytes.Equal(device, protocol.LocalDeviceID[:]) == local {
|
||||
t.Delete(dbi.Key())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (db *Instance) dropMtimes(folder []byte) {
|
||||
db.dropPrefix(db.mtimesKey(folder))
|
||||
}
|
||||
|
||||
func (db *Instance) dropFolderMeta(folder []byte) {
|
||||
db.dropPrefix(db.folderMetaKey(folder))
|
||||
}
|
||||
|
||||
func (db *Instance) dropPrefix(prefix []byte) {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
dbi := t.NewIterator(util.BytesPrefix(prefix), nil)
|
||||
defer dbi.Release()
|
||||
|
||||
for dbi.Next() {
|
||||
t.Delete(dbi.Key())
|
||||
}
|
||||
}
|
||||
|
||||
func unmarshalTrunc(bs []byte, truncate bool) (FileIntf, error) {
|
||||
if truncate {
|
||||
var tf FileInfoTruncated
|
||||
err := tf.Unmarshal(bs)
|
||||
return tf, err
|
||||
}
|
||||
|
||||
var tf protocol.FileInfo
|
||||
err := tf.Unmarshal(bs)
|
||||
return tf, err
|
||||
}
|
||||
|
||||
func unmarshalVersionList(data []byte) (VersionList, bool) {
|
||||
var vl VersionList
|
||||
if err := vl.Unmarshal(data); err != nil {
|
||||
l.Debugln("unmarshal error:", err)
|
||||
return VersionList{}, false
|
||||
}
|
||||
if len(vl.Versions) == 0 {
|
||||
l.Debugln("empty version list")
|
||||
return VersionList{}, false
|
||||
}
|
||||
return vl, true
|
||||
}
|
||||
|
||||
// A "better" version of leveldb's errors.IsCorrupted.
|
||||
func leveldbIsCorrupted(err error) bool {
|
||||
switch {
|
||||
case err == nil:
|
||||
return false
|
||||
|
||||
case errors.IsCorrupted(err):
|
||||
return true
|
||||
|
||||
case strings.Contains(err.Error(), "corrupted"):
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// A smallIndex is an in memory bidirectional []byte to uint32 map. It gives
|
||||
// fast lookups in both directions and persists to the database. Don't use for
|
||||
// storing more items than fit comfortably in RAM.
|
||||
type smallIndex struct {
|
||||
db *Instance
|
||||
prefix []byte
|
||||
id2val map[uint32]string
|
||||
val2id map[string]uint32
|
||||
nextID uint32
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
func newSmallIndex(db *Instance, prefix []byte) *smallIndex {
|
||||
idx := &smallIndex{
|
||||
db: db,
|
||||
prefix: prefix,
|
||||
id2val: make(map[uint32]string),
|
||||
val2id: make(map[string]uint32),
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
idx.load()
|
||||
return idx
|
||||
}
|
||||
|
||||
// load iterates over the prefix space in the database and populates the in
|
||||
// memory maps.
|
||||
func (i *smallIndex) load() {
|
||||
tr := i.db.newReadOnlyTransaction()
|
||||
it := tr.NewIterator(util.BytesPrefix(i.prefix), nil)
|
||||
for it.Next() {
|
||||
val := string(it.Value())
|
||||
id := binary.BigEndian.Uint32(it.Key()[len(i.prefix):])
|
||||
i.id2val[id] = val
|
||||
i.val2id[val] = id
|
||||
if id >= i.nextID {
|
||||
i.nextID = id + 1
|
||||
}
|
||||
}
|
||||
it.Release()
|
||||
tr.close()
|
||||
}
|
||||
|
||||
// ID returns the index number for the given byte slice, allocating a new one
|
||||
// and persisting this to the database if necessary.
|
||||
func (i *smallIndex) ID(val []byte) uint32 {
|
||||
i.mut.Lock()
|
||||
// intentionally avoiding defer here as we want this call to be as fast as
|
||||
// possible in the general case (folder ID already exists). The map lookup
|
||||
// with the conversion of []byte to string is compiler optimized to not
|
||||
// copy the []byte, which is why we don't assign it to a temp variable
|
||||
// here.
|
||||
if id, ok := i.val2id[string(val)]; ok {
|
||||
i.mut.Unlock()
|
||||
return id
|
||||
}
|
||||
|
||||
id := i.nextID
|
||||
i.nextID++
|
||||
|
||||
valStr := string(val)
|
||||
i.val2id[valStr] = id
|
||||
i.id2val[id] = valStr
|
||||
|
||||
key := make([]byte, len(i.prefix)+8) // prefix plus uint32 id
|
||||
copy(key, i.prefix)
|
||||
binary.BigEndian.PutUint32(key[len(i.prefix):], id)
|
||||
i.db.Put(key, val, nil)
|
||||
|
||||
i.mut.Unlock()
|
||||
return id
|
||||
}
|
||||
|
||||
// Val returns the value for the given index number, or (nil, false) if there
|
||||
// is no such index number.
|
||||
func (i *smallIndex) Val(id uint32) ([]byte, bool) {
|
||||
i.mut.Lock()
|
||||
val, ok := i.id2val[id]
|
||||
i.mut.Unlock()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return []byte(val), true
|
||||
}
|
||||
|
||||
// resize returns a byte array of length reqLen, reusing k if possible
|
||||
func resize(k []byte, reqLen int) []byte {
|
||||
if cap(k) < reqLen {
|
||||
return make([]byte, reqLen)
|
||||
}
|
||||
return k[:reqLen]
|
||||
}
|
||||
|
||||
type errorSuggestion struct {
|
||||
inner error
|
||||
suggestion string
|
||||
}
|
||||
|
||||
func (e errorSuggestion) Error() string {
|
||||
return fmt.Sprintf("%s (%s)", e.inner.Error(), e.suggestion)
|
||||
}
|
||||
122
lib/db/lowlevel.go
Normal file
122
lib/db/lowlevel.go
Normal file
@@ -0,0 +1,122 @@
|
||||
// Copyright (C) 2018 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/errors"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
"github.com/syndtr/goleveldb/leveldb/storage"
|
||||
)
|
||||
|
||||
const (
|
||||
dbMaxOpenFiles = 100
|
||||
dbWriteBuffer = 4 << 20
|
||||
)
|
||||
|
||||
// Lowlevel is the lowest level database interface. It has a very simple
|
||||
// purpose: hold the actual *leveldb.DB database, and the in-memory state
|
||||
// that belong to that database. In the same way that a single on disk
|
||||
// database can only be opened once, there should be only one Lowlevel for
|
||||
// any given *leveldb.DB.
|
||||
type Lowlevel struct {
|
||||
committed int64 // atomic, must come first
|
||||
*leveldb.DB
|
||||
location string
|
||||
folderIdx *smallIndex
|
||||
deviceIdx *smallIndex
|
||||
}
|
||||
|
||||
// Open attempts to open the database at the given location, and runs
|
||||
// recovery on it if opening fails. Worst case, if recovery is not possible,
|
||||
// the database is erased and created from scratch.
|
||||
func Open(location string) (*Lowlevel, error) {
|
||||
opts := &opt.Options{
|
||||
OpenFilesCacheCapacity: dbMaxOpenFiles,
|
||||
WriteBuffer: dbWriteBuffer,
|
||||
}
|
||||
|
||||
db, err := leveldb.OpenFile(location, opts)
|
||||
if leveldbIsCorrupted(err) {
|
||||
db, err = leveldb.RecoverFile(location, opts)
|
||||
}
|
||||
if leveldbIsCorrupted(err) {
|
||||
// The database is corrupted, and we've tried to recover it but it
|
||||
// didn't work. At this point there isn't much to do beyond dropping
|
||||
// the database and reindexing...
|
||||
l.Infoln("Database corruption detected, unable to recover. Reinitializing...")
|
||||
if err := os.RemoveAll(location); err != nil {
|
||||
return nil, errorSuggestion{err, "failed to delete corrupted database"}
|
||||
}
|
||||
db, err = leveldb.OpenFile(location, opts)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, errorSuggestion{err, "is another instance of Syncthing running?"}
|
||||
}
|
||||
return NewLowlevel(db, location), nil
|
||||
}
|
||||
|
||||
// OpenMemory returns a new Lowlevel referencing an in-memory database.
|
||||
func OpenMemory() *Lowlevel {
|
||||
db, _ := leveldb.Open(storage.NewMemStorage(), nil)
|
||||
return NewLowlevel(db, "<memory>")
|
||||
}
|
||||
|
||||
// Location returns the filesystem path where the database is stored
|
||||
func (db *Lowlevel) Location() string {
|
||||
return db.location
|
||||
}
|
||||
|
||||
// ListFolders returns the list of folders currently in the database
|
||||
func (db *Lowlevel) ListFolders() []string {
|
||||
return db.folderIdx.Values()
|
||||
}
|
||||
|
||||
// Committed returns the number of items committed to the database since startup
|
||||
func (db *Lowlevel) Committed() int64 {
|
||||
return atomic.LoadInt64(&db.committed)
|
||||
}
|
||||
|
||||
func (db *Lowlevel) Put(key, val []byte, wo *opt.WriteOptions) error {
|
||||
atomic.AddInt64(&db.committed, 1)
|
||||
return db.DB.Put(key, val, wo)
|
||||
}
|
||||
|
||||
func (db *Lowlevel) Delete(key []byte, wo *opt.WriteOptions) error {
|
||||
atomic.AddInt64(&db.committed, 1)
|
||||
return db.DB.Delete(key, wo)
|
||||
}
|
||||
|
||||
// NewLowlevel wraps the given *leveldb.DB into a *lowlevel
|
||||
func NewLowlevel(db *leveldb.DB, location string) *Lowlevel {
|
||||
return &Lowlevel{
|
||||
DB: db,
|
||||
location: location,
|
||||
folderIdx: newSmallIndex(db, []byte{KeyTypeFolderIdx}),
|
||||
deviceIdx: newSmallIndex(db, []byte{KeyTypeDeviceIdx}),
|
||||
}
|
||||
}
|
||||
|
||||
// A "better" version of leveldb's errors.IsCorrupted.
|
||||
func leveldbIsCorrupted(err error) bool {
|
||||
switch {
|
||||
case err == nil:
|
||||
return false
|
||||
|
||||
case errors.IsCorrupted(err):
|
||||
return true
|
||||
|
||||
case strings.Contains(err.Error(), "corrupted"):
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
@@ -20,6 +20,7 @@ type metadataTracker struct {
|
||||
mut sync.RWMutex
|
||||
counts CountsSet
|
||||
indexes map[metaKey]int // device ID + local flags -> index in counts
|
||||
dirty bool
|
||||
}
|
||||
|
||||
type metaKey struct {
|
||||
@@ -55,19 +56,32 @@ func (m *metadataTracker) Marshal() ([]byte, error) {
|
||||
|
||||
// toDB saves the marshalled metadataTracker to the given db, under the key
|
||||
// corresponding to the given folder
|
||||
func (m *metadataTracker) toDB(db *Instance, folder []byte) error {
|
||||
key := db.folderMetaKey(folder)
|
||||
func (m *metadataTracker) toDB(db *instance, folder []byte) error {
|
||||
key := db.keyer.GenerateFolderMetaKey(nil, folder)
|
||||
|
||||
m.mut.RLock()
|
||||
defer m.mut.RUnlock()
|
||||
|
||||
if !m.dirty {
|
||||
return nil
|
||||
}
|
||||
|
||||
bs, err := m.Marshal()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return db.Put(key, bs, nil)
|
||||
err = db.Put(key, bs, nil)
|
||||
if err == nil {
|
||||
m.dirty = false
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
// fromDB initializes the metadataTracker from the marshalled data found in
|
||||
// the database under the key corresponding to the given folder
|
||||
func (m *metadataTracker) fromDB(db *Instance, folder []byte) error {
|
||||
key := db.folderMetaKey(folder)
|
||||
func (m *metadataTracker) fromDB(db *instance, folder []byte) error {
|
||||
key := db.keyer.GenerateFolderMetaKey(nil, folder)
|
||||
bs, err := db.Get(key, nil)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -99,6 +113,7 @@ func (m *metadataTracker) addFile(dev protocol.DeviceID, f FileIntf) {
|
||||
}
|
||||
|
||||
m.mut.Lock()
|
||||
m.dirty = true
|
||||
|
||||
if flags := f.FileLocalFlags(); flags == 0 {
|
||||
// Account regular files in the zero-flags bucket.
|
||||
@@ -141,6 +156,7 @@ func (m *metadataTracker) removeFile(dev protocol.DeviceID, f FileIntf) {
|
||||
}
|
||||
|
||||
m.mut.Lock()
|
||||
m.dirty = true
|
||||
|
||||
if flags := f.FileLocalFlags(); flags == 0 {
|
||||
// Remove regular files from the zero-flags bucket
|
||||
@@ -194,6 +210,7 @@ func (m *metadataTracker) removeFileLocked(dev protocol.DeviceID, flags uint32,
|
||||
// resetAll resets all metadata for the given device
|
||||
func (m *metadataTracker) resetAll(dev protocol.DeviceID) {
|
||||
m.mut.Lock()
|
||||
m.dirty = true
|
||||
for i, c := range m.counts.Counts {
|
||||
if bytes.Equal(c.DeviceID, dev[:]) {
|
||||
m.counts.Counts[i] = Counts{
|
||||
@@ -209,6 +226,7 @@ func (m *metadataTracker) resetAll(dev protocol.DeviceID) {
|
||||
// sequence number
|
||||
func (m *metadataTracker) resetCounts(dev protocol.DeviceID) {
|
||||
m.mut.Lock()
|
||||
m.dirty = true
|
||||
|
||||
for i, c := range m.counts.Counts {
|
||||
if bytes.Equal(c.DeviceID, dev[:]) {
|
||||
@@ -285,6 +303,7 @@ func (m *metadataTracker) Created() time.Time {
|
||||
func (m *metadataTracker) SetCreated() {
|
||||
m.mut.Lock()
|
||||
m.counts.Created = time.Now().UnixNano()
|
||||
m.dirty = true
|
||||
m.mut.Unlock()
|
||||
}
|
||||
|
||||
|
||||
@@ -17,13 +17,13 @@ import (
|
||||
// NamespacedKV is a simple key-value store using a specific namespace within
|
||||
// a leveldb.
|
||||
type NamespacedKV struct {
|
||||
db *Instance
|
||||
db *Lowlevel
|
||||
prefix []byte
|
||||
}
|
||||
|
||||
// NewNamespacedKV returns a new NamespacedKV that lives in the namespace
|
||||
// specified by the prefix.
|
||||
func NewNamespacedKV(db *Instance, prefix string) *NamespacedKV {
|
||||
func NewNamespacedKV(db *Lowlevel, prefix string) *NamespacedKV {
|
||||
return &NamespacedKV{
|
||||
db: db,
|
||||
prefix: []byte(prefix),
|
||||
@@ -157,3 +157,23 @@ func (n NamespacedKV) Delete(key string) {
|
||||
keyBs := append(n.prefix, []byte(key)...)
|
||||
n.db.Delete(keyBs, nil)
|
||||
}
|
||||
|
||||
// Well known namespaces that can be instantiated without knowing the key
|
||||
// details.
|
||||
|
||||
// NewDeviceStatisticsNamespace creates a KV namespace for device statistics
|
||||
// for the given device.
|
||||
func NewDeviceStatisticsNamespace(db *Lowlevel, device string) *NamespacedKV {
|
||||
return NewNamespacedKV(db, string(KeyTypeDeviceStatistic)+device)
|
||||
}
|
||||
|
||||
// NewFolderStatisticsNamespace creates a KV namespace for folder statistics
|
||||
// for the given folder.
|
||||
func NewFolderStatisticsNamespace(db *Lowlevel, folder string) *NamespacedKV {
|
||||
return NewNamespacedKV(db, string(KeyTypeFolderStatistic)+folder)
|
||||
}
|
||||
|
||||
// NewMiscDateNamespace creates a KV namespace for miscellaneous metadata.
|
||||
func NewMiscDataNamespace(db *Lowlevel) *NamespacedKV {
|
||||
return NewNamespacedKV(db, string(KeyTypeMiscData))
|
||||
}
|
||||
|
||||
@@ -38,8 +38,17 @@ func (e databaseDowngradeError) Error() string {
|
||||
return fmt.Sprintf("Syncthing %s required", e.minSyncthingVersion)
|
||||
}
|
||||
|
||||
func (db *Instance) updateSchema() error {
|
||||
miscDB := NewNamespacedKV(db, string(KeyTypeMiscData))
|
||||
func UpdateSchema(ll *Lowlevel) error {
|
||||
updater := &schemaUpdater{newInstance(ll)}
|
||||
return updater.updateSchema()
|
||||
}
|
||||
|
||||
type schemaUpdater struct {
|
||||
*instance
|
||||
}
|
||||
|
||||
func (db *schemaUpdater) updateSchema() error {
|
||||
miscDB := NewMiscDataNamespace(db.Lowlevel)
|
||||
prevVersion, _ := miscDB.Int64("dbVersion")
|
||||
|
||||
if prevVersion > dbVersion {
|
||||
@@ -77,7 +86,7 @@ func (db *Instance) updateSchema() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (db *Instance) updateSchema0to1() {
|
||||
func (db *schemaUpdater) updateSchema0to1() {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -91,16 +100,28 @@ func (db *Instance) updateSchema0to1() {
|
||||
var gk []byte
|
||||
|
||||
for dbi.Next() {
|
||||
folder := db.deviceKeyFolder(dbi.Key())
|
||||
device := db.deviceKeyDevice(dbi.Key())
|
||||
name := db.deviceKeyName(dbi.Key())
|
||||
folder, ok := db.keyer.FolderFromDeviceFileKey(dbi.Key())
|
||||
if !ok {
|
||||
// not having the folder in the index is bad; delete and continue
|
||||
t.Delete(dbi.Key())
|
||||
t.checkFlush()
|
||||
continue
|
||||
}
|
||||
device, ok := db.keyer.DeviceFromDeviceFileKey(dbi.Key())
|
||||
if !ok {
|
||||
// not having the device in the index is bad; delete and continue
|
||||
t.Delete(dbi.Key())
|
||||
t.checkFlush()
|
||||
continue
|
||||
}
|
||||
name := db.keyer.NameFromDeviceFileKey(dbi.Key())
|
||||
|
||||
// Remove files with absolute path (see #4799)
|
||||
if strings.HasPrefix(string(name), "/") {
|
||||
if _, ok := changedFolders[string(folder)]; !ok {
|
||||
changedFolders[string(folder)] = struct{}{}
|
||||
}
|
||||
gk = db.globalKeyInto(gk, folder, name)
|
||||
gk = db.keyer.GenerateGlobalVersionKey(gk, folder, name)
|
||||
t.removeFromGlobal(gk, folder, device, nil, nil)
|
||||
t.Delete(dbi.Key())
|
||||
t.checkFlush()
|
||||
@@ -130,7 +151,7 @@ func (db *Instance) updateSchema0to1() {
|
||||
|
||||
// Add invalid files to global list
|
||||
if f.IsInvalid() {
|
||||
gk = db.globalKeyInto(gk, folder, name)
|
||||
gk = db.keyer.GenerateGlobalVersionKey(gk, folder, name)
|
||||
if t.updateGlobal(gk, folder, device, f, meta) {
|
||||
if _, ok := changedFolders[string(folder)]; !ok {
|
||||
changedFolders[string(folder)] = struct{}{}
|
||||
@@ -147,7 +168,7 @@ func (db *Instance) updateSchema0to1() {
|
||||
|
||||
// updateSchema1to2 introduces a sequenceKey->deviceKey bucket for local items
|
||||
// to allow iteration in sequence order (simplifies sending indexes).
|
||||
func (db *Instance) updateSchema1to2() {
|
||||
func (db *schemaUpdater) updateSchema1to2() {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -156,8 +177,8 @@ func (db *Instance) updateSchema1to2() {
|
||||
for _, folderStr := range db.ListFolders() {
|
||||
folder := []byte(folderStr)
|
||||
db.withHave(folder, protocol.LocalDeviceID[:], nil, true, func(f FileIntf) bool {
|
||||
sk = db.sequenceKeyInto(sk, folder, f.SequenceNo())
|
||||
dk = db.deviceKeyInto(dk, folder, protocol.LocalDeviceID[:], []byte(f.FileName()))
|
||||
sk = db.keyer.GenerateSequenceKey(sk, folder, f.SequenceNo())
|
||||
dk = db.keyer.GenerateDeviceFileKey(dk, folder, protocol.LocalDeviceID[:], []byte(f.FileName()))
|
||||
t.Put(sk, dk)
|
||||
t.checkFlush()
|
||||
return true
|
||||
@@ -166,7 +187,7 @@ func (db *Instance) updateSchema1to2() {
|
||||
}
|
||||
|
||||
// updateSchema2to3 introduces a needKey->nil bucket for locally needed files.
|
||||
func (db *Instance) updateSchema2to3() {
|
||||
func (db *schemaUpdater) updateSchema2to3() {
|
||||
t := db.newReadWriteTransaction()
|
||||
defer t.close()
|
||||
|
||||
@@ -176,7 +197,7 @@ func (db *Instance) updateSchema2to3() {
|
||||
folder := []byte(folderStr)
|
||||
db.withGlobal(folder, nil, true, func(f FileIntf) bool {
|
||||
name := []byte(f.FileName())
|
||||
dk = db.deviceKeyInto(dk, folder, protocol.LocalDeviceID[:], name)
|
||||
dk = db.keyer.GenerateDeviceFileKey(dk, folder, protocol.LocalDeviceID[:], name)
|
||||
var v protocol.Vector
|
||||
haveFile, ok := db.getFileTrunc(dk, true)
|
||||
if ok {
|
||||
@@ -185,7 +206,7 @@ func (db *Instance) updateSchema2to3() {
|
||||
if !need(f, ok, v) {
|
||||
return true
|
||||
}
|
||||
nk = t.db.needKeyInto(nk, folder, []byte(f.FileName()))
|
||||
nk = t.db.keyer.GenerateNeedFileKey(nk, folder, []byte(f.FileName()))
|
||||
t.Put(nk, nil)
|
||||
t.checkFlush()
|
||||
return true
|
||||
@@ -197,11 +218,11 @@ func (db *Instance) updateSchema2to3() {
|
||||
// release candidates (dbVersion 3 and 4)
|
||||
// https://github.com/syncthing/syncthing/issues/5007
|
||||
// https://github.com/syncthing/syncthing/issues/5053
|
||||
func (db *Instance) updateSchemaTo5() {
|
||||
func (db *schemaUpdater) updateSchemaTo5() {
|
||||
t := db.newReadWriteTransaction()
|
||||
var nk []byte
|
||||
for _, folderStr := range db.ListFolders() {
|
||||
nk = db.needKeyInto(nk, []byte(folderStr), nil)
|
||||
nk = db.keyer.GenerateNeedFileKey(nk, []byte(folderStr), nil)
|
||||
t.deleteKeyPrefix(nk[:keyPrefixLen+keyFolderLen])
|
||||
}
|
||||
t.close()
|
||||
@@ -209,7 +230,7 @@ func (db *Instance) updateSchemaTo5() {
|
||||
db.updateSchema2to3()
|
||||
}
|
||||
|
||||
func (db *Instance) updateSchema5to6() {
|
||||
func (db *schemaUpdater) updateSchema5to6() {
|
||||
// For every local file with the Invalid bit set, clear the Invalid bit and
|
||||
// set LocalFlags = FlagLocalIgnored.
|
||||
|
||||
@@ -230,7 +251,7 @@ func (db *Instance) updateSchema5to6() {
|
||||
fi.LocalFlags = protocol.FlagLocalIgnored
|
||||
bs, _ := fi.Marshal()
|
||||
|
||||
dk = db.deviceKeyInto(dk, folder, protocol.LocalDeviceID[:], []byte(fi.Name))
|
||||
dk = db.keyer.GenerateDeviceFileKey(dk, folder, protocol.LocalDeviceID[:], []byte(fi.Name))
|
||||
t.Put(dk, bs)
|
||||
|
||||
t.checkFlush()
|
||||
@@ -21,12 +21,13 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
type FileSet struct {
|
||||
folder string
|
||||
fs fs.Filesystem
|
||||
db *Instance
|
||||
db *instance
|
||||
blockmap *BlockMap
|
||||
meta *metadataTracker
|
||||
|
||||
@@ -66,12 +67,14 @@ func init() {
|
||||
}
|
||||
}
|
||||
|
||||
func NewFileSet(folder string, fs fs.Filesystem, db *Instance) *FileSet {
|
||||
func NewFileSet(folder string, fs fs.Filesystem, ll *Lowlevel) *FileSet {
|
||||
db := newInstance(ll)
|
||||
|
||||
var s = FileSet{
|
||||
folder: folder,
|
||||
fs: fs,
|
||||
db: db,
|
||||
blockmap: NewBlockMap(db, db.folderIdx.ID([]byte(folder))),
|
||||
blockmap: NewBlockMap(ll, folder),
|
||||
meta: newMetadataTracker(),
|
||||
updateMutex: sync.NewMutex(),
|
||||
}
|
||||
@@ -160,7 +163,7 @@ func (s *FileSet) Update(device protocol.DeviceID, fs []protocol.FileInfo) {
|
||||
var dk []byte
|
||||
folder := []byte(s.folder)
|
||||
for _, nf := range oldFs {
|
||||
dk = s.db.deviceKeyInto(dk, folder, device[:], []byte(osutil.NormalizedFilename(nf.Name)))
|
||||
dk = s.db.keyer.GenerateDeviceFileKey(dk, folder, device[:], []byte(osutil.NormalizedFilename(nf.Name)))
|
||||
ef, ok := s.db.getFile(dk)
|
||||
if ok && ef.Version.Equal(nf.Version) && ef.IsInvalid() == nf.IsInvalid() {
|
||||
continue
|
||||
@@ -242,7 +245,7 @@ func (s *FileSet) WithPrefixedGlobalTruncated(prefix string, fn Iterator) {
|
||||
}
|
||||
|
||||
func (s *FileSet) Get(device protocol.DeviceID, file string) (protocol.FileInfo, bool) {
|
||||
f, ok := s.db.getFile(s.db.deviceKey([]byte(s.folder), device[:], []byte(osutil.NormalizedFilename(file))))
|
||||
f, ok := s.db.getFile(s.db.keyer.GenerateDeviceFileKey(nil, []byte(s.folder), device[:], []byte(osutil.NormalizedFilename(file))))
|
||||
f.Name = osutil.NativeFilename(f.Name)
|
||||
return f, ok
|
||||
}
|
||||
@@ -309,8 +312,8 @@ func (s *FileSet) SetIndexID(device protocol.DeviceID, id protocol.IndexID) {
|
||||
}
|
||||
|
||||
func (s *FileSet) MtimeFS() *fs.MtimeFS {
|
||||
prefix := s.db.mtimesKey([]byte(s.folder))
|
||||
kv := NewNamespacedKV(s.db, string(prefix))
|
||||
prefix := s.db.keyer.GenerateMtimesKey(nil, []byte(s.folder))
|
||||
kv := NewNamespacedKV(s.db.Lowlevel, string(prefix))
|
||||
return fs.NewMtimeFS(s.fs, kv)
|
||||
}
|
||||
|
||||
@@ -320,15 +323,26 @@ func (s *FileSet) ListDevices() []protocol.DeviceID {
|
||||
|
||||
// DropFolder clears out all information related to the given folder from the
|
||||
// database.
|
||||
func DropFolder(db *Instance, folder string) {
|
||||
func DropFolder(ll *Lowlevel, folder string) {
|
||||
db := newInstance(ll)
|
||||
db.dropFolder([]byte(folder))
|
||||
db.dropMtimes([]byte(folder))
|
||||
db.dropFolderMeta([]byte(folder))
|
||||
bm := &BlockMap{
|
||||
db: db,
|
||||
folder: db.folderIdx.ID([]byte(folder)),
|
||||
}
|
||||
bm := NewBlockMap(ll, folder)
|
||||
bm.Drop()
|
||||
|
||||
// Also clean out the folder ID mapping.
|
||||
db.folderIdx.Delete([]byte(folder))
|
||||
}
|
||||
|
||||
// DropDeltaIndexIDs removes all delta index IDs from the database.
|
||||
// This will cause a full index transmission on the next connection.
|
||||
func DropDeltaIndexIDs(db *Lowlevel) {
|
||||
dbi := db.NewIterator(util.BytesPrefix([]byte{KeyTypeIndexID}), nil)
|
||||
defer dbi.Release()
|
||||
for dbi.Next() {
|
||||
db.Delete(dbi.Key(), nil)
|
||||
}
|
||||
}
|
||||
|
||||
func normalizeFilenames(fs []protocol.FileInfo) {
|
||||
|
||||
@@ -1178,8 +1178,8 @@ func TestReceiveOnlyAccounting(t *testing.T) {
|
||||
if n := s.GlobalSize().Files; n != 3 {
|
||||
t.Fatal("expected 3 global files after local change, not", n)
|
||||
}
|
||||
if n := s.GlobalSize().Bytes; n != 120 {
|
||||
t.Fatal("expected 120 global bytes after local change, not", n)
|
||||
if n := s.GlobalSize().Bytes; n != 30 {
|
||||
t.Fatal("expected 30 global files after local change, not", n)
|
||||
}
|
||||
if n := s.ReceiveOnlyChangedSize().Files; n != 1 {
|
||||
t.Fatal("expected 1 receive only changed file after local change, not", n)
|
||||
@@ -1271,6 +1271,44 @@ func TestRemoteInvalidNotAccounted(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestNeedWithNewerInvalid(t *testing.T) {
|
||||
ldb := db.OpenMemory()
|
||||
|
||||
s := db.NewFileSet("default", fs.NewFilesystem(fs.FilesystemTypeBasic, "."), ldb)
|
||||
|
||||
rem0ID := remoteDevice0.Short()
|
||||
rem1ID := remoteDevice1.Short()
|
||||
|
||||
// Initial state: file present on rem0 and rem1, but not locally.
|
||||
file := protocol.FileInfo{Name: "foo"}
|
||||
file.Version = file.Version.Update(rem0ID)
|
||||
s.Update(remoteDevice0, fileList{file})
|
||||
s.Update(remoteDevice1, fileList{file})
|
||||
|
||||
need := needList(s, protocol.LocalDeviceID)
|
||||
if len(need) != 1 {
|
||||
t.Fatal("Locally missing file should be needed")
|
||||
}
|
||||
if !need[0].IsEquivalent(file) {
|
||||
t.Fatalf("Got needed file %v, expected %v", need[0], file)
|
||||
}
|
||||
|
||||
// rem1 sends an invalid file with increased version
|
||||
inv := file
|
||||
inv.Version = inv.Version.Update(rem1ID)
|
||||
inv.RawInvalid = true
|
||||
s.Update(remoteDevice1, fileList{inv})
|
||||
|
||||
// We still have an old file, we need the newest valid file
|
||||
need = needList(s, protocol.LocalDeviceID)
|
||||
if len(need) != 1 {
|
||||
t.Fatal("Locally missing file should be needed regardless of invalid files")
|
||||
}
|
||||
if !need[0].IsEquivalent(file) {
|
||||
t.Fatalf("Got needed file %v, expected %v", need[0], file)
|
||||
}
|
||||
}
|
||||
|
||||
func replace(fs *db.FileSet, device protocol.DeviceID, files []protocol.FileInfo) {
|
||||
fs.Drop(device)
|
||||
fs.Update(device, files)
|
||||
|
||||
144
lib/db/smallindex.go
Normal file
144
lib/db/smallindex.go
Normal file
@@ -0,0 +1,144 @@
|
||||
// Copyright (C) 2018 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import (
|
||||
"encoding/binary"
|
||||
"sort"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
// A smallIndex is an in memory bidirectional []byte to uint32 map. It gives
|
||||
// fast lookups in both directions and persists to the database. Don't use for
|
||||
// storing more items than fit comfortably in RAM.
|
||||
type smallIndex struct {
|
||||
db *leveldb.DB
|
||||
prefix []byte
|
||||
id2val map[uint32]string
|
||||
val2id map[string]uint32
|
||||
nextID uint32
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
func newSmallIndex(db *leveldb.DB, prefix []byte) *smallIndex {
|
||||
idx := &smallIndex{
|
||||
db: db,
|
||||
prefix: prefix,
|
||||
id2val: make(map[uint32]string),
|
||||
val2id: make(map[string]uint32),
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
idx.load()
|
||||
return idx
|
||||
}
|
||||
|
||||
// load iterates over the prefix space in the database and populates the in
|
||||
// memory maps.
|
||||
func (i *smallIndex) load() {
|
||||
it := i.db.NewIterator(util.BytesPrefix(i.prefix), nil)
|
||||
defer it.Release()
|
||||
for it.Next() {
|
||||
val := string(it.Value())
|
||||
id := binary.BigEndian.Uint32(it.Key()[len(i.prefix):])
|
||||
if val != "" {
|
||||
// Empty value means the entry has been deleted.
|
||||
i.id2val[id] = val
|
||||
i.val2id[val] = id
|
||||
}
|
||||
if id >= i.nextID {
|
||||
i.nextID = id + 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ID returns the index number for the given byte slice, allocating a new one
|
||||
// and persisting this to the database if necessary.
|
||||
func (i *smallIndex) ID(val []byte) uint32 {
|
||||
i.mut.Lock()
|
||||
// intentionally avoiding defer here as we want this call to be as fast as
|
||||
// possible in the general case (folder ID already exists). The map lookup
|
||||
// with the conversion of []byte to string is compiler optimized to not
|
||||
// copy the []byte, which is why we don't assign it to a temp variable
|
||||
// here.
|
||||
if id, ok := i.val2id[string(val)]; ok {
|
||||
i.mut.Unlock()
|
||||
return id
|
||||
}
|
||||
|
||||
id := i.nextID
|
||||
i.nextID++
|
||||
|
||||
valStr := string(val)
|
||||
i.val2id[valStr] = id
|
||||
i.id2val[id] = valStr
|
||||
|
||||
key := make([]byte, len(i.prefix)+8) // prefix plus uint32 id
|
||||
copy(key, i.prefix)
|
||||
binary.BigEndian.PutUint32(key[len(i.prefix):], id)
|
||||
i.db.Put(key, val, nil)
|
||||
|
||||
i.mut.Unlock()
|
||||
return id
|
||||
}
|
||||
|
||||
// Val returns the value for the given index number, or (nil, false) if there
|
||||
// is no such index number.
|
||||
func (i *smallIndex) Val(id uint32) ([]byte, bool) {
|
||||
i.mut.Lock()
|
||||
val, ok := i.id2val[id]
|
||||
i.mut.Unlock()
|
||||
if !ok {
|
||||
return nil, false
|
||||
}
|
||||
|
||||
return []byte(val), true
|
||||
}
|
||||
|
||||
func (i *smallIndex) Delete(val []byte) {
|
||||
i.mut.Lock()
|
||||
defer i.mut.Unlock()
|
||||
|
||||
// Check the reverse mapping to get the ID for the value.
|
||||
if id, ok := i.val2id[string(val)]; ok {
|
||||
// Generate the corresponding database key.
|
||||
key := make([]byte, len(i.prefix)+8) // prefix plus uint32 id
|
||||
copy(key, i.prefix)
|
||||
binary.BigEndian.PutUint32(key[len(i.prefix):], id)
|
||||
|
||||
// Put an empty value into the database. This indicates that the
|
||||
// entry does not exist any more and prevents the ID from being
|
||||
// reused in the future.
|
||||
i.db.Put(key, []byte{}, nil)
|
||||
|
||||
// Delete reverse mapping.
|
||||
delete(i.id2val, id)
|
||||
}
|
||||
|
||||
// Delete forward mapping.
|
||||
delete(i.val2id, string(val))
|
||||
}
|
||||
|
||||
// Values returns the set of values in the index
|
||||
func (i *smallIndex) Values() []string {
|
||||
// In principle this method should return [][]byte because all the other
|
||||
// methods deal in []byte keys. However, in practice, where it's used
|
||||
// wants a []string and it's easier to just create that here rather than
|
||||
// having to convert both here and there...
|
||||
|
||||
i.mut.Lock()
|
||||
vals := make([]string, 0, len(i.val2id))
|
||||
for val := range i.val2id {
|
||||
vals = append(vals, val)
|
||||
}
|
||||
i.mut.Unlock()
|
||||
|
||||
sort.Strings(vals)
|
||||
return vals
|
||||
}
|
||||
52
lib/db/smallindex_test.go
Normal file
52
lib/db/smallindex_test.go
Normal file
@@ -0,0 +1,52 @@
|
||||
// Copyright (C) 2018 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package db
|
||||
|
||||
import "testing"
|
||||
|
||||
func TestSmallIndex(t *testing.T) {
|
||||
db := OpenMemory()
|
||||
idx := newSmallIndex(db.DB, []byte{12, 34})
|
||||
|
||||
// ID zero should be unallocated
|
||||
if val, ok := idx.Val(0); ok || val != nil {
|
||||
t.Fatal("Unexpected return for nonexistent ID 0")
|
||||
}
|
||||
|
||||
// A new key should get ID zero
|
||||
if id := idx.ID([]byte("hello")); id != 0 {
|
||||
t.Fatal("Expected 0, not", id)
|
||||
}
|
||||
// Looking up ID zero should work
|
||||
if val, ok := idx.Val(0); !ok || string(val) != "hello" {
|
||||
t.Fatalf(`Expected true, "hello", not %v, %q`, ok, val)
|
||||
}
|
||||
|
||||
// Delete the key
|
||||
idx.Delete([]byte("hello"))
|
||||
|
||||
// Next ID should be one
|
||||
if id := idx.ID([]byte("key2")); id != 1 {
|
||||
t.Fatal("Expected 1, not", id)
|
||||
}
|
||||
|
||||
// Now lets create a new index instance based on what's actually serialized to the database.
|
||||
idx = newSmallIndex(db.DB, []byte{12, 34})
|
||||
|
||||
// Status should be about the same as before.
|
||||
if val, ok := idx.Val(0); ok || val != nil {
|
||||
t.Fatal("Unexpected return for deleted ID 0")
|
||||
}
|
||||
if id := idx.ID([]byte("key2")); id != 1 {
|
||||
t.Fatal("Expected 1, not", id)
|
||||
}
|
||||
|
||||
// Setting "hello" again should get us ID 2, not 0 as it was originally.
|
||||
if id := idx.ID([]byte("hello")); id != 2 {
|
||||
t.Fatal("Expected 2, not", id)
|
||||
}
|
||||
}
|
||||
@@ -10,7 +10,9 @@
|
||||
package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
@@ -139,3 +141,107 @@ func (c Counts) Add(other Counts) Counts {
|
||||
LocalFlags: c.LocalFlags | other.LocalFlags,
|
||||
}
|
||||
}
|
||||
|
||||
func (vl VersionList) String() string {
|
||||
var b bytes.Buffer
|
||||
var id protocol.DeviceID
|
||||
b.WriteString("{")
|
||||
for i, v := range vl.Versions {
|
||||
if i > 0 {
|
||||
b.WriteString(", ")
|
||||
}
|
||||
copy(id[:], v.Device)
|
||||
fmt.Fprintf(&b, "{%v, %v}", v.Version, id)
|
||||
}
|
||||
b.WriteString("}")
|
||||
return b.String()
|
||||
}
|
||||
|
||||
// update brings the VersionList up to date with file. It returns the updated
|
||||
// VersionList, a potentially removed old FileVersion and its index, as well as
|
||||
// the index where the new FileVersion was inserted.
|
||||
func (vl VersionList) update(folder, device []byte, file protocol.FileInfo, db *instance) (_ VersionList, removedFV FileVersion, removedAt int, insertedAt int) {
|
||||
removedAt, insertedAt = -1, -1
|
||||
for i, v := range vl.Versions {
|
||||
if bytes.Equal(v.Device, device) {
|
||||
removedAt = i
|
||||
removedFV = v
|
||||
vl.Versions = append(vl.Versions[:i], vl.Versions[i+1:]...)
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
nv := FileVersion{
|
||||
Device: device,
|
||||
Version: file.Version,
|
||||
Invalid: file.IsInvalid(),
|
||||
}
|
||||
i := 0
|
||||
if nv.Invalid {
|
||||
i = sort.Search(len(vl.Versions), func(j int) bool {
|
||||
return vl.Versions[j].Invalid
|
||||
})
|
||||
}
|
||||
for ; i < len(vl.Versions); i++ {
|
||||
switch vl.Versions[i].Version.Compare(file.Version) {
|
||||
case protocol.Equal:
|
||||
fallthrough
|
||||
|
||||
case protocol.Lesser:
|
||||
// The version at this point in the list is equal to or lesser
|
||||
// ("older") than us. We insert ourselves in front of it.
|
||||
vl = vl.insertAt(i, nv)
|
||||
return vl, removedFV, removedAt, i
|
||||
|
||||
case protocol.ConcurrentLesser, protocol.ConcurrentGreater:
|
||||
// The version at this point is in conflict with us. We must pull
|
||||
// the actual file metadata to determine who wins. If we win, we
|
||||
// insert ourselves in front of the loser here. (The "Lesser" and
|
||||
// "Greater" in the condition above is just based on the device
|
||||
// IDs in the version vector, which is not the only thing we use
|
||||
// to determine the winner.)
|
||||
//
|
||||
// A surprise missing file entry here is counted as a win for us.
|
||||
if of, ok := db.getFile(db.keyer.GenerateDeviceFileKey(nil, folder, vl.Versions[i].Device, []byte(file.Name))); !ok || file.WinsConflict(of) {
|
||||
vl = vl.insertAt(i, nv)
|
||||
return vl, removedFV, removedAt, i
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We didn't find a position for an insert above, so append to the end.
|
||||
vl.Versions = append(vl.Versions, nv)
|
||||
|
||||
return vl, removedFV, removedAt, len(vl.Versions) - 1
|
||||
}
|
||||
|
||||
func (vl VersionList) insertAt(i int, v FileVersion) VersionList {
|
||||
vl.Versions = append(vl.Versions, FileVersion{})
|
||||
copy(vl.Versions[i+1:], vl.Versions[i:])
|
||||
vl.Versions[i] = v
|
||||
return vl
|
||||
}
|
||||
|
||||
func (vl VersionList) Get(device []byte) (FileVersion, bool) {
|
||||
for _, v := range vl.Versions {
|
||||
if bytes.Equal(v.Device, device) {
|
||||
return v, true
|
||||
}
|
||||
}
|
||||
|
||||
return FileVersion{}, false
|
||||
}
|
||||
|
||||
type fileList []protocol.FileInfo
|
||||
|
||||
func (fl fileList) Len() int {
|
||||
return len(fl)
|
||||
}
|
||||
|
||||
func (fl fileList) Swap(a, b int) {
|
||||
fl[a], fl[b] = fl[b], fl[a]
|
||||
}
|
||||
|
||||
func (fl fileList) Less(a, b int) bool {
|
||||
return fl[a].Name < fl[b].Name
|
||||
}
|
||||
|
||||
@@ -8,20 +8,22 @@ package db
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"sync/atomic"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/util"
|
||||
)
|
||||
|
||||
// Flush batches to disk when they contain this many records.
|
||||
const batchFlushSize = 64
|
||||
|
||||
// A readOnlyTransaction represents a database snapshot.
|
||||
type readOnlyTransaction struct {
|
||||
*leveldb.Snapshot
|
||||
db *Instance
|
||||
db *instance
|
||||
}
|
||||
|
||||
func (db *Instance) newReadOnlyTransaction() readOnlyTransaction {
|
||||
func (db *instance) newReadOnlyTransaction() readOnlyTransaction {
|
||||
snap, err := db.GetSnapshot()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
@@ -37,7 +39,7 @@ func (t readOnlyTransaction) close() {
|
||||
}
|
||||
|
||||
func (t readOnlyTransaction) getFile(folder, device, file []byte) (protocol.FileInfo, bool) {
|
||||
return t.db.getFile(t.db.deviceKey(folder, device, file))
|
||||
return t.db.getFile(t.db.keyer.GenerateDeviceFileKey(nil, folder, device, file))
|
||||
}
|
||||
|
||||
// A readWriteTransaction is a readOnlyTransaction plus a batch for writes.
|
||||
@@ -48,7 +50,7 @@ type readWriteTransaction struct {
|
||||
*leveldb.Batch
|
||||
}
|
||||
|
||||
func (db *Instance) newReadWriteTransaction() readWriteTransaction {
|
||||
func (db *instance) newReadWriteTransaction() readWriteTransaction {
|
||||
t := db.newReadOnlyTransaction()
|
||||
return readWriteTransaction{
|
||||
readOnlyTransaction: t,
|
||||
@@ -72,7 +74,6 @@ func (t readWriteTransaction) flush() {
|
||||
if err := t.db.Write(t.Batch, nil); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
atomic.AddInt64(&t.db.committed, int64(t.Batch.Len()))
|
||||
}
|
||||
|
||||
func (t readWriteTransaction) insertFile(fk, folder, device []byte, file protocol.FileInfo) {
|
||||
@@ -111,7 +112,7 @@ func (t readWriteTransaction) updateGlobal(gk, folder, device []byte, file proto
|
||||
}
|
||||
|
||||
// Fixup the list of files we need.
|
||||
nk := t.db.needKey(folder, name)
|
||||
nk := t.db.keyer.GenerateNeedFileKey(nil, folder, name)
|
||||
hasNeeded, _ := t.db.Has(nk, nil)
|
||||
if localFV, haveLocalFV := fl.Get(protocol.LocalDeviceID[:]); need(newGlobal, haveLocalFV, localFV.Version) {
|
||||
if !hasNeeded {
|
||||
@@ -162,7 +163,7 @@ func need(global FileIntf, haveLocal bool, localVersion protocol.Vector) bool {
|
||||
return false
|
||||
}
|
||||
// We don't need the global file if we already have the same version.
|
||||
if haveLocal && localVersion.Equal(global.FileVersion()) {
|
||||
if haveLocal && localVersion.GreaterEqual(global.FileVersion()) {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
@@ -11,6 +11,7 @@ package fs
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -73,3 +74,5 @@ func (f *BasicFilesystem) unrootedChecked(absPath, root string) string {
|
||||
func rel(path, prefix string) string {
|
||||
return strings.TrimPrefix(strings.TrimPrefix(path, prefix), string(PathSeparator))
|
||||
}
|
||||
|
||||
var evalSymlinks = filepath.EvalSymlinks
|
||||
|
||||
@@ -12,7 +12,6 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
|
||||
"github.com/syncthing/notify"
|
||||
)
|
||||
@@ -23,13 +22,10 @@ import (
|
||||
var backendBuffer = 500
|
||||
|
||||
func (f *BasicFilesystem) Watch(name string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, error) {
|
||||
evalRoot, err := filepath.EvalSymlinks(f.root)
|
||||
evalRoot, err := evalSymlinks(f.root)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if runtime.GOOS == "windows" {
|
||||
evalRoot = longFilenameSupport(evalRoot)
|
||||
}
|
||||
|
||||
absName, err := rooted(name, evalRoot)
|
||||
if err != nil {
|
||||
|
||||
@@ -34,7 +34,7 @@ func TestMain(m *testing.M) {
|
||||
panic("Cannot get absolute path to working dir")
|
||||
}
|
||||
|
||||
dir, err = filepath.EvalSymlinks(dir)
|
||||
dir, err = evalSymlinks(dir)
|
||||
if err != nil {
|
||||
panic("Cannot get real path to working dir")
|
||||
}
|
||||
|
||||
@@ -210,3 +210,15 @@ func isMaybeWin83(absPath string) bool {
|
||||
}
|
||||
return strings.Contains(strings.TrimPrefix(filepath.Base(absPath), WindowsTempPrefix), "~")
|
||||
}
|
||||
|
||||
func evalSymlinks(in string) (string, error) {
|
||||
out, err := filepath.EvalSymlinks(in)
|
||||
if err != nil && strings.HasPrefix(in, `\\?\`) {
|
||||
// Try again without the `\\?\` prefix
|
||||
out, err = filepath.EvalSymlinks(in[4:])
|
||||
}
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return longFilenameSupport(out), nil
|
||||
}
|
||||
|
||||
673
lib/fs/fakefs.go
Normal file
673
lib/fs/fakefs.go
Normal file
@@ -0,0 +1,673 @@
|
||||
// Copyright (C) 2018 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"hash/fnv"
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"net/url"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
)
|
||||
|
||||
// see readShortAt()
|
||||
const randomBlockShift = 14 // 128k
|
||||
|
||||
// fakefs is a fake filesystem for testing and benchmarking. It has the
|
||||
// following properties:
|
||||
//
|
||||
// - File metadata is kept in RAM. Specifically, we remember which files and
|
||||
// directories exist, their dates, permissions and sizes. Symlinks are
|
||||
// not supported.
|
||||
//
|
||||
// - File contents are generated pseudorandomly with just the file name as
|
||||
// seed. Writes are discarded, other than having the effect of increasing
|
||||
// the file size. If you only write data that you've read from a file with
|
||||
// the same name on a different fakefs, you'll never know the difference...
|
||||
//
|
||||
// - We totally ignore permissions - pretend you are root.
|
||||
//
|
||||
// - The root path can contain URL query-style parameters that pre populate
|
||||
// the filesystem at creation with a certain amount of random data:
|
||||
//
|
||||
// files=n to generate n random files (default 0)
|
||||
// maxsize=n to generate files up to a total of n MiB (default 0)
|
||||
// sizeavg=n to set the average size of random files, in bytes (default 1<<20)
|
||||
// seed=n to set the initial random seed (default 0)
|
||||
//
|
||||
// - Two fakefs:s pointing at the same root path see the same files.
|
||||
//
|
||||
type fakefs struct {
|
||||
mut sync.Mutex
|
||||
root *fakeEntry
|
||||
}
|
||||
|
||||
var (
|
||||
fakefsMut sync.Mutex
|
||||
fakefsFs = make(map[string]*fakefs)
|
||||
)
|
||||
|
||||
func newFakeFilesystem(root string) *fakefs {
|
||||
fakefsMut.Lock()
|
||||
defer fakefsMut.Unlock()
|
||||
|
||||
var params url.Values
|
||||
uri, err := url.Parse(root)
|
||||
if err == nil {
|
||||
root = uri.Path
|
||||
params = uri.Query()
|
||||
}
|
||||
|
||||
if fs, ok := fakefsFs[root]; ok {
|
||||
// Already have an fs at this path
|
||||
return fs
|
||||
}
|
||||
|
||||
fs := &fakefs{
|
||||
root: &fakeEntry{
|
||||
name: "/",
|
||||
isdir: true,
|
||||
mode: 0700,
|
||||
mtime: time.Now(),
|
||||
children: make(map[string]*fakeEntry),
|
||||
},
|
||||
}
|
||||
|
||||
files, _ := strconv.Atoi(params.Get("files"))
|
||||
maxsize, _ := strconv.Atoi(params.Get("maxsize"))
|
||||
sizeavg, _ := strconv.Atoi(params.Get("sizeavg"))
|
||||
seed, _ := strconv.Atoi(params.Get("seed"))
|
||||
if sizeavg == 0 {
|
||||
sizeavg = 1 << 20
|
||||
}
|
||||
|
||||
if files > 0 || maxsize > 0 {
|
||||
// Generate initial data according to specs. Operations in here
|
||||
// *look* like file I/O, but they are not. Do not worry that they
|
||||
// might fail.
|
||||
|
||||
rng := rand.New(rand.NewSource(int64(seed)))
|
||||
var createdFiles int
|
||||
var writtenData int64
|
||||
for (files == 0 || createdFiles < files) && (maxsize == 0 || writtenData>>20 < int64(maxsize)) {
|
||||
dir := filepath.Join(fmt.Sprintf("%02x", rng.Intn(255)), fmt.Sprintf("%02x", rng.Intn(255)))
|
||||
file := fmt.Sprintf("%016x", rng.Int63())
|
||||
fs.MkdirAll(dir, 0755)
|
||||
|
||||
fd, _ := fs.Create(filepath.Join(dir, file))
|
||||
createdFiles++
|
||||
|
||||
fsize := int64(sizeavg/2 + rng.Intn(sizeavg))
|
||||
fd.Truncate(fsize)
|
||||
writtenData += fsize
|
||||
|
||||
ftime := time.Unix(1000000000+rng.Int63n(10*365*86400), 0)
|
||||
fs.Chtimes(filepath.Join(dir, file), ftime, ftime)
|
||||
}
|
||||
}
|
||||
|
||||
// Also create a default folder marker for good measure
|
||||
fs.Mkdir(".stfolder", 0700)
|
||||
|
||||
fakefsFs[root] = fs
|
||||
return fs
|
||||
}
|
||||
|
||||
// fakeEntry is an entry (file or directory) in the fake filesystem
|
||||
type fakeEntry struct {
|
||||
name string
|
||||
isdir bool
|
||||
size int64
|
||||
mode FileMode
|
||||
mtime time.Time
|
||||
children map[string]*fakeEntry
|
||||
}
|
||||
|
||||
func (fs *fakefs) entryForName(name string) *fakeEntry {
|
||||
name = filepath.ToSlash(name)
|
||||
if name == "." || name == "/" {
|
||||
return fs.root
|
||||
}
|
||||
|
||||
name = strings.Trim(name, "/")
|
||||
comps := strings.Split(name, "/")
|
||||
entry := fs.root
|
||||
for _, comp := range comps {
|
||||
var ok bool
|
||||
entry, ok = entry.children[comp]
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
}
|
||||
return entry
|
||||
}
|
||||
|
||||
func (fs *fakefs) Chmod(name string, mode FileMode) error {
|
||||
fs.mut.Lock()
|
||||
defer fs.mut.Unlock()
|
||||
entry := fs.entryForName(name)
|
||||
if entry == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
entry.mode = mode
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||
fs.mut.Lock()
|
||||
defer fs.mut.Unlock()
|
||||
entry := fs.entryForName(name)
|
||||
if entry == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
entry.mtime = mtime
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) Create(name string) (File, error) {
|
||||
fs.mut.Lock()
|
||||
defer fs.mut.Unlock()
|
||||
|
||||
if entry := fs.entryForName(name); entry != nil {
|
||||
if entry.isdir {
|
||||
return nil, os.ErrExist
|
||||
}
|
||||
entry.size = 0
|
||||
entry.mtime = time.Now()
|
||||
entry.mode = 0666
|
||||
return &fakeFile{fakeEntry: entry}, nil
|
||||
}
|
||||
|
||||
dir := filepath.Dir(name)
|
||||
base := filepath.Base(name)
|
||||
entry := fs.entryForName(dir)
|
||||
if entry == nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
new := &fakeEntry{
|
||||
name: base,
|
||||
mode: 0666,
|
||||
mtime: time.Now(),
|
||||
}
|
||||
entry.children[base] = new
|
||||
return &fakeFile{fakeEntry: new}, nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) CreateSymlink(target, name string) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (fs *fakefs) DirNames(name string) ([]string, error) {
|
||||
fs.mut.Lock()
|
||||
defer fs.mut.Unlock()
|
||||
|
||||
entry := fs.entryForName(name)
|
||||
if entry == nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
names := make([]string, 0, len(entry.children))
|
||||
for name := range entry.children {
|
||||
names = append(names, name)
|
||||
}
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) Lstat(name string) (FileInfo, error) {
|
||||
fs.mut.Lock()
|
||||
defer fs.mut.Unlock()
|
||||
|
||||
entry := fs.entryForName(name)
|
||||
if entry == nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return &fakeFileInfo{*entry}, nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) Mkdir(name string, perm FileMode) error {
|
||||
fs.mut.Lock()
|
||||
defer fs.mut.Unlock()
|
||||
|
||||
dir := filepath.Dir(name)
|
||||
base := filepath.Base(name)
|
||||
entry := fs.entryForName(dir)
|
||||
if entry == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
if _, ok := entry.children[base]; ok {
|
||||
return os.ErrExist
|
||||
}
|
||||
|
||||
entry.children[base] = &fakeEntry{
|
||||
name: base,
|
||||
isdir: true,
|
||||
mode: perm,
|
||||
mtime: time.Now(),
|
||||
children: make(map[string]*fakeEntry),
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) MkdirAll(name string, perm FileMode) error {
|
||||
name = filepath.ToSlash(name)
|
||||
name = strings.Trim(name, "/")
|
||||
comps := strings.Split(name, "/")
|
||||
entry := fs.root
|
||||
for _, comp := range comps {
|
||||
next, ok := entry.children[comp]
|
||||
|
||||
if !ok {
|
||||
new := &fakeEntry{
|
||||
name: comp,
|
||||
isdir: true,
|
||||
mode: perm,
|
||||
mtime: time.Now(),
|
||||
children: make(map[string]*fakeEntry),
|
||||
}
|
||||
entry.children[comp] = new
|
||||
next = new
|
||||
} else if !next.isdir {
|
||||
return errors.New("not a directory")
|
||||
}
|
||||
|
||||
entry = next
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) Open(name string) (File, error) {
|
||||
fs.mut.Lock()
|
||||
defer fs.mut.Unlock()
|
||||
|
||||
entry := fs.entryForName(name)
|
||||
if entry == nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
return &fakeFile{fakeEntry: entry}, nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) OpenFile(name string, flags int, mode FileMode) (File, error) {
|
||||
fs.mut.Lock()
|
||||
defer fs.mut.Unlock()
|
||||
|
||||
if flags&os.O_CREATE == 0 {
|
||||
return fs.Open(name)
|
||||
}
|
||||
|
||||
dir := filepath.Dir(name)
|
||||
base := filepath.Base(name)
|
||||
entry := fs.entryForName(dir)
|
||||
if entry == nil {
|
||||
return nil, os.ErrNotExist
|
||||
}
|
||||
|
||||
if flags&os.O_EXCL != 0 {
|
||||
if _, ok := entry.children[base]; ok {
|
||||
return nil, os.ErrExist
|
||||
}
|
||||
}
|
||||
|
||||
newEntry := &fakeEntry{
|
||||
name: base,
|
||||
mode: mode,
|
||||
mtime: time.Now(),
|
||||
}
|
||||
|
||||
entry.children[base] = newEntry
|
||||
return &fakeFile{fakeEntry: newEntry}, nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) ReadSymlink(name string) (string, error) {
|
||||
return "", errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (fs *fakefs) Remove(name string) error {
|
||||
fs.mut.Lock()
|
||||
defer fs.mut.Unlock()
|
||||
|
||||
entry := fs.entryForName(name)
|
||||
if entry == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
if len(entry.children) != 0 {
|
||||
return errors.New("not empty")
|
||||
}
|
||||
|
||||
entry = fs.entryForName(filepath.Dir(name))
|
||||
delete(entry.children, filepath.Base(name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) RemoveAll(name string) error {
|
||||
fs.mut.Lock()
|
||||
defer fs.mut.Unlock()
|
||||
|
||||
entry := fs.entryForName(filepath.Dir(name))
|
||||
if entry == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
// RemoveAll is easy when the file system uses garbage collection under
|
||||
// the hood... We even get the correct semantics for open fd:s for free.
|
||||
delete(entry.children, filepath.Base(name))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) Rename(oldname, newname string) error {
|
||||
fs.mut.Lock()
|
||||
defer fs.mut.Unlock()
|
||||
|
||||
p0 := fs.entryForName(filepath.Dir(oldname))
|
||||
if p0 == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
entry := p0.children[filepath.Base(oldname)]
|
||||
if entry == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
p1 := fs.entryForName(filepath.Dir(newname))
|
||||
if p1 == nil {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
|
||||
dst, ok := p1.children[filepath.Base(newname)]
|
||||
if ok && dst.isdir {
|
||||
return errors.New("is a directory")
|
||||
}
|
||||
|
||||
p1.children[filepath.Base(newname)] = entry
|
||||
delete(p0.children, filepath.Base(oldname))
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) Stat(name string) (FileInfo, error) {
|
||||
return fs.Lstat(name)
|
||||
}
|
||||
|
||||
func (fs *fakefs) SymlinksSupported() bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func (fs *fakefs) Walk(name string, walkFn WalkFunc) error {
|
||||
return errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (fs *fakefs) Watch(path string, ignore Matcher, ctx context.Context, ignorePerms bool) (<-chan Event, error) {
|
||||
return nil, ErrWatchNotSupported
|
||||
}
|
||||
|
||||
func (fs *fakefs) Hide(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) Unhide(name string) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) Glob(pattern string) ([]string, error) {
|
||||
// gnnh we don't seem to actually require this in practice
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (fs *fakefs) Roots() ([]string, error) {
|
||||
return []string{"/"}, nil
|
||||
}
|
||||
|
||||
func (fs *fakefs) Usage(name string) (Usage, error) {
|
||||
return Usage{}, errors.New("not implemented")
|
||||
}
|
||||
|
||||
func (fs *fakefs) Type() FilesystemType {
|
||||
return FilesystemTypeFake
|
||||
}
|
||||
|
||||
func (fs *fakefs) URI() string {
|
||||
return "fake://" + fs.root.name
|
||||
}
|
||||
|
||||
func (fs *fakefs) SameFile(fi1, fi2 FileInfo) bool {
|
||||
return fi1.Name() == fi1.Name()
|
||||
}
|
||||
|
||||
// fakeFile is the representation of an open file. We don't care if it's
|
||||
// opened for reading or writing, it's all good.
|
||||
type fakeFile struct {
|
||||
*fakeEntry
|
||||
mut sync.Mutex
|
||||
rng io.Reader
|
||||
seed int64
|
||||
offset int64
|
||||
seedOffs int64
|
||||
}
|
||||
|
||||
func (f *fakeFile) Close() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeFile) Read(p []byte) (int, error) {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
return f.readShortAt(p, f.offset)
|
||||
}
|
||||
|
||||
func (f *fakeFile) ReadAt(p []byte, offs int64) (int, error) {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
|
||||
// ReadAt is spec:ed to always read a full block unless EOF or failure,
|
||||
// so we must loop. It's also not supposed to affect the seek position,
|
||||
// but that would make things annoying or inefficient in terms of
|
||||
// generating the appropriate RNG etc so I ignore that. In practice we
|
||||
// currently don't depend on that aspect of it...
|
||||
|
||||
var read int
|
||||
for {
|
||||
n, err := f.readShortAt(p[read:], offs+int64(read))
|
||||
read += n
|
||||
if err != nil {
|
||||
return read, err
|
||||
}
|
||||
if read == len(p) {
|
||||
return read, nil
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (f *fakeFile) readShortAt(p []byte, offs int64) (int, error) {
|
||||
// Here be a certain amount of magic... We want to return pseudorandom,
|
||||
// predictable data so that a read from the same offset in the same file
|
||||
// always returns the same data. But the RNG is a stream, and reads can
|
||||
// be random.
|
||||
//
|
||||
// We split the file into "blocks" numbered by "seedNo", where each
|
||||
// block becomes an instantiation of the RNG, seeded with the hash of
|
||||
// the file number plus the seedNo (block number). We keep the RNG
|
||||
// around in the hope that the next read will be sequential to this one
|
||||
// and we can continue reading from the same RNG.
|
||||
//
|
||||
// When that's not the case we create a new RNG for the block we are in,
|
||||
// read as many bytes from it as necessary to get to the right offset,
|
||||
// and then serve the read from there. We limit the length of the read
|
||||
// to the end of the block, as another RNG needs to be created to serve
|
||||
// the next block.
|
||||
//
|
||||
// The size of the blocks are a matter of taste... Larger blocks give
|
||||
// better performance for sequential reads, but worse for random reads
|
||||
// as we often need to generate and throw away a lot of data at the
|
||||
// start of the block to serve a given read. 128 KiB blocks fit
|
||||
// reasonably well with the type of IO Syncthing tends to do.
|
||||
|
||||
if f.isdir {
|
||||
return 0, errors.New("is a directory")
|
||||
}
|
||||
|
||||
if offs >= f.size {
|
||||
return 0, io.EOF
|
||||
}
|
||||
|
||||
// Lazily calculate our main seed, a simple 64 bit FNV hash our file
|
||||
// name.
|
||||
if f.seed == 0 {
|
||||
hf := fnv.New64()
|
||||
hf.Write([]byte(f.name))
|
||||
f.seed = int64(hf.Sum64())
|
||||
}
|
||||
|
||||
// Check whether the read is a continuation of an RNG we already have or
|
||||
// we need to set up a new one.
|
||||
seedNo := offs >> randomBlockShift
|
||||
minOffs := seedNo << randomBlockShift
|
||||
nextBlockOffs := (seedNo + 1) << randomBlockShift
|
||||
if f.rng == nil || f.offset != offs || seedNo != f.seedOffs {
|
||||
// This is not a straight read continuing from a previous one
|
||||
f.rng = rand.New(rand.NewSource(f.seed + seedNo))
|
||||
|
||||
// If the read is not at the start of the block, discard data
|
||||
// accordingly.
|
||||
diff := offs - minOffs
|
||||
if diff > 0 {
|
||||
lr := io.LimitReader(f.rng, diff)
|
||||
io.Copy(ioutil.Discard, lr)
|
||||
}
|
||||
|
||||
f.offset = offs
|
||||
f.seedOffs = seedNo
|
||||
}
|
||||
|
||||
size := len(p)
|
||||
|
||||
// Don't read past the end of the file
|
||||
if offs+int64(size) > f.size {
|
||||
size = int(f.size - offs)
|
||||
}
|
||||
|
||||
// Don't read across the block boundary
|
||||
if offs+int64(size) > nextBlockOffs {
|
||||
size = int(nextBlockOffs - offs)
|
||||
}
|
||||
|
||||
f.offset += int64(size)
|
||||
return f.rng.Read(p[:size])
|
||||
}
|
||||
|
||||
func (f *fakeFile) Seek(offset int64, whence int) (int64, error) {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
|
||||
if f.isdir {
|
||||
return 0, errors.New("is a directory")
|
||||
}
|
||||
|
||||
f.rng = nil
|
||||
|
||||
switch whence {
|
||||
case io.SeekCurrent:
|
||||
f.offset += offset
|
||||
case io.SeekEnd:
|
||||
f.offset = f.size - offset
|
||||
case io.SeekStart:
|
||||
f.offset = offset
|
||||
}
|
||||
if f.offset < 0 {
|
||||
f.offset = 0
|
||||
return f.offset, errors.New("seek before start")
|
||||
}
|
||||
if f.offset > f.size {
|
||||
f.offset = f.size
|
||||
return f.offset, io.EOF
|
||||
}
|
||||
return f.offset, nil
|
||||
}
|
||||
|
||||
func (f *fakeFile) Write(p []byte) (int, error) {
|
||||
return f.WriteAt(p, f.offset)
|
||||
}
|
||||
|
||||
func (f *fakeFile) WriteAt(p []byte, off int64) (int, error) {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
|
||||
if f.isdir {
|
||||
return 0, errors.New("is a directory")
|
||||
}
|
||||
|
||||
f.rng = nil
|
||||
f.offset = off + int64(len(p))
|
||||
if f.offset > f.size {
|
||||
f.size = f.offset
|
||||
}
|
||||
return len(p), nil
|
||||
}
|
||||
|
||||
func (f *fakeFile) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f *fakeFile) Truncate(size int64) error {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
|
||||
f.rng = nil
|
||||
f.size = size
|
||||
if f.offset > size {
|
||||
f.offset = size
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f *fakeFile) Stat() (FileInfo, error) {
|
||||
return &fakeFileInfo{*f.fakeEntry}, nil
|
||||
}
|
||||
|
||||
func (f *fakeFile) Sync() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// fakeFileInfo is the stat result.
|
||||
type fakeFileInfo struct {
|
||||
fakeEntry // intentionally a copy of the struct
|
||||
}
|
||||
|
||||
func (f *fakeFileInfo) Name() string {
|
||||
return f.name
|
||||
}
|
||||
|
||||
func (f *fakeFileInfo) Mode() FileMode {
|
||||
return f.mode
|
||||
}
|
||||
|
||||
func (f *fakeFileInfo) Size() int64 {
|
||||
return f.size
|
||||
}
|
||||
|
||||
func (f *fakeFileInfo) ModTime() time.Time {
|
||||
return f.mtime
|
||||
}
|
||||
|
||||
func (f *fakeFileInfo) IsDir() bool {
|
||||
return f.isdir
|
||||
}
|
||||
|
||||
func (f *fakeFileInfo) IsRegular() bool {
|
||||
return !f.isdir
|
||||
}
|
||||
|
||||
func (f *fakeFileInfo) IsSymlink() bool {
|
||||
return false
|
||||
}
|
||||
154
lib/fs/fakefs_test.go
Normal file
154
lib/fs/fakefs_test.go
Normal file
@@ -0,0 +1,154 @@
|
||||
// Copyright (C) 2018 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 https://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestFakeFS(t *testing.T) {
|
||||
// Test some basic aspects of the fakefs
|
||||
|
||||
fs := newFakeFilesystem("/foo/bar/baz")
|
||||
|
||||
// MkdirAll
|
||||
err := fs.MkdirAll("dira/dirb", 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
info, err := fs.Stat("dira/dirb")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Mkdir
|
||||
err = fs.Mkdir("dira/dirb/dirc", 0755)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
info, err = fs.Stat("dira/dirb/dirc")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Create
|
||||
fd, err := fs.Create("/dira/dirb/test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Write
|
||||
_, err = fd.Write([]byte("hello"))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Stat on fd
|
||||
info, err = fd.Stat()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Name() != "test" {
|
||||
t.Error("wrong name:", info.Name())
|
||||
}
|
||||
if info.Size() != 5 {
|
||||
t.Error("wrong size:", info.Size())
|
||||
}
|
||||
|
||||
// Stat on fs
|
||||
info, err = fs.Stat("dira/dirb/test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if info.Name() != "test" {
|
||||
t.Error("wrong name:", info.Name())
|
||||
}
|
||||
if info.Size() != 5 {
|
||||
t.Error("wrong size:", info.Size())
|
||||
}
|
||||
|
||||
// Seek
|
||||
_, err = fd.Seek(1, os.SEEK_SET)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// Read
|
||||
bs0, err := ioutil.ReadAll(fd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(bs0) != 4 {
|
||||
t.Error("wrong number of bytes:", len(bs0))
|
||||
}
|
||||
|
||||
// Read again, same data hopefully
|
||||
_, err = fd.Seek(0, os.SEEK_SET)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
bs1, err := ioutil.ReadAll(fd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(bs0, bs1[1:]) {
|
||||
t.Error("wrong data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestFakeFSRead(t *testing.T) {
|
||||
// Test some basic aspects of the fakefs
|
||||
|
||||
fs := newFakeFilesystem("/foo/bar/baz")
|
||||
|
||||
// Create
|
||||
fd, _ := fs.Create("test")
|
||||
fd.Truncate(3 * 1 << randomBlockShift)
|
||||
|
||||
// Read
|
||||
fd.Seek(0, 0)
|
||||
bs0, err := ioutil.ReadAll(fd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(bs0) != 3*1<<randomBlockShift {
|
||||
t.Error("wrong number of bytes:", len(bs0))
|
||||
}
|
||||
|
||||
// Read again, starting at an odd offset
|
||||
fd.Seek(0, 0)
|
||||
buf0 := make([]byte, 12345)
|
||||
n, _ := fd.Read(buf0)
|
||||
if n != len(buf0) {
|
||||
t.Fatal("short read")
|
||||
}
|
||||
buf1, err := ioutil.ReadAll(fd)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if len(buf1) != 3*1<<randomBlockShift-len(buf0) {
|
||||
t.Error("wrong number of bytes:", len(buf1))
|
||||
}
|
||||
|
||||
bs1 := append(buf0, buf1...)
|
||||
if !bytes.Equal(bs0, bs1) {
|
||||
t.Error("data mismatch")
|
||||
}
|
||||
|
||||
// Read large block with ReadAt
|
||||
bs2 := make([]byte, 3*1<<randomBlockShift)
|
||||
_, err = fd.ReadAt(bs2, 0)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if !bytes.Equal(bs0, bs2) {
|
||||
t.Error("data mismatch")
|
||||
}
|
||||
}
|
||||
@@ -167,6 +167,8 @@ func NewFilesystem(fsType FilesystemType, uri string) Filesystem {
|
||||
switch fsType {
|
||||
case FilesystemTypeBasic:
|
||||
fs = newBasicFilesystem(uri)
|
||||
case FilesystemTypeFake:
|
||||
fs = newFakeFilesystem(uri)
|
||||
default:
|
||||
l.Debugln("Unknown filesystem", fsType, uri)
|
||||
fs = &errorFilesystem{
|
||||
|
||||
@@ -8,8 +8,28 @@ package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var execExts map[string]bool
|
||||
|
||||
func init() {
|
||||
// PATHEXT contains a list of executable file extensions, on Windows
|
||||
pathext := filepath.SplitList(os.Getenv("PATHEXT"))
|
||||
// We want the extensions in execExts to be lower case
|
||||
execExts = make(map[string]bool, len(pathext))
|
||||
for _, ext := range pathext {
|
||||
execExts[strings.ToLower(ext)] = true
|
||||
}
|
||||
}
|
||||
|
||||
// isWindowsExecutable returns true if the given path has an extension that is
|
||||
// in the list of executable extensions.
|
||||
func isWindowsExecutable(path string) bool {
|
||||
return execExts[strings.ToLower(filepath.Ext(path))]
|
||||
}
|
||||
|
||||
func (e fsFileInfo) Mode() FileMode {
|
||||
m := e.FileInfo.Mode()
|
||||
if m&os.ModeSymlink != 0 && e.Size() > 0 {
|
||||
@@ -17,5 +37,14 @@ func (e fsFileInfo) Mode() FileMode {
|
||||
// NTFS deduped files. Remove the symlink bit.
|
||||
m &^= os.ModeSymlink
|
||||
}
|
||||
// Set executable bits on files with executable extenions (.exe, .bat, etc).
|
||||
if isWindowsExecutable(e.Name()) {
|
||||
m |= 0111
|
||||
}
|
||||
// There is no user/group/others in Windows' read-only attribute, and
|
||||
// all "w" bits are set if the file is not read-only. Do not send these
|
||||
// group/others-writable bits to other devices in order to avoid
|
||||
// unexpected world-writable files on other platforms.
|
||||
m &^= 0022
|
||||
return FileMode(m)
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user