github.com/mckael/restic@v0.8.3/internal/backend/sftp/sftp.go (about) 1 package sftp 2 3 import ( 4 "bufio" 5 "context" 6 "fmt" 7 "io" 8 "os" 9 "os/exec" 10 "path" 11 "strings" 12 "time" 13 14 "github.com/restic/restic/internal/errors" 15 "github.com/restic/restic/internal/restic" 16 17 "github.com/restic/restic/internal/backend" 18 "github.com/restic/restic/internal/debug" 19 20 "github.com/pkg/sftp" 21 ) 22 23 // SFTP is a backend in a directory accessed via SFTP. 24 type SFTP struct { 25 c *sftp.Client 26 p string 27 28 cmd *exec.Cmd 29 result <-chan error 30 31 backend.Layout 32 Config 33 } 34 35 var _ restic.Backend = &SFTP{} 36 37 const defaultLayout = "default" 38 39 func startClient(program string, args ...string) (*SFTP, error) { 40 debug.Log("start client %v %v", program, args) 41 // Connect to a remote host and request the sftp subsystem via the 'ssh' 42 // command. This assumes that passwordless login is correctly configured. 43 cmd := exec.Command(program, args...) 44 45 // prefix the errors with the program name 46 stderr, err := cmd.StderrPipe() 47 if err != nil { 48 return nil, errors.Wrap(err, "cmd.StderrPipe") 49 } 50 51 go func() { 52 sc := bufio.NewScanner(stderr) 53 for sc.Scan() { 54 fmt.Fprintf(os.Stderr, "subprocess %v: %v\n", program, sc.Text()) 55 } 56 }() 57 58 // get stdin and stdout 59 wr, err := cmd.StdinPipe() 60 if err != nil { 61 return nil, errors.Wrap(err, "cmd.StdinPipe") 62 } 63 rd, err := cmd.StdoutPipe() 64 if err != nil { 65 return nil, errors.Wrap(err, "cmd.StdoutPipe") 66 } 67 68 bg, err := startForeground(cmd) 69 if err != nil { 70 return nil, errors.Wrap(err, "cmd.Start") 71 } 72 73 // wait in a different goroutine 74 ch := make(chan error, 1) 75 go func() { 76 err := cmd.Wait() 77 debug.Log("ssh command exited, err %v", err) 78 ch <- errors.Wrap(err, "cmd.Wait") 79 }() 80 81 // open the SFTP session 82 client, err := sftp.NewClientPipe(rd, wr) 83 if err != nil { 84 return nil, errors.Errorf("unable to start the sftp session, error: %v", err) 85 } 86 87 err = bg() 88 if err != nil { 89 return nil, errors.Wrap(err, "bg") 90 } 91 92 return &SFTP{c: client, cmd: cmd, result: ch}, nil 93 } 94 95 // clientError returns an error if the client has exited. Otherwise, nil is 96 // returned immediately. 97 func (r *SFTP) clientError() error { 98 select { 99 case err := <-r.result: 100 debug.Log("client has exited with err %v", err) 101 return err 102 default: 103 } 104 105 return nil 106 } 107 108 // Open opens an sftp backend as described by the config by running 109 // "ssh" with the appropriate arguments (or cfg.Command, if set). The function 110 // preExec is run just before, postExec just after starting a program. 111 func Open(cfg Config) (*SFTP, error) { 112 debug.Log("open backend with config %#v", cfg) 113 114 cmd, args, err := buildSSHCommand(cfg) 115 if err != nil { 116 return nil, err 117 } 118 119 sftp, err := startClient(cmd, args...) 120 if err != nil { 121 debug.Log("unable to start program: %v", err) 122 return nil, err 123 } 124 125 sftp.Layout, err = backend.ParseLayout(sftp, cfg.Layout, defaultLayout, cfg.Path) 126 if err != nil { 127 return nil, err 128 } 129 130 debug.Log("layout: %v\n", sftp.Layout) 131 132 sftp.Config = cfg 133 sftp.p = cfg.Path 134 return sftp, nil 135 } 136 137 func (r *SFTP) mkdirAllDataSubdirs() error { 138 for _, d := range r.Paths() { 139 err := r.mkdirAll(d, backend.Modes.Dir) 140 debug.Log("mkdirAll %v -> %v", d, err) 141 if err != nil { 142 return err 143 } 144 } 145 146 return nil 147 } 148 149 // Join combines path components with slashes (according to the sftp spec). 150 func (r *SFTP) Join(p ...string) string { 151 return path.Join(p...) 152 } 153 154 // ReadDir returns the entries for a directory. 155 func (r *SFTP) ReadDir(dir string) ([]os.FileInfo, error) { 156 fi, err := r.c.ReadDir(dir) 157 158 // sftp client does not specify dir name on error, so add it here 159 err = errors.Wrapf(err, "(%v)", dir) 160 161 return fi, err 162 } 163 164 // IsNotExist returns true if the error is caused by a not existing file. 165 func (r *SFTP) IsNotExist(err error) bool { 166 err = errors.Cause(err) 167 168 if os.IsNotExist(err) { 169 return true 170 } 171 172 statusError, ok := err.(*sftp.StatusError) 173 if !ok { 174 return false 175 } 176 177 return statusError.Error() == `sftp: "No such file" (SSH_FX_NO_SUCH_FILE)` 178 } 179 180 func buildSSHCommand(cfg Config) (cmd string, args []string, err error) { 181 if cfg.Command != "" { 182 return SplitShellArgs(cfg.Command) 183 } 184 185 cmd = "ssh" 186 187 hostport := strings.Split(cfg.Host, ":") 188 args = []string{hostport[0]} 189 if len(hostport) > 1 { 190 args = append(args, "-p", hostport[1]) 191 } 192 if cfg.User != "" { 193 args = append(args, "-l") 194 args = append(args, cfg.User) 195 } 196 args = append(args, "-s") 197 args = append(args, "sftp") 198 return cmd, args, nil 199 } 200 201 // Create creates an sftp backend as described by the config by running "ssh" 202 // with the appropriate arguments (or cfg.Command, if set). The function 203 // preExec is run just before, postExec just after starting a program. 204 func Create(cfg Config) (*SFTP, error) { 205 cmd, args, err := buildSSHCommand(cfg) 206 if err != nil { 207 return nil, err 208 } 209 210 sftp, err := startClient(cmd, args...) 211 if err != nil { 212 debug.Log("unable to start program: %v", err) 213 return nil, err 214 } 215 216 sftp.Layout, err = backend.ParseLayout(sftp, cfg.Layout, defaultLayout, cfg.Path) 217 if err != nil { 218 return nil, err 219 } 220 221 // test if config file already exists 222 _, err = sftp.c.Lstat(Join(cfg.Path, backend.Paths.Config)) 223 if err == nil { 224 return nil, errors.New("config file already exists") 225 } 226 227 // create paths for data and refs 228 if err = sftp.mkdirAllDataSubdirs(); err != nil { 229 return nil, err 230 } 231 232 err = sftp.Close() 233 if err != nil { 234 return nil, errors.Wrap(err, "Close") 235 } 236 237 // open backend 238 return Open(cfg) 239 } 240 241 // Location returns this backend's location (the directory name). 242 func (r *SFTP) Location() string { 243 return r.p 244 } 245 246 func (r *SFTP) mkdirAll(dir string, mode os.FileMode) error { 247 // check if directory already exists 248 fi, err := r.c.Lstat(dir) 249 if err == nil { 250 if fi.IsDir() { 251 return nil 252 } 253 254 return errors.Errorf("mkdirAll(%s): entry exists but is not a directory", dir) 255 } 256 257 // create parent directories 258 errMkdirAll := r.mkdirAll(path.Dir(dir), backend.Modes.Dir) 259 260 // create directory 261 errMkdir := r.c.Mkdir(dir) 262 263 // test if directory was created successfully 264 fi, err = r.c.Lstat(dir) 265 if err != nil { 266 // return previous errors 267 return errors.Errorf("mkdirAll(%s): unable to create directories: %v, %v", dir, errMkdirAll, errMkdir) 268 } 269 270 if !fi.IsDir() { 271 return errors.Errorf("mkdirAll(%s): entry exists but is not a directory", dir) 272 } 273 274 // set mode 275 return r.c.Chmod(dir, mode) 276 } 277 278 // Join joins the given paths and cleans them afterwards. This always uses 279 // forward slashes, which is required by sftp. 280 func Join(parts ...string) string { 281 return path.Clean(path.Join(parts...)) 282 } 283 284 // Save stores data in the backend at the handle. 285 func (r *SFTP) Save(ctx context.Context, h restic.Handle, rd io.Reader) (err error) { 286 debug.Log("Save %v", h) 287 if err := r.clientError(); err != nil { 288 return err 289 } 290 291 if err := h.Valid(); err != nil { 292 return err 293 } 294 295 filename := r.Filename(h) 296 297 // create new file 298 f, err := r.c.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY) 299 300 if r.IsNotExist(err) { 301 // error is caused by a missing directory, try to create it 302 mkdirErr := r.mkdirAll(r.Dirname(h), backend.Modes.Dir) 303 if mkdirErr != nil { 304 debug.Log("error creating dir %v: %v", r.Dirname(h), mkdirErr) 305 } else { 306 // try again 307 f, err = r.c.OpenFile(filename, os.O_CREATE|os.O_EXCL|os.O_WRONLY) 308 } 309 } 310 311 if err != nil { 312 return errors.Wrap(err, "OpenFile") 313 } 314 315 // save data 316 _, err = io.Copy(f, rd) 317 if err != nil { 318 _ = f.Close() 319 return errors.Wrap(err, "Write") 320 } 321 322 err = f.Close() 323 if err != nil { 324 return errors.Wrap(err, "Close") 325 } 326 327 return errors.Wrap(r.c.Chmod(filename, backend.Modes.File), "Chmod") 328 } 329 330 // Load runs fn with a reader that yields the contents of the file at h at the 331 // given offset. 332 func (r *SFTP) Load(ctx context.Context, h restic.Handle, length int, offset int64, fn func(rd io.Reader) error) error { 333 return backend.DefaultLoad(ctx, h, length, offset, r.openReader, fn) 334 } 335 336 func (r *SFTP) openReader(ctx context.Context, h restic.Handle, length int, offset int64) (io.ReadCloser, error) { 337 debug.Log("Load %v, length %v, offset %v", h, length, offset) 338 if err := h.Valid(); err != nil { 339 return nil, err 340 } 341 342 if offset < 0 { 343 return nil, errors.New("offset is negative") 344 } 345 346 f, err := r.c.Open(r.Filename(h)) 347 if err != nil { 348 return nil, err 349 } 350 351 if offset > 0 { 352 _, err = f.Seek(offset, 0) 353 if err != nil { 354 _ = f.Close() 355 return nil, err 356 } 357 } 358 359 if length > 0 { 360 return backend.LimitReadCloser(f, int64(length)), nil 361 } 362 363 return f, nil 364 } 365 366 // Stat returns information about a blob. 367 func (r *SFTP) Stat(ctx context.Context, h restic.Handle) (restic.FileInfo, error) { 368 debug.Log("Stat(%v)", h) 369 if err := r.clientError(); err != nil { 370 return restic.FileInfo{}, err 371 } 372 373 if err := h.Valid(); err != nil { 374 return restic.FileInfo{}, err 375 } 376 377 fi, err := r.c.Lstat(r.Filename(h)) 378 if err != nil { 379 return restic.FileInfo{}, errors.Wrap(err, "Lstat") 380 } 381 382 return restic.FileInfo{Size: fi.Size(), Name: h.Name}, nil 383 } 384 385 // Test returns true if a blob of the given type and name exists in the backend. 386 func (r *SFTP) Test(ctx context.Context, h restic.Handle) (bool, error) { 387 debug.Log("Test(%v)", h) 388 if err := r.clientError(); err != nil { 389 return false, err 390 } 391 392 _, err := r.c.Lstat(r.Filename(h)) 393 if os.IsNotExist(errors.Cause(err)) { 394 return false, nil 395 } 396 397 if err != nil { 398 return false, errors.Wrap(err, "Lstat") 399 } 400 401 return true, nil 402 } 403 404 // Remove removes the content stored at name. 405 func (r *SFTP) Remove(ctx context.Context, h restic.Handle) error { 406 debug.Log("Remove(%v)", h) 407 if err := r.clientError(); err != nil { 408 return err 409 } 410 411 return r.c.Remove(r.Filename(h)) 412 } 413 414 // List runs fn for each file in the backend which has the type t. When an 415 // error occurs (or fn returns an error), List stops and returns it. 416 func (r *SFTP) List(ctx context.Context, t restic.FileType, fn func(restic.FileInfo) error) error { 417 debug.Log("List %v", t) 418 419 basedir, subdirs := r.Basedir(t) 420 walker := r.c.Walk(basedir) 421 for walker.Step() { 422 if walker.Err() != nil { 423 return walker.Err() 424 } 425 426 if walker.Path() == basedir { 427 continue 428 } 429 430 if walker.Stat().IsDir() && !subdirs { 431 walker.SkipDir() 432 continue 433 } 434 435 fi := walker.Stat() 436 if !fi.Mode().IsRegular() { 437 continue 438 } 439 440 debug.Log("send %v\n", path.Base(walker.Path())) 441 442 rfi := restic.FileInfo{ 443 Name: path.Base(walker.Path()), 444 Size: fi.Size(), 445 } 446 447 if ctx.Err() != nil { 448 return ctx.Err() 449 } 450 451 err := fn(rfi) 452 if err != nil { 453 return err 454 } 455 456 if ctx.Err() != nil { 457 return ctx.Err() 458 } 459 } 460 461 return ctx.Err() 462 } 463 464 var closeTimeout = 2 * time.Second 465 466 // Close closes the sftp connection and terminates the underlying command. 467 func (r *SFTP) Close() error { 468 debug.Log("Close") 469 if r == nil { 470 return nil 471 } 472 473 err := r.c.Close() 474 debug.Log("Close returned error %v", err) 475 476 // wait for closeTimeout before killing the process 477 select { 478 case err := <-r.result: 479 return err 480 case <-time.After(closeTimeout): 481 } 482 483 if err := r.cmd.Process.Kill(); err != nil { 484 return err 485 } 486 487 // get the error, but ignore it 488 <-r.result 489 return nil 490 } 491 492 func (r *SFTP) deleteRecursive(name string) error { 493 entries, err := r.ReadDir(name) 494 if err != nil { 495 return errors.Wrap(err, "ReadDir") 496 } 497 498 for _, fi := range entries { 499 itemName := r.Join(name, fi.Name()) 500 if fi.IsDir() { 501 err := r.deleteRecursive(itemName) 502 if err != nil { 503 return errors.Wrap(err, "ReadDir") 504 } 505 506 err = r.c.RemoveDirectory(itemName) 507 if err != nil { 508 return errors.Wrap(err, "RemoveDirectory") 509 } 510 511 continue 512 } 513 514 err := r.c.Remove(itemName) 515 if err != nil { 516 return errors.Wrap(err, "ReadDir") 517 } 518 } 519 520 return nil 521 } 522 523 // Delete removes all data in the backend. 524 func (r *SFTP) Delete(context.Context) error { 525 return r.deleteRecursive(r.p) 526 }