bitbucket.org/ai69/amoy@v0.2.3/zip.go (about)

     1  package amoy
     2  
     3  import (
     4  	"archive/zip"
     5  	"errors"
     6  	"fmt"
     7  	"io"
     8  	"io/ioutil"
     9  	"os"
    10  	"path/filepath"
    11  	"strings"
    12  	"time"
    13  )
    14  
    15  // ArchiveContent represents a map between filename and data.
    16  type ArchiveContent map[string][]byte
    17  
    18  //revive:disable:error-naming It's not a real error
    19  var (
    20  	// QuitUnzip indicates the arbitrary error means to quit from unzip.
    21  	QuitUnzip = errors.New("amoy: quit unzip file")
    22  )
    23  
    24  // ZipDir compresses one or many directories into a single zip archive file. Existing destination file will be overwritten.
    25  func ZipDir(destZip string, srcDirs ...string) error {
    26  	fw, err := os.Create(destZip)
    27  	if err != nil {
    28  		return err
    29  	}
    30  	defer fw.Close()
    31  
    32  	zw := zip.NewWriter(fw)
    33  	defer zw.Close()
    34  
    35  	for _, d := range srcDirs {
    36  		if err := addDirToZip(zw, d); err != nil {
    37  			return err
    38  		}
    39  	}
    40  	return nil
    41  }
    42  
    43  // ZipFile compresses one or many files into a single zip archive file.
    44  func ZipFile(destZip string, srcFiles ...string) error {
    45  	fw, err := os.Create(destZip)
    46  	if err != nil {
    47  		return err
    48  	}
    49  	defer fw.Close()
    50  
    51  	zw := zip.NewWriter(fw)
    52  	defer zw.Close()
    53  
    54  	for _, f := range srcFiles {
    55  		if err := addFileToZip(zw, f); err != nil {
    56  			return err
    57  		}
    58  	}
    59  	return nil
    60  }
    61  
    62  // ZipContent compresses data entries into a single zip archive file.
    63  func ZipContent(destZip string, content ArchiveContent) error {
    64  	fw, err := os.Create(destZip)
    65  	if err != nil {
    66  		return err
    67  	}
    68  	defer fw.Close()
    69  
    70  	zw := zip.NewWriter(fw)
    71  	defer zw.Close()
    72  
    73  	for name, data := range content {
    74  		if err := addContentToZip(zw, name, data); err != nil {
    75  			return err
    76  		}
    77  	}
    78  	return nil
    79  }
    80  
    81  func addDirToZip(zw *zip.Writer, dirname string) error {
    82  	baseDir := filepath.Base(dirname)
    83  	err := filepath.Walk(dirname, func(path string, info os.FileInfo, err error) error {
    84  		if err != nil {
    85  			return err
    86  		}
    87  
    88  		header, err := zip.FileInfoHeader(info)
    89  		if err != nil {
    90  			return err
    91  		}
    92  
    93  		// get relative entry name
    94  		switch baseDir {
    95  		case ".":
    96  			header.Name = path
    97  		case "..":
    98  			header.Name = strings.TrimLeft(strings.TrimPrefix(path, dirname), `\/`)
    99  		default:
   100  			header.Name = filepath.Join(baseDir, strings.TrimPrefix(path, dirname))
   101  		}
   102  
   103  		// skip directory like ".", "..", "../.."
   104  		if header.Name == "" || header.Name == "." {
   105  			return nil
   106  		}
   107  
   108  		header.Name = filepath.ToSlash(header.Name)
   109  		if info.IsDir() {
   110  			header.Name += "/"
   111  			// create directory entry
   112  			if _, err := zw.CreateHeader(header); err != nil {
   113  				return err
   114  			}
   115  			return nil
   116  		}
   117  
   118  		header.Method = zip.Deflate
   119  		// open source file
   120  		fr, err := os.Open(path)
   121  		if err != nil {
   122  			return err
   123  		}
   124  		defer fr.Close()
   125  
   126  		// create file entry
   127  		fw, err := zw.CreateHeader(header)
   128  		if err != nil {
   129  			return err
   130  		}
   131  		_, err = io.Copy(fw, fr)
   132  		return err
   133  	})
   134  	return err
   135  }
   136  
   137  func addFileToZip(zw *zip.Writer, filename string) error {
   138  	fr, err := os.Open(filename)
   139  	if err != nil {
   140  		return err
   141  	}
   142  	defer fr.Close()
   143  
   144  	info, err := fr.Stat()
   145  	if err != nil {
   146  		return err
   147  	}
   148  
   149  	header, err := zip.FileInfoHeader(info)
   150  	if err != nil {
   151  		return err
   152  	}
   153  	header.Name = filepath.ToSlash(filename)
   154  	header.Method = zip.Deflate
   155  	fw, err := zw.CreateHeader(header)
   156  	if err != nil {
   157  		return err
   158  	}
   159  
   160  	_, err = io.Copy(fw, fr)
   161  	return err
   162  }
   163  
   164  func addContentToZip(zw *zip.Writer, filename string, data []byte) error {
   165  	header := new(zip.FileHeader)
   166  	header.Name = filepath.ToSlash(filename)
   167  	header.Method = zip.Deflate
   168  	header.Modified = time.Now()
   169  	header.UncompressedSize64 = uint64(len(data))
   170  	if header.UncompressedSize64 > uint64(MaxUint32) {
   171  		header.UncompressedSize = MaxUint32
   172  	} else {
   173  		header.UncompressedSize = uint32(header.UncompressedSize64)
   174  	}
   175  
   176  	fw, err := zw.CreateHeader(header)
   177  	if err != nil {
   178  		return err
   179  	}
   180  	_, err = fw.Write(data)
   181  	return err
   182  }
   183  
   184  // UnzipConflictStrategy defines the strategy to handle the conflict when extracting files from a zip archive.
   185  type UnzipConflictStrategy uint8
   186  
   187  const (
   188  	// UnzipConflictSkip skips the file if it already exists in the destination folder.
   189  	UnzipConflictSkip UnzipConflictStrategy = iota
   190  	// UnzipConflictOverwrite overwrites the file if it already exists in the destination folder.
   191  	UnzipConflictOverwrite
   192  	// UnzipConflictRename renames the new file if it already exists in the destination folder.
   193  	UnzipConflictRename
   194  	// UnzipConflictKeepOld keeps the file with earlier modification time if it already exists in the destination folder.
   195  	UnzipConflictKeepOld
   196  	// UnzipConflictKeepNew keeps the file with later modification time if it already exists in the destination folder.
   197  	UnzipConflictKeepNew
   198  	maxCountUnzipConflict
   199  )
   200  
   201  // UnzipDir decompresses a zip archive, extracts all files and folders within the zip file to an output directory.
   202  func UnzipDir(srcZip, destDir string, opts ...UnzipConflictStrategy) ([]string, error) {
   203  	var filenames []string
   204  
   205  	// Conflict strategy
   206  	strategy := UnzipConflictSkip
   207  	if len(opts) > 0 {
   208  		for _, o := range opts {
   209  			if o < maxCountUnzipConflict {
   210  				strategy = o
   211  				break
   212  			}
   213  		}
   214  	}
   215  
   216  	zr, err := zip.OpenReader(srcZip)
   217  	if err != nil {
   218  		return filenames, err
   219  	}
   220  	defer zr.Close()
   221  
   222  	// explicit the destination folder
   223  	destDir, _ = filepath.Abs(destDir)
   224  
   225  	for _, f := range zr.File {
   226  		// Store filename/path for returning and using later on
   227  		fpath := filepath.Join(destDir, f.Name)
   228  		mt := f.Modified
   229  
   230  		// Check for ZipSlip. More Info: http://bit.ly/2MsjAWE
   231  		if !strings.HasPrefix(fpath, filepath.Clean(destDir)+string(os.PathSeparator)) {
   232  			return filenames, fmt.Errorf("%s: illegal file path", fpath)
   233  		}
   234  
   235  		// Create directory and skip
   236  		if f.FileInfo().IsDir() {
   237  			// Make Folder
   238  			if err := os.MkdirAll(fpath, f.Mode()); err != nil {
   239  				return filenames, err
   240  			}
   241  			continue
   242  		}
   243  
   244  		// Make parent directory if not exist
   245  		filenames = append(filenames, fpath)
   246  		if err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm); err != nil {
   247  			return filenames, err
   248  		}
   249  
   250  		// Check if path already exists
   251  		fi, err := os.Stat(fpath)
   252  		if err == nil {
   253  			// Skip if it is a directory
   254  			if fi.IsDir() {
   255  				continue
   256  			}
   257  			// If file already exists, check the conflict strategy
   258  			switch strategy {
   259  			case UnzipConflictSkip:
   260  				continue
   261  			case UnzipConflictOverwrite:
   262  			case UnzipConflictRename:
   263  				fpath = renameConflictPath(fpath)
   264  			case UnzipConflictKeepOld:
   265  				if fi.ModTime().Before(mt) {
   266  					continue
   267  				}
   268  			case UnzipConflictKeepNew:
   269  				if fi.ModTime().After(mt) {
   270  					continue
   271  				}
   272  			}
   273  		} else if !os.IsNotExist(err) {
   274  			// If file does not exist, continue, returns error if it is other error
   275  			return filenames, err
   276  		}
   277  
   278  		// Create new file for writing
   279  		fw, err := os.OpenFile(fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
   280  		if err != nil {
   281  			return filenames, err
   282  		}
   283  
   284  		// Open zip entry for reading
   285  		fr, err := f.Open()
   286  		if err != nil {
   287  			return filenames, err
   288  		}
   289  
   290  		// Write files
   291  		_, err = io.Copy(fw, fr)
   292  
   293  		// Close the file without defer to close before next iteration of loop
   294  		_ = fw.Close()
   295  		_ = fr.Close()
   296  
   297  		// Set modification time, timezone offset may be lost for zip file created on Windows
   298  		_ = os.Chtimes(fpath, mt, mt)
   299  
   300  		if err != nil {
   301  			return filenames, err
   302  		}
   303  	}
   304  
   305  	return filenames, nil
   306  }
   307  
   308  // renameConflictPath renames the path if it already exists.
   309  func renameConflictPath(path string) string {
   310  	// check if the path exists
   311  	_, err := os.Stat(path)
   312  	if err != nil && os.IsNotExist(err) {
   313  		// if it does not exist, return the original path
   314  		return path
   315  	}
   316  
   317  	// for other cases, add a suffix to the path and check again
   318  	ext := filepath.Ext(path)
   319  	name := strings.TrimSuffix(path, ext)
   320  	for i := 1; ; i++ {
   321  		newPath := fmt.Sprintf("%s-%d%s", name, i, ext)
   322  		_, err := os.Stat(newPath)
   323  		if err == nil {
   324  			// skip if it exists, no matter it is a file or a directory
   325  			continue
   326  		}
   327  		if os.IsNotExist(err) {
   328  			// great! the path does not exist
   329  			return newPath
   330  		}
   331  	}
   332  }
   333  
   334  // UnzipFile decompresses a zip archive, extracts all files and call handlers.
   335  func UnzipFile(srcZip string, handle func(file *zip.File) error) error {
   336  	zr, err := zip.OpenReader(srcZip)
   337  	if err != nil {
   338  		return err
   339  	}
   340  	defer zr.Close()
   341  
   342  	for _, f := range zr.File {
   343  		// skip directory
   344  		if f.FileInfo().IsDir() {
   345  			continue
   346  		}
   347  
   348  		// call handler
   349  		if err = handle(f); err != nil {
   350  			break
   351  		}
   352  	}
   353  
   354  	if err == QuitUnzip {
   355  		err = nil
   356  	}
   357  	return err
   358  }
   359  
   360  // UnzipContent decompresses a zip archive, extracts all files within the zip file to map of bytes.
   361  func UnzipContent(srcZip string) (ArchiveContent, error) {
   362  	store := make(ArchiveContent)
   363  	err := UnzipFile(srcZip, func(f *zip.File) error {
   364  		// Open file
   365  		fr, err := f.Open()
   366  		if err != nil {
   367  			return err
   368  		}
   369  		defer fr.Close()
   370  
   371  		// Read content
   372  		bytes, err := ioutil.ReadAll(fr)
   373  		if err == nil {
   374  			store[f.Name] = bytes
   375  		}
   376  		return err
   377  	})
   378  	return store, err
   379  }