github.com/paketo-buildpacks/libpak/v2@v2.0.0-alpha.3.0.20231023030503-8365f81de65a/crush/crush.go (about)

     1  /*
     2   * Copyright 2018-2020 the original author or authors.
     3   *
     4   * Licensed under the Apache License, Version 2.0 (the "License");
     5   * you may not use this file except in compliance with the License.
     6   * You may obtain a copy of the License at
     7   *
     8   *      https://www.apache.org/licenses/LICENSE-2.0
     9   *
    10   * Unless required by applicable law or agreed to in writing, software
    11   * distributed under the License is distributed on an "AS IS" BASIS,
    12   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    13   * See the License for the specific language governing permissions and
    14   * limitations under the License.
    15   */
    16  
    17  package crush
    18  
    19  import (
    20  	"archive/tar"
    21  	"archive/zip"
    22  	"bytes"
    23  	"compress/bzip2"
    24  	"compress/gzip"
    25  	"fmt"
    26  	"io"
    27  	"os"
    28  	"path/filepath"
    29  	"strings"
    30  
    31  	"github.com/h2non/filetype"
    32  	"github.com/xi2/xz"
    33  )
    34  
    35  // CreateTar writes a TAR to the destination io.Writer containing the directories and files in the source folder.
    36  func CreateTar(destination io.Writer, source string) error {
    37  	t := tar.NewWriter(destination)
    38  	defer t.Close()
    39  
    40  	if err := filepath.Walk(source, func(path string, info os.FileInfo, err error) error {
    41  		if err != nil {
    42  			return err
    43  		}
    44  
    45  		rel, err := filepath.Rel(source, path)
    46  		if err != nil {
    47  			return fmt.Errorf("unable to calculate relative path %s -> %s\n%w", source, path, err)
    48  		}
    49  		if info.IsDir() {
    50  			rel = fmt.Sprintf("%s/", rel)
    51  		}
    52  
    53  		if rel == "./" {
    54  			return nil
    55  		}
    56  
    57  		name := info.Name()
    58  		if info.Mode()&os.ModeSymlink == os.ModeSymlink {
    59  			name, err = os.Readlink(path)
    60  			if err != nil {
    61  				return fmt.Errorf("unable to read link from %s\n%w", info.Name(), err)
    62  			}
    63  		}
    64  
    65  		h, err := tar.FileInfoHeader(info, name)
    66  		if err != nil {
    67  			return fmt.Errorf("unable to create TAR header from %+v\n%w", info, err)
    68  		}
    69  		h.Name = rel
    70  
    71  		if err := t.WriteHeader(h); err != nil {
    72  			return fmt.Errorf("unable to write header %+v\n%w", h, err)
    73  		}
    74  
    75  		if !info.Mode().IsRegular() {
    76  			return nil
    77  		}
    78  
    79  		in, err := os.Open(path)
    80  		if err != nil {
    81  			return fmt.Errorf("unable to open %s\n%w", path, err)
    82  		}
    83  		defer in.Close()
    84  
    85  		if _, err := io.Copy(t, in); err != nil {
    86  			return fmt.Errorf("unable to copy %s to %s\n%w", path, h.Name, err)
    87  		}
    88  
    89  		return nil
    90  	}); err != nil {
    91  		return fmt.Errorf("unable to create tar from %s\n%w", source, err)
    92  	}
    93  
    94  	return nil
    95  }
    96  
    97  // CreateTarGz writes a GZIP'd TAR to the destination io.Writer containing the directories and files in the source
    98  // folder.
    99  func CreateTarGz(destination io.Writer, source string) error {
   100  	gz := gzip.NewWriter(destination)
   101  	defer gz.Close()
   102  
   103  	return CreateTar(gz, source)
   104  }
   105  
   106  // Extract decompresses and extract source files to a destination directory or path. For archives, an arbitrary number of top-level directory
   107  // components can be stripped from each path.
   108  func Extract(source io.Reader, destination string, stripComponents int) error {
   109  	buf := &bytes.Buffer{}
   110  
   111  	kind, err := filetype.MatchReader(io.TeeReader(source, buf))
   112  	if err != nil {
   113  		return err
   114  	}
   115  
   116  	source = io.MultiReader(buf, source)
   117  
   118  	switch kind.MIME.Value {
   119  	case "application/x-tar":
   120  		return extractTar(source, destination, stripComponents)
   121  	case "application/zip":
   122  		return extractZip(source, destination, stripComponents)
   123  	case "application/x-bzip2":
   124  		return Extract(bzip2.NewReader(source), destination, stripComponents)
   125  	case "application/gzip":
   126  		gz, err := gzip.NewReader(source)
   127  		if err != nil {
   128  			return fmt.Errorf("unable to create GZIP reader\n%w", err)
   129  		}
   130  		defer gz.Close()
   131  		return Extract(gz, destination, stripComponents)
   132  	case "application/x-xz":
   133  		xz, err := xz.NewReader(source, 0)
   134  		if err != nil {
   135  			return fmt.Errorf("unable to create XZ reader\n%w", err)
   136  		}
   137  		return Extract(xz, destination, stripComponents)
   138  	default:
   139  		// no archive, can happen with xz/gzip/bz2 if compressed file is not an archive
   140  		in, err := os.Create(destination)
   141  		if err != nil {
   142  			return fmt.Errorf("unable to open %s\n%w", destination, err)
   143  		}
   144  		defer in.Close()
   145  
   146  		if _, err := io.Copy(in, source); err != nil {
   147  			return fmt.Errorf("unable to copy to %s\n%w", destination, err)
   148  		}
   149  	}
   150  
   151  	return nil
   152  }
   153  
   154  // ExtractTar extracts source TAR file to a destination directory.  An arbitrary number of top-level directory
   155  // components can be stripped from each path.
   156  //
   157  // Deprecated: use Extract instead
   158  func ExtractTar(source io.Reader, destination string, stripComponents int) error {
   159  	return extractTar(source, destination, stripComponents)
   160  }
   161  
   162  func extractTar(source io.Reader, destination string, stripComponents int) error {
   163  	t := tar.NewReader(source)
   164  
   165  	for {
   166  		f, err := t.Next()
   167  		if err != nil && err == io.EOF {
   168  			break
   169  		} else if err != nil {
   170  			return fmt.Errorf("unable to read TAR file\n%w", err)
   171  		}
   172  
   173  		target := strippedPath(f.Name, destination, stripComponents)
   174  		if target == "" {
   175  			continue
   176  		}
   177  
   178  		info := f.FileInfo()
   179  		if info.IsDir() {
   180  			if err := os.MkdirAll(target, 0755); err != nil {
   181  				return fmt.Errorf("unable to make directory %s\n%w", target, err)
   182  			}
   183  		} else if info.Mode()&os.ModeSymlink != 0 {
   184  			if err := writeSymlink(f.Linkname, target); err != nil {
   185  				return err
   186  			}
   187  		} else {
   188  			if err := writeFile(t, target, info.Mode()); err != nil {
   189  				return err
   190  			}
   191  		}
   192  	}
   193  
   194  	return nil
   195  }
   196  
   197  // ExtractTarBz2 extracts source BZIP2'd TAR file to a destination directory.  An arbitrary number of top-level
   198  // directory components can be stripped from each path.
   199  //
   200  // Deprecated: use Extract instead
   201  func ExtractTarBz2(source io.Reader, destination string, stripComponents int) error {
   202  	return ExtractTar(bzip2.NewReader(source), destination, stripComponents)
   203  }
   204  
   205  // ExtractTarGz extracts source GZIP'd TAR file to a destination directory.  An arbitrary number of top-level directory
   206  // components can be stripped from each path.
   207  //
   208  // Deprecated: use Extract instead
   209  func ExtractTarGz(source io.Reader, destination string, stripComponents int) error {
   210  	gz, err := gzip.NewReader(source)
   211  	if err != nil {
   212  		return fmt.Errorf("unable to create GZIP reader\n%w", err)
   213  	}
   214  	defer gz.Close()
   215  
   216  	return ExtractTar(gz, destination, stripComponents)
   217  }
   218  
   219  // ExtractTarXz extracts source XZ'd TAR file to a destination directory.  An arbitrary number of top-level directory
   220  // components can be stripped from each path.
   221  //
   222  // Deprecated: use Extract instead
   223  func ExtractTarXz(source io.Reader, destination string, stripComponents int) error {
   224  	xz, err := xz.NewReader(source, 0)
   225  	if err != nil {
   226  		return fmt.Errorf("unable to create XZ reader\n%w", err)
   227  	}
   228  
   229  	return ExtractTar(xz, destination, stripComponents)
   230  }
   231  
   232  // ExtractZip extracts source ZIP file to a destination directory.  An arbitrary number of top-level directory
   233  // components can be stripped from each path.
   234  //
   235  // Deprecated: use Extract instead
   236  func ExtractZip(source io.Reader, destination string, stripComponents int) error {
   237  	return extractZip(source, destination, stripComponents)
   238  }
   239  
   240  func extractZip(source io.Reader, destination string, stripComponents int) error {
   241  	buffer, err := os.CreateTemp("", "")
   242  	if err != nil {
   243  		return err
   244  	}
   245  	defer os.Remove(buffer.Name())
   246  
   247  	size, err := io.Copy(buffer, source)
   248  	if err != nil {
   249  		return err
   250  	}
   251  
   252  	z, err := zip.NewReader(buffer, size)
   253  	if err != nil {
   254  		return err
   255  	}
   256  
   257  	for _, f := range z.File {
   258  		target := strippedPath(f.Name, destination, stripComponents)
   259  		if target == "" {
   260  			continue
   261  		}
   262  
   263  		if f.FileInfo().IsDir() {
   264  			if err := os.MkdirAll(target, 0755); err != nil {
   265  				return err
   266  			}
   267  		} else {
   268  			if err := writeZipEntry(f, target); err != nil {
   269  				return err
   270  			}
   271  		}
   272  	}
   273  
   274  	return nil
   275  }
   276  
   277  func strippedPath(source string, destination string, stripComponents int) string {
   278  	components := strings.Split(source, string(filepath.Separator))
   279  
   280  	if len(components) <= stripComponents {
   281  		return ""
   282  	}
   283  
   284  	return filepath.Join(append([]string{destination}, components[stripComponents:]...)...)
   285  }
   286  
   287  func writeFile(source io.Reader, path string, perm os.FileMode) error {
   288  	file := filepath.Dir(path)
   289  	if err := os.MkdirAll(file, 0755); err != nil {
   290  		return fmt.Errorf("unable to create directory %s\n%w", file, err)
   291  	}
   292  
   293  	out, err := os.OpenFile(path, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, perm)
   294  	if err != nil {
   295  		return fmt.Errorf("unable to open file %s\n%w", path, err)
   296  	}
   297  	defer out.Close()
   298  
   299  	if _, err := io.Copy(out, source); err != nil {
   300  		return fmt.Errorf("unable to write data to %s\n%w", path, err)
   301  	}
   302  
   303  	return nil
   304  }
   305  
   306  func writeZipEntry(file *zip.File, path string) error {
   307  	in, err := file.Open()
   308  	if err != nil {
   309  		return fmt.Errorf("unable to open %s\n%w", file.Name, err)
   310  	}
   311  	defer in.Close()
   312  
   313  	return writeFile(in, path, file.Mode())
   314  }
   315  
   316  func writeSymlink(oldName string, newName string) error {
   317  	file := filepath.Dir(newName)
   318  	if err := os.MkdirAll(file, 0755); err != nil {
   319  		return fmt.Errorf("unable to create directory %s\n%w", file, err)
   320  	}
   321  
   322  	if err := os.Symlink(oldName, newName); err != nil {
   323  		return fmt.Errorf("unable to create '%s' as symlink to '%s': %v", newName, oldName, err)
   324  	}
   325  
   326  	return nil
   327  }