zephinzer/dev

View on GitHub
pkg/utils/unzip.go

Summary

Maintainability
C
1 day
Test Coverage
F
58%
package utils

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

type UnzipState string

const (
    UnzipStateStarting   UnzipState = "unzip_starting"
    UnzipStateProcessing UnzipState = "unzip_processing"
    UnzipStateError      UnzipState = "unzip_error"
    UnzipStateOK         UnzipState = "unzip_ok"
    UnzipStateStatus     UnzipState = "unzip_status"
    UnzipStateSuccess    UnzipState = "unzip_success"
    UnzipStateFailed     UnzipState = "unzip_failed"
)

// UnzipEvent is an object that is passed to the events stream
// for the consumer to know what's going on inside
type UnzipEvent struct {
    // State is a string code that indicates the underlying operation
    State UnzipState
    // Path is a string indicating path to a file if it's non-empty
    Path string
    // Message is an arbitrary string
    Message string
    // Status should provide metadata on the underlying operation
    Status *UnzipStatus
}

// UnzipStatus stores the status of the unzipping process and is
// returned through the UnzipEvent object
type UnzipStatus struct {
    // BytesTotal is the total number of bytes to extract
    BytesTotal int64
    // BytesProcessed is the number of bytes processed so far
    BytesProcessed int64
    // FilesTotalCount is the total number of files to extract
    FilesTotalCount int
    // FilesProcessedCount is the number of files processed so far
    FilesProcessedCount int
}

func (us UnzipStatus) GetPercentDoneByBytes() float64 {
    return float64(us.BytesProcessed) / float64(us.BytesTotal) * 100
}

func (us UnzipStatus) GetPercentDoneByFiles() float64 {
    return float64(us.FilesProcessedCount) / float64(us.FilesTotalCount) * 100
}

type UnzipOptions struct {
    // Events, if populated, receives events for logging purposes
    Events chan UnzipEvent
    // InputPath defines the path to the .zip file we want to unzip
    InputPath string
    // OutputPath defines the path to a directory where the unzipped files
    // should go
    OutputPath string
    // ReturnOnFileError indicates whether to return an error instantly
    // when an error is encountered
    ReturnOnFileError bool
}

// Unzip takes the .zip file located at options.InputPath and unzips its contents
// to options.OutputPath; returns an error if something unexpected happened, or
// nil if all is well
//
// Made possible with guidance from: https://golangcode.com/unzip-files-in-go/
func Unzip(options UnzipOptions) []error {
    var err error
    status := UnzipStatus{}

    if options.Events == nil {
        options.Events = make(chan UnzipEvent, 16)
        go func() {
            for {
                if _, ok := <-options.Events; !ok {
                    return
                }
            }
        }()
    }
    defer close(options.Events)

    // configure paths using absolute paths
    pathToZip := options.InputPath
    pathToExtractTo := options.OutputPath

    // configure the reader for zip files
    var zipReader *zip.ReadCloser
    if zipReader, err = zip.OpenReader(pathToZip); err != nil {
        options.Events <- UnzipEvent{UnzipStateError, "", fmt.Sprintf("failed to open zip file at '%s': %s", pathToZip, err), &status}
        return []error{err}
    }
    defer zipReader.Close()

    // prepare for iteration
    errors := []error{}
    for _, file := range zipReader.File {
        status.BytesTotal += file.FileInfo().Size()
    }
    status.FilesTotalCount = len(zipReader.File)
    options.Events <- UnzipEvent{UnzipStateStarting, "", "", &status}

    // go through all files in the zip file
    for _, file := range zipReader.File {
        status.FilesProcessedCount++
        absoluteOutputPath := path.Join(pathToExtractTo, file.Name)
        options.Events <- UnzipEvent{UnzipStateProcessing, absoluteOutputPath, "", &status}
        if file.FileInfo().IsDir() {
            os.MkdirAll(absoluteOutputPath, os.ModePerm)
            options.Events <- UnzipEvent{UnzipStateProcessing, absoluteOutputPath, "created dir", &status}
            continue
        }

        // ensure the directory to the file has been created
        if os.MkdirAll(filepath.Dir(absoluteOutputPath), os.ModePerm); err != nil {
            options.Events <- UnzipEvent{UnzipStateError, absoluteOutputPath, err.Error(), &status}
            if options.ReturnOnFileError {
                return []error{err}
            }
            errors = append(errors, err)
            continue
        }

        // open the file for writing (create if doesn't exist)
        outputFile, err := os.OpenFile(absoluteOutputPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, os.ModePerm)
        if err != nil {
            options.Events <- UnzipEvent{UnzipStateError, absoluteOutputPath, err.Error(), &status}
            if options.ReturnOnFileError {
                options.Events <- UnzipEvent{UnzipStateFailed, absoluteOutputPath, err.Error(), &status}
                return []error{err}
            }
            errors = append(errors, err)
            continue
        }
        options.Events <- UnzipEvent{UnzipStateProcessing, absoluteOutputPath, "created/opened file", &status}
        inputFile, err := file.Open()
        if err != nil {
            options.Events <- UnzipEvent{UnzipStateError, absoluteOutputPath, err.Error(), &status}
            if options.ReturnOnFileError {
                options.Events <- UnzipEvent{UnzipStateFailed, absoluteOutputPath, err.Error(), &status}
                return []error{err}
            }
            errors = append(errors, err)
            continue
        }

        size, copyErr := io.Copy(outputFile, inputFile)
        if outputErr := outputFile.Close(); err != nil {
            options.Events <- UnzipEvent{UnzipStateError, absoluteOutputPath, outputErr.Error(), &status}
            if options.ReturnOnFileError {
                options.Events <- UnzipEvent{UnzipStateFailed, absoluteOutputPath, outputErr.Error(), &status}
                return []error{outputErr}
            }
            errors = append(errors, outputErr)
            continue
        }
        if inputErr := inputFile.Close(); err != nil {
            options.Events <- UnzipEvent{UnzipStateError, absoluteOutputPath, inputErr.Error(), &status}
            if options.ReturnOnFileError {
                options.Events <- UnzipEvent{UnzipStateFailed, absoluteOutputPath, inputErr.Error(), &status}
                return []error{inputErr}
            }
            errors = append(errors, inputErr)
            continue
        }

        if copyErr != nil {
            options.Events <- UnzipEvent{UnzipStateError, absoluteOutputPath, copyErr.Error(), &status}
            if options.ReturnOnFileError {
                options.Events <- UnzipEvent{UnzipStateFailed, absoluteOutputPath, copyErr.Error(), &status}
                return []error{copyErr}
            }
            errors = append(errors, copyErr)
            continue
        }
        status.BytesProcessed += size
        options.Events <- UnzipEvent{UnzipStateOK, absoluteOutputPath, fmt.Sprintf("extracted %v bytes", size), &status}
    }

    if len(errors) > 0 {
        options.Events <- UnzipEvent{UnzipStateFailed, "", "", &status}
        return errors
    }
    options.Events <- UnzipEvent{UnzipStateSuccess, "", "", &status}
    return nil
}