github.com/ActiveState/cli@v0.0.0-20240508170324-6801f60cd051/internal/unarchiver/unarchiver.go (about)

     1  // Package unarchiver provides a method to unarchive tar.gz or zip archives with progress bar feedback
     2  // Currently, this implementation copies a lot of methods that are internal to the ActiveState/archiver dependency.
     3  package unarchiver
     4  
     5  import (
     6  	"fmt"
     7  	"io"
     8  	"os"
     9  	"path/filepath"
    10  	"runtime"
    11  
    12  	"github.com/mholt/archiver"
    13  
    14  	"github.com/ActiveState/cli/internal/fileutils"
    15  )
    16  
    17  // SingleUnarchiver is an interface for an unarchiver that can unpack the next file
    18  // It extends the existing archiver.Reader with a method to extract a single file from the archive
    19  type SingleUnarchiver interface {
    20  	archiver.Reader
    21  
    22  	// ExtractNext extracts the next file in the archive
    23  	ExtractNext(destination string) (f archiver.File, err error)
    24  
    25  	// CheckExt checks that the file extension is appropriate for the archive
    26  	CheckExt(archiveName string) error
    27  
    28  	// Ext returns a valid file name extension for this archiver
    29  	Ext() string
    30  }
    31  
    32  // ExtractNotifier gets called when a new file has been extracted from the archive
    33  type ExtractNotifier func(fileName string, size int64, isDir bool)
    34  
    35  // Unarchiver wraps an implementation of an unarchiver that can unpack one file at a time.
    36  type Unarchiver struct {
    37  	// wraps a struct that can unpack one file at a time.
    38  	impl SingleUnarchiver
    39  
    40  	notifier ExtractNotifier
    41  }
    42  
    43  func (ua *Unarchiver) Ext() string {
    44  	return ua.impl.Ext()
    45  }
    46  
    47  // SetNotifier sets the notification function to be called after extracting a file
    48  func (ua *Unarchiver) SetNotifier(cb ExtractNotifier) {
    49  	ua.notifier = cb
    50  }
    51  
    52  // PrepareUnpacking prepares the destination directory and the archive for unpacking
    53  // Returns the opened file and its size
    54  func (ua *Unarchiver) PrepareUnpacking(source, destination string) (archiveFile *os.File, fileSize int64, err error) {
    55  
    56  	if !fileutils.DirExists(destination) {
    57  		err := mkdir(destination)
    58  		if err != nil {
    59  			return nil, 0, fmt.Errorf("preparing destination: %v", err)
    60  		}
    61  	}
    62  
    63  	archiveFile, err = os.Open(source)
    64  	if err != nil {
    65  		return nil, 0, err
    66  	}
    67  
    68  	fileInfo, err := archiveFile.Stat()
    69  	if err != nil {
    70  		archiveFile.Close()
    71  		return nil, 0, fmt.Errorf("statting source file: %v", err)
    72  	}
    73  
    74  	return archiveFile, fileInfo.Size(), nil
    75  
    76  }
    77  
    78  // CheckExt checks that the file extension is appropriate for the given unarchiver
    79  func (ua *Unarchiver) CheckExt(archiveName string) error {
    80  	return ua.impl.CheckExt(archiveName)
    81  }
    82  
    83  // Unarchive unarchives an archive file ` and unpacks it in `destination`
    84  func (ua *Unarchiver) Unarchive(archiveStream io.Reader, archiveSize int64, destination string) (err error) {
    85  	// impl is the actual implementation of the unarchiver (tar.gz or zip)
    86  	impl := ua.impl
    87  
    88  	// read one file at a time from the archive
    89  	err = impl.Open(archiveStream, archiveSize)
    90  	if err != nil {
    91  		return
    92  	}
    93  	// note: that this is obviously not thread-safe
    94  	defer impl.Close()
    95  
    96  	for {
    97  		// extract one file at a time
    98  		var f archiver.File
    99  		f, err = impl.ExtractNext(destination)
   100  		if err == io.EOF {
   101  			break
   102  		}
   103  		if err != nil {
   104  			return
   105  		}
   106  
   107  		//logging.Debug("Extracted %s File size: %d", f.Name(), f.Size())
   108  		ua.notifier(f.Name(), f.Size(), f.IsDir())
   109  	}
   110  
   111  	return nil
   112  }
   113  
   114  // the following files are just copied from the ActiveState/archiver repository
   115  // so we can use them in our extensions
   116  
   117  func writeNewFile(fpath string, in io.Reader, fm os.FileMode) error {
   118  	err := os.MkdirAll(filepath.Dir(fpath), 0755)
   119  	if err != nil {
   120  		return fmt.Errorf("%s: making directory for file: %v", fpath, err)
   121  	}
   122  
   123  	out, err := os.Create(fpath)
   124  	if err != nil {
   125  		return fmt.Errorf("%s: creating new file: %v", fpath, err)
   126  	}
   127  	defer out.Close()
   128  
   129  	err = out.Chmod(fm)
   130  	if err != nil && runtime.GOOS != "windows" {
   131  		return fmt.Errorf("%s: changing file mode: %v", fpath, err)
   132  	}
   133  
   134  	_, err = io.Copy(out, in)
   135  	if err != nil {
   136  		return fmt.Errorf("%s: writing file: %v", fpath, err)
   137  	}
   138  	return nil
   139  }
   140  
   141  func writeNewSymbolicLink(fpath string, target string) error {
   142  	err := os.MkdirAll(filepath.Dir(fpath), 0755)
   143  	if err != nil {
   144  		return fmt.Errorf("%s: making directory for file: %v", fpath, err)
   145  	}
   146  
   147  	err = os.Symlink(target, fpath)
   148  	if err != nil {
   149  		return fmt.Errorf("%s: making symbolic link for: %v", fpath, err)
   150  	}
   151  
   152  	return nil
   153  }
   154  
   155  func writeNewHardLink(fpath string, target string) error {
   156  	err := os.MkdirAll(filepath.Dir(fpath), 0755)
   157  	if err != nil {
   158  		return fmt.Errorf("%s: making directory for file: %v", fpath, err)
   159  	}
   160  
   161  	err = os.Link(target, fpath)
   162  	if err != nil {
   163  		return fmt.Errorf("%s: making hard link for: %v", fpath, err)
   164  	}
   165  
   166  	return nil
   167  }
   168  
   169  // ensure the implementation of the interface
   170  func fileExists(name string) bool {
   171  	_, err := os.Stat(name)
   172  	return !os.IsNotExist(err)
   173  }
   174  
   175  func mkdir(dirPath string) error {
   176  	err := os.MkdirAll(dirPath, 0755)
   177  	if err != nil {
   178  		return fmt.Errorf("%s: making directory: %v", dirPath, err)
   179  	}
   180  	return nil
   181  }