is there a easy way to unzip file with golang ?

right now my code is:

func Unzip(src, dest string) error {
    r, err := zip.OpenReader(src)
    if err != nil {
        return err
    }
    defer r.Close()

    for _, f := range r.File {
        rc, err := f.Open()
        if err != nil {
            return err
        }
        defer rc.Close()

        path := filepath.Join(dest, f.Name)
        if f.FileInfo().IsDir() {
            os.MkdirAll(path, f.Mode())
        } else {
            f, err := os.OpenFile(
                path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
            if err != nil {
                return err
            }
            defer f.Close()

            _, err = io.Copy(f, rc)
            if err != nil {
                return err
            }
        }
    }

    return nil
}
有帮助吗?

解决方案

Slight rework of the OP's solution to create the containing directory dest if it doesn't exist, and to wrap the file extraction/writing in a closure to eliminate stacking of defer .Close() calls per @Nick Craig-Wood's comment:

func Unzip(src, dest string) error {
    r, err := zip.OpenReader(src)
    if err != nil {
        return err
    }
    defer func() {
        if err := r.Close(); err != nil {
            panic(err)
        }
    }()

    os.MkdirAll(dest, 0755)

    // Closure to address file descriptors issue with all the deferred .Close() methods
    extractAndWriteFile := func(f *zip.File) error {
        rc, err := f.Open()
        if err != nil {
            return err
        }
        defer func() {
            if err := rc.Close(); err != nil {
                panic(err)
            }
        }()

        path := filepath.Join(dest, f.Name)

        // Check for ZipSlip (Directory traversal)
        if !strings.HasPrefix(path, filepath.Clean(dest) + string(os.PathSeparator)) {
            return fmt.Errorf("illegal file path: %s", path)
        }

        if f.FileInfo().IsDir() {
            os.MkdirAll(path, f.Mode())
        } else {
            os.MkdirAll(filepath.Dir(path), f.Mode())
            f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
            if err != nil {
                return err
            }
            defer func() {
                if err := f.Close(); err != nil {
                    panic(err)
                }
            }()

            _, err = io.Copy(f, rc)
            if err != nil {
                return err
            }
        }
        return nil
    }

    for _, f := range r.File {
        err := extractAndWriteFile(f)
        if err != nil {
            return err
        }
    }

    return nil
}

Note: Updated to include Close() error handling as well (if we're looking for best practices, may as well follow ALL of them).

其他提示

I'm using archive/zip package to read .zip files and copy to the local disk. Below is the source code to unzip .zip files for my own needs.

import (
    "archive/zip"
    "io"
    "log"
    "os"
    "path/filepath"
    "strings"
)

func unzip(src, dest string) error {
    r, err := zip.OpenReader(src)
    if err != nil {
        return err
    }
    defer r.Close()

    for _, f := range r.File {
        rc, err := f.Open()
        if err != nil {
            return err
        }
        defer rc.Close()

        fpath := filepath.Join(dest, f.Name)
        if f.FileInfo().IsDir() {
            os.MkdirAll(fpath, f.Mode())
        } else {
            var fdir string
            if lastIndex := strings.LastIndex(fpath,string(os.PathSeparator)); lastIndex > -1 {
                fdir = fpath[:lastIndex]
            }

            err = os.MkdirAll(fdir, f.Mode())
            if err != nil {
                log.Fatal(err)
                return err
            }
            f, err := os.OpenFile(
                fpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
            if err != nil {
                return err
            }
            defer f.Close()

            _, err = io.Copy(f, rc)
            if err != nil {
                return err
            }
        }
    }
    return nil
}

I would prefer using 7zip with Go, which would give you something like this.

func extractZip() {
    fmt.Println("extracting", zip_path)
    commandString := fmt.Sprintf(`7za e %s %s`, zip_path, dest_path)
    commandSlice := strings.Fields(commandString)
    fmt.Println(commandString)
    c := exec.Command(commandSlice[0], commandSlice[1:]...)
    e := c.Run()
    checkError(e)
}

Better example code

However, if using 7zip isn't possible, then try this. Defer a recover to catch the panics. (Example)

func checkError(e error){
  if e != nil {
    panic(e)
  }
}
func cloneZipItem(f *zip.File, dest string){
    // Create full directory path
    path := filepath.Join(dest, f.Name)
    fmt.Println("Creating", path)
    err := os.MkdirAll(filepath.Dir(path), os.ModeDir|os.ModePerm)
    checkError(err)

    // Clone if item is a file
    rc, err := f.Open()
    checkError(err)
    if !f.FileInfo().IsDir() {
        // Use os.Create() since Zip don't store file permissions.
        fileCopy, err := os.Create(path)
        checkError(err)
        _, err = io.Copy(fileCopy, rc)
        fileCopy.Close()
        checkError(err)
    }
    rc.Close()
}
func Extract(zip_path, dest string) {
    r, err := zip.OpenReader(zip_path)
    checkError(err)
    defer r.Close()
    for _, f := range r.File {
        cloneZipItem(f, dest)
    }
}

Most answers here are wrong, as they assume that the Zip archive will emit directory entries. Zip archives are not required to emit directory entries, and so given an archive such as that, a naive program will crash upon encountering the first non-root file, as no directories will ever be created, and the file creation will fail as the directory has not been created yet. Here is a different approach:

package main

import (
   "archive/zip"
   "os"
   "path"
)

func unzip(source, dest string) error {
   read, err := zip.OpenReader(source)
   if err != nil { return err }
   defer read.Close()
   for _, file := range read.File {
      if file.Mode().IsDir() { continue }
      open, err := file.Open()
      if err != nil { return err }
      name := path.Join(dest, file.Name)
      os.MkdirAll(path.Dir(name), os.ModeDir)
      create, err := os.Create(name)
      if err != nil { return err }
      defer create.Close()
      create.ReadFrom(open)
   }
   return nil
}

func main() {
   unzip("Microsoft.VisualCpp.Tools.HostX64.TargetX64.vsix", "tools")
}

I have been doing some browsing of google and repeatedly found people saying that there is no library that can handle that. Maybe I missed a custom repository in my search though and someone else will find it for us.

You may be able to make use of io.Copy(src, dest) to ease the process but I haven't tested it at all.

For instance:

os.MkDirAll(dest, r.File.Mode)
d, _ := os.Open(dest)
io.Copy(r.File, d)

Honestly to me your code looks pretty nice and if I were to do an Extract function myself (and the above doesn't work) then I would probably take a page from your book.

While working on the query for catching ZipSlip vulnerabilities in Go on LGTM.com (of which I am a developer), I noticed code similar to the accepted answer in several projects, e.g. rclone.

As @woogoo has pointed out, this code is vulnerable to ZipSlip, so I believe the answer should be updated to something like the following (code taken from rclone fix):

func Unzip(src, dest string) error {
    dest = filepath.Clean(dest) + string(os.PathSeparator)

    r, err := zip.OpenReader(src)
    if err != nil {
        return err
    }
    defer func() {
        if err := r.Close(); err != nil {
            panic(err)
        }
    }()

    os.MkdirAll(dest, 0755)

    // Closure to address file descriptors issue with all the deferred .Close() methods
    extractAndWriteFile := func(f *zip.File) error {
        path := filepath.Join(dest, f.Name)
        // Check for ZipSlip: https://snyk.io/research/zip-slip-vulnerability
        if !strings.HasPrefix(path, dest) {
            return fmt.Errorf("%s: illegal file path", path)
        }

        rc, err := f.Open()
        if err != nil {
            return err
        }
        defer func() {
            if err := rc.Close(); err != nil {
                panic(err)
            }
        }()

        if f.FileInfo().IsDir() {
            os.MkdirAll(path, f.Mode())
        } else {
            os.MkdirAll(filepath.Dir(path), f.Mode())
            f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.Mode())
            if err != nil {
                return err
            }
            defer func() {
                if err := f.Close(); err != nil {
                    panic(err)
                }
            }()

            _, err = io.Copy(f, rc)
            if err != nil {
                return err
            }
        }
        return nil
    }

    for _, f := range r.File {
        err := extractAndWriteFile(f)
        if err != nil {
            return err
        }
    }

    return nil
}

there is Zip Slip Vulnerability in the code of @Astockwell , see: https://snyk.io/research/zip-slip-vulnerability

package main

import (
    "os"
    "io"
    "io/ioutil"
    "fmt"
    "strings"
    "archive/zip"
    "path/filepath"
)

func main() {
    if err := foo("test.zip"); err != nil {
        fmt.Println(err)
    }
}

func foo(yourZipFile string) error {
    tmpDir, err := ioutil.TempDir("/tmp", "yourPrefix-")
    if err != nil {
        return err
    }
    defer os.RemoveAll(tmpDir)
    if filenames, err := Unzip(yourZipFile, tmpDir); err != nil {
        return err
    } else {
        for f := range filenames {
            fmt.Println(filenames[f])
        }
    }
    return nil
}


func Unzip(src string, dst string) ([]string, error) {
    var filenames []string
    r, err := zip.OpenReader(src)
    if err != nil {
        return nil, err
    }
    defer r.Close()
    for f := range r.File {
        dstpath := filepath.Join(dst, r.File[f].Name)
        if !strings.HasPrefix(dstpath, filepath.Clean(dst) + string(os.PathSeparator)) {
            return nil, fmt.Errorf("%s: illegal file path", src)
        }
        if r.File[f].FileInfo().IsDir() {
            if err := os.MkdirAll(dstpath, os.ModePerm); err != nil {
                return nil, err
            }
        } else {
            if rc, err := r.File[f].Open(); err != nil {
                return nil, err
            } else {
                defer rc.Close()
                if of, err := os.OpenFile(dstpath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, r.File[f].Mode()); err != nil {
                    return nil, err
                } else {
                    defer of.Close()
                    if _, err = io.Copy(of, rc); err != nil {
                        return nil, err
                    } else {
                        of.Close()
                        rc.Close()
                        filenames = append(filenames, dstpath)
                    }
                }
            }
        }
    }
    if len(filenames) == 0 {
        return nil, fmt.Errorf("zip file is empty")
    }
    return filenames, nil
}
许可以下: CC-BY-SA归因
不隶属于 StackOverflow
scroll top