2015-03-28 11:50:23 +01:00
package sftp
2014-10-04 19:20:15 +02:00
import (
2016-08-28 19:53:18 +02:00
"bufio"
2017-06-03 17:39:57 +02:00
"context"
2021-09-22 22:28:57 +02:00
"crypto/rand"
"encoding/hex"
2014-10-04 19:20:15 +02:00
"fmt"
2020-12-19 12:39:48 +01:00
"hash"
2014-10-04 19:20:15 +02:00
"io"
"os"
"os/exec"
2016-08-11 19:10:51 +02:00
"path"
2016-08-28 19:17:17 +02:00
"time"
2014-10-04 19:20:15 +02:00
2017-07-23 14:21:03 +02:00
"github.com/restic/restic/internal/backend"
2022-10-15 16:23:39 +02:00
"github.com/restic/restic/internal/backend/layout"
2023-06-08 13:04:34 +02:00
"github.com/restic/restic/internal/backend/limiter"
"github.com/restic/restic/internal/backend/location"
2023-10-01 10:24:33 +02:00
"github.com/restic/restic/internal/backend/util"
2017-07-23 14:21:03 +02:00
"github.com/restic/restic/internal/debug"
2022-06-12 17:45:34 +02:00
"github.com/restic/restic/internal/errors"
2024-05-12 11:55:34 +02:00
"github.com/restic/restic/internal/feature"
2016-08-11 19:10:51 +02:00
2020-12-16 16:00:21 +01:00
"github.com/cenkalti/backoff/v4"
2016-08-11 19:10:51 +02:00
"github.com/pkg/sftp"
2022-07-30 12:24:04 +02:00
"golang.org/x/sync/errgroup"
2014-10-04 19:20:15 +02:00
)
2016-01-24 20:23:50 +01:00
// SFTP is a backend in a directory accessed via SFTP.
2014-10-04 19:20:15 +02:00
type SFTP struct {
2015-05-03 16:43:27 +02:00
c * sftp . Client
p string
2014-10-04 19:20:15 +02:00
2016-08-28 19:17:17 +02:00
cmd * exec . Cmd
result <- chan error
2017-04-10 22:40:24 +02:00
2022-06-06 13:17:07 +02:00
posixRename bool
2022-10-15 16:23:39 +02:00
layout . Layout
2017-04-10 22:41:06 +02:00
Config
2023-10-01 10:24:33 +02:00
util . Modes
2014-10-04 19:20:15 +02:00
}
2023-10-01 11:40:12 +02:00
var _ backend . Backend = & SFTP { }
2016-08-31 22:51:35 +02:00
2024-05-10 23:57:52 +02:00
var errTooShort = fmt . Errorf ( "file is too short" )
2023-06-08 13:04:34 +02:00
func NewFactory ( ) location . Factory {
2023-06-08 17:32:43 +02:00
return location . NewLimitedBackendFactory ( "sftp" , ParseConfig , location . NoPassword , limiter . WrapBackendConstructor ( Create ) , limiter . WrapBackendConstructor ( Open ) )
2023-06-08 13:04:34 +02:00
}
2022-08-20 19:49:30 +02:00
func startClient ( cfg Config ) ( * SFTP , error ) {
program , args , err := buildSSHCommand ( cfg )
if err != nil {
return nil , err
}
2017-04-03 21:05:42 +02:00
debug . Log ( "start client %v %v" , program , args )
2014-10-04 19:20:15 +02:00
// Connect to a remote host and request the sftp subsystem via the 'ssh'
// command. This assumes that passwordless login is correctly configured.
cmd := exec . Command ( program , args ... )
2016-08-28 19:53:18 +02:00
// prefix the errors with the program name
stderr , err := cmd . StderrPipe ( )
if err != nil {
2016-08-29 21:54:50 +02:00
return nil , errors . Wrap ( err , "cmd.StderrPipe" )
2016-08-28 19:53:18 +02:00
}
go func ( ) {
sc := bufio . NewScanner ( stderr )
for sc . Scan ( ) {
fmt . Fprintf ( os . Stderr , "subprocess %v: %v\n" , program , sc . Text ( ) )
}
} ( )
2014-10-04 19:20:15 +02:00
// get stdin and stdout
wr , err := cmd . StdinPipe ( )
if err != nil {
2016-08-29 21:54:50 +02:00
return nil , errors . Wrap ( err , "cmd.StdinPipe" )
2014-10-04 19:20:15 +02:00
}
rd , err := cmd . StdoutPipe ( )
if err != nil {
2016-08-29 21:54:50 +02:00
return nil , errors . Wrap ( err , "cmd.StdoutPipe" )
2014-10-04 19:20:15 +02:00
}
2023-10-01 10:24:33 +02:00
bg , err := util . StartForeground ( cmd )
2018-01-17 23:02:47 +01:00
if err != nil {
2024-07-26 19:07:14 +02:00
if errors . Is ( err , exec . ErrDot ) {
2022-09-20 21:26:01 +02:00
return nil , errors . Errorf ( "cannot implicitly run relative executable %v found in current directory, use -o sftp.command=./<command> to override" , cmd . Path )
}
return nil , err
2014-10-04 19:20:15 +02:00
}
2016-08-28 19:17:17 +02:00
// wait in a different goroutine
ch := make ( chan error , 1 )
go func ( ) {
err := cmd . Wait ( )
2016-09-27 22:35:08 +02:00
debug . Log ( "ssh command exited, err %v" , err )
sftp: persist "ssh command exited" error
If our ssh process has died, not only the next, but all subsequent
calls to clientError() should indicate the error.
restic output when the ssh process is killed with "kill -9":
Save(<data/afb68adbf9>) returned error, retrying after 253.661803ms: Write: failed to send packet header: write |1: file already closed
Save(<data/afb68adbf9>) returned error, retrying after 580.752212ms: ssh command exited: signal: killed
Save(<data/afb68adbf9>) returned error, retrying after 790.150468ms: ssh command exited: signal: killed
Save(<data/afb68adbf9>) returned error, retrying after 1.769595051s: ssh command exited: signal: killed
[...]
error in cleanup handler: ssh command exited: signal: killed
Before this patch:
Save(<data/de698d934f>) returned error, retrying after 252.84163ms: Write: failed to send packet header: write |1: file already closed
Save(<data/de698d934f>) returned error, retrying after 660.236963ms: OpenFile: failed to send packet header: write |1: file already closed
Save(<data/de698d934f>) returned error, retrying after 568.049909ms: OpenFile: failed to send packet header: write |1: file already closed
Save(<data/de698d934f>) returned error, retrying after 2.428813824s: OpenFile: failed to send packet header: write |1: file already closed
[...]
error in cleanup handler: failed to send packet header: write |1: file already closed
2018-05-30 19:28:14 +02:00
for {
ch <- errors . Wrap ( err , "ssh command exited" )
}
2016-08-28 19:17:17 +02:00
} ( )
2014-10-04 19:20:15 +02:00
// open the SFTP session
2024-04-28 11:48:26 +02:00
client , err := sftp . NewClientPipe ( rd , wr ,
// write multiple packets (32kb) in parallel per file
// not strictly necessary as we use ReadFromWithConcurrency
2024-04-28 11:50:09 +02:00
sftp . UseConcurrentWrites ( true ) ,
// increase send buffer per file to 4MB
sftp . MaxConcurrentRequestsPerFile ( 128 ) )
2014-10-04 19:20:15 +02:00
if err != nil {
2016-08-21 17:48:36 +02:00
return nil , errors . Errorf ( "unable to start the sftp session, error: %v" , err )
2014-10-04 19:20:15 +02:00
}
2018-01-17 23:02:47 +01:00
err = bg ( )
if err != nil {
return nil , errors . Wrap ( err , "bg" )
}
2022-06-06 13:17:07 +02:00
_ , posixRename := client . HasExtension ( "posix-rename@openssh.com" )
2024-08-26 20:28:39 +02:00
return & SFTP {
c : client ,
cmd : cmd ,
result : ch ,
posixRename : posixRename ,
Layout : layout . NewDefaultLayout ( cfg . Path , path . Join ) ,
} , nil
2014-10-04 19:20:15 +02:00
}
2016-08-28 19:17:17 +02:00
// clientError returns an error if the client has exited. Otherwise, nil is
// returned immediately.
func ( r * SFTP ) clientError ( ) error {
select {
case err := <- r . result :
2016-09-27 22:35:08 +02:00
debug . Log ( "client has exited with err %v" , err )
2020-12-16 16:00:21 +01:00
return backoff . Permanent ( err )
2016-08-28 19:17:17 +02:00
default :
}
return nil
}
2017-04-10 22:41:20 +02:00
// Open opens an sftp backend as described by the config by running
2022-07-30 12:57:18 +02:00
// "ssh" with the appropriate arguments (or cfg.Command, if set).
2024-08-26 21:16:22 +02:00
func Open ( _ context . Context , cfg Config ) ( * SFTP , error ) {
2017-04-10 22:41:20 +02:00
debug . Log ( "open backend with config %#v" , cfg )
2022-08-20 19:49:30 +02:00
sftp , err := startClient ( cfg )
2021-08-07 19:56:59 +02:00
if err != nil {
2022-08-20 19:49:30 +02:00
debug . Log ( "unable to start program: %v" , err )
2021-08-07 19:56:59 +02:00
return nil , err
}
2024-08-26 21:16:22 +02:00
return open ( sftp , cfg )
2022-08-20 19:49:30 +02:00
}
2017-04-10 22:41:20 +02:00
2024-08-26 21:16:22 +02:00
func open ( sftp * SFTP , cfg Config ) ( * SFTP , error ) {
2023-10-01 11:40:12 +02:00
fi , err := sftp . c . Stat ( sftp . Layout . Filename ( backend . Handle { Type : backend . ConfigFile } ) )
2023-10-01 10:24:33 +02:00
m := util . DeriveModesFromFileInfo ( fi , err )
2022-04-26 19:15:09 +02:00
debug . Log ( "using (%03O file, %03O dir) permissions" , m . File , m . Dir )
2017-04-10 22:41:20 +02:00
sftp . Config = cfg
sftp . p = cfg . Path
2022-04-26 19:15:09 +02:00
sftp . Modes = m
2014-10-04 19:20:15 +02:00
return sftp , nil
}
2022-07-30 12:24:04 +02:00
func ( r * SFTP ) mkdirAllDataSubdirs ( ctx context . Context , nconn uint ) error {
// Run multiple MkdirAll calls concurrently. These involve multiple
// round-trips and we do a lot of them, so this whole operation can be slow
// on high-latency links.
g , _ := errgroup . WithContext ( ctx )
// Use errgroup's built-in semaphore, because r.sem is not initialized yet.
g . SetLimit ( int ( nconn ) )
2017-07-16 15:10:06 +02:00
for _ , d := range r . Paths ( ) {
2022-07-30 12:24:04 +02:00
d := d
g . Go ( func ( ) error {
// First try Mkdir. For most directories in Paths, this takes one
// round trip, not counting duplicate parent creations causes by
// concurrency. MkdirAll first does Stat, then recursive MkdirAll
// on the parent, so calls typically take three round trips.
if err := r . c . Mkdir ( d ) ; err == nil {
return nil
}
return r . c . MkdirAll ( d )
} )
2017-07-16 15:10:06 +02:00
}
2022-07-30 12:24:04 +02:00
return g . Wait ( )
2017-07-16 15:10:06 +02:00
}
2017-04-10 22:17:50 +02:00
// IsNotExist returns true if the error is caused by a not existing file.
func ( r * SFTP ) IsNotExist ( err error ) bool {
2021-03-10 20:38:22 +01:00
return errors . Is ( err , os . ErrNotExist )
2017-04-10 22:17:50 +02:00
}
2024-05-10 23:57:52 +02:00
func ( r * SFTP ) IsPermanentError ( err error ) bool {
return r . IsNotExist ( err ) || errors . Is ( err , errTooShort ) || errors . Is ( err , os . ErrPermission )
}
2017-04-10 22:41:20 +02:00
func buildSSHCommand ( cfg Config ) ( cmd string , args [ ] string , err error ) {
if cfg . Command != "" {
2018-03-13 20:50:37 +01:00
args , err := backend . SplitShellStrings ( cfg . Command )
if err != nil {
return "" , nil , err
}
2023-10-18 03:21:06 +02:00
if cfg . Args != "" {
return "" , nil , errors . New ( "cannot specify both sftp.command and sftp.args options" )
}
2018-03-13 20:50:37 +01:00
return args [ 0 ] , args [ 1 : ] , nil
2017-04-10 22:41:20 +02:00
}
cmd = "ssh"
2020-03-08 16:45:33 +01:00
host , port := cfg . Host , cfg . Port
args = [ ] string { host }
if port != "" {
args = append ( args , "-p" , port )
2016-02-15 19:17:41 +01:00
}
2015-12-28 18:22:19 +01:00
if cfg . User != "" {
2023-10-18 03:21:06 +02:00
args = append ( args , "-l" , cfg . User )
}
if cfg . Args != "" {
a , err := backend . SplitShellStrings ( cfg . Args )
if err != nil {
return "" , nil , err
}
args = append ( args , a ... )
2015-12-28 18:22:19 +01:00
}
2023-10-18 03:21:06 +02:00
args = append ( args , "-s" , "sftp" )
2017-04-10 22:41:20 +02:00
return cmd , args , nil
2015-12-28 18:22:19 +01:00
}
2017-09-23 11:21:27 +02:00
// Create creates an sftp backend as described by the config by running "ssh"
2022-07-30 12:57:18 +02:00
// with the appropriate arguments (or cfg.Command, if set).
2020-09-19 22:01:32 +02:00
func Create ( ctx context . Context , cfg Config ) ( * SFTP , error ) {
2022-08-20 19:49:30 +02:00
sftp , err := startClient ( cfg )
2017-04-03 21:05:42 +02:00
if err != nil {
2017-04-10 22:41:20 +02:00
debug . Log ( "unable to start program: %v" , err )
2017-04-03 21:05:42 +02:00
return nil , err
}
2023-10-01 10:24:33 +02:00
sftp . Modes = util . DefaultModes
2022-04-26 19:15:09 +02:00
2015-05-04 20:39:45 +02:00
// test if config file already exists
2023-10-01 11:40:12 +02:00
_ , err = sftp . c . Lstat ( sftp . Layout . Filename ( backend . Handle { Type : backend . ConfigFile } ) )
2014-10-04 19:20:15 +02:00
if err == nil {
2015-05-03 16:43:27 +02:00
return nil , errors . New ( "config file already exists" )
2015-03-14 11:56:45 +01:00
}
2017-04-19 18:56:01 +02:00
// create paths for data and refs
2022-07-30 12:24:04 +02:00
if err = sftp . mkdirAllDataSubdirs ( ctx , cfg . Connections ) ; err != nil {
2017-07-16 15:10:06 +02:00
return nil , err
2014-10-04 19:20:15 +02:00
}
2022-08-20 19:49:30 +02:00
// repurpose existing connection
2024-08-26 21:16:22 +02:00
return open ( sftp , cfg )
2015-12-28 18:22:19 +01:00
}
2021-08-07 19:56:59 +02:00
func ( r * SFTP ) Connections ( ) uint {
return r . Config . Connections
}
2020-12-19 12:39:48 +01:00
// Hasher may return a hash function for calculating a content hash for the backend
func ( r * SFTP ) Hasher ( ) hash . Hash {
return nil
}
2022-05-01 20:07:29 +02:00
// HasAtomicReplace returns whether Save() can atomically replace files
func ( r * SFTP ) HasAtomicReplace ( ) bool {
2022-06-06 13:17:07 +02:00
return r . posixRename
2022-05-01 20:07:29 +02:00
}
2021-09-22 22:28:57 +02:00
// tempSuffix generates a random string suffix that should be sufficiently long
2023-12-06 13:11:55 +01:00
// to avoid accidental conflicts
2021-09-22 22:28:57 +02:00
func tempSuffix ( ) string {
var nonce [ 16 ] byte
_ , err := rand . Read ( nonce [ : ] )
if err != nil {
panic ( err )
}
return hex . EncodeToString ( nonce [ : ] )
}
2016-01-24 01:15:35 +01:00
// Save stores data in the backend at the handle.
2023-10-01 11:40:12 +02:00
func ( r * SFTP ) Save ( _ context . Context , h backend . Handle , rd backend . RewindReader ) error {
2016-08-28 19:17:17 +02:00
if err := r . clientError ( ) ; err != nil {
return err
}
2017-04-10 22:40:24 +02:00
filename := r . Filename ( h )
2021-09-22 22:28:57 +02:00
tmpFilename := filename + "-restic-temp-" + tempSuffix ( )
2021-03-16 21:35:10 +01:00
dirname := r . Dirname ( h )
2017-04-10 22:40:24 +02:00
2017-05-03 21:18:49 +02:00
// create new file
2021-09-22 22:28:57 +02:00
f , err := r . c . OpenFile ( tmpFilename , os . O_CREATE | os . O_EXCL | os . O_WRONLY )
2017-05-03 21:18:49 +02:00
2018-01-05 17:51:09 +01:00
if r . IsNotExist ( err ) {
// error is caused by a missing directory, try to create it
2020-01-01 17:26:38 +01:00
mkdirErr := r . c . MkdirAll ( r . Dirname ( h ) )
2018-01-05 17:51:09 +01:00
if mkdirErr != nil {
debug . Log ( "error creating dir %v: %v" , r . Dirname ( h ) , mkdirErr )
} else {
// try again
2021-09-22 22:28:57 +02:00
f , err = r . c . OpenFile ( tmpFilename , os . O_CREATE | os . O_EXCL | os . O_WRONLY )
2018-01-05 17:51:09 +01:00
}
2017-04-10 22:40:24 +02:00
}
2021-03-16 21:35:10 +01:00
// pkg/sftp doesn't allow creating with a mode.
// Chmod while the file is still empty.
if err == nil {
2022-04-26 19:15:09 +02:00
err = f . Chmod ( r . Modes . File )
2021-03-16 21:35:10 +01:00
}
2017-04-10 22:17:50 +02:00
if err != nil {
return errors . Wrap ( err , "OpenFile" )
2017-04-10 22:40:24 +02:00
}
2021-03-16 21:35:10 +01:00
defer func ( ) {
if err == nil {
return
}
// Try not to leave a partial file behind.
rmErr := r . c . Remove ( f . Name ( ) )
if rmErr != nil {
debug . Log ( "sftp: failed to remove broken file %v: %v" ,
2021-09-22 22:28:57 +02:00
f . Name ( ) , rmErr )
2021-03-16 21:35:10 +01:00
}
} ( )
2021-01-01 12:19:48 +01:00
// save data, make sure to use the optimized sftp upload method
2024-04-28 11:48:26 +02:00
wbytes , err := f . ReadFromWithConcurrency ( rd , 0 )
2017-04-10 22:40:24 +02:00
if err != nil {
2017-05-11 21:53:57 +02:00
_ = f . Close ( )
2022-12-07 09:13:19 +01:00
err = r . checkNoSpace ( dirname , rd . Length ( ) , err )
2017-04-10 22:17:50 +02:00
return errors . Wrap ( err , "Write" )
}
2020-12-18 23:41:29 +01:00
// sanity check
if wbytes != rd . Length ( ) {
_ = f . Close ( )
return errors . Errorf ( "wrote %d bytes instead of the expected %d bytes" , wbytes , rd . Length ( ) )
}
2017-04-10 22:17:50 +02:00
err = f . Close ( )
2021-09-22 22:28:57 +02:00
if err != nil {
return errors . Wrap ( err , "Close" )
}
2022-06-06 13:17:07 +02:00
// Prefer POSIX atomic rename if available.
if r . posixRename {
err = r . c . PosixRename ( tmpFilename , filename )
} else {
err = r . c . Rename ( tmpFilename , filename )
}
2021-09-22 22:28:57 +02:00
return errors . Wrap ( err , "Rename" )
2021-03-16 21:35:10 +01:00
}
// checkNoSpace checks if err was likely caused by lack of available space
// on the remote, and if so, makes it permanent.
func ( r * SFTP ) checkNoSpace ( dir string , size int64 , origErr error ) error {
// The SFTP protocol has a message for ENOSPC,
// but pkg/sftp doesn't export it and OpenSSH's sftp-server
// sends FX_FAILURE instead.
e , ok := origErr . ( * sftp . StatusError )
_ , hasExt := r . c . HasExtension ( "statvfs@openssh.com" )
if ! ok || e . FxCode ( ) != sftp . ErrSSHFxFailure || ! hasExt {
return origErr
2017-04-10 22:40:24 +02:00
}
2021-03-16 21:35:10 +01:00
fsinfo , err := r . c . StatVFS ( dir )
if err != nil {
debug . Log ( "sftp: StatVFS returned %v" , err )
return origErr
}
2022-12-07 09:13:19 +01:00
if fsinfo . Favail == 0 || fsinfo . Frsize * fsinfo . Bavail < uint64 ( size ) {
2021-03-16 21:35:10 +01:00
err := errors . New ( "sftp: no space left on device" )
return backoff . Permanent ( err )
}
return origErr
2016-01-24 01:15:35 +01:00
}
2018-01-17 05:59:16 +01:00
// Load runs fn with a reader that yields the contents of the file at h at the
// given offset.
2023-10-01 11:40:12 +02:00
func ( r * SFTP ) Load ( ctx context . Context , h backend . Handle , length int , offset int64 , fn func ( rd io . Reader ) error ) error {
2024-10-21 21:41:56 +02:00
if err := r . clientError ( ) ; err != nil {
return err
}
2024-05-11 22:08:12 +02:00
return util . DefaultLoad ( ctx , h , length , offset , r . openReader , func ( rd io . Reader ) error {
2024-05-12 11:55:34 +02:00
if length == 0 || ! feature . Flag . Enabled ( feature . BackendErrorRedesign ) {
2024-05-11 22:08:12 +02:00
return fn ( rd )
}
// there is no direct way to efficiently check whether the file is too short
// rd is already a LimitedReader which can be used to track the number of bytes read
err := fn ( rd )
// check the underlying reader to be agnostic to however fn() handles the returned error
_ , rderr := rd . Read ( [ ] byte { 0 } )
2024-04-25 21:20:23 +02:00
if rderr == io . EOF && rd . ( * util . LimitedReadCloser ) . N != 0 {
2024-05-11 22:08:12 +02:00
// file is too short
return fmt . Errorf ( "%w: %v" , errTooShort , err )
}
return err
} )
2018-01-17 05:59:16 +01:00
}
2023-10-01 11:40:12 +02:00
func ( r * SFTP ) openReader ( _ context . Context , h backend . Handle , length int , offset int64 ) ( io . ReadCloser , error ) {
2017-04-10 22:26:17 +02:00
f , err := r . c . Open ( r . Filename ( h ) )
2017-01-22 22:01:12 +01:00
if err != nil {
return nil , err
}
if offset > 0 {
_ , err = f . Seek ( offset , 0 )
if err != nil {
2017-01-23 16:20:07 +01:00
_ = f . Close ( )
2017-01-22 22:01:12 +01:00
return nil , err
}
}
if length > 0 {
2021-01-01 12:19:48 +01:00
// unlimited reads usually use io.Copy which needs WriteTo support at the underlying reader
// limited reads are usually combined with io.ReadFull which reads all required bytes into a buffer in one go
2024-04-25 21:20:23 +02:00
return util . LimitReadCloser ( f , int64 ( length ) ) , nil
2017-01-22 22:01:12 +01:00
}
2023-04-07 23:02:35 +02:00
return f , nil
2017-01-22 22:01:12 +01:00
}
2016-01-23 23:27:58 +01:00
// Stat returns information about a blob.
2023-10-01 11:40:12 +02:00
func ( r * SFTP ) Stat ( _ context . Context , h backend . Handle ) ( backend . FileInfo , error ) {
2016-08-28 19:17:17 +02:00
if err := r . clientError ( ) ; err != nil {
2023-10-01 11:40:12 +02:00
return backend . FileInfo { } , err
2016-08-28 19:17:17 +02:00
}
2017-04-10 22:26:17 +02:00
fi , err := r . c . Lstat ( r . Filename ( h ) )
2016-01-23 23:27:58 +01:00
if err != nil {
2023-10-01 11:40:12 +02:00
return backend . FileInfo { } , errors . Wrap ( err , "Lstat" )
2016-01-23 23:27:58 +01:00
}
2023-10-01 11:40:12 +02:00
return backend . FileInfo { Size : fi . Size ( ) , Name : h . Name } , nil
2016-01-23 23:27:58 +01:00
}
2015-03-28 11:50:23 +01:00
// Remove removes the content stored at name.
2023-10-01 11:40:12 +02:00
func ( r * SFTP ) Remove ( _ context . Context , h backend . Handle ) error {
2016-08-28 19:17:17 +02:00
if err := r . clientError ( ) ; err != nil {
return err
}
2017-04-10 22:26:17 +02:00
return r . c . Remove ( r . Filename ( h ) )
2014-10-04 19:20:15 +02:00
}
2018-01-20 19:34:38 +01:00
// List runs fn for each file in the backend which has the type t. When an
// error occurs (or fn returns an error), List stops and returns it.
2023-10-01 11:40:12 +02:00
func ( r * SFTP ) List ( ctx context . Context , t backend . FileType , fn func ( backend . FileInfo ) error ) error {
2024-10-21 21:41:56 +02:00
if err := r . clientError ( ) ; err != nil {
return err
}
2018-01-20 19:34:38 +01:00
basedir , subdirs := r . Basedir ( t )
walker := r . c . Walk ( basedir )
2021-08-07 19:56:59 +02:00
for {
ok := walker . Step ( )
if ! ok {
break
}
2018-01-20 19:34:38 +01:00
if walker . Err ( ) != nil {
2018-04-10 21:35:30 +02:00
if r . IsNotExist ( walker . Err ( ) ) {
debug . Log ( "ignoring non-existing directory" )
return nil
}
2018-01-20 19:34:38 +01:00
return walker . Err ( )
}
2014-11-30 15:50:30 +01:00
2018-01-20 19:34:38 +01:00
if walker . Path ( ) == basedir {
continue
}
if walker . Stat ( ) . IsDir ( ) && ! subdirs {
walker . SkipDir ( )
continue
}
fi := walker . Stat ( )
if ! fi . Mode ( ) . IsRegular ( ) {
continue
}
debug . Log ( "send %v\n" , path . Base ( walker . Path ( ) ) )
2023-10-01 11:40:12 +02:00
rfi := backend . FileInfo {
2018-01-20 19:34:38 +01:00
Name : path . Base ( walker . Path ( ) ) ,
Size : fi . Size ( ) ,
}
if ctx . Err ( ) != nil {
return ctx . Err ( )
2014-10-04 19:20:15 +02:00
}
2015-03-28 11:50:23 +01:00
2018-01-20 19:34:38 +01:00
err := fn ( rfi )
if err != nil {
return err
}
if ctx . Err ( ) != nil {
return ctx . Err ( )
}
}
2014-10-04 19:20:15 +02:00
2018-01-20 19:34:38 +01:00
return ctx . Err ( )
2014-10-04 19:20:15 +02:00
}
2016-08-28 19:17:17 +02:00
var closeTimeout = 2 * time . Second
2014-10-04 19:20:15 +02:00
// Close closes the sftp connection and terminates the underlying command.
2016-01-24 20:23:50 +01:00
func ( r * SFTP ) Close ( ) error {
if r == nil {
2015-05-01 17:13:03 +02:00
return nil
}
2016-01-26 22:16:17 +01:00
err := r . c . Close ( )
2016-09-27 22:35:08 +02:00
debug . Log ( "Close returned error %v" , err )
2015-07-05 11:06:28 +02:00
2016-08-28 19:17:17 +02:00
// wait for closeTimeout before killing the process
select {
case err := <- r . result :
return err
case <- time . After ( closeTimeout ) :
}
2016-01-24 20:23:50 +01:00
if err := r . cmd . Process . Kill ( ) ; err != nil {
2015-07-05 11:06:28 +02:00
return err
}
2016-08-28 19:17:17 +02:00
// get the error, but ignore it
<- r . result
return nil
2014-10-04 19:20:15 +02:00
}
2017-10-14 13:38:17 +02:00
2020-09-19 22:01:32 +02:00
func ( r * SFTP ) deleteRecursive ( ctx context . Context , name string ) error {
2024-08-26 21:16:22 +02:00
entries , err := r . c . ReadDir ( name )
2017-10-14 16:08:15 +02:00
if err != nil {
2024-08-26 21:16:22 +02:00
return errors . Wrapf ( err , "ReadDir(%v)" , name )
2017-10-14 16:08:15 +02:00
}
for _ , fi := range entries {
2024-07-31 19:30:47 +02:00
if ctx . Err ( ) != nil {
return ctx . Err ( )
}
2024-08-26 20:28:39 +02:00
itemName := path . Join ( name , fi . Name ( ) )
2017-10-14 16:08:15 +02:00
if fi . IsDir ( ) {
2020-09-19 22:01:32 +02:00
err := r . deleteRecursive ( ctx , itemName )
2017-10-14 16:08:15 +02:00
if err != nil {
return errors . Wrap ( err , "ReadDir" )
}
err = r . c . RemoveDirectory ( itemName )
if err != nil {
return errors . Wrap ( err , "RemoveDirectory" )
}
continue
}
err := r . c . Remove ( itemName )
if err != nil {
return errors . Wrap ( err , "ReadDir" )
}
}
return nil
}
2017-10-14 13:38:17 +02:00
// Delete removes all data in the backend.
2020-09-19 22:01:32 +02:00
func ( r * SFTP ) Delete ( ctx context . Context ) error {
return r . deleteRecursive ( ctx , r . p )
2017-10-14 13:38:17 +02:00
}
feat(backends/s3): add warmup support before repacks and restores (#5173)
* feat(backends/s3): add warmup support before repacks and restores
This commit introduces basic support for transitioning pack files stored
in cold storage to hot storage on S3 and S3-compatible providers.
To prevent unexpected behavior for existing users, the feature is gated
behind new flags:
- `s3.enable-restore`: opt-in flag (defaults to false)
- `s3.restore-days`: number of days for the restored objects to remain
in hot storage (defaults to `7`)
- `s3.restore-timeout`: maximum time to wait for a single restoration
(default to `1 day`)
- `s3.restore-tier`: retrieval tier at which the restore will be
processed. (default to `Standard`)
As restoration times can be lengthy, this implementation preemptively
restores selected packs to prevent incessant restore-delays during
downloads. This is slightly sub-optimal as we could process packs
out-of-order (as soon as they're transitioned), but this would really
add too much complexity for a marginal gain in speed.
To maintain simplicity and prevent resources exhautions with lots of
packs, no new concurrency mechanisms or goroutines were added. This just
hooks gracefully into the existing routines.
**Limitations:**
- Tests against the backend were not written due to the lack of cold
storage class support in MinIO. Testing was done manually on
Scaleway's S3-compatible object storage. If necessary, we could
explore testing with LocalStack or mocks, though this requires further
discussion.
- Currently, this feature only warms up before restores and repacks
(prune/copy), as those are the two main use-cases I came across.
Support for other commands may be added in future iterations, as long
as affected packs can be calculated in advance.
- The feature is gated behind a new alpha `s3-restore` feature flag to
make it explicit that the feature is still wet behind the ears.
- There is no explicit user notification for ongoing pack restorations.
While I think it is not necessary because of the opt-in flag, showing
some notice may improve usability (but would probably require major
refactoring in the progress bar which I didn't want to start). Another
possibility would be to add a flag to send restores requests and fail
early.
See https://github.com/restic/restic/issues/3202
* ui: warn user when files are warming up from cold storage
* refactor: remove the PacksWarmer struct
It's easier to handle multiple handles in the backend directly, and it
may open the door to reducing the number of requests made to the backend
in the future.
2025-02-01 19:26:27 +01:00
// Warmup not implemented
func ( r * SFTP ) Warmup ( _ context . Context , _ [ ] backend . Handle ) ( [ ] backend . Handle , error ) {
return [ ] backend . Handle { } , nil
}
func ( r * SFTP ) WarmupWait ( _ context . Context , _ [ ] backend . Handle ) error { return nil }