mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-06 04:49:09 -05:00
Compare commits
32 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9da422f1c5 | ||
|
|
ab1739ba34 | ||
|
|
fc1430aa92 | ||
|
|
3cde608eda | ||
|
|
2898552f4b | ||
|
|
911c148c71 | ||
|
|
91568a173a | ||
|
|
e57f5499a1 | ||
|
|
9abb7b71a9 | ||
|
|
724c354d62 | ||
|
|
c44779094d | ||
|
|
eeedab4091 | ||
|
|
8559e20237 | ||
|
|
26730eb083 | ||
|
|
4160ce674d | ||
|
|
2dbeea21c4 | ||
|
|
a2b8485a89 | ||
|
|
5bb74ee61c | ||
|
|
462fde5e7d | ||
|
|
f1e83a57cd | ||
|
|
cc9a9fb390 | ||
|
|
8fbcceb742 | ||
|
|
d3a251e6d9 | ||
|
|
be80b26c18 | ||
|
|
1574b7d834 | ||
|
|
51e10e344d | ||
|
|
0d55d8c5b0 | ||
|
|
1392589d36 | ||
|
|
548a324256 | ||
|
|
a8a0bc356a | ||
|
|
faee1d5a8d | ||
|
|
3088dac33b |
3
AUTHORS
3
AUTHORS
@@ -61,6 +61,7 @@ Karol Różycki (krozycki) <rozycki.karol@gmail.com>
|
||||
Kelong Cong (kc1212) <kc04bc@gmx.com> <kc1212@users.noreply.github.com>
|
||||
Ken'ichi Kamada (kamadak) <kamada@nanohz.org>
|
||||
Kevin Allen (ironmig) <kma1660@gmail.com>
|
||||
Kevin White, Jr. (kwhite17) <kevinwhite1710@gmail.com>
|
||||
Lars K.W. Gohlke (lkwg82) <lkwg82@gmx.de>
|
||||
Laurent Etiemble (letiemble) <laurent.etiemble@gmail.com> <laurent.etiemble@monobjc.net>
|
||||
Leo Arias (elopio) <yo@elopio.net>
|
||||
@@ -82,6 +83,7 @@ Peter Hoeg (peterhoeg) <peter@speartail.com>
|
||||
Philippe Schommers (filoozoom) <philippe@schommers.be>
|
||||
Phill Luby (pluby) <phill.luby@newredo.com>
|
||||
Piotr Bejda (piobpl) <piotrb10@gmail.com>
|
||||
Roman Zaynetdinov (zaynetro) <romanznet@gmail.com>
|
||||
Ryan Sullivan (KayoticSully) <kayoticsully@gmail.com>
|
||||
Scott Klupfel (kluppy) <kluppy@going2blue.com>
|
||||
Sergey Mishin (ralder) <ralder@yandex.ru>
|
||||
@@ -94,6 +96,7 @@ Tobias Nygren (tnn2) <tnn@nygren.pp.se>
|
||||
Tomas Cerveny (kozec) <kozec@kozec.com>
|
||||
Tully Robinson (tojrobinson) <tully@tojr.org>
|
||||
Tyler Brazier (tylerbrazier) <tyler@tylerbrazier.com>
|
||||
Unrud (Unrud) <unrud@openaliasbox.org> <Unrud@users.noreply.github.com>
|
||||
Veeti Paananen (veeti) <veeti.paananen@rojekti.fi>
|
||||
Victor Buinsky (buinsky) <vix_booja@tut.by>
|
||||
Vil Brekin (Vilbrekin) <vilbrekin@gmail.com>
|
||||
|
||||
4
NICKS
4
NICKS
@@ -63,6 +63,7 @@ kozec <kozec@kozec.com>
|
||||
kralo <max.schulze@online.de>
|
||||
kralo <kralo@users.noreply.github.com>
|
||||
krozycki <rozycki.karol@gmail.com>
|
||||
kwhite17 <kevinwhite1710@gmail.com>
|
||||
letiemble <laurent.etiemble@gmail.com>
|
||||
letiemble <laurent.etiemble@monobjc.net>
|
||||
lkwg82 <lkwg82@gmx.de>
|
||||
@@ -109,6 +110,8 @@ tnn2 <tnn@nygren.pp.se>
|
||||
tojrobinson <tully@tojr.org>
|
||||
tpng <benny.tpng@gmail.com>
|
||||
tylerbrazier <tyler@tylerbrazier.com>
|
||||
Unrud <unrud@openaliasbox.org>
|
||||
Unrud <Unrud@users.noreply.github.com>
|
||||
uok <ueomkail@gmail.com>
|
||||
uok <uok@users.noreply.github.com>
|
||||
veeti <veeti.paananen@rojekti.fi>
|
||||
@@ -118,5 +121,6 @@ WSGCSysadmin <e.meitner@willystreet.coop>
|
||||
wweich <wweich@users.noreply.github.com>
|
||||
wweich <wweich@gmx.de>
|
||||
xduugu <cedric@gmx.ca>
|
||||
zaynetro <romanznet@gmail.com>
|
||||
Zillode <zillode@zillode.be>
|
||||
zukoo <fxgsell@gmail.com>
|
||||
|
||||
26
build.go
26
build.go
@@ -298,6 +298,7 @@ func runCommand(cmd string, target target) {
|
||||
ok := gometalinter("deadcode", dirs, "test/util.go")
|
||||
ok = gometalinter("structcheck", dirs) && ok
|
||||
ok = gometalinter("varcheck", dirs) && ok
|
||||
ok = gometalinter("ineffassign", dirs) && ok
|
||||
if !ok {
|
||||
os.Exit(1)
|
||||
}
|
||||
@@ -356,14 +357,23 @@ func checkRequiredGoVersion() (float64, bool) {
|
||||
}
|
||||
|
||||
func setup() {
|
||||
runPrint("go", "get", "-v", "github.com/golang/lint/golint")
|
||||
runPrint("go", "get", "-v", "golang.org/x/tools/cmd/cover")
|
||||
runPrint("go", "get", "-v", "golang.org/x/net/html")
|
||||
runPrint("go", "get", "-v", "github.com/FiloSottile/gvt")
|
||||
runPrint("go", "get", "-v", "github.com/axw/gocov/gocov")
|
||||
runPrint("go", "get", "-v", "github.com/AlekSi/gocov-xml")
|
||||
runPrint("go", "get", "-v", "github.com/alecthomas/gometalinter")
|
||||
runPrint("go", "get", "-v", "github.com/mitchellh/go-wordwrap")
|
||||
packages := []string{
|
||||
"github.com/alecthomas/gometalinter",
|
||||
"github.com/AlekSi/gocov-xml",
|
||||
"github.com/axw/gocov/gocov",
|
||||
"github.com/FiloSottile/gvt",
|
||||
"github.com/golang/lint/golint",
|
||||
"github.com/gordonklaus/ineffassign",
|
||||
"github.com/mitchellh/go-wordwrap",
|
||||
"github.com/opennota/check/cmd/...",
|
||||
"github.com/tsenart/deadcode",
|
||||
"golang.org/x/net/html",
|
||||
"golang.org/x/tools/cmd/cover",
|
||||
}
|
||||
for _, pkg := range packages {
|
||||
fmt.Println(pkg)
|
||||
runPrint("go", "get", "-u", pkg)
|
||||
}
|
||||
}
|
||||
|
||||
func test(pkgs ...string) {
|
||||
|
||||
@@ -62,6 +62,10 @@ func (i requestID) String() string {
|
||||
return fmt.Sprintf("%016x", int64(i))
|
||||
}
|
||||
|
||||
type contextKey int
|
||||
|
||||
const idKey contextKey = iota
|
||||
|
||||
func negCacheFor(lastSeen time.Time) int {
|
||||
since := time.Since(lastSeen).Seconds()
|
||||
if since >= maxDeviceAge {
|
||||
@@ -132,7 +136,7 @@ var topCtx = context.Background()
|
||||
|
||||
func (s *querysrv) handler(w http.ResponseWriter, req *http.Request) {
|
||||
reqID := requestID(rand.Int63())
|
||||
ctx := context.WithValue(topCtx, "id", reqID)
|
||||
ctx := context.WithValue(topCtx, idKey, reqID)
|
||||
|
||||
if debug {
|
||||
log.Println(reqID, req.Method, req.URL)
|
||||
@@ -186,7 +190,7 @@ func (s *querysrv) handler(w http.ResponseWriter, req *http.Request) {
|
||||
}
|
||||
|
||||
func (s *querysrv) handleGET(ctx context.Context, w http.ResponseWriter, req *http.Request) {
|
||||
reqID := ctx.Value("id").(requestID)
|
||||
reqID := ctx.Value(idKey).(requestID)
|
||||
|
||||
deviceID, err := protocol.DeviceIDFromString(req.URL.Query().Get("device"))
|
||||
if err != nil {
|
||||
@@ -238,7 +242,7 @@ func (s *querysrv) handleGET(ctx context.Context, w http.ResponseWriter, req *ht
|
||||
}
|
||||
|
||||
func (s *querysrv) handlePOST(ctx context.Context, remoteIP net.IP, w http.ResponseWriter, req *http.Request) {
|
||||
reqID := ctx.Value("id").(requestID)
|
||||
reqID := ctx.Value(idKey).(requestID)
|
||||
|
||||
rawCert := certificateBytes(req)
|
||||
if rawCert == nil {
|
||||
@@ -299,7 +303,7 @@ func (s *querysrv) Stop() {
|
||||
}
|
||||
|
||||
func (s *querysrv) handleAnnounce(ctx context.Context, remote net.IP, deviceID protocol.DeviceID, addresses []string) (userErr, internalErr error) {
|
||||
reqID := ctx.Value("id").(requestID)
|
||||
reqID := ctx.Value(idKey).(requestID)
|
||||
|
||||
tx, err := s.db.Begin()
|
||||
if err != nil {
|
||||
@@ -383,7 +387,7 @@ func (s *querysrv) limit(remote net.IP) bool {
|
||||
}
|
||||
|
||||
func (s *querysrv) updateDevice(ctx context.Context, tx *sql.Tx, device protocol.DeviceID) error {
|
||||
reqID := ctx.Value("id").(requestID)
|
||||
reqID := ctx.Value(idKey).(requestID)
|
||||
t0 := time.Now()
|
||||
res, err := tx.Stmt(s.prep["updateDevice"]).Exec(device.String())
|
||||
if err != nil {
|
||||
|
||||
@@ -116,7 +116,7 @@ func saveCsrfTokens() {
|
||||
// nothing relevant we can do about them anyway...
|
||||
|
||||
name := locations[locCsrfTokens]
|
||||
f, err := osutil.CreateAtomic(name, 0600)
|
||||
f, err := osutil.CreateAtomic(name)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -507,6 +507,9 @@ func TestCSRFRequired(t *testing.T) {
|
||||
cfg := new(mockedConfig)
|
||||
cfg.gui.APIKey = testAPIKey
|
||||
baseURL, err := startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal("Unexpected error from getting base URL:", err)
|
||||
}
|
||||
|
||||
cli := &http.Client{
|
||||
Timeout: time.Second,
|
||||
|
||||
@@ -278,6 +278,11 @@ func parseCommandLineOptions() RuntimeOptions {
|
||||
flag.Usage = usageFor(flag.CommandLine, usage, longUsage)
|
||||
flag.Parse()
|
||||
|
||||
if len(flag.Args()) > 0 {
|
||||
flag.Usage()
|
||||
os.Exit(2)
|
||||
}
|
||||
|
||||
return options
|
||||
}
|
||||
|
||||
@@ -955,9 +960,8 @@ func defaultConfig(myName string) config.Configuration {
|
||||
|
||||
if !noDefaultFolder {
|
||||
l.Infoln("Default folder created and/or linked to new config")
|
||||
folderID := strings.ToLower(rand.String(5) + "-" + rand.String(5))
|
||||
defaultFolder = config.NewFolderConfiguration(folderID, locations[locDefFolder])
|
||||
defaultFolder.Label = "Default Folder (" + folderID + ")"
|
||||
defaultFolder = config.NewFolderConfiguration("default", locations[locDefFolder])
|
||||
defaultFolder.Label = "Default Folder"
|
||||
defaultFolder.RescanIntervalS = 60
|
||||
defaultFolder.MinDiskFreePct = 1
|
||||
defaultFolder.Devices = []config.FolderDeviceConfiguration{{DeviceID: myID}}
|
||||
|
||||
@@ -249,6 +249,10 @@ ul.three-columns li, ul.two-columns li {
|
||||
text-indent: -0.5em;
|
||||
}
|
||||
|
||||
.navbar-fixed-bottom {
|
||||
z-index: 980;
|
||||
}
|
||||
|
||||
/** Footer nav on small devices **/
|
||||
@media (max-width: 1199px) {
|
||||
/* Stay at the end of the page, with space reserved for the footer
|
||||
|
||||
@@ -86,7 +86,7 @@
|
||||
"Ignore Patterns": "Πρότυπο για αγνόηση",
|
||||
"Ignore Permissions": "Αγνόησε τα δικαιώματα",
|
||||
"Incoming Rate Limit (KiB/s)": "Περιορισμός ταχύτητας λήψης (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Με μια εσφαλμένη ρύθμιση μπορεί προκαληθεί ζημιά στα περιεχόμενα του φακέλου και το Syncthing μπορεί να σταματήσει να λειτουργεί.",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Με μια εσφαλμένη ρύθμιση μπορεί να προκληθεί ζημιά στα περιεχόμενα των φακέλων και το Syncthing ενδέχεται να σταματήσει να λειτουργεί.",
|
||||
"Introducer": "Βασικός κόμβος",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Αντιστροφή της δοσμένης συνθήκης (π.χ. να μην εξαιρείς) ",
|
||||
"Keep Versions": "Διατήρηση εκδόσεων",
|
||||
|
||||
@@ -9,7 +9,7 @@
|
||||
"Add Device": "Gaineratu makina",
|
||||
"Add Folder": "Gaineratu partekatze",
|
||||
"Add Remote Device": "Gaineratu makinan izan",
|
||||
"Add new folder?": "Gaineratu berri partekatze ?",
|
||||
"Add new folder?": "Gaineratu hau partekatze ?",
|
||||
"Address": "Helbide",
|
||||
"Addresses": "Helbidek",
|
||||
"Advanced": "Aditu",
|
||||
@@ -50,7 +50,7 @@
|
||||
"Documentation": "Dokumentazio",
|
||||
"Download Rate": "Deskargatze emari",
|
||||
"Downloaded": "Telekargatua",
|
||||
"Downloading": "Deskargatua",
|
||||
"Downloading": "Deskargatze",
|
||||
"Edit": "Aldatu",
|
||||
"Editing": "Aldaketa",
|
||||
"Enable NAT traversal": "Ahalbidetu NAT",
|
||||
@@ -235,6 +235,6 @@
|
||||
"files": "fitxategik",
|
||||
"full documentation": "Dokumentazio osoa",
|
||||
"items": "Elementuak",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} banaketa \"{{folderLabel}}\" gomitatzen zaitu.",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} \"{{folderLabel}}\" ({{folder}}) gomitatzen zaitu."
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} banaketa \"{{folder}}\" gomitatzen zaitu.",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} \"{{folderlabel}}\" ({{folder}}) gomitatzen zaitu."
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
"Add Device": "Ajouter l'appareil",
|
||||
"Add Folder": "Ajouter un partage",
|
||||
"Add Remote Device": "Ajouter un appareil",
|
||||
"Add new folder?": "Ajouter un nouveau partage ?",
|
||||
"Add new folder?": "Ajouter ce partage ?",
|
||||
"Address": "Adresse",
|
||||
"Addresses": "Adresses",
|
||||
"Advanced": "Avancé",
|
||||
@@ -50,7 +50,7 @@
|
||||
"Documentation": "Documentation",
|
||||
"Download Rate": "Débit de réception",
|
||||
"Downloaded": "Téléchargé",
|
||||
"Downloading": "En cours de téléchargement",
|
||||
"Downloading": "Téléchargement",
|
||||
"Edit": "Modifier",
|
||||
"Editing": "Modifications",
|
||||
"Enable NAT traversal": "Activer transfert d'adresses NAT",
|
||||
@@ -235,6 +235,6 @@
|
||||
"files": "fichiers",
|
||||
"full documentation": "documentation complète",
|
||||
"items": "éléments",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vous invite au partage \"{{folderLabel}}\".",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vous invite au partage \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -9,7 +9,7 @@
|
||||
"Add Device": "Ajouter l'appareil",
|
||||
"Add Folder": "Ajouter un partage",
|
||||
"Add Remote Device": "Ajouter un appareil",
|
||||
"Add new folder?": "Ajouter un nouveau partage ?",
|
||||
"Add new folder?": "Ajouter ce partage ?",
|
||||
"Address": "Adresse",
|
||||
"Addresses": "Adresses",
|
||||
"Advanced": "Avancé",
|
||||
@@ -50,7 +50,7 @@
|
||||
"Documentation": "Documentation",
|
||||
"Download Rate": "Vitesse de réception",
|
||||
"Downloaded": "Téléchargé",
|
||||
"Downloading": "En cours de téléchargement",
|
||||
"Downloading": "Téléchargement",
|
||||
"Edit": "Modifier",
|
||||
"Editing": "Modifications",
|
||||
"Enable NAT traversal": "Activer la translation d'adresses (NAT)",
|
||||
@@ -235,6 +235,6 @@
|
||||
"files": "Fichiers",
|
||||
"full documentation": "documentation complète",
|
||||
"items": "éléments",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vous invite au partage \"{{folderLabel}}\".",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderLabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} vous invite au partage \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} vous invite au partage \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -135,7 +135,7 @@
|
||||
"Quick guide to supported patterns": "Krótki przewodnik po obsługiwanych wzorcach",
|
||||
"RAM Utilization": "Użycie pamięci RAM",
|
||||
"Random": "Losowo",
|
||||
"Reduced by ignore patterns": "Reduced by ignore patterns",
|
||||
"Reduced by ignore patterns": "Ograniczono przez wzorce ignorowania",
|
||||
"Release Notes": "Informacje o wydaniu",
|
||||
"Remote Devices": "Urządzenia zdalne",
|
||||
"Remove": "Usuń",
|
||||
@@ -231,8 +231,8 @@
|
||||
"Yes": "Tak",
|
||||
"You must keep at least one version.": "Musisz posiadać przynajmniej jedną wersję",
|
||||
"days": "dni",
|
||||
"directories": "directories",
|
||||
"files": "files",
|
||||
"directories": "katalogi",
|
||||
"files": "pliki",
|
||||
"full documentation": "pełna dokumentacja",
|
||||
"items": "pozycji",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} chce udostępnić folder \"{{folder}}\"",
|
||||
|
||||
@@ -236,7 +236,10 @@
|
||||
</h3>
|
||||
</div>
|
||||
<div class="panel-body">
|
||||
<p ng-repeat="err in errorList()"><small>{{err.when | date:"yyyy-MM-dd HH:mm:ss"}}:</small> {{friendlyDevices(err.message)}}</p>
|
||||
<p ng-repeat="err in errorList()">
|
||||
<small>{{err.when | date:"yyyy-MM-dd HH:mm:ss"}}:</small>
|
||||
<span ng-bind-html="friendlyDevices(err.message) | linky: '_blank'"></span>
|
||||
</p>
|
||||
</div>
|
||||
<div class="panel-footer">
|
||||
<button type="button" class="btn btn-sm btn-default pull-right" ng-click="clearErrors()">
|
||||
@@ -658,6 +661,7 @@
|
||||
<!-- vendor scripts -->
|
||||
<script src="vendor/jquery/jquery-2.2.2.js"></script>
|
||||
<script src="vendor/angular/angular.js"></script>
|
||||
<script src="vendor/angular/angular-sanitize.js"></script>
|
||||
<script src="vendor/angular/angular-translate.js"></script>
|
||||
<script src="vendor/angular/angular-translate-loader-static-files.js"></script>
|
||||
<script src="vendor/angular/angular-dirPagination.js"></script>
|
||||
|
||||
@@ -10,7 +10,7 @@
|
||||
|
||||
var syncthing = angular.module('syncthing', [
|
||||
'angularUtils.directives.dirPagination',
|
||||
'pascalprecht.translate',
|
||||
'pascalprecht.translate', 'ngSanitize',
|
||||
|
||||
'syncthing.core'
|
||||
]);
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
<p translate>Copyright © 2014-2016 the following Contributors:</p>
|
||||
<div class="row">
|
||||
<div class="col-md-12" id="contributor-list">
|
||||
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Alexandre Viau, Andrew Dunham, Andrey D, Antoine Lamielle, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Laurent Etiemble, Leo Arias, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Scott Klupfel, Simon Frei, Stefan Kuntz, Tim Abell, Tim Howes, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Xavier O., Yannic A.
|
||||
Jakob Borg, Audrius Butkevicius, Alexander Graf, Anderson Mesquita, Antony Male, Ben Schulz, Caleb Callaway, Daniel Harte, Lars K.W. Gohlke, Lode Hoste, Michael Ploujnikov, Philippe Schommers, Ryan Sullivan, Sergey Mishin, Stefan Tatschner, Aaron Bieber, Adam Piggott, Alessandro G., Alexandre Viau, Andrew Dunham, Andrey D, Antoine Lamielle, Arthur Axel fREW Schmidt, Bart De Vries, Ben Curthoys, Ben Sidhom, Benny Ng, Brandon Philips, Brendan Long, Brian R. Becker, Carsten Hagemann, Cathryne Linenweaver, Cedric Staniewski, Chris Howie, Chris Joel, Colin Kennedy, Daniel Bergmann, Daniel Martí, David Rimmer, Denis A., Dennis Wilson, Dominik Heidler, Elias Jarlebring, Emil Hessman, Erik Meitner, Federico Castagnini, Felix Ableitner, Felix Unterpaintner, Francois-Xavier Gsell, Frank Isemann, Gilli Sigurdsson, Jaakko Hannikainen, Jacek Szafarkiewicz, Jake Peterson, James Patterson, Jaroslav Malec, Jens Diemer, Jochen Voss, Johan Vromans, Karol Różycki, Kelong Cong, Ken'ichi Kamada, Kevin Allen, Kevin White, Jr., Laurent Etiemble, Leo Arias, Lord Landon Agahnim, Majed Abdulaziz, Marc Laporte, Marc Pujol, Marcin Dziadus, Mateusz Naściszewski, Matt Burke, Max Schulze, Michael Jephcote, Michael Tilli, Nate Morrison, Pascal Jungblut, Peter Hoeg, Phill Luby, Piotr Bejda, Roman Zaynetdinov, Scott Klupfel, Simon Frei, Stefan Kuntz, Tim Abell, Tim Howes, Tobias Nygren, Tomas Cerveny, Tully Robinson, Tyler Brazier, Unrud, Veeti Paananen, Victor Buinsky, Vil Brekin, William A. Kennington III, Wulf Weich, Xavier O., Yannic A.
|
||||
</div>
|
||||
</div>
|
||||
<hr/>
|
||||
|
||||
@@ -1318,6 +1318,7 @@ angular.module('syncthing.core')
|
||||
rescanIntervalS: 60,
|
||||
minDiskFreePct: 1,
|
||||
maxConflicts: 10,
|
||||
fsync: true,
|
||||
order: "random",
|
||||
fileVersioningSelector: "none",
|
||||
trashcanClean: 0,
|
||||
@@ -1345,6 +1346,7 @@ angular.module('syncthing.core')
|
||||
rescanIntervalS: 60,
|
||||
minDiskFreePct: 1,
|
||||
maxConflicts: 10,
|
||||
fsync: true,
|
||||
order: "random",
|
||||
fileVersioningSelector: "none",
|
||||
trashcanClean: 0,
|
||||
|
||||
647
gui/default/vendor/angular/angular-sanitize.js
vendored
Normal file
647
gui/default/vendor/angular/angular-sanitize.js
vendored
Normal file
@@ -0,0 +1,647 @@
|
||||
/**
|
||||
* @license AngularJS v1.2.27
|
||||
* (c) 2010-2014 Google, Inc. http://angularjs.org
|
||||
* License: MIT
|
||||
*/
|
||||
(function(window, angular, undefined) {'use strict';
|
||||
|
||||
var $sanitizeMinErr = angular.$$minErr('$sanitize');
|
||||
|
||||
/**
|
||||
* @ngdoc module
|
||||
* @name ngSanitize
|
||||
* @description
|
||||
*
|
||||
* # ngSanitize
|
||||
*
|
||||
* The `ngSanitize` module provides functionality to sanitize HTML.
|
||||
*
|
||||
*
|
||||
* <div doc-module-components="ngSanitize"></div>
|
||||
*
|
||||
* See {@link ngSanitize.$sanitize `$sanitize`} for usage.
|
||||
*/
|
||||
|
||||
/*
|
||||
* HTML Parser By Misko Hevery (misko@hevery.com)
|
||||
* based on: HTML Parser By John Resig (ejohn.org)
|
||||
* Original code by Erik Arvidsson, Mozilla Public License
|
||||
* http://erik.eae.net/simplehtmlparser/simplehtmlparser.js
|
||||
*
|
||||
* // Use like so:
|
||||
* htmlParser(htmlString, {
|
||||
* start: function(tag, attrs, unary) {},
|
||||
* end: function(tag) {},
|
||||
* chars: function(text) {},
|
||||
* comment: function(text) {}
|
||||
* });
|
||||
*
|
||||
*/
|
||||
|
||||
|
||||
/**
|
||||
* @ngdoc service
|
||||
* @name $sanitize
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* The input is sanitized by parsing the html into tokens. All safe tokens (from a whitelist) are
|
||||
* then serialized back to properly escaped html string. This means that no unsafe input can make
|
||||
* it into the returned string, however, since our parser is more strict than a typical browser
|
||||
* parser, it's possible that some obscure input, which would be recognized as valid HTML by a
|
||||
* browser, won't make it through the sanitizer.
|
||||
* The whitelist is configured using the functions `aHrefSanitizationWhitelist` and
|
||||
* `imgSrcSanitizationWhitelist` of {@link ng.$compileProvider `$compileProvider`}.
|
||||
*
|
||||
* @param {string} html Html input.
|
||||
* @returns {string} Sanitized html.
|
||||
*
|
||||
* @example
|
||||
<example module="sanitizeExample" deps="angular-sanitize.js">
|
||||
<file name="index.html">
|
||||
<script>
|
||||
angular.module('sanitizeExample', ['ngSanitize'])
|
||||
.controller('ExampleController', ['$scope', '$sce', function($scope, $sce) {
|
||||
$scope.snippet =
|
||||
'<p style="color:blue">an html\n' +
|
||||
'<em onmouseover="this.textContent=\'PWN3D!\'">click here</em>\n' +
|
||||
'snippet</p>';
|
||||
$scope.deliberatelyTrustDangerousSnippet = function() {
|
||||
return $sce.trustAsHtml($scope.snippet);
|
||||
};
|
||||
}]);
|
||||
</script>
|
||||
<div ng-controller="ExampleController">
|
||||
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Directive</td>
|
||||
<td>How</td>
|
||||
<td>Source</td>
|
||||
<td>Rendered</td>
|
||||
</tr>
|
||||
<tr id="bind-html-with-sanitize">
|
||||
<td>ng-bind-html</td>
|
||||
<td>Automatically uses $sanitize</td>
|
||||
<td><pre><div ng-bind-html="snippet"><br/></div></pre></td>
|
||||
<td><div ng-bind-html="snippet"></div></td>
|
||||
</tr>
|
||||
<tr id="bind-html-with-trust">
|
||||
<td>ng-bind-html</td>
|
||||
<td>Bypass $sanitize by explicitly trusting the dangerous value</td>
|
||||
<td>
|
||||
<pre><div ng-bind-html="deliberatelyTrustDangerousSnippet()">
|
||||
</div></pre>
|
||||
</td>
|
||||
<td><div ng-bind-html="deliberatelyTrustDangerousSnippet()"></div></td>
|
||||
</tr>
|
||||
<tr id="bind-default">
|
||||
<td>ng-bind</td>
|
||||
<td>Automatically escapes</td>
|
||||
<td><pre><div ng-bind="snippet"><br/></div></pre></td>
|
||||
<td><div ng-bind="snippet"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</div>
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should sanitize the html snippet by default', function() {
|
||||
expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
|
||||
toBe('<p>an html\n<em>click here</em>\nsnippet</p>');
|
||||
});
|
||||
|
||||
it('should inline raw snippet if bound to a trusted value', function() {
|
||||
expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).
|
||||
toBe("<p style=\"color:blue\">an html\n" +
|
||||
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
|
||||
"snippet</p>");
|
||||
});
|
||||
|
||||
it('should escape snippet without any filter', function() {
|
||||
expect(element(by.css('#bind-default div')).getInnerHtml()).
|
||||
toBe("<p style=\"color:blue\">an html\n" +
|
||||
"<em onmouseover=\"this.textContent='PWN3D!'\">click here</em>\n" +
|
||||
"snippet</p>");
|
||||
});
|
||||
|
||||
it('should update', function() {
|
||||
element(by.model('snippet')).clear();
|
||||
element(by.model('snippet')).sendKeys('new <b onclick="alert(1)">text</b>');
|
||||
expect(element(by.css('#bind-html-with-sanitize div')).getInnerHtml()).
|
||||
toBe('new <b>text</b>');
|
||||
expect(element(by.css('#bind-html-with-trust div')).getInnerHtml()).toBe(
|
||||
'new <b onclick="alert(1)">text</b>');
|
||||
expect(element(by.css('#bind-default div')).getInnerHtml()).toBe(
|
||||
"new <b onclick=\"alert(1)\">text</b>");
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
function $SanitizeProvider() {
|
||||
this.$get = ['$$sanitizeUri', function($$sanitizeUri) {
|
||||
return function(html) {
|
||||
var buf = [];
|
||||
htmlParser(html, htmlSanitizeWriter(buf, function(uri, isImage) {
|
||||
return !/^unsafe/.test($$sanitizeUri(uri, isImage));
|
||||
}));
|
||||
return buf.join('');
|
||||
};
|
||||
}];
|
||||
}
|
||||
|
||||
function sanitizeText(chars) {
|
||||
var buf = [];
|
||||
var writer = htmlSanitizeWriter(buf, angular.noop);
|
||||
writer.chars(chars);
|
||||
return buf.join('');
|
||||
}
|
||||
|
||||
|
||||
// Regular Expressions for parsing tags and attributes
|
||||
var START_TAG_REGEXP =
|
||||
/^<((?:[a-zA-Z])[\w:-]*)((?:\s+[\w:-]+(?:\s*=\s*(?:(?:"[^"]*")|(?:'[^']*')|[^>\s]+))?)*)\s*(\/?)\s*(>?)/,
|
||||
END_TAG_REGEXP = /^<\/\s*([\w:-]+)[^>]*>/,
|
||||
ATTR_REGEXP = /([\w:-]+)(?:\s*=\s*(?:(?:"((?:[^"])*)")|(?:'((?:[^'])*)')|([^>\s]+)))?/g,
|
||||
BEGIN_TAG_REGEXP = /^</,
|
||||
BEGING_END_TAGE_REGEXP = /^<\//,
|
||||
COMMENT_REGEXP = /<!--(.*?)-->/g,
|
||||
DOCTYPE_REGEXP = /<!DOCTYPE([^>]*?)>/i,
|
||||
CDATA_REGEXP = /<!\[CDATA\[(.*?)]]>/g,
|
||||
SURROGATE_PAIR_REGEXP = /[\uD800-\uDBFF][\uDC00-\uDFFF]/g,
|
||||
// Match everything outside of normal chars and " (quote character)
|
||||
NON_ALPHANUMERIC_REGEXP = /([^\#-~| |!])/g;
|
||||
|
||||
|
||||
// Good source of info about elements and attributes
|
||||
// http://dev.w3.org/html5/spec/Overview.html#semantics
|
||||
// http://simon.html5.org/html-elements
|
||||
|
||||
// Safe Void Elements - HTML5
|
||||
// http://dev.w3.org/html5/spec/Overview.html#void-elements
|
||||
var voidElements = makeMap("area,br,col,hr,img,wbr");
|
||||
|
||||
// Elements that you can, intentionally, leave open (and which close themselves)
|
||||
// http://dev.w3.org/html5/spec/Overview.html#optional-tags
|
||||
var optionalEndTagBlockElements = makeMap("colgroup,dd,dt,li,p,tbody,td,tfoot,th,thead,tr"),
|
||||
optionalEndTagInlineElements = makeMap("rp,rt"),
|
||||
optionalEndTagElements = angular.extend({},
|
||||
optionalEndTagInlineElements,
|
||||
optionalEndTagBlockElements);
|
||||
|
||||
// Safe Block Elements - HTML5
|
||||
var blockElements = angular.extend({}, optionalEndTagBlockElements, makeMap("address,article," +
|
||||
"aside,blockquote,caption,center,del,dir,div,dl,figure,figcaption,footer,h1,h2,h3,h4,h5," +
|
||||
"h6,header,hgroup,hr,ins,map,menu,nav,ol,pre,script,section,table,ul"));
|
||||
|
||||
// Inline Elements - HTML5
|
||||
var inlineElements = angular.extend({}, optionalEndTagInlineElements, makeMap("a,abbr,acronym,b," +
|
||||
"bdi,bdo,big,br,cite,code,del,dfn,em,font,i,img,ins,kbd,label,map,mark,q,ruby,rp,rt,s," +
|
||||
"samp,small,span,strike,strong,sub,sup,time,tt,u,var"));
|
||||
|
||||
|
||||
// Special Elements (can contain anything)
|
||||
var specialElements = makeMap("script,style");
|
||||
|
||||
var validElements = angular.extend({},
|
||||
voidElements,
|
||||
blockElements,
|
||||
inlineElements,
|
||||
optionalEndTagElements);
|
||||
|
||||
//Attributes that have href and hence need to be sanitized
|
||||
var uriAttrs = makeMap("background,cite,href,longdesc,src,usemap");
|
||||
var validAttrs = angular.extend({}, uriAttrs, makeMap(
|
||||
'abbr,align,alt,axis,bgcolor,border,cellpadding,cellspacing,class,clear,'+
|
||||
'color,cols,colspan,compact,coords,dir,face,headers,height,hreflang,hspace,'+
|
||||
'ismap,lang,language,nohref,nowrap,rel,rev,rows,rowspan,rules,'+
|
||||
'scope,scrolling,shape,size,span,start,summary,target,title,type,'+
|
||||
'valign,value,vspace,width'));
|
||||
|
||||
function makeMap(str) {
|
||||
var obj = {}, items = str.split(','), i;
|
||||
for (i = 0; i < items.length; i++) obj[items[i]] = true;
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* @example
|
||||
* htmlParser(htmlString, {
|
||||
* start: function(tag, attrs, unary) {},
|
||||
* end: function(tag) {},
|
||||
* chars: function(text) {},
|
||||
* comment: function(text) {}
|
||||
* });
|
||||
*
|
||||
* @param {string} html string
|
||||
* @param {object} handler
|
||||
*/
|
||||
function htmlParser( html, handler ) {
|
||||
if (typeof html !== 'string') {
|
||||
if (html === null || typeof html === 'undefined') {
|
||||
html = '';
|
||||
} else {
|
||||
html = '' + html;
|
||||
}
|
||||
}
|
||||
var index, chars, match, stack = [], last = html, text;
|
||||
stack.last = function() { return stack[ stack.length - 1 ]; };
|
||||
|
||||
while ( html ) {
|
||||
text = '';
|
||||
chars = true;
|
||||
|
||||
// Make sure we're not in a script or style element
|
||||
if ( !stack.last() || !specialElements[ stack.last() ] ) {
|
||||
|
||||
// Comment
|
||||
if ( html.indexOf("<!--") === 0 ) {
|
||||
// comments containing -- are not allowed unless they terminate the comment
|
||||
index = html.indexOf("--", 4);
|
||||
|
||||
if ( index >= 0 && html.lastIndexOf("-->", index) === index) {
|
||||
if (handler.comment) handler.comment( html.substring( 4, index ) );
|
||||
html = html.substring( index + 3 );
|
||||
chars = false;
|
||||
}
|
||||
// DOCTYPE
|
||||
} else if ( DOCTYPE_REGEXP.test(html) ) {
|
||||
match = html.match( DOCTYPE_REGEXP );
|
||||
|
||||
if ( match ) {
|
||||
html = html.replace( match[0], '');
|
||||
chars = false;
|
||||
}
|
||||
// end tag
|
||||
} else if ( BEGING_END_TAGE_REGEXP.test(html) ) {
|
||||
match = html.match( END_TAG_REGEXP );
|
||||
|
||||
if ( match ) {
|
||||
html = html.substring( match[0].length );
|
||||
match[0].replace( END_TAG_REGEXP, parseEndTag );
|
||||
chars = false;
|
||||
}
|
||||
|
||||
// start tag
|
||||
} else if ( BEGIN_TAG_REGEXP.test(html) ) {
|
||||
match = html.match( START_TAG_REGEXP );
|
||||
|
||||
if ( match ) {
|
||||
// We only have a valid start-tag if there is a '>'.
|
||||
if ( match[4] ) {
|
||||
html = html.substring( match[0].length );
|
||||
match[0].replace( START_TAG_REGEXP, parseStartTag );
|
||||
}
|
||||
chars = false;
|
||||
} else {
|
||||
// no ending tag found --- this piece should be encoded as an entity.
|
||||
text += '<';
|
||||
html = html.substring(1);
|
||||
}
|
||||
}
|
||||
|
||||
if ( chars ) {
|
||||
index = html.indexOf("<");
|
||||
|
||||
text += index < 0 ? html : html.substring( 0, index );
|
||||
html = index < 0 ? "" : html.substring( index );
|
||||
|
||||
if (handler.chars) handler.chars( decodeEntities(text) );
|
||||
}
|
||||
|
||||
} else {
|
||||
html = html.replace(new RegExp("(.*)<\\s*\\/\\s*" + stack.last() + "[^>]*>", 'i'),
|
||||
function(all, text){
|
||||
text = text.replace(COMMENT_REGEXP, "$1").replace(CDATA_REGEXP, "$1");
|
||||
|
||||
if (handler.chars) handler.chars( decodeEntities(text) );
|
||||
|
||||
return "";
|
||||
});
|
||||
|
||||
parseEndTag( "", stack.last() );
|
||||
}
|
||||
|
||||
if ( html == last ) {
|
||||
throw $sanitizeMinErr('badparse', "The sanitizer was unable to parse the following block " +
|
||||
"of html: {0}", html);
|
||||
}
|
||||
last = html;
|
||||
}
|
||||
|
||||
// Clean up any remaining tags
|
||||
parseEndTag();
|
||||
|
||||
function parseStartTag( tag, tagName, rest, unary ) {
|
||||
tagName = angular.lowercase(tagName);
|
||||
if ( blockElements[ tagName ] ) {
|
||||
while ( stack.last() && inlineElements[ stack.last() ] ) {
|
||||
parseEndTag( "", stack.last() );
|
||||
}
|
||||
}
|
||||
|
||||
if ( optionalEndTagElements[ tagName ] && stack.last() == tagName ) {
|
||||
parseEndTag( "", tagName );
|
||||
}
|
||||
|
||||
unary = voidElements[ tagName ] || !!unary;
|
||||
|
||||
if ( !unary )
|
||||
stack.push( tagName );
|
||||
|
||||
var attrs = {};
|
||||
|
||||
rest.replace(ATTR_REGEXP,
|
||||
function(match, name, doubleQuotedValue, singleQuotedValue, unquotedValue) {
|
||||
var value = doubleQuotedValue
|
||||
|| singleQuotedValue
|
||||
|| unquotedValue
|
||||
|| '';
|
||||
|
||||
attrs[name] = decodeEntities(value);
|
||||
});
|
||||
if (handler.start) handler.start( tagName, attrs, unary );
|
||||
}
|
||||
|
||||
function parseEndTag( tag, tagName ) {
|
||||
var pos = 0, i;
|
||||
tagName = angular.lowercase(tagName);
|
||||
if ( tagName )
|
||||
// Find the closest opened tag of the same type
|
||||
for ( pos = stack.length - 1; pos >= 0; pos-- )
|
||||
if ( stack[ pos ] == tagName )
|
||||
break;
|
||||
|
||||
if ( pos >= 0 ) {
|
||||
// Close all the open elements, up the stack
|
||||
for ( i = stack.length - 1; i >= pos; i-- )
|
||||
if (handler.end) handler.end( stack[ i ] );
|
||||
|
||||
// Remove the open elements from the stack
|
||||
stack.length = pos;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var hiddenPre=document.createElement("pre");
|
||||
var spaceRe = /^(\s*)([\s\S]*?)(\s*)$/;
|
||||
/**
|
||||
* decodes all entities into regular string
|
||||
* @param value
|
||||
* @returns {string} A string with decoded entities.
|
||||
*/
|
||||
function decodeEntities(value) {
|
||||
if (!value) { return ''; }
|
||||
|
||||
// Note: IE8 does not preserve spaces at the start/end of innerHTML
|
||||
// so we must capture them and reattach them afterward
|
||||
var parts = spaceRe.exec(value);
|
||||
var spaceBefore = parts[1];
|
||||
var spaceAfter = parts[3];
|
||||
var content = parts[2];
|
||||
if (content) {
|
||||
hiddenPre.innerHTML=content.replace(/</g,"<");
|
||||
// innerText depends on styling as it doesn't display hidden elements.
|
||||
// Therefore, it's better to use textContent not to cause unnecessary
|
||||
// reflows. However, IE<9 don't support textContent so the innerText
|
||||
// fallback is necessary.
|
||||
content = 'textContent' in hiddenPre ?
|
||||
hiddenPre.textContent : hiddenPre.innerText;
|
||||
}
|
||||
return spaceBefore + content + spaceAfter;
|
||||
}
|
||||
|
||||
/**
|
||||
* Escapes all potentially dangerous characters, so that the
|
||||
* resulting string can be safely inserted into attribute or
|
||||
* element text.
|
||||
* @param value
|
||||
* @returns {string} escaped text
|
||||
*/
|
||||
function encodeEntities(value) {
|
||||
return value.
|
||||
replace(/&/g, '&').
|
||||
replace(SURROGATE_PAIR_REGEXP, function (value) {
|
||||
var hi = value.charCodeAt(0);
|
||||
var low = value.charCodeAt(1);
|
||||
return '&#' + (((hi - 0xD800) * 0x400) + (low - 0xDC00) + 0x10000) + ';';
|
||||
}).
|
||||
replace(NON_ALPHANUMERIC_REGEXP, function(value){
|
||||
return '&#' + value.charCodeAt(0) + ';';
|
||||
}).
|
||||
replace(/</g, '<').
|
||||
replace(/>/g, '>');
|
||||
}
|
||||
|
||||
/**
|
||||
* create an HTML/XML writer which writes to buffer
|
||||
* @param {Array} buf use buf.jain('') to get out sanitized html string
|
||||
* @returns {object} in the form of {
|
||||
* start: function(tag, attrs, unary) {},
|
||||
* end: function(tag) {},
|
||||
* chars: function(text) {},
|
||||
* comment: function(text) {}
|
||||
* }
|
||||
*/
|
||||
function htmlSanitizeWriter(buf, uriValidator){
|
||||
var ignore = false;
|
||||
var out = angular.bind(buf, buf.push);
|
||||
return {
|
||||
start: function(tag, attrs, unary){
|
||||
tag = angular.lowercase(tag);
|
||||
if (!ignore && specialElements[tag]) {
|
||||
ignore = tag;
|
||||
}
|
||||
if (!ignore && validElements[tag] === true) {
|
||||
out('<');
|
||||
out(tag);
|
||||
angular.forEach(attrs, function(value, key){
|
||||
var lkey=angular.lowercase(key);
|
||||
var isImage = (tag === 'img' && lkey === 'src') || (lkey === 'background');
|
||||
if (validAttrs[lkey] === true &&
|
||||
(uriAttrs[lkey] !== true || uriValidator(value, isImage))) {
|
||||
out(' ');
|
||||
out(key);
|
||||
out('="');
|
||||
out(encodeEntities(value));
|
||||
out('"');
|
||||
}
|
||||
});
|
||||
out(unary ? '/>' : '>');
|
||||
}
|
||||
},
|
||||
end: function(tag){
|
||||
tag = angular.lowercase(tag);
|
||||
if (!ignore && validElements[tag] === true) {
|
||||
out('</');
|
||||
out(tag);
|
||||
out('>');
|
||||
}
|
||||
if (tag == ignore) {
|
||||
ignore = false;
|
||||
}
|
||||
},
|
||||
chars: function(chars){
|
||||
if (!ignore) {
|
||||
out(encodeEntities(chars));
|
||||
}
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
// define ngSanitize module and register $sanitize service
|
||||
angular.module('ngSanitize', []).provider('$sanitize', $SanitizeProvider);
|
||||
|
||||
/* global sanitizeText: false */
|
||||
|
||||
/**
|
||||
* @ngdoc filter
|
||||
* @name linky
|
||||
* @kind function
|
||||
*
|
||||
* @description
|
||||
* Finds links in text input and turns them into html links. Supports http/https/ftp/mailto and
|
||||
* plain email address links.
|
||||
*
|
||||
* Requires the {@link ngSanitize `ngSanitize`} module to be installed.
|
||||
*
|
||||
* @param {string} text Input text.
|
||||
* @param {string} target Window (_blank|_self|_parent|_top) or named frame to open links in.
|
||||
* @returns {string} Html-linkified text.
|
||||
*
|
||||
* @usage
|
||||
<span ng-bind-html="linky_expression | linky"></span>
|
||||
*
|
||||
* @example
|
||||
<example module="linkyExample" deps="angular-sanitize.js">
|
||||
<file name="index.html">
|
||||
<script>
|
||||
angular.module('linkyExample', ['ngSanitize'])
|
||||
.controller('ExampleController', ['$scope', function($scope) {
|
||||
$scope.snippet =
|
||||
'Pretty text with some links:\n'+
|
||||
'http://angularjs.org/,\n'+
|
||||
'mailto:us@somewhere.org,\n'+
|
||||
'another@somewhere.org,\n'+
|
||||
'and one more: ftp://127.0.0.1/.';
|
||||
$scope.snippetWithTarget = 'http://angularjs.org/';
|
||||
}]);
|
||||
</script>
|
||||
<div ng-controller="ExampleController">
|
||||
Snippet: <textarea ng-model="snippet" cols="60" rows="3"></textarea>
|
||||
<table>
|
||||
<tr>
|
||||
<td>Filter</td>
|
||||
<td>Source</td>
|
||||
<td>Rendered</td>
|
||||
</tr>
|
||||
<tr id="linky-filter">
|
||||
<td>linky filter</td>
|
||||
<td>
|
||||
<pre><div ng-bind-html="snippet | linky"><br></div></pre>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-bind-html="snippet | linky"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="linky-target">
|
||||
<td>linky target</td>
|
||||
<td>
|
||||
<pre><div ng-bind-html="snippetWithTarget | linky:'_blank'"><br></div></pre>
|
||||
</td>
|
||||
<td>
|
||||
<div ng-bind-html="snippetWithTarget | linky:'_blank'"></div>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="escaped-html">
|
||||
<td>no filter</td>
|
||||
<td><pre><div ng-bind="snippet"><br></div></pre></td>
|
||||
<td><div ng-bind="snippet"></div></td>
|
||||
</tr>
|
||||
</table>
|
||||
</file>
|
||||
<file name="protractor.js" type="protractor">
|
||||
it('should linkify the snippet with urls', function() {
|
||||
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
|
||||
toBe('Pretty text with some links: http://angularjs.org/, us@somewhere.org, ' +
|
||||
'another@somewhere.org, and one more: ftp://127.0.0.1/.');
|
||||
expect(element.all(by.css('#linky-filter a')).count()).toEqual(4);
|
||||
});
|
||||
|
||||
it('should not linkify snippet without the linky filter', function() {
|
||||
expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText()).
|
||||
toBe('Pretty text with some links: http://angularjs.org/, mailto:us@somewhere.org, ' +
|
||||
'another@somewhere.org, and one more: ftp://127.0.0.1/.');
|
||||
expect(element.all(by.css('#escaped-html a')).count()).toEqual(0);
|
||||
});
|
||||
|
||||
it('should update', function() {
|
||||
element(by.model('snippet')).clear();
|
||||
element(by.model('snippet')).sendKeys('new http://link.');
|
||||
expect(element(by.id('linky-filter')).element(by.binding('snippet | linky')).getText()).
|
||||
toBe('new http://link.');
|
||||
expect(element.all(by.css('#linky-filter a')).count()).toEqual(1);
|
||||
expect(element(by.id('escaped-html')).element(by.binding('snippet')).getText())
|
||||
.toBe('new http://link.');
|
||||
});
|
||||
|
||||
it('should work with the target property', function() {
|
||||
expect(element(by.id('linky-target')).
|
||||
element(by.binding("snippetWithTarget | linky:'_blank'")).getText()).
|
||||
toBe('http://angularjs.org/');
|
||||
expect(element(by.css('#linky-target a')).getAttribute('target')).toEqual('_blank');
|
||||
});
|
||||
</file>
|
||||
</example>
|
||||
*/
|
||||
angular.module('ngSanitize').filter('linky', ['$sanitize', function($sanitize) {
|
||||
var LINKY_URL_REGEXP =
|
||||
/((ftp|https?):\/\/|(mailto:)?[A-Za-z0-9._%+-]+@)\S*[^\s.;,(){}<>"]/,
|
||||
MAILTO_REGEXP = /^mailto:/;
|
||||
|
||||
return function(text, target) {
|
||||
if (!text) return text;
|
||||
var match;
|
||||
var raw = text;
|
||||
var html = [];
|
||||
var url;
|
||||
var i;
|
||||
while ((match = raw.match(LINKY_URL_REGEXP))) {
|
||||
// We can not end in these as they are sometimes found at the end of the sentence
|
||||
url = match[0];
|
||||
// if we did not match ftp/http/mailto then assume mailto
|
||||
if (match[2] == match[3]) url = 'mailto:' + url;
|
||||
i = match.index;
|
||||
addText(raw.substr(0, i));
|
||||
addLink(url, match[0].replace(MAILTO_REGEXP, ''));
|
||||
raw = raw.substring(i + match[0].length);
|
||||
}
|
||||
addText(raw);
|
||||
return $sanitize(html.join(''));
|
||||
|
||||
function addText(text) {
|
||||
if (!text) {
|
||||
return;
|
||||
}
|
||||
html.push(sanitizeText(text));
|
||||
}
|
||||
|
||||
function addLink(url, text) {
|
||||
html.push('<a ');
|
||||
if (angular.isDefined(target)) {
|
||||
html.push('target="');
|
||||
html.push(target);
|
||||
html.push('" ');
|
||||
}
|
||||
html.push('href="');
|
||||
html.push(url);
|
||||
html.push('">');
|
||||
addText(text);
|
||||
html.push('</a>');
|
||||
}
|
||||
};
|
||||
}]);
|
||||
|
||||
|
||||
})(window, window.angular);
|
||||
@@ -26,7 +26,7 @@ import (
|
||||
|
||||
const (
|
||||
OldestHandledVersion = 10
|
||||
CurrentVersion = 16
|
||||
CurrentVersion = 17
|
||||
MaxRescanIntervalS = 365 * 24 * 60 * 60
|
||||
)
|
||||
|
||||
@@ -254,6 +254,9 @@ func (cfg *Configuration) clean() error {
|
||||
if cfg.Version == 15 {
|
||||
convertV15V16(cfg)
|
||||
}
|
||||
if cfg.Version == 16 {
|
||||
convertV16V17(cfg)
|
||||
}
|
||||
|
||||
// Build a list of available devices
|
||||
existingDevices := make(map[protocol.DeviceID]bool)
|
||||
@@ -327,6 +330,14 @@ func convertV15V16(cfg *Configuration) {
|
||||
cfg.Version = 16
|
||||
}
|
||||
|
||||
func convertV16V17(cfg *Configuration) {
|
||||
for i := range cfg.Folders {
|
||||
cfg.Folders[i].Fsync = true
|
||||
}
|
||||
|
||||
cfg.Version = 17
|
||||
}
|
||||
|
||||
func convertV13V14(cfg *Configuration) {
|
||||
// Not using the ignore cache is the new default. Disable it on existing
|
||||
// configurations.
|
||||
|
||||
@@ -104,6 +104,7 @@ func TestDeviceConfig(t *testing.T) {
|
||||
AutoNormalize: true,
|
||||
MinDiskFreePct: 1,
|
||||
MaxConflicts: -1,
|
||||
Fsync: true,
|
||||
Versioning: VersioningConfiguration{
|
||||
Params: map[string]string{},
|
||||
},
|
||||
|
||||
@@ -7,6 +7,7 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
@@ -38,6 +39,7 @@ type FolderConfiguration struct {
|
||||
MaxConflicts int `xml:"maxConflicts" json:"maxConflicts"`
|
||||
DisableSparseFiles bool `xml:"disableSparseFiles" json:"disableSparseFiles"`
|
||||
DisableTempIndexes bool `xml:"disableTempIndexes" json:"disableTempIndexes"`
|
||||
Fsync bool `xml:"fsync" json:"fsync"`
|
||||
|
||||
cachedPath string
|
||||
|
||||
@@ -85,6 +87,9 @@ func (f *FolderConfiguration) CreateMarker() error {
|
||||
return err
|
||||
}
|
||||
fd.Close()
|
||||
if err := osutil.SyncDir(filepath.Dir(marker)); err != nil {
|
||||
l.Infof("fsync %q failed: %v", filepath.Dir(marker), err)
|
||||
}
|
||||
osutil.HideFile(marker)
|
||||
}
|
||||
|
||||
@@ -99,6 +104,10 @@ func (f *FolderConfiguration) HasMarker() bool {
|
||||
return true
|
||||
}
|
||||
|
||||
func (f FolderConfiguration) Description() string {
|
||||
return fmt.Sprintf("%q (%s)", f.Label, f.ID)
|
||||
}
|
||||
|
||||
func (f *FolderConfiguration) DeviceIDs() []protocol.DeviceID {
|
||||
deviceIDs := make([]protocol.DeviceID, len(f.Devices))
|
||||
for i, n := range f.Devices {
|
||||
|
||||
15
lib/config/testdata/v17.xml
vendored
Normal file
15
lib/config/testdata/v17.xml
vendored
Normal file
@@ -0,0 +1,15 @@
|
||||
<configuration version="17">
|
||||
<folder id="test" path="testdata" type="readonly" ignorePerms="false" rescanIntervalS="600" autoNormalize="true">
|
||||
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR"></device>
|
||||
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2"></device>
|
||||
<minDiskFreePct>1</minDiskFreePct>
|
||||
<maxConflicts>-1</maxConflicts>
|
||||
<fsync>true</fsync>
|
||||
</folder>
|
||||
<device id="AIR6LPZ-7K4PTTV-UXQSMUU-CPQ5YWH-OEDFIIQ-JUG777G-2YQXXR5-YD6AWQR" name="node one" compression="metadata">
|
||||
<address>tcp://a</address>
|
||||
</device>
|
||||
<device id="P56IOI7-MZJNU2Y-IQGDREY-DM2MGTI-MGL3BXN-PQ6W5BM-TBBZ4TJ-XZWICQ2" name="node two" compression="metadata">
|
||||
<address>tcp://b</address>
|
||||
</device>
|
||||
</configuration>
|
||||
@@ -209,6 +209,27 @@ func (w *Wrapper) SetDevice(dev DeviceConfiguration) error {
|
||||
return w.replaceLocked(newCfg)
|
||||
}
|
||||
|
||||
// RemoveDevice removes the device from the configuration
|
||||
func (w *Wrapper) RemoveDevice(id protocol.DeviceID) error {
|
||||
w.mut.Lock()
|
||||
defer w.mut.Unlock()
|
||||
|
||||
newCfg := w.cfg.Copy()
|
||||
removed := false
|
||||
for i := range newCfg.Devices {
|
||||
if newCfg.Devices[i].DeviceID == id {
|
||||
newCfg.Devices = append(newCfg.Devices[:i], newCfg.Devices[i+1:]...)
|
||||
removed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !removed {
|
||||
return nil
|
||||
}
|
||||
|
||||
return w.replaceLocked(newCfg)
|
||||
}
|
||||
|
||||
// Folders returns a map of folders. Folder structures should not be changed,
|
||||
// other than for the purpose of updating via SetFolder().
|
||||
func (w *Wrapper) Folders() map[string]FolderConfiguration {
|
||||
@@ -304,7 +325,7 @@ func (w *Wrapper) Device(id protocol.DeviceID) (DeviceConfiguration, bool) {
|
||||
|
||||
// Save writes the configuration to disk, and generates a ConfigSaved event.
|
||||
func (w *Wrapper) Save() error {
|
||||
fd, err := osutil.CreateAtomic(w.path, 0600)
|
||||
fd, err := osutil.CreateAtomic(w.path)
|
||||
if err != nil {
|
||||
l.Debugln("CreateAtomic:", err)
|
||||
return err
|
||||
|
||||
@@ -59,9 +59,10 @@ type Service struct {
|
||||
natService *nat.Service
|
||||
natServiceToken *suture.ServiceToken
|
||||
|
||||
listenersMut sync.RWMutex
|
||||
listeners map[string]genericListener
|
||||
listenerTokens map[string]suture.ServiceToken
|
||||
listenersMut sync.RWMutex
|
||||
listeners map[string]genericListener
|
||||
listenerTokens map[string]suture.ServiceToken
|
||||
listenerSupervisor *suture.Supervisor
|
||||
|
||||
curConMut sync.Mutex
|
||||
currentConnection map[protocol.DeviceID]Connection
|
||||
@@ -71,7 +72,11 @@ func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *
|
||||
bepProtocolName string, tlsDefaultCommonName string, lans []*net.IPNet) *Service {
|
||||
|
||||
service := &Service{
|
||||
Supervisor: suture.NewSimple("connections.Service"),
|
||||
Supervisor: suture.New("connections.Service", suture.Spec{
|
||||
Log: func(line string) {
|
||||
l.Infoln(line)
|
||||
},
|
||||
}),
|
||||
cfg: cfg,
|
||||
myID: myID,
|
||||
model: mdl,
|
||||
@@ -87,6 +92,18 @@ func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *
|
||||
listeners: make(map[string]genericListener),
|
||||
listenerTokens: make(map[string]suture.ServiceToken),
|
||||
|
||||
// A listener can fail twice, rapidly. Any more than that and it
|
||||
// will be put on suspension for ten minutes. Restarts and changes
|
||||
// due to config are done by removing and adding services, so are
|
||||
// not subject to these limitations.
|
||||
listenerSupervisor: suture.New("c.S.listenerSupervisor", suture.Spec{
|
||||
Log: func(line string) {
|
||||
l.Infoln(line)
|
||||
},
|
||||
FailureThreshold: 2,
|
||||
FailureBackoff: 600 * time.Second,
|
||||
}),
|
||||
|
||||
curConMut: sync.NewMutex(),
|
||||
currentConnection: make(map[protocol.DeviceID]Connection),
|
||||
}
|
||||
@@ -111,6 +128,7 @@ func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *
|
||||
|
||||
service.Add(serviceFunc(service.connect))
|
||||
service.Add(serviceFunc(service.handle))
|
||||
service.Add(service.listenerSupervisor)
|
||||
|
||||
raw := cfg.RawCopy()
|
||||
// Actually starts the listeners and NAT service
|
||||
@@ -417,7 +435,7 @@ func (s *Service) createListener(factory listenerFactory, uri *url.URL) bool {
|
||||
listener := factory.New(uri, s.cfg, s.tlsCfg, s.conns, s.natService)
|
||||
listener.OnAddressesChanged(s.logListenAddressesChangedEvent)
|
||||
s.listeners[uri.String()] = listener
|
||||
s.listenerTokens[uri.String()] = s.Add(listener)
|
||||
s.listenerTokens[uri.String()] = s.listenerSupervisor.Add(listener)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -481,7 +499,7 @@ func (s *Service) CommitConfiguration(from, to config.Configuration) bool {
|
||||
for addr, listener := range s.listeners {
|
||||
if _, ok := seen[addr]; !ok || !listener.Factory().Enabled(to) {
|
||||
l.Debugln("Stopping listener", addr)
|
||||
s.Remove(s.listenerTokens[addr])
|
||||
s.listenerSupervisor.Remove(s.listenerTokens[addr])
|
||||
delete(s.listenerTokens, addr)
|
||||
delete(s.listeners, addr)
|
||||
}
|
||||
|
||||
@@ -128,7 +128,7 @@ func (t readWriteTransaction) updateGlobal(folder, device []byte, file protocol.
|
||||
Version: file.Version,
|
||||
}
|
||||
|
||||
insertedAt := -1
|
||||
var insertedAt int
|
||||
// Find a position in the list to insert this file. The file at the front
|
||||
// of the list is the newer, the "global".
|
||||
for i := range fl.Versions {
|
||||
|
||||
96
lib/fs/basicfs.go
Normal file
96
lib/fs/basicfs.go
Normal file
@@ -0,0 +1,96 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"time"
|
||||
)
|
||||
|
||||
// The BasicFilesystem implements all aspects by delegating to package os.
|
||||
type BasicFilesystem struct {
|
||||
}
|
||||
|
||||
func NewBasicFilesystem() *BasicFilesystem {
|
||||
return new(BasicFilesystem)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Chmod(name string, mode FileMode) error {
|
||||
return os.Chmod(name, os.FileMode(mode))
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Chtimes(name string, atime time.Time, mtime time.Time) error {
|
||||
return os.Chtimes(name, atime, mtime)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Mkdir(name string, perm FileMode) error {
|
||||
return os.Mkdir(name, os.FileMode(perm))
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Lstat(name string) (FileInfo, error) {
|
||||
fi, err := os.Lstat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fsFileInfo{fi}, err
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Remove(name string) error {
|
||||
return os.Remove(name)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Rename(oldpath, newpath string) error {
|
||||
return os.Rename(oldpath, newpath)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Stat(name string) (FileInfo, error) {
|
||||
fi, err := os.Stat(name)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return fsFileInfo{fi}, err
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) DirNames(name string) ([]string, error) {
|
||||
fd, err := os.OpenFile(name, os.O_RDONLY, 0777)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
names, err := fd.Readdirnames(-1)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return names, nil
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Open(name string) (File, error) {
|
||||
return os.Open(name)
|
||||
}
|
||||
|
||||
func (f *BasicFilesystem) Create(name string) (File, error) {
|
||||
return os.Create(name)
|
||||
}
|
||||
|
||||
// fsFileInfo implements the fs.FileInfo interface on top of an os.FileInfo.
|
||||
type fsFileInfo struct {
|
||||
os.FileInfo
|
||||
}
|
||||
|
||||
func (e fsFileInfo) Mode() FileMode {
|
||||
return FileMode(e.FileInfo.Mode())
|
||||
}
|
||||
|
||||
func (e fsFileInfo) IsRegular() bool {
|
||||
return e.FileInfo.Mode().IsRegular()
|
||||
}
|
||||
|
||||
func (e fsFileInfo) IsSymlink() bool {
|
||||
return e.FileInfo.Mode()&os.ModeSymlink == os.ModeSymlink
|
||||
}
|
||||
43
lib/fs/basicfs_symlink_unix.go
Normal file
43
lib/fs/basicfs_symlink_unix.go
Normal file
@@ -0,0 +1,43 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build !windows
|
||||
|
||||
package fs
|
||||
|
||||
import "os"
|
||||
|
||||
var symlinksSupported = true
|
||||
|
||||
func DisableSymlinks() {
|
||||
symlinksSupported = false
|
||||
}
|
||||
|
||||
func (BasicFilesystem) SymlinksSupported() bool {
|
||||
return symlinksSupported
|
||||
}
|
||||
|
||||
func (BasicFilesystem) CreateSymlink(name, target string, _ LinkTargetType) error {
|
||||
return os.Symlink(target, name)
|
||||
}
|
||||
|
||||
func (BasicFilesystem) ChangeSymlinkType(_ string, _ LinkTargetType) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (BasicFilesystem) ReadSymlink(path string) (string, LinkTargetType, error) {
|
||||
tt := LinkTargetUnknown
|
||||
if stat, err := os.Stat(path); err == nil {
|
||||
if stat.IsDir() {
|
||||
tt = LinkTargetDirectory
|
||||
} else {
|
||||
tt = LinkTargetFile
|
||||
}
|
||||
}
|
||||
|
||||
path, err := os.Readlink(path)
|
||||
return path, tt, err
|
||||
}
|
||||
195
lib/fs/basicfs_symlink_windows.go
Normal file
195
lib/fs/basicfs_symlink_windows.go
Normal file
@@ -0,0 +1,195 @@
|
||||
// Copyright (C) 2014 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
// +build windows
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
|
||||
"syscall"
|
||||
"unicode/utf16"
|
||||
"unsafe"
|
||||
)
|
||||
|
||||
const (
|
||||
win32FsctlGetReparsePoint = 0x900a8
|
||||
win32FileFlagOpenReparsePoint = 0x00200000
|
||||
win32SymbolicLinkFlagDirectory = 0x1
|
||||
)
|
||||
|
||||
var (
|
||||
modkernel32 = syscall.NewLazyDLL("kernel32.dll")
|
||||
procDeviceIoControl = modkernel32.NewProc("DeviceIoControl")
|
||||
procCreateSymbolicLink = modkernel32.NewProc("CreateSymbolicLinkW")
|
||||
symlinksSupported = false
|
||||
)
|
||||
|
||||
func init() {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
// Ensure that the supported flag is disabled when we hit an
|
||||
// error, even though it should already be. Also, silently swallow
|
||||
// the error since it's fine for a system not to support symlinks.
|
||||
symlinksSupported = false
|
||||
}
|
||||
}()
|
||||
|
||||
// Needs administrator privileges.
|
||||
// Let's check that everything works.
|
||||
// This could be done more officially:
|
||||
// http://stackoverflow.com/questions/2094663/determine-if-windows-process-has-privilege-to-create-symbolic-link
|
||||
// But I don't want to define 10 more structs just to look this up.
|
||||
base := os.TempDir()
|
||||
path := filepath.Join(base, "symlinktest")
|
||||
defer os.Remove(path)
|
||||
|
||||
err := DefaultFilesystem.CreateSymlink(path, base, LinkTargetDirectory)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
stat, err := osutil.Lstat(path)
|
||||
if err != nil || stat.Mode()&os.ModeSymlink == 0 {
|
||||
return
|
||||
}
|
||||
|
||||
target, tt, err := DefaultFilesystem.ReadSymlink(path)
|
||||
if err != nil || osutil.NativeFilename(target) != base || tt != LinkTargetDirectory {
|
||||
return
|
||||
}
|
||||
symlinksSupported = true
|
||||
}
|
||||
|
||||
func DisableSymlinks() {
|
||||
symlinksSupported = false
|
||||
}
|
||||
|
||||
func (BasicFilesystem) SymlinksSupported() bool {
|
||||
return symlinksSupported
|
||||
}
|
||||
|
||||
func (BasicFilesystem) ReadSymlink(path string) (string, LinkTargetType, error) {
|
||||
ptr, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return "", LinkTargetUnknown, err
|
||||
}
|
||||
handle, err := syscall.CreateFile(ptr, 0, syscall.FILE_SHARE_READ|syscall.FILE_SHARE_WRITE|syscall.FILE_SHARE_DELETE, nil, syscall.OPEN_EXISTING, syscall.FILE_FLAG_BACKUP_SEMANTICS|win32FileFlagOpenReparsePoint, 0)
|
||||
if err != nil || handle == syscall.InvalidHandle {
|
||||
return "", LinkTargetUnknown, err
|
||||
}
|
||||
defer syscall.Close(handle)
|
||||
var ret uint16
|
||||
var data reparseData
|
||||
|
||||
r1, _, err := syscall.Syscall9(procDeviceIoControl.Addr(), 8, uintptr(handle), win32FsctlGetReparsePoint, 0, 0, uintptr(unsafe.Pointer(&data)), unsafe.Sizeof(data), uintptr(unsafe.Pointer(&ret)), 0, 0)
|
||||
if r1 == 0 {
|
||||
return "", LinkTargetUnknown, err
|
||||
}
|
||||
|
||||
tt := LinkTargetUnknown
|
||||
if attr, err := syscall.GetFileAttributes(ptr); err == nil {
|
||||
if attr&syscall.FILE_ATTRIBUTE_DIRECTORY != 0 {
|
||||
tt = LinkTargetDirectory
|
||||
} else {
|
||||
tt = LinkTargetFile
|
||||
}
|
||||
}
|
||||
|
||||
return osutil.NormalizedFilename(data.printName()), tt, nil
|
||||
}
|
||||
|
||||
func (BasicFilesystem) CreateSymlink(path, target string, tt LinkTargetType) error {
|
||||
srcp, err := syscall.UTF16PtrFromString(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
trgp, err := syscall.UTF16PtrFromString(osutil.NativeFilename(target))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Sadly for Windows we need to specify the type of the symlink,
|
||||
// whether it's a directory symlink or a file symlink.
|
||||
// If the flags doesn't reveal the target type, try to evaluate it
|
||||
// ourselves, and worst case default to the symlink pointing to a file.
|
||||
mode := 0
|
||||
if tt == LinkTargetUnknown {
|
||||
path := target
|
||||
if !filepath.IsAbs(target) {
|
||||
path = filepath.Join(filepath.Dir(path), target)
|
||||
}
|
||||
|
||||
stat, err := os.Stat(path)
|
||||
if err == nil && stat.IsDir() {
|
||||
mode = win32SymbolicLinkFlagDirectory
|
||||
}
|
||||
} else if tt == LinkTargetDirectory {
|
||||
mode = win32SymbolicLinkFlagDirectory
|
||||
}
|
||||
|
||||
r0, _, err := syscall.Syscall(procCreateSymbolicLink.Addr(), 3, uintptr(unsafe.Pointer(srcp)), uintptr(unsafe.Pointer(trgp)), uintptr(mode))
|
||||
if r0 == 1 {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func (fs BasicFilesystem) ChangeSymlinkType(path string, tt LinkTargetType) error {
|
||||
target, existingTargetType, err := fs.ReadSymlink(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If it's the same type, nothing to do.
|
||||
if tt == existingTargetType {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the actual type is unknown, but the new type is file, nothing to do
|
||||
if existingTargetType == LinkTargetUnknown && tt != LinkTargetDirectory {
|
||||
return nil
|
||||
}
|
||||
return osutil.InWritableDir(func(path string) error {
|
||||
// It should be a symlink as well hence no need to change permissions on
|
||||
// the file.
|
||||
os.Remove(path)
|
||||
return fs.CreateSymlink(path, target, tt)
|
||||
}, path)
|
||||
}
|
||||
|
||||
type reparseData struct {
|
||||
reparseTag uint32
|
||||
reparseDataLength uint16
|
||||
reserved uint16
|
||||
substitueNameOffset uint16
|
||||
substitueNameLength uint16
|
||||
printNameOffset uint16
|
||||
printNameLength uint16
|
||||
flags uint32
|
||||
// substituteName - 264 widechars max = 528 bytes
|
||||
// printName - 260 widechars max = 520 bytes
|
||||
// = 1048 bytes total
|
||||
buffer [1048 / 2]uint16
|
||||
}
|
||||
|
||||
func (r *reparseData) printName() string {
|
||||
// offset and length are in bytes but we're indexing a []uint16
|
||||
offset := r.printNameOffset / 2
|
||||
length := r.printNameLength / 2
|
||||
return string(utf16.Decode(r.buffer[offset : offset+length]))
|
||||
}
|
||||
|
||||
func (r *reparseData) substituteName() string {
|
||||
// offset and length are in bytes but we're indexing a []uint16
|
||||
offset := r.substitueNameOffset / 2
|
||||
length := r.substitueNameLength / 2
|
||||
return string(utf16.Decode(r.buffer[offset : offset+length]))
|
||||
}
|
||||
81
lib/fs/basicfs_walk.go
Normal file
81
lib/fs/basicfs_walk.go
Normal file
@@ -0,0 +1,81 @@
|
||||
// Copyright 2009 The Go Authors. All rights reserved.
|
||||
// Use of this source code is governed by a BSD-style
|
||||
// license that can be found in the LICENSE file.
|
||||
|
||||
// This part copied directly from golang.org/src/path/filepath/path.go (Go
|
||||
// 1.6) and lightly modified to be methods on BasicFilesystem.
|
||||
|
||||
// In our Walk() all paths given to a WalkFunc() are relative to the
|
||||
// filesystem root.
|
||||
|
||||
package fs
|
||||
|
||||
import "path/filepath"
|
||||
|
||||
// WalkFunc is the type of the function called for each file or directory
|
||||
// visited by Walk. The path argument contains the argument to Walk as a
|
||||
// prefix; that is, if Walk is called with "dir", which is a directory
|
||||
// containing the file "a", the walk function will be called with argument
|
||||
// "dir/a". The info argument is the FileInfo for the named path.
|
||||
//
|
||||
// If there was a problem walking to the file or directory named by path, the
|
||||
// incoming error will describe the problem and the function can decide how
|
||||
// to handle that error (and Walk will not descend into that directory). If
|
||||
// an error is returned, processing stops. The sole exception is when the function
|
||||
// returns the special value SkipDir. If the function returns SkipDir when invoked
|
||||
// on a directory, Walk skips the directory's contents entirely.
|
||||
// If the function returns SkipDir when invoked on a non-directory file,
|
||||
// Walk skips the remaining files in the containing directory.
|
||||
type WalkFunc func(path string, info FileInfo, err error) error
|
||||
|
||||
// walk recursively descends path, calling walkFn.
|
||||
func (f *BasicFilesystem) walk(path string, info FileInfo, walkFn WalkFunc) error {
|
||||
err := walkFn(path, info, nil)
|
||||
if err != nil {
|
||||
if info.IsDir() && err == SkipDir {
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
if !info.IsDir() {
|
||||
return nil
|
||||
}
|
||||
|
||||
names, err := f.DirNames(path)
|
||||
if err != nil {
|
||||
return walkFn(path, info, err)
|
||||
}
|
||||
|
||||
for _, name := range names {
|
||||
filename := filepath.Join(path, name)
|
||||
fileInfo, err := f.Lstat(filename)
|
||||
if err != nil {
|
||||
if err := walkFn(filename, fileInfo, err); err != nil && err != SkipDir {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err = f.walk(filename, fileInfo, walkFn)
|
||||
if err != nil {
|
||||
if !fileInfo.IsDir() || err != SkipDir {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Walk walks the file tree rooted at root, calling walkFn for each file or
|
||||
// directory in the tree, including root. All errors that arise visiting files
|
||||
// and directories are filtered by walkFn. The files are walked in lexical
|
||||
// order, which makes the output deterministic but means that for very
|
||||
// large directories Walk can be inefficient.
|
||||
// Walk does not follow symbolic links.
|
||||
func (f *BasicFilesystem) Walk(root string, walkFn WalkFunc) error {
|
||||
info, err := f.Lstat(root)
|
||||
if err != nil {
|
||||
return walkFn(root, nil, err)
|
||||
}
|
||||
return f.walk(root, info, walkFn)
|
||||
}
|
||||
77
lib/fs/filesystem.go
Normal file
77
lib/fs/filesystem.go
Normal file
@@ -0,0 +1,77 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package fs
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"io"
|
||||
"time"
|
||||
)
|
||||
|
||||
type LinkTargetType int
|
||||
|
||||
const (
|
||||
LinkTargetFile LinkTargetType = iota
|
||||
LinkTargetDirectory
|
||||
LinkTargetUnknown
|
||||
)
|
||||
|
||||
// The Filesystem interface abstracts access to the file system.
|
||||
type Filesystem interface {
|
||||
ChangeSymlinkType(name string, tt LinkTargetType) error
|
||||
Chmod(name string, mode FileMode) error
|
||||
Chtimes(name string, atime time.Time, mtime time.Time) error
|
||||
Create(name string) (File, error)
|
||||
CreateSymlink(name, target string, tt LinkTargetType) error
|
||||
DirNames(name string) ([]string, error)
|
||||
Lstat(name string) (FileInfo, error)
|
||||
Mkdir(name string, perm FileMode) error
|
||||
Open(name string) (File, error)
|
||||
ReadSymlink(name string) (string, LinkTargetType, error)
|
||||
Remove(name string) error
|
||||
Rename(oldname, newname string) error
|
||||
Stat(name string) (FileInfo, error)
|
||||
SymlinksSupported() bool
|
||||
Walk(root string, walkFn WalkFunc) error
|
||||
}
|
||||
|
||||
// The File interface abstracts access to a regular file, being a somewhat
|
||||
// smaller interface than os.File
|
||||
type File interface {
|
||||
io.Reader
|
||||
io.WriterAt
|
||||
io.Closer
|
||||
Truncate(size int64) error
|
||||
}
|
||||
|
||||
// The FileInfo interface is almost the same as os.FileInfo, but with the
|
||||
// Sys method removed (as we don't want to expose whatever is underlying)
|
||||
// and with a couple of convenience methods added.
|
||||
type FileInfo interface {
|
||||
// Standard things present in os.FileInfo
|
||||
Name() string
|
||||
Mode() FileMode
|
||||
Size() int64
|
||||
ModTime() time.Time
|
||||
IsDir() bool
|
||||
// Extensions
|
||||
IsRegular() bool
|
||||
IsSymlink() bool
|
||||
}
|
||||
|
||||
// FileMode is similar to os.FileMode
|
||||
type FileMode uint32
|
||||
|
||||
// DefaultFilesystem is the fallback to use when nothing explicitly has
|
||||
// been passed.
|
||||
var DefaultFilesystem Filesystem = new(BasicFilesystem)
|
||||
|
||||
// SkipDir is used as a return value from WalkFuncs to indicate that
|
||||
// the directory named in the call is to be skipped. It is not returned
|
||||
// as an error by any function.
|
||||
var errSkipDir = errors.New("skip this directory")
|
||||
var SkipDir = errSkipDir // silences the lint warning...
|
||||
@@ -69,6 +69,7 @@ type Matcher struct {
|
||||
matches *cache
|
||||
curHash string
|
||||
stop chan struct{}
|
||||
modtimes map[string]time.Time
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
@@ -85,25 +86,41 @@ func New(withCache bool) *Matcher {
|
||||
}
|
||||
|
||||
func (m *Matcher) Load(file string) error {
|
||||
// No locking, Parse() does the locking
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
|
||||
if m.patternsUnchanged(file) {
|
||||
return nil
|
||||
}
|
||||
|
||||
fd, err := os.Open(file)
|
||||
if err != nil {
|
||||
// We do a parse with empty patterns to clear out the hash, cache etc.
|
||||
m.Parse(&bytes.Buffer{}, file)
|
||||
m.parseLocked(&bytes.Buffer{}, file)
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
return m.Parse(fd, file)
|
||||
info, err := fd.Stat()
|
||||
if err != nil {
|
||||
m.parseLocked(&bytes.Buffer{}, file)
|
||||
return err
|
||||
}
|
||||
|
||||
m.modtimes = map[string]time.Time{
|
||||
file: info.ModTime(),
|
||||
}
|
||||
|
||||
return m.parseLocked(fd, file)
|
||||
}
|
||||
|
||||
func (m *Matcher) Parse(r io.Reader, file string) error {
|
||||
m.mut.Lock()
|
||||
defer m.mut.Unlock()
|
||||
return m.parseLocked(r, file)
|
||||
}
|
||||
|
||||
seen := map[string]bool{file: true}
|
||||
patterns, err := parseIgnoreFile(r, file, seen)
|
||||
func (m *Matcher) parseLocked(r io.Reader, file string) error {
|
||||
patterns, err := parseIgnoreFile(r, file, m.modtimes)
|
||||
// Error is saved and returned at the end. We process the patterns
|
||||
// (possibly blank) anyway.
|
||||
|
||||
@@ -122,6 +139,26 @@ func (m *Matcher) Parse(r io.Reader, file string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// patternsUnchanged returns true if none of the files making up the loaded
|
||||
// patterns have changed since last check.
|
||||
func (m *Matcher) patternsUnchanged(file string) bool {
|
||||
if _, ok := m.modtimes[file]; !ok {
|
||||
return false
|
||||
}
|
||||
|
||||
for filename, modtime := range m.modtimes {
|
||||
info, err := os.Stat(filename)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !info.ModTime().Equal(modtime) {
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
func (m *Matcher) Match(file string) (result Result) {
|
||||
if m == nil {
|
||||
return resultNotMatched
|
||||
@@ -221,11 +258,10 @@ func hashPatterns(patterns []Pattern) string {
|
||||
return fmt.Sprintf("%x", h.Sum(nil))
|
||||
}
|
||||
|
||||
func loadIgnoreFile(file string, seen map[string]bool) ([]Pattern, error) {
|
||||
if seen[file] {
|
||||
func loadIgnoreFile(file string, modtimes map[string]time.Time) ([]Pattern, error) {
|
||||
if _, ok := modtimes[file]; ok {
|
||||
return nil, fmt.Errorf("Multiple include of ignore file %q", file)
|
||||
}
|
||||
seen[file] = true
|
||||
|
||||
fd, err := os.Open(file)
|
||||
if err != nil {
|
||||
@@ -233,10 +269,16 @@ func loadIgnoreFile(file string, seen map[string]bool) ([]Pattern, error) {
|
||||
}
|
||||
defer fd.Close()
|
||||
|
||||
return parseIgnoreFile(fd, file, seen)
|
||||
info, err := fd.Stat()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
modtimes[file] = info.ModTime()
|
||||
|
||||
return parseIgnoreFile(fd, file, modtimes)
|
||||
}
|
||||
|
||||
func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]Pattern, error) {
|
||||
func parseIgnoreFile(fd io.Reader, currentFile string, modtimes map[string]time.Time) ([]Pattern, error) {
|
||||
var patterns []Pattern
|
||||
|
||||
defaultResult := resultInclude
|
||||
@@ -302,7 +344,7 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
|
||||
} else if strings.HasPrefix(line, "#include ") {
|
||||
includeRel := line[len("#include "):]
|
||||
includeFile := filepath.Join(filepath.Dir(currentFile), includeRel)
|
||||
includes, err := loadIgnoreFile(includeFile, seen)
|
||||
includes, err := loadIgnoreFile(includeFile, modtimes)
|
||||
if err != nil {
|
||||
return fmt.Errorf("include of %q: %v", includeRel, err)
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestIgnore(t *testing.T) {
|
||||
@@ -276,9 +277,13 @@ func TestCaching(t *testing.T) {
|
||||
t.Fatal("Expected 4 cached results")
|
||||
}
|
||||
|
||||
// Modify the include file, expect empty cache
|
||||
// Modify the include file, expect empty cache. Ensure the timestamp on
|
||||
// the file changes.
|
||||
|
||||
fd2.WriteString("/z/\n")
|
||||
fd2.Sync()
|
||||
fakeTime := time.Now().Add(5 * time.Second)
|
||||
os.Chtimes(fd2.Name(), fakeTime, fakeTime)
|
||||
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
@@ -308,6 +313,9 @@ func TestCaching(t *testing.T) {
|
||||
// Modify the root file, expect cache to be invalidated
|
||||
|
||||
fd1.WriteString("/a/\n")
|
||||
fd1.Sync()
|
||||
fakeTime = time.Now().Add(5 * time.Second)
|
||||
os.Chtimes(fd1.Name(), fakeTime, fakeTime)
|
||||
|
||||
err = pats.Load(fd1.Name())
|
||||
if err != nil {
|
||||
@@ -484,6 +492,9 @@ func TestCacheReload(t *testing.T) {
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fd.Sync()
|
||||
fakeTime := time.Now().Add(5 * time.Second)
|
||||
os.Chtimes(fd.Name(), fakeTime, fakeTime)
|
||||
|
||||
err = pats.Load(fd.Name())
|
||||
if err != nil {
|
||||
|
||||
@@ -189,12 +189,12 @@ func (m *Model) StartFolder(folder string) {
|
||||
func (m *Model) startFolderLocked(folder string) config.FolderType {
|
||||
cfg, ok := m.folderCfgs[folder]
|
||||
if !ok {
|
||||
panic("cannot start nonexistent folder " + folder)
|
||||
panic("cannot start nonexistent folder " + cfg.Description())
|
||||
}
|
||||
|
||||
_, ok = m.folderRunners[folder]
|
||||
if ok {
|
||||
panic("cannot start already running folder " + folder)
|
||||
panic("cannot start already running folder " + cfg.Description())
|
||||
}
|
||||
|
||||
folderFactory, ok := folderFactories[cfg.Type]
|
||||
@@ -777,7 +777,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
"folderLabel": folder.Label,
|
||||
"device": deviceID.String(),
|
||||
})
|
||||
l.Infof("Unexpected folder ID %q sent from device %q; ensure that the folder exists and that this device is selected under \"Share With\" in the folder configuration.", folder.ID, deviceID)
|
||||
l.Infof("Unexpected folder %s sent from device %q; ensure that the folder exists and that this device is selected under \"Share With\" in the folder configuration.", folder.Description(), deviceID)
|
||||
continue
|
||||
}
|
||||
if !folder.DisableTempIndexes {
|
||||
@@ -806,19 +806,19 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
// the IndexID, or something else weird has
|
||||
// happened. We send a full index to reset the
|
||||
// situation.
|
||||
l.Infof("Device %v folder %q is delta index compatible, but seems out of sync with reality", deviceID, folder.ID)
|
||||
l.Infof("Device %v folder %s is delta index compatible, but seems out of sync with reality", deviceID, folder.Description())
|
||||
startSequence = 0
|
||||
continue
|
||||
}
|
||||
|
||||
l.Debugf("Device %v folder %q is delta index compatible (mlv=%d)", deviceID, folder.ID, dev.MaxSequence)
|
||||
l.Debugf("Device %v folder %s is delta index compatible (mlv=%d)", deviceID, folder.Description(), dev.MaxSequence)
|
||||
startSequence = dev.MaxSequence
|
||||
} else if dev.IndexID != 0 {
|
||||
// They say they've seen an index ID from us, but it's
|
||||
// not the right one. Either they are confused or we
|
||||
// must have reset our database since last talking to
|
||||
// them. We'll start with a full index transfer.
|
||||
l.Infof("Device %v folder %q has mismatching index ID for us (%v != %v)", deviceID, folder.ID, dev.IndexID, myIndexID)
|
||||
l.Infof("Device %v folder %s has mismatching index ID for us (%v != %v)", deviceID, folder.Description(), dev.IndexID, myIndexID)
|
||||
startSequence = 0
|
||||
}
|
||||
} else if dev.ID == deviceID && dev.IndexID != 0 {
|
||||
@@ -840,7 +840,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
// will probably send us a full index. We drop any
|
||||
// information we have and remember this new index ID
|
||||
// instead.
|
||||
l.Infof("Device %v folder %q has a new index ID (%v)", deviceID, folder.ID, dev.IndexID)
|
||||
l.Infof("Device %v folder %s has a new index ID (%v)", deviceID, folder.Description(), dev.IndexID)
|
||||
fs.Replace(deviceID, nil)
|
||||
fs.SetIndexID(deviceID, dev.IndexID)
|
||||
} else {
|
||||
@@ -857,7 +857,6 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
|
||||
go sendIndexes(conn, folder.ID, fs, m.folderIgnores[folder.ID], startSequence, dbLocation)
|
||||
}
|
||||
m.fmut.Unlock()
|
||||
|
||||
// This breaks if we send multiple CM messages during the same connection.
|
||||
if len(tempIndexFolders) > 0 {
|
||||
@@ -883,6 +882,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
m.fmut.Unlock()
|
||||
|
||||
if changed {
|
||||
if err := m.cfg.Save(); err != nil {
|
||||
@@ -949,9 +949,10 @@ func (m *Model) handleDeintroductions(introducerCfg config.DeviceConfiguration,
|
||||
for i := 0; i < len(folderCfg.Devices); i++ {
|
||||
if folderCfg.Devices[i].IntroducedBy == introducerCfg.DeviceID {
|
||||
if !foldersDevices.has(folderCfg.Devices[i].DeviceID, folderCfg.ID) {
|
||||
// We could not find that folder shared on the introducer with the device that was introduced to us.
|
||||
// We could not find that folder shared on the
|
||||
// introducer with the device that was introduced to us.
|
||||
// We should follow and unshare aswell.
|
||||
l.Infof("Unsharing folder %q with %v as introducer %v no longer shares the folder with that device", folderCfg.ID, folderCfg.Devices[i].DeviceID, folderCfg.Devices[i].IntroducedBy)
|
||||
l.Infof("Unsharing folder %s with %v as introducer %v no longer shares the folder with that device", folderCfg.Description(), folderCfg.Devices[i].DeviceID, folderCfg.Devices[i].IntroducedBy)
|
||||
folderCfg.Devices = append(folderCfg.Devices[:i], folderCfg.Devices[i+1:]...)
|
||||
i--
|
||||
folderChanged = true
|
||||
@@ -968,36 +969,26 @@ func (m *Model) handleDeintroductions(introducerCfg config.DeviceConfiguration,
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we should remove some devices, if the introducer no longer shares any folder with them.
|
||||
// Yet do not remove if we share other folders that haven't been introduced by the introducer.
|
||||
raw := m.cfg.RawCopy()
|
||||
deviceChanged := false
|
||||
for i := 0; i < len(raw.Devices); i++ {
|
||||
if raw.Devices[i].IntroducedBy == introducerCfg.DeviceID {
|
||||
if !foldersDevices.hasDevice(raw.Devices[i].DeviceID) {
|
||||
if foldersIntroducedByOthers.hasDevice(raw.Devices[i].DeviceID) {
|
||||
l.Infof("Would have removed %v as %v no longer shares any folders, yet there are other folders that are shared with this device that haven't been introduced by this introducer.", raw.Devices[i].DeviceID, raw.Devices[i].IntroducedBy)
|
||||
// Check if we should remove some devices, if the introducer no longer
|
||||
// shares any folder with them. Yet do not remove if we share other
|
||||
// folders that haven't been introduced by the introducer.
|
||||
for _, device := range m.cfg.Devices() {
|
||||
if device.IntroducedBy == introducerCfg.DeviceID {
|
||||
if !foldersDevices.hasDevice(device.DeviceID) {
|
||||
if foldersIntroducedByOthers.hasDevice(device.DeviceID) {
|
||||
l.Infof("Would have removed %v as %v no longer shares any folders, yet there are other folders that are shared with this device that haven't been introduced by this introducer.", device.DeviceID, device.IntroducedBy)
|
||||
continue
|
||||
}
|
||||
// The introducer no longer shares any folder with the device, remove the device.
|
||||
l.Infof("Removing device %v as introducer %v no longer shares any folders with that device", raw.Devices[i].DeviceID, raw.Devices[i].IntroducedBy)
|
||||
raw.Devices = append(raw.Devices[:i], raw.Devices[i+1:]...)
|
||||
i--
|
||||
deviceChanged = true
|
||||
// The introducer no longer shares any folder with the
|
||||
// device, remove the device.
|
||||
l.Infof("Removing device %v as introducer %v no longer shares any folders with that device", device.DeviceID, device.IntroducedBy)
|
||||
m.cfg.RemoveDevice(device.DeviceID)
|
||||
changed = true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// We've removed a device, replace the config.
|
||||
if deviceChanged {
|
||||
if err := m.cfg.Replace(raw); err != nil {
|
||||
l.Warnln("Failed to save config", err)
|
||||
}
|
||||
changed = true
|
||||
}
|
||||
|
||||
return changed
|
||||
|
||||
}
|
||||
|
||||
func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.DeviceConfiguration) {
|
||||
@@ -1029,7 +1020,7 @@ func (m *Model) introduceDevice(device protocol.Device, introducerCfg config.Dev
|
||||
}
|
||||
|
||||
func (m *Model) introduceDeviceToFolder(device protocol.Device, folder protocol.Folder, introducerCfg config.DeviceConfiguration) {
|
||||
l.Infof("Sharing folder %q with %v (vouched for by introducer %v)", folder.ID, device.ID, introducerCfg.DeviceID)
|
||||
l.Infof("Sharing folder %s with %v (vouched for by introducer %v)", folder.Description(), device.ID, introducerCfg.DeviceID)
|
||||
|
||||
m.deviceFolders[device.ID] = append(m.deviceFolders[device.ID], folder.ID)
|
||||
m.folderDevices.set(device.ID, folder.ID)
|
||||
@@ -1257,7 +1248,7 @@ func (m *Model) SetIgnores(folder string, content []string) error {
|
||||
|
||||
path := filepath.Join(cfg.Path(), ".stignore")
|
||||
|
||||
fd, err := osutil.CreateAtomic(path, 0644)
|
||||
fd, err := osutil.CreateAtomic(path)
|
||||
if err != nil {
|
||||
l.Warnln("Saving .stignore:", err)
|
||||
return err
|
||||
@@ -1744,14 +1735,14 @@ func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error
|
||||
|
||||
if err := m.CheckFolderHealth(folder); err != nil {
|
||||
runner.setError(err)
|
||||
l.Infof("Stopping folder %s due to error: %s", folder, err)
|
||||
l.Infof("Stopping folder %s due to error: %s", folderCfg.Description(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ignores.Load(filepath.Join(folderCfg.Path(), ".stignore")); err != nil && !os.IsNotExist(err) {
|
||||
err = fmt.Errorf("loading ignores: %v", err)
|
||||
runner.setError(err)
|
||||
l.Infof("Stopping folder %s due to error: %s", folder, err)
|
||||
l.Infof("Stopping folder %s due to error: %s", folderCfg.Description(), err)
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1808,7 +1799,7 @@ func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error
|
||||
for f := range fchan {
|
||||
if len(batch) == batchSizeFiles || blocksHandled > batchSizeBlocks {
|
||||
if err := m.CheckFolderHealth(folder); err != nil {
|
||||
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folder, err)
|
||||
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folderCfg.Description(), err)
|
||||
return err
|
||||
}
|
||||
m.updateLocalsFromScanning(folder, batch)
|
||||
@@ -1820,7 +1811,7 @@ func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error
|
||||
}
|
||||
|
||||
if err := m.CheckFolderHealth(folder); err != nil {
|
||||
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folder, err)
|
||||
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folderCfg.Description(), err)
|
||||
return err
|
||||
} else if len(batch) > 0 {
|
||||
m.updateLocalsFromScanning(folder, batch)
|
||||
@@ -1896,13 +1887,13 @@ func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error
|
||||
})
|
||||
|
||||
if iterError != nil {
|
||||
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folder, iterError)
|
||||
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folderCfg.Description(), iterError)
|
||||
return iterError
|
||||
}
|
||||
}
|
||||
|
||||
if err := m.CheckFolderHealth(folder); err != nil {
|
||||
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folder, err)
|
||||
l.Infof("Stopping folder %s mid-scan due to folder error: %s", folderCfg.Description(), err)
|
||||
return err
|
||||
} else if len(batch) > 0 {
|
||||
m.updateLocalsFromScanning(folder, batch)
|
||||
@@ -1958,7 +1949,12 @@ func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
|
||||
var message protocol.ClusterConfig
|
||||
|
||||
m.fmut.RLock()
|
||||
for _, folder := range m.deviceFolders[device] {
|
||||
// The list of folders in the message is sorted, so we always get the
|
||||
// same order.
|
||||
folders := m.deviceFolders[device]
|
||||
sort.Strings(folders)
|
||||
|
||||
for _, folder := range folders {
|
||||
folderCfg := m.cfg.Folders()[folder]
|
||||
fs := m.folderFiles[folder]
|
||||
|
||||
@@ -1971,12 +1967,8 @@ func (m *Model) generateClusterConfig(device protocol.DeviceID) protocol.Cluster
|
||||
DisableTempIndexes: folderCfg.DisableTempIndexes,
|
||||
}
|
||||
|
||||
for device := range m.folderDevices[folder] {
|
||||
// DeviceID is a value type, but with an underlying array. Copy it
|
||||
// so we don't grab aliases to the same array later on in device[:]
|
||||
device := device
|
||||
// TODO: Set read only bit when relevant, and when we have per device
|
||||
// access controls.
|
||||
// Devices are sorted, so we always get the same order.
|
||||
for _, device := range m.folderDevices.sortedDevices(folder) {
|
||||
deviceCfg := m.cfg.Devices()[device]
|
||||
|
||||
var indexID protocol.IndexID
|
||||
@@ -2299,9 +2291,9 @@ func (m *Model) runnerExchangeError(folder config.FolderConfiguration, err error
|
||||
|
||||
if err != nil {
|
||||
if oldErr != nil && oldErr.Error() != err.Error() {
|
||||
l.Infof("Folder %q error changed: %q -> %q", folder.ID, oldErr, err)
|
||||
l.Infof("Folder %s error changed: %q -> %q", folder.Description(), oldErr, err)
|
||||
} else if oldErr == nil {
|
||||
l.Warnf("Stopping folder %q - %v", folder.ID, err)
|
||||
l.Warnf("Stopping folder %s - %v", folder.Description(), err)
|
||||
}
|
||||
if runnerExists {
|
||||
runner.setError(err)
|
||||
@@ -2604,3 +2596,13 @@ func (s folderDeviceSet) hasDevice(dev protocol.DeviceID) bool {
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// sortedDevices returns the list of devices for a given folder, sorted
|
||||
func (s folderDeviceSet) sortedDevices(folder string) []protocol.DeviceID {
|
||||
devs := make([]protocol.DeviceID, 0, len(s[folder]))
|
||||
for dev := range s[folder] {
|
||||
devs = append(devs, dev)
|
||||
}
|
||||
sort.Sort(protocol.DeviceIDs(devs))
|
||||
return devs
|
||||
}
|
||||
|
||||
@@ -445,13 +445,13 @@ func TestClusterConfig(t *testing.T) {
|
||||
t.Errorf("Incorrect number of devices %d != 2", l)
|
||||
}
|
||||
if id := r.Devices[0].ID; id != device1 {
|
||||
t.Errorf("Incorrect device ID %x != %x", id, device1)
|
||||
t.Errorf("Incorrect device ID %s != %s", id, device1)
|
||||
}
|
||||
if !r.Devices[0].Introducer {
|
||||
t.Error("Device1 should be flagged as Introducer")
|
||||
}
|
||||
if id := r.Devices[1].ID; id != device2 {
|
||||
t.Errorf("Incorrect device ID %x != %x", id, device2)
|
||||
t.Errorf("Incorrect device ID %s != %s", id, device2)
|
||||
}
|
||||
if r.Devices[1].Introducer {
|
||||
t.Error("Device2 should not be flagged as Introducer")
|
||||
@@ -465,13 +465,13 @@ func TestClusterConfig(t *testing.T) {
|
||||
t.Errorf("Incorrect number of devices %d != 2", l)
|
||||
}
|
||||
if id := r.Devices[0].ID; id != device1 {
|
||||
t.Errorf("Incorrect device ID %x != %x", id, device1)
|
||||
t.Errorf("Incorrect device ID %s != %s", id, device1)
|
||||
}
|
||||
if !r.Devices[0].Introducer {
|
||||
t.Error("Device1 should be flagged as Introducer")
|
||||
}
|
||||
if id := r.Devices[1].ID; id != device2 {
|
||||
t.Errorf("Incorrect device ID %x != %x", id, device2)
|
||||
t.Errorf("Incorrect device ID %s != %s", id, device2)
|
||||
}
|
||||
if r.Devices[1].Introducer {
|
||||
t.Error("Device2 should not be flagged as Introducer")
|
||||
@@ -921,7 +921,7 @@ func TestIgnores(t *testing.T) {
|
||||
t.Errorf("Incorrect ignores: %v != %v", ignores, expected)
|
||||
}
|
||||
|
||||
ignores, _, err = m.GetIgnores("doesnotexist")
|
||||
_, _, err = m.GetIgnores("doesnotexist")
|
||||
if err == nil {
|
||||
t.Error("No error")
|
||||
}
|
||||
@@ -1773,6 +1773,8 @@ func TestScanNoDatabaseWrite(t *testing.T) {
|
||||
}
|
||||
defer m.SetIgnores("default", curIgn)
|
||||
m.SetIgnores("default", nil)
|
||||
fakeTime := time.Now().Add(5 * time.Second)
|
||||
os.Chtimes("testdata/.stignore", fakeTime, fakeTime)
|
||||
|
||||
// Scan the folder twice. The second scan should be a no-op database wise
|
||||
|
||||
@@ -1789,6 +1791,8 @@ func TestScanNoDatabaseWrite(t *testing.T) {
|
||||
// Ignore a file we know exists. It'll be updated in the database.
|
||||
|
||||
m.SetIgnores("default", []string{"foo"})
|
||||
fakeTime = time.Now().Add(10 * time.Second)
|
||||
os.Chtimes("testdata/.stignore", fakeTime, fakeTime)
|
||||
|
||||
m.ScanFolder("default")
|
||||
c2 := db.Committed()
|
||||
|
||||
@@ -22,11 +22,11 @@ type roFolder struct {
|
||||
folder
|
||||
}
|
||||
|
||||
func newROFolder(model *Model, config config.FolderConfiguration, _ versioner.Versioner, _ *fs.MtimeFS) service {
|
||||
func newROFolder(model *Model, cfg config.FolderConfiguration, _ versioner.Versioner, _ *fs.MtimeFS) service {
|
||||
return &roFolder{
|
||||
folder: folder{
|
||||
stateTracker: newStateTracker(config.ID),
|
||||
scan: newFolderScanner(config),
|
||||
stateTracker: newStateTracker(cfg.ID),
|
||||
scan: newFolderScanner(cfg),
|
||||
stop: make(chan struct{}),
|
||||
model: model,
|
||||
},
|
||||
|
||||
@@ -91,6 +91,7 @@ type rwFolder struct {
|
||||
allowSparse bool
|
||||
checkFreeSpace bool
|
||||
ignoreDelete bool
|
||||
fsync bool
|
||||
|
||||
copiers int
|
||||
pullers int
|
||||
@@ -126,6 +127,7 @@ func newRWFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Ver
|
||||
allowSparse: !cfg.DisableSparseFiles,
|
||||
checkFreeSpace: cfg.MinDiskFreePct != 0,
|
||||
ignoreDelete: cfg.IgnoreDelete,
|
||||
fsync: cfg.Fsync,
|
||||
|
||||
queue: newJobQueue(),
|
||||
pullTimer: time.NewTimer(time.Second),
|
||||
@@ -141,7 +143,7 @@ func newRWFolder(model *Model, cfg config.FolderConfiguration, ver versioner.Ver
|
||||
return f
|
||||
}
|
||||
|
||||
func (f *rwFolder) configureCopiersAndPullers(config config.FolderConfiguration) {
|
||||
func (f *rwFolder) configureCopiersAndPullers(cfg config.FolderConfiguration) {
|
||||
if f.copiers == 0 {
|
||||
f.copiers = defaultCopiers
|
||||
}
|
||||
@@ -149,16 +151,16 @@ func (f *rwFolder) configureCopiersAndPullers(config config.FolderConfiguration)
|
||||
f.pullers = defaultPullers
|
||||
}
|
||||
|
||||
if config.PullerPauseS == 0 {
|
||||
if cfg.PullerPauseS == 0 {
|
||||
f.pause = defaultPullerPause
|
||||
} else {
|
||||
f.pause = time.Duration(config.PullerPauseS) * time.Second
|
||||
f.pause = time.Duration(cfg.PullerPauseS) * time.Second
|
||||
}
|
||||
|
||||
if config.PullerSleepS == 0 {
|
||||
if cfg.PullerSleepS == 0 {
|
||||
f.sleep = defaultPullerSleep
|
||||
} else {
|
||||
f.sleep = time.Duration(config.PullerSleepS) * time.Second
|
||||
f.sleep = time.Duration(cfg.PullerSleepS) * time.Second
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1372,12 +1374,50 @@ func (f *rwFolder) dbUpdaterRoutine() {
|
||||
tick := time.NewTicker(maxBatchTime)
|
||||
defer tick.Stop()
|
||||
|
||||
var changedFiles []string
|
||||
var changedDirs []string
|
||||
if f.fsync {
|
||||
changedFiles = make([]string, 0, maxBatchSize)
|
||||
changedDirs = make([]string, 0, maxBatchSize)
|
||||
}
|
||||
|
||||
syncFilesOnce := func(files []string, syncFn func(string) error) {
|
||||
sort.Strings(files)
|
||||
var lastFile string
|
||||
for _, file := range files {
|
||||
if lastFile == file {
|
||||
continue
|
||||
}
|
||||
lastFile = file
|
||||
if err := syncFn(file); err != nil {
|
||||
l.Infof("fsync %q failed: %v", file, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
handleBatch := func() {
|
||||
found := false
|
||||
var lastFile protocol.FileInfo
|
||||
|
||||
for _, job := range batch {
|
||||
files = append(files, job.file)
|
||||
if f.fsync {
|
||||
// collect changed files and dirs
|
||||
switch job.jobType {
|
||||
case dbUpdateHandleFile, dbUpdateShortcutFile:
|
||||
// fsyncing symlinks is only supported by MacOS
|
||||
if !job.file.IsSymlink() {
|
||||
changedFiles = append(changedFiles,
|
||||
filepath.Join(f.dir, job.file.Name))
|
||||
}
|
||||
case dbUpdateHandleDir:
|
||||
changedDirs = append(changedDirs, filepath.Join(f.dir, job.file.Name))
|
||||
}
|
||||
if job.jobType != dbUpdateShortcutFile {
|
||||
changedDirs = append(changedDirs,
|
||||
filepath.Dir(filepath.Join(f.dir, job.file.Name)))
|
||||
}
|
||||
}
|
||||
if job.file.IsInvalid() || (job.file.IsDirectory() && !job.file.IsSymlink()) {
|
||||
continue
|
||||
}
|
||||
@@ -1390,6 +1430,14 @@ func (f *rwFolder) dbUpdaterRoutine() {
|
||||
lastFile = job.file
|
||||
}
|
||||
|
||||
if f.fsync {
|
||||
// sync files and dirs to disk
|
||||
syncFilesOnce(changedFiles, osutil.SyncFile)
|
||||
changedFiles = changedFiles[:0]
|
||||
syncFilesOnce(changedDirs, osutil.SyncDir)
|
||||
changedDirs = changedDirs[:0]
|
||||
}
|
||||
|
||||
// All updates to file/folder objects that originated remotely
|
||||
// (across the network) use this call to updateLocals
|
||||
f.model.updateLocalsFromPulling(f.folderID, files)
|
||||
|
||||
@@ -29,23 +29,19 @@ type AtomicWriter struct {
|
||||
err error
|
||||
}
|
||||
|
||||
// CreateAtomic is like os.Create with a FileMode, except a temporary file
|
||||
// name is used instead of the given name.
|
||||
func CreateAtomic(path string, mode os.FileMode) (*AtomicWriter, error) {
|
||||
// CreateAtomic is like os.Create, except a temporary file name is used
|
||||
// instead of the given name. The file is created with secure (0600)
|
||||
// permissions.
|
||||
func CreateAtomic(path string) (*AtomicWriter, error) {
|
||||
// The security of this depends on the tempfile having secure
|
||||
// permissions, 0600, from the beginning. This is what ioutil.TempFile
|
||||
// does. We have a test that verifies that that is the case, should this
|
||||
// ever change in the standard library in the future.
|
||||
fd, err := ioutil.TempFile(filepath.Dir(path), TempPrefix)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// chmod fails on Android so don't even try
|
||||
if runtime.GOOS != "android" {
|
||||
if err := os.Chmod(fd.Name(), mode); err != nil {
|
||||
fd.Close()
|
||||
os.Remove(fd.Name())
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
w := &AtomicWriter{
|
||||
path: path,
|
||||
next: fd,
|
||||
@@ -77,6 +73,11 @@ func (w *AtomicWriter) Close() error {
|
||||
// Try to not leave temp file around, but ignore error.
|
||||
defer os.Remove(w.next.Name())
|
||||
|
||||
if err := w.next.Sync(); err != nil {
|
||||
w.err = err
|
||||
return err
|
||||
}
|
||||
|
||||
if err := w.next.Close(); err != nil {
|
||||
w.err = err
|
||||
return err
|
||||
@@ -97,6 +98,8 @@ func (w *AtomicWriter) Close() error {
|
||||
return err
|
||||
}
|
||||
|
||||
SyncDir(filepath.Dir(w.next.Name()))
|
||||
|
||||
// Set w.err to return appropriately for any future operations.
|
||||
w.err = ErrClosed
|
||||
|
||||
|
||||
@@ -21,7 +21,7 @@ func TestCreateAtomicCreate(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w, err := CreateAtomic("testdata/file", 0644)
|
||||
w, err := CreateAtomic("testdata/file")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
@@ -63,7 +63,7 @@ func TestCreateAtomicReplace(t *testing.T) {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
w, err := CreateAtomic("testdata/file", 0644)
|
||||
w, err := CreateAtomic("testdata/file")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
44
lib/osutil/atomic_unix_test.go
Normal file
44
lib/osutil/atomic_unix_test.go
Normal file
@@ -0,0 +1,44 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
//+build !windows
|
||||
|
||||
// (No syscall.Umask or the equivalent on Windows)
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"syscall"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTempFilePermissions(t *testing.T) {
|
||||
// Set a zero umask, so any files created will have the permission bits
|
||||
// asked for in the create call and nothing less.
|
||||
oldMask := syscall.Umask(0)
|
||||
defer syscall.Umask(oldMask)
|
||||
|
||||
fd, err := ioutil.TempFile("", "test")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
info, err := fd.Stat()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(fd.Name())
|
||||
defer fd.Close()
|
||||
|
||||
// The temp file should have 0600 permissions at the most, or we have a
|
||||
// security problem in CreateAtomic.
|
||||
t.Logf("Got 0%03o", info.Mode())
|
||||
if info.Mode()&^0600 != 0 {
|
||||
t.Errorf("Permission 0%03o is too generous", info.Mode())
|
||||
}
|
||||
}
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
)
|
||||
|
||||
var ErrNoHome = errors.New("No home directory found - set $HOME (or the platform equivalent).")
|
||||
var errNoHome = errors.New("no home directory found - set $HOME (or the platform equivalent)")
|
||||
|
||||
// Try to keep this entire operation atomic-like. We shouldn't be doing this
|
||||
// often enough that there is any contention on this lock.
|
||||
@@ -123,7 +123,7 @@ func getHomeDir() (string, error) {
|
||||
}
|
||||
|
||||
if home == "" {
|
||||
return "", ErrNoHome
|
||||
return "", errNoHome
|
||||
}
|
||||
|
||||
return home, nil
|
||||
@@ -172,10 +172,7 @@ func copyFileContents(src, dst string) (err error) {
|
||||
err = cerr
|
||||
}
|
||||
}()
|
||||
if _, err = io.Copy(out, in); err != nil {
|
||||
return
|
||||
}
|
||||
err = out.Sync()
|
||||
_, err = io.Copy(out, in)
|
||||
return
|
||||
}
|
||||
|
||||
|
||||
37
lib/osutil/sync.go
Normal file
37
lib/osutil/sync.go
Normal file
@@ -0,0 +1,37 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package osutil
|
||||
|
||||
import (
|
||||
"os"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
func SyncFile(path string) error {
|
||||
flag := 0
|
||||
if runtime.GOOS == "windows" {
|
||||
flag = os.O_WRONLY
|
||||
}
|
||||
fd, err := os.OpenFile(path, flag, 0)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer fd.Close()
|
||||
// MacOS and Windows do not flush the disk cache
|
||||
if err := fd.Sync(); err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func SyncDir(path string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
// not supported by Windows
|
||||
return nil
|
||||
}
|
||||
return SyncFile(path)
|
||||
}
|
||||
@@ -134,3 +134,8 @@ func (i *IndexID) Unmarshal(bs []byte) error {
|
||||
func NewIndexID() IndexID {
|
||||
return IndexID(rand.Int64())
|
||||
}
|
||||
|
||||
func (f Folder) Description() string {
|
||||
// used by logging stuff
|
||||
return fmt.Sprintf("%q (%s)", f.Label, f.ID)
|
||||
}
|
||||
|
||||
@@ -203,3 +203,18 @@ func untypeoify(s string) string {
|
||||
s = strings.Replace(s, "8", "B", -1)
|
||||
return s
|
||||
}
|
||||
|
||||
// DeviceIDs is a sortable slice of DeviceID
|
||||
type DeviceIDs []DeviceID
|
||||
|
||||
func (l DeviceIDs) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l DeviceIDs) Less(a, b int) bool {
|
||||
return l[a].Compare(l[b]) == -1
|
||||
}
|
||||
|
||||
func (l DeviceIDs) Swap(a, b int) {
|
||||
l[a], l[b] = l[b], l[a]
|
||||
}
|
||||
|
||||
@@ -85,7 +85,6 @@ func (h holder) String() string {
|
||||
|
||||
type loggedMutex struct {
|
||||
sync.Mutex
|
||||
start time.Time
|
||||
holder atomic.Value
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "STDISCOSRV" "1" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "STDISCOSRV" "1" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
stdiscosrv \- Syncthing Discovery Server
|
||||
.
|
||||
@@ -46,7 +46,7 @@ stdiscosrv [\-cert=<file>] [\-db\-backend=<string>] [\-db\-dsn=<string>] [\-debu
|
||||
.SH DESCRIPTION
|
||||
.sp
|
||||
Syncthing relies on a discovery server to find peers on the internet. Anyone
|
||||
can run a discovery server and point its syncthing installations to it.
|
||||
can run a discovery server and point Syncthing installations to it.
|
||||
.SH OPTIONS
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
@@ -137,9 +137,9 @@ to select a different location.
|
||||
\fBNOTE:\fP
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
If you are running an instance of syncthing on the discovery server,
|
||||
you must either add that instance to other nodes using a static
|
||||
address or bind the discovery server and syncthing instances to
|
||||
If you are running an instance of Syncthing on the discovery server,
|
||||
you must either add that instance to other devices using a static
|
||||
address or bind the discovery server and Syncthing instances to
|
||||
different IP addresses.
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "STRELAYSRV" "1" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "STRELAYSRV" "1" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
strelaysrv \- Syncthing Relay Server
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-BEP" "7" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-BEP" "7" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-bep \- Block Exchange Protocol v1
|
||||
.
|
||||
@@ -290,14 +290,15 @@ message Folder {
|
||||
}
|
||||
|
||||
message Device {
|
||||
bytes id = 1;
|
||||
string name = 2;
|
||||
repeated string addresses = 3;
|
||||
Compression compression = 4;
|
||||
string cert_name = 5;
|
||||
int64 max_sequence = 6;
|
||||
bool introducer = 7;
|
||||
uint64 index_id = 8;
|
||||
bytes id = 1;
|
||||
string name = 2;
|
||||
repeated string addresses = 3;
|
||||
Compression compression = 4;
|
||||
string cert_name = 5;
|
||||
int64 max_sequence = 6;
|
||||
bool introducer = 7;
|
||||
uint64 index_id = 8;
|
||||
bool skip_introduction_removals = 9;
|
||||
}
|
||||
|
||||
enum Compression {
|
||||
@@ -373,6 +374,10 @@ introducers.
|
||||
.sp
|
||||
The \fBindex id\fP field contains the unique identifier for the current set of
|
||||
index data. See \fI\%Delta Index Exchange\fP for the usage of this field.
|
||||
.sp
|
||||
The \fBskip introduction removals\fP field signifies if the remote device has
|
||||
opted to ignore introduction removals for the given device. This setting is
|
||||
copied across as we are being introduced to a new device.
|
||||
.SS Index and Index Update
|
||||
.sp
|
||||
The Index and Index Update messages define the contents of the senders
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-CONFIG" "5" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-CONFIG" "5" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-config \- Syncthing Configuration
|
||||
.
|
||||
@@ -97,6 +97,7 @@ The following shows an example of the default configuration file (IDs will diffe
|
||||
<maxConflicts>\-1</maxConflicts>
|
||||
<disableSparseFiles>false</disableSparseFiles>
|
||||
<disableTempIndexes>false</disableTempIndexes>
|
||||
<fsync>false</fsync>
|
||||
</folder>
|
||||
<device id="3LT2GA5\-CQI4XJM\-WTZ264P\-MLOGMHL\-MCRLDNT\-MZV4RD3\-KA745CL\-OGAERQZ" name="syno" compression="metadata" introducer="false">
|
||||
<address>dynamic</address>
|
||||
@@ -161,7 +162,7 @@ migration from previous formats.
|
||||
.nf
|
||||
.ft C
|
||||
<folder id="zj2AA\-q55a7" label="Default Folder (zj2AA\-q55a7)" path="/Users/jb/Sync/" type="readwrite" rescanIntervalS="60" ignorePerms="false" autoNormalize="true" ro="false">
|
||||
<device id="3LT2GA5\-CQI4XJM\-WTZ264P\-MLOGMHL\-MCRLDNT\-MZV4RD3\-KA745CL\-OGAERQZ"></device>
|
||||
<device id="3LT2GA5\-CQI4XJM\-WTZ264P\-MLOGMHL\-MCRLDNT\-MZV4RD3\-KA745CL\-OGAERQZ" introducedBy="2CYF2WQ\-AKZO2QZ\-JAKWLYD\-AGHMQUM\-BGXUOIS\-GYILW34\-HJG3DUK\-LRRYQAR"></device>
|
||||
<minDiskFreePct>1</minDiskFreePct>
|
||||
<versioning></versioning>
|
||||
<copiers>0</copiers>
|
||||
@@ -175,6 +176,7 @@ migration from previous formats.
|
||||
<maxConflicts>\-1</maxConflicts>
|
||||
<disableSparseFiles>false</disableSparseFiles>
|
||||
<disableTempIndexes>false</disableTempIndexes>
|
||||
<fsync>false</fsync>
|
||||
</folder>
|
||||
.ft P
|
||||
.fi
|
||||
@@ -225,10 +227,13 @@ The following child elements may exist:
|
||||
.INDENT 0.0
|
||||
.TP
|
||||
.B device
|
||||
These must have the \fBid\fP attribute and nothing else. Mentioned devices
|
||||
are those that will be sharing the folder in question. Each mentioned
|
||||
device must have a separate \fBdevice\fP element later in the file. It is
|
||||
customary that the local device ID is included in all repositories.
|
||||
These must have the \fBid\fP attribute and can have an \fBintroducedBy\fP attribute,
|
||||
identifying the device that introduced us to share this folder with the given device.
|
||||
If the original introducer unshares this folder with this device, our device will follow
|
||||
and unshare the folder (subject to skipIntroductionRemovals being false on the introducer device).
|
||||
All mentioned devices are those that will be sharing the folder in question.
|
||||
Each mentioned device must have a separate \fBdevice\fP element later in the file.
|
||||
It is customary that the local device ID is included in all folders.
|
||||
Syncthing will currently add this automatically if it is not present in
|
||||
the configuration file.
|
||||
.TP
|
||||
@@ -301,6 +306,12 @@ sparse files will not be created.
|
||||
By default, devices exchange information about blocks available in
|
||||
transfers that are still in progress. When set to true, such information
|
||||
is not exchanged for this folder.
|
||||
.INDENT 7.0
|
||||
.TP
|
||||
.B fsync
|
||||
Transfer updated (from other devices) files to permanent storage before
|
||||
committing the changes to the internal database.
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.SH DEVICE ELEMENT
|
||||
.INDENT 0.0
|
||||
@@ -308,7 +319,7 @@ is not exchanged for this folder.
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
<device id="5SYI2FS\-LW6YAXI\-JJDYETS\-NDBBPIO\-256MWBO\-XDPXWVG\-24QPUM4\-PDW4UQU" name="syno" compression="metadata" introducer="false">
|
||||
<device id="5SYI2FS\-LW6YAXI\-JJDYETS\-NDBBPIO\-256MWBO\-XDPXWVG\-24QPUM4\-PDW4UQU" name="syno" compression="metadata" introducer="false" introducedBy="2CYF2WQ\-AKZO2QZ\-JAKWLYD\-AGHMQUM\-BGXUOIS\-GYILW34\-HJG3DUK\-LRRYQAR">
|
||||
<address>dynamic</address>
|
||||
</device>
|
||||
<device id="2CYF2WQ\-AKZO2QZ\-JAKWLYD\-AGHMQUM\-BGXUOIS\-GYILW34\-HJG3DUK\-LRRYQAR" name="syno local" compression="metadata" introducer="false">
|
||||
@@ -354,6 +365,14 @@ Disable all compression.
|
||||
.B introducer
|
||||
Set to true if this device should be trusted as an introducer, i.e. we
|
||||
should copy their list of devices per folder when connecting.
|
||||
.TP
|
||||
.B skipIntroductionRemovals
|
||||
Set to true if you wish to follow only introductions and not de\-introductions.
|
||||
For example, if this is set, we would not remove a device that we were introduced
|
||||
to even if the original introducer is no longer listing the remote device as known.
|
||||
.TP
|
||||
.B introducedBy
|
||||
Defines which device has introduced us to this device. Used only for following de\-introductions.
|
||||
.UNINDENT
|
||||
.sp
|
||||
In addition, one or more \fBaddress\fP child elements must be present. Each
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-device-ids \- Understanding Device IDs
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-EVENT-API" "7" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-EVENT-API" "7" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-event-api \- Event API
|
||||
.
|
||||
@@ -572,7 +572,7 @@ New in version 0.11.10: The \fBmetadata\fP action.
|
||||
.sp
|
||||
Generated upon scan whenever the local disk has discovered an updated file from the
|
||||
previous scan. This does NOT include events that are discovered and copied from
|
||||
other nodes, only files that were changed on the local filesystem.
|
||||
other devices, only files that were changed on the local filesystem.
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-FAQ" "7" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-FAQ" "7" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-faq \- Frequently Asked Questions
|
||||
.
|
||||
@@ -45,12 +45,12 @@ It\(aqs \fBSyncthing\fP, although the command and source repository is spelled
|
||||
\fBsyncthing\fP so it may be referred to in that way as well. It\(aqs definitely not
|
||||
SyncThing, even though the abbreviation \fBst\fP is used in some
|
||||
circumstances and file names.
|
||||
.SS How does Syncthing differ from BitTorrent Sync?
|
||||
.SS How does Syncthing differ from BitTorrent/Resilio Sync?
|
||||
.sp
|
||||
The two are different and not related. Syncthing and BitTorrent Sync accomplish
|
||||
The two are different and not related. Syncthing and BitTorrent/Resilio Sync accomplish
|
||||
some of the same things, namely syncing files between two or more computers.
|
||||
.sp
|
||||
BitTorrent Sync by BitTorrent, Inc is a proprietary peer\-to\-peer file
|
||||
BitTorrent Sync, now called Resilio Sync, is a proprietary peer\-to\-peer file
|
||||
synchronization tool available for Windows, Mac, Linux, Android, iOS, Windows
|
||||
Phone, Amazon Kindle Fire and BSD. [1] Syncthing is an open source file
|
||||
synchronization tool.
|
||||
@@ -103,8 +103,8 @@ Sparse file sparseness (will become sparse, when supported by the OS & filesyste
|
||||
.sp
|
||||
Syncthing segments files into pieces, called blocks, to transfer data from one
|
||||
device to another. Therefore, multiple devices can share the synchronization
|
||||
load, in a similar way as the torrent protocol. The more devices you have online
|
||||
(and synchronized), the faster an additional device will receive the data
|
||||
load, in a similar way to the torrent protocol. The more devices you have online,
|
||||
the faster an additional device will receive the data
|
||||
because small blocks will be fetched from all devices in parallel.
|
||||
.sp
|
||||
Syncthing handles renaming files and updating their metadata in an efficient
|
||||
@@ -176,11 +176,11 @@ Android. For other setups, consider using \fI\%syncthing\-inotify\fP <\fBhttps:/
|
||||
.SS Should I keep my device IDs secret?
|
||||
.sp
|
||||
No. The IDs are not sensitive. Given a device ID it\(aqs possible to find the IP
|
||||
address for that node, if global discovery is enabled on it. Knowing the device
|
||||
ID doesn\(aqt help you actually establish a connection to that node or get a list
|
||||
address for that device, if global discovery is enabled on it. Knowing the device
|
||||
ID doesn\(aqt help you actually establish a connection to that device or get a list
|
||||
of files, etc.
|
||||
.sp
|
||||
For a connection to be established, both nodes need to know about the other\(aqs
|
||||
For a connection to be established, both devices need to know about the other\(aqs
|
||||
device ID. It\(aqs not possible (in practice) to forge a device ID. (To forge a
|
||||
device ID you need to create a TLS certificate with that specific SHA\-256 hash.
|
||||
If you can do that, you can spoof any TLS certificate. The world is your
|
||||
@@ -226,12 +226,12 @@ the new path.
|
||||
.sp
|
||||
It\(aqs best to do this when the folder is already in sync between your
|
||||
devices, as it is otherwise unpredictable which changes will "win" after the
|
||||
move. Changes made on other devices may be overwritten, or changed made
|
||||
move. Changes made on other devices may be overwritten, or changes made
|
||||
locally may be overwritten by those on other devices.
|
||||
.sp
|
||||
An alternative way is to shut down Syncthing, move the folder on disk, edit
|
||||
the path directly in the configuration file and then start Syncthing again.
|
||||
.SS How to configure multiple users on a single machine?
|
||||
.SS How do I configure multiple users on a single machine?
|
||||
.sp
|
||||
Each user should run their own Syncthing instance. Be aware that you might need
|
||||
to configure listening ports such that they do not overlap (see config).
|
||||
@@ -243,7 +243,7 @@ programs to achieve this such as rsync or Unison.
|
||||
.SS Is Syncthing my ideal backup application?
|
||||
.sp
|
||||
No. Syncthing is not a great backup application because all changes to your
|
||||
files (modifications, deletions, etc) will be propagated to all your
|
||||
files (modifications, deletions, etc.) will be propagated to all your
|
||||
devices. You can enable versioning, but we encourage the use of other tools
|
||||
to keep your data safe from your (or our) mistakes.
|
||||
.SS Why is there no iOS client?
|
||||
@@ -264,11 +264,11 @@ the brackets, like so: \fBq\e[abc\e]x\fP\&.
|
||||
On Windows, escaping special characters is not supported as the \fB\e\fP
|
||||
character is used as a path separator. On the other hand, special characters
|
||||
such as \fB[\fP and \fB?\fP are not allowed in file names on Windows.
|
||||
.SS Why is the setup more complicated than BTSync?
|
||||
.SS Why is the setup more complicated than BitTorrent/Resilio Sync?
|
||||
.sp
|
||||
Security over convenience. In Syncthing you have to setup both sides to
|
||||
connect two nodes. An attacker can\(aqt do much with a stolen node ID, because
|
||||
you have to add the node on the other side too. You have better control
|
||||
connect two devices. An attacker can\(aqt do much with a stolen device ID, because
|
||||
you have to add the device on the other side too. You have better control
|
||||
where your files are transferred.
|
||||
.sp
|
||||
This is an area that we are working to improve in the long term.
|
||||
@@ -306,7 +306,7 @@ to
|
||||
Then the GUI is accessible from everywhere. You should set a password and
|
||||
enable HTTPS with this configuration. You can do this from inside the GUI.
|
||||
.sp
|
||||
If both your computers are Unixy (Linux, Mac, etc) You can also leave the
|
||||
If both your computers are Unix\-like (Linux, Mac, etc.) you can also leave the
|
||||
GUI settings at default and use an ssh port forward to access it. For
|
||||
example,
|
||||
.INDENT 0.0
|
||||
@@ -336,7 +336,7 @@ $ ssh \-N \-L 9090:127.0.0.1:8384 user@othercomputer.example.com
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
If only your remote computer is Unixy,
|
||||
If only your remote computer is Unix\-like,
|
||||
you can still access it with ssh from Windows.
|
||||
.sp
|
||||
Under Windows 10 (64 bit) you can use the same ssh command if you install
|
||||
@@ -374,7 +374,7 @@ In all cases, username/password authentication and HTTPS should be used.
|
||||
.SS My Syncthing database is corrupt
|
||||
.sp
|
||||
This is almost always a result of bad RAM, storage device or other hardware. When the index database is found to be corrupt Syncthing cannot operate and will note this in the logs and exit. To overcome this delete the \fI\%database folder\fP <\fBhttps://docs.syncthing.net/users/config.html#description\fP> inside Syncthing\(aqs home directory and re\-start Syncthing. It will then need to perform a full re\-hashing of all shared folders. You should check your system in case the underlying cause is indeed faulty hardware which may put the system at risk of further data loss.
|
||||
.SS I don\(aqt like the GUI / Theme. Can it be changed?
|
||||
.SS I don\(aqt like the GUI or the theme. Can it be changed?
|
||||
.sp
|
||||
You can change the theme in the settings. Syncthing ships with other themes
|
||||
than the default.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-globaldisco \- Global Discovery Protocol v3
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-localdisco \- Local Discovery Protocol v4
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-NETWORKING" "7" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-NETWORKING" "7" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-networking \- Firewall Setup
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-RELAY" "7" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-RELAY" "7" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-relay \- Relay Protocol v1
|
||||
.
|
||||
@@ -604,12 +604,12 @@ did capture all the traffic, and even if the attacker did get their hands on the
|
||||
device keys, they would still not be able to recover/decrypt any traffic which
|
||||
was transported via the relay.
|
||||
.sp
|
||||
After establishing a relay session, syncthing looks at the SessionInvitation
|
||||
After establishing a relay session, Syncthing looks at the SessionInvitation
|
||||
message, and depending which side it has received, wraps the raw socket in
|
||||
either a TLS client socket or a TLS server socket depending on the ServerSocket
|
||||
boolean value in the SessionInvitation, and starts the TLS handshake.
|
||||
.sp
|
||||
From that point onwards it functions exactly the same way as if syncthing was
|
||||
From that point onwards it functions exactly the same way as if Syncthing was
|
||||
establishing a direct connection with the other device over the internet,
|
||||
performing device ID validation, and full TLS encryption, and provides the same
|
||||
security properties as it would provide when connecting over the internet.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-REST-API" "7" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-REST-API" "7" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-rest-api \- REST API
|
||||
.
|
||||
@@ -86,6 +86,7 @@ Returns the current configuration.
|
||||
"maxConflicts": 10,
|
||||
"disableSparseFiles": false,
|
||||
"disableTempIndexes": false,
|
||||
"fsync": false,
|
||||
"invalid": ""
|
||||
}
|
||||
],
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-SECURITY" "7" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-SECURITY" "7" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-security \- Security Principles
|
||||
.
|
||||
@@ -36,9 +36,9 @@ possible for an attacker to join a cluster uninvited, and it should not be
|
||||
possible to extract private information from intercepted traffic. Currently this
|
||||
is implemented as follows.
|
||||
.sp
|
||||
All device to device traffic is protected by TLS. To prevent uninvited nodes
|
||||
from joining a cluster, the certificate fingerprint of each node is compared
|
||||
to a preset list of acceptable nodes at connection establishment. The
|
||||
All device to device traffic is protected by TLS. To prevent uninvited devices
|
||||
from joining a cluster, the certificate fingerprint of each device is compared
|
||||
to a preset list of acceptable devices at connection establishment. The
|
||||
fingerprint is computed as the SHA\-256 hash of the certificate and displayed
|
||||
in BASE32 encoding to form a reasonably compact and convenient string.
|
||||
.sp
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-STIGNORE" "5" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-STIGNORE" "5" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-stignore \- Prevent files from being synchronized to other nodes
|
||||
.
|
||||
@@ -43,12 +43,12 @@ level margin: \\n[rst2man-indent\\n[rst2man-indent-level]]
|
||||
.UNINDENT
|
||||
.SH DESCRIPTION
|
||||
.sp
|
||||
If some files should not be synchronized to other nodes, a file called
|
||||
If some files should not be synchronized to other devices, a file called
|
||||
\fB\&.stignore\fP can be created containing file patterns to ignore. The
|
||||
\fB\&.stignore\fP file must be placed in the root of the repository. The
|
||||
\fB\&.stignore\fP file itself will never be synced to other nodes, although it can
|
||||
\fB#include\fP files that \fIare\fP synchronized between nodes. All patterns are
|
||||
relative to the repository root.
|
||||
\fB\&.stignore\fP file must be placed in the root of the folder. The
|
||||
\fB\&.stignore\fP file itself will never be synced to other devices, although it can
|
||||
\fB#include\fP files that \fIare\fP synchronized between devices. All patterns are
|
||||
relative to the folder root.
|
||||
.sp
|
||||
\fBNOTE:\fP
|
||||
.INDENT 0.0
|
||||
@@ -88,7 +88,7 @@ A pattern beginning with \fB#include\fP results in loading patterns
|
||||
from the named file. It is an error for a file to not exist or be
|
||||
included more than once. Note that while this can be used to include
|
||||
patterns from a file in a subdirectory, the patterns themselves are
|
||||
still relative to the repository \fIroot\fP\&. Example:
|
||||
still relative to the folder \fIroot\fP\&. Example:
|
||||
\fB#include more\-patterns.txt\fP\&.
|
||||
.IP \(bu 2
|
||||
A pattern beginning with a \fB!\fP prefix negates the pattern: matching files
|
||||
@@ -201,7 +201,7 @@ Currently the effects on who is in sync with what can be a bit confusing
|
||||
when using ignore patterns. This should be cleared up in a future
|
||||
version...
|
||||
.sp
|
||||
Assume two nodes, Alice and Bob, where Alice has 100 files to share, but
|
||||
Assume two devices, Alice and Bob, where Alice has 100 files to share, but
|
||||
Bob ignores 25 of these. From Alice\(aqs point of view Bob will become
|
||||
about 75% in sync (the actual number depends on the sizes of the
|
||||
individual files) and remain in "Syncing" state even though it is in
|
||||
@@ -211,7 +211,7 @@ view.
|
||||
.sp
|
||||
If Bob adds files that have already been synced to the ignore list, they
|
||||
will remain in the "global" view but disappear from the "local" view.
|
||||
The end result is more files in the global repository than in the local,
|
||||
The end result is more files in the global folder than in the local,
|
||||
but still 100% in sync (\fI\%issue #624\fP <\fBhttps://github.com/syncthing/syncthing/issues/624\fP>). From Alice\(aqs point of view, Bob
|
||||
will remain 100% in sync until the next reconnect, because Bob has
|
||||
already announced that he has the files that are now suddenly ignored.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-VERSIONING" "7" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-VERSIONING" "7" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-versioning \- Keep automatic backups of deleted files by other nodes
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING" "1" "November 12, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING" "1" "November 25, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing \- Syncthing
|
||||
.
|
||||
|
||||
Reference in New Issue
Block a user