Compare commits

...
Author SHA1 Message Date
Jakob Borg c6300a5da8 Tone down UPnP errors a little 2015-04-16 23:45:12 +09:00
Jakob Borg 9359daa0d9 Merge branch 'pr-1636'
* pr-1636:
  Store and use _localStorage object
  fix using detect localStorage
2015-04-16 23:44:59 +09:00
Jakob Borg 2322e9cff7 Store and use _localStorage object 2015-04-16 23:44:34 +09:00
Jakob Borg a876e1e348 Merge remote-tracking branch 'syncthing/pr/1636' into pr-1636
* syncthing/pr/1636:
  fix using detect localStorage
2015-04-16 23:32:48 +09:00
Jakob Borg 6a863c8f71 Translation update 2015-04-16 23:27:27 +09:00
Jakob Borg 392b006b06 Add Moter8 2015-04-16 23:23:34 +09:00
Audrius Butkevicius 96289f42b7 Merge pull request #1644 from syncthing/timeout
UPnP refactor/fixes
2015-04-16 14:32:16 +01:00
Audrius Butkevicius 1b69c2441c Make UPnP discovery requests on each interface explicitly (fixes #1113) 2015-04-16 14:23:36 +01:00
Audrius Butkevicius 8ca85a4918 Merge pull request #1639 from calmh/events
Improve event handling a little bit.
2015-04-16 14:18:52 +01:00
Audrius Butkevicius 2a31031cbc Add unit suffix to UPnP settings 2015-04-16 10:32:22 +01:00
Audrius Butkevicius d148cd8ccc Make UPnP timeout configurable 2015-04-16 10:32:12 +01:00
Jakob Borg d1cc1828b8 Improve ItemStarted/ItemFinished events
- Remove full details from ItemStarted (unnecessary, incorrect CamelCase)

 - Add "type" ("file" or "dir") to both events

 - Add "action" (what we tried to do - "delete" or "update") to both
   events.
2015-04-14 23:31:39 +09:00
Jakob Borg 069e8cf122 Don't schedule summaries on all state changes
Prior to this change we schedule summaries on each state change, i.e.
scanning->idle and idle->scanning, which is unnecessary. Now we only do
it on index updates, plus the immediate one on going syncing->idle.
2015-04-14 20:57:42 +09:00
Audrius Butkevicius 45cbcaca6d Merge pull request #1638 from calmh/lstat
Work around broken Lstat on Android
2015-04-14 11:59:20 +01:00
Jakob Borg 102a2db1f3 Work around broken Lstat on Android 2015-04-14 19:53:49 +09:00
Sergey Mishin 9f81c85ca7 fix using detect localStorage 2015-04-13 19:07:39 +03:00
Audrius Butkevicius ba4a6fc0c5 Merge pull request #1633 from calmh/errorstate
Move folder errors to state
2015-04-13 00:48:13 +01:00
Jakob Borg aa803ce2ff Move folder errors to state
The "Invalid" config attribute is retained for errors discovered during
config loading (empty path, duplicate ID). This can only be set or
cleared at config loading time.

Errors discovered during runtime (I/O problems, etc) are now in the
folder state instead. Changes to these are sent as any other folder
state change.
2015-04-13 07:43:45 +09:00
Jakob Borg a027a60f5d Correctly feature detect localStorage (fixes #1632) 2015-04-13 06:50:07 +09:00
Jakob Borg 270649535e Merge pull request #1625 from Moter8/patch-1
Reword and clarify some sentences.
2015-04-10 13:48:14 +02:00
Carsten H cf80ba71f4 Reword and clarify some sentences 2015-04-10 13:46:38 +02:00
33 changed files with 399 additions and 312 deletions
+1
View File
@@ -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>
+1
View File
@@ -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>
+9 -10
View File
@@ -5,18 +5,17 @@ syncthing
[![API Documentation](http://img.shields.io/badge/api-Godoc-blue.svg?style=flat-square)](http://godoc.org/github.com/syncthing/syncthing)
[![MPLv2 License](http://img.shields.io/badge/license-MPLv2-blue.svg?style=flat-square)](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).
+6 -1
View File
@@ -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)
+9 -7
View File
@@ -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
+7 -3
View File
@@ -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
View File
@@ -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.
+1 -1
View File
@@ -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.",
+4 -4
View File
@@ -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.",
+6 -6
View File
@@ -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",
+5 -5
View File
@@ -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
View File
@@ -193,9 +193,9 @@
<th><span class="glyphicon glyphicon-folder-open"></span>&emsp;<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>&emsp;<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>&emsp;<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
+3 -2
View File
@@ -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"`
+6 -4
View File
@@ -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,
+1
View File
@@ -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>
-23
View File
@@ -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 {
-9
View File
@@ -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()
+41 -17
View File
@@ -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
View File
@@ -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
+18 -10
View File
@@ -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
}
+2 -2
View File
@@ -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
View File
@@ -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)
+29
View File
@@ -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
}
+15
View File
@@ -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)
}
+3 -2
View File
@@ -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")
+1 -1
View File
@@ -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
View File
@@ -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) {
+4 -2
View File
@@ -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")
+1 -1
View File
@@ -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)
+2 -2
View File
@@ -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)