go-hep.org/x/hep@v0.38.1/xrootd/cmd/xrd-cp/main.go (about)

     1  // Copyright ©2018 The go-hep Authors. All rights reserved.
     2  // Use of this source code is governed by a BSD-style
     3  // license that can be found in the LICENSE file.
     4  
     5  // Command xrd-cp copies files and directories from a remote xrootd server
     6  // to local storage.
     7  //
     8  // Usage:
     9  //
    10  //	$> xrd-cp [OPTIONS] <src-1> [<src-2> [...]] <dst>
    11  //
    12  // Example:
    13  //
    14  //	$> xrd-cp root://server.example.com/some/file1.txt .
    15  //	$> xrd-cp root://gopher@server.example.com/some/file1.txt .
    16  //	$> xrd-cp root://server.example.com/some/file1.txt foo.txt
    17  //	$> xrd-cp root://server.example.com/some/file1.txt - > foo.txt
    18  //	$> xrd-cp -r root://server.example.com/some/dir .
    19  //	$> xrd-cp -r root://server.example.com/some/dir outdir
    20  //
    21  // Options:
    22  //
    23  //	-r	copy directories recursively
    24  //	-v	enable verbose mode
    25  package main
    26  
    27  import (
    28  	"context"
    29  	"flag"
    30  	"fmt"
    31  	"io"
    32  	"log"
    33  	"os"
    34  	stdpath "path"
    35  
    36  	"go-hep.org/x/hep/xrootd"
    37  	"go-hep.org/x/hep/xrootd/xrdfs"
    38  	"go-hep.org/x/hep/xrootd/xrdio"
    39  )
    40  
    41  func init() {
    42  	flag.Usage = func() {
    43  		fmt.Fprintf(os.Stderr, `xrd-cp copies files and directories from a remote xrootd server to local storage.
    44  
    45  Usage:
    46  
    47   $> xrd-cp [OPTIONS] <src-1> [<src-2> [...]] <dst>
    48  
    49  Example:
    50  
    51   $> xrd-cp root://server.example.com/some/file1.txt .
    52   $> xrd-cp root://gopher@server.example.com/some/file1.txt .
    53   $> xrd-cp root://server.example.com/some/file1.txt foo.txt
    54   $> xrd-cp root://server.example.com/some/file1.txt - > foo.txt
    55   $> xrd-cp -r root://server.example.com/some/dir .
    56   $> xrd-cp -r root://server.example.com/some/dir outdir
    57  
    58  Options:
    59  `)
    60  		flag.PrintDefaults()
    61  	}
    62  }
    63  
    64  func main() {
    65  	log.SetPrefix("xrd-cp: ")
    66  	log.SetFlags(0)
    67  
    68  	var (
    69  		recFlag     = flag.Bool("r", false, "copy directories recursively")
    70  		verboseFlag = flag.Bool("v", false, "enable verbose mode")
    71  	)
    72  
    73  	flag.Parse()
    74  
    75  	switch n := flag.NArg(); n {
    76  	case 0:
    77  		flag.Usage()
    78  		log.Fatalf("missing file operand")
    79  	case 1:
    80  		flag.Usage()
    81  		log.Fatalf("missing destination file operand after %q", flag.Arg(0))
    82  	case 2:
    83  		err := xrdcopy(flag.Arg(1), flag.Arg(0), *recFlag, *verboseFlag)
    84  		if err != nil {
    85  			log.Fatalf("could not copy %q to %q: %v", flag.Arg(0), flag.Arg(1), err)
    86  		}
    87  	default:
    88  		dst := flag.Arg(flag.NArg() - 1)
    89  		for _, src := range flag.Args()[:flag.NArg()-1] {
    90  			err := xrdcopy(dst, src, *recFlag, *verboseFlag)
    91  			if err != nil {
    92  				log.Fatalf("could not copy %q to %q: %v", src, dst, err)
    93  			}
    94  		}
    95  	}
    96  }
    97  
    98  func xrdcopy(dst, srcPath string, recursive, verbose bool) error {
    99  	cli, src, err := xrdremote(srcPath)
   100  	if err != nil {
   101  		return err
   102  	}
   103  	defer cli.Close()
   104  
   105  	ctx := context.Background()
   106  
   107  	fs := cli.FS()
   108  	var jobs jobs
   109  	var addDir func(root, src string) error
   110  
   111  	addDir = func(root, src string) error {
   112  		fi, err := fs.Stat(ctx, src)
   113  		if err != nil {
   114  			return fmt.Errorf("could not stat remote src: %w", err)
   115  		}
   116  		switch {
   117  		case fi.IsDir():
   118  			if !recursive {
   119  				return fmt.Errorf("xrd-cp: -r not specified; omitting directory %q", src)
   120  			}
   121  			dst := stdpath.Join(root, stdpath.Base(src))
   122  			err = os.MkdirAll(dst, 0755)
   123  			if err != nil {
   124  				return fmt.Errorf("could not create output directory: %w", err)
   125  			}
   126  
   127  			ents, err := fs.Dirlist(ctx, src)
   128  			if err != nil {
   129  				return fmt.Errorf("could not list directory: %w", err)
   130  			}
   131  			for _, e := range ents {
   132  				err = addDir(dst, stdpath.Join(src, e.Name()))
   133  				if err != nil {
   134  					return err
   135  				}
   136  			}
   137  		default:
   138  			jobs.add(job{
   139  				fs:  fs,
   140  				src: src,
   141  				dst: stdpath.Join(root, stdpath.Base(src)),
   142  			})
   143  		}
   144  		return nil
   145  	}
   146  
   147  	fiSrc, err := fs.Stat(ctx, src)
   148  	if err != nil {
   149  		return fmt.Errorf("could not stat remote src: %w", err)
   150  	}
   151  
   152  	fiDst, errDst := os.Stat(dst)
   153  	switch {
   154  	case fiSrc.IsDir():
   155  		switch {
   156  		case errDst != nil && os.IsNotExist(errDst):
   157  			err = os.MkdirAll(dst, 0755)
   158  			if err != nil {
   159  				return fmt.Errorf("could not create output directory: %w", err)
   160  			}
   161  			ents, err := fs.Dirlist(ctx, src)
   162  			if err != nil {
   163  				return fmt.Errorf("could not list directory: %w", err)
   164  			}
   165  			for _, e := range ents {
   166  				err = addDir(dst, stdpath.Join(src, e.Name()))
   167  				if err != nil {
   168  					return err
   169  				}
   170  			}
   171  
   172  		case errDst != nil:
   173  			return fmt.Errorf("could not stat local dst: %w", errDst)
   174  		case fiDst.IsDir():
   175  			err = addDir(dst, src)
   176  			if err != nil {
   177  				return err
   178  			}
   179  		}
   180  
   181  	default:
   182  		switch {
   183  		case errDst != nil && os.IsNotExist(errDst):
   184  			// ok... dst will be the output file.
   185  		case errDst != nil:
   186  			return fmt.Errorf("could not stat local dst: %w", errDst)
   187  		case fiDst.IsDir():
   188  			dst = stdpath.Join(dst, stdpath.Base(src))
   189  		}
   190  
   191  		jobs.add(job{
   192  			fs:  fs,
   193  			src: src,
   194  			dst: dst,
   195  		})
   196  	}
   197  
   198  	n, err := jobs.run(ctx)
   199  	if verbose {
   200  		log.Printf("transferred %d bytes", n)
   201  	}
   202  	return err
   203  }
   204  
   205  func xrdremote(name string) (client *xrootd.Client, path string, err error) {
   206  	url, err := xrdio.Parse(name)
   207  	if err != nil {
   208  		return nil, "", fmt.Errorf("could not parse %q: %w", name, err)
   209  	}
   210  
   211  	path = url.Path
   212  	client, err = xrootd.NewClient(context.Background(), url.Addr, url.User)
   213  	return client, path, err
   214  }
   215  
   216  type job struct {
   217  	fs  xrdfs.FileSystem
   218  	src string
   219  	dst string
   220  }
   221  
   222  func (j job) run(ctx context.Context) (int, error) {
   223  	var (
   224  		o   io.WriteCloser
   225  		err error
   226  	)
   227  	switch j.dst {
   228  	case "-", "":
   229  		o = os.Stdout
   230  	case ".":
   231  		j.dst = stdpath.Base(j.src)
   232  		fallthrough
   233  	default:
   234  		o, err = os.Create(j.dst)
   235  		if err != nil {
   236  			return 0, fmt.Errorf("could not create output file: %w", err)
   237  		}
   238  	}
   239  	defer o.Close()
   240  
   241  	f, err := xrdio.OpenFrom(j.fs, j.src)
   242  	if err != nil {
   243  		return 0, err
   244  	}
   245  	defer f.Close()
   246  
   247  	// TODO(sbinet): make buffer a field of job to reduce memory pressure.
   248  	// TODO(sbinet): use clever heuristics for buffer size?
   249  	n, err := io.CopyBuffer(o, f, make([]byte, 16*1024*1024))
   250  	if err != nil {
   251  		return int(n), fmt.Errorf("could not copy to output file: %w", err)
   252  	}
   253  
   254  	err = o.Close()
   255  	if err != nil {
   256  		return int(n), fmt.Errorf("could not close output file: %w", err)
   257  	}
   258  
   259  	return int(n), nil
   260  }
   261  
   262  type jobs struct {
   263  	slice []job
   264  }
   265  
   266  func (js *jobs) add(j job) {
   267  	js.slice = append(js.slice, j)
   268  }
   269  
   270  func (js *jobs) run(ctx context.Context) (int, error) {
   271  	var n int
   272  	for _, j := range js.slice {
   273  		nn, err := j.run(ctx)
   274  		n += nn
   275  		if err != nil {
   276  			return n, err
   277  		}
   278  	}
   279  	return n, nil
   280  }