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 }