Compare commits
21
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c6300a5da8 | ||
|
|
9359daa0d9 | ||
|
|
2322e9cff7 | ||
|
|
a876e1e348 | ||
|
|
6a863c8f71 | ||
|
|
392b006b06 | ||
|
|
96289f42b7 | ||
|
|
1b69c2441c | ||
|
|
8ca85a4918 | ||
|
|
2a31031cbc | ||
|
|
d148cd8ccc | ||
|
|
d1cc1828b8 | ||
|
|
069e8cf122 | ||
|
|
45cbcaca6d | ||
|
|
102a2db1f3 | ||
|
|
9f81c85ca7 | ||
|
|
ba4a6fc0c5 | ||
|
|
aa803ce2ff | ||
|
|
a027a60f5d | ||
|
|
270649535e | ||
|
|
cf80ba71f4 |
@@ -11,6 +11,7 @@ Ben Sidhom <bsidhom@gmail.com>
|
||||
Brandon Philips <brandon@ifup.org>
|
||||
Brendan Long <self@brendanlong.com>
|
||||
Caleb Callaway <enlightened.despot@gmail.com>
|
||||
Carsten Hagemann <moter8@gmail.com>
|
||||
Cathryne Linenweaver <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
|
||||
Chris Joel <chris@scriptolo.gy>
|
||||
Colin Kennedy <moshen.colin@gmail.com>
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
AudriusButkevicius <audrius.butkevicius@gmail.com>
|
||||
Cathryne <cathryne.linenweaver@gmail.com> <Cathryne@users.noreply.github.com>
|
||||
KayoticSully <kayoticsully@gmail.com>
|
||||
Moter8 <moter8@gmail.com>
|
||||
Nutomic <me@nutomic.com>
|
||||
Rewt0r <rewt0r@gmx.com> <Rewt0r@users.noreply.github.com>
|
||||
Vilbrekin <vilbrekin@gmail.com>
|
||||
|
||||
@@ -5,18 +5,17 @@ syncthing
|
||||
[](http://godoc.org/github.com/syncthing/syncthing)
|
||||
[](https://www.mozilla.org/MPL/2.0/)
|
||||
|
||||
This is the `syncthing` project. The following are the project goals:
|
||||
This is the `syncthing` project which pursues the following goals:
|
||||
|
||||
1. Define a protocol for synchronization of a folder between a number of
|
||||
collaborating devices. The protocol should be well defined, unambiguous,
|
||||
collaborating devices. This protocol should be well defined, unambiguous,
|
||||
easily understood, free to use, efficient, secure and language neutral.
|
||||
This is the [Block Exchange
|
||||
This is called the [Block Exchange
|
||||
Protocol](https://github.com/syncthing/specs/blob/master/BEPv1.md).
|
||||
|
||||
2. Provide the reference implementation to demonstrate the usability of
|
||||
said protocol. This is the `syncthing` utility. It is the hope that
|
||||
alternative, compatible implementations of the protocol will come to
|
||||
exist.
|
||||
said protocol. This is the `syncthing` utility. We hope that
|
||||
alternative, compatible implementations of the protocol will arrise.
|
||||
|
||||
The two are evolving together; the protocol is not to be considered
|
||||
stable until syncthing 1.0 is released, at which point it is locked down
|
||||
@@ -32,20 +31,20 @@ There are a few examples for keeping syncthing running in the background
|
||||
on your system in [the etc directory](https://github.com/syncthing/syncthing/blob/master/etc).
|
||||
|
||||
There is an IRC channel, `#syncthing` on Freenode, for talking directly
|
||||
to developers and users (when awake and present, etc.).
|
||||
to developers and users.
|
||||
|
||||
Building
|
||||
--------
|
||||
|
||||
Building Syncthing from source is easy, and there's a
|
||||
[guide](https://github.com/syncthing/syncthing/wiki/Building).
|
||||
that describes it for both Unix and Windows.
|
||||
that describes it for both Unix and Windows systems.
|
||||
|
||||
Signed Releases
|
||||
---------------
|
||||
|
||||
As of v0.10.15 and onwards, git tags and release binaries are GPG signed
|
||||
with the key D26E6ED000654A3E (see http://syncthing.net/security.html).
|
||||
with the key D26E6ED000654A3E (see https://syncthing.net/security.html).
|
||||
For release binaries, MD5 and SHA1 checksums are calculated and signed,
|
||||
available in the md5sum.txt.asc and sha1sum.txt.asc files.
|
||||
|
||||
@@ -57,4 +56,4 @@ documentation](https://github.com/syncthing/syncthing/wiki/) is on the
|
||||
Github wiki.
|
||||
|
||||
All code is licensed under the
|
||||
[MPLv2](https://github.com/syncthing/syncthing/blob/master/LICENSE).
|
||||
[MPLv2 License](https://github.com/syncthing/syncthing/blob/master/LICENSE).
|
||||
|
||||
@@ -354,7 +354,12 @@ func folderSummary(m *model.Model, folder string) map[string]interface{} {
|
||||
|
||||
res["inSyncFiles"], res["inSyncBytes"] = globalFiles-needFiles, globalBytes-needBytes
|
||||
|
||||
res["state"], res["stateChanged"] = m.State(folder)
|
||||
var err error
|
||||
res["state"], res["stateChanged"], err = m.State(folder)
|
||||
if err != nil {
|
||||
res["error"] = err.Error()
|
||||
}
|
||||
|
||||
res["version"] = m.CurrentLocalVersion(folder) + m.RemoteLocalVersion(folder)
|
||||
|
||||
ignorePatterns, _, _ := m.GetIgnores(folder)
|
||||
|
||||
@@ -727,7 +727,7 @@ func setupUPnP() {
|
||||
} else {
|
||||
// Set up incoming port forwarding, if necessary and possible
|
||||
port, _ := strconv.Atoi(portStr)
|
||||
igds := upnp.Discover()
|
||||
igds := upnp.Discover(time.Duration(cfg.Options().UPnPTimeoutS) * time.Second)
|
||||
if len(igds) > 0 {
|
||||
// Configure the first discovered IGD only. This is a work-around until we have a better mechanism
|
||||
// for handling multiple IGDs, which will require changes to the global discovery service
|
||||
@@ -739,7 +739,7 @@ func setupUPnP() {
|
||||
} else {
|
||||
l.Infof("Created UPnP port mapping for external port %d on UPnP device %s.", externalPort, igd.FriendlyIdentifier())
|
||||
|
||||
if opts.UPnPRenewal > 0 {
|
||||
if opts.UPnPRenewalM > 0 {
|
||||
go renewUPnP(port)
|
||||
}
|
||||
}
|
||||
@@ -757,7 +757,7 @@ func setupExternalPort(igd *upnp.IGD, port int) int {
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
r := 1024 + predictableRandom.Intn(65535-1024)
|
||||
err := igd.AddPortMapping(upnp.TCP, r, port, fmt.Sprintf("syncthing-%d", r), cfg.Options().UPnPLease*60)
|
||||
err := igd.AddPortMapping(upnp.TCP, r, port, fmt.Sprintf("syncthing-%d", r), cfg.Options().UPnPLeaseM*60)
|
||||
if err == nil {
|
||||
return r
|
||||
}
|
||||
@@ -768,14 +768,16 @@ func setupExternalPort(igd *upnp.IGD, port int) int {
|
||||
func renewUPnP(port int) {
|
||||
for {
|
||||
opts := cfg.Options()
|
||||
time.Sleep(time.Duration(opts.UPnPRenewal) * time.Minute)
|
||||
time.Sleep(time.Duration(opts.UPnPRenewalM) * time.Minute)
|
||||
// Some values might have changed while we were sleeping
|
||||
opts = cfg.Options()
|
||||
|
||||
// Make sure our IGD reference isn't nil
|
||||
if igd == nil {
|
||||
if debugNet {
|
||||
l.Debugln("Undefined IGD during UPnP port renewal. Re-discovering...")
|
||||
}
|
||||
igds := upnp.Discover()
|
||||
igds := upnp.Discover(time.Duration(opts.UPnPTimeoutS) * time.Second)
|
||||
if len(igds) > 0 {
|
||||
// Configure the first discovered IGD only. This is a work-around until we have a better mechanism
|
||||
// for handling multiple IGDs, which will require changes to the global discovery service
|
||||
@@ -790,7 +792,7 @@ func renewUPnP(port int) {
|
||||
|
||||
// Just renew the same port that we already have
|
||||
if externalPort != 0 {
|
||||
err := igd.AddPortMapping(upnp.TCP, externalPort, port, "syncthing", opts.UPnPLease*60)
|
||||
err := igd.AddPortMapping(upnp.TCP, externalPort, port, "syncthing", opts.UPnPLeaseM*60)
|
||||
if err != nil {
|
||||
l.Warnf("Error renewing UPnP port mapping for external port %d on device %s: %s", externalPort, igd.FriendlyIdentifier(), err.Error())
|
||||
} else if debugNet {
|
||||
@@ -1019,7 +1021,7 @@ func cleanConfigDirectory() {
|
||||
}
|
||||
|
||||
for _, file := range files {
|
||||
info, err := os.Lstat(file)
|
||||
info, err := osutil.Lstat(file)
|
||||
if err != nil {
|
||||
l.Infoln("Cleaning:", err)
|
||||
continue
|
||||
|
||||
@@ -42,6 +42,7 @@ func TestFolderErrors(t *testing.T) {
|
||||
|
||||
m := model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
|
||||
m.AddFolder(fcfg)
|
||||
m.StartFolderRW("folder")
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err != nil {
|
||||
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
|
||||
@@ -69,6 +70,7 @@ func TestFolderErrors(t *testing.T) {
|
||||
|
||||
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
|
||||
m.AddFolder(fcfg)
|
||||
m.StartFolderRW("folder")
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err != nil {
|
||||
t.Error("Unexpected error", cfg.Folders()["folder"].Invalid)
|
||||
@@ -90,8 +92,9 @@ func TestFolderErrors(t *testing.T) {
|
||||
|
||||
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
|
||||
m.AddFolder(fcfg)
|
||||
m.StartFolderRW("folder")
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "Folder marker missing" {
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "folder marker missing" {
|
||||
t.Error("Incorrect error: Folder marker missing !=", m.CheckFolderHealth("folder"))
|
||||
}
|
||||
|
||||
@@ -117,8 +120,9 @@ func TestFolderErrors(t *testing.T) {
|
||||
|
||||
m = model.NewModel(cfg, protocol.LocalDeviceID, "device", "syncthing", "dev", ldb)
|
||||
m.AddFolder(fcfg)
|
||||
m.StartFolderRW("folder")
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "Folder path missing" {
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "folder path missing" {
|
||||
t.Error("Incorrect error: Folder path missing !=", m.CheckFolderHealth("folder"))
|
||||
}
|
||||
|
||||
@@ -126,7 +130,7 @@ func TestFolderErrors(t *testing.T) {
|
||||
|
||||
os.Mkdir("testdata/testfolder", 0700)
|
||||
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "Folder marker missing" {
|
||||
if err := m.CheckFolderHealth("folder"); err == nil || err.Error() != "folder marker missing" {
|
||||
t.Error("Incorrect error: Folder marker missing !=", m.CheckFolderHealth("folder"))
|
||||
}
|
||||
|
||||
|
||||
+16
-12
@@ -66,21 +66,25 @@ func (c *folderSummarySvc) listenForUpdates() {
|
||||
data := ev.Data.(map[string]interface{})
|
||||
folder := data["folder"].(string)
|
||||
|
||||
if ev.Type == events.StateChanged && data["to"].(string) == "idle" && data["from"].(string) == "syncing" {
|
||||
// The folder changed to idle from syncing. We should do an
|
||||
// immediate refresh to update the GUI. The send to
|
||||
// c.immediate must be nonblocking so that we can continue
|
||||
// handling events.
|
||||
switch ev.Type {
|
||||
case events.StateChanged:
|
||||
if data["to"].(string) == "idle" && data["from"].(string) == "syncing" {
|
||||
// The folder changed to idle from syncing. We should do an
|
||||
// immediate refresh to update the GUI. The send to
|
||||
// c.immediate must be nonblocking so that we can continue
|
||||
// handling events.
|
||||
|
||||
select {
|
||||
case c.immediate <- folder:
|
||||
c.foldersMut.Lock()
|
||||
delete(c.folders, folder)
|
||||
c.foldersMut.Unlock()
|
||||
select {
|
||||
case c.immediate <- folder:
|
||||
c.foldersMut.Lock()
|
||||
delete(c.folders, folder)
|
||||
c.foldersMut.Unlock()
|
||||
|
||||
default:
|
||||
default:
|
||||
}
|
||||
}
|
||||
} else {
|
||||
|
||||
default:
|
||||
// This folder needs to be refreshed whenever we do the next
|
||||
// refresh.
|
||||
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
"Automatic upgrades": "automatische Updates",
|
||||
"Bugs": "Fehler",
|
||||
"CPU Utilization": "Prozessorauslastung",
|
||||
"Changelog": "Versionsinfo",
|
||||
"Changelog": "Änderungsprotokoll",
|
||||
"Close": "Schließen",
|
||||
"Command": "Kommando",
|
||||
"Comment, when used at the start of a line": "Kommentar, wenn am Anfang der Zeile benutzt.",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"Connection Error": "Errore di Connessione",
|
||||
"Copied from elsewhere": "Copiato da qualche altra parte",
|
||||
"Copied from original": "Copiato dall'originale",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 i seguenti Collaboratori:",
|
||||
"Delete": "Elimina",
|
||||
"Device ID": "ID Dispositivo",
|
||||
"Device Identification": "Identificazione Dispositivo",
|
||||
@@ -45,8 +45,8 @@
|
||||
"Error": "Errore",
|
||||
"External File Versioning": "Controllo Versione Esterno",
|
||||
"File Versioning": "Controllo Versione dei File",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Il software evita i bit dei permessi dei file durante il controllo delle modifiche. Utilizzato nei filesystem FAT.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "I file sostituiti o eliminati da Syncthing vengono datati e spostati in una cartella .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.": "I file sono protetti dalle modifiche effettuate negli altri dispositivi, ma le modifiche effettuate in questo dispositivo verranno inviate anche al resto del cluster.",
|
||||
"Folder ID": "ID Cartella",
|
||||
"Folder Master": "Cartella Principale",
|
||||
@@ -132,7 +132,7 @@
|
||||
"Syncthing is restarting.": "Riavvio di Syncthing in corso.",
|
||||
"Syncthing is upgrading.": "Aggiornamento di Syncthing in corso.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing sembra inattivo, oppure c'è un problema con la tua connessione a Internet. Nuovo tentativo…",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "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.": "Sembra che Syncthing non sia in grado di elaborare il tuo comando. Se il problema persiste prova a ricaricare la pagina nel tuo navigatore oppure prova a riavviare Syncthing.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Le statistiche aggregate sono disponibili pubblicamente su {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "La configurazione è stata salvata ma non attivata. Devi riavviare Syncthing per attivare la nuova configurazione.",
|
||||
"The device ID cannot be blank.": "L'ID del dispositivo non può essere vuoto.",
|
||||
|
||||
@@ -23,7 +23,7 @@
|
||||
"Connection Error": "Bağlantı hatası",
|
||||
"Copied from elsewhere": "Başka bir yerden kopyalanmış",
|
||||
"Copied from original": "Aslından kopyalanmış",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Telif Hakkı © 2015 Katkıda bulunanlar:",
|
||||
"Delete": "Sil",
|
||||
"Device ID": "Cihaz ID",
|
||||
"Device Identification": "Cihaz Kimliği",
|
||||
@@ -43,10 +43,10 @@
|
||||
"Enter comma separated \"ip:port\" addresses or \"dynamic\" to perform automatic discovery of the address.": "IP adresleri eklemek için virgül ile ayırarak \"ip:port\" yazın, ya da \"dynamic\" yazarak otomatik bulma işlemini seçin.",
|
||||
"Enter ignore patterns, one per line.": "Yoksayılacak kalıp dizilerini her satıra bir tane olacak şekilde girin.",
|
||||
"Error": "Hata",
|
||||
"External File Versioning": "External File Versioning",
|
||||
"External File Versioning": "Harici Dosya Sürümlendirme",
|
||||
"File Versioning": "Dosya Sürümlendirme",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Değişimleri yoklarken dosya izin bilgilerini ihmal et. FAT dosya sistemlerinde kullanın.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dosyalar Syncthing tarafından değiştirildiğinde ya da silindiğinde, tarih damgalı sürümleri .stversions dizinine taşınır.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dosyalar diğer cihazlarda yapılan değişikliklerden korunur, ancak bu cihazdaki değişiklikler kümedeki diğer cihazlara gönderilir.",
|
||||
"Folder ID": "Klasör ID",
|
||||
"Folder Master": "Ana Klasör",
|
||||
@@ -132,7 +132,7 @@
|
||||
"Syncthing is restarting.": "Syncthing yeniden başlatılıyor.",
|
||||
"Syncthing is upgrading.": "Syncthing yükseltiliyor.",
|
||||
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing görünüşe durdu veya internetin bağlantınızda problem var. Tekrar deniyor....",
|
||||
"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.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing isteminizi işleme alırken bir sorunla karşılaştı. Lütfen sayfanızı yenileyin veya sorun devam ediyorsa Syncthing'i yeniden başlatın.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Toplanan halka açık istatistiklere ulaşabileceğiniz adres {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Ayarlar kaydedildi ancak aktifleştirilmedi. Aktifleştirmek için Syncthing yeniden başlatılmalı.",
|
||||
"The device ID cannot be blank.": "Cihaz ID boş olamaz.",
|
||||
@@ -149,7 +149,7 @@
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Bir sürümün tutulması için belirlenen azami süre (sürümleri daimi olarak tutabilmek için 0 değeri atayın)",
|
||||
"The number of old versions to keep, per file.": "Dosya başına saklanacak eski sürüm.",
|
||||
"The number of versions must be a number and cannot be blank.": "Sürümlerin sayısı sayı olmalı ve boş bırakılamaz.",
|
||||
"The path cannot be blank.": "The path cannot be blank.",
|
||||
"The path cannot be blank.": "Dizin yolu boş bırakılamaz.",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Tarama zaman aralığı, saniye cinsinden negatif olmayan bir sayı olmalıdır.",
|
||||
"Unknown": "Bilinmiyor",
|
||||
"Unshared": "Paylaşılmayan",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"Addresses": "Адреси",
|
||||
"All Data": "Усі дані",
|
||||
"Allow Anonymous Usage Reporting?": "Дозволити програмі збирати анонімну статистику використання?",
|
||||
"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.",
|
||||
"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": "Автоматичні оновлення",
|
||||
@@ -23,7 +23,7 @@
|
||||
"Connection Error": "Помилка з’єднання",
|
||||
"Copied from elsewhere": "Скопійовано з іншого місця",
|
||||
"Copied from original": "Скопійовано з оригіналу",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 the following Contributors:",
|
||||
"Copyright © 2015 the following Contributors:": "Copyright © 2015 наступних контриб’юторів:",
|
||||
"Delete": "Видалити",
|
||||
"Device ID": "ID пристрою",
|
||||
"Device Identification": "Ідентифікатор пристрою",
|
||||
@@ -45,8 +45,8 @@
|
||||
"Error": "Помилка",
|
||||
"External File Versioning": "Зовнішне керування версіями",
|
||||
"File Versioning": "Керування версіями",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "File permission bits are ignored when looking for changes. Use on FAT file systems.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Біти прав доступу до файлів будуть проігноровані під час пошуку змін. Використовуйте на файлових системах FAT.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Файли будуть поміщатися у директорію .stversions із відповідною позначкою часу, коли вони будуть замінятися або видалятися програмою.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Файли захищено від змін зроблених на інших пристроях, але зміни зроблені на цьому пристрої будуть надіслані решті кластеру.",
|
||||
"Folder ID": "ID директорії",
|
||||
"Folder Master": "Центральна директорія",
|
||||
@@ -132,7 +132,7 @@
|
||||
"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 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.",
|
||||
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Схоже на те, що Syncthing стикнувся з проблемою оброблюючи ваш запит. Будь ласка перезавантажте сторінку в браузері або перезапустіть Syncthing.",
|
||||
"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.": "ID пристрою не може бути порожнім.",
|
||||
|
||||
+3
-2
@@ -193,9 +193,9 @@
|
||||
<th><span class="glyphicon glyphicon-folder-open"></span> <span translate>Folder Path</span></th>
|
||||
<td class="text-right">{{folder.path}}</td>
|
||||
</tr>
|
||||
<tr ng-if="model[folder.id].invalid">
|
||||
<tr ng-if="model[folder.id].invalid || model[folder.id].error">
|
||||
<th><span class="glyphicon glyphicon-warning-sign"></span> <span translate>Error</span></th>
|
||||
<td class="text-right">{{model[folder.id].invalid}}</td>
|
||||
<td class="text-right">{{model[folder.id].invalid || model[folder.id].error}}</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><span class="glyphicon glyphicon-globe"></span> <span translate>Global State</span></th>
|
||||
@@ -963,6 +963,7 @@
|
||||
<li class="auto-generated">Brandon Philips</li>
|
||||
<li class="auto-generated">Brendan Long</li>
|
||||
<li class="auto-generated">Caleb Callaway</li>
|
||||
<li class="auto-generated">Carsten Hagemann</li>
|
||||
<li class="auto-generated">Cathryne Linenweaver</li>
|
||||
<li class="auto-generated">Chris Joel</li>
|
||||
<li class="auto-generated">Colin Kennedy</li>
|
||||
|
||||
@@ -461,10 +461,14 @@ angular.module('syncthing.core')
|
||||
return 'unshared';
|
||||
}
|
||||
|
||||
if ($scope.model[folderCfg.id].invalid !== '') {
|
||||
if ($scope.model[folderCfg.id].invalid) {
|
||||
return 'stopped';
|
||||
}
|
||||
|
||||
if ($scope.model[folderCfg.id].state == 'error') {
|
||||
return 'stopped'; // legacy, the state is called "stopped" in the GUI
|
||||
}
|
||||
|
||||
return '' + $scope.model[folderCfg.id].state;
|
||||
};
|
||||
|
||||
@@ -494,6 +498,9 @@ angular.module('syncthing.core')
|
||||
if (state == 'scanning') {
|
||||
return 'primary';
|
||||
}
|
||||
if (state == 'error') {
|
||||
return 'danger';
|
||||
}
|
||||
return 'info';
|
||||
};
|
||||
|
||||
|
||||
@@ -2,8 +2,23 @@ angular.module('syncthing.core')
|
||||
.provider('LocaleService', function () {
|
||||
'use strict';
|
||||
|
||||
function detectLocalStorage() {
|
||||
// Feature detect localStorage; https://mathiasbynens.be/notes/localstorage-pattern
|
||||
try {
|
||||
var uid = new Date();
|
||||
var storage = window.localStorage;
|
||||
storage.setItem(uid, uid);
|
||||
var success = storage.getItem(uid) == uid;
|
||||
storage.removeItem(uid);
|
||||
return storage;
|
||||
} catch (exception) {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
var _defaultLocale,
|
||||
_availableLocales;
|
||||
_availableLocales,
|
||||
_localStorage = detectLocalStorage();
|
||||
|
||||
var _SYNLANG = "SYN_LANG"; // const key for localStorage
|
||||
|
||||
@@ -18,6 +33,7 @@ angular.module('syncthing.core')
|
||||
_availableLocales = locales;
|
||||
};
|
||||
|
||||
|
||||
this.$get = ['$http', '$translate', '$location', function ($http, $translate, $location) {
|
||||
|
||||
/**
|
||||
@@ -33,7 +49,10 @@ angular.module('syncthing.core')
|
||||
|
||||
function autoConfigLocale() {
|
||||
var params = $location.search();
|
||||
var savedLang = typeof(localStorage) != 'undefined' && localStorage[_SYNLANG];
|
||||
var savedLang;
|
||||
if (_localStorage) {
|
||||
savedLang = _localStorage[_SYNLANG];
|
||||
}
|
||||
|
||||
if(params.lang) {
|
||||
useLocale(params.lang, true);
|
||||
@@ -84,8 +103,8 @@ angular.module('syncthing.core')
|
||||
function useLocale(language, save2Storage) {
|
||||
if (language) {
|
||||
$translate.use(language).then(function () {
|
||||
if (save2Storage && typeof(localStorage) != 'undefined')
|
||||
localStorage[_SYNLANG] = language;
|
||||
if (save2Storage && _localStorage)
|
||||
_localStorage[_SYNLANG] = language;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -227,8 +227,9 @@ type OptionsConfiguration struct {
|
||||
ReconnectIntervalS int `xml:"reconnectionIntervalS" json:"reconnectionIntervalS" default:"60"`
|
||||
StartBrowser bool `xml:"startBrowser" json:"startBrowser" default:"true"`
|
||||
UPnPEnabled bool `xml:"upnpEnabled" json:"upnpEnabled" default:"true"`
|
||||
UPnPLease int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"0"`
|
||||
UPnPRenewal int `xml:"upnpRenewalMinutes" json:"upnpRenewalMinutes" default:"30"`
|
||||
UPnPLeaseM int `xml:"upnpLeaseMinutes" json:"upnpLeaseMinutes" default:"0"`
|
||||
UPnPRenewalM int `xml:"upnpRenewalMinutes" json:"upnpRenewalMinutes" default:"30"`
|
||||
UPnPTimeoutS int `xml:"upnpTimeoutSeconds" json:"upnpTimeoutSeconds" default:"3"`
|
||||
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.
|
||||
RestartOnWakeup bool `xml:"restartOnWakeup" json:"restartOnWakeup" default:"true"`
|
||||
|
||||
@@ -42,8 +42,9 @@ func TestDefaultValues(t *testing.T) {
|
||||
ReconnectIntervalS: 60,
|
||||
StartBrowser: true,
|
||||
UPnPEnabled: true,
|
||||
UPnPLease: 0,
|
||||
UPnPRenewal: 30,
|
||||
UPnPLeaseM: 0,
|
||||
UPnPRenewalM: 30,
|
||||
UPnPTimeoutS: 3,
|
||||
RestartOnWakeup: true,
|
||||
AutoUpgradeIntervalH: 12,
|
||||
KeepTemporariesH: 24,
|
||||
@@ -147,8 +148,9 @@ func TestOverriddenValues(t *testing.T) {
|
||||
ReconnectIntervalS: 6000,
|
||||
StartBrowser: false,
|
||||
UPnPEnabled: false,
|
||||
UPnPLease: 60,
|
||||
UPnPRenewal: 15,
|
||||
UPnPLeaseM: 60,
|
||||
UPnPRenewalM: 15,
|
||||
UPnPTimeoutS: 15,
|
||||
RestartOnWakeup: false,
|
||||
AutoUpgradeIntervalH: 24,
|
||||
KeepTemporariesH: 48,
|
||||
|
||||
@@ -15,6 +15,7 @@
|
||||
<upnpEnabled>false</upnpEnabled>
|
||||
<upnpLeaseMinutes>60</upnpLeaseMinutes>
|
||||
<upnpRenewalMinutes>15</upnpRenewalMinutes>
|
||||
<upnpTimeoutSeconds>15</upnpTimeoutSeconds>
|
||||
<restartOnWakeup>false</restartOnWakeup>
|
||||
<autoUpgradeIntervalH>24</autoUpgradeIntervalH>
|
||||
<keepTemporariesH>48</keepTemporariesH>
|
||||
|
||||
@@ -215,29 +215,6 @@ func (w *Wrapper) SetGUI(gui GUIConfiguration) {
|
||||
w.replaces <- w.cfg.Copy()
|
||||
}
|
||||
|
||||
// Sets the folder error state. Emits ConfigSaved to cause a GUI refresh.
|
||||
func (w *Wrapper) SetFolderError(id string, err error) {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
w.folderMap = nil
|
||||
|
||||
for i := range w.cfg.Folders {
|
||||
if w.cfg.Folders[i].ID == id {
|
||||
errstr := ""
|
||||
if err != nil {
|
||||
errstr = err.Error()
|
||||
}
|
||||
if errstr != w.cfg.Folders[i].Invalid {
|
||||
w.cfg.Folders[i].Invalid = errstr
|
||||
events.Default.Log(events.ConfigSaved, w.cfg)
|
||||
w.replaces <- w.cfg.Copy()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Returns whether or not connection attempts from the given device should be
|
||||
// silently ignored.
|
||||
func (w *Wrapper) IgnoredDevice(id protocol.DeviceID) bool {
|
||||
|
||||
@@ -13,15 +13,6 @@ type FileInfoTruncated struct {
|
||||
ActualSize int64
|
||||
}
|
||||
|
||||
func ToTruncated(file protocol.FileInfo) FileInfoTruncated {
|
||||
t := FileInfoTruncated{
|
||||
FileInfo: file,
|
||||
ActualSize: file.Size(),
|
||||
}
|
||||
t.FileInfo.Blocks = nil
|
||||
return t
|
||||
}
|
||||
|
||||
func (f *FileInfoTruncated) UnmarshalXDR(bs []byte) error {
|
||||
err := f.FileInfo.UnmarshalXDR(bs)
|
||||
f.ActualSize = f.FileInfo.Size()
|
||||
|
||||
@@ -1,17 +1,8 @@
|
||||
// Copyright (C) 2015 The Syncthing Authors.
|
||||
//
|
||||
// This program is free software: you can redistribute it and/or modify it
|
||||
// under the terms of the GNU General Public License as published by the Free
|
||||
// Software Foundation, either version 3 of the License, or (at your option)
|
||||
// any later version.
|
||||
//
|
||||
// This program is distributed in the hope that it will be useful, but WITHOUT
|
||||
// ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
|
||||
// FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
|
||||
// more details.
|
||||
//
|
||||
// You should have received a copy of the GNU General Public License along
|
||||
// with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
// 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
|
||||
|
||||
@@ -28,7 +19,7 @@ const (
|
||||
FolderIdle folderState = iota
|
||||
FolderScanning
|
||||
FolderSyncing
|
||||
FolderCleaning
|
||||
FolderError
|
||||
)
|
||||
|
||||
func (s folderState) String() string {
|
||||
@@ -37,10 +28,10 @@ func (s folderState) String() string {
|
||||
return "idle"
|
||||
case FolderScanning:
|
||||
return "scanning"
|
||||
case FolderCleaning:
|
||||
return "cleaning"
|
||||
case FolderSyncing:
|
||||
return "syncing"
|
||||
case FolderError:
|
||||
return "error"
|
||||
default:
|
||||
return "unknown"
|
||||
}
|
||||
@@ -51,10 +42,16 @@ type stateTracker struct {
|
||||
|
||||
mut sync.Mutex
|
||||
current folderState
|
||||
err error
|
||||
changed time.Time
|
||||
}
|
||||
|
||||
// setState sets the new folder state, for states other than FolderError.
|
||||
func (s *stateTracker) setState(newState folderState) {
|
||||
if newState == FolderError {
|
||||
panic("must use setError")
|
||||
}
|
||||
|
||||
s.mut.Lock()
|
||||
if newState != s.current {
|
||||
/* This should hold later...
|
||||
@@ -74,6 +71,7 @@ func (s *stateTracker) setState(newState folderState) {
|
||||
}
|
||||
|
||||
s.current = newState
|
||||
s.err = nil
|
||||
s.changed = time.Now()
|
||||
|
||||
events.Default.Log(events.StateChanged, eventData)
|
||||
@@ -81,9 +79,35 @@ func (s *stateTracker) setState(newState folderState) {
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
func (s *stateTracker) getState() (current folderState, changed time.Time) {
|
||||
// getState returns the current state, the time when it last changed, and the
|
||||
// current error or nil.
|
||||
func (s *stateTracker) getState() (current folderState, changed time.Time, err error) {
|
||||
s.mut.Lock()
|
||||
current, changed = s.current, s.changed
|
||||
current, changed, err = s.current, s.changed, s.err
|
||||
s.mut.Unlock()
|
||||
return
|
||||
}
|
||||
|
||||
// setError sets the folder state to FolderError with the specified error.
|
||||
func (s *stateTracker) setError(err error) {
|
||||
s.mut.Lock()
|
||||
if s.current != FolderError || s.err.Error() != err.Error() {
|
||||
eventData := map[string]interface{}{
|
||||
"folder": s.folder,
|
||||
"to": FolderError.String(),
|
||||
"from": s.current.String(),
|
||||
"error": err.Error(),
|
||||
}
|
||||
|
||||
if !s.changed.IsZero() {
|
||||
eventData["duration"] = time.Since(s.changed).Seconds()
|
||||
}
|
||||
|
||||
s.current = FolderError
|
||||
s.err = err
|
||||
s.changed = time.Now()
|
||||
|
||||
events.Default.Log(events.StateChanged, eventData)
|
||||
}
|
||||
s.mut.Unlock()
|
||||
}
|
||||
|
||||
+40
-45
@@ -48,8 +48,9 @@ type service interface {
|
||||
Jobs() ([]string, []string) // In progress, Queued
|
||||
BringToFront(string)
|
||||
|
||||
setState(folderState)
|
||||
getState() (folderState, time.Time)
|
||||
setState(state folderState)
|
||||
setError(err error)
|
||||
getState() (folderState, time.Time, error)
|
||||
}
|
||||
|
||||
type Model struct {
|
||||
@@ -1083,13 +1084,13 @@ func (m *Model) AddFolder(cfg config.FolderConfiguration) {
|
||||
|
||||
func (m *Model) ScanFolders() map[string]error {
|
||||
m.fmut.RLock()
|
||||
var folders = make([]string, 0, len(m.folderCfgs))
|
||||
folders := make([]string, 0, len(m.folderCfgs))
|
||||
for folder := range m.folderCfgs {
|
||||
folders = append(folders, folder)
|
||||
}
|
||||
m.fmut.RUnlock()
|
||||
|
||||
var errors = make(map[string]error, len(m.folderCfgs))
|
||||
errors := make(map[string]error, len(m.folderCfgs))
|
||||
var errorsMut sync.Mutex
|
||||
|
||||
var wg sync.WaitGroup
|
||||
@@ -1102,11 +1103,15 @@ func (m *Model) ScanFolders() map[string]error {
|
||||
errorsMut.Lock()
|
||||
errors[folder] = err
|
||||
errorsMut.Unlock()
|
||||
|
||||
// Potentially sets the error twice, once in the scanner just
|
||||
// by doing a check, and once here, if the error returned is
|
||||
// the same one as returned by CheckFolderHealth, though
|
||||
// duplicate set is handled by SetFolderError
|
||||
m.cfg.SetFolderError(folder, err)
|
||||
// duplicate set is handled by setError.
|
||||
m.fmut.RLock()
|
||||
srv := m.folderRunners[folder]
|
||||
m.fmut.RUnlock()
|
||||
srv.setError(err)
|
||||
}
|
||||
wg.Done()
|
||||
}()
|
||||
@@ -1182,13 +1187,13 @@ nextSub:
|
||||
}
|
||||
|
||||
runner.setState(FolderScanning)
|
||||
defer runner.setState(FolderIdle)
|
||||
fchan, err := w.Walk()
|
||||
|
||||
fchan, err := w.Walk()
|
||||
if err != nil {
|
||||
m.cfg.SetFolderError(folder, err)
|
||||
runner.setError(err)
|
||||
return err
|
||||
}
|
||||
|
||||
batchSize := 100
|
||||
batch := make([]protocol.FileInfo, 0, batchSize)
|
||||
for f := range fchan {
|
||||
@@ -1266,7 +1271,7 @@ nextSub:
|
||||
"size": f.Size(),
|
||||
})
|
||||
batch = append(batch, nf)
|
||||
} else if _, err := os.Lstat(filepath.Join(folderCfg.Path(), f.Name)); err != nil {
|
||||
} else if _, err := osutil.Lstat(filepath.Join(folderCfg.Path(), f.Name)); err != nil {
|
||||
// File has been deleted.
|
||||
|
||||
// We don't specifically verify that the error is
|
||||
@@ -1298,6 +1303,7 @@ nextSub:
|
||||
fs.Update(protocol.LocalDeviceID, batch)
|
||||
}
|
||||
|
||||
runner.setState(FolderIdle)
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1340,15 +1346,18 @@ func (m *Model) clusterConfig(device protocol.DeviceID) protocol.ClusterConfigMe
|
||||
return cm
|
||||
}
|
||||
|
||||
func (m *Model) State(folder string) (string, time.Time) {
|
||||
func (m *Model) State(folder string) (string, time.Time, error) {
|
||||
m.fmut.RLock()
|
||||
runner, ok := m.folderRunners[folder]
|
||||
m.fmut.RUnlock()
|
||||
if !ok {
|
||||
return "", time.Time{}
|
||||
// The returned error should be an actual folder error, so returning
|
||||
// errors.New("does not exist") or similar here would be
|
||||
// inappropriate.
|
||||
return "", time.Time{}, nil
|
||||
}
|
||||
state, changed := runner.getState()
|
||||
return state.String(), changed
|
||||
state, changed, err := runner.getState()
|
||||
return state.String(), changed, err
|
||||
}
|
||||
|
||||
func (m *Model) Override(folder string) {
|
||||
@@ -1528,7 +1537,7 @@ func (m *Model) BringToFront(folder, file string) {
|
||||
func (m *Model) CheckFolderHealth(id string) error {
|
||||
folder, ok := m.cfg.Folders()[id]
|
||||
if !ok {
|
||||
return errors.New("Folder does not exist")
|
||||
return errors.New("folder does not exist")
|
||||
}
|
||||
|
||||
fi, err := os.Stat(folder.Path())
|
||||
@@ -1538,9 +1547,9 @@ func (m *Model) CheckFolderHealth(id string) error {
|
||||
// that all files have been deleted which might not be the case,
|
||||
// so mark it as invalid instead.
|
||||
if err != nil || !fi.IsDir() {
|
||||
err = errors.New("Folder path missing")
|
||||
err = errors.New("folder path missing")
|
||||
} else if !folder.HasMarker() {
|
||||
err = errors.New("Folder marker missing")
|
||||
err = errors.New("folder marker missing")
|
||||
}
|
||||
} else if os.IsNotExist(err) {
|
||||
// If we don't have any files in the index, and the directory
|
||||
@@ -1555,35 +1564,21 @@ func (m *Model) CheckFolderHealth(id string) error {
|
||||
err = folder.CreateMarker()
|
||||
}
|
||||
|
||||
if err == nil {
|
||||
if folder.Invalid != "" {
|
||||
l.Infof("Starting folder %q after error %q", folder.ID, folder.Invalid)
|
||||
m.cfg.SetFolderError(id, nil)
|
||||
m.fmut.RLock()
|
||||
runner := m.folderRunners[folder.ID]
|
||||
m.fmut.RUnlock()
|
||||
_, _, oldErr := runner.getState()
|
||||
|
||||
if err != nil {
|
||||
if oldErr != nil && oldErr.Error() != err.Error() {
|
||||
l.Infof("Folder %q error changed: %q -> %q", folder.ID, oldErr, err)
|
||||
} else if oldErr == nil {
|
||||
l.Warnf("Stopping folder %q - %v", folder.ID, err)
|
||||
}
|
||||
|
||||
if folder, ok := m.cfg.Folders()[id]; !ok || folder.Invalid != "" {
|
||||
panic("Unable to unset folder \"" + id + "\" error.")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
if folder.Invalid == err.Error() {
|
||||
return err
|
||||
}
|
||||
|
||||
// folder is a copy of the original struct, hence Invalid value is
|
||||
// preserved after the set.
|
||||
m.cfg.SetFolderError(id, err)
|
||||
|
||||
if folder.Invalid == "" {
|
||||
l.Warnf("Stopping folder %q - %v", folder.ID, err)
|
||||
} else {
|
||||
l.Infof("Folder %q error changed: %q -> %q", folder.ID, folder.Invalid, err)
|
||||
}
|
||||
|
||||
if folder, ok := m.cfg.Folders()[id]; !ok || folder.Invalid != err.Error() {
|
||||
panic("Unable to set folder \"" + id + "\" error.")
|
||||
runner.setError(err)
|
||||
} else if oldErr != nil {
|
||||
l.Infof("Folder %q error is cleared, restarting", folder.ID)
|
||||
runner.setState(FolderIdle)
|
||||
}
|
||||
|
||||
return err
|
||||
|
||||
@@ -621,21 +621,25 @@ func TestROScanRecovery(t *testing.T) {
|
||||
if time.Now().After(timeout) {
|
||||
return fmt.Errorf("Timed out waiting for status: %s, current status: %s", status, m.cfg.Folders()["default"].Invalid)
|
||||
}
|
||||
if m.cfg.Folders()["default"].Invalid == status {
|
||||
_, _, err := m.State("default")
|
||||
if err == nil && status == "" {
|
||||
return nil
|
||||
}
|
||||
if err != nil && err.Error() == status {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
if err := waitFor("Folder path missing"); err != nil {
|
||||
if err := waitFor("folder path missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
os.Mkdir(fcfg.RawPath, 0700)
|
||||
|
||||
if err := waitFor("Folder marker missing"); err != nil {
|
||||
if err := waitFor("folder marker missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
@@ -654,14 +658,14 @@ func TestROScanRecovery(t *testing.T) {
|
||||
|
||||
os.Remove(filepath.Join(fcfg.RawPath, ".stfolder"))
|
||||
|
||||
if err := waitFor("Folder marker missing"); err != nil {
|
||||
if err := waitFor("folder marker missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
os.Remove(fcfg.RawPath)
|
||||
|
||||
if err := waitFor("Folder path missing"); err != nil {
|
||||
if err := waitFor("folder path missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
@@ -701,21 +705,25 @@ func TestRWScanRecovery(t *testing.T) {
|
||||
if time.Now().After(timeout) {
|
||||
return fmt.Errorf("Timed out waiting for status: %s, current status: %s", status, m.cfg.Folders()["default"].Invalid)
|
||||
}
|
||||
if m.cfg.Folders()["default"].Invalid == status {
|
||||
_, _, err := m.State("default")
|
||||
if err == nil && status == "" {
|
||||
return nil
|
||||
}
|
||||
if err != nil && err.Error() == status {
|
||||
return nil
|
||||
}
|
||||
time.Sleep(10 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
|
||||
if err := waitFor("Folder path missing"); err != nil {
|
||||
if err := waitFor("folder path missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
os.Mkdir(fcfg.RawPath, 0700)
|
||||
|
||||
if err := waitFor("Folder marker missing"); err != nil {
|
||||
if err := waitFor("folder marker missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
@@ -734,14 +742,14 @@ func TestRWScanRecovery(t *testing.T) {
|
||||
|
||||
os.Remove(filepath.Join(fcfg.RawPath, ".stfolder"))
|
||||
|
||||
if err := waitFor("Folder marker missing"); err != nil {
|
||||
if err := waitFor("folder marker missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
os.Remove(fcfg.RawPath)
|
||||
|
||||
if err := waitFor("Folder path missing"); err != nil {
|
||||
if err := waitFor("folder path missing"); err != nil {
|
||||
t.Error(err)
|
||||
return
|
||||
}
|
||||
|
||||
@@ -67,8 +67,8 @@ func (s *roFolder) Serve() {
|
||||
// Potentially sets the error twice, once in the scanner just
|
||||
// by doing a check, and once here, if the error returned is
|
||||
// the same one as returned by CheckFolderHealth, though
|
||||
// duplicate set is handled by SetFolderError
|
||||
s.model.cfg.SetFolderError(s.folder, err)
|
||||
// duplicate set is handled by setError.
|
||||
s.setError(err)
|
||||
reschedule()
|
||||
continue
|
||||
}
|
||||
|
||||
+45
-22
@@ -245,8 +245,8 @@ func (p *rwFolder) Serve() {
|
||||
// Potentially sets the error twice, once in the scanner just
|
||||
// by doing a check, and once here, if the error returned is
|
||||
// the same one as returned by CheckFolderHealth, though
|
||||
// duplicate set is handled by SetFolderError
|
||||
p.model.cfg.SetFolderError(p.folder, err)
|
||||
// duplicate set is handled by setError.
|
||||
p.setError(err)
|
||||
rescheduleScan()
|
||||
continue
|
||||
}
|
||||
@@ -476,15 +476,19 @@ nextFile:
|
||||
func (p *rwFolder) handleDir(file protocol.FileInfo) {
|
||||
var err error
|
||||
events.Default.Log(events.ItemStarted, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"details": db.ToTruncated(file),
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"type": "dir",
|
||||
"action": "update",
|
||||
})
|
||||
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"error": err,
|
||||
"type": "dir",
|
||||
"action": "update",
|
||||
})
|
||||
}()
|
||||
|
||||
@@ -499,7 +503,7 @@ func (p *rwFolder) handleDir(file protocol.FileInfo) {
|
||||
l.Debugf("need dir\n\t%v\n\t%v", file, curFile)
|
||||
}
|
||||
|
||||
info, err := os.Lstat(realName)
|
||||
info, err := osutil.Lstat(realName)
|
||||
switch {
|
||||
// There is already something under that name, but it's a file/link.
|
||||
// Most likely a file/link is getting replaced with a directory.
|
||||
@@ -555,15 +559,18 @@ func (p *rwFolder) handleDir(file protocol.FileInfo) {
|
||||
func (p *rwFolder) deleteDir(file protocol.FileInfo) {
|
||||
var err error
|
||||
events.Default.Log(events.ItemStarted, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"details": db.ToTruncated(file),
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"type": "dir",
|
||||
"action": "delete",
|
||||
})
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"error": err,
|
||||
"type": "dir",
|
||||
"action": "delete",
|
||||
})
|
||||
}()
|
||||
|
||||
@@ -590,15 +597,18 @@ func (p *rwFolder) deleteDir(file protocol.FileInfo) {
|
||||
func (p *rwFolder) deleteFile(file protocol.FileInfo) {
|
||||
var err error
|
||||
events.Default.Log(events.ItemStarted, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"details": db.ToTruncated(file),
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"type": "file",
|
||||
"action": "delete",
|
||||
})
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"error": err,
|
||||
"type": "file",
|
||||
"action": "delete",
|
||||
})
|
||||
}()
|
||||
|
||||
@@ -629,25 +639,31 @@ func (p *rwFolder) deleteFile(file protocol.FileInfo) {
|
||||
func (p *rwFolder) renameFile(source, target protocol.FileInfo) {
|
||||
var err error
|
||||
events.Default.Log(events.ItemStarted, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": source.Name,
|
||||
"details": db.ToTruncated(source),
|
||||
"folder": p.folder,
|
||||
"item": source.Name,
|
||||
"type": "file",
|
||||
"action": "delete",
|
||||
})
|
||||
events.Default.Log(events.ItemStarted, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": target.Name,
|
||||
"details": db.ToTruncated(source),
|
||||
"folder": p.folder,
|
||||
"item": target.Name,
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": source.Name,
|
||||
"error": err,
|
||||
"type": "file",
|
||||
"action": "delete",
|
||||
})
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": target.Name,
|
||||
"error": err,
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
}()
|
||||
|
||||
@@ -698,9 +714,10 @@ func (p *rwFolder) renameFile(source, target protocol.FileInfo) {
|
||||
// changed file.
|
||||
func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocksState, finisherChan chan<- *sharedPullerState) {
|
||||
events.Default.Log(events.ItemStarted, map[string]interface{}{
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"details": db.ToTruncated(file),
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
|
||||
curFile, ok := p.model.CurrentFolderFile(p.folder, file.Name)
|
||||
@@ -723,6 +740,8 @@ func (p *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
"folder": p.folder,
|
||||
"item": file.Name,
|
||||
"error": err,
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
return
|
||||
}
|
||||
@@ -994,6 +1013,8 @@ func (p *rwFolder) performFinish(state *sharedPullerState) {
|
||||
"folder": p.folder,
|
||||
"item": state.file.Name,
|
||||
"error": err,
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
}()
|
||||
|
||||
@@ -1044,7 +1065,7 @@ func (p *rwFolder) performFinish(state *sharedPullerState) {
|
||||
|
||||
// If the target path is a symlink or a directory, we cannot copy
|
||||
// over it, hence remove it before proceeding.
|
||||
stat, err := os.Lstat(state.realName)
|
||||
stat, err := osutil.Lstat(state.realName)
|
||||
if err == nil && (stat.IsDir() || stat.Mode()&os.ModeSymlink != 0) {
|
||||
osutil.InWritableDir(os.Remove, state.realName)
|
||||
}
|
||||
@@ -1097,6 +1118,8 @@ func (p *rwFolder) finisherRoutine(in <-chan *sharedPullerState) {
|
||||
"folder": p.folder,
|
||||
"item": state.file.Name,
|
||||
"error": state.failed(),
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
}
|
||||
p.model.receivedFile(p.folder, state.file.Name)
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
// 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/.
|
||||
|
||||
// +build linux android
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
// Lstat is like os.Lstat, except lobotomized for Android. See
|
||||
// https://forum.syncthing.net/t/2395
|
||||
func Lstat(name string) (fi os.FileInfo, err error) {
|
||||
for i := 0; i < 10; i++ { // We have to draw the line somewhere
|
||||
fi, err = os.Lstat(name)
|
||||
if err, ok := err.(*os.PathError); ok && err.Err == syscall.EINTR {
|
||||
time.Sleep(time.Duration(i+1) * time.Millisecond)
|
||||
continue
|
||||
}
|
||||
return
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
// 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/.
|
||||
|
||||
// +build !linux,!android
|
||||
|
||||
package osutil
|
||||
|
||||
import "os"
|
||||
|
||||
func Lstat(name string) (fi os.FileInfo, err error) {
|
||||
return os.Lstat(name)
|
||||
}
|
||||
@@ -17,6 +17,7 @@ import (
|
||||
|
||||
"github.com/syncthing/protocol"
|
||||
"github.com/syncthing/syncthing/internal/ignore"
|
||||
"github.com/syncthing/syncthing/internal/osutil"
|
||||
"github.com/syncthing/syncthing/internal/symlinks"
|
||||
"golang.org/x/text/unicode/norm"
|
||||
)
|
||||
@@ -193,7 +194,7 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
|
||||
|
||||
// We will attempt to normalize it.
|
||||
normalizedPath := filepath.Join(w.Dir, normalizedRn)
|
||||
if _, err := os.Lstat(normalizedPath); os.IsNotExist(err) {
|
||||
if _, err := osutil.Lstat(normalizedPath); os.IsNotExist(err) {
|
||||
// Nothing exists with the normalized filename. Good.
|
||||
if err = os.Rename(p, normalizedPath); err != nil {
|
||||
l.Infof(`Error normalizing UTF8 encoding of file "%s": %v`, rn, err)
|
||||
@@ -356,7 +357,7 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
|
||||
}
|
||||
|
||||
func checkDir(dir string) error {
|
||||
if info, err := os.Lstat(dir); err != nil {
|
||||
if info, err := osutil.Lstat(dir); err != nil {
|
||||
return err
|
||||
} else if !info.IsDir() {
|
||||
return errors.New(dir + ": not a directory")
|
||||
|
||||
@@ -60,7 +60,7 @@ func init() {
|
||||
return
|
||||
}
|
||||
|
||||
stat, err := os.Lstat(path)
|
||||
stat, err := osutil.Lstat(path)
|
||||
if err != nil || stat.Mode()&os.ModeSymlink == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
+81
-106
@@ -91,44 +91,71 @@ type upnpRoot struct {
|
||||
}
|
||||
|
||||
// Discover discovers UPnP InternetGatewayDevices.
|
||||
// The order in which the devices appear in the result list is not deterministic.
|
||||
func Discover() []IGD {
|
||||
var result []IGD
|
||||
// The order in which the devices appear in the results list is not deterministic.
|
||||
func Discover(timeout time.Duration) []IGD {
|
||||
var results []IGD
|
||||
l.Infoln("Starting UPnP discovery...")
|
||||
|
||||
timeout := 3
|
||||
interfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
l.Infoln("Listing network interfaces:", err)
|
||||
return results
|
||||
}
|
||||
|
||||
// Search for InternetGatewayDevice:2 devices
|
||||
result = append(result, discover("urn:schemas-upnp-org:device:InternetGatewayDevice:2", timeout, result)...)
|
||||
resultChan := make(chan IGD, 16)
|
||||
|
||||
// Search for InternetGatewayDevice:1 devices
|
||||
// InternetGatewayDevice:2 devices that correctly respond to the IGD:1 request as well will not be re-added to the result list
|
||||
result = append(result, discover("urn:schemas-upnp-org:device:InternetGatewayDevice:1", timeout, result)...)
|
||||
|
||||
if len(result) > 0 && debug {
|
||||
l.Debugln("UPnP discovery result:")
|
||||
for _, resultDevice := range result {
|
||||
l.Debugln("[" + resultDevice.uuid + "]")
|
||||
|
||||
for _, resultService := range resultDevice.services {
|
||||
l.Debugln("* [" + resultService.serviceID + "] " + resultService.serviceURL)
|
||||
// Aggregator
|
||||
go func() {
|
||||
next:
|
||||
for result := range resultChan {
|
||||
for _, existingResult := range results {
|
||||
if existingResult.uuid == result.uuid {
|
||||
if debug {
|
||||
l.Debugf("Skipping duplicate result %s with services:", result.uuid)
|
||||
for _, svc := range result.services {
|
||||
l.Debugf("* [%s] %s", svc.serviceID, svc.serviceURL)
|
||||
}
|
||||
}
|
||||
goto next
|
||||
}
|
||||
}
|
||||
results = append(results, result)
|
||||
if debug {
|
||||
l.Debugf("UPnP discovery result %s with services:", result.uuid)
|
||||
for _, svc := range result.services {
|
||||
l.Debugf("* [%s] %s", svc.serviceID, svc.serviceURL)
|
||||
}
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
var wg sync.WaitGroup
|
||||
for _, intf := range interfaces {
|
||||
for _, deviceType := range []string{"urn:schemas-upnp-org:device:InternetGatewayDevice:1", "urn:schemas-upnp-org:device:InternetGatewayDevice:2"} {
|
||||
wg.Add(1)
|
||||
go func(intf net.Interface, deviceType string) {
|
||||
discover(&intf, deviceType, timeout, resultChan)
|
||||
wg.Done()
|
||||
}(intf, deviceType)
|
||||
}
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
close(resultChan)
|
||||
|
||||
suffix := "devices"
|
||||
if len(result) == 1 {
|
||||
if len(results) == 1 {
|
||||
suffix = "device"
|
||||
}
|
||||
|
||||
l.Infof("UPnP discovery complete (found %d %s).", len(result), suffix)
|
||||
l.Infof("UPnP discovery complete (found %d %s).", len(results), suffix)
|
||||
|
||||
return result
|
||||
return results
|
||||
}
|
||||
|
||||
// Search for UPnP InternetGatewayDevices for <timeout> seconds, ignoring responses from any devices listed in knownDevices.
|
||||
// The order in which the devices appear in the result list is not deterministic
|
||||
func discover(deviceType string, timeout int, knownDevices []IGD) []IGD {
|
||||
func discover(intf *net.Interface, deviceType string, timeout time.Duration, results chan<- IGD) {
|
||||
ssdp := &net.UDPAddr{IP: []byte{239, 255, 255, 250}, Port: 1900}
|
||||
|
||||
tpl := `M-SEARCH * HTTP/1.1
|
||||
@@ -138,44 +165,41 @@ Man: "ssdp:discover"
|
||||
Mx: %d
|
||||
|
||||
`
|
||||
searchStr := fmt.Sprintf(tpl, deviceType, timeout)
|
||||
searchStr := fmt.Sprintf(tpl, deviceType, timeout/time.Second)
|
||||
|
||||
search := []byte(strings.Replace(searchStr, "\n", "\r\n", -1))
|
||||
|
||||
if debug {
|
||||
l.Debugln("Starting discovery of device type " + deviceType + "...")
|
||||
l.Debugln("Starting discovery of device type " + deviceType + " on " + intf.Name)
|
||||
}
|
||||
|
||||
var results []IGD
|
||||
resultChannel := make(chan IGD, 8)
|
||||
|
||||
socket, err := net.ListenMulticastUDP("udp4", nil, &net.UDPAddr{IP: ssdp.IP})
|
||||
socket, err := net.ListenMulticastUDP("udp4", intf, &net.UDPAddr{IP: ssdp.IP})
|
||||
if err != nil {
|
||||
l.Infoln(err)
|
||||
return results
|
||||
if debug {
|
||||
l.Debugln(err)
|
||||
}
|
||||
return
|
||||
}
|
||||
defer socket.Close() // Make sure our socket gets closed
|
||||
|
||||
err = socket.SetDeadline(time.Now().Add(time.Duration(timeout) * time.Second))
|
||||
err = socket.SetDeadline(time.Now().Add(timeout))
|
||||
if err != nil {
|
||||
l.Infoln(err)
|
||||
return results
|
||||
return
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("Sending search request for device type " + deviceType + "...")
|
||||
l.Debugln("Sending search request for device type " + deviceType + " on " + intf.Name)
|
||||
}
|
||||
|
||||
var resultWaitGroup sync.WaitGroup
|
||||
|
||||
_, err = socket.WriteTo(search, ssdp)
|
||||
if err != nil {
|
||||
l.Infoln(err)
|
||||
return results
|
||||
return
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("Listening for UPnP response for device type " + deviceType + "...")
|
||||
l.Debugln("Listening for UPnP response for device type " + deviceType + " on " + intf.Name)
|
||||
}
|
||||
|
||||
// Listen for responses until a timeout is reached
|
||||
@@ -184,69 +208,42 @@ Mx: %d
|
||||
n, _, err := socket.ReadFrom(resp)
|
||||
if err != nil {
|
||||
if e, ok := err.(net.Error); !ok || !e.Timeout() {
|
||||
l.Infoln(err) //legitimate error, not a timeout.
|
||||
l.Infoln("UPnP read:", err) //legitimate error, not a timeout.
|
||||
}
|
||||
|
||||
break
|
||||
} else {
|
||||
// Process results in a separate go routine so we can immediately return to listening for more responses
|
||||
resultWaitGroup.Add(1)
|
||||
go handleSearchResponse(deviceType, knownDevices, resp, n, resultChannel, &resultWaitGroup)
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all result handlers to finish processing, then close result channel
|
||||
resultWaitGroup.Wait()
|
||||
close(resultChannel)
|
||||
|
||||
// Collect our results from the result handlers using the result channel
|
||||
for result := range resultChannel {
|
||||
// Check for existing results (some routers send multiple response packets)
|
||||
for _, existingResult := range results {
|
||||
if existingResult.uuid == result.uuid {
|
||||
if debug {
|
||||
l.Debugln("Already processed device with UUID", existingResult.uuid, "continuing...")
|
||||
}
|
||||
continue
|
||||
}
|
||||
igd, err := parseResponse(deviceType, resp[:n])
|
||||
if err != nil {
|
||||
l.Infoln("UPnP parse:", err)
|
||||
continue
|
||||
}
|
||||
|
||||
// No existing results, okay to append
|
||||
results = append(results, result)
|
||||
results <- igd
|
||||
}
|
||||
|
||||
if debug {
|
||||
l.Debugln("Discovery for device type " + deviceType + " finished.")
|
||||
l.Debugln("Discovery for device type " + deviceType + " on " + intf.Name + " finished.")
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
func handleSearchResponse(deviceType string, knownDevices []IGD, resp []byte, length int, resultChannel chan<- IGD, resultWaitGroup *sync.WaitGroup) {
|
||||
defer resultWaitGroup.Done() // Signal when we've finished processing
|
||||
|
||||
func parseResponse(deviceType string, resp []byte) (IGD, error) {
|
||||
if debug {
|
||||
l.Debugln("Handling UPnP response:\n\n" + string(resp[:length]))
|
||||
l.Debugln("Handling UPnP response:\n\n" + string(resp))
|
||||
}
|
||||
|
||||
reader := bufio.NewReader(bytes.NewBuffer(resp[:length]))
|
||||
reader := bufio.NewReader(bytes.NewBuffer(resp))
|
||||
request := &http.Request{}
|
||||
response, err := http.ReadResponse(reader, request)
|
||||
if err != nil {
|
||||
l.Infoln(err)
|
||||
return
|
||||
return IGD{}, err
|
||||
}
|
||||
|
||||
respondingDeviceType := response.Header.Get("St")
|
||||
if respondingDeviceType != deviceType {
|
||||
l.Infoln("Unrecognized UPnP device of type " + respondingDeviceType)
|
||||
return
|
||||
return IGD{}, errors.New("unrecognized UPnP device of type " + respondingDeviceType)
|
||||
}
|
||||
|
||||
deviceDescriptionLocation := response.Header.Get("Location")
|
||||
if deviceDescriptionLocation == "" {
|
||||
l.Infoln("Invalid IGD response: no location specified.")
|
||||
return
|
||||
return IGD{}, errors.New("invalid IGD response: no location specified.")
|
||||
}
|
||||
|
||||
deviceDescriptionURL, err := url.Parse(deviceDescriptionLocation)
|
||||
@@ -257,8 +254,7 @@ func handleSearchResponse(deviceType string, knownDevices []IGD, resp []byte, le
|
||||
|
||||
deviceUSN := response.Header.Get("USN")
|
||||
if deviceUSN == "" {
|
||||
l.Infoln("Invalid IGD response: USN not specified.")
|
||||
return
|
||||
return IGD{}, errors.New("invalid IGD response: USN not specified.")
|
||||
}
|
||||
|
||||
deviceUUID := strings.TrimLeft(strings.Split(deviceUSN, "::")[0], "uuid:")
|
||||
@@ -267,39 +263,25 @@ func handleSearchResponse(deviceType string, knownDevices []IGD, resp []byte, le
|
||||
l.Infoln("Invalid IGD response: invalid device UUID", deviceUUID, "(continuing anyway)")
|
||||
}
|
||||
|
||||
// Don't re-add devices that are already known
|
||||
for _, knownDevice := range knownDevices {
|
||||
if deviceUUID == knownDevice.uuid {
|
||||
if debug {
|
||||
l.Debugln("Ignoring known device with UUID " + deviceUUID)
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response, err = http.Get(deviceDescriptionLocation)
|
||||
if err != nil {
|
||||
l.Infoln(err)
|
||||
return
|
||||
return IGD{}, err
|
||||
}
|
||||
defer response.Body.Close()
|
||||
|
||||
if response.StatusCode >= 400 {
|
||||
l.Infoln(errors.New(response.Status))
|
||||
return
|
||||
return IGD{}, errors.New("bad status code:" + response.Status)
|
||||
}
|
||||
|
||||
var upnpRoot upnpRoot
|
||||
err = xml.NewDecoder(response.Body).Decode(&upnpRoot)
|
||||
if err != nil {
|
||||
l.Infoln(err)
|
||||
return
|
||||
return IGD{}, err
|
||||
}
|
||||
|
||||
services, err := getServiceDescriptions(deviceDescriptionLocation, upnpRoot.Device)
|
||||
if err != nil {
|
||||
l.Infoln(err)
|
||||
return
|
||||
return IGD{}, err
|
||||
}
|
||||
|
||||
// Figure out our IP number, on the network used to reach the IGD.
|
||||
@@ -308,23 +290,16 @@ func handleSearchResponse(deviceType string, knownDevices []IGD, resp []byte, le
|
||||
// suggestions on a better way to do this...
|
||||
localIPAddress, err := localIP(deviceDescriptionURL)
|
||||
if err != nil {
|
||||
l.Infoln(err)
|
||||
return
|
||||
return IGD{}, err
|
||||
}
|
||||
|
||||
igd := IGD{
|
||||
return IGD{
|
||||
uuid: deviceUUID,
|
||||
friendlyName: upnpRoot.Device.FriendlyName,
|
||||
url: deviceDescriptionURL,
|
||||
services: services,
|
||||
localIPAddress: localIPAddress,
|
||||
}
|
||||
|
||||
resultChannel <- igd
|
||||
|
||||
if debug {
|
||||
l.Debugln("Finished handling of UPnP response.")
|
||||
}
|
||||
}, nil
|
||||
}
|
||||
|
||||
func localIP(url *url.URL) (string, error) {
|
||||
|
||||
@@ -12,6 +12,8 @@ import (
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
|
||||
"github.com/syncthing/syncthing/internal/osutil"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -43,7 +45,7 @@ func NewExternal(folderID, folderPath string, params map[string]string) Versione
|
||||
// Move away the named file to a version archive. If this function returns
|
||||
// nil, the named file does not exist any more (has been archived).
|
||||
func (v External) Archive(filePath string) error {
|
||||
_, err := os.Lstat(filePath)
|
||||
_, err := osutil.Lstat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
if debug {
|
||||
l.Debugln("not archiving nonexistent file", filePath)
|
||||
@@ -82,7 +84,7 @@ func (v External) Archive(filePath string) error {
|
||||
}
|
||||
|
||||
// return error if the file was not removed
|
||||
if _, err = os.Lstat(filePath); os.IsNotExist(err) {
|
||||
if _, err = osutil.Lstat(filePath); os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
return errors.New("Versioner: file was not removed by external script")
|
||||
|
||||
@@ -46,7 +46,7 @@ func NewSimple(folderID, folderPath string, params map[string]string) Versioner
|
||||
// Move away the named file to a version archive. If this function returns
|
||||
// nil, the named file does not exist any more (has been archived).
|
||||
func (v Simple) Archive(filePath string) error {
|
||||
fileInfo, err := os.Lstat(filePath)
|
||||
fileInfo, err := osutil.Lstat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
if debug {
|
||||
l.Debugln("not archiving nonexistent file", filePath)
|
||||
|
||||
@@ -210,7 +210,7 @@ func (v Staggered) expire(versions []string) {
|
||||
var prevAge int64
|
||||
firstFile := true
|
||||
for _, file := range versions {
|
||||
fi, err := os.Lstat(file)
|
||||
fi, err := osutil.Lstat(file)
|
||||
if err != nil {
|
||||
l.Warnln("versioner:", err)
|
||||
continue
|
||||
@@ -281,7 +281,7 @@ func (v Staggered) Archive(filePath string) error {
|
||||
v.mutex.Lock()
|
||||
defer v.mutex.Unlock()
|
||||
|
||||
_, err := os.Lstat(filePath)
|
||||
_, err := osutil.Lstat(filePath)
|
||||
if os.IsNotExist(err) {
|
||||
if debug {
|
||||
l.Debugln("not archiving nonexistent file", filePath)
|
||||
|
||||
Reference in New Issue
Block a user