github.com/pelicanplatform/pelican@v1.0.5/client/pack_handler.go (about) 1 /*************************************************************** 2 * 3 * Copyright (C) 2023, Morgridge Institute for Research 4 * 5 * Licensed under the Apache License, Version 2.0 (the "License"); you 6 * may not use this file except in compliance with the License. You may 7 * obtain a copy of the License at 8 * 9 * http://www.apache.org/licenses/LICENSE-2.0 10 * 11 * Unless required by applicable law or agreed to in writing, software 12 * distributed under the License is distributed on an "AS IS" BASIS, 13 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 14 * See the License for the specific language governing permissions and 15 * limitations under the License. 16 * 17 ***************************************************************/ 18 19 package client 20 21 import ( 22 "archive/tar" 23 "bytes" 24 "compress/gzip" 25 "io" 26 "io/fs" 27 "os" 28 "path/filepath" 29 "runtime" 30 "strings" 31 "sync/atomic" 32 33 "github.com/pkg/errors" 34 log "github.com/sirupsen/logrus" 35 ) 36 37 type packerBehavior int 38 39 type packedError struct{ Value error } 40 41 type atomicError struct { 42 err atomic.Value 43 } 44 45 type autoUnpacker struct { 46 atomicError 47 Behavior packerBehavior 48 detectedType packerBehavior 49 destDir string 50 buffer bytes.Buffer 51 writer io.WriteCloser 52 } 53 54 type autoPacker struct { 55 atomicError 56 Behavior packerBehavior 57 srcDir string 58 reader io.ReadCloser 59 srcDirSize atomic.Int64 60 srcDirDone atomic.Int64 61 } 62 63 const ( 64 autoBehavior packerBehavior = iota 65 tarBehavior 66 tarGZBehavior 67 tarXZBehavior 68 zipBehavior 69 70 defaultBehavior packerBehavior = tarGZBehavior 71 ) 72 73 func newAutoUnpacker(destdir string, behavior packerBehavior) *autoUnpacker { 74 aup := &autoUnpacker{ 75 Behavior: behavior, 76 destDir: destdir, 77 } 78 aup.err.Store(packedError{}) 79 if os := runtime.GOOS; os == "windows" { 80 aup.StoreError(errors.New("Auto-unpacking functionality not supported on Windows")) 81 } 82 return aup 83 } 84 85 func newAutoPacker(srcdir string, behavior packerBehavior) *autoPacker { 86 ap := &autoPacker{ 87 Behavior: behavior, 88 srcDir: srcdir, 89 } 90 ap.err.Store(packedError{}) 91 if os := runtime.GOOS; os == "windows" { 92 ap.StoreError(errors.New("Auto-unpacking functionality not supported on Windows")) 93 } else { 94 go ap.calcDirectorySize() 95 } 96 return ap 97 } 98 99 func GetBehavior(behaviorName string) (packerBehavior, error) { 100 switch behaviorName { 101 case "auto": 102 return autoBehavior, nil 103 case "tar": 104 return tarBehavior, nil 105 case "tar.gz": 106 return tarGZBehavior, nil 107 case "tar.xz": 108 return tarXZBehavior, nil 109 case "zip": 110 return zipBehavior, nil 111 } 112 return autoBehavior, errors.Errorf("Unknown value for 'pack' parameter: %v", behaviorName) 113 } 114 115 func (aup *atomicError) Error() error { 116 value := aup.err.Load() 117 if err, ok := value.(packedError); ok { 118 return err.Value 119 } 120 return nil 121 } 122 123 func (aup *atomicError) StoreError(err error) { 124 aup.err.CompareAndSwap(packedError{}, packedError{Value: err}) 125 } 126 127 func (aup *autoUnpacker) detect() (packerBehavior, error) { 128 currentBytes := aup.buffer.Bytes() 129 // gzip streams start with 1F 8B 130 if len(currentBytes) >= 2 && bytes.Equal(currentBytes[0:2], []byte{0x1F, 0x8B}) { 131 return tarGZBehavior, nil 132 } 133 // xz streams start with FD 37 7A 58 5A 00 134 if len(currentBytes) >= 6 && bytes.Equal(currentBytes[0:6], []byte{0xFD, 0x37, 0x7A, 0x58, 0x5A, 0x00}) { 135 return tarXZBehavior, nil 136 } 137 // tar files, at offset 257, have bytes 75 73 74 61 72 138 if len(currentBytes) >= (257+5) && bytes.Equal(currentBytes[257:257+5], []byte{0x75, 0x73, 0x74, 0x61, 0x72}) { 139 return tarBehavior, nil 140 } 141 // zip files start with 50 4B 03 04 142 if len(currentBytes) >= 4 && bytes.Equal(currentBytes[0:4], []byte{0x50, 0x4B, 0x03, 0x04}) { 143 return zipBehavior, nil 144 } 145 if len(currentBytes) > (257 + 5) { 146 return autoBehavior, errors.New("Unable to detect pack type") 147 } 148 return autoBehavior, nil 149 } 150 151 func writeRegFile(path string, mode int64, reader io.Reader) error { 152 fp, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE, fs.FileMode(mode)) 153 if err != nil { 154 return err 155 } 156 defer fp.Close() 157 _, err = io.Copy(fp, reader) 158 return err 159 } 160 161 type autoPackerHelper struct { 162 curFp io.Reader 163 ap *autoPacker 164 } 165 166 func (aph *autoPackerHelper) Read(p []byte) (n int, err error) { 167 n, err = aph.curFp.Read(p) 168 aph.ap.srcDirDone.Add(int64(n)) 169 return 170 } 171 172 func (ap *autoPacker) readRegFile(path string, writer io.Writer) error { 173 fp, err := os.OpenFile(path, os.O_RDONLY, 0) 174 if err != nil { 175 return err 176 } 177 aph := &autoPackerHelper{fp, ap} 178 defer fp.Close() 179 _, err = io.Copy(writer, aph) 180 return err 181 } 182 183 func (ap *autoPacker) calcDirectorySize() { 184 err := filepath.WalkDir(ap.srcDir, func(path string, dent fs.DirEntry, err error) error { 185 if err != nil { 186 log.Warningln("Error when walking source directory to calculate size:", err.Error()) 187 return filepath.SkipDir 188 } 189 if dent.Type().IsRegular() { 190 fi, err := dent.Info() 191 if err != nil { 192 log.Warningln("Error when stat'ing file:", err.Error()) 193 return nil 194 } 195 ap.srcDirSize.Add(fi.Size()) 196 } 197 return nil 198 }) 199 if err != nil { 200 log.Warningln("Failure when calculating the source directory size:", err.Error()) 201 } 202 } 203 204 func (ap *autoPacker) Size() int64 { 205 return ap.srcDirSize.Load() 206 } 207 208 func (ap *autoPacker) BytesComplete() int64 { 209 return ap.srcDirDone.Load() 210 } 211 212 func (ap *autoPacker) pack(tw *tar.Writer, gz *gzip.Writer, pwriter *io.PipeWriter) { 213 srcPrefix := filepath.Clean(ap.srcDir) + "/" 214 defer pwriter.Close() 215 err := filepath.WalkDir(ap.srcDir, func(path string, dent fs.DirEntry, err error) error { 216 if err != nil { 217 return err 218 } 219 path = filepath.Clean(path) 220 if !strings.HasPrefix(path, srcPrefix) { 221 return nil 222 } 223 tarName := path[len(srcPrefix):] 224 if tarName == "" || tarName[0] == '/' { 225 return errors.New("Invalid path provided by filepath.Walk") 226 } 227 228 fi, err := dent.Info() 229 if err != nil { 230 return err 231 } 232 link := "" 233 if (fi.Mode() & fs.ModeSymlink) == fs.ModeSymlink { 234 link, err = os.Readlink(path) 235 if err != nil { 236 return err 237 } 238 } 239 hdr, err := tar.FileInfoHeader(fi, link) 240 if err != nil { 241 return err 242 } 243 hdr.Name = tarName 244 if err = tw.WriteHeader(hdr); err != nil { 245 return err 246 } 247 if fi.Mode().IsRegular() { 248 if err = ap.readRegFile(path, tw); err != nil { 249 return err 250 } 251 } 252 return nil 253 }) 254 if err != nil { 255 ap.StoreError(err) 256 return 257 } 258 if err = tw.Close(); err != nil { 259 ap.StoreError(err) 260 return 261 } 262 if gz != nil { 263 if err = gz.Close(); err != nil { 264 ap.StoreError(err) 265 return 266 } 267 } 268 pwriter.CloseWithError(io.EOF) 269 } 270 271 func (aup *autoUnpacker) unpack(tr *tar.Reader, preader *io.PipeReader) { 272 log.Debugln("Beginning unpacker of type", aup.Behavior) 273 defer preader.Close() 274 for { 275 hdr, err := tr.Next() 276 if err == io.EOF { 277 preader.CloseWithError(err) 278 break 279 } 280 if err != nil { 281 aup.StoreError(err) 282 break 283 } 284 destPath := filepath.Join(aup.destDir, hdr.Name) 285 destPath = filepath.Clean(destPath) 286 if !strings.HasPrefix(destPath, aup.destDir) { 287 aup.StoreError(errors.New("Tarfile contains object outside the destination directory")) 288 break 289 } 290 switch hdr.Typeflag { 291 case tar.TypeReg: 292 err = writeRegFile(destPath, hdr.Mode, tr) 293 if err != nil { 294 aup.StoreError(errors.Wrapf(err, "Failure when unpacking file to %v", destPath)) 295 return 296 } 297 case tar.TypeLink: 298 targetPath := filepath.Join(aup.destDir, hdr.Linkname) 299 if !strings.HasPrefix(targetPath, aup.destDir) { 300 aup.StoreError(errors.New("Tarfile contains hard link target outside the destination directory")) 301 return 302 } 303 if err = os.Link(targetPath, destPath); err != nil { 304 aup.StoreError(errors.Wrapf(err, "Failure when unpacking hard link to %v", destPath)) 305 return 306 } 307 case tar.TypeSymlink: 308 if err = os.Symlink(hdr.Linkname, destPath); err != nil { 309 aup.StoreError(errors.Wrapf(err, "Failure when creating symlink at %v", destPath)) 310 return 311 } 312 case tar.TypeChar: 313 log.Debugln("Ignoring tar entry of type character device at", destPath) 314 case tar.TypeBlock: 315 log.Debugln("Ignoring tar entry of type block device at", destPath) 316 case tar.TypeDir: 317 if err = os.MkdirAll(destPath, fs.FileMode(hdr.Mode)); err != nil { 318 aup.StoreError(errors.Wrapf(err, "Failure when creating directory at %v", destPath)) 319 return 320 } 321 case tar.TypeFifo: 322 log.Debugln("Ignoring tar entry of type FIFO at", destPath) 323 case 103: // pax_global_header, written by git archive. OK to ignore 324 default: 325 log.Debugln("Ignoring unknown tar entry of type", hdr.Typeflag) 326 } 327 } 328 } 329 330 func (aup *autoUnpacker) configure() (err error) { 331 preader, pwriter := io.Pipe() 332 bufDrained := make(chan error) 333 // gzip.NewReader function will block reading from the pipe. 334 // Asynchronously write the contents of the buffer from a separate goroutine; 335 // Note we don't return from configure() until the buffer is consumed. 336 go func() { 337 _, err := aup.buffer.WriteTo(pwriter) 338 bufDrained <- err 339 }() 340 var tarUnpacker *tar.Reader 341 switch aup.detectedType { 342 case autoBehavior: 343 return errors.New("Configure invoked before file type is known") 344 case tarBehavior: 345 tarUnpacker = tar.NewReader(preader) 346 case tarGZBehavior: 347 gzStreamer, err := gzip.NewReader(preader) 348 if err != nil { 349 return err 350 } 351 tarUnpacker = tar.NewReader(gzStreamer) 352 case tarXZBehavior: 353 return errors.New("tar.xz has not yet been implemented") 354 case zipBehavior: 355 return errors.New("zip file support has not yet been implemented") 356 } 357 go aup.unpack(tarUnpacker, preader) 358 if err = <-bufDrained; err != nil { 359 return errors.Wrap(err, "Failed to copy byte buffer to unpacker") 360 } 361 aup.writer = pwriter 362 return nil 363 } 364 365 func (ap *autoPacker) configure() (err error) { 366 preader, pwriter := io.Pipe() 367 if ap.Behavior == autoBehavior { 368 ap.Behavior = defaultBehavior 369 } 370 var tarPacker *tar.Writer 371 var streamer *gzip.Writer 372 switch ap.Behavior { 373 case tarBehavior: 374 tarPacker = tar.NewWriter(pwriter) 375 case tarGZBehavior: 376 streamer = gzip.NewWriter(pwriter) 377 tarPacker = tar.NewWriter(streamer) 378 case tarXZBehavior: 379 return errors.New("tar.xz has not yet been implemented") 380 case zipBehavior: 381 return errors.New("zip file support has not yet been implemented") 382 } 383 go ap.pack(tarPacker, streamer, pwriter) 384 ap.reader = preader 385 return nil 386 } 387 388 func (ap *autoPacker) Read(p []byte) (n int, err error) { 389 if ap.srcDir == "" { 390 err = errors.New("AutoPacker object must be initialized via NewPacker") 391 return 392 } 393 394 if err = ap.Error(); err != nil { 395 if ap.reader != nil { 396 ap.reader.Close() 397 } 398 return 399 } 400 401 if ap.reader == nil { 402 if err = ap.configure(); err != nil { 403 return 404 } 405 } 406 407 n, readerErr := ap.reader.Read(p) 408 if err = ap.Error(); err != nil { 409 return 410 } 411 return n, readerErr 412 } 413 414 func (aup *autoUnpacker) Write(p []byte) (n int, err error) { 415 if aup.destDir == "" { 416 err = errors.New("AutoUnpacker object must be initialized via NewAutoUnpacker") 417 return 418 } 419 err = aup.Error() 420 if err != nil { 421 if aup.writer != nil { 422 aup.writer.Close() 423 } 424 return 425 } 426 427 if aup.detectedType == autoBehavior { 428 if n, err = aup.buffer.Write(p); err != nil { 429 return 430 } 431 if aup.detectedType, err = aup.detect(); aup.detectedType == autoBehavior { 432 n = len(p) 433 return 434 } else if err = aup.configure(); err != nil { 435 return 436 } 437 // Note the byte buffer already consumed all the bytes, hence return here. 438 return len(p), nil 439 } else if aup.writer == nil { 440 if err = aup.configure(); err != nil { 441 return 442 } 443 } 444 n, writerErr := aup.writer.Write(p) 445 if err = aup.Error(); err != nil { 446 return n, err 447 } else if writerErr != nil { 448 if writerErr == io.EOF { 449 return len(p), nil 450 } 451 } 452 return n, writerErr 453 } 454 455 func (aup autoUnpacker) Close() error { 456 if aup.buffer.Len() > 0 { 457 aup.StoreError(errors.New("AutoUnpacker was closed prior to detecting any file type; no bytes were written")) 458 } 459 if aup.Behavior == autoBehavior { 460 aup.StoreError(errors.New("AutoUnpacker was closed prior to any bytes written")) 461 } 462 return aup.Error() 463 } 464 465 func (ap *autoPacker) Close() error { 466 if ap.reader != nil { 467 return ap.reader.Close() 468 } 469 return nil 470 }