mirror of
https://github.com/syncthing/syncthing.git
synced 2026-01-14 08:49:17 -05:00
Compare commits
26 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
7569b75d61 | ||
|
|
8fcabac518 | ||
|
|
abb0cfde72 | ||
|
|
7990ffcc60 | ||
|
|
49910a1d85 | ||
|
|
46a143e80e | ||
|
|
69b7f26e4c | ||
|
|
5b37d0356c | ||
|
|
1188ebbb7b | ||
|
|
76b903b2e0 | ||
|
|
be38c2111f | ||
|
|
1de787fab8 | ||
|
|
81f683a61c | ||
|
|
db6f68d031 | ||
|
|
d0a1c805e9 | ||
|
|
00a654845f | ||
|
|
04dad8485a | ||
|
|
0b1475169f | ||
|
|
6ec4fbc82b | ||
|
|
18cc7a663b | ||
|
|
cf5febad47 | ||
|
|
42849af5a8 | ||
|
|
e6364407a9 | ||
|
|
480b78f2c8 | ||
|
|
fa8f339478 | ||
|
|
7776839c82 |
2
AUTHORS
2
AUTHORS
@@ -51,7 +51,7 @@ Gilli Sigurdsson (gillisig) <gilli@vx.is>
|
||||
Jaakko Hannikainen (jgke) <jgke@jgke.fi>
|
||||
Jacek Szafarkiewicz (hadogenes) <szafar@linux.pl>
|
||||
Jake Peterson (acogdev) <jake@acogdev.com>
|
||||
Jakob Borg (calmh) <jakob@nym.se>
|
||||
Jakob Borg (calmh) <jakob@nym.se> <jakob@kastelo.net>
|
||||
James Patterson (jpjp) <jamespatterson@operamail.com> <jpjp@users.noreply.github.com>
|
||||
Jaroslav Malec (dzarda) <dzardacz@gmail.com>
|
||||
Jens Diemer (jedie) <github.com@jensdiemer.de> <git@jensdiemer.de>
|
||||
|
||||
1
NICKS
1
NICKS
@@ -23,6 +23,7 @@ buinsky <vix_booja@tut.by>
|
||||
burkemw3 <mburke@amplify.com>
|
||||
burkemw3 <burkemw3@gmail.com>
|
||||
calmh <jakob@nym.se>
|
||||
calmh <jakob@kastelo.net>
|
||||
canton7 <antony.male@gmail.com>
|
||||
Cathryne <cathryne.linenweaver@gmail.com>
|
||||
Cathryne <Cathryne@users.noreply.github.com>
|
||||
|
||||
6
build.go
6
build.go
@@ -168,7 +168,7 @@ func init() {
|
||||
targets["syncthing"] = syncthingPkg
|
||||
}
|
||||
|
||||
const minGoVersion = 1.3
|
||||
const minGoVersion = 1.5
|
||||
|
||||
func main() {
|
||||
log.SetOutput(os.Stdout)
|
||||
@@ -642,7 +642,9 @@ func ldflags() string {
|
||||
|
||||
func rmr(paths ...string) {
|
||||
for _, path := range paths {
|
||||
log.Println("rm -r", path)
|
||||
if debug {
|
||||
log.Println("rm -r", path)
|
||||
}
|
||||
os.RemoveAll(path)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
"github.com/thejerf/suture"
|
||||
)
|
||||
|
||||
@@ -86,7 +87,11 @@ func main() {
|
||||
if !useHTTP {
|
||||
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to load X509 key pair:", err)
|
||||
log.Println("Failed to load keypair. Generating one, this might take a while...")
|
||||
cert, err = tlsutil.NewCertificate(certFile, keyFile, "stdiscosrv", 3072)
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to generate X509 key pair:", err)
|
||||
}
|
||||
}
|
||||
|
||||
devID := protocol.NewDeviceID(cert.Certificate[0])
|
||||
|
||||
@@ -27,8 +27,9 @@ func postgresSetup(db *sql.DB) error {
|
||||
return err
|
||||
}
|
||||
|
||||
var tmp string
|
||||
row := db.QueryRow(`SELECT 'DevicesDeviceIDIndex'::regclass`)
|
||||
if err = row.Scan(nil); err != nil {
|
||||
if err = row.Scan(&tmp); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX DevicesDeviceIDIndex ON Devices (DeviceID)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -36,7 +37,7 @@ func postgresSetup(db *sql.DB) error {
|
||||
}
|
||||
|
||||
row = db.QueryRow(`SELECT 'DevicesSeenIndex'::regclass`)
|
||||
if err = row.Scan(nil); err != nil {
|
||||
if err = row.Scan(&tmp); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX DevicesSeenIndex ON Devices (Seen)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -46,14 +47,14 @@ func postgresSetup(db *sql.DB) error {
|
||||
_, err = db.Exec(`CREATE TABLE IF NOT EXISTS Addresses (
|
||||
DeviceID CHAR(63) NOT NULL,
|
||||
Seen TIMESTAMP NOT NULL,
|
||||
Address VARCHAR(256) NOT NULL
|
||||
Address VARCHAR(2048) NOT NULL
|
||||
)`)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
row = db.QueryRow(`SELECT 'AddressesDeviceIDSeenIndex'::regclass`)
|
||||
if err = row.Scan(nil); err != nil {
|
||||
if err = row.Scan(&tmp); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX AddressesDeviceIDSeenIndex ON Addresses (DeviceID, Seen)`)
|
||||
}
|
||||
if err != nil {
|
||||
@@ -61,7 +62,7 @@ func postgresSetup(db *sql.DB) error {
|
||||
}
|
||||
|
||||
row = db.QueryRow(`SELECT 'AddressesDeviceIDAddressIndex'::regclass`)
|
||||
if err = row.Scan(nil); err != nil {
|
||||
if err = row.Scan(&tmp); err != nil {
|
||||
_, err = db.Exec(`CREATE INDEX AddressesDeviceIDAddressIndex ON Addresses (DeviceID, Address)`)
|
||||
}
|
||||
if err != nil {
|
||||
|
||||
@@ -451,7 +451,7 @@ func (s *querysrv) getDeviceSeen(device protocol.DeviceID) (time.Time, error) {
|
||||
if err := row.Scan(&seen); err != nil {
|
||||
return time.Time{}, err
|
||||
}
|
||||
return seen, nil
|
||||
return seen.In(time.UTC), nil
|
||||
}
|
||||
|
||||
func handlePing(w http.ResponseWriter, r *http.Request) {
|
||||
|
||||
@@ -66,7 +66,7 @@ func checkServers(deviceID protocol.DeviceID, servers ...string) {
|
||||
}()
|
||||
}
|
||||
|
||||
for _ = range servers {
|
||||
for range servers {
|
||||
res := <-resc
|
||||
|
||||
u, _ := url.Parse(res.server)
|
||||
|
||||
@@ -76,6 +76,7 @@ var (
|
||||
permRelaysFile string
|
||||
ipHeader string
|
||||
geoipPath string
|
||||
proto string
|
||||
|
||||
getMut = sync.NewRWMutex()
|
||||
getLRUCache *lru.Cache
|
||||
@@ -105,6 +106,7 @@ func main() {
|
||||
flag.StringVar(&permRelaysFile, "perm-relays", "", "Path to list of permanent relays")
|
||||
flag.StringVar(&ipHeader, "ip-header", "", "Name of header which holds clients ip:port. Only meaningful when running behind a reverse proxy.")
|
||||
flag.StringVar(&geoipPath, "geoip", "GeoLite2-City.mmdb", "Path to GeoLite2-City database")
|
||||
flag.StringVar(&proto, "protocol", "tcp", "Protocol used for listening. 'tcp' for IPv4 and IPv6, 'tcp4' for IPv4, 'tcp6' for IPv6")
|
||||
|
||||
flag.Parse()
|
||||
|
||||
@@ -154,12 +156,12 @@ func main() {
|
||||
},
|
||||
}
|
||||
|
||||
listener, err = tls.Listen("tcp", listen, tlsCfg)
|
||||
listener, err = tls.Listen(proto, listen, tlsCfg)
|
||||
} else {
|
||||
if debug {
|
||||
log.Println("Starting plain listener on", listen)
|
||||
}
|
||||
listener, err = net.Listen("tcp", listen)
|
||||
listener, err = net.Listen(proto, listen)
|
||||
}
|
||||
|
||||
if err != nil {
|
||||
|
||||
@@ -5,7 +5,7 @@ strelaysrv
|
||||
|
||||
This is the relay server for the `syncthing` project.
|
||||
|
||||
To get it, run `go get github.com/syncthing/strelaysrv` or download the
|
||||
To get it, run `go get github.com/syncthing/syncthing/cmd/strelaysrv` or download the
|
||||
[latest build](http://build.syncthing.net/job/strelaysrv/lastSuccessfulBuild/artifact/)
|
||||
from the build server.
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ var (
|
||||
numConnections int64
|
||||
)
|
||||
|
||||
func listener(addr string, config *tls.Config) {
|
||||
func listener(proto, addr string, config *tls.Config) {
|
||||
tcpListener, err := net.Listen("tcp", addr)
|
||||
if err != nil {
|
||||
log.Fatalln(err)
|
||||
@@ -167,10 +167,16 @@ func protocolConnectionHandler(tcpConn net.Conn, config *tls.Config) {
|
||||
continue
|
||||
}
|
||||
|
||||
peerOutbox <- serverInvitation
|
||||
select {
|
||||
case peerOutbox <- serverInvitation:
|
||||
if debug {
|
||||
log.Println("Sent invitation from", id, "to", requestedPeer)
|
||||
}
|
||||
default:
|
||||
if debug {
|
||||
log.Println("Could not send invitation from", id, "to", requestedPeer, "as peer disconnected")
|
||||
}
|
||||
|
||||
if debug {
|
||||
log.Println("Sent invitation from", id, "to", requestedPeer)
|
||||
}
|
||||
conn.Close()
|
||||
|
||||
|
||||
@@ -24,6 +24,11 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/relay/protocol"
|
||||
"github.com/syncthing/syncthing/lib/tlsutil"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/config"
|
||||
"github.com/syncthing/syncthing/lib/nat"
|
||||
_ "github.com/syncthing/syncthing/lib/pmp"
|
||||
_ "github.com/syncthing/syncthing/lib/upnp"
|
||||
|
||||
syncthingprotocol "github.com/syncthing/syncthing/lib/protocol"
|
||||
)
|
||||
|
||||
@@ -48,6 +53,7 @@ func init() {
|
||||
var (
|
||||
listen string
|
||||
debug bool
|
||||
proto string
|
||||
|
||||
sessionAddress []byte
|
||||
sessionPort uint16
|
||||
@@ -70,12 +76,17 @@ var (
|
||||
pools []string
|
||||
providedBy string
|
||||
defaultPoolAddrs = "https://relays.syncthing.net/endpoint"
|
||||
|
||||
natEnabled bool
|
||||
natLease int
|
||||
natRenewal int
|
||||
natTimeout int
|
||||
)
|
||||
|
||||
func main() {
|
||||
log.SetFlags(log.Lshortfile | log.LstdFlags)
|
||||
|
||||
var dir, extAddress string
|
||||
var dir, extAddress, proto string
|
||||
|
||||
flag.StringVar(&listen, "listen", ":22067", "Protocol listen address")
|
||||
flag.StringVar(&dir, "keys", ".", "Directory where cert.pem and key.pem is stored")
|
||||
@@ -89,14 +100,22 @@ func main() {
|
||||
flag.StringVar(&poolAddrs, "pools", defaultPoolAddrs, "Comma separated list of relay pool addresses to join")
|
||||
flag.StringVar(&providedBy, "provided-by", "", "An optional description about who provides the relay")
|
||||
flag.StringVar(&extAddress, "ext-address", "", "An optional address to advertise as being available on.\n\tAllows listening on an unprivileged port with port forwarding from e.g. 443, and be connected to on port 443.")
|
||||
|
||||
flag.StringVar(&proto, "protocol", "tcp", "Protocol used for listening. 'tcp' for IPv4 and IPv6, 'tcp4' for IPv4, 'tcp6' for IPv6")
|
||||
flag.BoolVar(&natEnabled, "nat", false, "Use UPnP/NAT-PMP to acquire external port mapping")
|
||||
flag.IntVar(&natLease, "nat-lease", 60, "NAT lease length in minutes")
|
||||
flag.IntVar(&natRenewal, "nat-renewal", 30, "NAT renewal frequency in minutes")
|
||||
flag.IntVar(&natTimeout, "nat-timeout", 10, "NAT discovery timeout in seconds")
|
||||
flag.Parse()
|
||||
|
||||
if extAddress == "" {
|
||||
extAddress = listen
|
||||
}
|
||||
|
||||
addr, err := net.ResolveTCPAddr("tcp", extAddress)
|
||||
if len(providedBy) > 30 {
|
||||
log.Fatal("Provided-by cannot be longer than 30 characters")
|
||||
}
|
||||
|
||||
addr, err := net.ResolveTCPAddr(proto, extAddress)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
@@ -149,6 +168,37 @@ func main() {
|
||||
log.Println("ID:", id)
|
||||
}
|
||||
|
||||
wrapper := config.Wrap("config", config.New(id))
|
||||
wrapper.SetOptions(config.OptionsConfiguration{
|
||||
NATLeaseM: natLease,
|
||||
NATRenewalM: natRenewal,
|
||||
NATTimeoutS: natTimeout,
|
||||
})
|
||||
natSvc := nat.NewService(id, wrapper)
|
||||
mapping := mapping{natSvc.NewMapping(nat.TCP, addr.IP, addr.Port)}
|
||||
|
||||
if natEnabled {
|
||||
go natSvc.Serve()
|
||||
found := make(chan struct{})
|
||||
mapping.OnChanged(func(_ *nat.Mapping, _, _ []nat.Address) {
|
||||
select {
|
||||
case found <- struct{}{}:
|
||||
default:
|
||||
}
|
||||
})
|
||||
|
||||
// Need to wait a few extra seconds, since NAT library waits exactly natTimeout seconds on all interfaces.
|
||||
timeout := time.Duration(natTimeout+2) * time.Second
|
||||
log.Printf("Waiting %s to acquire NAT mapping", timeout)
|
||||
|
||||
select {
|
||||
case <-found:
|
||||
log.Printf("Found NAT mapping: %s", mapping.ExternalAddresses())
|
||||
case <-time.After(timeout):
|
||||
log.Println("Timeout out waiting for NAT mapping.")
|
||||
}
|
||||
}
|
||||
|
||||
if sessionLimitBps > 0 {
|
||||
sessionLimiter = ratelimit.NewBucketWithRate(float64(sessionLimitBps), int64(2*sessionLimitBps))
|
||||
}
|
||||
@@ -160,7 +210,7 @@ func main() {
|
||||
go statusService(statusAddr)
|
||||
}
|
||||
|
||||
uri, err := url.Parse(fmt.Sprintf("relay://%s/?id=%s&pingInterval=%s&networkTimeout=%s&sessionLimitBps=%d&globalLimitBps=%d&statusAddr=%s&providedBy=%s", extAddress, id, pingInterval, networkTimeout, sessionLimitBps, globalLimitBps, statusAddr, providedBy))
|
||||
uri, err := url.Parse(fmt.Sprintf("relay://%s/?id=%s&pingInterval=%s&networkTimeout=%s&sessionLimitBps=%d&globalLimitBps=%d&statusAddr=%s&providedBy=%s", mapping.Address(), id, pingInterval, networkTimeout, sessionLimitBps, globalLimitBps, statusAddr, providedBy))
|
||||
if err != nil {
|
||||
log.Fatalln("Failed to construct URI", err)
|
||||
}
|
||||
@@ -178,11 +228,11 @@ func main() {
|
||||
for _, pool := range pools {
|
||||
pool = strings.TrimSpace(pool)
|
||||
if len(pool) > 0 {
|
||||
go poolHandler(pool, uri)
|
||||
go poolHandler(pool, uri, mapping)
|
||||
}
|
||||
}
|
||||
|
||||
go listener(listen, tlsCfg)
|
||||
go listener(proto, listen, tlsCfg)
|
||||
|
||||
sigs := make(chan os.Signal, 1)
|
||||
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
|
||||
@@ -212,7 +262,7 @@ func main() {
|
||||
|
||||
func monitorLimits() {
|
||||
limitCheckTimer = time.NewTimer(time.Minute)
|
||||
for _ = range limitCheckTimer.C {
|
||||
for range limitCheckTimer.C {
|
||||
if atomic.LoadInt64(&numConnections)+atomic.LoadInt64(&numProxies) > descriptorLimit {
|
||||
atomic.StoreInt32(&overLimit, 1)
|
||||
log.Println("Gone past our connection limits. Starting to refuse new/drop idle connections.")
|
||||
@@ -222,3 +272,15 @@ func monitorLimits() {
|
||||
limitCheckTimer.Reset(time.Minute)
|
||||
}
|
||||
}
|
||||
|
||||
type mapping struct {
|
||||
*nat.Mapping
|
||||
}
|
||||
|
||||
func (m *mapping) Address() nat.Address {
|
||||
ext := m.ExternalAddresses()
|
||||
if len(ext) > 0 {
|
||||
return ext[0]
|
||||
}
|
||||
return m.Mapping.Address()
|
||||
}
|
||||
|
||||
@@ -12,16 +12,19 @@ import (
|
||||
"time"
|
||||
)
|
||||
|
||||
func poolHandler(pool string, uri *url.URL) {
|
||||
func poolHandler(pool string, uri *url.URL, mapping mapping) {
|
||||
if debug {
|
||||
log.Println("Joining", pool)
|
||||
}
|
||||
for {
|
||||
uriCopy := *uri
|
||||
uriCopy.Host = mapping.Address().String()
|
||||
|
||||
var b bytes.Buffer
|
||||
json.NewEncoder(&b).Encode(struct {
|
||||
URL string `json:"url"`
|
||||
}{
|
||||
uri.String(),
|
||||
uriCopy.String(),
|
||||
})
|
||||
|
||||
resp, err := http.Post(pool, "application/json", &b)
|
||||
@@ -39,7 +42,7 @@ func poolHandler(pool string, uri *url.URL) {
|
||||
log.Println(pool, "under load, will retry in a minute")
|
||||
time.Sleep(time.Minute)
|
||||
continue
|
||||
} else if resp.StatusCode == 403 {
|
||||
} else if resp.StatusCode == 401 {
|
||||
log.Println(pool, "failed to join due to IP address not matching external address. Aborting")
|
||||
return
|
||||
} else if resp.StatusCode == 200 {
|
||||
|
||||
@@ -130,7 +130,7 @@ func printProgress(prefix string, count *int64) {
|
||||
expectedIterations := float64(int(1) << uint(wantBits))
|
||||
fmt.Printf("Want %d bits for prefix %q, about %.2g certs to test (statistically speaking)\n", wantBits, prefix, expectedIterations)
|
||||
|
||||
for _ = range time.NewTicker(15 * time.Second).C {
|
||||
for range time.NewTicker(15 * time.Second).C {
|
||||
tried := atomic.LoadInt64(count)
|
||||
elapsed := time.Since(started)
|
||||
rate := float64(tried) / elapsed.Seconds()
|
||||
|
||||
@@ -68,10 +68,10 @@ type apiService struct {
|
||||
|
||||
type modelIntf interface {
|
||||
GlobalDirectoryTree(folder, prefix string, levels int, dirsonly bool) map[string]interface{}
|
||||
Completion(device protocol.DeviceID, folder string) float64
|
||||
Completion(device protocol.DeviceID, folder string) model.FolderCompletion
|
||||
Override(folder string)
|
||||
NeedFolderFiles(folder string, page, perpage int) ([]db.FileInfoTruncated, []db.FileInfoTruncated, []db.FileInfoTruncated, int)
|
||||
NeedSize(folder string) (nfiles int, bytes int64)
|
||||
NeedSize(folder string) (nfiles, ndeletes int, bytes int64)
|
||||
ConnectionStats() map[string]interface{}
|
||||
DeviceStatistics() map[string]stats.DeviceStatistics
|
||||
FolderStatistics() map[string]stats.FolderStatistics
|
||||
@@ -313,6 +313,11 @@ func (s *apiService) Serve() {
|
||||
// Add the CORS handling
|
||||
handler = corsMiddleware(handler)
|
||||
|
||||
if addressIsLocalhost(guiCfg.Address()) && !guiCfg.InsecureSkipHostCheck {
|
||||
// Verify source host
|
||||
handler = localhostMiddleware(handler)
|
||||
}
|
||||
|
||||
handler = debugMiddleware(handler)
|
||||
|
||||
srv := http.Server{
|
||||
@@ -495,6 +500,17 @@ func withDetailsMiddleware(id protocol.DeviceID, h http.Handler) http.Handler {
|
||||
})
|
||||
}
|
||||
|
||||
func localhostMiddleware(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if addressIsLocalhost(r.Host) {
|
||||
h.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "Host check error", http.StatusForbidden)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *apiService) whenDebugging(h http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if s.cfg.GUI().Debugging {
|
||||
@@ -503,7 +519,6 @@ func (s *apiService) whenDebugging(h http.Handler) http.Handler {
|
||||
}
|
||||
|
||||
http.Error(w, "Debugging disabled", http.StatusBadRequest)
|
||||
return
|
||||
})
|
||||
}
|
||||
|
||||
@@ -583,8 +598,12 @@ func (s *apiService) getDBCompletion(w http.ResponseWriter, r *http.Request) {
|
||||
return
|
||||
}
|
||||
|
||||
sendJSON(w, map[string]float64{
|
||||
"completion": s.model.Completion(device, folder),
|
||||
comp := s.model.Completion(device, folder)
|
||||
sendJSON(w, map[string]interface{}{
|
||||
"completion": comp.CompletionPct,
|
||||
"needBytes": comp.NeedBytes,
|
||||
"globalBytes": comp.GlobalBytes,
|
||||
"needDeletes": comp.NeedDeletes,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -605,8 +624,8 @@ func folderSummary(cfg configIntf, m modelIntf, folder string) map[string]interf
|
||||
localFiles, localDeleted, localBytes := m.LocalSize(folder)
|
||||
res["localFiles"], res["localDeleted"], res["localBytes"] = localFiles, localDeleted, localBytes
|
||||
|
||||
needFiles, needBytes := m.NeedSize(folder)
|
||||
res["needFiles"], res["needBytes"] = needFiles, needBytes
|
||||
needFiles, needDeletes, needBytes := m.NeedSize(folder)
|
||||
res["needFiles"], res["needDeletes"], res["needBytes"] = needFiles, needDeletes, needBytes
|
||||
|
||||
res["inSyncFiles"], res["inSyncBytes"] = globalFiles-needFiles, globalBytes-needBytes
|
||||
|
||||
@@ -1142,7 +1161,7 @@ func (s *apiService) getPeerCompletion(w http.ResponseWriter, r *http.Request) {
|
||||
for _, device := range folder.DeviceIDs() {
|
||||
deviceStr := device.String()
|
||||
if s.model.ConnectedTo(device) {
|
||||
tot[deviceStr] += s.model.Completion(device, folder.ID)
|
||||
tot[deviceStr] += s.model.Completion(device, folder.ID).CompletionPct
|
||||
} else {
|
||||
tot[deviceStr] = 0
|
||||
}
|
||||
@@ -1288,3 +1307,17 @@ func dirNames(dir string) []string {
|
||||
sort.Strings(dirs)
|
||||
return dirs
|
||||
}
|
||||
|
||||
func addressIsLocalhost(addr string) bool {
|
||||
host, _, err := net.SplitHostPort(addr)
|
||||
if err != nil {
|
||||
// There was no port, so we assume the address was just a hostname
|
||||
host = addr
|
||||
}
|
||||
switch host {
|
||||
case "127.0.0.1", "::1", "localhost":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
@@ -78,7 +78,7 @@ func trackCPUUsage() {
|
||||
var prevTime = time.Now().UnixNano()
|
||||
var rusage prusage_t
|
||||
var pid = os.Getpid()
|
||||
for _ = range time.NewTicker(time.Second).C {
|
||||
for range time.NewTicker(time.Second).C {
|
||||
err := solarisPrusage(pid, &rusage)
|
||||
if err != nil {
|
||||
l.Warnln("getting prusage:", err)
|
||||
|
||||
@@ -16,6 +16,7 @@ import (
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
@@ -460,7 +461,6 @@ func TestHTTPLogin(t *testing.T) {
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Errorf("Unexpected non-200 return code %d for authed request (ISO-8859-1)", resp.StatusCode)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
func startHTTP(cfg *mockedConfig) (string, error) {
|
||||
@@ -491,7 +491,12 @@ func startHTTP(cfg *mockedConfig) (string, error) {
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("Weird address from API service: %v", err)
|
||||
}
|
||||
baseURL := fmt.Sprintf("http://127.0.0.1:%d", tcpAddr.Port)
|
||||
|
||||
host, _, _ := net.SplitHostPort(cfg.gui.RawAddress)
|
||||
if host == "" || host == "0.0.0.0" {
|
||||
host = "127.0.0.1"
|
||||
}
|
||||
baseURL := fmt.Sprintf("http://%s", net.JoinHostPort(host, strconv.Itoa(tcpAddr.Port)))
|
||||
|
||||
return baseURL, nil
|
||||
}
|
||||
@@ -666,3 +671,189 @@ func testConfigPost(data io.Reader) (*http.Response, error) {
|
||||
req.Header.Set("X-API-Key", testAPIKey)
|
||||
return cli.Do(req)
|
||||
}
|
||||
|
||||
func TestHostCheck(t *testing.T) {
|
||||
// An API service bound to localhost should reject non-localhost host Headers
|
||||
|
||||
cfg := new(mockedConfig)
|
||||
cfg.gui.RawAddress = "127.0.0.1:0"
|
||||
baseURL, err := startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// A normal HTTP get to the localhost-bound service should succeed
|
||||
|
||||
resp, err := http.Get(baseURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Error("Regular HTTP get: expected 200 OK, not", resp.Status)
|
||||
}
|
||||
|
||||
// A request with a suspicious Host header should fail
|
||||
|
||||
req, _ := http.NewRequest("GET", baseURL, nil)
|
||||
req.Host = "example.com"
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Error("Suspicious Host header: expected 403 Forbidden, not", resp.Status)
|
||||
}
|
||||
|
||||
// A request with an explicit "localhost:8384" Host header should pass
|
||||
|
||||
req, _ = http.NewRequest("GET", baseURL, nil)
|
||||
req.Host = "localhost:8384"
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Error("Explicit localhost:8384: expected 200 OK, not", resp.Status)
|
||||
}
|
||||
|
||||
// A request with an explicit "localhost" Host header (no port) should pass
|
||||
|
||||
req, _ = http.NewRequest("GET", baseURL, nil)
|
||||
req.Host = "localhost"
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Error("Explicit localhost: expected 200 OK, not", resp.Status)
|
||||
}
|
||||
|
||||
// A server with InsecureSkipHostCheck set behaves differently
|
||||
|
||||
cfg = new(mockedConfig)
|
||||
cfg.gui.RawAddress = "127.0.0.1:0"
|
||||
cfg.gui.InsecureSkipHostCheck = true
|
||||
baseURL, err = startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// A request with a suspicious Host header should be allowed
|
||||
|
||||
req, _ = http.NewRequest("GET", baseURL, nil)
|
||||
req.Host = "example.com"
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Error("Incorrect host header, check disabled: expected 200 OK, not", resp.Status)
|
||||
}
|
||||
|
||||
// A server bound to a wildcard address also doesn't do the check
|
||||
|
||||
cfg = new(mockedConfig)
|
||||
cfg.gui.RawAddress = "0.0.0.0:0"
|
||||
cfg.gui.InsecureSkipHostCheck = true
|
||||
baseURL, err = startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// A request with a suspicious Host header should be allowed
|
||||
|
||||
req, _ = http.NewRequest("GET", baseURL, nil)
|
||||
req.Host = "example.com"
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Error("Incorrect host header, wildcard bound: expected 200 OK, not", resp.Status)
|
||||
}
|
||||
|
||||
// This should all work over IPv6 as well
|
||||
|
||||
cfg = new(mockedConfig)
|
||||
cfg.gui.RawAddress = "[::1]:0"
|
||||
baseURL, err = startHTTP(cfg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// A normal HTTP get to the localhost-bound service should succeed
|
||||
|
||||
resp, err = http.Get(baseURL)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Error("Regular HTTP get (IPv6): expected 200 OK, not", resp.Status)
|
||||
}
|
||||
|
||||
// A request with a suspicious Host header should fail
|
||||
|
||||
req, _ = http.NewRequest("GET", baseURL, nil)
|
||||
req.Host = "example.com"
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusForbidden {
|
||||
t.Error("Suspicious Host header (IPv6): expected 403 Forbidden, not", resp.Status)
|
||||
}
|
||||
|
||||
// A request with an explicit "localhost:8384" Host header should pass
|
||||
|
||||
req, _ = http.NewRequest("GET", baseURL, nil)
|
||||
req.Host = "localhost:8384"
|
||||
resp, err = http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
t.Error("Explicit localhost:8384 (IPv6): expected 200 OK, not", resp.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestAddressIsLocalhost(t *testing.T) {
|
||||
testcases := []struct {
|
||||
address string
|
||||
result bool
|
||||
}{
|
||||
// These are all valid localhost addresses
|
||||
{"localhost", true},
|
||||
{"::1", true},
|
||||
{"127.0.0.1", true},
|
||||
{"localhost:8080", true},
|
||||
{"[::1]:8080", true},
|
||||
{"127.0.0.1:8080", true},
|
||||
|
||||
// These are all non-localhost addresses
|
||||
{"example.com", false},
|
||||
{"example.com:8080", false},
|
||||
{"192.0.2.10", false},
|
||||
{"192.0.2.10:8080", false},
|
||||
{"0.0.0.0", false},
|
||||
{"0.0.0.0:8080", false},
|
||||
{"::", false},
|
||||
{"[::]:8080", false},
|
||||
{":8080", false},
|
||||
}
|
||||
|
||||
for _, tc := range testcases {
|
||||
result := addressIsLocalhost(tc.address)
|
||||
if result != tc.result {
|
||||
t.Errorf("addressIsLocalhost(%q)=%v, expected %v", tc.address, result, tc.result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -21,7 +21,7 @@ func trackCPUUsage() {
|
||||
var prevUsage int64
|
||||
var prevTime = time.Now().UnixNano()
|
||||
var rusage syscall.Rusage
|
||||
for _ = range time.NewTicker(time.Second).C {
|
||||
for range time.NewTicker(time.Second).C {
|
||||
syscall.Getrusage(syscall.RUSAGE_SELF, &rusage)
|
||||
curTime := time.Now().UnixNano()
|
||||
timeDiff := curTime - prevTime
|
||||
|
||||
@@ -34,7 +34,7 @@ func trackCPUUsage() {
|
||||
prevTime := ctime.Nanoseconds()
|
||||
prevUsage := ktime.Nanoseconds() + utime.Nanoseconds() // Always overflows
|
||||
|
||||
for _ = range time.NewTicker(time.Second).C {
|
||||
for range time.NewTicker(time.Second).C {
|
||||
err := syscall.GetProcessTimes(handle, &ctime, &etime, &ktime, &utime)
|
||||
if err != nil {
|
||||
continue
|
||||
|
||||
@@ -889,19 +889,32 @@ func loadOrCreateConfig() *config.Wrapper {
|
||||
}
|
||||
|
||||
func archiveAndSaveConfig(cfg *config.Wrapper) error {
|
||||
// To prevent previous config from being cleaned up, quickly touch it too
|
||||
now := time.Now()
|
||||
_ = os.Chtimes(cfg.ConfigPath(), now, now) // May return error on Android etc; no worries
|
||||
|
||||
// Copy the existing config to an archive copy
|
||||
archivePath := cfg.ConfigPath() + fmt.Sprintf(".v%d", cfg.Raw().OriginalVersion)
|
||||
l.Infoln("Archiving a copy of old config file format at:", archivePath)
|
||||
if err := osutil.Rename(cfg.ConfigPath(), archivePath); err != nil {
|
||||
if err := copyFile(cfg.ConfigPath(), archivePath); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// Do a regular atomic config sve
|
||||
return cfg.Save()
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
bs, err := ioutil.ReadFile(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := ioutil.WriteFile(dst, bs, 0600); err != nil {
|
||||
// Attempt to clean up
|
||||
os.Remove(dst)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func startAuditing(mainService *suture.Supervisor) {
|
||||
auditFile := timestampedLoc(locAuditLog)
|
||||
fd, err := os.OpenFile(auditFile, os.O_WRONLY|os.O_CREATE|os.O_EXCL, 0600)
|
||||
@@ -946,7 +959,7 @@ func defaultConfig(myName string) config.Configuration {
|
||||
|
||||
if !noDefaultFolder {
|
||||
l.Infoln("Default folder created and/or linked to new config")
|
||||
folderID := rand.String(5) + "-" + rand.String(5)
|
||||
folderID := strings.ToLower(rand.String(5) + "-" + rand.String(5))
|
||||
defaultFolder = config.NewFolderConfiguration(folderID, locations[locDefFolder])
|
||||
defaultFolder.Label = "Default Folder (" + folderID + ")"
|
||||
defaultFolder.RescanIntervalS = 60
|
||||
@@ -1124,15 +1137,16 @@ func autoUpgrade(cfg *config.Wrapper) {
|
||||
// suitable time after they have gone out of fashion.
|
||||
func cleanConfigDirectory() {
|
||||
patterns := map[string]time.Duration{
|
||||
"panic-*.log": 7 * 24 * time.Hour, // keep panic logs for a week
|
||||
"audit-*.log": 7 * 24 * time.Hour, // keep audit logs for a week
|
||||
"index": 14 * 24 * time.Hour, // keep old index format for two weeks
|
||||
"index-v0.11.0.db": 14 * 24 * time.Hour, // keep old index format for two weeks
|
||||
"index-v0.13.0.db": 14 * 24 * time.Hour, // keep old index format for two weeks
|
||||
"index*.converted": 14 * 24 * time.Hour, // keep old converted indexes for two weeks
|
||||
"config.xml.v*": 30 * 24 * time.Hour, // old config versions for a month
|
||||
"*.idx.gz": 30 * 24 * time.Hour, // these should for sure no longer exist
|
||||
"backup-of-v0.8": 30 * 24 * time.Hour, // these neither
|
||||
"panic-*.log": 7 * 24 * time.Hour, // keep panic logs for a week
|
||||
"audit-*.log": 7 * 24 * time.Hour, // keep audit logs for a week
|
||||
"index": 14 * 24 * time.Hour, // keep old index format for two weeks
|
||||
"index-v0.11.0.db": 14 * 24 * time.Hour, // keep old index format for two weeks
|
||||
"index-v0.13.0.db": 14 * 24 * time.Hour, // keep old index format for two weeks
|
||||
"index*.converted": 14 * 24 * time.Hour, // keep old converted indexes for two weeks
|
||||
"config.xml.v*": 30 * 24 * time.Hour, // old config versions for a month
|
||||
"*.idx.gz": 30 * 24 * time.Hour, // these should for sure no longer exist
|
||||
"backup-of-v0.8": 30 * 24 * time.Hour, // these neither
|
||||
"tmp-index-sorter.*": time.Minute, // these should never exist on startup
|
||||
}
|
||||
|
||||
for pat, dur := range patterns {
|
||||
|
||||
@@ -20,9 +20,8 @@ var (
|
||||
func memorySize() (int64, error) {
|
||||
var memoryStatusEx [64]byte
|
||||
binary.LittleEndian.PutUint32(memoryStatusEx[:], 64)
|
||||
p := uintptr(unsafe.Pointer(&memoryStatusEx[0]))
|
||||
|
||||
ret, _, callErr := syscall.Syscall(uintptr(globalMemoryStatusEx), 1, p, 0, 0)
|
||||
ret, _, callErr := syscall.Syscall(uintptr(globalMemoryStatusEx), 1, uintptr(unsafe.Pointer(&memoryStatusEx[0])), 0, 0)
|
||||
if ret == 0 {
|
||||
return 0, callErr
|
||||
}
|
||||
|
||||
@@ -21,8 +21,8 @@ func (m *mockedModel) GlobalDirectoryTree(folder, prefix string, levels int, dir
|
||||
return nil
|
||||
}
|
||||
|
||||
func (m *mockedModel) Completion(device protocol.DeviceID, folder string) float64 {
|
||||
return 0
|
||||
func (m *mockedModel) Completion(device protocol.DeviceID, folder string) model.FolderCompletion {
|
||||
return model.FolderCompletion{}
|
||||
}
|
||||
|
||||
func (m *mockedModel) Override(folder string) {}
|
||||
@@ -31,8 +31,8 @@ func (m *mockedModel) NeedFolderFiles(folder string, page, perpage int) ([]db.Fi
|
||||
return nil, nil, nil, 0
|
||||
}
|
||||
|
||||
func (m *mockedModel) NeedSize(folder string) (nfiles int, bytes int64) {
|
||||
return 0, 0
|
||||
func (m *mockedModel) NeedSize(folder string) (nfiles, ndeletes int, bytes int64) {
|
||||
return 0, 0, 0
|
||||
}
|
||||
|
||||
func (m *mockedModel) ConnectionStats() map[string]interface{} {
|
||||
|
||||
@@ -207,9 +207,11 @@ func (c *folderSummaryService) sendSummary(folder string) {
|
||||
// remote device.
|
||||
comp := c.model.Completion(devCfg.DeviceID, folder)
|
||||
events.Default.Log(events.FolderCompletion, map[string]interface{}{
|
||||
"folder": folder,
|
||||
"device": devCfg.DeviceID.String(),
|
||||
"completion": comp,
|
||||
"folder": folder,
|
||||
"device": devCfg.DeviceID.String(),
|
||||
"completion": comp.CompletionPct,
|
||||
"needBytes": comp.NeedBytes,
|
||||
"globalBytes": comp.GlobalBytes,
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +117,7 @@
|
||||
"New Folder": "Nouveau partage",
|
||||
"Newest First": "Les plus récents en premier",
|
||||
"No": "Non",
|
||||
"No File Versioning": "Pas de version de fichier",
|
||||
"No File Versioning": "Pas de préservation",
|
||||
"Normal": "Normal",
|
||||
"Notice": "Notification",
|
||||
"OK": "OK",
|
||||
@@ -159,15 +159,15 @@
|
||||
"Save": "Sauver",
|
||||
"Scan Time Remaining": "Temps d'analyse restant",
|
||||
"Scanning": "Analyse en cours",
|
||||
"Select the devices to share this folder with.": "Sélectionner les appareils invités à ce partage.",
|
||||
"Select the devices to share this folder with.": "Synchroniser avec :",
|
||||
"Select the folders to share with this device.": "Sélectionner les partages auxquels participe cet appareil.",
|
||||
"Settings": "Configuration",
|
||||
"Share": "Partager",
|
||||
"Share Folder": "Partager",
|
||||
"Share Folders With Device": "Partages avec cet appareil",
|
||||
"Share With Devices": "Partage avec des appareils",
|
||||
"Share With Devices": "Synchroniser avec des appareils",
|
||||
"Share this folder?": "Acceptez-vous ce partage ?",
|
||||
"Shared With": "Partagé avec",
|
||||
"Shared With": "Synchronisé avec",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Identifiant court du partage. Il sera le même sur l'ensemble des appareils du groupe.",
|
||||
"Show ID": "Afficher mon ID",
|
||||
"Show QR": "Afficher l'image QR",
|
||||
@@ -175,11 +175,11 @@
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Affiché à la place de l'ID de l'appareil dans le groupe. Si laissé vide, il sera mis à jour par le nom proposé par l'appareil distant.",
|
||||
"Shutdown": "Arrêter",
|
||||
"Shutdown Complete": "Arrêté !",
|
||||
"Simple File Versioning": "Suivi simple des versions de fichier",
|
||||
"Simple File Versioning": "Suivi simple des versions de fichiers",
|
||||
"Single level wildcard (matches within a directory only)": "Astérisque à un seul niveau (correspond uniquement à l’intérieur du répertoire)",
|
||||
"Smallest First": "Les plus petits en premier",
|
||||
"Source Code": "Code source",
|
||||
"Staggered File Versioning": "Versions échelonnées de fichier",
|
||||
"Staggered File Versioning": "Versions échelonnées de fichiers",
|
||||
"Start Browser": "Démarrer le navigateur web",
|
||||
"Statistics": "Statistiques",
|
||||
"Stopped": "Arrêté",
|
||||
@@ -222,7 +222,7 @@
|
||||
"This Device": "Cet appareil",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Ceci peut aisément permettre à un intrus de lire et modifier n'importe quel fichier de votre ordinateur. ",
|
||||
"This is a major version upgrade.": "Ceci est une mise à jour majeure.",
|
||||
"Trash Can File Versioning": "Gestion des versions de fichier style poubelle.",
|
||||
"Trash Can File Versioning": "Style poubelle.",
|
||||
"Unknown": "Inconnu",
|
||||
"Unshared": "Non partagé",
|
||||
"Unused": "Non utilisé",
|
||||
|
||||
@@ -93,7 +93,7 @@
|
||||
"Ignore Permissions": "Ignorer les permissions",
|
||||
"Incoming Rate Limit (KiB/s)": "Limite du débit de réception (Ko/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Une configuration incorrecte peut créer des dommages dans vos répertoires et mettre Syncthing hors-service.",
|
||||
"Introducer": "introductrice",
|
||||
"Introducer": "introducteur",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Inverser la condition donnée (i.e. ne pas exclure)",
|
||||
"Keep Versions": "Conserver les versions",
|
||||
"Largest First": "Les plus volumineux en premier",
|
||||
@@ -117,7 +117,7 @@
|
||||
"New Folder": "Nouveau partage",
|
||||
"Newest First": "Les plus récents en premier",
|
||||
"No": "Non",
|
||||
"No File Versioning": "Pas de préservation des fichiers",
|
||||
"No File Versioning": "Pas de préservation",
|
||||
"Normal": "Normal",
|
||||
"Notice": "Notification",
|
||||
"OK": "OK",
|
||||
@@ -129,7 +129,7 @@
|
||||
"Out of Sync Items": "Fichiers non synchronisés",
|
||||
"Outgoing Rate Limit (KiB/s)": "Limite du débit d'émission (Ko/s)",
|
||||
"Override Changes": "Écraser les changements",
|
||||
"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": "Chemin vers le répertoire dans l'appareil local. Il sera créé s'il n'existe pas. Le caractère tilde (~) peut être utilisé comme raccourci vers",
|
||||
"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": "Chemin vers le répertoire dans l'appareil local. Il sera créé s'il n'existe pas. Le caractère tilde (~, ou ~+Espace sous Windows+Azerty) peut être utilisé comme raccourci vers",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Chemin où les copies doivent être conservées (laisser vide pour le chemin par défaut de .stversions dans le répertoire)",
|
||||
"Pause": "Pause",
|
||||
"Paused": "En pause",
|
||||
@@ -159,15 +159,15 @@
|
||||
"Save": "Enregistrer",
|
||||
"Scan Time Remaining": "Temps d'analyse restant",
|
||||
"Scanning": "Analyse en cours",
|
||||
"Select the devices to share this folder with.": "Sélectionner les appareils invités à ce partage.",
|
||||
"Select the folders to share with this device.": "Sélectionner les partages auxquels cet appareil participe.",
|
||||
"Select the devices to share this folder with.": "Synchroniser avec :",
|
||||
"Select the folders to share with this device.": "Sélectionner les partages auxquels participe cet appareil.",
|
||||
"Settings": "Configuration",
|
||||
"Share": "Partager",
|
||||
"Share Folder": "Partager",
|
||||
"Share Folders With Device": "Partages avec cet appareil",
|
||||
"Share With Devices": "Partage avec des appareils",
|
||||
"Share With Devices": "Synchroniser avec des appareils",
|
||||
"Share this folder?": "Acceptez-vous ce partage ?",
|
||||
"Shared With": "Partagé avec",
|
||||
"Shared With": "Synchronisé avec",
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Identifiant court du partage. Il sera le même sur l'ensemble des appareils du groupe.",
|
||||
"Show ID": "Afficher mon ID",
|
||||
"Show QR": "Afficher le QR",
|
||||
@@ -175,11 +175,11 @@
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Affiché à la place de l'ID de l'appareil dans le groupe. Si laissé vide, il sera mis à jour par le nom proposé par l'appareil distant.",
|
||||
"Shutdown": "Arrêter",
|
||||
"Shutdown Complete": "Arrêté !",
|
||||
"Simple File Versioning": "Suivi simple des versions de fichier",
|
||||
"Simple File Versioning": "Suivi simple des versions de fichiers",
|
||||
"Single level wildcard (matches within a directory only)": "Astérisque à un seul niveau (correspond uniquement à l’intérieur du répertoire)",
|
||||
"Smallest First": "Les plus petits en premier",
|
||||
"Source Code": "Code source",
|
||||
"Staggered File Versioning": "Versions échelonnées de fichier",
|
||||
"Staggered File Versioning": "Versions échelonnées de fichiers",
|
||||
"Start Browser": "Démarrer le navigateur web",
|
||||
"Statistics": "Statistiques",
|
||||
"Stopped": "Arrêté",
|
||||
@@ -222,7 +222,7 @@
|
||||
"This Device": "Cet appareil",
|
||||
"This can easily give hackers access to read and change any files on your computer.": "Ceci peut aisément permettre à un intrus de lire et modifier n'importe quel fichier de votre ordinateur.",
|
||||
"This is a major version upgrade.": "Ceci est une mise à jour majeure.",
|
||||
"Trash Can File Versioning": "Préservation style poubelle",
|
||||
"Trash Can File Versioning": "Style poubelle",
|
||||
"Unknown": "Inconnu",
|
||||
"Unshared": "Non partagé",
|
||||
"Unused": "Non utilisé",
|
||||
|
||||
@@ -35,7 +35,7 @@
|
||||
"Connection Type": "Tilkoplingstype",
|
||||
"Copied from elsewhere": "Kopiert frå ein annan stad",
|
||||
"Copied from original": "Kopiert frå originalen",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Copyright © 2014-2016 the following Contributors:",
|
||||
"Copyright © 2014-2016 the following Contributors:": "Opphavsrett © 2014-2016 for følgjande bidragsyterar:",
|
||||
"Copyright © 2015 the following Contributors:": "Opphavsrett © 2015 og desse bidragsytarane:",
|
||||
"Danger!": "Fare!",
|
||||
"Delete": "Slett",
|
||||
@@ -123,7 +123,7 @@
|
||||
"OK": "OK",
|
||||
"Off": "Av",
|
||||
"Oldest First": "Elste fyrst",
|
||||
"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.": "Valfri merkelapp på katalogen. Denne kan være ulik på andre enheter.",
|
||||
"Options": "Val",
|
||||
"Out of Sync": "Ikkje synkronisert",
|
||||
"Out of Sync Items": "Ikkje-synkroniserte element",
|
||||
@@ -147,7 +147,7 @@
|
||||
"Release Notes": "Utgivingsnotat",
|
||||
"Remote Devices": "Eksterne Einingar",
|
||||
"Remove": "Fjern",
|
||||
"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.",
|
||||
"Required identifier for the folder. Must be the same on all cluster devices.": "Påkrevd identifikator for katalogen. Denne må være lik på alle einingar i samme klynge.",
|
||||
"Rescan": "Skann På Ny",
|
||||
"Rescan All": "Skann alle på nytt",
|
||||
"Rescan Interval": "Skanneintervall",
|
||||
@@ -197,7 +197,7 @@
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Samla statistikk er opent tilgjengeleg på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Instillingane har blitt lagra men ikkje aktivert. Syncthing må starta på ny for å aktivera dei nye instillingane.",
|
||||
"The device ID cannot be blank.": "Eining ID kan ikkje vera tom.",
|
||||
"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).": "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).",
|
||||
"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).": "Einings-IDen som skal oppgis her kan hentast fram via \"Rediger > Vis ID\"-dialogboksen på den andre eininga. Mellomrom og bindestrek er valfritt (blir ignorert).",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Einings-ID-en du skal nytta her finn du i meldingsvindauget \"Rediger > Vis ID\" på den andre eininga. Mellomrom og bindestrek er valfrie (vert ignorert).",
|
||||
"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.": "Den krypterte bruksrapporten vert send dagleg. Han vert nytta til å spora vanlege plattformer, mappestorleikar og programutgåvene. Om datasettet endrar seg, vil dette meldingsvindauget dukka opp att og du vil verta beden om å godkjenna det.",
|
||||
"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.": "Einings-ID-en er ikkje gyldig. Han må vera på 52 eller 56 teikn og vera samansett av bokstavar og tal med valfrie mellomrom og bindestrekar.",
|
||||
@@ -237,7 +237,7 @@
|
||||
"Version": "Versjon",
|
||||
"Versions Path": "Utgåvebane",
|
||||
"Versions are automatically deleted if they are older than the maximum age or exceed the number of files allowed in an interval.": "Utgåver vert automatisk sletta når maksimal levetid er nådd eller når det høgaste tillate talet på filer innan eit intervall vert overskride.",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Warning, this path is a subdirectory of an existing folder \"{{otherFolder}}\".",
|
||||
"Warning, this path is a subdirectory of an existing folder \"{%otherFolder%}\".": "Åtvaring, denne banen er ei undermappe av den eksisterande mappa \"{{otherFolder}}\".",
|
||||
"When adding a new device, keep in mind that this device must be added on the other side too.": "Hugs at når ei ny eining vert lagt til må ho òg leggjast til på andre sida.",
|
||||
"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.": "Hugs at når ei ny mappe vert lagt til, vert mappe-ID-en brukt til å binda saman mappene mellom einingane. Det er skilnad på store og små bokstavar, så ID-ane må vera identiske på alle einingane.",
|
||||
"Yes": "Ja",
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
"Danger!": "Perigo!",
|
||||
"Delete": "Apagar",
|
||||
"Deleted": "Apagado",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Dispositivo \"{{name}}\" ({{device}} em {{address}}) deseja se conectar. Adicionar novo dispositivo?",
|
||||
"Device \"{%name%}\" ({%device%} at {%address%}) wants to connect. Add new device?": "Dispositivo \"{{name}}\" ({{device}} em {{address}}) quer se conectar. Adicionar novo dispositivo?",
|
||||
"Device ID": "ID do dispositivo",
|
||||
"Device Identification": "Identificação do dispositivo",
|
||||
"Device Name": "Nome do dispositivo",
|
||||
@@ -246,6 +246,6 @@
|
||||
"full documentation": "documentação completa",
|
||||
"items": "itens",
|
||||
"{%device%} wants to share folder \"{%folder%}\".": "{{device}} quer compartilhar a pasta \"{{folder}}\".",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} deseja compartilhar a pasta \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderLabel%}\" ({%folder%}).": "{{device}} quer compartilhar a pasta \"{{folderLabel}}\" ({{folder}}).",
|
||||
"{%device%} wants to share folder \"{%folderlabel%}\" ({%folder%}).": "{{device}} quer compartilhar a pasta \"{{folderlabel}}\" ({{folder}})."
|
||||
}
|
||||
@@ -42,12 +42,12 @@
|
||||
"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": "Enhets ID",
|
||||
"Device Identification": "Enhetsidentifikation",
|
||||
"Device Name": "Enhetens namn",
|
||||
"Device Identification": "Enhets identifikation",
|
||||
"Device Name": "Enhets namn",
|
||||
"Device {%device%} ({%address%}) wants to connect. Add new device?": "Enheten {{device}} ({{address}}) vill ansluta. Lägg till ny enhet?",
|
||||
"Devices": "Enheter",
|
||||
"Disconnected": "Frånkopplad",
|
||||
"Discovery": "Upptäckt",
|
||||
"Discovery": "Annonsering",
|
||||
"Documentation": "Dokumentation",
|
||||
"Download Rate": "Nedladdningshastighet",
|
||||
"Downloaded": "Hämtat",
|
||||
@@ -80,18 +80,18 @@
|
||||
"GUI": "Grafiskt gränssnitt",
|
||||
"GUI Authentication Password": "Lösenord för GUI",
|
||||
"GUI Authentication User": "Användare för GUI",
|
||||
"GUI Listen Addresses": "Lyssningsadresser för GUI",
|
||||
"GUI Listen Addresses": "Lyssnadresser för GUI",
|
||||
"Generate": "Generera",
|
||||
"Global Discovery": "Global upptäckt",
|
||||
"Global Discovery Server": "Global upptäcktsserver",
|
||||
"Global Discovery Servers": "Globala upptäcktsservrar",
|
||||
"Global Discovery": "Global annonsering",
|
||||
"Global Discovery Server": "Global annonseringsserver",
|
||||
"Global Discovery Servers": "Globala annonseringsservrar",
|
||||
"Global State": "Global status",
|
||||
"Help": "Hjälp",
|
||||
"Home page": "Hemsida",
|
||||
"Ignore": "Ignorera",
|
||||
"Ignore Patterns": "Ignorera mönster",
|
||||
"Ignore Permissions": "Ignorera rättigheter",
|
||||
"Incoming Rate Limit (KiB/s)": "Nedladdningshastighetsgräns (KiB/s)",
|
||||
"Incoming Rate Limit (KiB/s)": "Inkommande hastighetsbegränsning (KiB/s)",
|
||||
"Incorrect configuration may damage your folder contents and render Syncthing inoperable.": "Inkorrekt konfiguration kan skada innehållet i katalogen and få Syncthing att sluta fungera.",
|
||||
"Introducer": "introduktör",
|
||||
"Inversion of the given condition (i.e. do not exclude)": "Vänder på villkoret, d.v.s. exkluderar inte.",
|
||||
@@ -102,7 +102,7 @@
|
||||
"Last seen": "Senast sedd",
|
||||
"Later": "Senare",
|
||||
"Listeners": "Lyssnare",
|
||||
"Local Discovery": "Lokal upptäckt",
|
||||
"Local Discovery": "Lokal annonsering",
|
||||
"Local State": "Lokal status",
|
||||
"Local State (Total)": "Lokal status (totalt)",
|
||||
"Major Upgrade": "Stor uppgradering",
|
||||
@@ -127,7 +127,7 @@
|
||||
"Options": "Alternativ",
|
||||
"Out of Sync": "Osynkroniserad",
|
||||
"Out of Sync Items": "Osynkroniserade objekt",
|
||||
"Outgoing Rate Limit (KiB/s)": "Uppladdningshastighetsgräns (KiB/s)",
|
||||
"Outgoing Rate Limit (KiB/s)": "Utgående hastighetsbegränsning (KiB/s)",
|
||||
"Override Changes": "Åsidosätt förändringar",
|
||||
"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": "Sökväg till katalogen på din dator. Kommer att skapas om det inte finns. Tecknet tilde (~) kan användas som en genväg för",
|
||||
"Path where versions should be stored (leave empty for the default .stversions folder in the folder).": "Sökväg där versioner sparas (lämna tomt för att använda .stversions i den ordinarie katalogen).",
|
||||
@@ -171,8 +171,8 @@
|
||||
"Short identifier for the folder. Must be the same on all cluster devices.": "Kort identifieringssträng för katalogen. Måste vara samma på alla enheter i klustret.",
|
||||
"Show ID": "Visa ID",
|
||||
"Show QR": "Visa QR",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas i stället för enhetens ID. Skickas till andra enheter som namn på denna enhet.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas i stället för enhetens ID. Sätts till namnet på den andra enheten vid första anslutning om det lämnas tomt.",
|
||||
"Shown instead of Device ID in the cluster status. Will be advertised to other devices as an optional default name.": "Visas i stället för enhets ID i samlingsstatusen. Skickas till andra enheter som namn på denna enhet.",
|
||||
"Shown instead of Device ID in the cluster status. Will be updated to the name the device advertises if left empty.": "Visas i stället för enhets ID i samlingsstatusen. Sätts till namnet på den andra enheten vid första anslutning om det lämnas tomt.",
|
||||
"Shutdown": "Stäng av",
|
||||
"Shutdown Complete": "Avstängning klar",
|
||||
"Simple File Versioning": "Enkel filversionshantering",
|
||||
@@ -184,7 +184,7 @@
|
||||
"Statistics": "Statistik",
|
||||
"Stopped": "Stoppad",
|
||||
"Support": "Support",
|
||||
"Sync Protocol Listen Addresses": "Synkroniseringsprotokollets lyssningsadresser",
|
||||
"Sync Protocol Listen Addresses": "Synkroniseringsprotokollets lyssnaradresser",
|
||||
"Syncing": "Synkroniserar",
|
||||
"Syncthing has been shut down.": "Syncthing har stängts.",
|
||||
"Syncthing includes the following software or portions thereof:": "Syncthing innehåller följande mjukvarupaket eller delar av dem:",
|
||||
@@ -196,11 +196,11 @@
|
||||
"The aggregated statistics are publicly available at the URL below.": "Den aggregerade statistiken är offentligt tillgängliga på webbadressen nedan.",
|
||||
"The aggregated statistics are publicly available at {%url%}.": "Sammanställd statistik finns publikt tillgänglig på {{url}}.",
|
||||
"The configuration has been saved but not activated. Syncthing must restart to activate the new configuration.": "Konfigurationen har sparats men inte aktiverats. Syncthing måste startas om för att aktivera den nya konfigurationen.",
|
||||
"The device ID cannot be blank.": "Enhetens ID kan inte vara tomt.",
|
||||
"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).": "Enhetens ID som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhetens ID som behövs här kan du hitta i \"Redigera > Visa ID\"-dialogen på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
|
||||
"The device ID cannot be blank.": "Enhets ID:t kan inte vara tomt.",
|
||||
"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).": "Enhets ID:t som behövs här kan du hitta i \"Funktioner > Visa ID\"-dialogrutan på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
|
||||
"The device ID to enter here can be found in the \"Edit > Show ID\" dialog on the other device. Spaces and dashes are optional (ignored).": "Enhets ID:t som behövs här kan du hitta i \"Funktioner > Visa ID\"-dialogrutan på den andra enheten. Mellanrum och bindestreck är valfria (ignoreras).",
|
||||
"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.": "Den krypterade användarstatistiken skickas dagligen. Den används för att spåra vanliga plattformar, katalogstorlekar och versioner. Om datan som rapporteras ändras så kommer du att bli tillfrågad igen.",
|
||||
"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.": "Det inmatade enhetens ID verkar inte korrekt. Det ska vara en 52 eller 56 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
|
||||
"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.": "Det inmatade enhets ID:t verkar inte korrekt. Det ska vara en 52 eller 56 teckens sträng bestående av siffror och bokstäver, eventuellt med mellanrum och bindestreck.",
|
||||
"The first command line parameter is the folder path and the second parameter is the relative path in the folder.": "Den första kommandoparametern är sökvägen till mappen och den andra parametern är den relativa sökvägen i mappen.",
|
||||
"The folder ID cannot be blank.": "Katalogens ID får inte vara tomt.",
|
||||
"The folder ID must be a short identifier (64 characters or less) consisting of letters, numbers and the dot (.), dash (-) and underscode (_) characters only.": "Katalogens ID måste vara en kort sträng (64 tecken eller mindre), bestående av endast bokstäver, siffror, punkt (.), bindestreck (-) och understreck (_).",
|
||||
|
||||
@@ -309,28 +309,8 @@ angular.module('syncthing.core')
|
||||
if (!$scope.completion[data.device]) {
|
||||
$scope.completion[data.device] = {};
|
||||
}
|
||||
$scope.completion[data.device][data.folder] = data.completion;
|
||||
|
||||
var tot = 0,
|
||||
cnt = 0,
|
||||
isComplete = true;
|
||||
for (var cmp in $scope.completion[data.device]) {
|
||||
if (cmp === "_total") {
|
||||
continue;
|
||||
}
|
||||
tot += $scope.completion[data.device][cmp] * $scope.model[cmp].globalBytes;
|
||||
cnt += $scope.model[cmp].globalBytes;
|
||||
if ($scope.completion[data.device][cmp] != 100) {
|
||||
isComplete = false;
|
||||
}
|
||||
}
|
||||
//To be sure that we won't get any rounding errors resulting in non-100% status when it should be
|
||||
if (isComplete) {
|
||||
$scope.completion[data.device]._total = 100;
|
||||
}
|
||||
else {
|
||||
$scope.completion[data.device]._total = tot / cnt;
|
||||
}
|
||||
$scope.completion[data.device][data.folder] = data;
|
||||
recalcCompletion(data.device);
|
||||
});
|
||||
|
||||
$scope.$on(Events.FOLDER_ERRORS, function (event, arg) {
|
||||
@@ -458,6 +438,32 @@ angular.module('syncthing.core')
|
||||
}
|
||||
}
|
||||
|
||||
function recalcCompletion(device) {
|
||||
var total = 0, needed = 0, deletes = 0;
|
||||
for (var folder in $scope.completion[device]) {
|
||||
if (folder === "_total") {
|
||||
continue;
|
||||
}
|
||||
total += $scope.completion[device][folder].globalBytes;
|
||||
needed += $scope.completion[device][folder].needBytes;
|
||||
deletes += $scope.completion[device][folder].needDeletes;
|
||||
}
|
||||
if (total == 0) {
|
||||
$scope.completion[device]._total = 100;
|
||||
} else {
|
||||
$scope.completion[device]._total = 100 * (1 - needed / total);
|
||||
}
|
||||
|
||||
if (needed == 0 && deletes > 0) {
|
||||
// We don't need any data, but we have deletes that we need
|
||||
// to do. Drop down the completion percentage to indicate
|
||||
// that we have stuff to do.
|
||||
$scope.completion[device]._total = 95;
|
||||
}
|
||||
|
||||
console.log("recalcCompletion", device, $scope.completion[device]);
|
||||
}
|
||||
|
||||
function refreshCompletion(device, folder) {
|
||||
if (device === $scope.myID) {
|
||||
return;
|
||||
@@ -467,30 +473,8 @@ angular.module('syncthing.core')
|
||||
if (!$scope.completion[device]) {
|
||||
$scope.completion[device] = {};
|
||||
}
|
||||
$scope.completion[device][folder] = data.completion;
|
||||
|
||||
var tot = 0,
|
||||
cnt = 0,
|
||||
isComplete = true;
|
||||
for (var cmp in $scope.completion[device]) {
|
||||
if (cmp === "_total") {
|
||||
continue;
|
||||
}
|
||||
tot += $scope.completion[device][cmp] * $scope.model[cmp].globalBytes;
|
||||
cnt += $scope.model[cmp].globalBytes;
|
||||
if ($scope.completion[device][cmp] != 100) {
|
||||
isComplete = false;
|
||||
}
|
||||
}
|
||||
//To be sure that we won't get any rounding errors resulting in non-100% status when it should be
|
||||
if (isComplete) {
|
||||
$scope.completion[device]._total = 100;
|
||||
}
|
||||
else {
|
||||
$scope.completion[device]._total = tot / cnt;
|
||||
}
|
||||
|
||||
console.log("refreshCompletion", device, folder, $scope.completion[device]);
|
||||
$scope.completion[device][folder] = data;
|
||||
recalcCompletion(device);
|
||||
}).error($scope.emitHTTPError);
|
||||
}
|
||||
|
||||
|
||||
60
jenkins/build-linux.bash
Executable file
60
jenkins/build-linux.bash
Executable file
@@ -0,0 +1,60 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 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/.
|
||||
|
||||
# This script should be run by Jenkins as './src/github.com/syncthing/syncthing/jenkins/build-linux.bash',
|
||||
# that is, it should be run from $GOPATH.
|
||||
|
||||
. src/github.com/syncthing/syncthing/jenkins/common.bash
|
||||
|
||||
init
|
||||
|
||||
# after init we are in the source directory
|
||||
|
||||
clean
|
||||
fetchExtra
|
||||
buildSource
|
||||
build
|
||||
test
|
||||
testWithCoverage
|
||||
|
||||
platforms=(
|
||||
dragonfly-amd64
|
||||
freebsd-amd64 freebsd-386
|
||||
linux-amd64 linux-386 linux-arm linux-arm64 linux-ppc64 linux-ppc64le
|
||||
netbsd-amd64 netbsd-386
|
||||
openbsd-amd64 openbsd-386
|
||||
solaris-amd64
|
||||
)
|
||||
|
||||
echo Building
|
||||
for plat in "${platforms[@]}"; do
|
||||
echo Building "$plat"
|
||||
|
||||
goos="${plat%-*}"
|
||||
goarch="${plat#*-}"
|
||||
go run build.go -goos "$goos" -goarch "$goarch" tar
|
||||
mv *.tar.gz "$WORKSPACE"
|
||||
echo
|
||||
done
|
||||
|
||||
go run build.go -goarch amd64 deb
|
||||
fakeroot sh -c 'chown -R root:root deb ; dpkg-deb -b deb .'
|
||||
mv *.deb "$WORKSPACE"
|
||||
|
||||
go run build.go -goarch i386 deb
|
||||
fakeroot sh -c 'chown -R root:root deb ; dpkg-deb -b deb .'
|
||||
mv *.deb "$WORKSPACE"
|
||||
|
||||
go run build.go -goarch armel deb
|
||||
fakeroot sh -c 'chown -R root:root deb ; dpkg-deb -b deb .'
|
||||
mv *.deb "$WORKSPACE"
|
||||
|
||||
go run build.go -goarch armhf deb
|
||||
fakeroot sh -c 'chown -R root:root deb ; dpkg-deb -b deb .'
|
||||
mv *.deb "$WORKSPACE"
|
||||
37
jenkins/build-macos.bash
Executable file
37
jenkins/build-macos.bash
Executable file
@@ -0,0 +1,37 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 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/.
|
||||
|
||||
# This script should be run by Jenkins as './src/github.com/syncthing/syncthing/jenkins/build-macos.bash',
|
||||
# that is, it should be run from $GOPATH.
|
||||
|
||||
. src/github.com/syncthing/syncthing/jenkins/common.bash
|
||||
|
||||
init
|
||||
|
||||
# after init we are in the source directory
|
||||
|
||||
clean
|
||||
fetchExtra
|
||||
build
|
||||
test
|
||||
|
||||
platforms=(
|
||||
darwin-amd64 darwin-386
|
||||
)
|
||||
|
||||
echo Building
|
||||
for plat in "${platforms[@]}"; do
|
||||
echo Building "$plat"
|
||||
|
||||
goos="${plat%-*}"
|
||||
goarch="${plat#*-}"
|
||||
go run build.go -goos "$goos" -goarch "$goarch" tar
|
||||
mv *.tar.gz "$WORKSPACE"
|
||||
echo
|
||||
done
|
||||
54
jenkins/build-windows.bat
Normal file
54
jenkins/build-windows.bat
Normal file
@@ -0,0 +1,54 @@
|
||||
@echo off
|
||||
|
||||
rem Copyright (C) 2016 The Syncthing Authors.
|
||||
rem
|
||||
rem This Source Code Form is subject to the terms of the Mozilla Public
|
||||
rem License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
rem You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
rem This batch file should be run from the GOPATH.
|
||||
rem It expects to run on amd64, for windows-amd64 Go to be installed in C:\go
|
||||
rem and for windows-386 Go to be installed in C:\go-386.
|
||||
|
||||
rem cURL should be installed in C:\Program Files\cURL.
|
||||
|
||||
set ORIGPATH="C:\Program Files\cURL\bin";%PATH%
|
||||
set PATH=c:\go\bin;%ORIGPATH%
|
||||
set GOROOT=c:\go
|
||||
|
||||
cd >gopath
|
||||
set /p GOPATH= <gopath
|
||||
|
||||
cd src\github.com\syncthing\syncthing
|
||||
|
||||
echo Initializing ^& cleaning
|
||||
go version
|
||||
git clean -fxd || goto error
|
||||
echo.
|
||||
|
||||
echo Fetching extras
|
||||
mkdir extra
|
||||
curl -s -L -o extra/Getting-Started.pdf https://docs.syncthing.net/pdf/Getting-Started.pdf || goto :error
|
||||
curl -s -L -o extra/FAQ.pdf https://docs.syncthing.net/pdf/FAQ.pdf || goto :error
|
||||
echo.
|
||||
|
||||
echo Testing
|
||||
go run build.go test || goto :error
|
||||
echo.
|
||||
|
||||
echo Building (amd64)
|
||||
go run build.go zip || goto :error
|
||||
echo.
|
||||
|
||||
set PATH=c:\go-386\bin;%ORIGPATH%
|
||||
set GOROOT=c:\go-386
|
||||
|
||||
echo building (386)
|
||||
go run build.go zip || goto :error
|
||||
echo.
|
||||
|
||||
goto :EOF
|
||||
|
||||
:error
|
||||
echo code #%errorlevel%.
|
||||
exit /b %errorlevel%
|
||||
85
jenkins/common.bash
Normal file
85
jenkins/common.bash
Normal file
@@ -0,0 +1,85 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
# 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/.
|
||||
|
||||
ulimit -t 600 || true
|
||||
ulimit -d 1024000 || true
|
||||
ulimit -m 1024000 || true
|
||||
|
||||
export CGO_ENABLED=0
|
||||
export GO386=387
|
||||
export GOARM=5
|
||||
|
||||
function init {
|
||||
echo Initializing
|
||||
export GOPATH=$(pwd)
|
||||
export WORKSPACE="${WORKSPACE:-$GOPATH}"
|
||||
go version
|
||||
rm -f *.tar.gz *.zip *.deb
|
||||
cd src/github.com/syncthing/syncthing
|
||||
|
||||
version=$(git describe)
|
||||
echo "Building $version"
|
||||
echo
|
||||
}
|
||||
|
||||
function clean {
|
||||
echo Cleaning
|
||||
rm -rf "$GOPATH/pkg"
|
||||
git clean -fxd
|
||||
git fetch --prune
|
||||
echo
|
||||
}
|
||||
|
||||
function fetchExtra {
|
||||
echo Fetching extra resources
|
||||
mkdir extra
|
||||
curl -s -o extra/Getting-Started.pdf http://docs.syncthing.net/pdf/Getting-Started.pdf
|
||||
curl -s -o extra/FAQ.pdf http://docs.syncthing.net/pdf/FAQ.pdf
|
||||
echo
|
||||
}
|
||||
|
||||
function checkAuthorsCopyright {
|
||||
echo Basic metadata checks
|
||||
go run script/check-authors.go
|
||||
go run script/check-copyright.go lib/ cmd/ script/
|
||||
echo
|
||||
}
|
||||
|
||||
function build {
|
||||
echo Build
|
||||
go run build.go
|
||||
echo
|
||||
}
|
||||
|
||||
function test {
|
||||
echo Test with race
|
||||
CGO_ENABLED=1 go run build.go -race test
|
||||
echo
|
||||
}
|
||||
|
||||
function testWithCoverage {
|
||||
echo Test with coverage
|
||||
CGO_ENABLED=1 ./build.sh test-cov
|
||||
|
||||
notCovered=$(egrep -c '\s0$' coverage.out)
|
||||
total=$(wc -l coverage.out | awk '{print $1}')
|
||||
coverPct=$(awk "BEGIN{print (1 - $notCovered / $total) * 100}")
|
||||
echo "$coverPct" > "coverage.txt"
|
||||
echo "Test coverage is $coverPct%%"
|
||||
echo
|
||||
}
|
||||
|
||||
function buildSource {
|
||||
echo Archiving source
|
||||
echo "$version" > RELEASE
|
||||
pushd .. >/dev/null
|
||||
tar c -z -f "$WORKSPACE/syncthing-source-$version.tar.gz" --exclude .git syncthing
|
||||
popd >/dev/null
|
||||
echo
|
||||
}
|
||||
@@ -13,15 +13,16 @@ import (
|
||||
)
|
||||
|
||||
type GUIConfiguration struct {
|
||||
Enabled bool `xml:"enabled,attr" json:"enabled" default:"true"`
|
||||
RawAddress string `xml:"address" json:"address" default:"127.0.0.1:8384"`
|
||||
User string `xml:"user,omitempty" json:"user"`
|
||||
Password string `xml:"password,omitempty" json:"password"`
|
||||
RawUseTLS bool `xml:"tls,attr" json:"useTLS"`
|
||||
APIKey string `xml:"apikey,omitempty" json:"apiKey"`
|
||||
InsecureAdminAccess bool `xml:"insecureAdminAccess,omitempty" json:"insecureAdminAccess"`
|
||||
Theme string `xml:"theme" json:"theme" default:"default"`
|
||||
Debugging bool `xml:"debugging,attr" json:"debugging"`
|
||||
Enabled bool `xml:"enabled,attr" json:"enabled" default:"true"`
|
||||
RawAddress string `xml:"address" json:"address" default:"127.0.0.1:8384"`
|
||||
User string `xml:"user,omitempty" json:"user"`
|
||||
Password string `xml:"password,omitempty" json:"password"`
|
||||
RawUseTLS bool `xml:"tls,attr" json:"useTLS"`
|
||||
APIKey string `xml:"apikey,omitempty" json:"apiKey"`
|
||||
InsecureAdminAccess bool `xml:"insecureAdminAccess,omitempty" json:"insecureAdminAccess"`
|
||||
Theme string `xml:"theme" json:"theme" default:"default"`
|
||||
Debugging bool `xml:"debugging,attr" json:"debugging"`
|
||||
InsecureSkipHostCheck bool `xml:"insecureSkipHostcheck,omitempty" json:"insecureSkipHostcheck"`
|
||||
}
|
||||
|
||||
func (c GUIConfiguration) Address() string {
|
||||
|
||||
@@ -47,8 +47,11 @@ func (f FileInfoTruncated) HasPermissionBits() bool {
|
||||
}
|
||||
|
||||
func (f FileInfoTruncated) FileSize() int64 {
|
||||
if f.IsDirectory() || f.IsDeleted() {
|
||||
return 128
|
||||
if f.Deleted {
|
||||
return 0
|
||||
}
|
||||
if f.IsDirectory() {
|
||||
return protocol.SyntheticDirectorySize
|
||||
}
|
||||
return f.Size
|
||||
}
|
||||
|
||||
@@ -12,13 +12,11 @@ import (
|
||||
"os"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
)
|
||||
|
||||
func TestMtimeFS(t *testing.T) {
|
||||
osutil.RemoveAll("testdata")
|
||||
defer osutil.RemoveAll("testdata")
|
||||
os.RemoveAll("testdata")
|
||||
defer os.RemoveAll("testdata")
|
||||
os.Mkdir("testdata", 0755)
|
||||
ioutil.WriteFile("testdata/exists0", []byte("hello"), 0644)
|
||||
ioutil.WriteFile("testdata/exists1", []byte("hello"), 0644)
|
||||
|
||||
@@ -459,33 +459,49 @@ func (m *Model) FolderStatistics() map[string]stats.FolderStatistics {
|
||||
return res
|
||||
}
|
||||
|
||||
type FolderCompletion struct {
|
||||
CompletionPct float64
|
||||
NeedBytes int64
|
||||
GlobalBytes int64
|
||||
NeedDeletes int64
|
||||
}
|
||||
|
||||
// Completion returns the completion status, in percent, for the given device
|
||||
// and folder.
|
||||
func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
|
||||
func (m *Model) Completion(device protocol.DeviceID, folder string) FolderCompletion {
|
||||
m.fmut.RLock()
|
||||
rf, ok := m.folderFiles[folder]
|
||||
m.fmut.RUnlock()
|
||||
if !ok {
|
||||
return 0 // Folder doesn't exist, so we hardly have any of it
|
||||
return FolderCompletion{} // Folder doesn't exist, so we hardly have any of it
|
||||
}
|
||||
|
||||
_, _, tot := rf.GlobalSize()
|
||||
if tot == 0 {
|
||||
return 100 // Folder is empty, so we have all of it
|
||||
// Folder is empty, so we have all of it
|
||||
return FolderCompletion{
|
||||
CompletionPct: 100,
|
||||
}
|
||||
}
|
||||
|
||||
m.pmut.RLock()
|
||||
counts := m.deviceDownloads[device].GetBlockCounts(folder)
|
||||
m.pmut.RUnlock()
|
||||
|
||||
var need, fileNeed, downloaded int64
|
||||
var need, fileNeed, downloaded, deletes int64
|
||||
rf.WithNeedTruncated(device, func(f db.FileIntf) bool {
|
||||
ft := f.(db.FileInfoTruncated)
|
||||
|
||||
// If the file is deleted, we account it only in the deleted column.
|
||||
if ft.Deleted {
|
||||
deletes++
|
||||
return true
|
||||
}
|
||||
|
||||
// This might might be more than it really is, because some blocks can be of a smaller size.
|
||||
downloaded = int64(counts[ft.Name] * protocol.BlockSize)
|
||||
|
||||
fileNeed = ft.Size - downloaded
|
||||
fileNeed = ft.FileSize() - downloaded
|
||||
if fileNeed < 0 {
|
||||
fileNeed = 0
|
||||
}
|
||||
@@ -496,9 +512,23 @@ func (m *Model) Completion(device protocol.DeviceID, folder string) float64 {
|
||||
|
||||
needRatio := float64(need) / float64(tot)
|
||||
completionPct := 100 * (1 - needRatio)
|
||||
|
||||
// If the completion is 100% but there are deletes we need to handle,
|
||||
// drop it down a notch. Hack for consumers that look only at the
|
||||
// percentage (our own GUI does the same calculation as here on it's own
|
||||
// and needs the same fixup).
|
||||
if need == 0 && deletes > 0 {
|
||||
completionPct = 95 // chosen by fair dice roll
|
||||
}
|
||||
|
||||
l.Debugf("%v Completion(%s, %q): %f (%d / %d = %f)", m, device, folder, completionPct, need, tot, needRatio)
|
||||
|
||||
return completionPct
|
||||
return FolderCompletion{
|
||||
CompletionPct: completionPct,
|
||||
NeedBytes: need,
|
||||
GlobalBytes: tot,
|
||||
NeedDeletes: deletes,
|
||||
}
|
||||
}
|
||||
|
||||
func sizeOfFile(f db.FileIntf) (files, deleted int, bytes int64) {
|
||||
@@ -534,7 +564,7 @@ func (m *Model) LocalSize(folder string) (nfiles, deleted int, bytes int64) {
|
||||
}
|
||||
|
||||
// NeedSize returns the number and total size of currently needed files.
|
||||
func (m *Model) NeedSize(folder string) (nfiles int, bytes int64) {
|
||||
func (m *Model) NeedSize(folder string) (nfiles, ndeletes int, bytes int64) {
|
||||
m.fmut.RLock()
|
||||
defer m.fmut.RUnlock()
|
||||
if rf, ok := m.folderFiles[folder]; ok {
|
||||
@@ -546,7 +576,8 @@ func (m *Model) NeedSize(folder string) (nfiles int, bytes int64) {
|
||||
}
|
||||
|
||||
fs, de, by := sizeOfFile(f)
|
||||
nfiles += fs + de
|
||||
nfiles += fs
|
||||
ndeletes += de
|
||||
bytes += by
|
||||
return true
|
||||
})
|
||||
@@ -1704,41 +1735,46 @@ func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error
|
||||
subDirs = []string{""}
|
||||
}
|
||||
|
||||
// Do a scan of the database for each prefix, to check for deleted files.
|
||||
// Do a scan of the database for each prefix, to check for deleted and
|
||||
// ignored files.
|
||||
batch = batch[:0]
|
||||
for _, sub := range subDirs {
|
||||
var iterError error
|
||||
|
||||
fs.WithPrefixedHaveTruncated(protocol.LocalDeviceID, sub, func(fi db.FileIntf) bool {
|
||||
f := fi.(db.FileInfoTruncated)
|
||||
if !f.IsDeleted() {
|
||||
if len(batch) == batchSizeFiles {
|
||||
if err := m.CheckFolderHealth(folder); err != nil {
|
||||
iterError = err
|
||||
return false
|
||||
}
|
||||
m.updateLocalsFromScanning(folder, batch)
|
||||
batch = batch[:0]
|
||||
if len(batch) == batchSizeFiles {
|
||||
if err := m.CheckFolderHealth(folder); err != nil {
|
||||
iterError = err
|
||||
return false
|
||||
}
|
||||
m.updateLocalsFromScanning(folder, batch)
|
||||
batch = batch[:0]
|
||||
}
|
||||
|
||||
if !f.IsInvalid() && (ignores.Match(f.Name).IsIgnored() || symlinkInvalid(folder, f)) {
|
||||
// File has been ignored or an unsupported symlink. Set invalid bit.
|
||||
l.Debugln("setting invalid bit on ignored", f)
|
||||
nf := protocol.FileInfo{
|
||||
Name: f.Name,
|
||||
Type: f.Type,
|
||||
Size: f.Size,
|
||||
ModifiedS: f.ModifiedS,
|
||||
ModifiedNs: f.ModifiedNs,
|
||||
Permissions: f.Permissions,
|
||||
NoPermissions: f.NoPermissions,
|
||||
Invalid: true,
|
||||
Version: f.Version, // The file is still the same, so don't bump version
|
||||
}
|
||||
batch = append(batch, nf)
|
||||
} else if _, err := mtimefs.Lstat(filepath.Join(folderCfg.Path(), f.Name)); err != nil {
|
||||
// File has been deleted.
|
||||
switch {
|
||||
case !f.IsInvalid() && (ignores.Match(f.Name).IsIgnored() || symlinkInvalid(folder, f)):
|
||||
// File was valid at last pass but has been ignored or is an
|
||||
// unsupported symlink. Set invalid bit.
|
||||
l.Debugln("setting invalid bit on ignored", f)
|
||||
nf := protocol.FileInfo{
|
||||
Name: f.Name,
|
||||
Type: f.Type,
|
||||
Size: f.Size,
|
||||
ModifiedS: f.ModifiedS,
|
||||
ModifiedNs: f.ModifiedNs,
|
||||
Permissions: f.Permissions,
|
||||
NoPermissions: f.NoPermissions,
|
||||
Invalid: true,
|
||||
Version: f.Version, // The file is still the same, so don't bump version
|
||||
}
|
||||
batch = append(batch, nf)
|
||||
|
||||
case !f.IsInvalid() && !f.IsDeleted():
|
||||
// The file is valid and not deleted. Lets check if it's
|
||||
// still here.
|
||||
|
||||
if _, err := mtimefs.Lstat(filepath.Join(folderCfg.Path(), f.Name)); err != nil {
|
||||
// We don't specifically verify that the error is
|
||||
// os.IsNotExist because there is a corner case when a
|
||||
// directory is suddenly transformed into a file. When that
|
||||
@@ -1749,7 +1785,7 @@ func (m *Model) internalScanFolderSubdirs(folder string, subDirs []string) error
|
||||
nf := protocol.FileInfo{
|
||||
Name: f.Name,
|
||||
Type: f.Type,
|
||||
Size: f.Size,
|
||||
Size: 0,
|
||||
ModifiedS: f.ModifiedS,
|
||||
ModifiedNs: f.ModifiedNs,
|
||||
Deleted: true,
|
||||
@@ -1914,6 +1950,7 @@ func (m *Model) Override(folder string) {
|
||||
need.Deleted = true
|
||||
need.Blocks = nil
|
||||
need.Version = need.Version.Update(m.shortID)
|
||||
need.Size = 0
|
||||
} else {
|
||||
// We have the file, replace with our version
|
||||
have.Version = have.Version.Merge(need.Version).Update(m.shortID)
|
||||
|
||||
@@ -93,6 +93,7 @@ func TestRequest(t *testing.T) {
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolder("default")
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
m.ScanFolder("default")
|
||||
|
||||
bs := make([]byte, protocol.BlockSize)
|
||||
@@ -168,6 +169,7 @@ func benchmarkIndex(b *testing.B, nfiles int) {
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolder("default")
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
|
||||
files := genFiles(nfiles)
|
||||
m.Index(device1, "default", files)
|
||||
@@ -197,6 +199,7 @@ func benchmarkIndexUpdate(b *testing.B, nfiles, nufiles int) {
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolder("default")
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
|
||||
files := genFiles(nfiles)
|
||||
ufiles := genFiles(nufiles)
|
||||
@@ -278,6 +281,7 @@ func BenchmarkRequest(b *testing.B) {
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
m.ScanFolder("default")
|
||||
|
||||
const n = 1000
|
||||
@@ -346,6 +350,7 @@ func TestDeviceRename(t *testing.T) {
|
||||
m.AddConnection(conn, hello)
|
||||
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
|
||||
if cfg.Devices()[device1].Name != "" {
|
||||
t.Errorf("Device already has a name")
|
||||
@@ -424,6 +429,7 @@ func TestClusterConfig(t *testing.T) {
|
||||
m.AddFolder(cfg.Folders[0])
|
||||
m.AddFolder(cfg.Folders[1])
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
|
||||
cm := m.generateClusterConfig(device2)
|
||||
|
||||
@@ -495,6 +501,7 @@ func TestIgnores(t *testing.T) {
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.StartFolder("default")
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
|
||||
expected := []string{
|
||||
".*",
|
||||
@@ -590,6 +597,7 @@ func TestROScanRecovery(t *testing.T) {
|
||||
m.AddFolder(fcfg)
|
||||
m.StartFolder("default")
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
|
||||
waitFor := func(status string) error {
|
||||
timeout := time.Now().Add(2 * time.Second)
|
||||
@@ -676,6 +684,7 @@ func TestRWScanRecovery(t *testing.T) {
|
||||
m.AddFolder(fcfg)
|
||||
m.StartFolder("default")
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
|
||||
waitFor := func(status string) error {
|
||||
timeout := time.Now().Add(2 * time.Second)
|
||||
@@ -739,6 +748,7 @@ func TestGlobalDirectoryTree(t *testing.T) {
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
m.AddFolder(defaultFolderConfig)
|
||||
m.ServeBackground()
|
||||
defer m.Stop()
|
||||
|
||||
b := func(isfile bool, path ...string) protocol.FileInfo {
|
||||
typ := protocol.FileInfoTypeDirectory
|
||||
@@ -1343,8 +1353,8 @@ func TestIssue3028(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestIssue3164(t *testing.T) {
|
||||
osutil.RemoveAll("testdata/issue3164")
|
||||
defer osutil.RemoveAll("testdata/issue3164")
|
||||
os.RemoveAll("testdata/issue3164")
|
||||
defer os.RemoveAll("testdata/issue3164")
|
||||
|
||||
if err := os.MkdirAll("testdata/issue3164/oktodelete/foobar", 0777); err != nil {
|
||||
t.Fatal(err)
|
||||
@@ -1445,7 +1455,7 @@ func TestIssue2782(t *testing.T) {
|
||||
|
||||
testName := ".syncthing-test." + srand.String(16)
|
||||
testDir := filepath.Join(home, testName)
|
||||
if err := osutil.RemoveAll(testDir); err != nil {
|
||||
if err := os.RemoveAll(testDir); err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
if err := osutil.MkdirAll(testDir+"/syncdir", 0755); err != nil {
|
||||
@@ -1457,7 +1467,7 @@ func TestIssue2782(t *testing.T) {
|
||||
if err := os.Symlink("syncdir", testDir+"/synclink"); err != nil {
|
||||
t.Skip(err)
|
||||
}
|
||||
defer osutil.RemoveAll(testDir)
|
||||
defer os.RemoveAll(testDir)
|
||||
|
||||
db := db.OpenMemory()
|
||||
m := NewModel(defaultConfig, protocol.LocalDeviceID, "device", "syncthing", "dev", db, nil)
|
||||
@@ -1646,6 +1656,109 @@ func TestSharedWithClearedOnDisconnect(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestIssue3496(t *testing.T) {
|
||||
// It seems like lots of deleted files can cause negative completion
|
||||
// percentages. Lets make sure that doesn't happen. Also do some general
|
||||
// checks on the completion calculation stuff.
|
||||
|
||||
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.ScanFolder("default")
|
||||
|
||||
addFakeConn(m, device1)
|
||||
addFakeConn(m, device2)
|
||||
|
||||
// Reach into the model and grab the current file list...
|
||||
|
||||
m.fmut.RLock()
|
||||
fs := m.folderFiles["default"]
|
||||
m.fmut.RUnlock()
|
||||
var localFiles []protocol.FileInfo
|
||||
fs.WithHave(protocol.LocalDeviceID, func(i db.FileIntf) bool {
|
||||
localFiles = append(localFiles, i.(protocol.FileInfo))
|
||||
return true
|
||||
})
|
||||
|
||||
// Mark all files as deleted and fake it as update from device1
|
||||
|
||||
for i := range localFiles {
|
||||
localFiles[i].Deleted = true
|
||||
localFiles[i].Version = localFiles[i].Version.Update(device1.Short())
|
||||
localFiles[i].Blocks = nil
|
||||
}
|
||||
|
||||
// Also add a small file that we're supposed to need, or the global size
|
||||
// stuff will bail out early due to the entire folder being zero size.
|
||||
|
||||
localFiles = append(localFiles, protocol.FileInfo{
|
||||
Name: "fake",
|
||||
Size: 1234,
|
||||
Type: protocol.FileInfoTypeFile,
|
||||
Version: protocol.Vector{Counters: []protocol.Counter{{ID: device1.Short(), Value: 42}}},
|
||||
})
|
||||
|
||||
m.IndexUpdate(device1, "default", localFiles)
|
||||
|
||||
// Check that the completion percentage for us makes sense
|
||||
|
||||
comp := m.Completion(protocol.LocalDeviceID, "default")
|
||||
if comp.NeedBytes > comp.GlobalBytes {
|
||||
t.Errorf("Need more bytes than exist, not possible: %d > %d", comp.NeedBytes, comp.GlobalBytes)
|
||||
}
|
||||
if comp.CompletionPct < 0 {
|
||||
t.Errorf("Less than zero percent complete, not possible: %.02f%%", comp.CompletionPct)
|
||||
}
|
||||
if comp.NeedBytes == 0 {
|
||||
t.Error("Need no bytes even though some files are deleted")
|
||||
}
|
||||
if comp.CompletionPct == 100 {
|
||||
t.Errorf("Fully complete, not possible: %.02f%%", comp.CompletionPct)
|
||||
}
|
||||
t.Log(comp)
|
||||
|
||||
// Check that NeedSize does the correct thing
|
||||
files, deletes, bytes := m.NeedSize("default")
|
||||
if files != 1 || bytes != 1234 {
|
||||
// The one we added synthetically above
|
||||
t.Errorf("Incorrect need size; %d, %d != 1, 1234", files, bytes)
|
||||
}
|
||||
if deletes != len(localFiles)-1 {
|
||||
// The rest
|
||||
t.Errorf("Incorrect need deletes; %d != %d", deletes, len(localFiles)-1)
|
||||
}
|
||||
}
|
||||
|
||||
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{})
|
||||
|
||||
m.ClusterConfig(device1, protocol.ClusterConfig{
|
||||
Folders: []protocol.Folder{
|
||||
{
|
||||
ID: "default",
|
||||
Devices: []protocol.Device{
|
||||
{ID: device1[:]},
|
||||
{ID: device2[:]},
|
||||
},
|
||||
},
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
type fakeAddr struct{}
|
||||
|
||||
func (fakeAddr) Network() string {
|
||||
|
||||
@@ -190,6 +190,9 @@ func (t *ProgressEmitter) CommitConfiguration(from, to config.Configuration) boo
|
||||
defer t.mut.Unlock()
|
||||
|
||||
t.interval = time.Duration(to.Options.ProgressUpdateIntervalS) * time.Second
|
||||
if t.interval < time.Second {
|
||||
t.interval = time.Second
|
||||
}
|
||||
t.minBlocks = to.Options.TempIndexMinBlocks
|
||||
l.Debugln("progress emitter: updated interval", t.interval)
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ func TestProgressEmitter(t *testing.T) {
|
||||
|
||||
p := NewProgressEmitter(c)
|
||||
go p.Serve()
|
||||
p.interval = 0
|
||||
|
||||
expectTimeout(w, t)
|
||||
|
||||
|
||||
@@ -273,7 +273,7 @@ func BenchmarkJobQueuePushPopDone10k(b *testing.B) {
|
||||
for _, f := range files {
|
||||
q.Push(f.Name, 0, time.Time{})
|
||||
}
|
||||
for _ = range files {
|
||||
for range files {
|
||||
n, _ := q.Pop()
|
||||
q.Done(n)
|
||||
}
|
||||
|
||||
@@ -602,7 +602,7 @@ func (f *rwFolder) handleDir(file protocol.FileInfo) {
|
||||
// Most likely a file/link is getting replaced with a directory.
|
||||
// Remove the file/link and fall through to directory creation.
|
||||
case err == nil && (!info.IsDir() || info.Mode()&os.ModeSymlink != 0):
|
||||
err = osutil.InWritableDir(osutil.Remove, realName)
|
||||
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)
|
||||
@@ -687,13 +687,13 @@ func (f *rwFolder) deleteDir(file protocol.FileInfo, matcher *ignore.Matcher) {
|
||||
for _, dirFile := range files {
|
||||
fullDirFile := filepath.Join(file.Name, dirFile)
|
||||
if defTempNamer.IsTemporary(dirFile) || (matcher != nil && matcher.Match(fullDirFile).IsDeletable()) {
|
||||
osutil.RemoveAll(filepath.Join(f.dir, fullDirFile))
|
||||
os.RemoveAll(filepath.Join(f.dir, fullDirFile))
|
||||
}
|
||||
}
|
||||
dir.Close()
|
||||
}
|
||||
|
||||
err = osutil.InWritableDir(osutil.Remove, realName)
|
||||
err = osutil.InWritableDir(os.Remove, realName)
|
||||
if err == nil || os.IsNotExist(err) {
|
||||
// It was removed or it doesn't exist to start with
|
||||
f.dbUpdates <- dbUpdateJob{file, dbUpdateDeleteDir}
|
||||
@@ -740,7 +740,7 @@ func (f *rwFolder) deleteFile(file protocol.FileInfo) {
|
||||
} else if f.versioner != nil {
|
||||
err = osutil.InWritableDir(f.versioner.Archive, realName)
|
||||
} else {
|
||||
err = osutil.InWritableDir(osutil.Remove, realName)
|
||||
err = osutil.InWritableDir(os.Remove, realName)
|
||||
}
|
||||
|
||||
if err == nil || os.IsNotExist(err) {
|
||||
@@ -825,7 +825,7 @@ func (f *rwFolder) renameFile(source, target protocol.FileInfo) {
|
||||
// get rid of. Attempt to delete it instead so that we make *some*
|
||||
// progress. The target is unhandled.
|
||||
|
||||
err = osutil.InWritableDir(osutil.Remove, from)
|
||||
err = osutil.InWritableDir(os.Remove, from)
|
||||
if err != nil {
|
||||
l.Infof("Puller (folder %q, file %q): delete %q after failed rename: %v", f.folderID, target.Name, source.Name, err)
|
||||
f.newError(target.Name, err)
|
||||
@@ -976,7 +976,7 @@ func (f *rwFolder) handleFile(file protocol.FileInfo, copyChan chan<- copyBlocks
|
||||
// Otherwise, discard the file ourselves in order for the
|
||||
// sharedpuller not to panic when it fails to exclusively create a
|
||||
// file which already exists
|
||||
osutil.InWritableDir(osutil.Remove, tempName)
|
||||
osutil.InWritableDir(os.Remove, tempName)
|
||||
}
|
||||
} else {
|
||||
// Copy the blocks, as we don't want to shuffle them on the FileInfo
|
||||
@@ -1245,9 +1245,6 @@ func (f *rwFolder) performFinish(state *sharedPullerState) error {
|
||||
}
|
||||
}
|
||||
|
||||
// Set the correct timestamp on the new file
|
||||
f.mtimeFS.Chtimes(state.tempName, state.file.ModTime(), state.file.ModTime()) // never fails
|
||||
|
||||
if stat, err := f.mtimeFS.Lstat(state.realName); err == nil {
|
||||
// There is an old file or directory already in place. We need to
|
||||
// handle that.
|
||||
@@ -1262,7 +1259,7 @@ func (f *rwFolder) performFinish(state *sharedPullerState) error {
|
||||
// and future hard ignores before attempting a directory delete.
|
||||
// Should share code with f.deletDir().
|
||||
|
||||
if err = osutil.InWritableDir(osutil.Remove, state.realName); err != nil {
|
||||
if err = osutil.InWritableDir(os.Remove, state.realName); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@@ -1294,6 +1291,9 @@ func (f *rwFolder) performFinish(state *sharedPullerState) error {
|
||||
return err
|
||||
}
|
||||
|
||||
// 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)
|
||||
@@ -1458,14 +1458,14 @@ func removeAvailability(availabilities []Availability, availability Availability
|
||||
func (f *rwFolder) moveForConflict(name string) error {
|
||||
if strings.Contains(filepath.Base(name), ".sync-conflict-") {
|
||||
l.Infoln("Conflict for", name, "which is already a conflict copy; not copying again.")
|
||||
if err := osutil.Remove(name); err != nil && !os.IsNotExist(err) {
|
||||
if err := os.Remove(name); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
if f.maxConflicts == 0 {
|
||||
if err := osutil.Remove(name); err != nil && !os.IsNotExist(err) {
|
||||
if err := os.Remove(name); err != nil && !os.IsNotExist(err) {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
@@ -1487,7 +1487,7 @@ func (f *rwFolder) moveForConflict(name string) error {
|
||||
if gerr == nil && len(matches) > f.maxConflicts {
|
||||
sort.Sort(sort.Reverse(sort.StringSlice(matches)))
|
||||
for _, match := range matches[f.maxConflicts:] {
|
||||
gerr = osutil.Remove(match)
|
||||
gerr = os.Remove(match)
|
||||
if gerr != nil {
|
||||
l.Debugln(f, "removing extra conflict", gerr)
|
||||
}
|
||||
|
||||
@@ -9,9 +9,9 @@ package model
|
||||
import (
|
||||
"encoding/binary"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"sort"
|
||||
|
||||
"github.com/syncthing/syncthing/lib/osutil"
|
||||
"github.com/syncthing/syncthing/lib/protocol"
|
||||
"github.com/syndtr/goleveldb/leveldb"
|
||||
"github.com/syndtr/goleveldb/leveldb/opt"
|
||||
@@ -186,7 +186,7 @@ func (s *onDiskIndexSorter) Sorted(fn func(protocol.FileInfo) bool) {
|
||||
func (s *onDiskIndexSorter) Close() {
|
||||
l.Debugf("onDiskIndexSorter %p closes", s)
|
||||
s.db.Close()
|
||||
osutil.RemoveAll(s.dir)
|
||||
os.RemoveAll(s.dir)
|
||||
}
|
||||
|
||||
func (s *onDiskIndexSorter) full() bool {
|
||||
|
||||
@@ -15,7 +15,6 @@ import (
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
"syscall"
|
||||
|
||||
"github.com/calmh/du"
|
||||
"github.com/syncthing/syncthing/lib/sync"
|
||||
@@ -93,93 +92,6 @@ func InWritableDir(fn func(string) error, path string) error {
|
||||
return fn(path)
|
||||
}
|
||||
|
||||
// Remove removes the given path. On Windows, removes the read-only attribute
|
||||
// from the target prior to deletion.
|
||||
func Remove(path string) error {
|
||||
if runtime.GOOS == "windows" {
|
||||
info, err := os.Stat(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if info.Mode()&0200 == 0 {
|
||||
os.Chmod(path, 0700)
|
||||
}
|
||||
}
|
||||
return os.Remove(path)
|
||||
}
|
||||
|
||||
// RemoveAll is a copy of os.RemoveAll, but uses osutil.Remove.
|
||||
// RemoveAll removes path and any children it contains.
|
||||
// It removes everything it can but returns the first error
|
||||
// it encounters. If the path does not exist, RemoveAll
|
||||
// returns nil (no error).
|
||||
func RemoveAll(path string) error {
|
||||
// Simple case: if Remove works, we're done.
|
||||
err := Remove(path)
|
||||
if err == nil || os.IsNotExist(err) {
|
||||
return nil
|
||||
}
|
||||
|
||||
// Otherwise, is this a directory we need to recurse into?
|
||||
dir, serr := os.Lstat(path)
|
||||
if serr != nil {
|
||||
if serr, ok := serr.(*os.PathError); ok && (os.IsNotExist(serr.Err) || serr.Err == syscall.ENOTDIR) {
|
||||
return nil
|
||||
}
|
||||
return serr
|
||||
}
|
||||
if !dir.IsDir() {
|
||||
// Not a directory; return the error from Remove.
|
||||
return err
|
||||
}
|
||||
|
||||
// Directory.
|
||||
fd, err := os.Open(path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
// Race. It was deleted between the Lstat and Open.
|
||||
// Return nil per RemoveAll's docs.
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// Remove contents & return first error.
|
||||
err = nil
|
||||
for {
|
||||
names, err1 := fd.Readdirnames(100)
|
||||
for _, name := range names {
|
||||
err1 := RemoveAll(path + string(os.PathSeparator) + name)
|
||||
if err == nil {
|
||||
err = err1
|
||||
}
|
||||
}
|
||||
if err1 == io.EOF {
|
||||
break
|
||||
}
|
||||
// If Readdirnames returned an error, use it.
|
||||
if err == nil {
|
||||
err = err1
|
||||
}
|
||||
if len(names) == 0 {
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
// Close directory, because windows won't remove opened directory.
|
||||
fd.Close()
|
||||
|
||||
// Remove directory.
|
||||
err1 := Remove(path)
|
||||
if err1 == nil || os.IsNotExist(err1) {
|
||||
return nil
|
||||
}
|
||||
if err == nil {
|
||||
err = err1
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
func ExpandTilde(path string) (string, error) {
|
||||
if path == "~" {
|
||||
return getHomeDir()
|
||||
|
||||
@@ -71,6 +71,8 @@ func TestInWriteableDir(t *testing.T) {
|
||||
}
|
||||
|
||||
func TestInWritableDirWindowsRemove(t *testing.T) {
|
||||
// os.Remove should remove read only things on windows
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skipf("Tests not required")
|
||||
return
|
||||
@@ -100,20 +102,49 @@ func TestInWritableDirWindowsRemove(t *testing.T) {
|
||||
os.Chmod("testdata/windows/ro/readonly", 0500)
|
||||
|
||||
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
|
||||
err := os.Remove(path)
|
||||
if err == nil {
|
||||
t.Errorf("Expected error %s", path)
|
||||
}
|
||||
}
|
||||
|
||||
for _, path := range []string{"testdata/windows/ro/readonly", "testdata/windows/ro", "testdata/windows"} {
|
||||
err := osutil.InWritableDir(osutil.Remove, path)
|
||||
err := osutil.InWritableDir(os.Remove, path)
|
||||
if err != nil {
|
||||
t.Errorf("Unexpected error %s: %s", path, err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestInWritableDirWindowsRemoveAll(t *testing.T) {
|
||||
// os.RemoveAll should remove read only things on windows
|
||||
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skipf("Tests not required")
|
||||
return
|
||||
}
|
||||
|
||||
err := os.RemoveAll("testdata")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Chmod("testdata/windows/ro/readonlynew", 0700)
|
||||
defer os.RemoveAll("testdata")
|
||||
|
||||
create := func(name string) error {
|
||||
fd, err := os.Create(name)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fd.Close()
|
||||
return nil
|
||||
}
|
||||
|
||||
os.Mkdir("testdata", 0700)
|
||||
|
||||
os.Mkdir("testdata/windows", 0500)
|
||||
os.Mkdir("testdata/windows/ro", 0500)
|
||||
create("testdata/windows/ro/readonly")
|
||||
os.Chmod("testdata/windows/ro/readonly", 0500)
|
||||
|
||||
if err := os.RemoveAll("testdata/windows"); err != nil {
|
||||
t.Errorf("Unexpected error: %s", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInWritableDirWindowsRename(t *testing.T) {
|
||||
if runtime.GOOS != "windows" {
|
||||
t.Skipf("Tests not required")
|
||||
|
||||
@@ -16,6 +16,10 @@ import (
|
||||
"github.com/syncthing/syncthing/lib/rand"
|
||||
)
|
||||
|
||||
const (
|
||||
SyntheticDirectorySize = 128
|
||||
)
|
||||
|
||||
var (
|
||||
sha256OfEmptyBlock = sha256.Sum256(make([]byte, BlockSize))
|
||||
HelloMessageMagic = uint32(0x2EA7D90B)
|
||||
@@ -56,8 +60,11 @@ func (f FileInfo) HasPermissionBits() bool {
|
||||
}
|
||||
|
||||
func (f FileInfo) FileSize() int64 {
|
||||
if f.IsDirectory() || f.IsDeleted() {
|
||||
return 128
|
||||
if f.Deleted {
|
||||
return 0
|
||||
}
|
||||
if f.IsDirectory() {
|
||||
return SyntheticDirectorySize
|
||||
}
|
||||
return f.Size
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ func TestCompare(t *testing.T) {
|
||||
// Empty vectors are identical
|
||||
{Vector{}, Vector{}, Equal},
|
||||
{Vector{}, Vector{[]Counter{{42, 0}}}, Equal},
|
||||
{Vector{[]Counter{Counter{42, 0}}}, Vector{}, Equal},
|
||||
{Vector{[]Counter{{42, 0}}}, Vector{}, Equal},
|
||||
|
||||
// Zero is the implied value for a missing Counter
|
||||
{
|
||||
|
||||
@@ -289,8 +289,8 @@ func TestWalkSymlink(t *testing.T) {
|
||||
|
||||
// Create a folder with a symlink in it
|
||||
|
||||
osutil.RemoveAll("_symlinks")
|
||||
defer osutil.RemoveAll("_symlinks")
|
||||
os.RemoveAll("_symlinks")
|
||||
defer os.RemoveAll("_symlinks")
|
||||
|
||||
os.Mkdir("_symlinks", 0755)
|
||||
symlinks.Create("_symlinks/link", "destination", symlinks.TargetUnknown)
|
||||
|
||||
@@ -198,6 +198,7 @@ func upgradeToURL(archiveName, binary string, url string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(fname)
|
||||
|
||||
old := binary + ".old"
|
||||
os.Remove(old)
|
||||
@@ -205,7 +206,11 @@ func upgradeToURL(archiveName, binary string, url string) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return os.Rename(fname, binary)
|
||||
if os.Rename(fname, binary); err != nil {
|
||||
os.Rename(old, binary)
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readRelease(archiveName, dir, url string) (string, error) {
|
||||
|
||||
@@ -77,7 +77,7 @@ func NewStaggered(folderID, folderPath string, params map[string]string) Version
|
||||
if testCleanDone != nil {
|
||||
close(testCleanDone)
|
||||
}
|
||||
for _ = range time.Tick(time.Duration(cleanInterval) * time.Second) {
|
||||
for range time.Tick(time.Duration(cleanInterval) * time.Second) {
|
||||
s.clean()
|
||||
}
|
||||
}()
|
||||
@@ -165,7 +165,7 @@ func (v Staggered) expire(versions []string) {
|
||||
continue
|
||||
}
|
||||
|
||||
if err := osutil.Remove(file); err != nil {
|
||||
if err := os.Remove(file); err != nil {
|
||||
l.Warnf("Versioner: can't remove %q: %v", file, err)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,7 +144,7 @@ func (t *Trashcan) cleanoutArchive() error {
|
||||
// directory was empty and try to remove it. We ignore failure for
|
||||
// the time being.
|
||||
if currentDir != "" && filesInDir == 0 {
|
||||
osutil.Remove(currentDir)
|
||||
os.Remove(currentDir)
|
||||
}
|
||||
currentDir = path
|
||||
filesInDir = 0
|
||||
@@ -153,7 +153,7 @@ func (t *Trashcan) cleanoutArchive() error {
|
||||
|
||||
if info.ModTime().Before(cutoff) {
|
||||
// The file is too old; remove it.
|
||||
osutil.Remove(path)
|
||||
os.Remove(path)
|
||||
} else {
|
||||
// Keep this file, and remember it so we don't unnecessarily try
|
||||
// to remove this directory.
|
||||
@@ -169,7 +169,7 @@ func (t *Trashcan) cleanoutArchive() error {
|
||||
// The last directory seen by the walkFn may not have been removed as it
|
||||
// should be.
|
||||
if currentDir != "" && filesInDir == 0 {
|
||||
osutil.Remove(currentDir)
|
||||
os.Remove(currentDir)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "STDISCOSRV" "1" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "STDISCOSRV" "1" "August 22, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
stdiscosrv \- Syncthing Discovery Server
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "STRELAYSRV" "1" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "STRELAYSRV" "1" "August 22, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
strelaysrv \- Syncthing Relay Server
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-BEP" "7" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-BEP" "7" "August 22, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-bep \- Block Exchange Protocol v1
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-CONFIG" "5" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-CONFIG" "5" "August 22, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-config \- Syncthing Configuration
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-DEVICE-IDS" "7" "August 22, 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" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-EVENT-API" "7" "August 22, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-event-api \- Event API
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-FAQ" "7" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-FAQ" "7" "August 22, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-faq \- Frequently Asked Questions
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-GLOBALDISCO" "7" "August 22, 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" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-LOCALDISCO" "7" "August 22, 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" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-NETWORKING" "7" "August 22, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-networking \- Firewall Setup
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-RELAY" "7" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-RELAY" "7" "August 22, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-relay \- Relay Protocol v1
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-REST-API" "7" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-REST-API" "7" "August 22, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-rest-api \- REST API
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-SECURITY" "7" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-SECURITY" "7" "August 22, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-security \- Security Principles
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING-STIGNORE" "5" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING-STIGNORE" "5" "August 22, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing-stignore \- Prevent files from being synchronized to other nodes
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "TODO" "7" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "TODO" "7" "August 22, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
Todo \- Keep automatic backups of deleted files by other nodes
|
||||
.
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
.\" Man page generated from reStructuredText.
|
||||
.
|
||||
.TH "SYNCTHING" "1" "August 08, 2016" "v0.14" "Syncthing"
|
||||
.TH "SYNCTHING" "1" "August 22, 2016" "v0.14" "Syncthing"
|
||||
.SH NAME
|
||||
syncthing \- Syncthing
|
||||
.
|
||||
|
||||
@@ -208,7 +208,7 @@ func alterFiles(dir string) error {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
err := osutil.Remove(path)
|
||||
err := os.Remove(path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user