diff --git a/changelog/unreleased/issue-21895 b/changelog/unreleased/issue-21895 new file mode 100644 index 000000000..a566df2cf --- /dev/null +++ b/changelog/unreleased/issue-21895 @@ -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 diff --git a/internal/backend/sftp/sftp.go b/internal/backend/sftp/sftp.go index 75fcd94eb..dcc2ea5a7 100644 --- a/internal/backend/sftp/sftp.go +++ b/internal/backend/sftp/sftp.go @@ -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