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 }