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 }