Compare commits
3
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
aa65c6f249 | ||
|
|
8210466b03 | ||
|
|
3e24d82513 |
@@ -0,0 +1,19 @@
|
||||
FROM alpine
|
||||
ARG TARGETARCH
|
||||
|
||||
EXPOSE 8384 22000 21027/udp
|
||||
|
||||
VOLUME ["/var/syncthing"]
|
||||
|
||||
RUN apk add --no-cache ca-certificates su-exec tzdata
|
||||
|
||||
COPY ./syncthing-linux-$TARGETARCH /bin/syncthing
|
||||
COPY ./script/docker-entrypoint.sh /bin/entrypoint.sh
|
||||
|
||||
ENV PUID=1000 PGID=1000 HOME=/var/syncthing
|
||||
|
||||
HEALTHCHECK --interval=1m --timeout=10s \
|
||||
CMD nc -z 127.0.0.1 8384 || exit 1
|
||||
|
||||
ENV STGUIADDRESS=0.0.0.0:8384
|
||||
ENTRYPOINT ["/bin/entrypoint.sh", "/bin/syncthing", "-home", "/var/syncthing/config"]
|
||||
+30
-4
@@ -7,13 +7,39 @@
|
||||
package fs
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"unicode"
|
||||
"unicode/utf8"
|
||||
)
|
||||
|
||||
func UnicodeLowercase(s string) string {
|
||||
rs := []rune(s)
|
||||
for i, r := range rs {
|
||||
rs[i] = unicode.ToLower(unicode.ToUpper(r))
|
||||
i := firstCaseChange(s)
|
||||
if i == -1 {
|
||||
return s
|
||||
}
|
||||
return string(rs)
|
||||
|
||||
var rs strings.Builder
|
||||
// WriteRune always reserves utf8.UTFMax bytes for non-ASCII runes,
|
||||
// even if it doesn't need all that space. Overallocate now to prevent
|
||||
// it from ever triggering a reallocation.
|
||||
rs.Grow(utf8.UTFMax - 1 + len(s))
|
||||
rs.WriteString(s[:i])
|
||||
|
||||
for _, r := range s[i:] {
|
||||
rs.WriteRune(unicode.ToLower(unicode.ToUpper(r)))
|
||||
}
|
||||
return rs.String()
|
||||
}
|
||||
|
||||
// Byte index of the first rune r s.t. lower(upper(r)) != r.
|
||||
func firstCaseChange(s string) int {
|
||||
for i, r := range s {
|
||||
if r <= unicode.MaxASCII && (r < 'A' || r > 'Z') {
|
||||
continue
|
||||
}
|
||||
if unicode.ToLower(unicode.ToUpper(r)) != r {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
+60
-34
@@ -6,45 +6,71 @@
|
||||
|
||||
package fs
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
var caseCases = [][2]string{
|
||||
{"", ""},
|
||||
{"hej", "hej"},
|
||||
{"HeJ!@#", "hej!@#"},
|
||||
// Western Europe diacritical stuff is trivial.
|
||||
{"ÜBERRÄKSMÖRGÅS", "überräksmörgås"},
|
||||
// As are ligatures.
|
||||
{"Æglefinus", "æglefinus"},
|
||||
{"IJssel", "ijssel"},
|
||||
// Cyrillic seems regular as well.
|
||||
{"Привет", "привет"},
|
||||
// Greek has multiple lower case characters for things depending on
|
||||
// context; we should always choose the same one.
|
||||
{"Ὀδυσσεύς", "ὀδυσσεύσ"},
|
||||
{"ὈΔΥΣΣΕΎΣ", "ὀδυσσεύσ"},
|
||||
// German ß doesn't really have an upper case variant, and we
|
||||
// shouldn't mess things up when lower casing it either. We don't
|
||||
// attempt to make ß equivalent to "ss".
|
||||
{"Reichwaldstraße", "reichwaldstraße"},
|
||||
// The Turks do their thing with the Is.... Like the Greek example
|
||||
// we pick just the one canonicalized "i" although you can argue
|
||||
// with this... From what I understand most operating systems don't
|
||||
// get this right anyway.
|
||||
{"İI", "ii"},
|
||||
// Arabic doesn't do case folding.
|
||||
{"العَرَبِيَّة", "العَرَبِيَّة"},
|
||||
// Neither does Hebrew.
|
||||
{"עברית", "עברית"},
|
||||
// Nor Chinese, in any variant.
|
||||
{"汉语/漢語 or 中文", "汉语/漢語 or 中文"},
|
||||
// Nor katakana, as far as I can tell.
|
||||
{"チャーハン", "チャーハン"},
|
||||
// Some special Unicode characters, however, are folded by OSes.
|
||||
{"\u212A", "k"},
|
||||
}
|
||||
|
||||
func TestUnicodeLowercase(t *testing.T) {
|
||||
cases := [][2]string{
|
||||
{"", ""},
|
||||
{"hej", "hej"},
|
||||
{"HeJ!@#", "hej!@#"},
|
||||
// Western Europe diacritical stuff is trivial
|
||||
{"ÜBERRÄKSMÖRGÅS", "überräksmörgås"},
|
||||
// Cyrillic seems regular as well
|
||||
{"Привет", "привет"},
|
||||
// Greek has multiple lower case characters for things depending on
|
||||
// context; we should always choose the right one.
|
||||
{"Ὀδυσσεύς", "ὀδυσσεύσ"},
|
||||
{"ὈΔΥΣΣΕΎΣ", "ὀδυσσεύσ"},
|
||||
// German ß doesn't really have an upper case variant, and we
|
||||
// shouldn't mess things up when lower casing it either. We don't
|
||||
// attempt to make ß equivalent to "ss".
|
||||
{"Reichwaldstraße", "reichwaldstraße"},
|
||||
// The Turks do their thing with the Is.... Like the Greek example
|
||||
// we pick just the one canonicalized "i" although you can argue
|
||||
// with this... From what I understand most operating systems don't
|
||||
// get this right anyway.
|
||||
{"İI", "ii"},
|
||||
// Arabic doesn't do case folding.
|
||||
{"العَرَبِيَّة", "العَرَبِيَّة"},
|
||||
// Neither does Hebrew.
|
||||
{"עברית", "עברית"},
|
||||
// Nor Chinese, in any variant.
|
||||
{"汉语/漢語 or 中文", "汉语/漢語 or 中文"},
|
||||
// Niether katakana as far as I can tell.
|
||||
{"チャーハン", "チャーハン"},
|
||||
// Some special unicode characters, however, are folded by OSes
|
||||
{"\u212A", "k"},
|
||||
}
|
||||
for _, tc := range cases {
|
||||
for _, tc := range caseCases {
|
||||
res := UnicodeLowercase(tc[0])
|
||||
if res != tc[1] {
|
||||
t.Errorf("UnicodeLowercase(%q) => %q, expected %q", tc[0], res, tc[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnicodeLowercaseMaybeChange(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, s := range caseCases {
|
||||
UnicodeLowercase(s[0])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func BenchmarkUnicodeLowercaseNoChange(b *testing.B) {
|
||||
b.ReportAllocs()
|
||||
|
||||
for i := 0; i < b.N; i++ {
|
||||
for _, s := range caseCases {
|
||||
UnicodeLowercase(s[1])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -813,9 +813,10 @@ func (f *sendReceiveFolder) deleteDir(file protocol.FileInfo, snap *db.Snapshot,
|
||||
|
||||
cur, hasCur := snap.Get(protocol.LocalDeviceID, file.Name)
|
||||
|
||||
if err = f.checkToBeDeleted(file, cur, hasCur, dbUpdateDeleteDir, dbUpdateChan, scanChan); err != nil {
|
||||
if fs.IsNotExist(err) {
|
||||
if err = f.checkToBeDeleted(file, cur, hasCur, scanChan); err != nil {
|
||||
if fs.IsNotExist(err) || fs.IsErrCaseConflict(err) {
|
||||
err = nil
|
||||
dbUpdateChan <- dbUpdateJob{file, dbUpdateDeleteDir}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -860,9 +861,10 @@ func (f *sendReceiveFolder) deleteFileWithCurrent(file, cur protocol.FileInfo, h
|
||||
})
|
||||
}()
|
||||
|
||||
if err = f.checkToBeDeleted(file, cur, hasCur, dbUpdateDeleteFile, dbUpdateChan, scanChan); err != nil {
|
||||
if fs.IsNotExist(err) {
|
||||
if err = f.checkToBeDeleted(file, cur, hasCur, scanChan); err != nil {
|
||||
if fs.IsNotExist(err) || fs.IsErrCaseConflict(err) {
|
||||
err = nil
|
||||
dbUpdateChan <- dbUpdateJob{file, dbUpdateDeleteFile}
|
||||
}
|
||||
return
|
||||
}
|
||||
@@ -945,7 +947,7 @@ func (f *sendReceiveFolder) renameFile(cur, source, target protocol.FileInfo, sn
|
||||
l.Debugln(f, "taking rename shortcut", source.Name, "->", target.Name)
|
||||
|
||||
// Check that source is compatible with what we have in the DB
|
||||
if err = f.checkToBeDeleted(source, cur, true, dbUpdateDeleteFile, dbUpdateChan, scanChan); err != nil {
|
||||
if err = f.checkToBeDeleted(source, cur, true, scanChan); err != nil {
|
||||
return err
|
||||
}
|
||||
// Check that the target corresponds to what we have in the DB
|
||||
@@ -1979,26 +1981,25 @@ func (f *sendReceiveFolder) scanIfItemChanged(name string, stat fs.FileInfo, ite
|
||||
|
||||
// checkToBeDeleted makes sure the file on disk is compatible with what there is
|
||||
// in the DB before the caller proceeds with actually deleting it.
|
||||
// I.e. non-nil error status means "Do not delete!".
|
||||
func (f *sendReceiveFolder) checkToBeDeleted(file, cur protocol.FileInfo, hasCur bool, updateType dbUpdateType, dbUpdateChan chan<- dbUpdateJob, scanChan chan<- string) error {
|
||||
// I.e. non-nil error status means "Do not delete!" or "is already deleted".
|
||||
func (f *sendReceiveFolder) checkToBeDeleted(file, cur protocol.FileInfo, hasCur bool, scanChan chan<- string) error {
|
||||
if err := osutil.TraversesSymlink(f.fs, filepath.Dir(file.Name)); err != nil {
|
||||
l.Debugln(f, "not deleting item behind symlink on disk, but update db", file.Name)
|
||||
dbUpdateChan <- dbUpdateJob{file, updateType}
|
||||
return fs.ErrNotExist
|
||||
}
|
||||
|
||||
stat, err := f.fs.Lstat(file.Name)
|
||||
if !fs.IsNotExist(err) && err != nil {
|
||||
deleted := fs.IsNotExist(err) || fs.IsErrCaseConflict(err)
|
||||
if !deleted && err != nil {
|
||||
return err
|
||||
}
|
||||
if fs.IsNotExist(err) {
|
||||
if deleted {
|
||||
if hasCur && !cur.Deleted && !cur.IsUnsupported() {
|
||||
scanChan <- file.Name
|
||||
return errModified
|
||||
}
|
||||
l.Debugln(f, "not deleting item we don't have, but update db", file.Name)
|
||||
dbUpdateChan <- dbUpdateJob{file, updateType}
|
||||
return fs.ErrNotExist
|
||||
return err
|
||||
}
|
||||
|
||||
return f.scanIfItemChanged(file.Name, stat, cur, hasCur, scanChan)
|
||||
|
||||
@@ -781,18 +781,8 @@ func TestDeleteIgnorePerms(t *testing.T) {
|
||||
fi, err := scanner.CreateFileInfo(stat, name, ffs)
|
||||
must(t, err)
|
||||
ffs.Chmod(name, 0600)
|
||||
scanChan := make(chan string)
|
||||
dbUpdateChan := make(chan dbUpdateJob)
|
||||
finished := make(chan struct{})
|
||||
go func() {
|
||||
err = f.checkToBeDeleted(fi, fi, true, dbUpdateDeleteFile, dbUpdateChan, scanChan)
|
||||
close(finished)
|
||||
}()
|
||||
select {
|
||||
case <-scanChan:
|
||||
<-finished
|
||||
case <-finished:
|
||||
}
|
||||
scanChan := make(chan string, 1)
|
||||
err = f.checkToBeDeleted(fi, fi, true, scanChan)
|
||||
must(t, err)
|
||||
}
|
||||
|
||||
@@ -1325,6 +1315,40 @@ func TestPullSymlinkOverExistingWindows(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPullDeleteCaseConflict(t *testing.T) {
|
||||
m, f := setupSendReceiveFolder()
|
||||
defer cleanupSRFolder(f, m)
|
||||
|
||||
name := "foo"
|
||||
fi := protocol.FileInfo{Name: "Foo"}
|
||||
dbUpdateChan := make(chan dbUpdateJob, 1)
|
||||
scanChan := make(chan string)
|
||||
|
||||
if fd, err := f.fs.Create(name); err != nil {
|
||||
t.Fatal(err)
|
||||
} else {
|
||||
if _, err := fd.Write([]byte("data")); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
fd.Close()
|
||||
}
|
||||
f.deleteFileWithCurrent(fi, protocol.FileInfo{}, false, dbUpdateChan, scanChan)
|
||||
select {
|
||||
case <-dbUpdateChan:
|
||||
default:
|
||||
t.Error("Missing db update for file")
|
||||
}
|
||||
|
||||
snap := f.fset.Snapshot()
|
||||
defer snap.Release()
|
||||
f.deleteDir(fi, snap, dbUpdateChan, scanChan)
|
||||
select {
|
||||
case <-dbUpdateChan:
|
||||
default:
|
||||
t.Error("Missing db update for dir")
|
||||
}
|
||||
}
|
||||
|
||||
func cleanupSharedPullerState(s *sharedPullerState) {
|
||||
s.mut.Lock()
|
||||
defer s.mut.Unlock()
|
||||
|
||||
Reference in New Issue
Block a user