stash/vendor/github.com/asticode/go-astikit/archive.go
cj c1a096a1a6
Caption support (#2462)
Co-authored-by: WithoutPants <53250216+WithoutPants@users.noreply.github.com>
2022-05-06 11:59:28 +10:00

214 lines
5.7 KiB
Go

package astikit
import (
"archive/zip"
"context"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"strings"
)
// internal shouldn't lead with a "/"
func zipInternalPath(p string) (external, internal string) {
if items := strings.Split(p, ".zip"); len(items) > 1 {
external = items[0] + ".zip"
internal = strings.TrimPrefix(strings.Join(items[1:], ".zip"), string(os.PathSeparator))
return
}
external = p
return
}
// Zip zips a src into a dst
// Possible dst formats are:
// - /path/to/zip.zip
// - /path/to/zip.zip/root/path
func Zip(ctx context.Context, dst, src string) (err error) {
// Get external/internal path
externalPath, internalPath := zipInternalPath(dst)
// Make sure the directory exists
if err = os.MkdirAll(filepath.Dir(externalPath), DefaultDirMode); err != nil {
return fmt.Errorf("astikit: mkdirall %s failed: %w", filepath.Dir(externalPath), err)
}
// Create destination file
var dstFile *os.File
if dstFile, err = os.Create(externalPath); err != nil {
return fmt.Errorf("astikit: creating %s failed: %w", externalPath, err)
}
defer dstFile.Close()
// Create zip writer
var zw = zip.NewWriter(dstFile)
defer zw.Close()
// Walk
if err = filepath.Walk(src, func(path string, info os.FileInfo, e error) (err error) {
// Process error
if e != nil {
err = e
return
}
// Init header
var h *zip.FileHeader
if h, err = zip.FileInfoHeader(info); err != nil {
return fmt.Errorf("astikit: initializing zip header failed: %w", err)
}
// Set header info
h.Name = filepath.Join(internalPath, strings.TrimPrefix(path, src))
if info.IsDir() {
h.Name += string(os.PathSeparator)
} else {
h.Method = zip.Deflate
}
// Create writer
var w io.Writer
if w, err = zw.CreateHeader(h); err != nil {
return fmt.Errorf("astikit: creating zip header failed: %w", err)
}
// If path is dir, stop here
if info.IsDir() {
return
}
// Open path
var walkFile *os.File
if walkFile, err = os.Open(path); err != nil {
return fmt.Errorf("astikit: opening %s failed: %w", path, err)
}
defer walkFile.Close()
// Copy
if _, err = Copy(ctx, w, walkFile); err != nil {
return fmt.Errorf("astikit: copying failed: %w", err)
}
return
}); err != nil {
return fmt.Errorf("astikit: walking failed: %w", err)
}
return
}
// Unzip unzips a src into a dst
// Possible src formats are:
// - /path/to/zip.zip
// - /path/to/zip.zip/root/path
func Unzip(ctx context.Context, dst, src string) (err error) {
// Get external/internal path
externalPath, internalPath := zipInternalPath(src)
// Make sure the destination exists
if err = os.MkdirAll(dst, DefaultDirMode); err != nil {
return fmt.Errorf("astikit: mkdirall %s failed: %w", dst, err)
}
// Open overall reader
var r *zip.ReadCloser
if r, err = zip.OpenReader(externalPath); err != nil {
return fmt.Errorf("astikit: opening overall zip reader on %s failed: %w", externalPath, err)
}
defer r.Close()
// Loop through files to determine their type
var dirs, files, symlinks = make(map[string]*zip.File), make(map[string]*zip.File), make(map[string]*zip.File)
for _, f := range r.File {
// Validate internal path
if internalPath != "" && !strings.HasPrefix(f.Name, internalPath) {
continue
}
var p = filepath.Join(dst, strings.TrimPrefix(f.Name, internalPath))
// Check file type
if f.FileInfo().Mode()&os.ModeSymlink != 0 {
symlinks[p] = f
} else if f.FileInfo().IsDir() {
dirs[p] = f
} else {
files[p] = f
}
}
// Invalid internal path
if internalPath != "" && len(dirs) == 0 && len(files) == 0 && len(symlinks) == 0 {
return fmt.Errorf("astikit: content in archive does not match specified internal path %s", internalPath)
}
// Create dirs
for p, f := range dirs {
if err = os.MkdirAll(p, f.FileInfo().Mode().Perm()); err != nil {
return fmt.Errorf("astikit: mkdirall %s failed: %w", p, err)
}
}
// Create files
for p, f := range files {
if err = createZipFile(ctx, f, p); err != nil {
return fmt.Errorf("astikit: creating zip file into %s failed: %w", p, err)
}
}
// Create symlinks
for p, f := range symlinks {
if err = createZipSymlink(f, p); err != nil {
return fmt.Errorf("astikit: creating zip symlink into %s failed: %w", p, err)
}
}
return
}
func createZipFile(ctx context.Context, f *zip.File, p string) (err error) {
// Open file reader
var fr io.ReadCloser
if fr, err = f.Open(); err != nil {
return fmt.Errorf("astikit: opening zip reader on file %s failed: %w", f.Name, err)
}
defer fr.Close()
// Since dirs don't always come up we make sure the directory of the file exists with default
// file mode
if err = os.MkdirAll(filepath.Dir(p), DefaultDirMode); err != nil {
return fmt.Errorf("astikit: mkdirall %s failed: %w", filepath.Dir(p), err)
}
// Open the file
var fl *os.File
if fl, err = os.OpenFile(p, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, f.FileInfo().Mode().Perm()); err != nil {
return fmt.Errorf("astikit: opening file %s failed: %w", p, err)
}
defer fl.Close()
// Copy
if _, err = Copy(ctx, fl, fr); err != nil {
return fmt.Errorf("astikit: copying %s into %s failed: %w", f.Name, p, err)
}
return
}
func createZipSymlink(f *zip.File, p string) (err error) {
// Open file reader
var fr io.ReadCloser
if fr, err = f.Open(); err != nil {
return fmt.Errorf("astikit: opening zip reader on file %s failed: %w", f.Name, err)
}
defer fr.Close()
// If file is a symlink we retrieve the target path that is in the content of the file
var b []byte
if b, err = ioutil.ReadAll(fr); err != nil {
return fmt.Errorf("astikit: ioutil.Readall on %s failed: %w", f.Name, err)
}
// Create the symlink
if err = os.Symlink(string(b), p); err != nil {
return fmt.Errorf("astikit: creating symlink from %s to %s failed: %w", string(b), p, err)
}
return
}