Compare commits

..
Author SHA1 Message Date
Jakob Borg 123b1f01e4 Translation update 2015-03-22 11:33:10 +01:00
Jakob Borg 865f62e3eb Merge pull request #1505 from rumpelsepp/systemd
systemd: Set -logflags to 0, provide -no-browser flag
2015-03-21 23:28:54 +01:00
Stefan Tatschner 3ea93f52ee systemd: Set -logflags to 0, provide -no-browser flag
Syncthing should not try to start a browser when invoked by systemd.
Furthermore we do not need any timestamps in the journal as systemd
already handles this for us.
2015-03-21 20:55:29 +01:00
Audrius Butkevicius b53e545ebc Merge pull request #1502 from calmh/defaults
Set defaults correctly for autoNormalize
2015-03-21 14:44:46 +00:00
Jakob Borg 157a4c891c Update integration test configs to v10 2015-03-21 15:40:00 +01:00
Jakob Borg ad9ea07309 Set defaults correctly for autoNormalize
The default:"foo" struct tags aren't actually used for folder configs.
2015-03-21 15:33:31 +01:00
Jakob Borg fc483cdfc6 Assets 2015-03-20 09:35:02 +01:00
Jakob Borg 9033838cf2 Merge pull request #1492 from alex2108/fix-gui
use lowerCamelCase for file versioning display
2015-03-20 09:34:48 +01:00
Jakob Borg 8e5d2d5905 Merge pull request #1491 from alex2108/master
use Lstat instead of Stat to prevent errors with symlinks
2015-03-20 09:34:10 +01:00
Alexander Graf 18aa66dabb use lowerCamelCase for file versioning display 2015-03-19 18:16:48 +01:00
Alexander Graf a2f7b78453 use Lstat instead of Stat to prevent errors with symlinks 2015-03-19 17:36:15 +01:00
Jakob Borg 5c026cbe1d Merge pull request #1490 from syncthing/unspecified
Skip unspecified IPs
2015-03-19 14:02:47 +01:00
Audrius Butkevicius dc51476897 Skip unspecified IPs 2015-03-19 12:44:38 +00:00
Jakob Borg 39eaa577e0 Merge pull request #1489 from syncthing/lans
Print LANs on startup
2015-03-19 13:09:59 +01:00
Jakob Borg 1f006481ee Merge pull request #1484 from alex2108/master
Add external versioner (ref #573)
2015-03-19 13:08:54 +01:00
Audrius Butkevicius 60faabcbe2 Print LANs on startup 2015-03-19 11:07:20 +00:00
Alexander Graf d3f1eaf1a3 Add external versioner (ref #573) 2015-03-19 11:31:21 +01:00
Audrius Butkevicius f568e76fd4 Merge pull request #1488 from calmh/utf8
Automatically fix file name normalization errors (fixes #430)
2015-03-19 08:33:08 +00:00
Jakob Borg e947223aaa Decide once and for all to return filepath.SkipDir or nil 2015-03-19 07:46:13 +01:00
Jakob Borg 8311162be3 Automatically fix file name normalization errors (fixes #430) 2015-03-19 00:21:48 +01:00
Jakob Borg 75523556e8 Use SVG format logos 2015-03-18 12:51:23 +01:00
Audrius Butkevicius c82b5d4982 Merge pull request #1474 from calmh/refactor-states
Refactor state handling
2015-03-17 19:09:48 +00:00
Jakob Borg 1c3158099c Rename files to match type names 2015-03-17 19:37:06 +01:00
Jakob Borg bdbca75dfa Refactor state tracking (...)
Move state tracking into the puller/scanner objects. This is a first
step towards resolving #1391.

Rename Puller and Scanner to roFolder and rwFolder as they have more
duties than just pulling and scanning, and don't need to be exported.
2015-03-17 19:37:06 +01:00
Audrius Butkevicius 124b189cc0 Rebuild assets 2015-03-17 17:58:19 +00:00
Audrius Butkevicius de38b46392 Fix build 2015-03-17 17:54:25 +00:00
Jakob BorgandAudrius Butkevicius e1975644d6 Add /rest/filestatus 2015-03-17 17:51:50 +00:00
Jakob Borg d9fd27a9e8 Merge pull request #1421 from syncthing/mpl
Relicense to MPLv2
2015-03-17 16:42:33 +01:00
Jakob Borg 32425c5561 MPLv2 2015-03-17 16:02:27 +01:00
Johan VromansandJakob Borg 8d20923881 Suppress 'Last File Received' if a node is folder master (fixes #1472) 2015-03-17 08:44:17 +01:00
Jakob Borg 3a35b8b26c Add sciurius 2015-03-17 08:42:27 +01:00
Jakob Borg 36c93b755a Merge pull request #1465 from pascalj/lowercase-api
Use lowerCamelCase for the JSON API (fixes #1338)
2015-03-16 21:43:34 +01:00
Jakob Borg ea8c3debea Merge pull request #1470 from syncthing/silence
Silence warnings (ref #1388)
2015-03-16 12:01:29 +01:00
Pascal Jungblut 49bc74e7a0 Use lowerCamelCase for the JSON API (fixes #1338)
Replace the current mix of UpperCamelCase und lowerCamelCase with
consistent lowerCamelCase keys for the JSON API. Also adapt the frontend
so it works with the changed API.

Attention: this will break existing consumers of the API.
2015-03-16 10:05:01 +01:00
20 changed files with 511 additions and 425 deletions
+8 -10
View File
@@ -37,8 +37,8 @@ import (
)
type guiError struct {
Time time.Time
Error string
Time time.Time `json:"time"`
Error string `json:"error"`
}
var (
@@ -828,8 +828,6 @@ func mimeTypeForFile(file string) string {
return "application/x-font-ttf"
case ".woff":
return "application/x-font-woff"
case ".svg":
return "image/svg+xml"
default:
return mime.TypeByExtension(ext)
}
@@ -839,12 +837,12 @@ func toNeedSlice(fs []db.FileInfoTruncated) []map[string]interface{} {
output := make([]map[string]interface{}, len(fs))
for i, file := range fs {
output[i] = map[string]interface{}{
"Name": file.Name,
"Flags": file.Flags,
"Modified": file.Modified,
"Version": file.Version,
"LocalVersion": file.LocalVersion,
"Size": file.Size(),
"name": file.Name,
"flags": file.Flags,
"modified": file.Modified,
"version": file.Version,
"localVersion": file.LocalVersion,
"size": file.Size(),
}
}
return output
+5 -5
View File
@@ -7,7 +7,7 @@
"Add new folder?": "Aggiungere una nuova cartella?",
"Address": "Indirizzo",
"Addresses": "Indirizzi",
"All Data": "Tutti i dati",
"All Data": "All Data",
"Allow Anonymous Usage Reporting?": "Abilitare Statistiche Anonime di Utilizzo?",
"Anonymous Usage Reporting": "Statistiche Anonime di Utilizzo",
"Any devices configured on an introducer device will be added to this device as well.": "Qualsiasi dispositivo configurato in un introduttore verrà aggiunto anche a questo dispositivo.",
@@ -17,7 +17,7 @@
"Changelog": "Changelog",
"Close": "Chiudi",
"Comment, when used at the start of a line": "Per commentare, va inserito all'inizio di una riga",
"Compression": "Compressione",
"Compression": "Compression",
"Compression is recommended in most setups.": "La compressione è raccomandata nella maggior parte delle configurazioni.",
"Connection Error": "Errore di Connessione",
"Copied from elsewhere": "Copiato da qualche altra parte",
@@ -73,8 +73,8 @@
"Local Discovery": "Individuazione Locale",
"Local State": "Stato Locale",
"Maximum Age": "Durata Massima",
"Metadata Only": "Solo i metadati",
"Move to top of queue": "Posiziona in cima alla coda",
"Metadata Only": "Metadata Only",
"Move to top of queue": "Move to top of queue",
"Multi level wildcard (matches multiple directory levels)": "Metacarattere multi-livello (corrisponde alle cartelle e alle sotto-cartelle)",
"Never": "Mai",
"New Device": "Nuovo Dispositivo",
@@ -98,7 +98,7 @@
"Quick guide to supported patterns": "Guida veloce agli schemi supportati",
"RAM Utilization": "Utilizzo RAM",
"Rescan": "Riscansiona",
"Rescan All": "Riscansiona Tutto",
"Rescan All": "Rescan All",
"Rescan Interval": "Intervallo Scansione",
"Restart": "Riavvia",
"Restart Needed": "Riavvio Necessario",
+20 -20
View File
@@ -7,7 +7,7 @@
"Add new folder?": "Dodać nowy folder?",
"Address": "Adres",
"Addresses": "Adresy",
"All Data": "Wszystkie dane",
"All Data": "All Data",
"Allow Anonymous Usage Reporting?": "Zezwalaj na anonimowe statystyki użycia",
"Anonymous Usage Reporting": "Anonimowe statystyki użycia",
"Any devices configured on an introducer device will be added to this device as well.": "Wszystkie urządzenia skonfigurowane na urządzeniu wprowadzającym zostaną dodane także do tego urządzenia.",
@@ -17,17 +17,17 @@
"Changelog": "Historia zmian",
"Close": "Zamknij",
"Comment, when used at the start of a line": "Komentarz, jeżeli użyty na początku linii",
"Compression": "Kompresja",
"Compression": "Compression",
"Compression is recommended in most setups.": "Kompresja jest zalecana w większości przypadków",
"Connection Error": "Błąd połączenia",
"Copied from elsewhere": "Copied from elsewhere",
"Copied from original": "Skopiowane z oryginału",
"Copied from original": "Copied from original",
"Copyright © 2014 Jakob Borg and the following Contributors:": "Copyright © 2014 Jakob Borg i następujący współautorzy:",
"Delete": "Usuń",
"Device ID": "ID urządzenia",
"Device Identification": "Identyfikator urządzenia",
"Device Name": "Nazwa urządzenia",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Urządzenie {{device}} ({{address}}) chce się połączyć. Zezwolić?",
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Device {{device}} ({{address}}) wants to connect. Add new device?",
"Devices": "Urządzenia",
"Disconnected": "Rozłączony",
"Documentation": "Dokumentacja",
@@ -58,7 +58,7 @@
"Global Discovery Server": "Globalny serwer rozgłoszeniowy",
"Global State": "Status globalny",
"Idle": "Bezczynny",
"Ignore": "Ignoruj",
"Ignore": "Ignore",
"Ignore Patterns": "Wzorce ignorowania",
"Ignore Permissions": "Ignoruj uprawnienia",
"Incoming Rate Limit (KiB/s)": "Ograniczenie prędkości odbierania (KiB/s)",
@@ -68,13 +68,13 @@
"Last File Received": "Ostatni otrzymany plik",
"Last File Synced": "Ostatni zsynchronizowany plik",
"Last seen": "Ostatnio widziany",
"Later": "Później",
"Later": "Later",
"Latest Release": "Najnowsza wersja",
"Local Discovery": "Lokalne odnajdywanie",
"Local State": "Status lokalny",
"Maximum Age": "Maksymalny wiek",
"Metadata Only": "Tylko metadane",
"Move to top of queue": "Przenieś na początek kolejki",
"Metadata Only": "Metadata Only",
"Move to top of queue": "Move to top of queue",
"Multi level wildcard (matches multiple directory levels)": "Wieloznaczność na poziomie katalogów i plików (uwzględnia nazwy folderów i plików)",
"Never": "Nigdy",
"New Device": "Nowe urządzenie",
@@ -83,11 +83,11 @@
"No File Versioning": "Bez wersjonowania pliku",
"Notice": "Wskazówka",
"OK": "OK",
"Off": "Wyłącz",
"Off": "Off",
"Offline": "Rozłączony",
"Online": "Połączony",
"Out Of Sync": "Niezsynchronizowane",
"Out of Sync Items": "Niezsynchronizowane pliki",
"Out of Sync Items": "Out of Sync Items",
"Outgoing Rate Limit (KiB/s)": "Ograniczenie prędkości wysyłania (KiB/s)",
"Override Changes": "Nadpisz zmiany",
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Ścieżka do lokalnego folderu. Zostanie utworzona jeżeli nie istnieje.\nZnak tyldy (~) może zostać użyty jako skrót do",
@@ -98,29 +98,29 @@
"Quick guide to supported patterns": "Krótki przewodnik po obsługiwanych wzorcach",
"RAM Utilization": "Użycie pamięci RAM",
"Rescan": "Skanuj ponownie",
"Rescan All": "Skanuj wszystko ponownie",
"Rescan All": "Rescan All",
"Rescan Interval": "Interwał skanowania",
"Restart": "Uruchom ponownie",
"Restart Needed": "Wymagane ponowne uruchomienie",
"Restarting": "Uruchamianie ponowne",
"Reused": "Ponownie użyte",
"Reused": "Reused",
"Save": "Zapisz",
"Scanning": "Skanowanie",
"Select the devices to share this folder with.": "Wybierz urządzenie, któremu udostępnić folder.",
"Select the folders to share with this device.": "Wybierz foldery do współdzielenia z tym urządzeniem.",
"Settings": "Ustawienia",
"Share": "Udostępnij",
"Share Folder": "Udostępnij folder",
"Share Folders With Device": "Udostępnij foldery między urządzeniami",
"Share": "Share",
"Share Folder": "Share Folder",
"Share Folders With Device": "Share Folders With Device",
"Share With Devices": "Udostępnij dla urządzenia",
"Share this folder?": "Udostępnić ten folder?",
"Share this folder?": "Share this folder?",
"Shared With": "Współdzielony z",
"Short identifier for the folder. Must be the same on all cluster devices.": "Krótki identyfikator folderu. Musi być taki sam na wszystkich urządzeniach.",
"Show ID": "Pokaż ID",
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Pokazane w statusie zamiast ID urządzenia.Zostanie wysłane do innych urządzeń jako opcjonalna domyślna nazwa.",
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Pokazane w statusie zamiast ID urządzenia. Zostanie zaktualizowane do nazwy urządzenia jeżeli pozostanie puste.",
"Shutdown": "Wyłącz",
"Shutdown Complete": "Wyłączanie ukończone",
"Shutdown Complete": "Shutdown Complete",
"Simple File Versioning": "Proste wersjonowanie pliku",
"Single level wildcard (matches within a directory only)": "Wieloznaczność na poziomie plików (uwzględnia nazwy plików)",
"Source Code": "Kod źródłowy",
@@ -137,7 +137,7 @@
"Syncthing is restarting.": "Restart Syncthing",
"Syncthing is upgrading.": "Aktualizowanie Syncthing",
"Syncthing seems to be down, or there is a problem with your Internet connection. Retrying…": "Syncthing wydaje się być wyłączony lub jest problem z twoim połączeniem internetowym. Próbuje ponownie...",
"Syncthing seems to be experiencing a problem processing your request. Please refresh the page or restart Syncthing if the problem persists.": "Syncthing nie może przetworzyć twojego zapytania. Proszę przeładuj stronę lub restartuj Syncthing gdy problem pozostanie.",
"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 reload your browser or restart Syncthing if the problem persists.",
"The aggregated statistics are publicly available at {%url%}.": "Zebrane statystyki są publicznie dostępna pod adresem {{url}}.",
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfiguracja została zapisana lecz nie jest aktywna. Syncthing musi zostać zrestartowany aby aktywować nową konfiguracje.",
"The device ID cannot be blank.": "ID urządzenia nie może być puste.",
@@ -156,7 +156,7 @@
"The rescan interval must be a non-negative number of seconds.": "Interwał skanowania musi być niezerową liczbą sekund.",
"The rescan interval must be at least 5 seconds.": "Interwał skanowania musi wynosić co najmniej 5 sekund.",
"Unknown": "Nieznany",
"Unshared": "Nieudostępnione",
"Unshared": "Unshared",
"Unused": "Nieużywane",
"Up to Date": "Aktualny",
"Upgrade To {%version%}": "Aktualizuj do {{version}}",
@@ -173,5 +173,5 @@
"You must keep at least one version.": "Musisz posiadać przynajmniej jedną wersję",
"full documentation": "pełna dokumentacja",
"items": "pozycji",
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce udostępnić folder \"{{folder}}\""
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} wants to share folder \"{{folder}}\"."
}
+3 -3
View File
@@ -82,7 +82,7 @@
"No": "Ні",
"No File Versioning": "Версіонування вимкнено",
"Notice": "Повідомлення",
"OK": "Гаразд",
"OK": "OK",
"Off": "Вимкнути",
"Offline": "Офлайн",
"Online": "Онлайн",
@@ -106,8 +106,8 @@
"Reused": "Використано вдруге",
"Save": "Зберегти",
"Scanning": "Сканування",
"Select the devices to share this folder with.": "Оберіть пристрої, які матимуть доступ до цієї директорії.",
"Select the folders to share with this device.": "Оберіть директорії до яких матиме доступ цей пристрій.",
"Select the devices to share this folder with.": "Оберіть пристрої із якими обміняти дану директорію.",
"Select the folders to share with this device.": "Оберіть директорії які обмінювати із цим пристроєм.",
"Settings": "Налаштування",
"Share": "Розповсюдити ",
"Share Folder": "Розповсюдити каталог",
+109 -94
View File
@@ -147,7 +147,7 @@
<div class="panel panel-warning">
<div class="panel-heading"><h3 class="panel-title"><span class="glyphicon glyphicon-exclamation-sign"></span><span translate>Notice</span></h3></div>
<div class="panel-body">
<p ng-repeat="err in errorList()"><small>{{err.Time | date:"H:mm:ss"}}:</small> {{friendlyDevices(err.Error)}}</p>
<p ng-repeat="err in errorList()"><small>{{err.time | date:"H:mm:ss"}}:</small> {{friendlyDevices(err.error)}}</p>
</div>
<div class="panel-footer">
<button type="button" class="pull-right btn btn-sm btn-default" ng-click="clearErrors()"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>OK</span></button>
@@ -168,9 +168,9 @@
<div class="visible-xs"><h3><span translate>Folders</span></h3><hr></div>
<div class="panel panel-default" ng-repeat="folder in folderList()">
<div class="panel-heading" data-toggle="collapse" data-parent="#folders" href="#folder-{{$index}}" style="cursor: pointer">
<div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.ID)}}%"></div>
<div class="panel-progress" ng-show="folderStatus(folder) == 'syncing'" ng-attr-style="width: {{syncPercentage(folder.id)}}%"></div>
<h3 class="panel-title">
<span class="glyphicon glyphicon-hdd hidden-xs"></span>{{folder.ID}}
<span class="glyphicon glyphicon-hdd hidden-xs"></span>{{folder.id}}
<span class="pull-right text-{{folderClass(folder)}}" ng-switch="folderStatus(folder)">
<span ng-switch-when="unknown"><span class="hidden-xs" translate>Unknown</span><span class="visible-xs">&#9724;</span></span>
<span ng-switch-when="unshared"><span class="hidden-xs" translate>Unshared</span><span class="visible-xs">&#9724;</span></span>
@@ -179,7 +179,7 @@
<span ng-switch-when="idle"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs">&#9724;</span></span>
<span ng-switch-when="syncing">
<span class="hidden-xs" translate>Syncing</span>
({{syncPercentage(folder.ID)}}%)
({{syncPercentage(folder.id)}}%)
</span>
</span>
</h3>
@@ -190,64 +190,65 @@
<tbody>
<tr>
<th><span class="glyphicon glyphicon-folder-open"></span>&emsp;<span translate>Folder Path</span></th>
<td class="text-right">{{folder.Path}}</td>
<td class="text-right">{{folder.path}}</td>
</tr>
<tr ng-if="model[folder.ID].invalid">
<tr ng-if="model[folder.id].invalid">
<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}}</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-globe"></span>&emsp;<span translate>Global State</span></th>
<td class="text-right">{{model[folder.ID].globalFiles | alwaysNumber}} <span translate>items</span>, ~{{model[folder.ID].globalBytes | binary}}B</td>
<td class="text-right">{{model[folder.id].globalFiles | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].globalBytes | binary}}B</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-home"></span>&emsp;<span translate>Local State</span></th>
<td class="text-right">{{model[folder.ID].localFiles | alwaysNumber}} <span translate>items</span>, ~{{model[folder.ID].localBytes | binary}}B</td>
<td class="text-right">{{model[folder.id].localFiles | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].localBytes | binary}}B</td>
</tr>
<tr ng-if="model[folder.ID].needFiles > 0">
<tr ng-if="model[folder.id].needFiles > 0">
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;<span translate>Out Of Sync</span></th>
<td class="text-right">
<a ng-click="showNeed(folder.ID)" href="">{{model[folder.ID].needFiles | alwaysNumber}} <span translate>items</span>, ~{{model[folder.ID].needBytes | binary}}B</a>
<a ng-click="showNeed(folder.id)" href="">{{model[folder.id].needFiles | alwaysNumber}} <span translate>items</span>, ~{{model[folder.id].needBytes | binary}}B</a>
</td>
</tr>
<tr ng-if="folder.ReadOnly">
<tr ng-if="folder.readOnly">
<th><span class="glyphicon glyphicon-lock"></span>&emsp;<span translate>Folder Master</span></th>
<td class="text-right">
<span translate>Yes</span>
</td>
</tr>
<tr ng-if="model[folder.ID].ignorePatterns">
<tr ng-if="model[folder.id].ignorePatterns">
<th><span class="glyphicon glyphicon-eye-close"></span>&emsp;<span translate>Ignore Patterns</span></th>
<td class="text-right">
<span translate>Yes</span>
</td>
</tr>
<tr ng-if="folder.IgnorePerms">
<tr ng-if="folder.ignorePerms">
<th><span class="glyphicon glyphicon-unchecked"></span>&emsp;<span translate>Ignore Permissions</span></th>
<td class="text-right">
<span translate>Yes</span>
</td>
</tr>
<tr ng-if="folder.RescanIntervalS != 60">
<tr ng-if="folder.rescanIntervalS != 60">
<th><span class="glyphicon glyphicon-refresh"></span>&emsp;<span translate>Rescan Interval</span></th>
<td class="text-right">{{folder.RescanIntervalS}} s</td>
<td class="text-right">{{folder.rescanIntervalS}} s</td>
</tr>
<tr ng-if="folder.Versioning.Type">
<tr ng-if="folder.versioning.type">
<th><span class="glyphicon glyphicon-tags"></span>&emsp;<span translate>File Versioning</span></th>
<td class="text-right" ng-switch="folder.Versioning.Type">
<td class="text-right" ng-switch="folder.versioning.type">
<span ng-switch-when="staggered" translate>Staggered File Versioning</span>
<span ng-switch-when="simple" translate>Simple File Versioning</span>
<span ng-switch-when="external" translate>External File Versioning</span>
</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-share-alt"></span>&emsp;<span translate>Shared With</span></th>
<td class="text-right">{{sharesFolder(folder)}}</td>
</tr>
<tr ng-if="!folder.readOnly && folderStats[folder.ID].LastFile">
<tr ng-if="!folder.readOnly && folderStats[folder.id].lastFile">
<th><span class="glyphicon glyphicon-transfer"></span>&emsp;<span translate>Last File Received</span></th>
<td class="text-right">
<span title="{{folderStats[folder.ID].LastFile.Filename}} @ {{folderStats[folder.ID].LastFile.At | date:'yyyy-MM-dd HH:mm'}}">
{{folderStats[folder.ID].LastFile.Filename | basename}}
<span title="{{folderStats[folder.id].lastFile.filename}} @ {{folderStats[folder.id].lastFile.at | date:'yyyy-MM-dd HH:mm'}}">
{{folderStats[folder.id].lastFile.filename | basename}}
</span>
</td>
</tr>
@@ -255,9 +256,9 @@
</table>
</div>
<div class="panel-footer">
<button class="btn btn-sm btn-danger pull-left" ng-if="folder.ReadOnly && model[folder.ID].needFiles > 0" ng-click="override(folder.ID)" href=""><span class="glyphicon glyphicon-upload"></span>&emsp;<span translate>Override Changes</span></button>
<button class="btn btn-sm btn-danger pull-left" ng-if="folder.readOnly && model[folder.id].needFiles > 0" ng-click="override(folder.id)" href=""><span class="glyphicon glyphicon-upload"></span>&emsp;<span translate>Override Changes</span></button>
<span class="pull-right">
<button class="btn btn-sm btn-default" href="" ng-show="folderStatus(folder) == 'idle'" ng-click="rescanFolder(folder.ID)"><span class="glyphicon glyphicon-refresh"></span>&emsp;<span translate>Rescan</span></button>
<button class="btn btn-sm btn-default" href="" ng-show="folderStatus(folder) == 'idle'" ng-click="rescanFolder(folder.id)"><span class="glyphicon glyphicon-refresh"></span>&emsp;<span translate>Rescan</span></button>
<button class="btn btn-sm btn-default" href="" ng-click="editFolder(folder)"><span class="glyphicon glyphicon-pencil"></span>&emsp;<span translate>Edit</span></button>
</span>
<div class="clearfix"></div>
@@ -282,7 +283,7 @@
<div class="panel panel-default" ng-repeat="deviceCfg in [thisDevice()]">
<div class="panel-heading" data-toggle="collapse" href="#device-this" style="cursor: pointer">
<h3 class="panel-title">
<identicon data-value="deviceCfg.DeviceID"></identicon>&emsp;{{deviceName(deviceCfg)}}
<identicon data-value="deviceCfg.deviceID"></identicon>&emsp;{{deviceName(deviceCfg)}}
</h3>
</div>
<div id="device-this" class="panel-collapse collapse in">
@@ -291,11 +292,11 @@
<tbody>
<tr>
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;<span translate>Download Rate</span></th>
<td class="text-right">{{connections['total'].inbps | binary}}B/s ({{connections['total'].InBytesTotal | binary}}B)</td>
<td class="text-right">{{connections['total'].inbps | binary}}B/s ({{connections['total'].inBytesTotal | binary}}B)</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-cloud-upload"></span>&emsp;<span translate>Upload Rate</span></th>
<td class="text-right">{{connections['total'].outbps | binary}}B/s ({{connections['total'].OutBytesTotal | binary}}B)</td>
<td class="text-right">{{connections['total'].outbps | binary}}B/s ({{connections['total'].outBytesTotal | binary}}B)</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-th"></span>&emsp;<span translate>RAM Utilization</span></th>
@@ -333,13 +334,13 @@
<div class="panel-group" id="devices">
<div class="panel panel-default" ng-repeat="deviceCfg in otherDevices()">
<div class="panel-heading" data-toggle="collapse" data-parent="#devices" href="#device-{{$index}}" style="cursor: pointer">
<div class="panel-progress" ng-show="deviceStatus(deviceCfg) == 'syncing'" ng-attr-style="width: {{completion[deviceCfg.DeviceID]._total | number:0}}%"></div>
<div class="panel-progress" ng-show="deviceStatus(deviceCfg) == 'syncing'" ng-attr-style="width: {{completion[deviceCfg.deviceID]._total | number:0}}%"></div>
<h3 class="panel-title">
<identicon data-value="deviceCfg.DeviceID"></identicon>&emsp;{{deviceName(deviceCfg)}}
<identicon data-value="deviceCfg.deviceID"></identicon>&emsp;{{deviceName(deviceCfg)}}
<span ng-switch="deviceStatus(deviceCfg)" class="pull-right text-{{deviceClass(deviceCfg)}}">
<span ng-switch-when="insync"><span class="hidden-xs" translate>Up to Date</span><span class="visible-xs">&#9724;</span></span>
<span ng-switch-when="syncing">
<span class="hidden-xs" translate>Syncing</span> ({{completion[deviceCfg.DeviceID]._total | number:0}}%)
<span class="hidden-xs" translate>Syncing</span> ({{completion[deviceCfg.deviceID]._total | number:0}}%)
</span>
<span ng-switch-when="disconnected"><span class="hidden-xs" translate>Disconnected</span><span class="visible-xs">&#9724;</span></span>
<span ng-switch-when="unused"><span class="hidden-xs" translate>Unused</span><span class="visible-xs">&#9724;</span></span>
@@ -350,37 +351,37 @@
<div class="panel-body">
<table class="table table-condensed table-striped">
<tbody>
<tr ng-if="connections[deviceCfg.DeviceID]">
<tr ng-if="connections[deviceCfg.deviceID]">
<th><span class="glyphicon glyphicon-cloud-download"></span>&emsp;<span translate>Download Rate</span></th>
<td class="text-right">{{connections[deviceCfg.DeviceID].inbps | binary}}B/s ({{connections[deviceCfg.DeviceID].InBytesTotal | binary}}B)</td>
<td class="text-right">{{connections[deviceCfg.deviceID].inbps | binary}}B/s ({{connections[deviceCfg.deviceID].inBytesTotal | binary}}B)</td>
</tr>
<tr ng-if="connections[deviceCfg.DeviceID]">
<tr ng-if="connections[deviceCfg.deviceID]">
<th><span class="glyphicon glyphicon-cloud-upload"></span>&emsp;<span translate>Upload Rate</span></th>
<td class="text-right">{{connections[deviceCfg.DeviceID].outbps | binary}}B/s ({{connections[deviceCfg.DeviceID].OutBytesTotal | binary}}B)</td>
<td class="text-right">{{connections[deviceCfg.deviceID].outbps | binary}}B/s ({{connections[deviceCfg.deviceID].outBytesTotal | binary}}B)</td>
</tr>
<tr>
<th><span class="glyphicon glyphicon-link"></span>&emsp;<span translate>Address</span></th>
<td class="text-right">{{deviceAddr(deviceCfg)}}</td>
</tr>
<tr ng-if="deviceCfg.Compression != 'metadata'">
<tr ng-if="deviceCfg.compression != 'metadata'">
<th><span class="glyphicon glyphicon-compressed"></span>&emsp;<span translate>Compression</span></th>
<td class="text-right">
<span ng-if="deviceCfg.Compression == 'always'" translate>All Data</span>
<span ng-if="deviceCfg.Compression == 'never'" translate>Off</span>
<span ng-if="deviceCfg.compression == 'always'" translate>All Data</span>
<span ng-if="deviceCfg.compression == 'never'" translate>Off</span>
</td>
</tr>
<tr ng-if="deviceCfg.Introducer">
<tr ng-if="deviceCfg.introducer">
<th><span class="glyphicon glyphicon-thumbs-up"></span>&emsp;<span translate>Introducer</span></th>
<td translate class="text-right">Yes</td>
</tr>
<tr ng-if="connections[deviceCfg.DeviceID]">
<tr ng-if="connections[deviceCfg.deviceID]">
<th><span class="glyphicon glyphicon-tag"></span>&emsp;<span translate>Version</span></th>
<td class="text-right">{{connections[deviceCfg.DeviceID].ClientVersion}}</td>
<td class="text-right">{{connections[deviceCfg.deviceID].clientVersion}}</td>
</tr>
<tr ng-if="!connections[deviceCfg.DeviceID]">
<tr ng-if="!connections[deviceCfg.deviceID]">
<th><span class="glyphicon glyphicon-eye-open"></span>&emsp;<span translate>Last seen</span></th>
<td translate ng-if="!deviceStats[deviceCfg.DeviceID].LastSeenDays || deviceStats[deviceCfg.DeviceID].LastSeenDays >= 365" class="text-right">Never</td>
<td ng-if="deviceStats[deviceCfg.DeviceID].LastSeenDays < 365" class="text-right">{{deviceStats[deviceCfg.DeviceID].LastSeen | date:"yyyy-MM-dd HH:mm"}}</td>
<td translate ng-if="!deviceStats[deviceCfg.deviceID].lastSeenDays || deviceStats[deviceCfg.deviceID].lastSeenDays >= 365" class="text-right">Never</td>
<td ng-if="deviceStats[deviceCfg.deviceID].lastSeenDays < 365" class="text-right">{{deviceStats[deviceCfg.deviceID].lastSeen | date:"yyyy-MM-dd HH:mm"}}</td>
</tr>
<tr ng-if="deviceFolders(deviceCfg).length > 0">
<th><span class="glyphicon glyphicon-hdd"></span>&emsp;<span translate>Folders</span></th>
@@ -473,11 +474,11 @@
<form role="form" name="deviceEditor">
<div class="form-group" ng-class="{'has-error': deviceEditor.deviceID.$invalid && deviceEditor.deviceID.$dirty}">
<label translate for="deviceID">Device ID</label>
<input ng-if="!editingExisting" name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.DeviceID" required valid-deviceid list="discovery-list" />
<input ng-if="!editingExisting" name="deviceID" id="deviceID" class="form-control text-monospace" type="text" ng-model="currentDevice.deviceID" required valid-deviceid list="discovery-list" />
<datalist id="discovery-list" ng-if="!editingExisting">
<option ng-repeat="(id,address) in discovery" value="{{ id }}" />
</datalist>
<div ng-if="editingExisting" class="well well-sm text-monospace">{{currentDevice.DeviceID}}</div>
<div ng-if="editingExisting" class="well well-sm text-monospace">{{currentDevice.deviceID}}</div>
<p class="help-block">
<span translate ng-if="deviceEditor.deviceID.$valid || deviceEditor.deviceID.$pristine">The device ID to enter here can be found in the "Edit > Show ID" dialog on the other device. Spaces and dashes are optional (ignored).</span>
<span translate ng-show="!editingExisting && (deviceEditor.deviceID.$valid || deviceEditor.deviceID.$pristine)">When adding a new device, keep in mind that this device must be added on the other side too.</span>
@@ -487,18 +488,18 @@
</div>
<div class="form-group">
<label translate for="name">Device Name</label>
<input id="name" class="form-control" type="text" ng-model="currentDevice.Name"></input>
<p translate ng-if="currentDevice.DeviceID == myID" class="help-block">Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.</p>
<p translate ng-if="currentDevice.DeviceID != myID" class="help-block">Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.</p>
<input id="name" class="form-control" type="text" ng-model="currentDevice.name"></input>
<p translate ng-if="currentDevice.deviceID == myID" class="help-block">Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.</p>
<p translate ng-if="currentDevice.deviceID != myID" class="help-block">Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.</p>
</div>
<div class="form-group">
<label translate for="addresses">Addresses</label>
<input ng-disabled="currentDevice.DeviceID == myID" id="addresses" class="form-control" type="text" ng-model="currentDevice.AddressesStr"></input>
<input ng-disabled="currentDevice.deviceID == myID" id="addresses" class="form-control" type="text" ng-model="currentDevice.addressesStr"></input>
<p translate class="help-block">Enter comma separated "ip:port" addresses or "dynamic" to perform automatic discovery of the address.</p>
</div>
<div ng-if="!editingSelf" class="form-group">
<label translate>Compression</label>
<select class="form-control" ng-model="currentDevice.Compression">
<select class="form-control" ng-model="currentDevice.compression">
<option value="always" translate>All Data</option>
<option value="metadata" translate>Metadata Only</option>
<option value="never" translate>Off</option>
@@ -507,7 +508,7 @@
<div ng-if="!editingSelf" class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentDevice.Introducer"> <span translate>Introducer</span>
<input type="checkbox" ng-model="currentDevice.introducer"> <span translate>Introducer</span>
</label>
<p translate class="help-block">Any devices configured on an introducer device will be added to this device as well.</p>
</div>
@@ -521,7 +522,7 @@
<div class="three-columns">
<div class="checkbox" ng-repeat="folder in folderList()">
<label>
<input type="checkbox" ng-model="currentDevice.selectedFolders[folder.ID]"> {{folder.ID}}
<input type="checkbox" ng-model="currentDevice.selectedFolders[folder.id]"> {{folder.id}}
</label>
</div>
</div>
@@ -555,7 +556,7 @@
<div class="col-md-12">
<div class="form-group" ng-class="{'has-error': folderEditor.folderID.$invalid && folderEditor.folderID.$dirty}">
<label for="folderID"><span translate>Folder ID</span></label>
<input name="folderID" ng-readonly="editingExisting" id="folderID" class="form-control" type="text" ng-model="currentFolder.ID" required unique-folder ng-pattern="/^[a-zA-Z0-9-_.]{1,64}$/"></input>
<input name="folderID" ng-readonly="editingExisting" id="folderID" class="form-control" type="text" ng-model="currentFolder.id" required unique-folder ng-pattern="/^[a-zA-Z0-9-_.]{1,64}$/"></input>
<p class="help-block">
<span translate ng-if="folderEditor.folderID.$valid || folderEditor.folderID.$pristine">Short identifier for the folder. Must be the same on all cluster devices.</span>
<span translate ng-if="folderEditor.folderID.$error.uniqueFolder">The folder ID must be unique.</span>
@@ -565,7 +566,7 @@
</div>
<div class="form-group" ng-class="{'has-error': folderEditor.folderPath.$invalid && folderEditor.folderPath.$dirty}">
<label translate for="folderPath">Folder Path</label>
<input name="folderPath" ng-readonly="editingExisting" id="folderPath" class="form-control" type="text" ng-model="currentFolder.Path" list="directory-list" required />
<input name="folderPath" ng-readonly="editingExisting" id="folderPath" class="form-control" type="text" ng-model="currentFolder.path" list="directory-list" required />
<datalist id="directory-list">
<option ng-repeat="directory in directoryList" value="{{ directory }}" />
</datalist>
@@ -576,7 +577,7 @@
</div>
<div class="form-group" ng-class="{'has-error': folderEditor.rescanIntervalS.$invalid && folderEditor.rescanIntervalS.$dirty}">
<label for="rescanIntervalS"><span translate>Rescan Interval</span> (s)</label>
<input name="rescanIntervalS" id="rescanIntervalS" class="form-control" type="number" ng-model="currentFolder.RescanIntervalS" required min="0"></input>
<input name="rescanIntervalS" id="rescanIntervalS" class="form-control" type="number" ng-model="currentFolder.rescanIntervalS" required min="0"></input>
<p class="help-block">
<span translate ng-if="!folderEditor.rescanIntervalS.$valid && folderEditor.rescanIntervalS.$dirty">The rescan interval must be a non-negative number of seconds.</span>
</p>
@@ -588,7 +589,7 @@
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentFolder.ReadOnly"> <span translate>Folder Master</span>
<input type="checkbox" ng-model="currentFolder.readOnly"> <span translate>Folder Master</span>
</label>
</div>
<p translate class="help-block">Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.</p>
@@ -596,7 +597,7 @@
<div class="form-group">
<div class="checkbox">
<label>
<input type="checkbox" ng-model="currentFolder.IgnorePerms"> <span translate>Ignore Permissions</span>
<input type="checkbox" ng-model="currentFolder.ignorePerms"> <span translate>Ignore Permissions</span>
</label>
</div>
<p translate class="help-block">File permission bits are ignored when looking for changes. Use on FAT filesystems.</p>
@@ -607,21 +608,26 @@
<label translate>File Versioning</label>
<div class="radio">
<label>
<input type="radio" ng-model="currentFolder.FileVersioningSelector" value="none"> <span translate>No File Versioning</span>
<input type="radio" ng-model="currentFolder.fileVersioningSelector" value="none"> <span translate>No File Versioning</span>
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="currentFolder.FileVersioningSelector" value="simple"> <span translate>Simple File Versioning</span>
<input type="radio" ng-model="currentFolder.fileVersioningSelector" value="simple"> <span translate>Simple File Versioning</span>
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="currentFolder.FileVersioningSelector" value="staggered"> <span translate>Staggered File Versioning</span>
<input type="radio" ng-model="currentFolder.fileVersioningSelector" value="staggered"> <span translate>Staggered File Versioning</span>
</label>
</div>
<div class="radio">
<label>
<input type="radio" ng-model="currentFolder.fileVersioningSelector" value="external"> <span translate>External File Versioning</span>
</label>
</div>
</div>
<div class="form-group" ng-if="currentFolder.FileVersioningSelector=='simple'" ng-class="{'has-error': folderEditor.simpleKeep.$invalid && folderEditor.simpleKeep.$dirty}">
<div class="form-group" ng-if="currentFolder.fileVersioningSelector=='simple'" ng-class="{'has-error': folderEditor.simpleKeep.$invalid && folderEditor.simpleKeep.$dirty}">
<p translate class="help-block">Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.</p>
<label translate for="simpleKeep">Keep Versions</label>
<input name="simpleKeep" id="simpleKeep" class="form-control" type="number" ng-model="currentFolder.simpleKeep" required min="1"></input>
@@ -631,7 +637,7 @@
<span translate ng-if="folderEditor.simpleKeep.$error.min && folderEditor.simpleKeep.$dirty">You must keep at least one version.</span>
</p>
</div>
<div class="form-group" ng-if="currentFolder.FileVersioningSelector=='staggered'" ng-class="{'has-error': folderEditor.staggeredMaxAge.$invalid && folderEditor.staggeredMaxAge.$dirty}">
<div class="form-group" ng-if="currentFolder.fileVersioningSelector=='staggered'" ng-class="{'has-error': folderEditor.staggeredMaxAge.$invalid && folderEditor.staggeredMaxAge.$dirty}">
<p class="help-block"><span translate>Files are moved to date stamped versions in a .stversions folder when replaced or deleted by syncthing.</span> <span translate>Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.</span></p>
<p translate class="help-block">The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.</p>
<label translate for="staggeredMaxAge">Maximum Age</label>
@@ -641,11 +647,20 @@
<span translate ng-if="folderEditor.staggeredMaxAge.$error.required && folderEditor.staggeredMaxAge.$dirty">The maximum age must be a number and cannot be blank.</span>
</p>
</div>
<div class="form-group" ng-if="currentFolder.FileVersioningSelector == 'staggered'">
<div class="form-group" ng-if="currentFolder.fileVersioningSelector == 'staggered'">
<label translate for="staggeredVersionsPath">Versions Path</label>
<input name="staggeredVersionsPath" id="staggeredVersionsPath" class="form-control" type="text" ng-model="currentFolder.staggeredVersionsPath"></input>
<p translate class="help-block">Path where versions should be stored (leave empty for the default .stversions folder in the folder).</p>
</div>
<div class="form-group" ng-if="currentFolder.fileVersioningSelector=='external'" ng-class="{'has-error': folderEditor.externalCommand.$invalid && folderEditor.externalCommand.$dirty}">
<p translate class="help-block">A external command handles the versioning. It has to remove the file from the synced folder.</p>
<label translate for="externalCommand">Command</label>
<input name="externalCommand" id="externalCommand" class="form-control" type="text" ng-model="currentFolder.externalCommand" required></input>
<p class="help-block">
<span translate ng-if="folderEditor.externalCommand.$valid || folderEditor.externalCommand.$pristine">The first command line parameter is the folder path and the second parameter is the relative path in the folder.</span>
<span translate ng-if="folderEditor.externalCommand.$error.required && folderEditor.externalCommand.$dirty">The path cannot be blank.</span>
</p>
</div>
</div>
</div>
@@ -657,7 +672,7 @@
<div class="three-columns">
<div class="checkbox" ng-repeat="device in otherDevices()">
<label>
<input type="checkbox" ng-model="currentFolder.selectedDevices[device.DeviceID]"> {{deviceName(device)}}
<input type="checkbox" ng-model="currentFolder.selectedDevices[device.deviceID]"> {{deviceName(device)}}
</label>
</div>
</div>
@@ -701,7 +716,7 @@
</dl>
</div>
<div class="modal-footer">
<div class="pull-left"><span translate>Editing</span> <code>{{currentFolder.Path}}{{system.pathSeparator}}.stignore</code></div>
<div class="pull-left"><span translate>Editing</span> <code>{{currentFolder.path}}{{system.pathSeparator}}.stignore</code></div>
<button type="button" class="btn btn-primary btn-sm" data-dismiss="modal" ng-click="saveIgnores()"><span class="glyphicon glyphicon-ok"></span>&emsp;<span translate>Save</span></button>
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal"><span class="glyphicon glyphicon-remove"></span>&emsp;<span translate>Close</span></button>
</div>
@@ -724,32 +739,32 @@
<div class="col-md-6">
<div class="form-group">
<label translate for="DeviceName">Device Name</label>
<input id="DeviceName" class="form-control" type="text" ng-model="tmpOptions.DeviceName">
<input id="DeviceName" class="form-control" type="text" ng-model="tmpOptions.deviceName">
</div>
<div class="form-group">
<label translate for="ListenAddressStr">Sync Protocol Listen Addresses</label>
<input id="ListenAddressStr" class="form-control" type="text" ng-model="tmpOptions.ListenAddressStr">
<input id="ListenAddressStr" class="form-control" type="text" ng-model="tmpOptions.listenAddressStr">
</div>
<div class="form-group">
<label translate for="MaxRecvKbps">Incoming Rate Limit (KiB/s)</label>
<input id="MaxRecvKbps" class="form-control" type="number" ng-model="tmpOptions.MaxRecvKbps">
<input id="MaxRecvKbps" class="form-control" type="number" ng-model="tmpOptions.maxRecvKbps">
</div>
<div class="form-group">
<label translate for="MaxSendKbps">Outgoing Rate Limit (KiB/s)</label>
<input id="MaxSendKbps" class="form-control" type="number" ng-model="tmpOptions.MaxSendKbps">
<input id="MaxSendKbps" class="form-control" type="number" ng-model="tmpOptions.maxSendKbps">
</div>
<div class="col-md-6">
<div class="form-group">
<div class="checkbox">
<label>
<input id="UPnPEnabled" type="checkbox" ng-model="tmpOptions.UPnPEnabled"> <span translate>Enable UPnP</span>
<input id="UPnPEnabled" type="checkbox" ng-model="tmpOptions.upnpEnabled"> <span translate>Enable UPnP</span>
</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="GlobalAnnEnabled" type="checkbox" ng-model="tmpOptions.GlobalAnnEnabled"> <span translate>Global Discovery</span>
<input id="GlobalAnnEnabled" type="checkbox" ng-model="tmpOptions.globalAnnounceEnabled"> <span translate>Global Discovery</span>
</label>
</div>
</div>
@@ -758,55 +773,55 @@
<div class="form-group">
<div class="checkbox">
<label ng-if="upgradeInfo">
<input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.AutoUpgradeEnabled"> <span translate>Automatic upgrades</span>
<input id="AutoUpgradeEnabled" type="checkbox" ng-model="tmpOptions.autoUpgradeEnabled"> <span translate>Automatic upgrades</span>
</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="LocalAnnEnabled" type="checkbox" ng-model="tmpOptions.LocalAnnEnabled"> <span translate>Local Discovery</span>
<input id="LocalAnnEnabled" type="checkbox" ng-model="tmpOptions.localAnnounceEnabled"> <span translate>Local Discovery</span>
</label>
</div>
</div>
</div>
<div class="form-group">
<label translate for="GlobalAnnServersStr">Global Discovery Server</label>
<input ng-disabled="!tmpOptions.GlobalAnnEnabled" id="GlobalAnnServersStr" class="form-control" type="text" ng-model="tmpOptions.GlobalAnnServersStr">
<input ng-disabled="!tmpOptions.globalAnnounceEnabled" id="GlobalAnnServersStr" class="form-control" type="text" ng-model="tmpOptions.globalAnnounceServersStr">
</div>
</div>
<div class="col-md-6">
<div class="form-group">
<label translate for="Address">GUI Listen Addresses</label>
<input id="Address" class="form-control" type="text" ng-model="tmpGUI.Address">
<input id="Address" class="form-control" type="text" ng-model="tmpGUI.address">
</div>
<div class="form-group">
<label translate for="User">GUI Authentication User</label>
<input id="User" class="form-control" type="text" ng-model="tmpGUI.User">
<input id="User" class="form-control" type="text" ng-model="tmpGUI.user">
</div>
<div class="form-group">
<label translate for="Password">GUI Authentication Password</label>
<input id="Password" class="form-control" type="password" ng-model="tmpGUI.Password">
<input id="Password" class="form-control" type="password" ng-model="tmpGUI.password">
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="UseTLS" type="checkbox" ng-model="tmpGUI.UseTLS"> <span translate>Use HTTPS for GUI</span>
<input id="UseTLS" type="checkbox" ng-model="tmpGUI.useTLS"> <span translate>Use HTTPS for GUI</span>
</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="StartBrowser" type="checkbox" ng-model="tmpOptions.StartBrowser"> <span translate>Start Browser</span>
<input id="StartBrowser" type="checkbox" ng-model="tmpOptions.startBrowser"> <span translate>Start Browser</span>
</label>
</div>
</div>
<div class="form-group">
<div class="checkbox">
<label>
<input id="UREnabled" type="checkbox" ng-model="tmpOptions.UREnabled"> <span translate>Anonymous Usage Reporting</span> (<a translate ng-click="showURPreview()" href="#">Preview</a>)
<input id="UREnabled" type="checkbox" ng-model="tmpOptions.urEnabled"> <span translate>Anonymous Usage Reporting</span> (<a translate ng-click="showURPreview()" href="#">Preview</a>)
</label>
</div>
</div>
@@ -815,7 +830,7 @@
<div class="form-group">
<label><span translate>API Key</span></label>
<div class="well well-sm text-monospace">{{tmpGUI.APIKey || "-"}}</div>
<div class="well well-sm text-monospace">{{tmpGUI.apiKey || "-"}}</div>
<button translate type="button" class="btn btn-sm btn-default" ng-click="setAPIKey(tmpGUI)">Generate</button>
</div>
</div>
@@ -889,34 +904,34 @@
<table class="table table-striped table-condensed">
<tr ng-repeat="f in needed.progress" ng-init="a = needAction(f)">
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
<td title="{{f.Name}}">{{f.Name | basename}}</td>
<td ng-if="a == 'sync' && progress[neededFolder] && progress[neededFolder][f.Name]">
<td title="{{f.name}}">{{f.name | basename}}</td>
<td ng-if="a == 'sync' && progress[neededFolder] && progress[neededFolder][f.name]">
<div class="progress">
<div class="progress-bar progress-bar-success" style="width: {{progress[neededFolder][f.Name].Reused}}%"></div>
<div class="progress-bar" style="width: {{progress[neededFolder][f.Name].CopiedFromOrigin}}%"></div>
<div class="progress-bar progress-bar-info" style="width: {{progress[neededFolder][f.Name].CopiedFromElsewhere}}%"></div>
<div class="progress-bar progress-bar-warning" style="width: {{progress[neededFolder][f.Name].Pulled}}%"></div>
<div class="progress-bar progress-bar-danger progress-bar-striped active" style="width: {{progress[neededFolder][f.Name].Pulling}}%"></div>
<div class="progress-bar progress-bar-success" style="width: {{progress[neededFolder][f.name].reused}}%"></div>
<div class="progress-bar" style="width: {{progress[neededFolder][f.name].copiedFromOrigin}}%"></div>
<div class="progress-bar progress-bar-info" style="width: {{progress[neededFolder][f.name].copiedFromElsewhere}}%"></div>
<div class="progress-bar progress-bar-warning" style="width: {{progress[neededFolder][f.name].pulled}}%"></div>
<div class="progress-bar progress-bar-danger progress-bar-striped active" style="width: {{progress[neededFolder][f.name].pulling}}%"></div>
<span class="show frontal">
{{progress[neededFolder][f.Name].BytesDone | binary}}B / {{progress[neededFolder][f.Name].BytesTotal | binary}}B
{{progress[neededFolder][f.name].bytesDone | binary}}B / {{progress[neededFolder][f.name].bytesTotal | binary}}B
</span>
</div>
</td>
<td class="text-right small-data" ng-if="a != 'sync' || !progress[neededFolder] || !progress[neededFolder][f.Name]">
<span ng-if="f.Size > 0">{{f.Size | binary}}B</span>
<td class="text-right small-data" ng-if="a != 'sync' || !progress[neededFolder] || !progress[neededFolder][f.name]">
<span ng-if="f.size > 0">{{f.size | binary}}B</span>
</td>
</tr>
<tr ng-repeat="f in needed.queued" ng-init="a = needAction(f)">
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
<td><a href="" ng-if="$index != 0" ng-click="bumpFile(neededFolder, f.Name)" title="{{'Move to top of queue' | translate}}"><span class="glyphicon glyphicon-eject"></span></a><span ng-if="$index != 0">&ensp;</span><span title="{{f.Name}}">{{f.Name | basename}}</span></td>
<td><a href="" ng-if="$index != 0" ng-click="bumpFile(neededFolder, f.name)" title="{{'Move to top of queue' | translate}}"><span class="glyphicon glyphicon-eject"></span></a><span ng-if="$index != 0">&ensp;</span><span title="{{f.name}}">{{f.name | basename}}</span></td>
<td class="text-right small-data">
<span ng-if="f.Size > 0">{{f.Size | binary}}B</span>
<span ng-if="f.size > 0">{{f.size | binary}}B</span>
</td>
</tr>
<tr ng-repeat="f in needed.rest" ng-init="a = needAction(f)">
<td class="small-data"><span class="glyphicon glyphicon-{{needIcons[a]}}"></span> {{needActions[a]}}</td>
<td title="{{f.Name}}">{{f.Name | basename}}</td>
<td class="text-right small-data"><span ng-if="f.Size > 0">{{f.Size | binary}}B</span></td>
<td title="{{f.name}}">{{f.name | basename}}</td>
<td class="text-right small-data"><span ng-if="f.size > 0">{{f.size | binary}}B</span></td>
</tr>
</table>
+1 -1
View File
@@ -70,7 +70,7 @@ function folderCompare(a, b) {
function folderMap(l) {
var m = {};
l.forEach(function (r) {
m[r.ID] = r;
m[r.id] = r;
});
return m;
}
@@ -1,7 +1,4 @@
angular.module('syncthing.core')
.config(function($locationProvider) {
$locationProvider.html5Mode(true).hashPrefix('!');
})
.controller('SyncthingController', function ($scope, $http, $location, LocaleService) {
'use strict';
@@ -144,8 +141,8 @@ angular.module('syncthing.core')
refreshFolderStats();
// Update completion status for all devices that we share this folder with.
$scope.folders[data.folder].Devices.forEach(function (deviceCfg) {
refreshCompletion(deviceCfg.DeviceID, data.folder);
$scope.folders[data.folder].devices.forEach(function (deviceCfg) {
refreshCompletion(deviceCfg.deviceID, data.folder);
});
});
@@ -165,9 +162,9 @@ angular.module('syncthing.core')
$scope.connections[arg.data.id] = {
inbps: 0,
outbps: 0,
InBytesTotal: 0,
OutBytesTotal: 0,
Address: arg.data.addr
inBytesTotal: 0,
outBytesTotal: 0,
address: arg.data.addr
};
$scope.completion[arg.data.id] = {
_total: 100
@@ -176,7 +173,7 @@ angular.module('syncthing.core')
});
$scope.$on('ConfigLoaded', function (event) {
if ($scope.config.Options.URAccepted === 0) {
if ($scope.config.options.urAccepted === 0) {
// If usage reporting has been neither accepted nor declined,
// we want to ask the user to make a choice. But we don't want
// to bug them during initial setup, so we set a cookie with
@@ -219,23 +216,23 @@ angular.module('syncthing.core')
progress[folder] = {};
for (var file in stats[folder]) {
var s = stats[folder][file];
var reused = 100 * s.Reused / s.Total;
var copiedFromOrigin = 100 * s.CopiedFromOrigin / s.Total;
var copiedFromElsewhere = 100 * s.CopiedFromElsewhere / s.Total;
var pulled = 100 * s.Pulled / s.Total;
var pulling = 100 * s.Pulling / s.Total;
var reused = 100 * s.reused / s.total;
var copiedFromOrigin = 100 * s.copiedFromOrigin / s.total;
var copiedFromElsewhere = 100 * s.copiedFromElsewhere / s.total;
var pulled = 100 * s.pulled / s.total;
var pulling = 100 * s.pulling / s.total;
// We try to round up pulling to atleast a percent so that it would be atleast a bit visible.
if (pulling < 1 && pulled + copiedFromElsewhere + copiedFromOrigin + reused <= 99) {
pulling = 1;
}
progress[folder][file] = {
Reused: reused,
CopiedFromOrigin: copiedFromOrigin,
CopiedFromElsewhere: copiedFromElsewhere,
Pulled: pulled,
Pulling: pulling,
BytesTotal: s.BytesTotal,
BytesDone: s.BytesDone,
reused: reused,
copiedFromOrigin: copiedFromOrigin,
copiedFromElsewhere: copiedFromElsewhere,
pulled: pulled,
pulling: pulling,
bytesTotal: s.bytesTotal,
bytesDone: s.bytesDone,
};
}
}
@@ -281,22 +278,21 @@ angular.module('syncthing.core')
var hasConfig = !isEmptyObject($scope.config);
$scope.config = config;
$scope.config.Options.ListenAddressStr = $scope.config.Options.ListenAddress.join(', ');
$scope.config.Options.GlobalAnnServersStr = $scope.config.Options.GlobalAnnServers.join(', ');
$scope.config.options.listenAddressStr = $scope.config.options.listenAddress.join(', ');
$scope.config.options.globalAnnounceServersStr = $scope.config.options.globalAnnounceServers.join(', ');
$scope.devices = $scope.config.Devices;
$scope.devices = $scope.config.devices;
$scope.devices.forEach(function (deviceCfg) {
$scope.completion[deviceCfg.DeviceID] = {
$scope.completion[deviceCfg.deviceID] = {
_total: 100
};
});
$scope.devices.sort(deviceCompare);
$scope.folders = folderMap($scope.config.Folders);
$scope.folders = folderMap($scope.config.folders);
Object.keys($scope.folders).forEach(function (folder) {
refreshFolder(folder);
$scope.folders[folder].Devices.forEach(function (deviceCfg) {
refreshCompletion(deviceCfg.DeviceID, folder);
$scope.folders[folder].devices.forEach(function (deviceCfg) {
refreshCompletion(deviceCfg.deviceID, folder);
});
});
@@ -365,8 +361,8 @@ angular.module('syncthing.core')
continue;
}
try {
data[id].inbps = Math.max(0, (data[id].InBytesTotal - $scope.connections[id].InBytesTotal) / td);
data[id].outbps = Math.max(0, (data[id].OutBytesTotal - $scope.connections[id].OutBytesTotal) / td);
data[id].inbps = Math.max(0, (data[id].inBytesTotal - $scope.connections[id].inBytesTotal) / td);
data[id].outbps = Math.max(0, (data[id].outBytesTotal - $scope.connections[id].outBytesTotal) / td);
} catch (e) {
data[id].inbps = 0;
data[id].outbps = 0;
@@ -408,8 +404,8 @@ angular.module('syncthing.core')
$http.get(urlbase + "/stats/device").success(function (data) {
$scope.deviceStats = data;
for (var device in $scope.deviceStats) {
$scope.deviceStats[device].LastSeen = new Date($scope.deviceStats[device].LastSeen);
$scope.deviceStats[device].LastSeenDays = (new Date() - $scope.deviceStats[device].LastSeen) / 1000 / 86400;
$scope.deviceStats[device].lastSeen = new Date($scope.deviceStats[device].lastSeen);
$scope.deviceStats[device].lastSeenDays = (new Date() - $scope.deviceStats[device].lastSeen) / 1000 / 86400;
}
console.log("refreshDeviceStats", data);
}).error($scope.emitHTTPError);
@@ -419,8 +415,8 @@ angular.module('syncthing.core')
$http.get(urlbase + "/stats/folder").success(function (data) {
$scope.folderStats = data;
for (var folder in $scope.folderStats) {
if ($scope.folderStats[folder].LastFile) {
$scope.folderStats[folder].LastFile.At = new Date($scope.folderStats[folder].LastFile.At);
if ($scope.folderStats[folder].lastFile) {
$scope.folderStats[folder].lastFile.at = new Date($scope.folderStats[folder].lastFile.at);
}
}
console.log("refreshfolderStats", data);
@@ -434,38 +430,38 @@ angular.module('syncthing.core')
};
$scope.folderStatus = function (folderCfg) {
if (typeof $scope.model[folderCfg.ID] === 'undefined') {
if (typeof $scope.model[folderCfg.id] === 'undefined') {
return 'unknown';
}
if (folderCfg.Devices.length <= 1) {
if (folderCfg.devices.length <= 1) {
return 'unshared';
}
if ($scope.model[folderCfg.ID].invalid !== '') {
if ($scope.model[folderCfg.id].invalid !== '') {
return 'stopped';
}
return '' + $scope.model[folderCfg.ID].state;
return '' + $scope.model[folderCfg.id].state;
};
$scope.folderClass = function (folderCfg) {
if (typeof $scope.model[folderCfg.ID] === 'undefined') {
if (typeof $scope.model[folderCfg.id] === 'undefined') {
// Unknown
return 'info';
}
if (folderCfg.Devices.length <= 1) {
if (folderCfg.devices.length <= 1) {
// Unshared
return 'warning';
}
if ($scope.model[folderCfg.ID].invalid !== '') {
if ($scope.model[folderCfg.id].invalid !== '') {
// Errored
return 'danger';
}
var state = '' + $scope.model[folderCfg.ID].state;
var state = '' + $scope.model[folderCfg.id].state;
if (state == 'idle') {
return 'success';
}
@@ -491,8 +487,8 @@ angular.module('syncthing.core')
};
$scope.deviceIcon = function (deviceCfg) {
if ($scope.connections[deviceCfg.DeviceID]) {
if ($scope.completion[deviceCfg.DeviceID] && $scope.completion[deviceCfg.DeviceID]._total === 100) {
if ($scope.connections[deviceCfg.deviceID]) {
if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
return 'ok';
} else {
return 'refresh';
@@ -507,8 +503,8 @@ angular.module('syncthing.core')
return 'unused';
}
if ($scope.connections[deviceCfg.DeviceID]) {
if ($scope.completion[deviceCfg.DeviceID] && $scope.completion[deviceCfg.DeviceID]._total === 100) {
if ($scope.connections[deviceCfg.deviceID]) {
if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
return 'insync';
} else {
return 'syncing';
@@ -525,8 +521,8 @@ angular.module('syncthing.core')
return 'warning';
}
if ($scope.connections[deviceCfg.DeviceID]) {
if ($scope.completion[deviceCfg.DeviceID] && $scope.completion[deviceCfg.DeviceID]._total === 100) {
if ($scope.connections[deviceCfg.deviceID]) {
if ($scope.completion[deviceCfg.deviceID] && $scope.completion[deviceCfg.deviceID]._total === 100) {
return 'success';
} else {
return 'primary';
@@ -538,24 +534,24 @@ angular.module('syncthing.core')
};
$scope.deviceAddr = function (deviceCfg) {
var conn = $scope.connections[deviceCfg.DeviceID];
var conn = $scope.connections[deviceCfg.deviceID];
if (conn) {
return conn.Address;
return conn.address;
}
return '?';
};
$scope.deviceCompletion = function (deviceCfg) {
var conn = $scope.connections[deviceCfg.DeviceID];
var conn = $scope.connections[deviceCfg.deviceID];
if (conn) {
return conn.Completion + '%';
return conn.completion + '%';
}
return '';
};
$scope.findDevice = function (deviceID) {
var matches = $scope.devices.filter(function (n) {
return n.DeviceID == deviceID;
return n.deviceID == deviceID;
});
if (matches.length != 1) {
return undefined;
@@ -567,10 +563,10 @@ angular.module('syncthing.core')
if (typeof deviceCfg === 'undefined') {
return "";
}
if (deviceCfg.Name) {
return deviceCfg.Name;
if (deviceCfg.name) {
return deviceCfg.name;
}
return deviceCfg.DeviceID.substr(0, 6);
return deviceCfg.deviceID.substr(0, 6);
};
$scope.thisDeviceName = function () {
@@ -578,19 +574,19 @@ angular.module('syncthing.core')
if (typeof device === 'undefined') {
return "(unknown device)";
}
if (device.Name) {
return device.Name;
if (device.name) {
return device.name;
}
return device.DeviceID.substr(0, 6);
return device.deviceID.substr(0, 6);
};
$scope.editSettings = function () {
// Make a working copy
$scope.tmpOptions = angular.copy($scope.config.Options);
$scope.tmpOptions.UREnabled = ($scope.tmpOptions.URAccepted > 0);
$scope.tmpOptions.DeviceName = $scope.thisDevice().Name;
$scope.tmpOptions.AutoUpgradeEnabled = ($scope.tmpOptions.AutoUpgradeIntervalH > 0);
$scope.tmpGUI = angular.copy($scope.config.GUI);
$scope.tmpOptions = angular.copy($scope.config.options);
$scope.tmpOptions.urEnabled = ($scope.tmpOptions.urAccepted > 0);
$scope.tmpOptions.deviceName = $scope.thisDevice().name;
$scope.tmpOptions.autoUpgradeEnabled = ($scope.tmpOptions.autoUpgradeIntervalH > 0);
$scope.tmpGUI = angular.copy($scope.config.gui);
$('#settings').modal();
};
@@ -610,34 +606,34 @@ angular.module('syncthing.core')
$scope.saveSettings = function () {
// Make sure something changed
var changed = !angular.equals($scope.config.Options, $scope.tmpOptions) || !angular.equals($scope.config.GUI, $scope.tmpGUI);
var changed = !angular.equals($scope.config.options, $scope.tmpOptions) || !angular.equals($scope.config.gui, $scope.tmpGUI);
if (changed) {
// Check if usage reporting has been enabled or disabled
if ($scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted <= 0) {
$scope.tmpOptions.URAccepted = 1000;
} else if (!$scope.tmpOptions.UREnabled && $scope.tmpOptions.URAccepted > 0) {
$scope.tmpOptions.URAccepted = -1;
if ($scope.tmpOptions.urEnabled && $scope.tmpOptions.urAccepted <= 0) {
$scope.tmpOptions.urAccepted = 1000;
} else if (!$scope.tmpOptions.urEnabled && $scope.tmpOptions.urAccepted > 0) {
$scope.tmpOptions.urAccepted = -1;
}
// Check if auto-upgrade has been enabled or disabled
if ($scope.tmpOptions.AutoUpgradeEnabled) {
$scope.tmpOptions.AutoUpgradeIntervalH = $scope.tmpOptions.AutoUpgradeIntervalH || 12;
if ($scope.tmpOptions.autoUpgradeEnabled) {
$scope.tmpOptions.autoUpgradeIntervalH = $scope.tmpOptions.autoUpgradeIntervalH || 12;
} else {
$scope.tmpOptions.AutoUpgradeIntervalH = 0;
$scope.tmpOptions.autoUpgradeIntervalH = 0;
}
// Check if protocol will need to be changed on restart
if ($scope.config.GUI.UseTLS !== $scope.tmpGUI.UseTLS) {
if ($scope.config.gui.useTLS !== $scope.tmpGUI.useTLS) {
$scope.protocolChanged = true;
}
// Apply new settings locally
$scope.thisDevice().Name = $scope.tmpOptions.DeviceName;
$scope.config.Options = angular.copy($scope.tmpOptions);
$scope.config.GUI = angular.copy($scope.tmpGUI);
$scope.thisDevice().name = $scope.tmpOptions.deviceName;
$scope.config.options = angular.copy($scope.tmpOptions);
$scope.config.gui = angular.copy($scope.tmpGUI);
['ListenAddress', 'GlobalAnnServers'].forEach(function (key) {
$scope.config.Options[key] = $scope.config.Options[key + "Str"].split(/[ ,]+/).map(function (x) {
['listenAddress', 'globalAnnounceServers'].forEach(function (key) {
$scope.config.options[key] = $scope.config.options[key + "Str"].split(/[ ,]+/).map(function (x) {
return x.trim();
});
});
@@ -658,7 +654,7 @@ angular.module('syncthing.core')
if ($scope.protocolChanged) {
var protocol = 'http';
if ($scope.config.GUI.UseTLS) {
if ($scope.config.gui.useTLS) {
protocol = 'https';
}
@@ -692,8 +688,8 @@ angular.module('syncthing.core')
$scope.editDevice = function (deviceCfg) {
$scope.currentDevice = $.extend({}, deviceCfg);
$scope.editingExisting = true;
$scope.editingSelf = (deviceCfg.DeviceID == $scope.myID);
$scope.currentDevice.AddressesStr = deviceCfg.Addresses.join(', ');
$scope.editingSelf = (deviceCfg.deviceID == $scope.myID);
$scope.currentDevice.addressesStr = deviceCfg.addresses.join(', ');
if (!$scope.editingSelf) {
$scope.currentDevice.selectedFolders = {};
$scope.deviceFolders($scope.currentDevice).forEach(function (folder) {
@@ -715,9 +711,9 @@ angular.module('syncthing.core')
})
.then(function () {
$scope.currentDevice = {
AddressesStr: 'dynamic',
Compression: 'metadata',
Introducer: false,
addressesStr: 'dynamic',
compression: 'metadata',
introducer: false,
selectedFolders: {}
};
$scope.editingExisting = false;
@@ -734,18 +730,18 @@ angular.module('syncthing.core')
}
$scope.devices = $scope.devices.filter(function (n) {
return n.DeviceID !== $scope.currentDevice.DeviceID;
return n.deviceID !== $scope.currentDevice.deviceID;
});
$scope.config.Devices = $scope.devices;
$scope.config.devices = $scope.devices;
// In case we later added the device manually, remove the ignoral
// record.
$scope.config.IgnoredDevices = $scope.config.IgnoredDevices.filter(function (id) {
return id !== $scope.currentDevice.DeviceID;
$scope.config.ignoredDevices = $scope.config.ignoredDevices.filter(function (id) {
return id !== $scope.currentDevice.deviceID;
});
for (var id in $scope.folders) {
$scope.folders[id].Devices = $scope.folders[id].Devices.filter(function (n) {
return n.DeviceID !== $scope.currentDevice.DeviceID;
$scope.folders[id].devices = $scope.folders[id].devices.filter(function (n) {
return n.deviceID !== $scope.currentDevice.deviceID;
});
}
@@ -759,10 +755,10 @@ angular.module('syncthing.core')
$scope.addNewDeviceID = function (device) {
var deviceCfg = {
DeviceID: device,
AddressesStr: 'dynamic',
Compression: 'metadata',
Introducer: false,
deviceID: device,
addressesStr: 'dynamic',
compression: 'metadata',
introducer: false,
selectedFolders: {}
};
$scope.saveDeviceConfig(deviceCfg);
@@ -771,13 +767,13 @@ angular.module('syncthing.core')
$scope.saveDeviceConfig = function (deviceCfg) {
var done, i;
deviceCfg.Addresses = deviceCfg.AddressesStr.split(',').map(function (x) {
deviceCfg.addresses = deviceCfg.addressesStr.split(',').map(function (x) {
return x.trim();
});
done = false;
for (i = 0; i < $scope.devices.length; i++) {
if ($scope.devices[i].DeviceID === deviceCfg.DeviceID) {
if ($scope.devices[i].deviceID === deviceCfg.deviceID) {
$scope.devices[i] = deviceCfg;
done = true;
break;
@@ -789,32 +785,32 @@ angular.module('syncthing.core')
}
$scope.devices.sort(deviceCompare);
$scope.config.Devices = $scope.devices;
$scope.config.devices = $scope.devices;
// In case we are adding the device manually, remove the ignoral
// record.
$scope.config.IgnoredDevices = $scope.config.IgnoredDevices.filter(function (id) {
return id !== deviceCfg.DeviceID;
$scope.config.ignoredDevices = $scope.config.ignoredDevices.filter(function (id) {
return id !== deviceCfg.deviceID;
});
if (!$scope.editingSelf) {
for (var id in deviceCfg.selectedFolders) {
if (deviceCfg.selectedFolders[id]) {
var found = false;
for (i = 0; i < $scope.folders[id].Devices.length; i++) {
if ($scope.folders[id].Devices[i].DeviceID == deviceCfg.DeviceID) {
for (i = 0; i < $scope.folders[id].devices.length; i++) {
if ($scope.folders[id].devices[i].deviceID == deviceCfg.deviceID) {
found = true;
break;
}
}
if (!found) {
$scope.folders[id].Devices.push({
DeviceID: deviceCfg.DeviceID
$scope.folders[id].devices.push({
deviceID: deviceCfg.deviceID
});
}
} else {
$scope.folders[id].Devices = $scope.folders[id].Devices.filter(function (n) {
return n.DeviceID != deviceCfg.DeviceID;
$scope.folders[id].devices = $scope.folders[id].devices.filter(function (n) {
return n.deviceID != deviceCfg.deviceID;
});
}
}
@@ -828,14 +824,14 @@ angular.module('syncthing.core')
};
$scope.ignoreRejectedDevice = function (device) {
$scope.config.IgnoredDevices.push(device);
$scope.config.ignoredDevices.push(device);
$scope.saveConfig();
$scope.dismissDeviceRejection(device);
};
$scope.otherDevices = function () {
return $scope.devices.filter(function (n) {
return n.DeviceID !== $scope.myID;
return n.deviceID !== $scope.myID;
});
};
@@ -844,7 +840,7 @@ angular.module('syncthing.core')
for (i = 0; i < $scope.devices.length; i++) {
n = $scope.devices[i];
if (n.DeviceID === $scope.myID) {
if (n.deviceID === $scope.myID) {
return n;
}
}
@@ -858,19 +854,19 @@ angular.module('syncthing.core')
$scope.errorList = function () {
return $scope.errors.filter(function (e) {
return e.Time > $scope.seenError;
return e.time > $scope.seenError;
});
};
$scope.clearErrors = function () {
$scope.seenError = $scope.errors[$scope.errors.length - 1].Time;
$scope.seenError = $scope.errors[$scope.errors.length - 1].time;
$http.post(urlbase + '/error/clear');
};
$scope.friendlyDevices = function (str) {
for (var i = 0; i < $scope.devices.length; i++) {
var cfg = $scope.devices[i];
str = str.replace(cfg.DeviceID, $scope.deviceName(cfg));
str = str.replace(cfg.deviceID, $scope.deviceName(cfg));
}
return str;
};
@@ -881,7 +877,7 @@ angular.module('syncthing.core')
$scope.directoryList = [];
$scope.$watch('currentFolder.Path', function (newvalue) {
$scope.$watch('currentFolder.path', function (newvalue) {
$http.get(urlbase + '/autocomplete/directory', {
params: { current: newvalue }
}).success(function (data) {
@@ -891,25 +887,29 @@ angular.module('syncthing.core')
$scope.editFolder = function (folderCfg) {
$scope.currentFolder = angular.copy(folderCfg);
if ($scope.currentFolder.Path.slice(-1) == $scope.system.pathSeparator) {
$scope.currentFolder.Path = $scope.currentFolder.Path.slice(0, -1);
if ($scope.currentFolder.path.slice(-1) == $scope.system.pathSeparator) {
$scope.currentFolder.path = $scope.currentFolder.path.slice(0, -1);
}
$scope.currentFolder.selectedDevices = {};
$scope.currentFolder.Devices.forEach(function (n) {
$scope.currentFolder.selectedDevices[n.DeviceID] = true;
$scope.currentFolder.devices.forEach(function (n) {
$scope.currentFolder.selectedDevices[n.deviceID] = true;
});
if ($scope.currentFolder.Versioning && $scope.currentFolder.Versioning.Type === "simple") {
if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "simple") {
$scope.currentFolder.simpleFileVersioning = true;
$scope.currentFolder.FileVersioningSelector = "simple";
$scope.currentFolder.simpleKeep = +$scope.currentFolder.Versioning.Params.keep;
} else if ($scope.currentFolder.Versioning && $scope.currentFolder.Versioning.Type === "staggered") {
$scope.currentFolder.fileVersioningSelector = "simple";
$scope.currentFolder.simpleKeep = +$scope.currentFolder.versioning.params.keep;
} else if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "staggered") {
$scope.currentFolder.staggeredFileVersioning = true;
$scope.currentFolder.FileVersioningSelector = "staggered";
$scope.currentFolder.staggeredMaxAge = Math.floor(+$scope.currentFolder.Versioning.Params.maxAge / 86400);
$scope.currentFolder.staggeredCleanInterval = +$scope.currentFolder.Versioning.Params.cleanInterval;
$scope.currentFolder.staggeredVersionsPath = $scope.currentFolder.Versioning.Params.versionsPath;
$scope.currentFolder.fileVersioningSelector = "staggered";
$scope.currentFolder.staggeredMaxAge = Math.floor(+$scope.currentFolder.versioning.params.maxAge / 86400);
$scope.currentFolder.staggeredCleanInterval = +$scope.currentFolder.versioning.params.cleanInterval;
$scope.currentFolder.staggeredVersionsPath = $scope.currentFolder.versioning.params.versionsPath;
} else if ($scope.currentFolder.versioning && $scope.currentFolder.versioning.type === "external") {
$scope.currentFolder.externalFileVersioning = true;
$scope.currentFolder.fileVersioningSelector = "external";
$scope.currentFolder.externalCommand = $scope.currentFolder.versioning.params.command;
} else {
$scope.currentFolder.FileVersioningSelector = "none";
$scope.currentFolder.fileVersioningSelector = "none";
}
$scope.currentFolder.simpleKeep = $scope.currentFolder.simpleKeep || 5;
$scope.currentFolder.staggeredCleanInterval = $scope.currentFolder.staggeredCleanInterval || 3600;
@@ -921,6 +921,7 @@ angular.module('syncthing.core')
if (typeof $scope.currentFolder.staggeredMaxAge === 'undefined') {
$scope.currentFolder.staggeredMaxAge = 365;
}
$scope.currentFolder.externalCommand = $scope.currentFolder.externalCommand || "";
$scope.editingExisting = true;
$scope.folderEditor.$setPristine();
@@ -931,12 +932,13 @@ angular.module('syncthing.core')
$scope.currentFolder = {
selectedDevices: {}
};
$scope.currentFolder.RescanIntervalS = 60;
$scope.currentFolder.FileVersioningSelector = "none";
$scope.currentFolder.rescanIntervalS = 60;
$scope.currentFolder.fileVersioningSelector = "none";
$scope.currentFolder.simpleKeep = 5;
$scope.currentFolder.staggeredMaxAge = 365;
$scope.currentFolder.staggeredCleanInterval = 3600;
$scope.currentFolder.staggeredVersionsPath = "";
$scope.currentFolder.externalCommand = "";
$scope.currentFolder.autoNormalize = true;
$scope.editingExisting = false;
$scope.folderEditor.$setPristine();
@@ -951,12 +953,13 @@ angular.module('syncthing.core')
};
$scope.currentFolder.selectedDevices[device] = true;
$scope.currentFolder.RescanIntervalS = 60;
$scope.currentFolder.FileVersioningSelector = "none";
$scope.currentFolder.rescanIntervalS = 60;
$scope.currentFolder.fileVersioningSelector = "none";
$scope.currentFolder.simpleKeep = 5;
$scope.currentFolder.staggeredMaxAge = 365;
$scope.currentFolder.staggeredCleanInterval = 3600;
$scope.currentFolder.staggeredVersionsPath = "";
$scope.currentFolder.externalCommand = "";
$scope.currentFolder.autoNormalize = true;
$scope.editingExisting = false;
$scope.folderEditor.$setPristine();
@@ -964,10 +967,10 @@ angular.module('syncthing.core')
};
$scope.shareFolderWithDevice = function (folder, device) {
$scope.folders[folder].Devices.push({
DeviceID: device
$scope.folders[folder].devices.push({
deviceID: device
});
$scope.config.Folders = folderList($scope.folders);
$scope.config.folders = folderList($scope.folders);
$scope.saveConfig();
$scope.dismissFolderRejection(folder, device);
};
@@ -977,19 +980,19 @@ angular.module('syncthing.core')
$('#editFolder').modal('hide');
folderCfg = $scope.currentFolder;
folderCfg.Devices = [];
folderCfg.devices = [];
folderCfg.selectedDevices[$scope.myID] = true;
for (var deviceID in folderCfg.selectedDevices) {
if (folderCfg.selectedDevices[deviceID] === true) {
folderCfg.Devices.push({
DeviceID: deviceID
folderCfg.devices.push({
deviceID: deviceID
});
}
}
delete folderCfg.selectedDevices;
if (folderCfg.FileVersioningSelector === "simple") {
folderCfg.Versioning = {
if (folderCfg.fileVersioningSelector === "simple") {
folderCfg.versioning = {
'Type': 'simple',
'Params': {
'keep': '' + folderCfg.simpleKeep
@@ -997,10 +1000,10 @@ angular.module('syncthing.core')
};
delete folderCfg.simpleFileVersioning;
delete folderCfg.simpleKeep;
} else if (folderCfg.FileVersioningSelector === "staggered") {
folderCfg.Versioning = {
'Type': 'staggered',
'Params': {
} else if (folderCfg.fileVersioningSelector === "staggered") {
folderCfg.versioning = {
'type': 'staggered',
'params': {
'maxAge': '' + (folderCfg.staggeredMaxAge * 86400),
'cleanInterval': '' + folderCfg.staggeredCleanInterval,
'versionsPath': '' + folderCfg.staggeredVersionsPath
@@ -1011,12 +1014,21 @@ angular.module('syncthing.core')
delete folderCfg.staggeredCleanInterval;
delete folderCfg.staggeredVersionsPath;
} else if (folderCfg.fileVersioningSelector === "external") {
folderCfg.versioning = {
'Type': 'external',
'Params': {
'command': '' + folderCfg.externalCommand
}
};
delete folderCfg.externalFileVersioning;
delete folderCfg.externalCommand;
} else {
delete folderCfg.Versioning;
delete folderCfg.versioning;
}
$scope.folders[folderCfg.ID] = folderCfg;
$scope.config.Folders = folderList($scope.folders);
$scope.folders[folderCfg.id] = folderCfg;
$scope.config.folders = folderList($scope.folders);
$scope.saveConfig();
};
@@ -1027,9 +1039,9 @@ angular.module('syncthing.core')
$scope.sharesFolder = function (folderCfg) {
var names = [];
folderCfg.Devices.forEach(function (device) {
if (device.DeviceID != $scope.myID) {
names.push($scope.deviceName($scope.findDevice(device.DeviceID)));
folderCfg.devices.forEach(function (device) {
if (device.deviceID != $scope.myID) {
names.push($scope.deviceName($scope.findDevice(device.deviceID)));
}
});
names.sort();
@@ -1039,9 +1051,9 @@ angular.module('syncthing.core')
$scope.deviceFolders = function (deviceCfg) {
var folders = [];
for (var folderID in $scope.folders) {
var devices = $scope.folders[folderID].Devices
var devices = $scope.folders[folderID].devices
for (var i = 0; i < devices.length; i++) {
if (devices[i].DeviceID == deviceCfg.DeviceID) {
if (devices[i].deviceID == deviceCfg.deviceID) {
folders.push(folderID);
break;
}
@@ -1058,8 +1070,8 @@ angular.module('syncthing.core')
return;
}
delete $scope.folders[$scope.currentFolder.ID];
$scope.config.Folders = folderList($scope.folders);
delete $scope.folders[$scope.currentFolder.id];
$scope.config.folders = folderList($scope.folders);
$scope.saveConfig();
};
@@ -1070,7 +1082,7 @@ angular.module('syncthing.core')
}
$('#editIgnoresButton').attr('disabled', 'disabled');
$http.get(urlbase + '/ignores?folder=' + encodeURIComponent($scope.currentFolder.ID))
$http.get(urlbase + '/ignores?folder=' + encodeURIComponent($scope.currentFolder.id))
.success(function (data) {
data.ignore = data.ignore || [];
@@ -1097,13 +1109,13 @@ angular.module('syncthing.core')
return;
}
$http.post(urlbase + '/ignores?folder=' + encodeURIComponent($scope.currentFolder.ID), {
$http.post(urlbase + '/ignores?folder=' + encodeURIComponent($scope.currentFolder.id), {
ignore: $('#editIgnores textarea').val().split('\n')
});
};
$scope.setAPIKey = function (cfg) {
cfg.APIKey = randomString(32);
cfg.apiKey = randomString(32);
};
$scope.showURPreview = function () {
@@ -1114,13 +1126,13 @@ angular.module('syncthing.core')
};
$scope.acceptUR = function () {
$scope.config.Options.URAccepted = 1000; // Larger than the largest existing report version
$scope.config.options.urAccepted = 1000; // Larger than the largest existing report version
$scope.saveConfig();
$('#ur').modal('hide');
};
$scope.declineUR = function () {
$scope.config.Options.URAccepted = -1;
$scope.config.options.urAccepted = -1;
$scope.saveConfig();
$('#ur').modal('hide');
};
@@ -1138,11 +1150,11 @@ angular.module('syncthing.core')
var fDelete = 4096;
var fDirectory = 16384;
if ((file.Flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
if ((file.flags & (fDelete + fDirectory)) === fDelete + fDirectory) {
return 'rmdir';
} else if ((file.Flags & fDelete) === fDelete) {
} else if ((file.flags & fDelete) === fDelete) {
return 'rm';
} else if ((file.Flags & fDirectory) === fDirectory) {
} else if ((file.flags & fDirectory) === fDirectory) {
return 'touch';
} else {
return 'sync';
File diff suppressed because one or more lines are too long
+56 -61
View File
@@ -30,12 +30,12 @@ var l = logger.DefaultLogger
const CurrentVersion = 10
type Configuration struct {
Version int `xml:"version,attr"`
Folders []FolderConfiguration `xml:"folder"`
Devices []DeviceConfiguration `xml:"device"`
GUI GUIConfiguration `xml:"gui"`
Options OptionsConfiguration `xml:"options"`
IgnoredDevices []protocol.DeviceID `xml:"ignoredDevice"`
Version int `xml:"version,attr" json:"version"`
Folders []FolderConfiguration `xml:"folder" json:"folders"`
Devices []DeviceConfiguration `xml:"device" json:"devices"`
GUI GUIConfiguration `xml:"gui" json:"gui"`
Options OptionsConfiguration `xml:"options" json:"options"`
IgnoredDevices []protocol.DeviceID `xml:"ignoredDevice" json:"ignoredDevices"`
XMLName xml.Name `xml:"configuration" json:"-"`
OriginalVersion int `xml:"-" json:"-"` // The version we read from disk, before any conversion
@@ -44,20 +44,20 @@ type Configuration struct {
}
type FolderConfiguration struct {
ID string `xml:"id,attr"`
Path string `xml:"path,attr"`
Devices []FolderDeviceConfiguration `xml:"device"`
ReadOnly bool `xml:"ro,attr"`
RescanIntervalS int `xml:"rescanIntervalS,attr"`
IgnorePerms bool `xml:"ignorePerms,attr"`
AutoNormalize bool `xml:"autoNormalize,attr"`
Versioning VersioningConfiguration `xml:"versioning"`
LenientMtimes bool `xml:"lenientMtimes"`
Copiers int `xml:"copiers"` // This defines how many files are handled concurrently.
Pullers int `xml:"pullers"` // Defines how many blocks are fetched at the same time, possibly between separate copier routines.
Hashers int `xml:"hashers"` // Less than one sets the value to the number of cores. These are CPU bound due to hashing.
ID string `xml:"id,attr" json:"id"`
Path string `xml:"path,attr" json:"path"`
Devices []FolderDeviceConfiguration `xml:"device" json:"devices"`
ReadOnly bool `xml:"ro,attr" json:"readOnly"`
RescanIntervalS int `xml:"rescanIntervalS,attr" json:"rescanIntervalS"`
IgnorePerms bool `xml:"ignorePerms,attr" json:"ignorePerms"`
AutoNormalize bool `xml:"autoNormalize,attr" json:"autoNormalize"`
Versioning VersioningConfiguration `xml:"versioning" json:"versioning"`
LenientMtimes bool `xml:"lenientMtimes" json:"lenientMTimes"`
Copiers int `xml:"copiers" json:"copiers"` // This defines how many files are handled concurrently.
Pullers int `xml:"pullers" json:"pullers"` // Defines how many blocks are fetched at the same time, possibly between separate copier routines.
Hashers int `xml:"hashers" json:"hashers"` // Less than one sets the value to the number of cores. These are CPU bound due to hashing.
Invalid string `xml:"-"` // Set at runtime when there is an error, not saved
Invalid string `xml:"-" json:"invalid"` // Set at runtime when there is an error, not saved
deviceIDs []protocol.DeviceID
@@ -97,8 +97,8 @@ func (f *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
}
type VersioningConfiguration struct {
Type string `xml:"type,attr"`
Params map[string]string
Type string `xml:"type,attr" json:"type"`
Params map[string]string `json:"params"`
}
type InternalVersioningConfiguration struct {
@@ -138,44 +138,44 @@ func (c *VersioningConfiguration) UnmarshalXML(d *xml.Decoder, start xml.StartEl
}
type DeviceConfiguration struct {
DeviceID protocol.DeviceID `xml:"id,attr"`
Name string `xml:"name,attr,omitempty"`
Addresses []string `xml:"address,omitempty"`
Compression protocol.Compression `xml:"compression,attr"`
CertName string `xml:"certName,attr,omitempty"`
Introducer bool `xml:"introducer,attr"`
DeviceID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
Name string `xml:"name,attr,omitempty" json:"name"`
Addresses []string `xml:"address,omitempty" json:"addresses"`
Compression protocol.Compression `xml:"compression,attr" json:"compression"`
CertName string `xml:"certName,attr,omitempty" json:"certName"`
Introducer bool `xml:"introducer,attr" json:"introducer"`
}
type FolderDeviceConfiguration struct {
DeviceID protocol.DeviceID `xml:"id,attr"`
DeviceID protocol.DeviceID `xml:"id,attr" json:"deviceID"`
Deprecated_Name string `xml:"name,attr,omitempty" json:"-"`
Deprecated_Addresses []string `xml:"address,omitempty" json:"-"`
}
type OptionsConfiguration struct {
ListenAddress []string `xml:"listenAddress" default:"0.0.0.0:22000"`
GlobalAnnServers []string `xml:"globalAnnounceServer" default:"udp4://announce.syncthing.net:22026, udp6://announce-v6.syncthing.net:22026"`
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" default:"true"`
LocalAnnEnabled bool `xml:"localAnnounceEnabled" default:"true"`
LocalAnnPort int `xml:"localAnnouncePort" default:"21025"`
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" default:"[ff32::5222]:21026"`
MaxSendKbps int `xml:"maxSendKbps"`
MaxRecvKbps int `xml:"maxRecvKbps"`
ReconnectIntervalS int `xml:"reconnectionIntervalS" default:"60"`
StartBrowser bool `xml:"startBrowser" default:"true"`
UPnPEnabled bool `xml:"upnpEnabled" default:"true"`
UPnPLease int `xml:"upnpLeaseMinutes" default:"0"`
UPnPRenewal int `xml:"upnpRenewalMinutes" default:"30"`
URAccepted int `xml:"urAccepted"` // Accepted usage reporting version; 0 for off (undecided), -1 for off (permanently)
URUniqueID string `xml:"urUniqueID"` // Unique ID for reporting purposes, regenerated when UR is turned on.
RestartOnWakeup bool `xml:"restartOnWakeup" default:"true"`
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" default:"12"` // 0 for off
KeepTemporariesH int `xml:"keepTemporariesH" default:"24"` // 0 for off
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" default:"true"`
ProgressUpdateIntervalS int `xml:"progressUpdateIntervalS" default:"5"`
SymlinksEnabled bool `xml:"symlinksEnabled" default:"true"`
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" default:"false"`
ListenAddress []string `xml:"listenAddress" json:"listenAddress" default:"0.0.0.0:22000"`
GlobalAnnServers []string `xml:"globalAnnounceServer" json:"globalAnnounceServers" json:"globalAnnounceServer" default:"udp4://announce.syncthing.net:22026, udp6://announce-v6.syncthing.net:22026"`
GlobalAnnEnabled bool `xml:"globalAnnounceEnabled" json:"globalAnnounceEnabled" default:"true"`
LocalAnnEnabled bool `xml:"localAnnounceEnabled" json:"localAnnounceEnabled" default:"true"`
LocalAnnPort int `xml:"localAnnouncePort" json:"localAnnouncePort" default:"21025"`
LocalAnnMCAddr string `xml:"localAnnounceMCAddr" json:"localAnnounceMCAddr" default:"[ff32::5222]:21026"`
MaxSendKbps int `xml:"maxSendKbps" json:"maxSendKbps"`
MaxRecvKbps int `xml:"maxRecvKbps" json:"maxRecvKbps"`
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"`
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"`
AutoUpgradeIntervalH int `xml:"autoUpgradeIntervalH" json:"autoUpgradeIntervalH" default:"12"` // 0 for off
KeepTemporariesH int `xml:"keepTemporariesH" json:"keepTemporariesH" default:"24"` // 0 for off
CacheIgnoredFiles bool `xml:"cacheIgnoredFiles" json:"cacheIgnoredFiles" default:"true"`
ProgressUpdateIntervalS int `xml:"progressUpdateIntervalS" json:"progressUpdateIntervalS" default:"5"`
SymlinksEnabled bool `xml:"symlinksEnabled" json:"symlinksEnabled" default:"true"`
LimitBandwidthInLan bool `xml:"limitBandwidthInLan" json:"limitBandwidthInLan" default:"false"`
Deprecated_RescanIntervalS int `xml:"rescanIntervalS,omitempty" json:"-"`
Deprecated_UREnabled bool `xml:"urEnabled,omitempty" json:"-"`
@@ -186,12 +186,12 @@ type OptionsConfiguration struct {
}
type GUIConfiguration struct {
Enabled bool `xml:"enabled,attr" default:"true"`
Address string `xml:"address" default:"127.0.0.1:8080"`
User string `xml:"user,omitempty"`
Password string `xml:"password,omitempty"`
UseTLS bool `xml:"tls,attr"`
APIKey string `xml:"apikey,omitempty"`
Enabled bool `xml:"enabled,attr" json:"enabled" default:"true"`
Address string `xml:"address" json:"address" default:"127.0.0.1:8080"`
User string `xml:"user,omitempty" json:"user"`
Password string `xml:"password,omitempty" json:"password"`
UseTLS bool `xml:"tls,attr" json:"useTLS"`
APIKey string `xml:"apikey,omitempty" json:"apiKey"`
}
func New(myID protocol.DeviceID) Configuration {
@@ -370,11 +370,6 @@ func (cfg *Configuration) prepare(myID protocol.DeviceID) {
}
}
// Very short reconnection intervals are annoying
if cfg.Options.ReconnectIntervalS < 5 {
cfg.Options.ReconnectIntervalS = 5
}
cfg.Options.ListenAddress = uniqueStrings(cfg.Options.ListenAddress)
cfg.Options.GlobalAnnServers = uniqueStrings(cfg.Options.GlobalAnnServers)
+6 -26
View File
@@ -27,17 +27,14 @@ type Discoverer struct {
localBcastIntv time.Duration
localBcastStart time.Time
cacheLifetime time.Duration
negCacheCutoff time.Duration
broadcastBeacon beacon.Interface
multicastBeacon beacon.Interface
registry map[protocol.DeviceID][]CacheEntry
registryLock sync.RWMutex
extPort uint16
localBcastTick <-chan time.Time
forcedBcastTick chan time.Time
registryLock sync.RWMutex
registry map[protocol.DeviceID][]CacheEntry
lastLookup map[protocol.DeviceID]time.Time
clients []Client
mut sync.RWMutex
}
@@ -57,9 +54,7 @@ func NewDiscoverer(id protocol.DeviceID, addresses []string) *Discoverer {
listenAddrs: addresses,
localBcastIntv: 30 * time.Second,
cacheLifetime: 5 * time.Minute,
negCacheCutoff: 3 * time.Minute,
registry: make(map[protocol.DeviceID][]CacheEntry),
lastLookup: make(map[protocol.DeviceID]time.Time),
}
}
@@ -160,28 +155,18 @@ func (d *Discoverer) ExtAnnounceOK() map[string]bool {
func (d *Discoverer) Lookup(device protocol.DeviceID) []string {
d.registryLock.RLock()
cached := d.filterCached(d.registry[device])
lastLookup := d.lastLookup[device]
d.registryLock.RUnlock()
d.mut.RLock()
defer d.mut.RUnlock()
var addrs []string
if len(cached) > 0 {
// There are cached address entries.
addrs := make([]string, len(cached))
addrs = make([]string, len(cached))
for i := range cached {
addrs[i] = cached[i].Address
}
return addrs
}
if time.Since(lastLookup) < d.negCacheCutoff {
// We have recently tried to lookup this address and failed. Lets
// chill for a while.
return nil
}
if len(d.clients) != 0 && time.Since(d.localBcastStart) > d.localBcastIntv {
} else if len(d.clients) != 0 && time.Since(d.localBcastStart) > d.localBcastIntv {
// Only perform external lookups if we have at least one external
// server client and one local announcement interval has passed. This is
// to avoid finding local peers on their remote address at startup.
@@ -202,7 +187,6 @@ func (d *Discoverer) Lookup(device protocol.DeviceID) []string {
seen := make(map[string]struct{})
now := time.Now()
var addrs []string
for result := range results {
for _, addr := range result {
_, ok := seen[addr]
@@ -219,13 +203,9 @@ func (d *Discoverer) Lookup(device protocol.DeviceID) []string {
d.registryLock.Lock()
d.registry[device] = cached
d.lastLookup[device] = time.Now()
d.registryLock.Unlock()
return addrs
}
return nil
return addrs
}
func (d *Discoverer) Hint(device string, addrs []string) {
+11
View File
@@ -9,6 +9,7 @@ package model
import (
"bufio"
"crypto/tls"
"encoding/json"
"errors"
"fmt"
"io"
@@ -189,6 +190,16 @@ type ConnectionInfo struct {
ClientVersion string
}
func (info ConnectionInfo) MarshalJSON() ([]byte, error) {
return json.Marshal(map[string]interface{}{
"at": info.At,
"inBytesTotal": info.InBytesTotal,
"outBytesTotal": info.OutBytesTotal,
"address": info.Address,
"clientVersion": info.ClientVersion,
})
}
// ConnectionStats returns a map with connection statistics for each connected device.
func (m *Model) ConnectionStats() map[string]ConnectionInfo {
type remoteAddrer interface {
+1 -5
View File
@@ -489,11 +489,7 @@ func (p *rwFolder) handleDir(file protocol.FileInfo) {
// we can pass it to InWritableDir. We use a regular Mkdir and
// not MkdirAll because the parent should already exist.
mkdir := func(path string) error {
err = os.Mkdir(path, mode)
if err != nil || p.ignorePerms {
return err
}
return os.Chmod(path, mode)
return os.Mkdir(path, mode)
}
if err = osutil.InWritableDir(mkdir, realName); err == nil {
+8 -8
View File
@@ -40,14 +40,14 @@ type sharedPullerState struct {
// A momentary state representing the progress of the puller
type pullerProgress struct {
Total int
Reused int
CopiedFromOrigin int
CopiedFromElsewhere int
Pulled int
Pulling int
BytesDone int64
BytesTotal int64
Total int `json:"total"`
Reused int `json:"reused"`
CopiedFromOrigin int `json:"copiedFromOrigin"`
CopiedFromElsewhere int `json:"copiedFromElsewhere"`
Pulled int `json:"pulled"`
Pulling int `json:"pulling"`
BytesDone int64 `json:"bytesDone"`
BytesTotal int64 `json:"bytesTotal"`
}
// A lockedWriterAt synchronizes WriteAt calls with an external mutex.
+1 -2
View File
@@ -111,8 +111,7 @@ func (w *Walker) walkAndHashFiles(fchan chan protocol.FileInfo) filepath.WalkFun
// Return value used when we are returning early and don't want to
// process the item. For directories, this means do-not-descend.
var skip error // nil
// info nil when error is not nil
if info != nil && info.IsDir() {
if info.IsDir() {
skip = filepath.SkipDir
}
-8
View File
@@ -259,14 +259,6 @@ func TestNormalization(t *testing.T) {
}
}
func TestIssue1507(t *testing.T) {
w := Walker{}
c := make(chan protocol.FileInfo, 100)
fn := w.walkAndHashFiles(c)
fn("", nil, protocol.ErrClosed)
}
func walkDir(dir string) ([]protocol.FileInfo, error) {
w := Walker{
Dir: dir,
+1 -1
View File
@@ -15,7 +15,7 @@ import (
)
type DeviceStatistics struct {
LastSeen time.Time
LastSeen time.Time `json:"lastSeen"`
}
type DeviceStatisticsReference struct {
+3 -3
View File
@@ -14,7 +14,7 @@ import (
)
type FolderStatistics struct {
LastFile LastFile
LastFile LastFile `json:"lastFile"`
}
type FolderStatisticsReference struct {
@@ -23,8 +23,8 @@ type FolderStatisticsReference struct {
}
type LastFile struct {
At time.Time
Filename string
At time.Time `json:"at"`
Filename string `json:"filename"`
}
func NewFolderStatisticsReference(ldb *leveldb.DB, folder string) *FolderStatisticsReference {
+89
View File
@@ -0,0 +1,89 @@
// Copyright (C) 2015 The Syncthing Authors.
//
// This Source Code Form is subject to the terms of the Mozilla Public
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
// You can obtain one at http://mozilla.org/MPL/2.0/.
package versioner
import (
"errors"
"os"
"os/exec"
"path/filepath"
"strings"
)
func init() {
// Register the constructor for this type of versioner with the name "external"
Factories["external"] = NewExternal
}
// The type holds our configuration
type External struct {
command string
folderPath string
}
// The constructor function takes a map of parameters and creates the type.
func NewExternal(folderID, folderPath string, params map[string]string) Versioner {
command := params["command"]
s := External{
command: command,
folderPath: folderPath,
}
if debug {
l.Debugf("instantiated %#v", s)
}
return s
}
// 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)
if os.IsNotExist(err) {
if debug {
l.Debugln("not archiving nonexistent file", filePath)
}
return nil
} else if err != nil {
return err
}
if debug {
l.Debugln("archiving", filePath)
}
inFolderPath, err := filepath.Rel(v.folderPath, filePath)
if err != nil {
return err
}
if v.command == "" {
return errors.New("Versioner: command is empty, please enter a valid command")
}
cmd := exec.Command(v.command, v.folderPath, inFolderPath)
env := os.Environ()
// filter STGUIAUTH and STGUIAPIKEY from environment variables
filteredEnv := []string{}
for _, x := range env {
if !strings.HasPrefix(x, "STGUIAUTH=") && !strings.HasPrefix(x, "STGUIAPIKEY=") {
filteredEnv = append(filteredEnv, x)
}
}
cmd.Env = filteredEnv
err = cmd.Run()
if err != nil {
return err
}
// return error if the file was not removed
if _, err = os.Lstat(filePath); os.IsNotExist(err) {
return nil
}
return errors.New("Versioner: file was not removed by external script")
}
+5 -6
View File
@@ -47,13 +47,12 @@ func NewSimple(folderID, folderPath string, params map[string]string) Versioner
// nil, the named file does not exist any more (has been archived).
func (v Simple) Archive(filePath string) error {
fileInfo, err := os.Lstat(filePath)
if err != nil {
if os.IsNotExist(err) {
if debug {
l.Debugln("not archiving nonexistent file", filePath)
}
return nil
if os.IsNotExist(err) {
if debug {
l.Debugln("not archiving nonexistent file", filePath)
}
return nil
} else if err != nil {
return err
}
+6 -6
View File
@@ -281,13 +281,13 @@ func (v Staggered) Archive(filePath string) error {
v.mutex.Lock()
defer v.mutex.Unlock()
if _, err := os.Lstat(filePath); err != nil {
if os.IsNotExist(err) {
if debug {
l.Debugln("not archiving nonexistent file", filePath)
}
return nil
_, err := os.Lstat(filePath)
if os.IsNotExist(err) {
if debug {
l.Debugln("not archiving nonexistent file", filePath)
}
return nil
} else if err != nil {
return err
}