-->

easy way to unzip file with golang

2020-05-19 04:40发布

问题:

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
}

回答1:

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)

        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).



回答2:

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
}


回答3:

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)
    }
}


回答4:

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.



回答5:

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
}


回答6:

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



回答7:

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
}


标签: go zip unzip