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 }