Merge pull request #21897 from MichaelEischer/sftp-fix-chmod

backend/sftp: fix removing files on windows sftp
This commit is contained in:
Michael Eischer
2026-06-20 19:29:49 +02:00
committed by GitHub
2 changed files with 46 additions and 6 deletions
+9
View File
@@ -0,0 +1,9 @@
Bugfix: Remove read-only files via the SFTP backend on Windows servers
Since restic 0.19.0, repository files on the SFTP backend are marked
read-only after save. On Windows SFTP servers, removing them failed
with a permission error. The SFTP backend now clears the read-only flag
before removing the file.
https://github.com/restic/restic/issues/21895
https://github.com/restic/restic/pull/21897
+37 -6
View File
@@ -11,6 +11,7 @@ import (
"os"
"os/exec"
"path"
"sync/atomic"
"syscall"
"time"
@@ -37,7 +38,8 @@ type SFTP struct {
cmd *exec.Cmd
result <-chan error
posixRename bool
posixRename bool
chmodBeforeRemove atomic.Bool
layout.Layout
Config
@@ -405,12 +407,11 @@ func (r *SFTP) Save(_ context.Context, h backend.Handle, rd backend.RewindReader
} else {
err = r.c.Rename(tmpFilename, filename)
}
err = setFileReadonly(r.c, filename, r.Modes.File)
if err != nil {
return errors.Errorf("sftp setFileReadonly: %v", err)
return errors.Wrapf(err, "Rename %v", tmpFilename)
}
return errors.Wrapf(err, "Rename %v", tmpFilename)
err = setFileReadonly(r.c, filename, r.Modes.File)
return errors.Wrapf(err, "setFileReadonly %v", filename)
}
// checkNoSpace checks if err was likely caused by lack of available space
@@ -508,7 +509,37 @@ func (r *SFTP) Remove(_ context.Context, h backend.Handle) error {
return err
}
return errors.Wrapf(r.c.Remove(r.Filename(h)), "Remove %v", r.Filename(h))
path := r.Filename(h)
if r.chmodBeforeRemove.Load() {
return r.removeWithChmod(path)
}
// optimistically try to remove the file
err := r.c.Remove(path)
if err == nil {
return nil
}
if !errors.Is(err, os.ErrPermission) {
return errors.Wrapf(err, "Remove %v", path)
}
// fallback to chmod + remove
// this is necessary on Windows where read-only files cannot be deleted without chmod.
if err := r.removeWithChmod(path); err != nil {
return err
}
r.chmodBeforeRemove.Store(true)
return nil
}
func (r *SFTP) removeWithChmod(path string) error {
err := r.c.Chmod(path, r.Modes.File)
if err != nil {
return errors.Wrapf(err, "Chmod %v", path)
}
return errors.Wrapf(r.c.Remove(path), "Remove %v", path)
}
// List runs fn for each file in the backend which has the type t. When an