Skip to content

Commit

Permalink
feat(module): support archiver module
Browse files Browse the repository at this point in the history
feat: support 7z format
  • Loading branch information
aooohan committed Apr 7, 2024
1 parent b150857 commit 3134aff
Show file tree
Hide file tree
Showing 8 changed files with 452 additions and 7 deletions.
11 changes: 11 additions & 0 deletions docs/plugins/library/archiver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
# Archiver Library

`vfox` provides a decompression tool that supports `tar.gz`, `tgz`, `tar.xz`, `zip`, and `7z`. In Lua scripts, you can
use `require("vfox.archiver")` to access it.

**Usage**

```shell
local archiver = require("vfox.archiver")
local err = archiver.decompress("testdata/test.zip", "testdata/test")
```
10 changes: 10 additions & 0 deletions docs/zh-hans/plugins/library/archiver.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
# Archiver 标准库

`vfox` 提供了解压工具, 支持`tar.gz``tgz``tar.xz``zip``7z`。在Lua脚本中,你可以使用`require("vfox.archiver")`来访问它。
例如:

**Usage**
```shell
local archiver = require("vfox.archiver")
local err = archiver.decompress("testdata/test.zip", "testdata/test")
```
12 changes: 11 additions & 1 deletion go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ require (
github.com/lithammer/fuzzysearch v1.1.8
github.com/pterm/pterm v0.12.79
github.com/schollz/progressbar/v3 v3.14.2
github.com/ulikunitz/xz v0.5.11
github.com/ulikunitz/xz v0.5.12
github.com/urfave/cli/v2 v2.27.1
github.com/yuin/gopher-lua v1.1.1
golang.org/x/crypto v0.21.0
Expand All @@ -19,16 +19,26 @@ require (

require (
atomicgo.dev/schedule v0.1.0 // indirect
github.com/andybalholm/brotli v1.1.0 // indirect
github.com/andybalholm/cascadia v1.3.2 // indirect
github.com/bodgit/plumbing v1.3.0 // indirect
github.com/bodgit/sevenzip v1.5.1 // indirect
github.com/bodgit/windows v1.0.1 // indirect
github.com/containerd/console v1.0.4 // indirect
github.com/cpuguy83/go-md2man/v2 v2.0.4 // indirect
github.com/gookit/color v1.5.4 // indirect
github.com/hashicorp/errwrap v1.0.0 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
github.com/klauspost/compress v1.17.7 // indirect
github.com/mattn/go-runewidth v0.0.15 // indirect
github.com/mitchellh/colorstring v0.0.0-20190213212951-d06e56a500db // indirect
github.com/pierrec/lz4/v4 v4.1.21 // indirect
github.com/rivo/uniseg v0.4.7 // indirect
github.com/russross/blackfriday/v2 v2.1.0 // indirect
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
github.com/xrash/smetrics v0.0.0-20240312152122-5f08fbb34913 // indirect
go4.org v0.0.0-20200411211856-f5505b9728dd // indirect
golang.org/x/net v0.22.0 // indirect
golang.org/x/term v0.18.0 // indirect
golang.org/x/text v0.14.0 // indirect
Expand Down
229 changes: 229 additions & 0 deletions go.sum

Large diffs are not rendered by default.

41 changes: 41 additions & 0 deletions internal/module/archiver/archiver.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package string

import (
"github.com/version-fox/vfox/internal/util"
lua "github.com/yuin/gopher-lua"
)

// Preload adds strings to the given Lua state's package.preload table. After it
// has been preloaded, it can be loaded using require:
//
// local strings = require("vfox.archiver")
func Preload(L *lua.LState) {
L.PreloadModule("vfox.archiver", Loader)
}

// Loader is the module loader function.
func Loader(L *lua.LState) int {
t := L.NewTable()
L.SetFuncs(t, api)
L.Push(t)
return 1
}

var api = map[string]lua.LGFunction{
"decompress": decompress,
}

// decompress lua archiver.decompress(sourceFile, targetPath): port of go string.decompress() returns error
func decompress(L *lua.LState) int {
archiverPath := L.CheckString(1)
targetPath := L.CheckString(2)

err := util.NewDecompressor(archiverPath).Decompress(targetPath)
if err != nil {
L.Push(lua.LString(err.Error()))
return 1
} else {
L.Push(lua.LNil)
}
return 1
}
35 changes: 35 additions & 0 deletions internal/module/archiver/archiver_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package string

import (
lua "github.com/yuin/gopher-lua"
"os"
"testing"
)

func TestDecompress(t *testing.T) {
const str = `
local archiver = require("vfox.archiver")
local err = archiver.decompress("testdata/test.zip", "testdata/test")
assert(err == nil, "strings.decompress()")
local f = io.open("testdata/test/test.txt", "r")
if f then
f:close()
else
error("file not found")
end
`
defer func() {
_ = os.RemoveAll("testdata/test")
}()
eval(str, t)
}

func eval(str string, t *testing.T) {
s := lua.NewState()
defer s.Close()

Preload(s)
if err := s.DoString(str); err != nil {
t.Error(err)
}
}
Binary file added internal/module/archiver/testdata/test.zip
Binary file not shown.
121 changes: 115 additions & 6 deletions internal/util/decompressor.go
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,7 @@ import (
"bytes"
"compress/gzip"
"fmt"
"github.com/bodgit/sevenzip"
"io"
"os"
"path/filepath"
Expand Down Expand Up @@ -222,7 +223,7 @@ func findRootFolderInZip(zipFilePath string) string {
return firstElement
}

func (z *ZipDecompressor) isDir(path string) bool {
func isDir(path string) bool {
return strings.HasSuffix(path, "/")
}

Expand All @@ -244,19 +245,19 @@ func (z *ZipDecompressor) processZipFile(f *zip.File, dest string, rootFolderInZ
fname := strings.Join(parts, "/")

fpath := filepath.Join(dest, fname)
if f.FileInfo().IsDir() || z.isDir(fname) {
if f.FileInfo().IsDir() || isDir(fname) {
err := os.MkdirAll(fpath, os.ModePerm)
if err != nil {
return err
}
} else if z.isSymlink(f.FileInfo()) {
} else if isSymlink(f.FileInfo()) {
// symlink target is the contents of the file
buf := new(bytes.Buffer)
_, err := io.Copy(buf, rc)
if err != nil {
return fmt.Errorf("%s: reading symlink target: %v", f.FileHeader.Name, err)
}
return z.writeNewSymbolicLink(fpath, strings.TrimSpace(buf.String()))
return writeNewSymbolicLink(fpath, strings.TrimSpace(buf.String()))
} else {
err = os.MkdirAll(filepath.Dir(fpath), os.ModePerm)
if err != nil {
Expand All @@ -277,11 +278,11 @@ func (z *ZipDecompressor) processZipFile(f *zip.File, dest string, rootFolderInZ
return nil
}

func (z *ZipDecompressor) isSymlink(fi os.FileInfo) bool {
func isSymlink(fi os.FileInfo) bool {
return fi.Mode()&os.ModeSymlink != 0
}

func (z *ZipDecompressor) writeNewSymbolicLink(fpath string, target string) error {
func writeNewSymbolicLink(fpath string, target string) error {
err := os.MkdirAll(filepath.Dir(fpath), 0755)
if err != nil {
return fmt.Errorf("%s: making directory for file: %v", fpath, err)
Expand All @@ -302,6 +303,109 @@ func (z *ZipDecompressor) writeNewSymbolicLink(fpath string, target string) erro
return nil
}

type SevenZipDecompressor struct {
src string
}

func findRootFolderIn7Zip(zipFilePath string) string {
r, err := sevenzip.OpenReader(zipFilePath)
if err != nil {
return ""
}
defer r.Close()

var firstElement string

for _, f := range r.File {

normalizedPath := strings.ReplaceAll(f.Name, "\\", "/")

currentFirstElement := strings.Split(normalizedPath, "/")[0]

if firstElement != "" && firstElement != currentFirstElement {
return ""
}

if firstElement == "" {
firstElement = currentFirstElement
}
}
return firstElement
}

func (s *SevenZipDecompressor) Decompress(dest string) error {
rootFolderInZip := findRootFolderIn7Zip(s.src)
r, err := sevenzip.OpenReader(s.src)
if err != nil {
return err
}
defer r.Close()

for _, f := range r.File {
if err = s.extractFile(f, dest, rootFolderInZip); err != nil {
return err
}
}

return nil
}

func (s *SevenZipDecompressor) extractFile(f *sevenzip.File, dest string, rootFolderInZip string) error {
rc, err := f.Open()
if err != nil {
return err
}
defer rc.Close()

normalizedPath := strings.ReplaceAll(f.Name, "\\", "/")
// Split the file name into a slice
parts := strings.Split(normalizedPath, "/")
if len(parts) > 1 && rootFolderInZip != "" {
// Remove the first element
parts = parts[1:]
}
// Join the remaining elements to get the new file name
fname := strings.Join(parts, "/")

fpath := filepath.Join(dest, fname)
if f.FileInfo().IsDir() || isDir(fname) {
err := os.MkdirAll(fpath, os.ModePerm)
if err != nil {
return err
}
} else if isSymlink(f.FileInfo()) {
// symlink target is the contents of the file
buf := new(bytes.Buffer)
_, err := io.Copy(buf, rc)
if err != nil {
return fmt.Errorf("%s: reading symlink target: %v", f.FileHeader.Name, err)
}
return writeNewSymbolicLink(fpath, strings.TrimSpace(buf.String()))
} else {
var fdir string
if lastIndex := strings.LastIndex(fpath, string(os.PathSeparator)); lastIndex > -1 {
fdir = fpath[:lastIndex]
}

err = os.MkdirAll(fdir, os.ModePerm)
if err != nil {
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
}

func NewDecompressor(src string) Decompressor {
filename := filepath.Base(src)
if strings.HasSuffix(filename, ".tar.gz") || strings.HasSuffix(filename, ".tgz") {
Expand All @@ -319,5 +423,10 @@ func NewDecompressor(src string) Decompressor {
src: src,
}
}
if strings.HasSuffix(filename, ".7z") {
return &SevenZipDecompressor{
src: src,
}
}
return nil
}

0 comments on commit 3134aff

Please sign in to comment.