Compare commits
16
Commits
@@ -26,6 +26,7 @@ import (
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
"syscall"
|
||||
"text/template"
|
||||
"time"
|
||||
)
|
||||
@@ -161,9 +162,8 @@ func main() {
|
||||
}
|
||||
install(targets["all"], tags)
|
||||
|
||||
vet("./cmd/syncthing")
|
||||
vet("./lib/...")
|
||||
lint("./cmd/syncthing")
|
||||
vet("cmd", "lib")
|
||||
lint("./cmd/...")
|
||||
lint("./lib/...")
|
||||
return
|
||||
}
|
||||
@@ -230,11 +230,10 @@ func main() {
|
||||
clean()
|
||||
|
||||
case "vet":
|
||||
vet("./cmd/syncthing")
|
||||
vet("./lib/...")
|
||||
vet("cmd", "lib")
|
||||
|
||||
case "lint":
|
||||
lint("./cmd/syncthing")
|
||||
lint("./cmd/...")
|
||||
lint("./lib/...")
|
||||
|
||||
default:
|
||||
@@ -852,24 +851,25 @@ func zipFile(out string, files []archiveFile) {
|
||||
}
|
||||
}
|
||||
|
||||
func vet(pkg string) {
|
||||
bs, err := runError("go", "vet", pkg)
|
||||
if err != nil && err.Error() == "exit status 3" || bytes.Contains(bs, []byte("no such tool \"vet\"")) {
|
||||
// Go said there is no go vet
|
||||
log.Println(`- No go vet, no vetting. Try "go get -u golang.org/x/tools/cmd/vet".`)
|
||||
return
|
||||
func vet(dirs ...string) {
|
||||
params := []string{"tool", "vet", "-all"}
|
||||
params = append(params, dirs...)
|
||||
bs, err := runError("go", params...)
|
||||
|
||||
if len(bs) > 0 {
|
||||
log.Printf("%s", bs)
|
||||
}
|
||||
|
||||
falseAlarmComposites := regexp.MustCompile("composite literal uses unkeyed fields")
|
||||
exitStatus := regexp.MustCompile("exit status 1")
|
||||
for _, line := range bytes.Split(bs, []byte("\n")) {
|
||||
if falseAlarmComposites.Match(line) || exitStatus.Match(line) {
|
||||
continue
|
||||
}
|
||||
if len(line) > 0 {
|
||||
log.Printf("%s", line)
|
||||
if err != nil {
|
||||
if exitStatus(err) == 3 {
|
||||
// Exit code 3, the "vet" tool is not installed
|
||||
return
|
||||
}
|
||||
|
||||
// A genuine error exit from the vet tool.
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func lint(pkg string) {
|
||||
@@ -908,3 +908,13 @@ func macosCodesign(file string) {
|
||||
log.Println("Codesign: successfully signed", file)
|
||||
}
|
||||
}
|
||||
|
||||
func exitStatus(err error) int {
|
||||
if err, ok := err.(*exec.ExitError); ok {
|
||||
if ws, ok := err.ProcessState.Sys().(syscall.WaitStatus); ok {
|
||||
return ws.ExitStatus()
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
)
|
||||
|
||||
// An IntHeap is a min-heap of ints.
|
||||
type SizedElement struct {
|
||||
key string
|
||||
size int
|
||||
|
||||
@@ -32,6 +32,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/discover"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/relay"
|
||||
@@ -85,7 +86,7 @@ type modelIntf interface {
|
||||
CurrentFolderFile(folder string, file string) (protocol.FileInfo, bool)
|
||||
CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool)
|
||||
ResetFolder(folder string)
|
||||
Availability(folder, file string) []protocol.DeviceID
|
||||
Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []model.Availability
|
||||
GetIgnores(folder string) ([]string, []string, error)
|
||||
SetIgnores(folder string, content []string) error
|
||||
PauseDevice(device protocol.DeviceID)
|
||||
@@ -199,7 +200,10 @@ func (s *apiService) getListener(guiCfg config.GUIConfiguration) (net.Listener,
|
||||
return nil, err
|
||||
}
|
||||
|
||||
listener := &tlsutil.DowngradingListener{rawListener, tlsCfg}
|
||||
listener := &tlsutil.DowngradingListener{
|
||||
Listener: rawListener,
|
||||
TLSConfig: tlsCfg,
|
||||
}
|
||||
return listener, nil
|
||||
}
|
||||
|
||||
@@ -693,7 +697,7 @@ func (s *apiService) getDBFile(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
av := s.model.Availability(folder, file)
|
||||
av := s.model.Availability(folder, file, protocol.Vector{}, protocol.BlockInfo{})
|
||||
sendJSON(w, map[string]interface{}{
|
||||
"global": jsonFileInfo(gf),
|
||||
"local": jsonFileInfo(lf),
|
||||
|
||||
+28
-22
@@ -38,15 +38,19 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/nat"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/relay"
|
||||
"github.com/syncthing/syncthing/lib/symlinks"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
"github.com/syncthing/syncthing/lib/upnp"
|
||||
"github.com/syncthing/syncthing/lib/util"
|
||||
|
||||
// Registers NAT service providers
|
||||
_ "github.com/syncthing/syncthing/lib/pmp"
|
||||
_ "github.com/syncthing/syncthing/lib/upnp"
|
||||
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
@@ -117,7 +121,6 @@ func init() {
|
||||
var (
|
||||
myID protocol.DeviceID
|
||||
stop = make(chan int)
|
||||
cert tls.Certificate
|
||||
lans []*net.IPNet
|
||||
)
|
||||
|
||||
@@ -558,10 +561,6 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
}
|
||||
}
|
||||
|
||||
// We reinitialize the predictable RNG with our device ID, to get a
|
||||
// sequence that is always the same but unique to this syncthing instance.
|
||||
util.PredictableRandom.Seed(util.SeedFromBytes(cert.Certificate[0]))
|
||||
|
||||
myID = protocol.NewDeviceID(cert.Certificate[0])
|
||||
l.SetPrefix(fmt.Sprintf("[%s] ", myID.String()[:5]))
|
||||
|
||||
@@ -710,23 +709,30 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
|
||||
mainService.Add(m)
|
||||
|
||||
// The default port we announce, possibly modified by setupUPnP next.
|
||||
// Start NAT service
|
||||
var natService *nat.Service
|
||||
var mappings []*nat.Mapping
|
||||
if opts.NATEnabled {
|
||||
natService = nat.NewService(myID, cfg)
|
||||
for _, addrStr := range opts.ListenAddress {
|
||||
uri, err := url.Parse(addrStr)
|
||||
if err != nil {
|
||||
l.Fatalf("Failed to parse listen address %s: %v", addrStr, err)
|
||||
}
|
||||
|
||||
uri, err := url.Parse(opts.ListenAddress[0])
|
||||
if err != nil {
|
||||
l.Fatalf("Failed to parse listen address %s: %v", opts.ListenAddress[0], err)
|
||||
}
|
||||
if uri.Scheme == "tcp" || uri.Scheme == "tcp4" {
|
||||
addr, err := net.ResolveTCPAddr(uri.Scheme, uri.Host)
|
||||
if err != nil {
|
||||
l.Fatalln("Bad listen address:", err)
|
||||
}
|
||||
if addr.Port == 0 {
|
||||
l.Fatalf("Listen address %s: invalid port", uri)
|
||||
}
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", uri.Host)
|
||||
if err != nil {
|
||||
l.Fatalln("Bad listen address:", err)
|
||||
}
|
||||
|
||||
// Start UPnP
|
||||
var upnpService *upnp.Service
|
||||
if opts.UPnPEnabled {
|
||||
upnpService = upnp.NewUPnPService(cfg, addr.Port)
|
||||
mainService.Add(upnpService)
|
||||
mappings = append(mappings, natService.NewMapping(nat.TCP, addr.IP, addr.Port))
|
||||
}
|
||||
}
|
||||
mainService.Add(natService)
|
||||
}
|
||||
|
||||
// Start relay management
|
||||
@@ -744,7 +750,7 @@ func syncthingMain(runtimeOptions RuntimeOptions) {
|
||||
|
||||
// Start connection management
|
||||
|
||||
connectionService := connections.NewConnectionService(cfg, myID, m, tlsCfg, cachedDiscovery, upnpService, relayService, bepProtocolName, tlsDefaultCommonName, lans)
|
||||
connectionService := connections.NewConnectionService(cfg, myID, m, tlsCfg, cachedDiscovery, mappings, relayService, bepProtocolName, tlsDefaultCommonName, lans)
|
||||
mainService.Add(connectionService)
|
||||
|
||||
if cfg.Options().GlobalAnnEnabled {
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/stats"
|
||||
)
|
||||
@@ -57,7 +58,7 @@ func (m *mockedModel) CurrentGlobalFile(folder string, file string) (protocol.Fi
|
||||
func (m *mockedModel) ResetFolder(folder string) {
|
||||
}
|
||||
|
||||
func (m *mockedModel) Availability(folder, file string) []protocol.DeviceID {
|
||||
func (m *mockedModel) Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []model.Availability {
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
@@ -147,11 +147,13 @@ func (s *verboseService) formatEvent(ev events.Event) string {
|
||||
data := ev.Data.(map[string]string)
|
||||
device := data["device"]
|
||||
return fmt.Sprintf("Device %v was resumed", device)
|
||||
|
||||
case events.ExternalPortMappingChanged:
|
||||
data := ev.Data.(map[string]int)
|
||||
port := data["port"]
|
||||
return fmt.Sprintf("External port mapping changed; new port is %d.", port)
|
||||
data := ev.Data.(map[string]interface{})
|
||||
protocol := data["protocol"]
|
||||
local := data["local"]
|
||||
added := data["added"]
|
||||
removed := data["removed"]
|
||||
return fmt.Sprintf("External port mapping changed; protocol: %s, local: %s, added: %s, removed: %s", protocol, local, added, removed)
|
||||
case events.RelayStateChanged:
|
||||
data := ev.Data.(map[string][]string)
|
||||
newRelays := data["new"]
|
||||
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "пълна документация",
|
||||
"items": "елемента",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} желае да сподели папка \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "documentació sencera",
|
||||
"items": "Elements",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartir la carpeta \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "Documentació completa",
|
||||
"items": "Elements",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vol compartit la carpeta \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
"Add": "Přidat",
|
||||
"Add Device": "Přidat přístroj",
|
||||
"Add Folder": "Přidat adresář",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add Remote Device": "Přidat vzdálené zařízení",
|
||||
"Add new folder?": "Přidat nový adresář?",
|
||||
"Address": "Adresa",
|
||||
"Addresses": "Adresy",
|
||||
"Advanced": "Pokročilé",
|
||||
"Advanced Configuration": "Pokročilá nastavení",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"Advanced settings": "Pokročilá nastavení",
|
||||
"All Data": "Všechna data",
|
||||
"Allow Anonymous Usage Reporting?": "Povolit anonymní hlášení o používání?",
|
||||
"Alphabetic": "Abecedně",
|
||||
@@ -34,12 +34,12 @@
|
||||
"Connection Error": "Chyba připojení",
|
||||
"Copied from elsewhere": "Zkopírováno odjinud",
|
||||
"Copied from original": "Zkopírováno z originálu",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 následující přispěvatelé:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 následující přispěvatelé:",
|
||||
"Danger!": "Pozor!",
|
||||
"Delete": "Smazat",
|
||||
"Deleted": "Smazáno",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Zařízení \"{{name}}\" ({{device}} na {{address}}) se chce připojit. Přidat nové zařízení?",
|
||||
"Device ID": "ID přístroje",
|
||||
"Device Identification": "Identifikace přístroje",
|
||||
"Device Name": "Jméno přístroje",
|
||||
@@ -70,7 +70,7 @@
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Soubory jsou chráněny před změnami na ostatních přístrojích, ale změny provedené z tohoto přístroje budou rozeslány na zbytek clusteru.",
|
||||
"Folder": "Adresář",
|
||||
"Folder ID": "ID adresáře",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Label": "Jmenovka adresáře",
|
||||
"Folder Master": "Master adresář",
|
||||
"Folder Path": "Cesta k adresáři",
|
||||
"Folders": "Adresáře",
|
||||
@@ -116,7 +116,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "Vypnuta",
|
||||
"Oldest First": "Od nejstaršího",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Toto zařízení",
|
||||
"Options": "Nastavení",
|
||||
"Out of Sync": "Nesesynchronizováno",
|
||||
"Out of Sync Items": "Nesesynchronizované položky",
|
||||
@@ -138,9 +138,9 @@
|
||||
"Relayed via": "Přenášené přes",
|
||||
"Relays": "Přenašeče",
|
||||
"Release Notes": "Poznámky k vydání",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remote Devices": "Vzdálená zařízení",
|
||||
"Remove": "Odstranit",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Požadovaný identifikátor adresáře. Musí být stejný na všech zařízeních.",
|
||||
"Rescan": "Opakovat skenování",
|
||||
"Rescan All": "Opakovat skenování všech",
|
||||
"Rescan Interval": "Interval opakování skenování",
|
||||
@@ -211,7 +211,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Limit rychlosti musí být nezáporné číslo (0: bez limitu)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Interval opakování skenování musí být pozitivní číslo.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Nové pokusy o synchronizaci budou probíhat automaticky a položky budou synchronizovány jakmile bude chyba odstraněna.",
|
||||
"This Device": "This Device",
|
||||
"This Device": "Toto zařízení",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "To může útočníkům jednoduše povolit čtení a úpravy souborů na vašem přístroji. ",
|
||||
"This is a major version upgrade.": "Toto je důležitá aktualizace.",
|
||||
"Trash Can File Versioning": "Verzování souborů v koši",
|
||||
@@ -229,7 +229,7 @@
|
||||
"Version": "Verze",
|
||||
"Versions Path": "Cesta k verzím",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Verze jsou automaticky smazány, pokud jsou starší než maximální časový limit nebo překročí počet souborů povolených pro interval.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varování: tato cesta je podadresářem existujícího adresáře \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Při přidávání nového přístroje mějte na paměti, že je ho třeba také zadat na druhé straně.",
|
||||
"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.": "Při přidávání nového adresáře mějte na paměti, že jeho ID je použito ke svázání adresářů napříč přístoji. Rozlišují se malá a velká písmena a musí přesně souhlasit mezi všemi přístroji.",
|
||||
"Yes": "Ano",
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "plná dokumentace",
|
||||
"items": "položky",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce sdílet adresář \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} chce sdílet adresář \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "Fuld dokumentation",
|
||||
"items": "poster",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker at dele mappen \"{{folder}}\". ",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -39,7 +39,7 @@
|
||||
"Danger!": "Achtung!",
|
||||
"Delete": "Löschen",
|
||||
"Deleted": "Gelöscht",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Gerät \"{{name}}\" ({{device}} {{address}}) möchte sich verbinden. Gerät hinzufügen?",
|
||||
"Device ID": "Geräte ID",
|
||||
"Device Identification": "Geräte Identifikation",
|
||||
"Device Name": "Gerätename",
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "Komplette Dokumentation",
|
||||
"items": "Objekte",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} möchte das Verzeichnis \"{{folder}}\" teilen.",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} möchte das Verzeichnis \"{{folderLabel}}\" ({{folder}}) teilen."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} möchte das Verzeichnis \"{{folderLabel}}\" ({{folder}}) teilen.",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} möchte das Verzeichnis \"{{folderLabel}}\" ({{folder}}) teilen."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "πλήρης τεκμηρίωση",
|
||||
"items": "εγγραφές",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "Η συσκευή {{device}} θέλει να μοιράσει τον φάκελο «{{folder}}».",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "full documentation",
|
||||
"items": "items",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,6 +55,7 @@
|
||||
"Edit Device": "Edit Device",
|
||||
"Edit Folder": "Edit Folder",
|
||||
"Editing": "Editing",
|
||||
"Enable NAT traversal": "Enable NAT traversal",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable UPnP": "Enable UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
@@ -238,5 +239,6 @@
|
||||
"full documentation": "full documentation",
|
||||
"items": "items",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "Documentación completa",
|
||||
"items": "Elementos",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir la carpeta \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "documentación completa",
|
||||
"items": "ítems",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quiere compartir repositorio \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} qiuere compartir el repositorio \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} qiuere compartir el repositorio \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "täysi dokumentaatio",
|
||||
"items": "kohteet",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} haluaa jakaa kansion \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "documentation complète",
|
||||
"items": "éléments",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le dossier \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "documentation complète",
|
||||
"items": "fichiers",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} veut partager le dossier \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} veut partager le dossier \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} veut partager le dossier \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} veut partager le dossier \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
"Add": "Taheakje",
|
||||
"Add Device": "Apparaat taheakje",
|
||||
"Add Folder": "Map taheakje",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add Remote Device": "Apparaat op Ofstân Taheakje",
|
||||
"Add new folder?": "Nije map taheakje?",
|
||||
"Address": "Adres",
|
||||
"Addresses": "Adressen",
|
||||
"Advanced": "Avansearre",
|
||||
"Advanced Configuration": "Avansearre konfiguraasje",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"Advanced settings": "Avansearre ynstellings",
|
||||
"All Data": "Alle data",
|
||||
"Allow Anonymous Usage Reporting?": "Anonime brûkensrapportaazje tastean?",
|
||||
"Alphabetic": "Alfabetysk",
|
||||
@@ -34,12 +34,12 @@
|
||||
"Connection Error": "Ferbiningsflater",
|
||||
"Copied from elsewhere": "Oernommen fan earne oars",
|
||||
"Copied from original": "Oernommen fan orizjineel",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 de folgende bydragers:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 de folgende bydragers:",
|
||||
"Danger!": "Gefaar!",
|
||||
"Delete": "Fuortsmite",
|
||||
"Deleted": "Fuortsmiten",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Apparaat \"{{name}}\" {{device}} op ({{address}}) wol ferbining meitsje. Nij apparaat taheakje?",
|
||||
"Device ID": "Apparaat-ID",
|
||||
"Device Identification": "Apparaatidentifikaasje",
|
||||
"Device Name": "Apparaatnamme",
|
||||
@@ -67,10 +67,10 @@
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Bits foar triemrjochten wurde negearre yn it sykjen foar feroarings. Brûk dit op FAT-triemsystemen.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Triemen wurde ferset nei map .stversions wannear't 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 ferstjoerd.",
|
||||
"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.",
|
||||
"Folder": "Map",
|
||||
"Folder ID": "Map-ID",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Label": "Map-opskrift",
|
||||
"Folder Master": "Map-master",
|
||||
"Folder Path": "Map-paad",
|
||||
"Folders": "Mappen",
|
||||
@@ -116,7 +116,7 @@
|
||||
"OK": "Okee",
|
||||
"Off": "Ut",
|
||||
"Oldest First": "Aldste earst",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Opsjoneel beskriuwend opskrift foar de map. Mei op ider apparaat oars wêze.",
|
||||
"Options": "Opsjes",
|
||||
"Out of Sync": "Net syngronisearre",
|
||||
"Out of Sync Items": "Net syngronisearre items",
|
||||
@@ -138,9 +138,9 @@
|
||||
"Relayed via": "Trochjûn fia",
|
||||
"Relays": "Trochjouers",
|
||||
"Release Notes": "Utjeftenotysjes",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remote Devices": "Apparaten op Ofstân",
|
||||
"Remove": "Fuortsmite",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Ferplicht ID foar de map. Moat op alle bondelapparaten itselde wêze.",
|
||||
"Rescan": "Sken opnij",
|
||||
"Rescan All": "Sken alles opnij",
|
||||
"Rescan Interval": "Wersken ynterval",
|
||||
@@ -161,11 +161,11 @@
|
||||
"Share With Devices": "Diele mei apparaten",
|
||||
"Share this folder?": "Dizze map diele?",
|
||||
"Shared With": "Dielt mei",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Koart opskrift foar de map. Moat op alle apparaten itselde wêze.",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Koarte ID foar de map. Moat op alle bondelapparaten itselde wêze.",
|
||||
"Show ID": "ID sjen litte",
|
||||
"Show QR": "QR sjen litte",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Wurd ynstee fan apparaat-ID sjen litten by de bondeltastân. Wurd nei oare apparaten advertearre as in mooglike standertnamme.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wurd yn de bondel-tastân sjen litten ynstee fan apparaat-ID. Wannear't leech litten wurd, wurd it fernijt nei de namme die it apparaat útstjoert.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Wurd yn de bondeltastân sjen litten ynstee fan apparaat-ID. Wannear't leech litten wurd, wurd it fernijt nei de namme die it apparaat útstjoert.",
|
||||
"Shutdown": "Ofslute",
|
||||
"Shutdown Complete": "Ofsluten klear",
|
||||
"Simple File Versioning": "Ienfâldich triemferzjebehear",
|
||||
@@ -211,7 +211,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "It fluggenslimyt moat in posityf nûmer wêze (0: gjin limyt)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "It wersken-ynterfal moat in posityf tal fan sekonden wêze.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Sy wurde automatysk opnij probearre en sille syngronisearre wurde wannear at de flater oplost is.",
|
||||
"This Device": "This Device",
|
||||
"This Device": "Dit Apparaat",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Dit kin samar ynkringers (hackers) tagong jaan om elke triem op jo kompjûter te besjen en te feroarjen.",
|
||||
"This is a major version upgrade.": "Dit is in wichtige ferzjefernijing.",
|
||||
"Trash Can File Versioning": "Jiskefet-triemferzjebehear",
|
||||
@@ -229,7 +229,7 @@
|
||||
"Version": "Ferzje",
|
||||
"Versions Path": "Ferzjes-paad",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Ferzjes wurde automatysk fuortsmiten wannear't se âlder binne dan de maksimale âldens of wannear it tal fan triemen yn in ynterval grutter is dan tastean.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warskôging, dit paad is in ûnderlizzende triemtafel fan in besteande map \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Hâld by it taheakjen fan in nij apparaat yn de holle dat it apparaat oan de oare kant ek taheakke wurde moat. ",
|
||||
"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.": "Hâld by it taheakjen fan in nije map yn de holle dat de map-ID brûkt wurd om de mappen tusken apparaten mei-inoar te ferbinen. Se binne haadlettergefoelich en moatte oer alle apparaten eksakt oerienkomme.",
|
||||
"Yes": "Ja",
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "komplete dokumintaasje",
|
||||
"items": "items",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wol map \"{{folder}}\" diele.",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wol de map \"{{folderLabel}}\" ({{folder}}) diele.",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wol map \"{{folderlabel}}\" ({{folder}}) diele."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "teljes dokumentáció",
|
||||
"items": "elem",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} meg szeretné osztani a \"{{folder}}\" nevű mappát.",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} meg szeretné osztani \"{{folderLabel}}\" ({{folder}}) mappát."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} meg szeretné osztani \"{{folderLabel}}\" ({{folder}}) mappát.",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} szeretné megosztani \"{{folderlabel}}\" ({{folder}}) mappát."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "full documentation",
|
||||
"items": "items",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "documentazione completa",
|
||||
"items": "elementi",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vuole condividere la cartella \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vuole condividere la cartella \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vuole condividere la cartella \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vuole condividere la cartella \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -14,7 +14,7 @@
|
||||
"Addresses": "アドレス",
|
||||
"Advanced": "高度な設定",
|
||||
"Advanced Configuration": "高度な設定",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"Advanced settings": "高度な設定",
|
||||
"All Data": "全てのデータ",
|
||||
"Allow Anonymous Usage Reporting?": "匿名で利用状況をレポートすることを許可しますか?",
|
||||
"Alphabetic": "アルファベット順",
|
||||
@@ -39,7 +39,7 @@
|
||||
"Danger!": "危険",
|
||||
"Delete": "削除",
|
||||
"Deleted": "削除",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "デバイス「{{name}}」 ({{address}} の {{device}}) が接続を求めています。新しいデバイスとして追加しますか?",
|
||||
"Device ID": "デバイスID",
|
||||
"Device Identification": "デバイス識別情報",
|
||||
"Device Name": "デバイス名",
|
||||
@@ -70,7 +70,7 @@
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "ファイルを他のデバイスによる変更から保護します。一方、このデバイスでの変更は他のデバイスに送信されます。",
|
||||
"Folder": "フォルダー",
|
||||
"Folder ID": "フォルダーID",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Label": "フォルダー名",
|
||||
"Folder Master": "フォルダーのマスター",
|
||||
"Folder Path": "フォルダーパス",
|
||||
"Folders": "フォルダー",
|
||||
@@ -116,7 +116,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "オフ",
|
||||
"Oldest First": "古い順",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "分かりやすいフォルダーの名前で、設定は任意です。デバイスごとに異なってもかまいません。",
|
||||
"Options": "オプション",
|
||||
"Out of Sync": "未同期",
|
||||
"Out of Sync Items": "同期の必要な項目",
|
||||
@@ -140,7 +140,7 @@
|
||||
"Release Notes": "リリースノート",
|
||||
"Remote Devices": "他のデバイス",
|
||||
"Remove": "除去",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "フォルダーの識別子で、必須です。このフォルダーを共有する全てのデバイス上で同一でなくてはなりません。",
|
||||
"Rescan": "再スキャン",
|
||||
"Rescan All": "すべて再スキャン",
|
||||
"Rescan Interval": "再スキャン間隔",
|
||||
@@ -229,7 +229,7 @@
|
||||
"Version": "バージョン",
|
||||
"Versions Path": "古いバージョンを保存するパス",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "古いバージョンは、最大寿命もしくは期間ごとの最大保存数を超えた場合、自動的に削除されます。",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "警告: 入力されたパスは、設定済みのフォルダー「{{otherFolder}}」のサブディレクトリです。",
|
||||
"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.": "新しいフォルダーを追加する際、フォルダーIDはデバイス間でフォルダーの対応づけに使われることに注意してください。フォルダーIDは大文字と小文字が区別され、共有するすべてのデバイスの間で完全に一致しなくてはなりません。",
|
||||
"Yes": "はい",
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "詳細なマニュアル",
|
||||
"items": "項目",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} がフォルダー \"{{folder}}\" を共有するよう求めています。",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} がフォルダー「{{folderLabel}}」 ({{folder}}) を共有するよう求めています。",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} がフォルダー「{{folderlabel}}」 ({{folder}}) を共有するよう求めています。"
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "전체 문서",
|
||||
"items": "항목",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 에서 폴더 \\\"{{folder}}\\\" 를 공유하길 원합니다.",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -12,9 +12,9 @@
|
||||
"Add new folder?": "Pridėti naują aplanką?",
|
||||
"Address": "Adresas",
|
||||
"Addresses": "Adresai",
|
||||
"Advanced": "Pažangus",
|
||||
"Advanced": "Išplėstiniai",
|
||||
"Advanced Configuration": "Išplėstinė konfigūracija",
|
||||
"Advanced settings": "Išpėstiniai nustatymai",
|
||||
"Advanced settings": "Išplėstiniai nustatymai",
|
||||
"All Data": "Visiems duomenims",
|
||||
"Allow Anonymous Usage Reporting?": "Siųsti anonimišką vartojimo ataskaitą?",
|
||||
"Alphabetic": "Abėcėlės tvarka",
|
||||
@@ -26,20 +26,20 @@
|
||||
"Bugs": "Klaidos",
|
||||
"CPU Utilization": "Procesoriaus panaudojimas",
|
||||
"Changelog": "Pasikeitimai",
|
||||
"Clean out after": "Išvalyto po",
|
||||
"Clean out after": "Išvalyti po",
|
||||
"Close": "Uždaryti",
|
||||
"Command": "Komanda",
|
||||
"Comment, when used at the start of a line": "Komentaras naudojamas naujoje eilutėje",
|
||||
"Compression": "Kompresija",
|
||||
"Connection Error": "Susijungimo klaida",
|
||||
"Copied from elsewhere": "Nukopijuota iš betkur",
|
||||
"Copied from elsewhere": "Nukopijuota iš kitur",
|
||||
"Copied from original": "Nukopijuota iš originalo",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Autorių teisės © 2014-2016 šių bendraautorių:",
|
||||
"Copyright © 2015 the following Contributors:": "Visos teisės saugomos © 2015 šių bendraautorių:",
|
||||
"Danger!": "Pavojus!",
|
||||
"Delete": "Ištrinti",
|
||||
"Deleted": "Ištrinta",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Įrenginys \"{{name}}\" ({{device}} {{address}}) nori prisijungti. Pridėti naują įrenginį?",
|
||||
"Device ID": "Įrenginio ID",
|
||||
"Device Identification": "Įrenginio identifikacija",
|
||||
"Device Name": "Įrenginio pavadinimas",
|
||||
@@ -67,12 +67,12 @@
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Ieškant pakeitimų, į failų leidimų bitus yra nekreipiama dėmesio. Naudoti FAT failų sistemose.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Failai perkeliami į .stversions aplanką kai tampa pakeisti arba ištrinti.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Programai Syncthing pakeičiant ar ištrinant failus, jie yra perkeliami į datomis pažymėtas versijas, aplanke .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.": "Failai apsaugoti nuo pakeitimų atliktų kituose įrenginiuose, bet pakeitimai šiame įrenginyje bus nusiųsti kitiems.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Failai yra apsaugoti nuo kituose įrenginiuose atliktų pakeitimų, bet pakeitimai šiame įrenginyje bus nusiųsti kitiems įrenginiams.",
|
||||
"Folder": "Aplankas",
|
||||
"Folder ID": "Aplanko ID",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Label": "Aplanko etiketė",
|
||||
"Folder Master": "Aplanko vadovas",
|
||||
"Folder Path": "Kelias iki apkanko",
|
||||
"Folder Path": "Kelias iki aplanko",
|
||||
"Folders": "Aplankai",
|
||||
"GUI": "Valdymo skydelis",
|
||||
"GUI Authentication Password": "Valdymo skydelio slaptažodis",
|
||||
@@ -116,7 +116,7 @@
|
||||
"OK": "Gerai",
|
||||
"Off": "Netaikoma",
|
||||
"Oldest First": "Seniausi pirmiau",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Nebūtina aprašomoji aplanko etiketė. Kiekviename įrenginyje gali būti skirtinga.",
|
||||
"Options": "Parametrai",
|
||||
"Out of Sync": "Išsisinchronizavę",
|
||||
"Out of Sync Items": "Nesutikrinta",
|
||||
@@ -140,7 +140,7 @@
|
||||
"Release Notes": "Laidos Informacija",
|
||||
"Remote Devices": "Nuotoliniai įrenginiai",
|
||||
"Remove": "Pašalinti",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Reikalaujamas aplanko identifikatorius. Privalo būti toks pats visuose įrenginiuose.",
|
||||
"Rescan": "Nuskaityti iš naujo",
|
||||
"Rescan All": "Nuskaityti visus aplankus",
|
||||
"Rescan Interval": "Pertrauka tarp nuskaitymų",
|
||||
@@ -210,7 +210,7 @@
|
||||
"The path cannot be blank.": "Kelias negali būti tuščias.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Srauto maksimalus greitis privalo būti ne neigiamas skaičius (0: nėra apribojimo)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Nuskaitymo dažnis negali būti neigiamas skaičius.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Failus bus automatiškai badoma parsiųsti dar kartą kai išspręsite klaidas",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Failus bus automatiškai bandoma parsiųsti dar kartą kai išspręsite klaidas",
|
||||
"This Device": "This Device",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Tai gali suteikti programišiams lengvą prieigą skaityti ir keisti bet kokius failus jūsų kompiuteryje.",
|
||||
"This is a major version upgrade.": "Tai yra stambus atnaujinimas.",
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "pilna dokumentacija",
|
||||
"items": "įrašai",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} nori dalintis aplanku \"{{folder}}\"",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} nori dalintis aplanku \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} nori dalintis aplanku \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "all dokumentasjon",
|
||||
"items": "elementer",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker å dele mappen \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "volledige documentatie",
|
||||
"items": "objecten",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wil de map \"{{folder}}\" delen.",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "all dokumentasjon",
|
||||
"items": "element",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønskjer å dela mappa \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "pełna dokumentacja",
|
||||
"items": "pozycji",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce udostępnić folder \"{{folder}}\"",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "documentação completa",
|
||||
"items": "itens",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer compartilhar a pasta \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} deseja compartilhar a pasta \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} deseja compartilhar a pasta \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quer compartilhar a pasta \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -8,13 +8,13 @@
|
||||
"Add": "Adicionar",
|
||||
"Add Device": "Adicionar dispositivo",
|
||||
"Add Folder": "Adicionar pasta",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add Remote Device": "Adicionar dispositivo remoto",
|
||||
"Add new folder?": "Adicionar nova pasta?",
|
||||
"Address": "Endereço",
|
||||
"Addresses": "Endereços",
|
||||
"Advanced": "Avançadas",
|
||||
"Advanced Configuration": "Configuração avançada",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"Advanced settings": "Configurações avançadas",
|
||||
"All Data": "Todos os dados",
|
||||
"Allow Anonymous Usage Reporting?": "Permitir envio de relatórios anónimos de utilização?",
|
||||
"Alphabetic": "Alfabética",
|
||||
@@ -34,12 +34,12 @@
|
||||
"Connection Error": "Erro de ligação",
|
||||
"Copied from elsewhere": "Copiado doutro sítio",
|
||||
"Copied from original": "Copiado do original",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 os seguintes contribuidores:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 os seguintes contribuidores:",
|
||||
"Danger!": "Perigo!",
|
||||
"Delete": "Eliminar",
|
||||
"Deleted": "Eliminado",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "O dispositivo \"{{name}}\" ({{device}} em {{address}}) quer conectar-se. Adiciono este novo dispositivo?",
|
||||
"Device ID": "ID do dispositivo",
|
||||
"Device Identification": "Identificação do dispositivo",
|
||||
"Device Name": "Nome do dispositivo",
|
||||
@@ -70,7 +70,7 @@
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Os ficheiros estão protegidos contra alterações feitas noutros dispositivos, mas alterações feitas neste dispositivo serão enviadas ao resto do grupo.",
|
||||
"Folder": "Pasta",
|
||||
"Folder ID": "ID da pasta",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Label": "Etiqueta da pasta",
|
||||
"Folder Master": "Pasta mestre",
|
||||
"Folder Path": "Caminho da pasta",
|
||||
"Folders": "Pastas",
|
||||
@@ -116,7 +116,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "Desligada",
|
||||
"Oldest First": "Primeiro os mais antigos",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Etiqueta descritiva opcional para a pasta. Pode ser diferente em cada dispositivo.",
|
||||
"Options": "Opções",
|
||||
"Out of Sync": "Fora de sincronia",
|
||||
"Out of Sync Items": "Itens por sincronizar",
|
||||
@@ -138,9 +138,9 @@
|
||||
"Relayed via": "Retransmitido via",
|
||||
"Relays": "Retransmissores",
|
||||
"Release Notes": "Notas de lançamento",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remote Devices": "Dispositivos remotos",
|
||||
"Remove": "Remover",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Identificador obrigatório para a pasta. Tem que ser igual em todos os dispositivos do grupo.",
|
||||
"Rescan": "Verificar agora",
|
||||
"Rescan All": "Verificar todas agora",
|
||||
"Rescan Interval": "Intervalo entre verificações",
|
||||
@@ -150,7 +150,7 @@
|
||||
"Resume": "Retomar",
|
||||
"Reused": "Reutilizado",
|
||||
"Save": "Gravar",
|
||||
"Scan Time Remaining": "Tempo restante de rastreio",
|
||||
"Scan Time Remaining": "Tempo restante da verificação",
|
||||
"Scanning": "Verificando",
|
||||
"Select the devices to share this folder with.": "Seleccione os dispositivos com os quais vai partilhar esta pasta.",
|
||||
"Select the folders to share with this device.": "Seleccione as pastas a partilhar com este dispositivo.",
|
||||
@@ -211,7 +211,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "O limite de velocidade tem que ser um número que não seja negativo (0: sem limite)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "O intervalo entre verificações tem que ser um valor não negativo de segundos.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Será tentado automaticamente e os itens serão sincronizados assim que o erro seja resolvido.",
|
||||
"This Device": "This Device",
|
||||
"This Device": "Este dispositivo",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Isso facilmente dará acesso aos piratas informáticos para lerem e modificarem quaisquer ficheiros no seu computador.",
|
||||
"This is a major version upgrade.": "Esta é uma actualização para uma versão importante.",
|
||||
"Trash Can File Versioning": "Reciclagem",
|
||||
@@ -229,7 +229,7 @@
|
||||
"Version": "Versão",
|
||||
"Versions Path": "Caminho das versões",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "As versões são eliminadas automaticamente se forem mais antigas do que a idade máxima ou excederem o número máximo de ficheiros permitido num intervalo.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Aviso: Este caminho é uma subpasta da pasta \"{{otherFolder}}\" já existente.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Quando adicionar um novo dispositivo, lembre-se que este dispositivo tem que ser adicionado do outro lado também.",
|
||||
"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.": "Quando adicionar uma nova pasta, lembre-se que o ID da pasta é utilizado para ligar as pastas entre dispositivos. É sensível às diferenças entre maiúsculas e minúsculas e tem que ter uma correspondência perfeita entre todos os dispositivos.",
|
||||
"Yes": "Sim",
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "documentação completa",
|
||||
"items": "itens",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer partilhar a pasta \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} quer partilhar a pasta \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quer partilhar a pasta \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "полная документация",
|
||||
"items": "элементы",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хочет поделиться папкой \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} хочет поделиться папкой «{{folderLabel}}» ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} хочет поделиться папкой «{{folderLabel}}» ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -55,7 +55,7 @@
|
||||
"Edit Device": "Redigera enhet",
|
||||
"Edit Folder": "Redigera katalog",
|
||||
"Editing": "Redigerar",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable Relaying": "Aktivera reläa",
|
||||
"Enable UPnP": "Använd UPnP",
|
||||
"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 filmönster, ett per rad.",
|
||||
@@ -70,7 +70,7 @@
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Filer skyddas från ändringar gjorda på andra enheter, men ändringar som görs på den här noden skickas till de andra klustermedlemmarna.",
|
||||
"Folder": "Katalog",
|
||||
"Folder ID": "Katalog-ID",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Label": "Katalog etikett",
|
||||
"Folder Master": "Huvudlagring",
|
||||
"Folder Path": "Sökväg",
|
||||
"Folders": "Kataloger",
|
||||
@@ -81,7 +81,7 @@
|
||||
"Generate": "Skapa",
|
||||
"Global Discovery": "Global uppslagning",
|
||||
"Global Discovery Server": "Global uppslagningsserver",
|
||||
"Global Discovery Servers": "Global Discovery Servers",
|
||||
"Global Discovery Servers": "Globala uppslagningsservrar",
|
||||
"Global State": "Global status",
|
||||
"Help": "Hjälp",
|
||||
"Home page": "Hemsida",
|
||||
@@ -116,7 +116,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "Av",
|
||||
"Oldest First": "Äldst först",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Valfri beskrivande etikett för katalogen. Kan vara olika på varje enhet.",
|
||||
"Options": "Alternativ",
|
||||
"Out of Sync": "Osynkad",
|
||||
"Out of Sync Items": "Osynkade poster",
|
||||
@@ -127,14 +127,14 @@
|
||||
"Pause": "Paus",
|
||||
"Paused": "Pausad",
|
||||
"Please consult the release notes before performing a major upgrade.": "Läs igenom versionsnyheterna innan den stora uppgraderingen.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Please set a GUI Authentication User and Password in the Settings dialog.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Ställ in ett grafiskt användarautentisering och lösenord i dialogrutan Inställningar.",
|
||||
"Please wait": "Var god vänta",
|
||||
"Preview": "Förhandsgranska",
|
||||
"Preview Usage Report": "Förhandsgranska statistik",
|
||||
"Quick guide to supported patterns": "Snabb guide till filmönster som stöds",
|
||||
"RAM Utilization": "Minnesanvändning",
|
||||
"Random": "Slumpmässig",
|
||||
"Relay Servers": "Relay Servers",
|
||||
"Relay Servers": "Reläservrar",
|
||||
"Relayed via": "Vidarbefordras via",
|
||||
"Relays": "Vidarbefordringar",
|
||||
"Release Notes": "versionsnyheter",
|
||||
@@ -212,7 +212,7 @@
|
||||
"The rescan interval must be a non-negative number of seconds.": "Förnyelseintervallet måste vara ett positivt antal sekunder",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "De omprövas automatiskt och kommer att synkroniseras när felet är löst.",
|
||||
"This Device": "Enheten",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "This can easily give hackers access to read and change any files on your computer.",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Detta kan lätt ge hackare tillgång till att läsa och ändra några filer på datorn.",
|
||||
"This is a major version upgrade.": "Det här är en stor uppgradering.",
|
||||
"Trash Can File Versioning": "Versionshantering på filer i papperskorgen",
|
||||
"Unknown": "Okänt",
|
||||
@@ -229,7 +229,7 @@
|
||||
"Version": "Version",
|
||||
"Versions Path": "Katalog för versioner",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Versioner tas bort automatiskt när de är äldre än den maximala åldersgränsen eller överstiger frekvensen i sitt interval.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Varning, denna sökväg är en underkatalog till en befintlig katalog \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "När du lägger till en ny enhet, kom ihåg att den här enheten måste läggas till på den andra enheten också.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "När du lägger till ny katalog, tänk på att katalog-ID:t knyter ihop katalogen mellan olika noder. De måste vara exakt desamma mellan noder och stora eller små bokstäver har betydelse.",
|
||||
"Yes": "Ja",
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "fullständig dokumentation",
|
||||
"items": "poster",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vill dela katalogen \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} vill dela mappen \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vill dela mappen \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "belgelendirme içeriğinin tümü",
|
||||
"items": "öğel",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} \"{{folder}}\" klasörünü paylaşmak istiyor.",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -39,7 +39,7 @@
|
||||
"Danger!": "Небезпечно!",
|
||||
"Delete": "Видалити",
|
||||
"Deleted": "Видалене",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Пристрій \"{{name}}\" ({{device}} за адресою {{address}}) намагається під’єднатися. Додати новий пристрій?",
|
||||
"Device ID": "ID пристрою",
|
||||
"Device Identification": "Ідентифікатор пристрою",
|
||||
"Device Name": "Назва пристрою",
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "повна документація",
|
||||
"items": "елементи",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хоче поділитися директорією \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} хоче поділитися директорією \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} хоче поділитися директорією \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} хоче поділитися директорією \"{{folderLabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "tài liệu đầy đủ",
|
||||
"items": "nội dung",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} muốn chia sẻ thư mục \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} muốn chia sẻ thư mục \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} muốn chia sẻ thư mục \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"A device with that ID is already added.": "您已添加过该设备",
|
||||
"A negative number of days doesn't make sense.": "天数不能为负",
|
||||
"A device with that ID is already added.": "您已添加过相同 ID 的设备",
|
||||
"A negative number of days doesn't make sense.": "天数为负数没有意义。",
|
||||
"A new major version may not be compatible with previous versions.": "重大更新可能与之前的版本之间无法兼容",
|
||||
"API Key": "API Key",
|
||||
"About": "关于",
|
||||
@@ -39,7 +39,7 @@
|
||||
"Danger!": "危险!",
|
||||
"Delete": "删除",
|
||||
"Deleted": "已删除",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "设备 \"{{name}}\" (位于 {{address}} 的 {{device}}) 请求连接。是否添加新设备?",
|
||||
"Device ID": "设备标识",
|
||||
"Device Identification": "设备标识",
|
||||
"Device Name": "设备名",
|
||||
@@ -57,7 +57,7 @@
|
||||
"Editing": "正在编辑",
|
||||
"Enable Relaying": "开启中继",
|
||||
"Enable UPnP": "开启UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "输入以半角逗号分隔的 \"tcp://IP地址:端口号\", \"tcp://主机:端口号\" 设置可用地址列表,或者输入\"dynamic\"表示自动寻找地址。",
|
||||
"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": "外部版本控制",
|
||||
@@ -88,7 +88,7 @@
|
||||
"Ignore": "忽略",
|
||||
"Ignore Patterns": "忽略列表",
|
||||
"Ignore Permissions": "忽略文件权限",
|
||||
"Incoming Rate Limit (KiB/s)": "下载速率限制(KiB/s)",
|
||||
"Incoming Rate Limit (KiB/s)": "下载速率限制 (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "错误的配置可能损坏您文件夹内的内容,使得 Syncthing 无法工作。",
|
||||
"Introducer": "介绍人设备",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "对本条件取反(例如:不要排除某项)",
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "完整文档",
|
||||
"items": "条目",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想将 “{{folder}}” 文件夹共享给您",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}}想要分享文件夹\"{{folderLabel}}\" ({{folder}}).\n"
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} 想要分享文件夹 \"{{folderLabel}}\" ({{folder}})。",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} 想要分享文件夹 \"{{folderLabel}}\" ({{folder}})。"
|
||||
}
|
||||
@@ -1,28 +1,28 @@
|
||||
{
|
||||
"A device with that ID is already added.": "A device with that ID is already added.",
|
||||
"A device with that ID is already added.": "該裝置識別碼已被新增。",
|
||||
"A negative number of days doesn't make sense.": "一個負的天數並不合理。",
|
||||
"A new major version may not be compatible with previous versions.": "一個新的主要版本可能與以前的版本並不相容。",
|
||||
"A new major version may not be compatible with previous versions.": "新的主要版本可能與以前的版本不相容。",
|
||||
"API Key": "API 金鑰",
|
||||
"About": "關於",
|
||||
"Actions": "操作",
|
||||
"Add": "增加",
|
||||
"Add Device": "增加裝置",
|
||||
"Add Folder": "增加資料夾",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add Remote Device": "新增遠端裝置",
|
||||
"Add new folder?": "新增資料夾?",
|
||||
"Address": "位址",
|
||||
"Addresses": "位址",
|
||||
"Advanced": "進階",
|
||||
"Advanced Configuration": "進階設定",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"Advanced Configuration": "進階配置",
|
||||
"Advanced settings": "進階設定",
|
||||
"All Data": "全部資料",
|
||||
"Allow Anonymous Usage Reporting?": "允許匿名的使用資訊回報?",
|
||||
"Allow Anonymous Usage Reporting?": "允許匿名的使用資訊回報?",
|
||||
"Alphabetic": "字母順序",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "An external command handles the versioning. It has to remove the file from the synced folder.",
|
||||
"Anonymous Usage Reporting": "匿名的使用資訊回報",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "任何在引入者裝置所設置的裝置將會一併新增至此裝置",
|
||||
"Automatic upgrades": "自動升級",
|
||||
"Be careful!": "請小心!",
|
||||
"Be careful!": "請小心!",
|
||||
"Bugs": "程式錯誤",
|
||||
"CPU Utilization": "CPU 使用",
|
||||
"Changelog": "更新日誌",
|
||||
@@ -33,20 +33,20 @@
|
||||
"Compression": "壓縮",
|
||||
"Connection Error": "連線錯誤",
|
||||
"Copied from elsewhere": "從別處複製",
|
||||
"Copied from original": "從原來複製",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copied from original": "從原處複製",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 下列貢獻者:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 下列貢獻者:",
|
||||
"Danger!": "Danger!",
|
||||
"Danger!": "危險!",
|
||||
"Delete": "刪除",
|
||||
"Deleted": "已刪除",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "裝置 \"{{name}}\" ({{device}} 位於 {{address}}) 想要連線。 要增加新裝置嗎?",
|
||||
"Device ID": "裝置識別碼",
|
||||
"Device Identification": "裝置識別",
|
||||
"Device Name": "裝置名稱",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "裝置 {{device}} ({{address}}) 想要連線。要新增裝置嗎?",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "裝置 {{device}} ({{address}}) 想要連線。要新增裝置嗎?",
|
||||
"Devices": "裝置",
|
||||
"Disconnected": "斷線",
|
||||
"Discovery": "Discovery",
|
||||
"Discovery": "探索",
|
||||
"Documentation": "說明文件",
|
||||
"Download Rate": "下載速率",
|
||||
"Downloaded": "已下載",
|
||||
@@ -55,7 +55,7 @@
|
||||
"Edit Device": "編輯裝置",
|
||||
"Edit Folder": "編輯資料夾",
|
||||
"Editing": "正在編輯",
|
||||
"Enable Relaying": "Enable Relaying",
|
||||
"Enable Relaying": "啟用中繼",
|
||||
"Enable UPnP": "啟用 UPnP",
|
||||
"Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.": "Enter comma separated (\"tcp://ip:port\", \"tcp://host:port\") addresses or \"dynamic\" to perform automatic discovery of the address.",
|
||||
"Enter ignore patterns, one per line.": "輸入忽略樣式,每行一種。",
|
||||
@@ -70,26 +70,26 @@
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "其他裝置做的改變不會影響到此裝置的檔案,但在此裝置上的變化將被發送到叢集中的其他部分。",
|
||||
"Folder": "資料夾",
|
||||
"Folder ID": "資料夾識別碼",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Label": "資料夾標籤",
|
||||
"Folder Master": "主資料夾",
|
||||
"Folder Path": "資料夾路徑",
|
||||
"Folders": "資料夾",
|
||||
"GUI": "GUI",
|
||||
"GUI Authentication Password": "GUI 認證密碼",
|
||||
"GUI Authentication User": "GUI 認證使用者名稱",
|
||||
"GUI Authentication User": "GUI 使用者認證名稱",
|
||||
"GUI Listen Addresses": "GUI 監聽位址",
|
||||
"Generate": "產生",
|
||||
"Global Discovery": "全域探索",
|
||||
"Global Discovery Server": "全域探索伺服器",
|
||||
"Global Discovery Servers": "Global Discovery Servers",
|
||||
"Global Discovery Servers": "全域探索伺服器",
|
||||
"Global State": "全域狀態",
|
||||
"Help": "說明",
|
||||
"Home page": "首頁",
|
||||
"Ignore": "忽略",
|
||||
"Ignore Patterns": "忽略樣式",
|
||||
"Ignore Permissions": "忽略權限",
|
||||
"Incoming Rate Limit (KiB/s)": "連入速率限制 (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "不正確的設定可能會損壞你的資料夾內容,並引致 Syncthing 的不正當運作。",
|
||||
"Incoming Rate Limit (KiB/s)": "傳入速率限制 (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "不正確的設定可能會損壞您的資料夾內容,並導致 Syncthing 不正常運作。",
|
||||
"Introducer": "引入者",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "反轉給定條件 (即:不要排除)",
|
||||
"Keep Versions": "保留歷史版本數",
|
||||
@@ -97,9 +97,9 @@
|
||||
"Last File Received": "最後接收的檔案",
|
||||
"Last seen": "最後發現時間",
|
||||
"Later": "稍後",
|
||||
"Local Discovery": "本地探索",
|
||||
"Local State": "本地狀態",
|
||||
"Local State (Total)": "本地狀態 (總結)",
|
||||
"Local Discovery": "本機探索",
|
||||
"Local State": "本機狀態",
|
||||
"Local State (Total)": "本機狀態 (總結)",
|
||||
"Major Upgrade": "重大更新",
|
||||
"Maximum Age": "最長保留時間",
|
||||
"Metadata Only": "僅中繼資料",
|
||||
@@ -118,27 +118,27 @@
|
||||
"Oldest First": "最舊的優先",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Options": "選項",
|
||||
"Out of Sync": "Out of Sync",
|
||||
"Out of Sync": "不同步",
|
||||
"Out of Sync Items": "不同步物件",
|
||||
"Outgoing Rate Limit (KiB/s)": "連出速率限制 (KiB/s)",
|
||||
"Override Changes": "置換改變",
|
||||
"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 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 versions should be stored (leave empty for the default .stversions folder in the folder).": "儲存歷史版本的路徑 (若為空,則預設使用資料夾中的 .stversions 資料夾)。",
|
||||
"Pause": "暫停",
|
||||
"Paused": "暫停",
|
||||
"Please consult the release notes before performing a major upgrade.": "執行重大升級前請先參閱版本資訊。",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "Please set a GUI Authentication User and Password in the Settings dialog.",
|
||||
"Please set a GUI Authentication User and Password in the Settings dialog.": "請在設定對話方塊內設置 GUI 使用者認證名稱及密碼。",
|
||||
"Please wait": "請稍後",
|
||||
"Preview": "預覽",
|
||||
"Preview Usage Report": "預覽使用資訊報告",
|
||||
"Quick guide to supported patterns": "可支援樣式的快速指南",
|
||||
"RAM Utilization": "記憶體使用",
|
||||
"Random": "隨機",
|
||||
"Relay Servers": "Relay Servers",
|
||||
"Relay Servers": "中繼伺服器",
|
||||
"Relayed via": "中繼於",
|
||||
"Relays": "中繼點",
|
||||
"Release Notes": "版本資訊",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remote Devices": "遠端裝置",
|
||||
"Remove": "移除",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "重新掃描",
|
||||
@@ -159,11 +159,11 @@
|
||||
"Share Folder": "分享資料夾",
|
||||
"Share Folders With Device": "與裝置共享資料夾",
|
||||
"Share With Devices": "與這些裝置共享",
|
||||
"Share this folder?": "分享此資料夾?",
|
||||
"Share this folder?": "分享此資料夾?",
|
||||
"Shared With": "與誰共享",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "資料夾的簡短識別字。必須在叢集內所有的裝置上皆相同。",
|
||||
"Show ID": "顯示識別碼",
|
||||
"Show QR": "Show QR",
|
||||
"Show QR": "顯示 QR 碼",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "代替裝置識別碼顯示在叢集狀態中。這段文字將會廣播到其他的裝置作為一個可選的預設名稱。",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "代替裝置識別碼顯示在叢集狀態中。本欄若未填寫則將被更新為此裝置所廣播的名稱。",
|
||||
"Shutdown": "關閉",
|
||||
@@ -183,14 +183,14 @@
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing 包括以下軟體或其中的一部分:",
|
||||
"Syncthing is restarting.": "Syncthing 正在重新啟動。",
|
||||
"Syncthing is upgrading.": "Syncthing 正在進行升級。",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing 似乎下線了,或者您的網際網路連線出現問題。正在重試...",
|
||||
"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 seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.",
|
||||
"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 {%url%}.": "匯總統計資訊公佈於 {{url}}。",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "組態已經儲存但尚未啟用。Syncthing 必須重新啟動以便啟用新的組態。",
|
||||
"The device ID cannot be blank.": "裝置識別碼不能為空白。",
|
||||
"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).": "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).",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "要輸入在這裡的裝置識別碼可以在其他裝置的 \"編輯 > 顯示識別碼\" 對話框找到。空白以及連接符號可不輸入 (省略)",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "要輸入在這裡的裝置識別碼,可以在其它裝置的 \"編輯 > 顯示識別碼\" 對話框找到。空白以及連接符號可不輸入 (省略)。",
|
||||
"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.": "經過加密的使用資訊報告會每天傳送。報告是用來追蹤常用的平台、資料夾的大小以及應用程式的版本。若傳送的資料集有異動,您會再次看到這個對話框。",
|
||||
"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.": "輸入的裝置識別碼似乎無效。它應該為一串包含半形英文字母及數字,並可能會含有空白或連接符號的字串,且長度為 52 或 56 個字元。",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "The first command line parameter is the folder path and the second parameter is the relative path in the folder.",
|
||||
@@ -211,7 +211,7 @@
|
||||
"The rate limit must be a non-negative number (0: no limit)": "限制速率必須為非負的數字 (0: 不設限制)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "重新掃描間隔必須為一個非負數的秒數。",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "解決間題後,將會自動重試和同步。",
|
||||
"This Device": "This Device",
|
||||
"This Device": "本機",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "This can easily give hackers access to read and change any files on your computer.",
|
||||
"This is a major version upgrade.": "這是一個主要版本更新。",
|
||||
"Trash Can File Versioning": "Trash Can File Versioning",
|
||||
@@ -238,5 +238,6 @@
|
||||
"full documentation": "完整說明文件",
|
||||
"items": "個項目",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} 想要分享資料夾 \"{{folder}}\"。",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} 想要分享資料夾 \"{{folderLabel}}\" ({{folder}})。",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} 想要分享資料夾 \"{{folderlabel}}\" ({{folder}})。"
|
||||
}
|
||||
@@ -174,8 +174,8 @@
|
||||
<span ng-if="event.data.folderLabel.length == 0" translate translate-value-device="{{ deviceName(findDevice(event.data.device)) }}" translate-value-folder="{{ event.data.folder }}">
|
||||
{%device%} wants to share folder "{%folder%}".
|
||||
</span>
|
||||
<span ng-if="event.data.folderLabel.length != 0" translate translate-value-device="{{ deviceName(findDevice(event.data.device)) }}" translate-value-folder="{{ event.data.folder }}" translate-value-folderLabel="{{ event.data.folderLabel }}">
|
||||
{%device%} wants to share folder "{%folderLabel%}" ({%folder%}).
|
||||
<span ng-if="event.data.folderLabel.length != 0" translate translate-value-device="{{ deviceName(findDevice(event.data.device)) }}" translate-value-folder="{{ event.data.folder }}" translate-value-folderlabel="{{ event.data.folderLabel }}">
|
||||
{%device%} wants to share folder "{%folderlabel%}" ({%folder%}).
|
||||
</span>
|
||||
<span translate ng-if="folders[event.data.folder]">Share this folder?</span>
|
||||
<span translate ng-if="!folders[event.data.folder]">Add new folder?</span>
|
||||
|
||||
@@ -11,7 +11,7 @@
|
||||
<p translate>Copyright © 2014-2016 the following Contributors:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-12" id="contributor-list">
|
||||
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Ben Schulz, Caleb Callaway, Lode Hoste, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Andrew Dunham, Antony Male, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Harte, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Lars K.W. Gohlke, Laurent Etiemble, Lord Landon Agahnim, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Ploujnikov, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Stefan Kuntz, Tim Abell, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Yannic A.
|
||||
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Ben Schulz, Caleb Callaway, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Andrew Dunham, Antony Male, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Harte, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Lars K.W. Gohlke, Laurent Etiemble, Lord Landon Agahnim, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Stefan Kuntz, Tim Abell, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Yannic A.
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<div class="form-group">
|
||||
<div class="checkbox">
|
||||
<label>
|
||||
<input id="UPnPEnabled" type="checkbox" ng-model="tmpOptions.upnpEnabled"> <span translate>Enable UPnP</span>
|
||||
<input id="NATEnabled" type="checkbox" ng-model="tmpOptions.natEnabled"> <span translate>Enable NAT traversal</span>
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -242,6 +242,10 @@ func convertV12V13(cfg *Configuration) {
|
||||
// Not using the ignore cache is the new default. Disable it on existing
|
||||
// configurations.
|
||||
cfg.Options.CacheIgnoredFiles = false
|
||||
cfg.Options.NATEnabled = cfg.Options.DeprecatedUPnPEnabled
|
||||
cfg.Options.NATLeaseM = cfg.Options.DeprecatedUPnPLeaseM
|
||||
cfg.Options.NATRenewalM = cfg.Options.DeprecatedUPnPRenewalM
|
||||
cfg.Options.NATTimeoutS = cfg.Options.DeprecatedUPnPTimeoutS
|
||||
cfg.Version = 13
|
||||
}
|
||||
|
||||
|
||||
@@ -44,10 +44,10 @@ func TestDefaultValues(t *testing.T) {
|
||||
RelaysEnabled: true,
|
||||
RelayReconnectIntervalM: 10,
|
||||
StartBrowser: true,
|
||||
UPnPEnabled: true,
|
||||
UPnPLeaseM: 60,
|
||||
UPnPRenewalM: 30,
|
||||
UPnPTimeoutS: 10,
|
||||
NATEnabled: true,
|
||||
NATLeaseM: 60,
|
||||
NATRenewalM: 30,
|
||||
NATTimeoutS: 10,
|
||||
RestartOnWakeup: true,
|
||||
AutoUpgradeIntervalH: 12,
|
||||
KeepTemporariesH: 24,
|
||||
@@ -61,6 +61,8 @@ func TestDefaultValues(t *testing.T) {
|
||||
URPostInsecurely: false,
|
||||
ReleasesURL: "https://api.github.com/repos/syncthing/syncthing/releases?per_page=30",
|
||||
AlwaysLocalNets: []string{},
|
||||
OverwriteNames: false,
|
||||
TempIndexMinBlocks: 10,
|
||||
}
|
||||
|
||||
cfg := New(device1)
|
||||
@@ -173,10 +175,10 @@ func TestOverriddenValues(t *testing.T) {
|
||||
RelaysEnabled: false,
|
||||
RelayReconnectIntervalM: 20,
|
||||
StartBrowser: false,
|
||||
UPnPEnabled: false,
|
||||
UPnPLeaseM: 90,
|
||||
UPnPRenewalM: 15,
|
||||
UPnPTimeoutS: 15,
|
||||
NATEnabled: false,
|
||||
NATLeaseM: 90,
|
||||
NATRenewalM: 15,
|
||||
NATTimeoutS: 15,
|
||||
RestartOnWakeup: false,
|
||||
AutoUpgradeIntervalH: 24,
|
||||
KeepTemporariesH: 48,
|
||||
@@ -190,6 +192,8 @@ func TestOverriddenValues(t *testing.T) {
|
||||
URPostInsecurely: true,
|
||||
ReleasesURL: "https://localhost/releases",
|
||||
AlwaysLocalNets: []string{},
|
||||
OverwriteNames: true,
|
||||
TempIndexMinBlocks: 100,
|
||||
}
|
||||
|
||||
cfg, err := Load("testdata/overridenvalues.xml", device1)
|
||||
|
||||
@@ -37,6 +37,7 @@ type FolderConfiguration struct {
|
||||
PullerPauseS int `xml:"pullerPauseS" json:"pullerPauseS"`
|
||||
MaxConflicts int `xml:"maxConflicts" json:"maxConflicts"`
|
||||
DisableSparseFiles bool `xml:"disableSparseFiles" json:"disableSparseFiles"`
|
||||
DisableTempIndexes bool `xml:"disableTempIndexes" json:"disableTempIndexes"`
|
||||
|
||||
Invalid string `xml:"-" json:"invalid"` // Set at runtime when there is an error, not saved
|
||||
cachedPath string
|
||||
|
||||
@@ -20,10 +20,10 @@ type OptionsConfiguration struct {
|
||||
RelaysEnabled bool `xml:"relaysEnabled" json:"relaysEnabled" default:"true"`
|
||||
RelayReconnectIntervalM int `xml:"relayReconnectIntervalM" json:"relayReconnectIntervalM" default:"10"`
|
||||
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
|
||||
UPnPEnabled bool `xml:"upnpEnabled" json:"upnpEnabled" default:"true"`
|
||||
UPnPLeaseM int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"60"`
|
||||
UPnPRenewalM int `xml:"upnpRenewalMinutes" json:"upnpRenewalMinutes" default:"30"`
|
||||
UPnPTimeoutS int `xml:"upnpTimeoutSeconds" json:"upnpTimeoutSeconds" default:"10"`
|
||||
NATEnabled bool `xml:"natEnabled" json:"natEnabled" default:"true"`
|
||||
NATLeaseM int `xml:"natLeaseMinutes" json:"natLeaseMinutes" default:"60"`
|
||||
NATRenewalM int `xml:"natRenewalMinutes" json:"natRenewalMinutes" default:"30"`
|
||||
NATTimeoutS int `xml:"natTimeoutSeconds" json:"natTimeoutSeconds" default:"10"`
|
||||
URAccepted int `xml:"urAccepted" json:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
|
||||
URUniqueID string `xml:"urUniqueID" json:"urUniqueId"` // Unique ID for reporting purposes, regenerated when UR is turned on.
|
||||
URURL string `xml:"urURL" json:"urURL" default:"https://data.syncthing.net/newdata"`
|
||||
@@ -39,6 +39,13 @@ type OptionsConfiguration struct {
|
||||
MinHomeDiskFreePct float64 `xml:"minHomeDiskFreePct" json:"minHomeDiskFreePct" default:"1"`
|
||||
ReleasesURL string `xml:"releasesURL" json:"releasesURL" default:"https://api.github.com/repos/syncthing/syncthing/releases?per_page=30"`
|
||||
AlwaysLocalNets []string `xml:"alwaysLocalNet" json:"alwaysLocalNets"`
|
||||
OverwriteNames bool `xml:"overwriteNames" json:"overwriteNames" default:"false"`
|
||||
TempIndexMinBlocks int `xml:"tempIndexMinBlocks" json:"tempIndexMinBlocks" default:"10"`
|
||||
|
||||
DeprecatedUPnPEnabled bool `xml:"upnpEnabled"`
|
||||
DeprecatedUPnPLeaseM int `xml:"upnpLeaseMinutes"`
|
||||
DeprecatedUPnPRenewalM int `xml:"upnpRenewalMinutes"`
|
||||
DeprecatedUPnPTimeoutS int `xml:"upnpTimeoutSeconds"`
|
||||
}
|
||||
|
||||
func (orig OptionsConfiguration) Copy() OptionsConfiguration {
|
||||
|
||||
+6
-4
@@ -17,10 +17,10 @@
|
||||
<relayReconnectIntervalM>20</relayReconnectIntervalM>
|
||||
<relayWithoutGlobalAnn>true</relayWithoutGlobalAnn>
|
||||
<startBrowser>false</startBrowser>
|
||||
<upnpEnabled>false</upnpEnabled>
|
||||
<upnpLeaseMinutes>90</upnpLeaseMinutes>
|
||||
<upnpRenewalMinutes>15</upnpRenewalMinutes>
|
||||
<upnpTimeoutSeconds>15</upnpTimeoutSeconds>
|
||||
<natEnabled>false</natEnabled>
|
||||
<natLeaseMinutes>90</natLeaseMinutes>
|
||||
<natRenewalMinutes>15</natRenewalMinutes>
|
||||
<natTimeoutSeconds>15</natTimeoutSeconds>
|
||||
<restartOnWakeup>false</restartOnWakeup>
|
||||
<autoUpgradeIntervalH>24</autoUpgradeIntervalH>
|
||||
<keepTemporariesH>48</keepTemporariesH>
|
||||
@@ -34,5 +34,7 @@
|
||||
<urInitialDelayS>800</urInitialDelayS>
|
||||
<urPostInsecurely>true</urPostInsecurely>
|
||||
<releasesURL>https://localhost/releases</releasesURL>
|
||||
<overwriteNames>true</overwriteNames>
|
||||
<tempIndexMinBlocks>100</tempIndexMinBlocks>
|
||||
</options>
|
||||
</configuration>
|
||||
|
||||
@@ -19,11 +19,12 @@ import (
|
||||
"github.com/juju/ratelimit"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/discover"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/model"
|
||||
"github.com/syncthing/syncthing/lib/nat"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/relay"
|
||||
"github.com/syncthing/syncthing/lib/relay/client"
|
||||
"github.com/syncthing/syncthing/lib/upnp"
|
||||
"github.com/syncthing/syncthing/lib/util"
|
||||
|
||||
"github.com/thejerf/suture"
|
||||
@@ -56,7 +57,7 @@ type Service struct {
|
||||
tlsCfg *tls.Config
|
||||
discoverer discover.Finder
|
||||
conns chan model.IntermediateConnection
|
||||
upnpService *upnp.Service
|
||||
mappings []*nat.Mapping
|
||||
relayService relay.Service
|
||||
bepProtocolName string
|
||||
tlsDefaultCommonName string
|
||||
@@ -71,7 +72,7 @@ type Service struct {
|
||||
relaysEnabled bool
|
||||
}
|
||||
|
||||
func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, upnpService *upnp.Service,
|
||||
func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder, mappings []*nat.Mapping,
|
||||
relayService relay.Service, bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) *Service {
|
||||
service := &Service{
|
||||
Supervisor: suture.NewSimple("connections.Service"),
|
||||
@@ -80,7 +81,7 @@ func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model
|
||||
model: mdl,
|
||||
tlsCfg: tlsCfg,
|
||||
discoverer: discoverer,
|
||||
upnpService: upnpService,
|
||||
mappings: mappings,
|
||||
relayService: relayService,
|
||||
conns: make(chan model.IntermediateConnection),
|
||||
bepProtocolName: bepProtocolName,
|
||||
@@ -140,6 +141,17 @@ func NewConnectionService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model
|
||||
service.Add(serviceFunc(service.acceptRelayConns))
|
||||
}
|
||||
|
||||
for _, mapping := range mappings {
|
||||
mapping.OnChanged(func(m *nat.Mapping, added, removed []nat.Address) {
|
||||
events.Default.Log(events.ExternalPortMappingChanged, map[string]interface{}{
|
||||
"protocol": m.Protocol(),
|
||||
"local": m.Address().String(),
|
||||
"added": added,
|
||||
"removed": removed,
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
return service
|
||||
}
|
||||
|
||||
@@ -256,9 +268,9 @@ next:
|
||||
|
||||
s.mut.Lock()
|
||||
s.model.AddConnection(model.Connection{
|
||||
c,
|
||||
protoConn,
|
||||
c.Type,
|
||||
Conn: c,
|
||||
Connection: protoConn,
|
||||
Type: c.Type,
|
||||
}, hello)
|
||||
s.connType[remoteID] = c.Type
|
||||
s.mut.Unlock()
|
||||
@@ -307,7 +319,8 @@ func (s *Service) connect() {
|
||||
s.model.Close(deviceID, fmt.Errorf("switching connections"))
|
||||
}
|
||||
s.conns <- model.IntermediateConnection{
|
||||
conn, model.ConnectionTypeDirectDial,
|
||||
Conn: conn,
|
||||
Type: model.ConnectionTypeDirectDial,
|
||||
}
|
||||
continue nextDevice
|
||||
}
|
||||
@@ -338,7 +351,8 @@ func (s *Service) connect() {
|
||||
if conn := s.connectViaRelay(deviceID, addr); conn != nil {
|
||||
l.Debugln("Connecting to", deviceID, "via", addr, "succeeded")
|
||||
s.conns <- model.IntermediateConnection{
|
||||
conn, model.ConnectionTypeRelayDial,
|
||||
Conn: conn,
|
||||
Type: model.ConnectionTypeRelayDial,
|
||||
}
|
||||
continue nextDevice
|
||||
}
|
||||
@@ -531,11 +545,10 @@ func (s *Service) addresses(includePrivateIPV4 bool) []string {
|
||||
}
|
||||
}
|
||||
|
||||
// Get an external port mapping from the upnpService, if it has one. If so,
|
||||
// add it as another unspecified address.
|
||||
if s.upnpService != nil {
|
||||
if port := s.upnpService.ExternalPort(); port != 0 {
|
||||
addrs = append(addrs, fmt.Sprintf("tcp://:%d", port))
|
||||
// Add addresses provided by the mappings from the NAT service.
|
||||
for _, mapping := range s.mappings {
|
||||
for _, addr := range mapping.ExternalAddresses() {
|
||||
addrs = append(addrs, fmt.Sprintf("tcp://%s", addr))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -91,7 +91,8 @@ func makeTCPListener(network string) ListenerFactory {
|
||||
}
|
||||
|
||||
conns <- model.IntermediateConnection{
|
||||
tc, model.ConnectionTypeDirectAccept,
|
||||
Conn: tc,
|
||||
Type: model.ConnectionTypeDirectAccept,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -100,7 +100,7 @@ func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.
|
||||
var oldFile protocol.FileInfo
|
||||
var hasOldFile bool
|
||||
// Remove the device from the current version list
|
||||
if svl != nil {
|
||||
if len(svl) != 0 {
|
||||
err = fl.UnmarshalXDR(svl)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
|
||||
+12
-10
@@ -26,28 +26,30 @@ func newDeviceActivity() *deviceActivity {
|
||||
}
|
||||
}
|
||||
|
||||
func (m *deviceActivity) leastBusy(availability []protocol.DeviceID) protocol.DeviceID {
|
||||
func (m *deviceActivity) leastBusy(availability []Availability) (Availability, bool) {
|
||||
m.mut.Lock()
|
||||
low := 2<<30 - 1
|
||||
var selected protocol.DeviceID
|
||||
for _, device := range availability {
|
||||
if usage := m.act[device]; usage < low {
|
||||
found := false
|
||||
var selected Availability
|
||||
for _, info := range availability {
|
||||
if usage := m.act[info.ID]; usage < low {
|
||||
low = usage
|
||||
selected = device
|
||||
selected = info
|
||||
found = true
|
||||
}
|
||||
}
|
||||
m.mut.Unlock()
|
||||
return selected
|
||||
return selected, found
|
||||
}
|
||||
|
||||
func (m *deviceActivity) using(device protocol.DeviceID) {
|
||||
func (m *deviceActivity) using(availability Availability) {
|
||||
m.mut.Lock()
|
||||
m.act[device]++
|
||||
m.act[availability.ID]++
|
||||
m.mut.Unlock()
|
||||
}
|
||||
|
||||
func (m *deviceActivity) done(device protocol.DeviceID) {
|
||||
func (m *deviceActivity) done(availability Availability) {
|
||||
m.mut.Lock()
|
||||
m.act[device]--
|
||||
m.act[availability.ID]--
|
||||
m.mut.Unlock()
|
||||
}
|
||||
|
||||
@@ -13,46 +13,48 @@ import (
|
||||
)
|
||||
|
||||
func TestDeviceActivity(t *testing.T) {
|
||||
n0 := protocol.DeviceID([32]byte{1, 2, 3, 4})
|
||||
n1 := protocol.DeviceID([32]byte{5, 6, 7, 8})
|
||||
n2 := protocol.DeviceID([32]byte{9, 10, 11, 12})
|
||||
devices := []protocol.DeviceID{n0, n1, n2}
|
||||
n0 := Availability{protocol.DeviceID([32]byte{1, 2, 3, 4}), false}
|
||||
n1 := Availability{protocol.DeviceID([32]byte{5, 6, 7, 8}), true}
|
||||
n2 := Availability{protocol.DeviceID([32]byte{9, 10, 11, 12}), false}
|
||||
devices := []Availability{n0, n1, n2}
|
||||
na := newDeviceActivity()
|
||||
|
||||
if lb := na.leastBusy(devices); lb != n0 {
|
||||
if lb, ok := na.leastBusy(devices); !ok || lb != n0 {
|
||||
t.Errorf("Least busy device should be n0 (%v) not %v", n0, lb)
|
||||
}
|
||||
if lb := na.leastBusy(devices); lb != n0 {
|
||||
if lb, ok := na.leastBusy(devices); !ok || lb != n0 {
|
||||
t.Errorf("Least busy device should still be n0 (%v) not %v", n0, lb)
|
||||
}
|
||||
|
||||
na.using(na.leastBusy(devices))
|
||||
if lb := na.leastBusy(devices); lb != n1 {
|
||||
lb, _ := na.leastBusy(devices)
|
||||
na.using(lb)
|
||||
if lb, ok := na.leastBusy(devices); !ok || lb != n1 {
|
||||
t.Errorf("Least busy device should be n1 (%v) not %v", n1, lb)
|
||||
}
|
||||
|
||||
na.using(na.leastBusy(devices))
|
||||
if lb := na.leastBusy(devices); lb != n2 {
|
||||
lb, _ = na.leastBusy(devices)
|
||||
na.using(lb)
|
||||
if lb, ok := na.leastBusy(devices); !ok || lb != n2 {
|
||||
t.Errorf("Least busy device should be n2 (%v) not %v", n2, lb)
|
||||
}
|
||||
|
||||
na.using(na.leastBusy(devices))
|
||||
if lb := na.leastBusy(devices); lb != n0 {
|
||||
lb, _ = na.leastBusy(devices)
|
||||
na.using(lb)
|
||||
if lb, ok := na.leastBusy(devices); !ok || lb != n0 {
|
||||
t.Errorf("Least busy device should be n0 (%v) not %v", n0, lb)
|
||||
}
|
||||
|
||||
na.done(n1)
|
||||
if lb := na.leastBusy(devices); lb != n1 {
|
||||
if lb, ok := na.leastBusy(devices); !ok || lb != n1 {
|
||||
t.Errorf("Least busy device should be n1 (%v) not %v", n1, lb)
|
||||
}
|
||||
|
||||
na.done(n2)
|
||||
if lb := na.leastBusy(devices); lb != n1 {
|
||||
if lb, ok := na.leastBusy(devices); !ok || lb != n1 {
|
||||
t.Errorf("Least busy device should still be n1 (%v) not %v", n1, lb)
|
||||
}
|
||||
|
||||
na.done(n0)
|
||||
if lb := na.leastBusy(devices); lb != n0 {
|
||||
if lb, ok := na.leastBusy(devices); !ok || lb != n0 {
|
||||
t.Errorf("Least busy device should be n0 (%v) not %v", n0, lb)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
// deviceFolderFileDownloadState holds current download state of a file that
|
||||
// a remote device has advertised. blockIndexes represends indexes within
|
||||
// FileInfo.Blocks that the remote device already has, and version represents
|
||||
// the version of the file that the remote device is downloading.
|
||||
type deviceFolderFileDownloadState struct {
|
||||
blockIndexes []int32
|
||||
version protocol.Vector
|
||||
}
|
||||
|
||||
// deviceFolderDownloadState holds current download state of all files that
|
||||
// a remote device is currently downloading in a specific folder.
|
||||
type deviceFolderDownloadState struct {
|
||||
mut sync.RWMutex
|
||||
files map[string]deviceFolderFileDownloadState
|
||||
numberOfBlocksInProgress int
|
||||
}
|
||||
|
||||
// Has returns wether a block at that specific index, and that specific version of the file
|
||||
// is currently available on the remote device for pulling from a temporary file.
|
||||
func (p *deviceFolderDownloadState) Has(file string, version protocol.Vector, index int32) bool {
|
||||
p.mut.RLock()
|
||||
defer p.mut.RUnlock()
|
||||
|
||||
local, ok := p.files[file]
|
||||
|
||||
if !ok || !local.version.Equal(version) {
|
||||
return false
|
||||
}
|
||||
|
||||
for _, existingIndex := range local.blockIndexes {
|
||||
if existingIndex == index {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// Update updates internal state of what has been downloaded into the temporary
|
||||
// files by the remote device for this specific folder.
|
||||
func (p *deviceFolderDownloadState) Update(updates []protocol.FileDownloadProgressUpdate) {
|
||||
p.mut.Lock()
|
||||
defer p.mut.Unlock()
|
||||
|
||||
for _, update := range updates {
|
||||
local, ok := p.files[update.Name]
|
||||
if update.UpdateType == protocol.UpdateTypeForget && ok && local.version.Equal(update.Version) {
|
||||
p.numberOfBlocksInProgress -= len(local.blockIndexes)
|
||||
delete(p.files, update.Name)
|
||||
} else if update.UpdateType == protocol.UpdateTypeAppend {
|
||||
if !ok {
|
||||
local = deviceFolderFileDownloadState{
|
||||
blockIndexes: update.BlockIndexes,
|
||||
version: update.Version,
|
||||
}
|
||||
} else if !local.version.Equal(update.Version) {
|
||||
p.numberOfBlocksInProgress -= len(local.blockIndexes)
|
||||
local.blockIndexes = append(local.blockIndexes[:0], update.BlockIndexes...)
|
||||
local.version = update.Version
|
||||
} else {
|
||||
local.blockIndexes = append(local.blockIndexes, update.BlockIndexes...)
|
||||
}
|
||||
p.files[update.Name] = local
|
||||
p.numberOfBlocksInProgress += len(update.BlockIndexes)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// NumberOfBlocksInProgress returns the number of blocks the device has downloaded
|
||||
// for a specific folder.
|
||||
func (p *deviceFolderDownloadState) NumberOfBlocksInProgress() int {
|
||||
p.mut.RLock()
|
||||
n := p.numberOfBlocksInProgress
|
||||
p.mut.RUnlock()
|
||||
return n
|
||||
}
|
||||
|
||||
// deviceDownloadState represents the state of all in progress downloads
|
||||
// for all folders of a specific device.
|
||||
type deviceDownloadState struct {
|
||||
mut sync.RWMutex
|
||||
folders map[string]*deviceFolderDownloadState
|
||||
numberOfBlocksInProgress int
|
||||
}
|
||||
|
||||
// Update updates internal state of what has been downloaded into the temporary
|
||||
// files by the remote device for this specific folder.
|
||||
func (t *deviceDownloadState) Update(folder string, updates []protocol.FileDownloadProgressUpdate) {
|
||||
t.mut.RLock()
|
||||
f, ok := t.folders[folder]
|
||||
t.mut.RUnlock()
|
||||
|
||||
if !ok {
|
||||
f = &deviceFolderDownloadState{
|
||||
mut: sync.NewRWMutex(),
|
||||
files: make(map[string]deviceFolderFileDownloadState),
|
||||
}
|
||||
t.mut.Lock()
|
||||
t.folders[folder] = f
|
||||
t.mut.Unlock()
|
||||
}
|
||||
|
||||
f.Update(updates)
|
||||
}
|
||||
|
||||
// Has returns wether block at that specific index, and that specific version of the file
|
||||
// is currently available on the remote device for pulling from a temporary file.
|
||||
func (t *deviceDownloadState) Has(folder, file string, version protocol.Vector, index int32) bool {
|
||||
if t == nil {
|
||||
return false
|
||||
}
|
||||
t.mut.RLock()
|
||||
f, ok := t.folders[folder]
|
||||
t.mut.RUnlock()
|
||||
|
||||
if !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
return f.Has(file, version, index)
|
||||
}
|
||||
|
||||
// NumberOfBlocksInProgress returns the number of blocks the device has downloaded
|
||||
// for all folders.
|
||||
func (t *deviceDownloadState) NumberOfBlocksInProgress() int {
|
||||
if t == nil {
|
||||
return 0
|
||||
}
|
||||
|
||||
n := 0
|
||||
t.mut.RLock()
|
||||
for _, folder := range t.folders {
|
||||
n += folder.NumberOfBlocksInProgress()
|
||||
}
|
||||
t.mut.RUnlock()
|
||||
return n
|
||||
}
|
||||
|
||||
func newDeviceDownloadState() *deviceDownloadState {
|
||||
return &deviceDownloadState{
|
||||
mut: sync.NewRWMutex(),
|
||||
folders: make(map[string]*deviceFolderDownloadState),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,111 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func TestDeviceDownloadState(t *testing.T) {
|
||||
v1 := (protocol.Vector{}).Update(0)
|
||||
v2 := (protocol.Vector{}).Update(1)
|
||||
|
||||
// file 1 version 1 part 1
|
||||
f1v1p1 := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeAppend, Name: "f1", Version: v1, BlockIndexes: []int32{0, 1, 2}}
|
||||
f1v1p2 := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeAppend, Name: "f1", Version: v1, BlockIndexes: []int32{3, 4, 5}}
|
||||
f1v1del := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeForget, Name: "f1", Version: v1, BlockIndexes: nil}
|
||||
f1v2p1 := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeAppend, Name: "f1", Version: v2, BlockIndexes: []int32{10, 11, 12}}
|
||||
f1v2p2 := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeAppend, Name: "f1", Version: v2, BlockIndexes: []int32{13, 14, 15}}
|
||||
f1v2del := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeForget, Name: "f1", Version: v2, BlockIndexes: nil}
|
||||
|
||||
f2v1p1 := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeAppend, Name: "f2", Version: v1, BlockIndexes: []int32{20, 21, 22}}
|
||||
f2v1p2 := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeAppend, Name: "f2", Version: v1, BlockIndexes: []int32{23, 24, 25}}
|
||||
f2v1del := protocol.FileDownloadProgressUpdate{UpdateType: protocol.UpdateTypeForget, Name: "f2", Version: v1, BlockIndexes: nil}
|
||||
|
||||
tests := []struct {
|
||||
updates []protocol.FileDownloadProgressUpdate
|
||||
shouldHaveIndexesFrom []protocol.FileDownloadProgressUpdate
|
||||
shouldNotHaveIndexesFrom []protocol.FileDownloadProgressUpdate
|
||||
}{
|
||||
{ //1
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p2, f1v2p1, f1v2p2},
|
||||
},
|
||||
{ //2
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v2p1, f1v2p2},
|
||||
},
|
||||
{ //3
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v1del},
|
||||
nil,
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2p1, f1v2p2}},
|
||||
{ //4
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2del},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v2p1, f1v2p2},
|
||||
},
|
||||
{ //5
|
||||
// v2 replaces old v1 data
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2p1},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v2p1},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2p2},
|
||||
},
|
||||
{ //6
|
||||
// v1 delete on v2 data does nothing
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2p1, f1v1del},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v2p1},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2p2},
|
||||
},
|
||||
{ //7
|
||||
// v2 replacees v1, v2 gets deleted, and v2 part 2 gets added.
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2p1, f1v2del, f1v2p2},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v2p2},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f1v1p2, f1v2p1},
|
||||
},
|
||||
// Multiple files in one go
|
||||
{ //8
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f2v1p1},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f2v1p1},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p2, f2v1p2},
|
||||
},
|
||||
{ //9
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f2v1p1, f2v1del},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1},
|
||||
[]protocol.FileDownloadProgressUpdate{f2v1p1, f2v1p1},
|
||||
},
|
||||
{ //10
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f2v1del, f2v1p1},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p1, f2v1p1},
|
||||
[]protocol.FileDownloadProgressUpdate{f1v1p2, f2v1p2},
|
||||
},
|
||||
}
|
||||
|
||||
for i, test := range tests {
|
||||
s := newDeviceDownloadState()
|
||||
s.Update("folder", test.updates)
|
||||
|
||||
for _, expected := range test.shouldHaveIndexesFrom {
|
||||
for _, n := range expected.BlockIndexes {
|
||||
if !s.Has("folder", expected.Name, expected.Version, n) {
|
||||
t.Error("Test", i+1, "error:", expected.Name, expected.Version, "missing", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
for _, unexpected := range test.shouldNotHaveIndexesFrom {
|
||||
for _, n := range unexpected.BlockIndexes {
|
||||
if s.Has("folder", unexpected.Name, unexpected.Version, n) {
|
||||
t.Error("Test", i+1, "error:", unexpected.Name, unexpected.Version, "has extra", n)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
+139
-57
@@ -60,6 +60,11 @@ type service interface {
|
||||
getState() (folderState, time.Time, error)
|
||||
}
|
||||
|
||||
type Availability struct {
|
||||
ID protocol.DeviceID `json:"id"`
|
||||
FromTemporary bool `json:"fromTemporary"`
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
*suture.Supervisor
|
||||
|
||||
@@ -87,10 +92,12 @@ type Model struct {
|
||||
folderStatRefs map[string]*stats.FolderStatisticsReference // folder -> statsRef
|
||||
fmut sync.RWMutex // protects the above
|
||||
|
||||
conn map[protocol.DeviceID]Connection
|
||||
helloMessages map[protocol.DeviceID]protocol.HelloMessage
|
||||
devicePaused map[protocol.DeviceID]bool
|
||||
pmut sync.RWMutex // protects the above
|
||||
conn map[protocol.DeviceID]Connection
|
||||
helloMessages map[protocol.DeviceID]protocol.HelloMessage
|
||||
deviceClusterConf map[protocol.DeviceID]protocol.ClusterConfigMessage
|
||||
devicePaused map[protocol.DeviceID]bool
|
||||
deviceDownloads map[protocol.DeviceID]*deviceDownloadState
|
||||
pmut sync.RWMutex // protects the above
|
||||
}
|
||||
|
||||
var (
|
||||
@@ -129,10 +136,11 @@ func NewModel(cfg *config.Wrapper, id protocol.DeviceID, deviceName, clientName,
|
||||
folderStatRefs: make(map[string]*stats.FolderStatisticsReference),
|
||||
conn: make(map[protocol.DeviceID]Connection),
|
||||
helloMessages: make(map[protocol.DeviceID]protocol.HelloMessage),
|
||||
deviceClusterConf: make(map[protocol.DeviceID]protocol.ClusterConfigMessage),
|
||||
devicePaused: make(map[protocol.DeviceID]bool),
|
||||
|
||||
fmut: sync.NewRWMutex(),
|
||||
pmut: sync.NewRWMutex(),
|
||||
deviceDownloads: make(map[protocol.DeviceID]*deviceDownloadState),
|
||||
fmut: sync.NewRWMutex(),
|
||||
pmut: sync.NewRWMutex(),
|
||||
}
|
||||
if cfg.Options().ProgressUpdateIntervalS > -1 {
|
||||
go m.progressEmitter.Serve()
|
||||
@@ -304,10 +312,6 @@ func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
|
||||
|
||||
// ConnectionStats returns a map with connection statistics for each device.
|
||||
func (m *Model) ConnectionStats() map[string]interface{} {
|
||||
type remoteAddrer interface {
|
||||
RemoteAddr() net.Addr
|
||||
}
|
||||
|
||||
m.pmut.RLock()
|
||||
m.fmut.RLock()
|
||||
|
||||
@@ -392,6 +396,11 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
|
||||
return true
|
||||
})
|
||||
|
||||
// This might might be more than it really is, because some blocks can be of a smaller size.
|
||||
m.pmut.RLock()
|
||||
need -= int64(m.deviceDownloads[device].NumberOfBlocksInProgress() * protocol.BlockSize)
|
||||
m.pmut.RUnlock()
|
||||
|
||||
needRatio := float64(need) / float64(tot)
|
||||
completionPct := 100 * (1 - needRatio)
|
||||
l.Debugf("%v Completion(%s, %q): %f (%d / %d = %f)", m, device, folder, completionPct, need, tot, needRatio)
|
||||
@@ -613,6 +622,10 @@ func (m *Model) folderSharedWithUnlocked(folder string, deviceID protocol.Device
|
||||
func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterConfigMessage) {
|
||||
// Check the peer device's announced folders against our own. Emits events
|
||||
// for folders that we don't expect (unknown or not shared).
|
||||
// Also, collect a list of folders we do share, and if he's interested in
|
||||
// temporary indexes, subscribe the connection.
|
||||
|
||||
tempIndexFolders := make([]string, 0, len(cm.Folders))
|
||||
|
||||
m.fmut.Lock()
|
||||
nextFolder:
|
||||
@@ -639,9 +652,24 @@ nextFolder:
|
||||
l.Infof("Unexpected folder ID %q sent from device %q; ensure that the folder exists and that this device is selected under \"Share With\" in the folder configuration.", folder.ID, deviceID)
|
||||
continue
|
||||
}
|
||||
if folder.Flags&protocol.FlagFolderDisabledTempIndexes == 0 {
|
||||
tempIndexFolders = append(tempIndexFolders, folder.ID)
|
||||
}
|
||||
}
|
||||
m.fmut.Unlock()
|
||||
|
||||
// This breaks if we send multiple CM messages during the same connection.
|
||||
if len(tempIndexFolders) > 0 {
|
||||
m.pmut.RLock()
|
||||
conn, ok := m.conn[deviceID]
|
||||
m.pmut.RUnlock()
|
||||
// In case we've got ClusterConfig, and the connection disappeared
|
||||
// from infront of our nose.
|
||||
if ok {
|
||||
m.progressEmitter.temporaryIndexSubscribe(conn, tempIndexFolders)
|
||||
}
|
||||
}
|
||||
|
||||
var changed bool
|
||||
|
||||
if m.cfg.Devices()[deviceID].Introducer {
|
||||
@@ -649,9 +677,6 @@ nextFolder:
|
||||
// and devices and add what we are missing.
|
||||
|
||||
for _, folder := range cm.Folders {
|
||||
// If we don't have this folder yet, skip it. Ideally, we'd
|
||||
// offer up something in the GUI to create the folder, but for the
|
||||
// moment we only handle folders that we already have.
|
||||
if _, ok := m.folderDevices[folder.ID]; !ok {
|
||||
continue
|
||||
}
|
||||
@@ -740,10 +765,13 @@ func (m *Model) Close(device protocol.DeviceID, err error) {
|
||||
|
||||
conn, ok := m.conn[device]
|
||||
if ok {
|
||||
m.progressEmitter.temporaryIndexUnsubscribe(conn)
|
||||
closeRawConn(conn)
|
||||
}
|
||||
delete(m.conn, device)
|
||||
delete(m.helloMessages, device)
|
||||
delete(m.deviceClusterConf, device)
|
||||
delete(m.deviceDownloads, device)
|
||||
m.pmut.Unlock()
|
||||
}
|
||||
|
||||
@@ -756,19 +784,20 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
||||
|
||||
if !m.folderSharedWith(folder, deviceID) {
|
||||
l.Warnf("Request from %s for file %s in unshared folder %q", deviceID, name, folder)
|
||||
return protocol.ErrInvalid
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
if flags != 0 {
|
||||
// We don't currently support or expect any flags.
|
||||
return protocol.ErrInvalid
|
||||
if flags != 0 && flags != protocol.FlagFromTemporary {
|
||||
// We currently support only no flags, or FromTemporary flag.
|
||||
return fmt.Errorf("protocol error: unknown flags 0x%x in Request message", flags)
|
||||
}
|
||||
|
||||
if deviceID != protocol.LocalDeviceID {
|
||||
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
|
||||
l.Debugf("%v REQ(in): %s: %q / %q o=%d s=%d f=%d", m, deviceID, folder, name, offset, len(buf), flags)
|
||||
}
|
||||
m.fmut.RLock()
|
||||
folderPath := m.folderCfgs[folder].Path()
|
||||
folderCfg := m.folderCfgs[folder]
|
||||
folderPath := folderCfg.Path()
|
||||
folderIgnores := m.folderIgnores[folder]
|
||||
m.fmut.RUnlock()
|
||||
|
||||
@@ -806,8 +835,6 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
||||
}
|
||||
}
|
||||
|
||||
var reader io.ReaderAt
|
||||
var err error
|
||||
if info, err := os.Lstat(fn); err == nil && info.Mode()&os.ModeSymlink != 0 {
|
||||
target, _, err := symlinks.Read(fn)
|
||||
if err != nil {
|
||||
@@ -817,28 +844,30 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
||||
}
|
||||
return protocol.ErrGeneric
|
||||
}
|
||||
reader = strings.NewReader(target)
|
||||
} else {
|
||||
// Cannot easily cache fd's because we might need to delete the file
|
||||
// at any moment.
|
||||
reader, err = os.Open(fn)
|
||||
if err != nil {
|
||||
l.Debugln("os.Open:", err)
|
||||
if os.IsNotExist(err) {
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
if _, err := strings.NewReader(target).ReadAt(buf, offset); err != nil {
|
||||
l.Debugln("symlink.Reader.ReadAt", err)
|
||||
return protocol.ErrGeneric
|
||||
}
|
||||
|
||||
defer reader.(*os.File).Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
_, err = reader.ReadAt(buf, offset)
|
||||
if err != nil {
|
||||
l.Debugln("reader.ReadAt:", err)
|
||||
// Only check temp files if the flag is set, and if we are set to advertise
|
||||
// the temp indexes.
|
||||
if flags&protocol.FlagFromTemporary != 0 && !folderCfg.DisableTempIndexes {
|
||||
tempFn := filepath.Join(folderPath, defTempNamer.TempName(name))
|
||||
if err := readOffsetIntoBuf(tempFn, offset, buf); err == nil {
|
||||
return nil
|
||||
}
|
||||
// Fall through to reading from a non-temp file, just incase the temp
|
||||
// file has finished downloading.
|
||||
}
|
||||
|
||||
err := readOffsetIntoBuf(fn, offset, buf)
|
||||
if os.IsNotExist(err) {
|
||||
return protocol.ErrNoSuchFile
|
||||
} else if err != nil {
|
||||
return protocol.ErrGeneric
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -990,6 +1019,7 @@ func (m *Model) AddConnection(conn Connection, hello protocol.HelloMessage) {
|
||||
panic("add existing device")
|
||||
}
|
||||
m.conn[deviceID] = conn
|
||||
m.deviceDownloads[deviceID] = newDeviceDownloadState()
|
||||
|
||||
m.helloMessages[deviceID] = hello
|
||||
|
||||
@@ -1011,7 +1041,7 @@ func (m *Model) AddConnection(conn Connection, hello protocol.HelloMessage) {
|
||||
l.Infof(`Device %s client is "%s %s" named "%s"`, deviceID, hello.ClientName, hello.ClientVersion, hello.DeviceName)
|
||||
|
||||
device, ok := m.cfg.Devices()[deviceID]
|
||||
if ok && device.Name == "" {
|
||||
if ok && (device.Name == "" || m.cfg.Options().OverwriteNames) {
|
||||
device.Name = hello.DeviceName
|
||||
m.cfg.SetDevice(device)
|
||||
m.cfg.Save()
|
||||
@@ -1044,6 +1074,24 @@ func (m *Model) PauseDevice(device protocol.DeviceID) {
|
||||
events.Default.Log(events.DevicePaused, map[string]string{"device": device.String()})
|
||||
}
|
||||
|
||||
func (m *Model) DownloadProgress(device protocol.DeviceID, folder string, updates []protocol.FileDownloadProgressUpdate, flags uint32, options []protocol.Option) {
|
||||
if !m.folderSharedWith(folder, device) {
|
||||
return
|
||||
}
|
||||
|
||||
m.fmut.RLock()
|
||||
cfg, ok := m.folderCfgs[folder]
|
||||
m.fmut.RUnlock()
|
||||
|
||||
if !ok || cfg.ReadOnly || cfg.DisableTempIndexes {
|
||||
return
|
||||
}
|
||||
|
||||
m.pmut.RLock()
|
||||
m.deviceDownloads[device].Update(folder, updates)
|
||||
m.pmut.RUnlock()
|
||||
}
|
||||
|
||||
func (m *Model) ResumeDevice(device protocol.DeviceID) {
|
||||
m.pmut.Lock()
|
||||
m.devicePaused[device] = false
|
||||
@@ -1215,7 +1263,7 @@ func (m *Model) updateLocals(folder string, fs []protocol.FileInfo) {
|
||||
})
|
||||
}
|
||||
|
||||
func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, flags uint32, options []protocol.Option) ([]byte, error) {
|
||||
func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
|
||||
m.pmut.RLock()
|
||||
nc, ok := m.conn[deviceID]
|
||||
m.pmut.RUnlock()
|
||||
@@ -1224,9 +1272,9 @@ func (m *Model) requestGlobal(deviceID protocol.DeviceID, folder, name string, o
|
||||
return nil, fmt.Errorf("requestGlobal: no such device: %s", deviceID)
|
||||
}
|
||||
|
||||
l.Debugf("%v REQ(out): %s: %q / %q o=%d s=%d h=%x f=%x op=%s", m, deviceID, folder, name, offset, size, hash, flags, options)
|
||||
l.Debugf("%v REQ(out): %s: %q / %q o=%d s=%d h=%x ft=%t op=%s", m, deviceID, folder, name, offset, size, hash, fromTemporary)
|
||||
|
||||
return nc.Request(folder, name, offset, size, hash, flags, options)
|
||||
return nc.Request(folder, name, offset, size, hash, fromTemporary)
|
||||
}
|
||||
|
||||
func (m *Model) AddFolder(cfg config.FolderConfiguration) {
|
||||
@@ -1557,6 +1605,9 @@ func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
|
||||
if folderCfg.IgnoreDelete {
|
||||
flags |= protocol.FlagFolderIgnoreDelete
|
||||
}
|
||||
if folderCfg.DisableTempIndexes {
|
||||
flags |= protocol.FlagFolderDisabledTempIndexes
|
||||
}
|
||||
protocolFolder.Flags = flags
|
||||
for _, device := range m.folderDevices[folder] {
|
||||
// DeviceID is a value type, but with an underlying array. Copy it
|
||||
@@ -1740,7 +1791,7 @@ func (m *Model) GlobalDirectoryTree(folder, prefix string, levels int, dirsonly
|
||||
return output
|
||||
}
|
||||
|
||||
func (m *Model) Availability(folder, file string) []protocol.DeviceID {
|
||||
func (m *Model) Availability(folder, file string, version protocol.Vector, block protocol.BlockInfo) []Availability {
|
||||
// Acquire this lock first, as the value returned from foldersFiles can
|
||||
// get heavily modified on Close()
|
||||
m.pmut.RLock()
|
||||
@@ -1748,19 +1799,27 @@ func (m *Model) Availability(folder, file string) []protocol.DeviceID {
|
||||
|
||||
m.fmut.RLock()
|
||||
fs, ok := m.folderFiles[folder]
|
||||
devices := m.folderDevices[folder]
|
||||
m.fmut.RUnlock()
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
|
||||
availableDevices := []protocol.DeviceID{}
|
||||
var availabilities []Availability
|
||||
for _, device := range fs.Availability(file) {
|
||||
_, ok := m.conn[device]
|
||||
if ok {
|
||||
availableDevices = append(availableDevices, device)
|
||||
availabilities = append(availabilities, Availability{ID: device, FromTemporary: false})
|
||||
}
|
||||
}
|
||||
return availableDevices
|
||||
|
||||
for _, device := range devices {
|
||||
if m.deviceDownloads[device].Has(folder, file, version, int32(block.Offset/protocol.BlockSize)) {
|
||||
availabilities = append(availabilities, Availability{ID: device, FromTemporary: true})
|
||||
}
|
||||
}
|
||||
|
||||
return availabilities
|
||||
}
|
||||
|
||||
// BringToFront bumps the given files priority in the job queue.
|
||||
@@ -2090,31 +2149,54 @@ func stringSliceWithout(ss []string, s string) []string {
|
||||
return ss
|
||||
}
|
||||
|
||||
// unifySubs takes a list of files or directories and trims them down to
|
||||
// themselves or the closest parent that exists() returns true for, while
|
||||
// removing duplicates and subdirectories. That is, if we have foo/ in the
|
||||
// list, we don't also need foo/bar/ because that's already covered.
|
||||
func unifySubs(dirs []string, exists func(dir string) bool) []string {
|
||||
var subs []string
|
||||
func readOffsetIntoBuf(file string, offset int64, buf []byte) error {
|
||||
fd, err := os.Open(file)
|
||||
if err != nil {
|
||||
l.Debugln("readOffsetIntoBuf.Open", file, err)
|
||||
return err
|
||||
}
|
||||
|
||||
// Trim each item to itself or its closest known parent
|
||||
defer fd.Close()
|
||||
_, err = fd.ReadAt(buf, offset)
|
||||
if err != nil {
|
||||
l.Debugln("readOffsetIntoBuf.ReadAt", file, err)
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// The exists function is expected to return true for all known paths
|
||||
// (excluding "" and ".")
|
||||
func unifySubs(dirs []string, exists func(dir string) bool) []string {
|
||||
subs := trimUntilParentKnown(dirs, exists)
|
||||
sort.Strings(subs)
|
||||
return simplifySortedPaths(subs)
|
||||
}
|
||||
|
||||
func trimUntilParentKnown(dirs []string, exists func(dir string) bool) []string {
|
||||
var subs []string
|
||||
for _, sub := range dirs {
|
||||
for sub != "" && sub != ".stfolder" && sub != ".stignore" {
|
||||
if exists(sub) {
|
||||
sub = filepath.Clean(sub)
|
||||
parent := filepath.Dir(sub)
|
||||
if parent == "." || exists(parent) {
|
||||
break
|
||||
}
|
||||
sub = filepath.Dir(sub)
|
||||
sub = parent
|
||||
if sub == "." || sub == string(filepath.Separator) {
|
||||
// Shortcut. We are going to scan the full folder, so we can
|
||||
// just return an empty list of subs at this point.
|
||||
return nil
|
||||
}
|
||||
}
|
||||
if sub == "" {
|
||||
return nil
|
||||
}
|
||||
subs = append(subs, sub)
|
||||
}
|
||||
return subs
|
||||
}
|
||||
|
||||
// Remove any paths that are already covered by their parent
|
||||
sort.Strings(subs)
|
||||
func simplifySortedPaths(subs []string) []string {
|
||||
var cleaned []string
|
||||
next:
|
||||
for _, sub := range subs {
|
||||
|
||||
+65
-17
@@ -203,9 +203,17 @@ func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
|
||||
b.ReportAllocs()
|
||||
}
|
||||
|
||||
type downloadProgressMessage struct {
|
||||
folder string
|
||||
updates []protocol.FileDownloadProgressUpdate
|
||||
flags uint32
|
||||
options []protocol.Option
|
||||
}
|
||||
|
||||
type FakeConnection struct {
|
||||
id protocol.DeviceID
|
||||
requestData []byte
|
||||
id protocol.DeviceID
|
||||
requestData []byte
|
||||
downloadProgressMessages []downloadProgressMessage
|
||||
}
|
||||
|
||||
func (FakeConnection) Close() error {
|
||||
@@ -235,7 +243,7 @@ func (FakeConnection) IndexUpdate(string, []protocol.FileInfo, uint32, []protoco
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f FakeConnection) Request(folder, name string, offset int64, size int, hash []byte, flags uint32, options []protocol.Option) ([]byte, error) {
|
||||
func (f FakeConnection) Request(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
|
||||
return f.requestData, nil
|
||||
}
|
||||
|
||||
@@ -253,6 +261,15 @@ func (FakeConnection) Statistics() protocol.Statistics {
|
||||
return protocol.Statistics{}
|
||||
}
|
||||
|
||||
func (f *FakeConnection) DownloadProgress(folder string, updates []protocol.FileDownloadProgressUpdate, flags uint32, options []protocol.Option) {
|
||||
f.downloadProgressMessages = append(f.downloadProgressMessages, downloadProgressMessage{
|
||||
folder: folder,
|
||||
updates: updates,
|
||||
flags: flags,
|
||||
options: options,
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkRequest(b *testing.B) {
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
@@ -271,7 +288,7 @@ func BenchmarkRequest(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
fc := FakeConnection{
|
||||
fc := &FakeConnection{
|
||||
id: device1,
|
||||
requestData: []byte("some data to return"),
|
||||
}
|
||||
@@ -284,7 +301,7 @@ func BenchmarkRequest(b *testing.B) {
|
||||
|
||||
b.ResetTimer()
|
||||
for i := 0; i < b.N; i++ {
|
||||
data, err := m.requestGlobal(device1, "default", files[i%n].Name, 0, 32, nil, 0, nil)
|
||||
data, err := m.requestGlobal(device1, "default", files[i%n].Name, 0, 32, nil, false)
|
||||
if err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
@@ -318,7 +335,7 @@ func TestDeviceRename(t *testing.T) {
|
||||
|
||||
conn := Connection{
|
||||
&net.TCPConn{},
|
||||
FakeConnection{
|
||||
&FakeConnection{
|
||||
id: device1,
|
||||
requestData: []byte("some data to return"),
|
||||
},
|
||||
@@ -357,6 +374,19 @@ func TestDeviceRename(t *testing.T) {
|
||||
if cfgw.Devices()[device1].Name != "tester" {
|
||||
t.Errorf("Device name not saved in config")
|
||||
}
|
||||
|
||||
m.Close(device1, protocol.ErrTimeout)
|
||||
|
||||
opts := cfg.Options()
|
||||
opts.OverwriteNames = true
|
||||
cfg.SetOptions(opts)
|
||||
|
||||
hello.DeviceName = "tester2"
|
||||
m.AddConnection(conn, hello)
|
||||
|
||||
if cfg.Devices()[device1].Name != "tester2" {
|
||||
t.Errorf("Device name not overwritten")
|
||||
}
|
||||
}
|
||||
|
||||
func TestClusterConfig(t *testing.T) {
|
||||
@@ -1223,49 +1253,67 @@ func TestUnifySubs(t *testing.T) {
|
||||
out []string // expected output
|
||||
}{
|
||||
{
|
||||
// trailing slashes are cleaned, known paths are just passed on
|
||||
// 0. trailing slashes are cleaned, known paths are just passed on
|
||||
[]string{"foo/", "bar//"},
|
||||
[]string{"foo", "bar"},
|
||||
[]string{"bar", "foo"}, // the output is sorted
|
||||
},
|
||||
{
|
||||
// "foo/bar" gets trimmed as it's covered by foo
|
||||
// 1. "foo/bar" gets trimmed as it's covered by foo
|
||||
[]string{"foo", "bar/", "foo/bar/"},
|
||||
[]string{"foo", "bar"},
|
||||
[]string{"bar", "foo"},
|
||||
},
|
||||
{
|
||||
// "bar" gets trimmed to "" as it's unknown,
|
||||
// "" gets simplified to the empty list
|
||||
[]string{"foo", "bar", "foo/bar"},
|
||||
// 2. "" gets simplified to the empty list; ie scan all
|
||||
[]string{"foo", ""},
|
||||
[]string{"foo"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
// two independent known paths, both are kept
|
||||
// 3. "foo/bar" is unknown, but it's kept
|
||||
// because its parent is known
|
||||
[]string{"foo/bar"},
|
||||
[]string{"foo"},
|
||||
[]string{"foo/bar"},
|
||||
},
|
||||
{
|
||||
// 4. two independent known paths, both are kept
|
||||
// "usr/lib" is not a prefix of "usr/libexec"
|
||||
[]string{"usr/lib", "usr/libexec"},
|
||||
[]string{"usr/lib", "usr/libexec"},
|
||||
[]string{"usr", "usr/lib", "usr/libexec"},
|
||||
[]string{"usr/lib", "usr/libexec"},
|
||||
},
|
||||
{
|
||||
// "usr/lib" is a prefix of "usr/lib/exec"
|
||||
// 5. "usr/lib" is a prefix of "usr/lib/exec"
|
||||
[]string{"usr/lib", "usr/lib/exec"},
|
||||
[]string{"usr/lib", "usr/libexec"},
|
||||
[]string{"usr", "usr/lib", "usr/libexec"},
|
||||
[]string{"usr/lib"},
|
||||
},
|
||||
{
|
||||
// .stignore and .stfolder are special and are passed on
|
||||
// 6. .stignore and .stfolder are special and are passed on
|
||||
// verbatim even though they are unknown
|
||||
[]string{".stfolder", ".stignore"},
|
||||
[]string{},
|
||||
[]string{".stfolder", ".stignore"},
|
||||
},
|
||||
{
|
||||
// but the presense of something else unknown forces an actual
|
||||
// 7. but the presense of something else unknown forces an actual
|
||||
// scan
|
||||
[]string{".stfolder", ".stignore", "foo/bar"},
|
||||
[]string{},
|
||||
[]string{".stfolder", ".stignore", "foo"},
|
||||
},
|
||||
{
|
||||
// 8. explicit request to scan all
|
||||
nil,
|
||||
[]string{"foo"},
|
||||
nil,
|
||||
},
|
||||
{
|
||||
// 9. empty list of subs
|
||||
[]string{},
|
||||
[]string{"foo"},
|
||||
nil,
|
||||
},
|
||||
}
|
||||
|
||||
+159
-19
@@ -9,19 +9,22 @@ package model
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"reflect"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
type ProgressEmitter struct {
|
||||
registry map[string]*sharedPullerState
|
||||
interval time.Duration
|
||||
last map[string]map[string]*pullerProgress
|
||||
mut sync.Mutex
|
||||
registry map[string]*sharedPullerState
|
||||
interval time.Duration
|
||||
minBlocks int
|
||||
lastUpdate time.Time
|
||||
sentDownloadStates map[protocol.DeviceID]*sentDownloadState // States representing what we've sent to the other peer via DownloadProgress messages.
|
||||
connections map[string][]protocol.Connection
|
||||
mut sync.Mutex
|
||||
|
||||
timer *time.Timer
|
||||
|
||||
@@ -32,11 +35,12 @@ type ProgressEmitter struct {
|
||||
// DownloadProgress events every interval.
|
||||
func NewProgressEmitter(cfg *config.Wrapper) *ProgressEmitter {
|
||||
t := &ProgressEmitter{
|
||||
stop: make(chan struct{}),
|
||||
registry: make(map[string]*sharedPullerState),
|
||||
last: make(map[string]map[string]*pullerProgress),
|
||||
timer: time.NewTimer(time.Millisecond),
|
||||
mut: sync.NewMutex(),
|
||||
stop: make(chan struct{}),
|
||||
registry: make(map[string]*sharedPullerState),
|
||||
timer: time.NewTimer(time.Millisecond),
|
||||
sentDownloadStates: make(map[protocol.DeviceID]*sentDownloadState),
|
||||
connections: make(map[string][]protocol.Connection),
|
||||
mut: sync.NewMutex(),
|
||||
}
|
||||
|
||||
t.CommitConfiguration(config.Configuration{}, cfg.Raw())
|
||||
@@ -48,6 +52,8 @@ func NewProgressEmitter(cfg *config.Wrapper) *ProgressEmitter {
|
||||
// Serve starts the progress emitter which starts emitting DownloadProgress
|
||||
// events as the progress happens.
|
||||
func (t *ProgressEmitter) Serve() {
|
||||
var lastUpdate time.Time
|
||||
var lastCount, newCount int
|
||||
for {
|
||||
select {
|
||||
case <-t.stop:
|
||||
@@ -56,21 +62,28 @@ func (t *ProgressEmitter) Serve() {
|
||||
case <-t.timer.C:
|
||||
t.mut.Lock()
|
||||
l.Debugln("progress emitter: timer - looking after", len(t.registry))
|
||||
output := make(map[string]map[string]*pullerProgress)
|
||||
|
||||
newLastUpdated := lastUpdate
|
||||
newCount = len(t.registry)
|
||||
for _, puller := range t.registry {
|
||||
if output[puller.folder] == nil {
|
||||
output[puller.folder] = make(map[string]*pullerProgress)
|
||||
updated := puller.Updated()
|
||||
if updated.After(newLastUpdated) {
|
||||
newLastUpdated = updated
|
||||
}
|
||||
output[puller.folder][puller.file.Name] = puller.Progress()
|
||||
}
|
||||
if !reflect.DeepEqual(t.last, output) {
|
||||
events.Default.Log(events.DownloadProgress, output)
|
||||
t.last = output
|
||||
l.Debugf("progress emitter: emitting %#v", output)
|
||||
|
||||
if !newLastUpdated.Equal(lastUpdate) || newCount != lastCount {
|
||||
lastUpdate = newLastUpdated
|
||||
lastCount = newCount
|
||||
t.sendDownloadProgressEvent()
|
||||
if len(t.connections) > 0 {
|
||||
t.sendDownloadProgressMessages()
|
||||
}
|
||||
} else {
|
||||
l.Debugln("progress emitter: nothing new")
|
||||
}
|
||||
if len(t.registry) != 0 {
|
||||
|
||||
if newCount != 0 {
|
||||
t.timer.Reset(t.interval)
|
||||
}
|
||||
t.mut.Unlock()
|
||||
@@ -78,6 +91,95 @@ func (t *ProgressEmitter) Serve() {
|
||||
}
|
||||
}
|
||||
|
||||
func (t *ProgressEmitter) sendDownloadProgressEvent() {
|
||||
// registry lock already held
|
||||
output := make(map[string]map[string]*pullerProgress)
|
||||
for _, puller := range t.registry {
|
||||
if output[puller.folder] == nil {
|
||||
output[puller.folder] = make(map[string]*pullerProgress)
|
||||
}
|
||||
output[puller.folder][puller.file.Name] = puller.Progress()
|
||||
}
|
||||
events.Default.Log(events.DownloadProgress, output)
|
||||
l.Debugf("progress emitter: emitting %#v", output)
|
||||
}
|
||||
|
||||
func (t *ProgressEmitter) sendDownloadProgressMessages() {
|
||||
// registry lock already held
|
||||
sharedFolders := make(map[protocol.DeviceID][]string)
|
||||
deviceConns := make(map[protocol.DeviceID]protocol.Connection)
|
||||
subscribers := t.connections
|
||||
for folder, conns := range subscribers {
|
||||
for _, conn := range conns {
|
||||
id := conn.ID()
|
||||
|
||||
deviceConns[id] = conn
|
||||
sharedFolders[id] = append(sharedFolders[id], folder)
|
||||
|
||||
state, ok := t.sentDownloadStates[id]
|
||||
if !ok {
|
||||
state = &sentDownloadState{
|
||||
folderStates: make(map[string]*sentFolderDownloadState),
|
||||
}
|
||||
t.sentDownloadStates[id] = state
|
||||
}
|
||||
|
||||
var activePullers []*sharedPullerState
|
||||
for _, puller := range t.registry {
|
||||
if puller.folder != folder || puller.file.IsSymlink() || puller.file.IsDirectory() || len(puller.file.Blocks) <= t.minBlocks {
|
||||
continue
|
||||
}
|
||||
activePullers = append(activePullers, puller)
|
||||
}
|
||||
|
||||
// For every new puller that hasn't yet been seen, it will send all the blocks the puller has available
|
||||
// For every existing puller, it will check for new blocks, and send update for the new blocks only
|
||||
// For every puller that we've seen before but is no longer there, we will send a forget message
|
||||
updates := state.update(folder, activePullers)
|
||||
|
||||
if len(updates) > 0 {
|
||||
conn.DownloadProgress(folder, updates, 0, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Clean up sentDownloadStates for devices which we are no longer connected to.
|
||||
for id := range t.sentDownloadStates {
|
||||
_, ok := deviceConns[id]
|
||||
if !ok {
|
||||
// Null out outstanding entries for device
|
||||
delete(t.sentDownloadStates, id)
|
||||
}
|
||||
}
|
||||
|
||||
// If a folder was unshared from some device, tell it that all temp files
|
||||
// are now gone.
|
||||
for id, sharedDeviceFolders := range sharedFolders {
|
||||
state := t.sentDownloadStates[id]
|
||||
nextFolder:
|
||||
// For each of the folders that the state is aware of,
|
||||
// try to match it with a shared folder we've discovered above,
|
||||
for _, folder := range state.folders() {
|
||||
for _, existingFolder := range sharedDeviceFolders {
|
||||
if existingFolder == folder {
|
||||
continue nextFolder
|
||||
}
|
||||
}
|
||||
|
||||
// If we fail to find that folder, we tell the state to forget about it
|
||||
// and return us a list of updates which would clean up the state
|
||||
// on the remote end.
|
||||
updates := state.cleanup(folder)
|
||||
if len(updates) > 0 {
|
||||
// XXX: Don't send this now, as the only way we've unshared a folder
|
||||
// is by breaking the connection and reconnecting, hence sending
|
||||
// forget messages for some random folder currently makes no sense.
|
||||
// deviceConns[id].DownloadProgress(folder, updates, 0, nil)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// VerifyConfiguration implements the config.Committer interface
|
||||
func (t *ProgressEmitter) VerifyConfiguration(from, to config.Configuration) error {
|
||||
return nil
|
||||
@@ -89,6 +191,7 @@ func (t *ProgressEmitter) CommitConfiguration(from, to config.Configuration) boo
|
||||
defer t.mut.Unlock()
|
||||
|
||||
t.interval = time.Duration(to.Options.ProgressUpdateIntervalS) * time.Second
|
||||
t.minBlocks = to.Options.TempIndexMinBlocks
|
||||
l.Debugln("progress emitter: updated interval", t.interval)
|
||||
|
||||
return true
|
||||
@@ -115,7 +218,9 @@ func (t *ProgressEmitter) Register(s *sharedPullerState) {
|
||||
func (t *ProgressEmitter) Deregister(s *sharedPullerState) {
|
||||
t.mut.Lock()
|
||||
defer t.mut.Unlock()
|
||||
|
||||
l.Debugln("progress emitter: deregistering", s.folder, s.file.Name)
|
||||
|
||||
delete(t.registry, filepath.Join(s.folder, s.file.Name))
|
||||
}
|
||||
|
||||
@@ -142,3 +247,38 @@ func (t *ProgressEmitter) lenRegistry() int {
|
||||
defer t.mut.Unlock()
|
||||
return len(t.registry)
|
||||
}
|
||||
|
||||
func (t *ProgressEmitter) temporaryIndexSubscribe(conn protocol.Connection, folders []string) {
|
||||
t.mut.Lock()
|
||||
for _, folder := range folders {
|
||||
t.connections[folder] = append(t.connections[folder], conn)
|
||||
}
|
||||
t.mut.Unlock()
|
||||
}
|
||||
|
||||
func (t *ProgressEmitter) temporaryIndexUnsubscribe(conn protocol.Connection) {
|
||||
t.mut.Lock()
|
||||
left := make(map[string][]protocol.Connection, len(t.connections))
|
||||
for folder, conns := range t.connections {
|
||||
connsLeft := connsWithout(conns, conn)
|
||||
if len(connsLeft) > 0 {
|
||||
left[folder] = connsLeft
|
||||
}
|
||||
}
|
||||
t.connections = left
|
||||
t.mut.Unlock()
|
||||
}
|
||||
|
||||
func connsWithout(conns []protocol.Connection, conn protocol.Connection) []protocol.Connection {
|
||||
if len(conns) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
newConns := make([]protocol.Connection, 0, len(conns)-1)
|
||||
for _, existingConn := range conns {
|
||||
if existingConn != conn {
|
||||
newConns = append(newConns, existingConn)
|
||||
}
|
||||
}
|
||||
return newConns
|
||||
}
|
||||
|
||||
@@ -7,34 +7,46 @@
|
||||
package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
var timeout = 10 * time.Millisecond
|
||||
var timeout = 100 * time.Millisecond
|
||||
|
||||
func caller(skip int) string {
|
||||
_, file, line, ok := runtime.Caller(skip + 1)
|
||||
if !ok {
|
||||
return "unknown"
|
||||
}
|
||||
return fmt.Sprintf("%s:%d", filepath.Base(file), line)
|
||||
}
|
||||
|
||||
func expectEvent(w *events.Subscription, t *testing.T, size int) {
|
||||
event, err := w.Poll(timeout)
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error:", err)
|
||||
t.Fatal("Unexpected error:", err, "at", caller(1))
|
||||
}
|
||||
if event.Type != events.DownloadProgress {
|
||||
t.Fatal("Unexpected event:", event)
|
||||
t.Fatal("Unexpected event:", event, "at", caller(1))
|
||||
}
|
||||
data := event.Data.(map[string]map[string]*pullerProgress)
|
||||
if len(data) != size {
|
||||
t.Fatal("Unexpected event data size:", data)
|
||||
t.Fatal("Unexpected event data size:", data, "at", caller(1))
|
||||
}
|
||||
}
|
||||
|
||||
func expectTimeout(w *events.Subscription, t *testing.T) {
|
||||
_, err := w.Poll(timeout)
|
||||
if err != events.ErrTimeout {
|
||||
t.Fatal("Unexpected non-Timeout error:", err)
|
||||
t.Fatal("Unexpected non-Timeout error:", err, "at", caller(1))
|
||||
}
|
||||
}
|
||||
|
||||
@@ -52,14 +64,15 @@ func TestProgressEmitter(t *testing.T) {
|
||||
expectTimeout(w, t)
|
||||
|
||||
s := sharedPullerState{
|
||||
mut: sync.NewMutex(),
|
||||
updated: time.Now(),
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
p.Register(&s)
|
||||
|
||||
expectEvent(w, t, 1)
|
||||
expectTimeout(w, t)
|
||||
|
||||
s.copyDone()
|
||||
s.copyDone(protocol.BlockInfo{})
|
||||
|
||||
expectEvent(w, t, 1)
|
||||
expectTimeout(w, t)
|
||||
@@ -74,7 +87,7 @@ func TestProgressEmitter(t *testing.T) {
|
||||
expectEvent(w, t, 1)
|
||||
expectTimeout(w, t)
|
||||
|
||||
s.pullDone()
|
||||
s.pullDone(protocol.BlockInfo{})
|
||||
|
||||
expectEvent(w, t, 1)
|
||||
expectTimeout(w, t)
|
||||
@@ -85,3 +98,335 @@ func TestProgressEmitter(t *testing.T) {
|
||||
expectTimeout(w, t)
|
||||
|
||||
}
|
||||
|
||||
func TestSendDownloadProgressMessages(t *testing.T) {
|
||||
|
||||
c := config.Wrap("/tmp/test", config.Configuration{})
|
||||
c.SetOptions(config.OptionsConfiguration{
|
||||
ProgressUpdateIntervalS: 0,
|
||||
TempIndexMinBlocks: 10,
|
||||
})
|
||||
|
||||
fc := &FakeConnection{}
|
||||
|
||||
p := NewProgressEmitter(c)
|
||||
p.temporaryIndexSubscribe(fc, []string{"folder", "folder2"})
|
||||
|
||||
expect := func(updateIdx int, state *sharedPullerState, updateType uint32, version protocol.Vector, blocks []int32, remove bool) {
|
||||
messageIdx := -1
|
||||
for i, msg := range fc.downloadProgressMessages {
|
||||
if msg.folder == state.folder {
|
||||
messageIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
if messageIdx < 0 {
|
||||
t.Errorf("Message for folder %s does not exist at %s", state.folder, caller(1))
|
||||
}
|
||||
|
||||
msg := fc.downloadProgressMessages[messageIdx]
|
||||
|
||||
// Don't know the index (it's random due to iterating maps)
|
||||
if updateIdx == -1 {
|
||||
for i, upd := range msg.updates {
|
||||
if upd.Name == state.file.Name {
|
||||
updateIdx = i
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if updateIdx == -1 {
|
||||
t.Errorf("Could not find update for %s at %s", state.file.Name, caller(1))
|
||||
}
|
||||
|
||||
if updateIdx > len(msg.updates)-1 {
|
||||
t.Errorf("Update at index %d does not exist at %s", updateIdx, caller(1))
|
||||
}
|
||||
|
||||
update := msg.updates[updateIdx]
|
||||
|
||||
if update.UpdateType != updateType {
|
||||
t.Errorf("Wrong update type at %s", caller(1))
|
||||
}
|
||||
|
||||
if !update.Version.Equal(version) {
|
||||
t.Errorf("Wrong version at %s", caller(1))
|
||||
}
|
||||
|
||||
if len(update.BlockIndexes) != len(blocks) {
|
||||
t.Errorf("Wrong indexes. Have %d expect %d at %s", len(update.BlockIndexes), len(blocks), caller(1))
|
||||
}
|
||||
for i := range update.BlockIndexes {
|
||||
if update.BlockIndexes[i] != blocks[i] {
|
||||
t.Errorf("Index %d incorrect at %s", i, caller(1))
|
||||
}
|
||||
}
|
||||
|
||||
if remove {
|
||||
fc.downloadProgressMessages = append(fc.downloadProgressMessages[:messageIdx], fc.downloadProgressMessages[messageIdx+1:]...)
|
||||
}
|
||||
}
|
||||
expectEmpty := func() {
|
||||
if len(fc.downloadProgressMessages) > 0 {
|
||||
t.Errorf("Still have something at %s: %#v", caller(1), fc.downloadProgressMessages)
|
||||
}
|
||||
}
|
||||
|
||||
now := time.Now()
|
||||
tick := func() time.Time {
|
||||
now = now.Add(time.Second)
|
||||
return now
|
||||
}
|
||||
|
||||
if len(fc.downloadProgressMessages) != 0 {
|
||||
t.Error("Expected no requests")
|
||||
}
|
||||
|
||||
v1 := (protocol.Vector{}).Update(0)
|
||||
v2 := (protocol.Vector{}).Update(1)
|
||||
|
||||
// Requires more than 10 blocks to work.
|
||||
blocks := make([]protocol.BlockInfo, 11, 11)
|
||||
|
||||
state1 := &sharedPullerState{
|
||||
folder: "folder",
|
||||
file: protocol.FileInfo{
|
||||
Name: "state1",
|
||||
Version: v1,
|
||||
Blocks: blocks,
|
||||
},
|
||||
mut: sync.NewRWMutex(),
|
||||
availableUpdated: time.Now(),
|
||||
}
|
||||
p.registry["1"] = state1
|
||||
|
||||
// Has no blocks, hence no message is sent
|
||||
p.sendDownloadProgressMessages()
|
||||
expectEmpty()
|
||||
|
||||
// Returns update for puller with new extra blocks
|
||||
state1.available = []int32{1}
|
||||
p.sendDownloadProgressMessages()
|
||||
|
||||
expect(0, state1, protocol.UpdateTypeAppend, v1, []int32{1}, true)
|
||||
expectEmpty()
|
||||
|
||||
// Does nothing if nothing changes
|
||||
p.sendDownloadProgressMessages()
|
||||
expectEmpty()
|
||||
|
||||
// Does nothing if timestamp updated, but no new blocks (should never happen)
|
||||
state1.availableUpdated = tick()
|
||||
|
||||
p.sendDownloadProgressMessages()
|
||||
expectEmpty()
|
||||
|
||||
// Does not return an update if date blocks change but date does not (should never happen)
|
||||
state1.available = []int32{1, 2}
|
||||
|
||||
p.sendDownloadProgressMessages()
|
||||
expectEmpty()
|
||||
|
||||
// If the date and blocks changes, returns only the diff
|
||||
state1.availableUpdated = tick()
|
||||
|
||||
p.sendDownloadProgressMessages()
|
||||
|
||||
expect(0, state1, protocol.UpdateTypeAppend, v1, []int32{2}, true)
|
||||
expectEmpty()
|
||||
|
||||
// Returns forget and update if puller version has changed
|
||||
state1.file.Version = v2
|
||||
|
||||
p.sendDownloadProgressMessages()
|
||||
|
||||
expect(0, state1, protocol.UpdateTypeForget, v1, nil, false)
|
||||
expect(1, state1, protocol.UpdateTypeAppend, v2, []int32{1, 2}, true)
|
||||
expectEmpty()
|
||||
|
||||
// Sends an empty update if new file exists, but does not have any blocks yet. (To indicate that the old blocks are no longer available)
|
||||
state1.file.Version = v1
|
||||
state1.available = nil
|
||||
state1.availableUpdated = tick()
|
||||
|
||||
p.sendDownloadProgressMessages()
|
||||
|
||||
expect(0, state1, protocol.UpdateTypeForget, v2, nil, false)
|
||||
expect(1, state1, protocol.UpdateTypeAppend, v1, nil, true)
|
||||
expectEmpty()
|
||||
|
||||
// Updates for multiple files and folders can be combined
|
||||
state1.available = []int32{1, 2, 3}
|
||||
state1.availableUpdated = tick()
|
||||
|
||||
state2 := &sharedPullerState{
|
||||
folder: "folder2",
|
||||
file: protocol.FileInfo{
|
||||
Name: "state2",
|
||||
Version: v1,
|
||||
Blocks: blocks,
|
||||
},
|
||||
mut: sync.NewRWMutex(),
|
||||
available: []int32{1, 2, 3},
|
||||
availableUpdated: time.Now(),
|
||||
}
|
||||
state3 := &sharedPullerState{
|
||||
folder: "folder",
|
||||
file: protocol.FileInfo{
|
||||
Name: "state3",
|
||||
Version: v1,
|
||||
Blocks: blocks,
|
||||
},
|
||||
mut: sync.NewRWMutex(),
|
||||
available: []int32{1, 2, 3},
|
||||
availableUpdated: time.Now(),
|
||||
}
|
||||
state4 := &sharedPullerState{
|
||||
folder: "folder2",
|
||||
file: protocol.FileInfo{
|
||||
Name: "state4",
|
||||
Version: v1,
|
||||
Blocks: blocks,
|
||||
},
|
||||
mut: sync.NewRWMutex(),
|
||||
available: []int32{1, 2, 3},
|
||||
availableUpdated: time.Now(),
|
||||
}
|
||||
p.registry["2"] = state2
|
||||
p.registry["3"] = state3
|
||||
p.registry["4"] = state4
|
||||
|
||||
p.sendDownloadProgressMessages()
|
||||
|
||||
expect(-1, state1, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, false)
|
||||
expect(-1, state3, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, true)
|
||||
expect(-1, state2, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, false)
|
||||
expect(-1, state4, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, true)
|
||||
expectEmpty()
|
||||
|
||||
// Returns forget if puller no longer exists, as well as updates if it has been updated.
|
||||
state1.available = []int32{1, 2, 3, 4, 5}
|
||||
state1.availableUpdated = tick()
|
||||
state2.available = []int32{1, 2, 3, 4, 5}
|
||||
state2.availableUpdated = tick()
|
||||
|
||||
delete(p.registry, "3")
|
||||
delete(p.registry, "4")
|
||||
|
||||
p.sendDownloadProgressMessages()
|
||||
|
||||
expect(-1, state1, protocol.UpdateTypeAppend, v1, []int32{4, 5}, false)
|
||||
expect(-1, state3, protocol.UpdateTypeForget, v1, nil, true)
|
||||
expect(-1, state2, protocol.UpdateTypeAppend, v1, []int32{4, 5}, false)
|
||||
expect(-1, state4, protocol.UpdateTypeForget, v1, nil, true)
|
||||
expectEmpty()
|
||||
|
||||
// Deletions are sent only once (actual bug I found writing the tests)
|
||||
p.sendDownloadProgressMessages()
|
||||
p.sendDownloadProgressMessages()
|
||||
expectEmpty()
|
||||
|
||||
// Not sent for "inactive" (symlinks, dirs, or wrong folder) pullers
|
||||
// Directory
|
||||
state5 := &sharedPullerState{
|
||||
folder: "folder",
|
||||
file: protocol.FileInfo{
|
||||
Name: "state5",
|
||||
Version: v1,
|
||||
Flags: protocol.FlagDirectory,
|
||||
Blocks: blocks,
|
||||
},
|
||||
mut: sync.NewRWMutex(),
|
||||
available: []int32{1, 2, 3},
|
||||
availableUpdated: time.Now(),
|
||||
}
|
||||
// Symlink
|
||||
state6 := &sharedPullerState{
|
||||
folder: "folder",
|
||||
file: protocol.FileInfo{
|
||||
Name: "state6",
|
||||
Version: v1,
|
||||
Flags: protocol.FlagSymlink,
|
||||
},
|
||||
mut: sync.NewRWMutex(),
|
||||
available: []int32{1, 2, 3},
|
||||
availableUpdated: time.Now(),
|
||||
}
|
||||
// Some other directory
|
||||
state7 := &sharedPullerState{
|
||||
folder: "folderXXX",
|
||||
file: protocol.FileInfo{
|
||||
Name: "state7",
|
||||
Version: v1,
|
||||
Blocks: blocks,
|
||||
},
|
||||
mut: sync.NewRWMutex(),
|
||||
available: []int32{1, 2, 3},
|
||||
availableUpdated: time.Now(),
|
||||
}
|
||||
// Less than 10 blocks
|
||||
state8 := &sharedPullerState{
|
||||
folder: "folder",
|
||||
file: protocol.FileInfo{
|
||||
Name: "state8",
|
||||
Version: v1,
|
||||
Blocks: blocks[:3],
|
||||
},
|
||||
mut: sync.NewRWMutex(),
|
||||
available: []int32{1, 2, 3},
|
||||
availableUpdated: time.Now(),
|
||||
}
|
||||
p.registry["5"] = state5
|
||||
p.registry["6"] = state6
|
||||
p.registry["7"] = state7
|
||||
p.registry["8"] = state8
|
||||
|
||||
p.sendDownloadProgressMessages()
|
||||
|
||||
expectEmpty()
|
||||
|
||||
// Device is no longer subscribed to a particular folder
|
||||
delete(p.registry, "1") // Clean up first
|
||||
delete(p.registry, "2") // Clean up first
|
||||
|
||||
p.sendDownloadProgressMessages()
|
||||
expect(-1, state1, protocol.UpdateTypeForget, v1, nil, true)
|
||||
expect(-1, state2, protocol.UpdateTypeForget, v1, nil, true)
|
||||
|
||||
expectEmpty()
|
||||
|
||||
p.registry["1"] = state1
|
||||
p.registry["2"] = state2
|
||||
p.registry["3"] = state3
|
||||
p.registry["4"] = state4
|
||||
|
||||
p.sendDownloadProgressMessages()
|
||||
|
||||
expect(-1, state1, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3, 4, 5}, false)
|
||||
expect(-1, state3, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, true)
|
||||
expect(-1, state2, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3, 4, 5}, false)
|
||||
expect(-1, state4, protocol.UpdateTypeAppend, v1, []int32{1, 2, 3}, true)
|
||||
expectEmpty()
|
||||
|
||||
p.temporaryIndexUnsubscribe(fc)
|
||||
p.temporaryIndexSubscribe(fc, []string{"folder"})
|
||||
|
||||
p.sendDownloadProgressMessages()
|
||||
|
||||
// See progressemitter.go for explanation why this is commented out.
|
||||
// Search for state.cleanup
|
||||
//expect(-1, state2, protocol.UpdateTypeForget, v1, nil, false)
|
||||
//expect(-1, state4, protocol.UpdateTypeForget, v1, nil, true)
|
||||
|
||||
expectEmpty()
|
||||
|
||||
// Cleanup when device no longer exists
|
||||
p.temporaryIndexUnsubscribe(fc)
|
||||
|
||||
p.sendDownloadProgressMessages()
|
||||
_, ok := p.sentDownloadStates[fc.ID()]
|
||||
if ok {
|
||||
t.Error("Should not be there")
|
||||
}
|
||||
}
|
||||
|
||||
+51
-30
@@ -970,9 +970,9 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
|
||||
scanner.PopulateOffsets(file.Blocks)
|
||||
|
||||
reused := 0
|
||||
var blocks []protocol.BlockInfo
|
||||
var blocksSize int64
|
||||
var reused []int32
|
||||
|
||||
// Check for an old temporary file which might have some blocks we could
|
||||
// reuse.
|
||||
@@ -988,25 +988,27 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
}
|
||||
|
||||
// Since the blocks are already there, we don't need to get them.
|
||||
for _, block := range file.Blocks {
|
||||
for i, block := range file.Blocks {
|
||||
_, ok := existingBlocks[block.String()]
|
||||
if !ok {
|
||||
blocks = append(blocks, block)
|
||||
blocksSize += int64(block.Size)
|
||||
} else {
|
||||
reused = append(reused, int32(i))
|
||||
}
|
||||
}
|
||||
|
||||
// The sharedpullerstate will know which flags to use when opening the
|
||||
// temp file depending if we are reusing any blocks or not.
|
||||
reused = len(file.Blocks) - len(blocks)
|
||||
if reused == 0 {
|
||||
if len(reused) == 0 {
|
||||
// Otherwise, discard the file ourselves in order for the
|
||||
// sharedpuller not to panic when it fails to exclusively create a
|
||||
// file which already exists
|
||||
osutil.InWritableDir(osutil.Remove, tempName)
|
||||
}
|
||||
} else {
|
||||
blocks = file.Blocks
|
||||
// Copy the blocks, as we don't want to shuffle them on the FileInfo
|
||||
blocks = append(blocks, file.Blocks...)
|
||||
blocksSize = file.Size()
|
||||
}
|
||||
|
||||
@@ -1018,6 +1020,12 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
}
|
||||
}
|
||||
|
||||
// Shuffle the blocks
|
||||
for i := range blocks {
|
||||
j := rand.Intn(i + 1)
|
||||
blocks[i], blocks[j] = blocks[j], blocks[i]
|
||||
}
|
||||
|
||||
events.Default.Log(events.ItemStarted, map[string]string{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
@@ -1026,17 +1034,20 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
})
|
||||
|
||||
s := sharedPullerState{
|
||||
file: file,
|
||||
folder: p.folder,
|
||||
tempName: tempName,
|
||||
realName: realName,
|
||||
copyTotal: len(blocks),
|
||||
copyNeeded: len(blocks),
|
||||
reused: reused,
|
||||
ignorePerms: p.ignorePermissions(file),
|
||||
version: curFile.Version,
|
||||
mut: sync.NewMutex(),
|
||||
sparse: p.allowSparse,
|
||||
file: file,
|
||||
folder: p.folder,
|
||||
tempName: tempName,
|
||||
realName: realName,
|
||||
copyTotal: len(blocks),
|
||||
copyNeeded: len(blocks),
|
||||
reused: len(reused),
|
||||
updated: time.Now(),
|
||||
available: reused,
|
||||
availableUpdated: time.Now(),
|
||||
ignorePerms: p.ignorePermissions(file),
|
||||
version: curFile.Version,
|
||||
mut: sync.NewRWMutex(),
|
||||
sparse: p.allowSparse,
|
||||
}
|
||||
|
||||
l.Debugf("%v need file %s; copy %d, reused %v", p, file.Name, len(blocks), reused)
|
||||
@@ -1184,7 +1195,7 @@ func (p *rwFolder) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pull
|
||||
}
|
||||
pullChan <- ps
|
||||
} else {
|
||||
state.copyDone()
|
||||
state.copyDone(block)
|
||||
}
|
||||
}
|
||||
out <- state.sharedPullerState
|
||||
@@ -1210,19 +1221,19 @@ func (p *rwFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *sharedPul
|
||||
if p.allowSparse && state.reused == 0 && state.block.IsEmpty() {
|
||||
// There is no need to request a block of all zeroes. Pretend we
|
||||
// requested it and handled it correctly.
|
||||
state.pullDone()
|
||||
state.pullDone(state.block)
|
||||
out <- state.sharedPullerState
|
||||
continue
|
||||
}
|
||||
|
||||
var lastError error
|
||||
potentialDevices := p.model.Availability(p.folder, state.file.Name)
|
||||
candidates := p.model.Availability(p.folder, state.file.Name, state.file.Version, state.block)
|
||||
for {
|
||||
// Select the least busy device to pull the block from. If we found no
|
||||
// feasible device at all, fail the block (and in the long run, the
|
||||
// file).
|
||||
selected := activity.leastBusy(potentialDevices)
|
||||
if selected == (protocol.DeviceID{}) {
|
||||
selected, found := activity.leastBusy(candidates)
|
||||
if !found {
|
||||
if lastError != nil {
|
||||
state.fail("pull", lastError)
|
||||
} else {
|
||||
@@ -1231,12 +1242,12 @@ func (p *rwFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *sharedPul
|
||||
break
|
||||
}
|
||||
|
||||
potentialDevices = removeDevice(potentialDevices, selected)
|
||||
candidates = removeAvailability(candidates, selected)
|
||||
|
||||
// Fetch the block, while marking the selected device as in use so that
|
||||
// leastBusy can select another device when someone else asks.
|
||||
activity.using(selected)
|
||||
buf, lastError := p.model.requestGlobal(selected, p.folder, state.file.Name, state.block.Offset, int(state.block.Size), state.block.Hash, 0, nil)
|
||||
buf, lastError := p.model.requestGlobal(selected.ID, p.folder, state.file.Name, state.block.Offset, int(state.block.Size), state.block.Hash, selected.FromTemporary)
|
||||
activity.done(selected)
|
||||
if lastError != nil {
|
||||
l.Debugln("request:", p.folder, state.file.Name, state.block.Offset, state.block.Size, "returned error:", lastError)
|
||||
@@ -1256,7 +1267,7 @@ func (p *rwFolder) pullerRoutine(in <-chan pullBlockState, out chan<- *sharedPul
|
||||
if err != nil {
|
||||
state.fail("save", err)
|
||||
} else {
|
||||
state.pullDone()
|
||||
state.pullDone(state.block)
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -1481,14 +1492,24 @@ func (p *rwFolder) inConflict(current, replacement protocol.Vector) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
func removeDevice(devices []protocol.DeviceID, device protocol.DeviceID) []protocol.DeviceID {
|
||||
for i := range devices {
|
||||
if devices[i] == device {
|
||||
devices[i] = devices[len(devices)-1]
|
||||
return devices[:len(devices)-1]
|
||||
func invalidateFolder(cfg *config.Configuration, folderID string, err error) {
|
||||
for i := range cfg.Folders {
|
||||
folder := &cfg.Folders[i]
|
||||
if folder.ID == folderID {
|
||||
folder.Invalid = err.Error()
|
||||
return
|
||||
}
|
||||
}
|
||||
return devices
|
||||
}
|
||||
|
||||
func removeAvailability(availabilities []Availability, availability Availability) []Availability {
|
||||
for i := range availabilities {
|
||||
if availabilities[i] == availability {
|
||||
availabilities[i] = availabilities[len(availabilities)-1]
|
||||
return availabilities[:len(availabilities)-1]
|
||||
}
|
||||
}
|
||||
return availabilities
|
||||
}
|
||||
|
||||
func (p *rwFolder) moveForConflict(name string) error {
|
||||
|
||||
+36
-12
@@ -104,9 +104,16 @@ func TestHandleFile(t *testing.T) {
|
||||
t.Errorf("Unexpected count of copy blocks: %d != 8", len(toCopy.blocks))
|
||||
}
|
||||
|
||||
for i, block := range toCopy.blocks {
|
||||
if string(block.Hash) != string(blocks[i+1].Hash) {
|
||||
t.Errorf("Block mismatch: %s != %s", block.String(), blocks[i+1].String())
|
||||
for _, block := range blocks[1:] {
|
||||
found := false
|
||||
for _, toCopyBlock := range toCopy.blocks {
|
||||
if string(toCopyBlock.Hash) == string(block.Hash) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Did not find block %s", block.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -138,9 +145,17 @@ func TestHandleFileWithTemp(t *testing.T) {
|
||||
t.Errorf("Unexpected count of copy blocks: %d != 4", len(toCopy.blocks))
|
||||
}
|
||||
|
||||
for i, eq := range []int{1, 5, 6, 8} {
|
||||
if string(toCopy.blocks[i].Hash) != string(blocks[eq].Hash) {
|
||||
t.Errorf("Block mismatch: %s != %s", toCopy.blocks[i].String(), blocks[eq].String())
|
||||
for _, idx := range []int{1, 5, 6, 8} {
|
||||
found := false
|
||||
block := blocks[idx]
|
||||
for _, toCopyBlock := range toCopy.blocks {
|
||||
if string(toCopyBlock.Hash) == string(block.Hash) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !found {
|
||||
t.Errorf("Did not find block %s", block.String())
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -187,13 +202,22 @@ func TestCopierFinder(t *testing.T) {
|
||||
default:
|
||||
}
|
||||
|
||||
// Verify that the right blocks went into the pull list
|
||||
for i, eq := range []int{1, 5, 6, 8} {
|
||||
if string(pulls[i].block.Hash) != string(blocks[eq].Hash) {
|
||||
t.Errorf("Block %d mismatch: %s != %s", eq, pulls[i].block.String(), blocks[eq].String())
|
||||
// Verify that the right blocks went into the pull list.
|
||||
// They are pulled in random order.
|
||||
for _, idx := range []int{1, 5, 6, 8} {
|
||||
found := false
|
||||
block := blocks[idx]
|
||||
for _, pulledBlock := range pulls {
|
||||
if string(pulledBlock.block.Hash) == string(block.Hash) {
|
||||
found = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if string(finish.file.Blocks[eq-1].Hash) != string(blocks[eq].Hash) {
|
||||
t.Errorf("Block %d mismatch: %s != %s", eq, finish.file.Blocks[eq-1].String(), blocks[eq].String())
|
||||
if !found {
|
||||
t.Errorf("Did not find block %s", block.String())
|
||||
}
|
||||
if string(finish.file.Blocks[idx-1].Hash) != string(blocks[idx].Hash) {
|
||||
t.Errorf("Block %d mismatch: %s != %s", idx, finish.file.Blocks[idx-1].String(), blocks[idx].String())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,184 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
// sentFolderFileDownloadState represents a state of what we've announced as available
|
||||
// to some remote device for a specific file.
|
||||
type sentFolderFileDownloadState struct {
|
||||
blockIndexes []int32
|
||||
version protocol.Vector
|
||||
updated time.Time
|
||||
}
|
||||
|
||||
// sentFolderDownloadState represents a state of what we've announced as available
|
||||
// to some remote device for a specific folder.
|
||||
type sentFolderDownloadState struct {
|
||||
files map[string]*sentFolderFileDownloadState
|
||||
}
|
||||
|
||||
// update takes a set of currently active sharedPullerStates, and returns a list
|
||||
// of updates which we need to send to the client to become up to date.
|
||||
func (s *sentFolderDownloadState) update(pullers []*sharedPullerState) []protocol.FileDownloadProgressUpdate {
|
||||
var name string
|
||||
var updates []protocol.FileDownloadProgressUpdate
|
||||
seen := make(map[string]struct{}, len(pullers))
|
||||
|
||||
for _, puller := range pullers {
|
||||
name = puller.file.Name
|
||||
|
||||
seen[name] = struct{}{}
|
||||
|
||||
pullerBlockIndexes := puller.Available()
|
||||
pullerVersion := puller.file.Version
|
||||
pullerBlockIndexesUpdated := puller.AvailableUpdated()
|
||||
|
||||
localFile, ok := s.files[name]
|
||||
|
||||
// New file we haven't seen before
|
||||
if !ok {
|
||||
// Only send an update if the file actually has some blocks.
|
||||
if len(pullerBlockIndexes) > 0 {
|
||||
s.files[name] = &sentFolderFileDownloadState{
|
||||
blockIndexes: pullerBlockIndexes,
|
||||
updated: pullerBlockIndexesUpdated,
|
||||
version: pullerVersion,
|
||||
}
|
||||
|
||||
updates = append(updates, protocol.FileDownloadProgressUpdate{
|
||||
Name: name,
|
||||
Version: pullerVersion,
|
||||
UpdateType: protocol.UpdateTypeAppend,
|
||||
BlockIndexes: pullerBlockIndexes,
|
||||
})
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Existing file we've already sent an update for.
|
||||
if pullerBlockIndexesUpdated.Equal(localFile.updated) && pullerVersion.Equal(localFile.version) {
|
||||
// The file state hasn't changed, go to next.
|
||||
continue
|
||||
}
|
||||
|
||||
if !pullerVersion.Equal(localFile.version) {
|
||||
// The version has changed, clean up whatever we had for the old
|
||||
// file, and advertise the new file.
|
||||
updates = append(updates, protocol.FileDownloadProgressUpdate{
|
||||
Name: name,
|
||||
Version: localFile.version,
|
||||
UpdateType: protocol.UpdateTypeForget,
|
||||
})
|
||||
updates = append(updates, protocol.FileDownloadProgressUpdate{
|
||||
Name: name,
|
||||
Version: pullerVersion,
|
||||
UpdateType: protocol.UpdateTypeAppend,
|
||||
BlockIndexes: pullerBlockIndexes,
|
||||
})
|
||||
localFile.blockIndexes = pullerBlockIndexes
|
||||
localFile.updated = pullerBlockIndexesUpdated
|
||||
localFile.version = pullerVersion
|
||||
continue
|
||||
}
|
||||
|
||||
// Relies on the fact that sharedPullerState.Available() should always
|
||||
// append.
|
||||
newBlocks := pullerBlockIndexes[len(localFile.blockIndexes):]
|
||||
|
||||
localFile.blockIndexes = append(localFile.blockIndexes, newBlocks...)
|
||||
localFile.updated = pullerBlockIndexesUpdated
|
||||
|
||||
// If there are new blocks, send the update.
|
||||
if len(newBlocks) > 0 {
|
||||
updates = append(updates, protocol.FileDownloadProgressUpdate{
|
||||
Name: name,
|
||||
Version: localFile.version,
|
||||
UpdateType: protocol.UpdateTypeAppend,
|
||||
BlockIndexes: newBlocks,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// For each file that we are tracking, see if there still is a puller for it
|
||||
// if not, the file completed or errored out.
|
||||
for name, info := range s.files {
|
||||
_, ok := seen[name]
|
||||
if !ok {
|
||||
updates = append(updates, protocol.FileDownloadProgressUpdate{
|
||||
Name: name,
|
||||
Version: info.version,
|
||||
UpdateType: protocol.UpdateTypeForget,
|
||||
})
|
||||
delete(s.files, name)
|
||||
}
|
||||
}
|
||||
|
||||
return updates
|
||||
}
|
||||
|
||||
// destroy removes all stored state, and returns a set of updates we need to
|
||||
// dispatch to clean up the state on the remote end.
|
||||
func (s *sentFolderDownloadState) destroy() []protocol.FileDownloadProgressUpdate {
|
||||
updates := make([]protocol.FileDownloadProgressUpdate, 0, len(s.files))
|
||||
for name, info := range s.files {
|
||||
updates = append(updates, protocol.FileDownloadProgressUpdate{
|
||||
Name: name,
|
||||
Version: info.version,
|
||||
UpdateType: protocol.UpdateTypeForget,
|
||||
})
|
||||
delete(s.files, name)
|
||||
}
|
||||
return updates
|
||||
}
|
||||
|
||||
// sentDownloadState represents a state of what we've announced as available
|
||||
// to some remote device. It is used from within the progress emitter
|
||||
// which only has one routine, hence is deemed threadsafe.
|
||||
type sentDownloadState struct {
|
||||
folderStates map[string]*sentFolderDownloadState
|
||||
}
|
||||
|
||||
// update receives a folder, and a slice of pullers that are currently available
|
||||
// for the given folder, and according to the state of what we've seen before
|
||||
// returns a set of updates which we should send to the remote device to make
|
||||
// it aware of everything that we currently have available.
|
||||
func (s *sentDownloadState) update(folder string, pullers []*sharedPullerState) []protocol.FileDownloadProgressUpdate {
|
||||
fs, ok := s.folderStates[folder]
|
||||
if !ok {
|
||||
fs = &sentFolderDownloadState{
|
||||
files: make(map[string]*sentFolderFileDownloadState),
|
||||
}
|
||||
s.folderStates[folder] = fs
|
||||
}
|
||||
return fs.update(pullers)
|
||||
}
|
||||
|
||||
// folders returns a set of folders this state is currently aware off.
|
||||
func (s *sentDownloadState) folders() []string {
|
||||
folders := make([]string, 0, len(s.folderStates))
|
||||
for key := range s.folderStates {
|
||||
folders = append(folders, key)
|
||||
}
|
||||
return folders
|
||||
}
|
||||
|
||||
// cleanup cleans up all state related to a folder, and returns a set of updates
|
||||
// which would clean up the state on the remote device.
|
||||
func (s *sentDownloadState) cleanup(folder string) []protocol.FileDownloadProgressUpdate {
|
||||
fs, ok := s.folderStates[folder]
|
||||
if ok {
|
||||
updates := fs.destroy()
|
||||
delete(s.folderStates, folder)
|
||||
return updates
|
||||
}
|
||||
return nil
|
||||
}
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
@@ -30,15 +31,18 @@ type sharedPullerState struct {
|
||||
sparse bool
|
||||
|
||||
// Mutable, must be locked for access
|
||||
err error // The first error we hit
|
||||
fd *os.File // The fd of the temp file
|
||||
copyTotal int // Total number of copy actions for the whole job
|
||||
pullTotal int // Total number of pull actions for the whole job
|
||||
copyOrigin int // Number of blocks copied from the original file
|
||||
copyNeeded int // Number of copy actions still pending
|
||||
pullNeeded int // Number of block pulls still pending
|
||||
closed bool // True if the file has been finalClosed.
|
||||
mut sync.Mutex // Protects the above
|
||||
err error // The first error we hit
|
||||
fd *os.File // The fd of the temp file
|
||||
copyTotal int // Total number of copy actions for the whole job
|
||||
pullTotal int // Total number of pull actions for the whole job
|
||||
copyOrigin int // Number of blocks copied from the original file
|
||||
copyNeeded int // Number of copy actions still pending
|
||||
pullNeeded int // Number of block pulls still pending
|
||||
updated time.Time // Time when any of the counters above were last updated
|
||||
closed bool // True if the file has been finalClosed.
|
||||
available []int32 // Indexes of the blocks that are available in the temporary file
|
||||
availableUpdated time.Time // Time when list of available blocks was last updated
|
||||
mut sync.RWMutex // Protects the above
|
||||
}
|
||||
|
||||
// A momentary state representing the progress of the puller
|
||||
@@ -56,7 +60,7 @@ type pullerProgress struct {
|
||||
// A lockedWriterAt synchronizes WriteAt calls with an external mutex.
|
||||
// WriteAt() is goroutine safe by itself, but not against for example Close().
|
||||
type lockedWriterAt struct {
|
||||
mut *sync.Mutex
|
||||
mut *sync.RWMutex
|
||||
wr io.WriterAt
|
||||
}
|
||||
|
||||
@@ -196,15 +200,19 @@ func (s *sharedPullerState) failLocked(context string, err error) {
|
||||
}
|
||||
|
||||
func (s *sharedPullerState) failed() error {
|
||||
s.mut.Lock()
|
||||
defer s.mut.Unlock()
|
||||
s.mut.RLock()
|
||||
err := s.err
|
||||
s.mut.RUnlock()
|
||||
|
||||
return s.err
|
||||
return err
|
||||
}
|
||||
|
||||
func (s *sharedPullerState) copyDone() {
|
||||
func (s *sharedPullerState) copyDone(block protocol.BlockInfo) {
|
||||
s.mut.Lock()
|
||||
s.copyNeeded--
|
||||
s.updated = time.Now()
|
||||
s.available = append(s.available, int32(block.Offset/protocol.BlockSize))
|
||||
s.availableUpdated = time.Now()
|
||||
l.Debugln("sharedPullerState", s.folder, s.file.Name, "copyNeeded ->", s.copyNeeded)
|
||||
s.mut.Unlock()
|
||||
}
|
||||
@@ -212,6 +220,7 @@ func (s *sharedPullerState) copyDone() {
|
||||
func (s *sharedPullerState) copiedFromOrigin() {
|
||||
s.mut.Lock()
|
||||
s.copyOrigin++
|
||||
s.updated = time.Now()
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
@@ -221,13 +230,17 @@ func (s *sharedPullerState) pullStarted() {
|
||||
s.copyNeeded--
|
||||
s.pullTotal++
|
||||
s.pullNeeded++
|
||||
s.updated = time.Now()
|
||||
l.Debugln("sharedPullerState", s.folder, s.file.Name, "pullNeeded start ->", s.pullNeeded)
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
func (s *sharedPullerState) pullDone() {
|
||||
func (s *sharedPullerState) pullDone(block protocol.BlockInfo) {
|
||||
s.mut.Lock()
|
||||
s.pullNeeded--
|
||||
s.updated = time.Now()
|
||||
s.available = append(s.available, int32(block.Offset/protocol.BlockSize))
|
||||
s.availableUpdated = time.Now()
|
||||
l.Debugln("sharedPullerState", s.folder, s.file.Name, "pullNeeded done ->", s.pullNeeded)
|
||||
s.mut.Unlock()
|
||||
}
|
||||
@@ -265,10 +278,10 @@ func (s *sharedPullerState) finalClose() (bool, error) {
|
||||
return true, s.err
|
||||
}
|
||||
|
||||
// Returns the momentarily progress for the puller
|
||||
// Progress returns the momentarily progress for the puller
|
||||
func (s *sharedPullerState) Progress() *pullerProgress {
|
||||
s.mut.Lock()
|
||||
defer s.mut.Unlock()
|
||||
s.mut.RLock()
|
||||
defer s.mut.RUnlock()
|
||||
total := s.reused + s.copyTotal + s.pullTotal
|
||||
done := total - s.copyNeeded - s.pullNeeded
|
||||
return &pullerProgress{
|
||||
@@ -282,3 +295,27 @@ func (s *sharedPullerState) Progress() *pullerProgress {
|
||||
BytesDone: db.BlocksToSize(done),
|
||||
}
|
||||
}
|
||||
|
||||
// Updated returns the time when any of the progress related counters was last updated.
|
||||
func (s *sharedPullerState) Updated() time.Time {
|
||||
s.mut.RLock()
|
||||
t := s.updated
|
||||
s.mut.RUnlock()
|
||||
return t
|
||||
}
|
||||
|
||||
// AvailableUpdated returns the time last time list of available blocks was updated
|
||||
func (s *sharedPullerState) AvailableUpdated() time.Time {
|
||||
s.mut.RLock()
|
||||
t := s.availableUpdated
|
||||
s.mut.RUnlock()
|
||||
return t
|
||||
}
|
||||
|
||||
// Available returns blocks available in the current temporary file
|
||||
func (s *sharedPullerState) Available() []int32 {
|
||||
s.mut.RLock()
|
||||
blocks := s.available
|
||||
s.mut.RUnlock()
|
||||
return blocks
|
||||
}
|
||||
|
||||
@@ -16,7 +16,7 @@ import (
|
||||
func TestSourceFileOK(t *testing.T) {
|
||||
s := sharedPullerState{
|
||||
realName: "testdata/foo",
|
||||
mut: sync.NewMutex(),
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
|
||||
fd, err := s.sourceFile()
|
||||
@@ -45,7 +45,7 @@ func TestSourceFileOK(t *testing.T) {
|
||||
func TestSourceFileBad(t *testing.T) {
|
||||
s := sharedPullerState{
|
||||
realName: "nonexistent",
|
||||
mut: sync.NewMutex(),
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
|
||||
fd, err := s.sourceFile()
|
||||
@@ -71,7 +71,7 @@ func TestReadOnlyDir(t *testing.T) {
|
||||
|
||||
s := sharedPullerState{
|
||||
tempName: "testdata/read_only_dir/.temp_name",
|
||||
mut: sync.NewMutex(),
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
|
||||
fd, err := s.tempFile()
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package nat
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
l = logger.DefaultLogger.NewFacility("nat", "NAT discovery and port mapping")
|
||||
)
|
||||
|
||||
func init() {
|
||||
l.SetDebug("nat", strings.Contains(os.Getenv("STTRACE"), "nat") || os.Getenv("STTRACE") == "all")
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package nat
|
||||
|
||||
import (
|
||||
"net"
|
||||
"time"
|
||||
)
|
||||
|
||||
type Protocol string
|
||||
|
||||
const (
|
||||
TCP Protocol = "TCP"
|
||||
UDP = "UDP"
|
||||
)
|
||||
|
||||
type Device interface {
|
||||
ID() string
|
||||
GetLocalIPAddress() net.IP
|
||||
AddPortMapping(protocol Protocol, internalPort, externalPort int, description string, duration time.Duration) (int, error)
|
||||
GetExternalIPAddress() (net.IP, error)
|
||||
}
|
||||
@@ -0,0 +1,53 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package nat
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
type DiscoverFunc func(renewal, timeout time.Duration) []Device
|
||||
|
||||
var providers []DiscoverFunc
|
||||
|
||||
func Register(provider DiscoverFunc) {
|
||||
providers = append(providers, provider)
|
||||
}
|
||||
|
||||
func discoverAll(renewal, timeout time.Duration) map[string]Device {
|
||||
wg := sync.NewWaitGroup()
|
||||
wg.Add(len(providers))
|
||||
|
||||
c := make(chan Device)
|
||||
done := make(chan struct{})
|
||||
|
||||
for _, discoverFunc := range providers {
|
||||
go func(f DiscoverFunc) {
|
||||
for _, dev := range f(renewal, timeout) {
|
||||
c <- dev
|
||||
}
|
||||
wg.Done()
|
||||
}(discoverFunc)
|
||||
}
|
||||
|
||||
nats := make(map[string]Device)
|
||||
|
||||
go func() {
|
||||
for dev := range c {
|
||||
nats[dev.ID()] = dev
|
||||
}
|
||||
close(done)
|
||||
}()
|
||||
|
||||
wg.Wait()
|
||||
close(c)
|
||||
<-done
|
||||
|
||||
return nats
|
||||
}
|
||||
@@ -0,0 +1,298 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package nat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"math/rand"
|
||||
"net"
|
||||
stdsync "sync"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
// Service runs a loop for discovery of IGDs (Internet Gateway Devices) and
|
||||
// setup/renewal of a port mapping.
|
||||
type Service struct {
|
||||
id protocol.DeviceID
|
||||
cfg *config.Wrapper
|
||||
stop chan struct{}
|
||||
immediate chan chan struct{}
|
||||
timer *time.Timer
|
||||
announce *stdsync.Once
|
||||
|
||||
mappings []*Mapping
|
||||
mut sync.RWMutex
|
||||
}
|
||||
|
||||
func NewService(id protocol.DeviceID, cfg *config.Wrapper) *Service {
|
||||
return &Service{
|
||||
id: id,
|
||||
cfg: cfg,
|
||||
|
||||
immediate: make(chan chan struct{}),
|
||||
timer: time.NewTimer(time.Second),
|
||||
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Serve() {
|
||||
s.timer.Reset(0)
|
||||
s.stop = make(chan struct{})
|
||||
s.announce = &stdsync.Once{}
|
||||
|
||||
for {
|
||||
select {
|
||||
case result := <-s.immediate:
|
||||
s.process()
|
||||
close(result)
|
||||
case <-s.timer.C:
|
||||
s.process()
|
||||
case <-s.stop:
|
||||
s.timer.Stop()
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) process() {
|
||||
// toRenew are mappings which are due for renewal
|
||||
// toUpdate are the remaining mappings, which will only be updated if one of
|
||||
// the old IGDs has gone away, or a new IGD has appeared, but only if we
|
||||
// actually need to perform a renewal.
|
||||
var toRenew, toUpdate []*Mapping
|
||||
|
||||
renewIn := time.Duration(s.cfg.Options().NATRenewalM) * time.Minute
|
||||
if renewIn == 0 {
|
||||
// We always want to do renewal so lets just pick a nice sane number.
|
||||
renewIn = 30 * time.Minute
|
||||
}
|
||||
|
||||
s.mut.RLock()
|
||||
for _, mapping := range s.mappings {
|
||||
if mapping.expires.Before(time.Now()) {
|
||||
toRenew = append(toRenew, mapping)
|
||||
} else {
|
||||
toUpdate = append(toUpdate, mapping)
|
||||
mappingRenewIn := mapping.expires.Sub(time.Now())
|
||||
if mappingRenewIn < renewIn {
|
||||
renewIn = mappingRenewIn
|
||||
}
|
||||
}
|
||||
}
|
||||
s.mut.RUnlock()
|
||||
|
||||
s.timer.Reset(renewIn)
|
||||
|
||||
// Don't do anything, unless we really need to renew
|
||||
if len(toRenew) == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
nats := discoverAll(time.Duration(s.cfg.Options().NATRenewalM)*time.Minute, time.Duration(s.cfg.Options().NATTimeoutS)*time.Second)
|
||||
|
||||
s.announce.Do(func() {
|
||||
suffix := "s"
|
||||
if len(nats) == 1 {
|
||||
suffix = ""
|
||||
}
|
||||
l.Infoln("Detected", len(nats), "NAT device"+suffix)
|
||||
})
|
||||
|
||||
for _, mapping := range toRenew {
|
||||
s.updateMapping(mapping, nats, true)
|
||||
}
|
||||
|
||||
for _, mapping := range toUpdate {
|
||||
s.updateMapping(mapping, nats, false)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Stop() {
|
||||
close(s.stop)
|
||||
}
|
||||
|
||||
func (s *Service) NewMapping(protocol Protocol, ip net.IP, port int) *Mapping {
|
||||
mapping := &Mapping{
|
||||
protocol: protocol,
|
||||
address: Address{
|
||||
IP: ip,
|
||||
Port: port,
|
||||
},
|
||||
extAddresses: make(map[string]Address),
|
||||
mut: sync.NewRWMutex(),
|
||||
}
|
||||
|
||||
s.mut.Lock()
|
||||
s.mappings = append(s.mappings, mapping)
|
||||
s.mut.Unlock()
|
||||
|
||||
return mapping
|
||||
}
|
||||
|
||||
// Sync forces the service to recheck all mappings.
|
||||
func (s *Service) Sync() {
|
||||
wait := make(chan struct{})
|
||||
s.immediate <- wait
|
||||
<-wait
|
||||
}
|
||||
|
||||
// updateMapping compares the addresses of the existing mapping versus the natds
|
||||
// discovered, and removes any addresses of natds that do not exist, or tries to
|
||||
// acquire mappings for natds which the mapping was unaware of before.
|
||||
// Optionally takes renew flag which indicates whether or not we should renew
|
||||
// mappings with existing natds
|
||||
func (s *Service) updateMapping(mapping *Mapping, nats map[string]Device, renew bool) {
|
||||
var added, removed []Address
|
||||
|
||||
renewalTime := time.Duration(s.cfg.Options().NATRenewalM) * time.Minute
|
||||
mapping.expires = time.Now().Add(renewalTime)
|
||||
|
||||
newAdded, newRemoved := s.verifyExistingMappings(mapping, nats, renew)
|
||||
added = append(added, newAdded...)
|
||||
removed = append(removed, newRemoved...)
|
||||
|
||||
newAdded, newRemoved = s.acquireNewMappings(mapping, nats)
|
||||
added = append(added, newAdded...)
|
||||
removed = append(removed, newRemoved...)
|
||||
|
||||
if len(added) > 0 || len(removed) > 0 {
|
||||
mapping.notify(added, removed)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) verifyExistingMappings(mapping *Mapping, nats map[string]Device, renew bool) ([]Address, []Address) {
|
||||
var added, removed []Address
|
||||
|
||||
leaseTime := time.Duration(s.cfg.Options().NATLeaseM) * time.Minute
|
||||
|
||||
for id, address := range mapping.addressMap() {
|
||||
// Delete addresses for NATDevice's that do not exist anymore
|
||||
nat, ok := nats[id]
|
||||
if !ok {
|
||||
mapping.removeAddress(id)
|
||||
removed = append(removed, address)
|
||||
continue
|
||||
} else if renew {
|
||||
// Only perform renewals on the nat's that have the right local IP
|
||||
// address
|
||||
localIP := nat.GetLocalIPAddress()
|
||||
if !mapping.validGateway(localIP) {
|
||||
l.Debugf("Skipping %s for %s because of IP mismatch. %s != %s", id, mapping, mapping.address.IP, localIP)
|
||||
continue
|
||||
}
|
||||
|
||||
l.Debugf("Renewing %s -> %s mapping on %s", mapping, address, id)
|
||||
|
||||
addr, err := s.tryNATDevice(nat, mapping.address.Port, address.Port, leaseTime)
|
||||
if err != nil {
|
||||
l.Debugf("Failed to renew %s -> mapping on %s", mapping, address, id)
|
||||
mapping.removeAddress(id)
|
||||
removed = append(removed, address)
|
||||
continue
|
||||
}
|
||||
|
||||
l.Debugf("Renewed %s -> %s mapping on %s", mapping, address, id)
|
||||
|
||||
if !addr.Equal(address) {
|
||||
mapping.removeAddress(id)
|
||||
mapping.setAddress(id, addr)
|
||||
removed = append(removed, address)
|
||||
added = append(added, address)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return added, removed
|
||||
}
|
||||
|
||||
func (s *Service) acquireNewMappings(mapping *Mapping, nats map[string]Device) ([]Address, []Address) {
|
||||
var added, removed []Address
|
||||
|
||||
leaseTime := time.Duration(s.cfg.Options().NATLeaseM) * time.Minute
|
||||
addrMap := mapping.addressMap()
|
||||
|
||||
for id, nat := range nats {
|
||||
if _, ok := addrMap[id]; ok {
|
||||
continue
|
||||
}
|
||||
|
||||
// Only perform mappings on the nat's that have the right local IP
|
||||
// address
|
||||
localIP := nat.GetLocalIPAddress()
|
||||
if !mapping.validGateway(localIP) {
|
||||
l.Debugf("Skipping %s for %s because of IP mismatch. %s != %s", id, mapping, mapping.address.IP, localIP)
|
||||
continue
|
||||
}
|
||||
|
||||
l.Debugf("Acquiring %s mapping on %s", mapping, id)
|
||||
|
||||
addr, err := s.tryNATDevice(nat, mapping.address.Port, 0, leaseTime)
|
||||
if err != nil {
|
||||
l.Debugf("Failed to acquire %s mapping on %s", mapping, id)
|
||||
continue
|
||||
}
|
||||
|
||||
l.Debugf("Acquired %s -> %s mapping on %s", mapping, addr, id)
|
||||
|
||||
mapping.setAddress(id, addr)
|
||||
added = append(added, addr)
|
||||
}
|
||||
|
||||
return added, removed
|
||||
}
|
||||
|
||||
// tryNATDevice tries to acquire a port mapping for the given internal address to
|
||||
// the given external port. If external port is 0, picks a pseudo-random port.
|
||||
func (s *Service) tryNATDevice(natd Device, intPort, extPort int, leaseTime time.Duration) (Address, error) {
|
||||
var err error
|
||||
|
||||
// Generate a predictable random which is based on device ID + local port
|
||||
// number so that the ports we'd try to acquire for the mapping would always
|
||||
// be the same.
|
||||
predictableRand := rand.New(rand.NewSource(int64(s.id.Short()) + int64(intPort)))
|
||||
|
||||
if extPort != 0 {
|
||||
// First try renewing our existing mapping, if we have one.
|
||||
name := fmt.Sprintf("syncthing-%d", extPort)
|
||||
port, err := natd.AddPortMapping(TCP, intPort, extPort, name, leaseTime)
|
||||
if err == nil {
|
||||
extPort = port
|
||||
goto findIP
|
||||
}
|
||||
l.Debugln("Error extending lease on", natd.ID(), err)
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
// Then try up to ten random ports.
|
||||
extPort = 1024 + predictableRand.Intn(65535-1024)
|
||||
name := fmt.Sprintf("syncthing-%d", extPort)
|
||||
port, err := natd.AddPortMapping(TCP, intPort, extPort, name, leaseTime)
|
||||
if err == nil {
|
||||
extPort = port
|
||||
goto findIP
|
||||
}
|
||||
l.Debugln("Error getting new lease on", natd.ID(), err)
|
||||
}
|
||||
|
||||
return Address{}, err
|
||||
|
||||
findIP:
|
||||
ip, err := natd.GetExternalIPAddress()
|
||||
if err != nil {
|
||||
l.Debugln("Error getting external ip on", natd.ID(), err)
|
||||
ip = nil
|
||||
}
|
||||
return Address{
|
||||
IP: ip,
|
||||
Port: extPort,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,129 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package nat
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
type MappingChangeSubscriber func(*Mapping, []Address, []Address)
|
||||
|
||||
type Mapping struct {
|
||||
protocol Protocol
|
||||
address Address
|
||||
|
||||
extAddresses map[string]Address // NAT ID -> Address
|
||||
expires time.Time
|
||||
subscribers []MappingChangeSubscriber
|
||||
mut sync.RWMutex
|
||||
}
|
||||
|
||||
func (m *Mapping) setAddress(id string, address Address) {
|
||||
m.mut.Lock()
|
||||
if existing, ok := m.extAddresses[id]; !ok || !existing.Equal(address) {
|
||||
l.Infof("New NAT port mapping: external %s address %s to local address %s.", m.protocol, address, m.address)
|
||||
m.extAddresses[id] = address
|
||||
}
|
||||
m.mut.Unlock()
|
||||
}
|
||||
|
||||
func (m *Mapping) removeAddress(id string) {
|
||||
m.mut.Lock()
|
||||
addr, ok := m.extAddresses[id]
|
||||
if ok {
|
||||
l.Infof("Removing NAT port mapping: external %s address %s, NAT %s is no longer available.", m.protocol, addr, id)
|
||||
delete(m.extAddresses, id)
|
||||
}
|
||||
m.mut.Unlock()
|
||||
}
|
||||
|
||||
func (m *Mapping) notify(added, removed []Address) {
|
||||
m.mut.RLock()
|
||||
for _, subscriber := range m.subscribers {
|
||||
subscriber(m, added, removed)
|
||||
}
|
||||
m.mut.RUnlock()
|
||||
}
|
||||
|
||||
func (m *Mapping) addressMap() map[string]Address {
|
||||
m.mut.RLock()
|
||||
addrMap := m.extAddresses
|
||||
m.mut.RUnlock()
|
||||
return addrMap
|
||||
}
|
||||
|
||||
func (m *Mapping) Protocol() Protocol {
|
||||
return m.protocol
|
||||
}
|
||||
|
||||
func (m *Mapping) Address() Address {
|
||||
return m.address
|
||||
}
|
||||
|
||||
func (m *Mapping) ExternalAddresses() []Address {
|
||||
m.mut.RLock()
|
||||
addrs := make([]Address, 0, len(m.extAddresses))
|
||||
for _, addr := range m.extAddresses {
|
||||
addrs = append(addrs, addr)
|
||||
}
|
||||
m.mut.RUnlock()
|
||||
return addrs
|
||||
}
|
||||
|
||||
func (m *Mapping) OnChanged(subscribed MappingChangeSubscriber) {
|
||||
m.mut.Lock()
|
||||
m.subscribers = append(m.subscribers, subscribed)
|
||||
m.mut.Unlock()
|
||||
}
|
||||
|
||||
func (m *Mapping) String() string {
|
||||
return fmt.Sprintf("%s %s", m.protocol, m.address)
|
||||
}
|
||||
|
||||
func (m *Mapping) GoString() string {
|
||||
return m.String()
|
||||
}
|
||||
|
||||
// Checks if the mappings local IP address matches the IP address of the gateway
|
||||
// For example, if we are explicitly listening on 192.168.0.12, there is no
|
||||
// point trying to acquire a mapping on a gateway to which the local IP is
|
||||
// 10.0.0.1. Fallback to true if any of the IPs is not there.
|
||||
func (m *Mapping) validGateway(ip net.IP) bool {
|
||||
if m.address.IP == nil || ip == nil || m.address.IP.IsUnspecified() || ip.IsUnspecified() {
|
||||
return true
|
||||
}
|
||||
return m.address.IP.Equal(ip)
|
||||
}
|
||||
|
||||
// Address is essentially net.TCPAddr yet is more general, and has a few helper
|
||||
// methods which reduce boilerplate code.
|
||||
type Address struct {
|
||||
IP net.IP
|
||||
Port int
|
||||
}
|
||||
|
||||
func (a Address) Equal(b Address) bool {
|
||||
return a.Port == b.Port && a.IP.Equal(b.IP)
|
||||
}
|
||||
|
||||
func (a Address) String() string {
|
||||
var ipStr string
|
||||
if a.IP == nil {
|
||||
ipStr = net.IPv4zero.String()
|
||||
} else {
|
||||
ipStr = a.IP.String()
|
||||
}
|
||||
return net.JoinHostPort(ipStr, fmt.Sprintf("%d", a.Port))
|
||||
}
|
||||
|
||||
func (a Address) GoString() string {
|
||||
return a.String()
|
||||
}
|
||||
@@ -0,0 +1,54 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package nat
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestMappingValidGateway(t *testing.T) {
|
||||
a := net.ParseIP("10.0.0.1")
|
||||
b := net.ParseIP("192.168.0.1")
|
||||
tests := []struct {
|
||||
mappingLocalIP net.IP
|
||||
gatewayLocalIP net.IP
|
||||
expected bool
|
||||
}{
|
||||
// Any of the IPs is nil or unspecified implies correct
|
||||
{nil, nil, true},
|
||||
{net.IPv4zero, net.IPv4zero, true},
|
||||
{nil, net.IPv4zero, true},
|
||||
{net.IPv4zero, nil, true},
|
||||
{a, nil, true},
|
||||
{b, nil, true},
|
||||
{a, net.IPv4zero, true},
|
||||
{b, net.IPv4zero, true},
|
||||
{nil, a, true},
|
||||
{nil, b, true},
|
||||
{net.IPv4zero, a, true},
|
||||
{net.IPv4zero, b, true},
|
||||
// IPs are the same implies correct
|
||||
{a, a, true},
|
||||
{b, b, true},
|
||||
// IPs are specified and different, implies incorrect
|
||||
{a, b, false},
|
||||
{b, a, false},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
m := Mapping{
|
||||
address: Address{
|
||||
IP: test.mappingLocalIP,
|
||||
},
|
||||
}
|
||||
result := m.validGateway(test.gatewayLocalIP)
|
||||
if result != test.expected {
|
||||
t.Errorf("Incorrect: local %s gateway %s result %t expected %t", test.mappingLocalIP, test.gatewayLocalIP, result, test.expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,22 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package pmp
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/logger"
|
||||
)
|
||||
|
||||
var (
|
||||
l = logger.DefaultLogger.NewFacility("pmp", "NAT-PMP discovery and port mapping")
|
||||
)
|
||||
|
||||
func init() {
|
||||
l.SetDebug("pmp", strings.Contains(os.Getenv("STTRACE"), "pmp") || os.Getenv("STTRACE") == "all")
|
||||
}
|
||||
+105
@@ -0,0 +1,105 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package pmp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/AudriusButkevicius/go-nat-pmp"
|
||||
"github.com/jackpal/gateway"
|
||||
"github.com/syncthing/syncthing/lib/nat"
|
||||
)
|
||||
|
||||
func init() {
|
||||
nat.Register(Discover)
|
||||
}
|
||||
|
||||
func Discover(renewal, timeout time.Duration) []nat.Device {
|
||||
ip, err := gateway.DiscoverGateway()
|
||||
if err != nil {
|
||||
l.Debugln("Failed to discover gateway", err)
|
||||
return nil
|
||||
}
|
||||
|
||||
l.Debugln("Discovered gateway at", ip)
|
||||
|
||||
c := natpmp.NewClient(ip, timeout)
|
||||
// Try contacting the gateway, if it does not respond, assume it does not
|
||||
// speak NAT-PMP.
|
||||
_, err = c.GetExternalAddress()
|
||||
if err != nil && strings.Contains(err.Error(), "Timed out") {
|
||||
l.Debugln("Timeout trying to get external address, assume no NAT-PMP available")
|
||||
return nil
|
||||
}
|
||||
|
||||
var localIP net.IP
|
||||
// Port comes from the natpmp package
|
||||
conn, err := net.DialTimeout("udp", net.JoinHostPort(ip.String(), "5351"), timeout)
|
||||
if err == nil {
|
||||
conn.Close()
|
||||
localIPAddress, _, err := net.SplitHostPort(conn.LocalAddr().String())
|
||||
if err == nil {
|
||||
localIP = net.ParseIP(localIPAddress)
|
||||
} else {
|
||||
l.Debugln("Failed to lookup local IP", err)
|
||||
}
|
||||
}
|
||||
|
||||
return []nat.Device{&wrapper{
|
||||
renewal: renewal,
|
||||
localIP: localIP,
|
||||
gatewayIP: ip,
|
||||
client: c,
|
||||
}}
|
||||
}
|
||||
|
||||
type wrapper struct {
|
||||
renewal time.Duration
|
||||
localIP net.IP
|
||||
gatewayIP net.IP
|
||||
client *natpmp.Client
|
||||
}
|
||||
|
||||
func (w *wrapper) ID() string {
|
||||
return fmt.Sprintf("NAT-PMP@%s", w.gatewayIP.String())
|
||||
}
|
||||
|
||||
func (w *wrapper) GetLocalIPAddress() net.IP {
|
||||
return w.localIP
|
||||
}
|
||||
|
||||
func (w *wrapper) AddPortMapping(protocol nat.Protocol, internalPort, externalPort int, description string, duration time.Duration) (int, error) {
|
||||
// NAT-PMP says that if duration is 0, the mapping is actually removed
|
||||
// Swap the zero with the renewal value, which should make the lease for the
|
||||
// exact amount of time between the calls.
|
||||
if duration == 0 {
|
||||
duration = w.renewal
|
||||
}
|
||||
result, err := w.client.AddPortMapping(strings.ToLower(string(protocol)), internalPort, externalPort, int(duration/time.Second))
|
||||
port := 0
|
||||
if result != nil {
|
||||
port = int(result.MappedExternalPort)
|
||||
}
|
||||
return port, err
|
||||
}
|
||||
|
||||
func (w *wrapper) GetExternalIPAddress() (net.IP, error) {
|
||||
result, err := w.client.GetExternalAddress()
|
||||
ip := net.IPv4zero
|
||||
if result != nil {
|
||||
ip = net.IPv4(
|
||||
result.ExternalIPAddress[0],
|
||||
result.ExternalIPAddress[1],
|
||||
result.ExternalIPAddress[2],
|
||||
result.ExternalIPAddress[3],
|
||||
)
|
||||
}
|
||||
return ip, err
|
||||
}
|
||||
@@ -2,10 +2,7 @@
|
||||
|
||||
package protocol
|
||||
|
||||
import (
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
import "time"
|
||||
|
||||
type TestModel struct {
|
||||
data []byte
|
||||
@@ -52,6 +49,9 @@ func (t *TestModel) Close(deviceID DeviceID, err error) {
|
||||
func (t *TestModel) ClusterConfig(deviceID DeviceID, config ClusterConfigMessage) {
|
||||
}
|
||||
|
||||
func (t *TestModel) DownloadProgress(DeviceID, string, []FileDownloadProgressUpdate, uint32, []Option) {
|
||||
}
|
||||
|
||||
func (t *TestModel) closedError() error {
|
||||
select {
|
||||
case <-t.closedCh:
|
||||
@@ -60,24 +60,3 @@ func (t *TestModel) closedError() error {
|
||||
return nil // Timeout
|
||||
}
|
||||
}
|
||||
|
||||
type ErrPipe struct {
|
||||
io.PipeWriter
|
||||
written int
|
||||
max int
|
||||
err error
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (e *ErrPipe) Write(data []byte) (int, error) {
|
||||
if e.closed {
|
||||
return 0, e.err
|
||||
}
|
||||
if e.written+len(data) > e.max {
|
||||
n, _ := e.PipeWriter.Write(data[:e.max-e.written])
|
||||
e.PipeWriter.CloseWithError(e.err)
|
||||
e.closed = true
|
||||
return n, e.err
|
||||
}
|
||||
return e.PipeWriter.Write(data)
|
||||
}
|
||||
|
||||
@@ -138,6 +138,13 @@ type ClusterConfigMessage struct {
|
||||
Options []Option // max:64
|
||||
}
|
||||
|
||||
type DownloadProgressMessage struct {
|
||||
Folder string // max:64
|
||||
Updates []FileDownloadProgressUpdate // max:1000000
|
||||
Flags uint32
|
||||
Options []Option // max:64
|
||||
}
|
||||
|
||||
func (o *ClusterConfigMessage) GetOption(key string) string {
|
||||
for _, option := range o.Options {
|
||||
if option.Key == key {
|
||||
@@ -166,6 +173,13 @@ type Device struct {
|
||||
Options []Option // max:64
|
||||
}
|
||||
|
||||
type FileDownloadProgressUpdate struct {
|
||||
UpdateType uint32
|
||||
Name string // max:8192
|
||||
Version Vector
|
||||
BlockIndexes []int32 // max:1000000
|
||||
}
|
||||
|
||||
type Option struct {
|
||||
Key string // max:64
|
||||
Value string // max:1024
|
||||
|
||||
@@ -690,6 +690,135 @@ func (o *ClusterConfigMessage) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
|
||||
|
||||
/*
|
||||
|
||||
DownloadProgressMessage Structure:
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Folder (length + padded data) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Number of Updates |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Zero or more FileDownloadProgressUpdate Structures \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Flags |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Number of Options |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Zero or more Option Structures \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
|
||||
struct DownloadProgressMessage {
|
||||
string Folder<64>;
|
||||
FileDownloadProgressUpdate Updates<1000000>;
|
||||
unsigned int Flags;
|
||||
Option Options<64>;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func (o DownloadProgressMessage) XDRSize() int {
|
||||
return 4 + len(o.Folder) + xdr.Padding(len(o.Folder)) +
|
||||
4 + xdr.SizeOfSlice(o.Updates) + 4 +
|
||||
4 + xdr.SizeOfSlice(o.Options)
|
||||
}
|
||||
|
||||
func (o DownloadProgressMessage) MarshalXDR() ([]byte, error) {
|
||||
buf := make([]byte, o.XDRSize())
|
||||
m := &xdr.Marshaller{Data: buf}
|
||||
return buf, o.MarshalXDRInto(m)
|
||||
}
|
||||
|
||||
func (o DownloadProgressMessage) MustMarshalXDR() []byte {
|
||||
bs, err := o.MarshalXDR()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
func (o DownloadProgressMessage) MarshalXDRInto(m *xdr.Marshaller) error {
|
||||
if l := len(o.Folder); l > 64 {
|
||||
return xdr.ElementSizeExceeded("Folder", l, 64)
|
||||
}
|
||||
m.MarshalString(o.Folder)
|
||||
if l := len(o.Updates); l > 1000000 {
|
||||
return xdr.ElementSizeExceeded("Updates", l, 1000000)
|
||||
}
|
||||
m.MarshalUint32(uint32(len(o.Updates)))
|
||||
for i := range o.Updates {
|
||||
if err := o.Updates[i].MarshalXDRInto(m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
m.MarshalUint32(o.Flags)
|
||||
if l := len(o.Options); l > 64 {
|
||||
return xdr.ElementSizeExceeded("Options", l, 64)
|
||||
}
|
||||
m.MarshalUint32(uint32(len(o.Options)))
|
||||
for i := range o.Options {
|
||||
if err := o.Options[i].MarshalXDRInto(m); err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
return m.Error
|
||||
}
|
||||
|
||||
func (o *DownloadProgressMessage) UnmarshalXDR(bs []byte) error {
|
||||
u := &xdr.Unmarshaller{Data: bs}
|
||||
return o.UnmarshalXDRFrom(u)
|
||||
}
|
||||
func (o *DownloadProgressMessage) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
|
||||
o.Folder = u.UnmarshalStringMax(64)
|
||||
_UpdatesSize := int(u.UnmarshalUint32())
|
||||
if _UpdatesSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Updates", _UpdatesSize, 1000000)
|
||||
} else if _UpdatesSize == 0 {
|
||||
o.Updates = nil
|
||||
} else {
|
||||
if _UpdatesSize > 1000000 {
|
||||
return xdr.ElementSizeExceeded("Updates", _UpdatesSize, 1000000)
|
||||
}
|
||||
if _UpdatesSize <= len(o.Updates) {
|
||||
o.Updates = o.Updates[:_UpdatesSize]
|
||||
} else {
|
||||
o.Updates = make([]FileDownloadProgressUpdate, _UpdatesSize)
|
||||
}
|
||||
for i := range o.Updates {
|
||||
(&o.Updates[i]).UnmarshalXDRFrom(u)
|
||||
}
|
||||
}
|
||||
o.Flags = u.UnmarshalUint32()
|
||||
_OptionsSize := int(u.UnmarshalUint32())
|
||||
if _OptionsSize < 0 {
|
||||
return xdr.ElementSizeExceeded("Options", _OptionsSize, 64)
|
||||
} else if _OptionsSize == 0 {
|
||||
o.Options = nil
|
||||
} else {
|
||||
if _OptionsSize > 64 {
|
||||
return xdr.ElementSizeExceeded("Options", _OptionsSize, 64)
|
||||
}
|
||||
if _OptionsSize <= len(o.Options) {
|
||||
o.Options = o.Options[:_OptionsSize]
|
||||
} else {
|
||||
o.Options = make([]Option, _OptionsSize)
|
||||
}
|
||||
for i := range o.Options {
|
||||
(&o.Options[i]).UnmarshalXDRFrom(u)
|
||||
}
|
||||
}
|
||||
return u.Error
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Folder Structure:
|
||||
|
||||
0 1 2 3
|
||||
@@ -996,6 +1125,109 @@ func (o *Device) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
|
||||
|
||||
/*
|
||||
|
||||
FileDownloadProgressUpdate Structure:
|
||||
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Update Type |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Name (length + padded data) \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
\ Vector Structure \
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
| Number of Block Indexes |
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
/ /
|
||||
| Block Indexes (n items) |
|
||||
/ /
|
||||
+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+-+
|
||||
|
||||
|
||||
struct FileDownloadProgressUpdate {
|
||||
unsigned int UpdateType;
|
||||
string Name<8192>;
|
||||
Vector Version;
|
||||
int BlockIndexes<1000000>;
|
||||
}
|
||||
|
||||
*/
|
||||
|
||||
func (o FileDownloadProgressUpdate) XDRSize() int {
|
||||
return 4 +
|
||||
4 + len(o.Name) + xdr.Padding(len(o.Name)) +
|
||||
o.Version.XDRSize() +
|
||||
4 + len(o.BlockIndexes)*4
|
||||
}
|
||||
|
||||
func (o FileDownloadProgressUpdate) MarshalXDR() ([]byte, error) {
|
||||
buf := make([]byte, o.XDRSize())
|
||||
m := &xdr.Marshaller{Data: buf}
|
||||
return buf, o.MarshalXDRInto(m)
|
||||
}
|
||||
|
||||
func (o FileDownloadProgressUpdate) MustMarshalXDR() []byte {
|
||||
bs, err := o.MarshalXDR()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return bs
|
||||
}
|
||||
|
||||
func (o FileDownloadProgressUpdate) MarshalXDRInto(m *xdr.Marshaller) error {
|
||||
m.MarshalUint32(o.UpdateType)
|
||||
if l := len(o.Name); l > 8192 {
|
||||
return xdr.ElementSizeExceeded("Name", l, 8192)
|
||||
}
|
||||
m.MarshalString(o.Name)
|
||||
if err := o.Version.MarshalXDRInto(m); err != nil {
|
||||
return err
|
||||
}
|
||||
if l := len(o.BlockIndexes); l > 1000000 {
|
||||
return xdr.ElementSizeExceeded("BlockIndexes", l, 1000000)
|
||||
}
|
||||
m.MarshalUint32(uint32(len(o.BlockIndexes)))
|
||||
for i := range o.BlockIndexes {
|
||||
m.MarshalUint32(uint32(o.BlockIndexes[i]))
|
||||
}
|
||||
return m.Error
|
||||
}
|
||||
|
||||
func (o *FileDownloadProgressUpdate) UnmarshalXDR(bs []byte) error {
|
||||
u := &xdr.Unmarshaller{Data: bs}
|
||||
return o.UnmarshalXDRFrom(u)
|
||||
}
|
||||
func (o *FileDownloadProgressUpdate) UnmarshalXDRFrom(u *xdr.Unmarshaller) error {
|
||||
o.UpdateType = u.UnmarshalUint32()
|
||||
o.Name = u.UnmarshalStringMax(8192)
|
||||
(&o.Version).UnmarshalXDRFrom(u)
|
||||
_BlockIndexesSize := int(u.UnmarshalUint32())
|
||||
if _BlockIndexesSize < 0 {
|
||||
return xdr.ElementSizeExceeded("BlockIndexes", _BlockIndexesSize, 1000000)
|
||||
} else if _BlockIndexesSize == 0 {
|
||||
o.BlockIndexes = nil
|
||||
} else {
|
||||
if _BlockIndexesSize > 1000000 {
|
||||
return xdr.ElementSizeExceeded("BlockIndexes", _BlockIndexesSize, 1000000)
|
||||
}
|
||||
if _BlockIndexesSize <= len(o.BlockIndexes) {
|
||||
o.BlockIndexes = o.BlockIndexes[:_BlockIndexesSize]
|
||||
} else {
|
||||
o.BlockIndexes = make([]int32, _BlockIndexesSize)
|
||||
}
|
||||
for i := range o.BlockIndexes {
|
||||
o.BlockIndexes[i] = int32(u.UnmarshalUint32())
|
||||
}
|
||||
}
|
||||
return u.Error
|
||||
}
|
||||
|
||||
/*
|
||||
|
||||
Option Structure:
|
||||
|
||||
0 1 2 3
|
||||
|
||||
@@ -9,32 +9,24 @@ package protocol
|
||||
import "golang.org/x/text/unicode/norm"
|
||||
|
||||
type nativeModel struct {
|
||||
next Model
|
||||
Model
|
||||
}
|
||||
|
||||
func (m nativeModel) Index(deviceID DeviceID, folder string, files []FileInfo, flags uint32, options []Option) {
|
||||
for i := range files {
|
||||
files[i].Name = norm.NFD.String(files[i].Name)
|
||||
}
|
||||
m.next.Index(deviceID, folder, files, flags, options)
|
||||
m.Model.Index(deviceID, folder, files, flags, options)
|
||||
}
|
||||
|
||||
func (m nativeModel) IndexUpdate(deviceID DeviceID, folder string, files []FileInfo, flags uint32, options []Option) {
|
||||
for i := range files {
|
||||
files[i].Name = norm.NFD.String(files[i].Name)
|
||||
}
|
||||
m.next.IndexUpdate(deviceID, folder, files, flags, options)
|
||||
m.Model.IndexUpdate(deviceID, folder, files, flags, options)
|
||||
}
|
||||
|
||||
func (m nativeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, flags uint32, options []Option, buf []byte) error {
|
||||
name = norm.NFD.String(name)
|
||||
return m.next.Request(deviceID, folder, name, offset, hash, flags, options, buf)
|
||||
}
|
||||
|
||||
func (m nativeModel) ClusterConfig(deviceID DeviceID, config ClusterConfigMessage) {
|
||||
m.next.ClusterConfig(deviceID, config)
|
||||
}
|
||||
|
||||
func (m nativeModel) Close(deviceID DeviceID, err error) {
|
||||
m.next.Close(deviceID, err)
|
||||
return m.Model.Request(deviceID, folder, name, offset, hash, flags, options, buf)
|
||||
}
|
||||
|
||||
@@ -7,25 +7,5 @@ package protocol
|
||||
// Normal Unixes uses NFC and slashes, which is the wire format.
|
||||
|
||||
type nativeModel struct {
|
||||
next Model
|
||||
}
|
||||
|
||||
func (m nativeModel) Index(deviceID DeviceID, folder string, files []FileInfo, flags uint32, options []Option) {
|
||||
m.next.Index(deviceID, folder, files, flags, options)
|
||||
}
|
||||
|
||||
func (m nativeModel) IndexUpdate(deviceID DeviceID, folder string, files []FileInfo, flags uint32, options []Option) {
|
||||
m.next.IndexUpdate(deviceID, folder, files, flags, options)
|
||||
}
|
||||
|
||||
func (m nativeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, flags uint32, options []Option, buf []byte) error {
|
||||
return m.next.Request(deviceID, folder, name, offset, hash, flags, options, buf)
|
||||
}
|
||||
|
||||
func (m nativeModel) ClusterConfig(deviceID DeviceID, config ClusterConfigMessage) {
|
||||
m.next.ClusterConfig(deviceID, config)
|
||||
}
|
||||
|
||||
func (m nativeModel) Close(deviceID DeviceID, err error) {
|
||||
m.next.Close(deviceID, err)
|
||||
Model
|
||||
}
|
||||
|
||||
@@ -21,30 +21,22 @@ var disallowedCharacters = string([]rune{
|
||||
})
|
||||
|
||||
type nativeModel struct {
|
||||
next Model
|
||||
Model
|
||||
}
|
||||
|
||||
func (m nativeModel) Index(deviceID DeviceID, folder string, files []FileInfo, flags uint32, options []Option) {
|
||||
fixupFiles(folder, files)
|
||||
m.next.Index(deviceID, folder, files, flags, options)
|
||||
m.Model.Index(deviceID, folder, files, flags, options)
|
||||
}
|
||||
|
||||
func (m nativeModel) IndexUpdate(deviceID DeviceID, folder string, files []FileInfo, flags uint32, options []Option) {
|
||||
fixupFiles(folder, files)
|
||||
m.next.IndexUpdate(deviceID, folder, files, flags, options)
|
||||
m.Model.IndexUpdate(deviceID, folder, files, flags, options)
|
||||
}
|
||||
|
||||
func (m nativeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, flags uint32, options []Option, buf []byte) error {
|
||||
name = filepath.FromSlash(name)
|
||||
return m.next.Request(deviceID, folder, name, offset, hash, flags, options, buf)
|
||||
}
|
||||
|
||||
func (m nativeModel) ClusterConfig(deviceID DeviceID, config ClusterConfigMessage) {
|
||||
m.next.ClusterConfig(deviceID, config)
|
||||
}
|
||||
|
||||
func (m nativeModel) Close(deviceID DeviceID, err error) {
|
||||
m.next.Close(deviceID, err)
|
||||
return m.Model.Request(deviceID, folder, name, offset, hash, flags, options, buf)
|
||||
}
|
||||
|
||||
func fixupFiles(folder string, files []FileInfo) {
|
||||
|
||||
+67
-36
@@ -24,13 +24,14 @@ const (
|
||||
)
|
||||
|
||||
const (
|
||||
messageTypeClusterConfig = 0
|
||||
messageTypeIndex = 1
|
||||
messageTypeRequest = 2
|
||||
messageTypeResponse = 3
|
||||
messageTypePing = 4
|
||||
messageTypeIndexUpdate = 6
|
||||
messageTypeClose = 7
|
||||
messageTypeClusterConfig = 0
|
||||
messageTypeIndex = 1
|
||||
messageTypeRequest = 2
|
||||
messageTypeResponse = 3
|
||||
messageTypePing = 4
|
||||
messageTypeIndexUpdate = 6
|
||||
messageTypeClose = 7
|
||||
messageTypeDownloadProgress = 8
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -40,34 +41,41 @@ const (
|
||||
|
||||
// FileInfo flags
|
||||
const (
|
||||
FlagDeleted uint32 = 1 << 12
|
||||
FlagInvalid = 1 << 13
|
||||
FlagDirectory = 1 << 14
|
||||
FlagNoPermBits = 1 << 15
|
||||
FlagSymlink = 1 << 16
|
||||
FlagSymlinkMissingTarget = 1 << 17
|
||||
FlagDeleted uint32 = 1 << 12 // bit 19 in MSB order with the first bit being #0
|
||||
FlagInvalid = 1 << 13 // bit 18
|
||||
FlagDirectory = 1 << 14 // bit 17
|
||||
FlagNoPermBits = 1 << 15 // bit 16
|
||||
FlagSymlink = 1 << 16 // bit 15
|
||||
FlagSymlinkMissingTarget = 1 << 17 // bit 14
|
||||
|
||||
FlagsAll = (1 << 18) - 1
|
||||
|
||||
SymlinkTypeMask = FlagDirectory | FlagSymlinkMissingTarget
|
||||
)
|
||||
|
||||
// IndexMessage message flags (for IndexUpdate)
|
||||
const (
|
||||
FlagIndexTemporary uint32 = 1 << iota
|
||||
)
|
||||
|
||||
// Request message flags
|
||||
const (
|
||||
FlagRequestTemporary uint32 = 1 << iota
|
||||
FlagFromTemporary uint32 = 1 << iota
|
||||
)
|
||||
|
||||
// FileDownloadProgressUpdate update types
|
||||
const (
|
||||
UpdateTypeAppend uint32 = iota
|
||||
UpdateTypeForget
|
||||
)
|
||||
|
||||
// CLusterConfig flags
|
||||
const (
|
||||
FlagClusterConfigTemporaryIndexes uint32 = 1 << 0
|
||||
)
|
||||
|
||||
// ClusterConfigMessage.Folders flags
|
||||
const (
|
||||
FlagFolderReadOnly uint32 = 1 << 0
|
||||
FlagFolderIgnorePerms = 1 << 1
|
||||
FlagFolderIgnoreDelete = 1 << 2
|
||||
FlagFolderAll = 1<<3 - 1
|
||||
FlagFolderReadOnly uint32 = 1 << 0
|
||||
FlagFolderIgnorePerms = 1 << 1
|
||||
FlagFolderIgnoreDelete = 1 << 2
|
||||
FlagFolderDisabledTempIndexes = 1 << 3
|
||||
FlagFolderAll = 1<<4 - 1
|
||||
)
|
||||
|
||||
// ClusterConfigMessage.Folders.Devices flags
|
||||
@@ -97,6 +105,8 @@ type Model interface {
|
||||
ClusterConfig(deviceID DeviceID, config ClusterConfigMessage)
|
||||
// The peer device closed the connection
|
||||
Close(deviceID DeviceID, err error)
|
||||
// The peer device sent progress updates for the files it is currently downloading
|
||||
DownloadProgress(deviceID DeviceID, folder string, updates []FileDownloadProgressUpdate, flags uint32, options []Option)
|
||||
}
|
||||
|
||||
type Connection interface {
|
||||
@@ -105,8 +115,9 @@ type Connection interface {
|
||||
Name() string
|
||||
Index(folder string, files []FileInfo, flags uint32, options []Option) error
|
||||
IndexUpdate(folder string, files []FileInfo, flags uint32, options []Option) error
|
||||
Request(folder string, name string, offset int64, size int, hash []byte, flags uint32, options []Option) ([]byte, error)
|
||||
Request(folder string, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error)
|
||||
ClusterConfig(config ClusterConfigMessage)
|
||||
DownloadProgress(folder string, updates []FileDownloadProgressUpdate, flags uint32, options []Option)
|
||||
Statistics() Statistics
|
||||
Closed() bool
|
||||
}
|
||||
@@ -242,7 +253,7 @@ func (c *rawConnection) IndexUpdate(folder string, idx []FileInfo, flags uint32,
|
||||
}
|
||||
|
||||
// Request returns the bytes for the specified block after fetching them from the connected peer.
|
||||
func (c *rawConnection) Request(folder string, name string, offset int64, size int, hash []byte, flags uint32, options []Option) ([]byte, error) {
|
||||
func (c *rawConnection) Request(folder string, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
|
||||
var id int
|
||||
select {
|
||||
case id = <-c.nextID:
|
||||
@@ -250,6 +261,12 @@ func (c *rawConnection) Request(folder string, name string, offset int64, size i
|
||||
return nil, ErrClosed
|
||||
}
|
||||
|
||||
var flags uint32
|
||||
|
||||
if fromTemporary {
|
||||
flags |= FlagFromTemporary
|
||||
}
|
||||
|
||||
c.awaitingMut.Lock()
|
||||
if ch := c.awaiting[id]; ch != nil {
|
||||
panic("id taken")
|
||||
@@ -265,7 +282,7 @@ func (c *rawConnection) Request(folder string, name string, offset int64, size i
|
||||
Size: int32(size),
|
||||
Hash: hash,
|
||||
Flags: flags,
|
||||
Options: options,
|
||||
Options: nil,
|
||||
}, nil)
|
||||
if !ok {
|
||||
return nil, ErrClosed
|
||||
@@ -292,6 +309,16 @@ func (c *rawConnection) Closed() bool {
|
||||
}
|
||||
}
|
||||
|
||||
// DownloadProgress sends the progress updates for the files that are currently being downloaded.
|
||||
func (c *rawConnection) DownloadProgress(folder string, updates []FileDownloadProgressUpdate, flags uint32, options []Option) {
|
||||
c.send(-1, messageTypeDownloadProgress, DownloadProgressMessage{
|
||||
Folder: folder,
|
||||
Updates: updates,
|
||||
Flags: flags,
|
||||
Options: options,
|
||||
}, nil)
|
||||
}
|
||||
|
||||
func (c *rawConnection) ping() bool {
|
||||
var id int
|
||||
select {
|
||||
@@ -359,6 +386,12 @@ func (c *rawConnection) readerLoop() (err error) {
|
||||
}
|
||||
c.handleResponse(hdr.msgID, msg)
|
||||
|
||||
case DownloadProgressMessage:
|
||||
if state != stateReady {
|
||||
return fmt.Errorf("protocol error: response message in state %d", state)
|
||||
}
|
||||
c.receiver.DownloadProgress(c.id, msg.Folder, msg.Updates, msg.Flags, msg.Options)
|
||||
|
||||
case pingMessage:
|
||||
if state != stateReady {
|
||||
return fmt.Errorf("protocol error: ping message in state %d", state)
|
||||
@@ -469,6 +502,14 @@ func (c *rawConnection) readMessage() (hdr header, msg encodable, err error) {
|
||||
err = cm.UnmarshalXDR(msgBuf)
|
||||
msg = cm
|
||||
|
||||
case messageTypeDownloadProgress:
|
||||
var dp DownloadProgressMessage
|
||||
err := dp.UnmarshalXDR(msgBuf)
|
||||
if xdrErr, ok := err.(isEofer); ok && xdrErr.IsEOF() {
|
||||
err = nil
|
||||
}
|
||||
msg = dp
|
||||
|
||||
default:
|
||||
err = fmt.Errorf("protocol error: %s: unknown message type %#x", c.id, hdr.msgType)
|
||||
}
|
||||
@@ -566,16 +607,6 @@ func (c *rawConnection) handleResponse(msgID int, resp ResponseMessage) {
|
||||
c.awaitingMut.Unlock()
|
||||
}
|
||||
|
||||
func (c *rawConnection) handlePong(msgID int) {
|
||||
c.awaitingMut.Lock()
|
||||
if rc := c.awaiting[msgID]; rc != nil {
|
||||
c.awaiting[msgID] = nil
|
||||
rc <- asyncResult{}
|
||||
close(rc)
|
||||
}
|
||||
c.awaitingMut.Unlock()
|
||||
}
|
||||
|
||||
func (c *rawConnection) send(msgID int, msgType int, msg encodable, done chan struct{}) bool {
|
||||
if msgID < 0 {
|
||||
select {
|
||||
|
||||
@@ -183,7 +183,7 @@ func TestClose(t *testing.T) {
|
||||
c0.Index("default", nil, 0, nil)
|
||||
c0.Index("default", nil, 0, nil)
|
||||
|
||||
if _, err := c0.Request("default", "foo", 0, 0, nil, 0, nil); err == nil {
|
||||
if _, err := c0.Request("default", "foo", 0, 0, nil, false); err == nil {
|
||||
t.Error("Request should return an error")
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ func (c wireFormatConnection) IndexUpdate(folder string, fs []FileInfo, flags ui
|
||||
return c.Connection.IndexUpdate(folder, myFs, flags, options)
|
||||
}
|
||||
|
||||
func (c wireFormatConnection) Request(folder, name string, offset int64, size int, hash []byte, flags uint32, options []Option) ([]byte, error) {
|
||||
func (c wireFormatConnection) Request(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
|
||||
name = norm.NFC.String(filepath.ToSlash(name))
|
||||
return c.Connection.Request(folder, name, offset, size, hash, flags, options)
|
||||
return c.Connection.Request(folder, name, offset, size, hash, fromTemporary)
|
||||
}
|
||||
|
||||
@@ -45,10 +45,6 @@ var testdata = testfileList{
|
||||
{"further-excludes", 5, "7eb0a548094fa6295f7fd9200d69973e5f5ec5c04f2a86d998080ac43ecf89f1"},
|
||||
}
|
||||
|
||||
var correctIgnores = map[string][]string{
|
||||
".": {".*", "quux"},
|
||||
}
|
||||
|
||||
func init() {
|
||||
// This test runs the risk of entering infinite recursion if it fails.
|
||||
// Limit the stack size to 10 megs to crash early in that case instead of
|
||||
|
||||
+9
-6
@@ -13,6 +13,9 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/nat"
|
||||
)
|
||||
|
||||
// An IGD is a UPnP InternetGatewayDevice.
|
||||
@@ -24,7 +27,7 @@ type IGD struct {
|
||||
localIPAddress net.IP
|
||||
}
|
||||
|
||||
func (n *IGD) UUID() string {
|
||||
func (n *IGD) ID() string {
|
||||
return n.uuid
|
||||
}
|
||||
|
||||
@@ -47,14 +50,14 @@ func (n *IGD) URL() *url.URL {
|
||||
// if action is fails for _any_ of the relevant services. For this reason, it
|
||||
// is generally better to configure port mapping for each individual service
|
||||
// instead.
|
||||
func (n *IGD) AddPortMapping(protocol Protocol, externalPort, internalPort int, description string, timeout int) error {
|
||||
func (n *IGD) AddPortMapping(protocol nat.Protocol, externalPort, internalPort int, description string, duration time.Duration) (int, error) {
|
||||
for _, service := range n.services {
|
||||
err := service.AddPortMapping(n.localIPAddress, protocol, externalPort, internalPort, description, timeout)
|
||||
err := service.AddPortMapping(n.localIPAddress, protocol, externalPort, internalPort, description, duration)
|
||||
if err != nil {
|
||||
return err
|
||||
return externalPort, err
|
||||
}
|
||||
}
|
||||
return nil
|
||||
return externalPort, nil
|
||||
}
|
||||
|
||||
// DeletePortMapping deletes a port mapping from all relevant services on the
|
||||
@@ -62,7 +65,7 @@ func (n *IGD) AddPortMapping(protocol Protocol, externalPort, internalPort int,
|
||||
// if action is fails for _any_ of the relevant services. For this reason, it
|
||||
// is generally better to configure port mapping for each individual service
|
||||
// instead.
|
||||
func (n *IGD) DeletePortMapping(protocol Protocol, externalPort int) error {
|
||||
func (n *IGD) DeletePortMapping(protocol nat.Protocol, externalPort int) error {
|
||||
for _, service := range n.services {
|
||||
err := service.DeletePortMapping(protocol, externalPort)
|
||||
if err != nil {
|
||||
|
||||
@@ -13,6 +13,9 @@ import (
|
||||
"encoding/xml"
|
||||
"fmt"
|
||||
"net"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/nat"
|
||||
)
|
||||
|
||||
// An IGDService is a specific service provided by an IGD.
|
||||
@@ -23,7 +26,7 @@ type IGDService struct {
|
||||
}
|
||||
|
||||
// AddPortMapping adds a port mapping to the specified IGD service.
|
||||
func (s *IGDService) AddPortMapping(localIPAddress net.IP, protocol Protocol, externalPort, internalPort int, description string, timeout int) error {
|
||||
func (s *IGDService) AddPortMapping(localIPAddress net.IP, protocol nat.Protocol, externalPort, internalPort int, description string, duration time.Duration) error {
|
||||
tpl := `<u:AddPortMapping xmlns:u="%s">
|
||||
<NewRemoteHost></NewRemoteHost>
|
||||
<NewExternalPort>%d</NewExternalPort>
|
||||
@@ -34,10 +37,10 @@ func (s *IGDService) AddPortMapping(localIPAddress net.IP, protocol Protocol, ex
|
||||
<NewPortMappingDescription>%s</NewPortMappingDescription>
|
||||
<NewLeaseDuration>%d</NewLeaseDuration>
|
||||
</u:AddPortMapping>`
|
||||
body := fmt.Sprintf(tpl, s.URN, externalPort, protocol, internalPort, localIPAddress, description, timeout)
|
||||
body := fmt.Sprintf(tpl, s.URN, externalPort, protocol, internalPort, localIPAddress, description, duration/time.Second)
|
||||
|
||||
response, err := soapRequest(s.URL, s.URN, "AddPortMapping", body)
|
||||
if err != nil && timeout > 0 {
|
||||
if err != nil && duration > 0 {
|
||||
// Try to repair error code 725 - OnlyPermanentLeasesSupported
|
||||
envelope := &soapErrorResponse{}
|
||||
if unmarshalErr := xml.Unmarshal(response, envelope); unmarshalErr != nil {
|
||||
@@ -52,7 +55,7 @@ func (s *IGDService) AddPortMapping(localIPAddress net.IP, protocol Protocol, ex
|
||||
}
|
||||
|
||||
// DeletePortMapping deletes a port mapping from the specified IGD service.
|
||||
func (s *IGDService) DeletePortMapping(protocol Protocol, externalPort int) error {
|
||||
func (s *IGDService) DeletePortMapping(protocol nat.Protocol, externalPort int) error {
|
||||
tpl := `<u:DeletePortMapping xmlns:u="%s">
|
||||
<NewRemoteHost></NewRemoteHost>
|
||||
<NewExternalPort>%d</NewExternalPort>
|
||||
|
||||
@@ -1,132 +0,0 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package upnp
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/events"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/util"
|
||||
)
|
||||
|
||||
// Service runs a loop for discovery of IGDs (Internet Gateway Devices) and
|
||||
// setup/renewal of a port mapping.
|
||||
type Service struct {
|
||||
cfg *config.Wrapper
|
||||
localPort int
|
||||
extPort int
|
||||
extPortMut sync.Mutex
|
||||
stop chan struct{}
|
||||
}
|
||||
|
||||
func NewUPnPService(cfg *config.Wrapper, localPort int) *Service {
|
||||
return &Service{
|
||||
cfg: cfg,
|
||||
localPort: localPort,
|
||||
extPortMut: sync.NewMutex(),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Serve() {
|
||||
foundIGD := true
|
||||
s.stop = make(chan struct{})
|
||||
|
||||
for {
|
||||
igds := Discover(time.Duration(s.cfg.Options().UPnPTimeoutS) * time.Second)
|
||||
if len(igds) > 0 {
|
||||
foundIGD = true
|
||||
s.extPortMut.Lock()
|
||||
oldExtPort := s.extPort
|
||||
s.extPortMut.Unlock()
|
||||
|
||||
newExtPort := s.tryIGDs(igds, oldExtPort)
|
||||
|
||||
s.extPortMut.Lock()
|
||||
s.extPort = newExtPort
|
||||
s.extPortMut.Unlock()
|
||||
} else if foundIGD {
|
||||
// Only print a notice if we've previously found an IGD or this is
|
||||
// the first time around.
|
||||
foundIGD = false
|
||||
l.Infof("No UPnP device detected")
|
||||
}
|
||||
|
||||
d := time.Duration(s.cfg.Options().UPnPRenewalM) * time.Minute
|
||||
if d == 0 {
|
||||
// We always want to do renewal so lets just pick a nice sane number.
|
||||
d = 30 * time.Minute
|
||||
}
|
||||
|
||||
select {
|
||||
case <-s.stop:
|
||||
return
|
||||
case <-time.After(d):
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Service) Stop() {
|
||||
close(s.stop)
|
||||
}
|
||||
|
||||
func (s *Service) ExternalPort() int {
|
||||
s.extPortMut.Lock()
|
||||
port := s.extPort
|
||||
s.extPortMut.Unlock()
|
||||
return port
|
||||
}
|
||||
|
||||
func (s *Service) tryIGDs(igds []IGD, prevExtPort int) int {
|
||||
// Lets try all the IGDs we found and use the first one that works.
|
||||
// TODO: Use all of them, and sort out the resulting mess to the
|
||||
// discovery announcement code...
|
||||
for _, igd := range igds {
|
||||
extPort, err := s.tryIGD(igd, prevExtPort)
|
||||
if err != nil {
|
||||
l.Warnf("Failed to set UPnP port mapping: external port %d on device %s.", extPort, igd.FriendlyIdentifier())
|
||||
continue
|
||||
}
|
||||
|
||||
if extPort != prevExtPort {
|
||||
l.Infof("New UPnP port mapping: external port %d to local port %d.", extPort, s.localPort)
|
||||
events.Default.Log(events.ExternalPortMappingChanged, map[string]int{"port": extPort})
|
||||
}
|
||||
l.Debugf("Created/updated UPnP port mapping for external port %d on device %s.", extPort, igd.FriendlyIdentifier())
|
||||
return extPort
|
||||
}
|
||||
|
||||
return 0
|
||||
}
|
||||
|
||||
func (s *Service) tryIGD(igd IGD, suggestedPort int) (int, error) {
|
||||
var err error
|
||||
leaseTime := s.cfg.Options().UPnPLeaseM * 60
|
||||
|
||||
if suggestedPort != 0 {
|
||||
// First try renewing our existing mapping.
|
||||
name := fmt.Sprintf("syncthing-%d", suggestedPort)
|
||||
err = igd.AddPortMapping(TCP, suggestedPort, s.localPort, name, leaseTime)
|
||||
if err == nil {
|
||||
return suggestedPort, nil
|
||||
}
|
||||
}
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
// Then try up to ten random ports.
|
||||
extPort := 1024 + util.PredictableRandom.Intn(65535-1024)
|
||||
name := fmt.Sprintf("syncthing-%d", extPort)
|
||||
err = igd.AddPortMapping(TCP, extPort, s.localPort, name, leaseTime)
|
||||
if err == nil {
|
||||
return extPort, nil
|
||||
}
|
||||
}
|
||||
|
||||
return 0, err
|
||||
}
|
||||
+8
-10
@@ -26,15 +26,13 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/dialer"
|
||||
"github.com/syncthing/syncthing/lib/nat"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
type Protocol string
|
||||
|
||||
const (
|
||||
TCP Protocol = "TCP"
|
||||
UDP = "UDP"
|
||||
)
|
||||
func init() {
|
||||
nat.Register(Discover)
|
||||
}
|
||||
|
||||
type upnpService struct {
|
||||
ID string `xml:"serviceId"`
|
||||
@@ -55,8 +53,8 @@ type upnpRoot struct {
|
||||
|
||||
// Discover discovers UPnP InternetGatewayDevices.
|
||||
// The order in which the devices appear in the results list is not deterministic.
|
||||
func Discover(timeout time.Duration) []IGD {
|
||||
var results []IGD
|
||||
func Discover(renewal, timeout time.Duration) []nat.Device {
|
||||
var results []nat.Device
|
||||
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
@@ -91,7 +89,7 @@ func Discover(timeout time.Duration) []IGD {
|
||||
nextResult:
|
||||
for result := range resultChan {
|
||||
for _, existingResult := range results {
|
||||
if existingResult.uuid == result.uuid {
|
||||
if existingResult.ID() == result.ID() {
|
||||
l.Debugf("Skipping duplicate result %s with services:", result.uuid)
|
||||
for _, service := range result.services {
|
||||
l.Debugf("* [%s] %s", service.ID, service.URL)
|
||||
@@ -100,7 +98,7 @@ nextResult:
|
||||
}
|
||||
}
|
||||
|
||||
results = append(results, result)
|
||||
results = append(results, &result)
|
||||
l.Debugf("UPnP discovery result %s with services:", result.uuid)
|
||||
for _, service := range result.services {
|
||||
l.Debugf("* [%s] %s", service.ID, service.URL)
|
||||
|
||||
@@ -17,16 +17,6 @@ import (
|
||||
// randomCharset contains the characters that can make up a randomString().
|
||||
const randomCharset = "01234567890abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ-"
|
||||
|
||||
// PredictableRandom is an RNG that will always have the same sequence. It
|
||||
// will be seeded with the device ID during startup, so that the sequence is
|
||||
// predictable but varies between instances.
|
||||
var PredictableRandom = mathRand.New(mathRand.NewSource(42))
|
||||
|
||||
func init() {
|
||||
// The default RNG should be seeded with something good.
|
||||
mathRand.Seed(RandomInt64())
|
||||
}
|
||||
|
||||
// RandomString returns a string of random characters (taken from
|
||||
// randomCharset) of the specified length.
|
||||
func RandomString(l int) string {
|
||||
|
||||
+1
-20
@@ -6,26 +6,7 @@
|
||||
|
||||
package util
|
||||
|
||||
import (
|
||||
"runtime"
|
||||
"sync"
|
||||
"testing"
|
||||
)
|
||||
|
||||
var predictableRandomTest sync.Once
|
||||
|
||||
func TestPredictableRandom(t *testing.T) {
|
||||
if runtime.GOARCH != "amd64" {
|
||||
t.Skip("Test only for 64 bit platforms; but if it works there, it should work on 32 bit")
|
||||
}
|
||||
predictableRandomTest.Do(func() {
|
||||
// predictable random sequence is predictable
|
||||
e := int64(3440579354231278675)
|
||||
if v := int64(PredictableRandom.Int()); v != e {
|
||||
t.Errorf("Unexpected random value %d != %d", v, e)
|
||||
}
|
||||
})
|
||||
}
|
||||
import "testing"
|
||||
|
||||
func TestSeedFromBytes(t *testing.T) {
|
||||
// should always return the same seed for the same bytes
|
||||
|
||||
+11
-7
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-BEP" "7" "April 08, 2016" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-BEP" "7" "April 14, 2016" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-bep \- Block Exchange Protocol v1
|
||||
.
|
||||
@@ -679,7 +679,7 @@ The \fBFlags\fP field is made up of the following single bit flags:
|
||||
0 1 2 3
|
||||
0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1 2 3 4 5 6 7 8 9 0 1
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
| Reserved |U|S|P|I|D| Unix Perm. & Mode |
|
||||
| Reserved |U|S|P|D|I|R| Unix Perm. & Mode |
|
||||
+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+\-+
|
||||
.ft P
|
||||
.fi
|
||||
@@ -692,7 +692,7 @@ hold the common Unix permission and mode bits. An
|
||||
implementation MAY ignore or interpret these as is suitable on the
|
||||
host operating system.
|
||||
.TP
|
||||
.B Bit 19 ("D")
|
||||
.B Bit 19 ("R")
|
||||
is set when the file has been deleted. The block list
|
||||
SHALL be of length zero and the modification time indicates the time
|
||||
of deletion or, if the time of deletion is not reliably determinable,
|
||||
@@ -703,25 +703,29 @@ is set when the file is invalid and unavailable for
|
||||
synchronization. A peer MAY set this bit to indicate that it can
|
||||
temporarily not serve data for the file.
|
||||
.TP
|
||||
.B Bit 17 ("P")
|
||||
.B Bit 17 ("D")
|
||||
is set when the item represents a directory. The block
|
||||
list SHALL be of length zero.
|
||||
.TP
|
||||
.B Bit 16 ("P")
|
||||
is set when there is no permission information for the
|
||||
file. This is the case when it originates on a file system which
|
||||
does not support permissions. Changes to only permission bits SHOULD
|
||||
be disregarded on files with this bit set. The permissions bits MUST
|
||||
be set to the octal value 0666.
|
||||
.TP
|
||||
.B Bit 16 ("S")
|
||||
.B Bit 15 ("S")
|
||||
is set when the file is a symbolic link. The block list
|
||||
SHALL be of one or more blocks since the target of the symlink is
|
||||
stored within the blocks of the file.
|
||||
.TP
|
||||
.B Bit 15 ("U")
|
||||
.B Bit 14 ("U")
|
||||
is set when the symbolic links target does not exist. On
|
||||
systems where symbolic links have types, this bit being means that
|
||||
the default file symlink SHALL be used. If this bit is unset bit 19
|
||||
will decide the type of symlink to be created.
|
||||
.TP
|
||||
.B Bit 0 through 14
|
||||
.B Bit 0 through 13
|
||||
are reserved for future use and SHALL be set to
|
||||
zero.
|
||||
.UNINDENT
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-CONFIG" "5" "April 08, 2016" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-CONFIG" "5" "April 14, 2016" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-config \- Syncthing Configuration
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "April 08, 2016" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "April 14, 2016" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-device-ids \- Understanding Device IDs
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-EVENT-API" "7" "April 08, 2016" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-EVENT-API" "7" "April 14, 2016" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-event-api \- Event API
|
||||
.
|
||||
|
||||
+1
-1
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-FAQ" "7" "April 08, 2016" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-FAQ" "7" "April 14, 2016" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-faq \- Frequently Asked Questions
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "April 08, 2016" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "April 14, 2016" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-globaldisco \- Global Discovery Protocol v3
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "April 08, 2016" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "April 14, 2016" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-localdisco \- Local Discovery Protocol v3
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-NETWORKING" "7" "April 08, 2016" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-NETWORKING" "7" "April 14, 2016" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-networking \- Firewall Setup
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-RELAY" "7" "April 08, 2016" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-RELAY" "7" "April 14, 2016" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-relay \- Relay Protocol v1
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-REST-API" "7" "April 08, 2016" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-REST-API" "7" "April 14, 2016" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-rest-api \- REST API
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-SECURITY" "7" "April 08, 2016" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-SECURITY" "7" "April 14, 2016" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-security \- Security Principles
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-STIGNORE" "5" "April 08, 2016" "v0.12" "Syncthing"
|
||||
.TH "SYNCTHING-STIGNORE" "5" "April 14, 2016" "v0.12" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-stignore \- Prevent files from being synchronized to other nodes
|
||||
.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user