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  }