| // Copyright 2015 The Vanadium Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| package runutil |
| |
| import ( |
| "bytes" |
| "fmt" |
| "io" |
| "io/ioutil" |
| "os" |
| "path/filepath" |
| "runtime" |
| "sync" |
| "time" |
| ) |
| |
| // Sequence provides for convenient chaining of multiple calls to its |
| // methods to avoid repeated tests for error returns. The usage is: |
| // |
| // err := s.Run("echo", "a").Run("echo", "b").Done() |
| // |
| // The first method to encounter an error short circuits any following |
| // methods and the result of that first error is returned by the |
| // Done method or any of the other 'terminating methods' (see below). |
| // Sequence is not thread safe. It also good practice to use a new |
| // instance of a Sequence in defer's. |
| // |
| // Unless directed to specific stdout and stderr io.Writers using Capture(), |
| // the stdout and stderr output from the command is discarded, unless an error |
| // is encountered, in which case the output from the command that failed (both |
| // stdout and stderr) is written to the stderr io.Writer specified via |
| // NewSequence. In addition, in verbose mode, command execution logging |
| // is written to the stdout an stderr io.Writers configured via NewSequence. |
| // |
| // Modifier methods are provided that influence the behaviour of the |
| // next invocation of the Run method to set timeouts (Timed), to |
| // capture output (Capture), input (Read) and to set the environment (Env). |
| // For example, the following will result in a timeout error. |
| // |
| // err := s.Timeout(time.Second).Run("sleep","10").Done() |
| // err := s.Timeout(time.Second).Last("sleep","10") |
| // |
| // A sequence of commands must be terminated with a call to a 'terminating' |
| // method. The simplest are the Done or Last methods used in the examples above, |
| // but there are other methods which typically return results in addition to |
| // error, such as ReadFile(filename string) ([]byte, error). Here the usage |
| // would be: |
| // |
| // o.Stdout, _ = os.Create("foo") |
| // data, err := s.Capture(o, nil).Run("echo","b").ReadFile("foo") |
| // // data == "b" |
| // |
| // Note that terminating functions, even those that take an action, may |
| // return an error generated by a previous method. |
| // |
| // In addtion to Run which will always run a command as a subprocess, |
| // the Call method will invoke a function. Note that Capture and Timeout |
| // do not affect such calls. |
| type Sequence struct { |
| r *Run |
| err error |
| caller string |
| stdout, stderr io.Writer |
| stdin io.Reader |
| reading bool |
| env map[string]string |
| opts *Opts |
| defaultStdin io.Reader |
| defaultStdout, defaultStderr io.Writer |
| dirs []string |
| verbosity *bool |
| timeout time.Duration |
| serializedWriterLock sync.Mutex |
| } |
| |
| // NewSequence creates an instance of Sequence with default values for its |
| // environment, stdin, stderr, stdout and other supported options. |
| func NewSequence(env map[string]string, stdin io.Reader, stdout, stderr io.Writer, color, dryRun, verbose bool) *Sequence { |
| s := &Sequence{ |
| r: NewRun(env, stdin, stdout, stderr, color, dryRun, verbose), |
| defaultStdin: stdin, |
| } |
| s.defaultStdout, s.defaultStderr = s.serializeWriter(stdout), s.serializeWriter(stderr) |
| return s |
| } |
| |
| // Capture arranges for the next call to Run or Last to write its stdout and |
| // stderr output to the supplied io.Writers. This will be cleared and not used |
| // for any calls to Run or Last beyond the next one. Specifying nil for |
| // a writer will result in using the the corresponding io.Writer supplied |
| // to NewSequence. ioutil.Discard should be used to discard output. |
| func (s *Sequence) Capture(stdout, stderr io.Writer) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.stdout, s.stderr = stdout, stderr |
| return s |
| } |
| |
| // Read arranges for the next call to Run or Last to read from the supplied |
| // io.Reader. This will be cleared and not used for any calls to Run or Last |
| // beyond the next one. Specifying nil will result in reading from os.DevNull. |
| func (s *Sequence) Read(stdin io.Reader) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.reading = true |
| s.stdin = stdin |
| return s |
| } |
| |
| // Env arranges for the next call to Run, Call or Last to use the supplied |
| // environment variables. This will be cleared and not used for any calls |
| // to Run, Call or Last beyond the next one. |
| func (s *Sequence) Env(env map[string]string) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.env = env |
| return s |
| } |
| |
| // Verbosity arranges for the next call to Run, Call or Last to use the specified |
| // verbosity. This will be cleared and not used for any calls |
| // to Run, Call or Last beyond the next one. |
| func (s *Sequence) Verbose(verbosity bool) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.verbosity = &verbosity |
| return s |
| } |
| |
| // internal getOpts that doesn't override stdin, stdout, stderr |
| func (s *Sequence) getOpts() Opts { |
| var opts Opts |
| if s.opts != nil { |
| opts = *s.opts |
| } else { |
| opts = s.r.Opts() |
| } |
| return opts |
| } |
| |
| // Timeout arranges for the next call to Run or Last to be subject to the |
| // specified timeout. The timeout will be cleared and not used any calls to Run |
| // or Last beyond the next one. It has no effect for calls to Call. |
| func (s *Sequence) Timeout(timeout time.Duration) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.timeout = timeout |
| return s |
| } |
| |
| func (s *Sequence) setOpts(opts Opts) { |
| s.opts = &opts |
| } |
| |
| type wrappedError struct { |
| oe, we error |
| } |
| |
| func (ie *wrappedError) Error() string { |
| return ie.we.Error() |
| } |
| |
| // Error returns the error, if any, stored in the Sequence. |
| func (s *Sequence) Error() error { |
| if s.err != nil && len(s.caller) > 0 { |
| return &wrappedError{oe: s.err, we: fmt.Errorf("%s: %v", s.caller, s.err)} |
| } |
| return s.err |
| } |
| |
| // GetOriginalError gets the original error wrapped in the given err. |
| // If the given err is not a wrappedError, just return itself. |
| func GetOriginalError(err error) error { |
| if we, ok := err.(*wrappedError); ok { |
| return we.oe |
| } |
| return err |
| } |
| |
| // IsExist returns a boolean indicating whether the error is known |
| // to report that a file or directory already exists. |
| func IsExist(err error) bool { |
| if we, ok := err.(*wrappedError); ok { |
| return os.IsExist(we.oe) |
| } |
| return os.IsExist(err) |
| } |
| |
| // IsNotExist returns a boolean indicating whether the error is known |
| // to report that a file or directory does not exist. |
| func IsNotExist(err error) bool { |
| if we, ok := err.(*wrappedError); ok { |
| return os.IsNotExist(we.oe) |
| } |
| return os.IsNotExist(err) |
| } |
| |
| // IsPermission returns a boolean indicating whether the error is known |
| // to report that permission is denied. |
| func IsPermission(err error) bool { |
| if we, ok := err.(*wrappedError); ok { |
| return os.IsPermission(we.oe) |
| } |
| return os.IsPermission(err) |
| } |
| |
| func fmtError(depth int, err error, detail string) string { |
| _, file, line, _ := runtime.Caller(depth + 1) |
| return fmt.Sprintf("%s:%d: %s", filepath.Base(file), line, detail) |
| } |
| |
| func (s *Sequence) setError(err error, detail string) { |
| if err == nil || s.err != nil { |
| return |
| } |
| s.err = err |
| s.caller = fmtError(2, err, detail) |
| } |
| |
| func (s *Sequence) reset() { |
| s.stdin, s.stdout, s.stderr, s.env = nil, nil, nil, nil |
| s.opts, s.verbosity = nil, nil |
| s.reading = false |
| s.timeout = 0 |
| } |
| |
| func (s *Sequence) done(p1, p2 *io.PipeWriter, stdinCh, stderrCh chan error) error { |
| p1.Close() |
| p2.Close() |
| defer s.reset() |
| if stdinCh != nil { |
| if err := <-stdinCh; err != nil { |
| return err |
| } |
| } |
| if stderrCh != nil { |
| if err := <-stderrCh; err != nil { |
| return err |
| } |
| } |
| return nil |
| } |
| |
| func useIfNotNil(a, b io.Writer) io.Writer { |
| if a != nil { |
| return a |
| } |
| return b |
| } |
| |
| func writeOutput(from string, to io.Writer) { |
| if fi, err := os.Open(from); err == nil { |
| io.Copy(to, fi) |
| fi.Close() |
| } |
| if wd, err := os.Getwd(); err == nil { |
| fmt.Fprintf(to, "Current Directory: %v\n", wd) |
| } |
| } |
| |
| type sharedLockWriter struct { |
| mu *sync.Mutex |
| f io.Writer |
| } |
| |
| func (lw *sharedLockWriter) Write(d []byte) (int, error) { |
| lw.mu.Lock() |
| defer lw.mu.Unlock() |
| return lw.f.Write(d) |
| } |
| |
| func (s *Sequence) serializeWriter(a io.Writer) io.Writer { |
| if a != nil { |
| return &sharedLockWriter{&s.serializedWriterLock, a} |
| } |
| return nil |
| } |
| |
| func (s *Sequence) initAndDefer() func() { |
| if s.stdout == nil && s.stderr == nil { |
| fout, err := ioutil.TempFile("", "seq") |
| if err != nil { |
| return func() {} |
| } |
| opts := s.getOpts() |
| opts.Stdout, opts.Stderr = s.serializeWriter(fout), s.serializeWriter(fout) |
| opts.Env = s.env |
| if s.reading { |
| opts.Stdin = s.stdin |
| } |
| if s.verbosity != nil { |
| opts.Verbose = *s.verbosity |
| } |
| s.setOpts(opts) |
| return func() { |
| filename := fout.Name() |
| fout.Close() |
| defer func() { os.Remove(filename); s.opts = nil }() |
| if s.err != nil { |
| writeOutput(filename, useIfNotNil(s.defaultStderr, os.Stderr)) |
| } |
| if opts.Verbose && s.defaultStderr != s.defaultStdout { |
| writeOutput(filename, useIfNotNil(s.defaultStdout, os.Stdout)) |
| } |
| } |
| } |
| opts := s.getOpts() |
| rStdout, wStdout := io.Pipe() |
| rStderr, wStderr := io.Pipe() |
| opts.Stdout = wStdout |
| opts.Stderr = wStderr |
| opts.Env = s.env |
| if s.reading { |
| opts.Stdin = s.stdin |
| } |
| var stdinCh, stderrCh chan error |
| stdout, stderr := s.serializeWriter(s.stdout), s.serializeWriter(s.stderr) |
| if stdout != nil { |
| stdinCh = make(chan error) |
| go copy(stdout, rStdout, stdinCh) |
| } else { |
| opts.Stdout = s.defaultStdout |
| } |
| if stderr != nil { |
| stderrCh = make(chan error) |
| go copy(stderr, rStderr, stderrCh) |
| } else { |
| opts.Stderr = s.defaultStderr |
| } |
| if s.verbosity != nil { |
| opts.Verbose = *s.verbosity |
| } |
| s.setOpts(opts) |
| return func() { |
| if err := s.done(wStdout, wStderr, stdinCh, stderrCh); err != nil && s.err == nil { |
| s.err = err |
| } |
| } |
| } |
| |
| func fmtArgs(args ...interface{}) string { |
| if len(args) == 0 { |
| return "" |
| } |
| var out bytes.Buffer |
| for _, a := range args { |
| switch at := a.(type) { |
| case string: |
| fmt.Fprintf(&out, ", %q", at) |
| default: |
| fmt.Fprintf(&out, ", %v", a) |
| } |
| } |
| return out.String() |
| } |
| |
| func fmtStringArgs(args ...string) string { |
| if len(args) == 0 { |
| return "" |
| } |
| var out bytes.Buffer |
| for _, a := range args { |
| fmt.Fprintf(&out, ", %q", a) |
| } |
| return out.String() |
| } |
| |
| // Pushd pushes the current directory onto a stack and changes directory |
| // to the specified one. Calling any terminating function will pop back |
| // to the first element in the stack on completion of that function. |
| func (s *Sequence) Pushd(dir string) *Sequence { |
| cwd, err := os.Getwd() |
| if err != nil { |
| s.setError(err, "Pushd("+dir+"): os.Getwd") |
| return s |
| } |
| s.dirs = append(s.dirs, cwd) |
| s.setError(s.r.Chdir(dir), "Pushd("+dir+")") |
| return s |
| } |
| |
| // Popd popds the last directory from the directory stack and chdir's to it. |
| // Calling any termination function will pop back to the first element in |
| // the stack on completion of that function. |
| func (s *Sequence) Popd() *Sequence { |
| if s.err != nil { |
| return s |
| } |
| if len(s.dirs) == 0 { |
| s.setError(fmt.Errorf("directory stack is empty"), "Popd()") |
| return s |
| } |
| last := s.dirs[len(s.dirs)-1] |
| s.dirs = s.dirs[:len(s.dirs)-1] |
| s.setError(s.r.Chdir(last), "Popd() -> "+last) |
| return s |
| } |
| |
| // Run runs the given command as a subprocess. |
| func (s *Sequence) Run(path string, args ...string) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| defer s.initAndDefer()() |
| s.setError(s.r.command(s.timeout, s.getOpts(), path, args...), fmt.Sprintf("Run(%q%s)", path, fmtStringArgs(args...))) |
| return s |
| } |
| |
| // Last runs the given command as a subprocess and returns an error |
| // immediately terminating the sequence, it is equivalent to |
| // calling s.Run(path, args...).Done(). |
| func (s *Sequence) Last(path string, args ...string) error { |
| if s.err != nil { |
| return s.Done() |
| } |
| defer s.Done() |
| defer s.initAndDefer()() |
| s.setError(s.r.command(s.timeout, s.getOpts(), path, args...), fmt.Sprintf("Last(%q%s)", path, fmtStringArgs(args...))) |
| return s.err |
| } |
| |
| // Call runs the given function. Note that Capture and Timeout have no |
| // effect on invocations of Call, but Opts can control logging. |
| func (s *Sequence) Call(fn func() error, format string, args ...interface{}) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| defer s.initAndDefer()() |
| s.setError(s.r.FunctionWithOpts(s.getOpts(), fn, format, args...), fmt.Sprintf("Call(%s%s)", format, fmtArgs(args))) |
| return s |
| } |
| |
| // Chdir is a wrapper around os.Chdir that handles options such as |
| // "verbose" or "dry run". |
| func (s *Sequence) Chdir(dir string) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.setError(s.r.Chdir(dir), "Chdir("+dir+")") |
| return s |
| } |
| |
| // Chmod is a wrapper around os.Chmod that handles options such as |
| // "verbose" or "dry run". |
| func (s *Sequence) Chmod(dir string, mode os.FileMode) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| if err := s.r.Chmod(dir, mode); err != nil { |
| s.setError(err, fmt.Sprintf("Chmod(%s, %s)", dir, mode)) |
| } |
| return s |
| } |
| |
| // MkdirAll is a wrapper around os.MkdirAll that handles options such |
| // as "verbose" or "dry run". |
| func (s *Sequence) MkdirAll(dir string, mode os.FileMode) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.setError(s.r.MkdirAll(dir, mode), fmt.Sprintf("MkdirAll(%s, %s)", dir, mode)) |
| return s |
| } |
| |
| // RemoveAll is a wrapper around os.RemoveAll that handles options |
| // such as "verbose" or "dry run". |
| func (s *Sequence) RemoveAll(dir string) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.setError(s.r.RemoveAll(dir), fmt.Sprintf("RemoveAll(%s)", dir)) |
| return s |
| } |
| |
| // Remove is a wrapper around os.Remove that handles options |
| // such as "verbose" or "dry run". |
| func (s *Sequence) Remove(file string) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.setError(s.r.Remove(file), fmt.Sprintf("Remove(%s)", file)) |
| return s |
| } |
| |
| // Rename is a wrapper around os.Rename that handles options such as |
| // "verbose" or "dry run". |
| func (s *Sequence) Rename(src, dst string) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.setError(s.r.Rename(src, dst), fmt.Sprintf("Rename(%s, %s)", src, dst)) |
| return s |
| } |
| |
| // Symlink is a wrapper around os.Symlink that handles options such as |
| // "verbose" or "dry run". |
| func (s *Sequence) Symlink(src, dst string) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.setError(s.r.Symlink(src, dst), fmt.Sprintf("Symlink(%s, %s)", src, dst)) |
| return s |
| } |
| |
| // Output logs the given list of lines using the currently in effect verbosity |
| // as specified by Opts, or the default otherwise. |
| func (s *Sequence) Output(output []string) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| s.r.OutputWithOpts(s.getOpts(), output) |
| return s |
| } |
| |
| // Fprintf calls fmt.Fprintf. |
| func (s *Sequence) Fprintf(f io.Writer, format string, args ...interface{}) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| fmt.Fprintf(f, format, args...) |
| return s |
| } |
| |
| // Done returns the error stored in the Sequence and pops back to the first |
| // entry in the directory stack if Pushd has been called. Done is a terminating |
| // function. There is no need to ensure that Done is called before returning |
| // from a function that uses a sequence unless it is necessary to pop the |
| // stack. |
| func (s *Sequence) Done() error { |
| rerr := s.Error() |
| s.err = nil |
| s.caller = "" |
| s.reset() |
| if len(s.dirs) > 0 { |
| cwd := s.dirs[0] |
| s.dirs = nil |
| if err := s.r.Chdir(cwd); err != nil { |
| detail := "Done: Chdir(" + cwd + ")" |
| if rerr == nil { |
| s.setError(err, detail) |
| } else { |
| // In the unlikely event that Chdir fails in addition to an |
| // earlier error, we append an appropriate error message. |
| s.err = fmt.Errorf("%v\n%v", rerr, fmtError(1, err, detail)) |
| } |
| return s.Error() |
| } |
| } |
| return rerr |
| } |
| |
| // Open is a wrapper around os.Open that handles options such as |
| // "verbose" or "dry run". Open is a terminating function. |
| func (s *Sequence) Open(name string) (*os.File, error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| f, err := s.r.Open(name) |
| s.setError(err, fmt.Sprintf("Open(%s)", name)) |
| return f, s.Done() |
| } |
| |
| // OpenFile is a wrapper around os.OpenFile that handles options such as |
| // "verbose" or "dry run". OpenFile is a terminating function. |
| func (s *Sequence) OpenFile(name string, flag int, perm os.FileMode) (*os.File, error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| f, err := s.r.OpenFile(name, flag, perm) |
| s.setError(err, fmt.Sprintf("OpenFile(%s, %s, %s)", name, flag, perm)) |
| return f, s.Done() |
| } |
| |
| // Create is a wrapper around os.Create that handles options such as "verbose" |
| // or "dry run". Create is a terminating function. |
| func (s *Sequence) Create(name string) (*os.File, error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| f, err := s.r.Create(name) |
| s.setError(err, fmt.Sprintf("Create(%s)", name)) |
| return f, s.Done() |
| } |
| |
| // ReadDir is a wrapper around ioutil.ReadDir that handles options |
| // such as "verbose" or "dry run". ReadDir is a terminating function. |
| func (s *Sequence) ReadDir(dirname string) ([]os.FileInfo, error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| fi, err := s.r.ReadDir(dirname) |
| s.setError(err, fmt.Sprintf("ReadDir(%s)", dirname)) |
| return fi, s.Done() |
| } |
| |
| // ReadFile is a wrapper around ioutil.ReadFile that handles options |
| // such as "verbose" or "dry run". ReadFile is a terminating function. |
| func (s *Sequence) ReadFile(filename string) ([]byte, error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| data, err := s.r.ReadFile(filename) |
| s.setError(err, fmt.Sprintf("ReadFile(%s)", filename)) |
| return data, s.Done() |
| } |
| |
| // WriteFile is a wrapper around ioutil.WriteFile that handles options |
| // such as "verbose" or "dry run". |
| func (s *Sequence) WriteFile(filename string, data []byte, perm os.FileMode) *Sequence { |
| if s.err != nil { |
| return s |
| } |
| err := s.r.WriteFile(filename, data, perm) |
| s.setError(err, fmt.Sprintf("WriteFile(%s, %10s, %s)", filename, data, perm)) |
| return s |
| } |
| |
| // Copy is a wrapper around io.Copy that handles options such as "verbose" or |
| // "dry run". Copy is a terminating function. |
| func (s *Sequence) Copy(dst *os.File, src io.Reader) (int64, error) { |
| if s.err != nil { |
| return 0, s.Done() |
| } |
| n, err := s.r.Copy(dst, src) |
| s.setError(err, fmt.Sprintf("Copy(%s, %s)", dst, src)) |
| return n, s.Done() |
| } |
| |
| // Stat is a wrapper around os.Stat that handles options such as |
| // "verbose" or "dry run". Stat is a terminating function. |
| func (s *Sequence) Stat(name string) (os.FileInfo, error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| fi, err := s.r.Stat(name) |
| s.setError(err, fmt.Sprintf("Stat(%s)", name)) |
| return fi, s.Done() |
| } |
| |
| // Lstat is a wrapper around os.Lstat that handles options such as |
| // "verbose" or "dry run". Lstat is a terminating function. |
| func (s *Sequence) Lstat(name string) (os.FileInfo, error) { |
| if s.err != nil { |
| return nil, s.Done() |
| } |
| fi, err := s.r.Lstat(name) |
| s.setError(err, fmt.Sprintf("Lstat(%s)", name)) |
| return fi, s.Done() |
| } |
| |
| // Readlink is a wrapper around os.Readlink that handles options such as |
| // "verbose" or "dry run". Lstat is a terminating function. |
| func (s *Sequence) Readlink(name string) (string, error) { |
| if s.err != nil { |
| return "", s.Done() |
| } |
| link, err := s.r.Readlink(name) |
| s.setError(err, fmt.Sprintf("Readlink(%s)", name)) |
| return link, s.Done() |
| } |
| |
| // TempDir is a wrapper around ioutil.TempDir that handles options |
| // such as "verbose" or "dry run". TempDir is a terminating function. |
| func (s *Sequence) TempDir(dir, prefix string) (string, error) { |
| if s.err != nil { |
| return "", s.Done() |
| } |
| name, err := s.r.TempDir(dir, prefix) |
| s.setError(err, fmt.Sprintf("TempDir(%s,%s)", dir, prefix)) |
| return name, s.Done() |
| } |
| |
| // IsDir is a wrapper around os.Stat with appropriate logging. |
| // IsDir is a terminating function. |
| func (s *Sequence) IsDir(dirname string) (bool, error) { |
| if s.err != nil { |
| return false, s.Done() |
| } |
| t, err := s.r.IsDir(dirname) |
| s.setError(err, fmt.Sprintf("IsDir(%s)", dirname)) |
| return t, s.Done() |
| } |
| |
| // DirExists tests if a directory exists with appropriate logging. |
| // DirExists is a terminating function. |
| func (s *Sequence) DirectoryExists(dir string) (bool, error) { |
| if s.err != nil { |
| return false, s.Done() |
| } |
| isdir, err := s.r.IsDir(dir) |
| if err != nil { |
| return false, s.Done() |
| } |
| return isdir, s.Done() |
| } |
| |
| // FileExists tests if a file exists with appropriate logging. |
| // FileExists is a terminating function. |
| func (s *Sequence) FileExists(file string) (bool, error) { |
| if s.err != nil { |
| return false, s.Done() |
| } |
| _, err := s.r.Stat(file) |
| return err == nil, s.Done() |
| } |
| |
| func copy(to io.Writer, from io.Reader, ch chan error) { |
| _, err := io.Copy(to, from) |
| ch <- err |
| } |