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