github.com/angenalZZZ/gofunc@v0.0.0-20210507121333-48ff1be3917b/f/zip.go (about)

     1  package f
     2  
     3  import (
     4  	"bytes"
     5  	"fmt"
     6  	"github.com/klauspost/compress/flate"
     7  	"github.com/klauspost/compress/gzip"
     8  	"github.com/klauspost/compress/zip"
     9  	"github.com/klauspost/pgzip"
    10  	"io"
    11  	"os"
    12  	"path"
    13  	"path/filepath"
    14  	"strings"
    15  )
    16  
    17  // GzipCompress reads in, compresses it, and writes it to out.
    18  func GzipCompress(in io.Reader, out io.Writer, singleThreaded bool, compressionLevels ...int) error {
    19  	compressionLevel := gzip.DefaultCompression
    20  	if len(compressionLevels) == 1 {
    21  		compressionLevel = compressionLevels[0]
    22  	}
    23  	var w io.WriteCloser
    24  	var err error
    25  	if singleThreaded {
    26  		w, err = gzip.NewWriterLevel(out, compressionLevel)
    27  	} else {
    28  		w, err = pgzip.NewWriterLevel(out, compressionLevel)
    29  	}
    30  	if err != nil {
    31  		return err
    32  	}
    33  	_, err = io.Copy(w, in)
    34  	_ = w.Close()
    35  	return err
    36  }
    37  
    38  // GzipDecompress reads in, decompresses it, and writes it to out.
    39  func GzipDecompress(in io.Reader, out io.Writer, singleThreaded bool) error {
    40  	var r io.ReadCloser
    41  	var err error
    42  	if singleThreaded {
    43  		r, err = gzip.NewReader(in)
    44  	} else {
    45  		r, err = pgzip.NewReader(in)
    46  	}
    47  	if err != nil {
    48  		return err
    49  	}
    50  	_, err = io.Copy(out, r)
    51  	_ = r.Close()
    52  	return err
    53  }
    54  
    55  // IsZipFile check is zip file.
    56  func IsZipFile(file string) bool {
    57  	f, err := os.Open(file)
    58  	if err != nil {
    59  		return false
    60  	}
    61  	defer f.Close()
    62  
    63  	buf := make([]byte, 4)
    64  	if n, err := f.Read(buf); err != nil || n < 4 {
    65  		return false
    66  	}
    67  
    68  	return bytes.Equal(buf, []byte("PK\x03\x04"))
    69  }
    70  
    71  // ZipCompress creates a .zip file at destination containing
    72  // the files listed in sources. The destination must end
    73  // with ".zip". zipFileInfo paths can be those of regular files
    74  // or directories. Regular files are stored at the 'root'
    75  // of the archive, and directories are recursively added.
    76  func ZipCompress(sources []string, destination string, overwriteExisting, implicitTopLevelFolder bool, compressionLevels ...int) error {
    77  	if !strings.HasSuffix(destination, ".zip") {
    78  		return fmt.Errorf("filename must have a .zip extension")
    79  	}
    80  	if !overwriteExisting && FileExists(destination) {
    81  		return fmt.Errorf("file already exists: %s", destination)
    82  	}
    83  	compressionLevel := flate.DefaultCompression
    84  	if len(compressionLevels) == 1 {
    85  		compressionLevel = compressionLevels[0]
    86  	}
    87  	// make the folder to contain the resulting archive
    88  	// if it does not already exist
    89  	destDir := filepath.Dir(destination)
    90  	if !FileExists(destDir) {
    91  		err := MkdirAll(destDir)
    92  		if err != nil {
    93  			return fmt.Errorf("making folder for destination: %v", err)
    94  		}
    95  	}
    96  
    97  	out, err := os.Create(destination)
    98  	if err != nil {
    99  		return fmt.Errorf("creating %s: %v", destination, err)
   100  	}
   101  	defer out.Close()
   102  
   103  	zw := zip.NewWriter(out)
   104  	if compressionLevel != flate.DefaultCompression {
   105  		zw.RegisterCompressor(zip.Deflate, func(out io.Writer) (io.WriteCloser, error) {
   106  			return flate.NewWriter(out, compressionLevel)
   107  		})
   108  	}
   109  	defer zw.Close()
   110  
   111  	var topLevelFolder string
   112  	if implicitTopLevelFolder && FileExistMultipleTopLevels(sources) {
   113  		topLevelFolder = FolderNameFromFileName(destination)
   114  	}
   115  
   116  	for _, source := range sources {
   117  		err := zipWriteWalk(source, topLevelFolder, destination, zw)
   118  		if err != nil {
   119  			return fmt.Errorf("walking %s: %v", source, err)
   120  		}
   121  	}
   122  
   123  	return nil
   124  }
   125  
   126  func zipWriteWalk(source, topLevelFolder, destination string, zw *zip.Writer) error {
   127  	sourceInfo, err := os.Stat(source)
   128  	if err != nil {
   129  		return fmt.Errorf("%s: stat: %v", source, err)
   130  	}
   131  	destAbs, err := filepath.Abs(destination)
   132  	if err != nil {
   133  		return fmt.Errorf("%s: getting absolute path of destination %s: %v", source, destination, err)
   134  	}
   135  
   136  	return filepath.Walk(source, func(fpath string, info os.FileInfo, err error) error {
   137  		if err != nil {
   138  			return fmt.Errorf("traversing %s: %v", fpath, err)
   139  		}
   140  		if info == nil {
   141  			return fmt.Errorf("%s: no file info", fpath)
   142  		}
   143  
   144  		// make sure we do not copy the output file into the output
   145  		// file; that results in an infinite loop and disk exhaustion!
   146  		fpathAbs, err := filepath.Abs(fpath)
   147  		if err != nil {
   148  			return fmt.Errorf("%s: getting absolute path: %v", fpath, err)
   149  		}
   150  		if FileWithin(fpathAbs, destAbs) {
   151  			return nil
   152  		}
   153  
   154  		// build the name to be used within the archive
   155  		nameInArchive, err := MakeNameInArchive(sourceInfo, source, topLevelFolder, fpath)
   156  		if err != nil {
   157  			return err
   158  		}
   159  
   160  		var file io.ReadCloser
   161  		if info.Mode().IsRegular() {
   162  			file, err = os.Open(fpath)
   163  			if err != nil {
   164  				return fmt.Errorf("%s: opening: %v", fpath, err)
   165  			}
   166  			defer file.Close()
   167  		}
   168  		err = zipWrite(zipFileInfo{
   169  			FileInfo: zipFileCustomInfo{
   170  				FileInfo:   info,
   171  				CustomName: nameInArchive,
   172  			},
   173  			ReadCloser: file,
   174  		}, zw)
   175  		if err != nil {
   176  			return fmt.Errorf("%s: writing: %s", fpath, err)
   177  		}
   178  
   179  		return nil
   180  	})
   181  }
   182  
   183  // Write writes f to z, which must have been opened for writing first.
   184  func zipWrite(f zipFileInfo, zw *zip.Writer) error {
   185  	if f.FileInfo.Name() == "" {
   186  		return fmt.Errorf("missing file name")
   187  	}
   188  
   189  	header, err := zip.FileInfoHeader(f)
   190  	if err != nil {
   191  		return fmt.Errorf("%s: getting header: %v", f.Name(), err)
   192  	}
   193  
   194  	if f.IsDir() {
   195  		header.Name += "/" // required - strangely no mention of this in zip spec? but is in godoc...
   196  		header.Method = zip.Store
   197  	} else {
   198  		ext := strings.ToLower(path.Ext(header.Name))
   199  		if _, ok := CompressedFormats[ext]; ok {
   200  			header.Method = zip.Store
   201  		} else {
   202  			header.Method = zip.Deflate
   203  		}
   204  	}
   205  
   206  	writer, err := zw.CreateHeader(header)
   207  	if err != nil {
   208  		return fmt.Errorf("%s: making header: %v", f.Name(), err)
   209  	}
   210  
   211  	return zipWriteFile(f, writer)
   212  }
   213  
   214  func zipWriteFile(f zipFileInfo, writer io.Writer) error {
   215  	if f.IsDir() {
   216  		return nil // directories have no contents
   217  	}
   218  	if FileIsSymlink(f) {
   219  		// file body for symlinks is the symlink target
   220  		linkTarget, err := os.Readlink(f.Name())
   221  		if err != nil {
   222  			return fmt.Errorf("%s: readlink: %v", f.Name(), err)
   223  		}
   224  		_, err = writer.Write([]byte(filepath.ToSlash(linkTarget)))
   225  		if err != nil {
   226  			return fmt.Errorf("%s: writing symlink target: %v", f.Name(), err)
   227  		}
   228  		return nil
   229  	}
   230  
   231  	if f.ReadCloser == nil {
   232  		return fmt.Errorf("%s: no way to read file contents", f.Name())
   233  	}
   234  	_, err := io.Copy(writer, f)
   235  	if err != nil {
   236  		return fmt.Errorf("%s: copying contents: %v", f.Name(), err)
   237  	}
   238  
   239  	return nil
   240  }
   241  
   242  // zipFileInfo provides methods for accessing information about
   243  // or contents of a file within an archive.
   244  type zipFileInfo struct {
   245  	os.FileInfo
   246  
   247  	// The original header info; depends on
   248  	// type of archive -- could be nil, too.
   249  	Header interface{}
   250  
   251  	// Allow the file contents to be read (and closed)
   252  	io.ReadCloser
   253  }
   254  
   255  // zipFileCustomInfo is an os.zipFileCustomInfo but optionally with
   256  // a custom name, useful if dealing with files that
   257  // are not actual files on disk, or which have a
   258  // different name in an archive than on disk.
   259  type zipFileCustomInfo struct {
   260  	os.FileInfo
   261  	CustomName string
   262  }
   263  
   264  // Name returns fi.CustomName if not empty;
   265  // otherwise it returns fi.zipFileCustomInfo.Name().
   266  func (fi zipFileCustomInfo) Name() string {
   267  	if fi.CustomName != "" {
   268  		return fi.CustomName
   269  	}
   270  	return fi.FileInfo.Name()
   271  }
   272  
   273  // ZipOpenReader open a zip reader.
   274  var ZipOpenReader = zip.OpenReader
   275  
   276  // ZipNewReader gets a zip reader.
   277  func ZipNewReader(zipFile string) (*zip.Reader, error) {
   278  	file, err := os.Open(zipFile)
   279  	if err != nil {
   280  		return nil, err
   281  	}
   282  	fileInfo, err := file.Stat()
   283  	if err != nil {
   284  		return nil, err
   285  	}
   286  	reader, err := zip.NewReader(file, fileInfo.Size())
   287  	if err != nil {
   288  		return nil, err
   289  	}
   290  	return reader, nil
   291  }
   292  
   293  // ZipFind returns the cleaned path of every file in the supplied zip reader whose
   294  // base name matches the supplied pattern, which is interpreted as in path.Match.
   295  func ZipFind(reader *zip.Reader, patterns ...string) ([]string, error) {
   296  	// path.Match will only return an error if the pattern is not
   297  	// valid (*and* the supplied name is not empty, hence "check").
   298  	pattern := "*"
   299  	if len(patterns) == 1 {
   300  		pattern = patterns[0]
   301  	}
   302  	if _, err := path.Match(pattern, "check"); err != nil {
   303  		return nil, err
   304  	}
   305  	var matches []string
   306  	for _, zipFile := range reader.File {
   307  		cleanPath := path.Clean(zipFile.Name)
   308  		baseName := path.Base(cleanPath)
   309  		if match, _ := path.Match(pattern, baseName); match {
   310  			matches = append(matches, cleanPath)
   311  		}
   312  	}
   313  	return matches, nil
   314  }
   315  
   316  // ZipDecompress extracts files from the supplied zip reader, from the (internal, slash-
   317  // separated) source path into the (external, OS-specific) target path. If the
   318  // source path does not reference a directory, the referenced file will be written
   319  // directly to the target path.
   320  func ZipDecompress(reader *zip.Reader, targetRoot string, sourceRoot ...string) error {
   321  	source := ""
   322  	if len(sourceRoot) == 1 {
   323  		source = sourceRoot[0]
   324  	}
   325  	source = path.Clean(source)
   326  	if source == "." {
   327  		source = ""
   328  	}
   329  	if !IsSanePath(source) {
   330  		return fmt.Errorf("cannot extract files rooted at %q", source)
   331  	}
   332  	extractor := zipExtractor{targetRoot, source}
   333  	for _, zipFile := range reader.File {
   334  		if err := extractor.extract(zipFile); err != nil {
   335  			cleanName := path.Clean(zipFile.Name)
   336  			return fmt.Errorf("cannot extract %q: %v", cleanName, err)
   337  		}
   338  	}
   339  	return nil
   340  }
   341  
   342  // zipExtractor extracts files from the supplied zip path.
   343  type zipExtractor struct {
   344  	targetRoot string
   345  	sourceRoot string
   346  }
   347  
   348  // targetPath returns the target path for a given zip file and whether
   349  // it should be extracted.
   350  func (x zipExtractor) targetPath(zipFile *zip.File) (string, bool) {
   351  	cleanPath := path.Clean(zipFile.Name)
   352  	if cleanPath == x.sourceRoot {
   353  		return x.targetRoot, true
   354  	}
   355  	if x.sourceRoot != "" {
   356  		mustPrefix := x.sourceRoot + "/"
   357  		if !strings.HasPrefix(cleanPath, mustPrefix) {
   358  			return "", false
   359  		}
   360  		cleanPath = cleanPath[len(mustPrefix):]
   361  	}
   362  	return filepath.Join(x.targetRoot, filepath.FromSlash(cleanPath)), true
   363  }
   364  
   365  func (x zipExtractor) extract(zipFile *zip.File) error {
   366  	targetPath, ok := x.targetPath(zipFile)
   367  	if !ok {
   368  		return nil
   369  	}
   370  	parentPath := filepath.Dir(targetPath)
   371  	if err := os.MkdirAll(parentPath, 0777); err != nil {
   372  		return err
   373  	}
   374  	mode := zipFile.Mode()
   375  	modePerm := mode & os.ModePerm
   376  	modeType := mode & os.ModeType
   377  	switch modeType {
   378  	case os.ModeDir:
   379  		return x.writeDir(targetPath, modePerm)
   380  	case os.ModeSymlink:
   381  		return x.writeSymlink(targetPath, zipFile)
   382  	case 0:
   383  		return x.writeFile(targetPath, zipFile, modePerm)
   384  	}
   385  	return fmt.Errorf("unknown file type %d", modeType)
   386  }
   387  
   388  func (x zipExtractor) writeDir(targetPath string, modePerm os.FileMode) error {
   389  	fileInfo, err := os.Lstat(targetPath)
   390  	switch {
   391  	case err == nil:
   392  		mode := fileInfo.Mode()
   393  		if mode.IsDir() {
   394  			if mode&os.ModePerm != modePerm {
   395  				return os.Chmod(targetPath, modePerm)
   396  			}
   397  			return nil
   398  		}
   399  		fallthrough
   400  	case !os.IsNotExist(err):
   401  		if err := os.RemoveAll(targetPath); err != nil {
   402  			return err
   403  		}
   404  	}
   405  	return os.MkdirAll(targetPath, modePerm)
   406  }
   407  
   408  func (x zipExtractor) writeFile(targetPath string, zipFile *zip.File, modePerm os.FileMode) error {
   409  	if _, err := os.Lstat(targetPath); !os.IsNotExist(err) {
   410  		if err := os.RemoveAll(targetPath); err != nil {
   411  			return err
   412  		}
   413  	}
   414  	writer, err := os.OpenFile(targetPath, os.O_CREATE|os.O_EXCL|os.O_WRONLY, modePerm)
   415  	if err != nil {
   416  		return err
   417  	}
   418  	defer writer.Close()
   419  
   420  	if err := zipCopyTo(writer, zipFile); err != nil {
   421  		return err
   422  	}
   423  
   424  	if err := writer.Sync(); err != nil {
   425  		return err
   426  	}
   427  
   428  	if err := writer.Close(); err != nil {
   429  		return err
   430  	}
   431  	return nil
   432  }
   433  
   434  func (x zipExtractor) writeSymlink(targetPath string, zipFile *zip.File) error {
   435  	symlinkTarget, err := x.checkSymlink(targetPath, zipFile)
   436  	if err != nil {
   437  		return err
   438  	}
   439  	if _, err := os.Lstat(targetPath); !os.IsNotExist(err) {
   440  		if err := os.RemoveAll(targetPath); err != nil {
   441  			return err
   442  		}
   443  	}
   444  	return os.Symlink(symlinkTarget, targetPath)
   445  }
   446  
   447  func (x zipExtractor) checkSymlink(targetPath string, zipFile *zip.File) (string, error) {
   448  	var buffer bytes.Buffer
   449  	if err := zipCopyTo(&buffer, zipFile); err != nil {
   450  		return "", err
   451  	}
   452  	symlinkTarget := buffer.String()
   453  	if filepath.IsAbs(symlinkTarget) {
   454  		return "", fmt.Errorf("symlink %q is absolute", symlinkTarget)
   455  	}
   456  	finalPath := filepath.Join(filepath.Dir(targetPath), symlinkTarget)
   457  	relativePath, err := filepath.Rel(x.targetRoot, finalPath)
   458  	if err != nil {
   459  		// Not tested, because I don't know how to trigger this condition.
   460  		return "", fmt.Errorf("symlink %q not comprehensible", symlinkTarget)
   461  	}
   462  	if !IsSanePath(relativePath) {
   463  		return "", fmt.Errorf("symlink %q leads out of scope", symlinkTarget)
   464  	}
   465  	return symlinkTarget, nil
   466  }
   467  
   468  func zipCopyTo(writer io.Writer, zipFile *zip.File) error {
   469  	reader, err := zipFile.Open()
   470  	if err != nil {
   471  		return err
   472  	}
   473  	_, err = io.Copy(writer, reader)
   474  	_ = reader.Close()
   475  	return err
   476  }