diff --git a/lib/api/api.go b/lib/api/api.go
index 795f40fc9..7fbc2251b 100644
--- a/lib/api/api.go
+++ b/lib/api/api.go
@@ -43,6 +43,7 @@ import (
"github.com/syncthing/syncthing/lib/discover"
"github.com/syncthing/syncthing/lib/events"
"github.com/syncthing/syncthing/lib/fs"
+ "github.com/syncthing/syncthing/lib/ignore"
"github.com/syncthing/syncthing/lib/locations"
"github.com/syncthing/syncthing/lib/logger"
"github.com/syncthing/syncthing/lib/model"
@@ -1161,15 +1162,16 @@ func (s *service) getDBIgnores(w http.ResponseWriter, r *http.Request) {
folder := qs.Get("folder")
- ignores, patterns, err := s.model.GetIgnores(folder)
- if err != nil {
+ lines, patterns, err := s.model.GetIgnores(folder)
+ if err != nil && !ignore.IsParseError(err) {
http.Error(w, err.Error(), 500)
return
}
- sendJSON(w, map[string][]string{
- "ignore": ignores,
+ sendJSON(w, map[string]interface{}{
+ "ignore": lines,
"expanded": patterns,
+ "error": errorString(err),
})
}
@@ -1750,3 +1752,11 @@ func checkExpiry(cert tls.Certificate) error {
return nil
}
+
+func errorString(err error) *string {
+ if err != nil {
+ msg := err.Error()
+ return &msg
+ }
+ return nil
+}
diff --git a/lib/ignore/ignore.go b/lib/ignore/ignore.go
index c19584482..24709cf62 100644
--- a/lib/ignore/ignore.go
+++ b/lib/ignore/ignore.go
@@ -10,6 +10,7 @@ import (
"bufio"
"bytes"
"crypto/md5"
+ "errors"
"fmt"
"io"
"path/filepath"
@@ -18,7 +19,6 @@ import (
"time"
"github.com/gobwas/glob"
- "github.com/pkg/errors"
"github.com/syncthing/syncthing/lib/fs"
"github.com/syncthing/syncthing/lib/osutil"
@@ -40,6 +40,33 @@ func init() {
}
}
+// A ParseError signifies an error with contents of an ignore file,
+// including I/O errors on included files. An I/O error on the root level
+// ignore file is not a ParseError.
+type ParseError struct {
+ inner error
+}
+
+func (e *ParseError) Error() string {
+ return fmt.Sprintf("parse error: %v", e.inner)
+}
+
+func (e *ParseError) Unwrap() error {
+ return e.inner
+}
+
+func IsParseError(err error) bool {
+ var e *ParseError
+ return errors.As(err, &e)
+}
+
+func parseError(err error) error {
+ if err == nil {
+ return nil
+ }
+ return &ParseError{err}
+}
+
type Pattern struct {
pattern string
match glob.Glob
@@ -150,6 +177,10 @@ func New(fs fs.Filesystem, opts ...Option) *Matcher {
return m
}
+// Load and parse a file. The returned error may be of type *ParseError in
+// which case a file was loaded from disk but the patterns could not be
+// parsed. In this case the contents of the file are nonetheless available
+// in the Lines() method.
func (m *Matcher) Load(file string) error {
m.mut.Lock()
defer m.mut.Unlock()
@@ -176,6 +207,7 @@ func (m *Matcher) Load(file string) error {
return err
}
+// Load and parse an io.Reader. See Load() for notes on the returned error.
func (m *Matcher) Parse(r io.Reader, file string) error {
m.mut.Lock()
defer m.mut.Unlock()
@@ -376,7 +408,7 @@ func loadParseIncludeFile(filesystem fs.Filesystem, file string, cd ChangeDetect
}
if cd.Seen(filesystem, file) {
- return nil, fmt.Errorf("multiple include of ignore file %q", file)
+ return nil, parseError(fmt.Errorf("multiple include of ignore file %q", file))
}
fd, info, err := loadIgnoreFile(filesystem, file, cd)
@@ -418,7 +450,7 @@ func parseLine(line string) ([]Pattern, error) {
}
if line == "" {
- return nil, errors.New("missing pattern")
+ return nil, parseError(errors.New("missing pattern"))
}
if pattern.result.IsCaseFolded() {
@@ -431,14 +463,14 @@ func parseLine(line string) ([]Pattern, error) {
if strings.HasPrefix(line, "/") {
// Pattern is rooted in the current dir only
pattern.match, err = glob.Compile(line[1:], '/')
- return []Pattern{pattern}, err
+ return []Pattern{pattern}, parseError(err)
}
patterns := make([]Pattern, 2)
if strings.HasPrefix(line, "**/") {
// Add the pattern as is, and without **/ so it matches in current dir
pattern.match, err = glob.Compile(line, '/')
if err != nil {
- return nil, err
+ return nil, parseError(err)
}
patterns[0] = pattern
@@ -446,7 +478,7 @@ func parseLine(line string) ([]Pattern, error) {
pattern.pattern = line
pattern.match, err = glob.Compile(line, '/')
if err != nil {
- return nil, err
+ return nil, parseError(err)
}
patterns[1] = pattern
return patterns, nil
@@ -455,7 +487,7 @@ func parseLine(line string) ([]Pattern, error) {
// current directory and subdirs.
pattern.match, err = glob.Compile(line, '/')
if err != nil {
- return nil, err
+ return nil, parseError(err)
}
patterns[0] = pattern
@@ -463,30 +495,36 @@ func parseLine(line string) ([]Pattern, error) {
pattern.pattern = line
pattern.match, err = glob.Compile(line, '/')
if err != nil {
- return nil, err
+ return nil, parseError(err)
}
patterns[1] = pattern
return patterns, nil
}
func parseIgnoreFile(fs fs.Filesystem, fd io.Reader, currentFile string, cd ChangeDetector, linesSeen map[string]struct{}) ([]string, []Pattern, error) {
- var lines []string
var patterns []Pattern
addPattern := func(line string) error {
newPatterns, err := parseLine(line)
if err != nil {
- return errors.Wrapf(err, "invalid pattern %q in ignore file", line)
+ return fmt.Errorf("invalid pattern %q in ignore file: %w", line, err)
}
patterns = append(patterns, newPatterns...)
return nil
}
scanner := bufio.NewScanner(fd)
- var err error
+ var lines []string
for scanner.Scan() {
line := strings.TrimSpace(scanner.Text())
lines = append(lines, line)
+ }
+ if err := scanner.Err(); err != nil {
+ return nil, nil, err
+ }
+
+ var err error
+ for _, line := range lines {
if _, ok := linesSeen[line]; ok {
continue
}
@@ -503,13 +541,13 @@ func parseIgnoreFile(fs fs.Filesystem, fd io.Reader, currentFile string, cd Chan
case strings.HasPrefix(line, "#include"):
fields := strings.SplitN(line, " ", 2)
if len(fields) != 2 {
- err = errors.New("failed to parse #include line: no file?")
+ err = parseError(errors.New("failed to parse #include line: no file?"))
break
}
includeRel := strings.TrimSpace(fields[1])
if includeRel == "" {
- err = errors.New("failed to parse #include line: no file?")
+ err = parseError(errors.New("failed to parse #include line: no file?"))
break
}
@@ -522,7 +560,7 @@ func parseIgnoreFile(fs fs.Filesystem, fd io.Reader, currentFile string, cd Chan
// IsNotExists(err) == true error, which we use to check
// existance of the .stignore file, and just end up assuming
// there is none, rather than a broken include.
- err = fmt.Errorf("failed to load include file %s: %s", includeFile, err.Error())
+ err = parseError(fmt.Errorf("failed to load include file %s: %w", includeFile, err))
}
case strings.HasSuffix(line, "/**"):
err = addPattern(line)
@@ -535,7 +573,7 @@ func parseIgnoreFile(fs fs.Filesystem, fd io.Reader, currentFile string, cd Chan
}
}
if err != nil {
- return nil, nil, err
+ return lines, nil, err
}
}
diff --git a/lib/ignore/ignore_test.go b/lib/ignore/ignore_test.go
index 4e743cafb..941731144 100644
--- a/lib/ignore/ignore_test.go
+++ b/lib/ignore/ignore_test.go
@@ -195,6 +195,9 @@ func TestBadPatterns(t *testing.T) {
if err == nil {
t.Errorf("No error for pattern %q", pat)
}
+ if !IsParseError(err) {
+ t.Error("Should have been a parse error:", err)
+ }
}
}
@@ -1006,10 +1009,13 @@ func TestIssue4901(t *testing.T) {
for i := 0; i < 2; i++ {
err := pats.Load(".stignore")
if err == nil {
- t.Fatalf("expected an error")
+ t.Fatal("expected an error")
}
if fs.IsNotExist(err) {
- t.Fatalf("unexpected error type")
+ t.Fatal("unexpected error type")
+ }
+ if !IsParseError(err) {
+ t.Fatal("failure to load included file should be a parse error")
}
}
@@ -1094,9 +1100,13 @@ func TestPartialIncludeLine(t *testing.T) {
}
for _, tc := range cases {
- if err := pats.Parse(bytes.NewBufferString(tc), ".stignore"); err == nil {
+ err := pats.Parse(bytes.NewBufferString(tc), ".stignore")
+ if err == nil {
t.Fatal("should error out")
}
+ if !IsParseError(err) {
+ t.Fatal("failure to load included file should be a parse error")
+ }
}
}
@@ -1177,5 +1187,8 @@ func TestEmptyPatterns(t *testing.T) {
if err == nil {
t.Error("Should reject invalid pattern", tc)
}
+ if !IsParseError(err) {
+ t.Fatal("bad pattern should be a parse error")
+ }
}
}
diff --git a/lib/model/model.go b/lib/model/model.go
index 838f6cd3a..ddde176c6 100644
--- a/lib/model/model.go
+++ b/lib/model/model.go
@@ -1656,11 +1656,15 @@ func (m *model) GetIgnores(folder string) ([]string, []string, error) {
ignores = ignore.New(fs.NewFilesystem(cfg.FilesystemType, cfg.Path))
}
- if err := ignores.Load(".stignore"); err != nil && !fs.IsNotExist(err) {
- return nil, nil, err
+ err := ignores.Load(".stignore")
+ if fs.IsNotExist(err) {
+ // Having no ignores is not an error.
+ return nil, nil, nil
}
- return ignores.Lines(), ignores.Patterns(), nil
+ // Return lines and patterns, which may have some meaning even when err
+ // != nil, depending on the specific error.
+ return ignores.Lines(), ignores.Patterns(), err
}
func (m *model) SetIgnores(folder string, content []string) error {