mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-10 06:49:31 -05:00
Compare commits
40 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
0437f6dd66 | ||
|
|
11b35d650d | ||
|
|
b279e261a1 | ||
|
|
1ef75be1c6 | ||
|
|
3582783972 | ||
|
|
5070d52f2f | ||
|
|
7b07ed6580 | ||
|
|
f6a2b6252a | ||
|
|
a9b03de99a | ||
|
|
a7f7058636 | ||
|
|
8ce9b026e9 | ||
|
|
0dcf2f1bc8 | ||
|
|
99922feb3b | ||
|
|
2fd1dca905 | ||
|
|
e3cf718998 | ||
|
|
7157917a16 | ||
|
|
3266aae1c3 | ||
|
|
63194a37f6 | ||
|
|
cabe94552a | ||
|
|
48a229a0cd | ||
|
|
e4db86836b | ||
|
|
913a85c571 | ||
|
|
ed4f6fc4b3 | ||
|
|
9da422f1c5 | ||
|
|
ab1739ba34 | ||
|
|
fc1430aa92 | ||
|
|
3cde608eda | ||
|
|
2898552f4b | ||
|
|
911c148c71 | ||
|
|
91568a173a | ||
|
|
e57f5499a1 | ||
|
|
9abb7b71a9 | ||
|
|
724c354d62 | ||
|
|
c44779094d | ||
|
|
eeedab4091 | ||
|
|
8559e20237 | ||
|
|
26730eb083 | ||
|
|
4160ce674d | ||
|
|
2dbeea21c4 | ||
|
|
a2b8485a89 |
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 {
|
||||
|
||||
@@ -129,10 +129,10 @@ func main() {
|
||||
laddr.Port = 0
|
||||
transport, ok := http.DefaultTransport.(*http.Transport)
|
||||
if ok {
|
||||
transport.DialContext = (&net.Dialer{
|
||||
transport.Dial = (&net.Dialer{
|
||||
Timeout: 30 * time.Second,
|
||||
LocalAddr: laddr,
|
||||
}).DialContext
|
||||
}).Dial
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
|
||||
@@ -4,19 +4,28 @@ Installation
|
||||
-----------
|
||||
**Please note:** When you installed syncthing using the official deb package, you can skip the copying.
|
||||
|
||||
Copy the file `syncthing` to your ufw applications directory usually located at `/etc/ufw/applications.d/`. (root permissions required).
|
||||
Copy the file `syncthing` to your ufw applications directory usually located at `/etc/ufw/applications.d/` (root permissions required).
|
||||
|
||||
In a terminal run
|
||||
```
|
||||
sudo ufw app update syncthing
|
||||
sudo ufw app update syncthing-gui
|
||||
```
|
||||
to load the preset.
|
||||
to load the presets.
|
||||
To allow the syncthing ports, run
|
||||
```
|
||||
sudo ufw allow syncthing
|
||||
```
|
||||
You can then verify the opened ports
|
||||
If you want to access the web gui from anywhere (not only from localhost), you can also allow the gui port.
|
||||
This is step is **not** necessary for a "normal" installation!
|
||||
```
|
||||
sudo ufw allow syncthing-gui
|
||||
```
|
||||
|
||||
|
||||
Verification
|
||||
----------
|
||||
You can verify the opened ports by running
|
||||
```
|
||||
sudo ufw status verbose
|
||||
```
|
||||
|
||||
|
||||
@@ -2,3 +2,8 @@
|
||||
title=Syncthing
|
||||
description=Syncthing file synchronisation
|
||||
ports=22000/tcp|21027/udp
|
||||
|
||||
[syncthing-gui]
|
||||
title=Syncthing-GUI
|
||||
description=Syncthing web gui
|
||||
ports=8384/tcp
|
||||
|
||||
@@ -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": "Διατήρηση εκδόσεων",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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",
|
||||
|
||||
@@ -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)",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"Command": "Kommando",
|
||||
"Comment, when used at the start of a line": "Kommentar, når det blir brukt i starten av en linje.",
|
||||
"Compression": "Komprimering",
|
||||
"Configured": "Configured",
|
||||
"Configured": "Konfigurert",
|
||||
"Connection Error": "Tilkoblingsfeil",
|
||||
"Connection Type": "Tilkoblingstype",
|
||||
"Copied from elsewhere": "Kopiert fra et annet sted",
|
||||
@@ -45,7 +45,7 @@
|
||||
"Device Name": "Navn på Enhet",
|
||||
"Devices": "Enheter",
|
||||
"Disconnected": "Frakoblet",
|
||||
"Discovered": "Discovered",
|
||||
"Discovered": "Oppdaget",
|
||||
"Discovery": "Oppslag",
|
||||
"Documentation": "Dokumentasjon",
|
||||
"Download Rate": "Nedlastingsrate",
|
||||
@@ -135,7 +135,7 @@
|
||||
"Quick guide to supported patterns": "Kjapp innføring i godkjente mønster",
|
||||
"RAM Utilization": "RAM-utnyttelse",
|
||||
"Random": "Tilfeldig",
|
||||
"Reduced by ignore patterns": "Reduced by ignore patterns",
|
||||
"Reduced by ignore patterns": "Reduser med utelatelsesmønster",
|
||||
"Release Notes": "Utgivelsesnotat",
|
||||
"Remote Devices": "Andre enheter",
|
||||
"Remove": "Fjern",
|
||||
@@ -231,8 +231,8 @@
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Du må beholde minst én versjon",
|
||||
"days": "dager",
|
||||
"directories": "directories",
|
||||
"files": "files",
|
||||
"directories": "kataloger",
|
||||
"files": "filer",
|
||||
"full documentation": "all dokumentasjon",
|
||||
"items": "elementer",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønsker å dele mappen \"{{folder}}\".",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"Command": "Kommando",
|
||||
"Comment, when used at the start of a line": "Kommentar, når brukt i starten av linja",
|
||||
"Compression": "Komprimering",
|
||||
"Configured": "Configured",
|
||||
"Configured": "Konfigurert",
|
||||
"Connection Error": "Tilkoplingsfeil",
|
||||
"Connection Type": "Tilkoplingstype",
|
||||
"Copied from elsewhere": "Kopiert frå ein annan stad",
|
||||
@@ -45,7 +45,7 @@
|
||||
"Device Name": "Namn På Eining",
|
||||
"Devices": "Einingar",
|
||||
"Disconnected": "Fråkopla",
|
||||
"Discovered": "Discovered",
|
||||
"Discovered": "Oppdaga",
|
||||
"Discovery": "Oppdaging",
|
||||
"Documentation": "Dokumentasjon",
|
||||
"Download Rate": "Nedlastingsfart",
|
||||
@@ -135,7 +135,7 @@
|
||||
"Quick guide to supported patterns": "Kjapp innføring i godkjente mønster",
|
||||
"RAM Utilization": "Minnebruk",
|
||||
"Random": "Tilfeldig",
|
||||
"Reduced by ignore patterns": "Reduced by ignore patterns",
|
||||
"Reduced by ignore patterns": "Reduser med utelatelsesmønster",
|
||||
"Release Notes": "Utgivingsnotat",
|
||||
"Remote Devices": "Eksterne Einingar",
|
||||
"Remove": "Fjern",
|
||||
@@ -231,8 +231,8 @@
|
||||
"Yes": "Ja",
|
||||
"You must keep at least one version.": "Du må behalda minst ein versjon.",
|
||||
"days": "dagar",
|
||||
"directories": "directories",
|
||||
"files": "files",
|
||||
"directories": "kataloger",
|
||||
"files": "filer",
|
||||
"full documentation": "all dokumentasjon",
|
||||
"items": "element",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} ønskjer å dela mappa \"{{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}}\"",
|
||||
|
||||
@@ -31,7 +31,7 @@
|
||||
"Command": "Команда",
|
||||
"Comment, when used at the start of a line": "Комментарий, если используется в начале строки",
|
||||
"Compression": "Сжатие",
|
||||
"Configured": "Configured",
|
||||
"Configured": "Сконфигурировано",
|
||||
"Connection Error": "Ошибка подключения",
|
||||
"Connection Type": "Тип соединения",
|
||||
"Copied from elsewhere": "Скопировано из другого места",
|
||||
@@ -45,7 +45,7 @@
|
||||
"Device Name": "Имя устройства",
|
||||
"Devices": "Устройства",
|
||||
"Disconnected": "Нет соединения",
|
||||
"Discovered": "Discovered",
|
||||
"Discovered": "Обнаружено",
|
||||
"Discovery": "Обнаружение",
|
||||
"Documentation": "Документация",
|
||||
"Download Rate": "Скорость загрузки",
|
||||
@@ -135,7 +135,7 @@
|
||||
"Quick guide to supported patterns": "Краткое руководство по поддерживаемым шаблонам",
|
||||
"RAM Utilization": "Использование памяти",
|
||||
"Random": "Случайно",
|
||||
"Reduced by ignore patterns": "Reduced by ignore patterns",
|
||||
"Reduced by ignore patterns": "Уменьшено шаблонами игнорирования",
|
||||
"Release Notes": "Примечания к выпуску",
|
||||
"Remote Devices": "Удалённые устройства",
|
||||
"Remove": "Удалить",
|
||||
@@ -231,8 +231,8 @@
|
||||
"Yes": "Да",
|
||||
"You must keep at least one version.": "Вы должны хранить как минимум одну версию.",
|
||||
"days": "дней",
|
||||
"directories": "directories",
|
||||
"files": "files",
|
||||
"directories": "папок",
|
||||
"files": "файлов",
|
||||
"full documentation": "полная документация",
|
||||
"items": "элементы",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} хочет поделиться папкой «{{folder}}».",
|
||||
|
||||
@@ -41,8 +41,8 @@
|
||||
"Deleted": "Borttaget",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Enhet \"{{name}}\" ({{device}} på {{address}}) vill ansluta. Lägg till ny enhet?",
|
||||
"Device ID": "Enhet-ID",
|
||||
"Device Identification": "Enhet identifikation",
|
||||
"Device Name": "Enhets namn",
|
||||
"Device Identification": "Enhetsidentifikation",
|
||||
"Device Name": "Enhetsnamn",
|
||||
"Devices": "Enheter",
|
||||
"Disconnected": "Frånkopplad",
|
||||
"Discovered": "Upptäckt",
|
||||
|
||||
@@ -1,28 +1,28 @@
|
||||
{
|
||||
"A device with that ID is already added.": "Bu ID'yi taşıyan cihaz zaten eklendi.",
|
||||
"A device with that ID is already added.": "Bu ID'yi taşıyan aygıt zaten eklendi.",
|
||||
"A negative number of days doesn't make sense.": "Eksi gün sayısı mantıklı bir ifade değil.",
|
||||
"A new major version may not be compatible with previous versions.": "Yeni birincil sürümler önceki sürümlerle uyumlu olmayabilir.",
|
||||
"A new major version may not be compatible with previous versions.": "Yeni ana sürüm önceki sürümlerle uyumlu olmayabilir.",
|
||||
"API Key": "API Anahtarı",
|
||||
"About": "Hakkında",
|
||||
"Actions": "Eylemler",
|
||||
"Add": "Ekle",
|
||||
"Add Device": "Cihaz Ekle",
|
||||
"Add Device": "Aygıt Ekle",
|
||||
"Add Folder": "Klasör Ekle",
|
||||
"Add Remote Device": "Add Remote Device",
|
||||
"Add Remote Device": "Uzak Aygıt Ekle",
|
||||
"Add new folder?": "Yeni klasör ekle?",
|
||||
"Address": "Adres",
|
||||
"Addresses": "Adresler",
|
||||
"Advanced": "Gelişmiş Düzey",
|
||||
"Advanced Configuration": "Gelişmiş Yapılandırma",
|
||||
"Advanced settings": "Advanced settings",
|
||||
"All Data": "Bütün Veriler",
|
||||
"Advanced settings": "Gelişmiş ayarlar",
|
||||
"All Data": "Tüm Veriler",
|
||||
"Allow Anonymous Usage Reporting?": "Anonim kullanımın raporlanmasına izin veriyor musun ?",
|
||||
"Alphabetic": "Alfabetik",
|
||||
"An external command handles the versioning. It has to remove the file from the synced folder.": "Sürümlendirme işlemini harici bir komut yürütüyor. Dosyayı eşzamanlama klasöründen kaldırmak zorunda.",
|
||||
"Anonymous Usage Reporting": "Anonim Kullanım Raporlama",
|
||||
"Any devices configured on an introducer device will be added to this device as well.": "Tanıtıcı bir cihazda yapılandırılan cihazlar bu cihaza da eklenecektir.",
|
||||
"Automatic upgrades": "Otomatik güncellemeler",
|
||||
"Be careful!": "Dikkatli Ol!",
|
||||
"Be careful!": "Dikkatli ol!",
|
||||
"Bugs": "Hatalar",
|
||||
"CPU Utilization": "İşlemci Kullanımı",
|
||||
"Changelog": "Değişim Günlüğü",
|
||||
@@ -33,20 +33,20 @@
|
||||
"Compression": "Sıkıştırma",
|
||||
"Configured": "Configured",
|
||||
"Connection Error": "Bağlantı hatası",
|
||||
"Connection Type": "Connection Type",
|
||||
"Connection Type": "Bağlantı Türü",
|
||||
"Copied from elsewhere": "Başka bir yerden kopyalanmış",
|
||||
"Copied from original": "Aslından kopyalanmış",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Danger!": "Tehlike!",
|
||||
"Deleted": "Silindi",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Device \"{{name}}\" ({{device}} at {{address}}) wants to connect. Add new device?",
|
||||
"Device ID": "Cihaz ID",
|
||||
"Device Identification": "Cihaz Kimliği",
|
||||
"Device Name": "Cihaz Adı",
|
||||
"Devices": "Cihazlar",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "\"{{name}}\" aygıtı ({{address}} adresindeki {{device}}) bağlanmak istiyor. Yeni aygıtı ekle?",
|
||||
"Device ID": "Aygıt ID",
|
||||
"Device Identification": "Aygıt Kimliği",
|
||||
"Device Name": "Aygıt Adı",
|
||||
"Devices": "Aygıtlar",
|
||||
"Disconnected": "Bağlantı Kesik",
|
||||
"Discovered": "Discovered",
|
||||
"Discovery": "Discovery",
|
||||
"Discovered": "Keşfedildi",
|
||||
"Discovery": "Keşif",
|
||||
"Documentation": "Belgeleme",
|
||||
"Download Rate": "İndirme Hızı",
|
||||
"Downloaded": "İndirilmiş",
|
||||
@@ -59,68 +59,68 @@
|
||||
"Enter ignore patterns, one per line.": "Yoksayılacak/ihmal edilecek kalıp dizilerini her satıra bir tane olacak şekilde girin.",
|
||||
"Error": "Hata",
|
||||
"External File Versioning": "Harici Dosya Sürümlendirme",
|
||||
"Failed Items": "Başarısız olunan Öğeler",
|
||||
"File Pull Order": "File Pull Order",
|
||||
"Failed Items": "Başarısız Olunan Ögeler",
|
||||
"File Pull Order": "Dosya Koyma Düzeni",
|
||||
"File Versioning": "Dosya Sürümlendirme",
|
||||
"File permission bits are ignored when looking for changes. Use on FAT file systems.": "Değişimleri yoklarken dosya izin bilgilerini ihmal et. FAT dosya sistemlerinde kullanın.",
|
||||
"Files are moved to .stversions folder when replaced or deleted by Syncthing.": "Dosyalar Syncthing tarafından yeri değiştirildiğinde ya da silindiğinde .stversions klasörüne taşınır.",
|
||||
"Files are moved to date stamped versions in a .stversions folder when replaced or deleted by Syncthing.": "Dosyalar Syncthing tarafından yeri değiştirildiğinde ya da silindiğinde, tarih damgalı sürümleri .stversions klasörüne taşınır.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dosyalar diğer cihazlarda yapılan değişikliklerden korunur, ancak bu cihazdaki değişiklikler kümedeki diğer cihazlara gönderilir.",
|
||||
"Files are protected from changes made on other devices, but changes made on this device will be sent to the rest of the cluster.": "Dosyalar diğer aygıtlarda yapılan değişikliklerden korunur, ancak bu aygıttaki değişiklikler kümedeki diğer aygıtlara gönderilir.",
|
||||
"Folder": "Klasör",
|
||||
"Folder ID": "Klasör ID",
|
||||
"Folder Label": "Folder Label",
|
||||
"Folder Label": "Klasör Etiketi",
|
||||
"Folder Path": "Klasör Yolu",
|
||||
"Folder Type": "Folder Type",
|
||||
"Folder Type": "Klasör Türü",
|
||||
"Folders": "Klasörler",
|
||||
"GUI": "GUI / Kullanıcı Grafik Arayüzü",
|
||||
"GUI": "GUI / Grafiksel Kullanıcı Arayüzü",
|
||||
"GUI Authentication Password": "GUI Kimlik Doğrulaması için Kullanıcı Parolası",
|
||||
"GUI Authentication User": "GUI Kimlik Doğrulaması için Kullanıcı Adı",
|
||||
"GUI Listen Addresses": "GUI Dinleme/Bağlantı Adresleri",
|
||||
"Generate": "Oluştur",
|
||||
"Global Discovery": "Küresel Discovery",
|
||||
"Global Discovery Servers": "Global Discovery Servers",
|
||||
"Global Discovery Servers": "Küresel Keşif Sunucuları",
|
||||
"Global State": "Küresel Durum",
|
||||
"Help": "Yardım",
|
||||
"Home page": "Ana Sayfa",
|
||||
"Home page": "Ana sayfa",
|
||||
"Ignore": "Yoksay",
|
||||
"Ignore Patterns": "Kalıpları Yoksay",
|
||||
"Ignore Permissions": "İzinleri yoksay",
|
||||
"Incoming Rate Limit (KiB/s)": "İndirme Oranı Limiti (KiB/s)",
|
||||
"Incoming Rate Limit (KiB/s)": "İndirme Oranı Sınırı (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Yanlış yapılandırma klasör içeriğine zarar verebilir ve Syncthing'i çalışamaz hale getirebilir.",
|
||||
"Introducer": "Tanıtıcı",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Verilen koşulun ters çevirilmesi (örneğin: yok sayma)",
|
||||
"Keep Versions": "Sürümleri Tut",
|
||||
"Largest First": "En büyük olan önce",
|
||||
"Last File Received": "Alınan Son Dosya",
|
||||
"Last Scan": "Last Scan",
|
||||
"Last seen": "Son Görülen",
|
||||
"Last Scan": "Son Tarama",
|
||||
"Last seen": "Son görülme",
|
||||
"Later": "Sonra",
|
||||
"Listeners": "Listeners",
|
||||
"Listeners": "Dinleyiciler",
|
||||
"Local Discovery": "Yerel Discovery",
|
||||
"Local State": "Yerel Durum",
|
||||
"Local State (Total)": "Yerel Durum (Toplamı)",
|
||||
"Major Upgrade": "Birincil Yükseltme",
|
||||
"Master": "Master",
|
||||
"Master": "Ana",
|
||||
"Maximum Age": "Azami Süre",
|
||||
"Metadata Only": "Sadece Üstveri",
|
||||
"Metadata Only": "Yalnızca Üstveri",
|
||||
"Minimum Free Disk Space": "En Az Boş Disk Alanı",
|
||||
"Move to top of queue": "Kuyruğun başına taşı",
|
||||
"Multi level wildcard (matches multiple directory levels)": "Çoklu düzey wildcard (çok sayıda dizin düzeyinde eşleşme)",
|
||||
"Never": "Asla",
|
||||
"New Device": "Yeni Cihaz",
|
||||
"New Device": "Yeni Aygıt",
|
||||
"New Folder": "Yeni Klasör",
|
||||
"Newest First": "En yeni olan önce",
|
||||
"No": "Hayır",
|
||||
"No File Versioning": "Dosya Sürümlendirmesi Yok",
|
||||
"Normal": "Normal",
|
||||
"Normal": "Olağan",
|
||||
"Notice": "Uyarı",
|
||||
"OK": "Tamam",
|
||||
"Off": "Kapalı",
|
||||
"Oldest First": "En eski olan önce",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Optional descriptive label for the folder. Can be different on each device.",
|
||||
"Optional descriptive label for the folder. Can be different on each device.": "Klasör için isteğe bağlı açıklayıcı etiket. Her aygıtta başka olabilir.",
|
||||
"Options": "Seçenekler",
|
||||
"Out of Sync": "Eşzamanlama Dışı",
|
||||
"Out of Sync Items": "Eşzamanlama dışında kalan Öğeler",
|
||||
"Out of Sync Items": "Eşzamanlama Dışında Kalan Ögeler",
|
||||
"Outgoing Rate Limit (KiB/s)": "Yükleme hız sınırı (KB/sn)",
|
||||
"Override Changes": "Değişiklikleri Geçersiz kıl",
|
||||
"Path to the folder on the local computer. Will be created if it does not exist. The tilde character (~) can be used as a shortcut for": "Yerel bilgisayardaki klasöre ulaşım yolu. Klasör yoksa yaratılacak. Tilde (~) karakterinin kısayol olarak kullanılabileceği yol",
|
||||
@@ -137,7 +137,7 @@
|
||||
"Random": "Rastgele",
|
||||
"Reduced by ignore patterns": "Reduced by ignore patterns",
|
||||
"Release Notes": "Sürüm Notları",
|
||||
"Remote Devices": "Remote Devices",
|
||||
"Remote Devices": "Uzak Aygıtlar",
|
||||
"Remove": "Kaldır",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Required identifier for the folder. Must be the same on all cluster devices.",
|
||||
"Rescan": "Tekrar Tara",
|
||||
@@ -151,23 +151,23 @@
|
||||
"Save": "Kaydet",
|
||||
"Scan Time Remaining": "Kalan Tarama Zamanı",
|
||||
"Scanning": "Taranıyor",
|
||||
"Select the devices to share this folder with.": "Bu klasörü paylaşacağın cihazları seç.",
|
||||
"Select the folders to share with this device.": "Bu cihazla paylaşılacak klasörleri seç.",
|
||||
"Select the devices to share this folder with.": "Bu klasörü paylaşacağın aygıtları seç.",
|
||||
"Select the folders to share with this device.": "Bu aygıtla paylaşılacak klasörleri seç.",
|
||||
"Settings": "Ayarlar",
|
||||
"Share": "Paylaş",
|
||||
"Share Folder": "Paylaşım Klasörü",
|
||||
"Share Folders With Device": "Klasörü Cihazla Paylaş",
|
||||
"Share With Devices": "Cihazlar İle Paylaş",
|
||||
"Share this folder?": "Bu klasörü paylaşmak istiyor musun?",
|
||||
"Share Folders With Device": "Klasörü Aygıtla Paylaş",
|
||||
"Share With Devices": "Aygıtlar İle Paylaş",
|
||||
"Share this folder?": "Bu klasörü paylaş?",
|
||||
"Shared With": "Paylaşılan düğümler",
|
||||
"Show ID": "ID Göster",
|
||||
"Show QR": "QR Göster",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Küme durumunda Cihaz ID yerine bunu göster. Varsayılan isim isteğe bağlı olarak diğer cihazlara ilan edilecektir.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Küme durumunda Cihaz ID yerine bunu göster. Eğer düğüm ismi boş bırakılırsa düğüm ismi güncellenip ilan edilecektir.",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Küme durumunda Aygıt ID yerine bunu göster. Varsayılan ad isteğe bağlı olarak diğer aygıtlara ilan edilecektir.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Küme durumunda Aygıt ID yerine bunu göster. Eğer düğüm adı boş bırakılırsa düğüm adı güncellenip ilan edilecektir.",
|
||||
"Shutdown": "Kapat",
|
||||
"Shutdown Complete": "Kapatma İşlemi Tamamlandı",
|
||||
"Simple File Versioning": "Basit Dosya Sürümlendirme",
|
||||
"Single level wildcard (matches within a directory only)": "Tekli düzey wildcard (sadece bir dizin içinde eşleşme)",
|
||||
"Single level wildcard (matches within a directory only)": "Tekli düzey wildcard (yalnızca bir dizin içinde eşleşme)",
|
||||
"Smallest First": "En küçük olan önce",
|
||||
"Source Code": "Kaynak Kodu",
|
||||
"Staggered File Versioning": "Aşamalı Dosya Sürümlendirme",
|
||||
@@ -186,16 +186,16 @@
|
||||
"The Syncthing admin interface is configured to allow remote access without a password.": "Syncthing yönetici arayüzü parolasız olarak uzaktan erişime izin verilecek şekilde yapılandırıldı.",
|
||||
"The aggregated statistics are publicly available at the URL below.": "The aggregated statistics are publicly available at the URL below.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Yapılandırma kaydedildi ancak etkinleştirilmedi. Etkinleştirmek için Syncthing yeniden başlatılmalı.",
|
||||
"The device ID cannot be blank.": "Cihaz ID boş olamaz.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Buraya girilecek olan aygıt ID'si diğer cihazlarda, \"Eylemler > ID Göster\" penceresinde bulunabilir. Boşluklar ve çizgiler isteğe bağlıdır (yoksayılmış).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Şifrelenmiş kullanım bilgisi günlük olarak gönderilir. Platform, klasör büyüklüğü ve uygulama sürümü hakkında bilgi toplanır. Toplanan bilgi çeşidi değişecek olursa, sizden tekrar onay istenecek.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Girilen cihaz ID'si geçerli gibi gözükmüyor. 52 ya da 56 karakter uzunluğunda, harf ve rakamlardan oluşmalı. Boşlukların ve kısa çizgilerin olup olmaması önemli değildir.",
|
||||
"The device ID cannot be blank.": "Aygıt ID boş olamaz.",
|
||||
"The device ID to enter here can be found in the \"Actions > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Buraya girilecek olan aygıt ID'si diğer aygıtlarda, \"Eylemler > ID Göster\" penceresinde bulunabilir. Boşluklar ve çizgiler isteğe bağlıdır (yoksayılmış).",
|
||||
"The encrypted usage report is sent daily. It is used to track common platforms, folder sizes and app versions. If the reported data set is changed you will be prompted with this dialog again.": "Şifrelenmiş kullanım bilgisi günlük olarak gönderilir. Platform, klasör büyüklüğü ve uygulama sürümü hakkında bilgi toplanır. Toplanan bilgi türü değişecek olursa, sizden yeniden onay istenecek.",
|
||||
"The entered device ID does not look valid. It should be a 52 or 56 character string consisting of letters and numbers, with spaces and dashes being optional.": "Girilen aygıt ID'si geçerli gibi gözükmüyor. 52 ya da 56 karakter uzunluğunda, harf ve rakamlardan oluşmalı. Boşlukların ve kısa çizgilerin olup olmaması önemli değildir.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "İlk komut satırı parametresi klasör yoludur; ikinci parametre ise klasördeki göreceli yoldur. ",
|
||||
"The folder ID cannot be blank.": "Klasör ID boş olamaz.",
|
||||
"The folder ID must be unique.": "Klasör ID benzersiz olmalıdır.",
|
||||
"The folder path cannot be blank.": "Klasör dizini boş bırakılamaz.",
|
||||
"The following intervals are used: for the first hour a version is kept every 30 seconds, for the first day a version is kept every hour, for the first 30 days a version is kept every day, until the maximum age a version is kept every week.": "Kullanılan zaman aralıkları: ilk bir saat zarfında her 30 saniyede bir, ilk gün zarfında saatte bir, ilk 30 gün zarfında her gün, azami süreye kadar geçen zamanda ise her hafta yeni bir sürüm değeri oluşturulur/tutulur.",
|
||||
"The following items could not be synchronized.": "Aşağıdaki öğelerin eşzamanlama işlemi gerçekleştirilemedi.",
|
||||
"The following items could not be synchronized.": "Aşağıdaki ögelerin eşzamanlama işlemi gerçekleştirilemedi.",
|
||||
"The maximum age must be a number and cannot be blank.": "Azami süre tanımı boş bırakılmamalı ve bir sayı olarak tanımlanmalıdır.",
|
||||
"The maximum time to keep a version (in days, set to 0 to keep versions forever).": "Bir sürümün tutulması için belirlenen azami süre (sürümleri sürekli olarak tutabilmek için 0 değeri atayın)",
|
||||
"The minimum free disk space percentage must be a non-negative number between 0 and 100 (inclusive).": "En az boş disk alanı yüzde olarak 0 ve 100 (dahil) arasında kalan pozitif bir sayıyla tanımlanmalıdır.",
|
||||
@@ -203,11 +203,11 @@
|
||||
"The number of days to keep files in the trash can. Zero means forever.": "Dosyaları çöp kutusunda tutma süresini tanımlayan gün sayısı. Sıfır devamlı/sürekli anlamına gelir.",
|
||||
"The number of old versions to keep, per file.": "Dosya başına saklanacak/tutulacak eski sürüm sayısı.",
|
||||
"The number of versions must be a number and cannot be blank.": "Sürümlerin sayısı sayı olmalı ve boş bırakılamaz.",
|
||||
"The path cannot be blank.": "Dizin yolu boş bırakılamaz.",
|
||||
"The path cannot be blank.": "Yol boş bırakılamaz.",
|
||||
"The rate limit must be a non-negative number (0: no limit)": "Hız sınırı pozitif bir sayı olmalıdır. (0: sınırsız)",
|
||||
"The rescan interval must be a non-negative number of seconds.": "Tarama zaman aralığı, saniye cinsinden negatif olmayan bir sayı olmalıdır.",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Otomatik olarak yeniden deneniyor; hata giderildiğinde eşzamanlama gerçekleştirilecek.",
|
||||
"This Device": "This Device",
|
||||
"They are retried automatically and will be synced when the error is resolved.": "Kendiliğinden yeniden deneniyor; hata giderildiğinde eşzamanlama gerçekleştirilecek.",
|
||||
"This Device": "Bu Aygıt",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Hacker'ların bilgisayarındaki dosyaları okuma ve değiştirme yetkisine kolayca erişebilmelerini sağlayabilir.",
|
||||
"This is a major version upgrade.": "Birincil sürüm yükseltmesidir.",
|
||||
"Trash Can File Versioning": "Çöp Kutusu Dosya Sürümleme",
|
||||
@@ -225,16 +225,16 @@
|
||||
"Version": "Sürüm",
|
||||
"Versions Path": "Sürüm Dizin Yolu",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Sürümler, tanımlı azami süre veya belirlenen zaman aralığı için izin verilen dosya sayısı aşılmışsa kendiliğinden silinir.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Yeni bir cihaz eklendiğinde, bu cihazın karşı tarafa da eklenmesi gerektiğini unutmayın.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Yeni bir klasör eklendiğinde, Klasör ID'nin klasörleri cihazlar arasında bağlantılandırmak için kullanıldığını unutmayın. Klasör ID'ler büyük - küçük harf duyarlıdır ve bütün cihazlarda tamı tamına eşleşmelidir.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Uyarı, bu yol var olan bir klasörün \"{{otherFolder}}\" alt klasörüdür.",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Yeni bir aygıt eklendiğinde, bu aygıtın karşı tarafa da eklenmesi gerektiğini unutmayın.",
|
||||
"When adding a new folder, keep in mind that the Folder ID is used to tie folders together between devices. They are case sensitive and must match exactly between all devices.": "Yeni bir klasör eklendiğinde, Klasör ID'nin klasörleri aygıtlar arasında bağlantılandırmak için kullanıldığını unutmayın. Klasör ID'ler büyük - küçük harf duyarlıdır ve tüm aygıtlarda tamı tamına eşleşmelidir.",
|
||||
"Yes": "Evet",
|
||||
"You must keep at least one version.": "En az bir sürümü tutmalısınız.",
|
||||
"days": "günler",
|
||||
"directories": "directories",
|
||||
"files": "files",
|
||||
"days": "gün",
|
||||
"directories": "dizin",
|
||||
"files": "dosya",
|
||||
"full documentation": "belgelendirme içeriğinin tümü",
|
||||
"items": "öğel",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} \"{{folder}}\" klasörünü paylaşmak istiyor.",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} wants to share folder \"{{folderlabel}}\" ({{folder}})."
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}}, \"{{folderlabel}}\" ({{folder}}) klasörünü paylaşmak istiyor."
|
||||
}
|
||||
@@ -400,7 +400,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
<tr ng-if="folder.type != 'readonly' && folderStats[folder.id].lastFile && folderStats[folder.id].lastFile.filename">
|
||||
<th><span class="fa fa-fw fa-exchange"></span> <span translate>Last File Received</span></th>
|
||||
<th><span class="fa fa-fw fa-exchange"></span> <span translate>Latest Change</span></th>
|
||||
<td class="text-right">
|
||||
<span tooltip data-original-title="{{folderStats[folder.id].lastFile.filename}} @ {{folderStats[folder.id].lastFile.at | date:'yyyy-MM-dd HH:mm:ss'}}">
|
||||
<span translate ng-if="!folderStats[folder.id].lastFile.deleted">Updated</span>
|
||||
|
||||
@@ -33,11 +33,9 @@ angular.module('syncthing.core')
|
||||
lastID = lastEvent.id;
|
||||
}
|
||||
|
||||
$timeout(function () {
|
||||
$http.get(urlbase + '/events?since=' + lastID)
|
||||
.success(successFn)
|
||||
.error(errorFn);
|
||||
}, 500, false);
|
||||
$http.get(urlbase + '/events?since=' + lastID)
|
||||
.success(successFn)
|
||||
.error(errorFn);
|
||||
}
|
||||
|
||||
function errorFn (dummy) {
|
||||
|
||||
@@ -654,7 +654,7 @@ angular.module('syncthing.core')
|
||||
if (state === 'error') {
|
||||
return 'stopped'; // legacy, the state is called "stopped" in the GUI
|
||||
}
|
||||
if (state === 'idle' && $scope.model[folderCfg.id].needFiles + $scope.model[folderCfg.id].needDeletes > 0) {
|
||||
if (state === 'idle' && $scope.neededItems(folderCfg.id) > 0) {
|
||||
return 'outofsync';
|
||||
}
|
||||
if (state === 'scanning') {
|
||||
@@ -1071,6 +1071,13 @@ angular.module('syncthing.core')
|
||||
$scope.editDevice = function (deviceCfg) {
|
||||
$scope.currentDevice = $.extend({}, deviceCfg);
|
||||
$scope.editingExisting = true;
|
||||
$scope.willBeReintroducedBy = undefined;
|
||||
if (deviceCfg.introducedBy) {
|
||||
var introducerDevice = $scope.findDevice(deviceCfg.introducedBy);
|
||||
if (introducerDevice && introducerDevice.introducer) {
|
||||
$scope.willBeReintroducedBy = $scope.deviceName(introducerDevice);
|
||||
}
|
||||
}
|
||||
$scope.currentDevice._addressesStr = deviceCfg.addresses.join(', ');
|
||||
$scope.currentDevice.selectedFolders = {};
|
||||
$scope.deviceFolders($scope.currentDevice).forEach(function (folder) {
|
||||
|
||||
@@ -75,8 +75,13 @@
|
||||
<button type="button" class="btn btn-default btn-sm" data-dismiss="modal">
|
||||
<span class="fa fa-times"></span> <span translate>Close</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning pull-left btn-sm" ng-click="deleteDevice()" ng-if="editingExisting">
|
||||
<span class="fa fa-minus-circle"></span> <span translate>Remove</span>
|
||||
</button>
|
||||
<div ng-if="editingExisting" class="pull-left">
|
||||
<button type="button" class="btn btn-warning btn-sm disabled" ng-if="willBeReintroducedBy" tooltip data-original-title="This device will be reintroduced by {{ willBeReintroducedBy }}">
|
||||
<span class="fa fa-minus-circle"></span> <span translate>Remove</span>
|
||||
</button>
|
||||
<button type="button" class="btn btn-warning btn-sm" ng-click="deleteDevice()" ng-if="!willBeReintroducedBy">
|
||||
<span class="fa fa-minus-circle"></span> <span translate>Remove</span>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</modal>
|
||||
|
||||
@@ -325,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
|
||||
|
||||
@@ -28,21 +28,21 @@ type relayDialer struct {
|
||||
tlsCfg *tls.Config
|
||||
}
|
||||
|
||||
func (d *relayDialer) Dial(id protocol.DeviceID, uri *url.URL) (IntermediateConnection, error) {
|
||||
func (d *relayDialer) Dial(id protocol.DeviceID, uri *url.URL) (internalConn, error) {
|
||||
inv, err := client.GetInvitationFromRelay(uri, id, d.tlsCfg.Certificates, 10*time.Second)
|
||||
if err != nil {
|
||||
return IntermediateConnection{}, err
|
||||
return internalConn{}, err
|
||||
}
|
||||
|
||||
conn, err := client.JoinSession(inv)
|
||||
if err != nil {
|
||||
return IntermediateConnection{}, err
|
||||
return internalConn{}, err
|
||||
}
|
||||
|
||||
err = dialer.SetTCPOptions(conn)
|
||||
if err != nil {
|
||||
conn.Close()
|
||||
return IntermediateConnection{}, err
|
||||
return internalConn{}, err
|
||||
}
|
||||
|
||||
var tc *tls.Conn
|
||||
@@ -55,10 +55,10 @@ func (d *relayDialer) Dial(id protocol.DeviceID, uri *url.URL) (IntermediateConn
|
||||
err = tlsTimedHandshake(tc)
|
||||
if err != nil {
|
||||
tc.Close()
|
||||
return IntermediateConnection{}, err
|
||||
return internalConn{}, err
|
||||
}
|
||||
|
||||
return IntermediateConnection{tc, "Relay (Client)", relayPriority}, nil
|
||||
return internalConn{tc, connTypeRelayClient, relayPriority}, nil
|
||||
}
|
||||
|
||||
func (relayDialer) Priority() int {
|
||||
|
||||
@@ -30,7 +30,7 @@ type relayListener struct {
|
||||
|
||||
uri *url.URL
|
||||
tlsCfg *tls.Config
|
||||
conns chan IntermediateConnection
|
||||
conns chan internalConn
|
||||
factory listenerFactory
|
||||
|
||||
err error
|
||||
@@ -93,7 +93,7 @@ func (t *relayListener) Serve() {
|
||||
continue
|
||||
}
|
||||
|
||||
t.conns <- IntermediateConnection{tc, "Relay (Server)", relayPriority}
|
||||
t.conns <- internalConn{tc, connTypeRelayServer, relayPriority}
|
||||
|
||||
// Poor mans notifier that informs the connection service that the
|
||||
// relay URI has changed. This can only happen when we connect to a
|
||||
@@ -167,7 +167,7 @@ func (t *relayListener) String() string {
|
||||
|
||||
type relayListenerFactory struct{}
|
||||
|
||||
func (f *relayListenerFactory) New(uri *url.URL, cfg *config.Wrapper, tlsCfg *tls.Config, conns chan IntermediateConnection, natService *nat.Service) genericListener {
|
||||
func (f *relayListenerFactory) New(uri *url.URL, cfg *config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
|
||||
return &relayListener{
|
||||
uri: uri,
|
||||
tlsCfg: tlsCfg,
|
||||
|
||||
@@ -50,7 +50,7 @@ type Service struct {
|
||||
model Model
|
||||
tlsCfg *tls.Config
|
||||
discoverer discover.Finder
|
||||
conns chan IntermediateConnection
|
||||
conns chan internalConn
|
||||
bepProtocolName string
|
||||
tlsDefaultCommonName string
|
||||
lans []*net.IPNet
|
||||
@@ -65,7 +65,7 @@ type Service struct {
|
||||
listenerSupervisor *suture.Supervisor
|
||||
|
||||
curConMut sync.Mutex
|
||||
currentConnection map[protocol.DeviceID]Connection
|
||||
currentConnection map[protocol.DeviceID]completeConn
|
||||
}
|
||||
|
||||
func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *tls.Config, discoverer discover.Finder,
|
||||
@@ -82,7 +82,7 @@ func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *
|
||||
model: mdl,
|
||||
tlsCfg: tlsCfg,
|
||||
discoverer: discoverer,
|
||||
conns: make(chan IntermediateConnection),
|
||||
conns: make(chan internalConn),
|
||||
bepProtocolName: bepProtocolName,
|
||||
tlsDefaultCommonName: tlsDefaultCommonName,
|
||||
lans: lans,
|
||||
@@ -105,7 +105,7 @@ func NewService(cfg *config.Wrapper, myID protocol.DeviceID, mdl Model, tlsCfg *
|
||||
}),
|
||||
|
||||
curConMut: sync.NewMutex(),
|
||||
currentConnection: make(map[protocol.DeviceID]Connection),
|
||||
currentConnection: make(map[protocol.DeviceID]completeConn),
|
||||
}
|
||||
cfg.Subscribe(service)
|
||||
|
||||
@@ -204,7 +204,7 @@ next:
|
||||
// The Model will return an error for devices that we don't want to
|
||||
// have a connection with for whatever reason, for example unknown devices.
|
||||
if err := s.model.OnHello(remoteID, c.RemoteAddr(), hello); err != nil {
|
||||
l.Infof("Connection from %s at %s (%s) rejected: %v", remoteID, c.RemoteAddr(), c.Type, err)
|
||||
l.Infof("Connection from %s at %s (%s) rejected: %v", remoteID, c.RemoteAddr(), c.Type(), err)
|
||||
c.Close()
|
||||
continue
|
||||
}
|
||||
@@ -218,7 +218,7 @@ next:
|
||||
priorityKnown := ok && connected
|
||||
|
||||
// Lower priority is better, just like nice etc.
|
||||
if priorityKnown && ct.Priority > c.Priority {
|
||||
if priorityKnown && ct.internalConn.priority > c.priority {
|
||||
l.Debugln("Switching connections", remoteID)
|
||||
} else if connected {
|
||||
// We should not already be connected to the other party. TODO: This
|
||||
@@ -268,9 +268,9 @@ next:
|
||||
rd = NewReadLimiter(c, s.readRateLimit)
|
||||
}
|
||||
|
||||
name := fmt.Sprintf("%s-%s (%s)", c.LocalAddr(), c.RemoteAddr(), c.Type)
|
||||
name := fmt.Sprintf("%s-%s (%s)", c.LocalAddr(), c.RemoteAddr(), c.Type())
|
||||
protoConn := protocol.NewConnection(remoteID, rd, wr, s.model, name, deviceCfg.Compression)
|
||||
modelConn := Connection{c, protoConn}
|
||||
modelConn := completeConn{c, protoConn}
|
||||
|
||||
l.Infof("Established secure connection to %s at %s", remoteID, name)
|
||||
l.Debugf("cipher suite: %04X in lan: %t", c.ConnectionState().CipherSuite, !limit)
|
||||
@@ -329,7 +329,7 @@ func (s *Service) connect() {
|
||||
s.curConMut.Unlock()
|
||||
priorityKnown := ok && connected
|
||||
|
||||
if priorityKnown && ct.Priority == bestDialerPrio {
|
||||
if priorityKnown && ct.internalConn.priority == bestDialerPrio {
|
||||
// Things are already as good as they can get.
|
||||
continue
|
||||
}
|
||||
@@ -377,8 +377,8 @@ func (s *Service) connect() {
|
||||
continue
|
||||
}
|
||||
|
||||
if priorityKnown && dialerFactory.Priority() >= ct.Priority {
|
||||
l.Debugf("Not dialing using %s as priority is less than current connection (%d >= %d)", dialerFactory, dialerFactory.Priority(), ct.Priority)
|
||||
if priorityKnown && dialerFactory.Priority() >= ct.internalConn.priority {
|
||||
l.Debugf("Not dialing using %s as priority is less than current connection (%d >= %d)", dialerFactory, dialerFactory.Priority(), ct.internalConn.priority)
|
||||
continue
|
||||
}
|
||||
|
||||
|
||||
@@ -9,6 +9,7 @@ package connections
|
||||
import (
|
||||
"crypto/tls"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/url"
|
||||
"time"
|
||||
@@ -18,19 +19,61 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
type IntermediateConnection struct {
|
||||
*tls.Conn
|
||||
Type string
|
||||
Priority int
|
||||
// Connection is what we expose to the outside. It is a protocol.Connection
|
||||
// that can be closed and has some metadata.
|
||||
type Connection interface {
|
||||
protocol.Connection
|
||||
io.Closer
|
||||
Type() string
|
||||
RemoteAddr() net.Addr
|
||||
}
|
||||
|
||||
type Connection struct {
|
||||
IntermediateConnection
|
||||
// completeConn is the aggregation of an internalConn and the
|
||||
// protocol.Connection running on top of it. It implements the Connection
|
||||
// interface.
|
||||
type completeConn struct {
|
||||
internalConn
|
||||
protocol.Connection
|
||||
}
|
||||
|
||||
func (c Connection) String() string {
|
||||
return fmt.Sprintf("%s-%s/%s", c.LocalAddr(), c.RemoteAddr(), c.Type)
|
||||
// internalConn is the raw TLS connection plus some metadata on where it
|
||||
// came from (type, priority).
|
||||
type internalConn struct {
|
||||
*tls.Conn
|
||||
connType connType
|
||||
priority int
|
||||
}
|
||||
|
||||
type connType int
|
||||
|
||||
const (
|
||||
connTypeRelayClient connType = iota
|
||||
connTypeRelayServer
|
||||
connTypeTCPClient
|
||||
connTypeTCPServer
|
||||
)
|
||||
|
||||
func (t connType) String() string {
|
||||
switch t {
|
||||
case connTypeRelayClient:
|
||||
return "relay-client"
|
||||
case connTypeRelayServer:
|
||||
return "relay-server"
|
||||
case connTypeTCPClient:
|
||||
return "tcp-client"
|
||||
case connTypeTCPServer:
|
||||
return "tcp-server"
|
||||
default:
|
||||
return "unknown-type"
|
||||
}
|
||||
}
|
||||
|
||||
func (c internalConn) Type() string {
|
||||
return c.connType.String()
|
||||
}
|
||||
|
||||
func (c internalConn) String() string {
|
||||
return fmt.Sprintf("%s-%s/%s", c.LocalAddr(), c.RemoteAddr(), c.connType.String())
|
||||
}
|
||||
|
||||
type dialerFactory interface {
|
||||
@@ -41,12 +84,12 @@ type dialerFactory interface {
|
||||
}
|
||||
|
||||
type genericDialer interface {
|
||||
Dial(protocol.DeviceID, *url.URL) (IntermediateConnection, error)
|
||||
Dial(protocol.DeviceID, *url.URL) (internalConn, error)
|
||||
RedialFrequency() time.Duration
|
||||
}
|
||||
|
||||
type listenerFactory interface {
|
||||
New(*url.URL, *config.Wrapper, *tls.Config, chan IntermediateConnection, *nat.Service) genericListener
|
||||
New(*url.URL, *config.Wrapper, *tls.Config, chan internalConn, *nat.Service) genericListener
|
||||
Enabled(config.Configuration) bool
|
||||
}
|
||||
|
||||
|
||||
@@ -30,23 +30,23 @@ type tcpDialer struct {
|
||||
tlsCfg *tls.Config
|
||||
}
|
||||
|
||||
func (d *tcpDialer) Dial(id protocol.DeviceID, uri *url.URL) (IntermediateConnection, error) {
|
||||
func (d *tcpDialer) Dial(id protocol.DeviceID, uri *url.URL) (internalConn, error) {
|
||||
uri = fixupPort(uri)
|
||||
|
||||
conn, err := dialer.DialTimeout(uri.Scheme, uri.Host, 10*time.Second)
|
||||
if err != nil {
|
||||
l.Debugln(err)
|
||||
return IntermediateConnection{}, err
|
||||
return internalConn{}, err
|
||||
}
|
||||
|
||||
tc := tls.Client(conn, d.tlsCfg)
|
||||
err = tlsTimedHandshake(tc)
|
||||
if err != nil {
|
||||
tc.Close()
|
||||
return IntermediateConnection{}, err
|
||||
return internalConn{}, err
|
||||
}
|
||||
|
||||
return IntermediateConnection{tc, "TCP (Client)", tcpPriority}, nil
|
||||
return internalConn{tc, connTypeTCPClient, tcpPriority}, nil
|
||||
}
|
||||
|
||||
func (d *tcpDialer) RedialFrequency() time.Duration {
|
||||
|
||||
@@ -32,7 +32,7 @@ type tcpListener struct {
|
||||
uri *url.URL
|
||||
tlsCfg *tls.Config
|
||||
stop chan struct{}
|
||||
conns chan IntermediateConnection
|
||||
conns chan internalConn
|
||||
factory listenerFactory
|
||||
|
||||
natService *nat.Service
|
||||
@@ -115,7 +115,7 @@ func (t *tcpListener) Serve() {
|
||||
continue
|
||||
}
|
||||
|
||||
t.conns <- IntermediateConnection{tc, "TCP (Server)", tcpPriority}
|
||||
t.conns <- internalConn{tc, connTypeTCPServer, tcpPriority}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -173,7 +173,7 @@ func (t *tcpListener) Factory() listenerFactory {
|
||||
|
||||
type tcpListenerFactory struct{}
|
||||
|
||||
func (f *tcpListenerFactory) New(uri *url.URL, cfg *config.Wrapper, tlsCfg *tls.Config, conns chan IntermediateConnection, natService *nat.Service) genericListener {
|
||||
func (f *tcpListenerFactory) New(uri *url.URL, cfg *config.Wrapper, tlsCfg *tls.Config, conns chan internalConn, natService *nat.Service) genericListener {
|
||||
return &tcpListener{
|
||||
uri: fixupPort(uri),
|
||||
tlsCfg: tlsCfg,
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -73,7 +73,7 @@ func (s *sizeTracker) addFile(f FileIntf) {
|
||||
switch {
|
||||
case f.IsDeleted():
|
||||
s.Deleted++
|
||||
case f.IsDirectory():
|
||||
case f.IsDirectory() && !f.IsSymlink():
|
||||
s.Directories++
|
||||
case f.IsSymlink():
|
||||
s.Symlinks++
|
||||
@@ -93,7 +93,7 @@ func (s *sizeTracker) removeFile(f FileIntf) {
|
||||
switch {
|
||||
case f.IsDeleted():
|
||||
s.Deleted--
|
||||
case f.IsDirectory():
|
||||
case f.IsDirectory() && !f.IsSymlink():
|
||||
s.Directories--
|
||||
case f.IsSymlink():
|
||||
s.Symlinks--
|
||||
|
||||
@@ -50,7 +50,7 @@ func (f FileInfoTruncated) FileSize() int64 {
|
||||
if f.Deleted {
|
||||
return 0
|
||||
}
|
||||
if f.IsDirectory() {
|
||||
if f.IsDirectory() || f.IsSymlink() {
|
||||
return protocol.SyntheticDirectorySize
|
||||
}
|
||||
return f.Size
|
||||
|
||||
@@ -63,6 +63,7 @@ type FileInfoTruncated struct {
|
||||
NoPermissions bool `protobuf:"varint,8,opt,name=no_permissions,json=noPermissions,proto3" json:"no_permissions,omitempty"`
|
||||
Version protocol.Vector `protobuf:"bytes,9,opt,name=version" json:"version"`
|
||||
Sequence int64 `protobuf:"varint,10,opt,name=sequence,proto3" json:"sequence,omitempty"`
|
||||
SymlinkTarget string `protobuf:"bytes,17,opt,name=symlink_target,json=symlinkTarget,proto3" json:"symlink_target,omitempty"`
|
||||
}
|
||||
|
||||
func (m *FileInfoTruncated) Reset() { *m = FileInfoTruncated{} }
|
||||
@@ -225,6 +226,14 @@ func (m *FileInfoTruncated) MarshalTo(data []byte) (int, error) {
|
||||
i++
|
||||
i = encodeVarintStructs(data, i, uint64(m.ModifiedNs))
|
||||
}
|
||||
if len(m.SymlinkTarget) > 0 {
|
||||
data[i] = 0x8a
|
||||
i++
|
||||
data[i] = 0x1
|
||||
i++
|
||||
i = encodeVarintStructs(data, i, uint64(len(m.SymlinkTarget)))
|
||||
i += copy(data[i:], m.SymlinkTarget)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
@@ -315,6 +324,10 @@ func (m *FileInfoTruncated) ProtoSize() (n int) {
|
||||
if m.ModifiedNs != 0 {
|
||||
n += 1 + sovStructs(uint64(m.ModifiedNs))
|
||||
}
|
||||
l = len(m.SymlinkTarget)
|
||||
if l > 0 {
|
||||
n += 2 + l + sovStructs(uint64(l))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -785,6 +798,35 @@ func (m *FileInfoTruncated) Unmarshal(data []byte) error {
|
||||
break
|
||||
}
|
||||
}
|
||||
case 17:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field SymlinkTarget", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowStructs
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthStructs
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.SymlinkTarget = string(data[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipStructs(data[iNdEx:])
|
||||
@@ -912,32 +954,33 @@ var (
|
||||
)
|
||||
|
||||
var fileDescriptorStructs = []byte{
|
||||
// 425 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x51, 0x4f, 0x8b, 0xd3, 0x40,
|
||||
0x1c, 0xed, 0xb4, 0xd9, 0xfe, 0xf9, 0xc5, 0xae, 0x3a, 0xc8, 0x32, 0x14, 0x4c, 0x43, 0x41, 0x08,
|
||||
0x82, 0xa9, 0x56, 0xbc, 0x78, 0xdc, 0xc3, 0x82, 0x20, 0x22, 0x51, 0xd6, 0xe3, 0xd2, 0xcc, 0xfc,
|
||||
0xda, 0x1d, 0x48, 0x67, 0x62, 0x66, 0x52, 0xa8, 0x9f, 0xc4, 0xe3, 0x7e, 0x9c, 0x1e, 0xfd, 0x04,
|
||||
0xa2, 0xf5, 0x4b, 0x78, 0x94, 0x4e, 0xd2, 0x98, 0xe3, 0xde, 0xde, 0x9b, 0xdf, 0x7b, 0xbf, 0xf7,
|
||||
0x66, 0x06, 0xc6, 0xc6, 0x16, 0x25, 0xb7, 0x26, 0xce, 0x0b, 0x6d, 0x35, 0xed, 0x8a, 0x74, 0xf2,
|
||||
0x62, 0x2d, 0xed, 0x6d, 0x99, 0xc6, 0x5c, 0x6f, 0xe6, 0x6b, 0xbd, 0xd6, 0x73, 0x37, 0x4a, 0xcb,
|
||||
0x95, 0x63, 0x8e, 0x38, 0x54, 0x59, 0x26, 0x6f, 0x5a, 0x72, 0xb3, 0x53, 0xdc, 0xde, 0x4a, 0xb5,
|
||||
0x6e, 0xa1, 0x4c, 0xa6, 0xd5, 0x06, 0xae, 0xb3, 0x79, 0x8a, 0x79, 0x65, 0x9b, 0x7d, 0x01, 0xff,
|
||||
0x4a, 0x66, 0x78, 0x8d, 0x85, 0x91, 0x5a, 0xd1, 0x97, 0x30, 0xd8, 0x56, 0x90, 0x91, 0x90, 0x44,
|
||||
0xfe, 0xe2, 0x51, 0x7c, 0x32, 0xc5, 0xd7, 0xc8, 0xad, 0x2e, 0x2e, 0xbd, 0xfd, 0xcf, 0x69, 0x27,
|
||||
0x39, 0xc9, 0xe8, 0x05, 0xf4, 0x05, 0x6e, 0x25, 0x47, 0xd6, 0x0d, 0x49, 0xf4, 0x20, 0xa9, 0xd9,
|
||||
0xec, 0x0a, 0xfc, 0x7a, 0xe9, 0x7b, 0x69, 0x2c, 0x7d, 0x05, 0xc3, 0xda, 0x61, 0x18, 0x09, 0x7b,
|
||||
0x91, 0xbf, 0x78, 0x18, 0x8b, 0x34, 0x6e, 0x65, 0xd7, 0x8b, 0x1b, 0xd9, 0x5b, 0xef, 0xfb, 0xdd,
|
||||
0xb4, 0x33, 0xfb, 0xdb, 0x85, 0xc7, 0x47, 0xd5, 0x3b, 0xb5, 0xd2, 0x9f, 0x8b, 0x52, 0xf1, 0xa5,
|
||||
0x45, 0x41, 0x29, 0x78, 0x6a, 0xb9, 0x41, 0x57, 0x72, 0x94, 0x38, 0x4c, 0x9f, 0x83, 0x67, 0x77,
|
||||
0x79, 0xd5, 0xe3, 0x7c, 0x71, 0xf1, 0xbf, 0x78, 0x63, 0xdf, 0xe5, 0x98, 0x38, 0xcd, 0xd1, 0x6f,
|
||||
0xe4, 0x37, 0x64, 0xbd, 0x90, 0x44, 0xbd, 0xc4, 0x61, 0x1a, 0x82, 0x9f, 0x63, 0xb1, 0x91, 0xa6,
|
||||
0x6a, 0xe9, 0x85, 0x24, 0x1a, 0x27, 0xed, 0x23, 0xfa, 0x14, 0x60, 0xa3, 0x85, 0x5c, 0x49, 0x14,
|
||||
0x37, 0x86, 0x9d, 0x39, 0xef, 0xe8, 0x74, 0xf2, 0x89, 0x32, 0x18, 0x08, 0xcc, 0xd0, 0xa2, 0x60,
|
||||
0xfd, 0x90, 0x44, 0xc3, 0xe4, 0x44, 0x8f, 0x13, 0xa9, 0xb6, 0xcb, 0x4c, 0x0a, 0x36, 0xa8, 0x26,
|
||||
0x35, 0xa5, 0xcf, 0xe0, 0x5c, 0xe9, 0x9b, 0x76, 0xee, 0xd0, 0x09, 0xc6, 0x4a, 0x7f, 0x6c, 0x25,
|
||||
0xb7, 0xfe, 0x65, 0x74, 0xbf, 0x7f, 0x99, 0xc0, 0xd0, 0xe0, 0xd7, 0x12, 0x15, 0x47, 0x06, 0xae,
|
||||
0x69, 0xc3, 0xe9, 0x14, 0xfc, 0xe6, 0x1e, 0xca, 0x30, 0x3f, 0x24, 0xd1, 0x59, 0xd2, 0x5c, 0xed,
|
||||
0x43, 0xfd, 0xf4, 0x97, 0x4f, 0xf6, 0xbf, 0x83, 0xce, 0xfe, 0x10, 0x90, 0x1f, 0x87, 0x80, 0xfc,
|
||||
0x3a, 0x04, 0x9d, 0xbb, 0x3f, 0x01, 0x49, 0xfb, 0x2e, 0xf8, 0xf5, 0xbf, 0x00, 0x00, 0x00, 0xff,
|
||||
0xff, 0x2a, 0xae, 0x24, 0x77, 0xb3, 0x02, 0x00, 0x00,
|
||||
// 442 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x8c, 0x51, 0x5d, 0x8b, 0xd3, 0x40,
|
||||
0x14, 0x6d, 0xb7, 0xd9, 0x7e, 0xdc, 0xd8, 0xd5, 0x1d, 0x64, 0x19, 0x0a, 0xb6, 0x4b, 0x41, 0x10,
|
||||
0xc1, 0x54, 0x57, 0x7c, 0xf1, 0x71, 0x1f, 0x16, 0x04, 0x11, 0x19, 0x97, 0xf5, 0xb1, 0x34, 0x99,
|
||||
0xdb, 0xec, 0x60, 0x32, 0x53, 0x33, 0x93, 0x42, 0xfd, 0x25, 0xbe, 0xb9, 0x3f, 0xa7, 0x8f, 0xfe,
|
||||
0x02, 0xd1, 0xfa, 0x47, 0x9c, 0xce, 0xa4, 0x31, 0x8f, 0xfb, 0x10, 0xb8, 0xe7, 0x9e, 0x73, 0xee,
|
||||
0x3d, 0x93, 0x0b, 0x43, 0x6d, 0x8a, 0x32, 0x31, 0x3a, 0x5a, 0x15, 0xca, 0x28, 0x72, 0xc4, 0xe3,
|
||||
0xd1, 0x8b, 0x54, 0x98, 0xdb, 0x32, 0x8e, 0x12, 0x95, 0xcf, 0x52, 0x95, 0xaa, 0x99, 0xa3, 0xe2,
|
||||
0x72, 0xe9, 0x90, 0x03, 0xae, 0xf2, 0x96, 0xd1, 0x9b, 0x86, 0x5c, 0x6f, 0x64, 0x62, 0x6e, 0x85,
|
||||
0x4c, 0x1b, 0x55, 0x26, 0x62, 0x3f, 0x21, 0x51, 0xd9, 0x2c, 0xc6, 0x95, 0xb7, 0x4d, 0x3f, 0x43,
|
||||
0x78, 0x25, 0x32, 0xbc, 0xc1, 0x42, 0x0b, 0x25, 0xc9, 0x4b, 0xe8, 0xad, 0x7d, 0x49, 0xdb, 0xe7,
|
||||
0xed, 0x67, 0xe1, 0xc5, 0xa3, 0xe8, 0x60, 0x8a, 0x6e, 0x30, 0x31, 0xaa, 0xb8, 0x0c, 0xb6, 0xbf,
|
||||
0x26, 0x2d, 0x76, 0x90, 0x91, 0x33, 0xe8, 0x72, 0x5c, 0x8b, 0x04, 0xe9, 0x91, 0x35, 0x3c, 0x60,
|
||||
0x15, 0x9a, 0x5e, 0x41, 0x58, 0x0d, 0x7d, 0x2f, 0xb4, 0x21, 0xaf, 0xa0, 0x5f, 0x39, 0xb4, 0x9d,
|
||||
0xdc, 0xb1, 0x93, 0x1f, 0x46, 0x3c, 0x8e, 0x1a, 0xbb, 0xab, 0xc1, 0xb5, 0xec, 0x6d, 0xf0, 0xfd,
|
||||
0x6e, 0xd2, 0x9a, 0xfe, 0xe8, 0xc0, 0xe9, 0x5e, 0xf5, 0x4e, 0x2e, 0xd5, 0x75, 0x51, 0xca, 0x64,
|
||||
0x61, 0x90, 0x13, 0x02, 0x81, 0x5c, 0xe4, 0xe8, 0x42, 0x0e, 0x98, 0xab, 0xc9, 0x73, 0x08, 0xcc,
|
||||
0x66, 0xe5, 0x73, 0x9c, 0x5c, 0x9c, 0xfd, 0x0f, 0x5e, 0xdb, 0x2d, 0xcb, 0x9c, 0x66, 0xef, 0xd7,
|
||||
0xe2, 0x1b, 0xd2, 0x8e, 0xd5, 0x76, 0x98, 0xab, 0xc9, 0x39, 0x84, 0x2b, 0x2c, 0x72, 0xa1, 0x7d,
|
||||
0xca, 0xc0, 0x52, 0x43, 0xd6, 0x6c, 0x91, 0x27, 0x00, 0xb9, 0xe2, 0x62, 0x29, 0x90, 0xcf, 0x35,
|
||||
0x3d, 0x76, 0xde, 0xc1, 0xa1, 0xf3, 0x89, 0x50, 0xe8, 0x71, 0xcc, 0xd0, 0xe6, 0xa3, 0x5d, 0xcb,
|
||||
0xf5, 0xd9, 0x01, 0xee, 0x19, 0x21, 0xd7, 0x8b, 0x4c, 0x70, 0xda, 0xf3, 0x4c, 0x05, 0xc9, 0x53,
|
||||
0x38, 0x91, 0x6a, 0xde, 0xdc, 0xdb, 0x77, 0x82, 0xa1, 0x54, 0x1f, 0x1b, 0x9b, 0x1b, 0x77, 0x19,
|
||||
0xdc, 0xef, 0x2e, 0x23, 0xe8, 0x6b, 0xfc, 0x5a, 0xa2, 0xb4, 0x97, 0x01, 0x97, 0xb4, 0xc6, 0x64,
|
||||
0x02, 0x61, 0xfd, 0x0e, 0xbb, 0x31, 0xb4, 0xf4, 0x31, 0xab, 0x9f, 0xf6, 0x41, 0xef, 0x53, 0xe9,
|
||||
0x4d, 0x9e, 0x09, 0xf9, 0x65, 0x6e, 0x16, 0x45, 0x8a, 0x86, 0x9e, 0xba, 0x1f, 0x3d, 0xac, 0xba,
|
||||
0xd7, 0xae, 0xe9, 0x2f, 0x74, 0xf9, 0x78, 0xfb, 0x67, 0xdc, 0xda, 0xee, 0xc6, 0xed, 0x9f, 0xf6,
|
||||
0xfb, 0xbd, 0x1b, 0xb7, 0xee, 0xfe, 0x8e, 0xdb, 0x71, 0xd7, 0xe5, 0x7b, 0xfd, 0x2f, 0x00, 0x00,
|
||||
0xff, 0xff, 0xb1, 0x2f, 0x12, 0xb6, 0xda, 0x02, 0x00, 0x00,
|
||||
}
|
||||
|
||||
@@ -33,4 +33,5 @@ message FileInfoTruncated {
|
||||
bool no_permissions = 8;
|
||||
protocol.Vector version = 9 [(gogoproto.nullable) = false];
|
||||
int64 sequence = 10;
|
||||
string symlink_target = 17;
|
||||
}
|
||||
|
||||
@@ -382,21 +382,20 @@ var (
|
||||
)
|
||||
|
||||
var fileDescriptorLocal = []byte{
|
||||
// 241 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0x4c, 0x8e, 0x4f, 0x4e, 0x84, 0x30,
|
||||
0x14, 0xc6, 0x29, 0x24, 0x66, 0xa6, 0x63, 0x5c, 0x10, 0x17, 0xc4, 0x98, 0x42, 0x5c, 0xb1, 0x11,
|
||||
0x16, 0x7a, 0x01, 0x09, 0x9b, 0x6e, 0xb9, 0x80, 0x81, 0xb6, 0x32, 0x2f, 0xc1, 0x3e, 0x43, 0x61,
|
||||
0x12, 0x6f, 0xe3, 0x05, 0xbc, 0x07, 0x4b, 0xd7, 0x2e, 0x1a, 0xad, 0x17, 0x31, 0xe9, 0x68, 0x86,
|
||||
0xdd, 0xf7, 0xfd, 0xf2, 0x7b, 0x7f, 0xe8, 0x6e, 0x40, 0xd1, 0x0e, 0xc5, 0xcb, 0x88, 0x13, 0xc6,
|
||||
0x1b, 0x09, 0x46, 0xe0, 0x41, 0x8d, 0x57, 0xb7, 0x3d, 0x4c, 0xfb, 0xb9, 0x2b, 0x04, 0x3e, 0x97,
|
||||
0x3d, 0xf6, 0x58, 0x7a, 0xa1, 0x9b, 0x9f, 0x7c, 0xf3, 0xc5, 0xa7, 0xe3, 0xe0, 0xcd, 0x3b, 0xa1,
|
||||
0x9b, 0x07, 0xad, 0x71, 0xd6, 0x42, 0xc5, 0x0d, 0x0d, 0x41, 0x26, 0x24, 0x23, 0xf9, 0x79, 0x55,
|
||||
0x2d, 0x36, 0x0d, 0x3e, 0x6d, 0x7a, 0xbf, 0xda, 0x67, 0x5e, 0xb5, 0x98, 0xf6, 0xa0, 0xfb, 0x55,
|
||||
0x1a, 0xa0, 0x3b, 0x9e, 0x10, 0x38, 0x14, 0xb5, 0x3a, 0x80, 0x50, 0xbc, 0x76, 0x36, 0x0d, 0x79,
|
||||
0xdd, 0x84, 0x20, 0xe3, 0x6b, 0xba, 0x6d, 0xa5, 0x1c, 0x95, 0x31, 0xca, 0x24, 0x61, 0x16, 0xe5,
|
||||
0xdb, 0xe6, 0x04, 0xe2, 0x92, 0xee, 0x40, 0x9b, 0xa9, 0xd5, 0x42, 0x3d, 0x82, 0x4c, 0xa2, 0x8c,
|
||||
0xe4, 0x51, 0x75, 0xe1, 0x6c, 0x4a, 0xf9, 0x1f, 0xe6, 0x75, 0x43, 0xff, 0x15, 0x2e, 0xab, 0xcb,
|
||||
0xe5, 0x9b, 0x05, 0x8b, 0x63, 0xe4, 0xc3, 0x31, 0xf2, 0xe5, 0x58, 0xf0, 0xf6, 0xc3, 0x48, 0x77,
|
||||
0xe6, 0x3f, 0xb8, 0xfb, 0x0d, 0x00, 0x00, 0xff, 0xff, 0xa4, 0x46, 0x4f, 0x13, 0x14, 0x01, 0x00,
|
||||
0x00,
|
||||
// 235 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0xe2, 0xce, 0xc9, 0x4f, 0x4e,
|
||||
0xcc, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17, 0xe2, 0x48, 0xc9, 0x2c, 0x4e, 0xce, 0x2f, 0x4b,
|
||||
0x2d, 0x92, 0xd2, 0x4d, 0xcf, 0x2c, 0xc9, 0x28, 0x4d, 0xd2, 0x4b, 0xce, 0xcf, 0xd5, 0x4f, 0xcf,
|
||||
0x4f, 0xcf, 0xd7, 0x07, 0x2b, 0x48, 0x2a, 0x4d, 0x03, 0xf3, 0xc0, 0x1c, 0x30, 0x0b, 0xa2, 0x51,
|
||||
0x69, 0x2d, 0x23, 0x17, 0x87, 0x63, 0x5e, 0x5e, 0x7e, 0x69, 0x5e, 0x72, 0xaa, 0x50, 0x10, 0x17,
|
||||
0x53, 0x66, 0x8a, 0x04, 0xa3, 0x02, 0xa3, 0x06, 0x8f, 0x93, 0xd3, 0x89, 0x7b, 0xf2, 0x0c, 0xb7,
|
||||
0xee, 0xc9, 0x9b, 0x20, 0x99, 0x57, 0x5c, 0x99, 0x97, 0x5c, 0x92, 0x91, 0x99, 0x97, 0x8e, 0xc4,
|
||||
0xca, 0xc9, 0x4c, 0x82, 0x58, 0x91, 0x9c, 0x9f, 0xa3, 0xe7, 0x92, 0x5a, 0x96, 0x99, 0x9c, 0xea,
|
||||
0xe9, 0xf2, 0xe8, 0x9e, 0x3c, 0x93, 0xa7, 0x4b, 0x10, 0xd0, 0x34, 0x21, 0x19, 0x2e, 0xce, 0xc4,
|
||||
0x94, 0x94, 0xa2, 0xd4, 0xe2, 0xe2, 0xd4, 0x62, 0x09, 0x26, 0x05, 0x66, 0x0d, 0xce, 0x20, 0x84,
|
||||
0x80, 0x90, 0x3e, 0x17, 0x77, 0x66, 0x5e, 0x71, 0x49, 0x22, 0xd0, 0xf6, 0x78, 0xa0, 0xd5, 0xcc,
|
||||
0x40, 0xab, 0x99, 0x9d, 0xf8, 0x80, 0xda, 0xb9, 0x3c, 0xa1, 0xc2, 0x40, 0x63, 0xb8, 0x60, 0x4a,
|
||||
0x3c, 0x53, 0x9c, 0x44, 0x4e, 0x3c, 0x94, 0x63, 0x38, 0xf1, 0x48, 0x8e, 0xf1, 0x02, 0x10, 0x3f,
|
||||
0x78, 0x24, 0xc7, 0xb0, 0xe0, 0xb1, 0x1c, 0x63, 0x12, 0x1b, 0xd8, 0x05, 0xc6, 0x80, 0x00, 0x00,
|
||||
0x00, 0xff, 0xff, 0xa4, 0x46, 0x4f, 0x13, 0x14, 0x01, 0x00, 0x00,
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
@@ -359,3 +401,20 @@ func parseIgnoreFile(fd io.Reader, currentFile string, seen map[string]bool) ([]
|
||||
|
||||
return patterns, nil
|
||||
}
|
||||
|
||||
// IsInternal returns true if the file, as a path relative to the folder
|
||||
// root, represents an internal file that should always be ignored. The file
|
||||
// path must be clean (i.e., in canonical shortest form).
|
||||
func IsInternal(file string) bool {
|
||||
internals := []string{".stfolder", ".stignore", ".stversions"}
|
||||
pathSep := string(os.PathSeparator)
|
||||
for _, internal := range internals {
|
||||
if file == internal {
|
||||
return true
|
||||
}
|
||||
if strings.HasPrefix(file, internal+pathSep) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -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 {
|
||||
@@ -801,3 +812,34 @@ func TestGobwasGlobIssue18(t *testing.T) {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsInternal(t *testing.T) {
|
||||
cases := []struct {
|
||||
file string
|
||||
internal bool
|
||||
}{
|
||||
{".stfolder", true},
|
||||
{".stignore", true},
|
||||
{".stversions", true},
|
||||
{".stfolder/foo", true},
|
||||
{".stignore/foo", true},
|
||||
{".stversions/foo", true},
|
||||
|
||||
{".stfolderfoo", false},
|
||||
{".stignorefoo", false},
|
||||
{".stversionsfoo", false},
|
||||
{"foo.stfolder", false},
|
||||
{"foo.stignore", false},
|
||||
{"foo.stversions", false},
|
||||
{"foo/.stfolder", false},
|
||||
{"foo/.stignore", false},
|
||||
{"foo/.stversions", false},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
res := IsInternal(filepath.FromSlash(tc.file))
|
||||
if res != tc.internal {
|
||||
t.Errorf("Unexpected result: IsInteral(%q): %v should be %v", tc.file, res, tc.internal)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -35,6 +35,7 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/stats"
|
||||
"github.com/syncthing/syncthing/lib/symlinks"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
"github.com/syncthing/syncthing/lib/upgrade"
|
||||
"github.com/syncthing/syncthing/lib/versioner"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
@@ -119,6 +120,8 @@ var (
|
||||
errDeviceUnknown = errors.New("unknown device")
|
||||
errDevicePaused = errors.New("device is paused")
|
||||
errDeviceIgnored = errors.New("device is ignored")
|
||||
errNotRelative = errors.New("not a relative path")
|
||||
errNotDir = errors.New("parent is not a directory")
|
||||
)
|
||||
|
||||
// NewModel creates and starts a new model. The model starts in read-only mode,
|
||||
@@ -412,7 +415,7 @@ func (m *Model) ConnectionStats() map[string]interface{} {
|
||||
Paused: m.devicePaused[device],
|
||||
}
|
||||
if conn, ok := m.conn[device]; ok {
|
||||
ci.Type = conn.Type
|
||||
ci.Type = conn.Type()
|
||||
ci.Connected = ok
|
||||
ci.Statistics = conn.Statistics()
|
||||
if addr := conn.RemoteAddr(); addr != nil {
|
||||
@@ -442,7 +445,7 @@ func (m *Model) ConnectionStats() map[string]interface{} {
|
||||
|
||||
// DeviceStatistics returns statistics about each device
|
||||
func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
|
||||
var res = make(map[string]stats.DeviceStatistics)
|
||||
res := make(map[string]stats.DeviceStatistics)
|
||||
for id := range m.cfg.Devices() {
|
||||
res[id.String()] = m.deviceStatRef(id).GetStatistics()
|
||||
}
|
||||
@@ -451,7 +454,7 @@ func (m *Model) DeviceStatistics() map[string]stats.DeviceStatistics {
|
||||
|
||||
// FolderStatistics returns statistics about each folder
|
||||
func (m *Model) FolderStatistics() map[string]stats.FolderStatistics {
|
||||
var res = make(map[string]stats.FolderStatistics)
|
||||
res := make(map[string]stats.FolderStatistics)
|
||||
for id := range m.cfg.Folders() {
|
||||
res[id] = m.folderStatRef(id).GetStatistics()
|
||||
}
|
||||
@@ -542,7 +545,6 @@ func addSizeOfFile(s *db.Counts, f db.FileIntf) {
|
||||
s.Files++
|
||||
}
|
||||
s.Bytes += f.FileSize()
|
||||
return
|
||||
}
|
||||
|
||||
// GlobalSize returns the number of files, deleted files and total bytes for all
|
||||
@@ -577,7 +579,7 @@ func (m *Model) NeedSize(folder string) db.Counts {
|
||||
ignores := m.folderIgnores[folder]
|
||||
cfg := m.folderCfgs[folder]
|
||||
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
|
||||
if shouldIgnore(f, ignores, cfg.IgnoreDelete) {
|
||||
if shouldIgnore(f, ignores, cfg.IgnoreDelete, defTempNamer) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -641,7 +643,7 @@ func (m *Model) NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfo
|
||||
ignores := m.folderIgnores[folder]
|
||||
cfg := m.folderCfgs[folder]
|
||||
rf.WithNeedTruncated(protocol.LocalDeviceID, func(f db.FileIntf) bool {
|
||||
if shouldIgnore(f, ignores, cfg.IgnoreDelete) {
|
||||
if shouldIgnore(f, ignores, cfg.IgnoreDelete, defTempNamer) {
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -678,16 +680,16 @@ func (m *Model) Index(deviceID protocol.DeviceID, folder string, fs []protocol.F
|
||||
runner := m.folderRunners[folder]
|
||||
m.fmut.RUnlock()
|
||||
|
||||
if !ok {
|
||||
l.Fatalf("Index for nonexistent folder %q", folder)
|
||||
}
|
||||
|
||||
if runner != nil {
|
||||
// Runner may legitimately not be set if this is the "cleanup" Index
|
||||
// message at startup.
|
||||
defer runner.IndexUpdated()
|
||||
}
|
||||
|
||||
if !ok {
|
||||
l.Fatalf("Index for nonexistent folder %q", folder)
|
||||
}
|
||||
|
||||
m.pmut.RLock()
|
||||
m.deviceDownloads[deviceID].Update(folder, makeForgetUpdate(fs))
|
||||
m.pmut.RUnlock()
|
||||
@@ -739,8 +741,9 @@ func (m *Model) IndexUpdate(deviceID protocol.DeviceID, folder string, fs []prot
|
||||
|
||||
func (m *Model) folderSharedWith(folder string, deviceID protocol.DeviceID) bool {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
return m.folderSharedWithLocked(folder, deviceID)
|
||||
shared := m.folderSharedWithLocked(folder, deviceID)
|
||||
m.fmut.RUnlock()
|
||||
return shared
|
||||
}
|
||||
|
||||
func (m *Model) folderSharedWithLocked(folder string, deviceID protocol.DeviceID) bool {
|
||||
@@ -762,6 +765,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
|
||||
m.pmut.RLock()
|
||||
conn, ok := m.conn[deviceID]
|
||||
hello := m.helloMessages[deviceID]
|
||||
m.pmut.RUnlock()
|
||||
if !ok {
|
||||
panic("bug: ClusterConfig called on closed or nonexistent connection")
|
||||
@@ -769,6 +773,14 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
|
||||
dbLocation := filepath.Dir(m.db.Location())
|
||||
|
||||
// See issue #3802 - in short, we can't send modern symlink entries to older
|
||||
// clients.
|
||||
dropSymlinks := false
|
||||
if hello.ClientName == m.clientName && upgrade.CompareVersions(hello.ClientVersion, "v0.14.14") < 0 {
|
||||
l.Warnln("Not sending symlinks to old client", deviceID, "- please upgrade to v0.14.14 or newer")
|
||||
dropSymlinks = true
|
||||
}
|
||||
|
||||
m.fmut.Lock()
|
||||
for _, folder := range cm.Folders {
|
||||
if !m.folderSharedWithLocked(folder.ID, deviceID) {
|
||||
@@ -855,7 +867,7 @@ func (m *Model) ClusterConfig(deviceID protocol.DeviceID, cm protocol.ClusterCon
|
||||
}
|
||||
}
|
||||
|
||||
go sendIndexes(conn, folder.ID, fs, m.folderIgnores[folder.ID], startSequence, dbLocation)
|
||||
go sendIndexes(conn, folder.ID, fs, m.folderIgnores[folder.ID], startSequence, dbLocation, dropSymlinks)
|
||||
}
|
||||
|
||||
// This breaks if we send multiple CM messages during the same connection.
|
||||
@@ -1091,60 +1103,43 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
||||
folderIgnores := m.folderIgnores[folder]
|
||||
m.fmut.RUnlock()
|
||||
|
||||
// filepath.Join() returns a filepath.Clean()ed path, which (quoting the
|
||||
// docs for clarity here):
|
||||
//
|
||||
// Clean returns the shortest path name equivalent to path by purely lexical
|
||||
// processing. It applies the following rules iteratively until no further
|
||||
// processing can be done:
|
||||
//
|
||||
// 1. Replace multiple Separator elements with a single one.
|
||||
// 2. Eliminate each . path name element (the current directory).
|
||||
// 3. Eliminate each inner .. path name element (the parent directory)
|
||||
// along with the non-.. element that precedes it.
|
||||
// 4. Eliminate .. elements that begin a rooted path:
|
||||
// that is, replace "/.." by "/" at the beginning of a path,
|
||||
// assuming Separator is '/'.
|
||||
fn := filepath.Join(folderPath, name)
|
||||
|
||||
if !strings.HasPrefix(fn, folderPath) {
|
||||
fn, err := rootedJoinedPath(folderPath, name)
|
||||
if err != nil {
|
||||
// Request tries to escape!
|
||||
l.Debugf("%v Invalid REQ(in) tries to escape: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
|
||||
return protocol.ErrInvalid
|
||||
}
|
||||
|
||||
if folderIgnores != nil {
|
||||
// "rn" becomes the relative name of the file within the folder. This is
|
||||
// different than the original "name" parameter in that it's been
|
||||
// cleaned from any possible funny business.
|
||||
if rn, err := filepath.Rel(folderPath, fn); err != nil {
|
||||
return err
|
||||
} else if folderIgnores.Match(rn).IsIgnored() {
|
||||
l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
// Having passed the rootedJoinedPath check above, we know "name" is
|
||||
// acceptable relative to "folderPath" and in canonical form, so we can
|
||||
// trust it.
|
||||
|
||||
if ignore.IsInternal(name) {
|
||||
l.Debugf("%v REQ(in) for internal file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
if info, err := osutil.Lstat(fn); err == nil && info.Mode()&os.ModeSymlink != 0 {
|
||||
target, _, err := symlinks.Read(fn)
|
||||
if err != nil {
|
||||
l.Debugln("symlinks.Read:", err)
|
||||
if os.IsNotExist(err) {
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
return protocol.ErrGeneric
|
||||
}
|
||||
if _, err := strings.NewReader(target).ReadAt(buf, offset); err != nil {
|
||||
l.Debugln("symlink.Reader.ReadAt", err)
|
||||
return protocol.ErrGeneric
|
||||
}
|
||||
return nil
|
||||
if folderIgnores.Match(name).IsIgnored() {
|
||||
l.Debugf("%v REQ(in) for ignored file: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
if !osutil.IsDir(folderPath, filepath.Dir(name)) {
|
||||
l.Debugf("%v REQ(in) for file not in dir: %s: %q / %q o=%d s=%d", m, deviceID, folder, name, offset, len(buf))
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
// Only check temp files if the flag is set, and if we are set to advertise
|
||||
// the temp indexes.
|
||||
if fromTemporary && !folderCfg.DisableTempIndexes {
|
||||
tempFn := filepath.Join(folderPath, defTempNamer.TempName(name))
|
||||
|
||||
if info, err := osutil.Lstat(tempFn); err != nil || !info.Mode().IsRegular() {
|
||||
// Reject reads for anything that doesn't exist or is something
|
||||
// other than a regular file.
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
if err := readOffsetIntoBuf(tempFn, offset, buf); err == nil {
|
||||
return nil
|
||||
}
|
||||
@@ -1152,7 +1147,13 @@ func (m *Model) Request(deviceID protocol.DeviceID, folder, name string, offset
|
||||
// file has finished downloading.
|
||||
}
|
||||
|
||||
err := readOffsetIntoBuf(fn, offset, buf)
|
||||
if info, err := osutil.Lstat(fn); err != nil || !info.Mode().IsRegular() {
|
||||
// Reject reads for anything that doesn't exist or is something
|
||||
// other than a regular file.
|
||||
return protocol.ErrNoSuchFile
|
||||
}
|
||||
|
||||
err = readOffsetIntoBuf(fn, offset, buf)
|
||||
if os.IsNotExist(err) {
|
||||
return protocol.ErrNoSuchFile
|
||||
} else if err != nil {
|
||||
@@ -1168,8 +1169,7 @@ func (m *Model) CurrentFolderFile(folder string, file string) (protocol.FileInfo
|
||||
if !ok {
|
||||
return protocol.FileInfo{}, false
|
||||
}
|
||||
f, ok := fs.Get(protocol.LocalDeviceID, file)
|
||||
return f, ok
|
||||
return fs.Get(protocol.LocalDeviceID, file)
|
||||
}
|
||||
|
||||
func (m *Model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo, bool) {
|
||||
@@ -1179,8 +1179,7 @@ func (m *Model) CurrentGlobalFile(folder string, file string) (protocol.FileInfo
|
||||
if !ok {
|
||||
return protocol.FileInfo{}, false
|
||||
}
|
||||
f, ok := fs.GetGlobal(file)
|
||||
return f, ok
|
||||
return fs.GetGlobal(file)
|
||||
}
|
||||
|
||||
type cFiler struct {
|
||||
@@ -1248,7 +1247,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
|
||||
@@ -1334,7 +1333,7 @@ func (m *Model) AddConnection(conn connections.Connection, hello protocol.HelloR
|
||||
"deviceName": hello.DeviceName,
|
||||
"clientName": hello.ClientName,
|
||||
"clientVersion": hello.ClientVersion,
|
||||
"type": conn.Type,
|
||||
"type": conn.Type(),
|
||||
}
|
||||
|
||||
addr := conn.RemoteAddr()
|
||||
@@ -1445,7 +1444,7 @@ func (m *Model) receivedFile(folder string, file protocol.FileInfo) {
|
||||
m.folderStatRef(folder).ReceivedFile(file.Name, file.IsDeleted())
|
||||
}
|
||||
|
||||
func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, startSequence int64, dbLocation string) {
|
||||
func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, startSequence int64, dbLocation string, dropSymlinks bool) {
|
||||
deviceID := conn.ID()
|
||||
name := conn.Name()
|
||||
var err error
|
||||
@@ -1453,7 +1452,7 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
|
||||
l.Debugf("sendIndexes for %s-%s/%q starting (slv=%d)", deviceID, name, folder, startSequence)
|
||||
defer l.Debugf("sendIndexes for %s-%s/%q exiting: %v", deviceID, name, folder, err)
|
||||
|
||||
minSequence, err := sendIndexTo(startSequence, conn, folder, fs, ignores, dbLocation)
|
||||
minSequence, err := sendIndexTo(startSequence, conn, folder, fs, ignores, dbLocation, dropSymlinks)
|
||||
|
||||
// Subscribe to LocalIndexUpdated (we have new information to send) and
|
||||
// DeviceDisconnected (it might be us who disconnected, so we should
|
||||
@@ -1476,7 +1475,7 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
|
||||
continue
|
||||
}
|
||||
|
||||
minSequence, err = sendIndexTo(minSequence, conn, folder, fs, ignores, dbLocation)
|
||||
minSequence, err = sendIndexTo(minSequence, conn, folder, fs, ignores, dbLocation, dropSymlinks)
|
||||
|
||||
// Wait a short amount of time before entering the next loop. If there
|
||||
// are continuous changes happening to the local index, this gives us
|
||||
@@ -1485,7 +1484,7 @@ func sendIndexes(conn protocol.Connection, folder string, fs *db.FileSet, ignore
|
||||
}
|
||||
}
|
||||
|
||||
func sendIndexTo(minSequence int64, conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, dbLocation string) (int64, error) {
|
||||
func sendIndexTo(minSequence int64, conn protocol.Connection, folder string, fs *db.FileSet, ignores *ignore.Matcher, dbLocation string, dropSymlinks bool) (int64, error) {
|
||||
deviceID := conn.ID()
|
||||
name := conn.Name()
|
||||
batch := make([]protocol.FileInfo, 0, indexBatchSize)
|
||||
@@ -1507,6 +1506,14 @@ func sendIndexTo(minSequence int64, conn protocol.Connection, folder string, fs
|
||||
maxSequence = f.Sequence
|
||||
}
|
||||
|
||||
if dropSymlinks && f.IsSymlink() {
|
||||
// Do not send index entries with symlinks to clients that can't
|
||||
// handle it. Fixes issue #3802. Once both sides are upgraded, a
|
||||
// rescan (i.e., change) of the symlink is required for it to
|
||||
// sync again, due to delta indexes.
|
||||
return true
|
||||
}
|
||||
|
||||
sorter.Append(f)
|
||||
return true
|
||||
})
|
||||
@@ -1700,7 +1707,12 @@ func (m *Model) ScanFolderSubdirs(folder string, subs []string) error {
|
||||
func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error {
|
||||
for i, sub := range subDirs {
|
||||
sub = osutil.NativeFilename(sub)
|
||||
if p := filepath.Clean(filepath.Join(folder, sub)); !strings.HasPrefix(p, folder) {
|
||||
// We test each path by joining with "root". What we join with is
|
||||
// not relevant, we just want the dotdot escape detection here. For
|
||||
// historical reasons we may get paths that end in a slash. We
|
||||
// remove that first to allow the rootedJoinedPath to pass.
|
||||
sub = strings.TrimRight(sub, string(os.PathSeparator))
|
||||
if _, err := rootedJoinedPath("root", sub); err != nil {
|
||||
return errors.New("invalid subpath")
|
||||
}
|
||||
subDirs[i] = sub
|
||||
@@ -2495,7 +2507,7 @@ func unifySubs(dirs []string, exists func(dir string) bool) []string {
|
||||
func trimUntilParentKnown(dirs []string, exists func(dir string) bool) []string {
|
||||
var subs []string
|
||||
for _, sub := range dirs {
|
||||
for sub != "" && sub != ".stfolder" && sub != ".stignore" {
|
||||
for sub != "" && !ignore.IsInternal(sub) {
|
||||
sub = filepath.Clean(sub)
|
||||
parent := filepath.Dir(sub)
|
||||
if parent == "." || exists(parent) {
|
||||
@@ -2549,9 +2561,7 @@ func makeForgetUpdate(files []protocol.FileInfo) []protocol.FileDownloadProgress
|
||||
}
|
||||
|
||||
// shouldIgnore returns true when a file should be excluded from processing
|
||||
func shouldIgnore(file db.FileIntf, matcher *ignore.Matcher, ignoreDelete bool) bool {
|
||||
// We check things in a certain order here...
|
||||
|
||||
func shouldIgnore(file db.FileIntf, matcher *ignore.Matcher, ignoreDelete bool, tmpNamer tempNamer) bool {
|
||||
switch {
|
||||
case ignoreDelete && file.IsDeleted():
|
||||
// ignoreDelete first because it's a very cheap test so a win if it
|
||||
@@ -2559,9 +2569,13 @@ func shouldIgnore(file db.FileIntf, matcher *ignore.Matcher, ignoreDelete bool)
|
||||
// deleted files.
|
||||
return true
|
||||
|
||||
case tmpNamer.IsTemporary(file.FileName()):
|
||||
return true
|
||||
|
||||
case ignore.IsInternal(file.FileName()):
|
||||
return true
|
||||
|
||||
case matcher.Match(file.FileName()).IsIgnored():
|
||||
// ignore patterns second because ignoring them is a valid way to
|
||||
// silence warnings about them being invalid and so on.
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -2606,3 +2620,57 @@ func (s folderDeviceSet) sortedDevices(folder string) []protocol.DeviceID {
|
||||
sort.Sort(protocol.DeviceIDs(devs))
|
||||
return devs
|
||||
}
|
||||
|
||||
// rootedJoinedPath takes a root and a supposedly relative path inside that
|
||||
// root and returns the joined path. An error is returned if the joined path
|
||||
// is not in fact inside the root.
|
||||
func rootedJoinedPath(root, rel string) (string, error) {
|
||||
// The root must not be empty.
|
||||
if root == "" {
|
||||
return "", errInvalidFilename
|
||||
}
|
||||
|
||||
pathSep := string(os.PathSeparator)
|
||||
|
||||
// The expected prefix for the resulting path is the root, with a path
|
||||
// separator at the end.
|
||||
expectedPrefix := filepath.FromSlash(root)
|
||||
if !strings.HasSuffix(expectedPrefix, pathSep) {
|
||||
expectedPrefix += pathSep
|
||||
}
|
||||
|
||||
// The relative path should be clean from internal dotdots and similar
|
||||
// funkyness.
|
||||
rel = filepath.FromSlash(rel)
|
||||
if filepath.Clean(rel) != rel {
|
||||
return "", errInvalidFilename
|
||||
}
|
||||
|
||||
// It is not acceptable to attempt to traverse upwards or refer to the
|
||||
// root itself.
|
||||
switch rel {
|
||||
case ".", "..", pathSep:
|
||||
return "", errNotRelative
|
||||
}
|
||||
if strings.HasPrefix(rel, ".."+pathSep) {
|
||||
return "", errNotRelative
|
||||
}
|
||||
|
||||
if strings.HasPrefix(rel, pathSep+pathSep) {
|
||||
// The relative path may pretend to be an absolute path within the
|
||||
// root, but the double path separator on Windows implies something
|
||||
// else. It would get cleaned by the Join below, but it's out of
|
||||
// spec anyway.
|
||||
return "", errNotRelative
|
||||
}
|
||||
|
||||
// The supposedly correct path is the one filepath.Join will return, as
|
||||
// it does cleaning and so on. Check that one first to make sure no
|
||||
// obvious escape attempts have been made.
|
||||
joined := filepath.Join(root, rel)
|
||||
if !strings.HasPrefix(joined, expectedPrefix) {
|
||||
return "", errNotRelative
|
||||
}
|
||||
|
||||
return joined, nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"crypto/tls"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@@ -18,17 +17,18 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"sync"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/d4l3k/messagediff"
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/connections"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/ignore"
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
srand "github.com/syncthing/syncthing/lib/rand"
|
||||
"github.com/syncthing/syncthing/lib/scanner"
|
||||
)
|
||||
|
||||
var device1, device2 protocol.DeviceID
|
||||
@@ -218,65 +218,142 @@ type downloadProgressMessage struct {
|
||||
updates []protocol.FileDownloadProgressUpdate
|
||||
}
|
||||
|
||||
type FakeConnection struct {
|
||||
type fakeConnection struct {
|
||||
id protocol.DeviceID
|
||||
requestData []byte
|
||||
downloadProgressMessages []downloadProgressMessage
|
||||
closed bool
|
||||
files []protocol.FileInfo
|
||||
fileData map[string][]byte
|
||||
folder string
|
||||
model *Model
|
||||
indexFn func(string, []protocol.FileInfo)
|
||||
requestFn func(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error)
|
||||
mut sync.Mutex
|
||||
}
|
||||
|
||||
func (FakeConnection) Close() error {
|
||||
func (f *fakeConnection) Close() error {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
f.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f FakeConnection) Start() {
|
||||
func (f *fakeConnection) Start() {
|
||||
}
|
||||
|
||||
func (f FakeConnection) ID() protocol.DeviceID {
|
||||
func (f *fakeConnection) ID() protocol.DeviceID {
|
||||
return f.id
|
||||
}
|
||||
|
||||
func (f FakeConnection) Name() string {
|
||||
func (f *fakeConnection) Name() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (f FakeConnection) Option(string) string {
|
||||
func (f *fakeConnection) Option(string) string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (FakeConnection) Index(string, []protocol.FileInfo) error {
|
||||
func (f *fakeConnection) Index(folder string, fs []protocol.FileInfo) error {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
if f.indexFn != nil {
|
||||
f.indexFn(folder, fs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (FakeConnection) IndexUpdate(string, []protocol.FileInfo) error {
|
||||
func (f *fakeConnection) IndexUpdate(folder string, fs []protocol.FileInfo) error {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
if f.indexFn != nil {
|
||||
f.indexFn(folder, fs)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (f FakeConnection) Request(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
|
||||
return f.requestData, nil
|
||||
func (f *fakeConnection) Request(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
if f.requestFn != nil {
|
||||
return f.requestFn(folder, name, offset, size, hash, fromTemporary)
|
||||
}
|
||||
return f.fileData[name], nil
|
||||
}
|
||||
|
||||
func (FakeConnection) ClusterConfig(protocol.ClusterConfig) {}
|
||||
func (f *fakeConnection) ClusterConfig(protocol.ClusterConfig) {}
|
||||
|
||||
func (FakeConnection) Ping() bool {
|
||||
return true
|
||||
func (f *fakeConnection) Ping() bool {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
return f.closed
|
||||
}
|
||||
|
||||
func (FakeConnection) Closed() bool {
|
||||
return false
|
||||
func (f *fakeConnection) Closed() bool {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
return f.closed
|
||||
}
|
||||
|
||||
func (FakeConnection) Statistics() protocol.Statistics {
|
||||
func (f *fakeConnection) Statistics() protocol.Statistics {
|
||||
return protocol.Statistics{}
|
||||
}
|
||||
|
||||
func (f *FakeConnection) DownloadProgress(folder string, updates []protocol.FileDownloadProgressUpdate) {
|
||||
func (f *fakeConnection) RemoteAddr() net.Addr {
|
||||
return &fakeAddr{}
|
||||
}
|
||||
|
||||
func (f *fakeConnection) Type() string {
|
||||
return "fake"
|
||||
}
|
||||
|
||||
func (f *fakeConnection) DownloadProgress(folder string, updates []protocol.FileDownloadProgressUpdate) {
|
||||
f.downloadProgressMessages = append(f.downloadProgressMessages, downloadProgressMessage{
|
||||
folder: folder,
|
||||
updates: updates,
|
||||
})
|
||||
}
|
||||
|
||||
func BenchmarkRequest(b *testing.B) {
|
||||
func (f *fakeConnection) addFile(name string, flags uint32, ftype protocol.FileInfoType, data []byte) {
|
||||
f.mut.Lock()
|
||||
defer f.mut.Unlock()
|
||||
|
||||
blocks, _ := scanner.Blocks(bytes.NewReader(data), protocol.BlockSize, int64(len(data)), nil)
|
||||
var version protocol.Vector
|
||||
version = version.Update(f.id.Short())
|
||||
|
||||
if ftype == protocol.FileInfoTypeFile || ftype == protocol.FileInfoTypeDirectory {
|
||||
f.files = append(f.files, protocol.FileInfo{
|
||||
Name: name,
|
||||
Type: ftype,
|
||||
Size: int64(len(data)),
|
||||
ModifiedS: time.Now().Unix(),
|
||||
Permissions: flags,
|
||||
Version: version,
|
||||
Sequence: time.Now().UnixNano(),
|
||||
Blocks: blocks,
|
||||
})
|
||||
} else {
|
||||
// Symlink
|
||||
f.files = append(f.files, protocol.FileInfo{
|
||||
Name: name,
|
||||
Type: ftype,
|
||||
Version: version,
|
||||
Sequence: time.Now().UnixNano(),
|
||||
SymlinkTarget: string(data),
|
||||
})
|
||||
}
|
||||
|
||||
if f.fileData == nil {
|
||||
f.fileData = make(map[string][]byte)
|
||||
}
|
||||
f.fileData[name] = data
|
||||
}
|
||||
|
||||
func (f *fakeConnection) sendIndexUpdate() {
|
||||
f.model.IndexUpdate(f.id, f.folder, f.files)
|
||||
}
|
||||
|
||||
func BenchmarkRequestOut(b *testing.B) {
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
@@ -287,18 +364,11 @@ func BenchmarkRequest(b *testing.B) {
|
||||
const n = 1000
|
||||
files := genFiles(n)
|
||||
|
||||
fc := &FakeConnection{
|
||||
id: device1,
|
||||
requestData: []byte("some data to return"),
|
||||
fc := &fakeConnection{id: device1}
|
||||
for _, f := range files {
|
||||
fc.addFile(f.Name, 0644, protocol.FileInfoTypeFile, []byte("some data to return"))
|
||||
}
|
||||
m.AddConnection(connections.Connection{
|
||||
IntermediateConnection: connections.IntermediateConnection{
|
||||
Conn: tls.Client(&fakeConn{}, nil),
|
||||
Type: "foo",
|
||||
Priority: 10,
|
||||
},
|
||||
Connection: fc,
|
||||
}, protocol.HelloResult{})
|
||||
m.AddConnection(fc, protocol.HelloResult{})
|
||||
m.Index(device1, "default", files)
|
||||
|
||||
b.ResetTimer()
|
||||
@@ -313,6 +383,32 @@ func BenchmarkRequest(b *testing.B) {
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkRequestInSingleFile(b *testing.B) {
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
m.ScanFolder("default")
|
||||
|
||||
buf := make([]byte, 128<<10)
|
||||
rand.Read(buf)
|
||||
os.RemoveAll("testdata/request")
|
||||
defer os.RemoveAll("testdata/request")
|
||||
os.MkdirAll("testdata/request/for/a/file/in/a/couple/of/dirs", 0755)
|
||||
ioutil.WriteFile("testdata/request/for/a/file/in/a/couple/of/dirs/128k", buf, 0644)
|
||||
|
||||
b.ResetTimer()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
if err := m.Request(device1, "default", "request/for/a/file/in/a/couple/of/dirs/128k", 0, nil, false, buf); err != nil {
|
||||
b.Error(err)
|
||||
}
|
||||
}
|
||||
|
||||
b.SetBytes(128 << 10)
|
||||
}
|
||||
|
||||
func TestDeviceRename(t *testing.T) {
|
||||
hello := protocol.HelloResult{
|
||||
ClientName: "syncthing",
|
||||
@@ -335,17 +431,7 @@ func TestDeviceRename(t *testing.T) {
|
||||
t.Errorf("Device already has a name")
|
||||
}
|
||||
|
||||
conn := connections.Connection{
|
||||
IntermediateConnection: connections.IntermediateConnection{
|
||||
Conn: tls.Client(&fakeConn{}, nil),
|
||||
Type: "foo",
|
||||
Priority: 10,
|
||||
},
|
||||
Connection: &FakeConnection{
|
||||
id: device1,
|
||||
requestData: []byte("some data to return"),
|
||||
},
|
||||
}
|
||||
conn := &fakeConnection{id: device1}
|
||||
|
||||
m.AddConnection(conn, hello)
|
||||
|
||||
@@ -504,14 +590,7 @@ func TestIntroducer(t *testing.T) {
|
||||
m.AddFolder(folder)
|
||||
}
|
||||
m.ServeBackground()
|
||||
m.AddConnection(connections.Connection{
|
||||
IntermediateConnection: connections.IntermediateConnection{
|
||||
Conn: tls.Client(&fakeConn{}, nil),
|
||||
},
|
||||
Connection: &FakeConnection{
|
||||
id: device1,
|
||||
},
|
||||
}, protocol.HelloResult{})
|
||||
m.AddConnection(&fakeConnection{id: device1}, protocol.HelloResult{})
|
||||
return wcfg, m
|
||||
}
|
||||
|
||||
@@ -921,7 +1000,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 +1852,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 +1870,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()
|
||||
@@ -1900,34 +1983,14 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
|
||||
|
||||
wcfg := config.Wrap("/tmp/test", cfg)
|
||||
|
||||
d2c := &fakeConn{}
|
||||
|
||||
m := NewModel(wcfg, protocol.LocalDeviceID, "device", "syncthing", "dev", dbi, nil)
|
||||
m.AddFolder(fcfg)
|
||||
m.StartFolder(fcfg.ID)
|
||||
m.ServeBackground()
|
||||
|
||||
conn1 := connections.Connection{
|
||||
IntermediateConnection: connections.IntermediateConnection{
|
||||
Conn: tls.Client(&fakeConn{}, nil),
|
||||
Type: "foo",
|
||||
Priority: 10,
|
||||
},
|
||||
Connection: &FakeConnection{
|
||||
id: device1,
|
||||
},
|
||||
}
|
||||
conn1 := &fakeConnection{id: device1}
|
||||
m.AddConnection(conn1, protocol.HelloResult{})
|
||||
conn2 := connections.Connection{
|
||||
IntermediateConnection: connections.IntermediateConnection{
|
||||
Conn: tls.Client(d2c, nil),
|
||||
Type: "foo",
|
||||
Priority: 10,
|
||||
},
|
||||
Connection: &FakeConnection{
|
||||
id: device2,
|
||||
},
|
||||
}
|
||||
conn2 := &fakeConnection{id: device2}
|
||||
m.AddConnection(conn2, protocol.HelloResult{})
|
||||
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
@@ -1960,7 +2023,7 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
|
||||
t.Error("not shared with device2")
|
||||
}
|
||||
|
||||
if d2c.closed {
|
||||
if conn2.Closed() {
|
||||
t.Error("conn already closed")
|
||||
}
|
||||
|
||||
@@ -1980,7 +2043,7 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
|
||||
t.Error("shared with device2")
|
||||
}
|
||||
|
||||
if !d2c.closed {
|
||||
if !conn2.Closed() {
|
||||
t.Error("connection not closed")
|
||||
}
|
||||
|
||||
@@ -2103,20 +2166,171 @@ func TestIssue3496(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func addFakeConn(m *Model, dev protocol.DeviceID) {
|
||||
conn1 := connections.Connection{
|
||||
IntermediateConnection: connections.IntermediateConnection{
|
||||
Conn: tls.Client(&fakeConn{}, nil),
|
||||
Type: "foo",
|
||||
Priority: 10,
|
||||
},
|
||||
Connection: &FakeConnection{
|
||||
id: dev,
|
||||
},
|
||||
}
|
||||
m.AddConnection(conn1, protocol.HelloResult{})
|
||||
func TestIssue3804(t *testing.T) {
|
||||
dbi := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", dbi, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolder("default")
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
// Subdirs ending in slash should be accepted
|
||||
|
||||
if err := m.ScanFolderSubdirs("default", []string{"baz/", "foo"}); err != nil {
|
||||
t.Error("Unexpected error:", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRootedJoinedPath(t *testing.T) {
|
||||
type testcase struct {
|
||||
root string
|
||||
rel string
|
||||
joined string
|
||||
ok bool
|
||||
}
|
||||
cases := []testcase{
|
||||
// Valid cases
|
||||
{"foo", "bar", "foo/bar", true},
|
||||
{"foo", "/bar", "foo/bar", true},
|
||||
{"foo/", "bar", "foo/bar", true},
|
||||
{"foo/", "/bar", "foo/bar", true},
|
||||
{"baz/foo", "bar", "baz/foo/bar", true},
|
||||
{"baz/foo", "/bar", "baz/foo/bar", true},
|
||||
{"baz/foo/", "bar", "baz/foo/bar", true},
|
||||
{"baz/foo/", "/bar", "baz/foo/bar", true},
|
||||
{"foo", "bar/baz", "foo/bar/baz", true},
|
||||
{"foo", "/bar/baz", "foo/bar/baz", true},
|
||||
{"foo/", "bar/baz", "foo/bar/baz", true},
|
||||
{"foo/", "/bar/baz", "foo/bar/baz", true},
|
||||
{"baz/foo", "bar/baz", "baz/foo/bar/baz", true},
|
||||
{"baz/foo", "/bar/baz", "baz/foo/bar/baz", true},
|
||||
{"baz/foo/", "bar/baz", "baz/foo/bar/baz", true},
|
||||
{"baz/foo/", "/bar/baz", "baz/foo/bar/baz", true},
|
||||
|
||||
// Not escape attempts, but oddly formatted relative paths. Disallowed.
|
||||
{"foo", "./bar", "", false},
|
||||
{"baz/foo", "./bar", "", false},
|
||||
{"foo", "./bar/baz", "", false},
|
||||
{"baz/foo", "./bar/baz", "", false},
|
||||
{"baz/foo", "bar/../baz", "", false},
|
||||
{"baz/foo", "/bar/../baz", "", false},
|
||||
{"baz/foo", "./bar/../baz", "", false},
|
||||
{"baz/foo", "bar/../baz", "", false},
|
||||
{"baz/foo", "/bar/../baz", "", false},
|
||||
{"baz/foo", "./bar/../baz", "", false},
|
||||
|
||||
// Results in an allowed path, but does it by probing. Disallowed.
|
||||
{"foo", "../foo", "", false},
|
||||
{"foo", "../foo/bar", "", false},
|
||||
{"baz/foo", "../foo/bar", "", false},
|
||||
{"baz/foo", "../../baz/foo/bar", "", false},
|
||||
{"baz/foo", "bar/../../foo/bar", "", false},
|
||||
{"baz/foo", "bar/../../../baz/foo/bar", "", false},
|
||||
|
||||
// Escape attempts.
|
||||
{"foo", "", "", false},
|
||||
{"foo", "/", "", false},
|
||||
{"foo", "..", "", false},
|
||||
{"foo", "/..", "", false},
|
||||
{"foo", "../", "", false},
|
||||
{"foo", "../bar", "", false},
|
||||
{"foo", "../foobar", "", false},
|
||||
{"foo/", "../bar", "", false},
|
||||
{"foo/", "../foobar", "", false},
|
||||
{"baz/foo", "../bar", "", false},
|
||||
{"baz/foo", "../foobar", "", false},
|
||||
{"baz/foo/", "../bar", "", false},
|
||||
{"baz/foo/", "../foobar", "", false},
|
||||
{"baz/foo/", "bar/../../quux/baz", "", false},
|
||||
|
||||
// Empty root is a misconfiguration.
|
||||
{"", "/foo", "", false},
|
||||
{"", "foo", "", false},
|
||||
{"", ".", "", false},
|
||||
{"", "..", "", false},
|
||||
{"", "/", "", false},
|
||||
{"", "", "", false},
|
||||
|
||||
// Root=/ is valid, and things should be verified as usual.
|
||||
{"/", "foo", "/foo", true},
|
||||
{"/", "/foo", "/foo", true},
|
||||
{"/", "../foo", "", false},
|
||||
{"/", ".", "", false},
|
||||
{"/", "..", "", false},
|
||||
{"/", "/", "", false},
|
||||
{"/", "", "", false},
|
||||
}
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
extraCases := []testcase{
|
||||
{`c:\`, `foo`, `c:\foo`, true},
|
||||
{`\\?\c:\`, `foo`, `\\?\c:\foo`, true},
|
||||
{`c:\`, `\foo`, `c:\foo`, true},
|
||||
{`\\?\c:\`, `\foo`, `\\?\c:\foo`, true},
|
||||
|
||||
{`c:\`, `\\foo`, ``, false},
|
||||
{`c:\`, ``, ``, false},
|
||||
{`c:\`, `.`, ``, false},
|
||||
{`c:\`, `\`, ``, false},
|
||||
{`\\?\c:\`, `\\foo`, ``, false},
|
||||
{`\\?\c:\`, ``, ``, false},
|
||||
{`\\?\c:\`, `.`, ``, false},
|
||||
{`\\?\c:\`, `\`, ``, false},
|
||||
|
||||
// makes no sense, but will be treated simply as a bad filename
|
||||
{`c:\foo`, `d:\bar`, `c:\foo\d:\bar`, true},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
// Add case where root is backslashed, rel is forward slashed
|
||||
extraCases = append(extraCases, testcase{
|
||||
root: filepath.FromSlash(tc.root),
|
||||
rel: tc.rel,
|
||||
joined: tc.joined,
|
||||
ok: tc.ok,
|
||||
})
|
||||
// and the opposite
|
||||
extraCases = append(extraCases, testcase{
|
||||
root: tc.root,
|
||||
rel: filepath.FromSlash(tc.rel),
|
||||
joined: tc.joined,
|
||||
ok: tc.ok,
|
||||
})
|
||||
// and both backslashed
|
||||
extraCases = append(extraCases, testcase{
|
||||
root: filepath.FromSlash(tc.root),
|
||||
rel: filepath.FromSlash(tc.rel),
|
||||
joined: tc.joined,
|
||||
ok: tc.ok,
|
||||
})
|
||||
}
|
||||
|
||||
cases = append(cases, extraCases...)
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
res, err := rootedJoinedPath(tc.root, tc.rel)
|
||||
if tc.ok {
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error for rootedJoinedPath(%q, %q): %v", tc.root, tc.rel, err)
|
||||
continue
|
||||
}
|
||||
exp := filepath.FromSlash(tc.joined)
|
||||
if res != exp {
|
||||
t.Errorf("Unexpected result for rootedJoinedPath(%q, %q): %q != expected %q", tc.root, tc.rel, res, exp)
|
||||
}
|
||||
} else if err == nil {
|
||||
t.Errorf("Unexpected pass for rootedJoinedPath(%q, %q) => %q", tc.root, tc.rel, res)
|
||||
continue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func addFakeConn(m *Model, dev protocol.DeviceID) *fakeConnection {
|
||||
fc := &fakeConnection{id: dev, model: m}
|
||||
m.AddConnection(fc, protocol.HelloResult{})
|
||||
|
||||
m.ClusterConfig(dev, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: "default",
|
||||
@@ -2127,6 +2341,8 @@ func addFakeConn(m *Model, dev protocol.DeviceID) {
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
return fc
|
||||
}
|
||||
|
||||
type fakeAddr struct{}
|
||||
@@ -2138,40 +2354,3 @@ func (fakeAddr) Network() string {
|
||||
func (fakeAddr) String() string {
|
||||
return "address"
|
||||
}
|
||||
|
||||
type fakeConn struct {
|
||||
closed bool
|
||||
}
|
||||
|
||||
func (c *fakeConn) Close() error {
|
||||
c.closed = true
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fakeConn) LocalAddr() net.Addr {
|
||||
return &fakeAddr{}
|
||||
}
|
||||
|
||||
func (fakeConn) RemoteAddr() net.Addr {
|
||||
return &fakeAddr{}
|
||||
}
|
||||
|
||||
func (fakeConn) Read([]byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (fakeConn) Write([]byte) (int, error) {
|
||||
return 0, nil
|
||||
}
|
||||
|
||||
func (fakeConn) SetDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fakeConn) SetReadDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (fakeConn) SetWriteDeadline(time.Time) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -8,7 +8,6 @@ package model
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"path/filepath"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
@@ -213,7 +212,9 @@ func (t *ProgressEmitter) Register(s *sharedPullerState) {
|
||||
if len(t.registry) == 0 {
|
||||
t.timer.Reset(t.interval)
|
||||
}
|
||||
t.registry[filepath.Join(s.folder, s.file.Name)] = s
|
||||
// Separate the folder ID (arbitrary string) and and the file name by "//"
|
||||
// because it never appears in a valid file name.
|
||||
t.registry[s.folder+"//"+s.file.Name] = s
|
||||
}
|
||||
|
||||
// Deregister a puller which will stop broadcasting pullers state.
|
||||
@@ -223,7 +224,7 @@ func (t *ProgressEmitter) Deregister(s *sharedPullerState) {
|
||||
|
||||
l.Debugln("progress emitter: deregistering", s.folder, s.file.Name)
|
||||
|
||||
delete(t.registry, filepath.Join(s.folder, s.file.Name))
|
||||
delete(t.registry, s.folder+"//"+s.file.Name)
|
||||
}
|
||||
|
||||
// BytesCompleted returns the number of bytes completed in the given folder.
|
||||
|
||||
@@ -107,7 +107,7 @@ func TestSendDownloadProgressMessages(t *testing.T) {
|
||||
TempIndexMinBlocks: 10,
|
||||
})
|
||||
|
||||
fc := &FakeConnection{}
|
||||
fc := &fakeConnection{}
|
||||
|
||||
p := NewProgressEmitter(c)
|
||||
p.temporaryIndexSubscribe(fc, []string{"folder", "folder2"})
|
||||
|
||||
227
lib/model/requests_test.go
Normal file
227
lib/model/requests_test.go
Normal file
@@ -0,0 +1,227 @@
|
||||
// Copyright (C) 2016 The Syncthing Authors.
|
||||
//
|
||||
// This Source Code Form is subject to the terms of the Mozilla Public
|
||||
// License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
// You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
package model
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"runtime"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/db"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
func TestRequestSimple(t *testing.T) {
|
||||
// Verify that the model performs a request and creates a file based on
|
||||
// an incoming index update.
|
||||
|
||||
defer os.RemoveAll("_tmpfolder")
|
||||
|
||||
m, fc := setupModelWithConnection()
|
||||
defer m.Stop()
|
||||
|
||||
// We listen for incoming index updates and trigger when we see one for
|
||||
// the expected test file.
|
||||
done := make(chan struct{})
|
||||
fc.mut.Lock()
|
||||
fc.indexFn = func(folder string, fs []protocol.FileInfo) {
|
||||
for _, f := range fs {
|
||||
if f.Name == "testfile" {
|
||||
close(done)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
fc.mut.Unlock()
|
||||
|
||||
// Send an update for the test file, wait for it to sync and be reported back.
|
||||
contents := []byte("test file contents\n")
|
||||
fc.addFile("testfile", 0644, protocol.FileInfoTypeFile, contents)
|
||||
fc.sendIndexUpdate()
|
||||
<-done
|
||||
|
||||
// Verify the contents
|
||||
bs, err := ioutil.ReadFile("_tmpfolder/testfile")
|
||||
if err != nil {
|
||||
t.Error("File did not sync correctly:", err)
|
||||
return
|
||||
}
|
||||
if !bytes.Equal(bs, contents) {
|
||||
t.Error("File did not sync correctly: incorrect data")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSymlinkTraversalRead(t *testing.T) {
|
||||
// Verify that a symlink can not be traversed for reading.
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("no symlink support on CI")
|
||||
return
|
||||
}
|
||||
|
||||
defer os.RemoveAll("_tmpfolder")
|
||||
|
||||
m, fc := setupModelWithConnection()
|
||||
defer m.Stop()
|
||||
|
||||
// We listen for incoming index updates and trigger when we see one for
|
||||
// the expected test file.
|
||||
done := make(chan struct{})
|
||||
fc.mut.Lock()
|
||||
fc.indexFn = func(folder string, fs []protocol.FileInfo) {
|
||||
for _, f := range fs {
|
||||
if f.Name == "symlink" {
|
||||
close(done)
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
fc.mut.Unlock()
|
||||
|
||||
// Send an update for the symlink, wait for it to sync and be reported back.
|
||||
contents := []byte("..")
|
||||
fc.addFile("symlink", 0644, protocol.FileInfoTypeSymlinkDirectory, contents)
|
||||
fc.sendIndexUpdate()
|
||||
<-done
|
||||
|
||||
// Request a file by traversing the symlink
|
||||
buf := make([]byte, 10)
|
||||
err := m.Request(device1, "default", "symlink/requests_test.go", 0, nil, false, buf)
|
||||
if err == nil || !bytes.Equal(buf, make([]byte, 10)) {
|
||||
t.Error("Managed to traverse symlink")
|
||||
}
|
||||
}
|
||||
|
||||
func TestSymlinkTraversalWrite(t *testing.T) {
|
||||
// Verify that a symlink can not be traversed for writing.
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("no symlink support on CI")
|
||||
return
|
||||
}
|
||||
|
||||
defer os.RemoveAll("_tmpfolder")
|
||||
|
||||
m, fc := setupModelWithConnection()
|
||||
defer m.Stop()
|
||||
|
||||
// We listen for incoming index updates and trigger when we see one for
|
||||
// the expected names.
|
||||
done := make(chan struct{}, 1)
|
||||
badReq := make(chan string, 1)
|
||||
badIdx := make(chan string, 1)
|
||||
fc.mut.Lock()
|
||||
fc.indexFn = func(folder string, fs []protocol.FileInfo) {
|
||||
for _, f := range fs {
|
||||
if f.Name == "symlink" {
|
||||
done <- struct{}{}
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(f.Name, "symlink") {
|
||||
badIdx <- f.Name
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
fc.requestFn = func(folder, name string, offset int64, size int, hash []byte, fromTemporary bool) ([]byte, error) {
|
||||
if name != "symlink" && strings.HasPrefix(name, "symlink") {
|
||||
badReq <- name
|
||||
}
|
||||
return fc.fileData[name], nil
|
||||
}
|
||||
fc.mut.Unlock()
|
||||
|
||||
// Send an update for the symlink, wait for it to sync and be reported back.
|
||||
contents := []byte("..")
|
||||
fc.addFile("symlink", 0644, protocol.FileInfoTypeSymlinkDirectory, contents)
|
||||
fc.sendIndexUpdate()
|
||||
<-done
|
||||
|
||||
// Send an update for things behind the symlink, wait for requests for
|
||||
// blocks for any of them to come back, or index entries. Hopefully none
|
||||
// of that should happen.
|
||||
contents = []byte("testdata testdata\n")
|
||||
fc.addFile("symlink/testfile", 0644, protocol.FileInfoTypeFile, contents)
|
||||
fc.addFile("symlink/testdir", 0644, protocol.FileInfoTypeDirectory, contents)
|
||||
fc.addFile("symlink/testsyml", 0644, protocol.FileInfoTypeSymlinkFile, contents)
|
||||
fc.sendIndexUpdate()
|
||||
|
||||
select {
|
||||
case name := <-badReq:
|
||||
t.Fatal("Should not have requested the data for", name)
|
||||
case name := <-badIdx:
|
||||
t.Fatal("Should not have sent the index entry for", name)
|
||||
case <-time.After(3 * time.Second):
|
||||
// Unfortunately not much else to trigger on here. The puller sleep
|
||||
// interval is 1s so if we didn't get any requests within two
|
||||
// iterations we should be fine.
|
||||
}
|
||||
}
|
||||
|
||||
func TestRequestCreateTmpSymlink(t *testing.T) {
|
||||
// Verify that the model performs a request and creates a file based on
|
||||
// an incoming index update.
|
||||
|
||||
defer os.RemoveAll("_tmpfolder")
|
||||
|
||||
m, fc := setupModelWithConnection()
|
||||
defer m.Stop()
|
||||
|
||||
// We listen for incoming index updates and trigger when we see one for
|
||||
// the expected test file.
|
||||
badIdx := make(chan string)
|
||||
fc.mut.Lock()
|
||||
fc.indexFn = func(folder string, fs []protocol.FileInfo) {
|
||||
for _, f := range fs {
|
||||
if f.Name == ".syncthing.testlink.tmp" {
|
||||
badIdx <- f.Name
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
fc.mut.Unlock()
|
||||
|
||||
// Send an update for the test file, wait for it to sync and be reported back.
|
||||
fc.addFile(".syncthing.testlink.tmp", 0644, protocol.FileInfoTypeSymlinkDirectory, []byte(".."))
|
||||
fc.sendIndexUpdate()
|
||||
|
||||
select {
|
||||
case name := <-badIdx:
|
||||
t.Fatal("Should not have sent the index entry for", name)
|
||||
case <-time.After(3 * time.Second):
|
||||
// Unfortunately not much else to trigger on here. The puller sleep
|
||||
// interval is 1s so if we didn't get any requests within two
|
||||
// iterations we should be fine.
|
||||
}
|
||||
}
|
||||
|
||||
func setupModelWithConnection() (*Model, *fakeConnection) {
|
||||
cfg := defaultConfig.RawCopy()
|
||||
cfg.Folders[0] = config.NewFolderConfiguration("default", "_tmpfolder")
|
||||
cfg.Folders[0].PullerSleepS = 1
|
||||
cfg.Folders[0].Devices = []config.FolderDeviceConfiguration{
|
||||
{DeviceID: device1},
|
||||
{DeviceID: device2},
|
||||
}
|
||||
w := config.Wrap("/tmp/cfg", cfg)
|
||||
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(w, device1, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(cfg.Folders[0])
|
||||
m.ServeBackground()
|
||||
m.StartFolder("default")
|
||||
|
||||
fc := addFakeConn(m, device2)
|
||||
fc.folder = "default"
|
||||
|
||||
return m, fc
|
||||
}
|
||||
@@ -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,
|
||||
},
|
||||
|
||||
@@ -9,7 +9,6 @@ package model
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"math/rand"
|
||||
"os"
|
||||
"path/filepath"
|
||||
@@ -63,6 +62,7 @@ const (
|
||||
dbUpdateHandleFile
|
||||
dbUpdateDeleteFile
|
||||
dbUpdateShortcutFile
|
||||
dbUpdateHandleSymlink
|
||||
)
|
||||
|
||||
const (
|
||||
@@ -143,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
|
||||
}
|
||||
@@ -151,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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -382,23 +382,81 @@ func (f *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
|
||||
folderFiles := f.model.folderFiles[f.folderID]
|
||||
f.model.fmut.RUnlock()
|
||||
|
||||
// !!!
|
||||
// WithNeed takes a database snapshot (by necessity). By the time we've
|
||||
// handled a bunch of files it might have become out of date and we might
|
||||
// be attempting to sync with an old version of a file...
|
||||
// !!!
|
||||
|
||||
changed := 0
|
||||
var processDirectly []protocol.FileInfo
|
||||
|
||||
// Iterate the list of items that we need and sort them into piles.
|
||||
// Regular files to pull goes into the file queue, everything else
|
||||
// (directories, symlinks and deletes) goes into the "process directly"
|
||||
// pile.
|
||||
|
||||
folderFiles.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
|
||||
if shouldIgnore(intf, ignores, f.ignoreDelete, defTempNamer) {
|
||||
return true
|
||||
}
|
||||
|
||||
if err := fileValid(intf); err != nil {
|
||||
// The file isn't valid so we can't process it. Pretend that we
|
||||
// tried and set the error for the file.
|
||||
f.newError(intf.FileName(), err)
|
||||
changed++
|
||||
return true
|
||||
}
|
||||
|
||||
file := intf.(protocol.FileInfo)
|
||||
|
||||
switch {
|
||||
case file.IsDeleted():
|
||||
processDirectly = append(processDirectly, file)
|
||||
changed++
|
||||
|
||||
case file.Type == protocol.FileInfoTypeFile:
|
||||
// Queue files for processing after directories and symlinks, if
|
||||
// it has availability.
|
||||
|
||||
devices := folderFiles.Availability(file.Name)
|
||||
for _, dev := range devices {
|
||||
if f.model.ConnectedTo(dev) {
|
||||
f.queue.Push(file.Name, file.Size, file.ModTime())
|
||||
changed++
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
default:
|
||||
// Directories, symlinks
|
||||
processDirectly = append(processDirectly, file)
|
||||
changed++
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Sort the "process directly" pile by number of path components. This
|
||||
// ensures that we handle parents before children.
|
||||
|
||||
sort.Sort(byComponentCount(processDirectly))
|
||||
|
||||
// Process the list.
|
||||
|
||||
fileDeletions := map[string]protocol.FileInfo{}
|
||||
dirDeletions := []protocol.FileInfo{}
|
||||
buckets := map[string][]protocol.FileInfo{}
|
||||
|
||||
handleFile := func(fi protocol.FileInfo) bool {
|
||||
for _, fi := range processDirectly {
|
||||
// Verify that the thing we are handling lives inside a directory,
|
||||
// and not a symlink or empty space.
|
||||
if !osutil.IsDir(f.dir, filepath.Dir(fi.Name)) {
|
||||
f.newError(fi.Name, errNotDir)
|
||||
continue
|
||||
}
|
||||
|
||||
switch {
|
||||
case fi.IsDeleted():
|
||||
// A deleted file, directory or symlink
|
||||
if fi.IsDirectory() {
|
||||
// Perform directory deletions at the end, as we may have
|
||||
// files to delete inside them before we get to that point.
|
||||
dirDeletions = append(dirDeletions, fi)
|
||||
} else {
|
||||
fileDeletions[fi.Name] = fi
|
||||
@@ -413,55 +471,22 @@ func (f *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
|
||||
buckets[key] = append(buckets[key], df)
|
||||
}
|
||||
}
|
||||
|
||||
case fi.IsDirectory() && !fi.IsSymlink():
|
||||
// A new or changed directory
|
||||
l.Debugln("Creating directory", fi.Name)
|
||||
l.Debugln("Handling directory", fi.Name)
|
||||
f.handleDir(fi)
|
||||
|
||||
case fi.IsSymlink():
|
||||
l.Debugln("Handling symlink", fi.Name)
|
||||
f.handleSymlink(fi)
|
||||
|
||||
default:
|
||||
return false
|
||||
l.Warnln(fi)
|
||||
panic("unhandleable item type, can't happen")
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
folderFiles.WithNeed(protocol.LocalDeviceID, func(intf db.FileIntf) bool {
|
||||
// Needed items are delivered sorted lexicographically. We'll handle
|
||||
// directories as they come along, so parents before children. Files
|
||||
// are queued and the order may be changed later.
|
||||
|
||||
if shouldIgnore(intf, ignores, f.ignoreDelete) {
|
||||
return true
|
||||
}
|
||||
|
||||
if err := fileValid(intf); err != nil {
|
||||
// The file isn't valid so we can't process it. Pretend that we
|
||||
// tried and set the error for the file.
|
||||
f.newError(intf.FileName(), err)
|
||||
changed++
|
||||
return true
|
||||
}
|
||||
|
||||
file := intf.(protocol.FileInfo)
|
||||
l.Debugln(f, "handling", file.Name)
|
||||
if !handleFile(file) {
|
||||
// A new or changed file or symlink. This is the only case where
|
||||
// we do stuff concurrently in the background. We only queue
|
||||
// files where we are connected to at least one device that has
|
||||
// the file.
|
||||
|
||||
devices := folderFiles.Availability(file.Name)
|
||||
for _, dev := range devices {
|
||||
if f.model.ConnectedTo(dev) {
|
||||
f.queue.Push(file.Name, file.Size, file.ModTime())
|
||||
changed++
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
})
|
||||
|
||||
// Reorder the file queue according to configuration
|
||||
// Now do the file queue. Reorder it according to configuration.
|
||||
|
||||
switch f.order {
|
||||
case config.OrderRandom:
|
||||
@@ -478,7 +503,7 @@ func (f *rwFolder) pullerIteration(ignores *ignore.Matcher) int {
|
||||
f.queue.SortNewestFirst()
|
||||
}
|
||||
|
||||
// Process the file queue
|
||||
// Process the file queue.
|
||||
|
||||
nextFile:
|
||||
for {
|
||||
@@ -501,38 +526,45 @@ nextFile:
|
||||
continue
|
||||
}
|
||||
|
||||
// Handles races where an index update arrives changing what the file
|
||||
// is between queueing and retrieving it from the queue, effectively
|
||||
// changing how the file should be handled.
|
||||
if handleFile(fi) {
|
||||
if fi.IsDeleted() || fi.Type != protocol.FileInfoTypeFile {
|
||||
// The item has changed type or status in the index while we
|
||||
// were processing directories above.
|
||||
f.queue.Done(fileName)
|
||||
continue
|
||||
}
|
||||
|
||||
if !fi.IsSymlink() {
|
||||
key := string(fi.Blocks[0].Hash)
|
||||
for i, candidate := range buckets[key] {
|
||||
if scanner.BlocksEqual(candidate.Blocks, fi.Blocks) {
|
||||
// Remove the candidate from the bucket
|
||||
lidx := len(buckets[key]) - 1
|
||||
buckets[key][i] = buckets[key][lidx]
|
||||
buckets[key] = buckets[key][:lidx]
|
||||
// Verify that the thing we are handling lives inside a directory,
|
||||
// and not a symlink or empty space.
|
||||
if !osutil.IsDir(f.dir, filepath.Dir(fi.Name)) {
|
||||
f.newError(fi.Name, errNotDir)
|
||||
continue
|
||||
}
|
||||
|
||||
// candidate is our current state of the file, where as the
|
||||
// desired state with the delete bit set is in the deletion
|
||||
// map.
|
||||
desired := fileDeletions[candidate.Name]
|
||||
// Remove the pending deletion (as we perform it by renaming)
|
||||
delete(fileDeletions, candidate.Name)
|
||||
// Check our list of files to be removed for a match, in which case
|
||||
// we can just do a rename instead.
|
||||
key := string(fi.Blocks[0].Hash)
|
||||
for i, candidate := range buckets[key] {
|
||||
if scanner.BlocksEqual(candidate.Blocks, fi.Blocks) {
|
||||
// Remove the candidate from the bucket
|
||||
lidx := len(buckets[key]) - 1
|
||||
buckets[key][i] = buckets[key][lidx]
|
||||
buckets[key] = buckets[key][:lidx]
|
||||
|
||||
f.renameFile(desired, fi)
|
||||
// candidate is our current state of the file, where as the
|
||||
// desired state with the delete bit set is in the deletion
|
||||
// map.
|
||||
desired := fileDeletions[candidate.Name]
|
||||
// Remove the pending deletion (as we perform it by renaming)
|
||||
delete(fileDeletions, candidate.Name)
|
||||
|
||||
f.queue.Done(fileName)
|
||||
continue nextFile
|
||||
}
|
||||
f.renameFile(desired, fi)
|
||||
|
||||
f.queue.Done(fileName)
|
||||
continue nextFile
|
||||
}
|
||||
}
|
||||
|
||||
// Not a rename or a symlink, deal with it.
|
||||
// Handle the file normally, by coping and pulling, etc.
|
||||
f.handleFile(fi, copyChan, finisherChan)
|
||||
}
|
||||
|
||||
@@ -569,7 +601,10 @@ nextFile:
|
||||
|
||||
// handleDir creates or updates the given directory
|
||||
func (f *rwFolder) handleDir(file protocol.FileInfo) {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
|
||||
events.Default.Log(events.ItemStarted, map[string]string{
|
||||
"folder": f.folderID,
|
||||
"item": file.Name,
|
||||
@@ -587,7 +622,11 @@ func (f *rwFolder) handleDir(file protocol.FileInfo) {
|
||||
})
|
||||
}()
|
||||
|
||||
realName := filepath.Join(f.dir, file.Name)
|
||||
realName, err := rootedJoinedPath(f.dir, file.Name)
|
||||
if err != nil {
|
||||
f.newError(file.Name, err)
|
||||
return
|
||||
}
|
||||
mode := os.FileMode(file.Permissions & 0777)
|
||||
if f.ignorePermissions(file) {
|
||||
mode = 0777
|
||||
@@ -662,15 +701,93 @@ func (f *rwFolder) handleDir(file protocol.FileInfo) {
|
||||
}
|
||||
}
|
||||
|
||||
// handleSymlink creates or updates the given symlink
|
||||
func (f *rwFolder) handleSymlink(file protocol.FileInfo) {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
|
||||
events.Default.Log(events.ItemStarted, map[string]string{
|
||||
"folder": f.folderID,
|
||||
"item": file.Name,
|
||||
"type": "symlink",
|
||||
"action": "update",
|
||||
})
|
||||
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": f.folderID,
|
||||
"item": file.Name,
|
||||
"error": events.Error(err),
|
||||
"type": "symlink",
|
||||
"action": "update",
|
||||
})
|
||||
}()
|
||||
|
||||
realName, err := rootedJoinedPath(f.dir, file.Name)
|
||||
if err != nil {
|
||||
f.newError(file.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
if shouldDebug() {
|
||||
curFile, _ := f.model.CurrentFolderFile(f.folderID, file.Name)
|
||||
l.Debugf("need symlink\n\t%v\n\t%v", file, curFile)
|
||||
}
|
||||
|
||||
if len(file.SymlinkTarget) == 0 {
|
||||
// Index entry from a Syncthing predating the support for including
|
||||
// the link target in the index entry. We log this as an error.
|
||||
err = errors.New("incompatible symlink entry; rescan with newer Syncthing on source")
|
||||
l.Infof("Puller (folder %q, dir %q): %v", f.folderID, file.Name, err)
|
||||
f.newError(file.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
if _, err = f.mtimeFS.Lstat(realName); err == nil {
|
||||
// There is already something under that name. Remove it to replace
|
||||
// with the symlink. This also handles the "change symlink type"
|
||||
// path.
|
||||
err = osutil.InWritableDir(os.Remove, realName)
|
||||
if err != nil {
|
||||
l.Infof("Puller (folder %q, dir %q): %v", f.folderID, file.Name, err)
|
||||
f.newError(file.Name, err)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
tt := symlinks.TargetFile
|
||||
if file.IsDirectory() {
|
||||
tt = symlinks.TargetDirectory
|
||||
}
|
||||
|
||||
// We declare a function that acts on only the path name, so
|
||||
// we can pass it to InWritableDir.
|
||||
createLink := func(path string) error {
|
||||
return symlinks.Create(path, file.SymlinkTarget, tt)
|
||||
}
|
||||
|
||||
if err = osutil.InWritableDir(createLink, realName); err == nil {
|
||||
f.dbUpdates <- dbUpdateJob{file, dbUpdateHandleSymlink}
|
||||
} else {
|
||||
l.Infof("Puller (folder %q, dir %q): %v", f.folderID, file.Name, err)
|
||||
f.newError(file.Name, err)
|
||||
}
|
||||
}
|
||||
|
||||
// deleteDir attempts to delete the given directory
|
||||
func (f *rwFolder) deleteDir(file protocol.FileInfo, matcher *ignore.Matcher) {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
|
||||
events.Default.Log(events.ItemStarted, map[string]string{
|
||||
"folder": f.folderID,
|
||||
"item": file.Name,
|
||||
"type": "dir",
|
||||
"action": "delete",
|
||||
})
|
||||
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": f.folderID,
|
||||
@@ -681,7 +798,12 @@ func (f *rwFolder) deleteDir(file protocol.FileInfo, matcher *ignore.Matcher) {
|
||||
})
|
||||
}()
|
||||
|
||||
realName := filepath.Join(f.dir, file.Name)
|
||||
realName, err := rootedJoinedPath(f.dir, file.Name)
|
||||
if err != nil {
|
||||
f.newError(file.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
// Delete any temporary files lying around in the directory
|
||||
dir, _ := os.Open(realName)
|
||||
if dir != nil {
|
||||
@@ -713,13 +835,17 @@ func (f *rwFolder) deleteDir(file protocol.FileInfo, matcher *ignore.Matcher) {
|
||||
|
||||
// deleteFile attempts to delete the given file
|
||||
func (f *rwFolder) deleteFile(file protocol.FileInfo) {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
|
||||
events.Default.Log(events.ItemStarted, map[string]string{
|
||||
"folder": f.folderID,
|
||||
"item": file.Name,
|
||||
"type": "file",
|
||||
"action": "delete",
|
||||
})
|
||||
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": f.folderID,
|
||||
@@ -730,7 +856,11 @@ func (f *rwFolder) deleteFile(file protocol.FileInfo) {
|
||||
})
|
||||
}()
|
||||
|
||||
realName := filepath.Join(f.dir, file.Name)
|
||||
realName, err := rootedJoinedPath(f.dir, file.Name)
|
||||
if err != nil {
|
||||
f.newError(file.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
cur, ok := f.model.CurrentFolderFile(f.folderID, file.Name)
|
||||
if ok && f.inConflict(cur.Version, file.Version) {
|
||||
@@ -763,7 +893,10 @@ func (f *rwFolder) deleteFile(file protocol.FileInfo) {
|
||||
// renameFile attempts to rename an existing file to a destination
|
||||
// and set the right attributes on it.
|
||||
func (f *rwFolder) renameFile(source, target protocol.FileInfo) {
|
||||
// Used in the defer closure below, updated by the function body. Take
|
||||
// care not declare another err.
|
||||
var err error
|
||||
|
||||
events.Default.Log(events.ItemStarted, map[string]string{
|
||||
"folder": f.folderID,
|
||||
"item": source.Name,
|
||||
@@ -776,6 +909,7 @@ func (f *rwFolder) renameFile(source, target protocol.FileInfo) {
|
||||
"type": "file",
|
||||
"action": "update",
|
||||
})
|
||||
|
||||
defer func() {
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": f.folderID,
|
||||
@@ -795,8 +929,16 @@ func (f *rwFolder) renameFile(source, target protocol.FileInfo) {
|
||||
|
||||
l.Debugln(f, "taking rename shortcut", source.Name, "->", target.Name)
|
||||
|
||||
from := filepath.Join(f.dir, source.Name)
|
||||
to := filepath.Join(f.dir, target.Name)
|
||||
from, err := rootedJoinedPath(f.dir, source.Name)
|
||||
if err != nil {
|
||||
f.newError(source.Name, err)
|
||||
return
|
||||
}
|
||||
to, err := rootedJoinedPath(f.dir, target.Name)
|
||||
if err != nil {
|
||||
f.newError(target.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
if f.versioner != nil {
|
||||
err = osutil.Copy(from, to)
|
||||
@@ -892,13 +1034,7 @@ func (f *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
|
||||
f.queue.Done(file.Name)
|
||||
|
||||
var err error
|
||||
if file.IsSymlink() {
|
||||
err = f.shortcutSymlink(file)
|
||||
} else {
|
||||
err = f.shortcutFile(file)
|
||||
}
|
||||
|
||||
err := f.shortcutFile(file)
|
||||
events.Default.Log(events.ItemFinished, map[string]interface{}{
|
||||
"folder": f.folderID,
|
||||
"item": file.Name,
|
||||
@@ -918,8 +1054,16 @@ func (f *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
}
|
||||
|
||||
// Figure out the absolute filenames we need once and for all
|
||||
tempName := filepath.Join(f.dir, defTempNamer.TempName(file.Name))
|
||||
realName := filepath.Join(f.dir, file.Name)
|
||||
tempName, err := rootedJoinedPath(f.dir, defTempNamer.TempName(file.Name))
|
||||
if err != nil {
|
||||
f.newError(file.Name, err)
|
||||
return
|
||||
}
|
||||
realName, err := rootedJoinedPath(f.dir, file.Name)
|
||||
if err != nil {
|
||||
f.newError(file.Name, err)
|
||||
return
|
||||
}
|
||||
|
||||
if hasCurFile && !curFile.IsDirectory() && !curFile.IsSymlink() {
|
||||
// Check that the file on disk is what we expect it to be according to
|
||||
@@ -1037,7 +1181,11 @@ func (f *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
// shortcutFile sets file mode and modification time, when that's the only
|
||||
// thing that has changed.
|
||||
func (f *rwFolder) shortcutFile(file protocol.FileInfo) error {
|
||||
realName := filepath.Join(f.dir, file.Name)
|
||||
realName, err := rootedJoinedPath(f.dir, file.Name)
|
||||
if err != nil {
|
||||
f.newError(file.Name, err)
|
||||
return err
|
||||
}
|
||||
if !f.ignorePermissions(file) {
|
||||
if err := os.Chmod(realName, os.FileMode(file.Permissions&0777)); err != nil {
|
||||
l.Infof("Puller (folder %q, file %q): shortcut: chmod: %v", f.folderID, file.Name, err)
|
||||
@@ -1057,20 +1205,6 @@ func (f *rwFolder) shortcutFile(file protocol.FileInfo) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
// shortcutSymlink changes the symlinks type if necessary.
|
||||
func (f *rwFolder) shortcutSymlink(file protocol.FileInfo) (err error) {
|
||||
tt := symlinks.TargetFile
|
||||
if file.IsDirectory() {
|
||||
tt = symlinks.TargetDirectory
|
||||
}
|
||||
err = symlinks.ChangeType(filepath.Join(f.dir, file.Name), tt)
|
||||
if err != nil {
|
||||
l.Infof("Puller (folder %q, file %q): symlink shortcut: %v", f.folderID, file.Name, err)
|
||||
f.newError(file.Name, err)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// copierRoutine reads copierStates until the in channel closes and performs
|
||||
// the relevant copies when possible, or passes it to the puller routine.
|
||||
func (f *rwFolder) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pullBlockState, out chan<- *sharedPullerState) {
|
||||
@@ -1112,7 +1246,11 @@ func (f *rwFolder) copierRoutine(in <-chan copyBlocksState, pullChan chan<- pull
|
||||
|
||||
buf = buf[:int(block.Size)]
|
||||
found := f.model.finder.Iterate(folders, block.Hash, func(folder, file string, index int32) bool {
|
||||
fd, err := os.Open(filepath.Join(folderRoots[folder], file))
|
||||
inFile, err := rootedJoinedPath(folderRoots[folder], file)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
fd, err := os.Open(inFile)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
@@ -1296,27 +1434,6 @@ func (f *rwFolder) performFinish(state *sharedPullerState) error {
|
||||
// Set the correct timestamp on the new file
|
||||
f.mtimeFS.Chtimes(state.realName, state.file.ModTime(), state.file.ModTime()) // never fails
|
||||
|
||||
// If it's a symlink, the target of the symlink is inside the file.
|
||||
if state.file.IsSymlink() {
|
||||
content, err := ioutil.ReadFile(state.realName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove the file, and replace it with a symlink.
|
||||
err = osutil.InWritableDir(func(path string) error {
|
||||
os.Remove(path)
|
||||
tt := symlinks.TargetFile
|
||||
if state.file.IsDirectory() {
|
||||
tt = symlinks.TargetDirectory
|
||||
}
|
||||
return symlinks.Create(path, string(content), tt)
|
||||
}, state.realName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// Record the updated file in the index
|
||||
f.dbUpdates <- dbUpdateJob{state.file, dbUpdateHandleFile}
|
||||
return nil
|
||||
@@ -1405,17 +1522,14 @@ func (f *rwFolder) dbUpdaterRoutine() {
|
||||
// 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))
|
||||
}
|
||||
changedFiles = append(changedFiles, filepath.Join(f.dir, job.file.Name))
|
||||
case dbUpdateHandleDir:
|
||||
changedDirs = append(changedDirs, filepath.Join(f.dir, job.file.Name))
|
||||
case dbUpdateHandleSymlink:
|
||||
// fsyncing symlinks is only supported by MacOS, ignore
|
||||
}
|
||||
if job.jobType != dbUpdateShortcutFile {
|
||||
changedDirs = append(changedDirs,
|
||||
filepath.Dir(filepath.Join(f.dir, job.file.Name)))
|
||||
changedDirs = append(changedDirs, filepath.Dir(filepath.Join(f.dir, job.file.Name)))
|
||||
}
|
||||
}
|
||||
if job.file.IsInvalid() || (job.file.IsDirectory() && !job.file.IsSymlink()) {
|
||||
@@ -1638,3 +1752,29 @@ func windowsInvalidFilename(name string) bool {
|
||||
// The path must not contain any disallowed characters
|
||||
return strings.ContainsAny(name, windowsDisallowedCharacters)
|
||||
}
|
||||
|
||||
// byComponentCount sorts by the number of path components in Name, that is
|
||||
// "x/y" sorts before "foo/bar/baz".
|
||||
type byComponentCount []protocol.FileInfo
|
||||
|
||||
func (l byComponentCount) Len() int {
|
||||
return len(l)
|
||||
}
|
||||
|
||||
func (l byComponentCount) Less(a, b int) bool {
|
||||
return componentCount(l[a].Name) < componentCount(l[b].Name)
|
||||
}
|
||||
|
||||
func (l byComponentCount) Swap(a, b int) {
|
||||
l[a], l[b] = l[b], l[a]
|
||||
}
|
||||
|
||||
func componentCount(name string) int {
|
||||
count := 0
|
||||
for _, codepoint := range name {
|
||||
if codepoint == os.PathSeparator {
|
||||
count++
|
||||
}
|
||||
}
|
||||
return count
|
||||
}
|
||||
|
||||
@@ -19,8 +19,10 @@ import (
|
||||
)
|
||||
|
||||
func init() {
|
||||
// We do this to make sure that the temp file required for the tests does
|
||||
// not get removed during the tests.
|
||||
// We do this to make sure that the temp file required for the tests
|
||||
// does not get removed during the tests. Also set the prefix so it's
|
||||
// found correctly regardless of platform.
|
||||
defTempNamer.prefix = windowsTempPrefix
|
||||
future := time.Now().Add(time.Hour)
|
||||
err := os.Chtimes(filepath.Join("testdata", defTempNamer.TempName("file")), future, future)
|
||||
if err != nil {
|
||||
|
||||
@@ -15,26 +15,41 @@ import (
|
||||
)
|
||||
|
||||
type tempNamer struct {
|
||||
prefix string
|
||||
prefix string
|
||||
recognize []string
|
||||
}
|
||||
|
||||
const (
|
||||
windowsTempPrefix = "~syncthing~"
|
||||
unixTempPrefix = ".syncthing."
|
||||
)
|
||||
|
||||
var defTempNamer tempNamer
|
||||
|
||||
// Real filesystems usually handle 255 bytes. encfs has varying and
|
||||
// confusing file name limits. We take a safe way out and switch to hashing
|
||||
// quite early.
|
||||
const maxFilenameLength = 160 - len(".syncthing.") - len(".tmp")
|
||||
const maxFilenameLength = 160 - len(unixTempPrefix) - len(".tmp")
|
||||
|
||||
func init() {
|
||||
if runtime.GOOS == "windows" {
|
||||
defTempNamer = tempNamer{"~syncthing~"}
|
||||
defTempNamer = tempNamer{windowsTempPrefix, []string{unixTempPrefix, windowsTempPrefix}}
|
||||
} else {
|
||||
defTempNamer = tempNamer{".syncthing."}
|
||||
defTempNamer = tempNamer{unixTempPrefix, []string{unixTempPrefix, windowsTempPrefix}}
|
||||
}
|
||||
}
|
||||
|
||||
// IsTemporary is true if the file name has the temporary prefix. Regardless
|
||||
// of the normally used prefix, the standard Windows and Unix temp prefixes
|
||||
// are always recognized as temp files.
|
||||
func (t tempNamer) IsTemporary(name string) bool {
|
||||
return strings.HasPrefix(filepath.Base(name), t.prefix)
|
||||
name = filepath.Base(name)
|
||||
for _, prefix := range t.recognize {
|
||||
if strings.HasPrefix(name, prefix) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func (t tempNamer) TempName(name string) string {
|
||||
|
||||
BIN
lib/model/testdata/.syncthing.file.tmp
vendored
BIN
lib/model/testdata/.syncthing.file.tmp
vendored
Binary file not shown.
@@ -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,
|
||||
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
49
lib/osutil/isdir.go
Normal file
49
lib/osutil/isdir.go
Normal file
@@ -0,0 +1,49 @@
|
||||
// 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"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// IsDir returns true if base and every path component of name up to and
|
||||
// including filepath.Join(base, name) is a directory (and not a symlink or
|
||||
// similar). Base and name must both be clean and name must be relative to
|
||||
// base.
|
||||
func IsDir(base, name string) bool {
|
||||
path := base
|
||||
info, err := Lstat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return false
|
||||
}
|
||||
|
||||
if name == "." {
|
||||
// The result of calling IsDir("some/where", filepath.Dir("foo"))
|
||||
return true
|
||||
}
|
||||
|
||||
parts := strings.Split(name, string(os.PathSeparator))
|
||||
for _, part := range parts {
|
||||
path = filepath.Join(path, part)
|
||||
info, err := Lstat(path)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
if info.Mode()&os.ModeSymlink != 0 {
|
||||
return false
|
||||
}
|
||||
if !info.IsDir() {
|
||||
return false
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
75
lib/osutil/isdir_test.go
Normal file
75
lib/osutil/isdir_test.go
Normal file
@@ -0,0 +1,75 @@
|
||||
// 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_test
|
||||
|
||||
import (
|
||||
"os"
|
||||
"testing"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/symlinks"
|
||||
)
|
||||
|
||||
func TestIsDir(t *testing.T) {
|
||||
if !symlinks.Supported {
|
||||
t.Skip("pointless test")
|
||||
return
|
||||
}
|
||||
|
||||
os.RemoveAll("testdata")
|
||||
defer os.RemoveAll("testdata")
|
||||
os.MkdirAll("testdata/a/b/c", 0755)
|
||||
symlinks.Create("testdata/a/l", "b", symlinks.TargetDirectory)
|
||||
|
||||
// a/l -> b, so a/l/c should resolve by normal stat
|
||||
info, err := osutil.Lstat("testdata/a/l/c")
|
||||
if err != nil {
|
||||
t.Fatal("unexpected error", err)
|
||||
}
|
||||
if !info.IsDir() {
|
||||
t.Fatal("error in setup, a/l/c should be a directory")
|
||||
}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
isDir bool
|
||||
}{
|
||||
// Exist
|
||||
{".", true},
|
||||
{"a", true},
|
||||
{"a/b", true},
|
||||
{"a/b/c", true},
|
||||
// Don't exist
|
||||
{"x", false},
|
||||
{"a/x", false},
|
||||
{"a/b/x", false},
|
||||
{"a/x/c", false},
|
||||
// Symlink or behind symlink
|
||||
{"a/l", false},
|
||||
{"a/l/c", false},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
if res := osutil.IsDir("testdata", tc.name); res != tc.isDir {
|
||||
t.Errorf("IsDir(%q) = %v, should be %v", tc.name, res, tc.isDir)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
var isDirResult bool
|
||||
|
||||
func BenchmarkIsDir(b *testing.B) {
|
||||
os.RemoveAll("testdata")
|
||||
defer os.RemoveAll("testdata")
|
||||
os.MkdirAll("testdata/a/b/c", 0755)
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
isDirResult = osutil.IsDir("testdata", "a/b/c")
|
||||
}
|
||||
|
||||
b.ReportAllocs()
|
||||
}
|
||||
@@ -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
|
||||
|
||||
@@ -303,7 +303,8 @@ type FileInfo struct {
|
||||
NoPermissions bool `protobuf:"varint,8,opt,name=no_permissions,json=noPermissions,proto3" json:"no_permissions,omitempty"`
|
||||
Version Vector `protobuf:"bytes,9,opt,name=version" json:"version"`
|
||||
Sequence int64 `protobuf:"varint,10,opt,name=sequence,proto3" json:"sequence,omitempty"`
|
||||
Blocks []BlockInfo `protobuf:"bytes,16,rep,name=Blocks,json=blocks" json:"Blocks"`
|
||||
Blocks []BlockInfo `protobuf:"bytes,16,rep,name=Blocks" json:"Blocks"`
|
||||
SymlinkTarget string `protobuf:"bytes,17,opt,name=symlink_target,json=symlinkTarget,proto3" json:"symlink_target,omitempty"`
|
||||
}
|
||||
|
||||
func (m *FileInfo) Reset() { *m = FileInfo{} }
|
||||
@@ -870,6 +871,14 @@ func (m *FileInfo) MarshalTo(data []byte) (int, error) {
|
||||
i += n
|
||||
}
|
||||
}
|
||||
if len(m.SymlinkTarget) > 0 {
|
||||
data[i] = 0x8a
|
||||
i++
|
||||
data[i] = 0x1
|
||||
i++
|
||||
i = encodeVarintBep(data, i, uint64(len(m.SymlinkTarget)))
|
||||
i += copy(data[i:], m.SymlinkTarget)
|
||||
}
|
||||
return i, nil
|
||||
}
|
||||
|
||||
@@ -1394,6 +1403,10 @@ func (m *FileInfo) ProtoSize() (n int) {
|
||||
n += 2 + l + sovBep(uint64(l))
|
||||
}
|
||||
}
|
||||
l = len(m.SymlinkTarget)
|
||||
if l > 0 {
|
||||
n += 2 + l + sovBep(uint64(l))
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
@@ -2850,6 +2863,35 @@ func (m *FileInfo) Unmarshal(data []byte) error {
|
||||
return err
|
||||
}
|
||||
iNdEx = postIndex
|
||||
case 17:
|
||||
if wireType != 2 {
|
||||
return fmt.Errorf("proto: wrong wireType = %d for field SymlinkTarget", wireType)
|
||||
}
|
||||
var stringLen uint64
|
||||
for shift := uint(0); ; shift += 7 {
|
||||
if shift >= 64 {
|
||||
return ErrIntOverflowBep
|
||||
}
|
||||
if iNdEx >= l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
b := data[iNdEx]
|
||||
iNdEx++
|
||||
stringLen |= (uint64(b) & 0x7F) << shift
|
||||
if b < 0x80 {
|
||||
break
|
||||
}
|
||||
}
|
||||
intStringLen := int(stringLen)
|
||||
if intStringLen < 0 {
|
||||
return ErrInvalidLengthBep
|
||||
}
|
||||
postIndex := iNdEx + intStringLen
|
||||
if postIndex > l {
|
||||
return io.ErrUnexpectedEOF
|
||||
}
|
||||
m.SymlinkTarget = string(data[iNdEx:postIndex])
|
||||
iNdEx = postIndex
|
||||
default:
|
||||
iNdEx = preIndex
|
||||
skippy, err := skipBep(data[iNdEx:])
|
||||
@@ -3987,110 +4029,108 @@ var (
|
||||
)
|
||||
|
||||
var fileDescriptorBep = []byte{
|
||||
// 1665 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xac, 0x56, 0x4f, 0x73, 0xdb, 0xc6,
|
||||
0x15, 0x17, 0x48, 0xf0, 0xdf, 0x23, 0xa5, 0x40, 0x6b, 0x59, 0x41, 0x61, 0x85, 0x42, 0xe0, 0xb8,
|
||||
0x55, 0x34, 0x8d, 0xe2, 0xc6, 0x69, 0x33, 0xd3, 0x69, 0x3b, 0x43, 0x91, 0x90, 0xcc, 0x09, 0x0d,
|
||||
0x32, 0x4b, 0xca, 0xae, 0x73, 0x28, 0x06, 0x24, 0x96, 0x14, 0xc6, 0x20, 0x96, 0x05, 0x40, 0xd9,
|
||||
0xea, 0x47, 0x60, 0xbf, 0x40, 0x2f, 0x9c, 0xc9, 0xf4, 0xd6, 0x7b, 0x3f, 0x84, 0x8f, 0x99, 0x1c,
|
||||
0x7b, 0xf0, 0x34, 0xea, 0xa5, 0xc7, 0x5e, 0x7a, 0xef, 0x60, 0x17, 0x00, 0x41, 0xfd, 0xe9, 0xe4,
|
||||
0x90, 0x13, 0x77, 0xdf, 0xfb, 0xed, 0xdb, 0x7d, 0xbf, 0xf7, 0x7e, 0x8f, 0x80, 0xca, 0x90, 0xcc,
|
||||
0x8e, 0x66, 0x3e, 0x0d, 0x29, 0x2a, 0xb3, 0x9f, 0x11, 0x75, 0x95, 0x4f, 0x26, 0x4e, 0x78, 0x3e,
|
||||
0x1f, 0x1e, 0x8d, 0xe8, 0xf4, 0xd3, 0x09, 0x9d, 0xd0, 0x4f, 0x99, 0x67, 0x38, 0x1f, 0xb3, 0x1d,
|
||||
0xdb, 0xb0, 0x15, 0x3f, 0xa8, 0xcd, 0xa0, 0xf0, 0x94, 0xb8, 0x2e, 0x45, 0xfb, 0x50, 0xb5, 0xc9,
|
||||
0x85, 0x33, 0x22, 0xa6, 0x67, 0x4d, 0x89, 0x2c, 0xa8, 0xc2, 0x41, 0x05, 0x03, 0x37, 0x19, 0xd6,
|
||||
0x94, 0x44, 0x80, 0x91, 0xeb, 0x10, 0x2f, 0xe4, 0x80, 0x1c, 0x07, 0x70, 0x13, 0x03, 0x3c, 0x82,
|
||||
0xad, 0x18, 0x70, 0x41, 0xfc, 0xc0, 0xa1, 0x9e, 0x9c, 0x67, 0x98, 0x4d, 0x6e, 0x7d, 0xce, 0x8d,
|
||||
0x5a, 0x00, 0xc5, 0xa7, 0xc4, 0xb2, 0x89, 0x8f, 0x3e, 0x06, 0x31, 0xbc, 0x9c, 0xf1, 0xbb, 0xb6,
|
||||
0x3e, 0xbb, 0x7f, 0x94, 0xe4, 0x70, 0xf4, 0x8c, 0x04, 0x81, 0x35, 0x21, 0x83, 0xcb, 0x19, 0xc1,
|
||||
0x0c, 0x82, 0x7e, 0x07, 0xd5, 0x11, 0x9d, 0xce, 0x7c, 0x12, 0xb0, 0xc0, 0x39, 0x76, 0x62, 0xef,
|
||||
0xc6, 0x89, 0xe6, 0x0a, 0x83, 0xb3, 0x07, 0xb4, 0x06, 0x6c, 0x36, 0xdd, 0x79, 0x10, 0x12, 0xbf,
|
||||
0x49, 0xbd, 0xb1, 0x33, 0x41, 0x8f, 0xa1, 0x34, 0xa6, 0xae, 0x4d, 0xfc, 0x40, 0x16, 0xd4, 0xfc,
|
||||
0x41, 0xf5, 0x33, 0x69, 0x15, 0xec, 0x84, 0x39, 0x8e, 0xc5, 0xb7, 0xef, 0xf6, 0x37, 0x70, 0x02,
|
||||
0xd3, 0xfe, 0x9c, 0x83, 0x22, 0xf7, 0xa0, 0x5d, 0xc8, 0x39, 0x36, 0xa7, 0xe8, 0xb8, 0x78, 0xf5,
|
||||
0x6e, 0x3f, 0xd7, 0x6e, 0xe1, 0x9c, 0x63, 0xa3, 0x1d, 0x28, 0xb8, 0xd6, 0x90, 0xb8, 0x31, 0x39,
|
||||
0x7c, 0x83, 0x1e, 0x40, 0xc5, 0x27, 0x96, 0x6d, 0x52, 0xcf, 0xbd, 0x64, 0x94, 0x94, 0x71, 0x39,
|
||||
0x32, 0x74, 0x3d, 0xf7, 0x12, 0x7d, 0x02, 0xc8, 0x99, 0x78, 0xd4, 0x27, 0xe6, 0x8c, 0xf8, 0x53,
|
||||
0x87, 0xbd, 0x36, 0x90, 0x45, 0x86, 0xda, 0xe6, 0x9e, 0xde, 0xca, 0x81, 0x1e, 0xc2, 0x66, 0x0c,
|
||||
0xb7, 0x89, 0x4b, 0x42, 0x22, 0x17, 0x18, 0xb2, 0xc6, 0x8d, 0x2d, 0x66, 0x43, 0x8f, 0x61, 0xc7,
|
||||
0x76, 0x02, 0x6b, 0xe8, 0x12, 0x33, 0x24, 0xd3, 0x99, 0xe9, 0x78, 0x36, 0x79, 0x43, 0x02, 0xb9,
|
||||
0xc8, 0xb0, 0x28, 0xf6, 0x0d, 0xc8, 0x74, 0xd6, 0xe6, 0x9e, 0x88, 0x0d, 0x5e, 0xe9, 0x40, 0x96,
|
||||
0xae, 0xb3, 0xd1, 0x62, 0x8e, 0x84, 0x8d, 0x18, 0xa6, 0xfd, 0x27, 0x07, 0x45, 0xee, 0x41, 0x3f,
|
||||
0x4d, 0xd9, 0xa8, 0x1d, 0xef, 0x46, 0xa8, 0x7f, 0xbc, 0xdb, 0x2f, 0x73, 0x5f, 0xbb, 0x95, 0x61,
|
||||
0x07, 0x81, 0x98, 0xe9, 0x1c, 0xb6, 0x46, 0x7b, 0x50, 0xb1, 0x6c, 0x3b, 0xaa, 0x12, 0x09, 0xe4,
|
||||
0xbc, 0x9a, 0x3f, 0xa8, 0xe0, 0x95, 0x01, 0x7d, 0xb1, 0x5e, 0x75, 0xf1, 0x7a, 0x9f, 0xdc, 0x55,
|
||||
0xee, 0x88, 0xf2, 0x11, 0xf1, 0xe3, 0x4e, 0x2d, 0xb0, 0xfb, 0xca, 0x91, 0x81, 0xf5, 0xe9, 0x87,
|
||||
0x50, 0x9b, 0x5a, 0x6f, 0xcc, 0x80, 0xfc, 0x71, 0x4e, 0xbc, 0x11, 0x61, 0xb4, 0xe4, 0x71, 0x75,
|
||||
0x6a, 0xbd, 0xe9, 0xc7, 0x26, 0x54, 0x07, 0x70, 0xbc, 0xd0, 0xa7, 0xf6, 0x7c, 0x44, 0x7c, 0xb9,
|
||||
0xc4, 0x78, 0xcb, 0x58, 0xd0, 0x2f, 0xa1, 0xcc, 0x48, 0x35, 0x1d, 0x5b, 0x2e, 0xab, 0xc2, 0x81,
|
||||
0x78, 0xac, 0xc4, 0x89, 0x97, 0x18, 0xa5, 0x2c, 0xef, 0x64, 0x89, 0x4b, 0x0c, 0xdb, 0xb6, 0xd1,
|
||||
0x6f, 0x40, 0x09, 0x5e, 0x39, 0x51, 0x41, 0x78, 0xa4, 0xd0, 0xa1, 0x9e, 0xe9, 0x93, 0x29, 0xbd,
|
||||
0xb0, 0xdc, 0x40, 0xae, 0xb0, 0x6b, 0xe4, 0x08, 0xd1, 0xce, 0x00, 0x70, 0xec, 0xd7, 0xba, 0x50,
|
||||
0x60, 0x11, 0xd1, 0x2e, 0x14, 0x79, 0x53, 0xc6, 0x2a, 0x8d, 0x77, 0xe8, 0x08, 0x0a, 0x63, 0xc7,
|
||||
0x25, 0x81, 0x9c, 0x63, 0x35, 0x44, 0x99, 0x8e, 0x76, 0x5c, 0xd2, 0xf6, 0xc6, 0x34, 0xae, 0x22,
|
||||
0x87, 0x69, 0x67, 0x50, 0x65, 0x01, 0xcf, 0x66, 0xb6, 0x15, 0x92, 0x1f, 0x2d, 0xec, 0x5f, 0xf3,
|
||||
0x50, 0x4e, 0x3c, 0x69, 0xd1, 0x85, 0x4c, 0xd1, 0x0f, 0x63, 0xdd, 0x73, 0x15, 0xef, 0xde, 0x8c,
|
||||
0x97, 0x11, 0x3e, 0x02, 0x31, 0x70, 0xfe, 0x44, 0x98, 0x6e, 0xf2, 0x98, 0xad, 0x91, 0x0a, 0xd5,
|
||||
0xeb, 0x62, 0xd9, 0xc4, 0x59, 0x13, 0xfa, 0x00, 0x60, 0x4a, 0x6d, 0x67, 0xec, 0x10, 0xdb, 0x0c,
|
||||
0x58, 0x03, 0xe4, 0x71, 0x25, 0xb1, 0xf4, 0x91, 0x1c, 0xb5, 0x7b, 0x24, 0x15, 0x3b, 0xd6, 0x44,
|
||||
0xb2, 0x8d, 0x3c, 0x8e, 0x77, 0x61, 0xb9, 0x8e, 0x1d, 0x57, 0x3d, 0xd9, 0x46, 0xd3, 0xcd, 0xa3,
|
||||
0x6b, 0x22, 0x2d, 0x33, 0xc0, 0xa6, 0x47, 0xb3, 0x02, 0x7d, 0x0c, 0xa5, 0x64, 0xfa, 0x45, 0xf5,
|
||||
0x5c, 0x53, 0xd2, 0x73, 0x32, 0x0a, 0x69, 0x3a, 0x57, 0x62, 0x18, 0x52, 0xa0, 0x9c, 0xb6, 0x22,
|
||||
0xb0, 0x97, 0xa6, 0xfb, 0x68, 0xe6, 0xa6, 0x79, 0x78, 0x81, 0x5c, 0x55, 0x85, 0x83, 0x02, 0x4e,
|
||||
0x53, 0x33, 0x02, 0xf4, 0x0b, 0x28, 0x1e, 0xbb, 0x74, 0xf4, 0x2a, 0xd1, 0xed, 0xbd, 0xd5, 0x6d,
|
||||
0xcc, 0x9e, 0xa9, 0x4e, 0x71, 0xc8, 0x80, 0xbf, 0x16, 0xff, 0xf2, 0xcd, 0xfe, 0x86, 0xf6, 0x15,
|
||||
0x54, 0x52, 0x40, 0x54, 0x79, 0x3a, 0x1e, 0x07, 0x24, 0x64, 0x65, 0xca, 0xe3, 0x78, 0x97, 0x92,
|
||||
0x9f, 0x63, 0xf7, 0x72, 0xf2, 0x11, 0x88, 0xe7, 0x56, 0x70, 0xce, 0x0a, 0x52, 0xc3, 0x6c, 0x1d,
|
||||
0x87, 0xfc, 0x2d, 0x14, 0x79, 0x86, 0xe8, 0x09, 0x94, 0x47, 0x74, 0xee, 0x85, 0xab, 0xe9, 0xba,
|
||||
0x9d, 0x15, 0x2d, 0xf3, 0xc4, 0xaf, 0x4a, 0x81, 0xda, 0x09, 0x94, 0x62, 0x17, 0x7a, 0x94, 0x4e,
|
||||
0x14, 0xf1, 0xf8, 0x7e, 0x22, 0xac, 0xfe, 0x39, 0xf5, 0xc3, 0xb5, 0x81, 0xb2, 0x03, 0x85, 0x0b,
|
||||
0xcb, 0x9d, 0xf3, 0xf7, 0x89, 0x98, 0x6f, 0xb4, 0xbf, 0x0b, 0x50, 0xc2, 0x11, 0x81, 0x41, 0x98,
|
||||
0x19, 0xd4, 0x85, 0xb5, 0x41, 0xbd, 0x6a, 0xf5, 0xdc, 0x5a, 0xab, 0x27, 0xdd, 0x9a, 0xcf, 0x74,
|
||||
0xeb, 0x8a, 0x1c, 0xf1, 0x56, 0x72, 0x0a, 0xb7, 0x90, 0x53, 0x5c, 0x91, 0x13, 0x35, 0xce, 0xd8,
|
||||
0xa7, 0x53, 0x36, 0x8a, 0xa9, 0x6f, 0xf9, 0x97, 0x71, 0x67, 0x6d, 0x46, 0xd6, 0x41, 0x62, 0xd4,
|
||||
0x4c, 0x28, 0x63, 0x12, 0xcc, 0xa8, 0x17, 0x90, 0x3b, 0x9f, 0x8d, 0x40, 0xb4, 0xad, 0xd0, 0x62,
|
||||
0x8f, 0xae, 0x61, 0xb6, 0x46, 0x3f, 0x03, 0x71, 0x44, 0x6d, 0xfe, 0xe4, 0xad, 0x6c, 0xfd, 0x75,
|
||||
0xdf, 0xa7, 0x7e, 0x93, 0xda, 0x04, 0x33, 0x80, 0x36, 0x03, 0xa9, 0x45, 0x5f, 0x7b, 0x2e, 0xb5,
|
||||
0xec, 0x9e, 0x4f, 0x27, 0xd1, 0xa8, 0xbc, 0x53, 0xf2, 0x2d, 0x28, 0xcd, 0xd9, 0x50, 0x48, 0x44,
|
||||
0xff, 0xd1, 0xba, 0x48, 0xaf, 0x07, 0xe2, 0x13, 0x24, 0xe9, 0xec, 0xf8, 0xa8, 0xf6, 0x9d, 0x00,
|
||||
0xca, 0xdd, 0x68, 0xd4, 0x86, 0x2a, 0x47, 0x9a, 0x99, 0xaf, 0x80, 0x83, 0x1f, 0x72, 0x11, 0x9b,
|
||||
0x0f, 0x30, 0x4f, 0xd7, 0xb7, 0xfe, 0xb5, 0x64, 0x94, 0x98, 0xff, 0x61, 0x4a, 0x7c, 0x08, 0x9b,
|
||||
0x4c, 0x23, 0xe9, 0x1f, 0xa6, 0xa8, 0xe6, 0x0f, 0x0a, 0xb8, 0x36, 0xe4, 0x42, 0x61, 0x36, 0xad,
|
||||
0x08, 0x62, 0xcf, 0xf1, 0x26, 0xda, 0x3e, 0x14, 0x9a, 0x2e, 0x65, 0xc5, 0x2a, 0xfa, 0xc4, 0x0a,
|
||||
0xa8, 0x97, 0x70, 0xc8, 0x77, 0x87, 0xdf, 0xe5, 0xa0, 0x9a, 0xf9, 0x90, 0x41, 0x8f, 0x61, 0xab,
|
||||
0xd9, 0x39, 0xeb, 0x0f, 0x74, 0x6c, 0x36, 0xbb, 0xc6, 0x49, 0xfb, 0x54, 0xda, 0x50, 0xf6, 0x16,
|
||||
0x4b, 0x55, 0x9e, 0xae, 0x40, 0xeb, 0xdf, 0x28, 0xfb, 0x50, 0x68, 0x1b, 0x2d, 0xfd, 0xf7, 0x92,
|
||||
0xa0, 0xec, 0x2c, 0x96, 0xaa, 0x94, 0x01, 0xf2, 0x3f, 0x82, 0x9f, 0x43, 0x8d, 0x01, 0xcc, 0xb3,
|
||||
0x5e, 0xab, 0x31, 0xd0, 0xa5, 0x9c, 0xa2, 0x2c, 0x96, 0xea, 0xee, 0x75, 0x5c, 0xcc, 0xf7, 0x43,
|
||||
0x28, 0x61, 0xfd, 0xab, 0x33, 0xbd, 0x3f, 0x90, 0xf2, 0xca, 0xee, 0x62, 0xa9, 0xa2, 0x0c, 0x30,
|
||||
0x51, 0xcc, 0x23, 0x28, 0x63, 0xbd, 0xdf, 0xeb, 0x1a, 0x7d, 0x5d, 0x12, 0x95, 0xf7, 0x17, 0x4b,
|
||||
0xf5, 0xde, 0x1a, 0x2a, 0xee, 0xd0, 0x5f, 0xc1, 0x76, 0xab, 0xfb, 0xc2, 0xe8, 0x74, 0x1b, 0x2d,
|
||||
0xb3, 0x87, 0xbb, 0xa7, 0x58, 0xef, 0xf7, 0xa5, 0x82, 0xb2, 0xbf, 0x58, 0xaa, 0x0f, 0x32, 0xf8,
|
||||
0x1b, 0x0d, 0xf7, 0x01, 0x88, 0xbd, 0xb6, 0x71, 0x2a, 0x15, 0x95, 0x7b, 0x8b, 0xa5, 0xfa, 0x5e,
|
||||
0x06, 0x1a, 0x91, 0x1a, 0x65, 0xdc, 0xec, 0x74, 0xfb, 0xba, 0x54, 0xba, 0x91, 0x31, 0x23, 0xfb,
|
||||
0xf0, 0x0f, 0x80, 0x6e, 0x7e, 0xea, 0xa1, 0x8f, 0x40, 0x34, 0xba, 0x86, 0x2e, 0x6d, 0xf0, 0xfc,
|
||||
0x6f, 0x22, 0x0c, 0xea, 0x11, 0xa4, 0x41, 0xbe, 0xf3, 0xf5, 0xe7, 0x92, 0xa0, 0xfc, 0x64, 0xb1,
|
||||
0x54, 0xef, 0xdf, 0x04, 0x75, 0xbe, 0xfe, 0xfc, 0x90, 0x42, 0x35, 0x1b, 0x58, 0x83, 0xf2, 0x33,
|
||||
0x7d, 0xd0, 0x68, 0x35, 0x06, 0x0d, 0x69, 0x83, 0x3f, 0x29, 0x71, 0x3f, 0x23, 0xa1, 0xc5, 0x04,
|
||||
0xb8, 0x07, 0x05, 0x43, 0x7f, 0xae, 0x63, 0x49, 0x50, 0xb6, 0x17, 0x4b, 0x75, 0x33, 0x01, 0x18,
|
||||
0xe4, 0x82, 0xf8, 0xa8, 0x0e, 0xc5, 0x46, 0xe7, 0x45, 0xe3, 0x65, 0x5f, 0xca, 0x29, 0x68, 0xb1,
|
||||
0x54, 0xb7, 0x12, 0x77, 0xc3, 0x7d, 0x6d, 0x5d, 0x06, 0x87, 0xff, 0x15, 0xa0, 0x96, 0xfd, 0xdb,
|
||||
0x43, 0x75, 0x10, 0x4f, 0xda, 0x1d, 0x3d, 0xb9, 0x2e, 0xeb, 0x8b, 0xd6, 0xe8, 0x00, 0x2a, 0xad,
|
||||
0x36, 0xd6, 0x9b, 0x83, 0x2e, 0x7e, 0x99, 0xe4, 0x92, 0x05, 0xb5, 0x1c, 0x9f, 0x35, 0x77, 0xf4,
|
||||
0x69, 0x59, 0xeb, 0xbf, 0x7c, 0xd6, 0x69, 0x1b, 0x5f, 0x9a, 0x2c, 0x62, 0x4e, 0x79, 0xb0, 0x58,
|
||||
0xaa, 0xef, 0x67, 0xc1, 0xfd, 0xcb, 0xa9, 0xeb, 0x78, 0xaf, 0x58, 0xe0, 0x2f, 0x60, 0x3b, 0x81,
|
||||
0xaf, 0x2e, 0xc8, 0x2b, 0xea, 0x62, 0xa9, 0xee, 0xdd, 0x72, 0x66, 0x75, 0xcf, 0x13, 0x78, 0x2f,
|
||||
0x39, 0x78, 0x66, 0x7c, 0x69, 0x74, 0x5f, 0x18, 0x92, 0xa8, 0xd4, 0x17, 0x4b, 0x55, 0xb9, 0xe5,
|
||||
0xd8, 0x99, 0xf7, 0xca, 0xa3, 0xaf, 0xbd, 0xc3, 0xbf, 0x09, 0x50, 0x49, 0x27, 0x54, 0xc4, 0xb3,
|
||||
0xd1, 0x35, 0x75, 0x8c, 0xbb, 0x38, 0x49, 0x3c, 0x75, 0x1a, 0x94, 0x2d, 0xd1, 0x87, 0x50, 0x3a,
|
||||
0xd5, 0x0d, 0x1d, 0xb7, 0x9b, 0x89, 0x1e, 0x52, 0xc8, 0x29, 0xf1, 0x88, 0xef, 0x8c, 0xd0, 0xc7,
|
||||
0x50, 0x33, 0xba, 0x66, 0xff, 0xac, 0xf9, 0x34, 0xc9, 0x98, 0x35, 0x70, 0x26, 0x54, 0x7f, 0x3e,
|
||||
0x3a, 0x67, 0xd9, 0x1e, 0x46, 0xd2, 0x79, 0xde, 0xe8, 0xb4, 0x5b, 0x1c, 0x9a, 0x57, 0xe4, 0xc5,
|
||||
0x52, 0xdd, 0x49, 0xa1, 0x6d, 0xfe, 0xb7, 0x1f, 0x61, 0x0f, 0x6d, 0xa8, 0xff, 0xff, 0x59, 0x84,
|
||||
0x54, 0x28, 0x36, 0x7a, 0x3d, 0xdd, 0x68, 0x25, 0xaf, 0x5f, 0xf9, 0x1a, 0xb3, 0x19, 0xf1, 0xec,
|
||||
0x08, 0x71, 0xd2, 0xc5, 0xa7, 0xfa, 0x20, 0x79, 0xfc, 0x0a, 0x71, 0x42, 0xfd, 0x09, 0x09, 0x8f,
|
||||
0xf7, 0xde, 0x7e, 0x5f, 0xdf, 0xf8, 0xf6, 0xfb, 0xfa, 0xc6, 0xdb, 0xab, 0xba, 0xf0, 0xed, 0x55,
|
||||
0x5d, 0xf8, 0xe7, 0x55, 0x7d, 0xe3, 0xdf, 0x57, 0x75, 0xe1, 0x9b, 0x7f, 0xd5, 0x85, 0x61, 0x91,
|
||||
0xcd, 0xae, 0x27, 0xff, 0x0b, 0x00, 0x00, 0xff, 0xff, 0x64, 0x17, 0x1e, 0x19, 0xf4, 0x0d, 0x00,
|
||||
0x00,
|
||||
// 1645 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xac, 0x57, 0x4f, 0x73, 0xdb, 0x5a,
|
||||
0x15, 0x8f, 0x6d, 0xf9, 0xdf, 0xb5, 0x93, 0xe7, 0xdc, 0xa6, 0x79, 0x46, 0xcd, 0x4b, 0x82, 0xde,
|
||||
0x2b, 0x04, 0x0f, 0x4d, 0xa1, 0x05, 0x3a, 0xc3, 0x00, 0x33, 0x8e, 0xad, 0xa4, 0x9a, 0x3a, 0xb2,
|
||||
0x2b, 0xdb, 0x29, 0x65, 0x81, 0x46, 0xb6, 0xae, 0x1d, 0x4d, 0x64, 0x5d, 0x23, 0xc9, 0x6d, 0xc3,
|
||||
0x47, 0x80, 0x2f, 0xc0, 0x86, 0x99, 0x6e, 0xd9, 0xf3, 0x21, 0xca, 0xae, 0xd3, 0x25, 0x8b, 0x0e,
|
||||
0x94, 0x0d, 0x4b, 0x36, 0xec, 0x39, 0xf7, 0x5e, 0x49, 0x96, 0xf3, 0x87, 0xe9, 0xe2, 0x2d, 0x32,
|
||||
0xd6, 0x3d, 0xe7, 0x77, 0xcf, 0xb9, 0xe7, 0x77, 0xfe, 0xdc, 0x1b, 0x54, 0x1e, 0x91, 0xf9, 0xe1,
|
||||
0xdc, 0xa7, 0x21, 0xc5, 0x25, 0xfe, 0x33, 0xa6, 0xae, 0xfc, 0x60, 0xea, 0x84, 0xe7, 0x8b, 0xd1,
|
||||
0xe1, 0x98, 0xce, 0x1e, 0x4e, 0xe9, 0x94, 0x3e, 0xe4, 0x9a, 0xd1, 0x62, 0xc2, 0x57, 0x7c, 0xc1,
|
||||
0xbf, 0xc4, 0x46, 0x65, 0x8e, 0xf2, 0x4f, 0x89, 0xeb, 0x52, 0xbc, 0x87, 0x2a, 0x36, 0x79, 0xe5,
|
||||
0x8c, 0x89, 0xe9, 0x59, 0x33, 0x52, 0xcf, 0xec, 0x67, 0x0e, 0xca, 0x06, 0x12, 0x22, 0x1d, 0x24,
|
||||
0x0c, 0x30, 0x76, 0x1d, 0xe2, 0x85, 0x02, 0x90, 0x15, 0x00, 0x21, 0xe2, 0x80, 0xfb, 0x68, 0x23,
|
||||
0x02, 0xbc, 0x22, 0x7e, 0xe0, 0x50, 0xaf, 0x9e, 0xe3, 0x98, 0x75, 0x21, 0x3d, 0x13, 0x42, 0x25,
|
||||
0x40, 0x85, 0xa7, 0xc4, 0xb2, 0x89, 0x8f, 0x7f, 0x80, 0xa4, 0xf0, 0x72, 0x2e, 0x7c, 0x6d, 0x3c,
|
||||
0xba, 0x7b, 0x18, 0xc7, 0x70, 0x78, 0x4a, 0x82, 0xc0, 0x9a, 0x92, 0x01, 0x28, 0x0d, 0x0e, 0xc1,
|
||||
0xbf, 0x02, 0xe7, 0x74, 0x36, 0xf7, 0x41, 0xc1, 0x0c, 0x67, 0xf9, 0x8e, 0x9d, 0x6b, 0x3b, 0x5a,
|
||||
0x4b, 0x8c, 0x91, 0xde, 0xa0, 0x34, 0xd1, 0x7a, 0xcb, 0x5d, 0x04, 0x21, 0xf1, 0x5b, 0xd4, 0x9b,
|
||||
0x38, 0x53, 0xfc, 0x23, 0x54, 0x9c, 0x50, 0x17, 0x4e, 0x11, 0x80, 0xfb, 0xdc, 0x41, 0xe5, 0x51,
|
||||
0x6d, 0x69, 0xec, 0x98, 0x2b, 0x8e, 0xa4, 0x77, 0x1f, 0xf7, 0xd6, 0x8c, 0x18, 0xa6, 0xfc, 0x31,
|
||||
0x8b, 0x0a, 0x42, 0x83, 0xb7, 0x51, 0xd6, 0xb1, 0x05, 0x45, 0x47, 0x85, 0x4f, 0x1f, 0xf7, 0xb2,
|
||||
0x5a, 0xdb, 0x00, 0x09, 0xde, 0x42, 0x79, 0xd7, 0x1a, 0x11, 0x37, 0x22, 0x47, 0x2c, 0xf0, 0x3d,
|
||||
0x54, 0xf6, 0x21, 0x60, 0x93, 0x7a, 0xee, 0x25, 0xa7, 0xa4, 0x64, 0x94, 0x98, 0xa0, 0x0b, 0x6b,
|
||||
0xfc, 0x00, 0x61, 0x67, 0xea, 0x51, 0x9f, 0x98, 0x73, 0xe2, 0xcf, 0x1c, 0x7e, 0xda, 0xa0, 0x2e,
|
||||
0x71, 0xd4, 0xa6, 0xd0, 0xf4, 0x96, 0x0a, 0xfc, 0x35, 0x5a, 0x8f, 0xe0, 0x36, 0x71, 0x49, 0x48,
|
||||
0xea, 0x79, 0x8e, 0xac, 0x0a, 0x61, 0x9b, 0xcb, 0x20, 0xb6, 0x2d, 0xdb, 0x09, 0xac, 0x91, 0x4b,
|
||||
0xcc, 0x90, 0xcc, 0xe6, 0xa6, 0xe3, 0xd9, 0xe4, 0x0d, 0x09, 0xea, 0x05, 0x8e, 0xc5, 0x91, 0x6e,
|
||||
0x00, 0x2a, 0x4d, 0x68, 0x18, 0x1b, 0x22, 0xd3, 0x41, 0xbd, 0x76, 0x95, 0x8d, 0x36, 0x57, 0xc4,
|
||||
0x6c, 0x44, 0x30, 0xe5, 0x3f, 0xc0, 0x86, 0xd0, 0xe0, 0xef, 0x25, 0x6c, 0x54, 0x8f, 0xb6, 0x19,
|
||||
0xea, 0xef, 0x1f, 0xf7, 0x4a, 0x42, 0xa7, 0xb5, 0x53, 0xec, 0x60, 0x24, 0xa5, 0x2a, 0x87, 0x7f,
|
||||
0xe3, 0x1d, 0x54, 0xb6, 0x6c, 0x9b, 0x65, 0x09, 0x5c, 0xe7, 0xc0, 0x75, 0xd9, 0x58, 0x0a, 0xf0,
|
||||
0x93, 0xd5, 0xac, 0x4b, 0x57, 0xeb, 0xe4, 0xb6, 0x74, 0x33, 0xca, 0xc7, 0xc4, 0x8f, 0x2a, 0x35,
|
||||
0xcf, 0xfd, 0x95, 0x98, 0x80, 0xd7, 0xe9, 0x77, 0x51, 0x75, 0x66, 0xbd, 0x31, 0x03, 0xf2, 0xbb,
|
||||
0x05, 0xf1, 0xc6, 0x84, 0xd3, 0x92, 0x33, 0x2a, 0x20, 0xeb, 0x47, 0x22, 0xbc, 0x8b, 0x90, 0xe3,
|
||||
0x85, 0x3e, 0xb5, 0x17, 0xb0, 0xab, 0x5e, 0xe4, 0xbc, 0xa5, 0x24, 0xf8, 0xa7, 0xa8, 0xc4, 0x49,
|
||||
0x35, 0x21, 0xf0, 0x12, 0x68, 0xa5, 0x23, 0x39, 0x0a, 0xbc, 0xc8, 0x29, 0xe5, 0x71, 0xc7, 0x9f,
|
||||
0x46, 0x91, 0x63, 0x35, 0x1b, 0xff, 0x02, 0xc9, 0xc1, 0x85, 0xc3, 0x12, 0x22, 0x2c, 0x85, 0x70,
|
||||
0x56, 0xd3, 0x27, 0x33, 0xfa, 0xca, 0x72, 0x83, 0x7a, 0x99, 0xbb, 0xa9, 0x33, 0x84, 0x96, 0x02,
|
||||
0x18, 0x91, 0x5e, 0xe9, 0xa2, 0x3c, 0xb7, 0x08, 0xe5, 0x57, 0x10, 0x45, 0x19, 0x75, 0x69, 0xb4,
|
||||
0xc2, 0x87, 0x28, 0x3f, 0x71, 0x5c, 0x20, 0x32, 0xcb, 0x73, 0x88, 0x53, 0x15, 0x0d, 0x62, 0xcd,
|
||||
0x9b, 0xd0, 0x28, 0x8b, 0x02, 0xa6, 0x0c, 0x51, 0x85, 0x1b, 0x1c, 0xce, 0x6d, 0x0b, 0xca, 0xe6,
|
||||
0xdb, 0x32, 0xfb, 0xb7, 0x1c, 0x2a, 0xc5, 0x9a, 0x24, 0xe9, 0x99, 0x54, 0xd2, 0x1b, 0x51, 0xdf,
|
||||
0x8b, 0x2e, 0xde, 0xbe, 0x6e, 0x2f, 0xd5, 0xf8, 0xb0, 0x3f, 0x70, 0x7e, 0x4f, 0x78, 0xdf, 0xe4,
|
||||
0x0c, 0xfe, 0x8d, 0xf7, 0x51, 0xe5, 0x6a, 0xb3, 0xac, 0x1b, 0x69, 0x11, 0xfe, 0x0a, 0xa1, 0x19,
|
||||
0xb5, 0x9d, 0x89, 0x43, 0x6c, 0x33, 0xe0, 0x05, 0x90, 0x33, 0xca, 0xb1, 0xa4, 0x8f, 0xeb, 0xac,
|
||||
0xdc, 0x59, 0xab, 0xd8, 0x51, 0x4f, 0xc4, 0x4b, 0xa6, 0x71, 0x3c, 0x60, 0x1b, 0xf2, 0x2a, 0xb2,
|
||||
0x1e, 0x2f, 0xd9, 0x74, 0xf3, 0xe8, 0x4a, 0x93, 0x96, 0x38, 0x60, 0xdd, 0xa3, 0xe9, 0x06, 0x85,
|
||||
0x4e, 0x8a, 0xa7, 0x1f, 0xcb, 0xe7, 0x4a, 0x27, 0x9d, 0x91, 0x71, 0x48, 0x93, 0xb9, 0x12, 0xc1,
|
||||
0xb0, 0x8c, 0x4a, 0x49, 0x29, 0x22, 0x7e, 0xd2, 0x64, 0xcd, 0x66, 0x6e, 0x12, 0x07, 0x78, 0xac,
|
||||
0x80, 0x3a, 0x6f, 0x24, 0xa1, 0xe9, 0x01, 0xfe, 0x31, 0x2a, 0x1c, 0xb9, 0x74, 0x7c, 0x11, 0xf7,
|
||||
0xed, 0x9d, 0xa5, 0x37, 0x2e, 0x4f, 0x65, 0x27, 0x02, 0xb2, 0x40, 0x82, 0xcb, 0x99, 0xeb, 0x78,
|
||||
0x17, 0x66, 0x68, 0xf9, 0x53, 0x12, 0xd6, 0x37, 0xc5, 0x98, 0x8e, 0xa4, 0x03, 0x2e, 0xfc, 0xb9,
|
||||
0xf4, 0xa7, 0xb7, 0x7b, 0x6b, 0xca, 0x73, 0x54, 0x4e, 0xec, 0xb0, 0x02, 0xa1, 0x93, 0x49, 0x00,
|
||||
0x3b, 0x32, 0xfc, 0x9c, 0xd1, 0x2a, 0xc9, 0x51, 0x96, 0x1f, 0x4f, 0xe4, 0x08, 0x64, 0xe7, 0x56,
|
||||
0x70, 0xce, 0xf3, 0x56, 0x35, 0xf8, 0x77, 0x64, 0xf2, 0x97, 0xa8, 0x20, 0x88, 0xc0, 0x8f, 0x51,
|
||||
0x69, 0x4c, 0x17, 0x5e, 0xb8, 0x1c, 0xc2, 0x9b, 0xe9, 0xde, 0xe6, 0x9a, 0xe8, 0xf0, 0x09, 0x50,
|
||||
0x39, 0x46, 0xc5, 0x48, 0x05, 0x91, 0xc4, 0x83, 0x47, 0x3a, 0xba, 0x1b, 0xf7, 0x5f, 0xff, 0x9c,
|
||||
0xfa, 0xe1, 0xca, 0xdc, 0x81, 0xa9, 0x0c, 0x29, 0x5c, 0x88, 0xf3, 0x49, 0x86, 0x58, 0x28, 0x7f,
|
||||
0xcd, 0xa0, 0xa2, 0xc1, 0x78, 0x0e, 0xc2, 0xd4, 0x3c, 0xcf, 0xaf, 0xcc, 0xf3, 0x65, 0x47, 0x64,
|
||||
0x57, 0x3a, 0x22, 0x2e, 0xea, 0x5c, 0xaa, 0xa8, 0x97, 0xe4, 0x48, 0x37, 0x92, 0x93, 0xbf, 0x81,
|
||||
0x9c, 0xc2, 0x92, 0x1c, 0x96, 0x96, 0x89, 0x4f, 0x67, 0x7c, 0x62, 0x53, 0xdf, 0xf2, 0x2f, 0xa3,
|
||||
0x02, 0x5c, 0x67, 0xd2, 0x41, 0x2c, 0x54, 0x4c, 0x54, 0x32, 0x48, 0x30, 0x87, 0x52, 0x23, 0xb7,
|
||||
0x1e, 0x1b, 0xcc, 0x43, 0x43, 0x5b, 0xfc, 0xd0, 0x60, 0x9e, 0x7d, 0xe3, 0xef, 0x23, 0x69, 0x4c,
|
||||
0x6d, 0x71, 0xe4, 0x8d, 0x74, 0x99, 0xa8, 0xbe, 0x4f, 0xe1, 0x52, 0xb4, 0xa1, 0xe1, 0x18, 0x00,
|
||||
0x1e, 0x04, 0xb5, 0x36, 0x7d, 0xed, 0xb9, 0xd4, 0xb2, 0x7b, 0x3e, 0x9d, 0xb2, 0x89, 0x7a, 0xeb,
|
||||
0x64, 0x68, 0xa3, 0xe2, 0x82, 0xcf, 0x8e, 0x78, 0x36, 0x7c, 0xb3, 0xda, 0xcb, 0x57, 0x0d, 0x89,
|
||||
0x41, 0x13, 0x37, 0x40, 0xb4, 0x55, 0xf9, 0x90, 0x41, 0xf2, 0xed, 0x68, 0xac, 0xa1, 0x8a, 0x40,
|
||||
0x9a, 0xa9, 0xc7, 0xc2, 0xc1, 0xe7, 0x38, 0xe2, 0x63, 0x04, 0x2d, 0x92, 0xef, 0x1b, 0x6f, 0xa0,
|
||||
0x54, 0xc3, 0xe6, 0x3e, 0xaf, 0x61, 0xe1, 0x0e, 0x1e, 0xb1, 0x9e, 0x48, 0xee, 0x55, 0x09, 0x62,
|
||||
0xcf, 0x1b, 0xd5, 0x91, 0x68, 0x14, 0x2e, 0x53, 0x0a, 0x48, 0xea, 0x39, 0xde, 0x54, 0xd9, 0x43,
|
||||
0xf9, 0x96, 0x4b, 0x79, 0xb2, 0x0a, 0x70, 0xe9, 0x07, 0xe0, 0x26, 0xe2, 0x50, 0xac, 0x1a, 0x1f,
|
||||
0xb2, 0xa8, 0x92, 0x7a, 0xef, 0xc0, 0x79, 0x36, 0x5a, 0x9d, 0x61, 0x7f, 0xa0, 0x1a, 0x66, 0xab,
|
||||
0xab, 0x1f, 0x6b, 0x27, 0xb5, 0x35, 0x79, 0xe7, 0x0f, 0x7f, 0xde, 0xaf, 0xcf, 0x96, 0xa0, 0xd5,
|
||||
0xa7, 0x0c, 0xb8, 0xd0, 0xf4, 0xb6, 0xfa, 0xeb, 0x5a, 0x46, 0xde, 0x02, 0x60, 0x2d, 0x05, 0x14,
|
||||
0xf7, 0xc5, 0x0f, 0x51, 0x95, 0x03, 0xcc, 0x61, 0xaf, 0xdd, 0x1c, 0xa8, 0xb5, 0xac, 0x2c, 0x03,
|
||||
0x6e, 0xfb, 0x2a, 0x2e, 0xe2, 0xfb, 0x6b, 0xe8, 0x0b, 0xf5, 0xf9, 0x50, 0xed, 0x0f, 0x6a, 0x39,
|
||||
0x79, 0x1b, 0x80, 0x38, 0x05, 0x8c, 0x3b, 0xe6, 0x3e, 0x94, 0xa1, 0xda, 0xef, 0x75, 0xf5, 0xbe,
|
||||
0x5a, 0x93, 0xe4, 0x2f, 0x01, 0x75, 0x67, 0x05, 0x15, 0x55, 0xe8, 0xcf, 0xd0, 0x66, 0xbb, 0xfb,
|
||||
0x42, 0xef, 0x74, 0x9b, 0x6d, 0xb3, 0x67, 0x74, 0x4f, 0x60, 0x4f, 0xbf, 0x96, 0x97, 0xf7, 0x00,
|
||||
0x7f, 0x2f, 0x85, 0xbf, 0x56, 0x70, 0x5f, 0x01, 0x7b, 0x9a, 0x7e, 0x52, 0x2b, 0xc8, 0x77, 0x00,
|
||||
0xfa, 0x45, 0x0a, 0xca, 0x48, 0x65, 0x11, 0xb7, 0x3a, 0x5d, 0x70, 0x5d, 0xbc, 0x16, 0x31, 0x27,
|
||||
0xbb, 0xf1, 0x5b, 0x84, 0xaf, 0xbf, 0x08, 0xf1, 0x37, 0x48, 0xd2, 0xbb, 0xba, 0x0a, 0x84, 0xf2,
|
||||
0xf8, 0xaf, 0x23, 0x74, 0xea, 0x11, 0xac, 0xa0, 0x5c, 0xe7, 0x37, 0x3f, 0x01, 0x32, 0xbf, 0x03,
|
||||
0xa0, 0xbb, 0xd7, 0x41, 0xa0, 0x6c, 0x50, 0x54, 0x49, 0x1b, 0x56, 0x50, 0xe9, 0x54, 0x1d, 0x34,
|
||||
0x81, 0xdc, 0x26, 0x18, 0xe7, 0x47, 0x8a, 0xd5, 0xa7, 0x24, 0xb4, 0x78, 0x03, 0xee, 0xa0, 0xbc,
|
||||
0xae, 0x9e, 0xa9, 0x06, 0x18, 0xde, 0x04, 0xc0, 0x7a, 0x0c, 0xd0, 0x09, 0xd4, 0x15, 0x3c, 0x38,
|
||||
0x0a, 0xcd, 0xce, 0x8b, 0xe6, 0xcb, 0x3e, 0x24, 0x07, 0x83, 0x7a, 0x23, 0x56, 0x37, 0xdd, 0xd7,
|
||||
0xd6, 0x65, 0xd0, 0xf8, 0x6f, 0x06, 0x55, 0xd3, 0xb7, 0x23, 0x6c, 0x90, 0x8e, 0xb5, 0x8e, 0x1a,
|
||||
0xbb, 0x4b, 0xeb, 0xd8, 0x37, 0x3e, 0x40, 0xe5, 0xb6, 0x66, 0xa8, 0xad, 0x41, 0xd7, 0x78, 0x19,
|
||||
0xc7, 0x92, 0x06, 0xb5, 0x1d, 0x9f, 0x17, 0x37, 0x7b, 0x81, 0x56, 0xfb, 0x2f, 0x4f, 0x3b, 0x9a,
|
||||
0xfe, 0xcc, 0xe4, 0x16, 0xb3, 0xf2, 0x3d, 0x00, 0x7f, 0x99, 0x06, 0xf7, 0xc5, 0xcd, 0xc0, 0x0d,
|
||||
0x3f, 0x41, 0x9b, 0x31, 0x7c, 0xe9, 0x20, 0x27, 0xef, 0xc3, 0x9e, 0x9d, 0x1b, 0xf6, 0x2c, 0xfd,
|
||||
0x3c, 0x46, 0x5f, 0xc4, 0x1b, 0x87, 0xfa, 0x33, 0x1d, 0xca, 0x02, 0x2a, 0x67, 0x17, 0xb6, 0xc9,
|
||||
0x37, 0x6c, 0x1b, 0x7a, 0x17, 0x1e, 0x14, 0x45, 0xe3, 0x2f, 0x19, 0x54, 0x4e, 0x26, 0x14, 0xe3,
|
||||
0x59, 0xef, 0x9a, 0xaa, 0x61, 0x74, 0x8d, 0x38, 0xf0, 0x44, 0xa9, 0x53, 0xfe, 0x09, 0xaf, 0xbb,
|
||||
0xe2, 0x89, 0xaa, 0xab, 0x86, 0xd6, 0x8a, 0xfb, 0x21, 0x81, 0x9c, 0x10, 0x8f, 0xf8, 0xce, 0x18,
|
||||
0xfe, 0xef, 0xa8, 0x82, 0x99, 0xfe, 0xb0, 0xf5, 0x34, 0x8e, 0x98, 0x17, 0x70, 0xca, 0x54, 0x7f,
|
||||
0x31, 0x3e, 0xe7, 0xd1, 0x36, 0x58, 0xeb, 0x9c, 0x35, 0x3b, 0x5a, 0x5b, 0x40, 0x73, 0x72, 0x1d,
|
||||
0xa0, 0x5b, 0x09, 0x54, 0x13, 0xaf, 0x03, 0x86, 0x6d, 0xd8, 0x68, 0xf7, 0xff, 0xcf, 0x22, 0x78,
|
||||
0xb8, 0x14, 0x9a, 0xbd, 0x9e, 0xaa, 0xb7, 0xe3, 0xd3, 0x2f, 0x75, 0xcd, 0xf9, 0x9c, 0x78, 0x36,
|
||||
0x43, 0x1c, 0x77, 0x8d, 0x13, 0x75, 0x10, 0x1f, 0x7e, 0x89, 0x38, 0xa6, 0xec, 0x5e, 0x3e, 0xda,
|
||||
0x79, 0xf7, 0xcf, 0xdd, 0xb5, 0xf7, 0xf0, 0xf7, 0xee, 0xd3, 0x6e, 0xe6, 0x3d, 0xfc, 0xfd, 0xe3,
|
||||
0xd3, 0xee, 0xda, 0xbf, 0xe1, 0xf7, 0xed, 0xbf, 0x76, 0x33, 0xa3, 0x02, 0x9f, 0x5d, 0x8f, 0xff,
|
||||
0x17, 0x00, 0x00, 0xff, 0xff, 0x20, 0x74, 0xd7, 0x8f, 0x1b, 0x0e, 0x00, 0x00,
|
||||
}
|
||||
|
||||
@@ -106,7 +106,8 @@ message FileInfo {
|
||||
Vector version = 9 [(gogoproto.nullable) = false];
|
||||
int64 sequence = 10;
|
||||
|
||||
repeated BlockInfo Blocks = 16 [(gogoproto.nullable) = false];
|
||||
repeated BlockInfo Blocks = 16 [(gogoproto.nullable) = false];
|
||||
string symlink_target = 17;
|
||||
}
|
||||
|
||||
enum FileInfoType {
|
||||
|
||||
@@ -30,8 +30,19 @@ func (m Hello) Magic() uint32 {
|
||||
}
|
||||
|
||||
func (f FileInfo) String() string {
|
||||
return fmt.Sprintf("File{Name:%q, Type:%v, Sequence:%d, Permissions:0%o, ModTime:%v, Version:%v, Length:%d, Deleted:%v, Invalid:%v, NoPermissions:%v, Blocks:%v}",
|
||||
f.Name, f.Type, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.Size, f.Deleted, f.Invalid, f.NoPermissions, f.Blocks)
|
||||
switch f.Type {
|
||||
case FileInfoTypeDirectory:
|
||||
return fmt.Sprintf("Directory{Name:%q, Sequence:%d, Permissions:0%o, ModTime:%v, Version:%v, Deleted:%v, Invalid:%v, NoPermissions:%v}",
|
||||
f.Name, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.Deleted, f.Invalid, f.NoPermissions)
|
||||
case FileInfoTypeFile:
|
||||
return fmt.Sprintf("File{Name:%q, Sequence:%d, Permissions:0%o, ModTime:%v, Version:%v, Length:%d, Deleted:%v, Invalid:%v, NoPermissions:%v, Blocks:%v}",
|
||||
f.Name, f.Sequence, f.Permissions, f.ModTime(), f.Version, f.Size, f.Deleted, f.Invalid, f.NoPermissions, f.Blocks)
|
||||
case FileInfoTypeSymlinkDirectory, FileInfoTypeSymlinkFile, FileInfoTypeSymlinkUnknown:
|
||||
return fmt.Sprintf("Symlink{Name:%q, Type:%v, Sequence:%d, Version:%v, Deleted:%v, Invalid:%v, NoPermissions:%v, SymlinkTarget:%q}",
|
||||
f.Name, f.Type, f.Sequence, f.Version, f.Deleted, f.Invalid, f.NoPermissions, f.SymlinkTarget)
|
||||
default:
|
||||
panic("mystery file type detected")
|
||||
}
|
||||
}
|
||||
|
||||
func (f FileInfo) IsDeleted() bool {
|
||||
@@ -63,7 +74,7 @@ func (f FileInfo) FileSize() int64 {
|
||||
if f.Deleted {
|
||||
return 0
|
||||
}
|
||||
if f.IsDirectory() {
|
||||
if f.IsDirectory() || f.IsSymlink() {
|
||||
return SyntheticDirectorySize
|
||||
}
|
||||
return f.Size
|
||||
|
||||
@@ -427,16 +427,16 @@ var (
|
||||
)
|
||||
|
||||
var fileDescriptorDeviceidTest = []byte{
|
||||
// 176 bytes of a gzipped FileDescriptorProto
|
||||
// 171 bytes of a gzipped FileDescriptorProto
|
||||
0x1f, 0x8b, 0x08, 0x00, 0x00, 0x09, 0x6e, 0x88, 0x02, 0xff, 0xe2, 0x12, 0x4e, 0x49, 0x2d, 0xcb,
|
||||
0x4c, 0x4e, 0xcd, 0x4c, 0x89, 0x2f, 0x49, 0x2d, 0x2e, 0xd1, 0x2b, 0x28, 0xca, 0x2f, 0xc9, 0x17,
|
||||
0xe2, 0x00, 0x53, 0xc9, 0xf9, 0x39, 0x52, 0xba, 0xe9, 0x99, 0x25, 0x19, 0xa5, 0x49, 0x7a, 0xc9,
|
||||
0xf9, 0xb9, 0xfa, 0xe9, 0xf9, 0xe9, 0xf9, 0xfa, 0x60, 0x99, 0xa4, 0xd2, 0x34, 0x30, 0x0f, 0xcc,
|
||||
0x01, 0xb3, 0x20, 0x1a, 0x95, 0x54, 0xb9, 0xf8, 0x43, 0x52, 0x8b, 0x4b, 0xfc, 0x73, 0x52, 0x5c,
|
||||
0xc0, 0xc6, 0x7a, 0xba, 0x08, 0x09, 0x71, 0xb1, 0x80, 0x4c, 0x96, 0x60, 0x54, 0x60, 0xd4, 0xe0,
|
||||
0x09, 0x02, 0xb3, 0x95, 0xcc, 0x21, 0xca, 0xfc, 0x52, 0xcb, 0xe1, 0xca, 0x54, 0x90, 0x95, 0x39,
|
||||
0x09, 0x9c, 0xb8, 0x27, 0xcf, 0x70, 0xeb, 0x9e, 0x3c, 0x07, 0x4c, 0x1e, 0xa2, 0xd1, 0x49, 0xe6,
|
||||
0xc4, 0x43, 0x39, 0x86, 0x0b, 0x0f, 0xe5, 0x18, 0x4e, 0x3c, 0x92, 0x63, 0xbc, 0xf0, 0x48, 0x8e,
|
||||
0xf1, 0xc1, 0x23, 0x39, 0x86, 0x17, 0x8f, 0xe4, 0x18, 0x16, 0x3c, 0x96, 0x63, 0x4c, 0x62, 0x03,
|
||||
0x3b, 0xc2, 0x18, 0x10, 0x00, 0x00, 0xff, 0xff, 0x35, 0x9c, 0x00, 0x78, 0xd4, 0x00, 0x00, 0x00,
|
||||
0x01, 0xb3, 0x20, 0x1a, 0x95, 0x54, 0xb9, 0xf8, 0x43, 0x80, 0xc6, 0xf8, 0xe7, 0xa4, 0xb8, 0x80,
|
||||
0x8d, 0xf5, 0x74, 0x11, 0x12, 0xe2, 0x62, 0x01, 0x99, 0x2c, 0xc1, 0xa8, 0xc0, 0xa8, 0xc1, 0x13,
|
||||
0x04, 0x66, 0x2b, 0x99, 0x43, 0x94, 0xf9, 0xa5, 0x96, 0xc3, 0x95, 0xa9, 0x20, 0x2b, 0x73, 0x12,
|
||||
0x38, 0x71, 0x4f, 0x9e, 0xe1, 0xd6, 0x3d, 0x79, 0x0e, 0x98, 0x3c, 0x44, 0xa3, 0x93, 0xcc, 0x89,
|
||||
0x87, 0x72, 0x0c, 0x17, 0x80, 0xf8, 0xc4, 0x23, 0x39, 0xc6, 0x0b, 0x40, 0xfc, 0xe0, 0x91, 0x1c,
|
||||
0xc3, 0x0b, 0x20, 0x5e, 0xf0, 0x58, 0x8e, 0x31, 0x89, 0x0d, 0xec, 0x08, 0x63, 0x40, 0x00, 0x00,
|
||||
0x00, 0xff, 0xff, 0x35, 0x9c, 0x00, 0x78, 0xd4, 0x00, 0x00, 0x00,
|
||||
}
|
||||
|
||||
@@ -6,29 +6,64 @@ package protocol
|
||||
|
||||
// Windows uses backslashes as file separator
|
||||
|
||||
import "path/filepath"
|
||||
import (
|
||||
"path/filepath"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type nativeModel struct {
|
||||
Model
|
||||
}
|
||||
|
||||
func (m nativeModel) Index(deviceID DeviceID, folder string, files []FileInfo) {
|
||||
fixupFiles(folder, files)
|
||||
files = fixupFiles(files)
|
||||
m.Model.Index(deviceID, folder, files)
|
||||
}
|
||||
|
||||
func (m nativeModel) IndexUpdate(deviceID DeviceID, folder string, files []FileInfo) {
|
||||
fixupFiles(folder, files)
|
||||
files = fixupFiles(files)
|
||||
m.Model.IndexUpdate(deviceID, folder, files)
|
||||
}
|
||||
|
||||
func (m nativeModel) Request(deviceID DeviceID, folder string, name string, offset int64, hash []byte, fromTemporary bool, buf []byte) error {
|
||||
if strings.Contains(name, `\`) {
|
||||
l.Warnln("Dropping request for %s, contains invalid path separator", name)
|
||||
return ErrNoSuchFile
|
||||
}
|
||||
|
||||
name = filepath.FromSlash(name)
|
||||
return m.Model.Request(deviceID, folder, name, offset, hash, fromTemporary, buf)
|
||||
}
|
||||
|
||||
func fixupFiles(folder string, files []FileInfo) {
|
||||
func fixupFiles(files []FileInfo) []FileInfo {
|
||||
var out []FileInfo
|
||||
for i := range files {
|
||||
if strings.Contains(files[i].Name, `\`) {
|
||||
l.Warnln("Dropping index entry for %s, contains invalid path separator", files[i].Name)
|
||||
if out == nil {
|
||||
// Most incoming updates won't contain anything invalid, so
|
||||
// we delay the allocation and copy to output slice until we
|
||||
// really need to do it, then copy all the so-far valid
|
||||
// files to it.
|
||||
out = make([]FileInfo, i, len(files)-1)
|
||||
copy(out, files)
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
// Fixup the path separators
|
||||
files[i].Name = filepath.FromSlash(files[i].Name)
|
||||
|
||||
if out != nil {
|
||||
out = append(out, files[i])
|
||||
}
|
||||
}
|
||||
|
||||
if out != nil {
|
||||
// We did some filtering
|
||||
return out
|
||||
}
|
||||
|
||||
// Unchanged
|
||||
return files
|
||||
}
|
||||
|
||||
29
lib/protocol/nativemodel_windows_test.go
Normal file
29
lib/protocol/nativemodel_windows_test.go
Normal file
@@ -0,0 +1,29 @@
|
||||
// Copyright (C) 2016 The Protocol Authors.
|
||||
|
||||
package protocol
|
||||
|
||||
import "testing"
|
||||
import "reflect"
|
||||
|
||||
func TestFixupFiles(t *testing.T) {
|
||||
files := []FileInfo{
|
||||
{Name: "foo/bar"},
|
||||
{Name: `foo\bar`},
|
||||
{Name: "foo/baz"},
|
||||
{Name: "foo/quux"},
|
||||
{Name: `foo\fail`},
|
||||
}
|
||||
|
||||
// Filenames should be slash converted, except files which already have
|
||||
// backslashes in them which are instead filtered out.
|
||||
expected := []FileInfo{
|
||||
{Name: `foo\bar`},
|
||||
{Name: `foo\baz`},
|
||||
{Name: `foo\quux`},
|
||||
}
|
||||
|
||||
fixed := fixupFiles(files)
|
||||
if !reflect.DeepEqual(fixed, expected) {
|
||||
t.Errorf("Got %v, expected %v", fixed, expected)
|
||||
}
|
||||
}
|
||||
@@ -7,6 +7,8 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"path"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -55,6 +57,8 @@ var (
|
||||
ErrTimeout = errors.New("read timeout")
|
||||
ErrSwitchingConnections = errors.New("switching connections")
|
||||
errUnknownMessage = errors.New("unknown message")
|
||||
errInvalidFilename = errors.New("filename is invalid")
|
||||
errUncleanFilename = errors.New("filename not in canonical format")
|
||||
)
|
||||
|
||||
type Model interface {
|
||||
@@ -310,6 +314,9 @@ func (c *rawConnection) readerLoop() (err error) {
|
||||
if state != stateReady {
|
||||
return fmt.Errorf("protocol error: index message in state %d", state)
|
||||
}
|
||||
if err := checkFilenames(msg.Files); err != nil {
|
||||
return fmt.Errorf("protocol error: index: %v", err)
|
||||
}
|
||||
c.handleIndex(*msg)
|
||||
state = stateReady
|
||||
|
||||
@@ -318,6 +325,9 @@ func (c *rawConnection) readerLoop() (err error) {
|
||||
if state != stateReady {
|
||||
return fmt.Errorf("protocol error: index update message in state %d", state)
|
||||
}
|
||||
if err := checkFilenames(msg.Files); err != nil {
|
||||
return fmt.Errorf("protocol error: index update: %v", err)
|
||||
}
|
||||
c.handleIndexUpdate(*msg)
|
||||
state = stateReady
|
||||
|
||||
@@ -326,6 +336,9 @@ func (c *rawConnection) readerLoop() (err error) {
|
||||
if state != stateReady {
|
||||
return fmt.Errorf("protocol error: request message in state %d", state)
|
||||
}
|
||||
if err := checkFilename(msg.Name); err != nil {
|
||||
return fmt.Errorf("protocol error: request: %q: %v", msg.Name, err)
|
||||
}
|
||||
// Requests are handled asynchronously
|
||||
go c.handleRequest(*msg)
|
||||
|
||||
@@ -451,38 +464,50 @@ func (c *rawConnection) readHeader() (Header, error) {
|
||||
|
||||
func (c *rawConnection) handleIndex(im Index) {
|
||||
l.Debugf("Index(%v, %v, %d file)", c.id, im.Folder, len(im.Files))
|
||||
c.receiver.Index(c.id, im.Folder, filterIndexMessageFiles(im.Files))
|
||||
c.receiver.Index(c.id, im.Folder, im.Files)
|
||||
}
|
||||
|
||||
func (c *rawConnection) handleIndexUpdate(im IndexUpdate) {
|
||||
l.Debugf("queueing IndexUpdate(%v, %v, %d files)", c.id, im.Folder, len(im.Files))
|
||||
c.receiver.IndexUpdate(c.id, im.Folder, filterIndexMessageFiles(im.Files))
|
||||
c.receiver.IndexUpdate(c.id, im.Folder, im.Files)
|
||||
}
|
||||
|
||||
func filterIndexMessageFiles(fs []FileInfo) []FileInfo {
|
||||
var out []FileInfo
|
||||
for i, f := range fs {
|
||||
switch f.Name {
|
||||
case "", ".", "..", "/": // A few obviously invalid filenames
|
||||
l.Infof("Dropping invalid filename %q from incoming index", f.Name)
|
||||
if out == nil {
|
||||
// Most incoming updates won't contain anything invalid, so we
|
||||
// delay the allocation and copy to output slice until we
|
||||
// really need to do it, then copy all the so var valid files
|
||||
// to it.
|
||||
out = make([]FileInfo, i, len(fs)-1)
|
||||
copy(out, fs)
|
||||
}
|
||||
default:
|
||||
if out != nil {
|
||||
out = append(out, f)
|
||||
}
|
||||
func checkFilenames(fs []FileInfo) error {
|
||||
for _, f := range fs {
|
||||
if err := checkFilename(f.Name); err != nil {
|
||||
return fmt.Errorf("%q: %v", f.Name, err)
|
||||
}
|
||||
}
|
||||
if out != nil {
|
||||
return out
|
||||
return nil
|
||||
}
|
||||
|
||||
// checkFilename verifies that the given filename is valid according to the
|
||||
// spec on what's allowed over the wire. A filename failing this test is
|
||||
// grounds for disconnecting the device.
|
||||
func checkFilename(name string) error {
|
||||
cleanedName := path.Clean(name)
|
||||
if cleanedName != name {
|
||||
// The filename on the wire should be in canonical format. If
|
||||
// Clean() managed to clean it up, there was something wrong with
|
||||
// it.
|
||||
return errUncleanFilename
|
||||
}
|
||||
return fs
|
||||
|
||||
switch name {
|
||||
case "", ".", "..":
|
||||
// These names are always invalid.
|
||||
return errInvalidFilename
|
||||
}
|
||||
if strings.HasPrefix(name, "/") {
|
||||
// Names are folder relative, not absolute.
|
||||
return errInvalidFilename
|
||||
}
|
||||
if strings.HasPrefix(name, "../") {
|
||||
// Starting with a dotdot is not allowed. Any other dotdots would
|
||||
// have been handled by the Clean() call at the top.
|
||||
return errInvalidFilename
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (c *rawConnection) handleRequest(req Request) {
|
||||
|
||||
@@ -273,3 +273,43 @@ func TestLZ4Compression(t *testing.T) {
|
||||
t.Logf("OK #%d, %d -> %d -> %d", i, dataLen, len(comp), dataLen)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCheckFilename(t *testing.T) {
|
||||
cases := []struct {
|
||||
name string
|
||||
ok bool
|
||||
}{
|
||||
// Valid filenames
|
||||
{"foo", true},
|
||||
{"foo/bar/baz", true},
|
||||
{"foo/bar:baz", true}, // colon is ok in general, will be filtered on windows
|
||||
{`\`, true}, // path separator on the wire is forward slash, so as above
|
||||
{`\.`, true},
|
||||
{`\..`, true},
|
||||
{".foo", true},
|
||||
{"foo..", true},
|
||||
|
||||
// Invalid filenames
|
||||
{"foo/..", false},
|
||||
{"foo/../bar", false},
|
||||
{"../foo/../bar", false},
|
||||
{"", false},
|
||||
{".", false},
|
||||
{"..", false},
|
||||
{"/", false},
|
||||
{"/.", false},
|
||||
{"/..", false},
|
||||
{"/foo", false},
|
||||
{"./foo", false},
|
||||
{"foo./", false},
|
||||
{"foo/.", false},
|
||||
{"foo/", false},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
err := checkFilename(tc.name)
|
||||
if (err == nil) != tc.ok {
|
||||
t.Errorf("Unexpected result for checkFilename(%q): %v", tc.name, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,7 +11,6 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync/atomic"
|
||||
"time"
|
||||
"unicode/utf8"
|
||||
@@ -242,13 +241,12 @@ func (w *walker) walkAndHashFiles(fchan, dchan chan protocol.FileInfo) filepath.
|
||||
}
|
||||
|
||||
info, err = w.Lstater.Lstat(absPath)
|
||||
// An error here would be weird as we've already gotten to this point, but act on it ninetheless
|
||||
// An error here would be weird as we've already gotten to this point, but act on it nonetheless
|
||||
if err != nil {
|
||||
return skip
|
||||
}
|
||||
|
||||
if w.TempNamer.IsTemporary(relPath) {
|
||||
// A temporary file
|
||||
l.Debugln("temporary:", relPath)
|
||||
if info.Mode().IsRegular() && info.ModTime().Add(w.TempLifetime).Before(now) {
|
||||
os.Remove(absPath)
|
||||
@@ -257,10 +255,13 @@ func (w *walker) walkAndHashFiles(fchan, dchan chan protocol.FileInfo) filepath.
|
||||
return nil
|
||||
}
|
||||
|
||||
if sn := filepath.Base(relPath); sn == ".stignore" || sn == ".stfolder" ||
|
||||
strings.HasPrefix(relPath, ".stversions") || (w.Matcher != nil && w.Matcher.Match(relPath).IsIgnored()) {
|
||||
// An ignored file
|
||||
l.Debugln("ignored:", relPath)
|
||||
if ignore.IsInternal(relPath) {
|
||||
l.Debugln("ignored (internal):", relPath)
|
||||
return skip
|
||||
}
|
||||
|
||||
if w.Matcher.Match(relPath).IsIgnored() {
|
||||
l.Debugln("ignored (patterns):", relPath)
|
||||
return skip
|
||||
}
|
||||
|
||||
@@ -396,21 +397,15 @@ func (w *walker) walkSymlink(absPath, relPath string, dchan chan protocol.FileIn
|
||||
return true, nil
|
||||
}
|
||||
|
||||
blocks, err := Blocks(strings.NewReader(target), w.BlockSize, -1, nil)
|
||||
if err != nil {
|
||||
l.Debugln("hash link error:", absPath, err)
|
||||
return true, nil
|
||||
}
|
||||
|
||||
// A symlink is "unchanged", if
|
||||
// - it exists
|
||||
// - it wasn't deleted (because it isn't now)
|
||||
// - it was a symlink
|
||||
// - it wasn't invalid
|
||||
// - the symlink type (file/dir) was the same
|
||||
// - the block list (i.e. hash of target) was the same
|
||||
// - the target was the same
|
||||
cf, ok := w.CurrentFiler.CurrentFile(relPath)
|
||||
if ok && !cf.IsDeleted() && cf.IsSymlink() && !cf.IsInvalid() && SymlinkTypeEqual(targetType, cf) && BlocksEqual(cf.Blocks, blocks) {
|
||||
if ok && !cf.IsDeleted() && cf.IsSymlink() && !cf.IsInvalid() && SymlinkTypeEqual(targetType, cf) && cf.SymlinkTarget == target {
|
||||
return true, nil
|
||||
}
|
||||
|
||||
@@ -419,7 +414,7 @@ func (w *walker) walkSymlink(absPath, relPath string, dchan chan protocol.FileIn
|
||||
Type: SymlinkType(targetType),
|
||||
Version: cf.Version.Update(w.ShortID),
|
||||
NoPermissions: true, // Symlinks don't have permissions of their own
|
||||
Blocks: blocks,
|
||||
SymlinkTarget: target,
|
||||
}
|
||||
|
||||
l.Debugln("symlink changedb:", absPath, f)
|
||||
|
||||
@@ -311,23 +311,16 @@ func TestWalkSymlink(t *testing.T) {
|
||||
files = append(files, f)
|
||||
}
|
||||
|
||||
// Verify that we got one symlink and with the correct block contents
|
||||
// Verify that we got one symlink and with the correct attributes
|
||||
|
||||
if len(files) != 1 {
|
||||
t.Errorf("expected 1 symlink, not %d", len(files))
|
||||
}
|
||||
if len(files[0].Blocks) != 1 {
|
||||
t.Errorf("expected 1 block, not %d", len(files[0].Blocks))
|
||||
if len(files[0].Blocks) != 0 {
|
||||
t.Errorf("expected zero blocks for symlink, not %d", len(files[0].Blocks))
|
||||
}
|
||||
|
||||
if files[0].Blocks[0].Size != int32(len("destination")) {
|
||||
t.Errorf("expected block length %d, not %d", len("destination"), files[0].Blocks[0].Size)
|
||||
}
|
||||
|
||||
// echo -n "destination" | openssl dgst -sha256
|
||||
hash := "b5c755aaab1038b3d5627bbde7f47ca80c5f5c0481c6d33f04139d07aa1530e7"
|
||||
if fmt.Sprintf("%x", files[0].Blocks[0].Hash) != hash {
|
||||
t.Errorf("incorrect hash")
|
||||
if files[0].SymlinkTarget != "destination" {
|
||||
t.Errorf("expected symlink to have target destination, not %q", files[0].SymlinkTarget)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -35,7 +35,3 @@ func Read(path string) (string, TargetType, error) {
|
||||
func Create(source, target string, tt TargetType) error {
|
||||
return os.Symlink(osutil.NativeFilename(target), source)
|
||||
}
|
||||
|
||||
func ChangeType(path string, tt TargetType) error {
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -166,25 +166,3 @@ func Create(source, target string, tt TargetType) error {
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ChangeType(path string, tt TargetType) error {
|
||||
target, exTt, err := Read(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// If it's the same type, nothing to do.
|
||||
if tt == exTt {
|
||||
return nil
|
||||
}
|
||||
|
||||
// If the actual type is unknown, but the new type is file, nothing to do
|
||||
if exTt == TargetUnknown && tt != TargetDirectory {
|
||||
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 Create(path, target, tt)
|
||||
}, path)
|
||||
}
|
||||
|
||||
@@ -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 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "STDISCOSRV" "1" "December 11, 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 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "STRELAYSRV" "1" "December 11, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
strelaysrv \- Syncthing Relay Server
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-BEP" "7" "November 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-BEP" "7" "December 11, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-bep \- Block Exchange Protocol v1
|
||||
.
|
||||
@@ -419,7 +419,8 @@ message FileInfo {
|
||||
Vector version = 9;
|
||||
int64 sequence = 10;
|
||||
|
||||
repeated BlockInfo Blocks = 16;
|
||||
repeated BlockInfo Blocks = 16;
|
||||
string symlink_target = 17;
|
||||
}
|
||||
|
||||
enum FileInfoType {
|
||||
@@ -470,7 +471,7 @@ systems \- the implementation SHOULD nonetheless indicate the target type
|
||||
when possible.
|
||||
.sp
|
||||
The \fBsize\fP field contains the size of the file, in bytes. For directories
|
||||
the size is zero. For symlinks the size is the length of the target name.
|
||||
and symlinks the size is zero.
|
||||
.sp
|
||||
The \fBpermissions\fP field holds the common Unix permission bits. An
|
||||
implementation MAY ignore or interpret these as is suitable on the host
|
||||
@@ -508,7 +509,11 @@ database update, thus forming a sequence number over database updates.
|
||||
.sp
|
||||
The \fBblocks\fP list contains the size and hash for each block in the file.
|
||||
Each block represents a 128 KiB slice of the file, except for the last block
|
||||
which may represent a smaller amount of data.
|
||||
which may represent a smaller amount of data. The block list is empty for
|
||||
files and symlinks.
|
||||
.sp
|
||||
The \fBsymlink_target\fP field contains the symlink target, for entries of
|
||||
symlink type. It is empty for all other entry types.
|
||||
.SS Request
|
||||
.sp
|
||||
The Request message expresses the desire to receive a data block
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-CONFIG" "5" "November 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-CONFIG" "5" "December 11, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-config \- Syncthing Configuration
|
||||
.
|
||||
@@ -233,7 +233,7 @@ If the original introducer unshares this folder with this device, our device wil
|
||||
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 repositories.
|
||||
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
|
||||
@@ -306,13 +306,11 @@ 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
|
||||
.INDENT 3.5
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "November 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "December 11, 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 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-EVENT-API" "7" "December 11, 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 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-FAQ" "7" "December 11, 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 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "December 11, 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 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "December 11, 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 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-NETWORKING" "7" "December 11, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-networking \- Firewall Setup
|
||||
.
|
||||
@@ -84,6 +84,20 @@ sudo ufw allow syncthing
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
If you also want to allow external access to the Syncthing web GUI, run:
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
.sp
|
||||
.nf
|
||||
.ft C
|
||||
sudo ufw allow syncthing\-gui
|
||||
.ft P
|
||||
.fi
|
||||
.UNINDENT
|
||||
.UNINDENT
|
||||
.sp
|
||||
Allowing external access is \fBnot\fP necessary for a typical installation.
|
||||
.sp
|
||||
You can then verify that the ports mentioned above are allowed:
|
||||
.INDENT 0.0
|
||||
.INDENT 3.5
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-RELAY" "7" "November 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-RELAY" "7" "December 11, 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 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-REST-API" "7" "December 11, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-rest-api \- REST API
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-SECURITY" "7" "November 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-SECURITY" "7" "December 11, 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 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-STIGNORE" "5" "December 11, 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 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-VERSIONING" "7" "December 11, 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 21, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING" "1" "December 11, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing \- Syncthing
|
||||
.
|
||||
|
||||
7
vendor/github.com/gobwas/glob/compiler/compiler.go
generated
vendored
7
vendor/github.com/gobwas/glob/compiler/compiler.go
generated
vendored
@@ -5,10 +5,11 @@ package compiler
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/gobwas/glob/match"
|
||||
"github.com/gobwas/glob/syntax/ast"
|
||||
"github.com/gobwas/glob/util/runes"
|
||||
"reflect"
|
||||
)
|
||||
|
||||
func optimizeMatcher(matcher match.Matcher) match.Matcher {
|
||||
@@ -373,11 +374,11 @@ func commonChildren(nodes []*ast.Node) (commonLeft, commonRight []*ast.Node) {
|
||||
breakRight bool
|
||||
commonTotal int
|
||||
)
|
||||
for i, j := 0, treeLength-1; commonTotal < treeLength && j >= 0 && !(breakLeft && breakLeft); i, j = i+1, j-1 {
|
||||
for i, j := 0, treeLength-1; commonTotal < treeLength && j >= 0 && !(breakLeft && breakRight); i, j = i+1, j-1 {
|
||||
treeLeft := tree.Children[i]
|
||||
treeRight := tree.Children[j]
|
||||
|
||||
for k := 0; k < len(nodes) && !(breakLeft && breakLeft); k++ {
|
||||
for k := 0; k < len(nodes) && !(breakLeft && breakRight); k++ {
|
||||
// skip least children node
|
||||
if k == idx {
|
||||
continue
|
||||
|
||||
4
vendor/manifest
vendored
4
vendor/manifest
vendored
@@ -140,8 +140,8 @@
|
||||
{
|
||||
"importpath": "github.com/gobwas/glob",
|
||||
"repository": "https://github.com/gobwas/glob",
|
||||
"vcs": "",
|
||||
"revision": "0354991b92587e2742549d3036f3b5bae5ab03f2",
|
||||
"vcs": "git",
|
||||
"revision": "bea32b9cd2d6f55753d94a28e959b13f0244797a",
|
||||
"branch": "master"
|
||||
},
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user