- Initial commit.
This commit is contained in:
commit
27d3e52b46
2
.gitignore
vendored
Normal file
2
.gitignore
vendored
Normal file
@ -0,0 +1,2 @@
|
|||||||
|
*.log
|
||||||
|
.vscode/
|
||||||
8
CHANGELOG.md
Normal file
8
CHANGELOG.md
Normal file
@ -0,0 +1,8 @@
|
|||||||
|
# git.sa-roci.de/oss/go_zipper Release notes
|
||||||
|
|
||||||
|
All notable changes to this project will be documented in this file.
|
||||||
|
This project adheres to [Semantic Versioning](http://semver.org/).
|
||||||
|
|
||||||
|
## v. 0.0.1
|
||||||
|
|
||||||
|
- Initial Release.
|
||||||
9
LICENSE
Normal file
9
LICENSE
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
MIT License
|
||||||
|
|
||||||
|
Copyright (c) 2024 Sa Rocí Solutions
|
||||||
|
|
||||||
|
Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions:
|
||||||
|
|
||||||
|
The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
|
||||||
5
README.md
Normal file
5
README.md
Normal file
@ -0,0 +1,5 @@
|
|||||||
|
# git.sa-roci.de/oss/go_zipper
|
||||||
|
|
||||||
|
An extensible module for file (de-)compression.
|
||||||
|
|
||||||
|
Includes sub-packages for gzip and zip (de-)compression.
|
||||||
184
compress.go
Normal file
184
compress.go
Normal file
@ -0,0 +1,184 @@
|
|||||||
|
package zipper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Writer interface {
|
||||||
|
Flush() error
|
||||||
|
Close() error
|
||||||
|
}
|
||||||
|
|
||||||
|
type Compressor interface {
|
||||||
|
Compress(fileToZip *os.File, pathInArchive string) (err error)
|
||||||
|
OpenWriter(zipFile *os.File) (err error)
|
||||||
|
MultiFileSupported() bool
|
||||||
|
}
|
||||||
|
|
||||||
|
// zipper defines a struct for a Zip-file writer
|
||||||
|
type zipper struct {
|
||||||
|
filename string
|
||||||
|
zipFile *os.File
|
||||||
|
zipWriter Writer
|
||||||
|
isOpen bool
|
||||||
|
compressor Compressor
|
||||||
|
canCompress bool
|
||||||
|
}
|
||||||
|
|
||||||
|
type Zipper interface {
|
||||||
|
io.Closer
|
||||||
|
AddZipFilesPathed(files []string, pathInArchive, pathInFileSystem string) error
|
||||||
|
AddZipFiles(files []string, pathInArchive string) error
|
||||||
|
AddZipFilesPathedSimple(files []string, pathInFileSystem string) error
|
||||||
|
AddZipFilesSimple(files []string) error
|
||||||
|
AddFileToZipPathed(filename, pathInArchive, pathInFileSystem string) error
|
||||||
|
AddFileToZip(filename, pathInArchive string) error
|
||||||
|
AddFileToZipPathedSimple(filename, pathInFileSystem string) error
|
||||||
|
AddFileToZipSimple(filename string) error
|
||||||
|
SetCompressor(compressor Compressor) error
|
||||||
|
SetWriter(writer Writer)
|
||||||
|
GetWriter() (writer Writer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBaseZipper makes a new Zipper interface missing the compressor.
|
||||||
|
func NewBaseZipper(archiveFilename string) Zipper {
|
||||||
|
return &zipper{filename: archiveFilename, isOpen: false, canCompress: true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zipper *zipper) SetCompressor(compressor Compressor) (err error) {
|
||||||
|
if compressor != nil {
|
||||||
|
if zipper.compressor == nil {
|
||||||
|
zipper.compressor = compressor
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("compressor already defined")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("undefined compressor")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zipper *zipper) SetWriter(writer Writer) {
|
||||||
|
zipper.zipWriter = writer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (zipper *zipper) GetWriter() Writer {
|
||||||
|
return zipper.zipWriter
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the Closer interface
|
||||||
|
func (zipper *zipper) Close() (err error) {
|
||||||
|
if zipper.isOpen {
|
||||||
|
if nil != zipper.zipWriter {
|
||||||
|
zipper.zipWriter.Flush()
|
||||||
|
if err = zipper.zipWriter.Close(); err == nil {
|
||||||
|
zipper.zipWriter = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nil == err && nil != zipper.zipFile {
|
||||||
|
zipper.zipFile.Sync()
|
||||||
|
if err = zipper.zipFile.Close(); err == nil {
|
||||||
|
zipper.zipFile = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
zipper.isOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddZipFilesPathed compresses one or many files into the current zip archive file.
|
||||||
|
// Param 1: files is a list of files to add to the zip.
|
||||||
|
// Param 2: pathInArchive is subfolder definition within the zip, where the files are to be situated.
|
||||||
|
// Param 3: pathInFileSystem is the subfolder definition within the file system, where the file may be found.
|
||||||
|
func (zipper *zipper) AddZipFilesPathed(files []string, pathInArchive, pathInFileSystem string) (err error) {
|
||||||
|
// Add files to zip
|
||||||
|
for _, file := range files {
|
||||||
|
if err = zipper.AddFileToZipPathed(file, pathInArchive, pathInFileSystem); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddZipFiles compresses one or many files into the current zip archive file.
|
||||||
|
// Param 1: files is a list of files to add to the zip.
|
||||||
|
// Param 2: pathInArchive is subfolder definition within the zip, where the files are to be situated.
|
||||||
|
func (zipper *zipper) AddZipFiles(files []string, pathInArchive string) error {
|
||||||
|
return zipper.AddZipFilesPathed(files, pathInArchive, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddZipFilesPathedSimple compresses one or many files into the current zip archive file.
|
||||||
|
// Param 1: files is a list of files to add to the zip.
|
||||||
|
// Param 2: pathInFileSystem is the subfolder definition within the file system, where the file may be found.
|
||||||
|
func (zipper *zipper) AddZipFilesPathedSimple(files []string, pathInFileSystem string) error {
|
||||||
|
return zipper.AddZipFilesPathed(files, "", pathInFileSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddZipFilesSimple compresses one or many files into the current zip archive file.
|
||||||
|
// Param: files is a list of files to add to the zip.
|
||||||
|
func (zipper *zipper) AddZipFilesSimple(files []string) error {
|
||||||
|
return zipper.AddZipFilesPathed(files, "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFileToZip compresses a single file into the current zip archive file.
|
||||||
|
// Param 1: filename reflects the path of the file to add to the zip within the file system.
|
||||||
|
// Param 2: pathInArchive is the subfolder definition within the zip, where the file is to be situated.
|
||||||
|
func (zipper *zipper) AddFileToZip(filename, pathInArchive string) error {
|
||||||
|
return zipper.AddFileToZipPathed(filename, pathInArchive, "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFileToZipPathedSimple compresses a single file into the current zip archive file.
|
||||||
|
// Param 1: filename reflects the path of the file to add to the zip within the file system.
|
||||||
|
// Param 2: pathInFileSystem is the subfolder definition within the file system, where the file may be found.
|
||||||
|
func (zipper *zipper) AddFileToZipPathedSimple(filename, pathInFileSystem string) error {
|
||||||
|
return zipper.AddFileToZipPathed(filename, "", pathInFileSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFileToZipSimple compresses a single file into the current zip archive file.
|
||||||
|
// Param: filename reflects the path of the file to add to the zip within the file system.
|
||||||
|
func (zipper *zipper) AddFileToZipSimple(filename string) error {
|
||||||
|
return zipper.AddFileToZipPathed(filename, "", "")
|
||||||
|
}
|
||||||
|
|
||||||
|
// AddFileToZipPathed compresses a single file into the current zip archive file.
|
||||||
|
// Param 1: filename reflects the path of the file to add to the zip within the file system.
|
||||||
|
// Param 2: pathInArchive is the subfolder definition within the zip, where the file is to be situated.
|
||||||
|
// Param 3: pathInFileSystem is the subfolder definition within the file system, where the file may be found.
|
||||||
|
func (zipper *zipper) AddFileToZipPathed(filename, pathInArchive, pathInFileSystem string) (err error) {
|
||||||
|
if zipper.compressor == nil {
|
||||||
|
return fmt.Errorf("unable to initialise compressor")
|
||||||
|
}
|
||||||
|
if !zipper.isOpen {
|
||||||
|
if err = MakeSurePathExists(zipper.filename, 0644); err == nil {
|
||||||
|
if zipper.zipFile, err = os.Create(zipper.filename); err == nil {
|
||||||
|
err = zipper.compressor.OpenWriter(zipper.zipFile)
|
||||||
|
zipper.isOpen = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nil != err {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if !zipper.canCompress {
|
||||||
|
return fmt.Errorf("no further compression is supported")
|
||||||
|
}
|
||||||
|
|
||||||
|
var fileToZip *os.File
|
||||||
|
if fileToZip, err = os.Open(PrependPath(filename, pathInFileSystem)); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer fileToZip.Close()
|
||||||
|
|
||||||
|
if err = zipper.compressor.Compress(fileToZip, pathInArchive); err == nil {
|
||||||
|
if !zipper.compressor.MultiFileSupported() {
|
||||||
|
zipper.canCompress = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
78
gzip/compress.go
Normal file
78
gzip/compress.go
Normal file
@ -0,0 +1,78 @@
|
|||||||
|
package gzip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"compress/gzip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
zipper "git.sa-roci.de/oss/go_zipper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ArchiveExt = ".gz"
|
||||||
|
|
||||||
|
// NewZipper makes a new Zipper.
|
||||||
|
func NewGZipper(gzipFilename string, multiFile bool) zipper.Zipper {
|
||||||
|
if !strings.HasSuffix(strings.ToLower(gzipFilename), ArchiveExt) {
|
||||||
|
gzipFilename += ArchiveExt
|
||||||
|
}
|
||||||
|
gzipper := gZipper{
|
||||||
|
Zipper: zipper.NewBaseZipper(gzipFilename),
|
||||||
|
multiFile: multiFile,
|
||||||
|
}
|
||||||
|
gzipper.SetCompressor(&gzipper)
|
||||||
|
return gzipper
|
||||||
|
}
|
||||||
|
|
||||||
|
type gZipper struct {
|
||||||
|
zipper.Zipper
|
||||||
|
output io.Writer
|
||||||
|
multiFile bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gz *gZipper) OpenWriter(zipFile *os.File) error {
|
||||||
|
gz.SetWriter(gzip.NewWriter(zipFile))
|
||||||
|
gz.output = zipFile
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gz *gZipper) MultiFileSupported() bool {
|
||||||
|
return gz.multiFile
|
||||||
|
}
|
||||||
|
|
||||||
|
func (gz *gZipper) Compress(fileToZip *os.File, pathInArchive string) (err error) {
|
||||||
|
// Get the file information
|
||||||
|
var info fs.FileInfo
|
||||||
|
if info, err = fileToZip.Stat(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := info.Name()
|
||||||
|
|
||||||
|
if writer, ok := gz.GetWriter().(*gzip.Writer); ok {
|
||||||
|
writer.ModTime = info.ModTime()
|
||||||
|
|
||||||
|
// Using FileInfoHeader() above only uses the basename of the file. If we want
|
||||||
|
// to preserve the folder structure we can overwrite this with the full path.
|
||||||
|
if len(pathInArchive) > 0 || strings.Contains(filename, "/") || strings.Contains(filename, "\\") {
|
||||||
|
writer.Name = zipper.PrependPath(filename, pathInArchive)
|
||||||
|
} else {
|
||||||
|
writer.Name = filename
|
||||||
|
}
|
||||||
|
|
||||||
|
if _, err = io.Copy(writer, fileToZip); err == nil {
|
||||||
|
flushErr := writer.Flush()
|
||||||
|
if err = writer.Close(); err != nil {
|
||||||
|
err = flushErr
|
||||||
|
} else {
|
||||||
|
writer.Reset(gz.output)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("unable to access compressor")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
92
gzip/gzip_test.go
Normal file
92
gzip/gzip_test.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package gzip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
content = "test-content"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestZipper(t *testing.T) {
|
||||||
|
archiveFolder := "testdata"
|
||||||
|
archiveFilename := archiveFolder + "/dummy"
|
||||||
|
testFileName := "testZipper.data"
|
||||||
|
if os.WriteFile(testFileName, []byte(content), 0644) == nil {
|
||||||
|
defer os.Remove(testFileName)
|
||||||
|
}
|
||||||
|
z := NewGZipper(archiveFilename, true)
|
||||||
|
if z.GetWriter() != nil {
|
||||||
|
t.Log("Found unexpected writer")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test adding files
|
||||||
|
if z.AddZipFiles([]string{testFileName}, "testfolder") != nil {
|
||||||
|
t.Log("Unable to add a single file")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test adding more files than allowed
|
||||||
|
if z.AddFileToZip(testFileName, "testfolder/more") != nil {
|
||||||
|
t.Log("Unexpectedly failed in adding more than one file")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
z.Close()
|
||||||
|
|
||||||
|
uz := NewGUnzipper(archiveFilename + ArchiveExt)
|
||||||
|
|
||||||
|
if uz.ExtractFiles("") != nil {
|
||||||
|
t.Log("Failed to extract files")
|
||||||
|
t.Fail()
|
||||||
|
} else {
|
||||||
|
if data, err := os.ReadFile("testfolder/" + testFileName); err != nil {
|
||||||
|
t.Log("Expected extracted file #1 is missing")
|
||||||
|
t.Fail()
|
||||||
|
} else if string(data) != content {
|
||||||
|
t.Logf("Unexpected extracted file #1 content: '%s'", string(data))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if data, err := os.ReadFile("testfolder/more/" + testFileName); err != nil {
|
||||||
|
t.Log("Expected extracted file #2 is missing")
|
||||||
|
t.Fail()
|
||||||
|
} else if string(data) != content {
|
||||||
|
t.Logf("Unexpected extracted file #2 content: '%s'", string(data))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
removeRecursive("testfolder")
|
||||||
|
os.Remove(archiveFilename + ArchiveExt)
|
||||||
|
os.Remove(archiveFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeRecursive(path string) {
|
||||||
|
filepath.Walk(path, deleteFiles)
|
||||||
|
filepath.WalkDir(path, deleteFolders)
|
||||||
|
filepath.WalkDir(path, deleteFolders)
|
||||||
|
filepath.WalkDir(path, deleteFolders)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteFiles(path string, info fs.FileInfo, errIn error) (err error) {
|
||||||
|
if errIn != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
err = os.Remove(path)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteFolders(path string, info fs.DirEntry, errIn error) (err error) {
|
||||||
|
if errIn != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.Remove(path)
|
||||||
|
return
|
||||||
|
}
|
||||||
66
gzip/uncompress.go
Normal file
66
gzip/uncompress.go
Normal file
@ -0,0 +1,66 @@
|
|||||||
|
package gzip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"compress/gzip"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
zipper "git.sa-roci.de/oss/go_zipper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewGUnzipper makes a new Unzipper.
|
||||||
|
func NewGUnzipper(zipFilename string) zipper.Unzipper {
|
||||||
|
guz := gUnzipper{
|
||||||
|
Unzipper: zipper.NewBaseUnzipper(zipFilename),
|
||||||
|
}
|
||||||
|
guz.SetDecompressor(&guz)
|
||||||
|
return guz
|
||||||
|
}
|
||||||
|
|
||||||
|
type gUnzipper struct {
|
||||||
|
zipper.Unzipper
|
||||||
|
buffer bytes.Buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
func (guz *gUnzipper) OpenReader(zipFile *os.File) (err error) {
|
||||||
|
var reader *gzip.Reader
|
||||||
|
guz.buffer = bytes.Buffer{}
|
||||||
|
if _, err = io.Copy(&guz.buffer, zipFile); err == nil {
|
||||||
|
if reader, err = gzip.NewReader(&guz.buffer); err == nil {
|
||||||
|
guz.SetReader(reader)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (guz *gUnzipper) Decompress(targetFolderPathInFileSystem string) (err error) {
|
||||||
|
if reader, ok := guz.GetReader().(*gzip.Reader); ok {
|
||||||
|
if err = zipper.MakeSurePathExists(targetFolderPathInFileSystem+"/dummy.file", 0644); err == nil {
|
||||||
|
for {
|
||||||
|
if reader.Name == "" {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
reader.Multistream(false)
|
||||||
|
outputPath := zipper.PrependPath(reader.Name, targetFolderPathInFileSystem)
|
||||||
|
if err = zipper.MakeSurePathExists(outputPath, 0644); err == nil {
|
||||||
|
var dstFile *os.File
|
||||||
|
if dstFile, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0644); err == nil {
|
||||||
|
defer dstFile.Close()
|
||||||
|
if _, err = io.Copy(dstFile, reader); err == nil {
|
||||||
|
if err = reader.Reset(&guz.buffer); err == io.EOF {
|
||||||
|
err = nil
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
28
helper.go
Normal file
28
helper.go
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
package zipper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
func PrependPath(filename, path string) string {
|
||||||
|
if len(path) == 0 {
|
||||||
|
return strings.ReplaceAll(strings.ReplaceAll(filename, "\\", "/"), "//", "/")
|
||||||
|
}
|
||||||
|
return strings.ReplaceAll(strings.ReplaceAll(path+"/"+filename, "\\", "/"), "//", "/")
|
||||||
|
}
|
||||||
|
|
||||||
|
// MakeSurePathExists creates all missing parent directories, given a specific file path.
|
||||||
|
func MakeSurePathExists(filepath string, perm os.FileMode) error {
|
||||||
|
i := len(filepath)
|
||||||
|
for i > 0 && !os.IsPathSeparator(filepath[i-1]) { // Scan backward over element.
|
||||||
|
i--
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
if i > 1 {
|
||||||
|
// Create parent.
|
||||||
|
err = os.MkdirAll(filepath[:i-1], perm)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
99
uncompress.go
Normal file
99
uncompress.go
Normal file
@ -0,0 +1,99 @@
|
|||||||
|
package zipper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
)
|
||||||
|
|
||||||
|
// unzipper defines a struct for a Zip-file writer
|
||||||
|
type unzipper struct {
|
||||||
|
filename string
|
||||||
|
zipFile *os.File
|
||||||
|
zipReader io.Closer
|
||||||
|
isOpen bool
|
||||||
|
decompressor Decompressor
|
||||||
|
}
|
||||||
|
|
||||||
|
type Decompressor interface {
|
||||||
|
Decompress(targetFolderPathInFileSystem string) (err error)
|
||||||
|
OpenReader(zipFile *os.File) (err error)
|
||||||
|
}
|
||||||
|
|
||||||
|
type Unzipper interface {
|
||||||
|
io.Closer
|
||||||
|
ExtractFiles(targetFolderPathInFileSystem string) error
|
||||||
|
SetDecompressor(decompressor Decompressor) error
|
||||||
|
SetReader(reader io.Closer)
|
||||||
|
GetReader() (reader io.Closer)
|
||||||
|
}
|
||||||
|
|
||||||
|
// NewBaseUnzipper makes a new Unzipper interface missing the decompressor.
|
||||||
|
func NewBaseUnzipper(archiveFilename string) Unzipper {
|
||||||
|
return &unzipper{filename: archiveFilename, isOpen: false}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (unzipper *unzipper) SetDecompressor(decompressor Decompressor) (err error) {
|
||||||
|
if decompressor != nil {
|
||||||
|
if unzipper.decompressor == nil {
|
||||||
|
unzipper.decompressor = decompressor
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("decompressor already defined")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("undefined decompressor")
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (unzipper *unzipper) SetReader(reader io.Closer) {
|
||||||
|
unzipper.zipReader = reader
|
||||||
|
}
|
||||||
|
|
||||||
|
func (unzipper *unzipper) GetReader() io.Closer {
|
||||||
|
return unzipper.zipReader
|
||||||
|
}
|
||||||
|
|
||||||
|
// Close implements the Closer interface
|
||||||
|
func (unzipper *unzipper) Close() (err error) {
|
||||||
|
if unzipper.isOpen {
|
||||||
|
if nil != unzipper.zipReader {
|
||||||
|
if err = unzipper.zipReader.Close(); err == nil {
|
||||||
|
unzipper.zipReader = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if nil == err && nil != unzipper.zipFile {
|
||||||
|
if err = unzipper.zipFile.Close(); err == nil {
|
||||||
|
unzipper.zipFile = nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
unzipper.isOpen = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (unzipper *unzipper) ExtractFiles(targetFolderPathInFileSystem string) (err error) {
|
||||||
|
if unzipper.decompressor == nil {
|
||||||
|
return fmt.Errorf("unable to initialise decompressor")
|
||||||
|
}
|
||||||
|
if !unzipper.isOpen {
|
||||||
|
if unzipper.zipFile, err = os.Open(unzipper.filename); err == nil {
|
||||||
|
err = unzipper.decompressor.OpenReader(unzipper.zipFile)
|
||||||
|
unzipper.isOpen = true
|
||||||
|
defer func() {
|
||||||
|
errClose := unzipper.Close()
|
||||||
|
if err == nil {
|
||||||
|
err = errClose
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err == nil {
|
||||||
|
err = unzipper.decompressor.Decompress(targetFolderPathInFileSystem)
|
||||||
|
}
|
||||||
|
|
||||||
|
return err
|
||||||
|
}
|
||||||
77
zip/compress.go
Normal file
77
zip/compress.go
Normal file
@ -0,0 +1,77 @@
|
|||||||
|
package zip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"git.sa-roci.de/oss/go_zipper"
|
||||||
|
)
|
||||||
|
|
||||||
|
const ArchiveExt = ".zip"
|
||||||
|
|
||||||
|
// NewZipper makes a new Zipper.
|
||||||
|
func NewZipper(zipFilename string) zipper.Zipper {
|
||||||
|
if !strings.HasSuffix(strings.ToLower(zipFilename), ArchiveExt) {
|
||||||
|
zipFilename += ArchiveExt
|
||||||
|
}
|
||||||
|
z := zZipper{
|
||||||
|
Zipper: zipper.NewBaseZipper(zipFilename),
|
||||||
|
}
|
||||||
|
z.SetCompressor(z)
|
||||||
|
return z
|
||||||
|
}
|
||||||
|
|
||||||
|
type zZipper struct {
|
||||||
|
zipper.Zipper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z zZipper) OpenWriter(zipFile *os.File) error {
|
||||||
|
z.SetWriter(zip.NewWriter(zipFile))
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z zZipper) MultiFileSupported() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (z zZipper) Compress(fileToZip *os.File, pathInArchive string) (err error) {
|
||||||
|
// Get the file information
|
||||||
|
var info fs.FileInfo
|
||||||
|
if info, err = fileToZip.Stat(); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var header *zip.FileHeader
|
||||||
|
if header, err = zip.FileInfoHeader(info); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
filename := info.Name()
|
||||||
|
|
||||||
|
// Using FileInfoHeader() above only uses the basename of the file. If we want
|
||||||
|
// to preserve the folder structure we can overwrite this with the full path.
|
||||||
|
if len(pathInArchive) > 0 || strings.Contains(filename, "/") || strings.Contains(filename, "\\") {
|
||||||
|
header.Name = zipper.PrependPath(filename, pathInArchive)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Change to deflate to gain better compression
|
||||||
|
// see http://golang.org/pkg/archive/zip/#pkg-constants
|
||||||
|
header.Method = zip.Deflate
|
||||||
|
|
||||||
|
if zipWriter, ok := z.GetWriter().(*zip.Writer); ok {
|
||||||
|
var writer io.Writer
|
||||||
|
if writer, err = zipWriter.CreateHeader(header); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
_, err = io.Copy(writer, fileToZip)
|
||||||
|
} else {
|
||||||
|
err = fmt.Errorf("unable to access compressor")
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
60
zip/uncompress.go
Normal file
60
zip/uncompress.go
Normal file
@ -0,0 +1,60 @@
|
|||||||
|
package zip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"archive/zip"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"git.sa-roci.de/oss/go_zipper"
|
||||||
|
)
|
||||||
|
|
||||||
|
// NewUnzipper makes a new Unzipper.
|
||||||
|
func NewUnzipper(zipFilename string) zipper.Unzipper {
|
||||||
|
uz := zUnzipper{
|
||||||
|
Unzipper: zipper.NewBaseUnzipper(zipFilename),
|
||||||
|
}
|
||||||
|
uz.SetDecompressor(uz)
|
||||||
|
return uz
|
||||||
|
}
|
||||||
|
|
||||||
|
type zUnzipper struct {
|
||||||
|
zipper.Unzipper
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uz zUnzipper) OpenReader(zipFile *os.File) (err error) {
|
||||||
|
var reader *zip.ReadCloser
|
||||||
|
if reader, err = zip.OpenReader(zipFile.Name()); err == nil {
|
||||||
|
uz.SetReader(reader)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (uz zUnzipper) Decompress(targetFolderPathInFileSystem string) (err error) {
|
||||||
|
if zipReader, ok := uz.GetReader().(*zip.ReadCloser); ok {
|
||||||
|
if err = zipper.MakeSurePathExists(targetFolderPathInFileSystem+"/dummy.file", 0644); err == nil {
|
||||||
|
for _, entry := range zipReader.File {
|
||||||
|
outputPath := zipper.PrependPath(entry.Name, targetFolderPathInFileSystem)
|
||||||
|
if !entry.FileInfo().IsDir() {
|
||||||
|
if err = zipper.MakeSurePathExists(outputPath, 0644); err == nil {
|
||||||
|
var dstFile *os.File
|
||||||
|
if dstFile, err = os.OpenFile(outputPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, entry.Mode()); err == nil {
|
||||||
|
defer dstFile.Close()
|
||||||
|
var archivedFileData io.ReadCloser
|
||||||
|
if archivedFileData, err = entry.Open(); err == nil {
|
||||||
|
defer archivedFileData.Close()
|
||||||
|
_, err = io.Copy(dstFile, archivedFileData)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = zipper.MakeSurePathExists(outputPath+"/dummy.file", 0644)
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
92
zip/zip_test.go
Normal file
92
zip/zip_test.go
Normal file
@ -0,0 +1,92 @@
|
|||||||
|
package zip
|
||||||
|
|
||||||
|
import (
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
content = "test-content"
|
||||||
|
)
|
||||||
|
|
||||||
|
func TestZipper(t *testing.T) {
|
||||||
|
archiveFolder := "testdata"
|
||||||
|
archiveFilename := archiveFolder + "/dummy"
|
||||||
|
testFileName := "testZipper.data"
|
||||||
|
if os.WriteFile(testFileName, []byte(content), 0644) == nil {
|
||||||
|
defer os.Remove(testFileName)
|
||||||
|
}
|
||||||
|
z := NewZipper(archiveFilename)
|
||||||
|
if z.GetWriter() != nil {
|
||||||
|
t.Log("Found unexpected writer")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test adding files
|
||||||
|
if z.AddZipFiles([]string{testFileName}, "testfolder") != nil {
|
||||||
|
t.Log("Unable to add a single file")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test adding more files than allowed
|
||||||
|
if z.AddFileToZip(testFileName, "testfolder/more") != nil {
|
||||||
|
t.Log("Unexpectedly failed in adding more than one file")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
z.Close()
|
||||||
|
|
||||||
|
uz := NewUnzipper(archiveFilename + ArchiveExt)
|
||||||
|
|
||||||
|
if uz.ExtractFiles("") != nil {
|
||||||
|
t.Log("Failed to extract files")
|
||||||
|
t.Fail()
|
||||||
|
} else {
|
||||||
|
if data, err := os.ReadFile("testfolder/" + testFileName); err != nil {
|
||||||
|
t.Log("Expected extracted file #1 is missing")
|
||||||
|
t.Fail()
|
||||||
|
} else if string(data) != content {
|
||||||
|
t.Logf("Unexpected extracted file #1 content: '%s'", string(data))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if data, err := os.ReadFile("testfolder/more/" + testFileName); err != nil {
|
||||||
|
t.Log("Expected extracted file #2 is missing")
|
||||||
|
t.Fail()
|
||||||
|
} else if string(data) != content {
|
||||||
|
t.Logf("Unexpected extracted file #2 content: '%s'", string(data))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
removeRecursive("testfolder")
|
||||||
|
os.Remove(archiveFilename + ArchiveExt)
|
||||||
|
os.Remove(archiveFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
func removeRecursive(path string) {
|
||||||
|
filepath.Walk(path, deleteFiles)
|
||||||
|
filepath.WalkDir(path, deleteFolders)
|
||||||
|
filepath.WalkDir(path, deleteFolders)
|
||||||
|
filepath.WalkDir(path, deleteFolders)
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteFiles(path string, info fs.FileInfo, errIn error) (err error) {
|
||||||
|
if errIn != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if !info.IsDir() {
|
||||||
|
err = os.Remove(path)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteFolders(path string, info fs.DirEntry, errIn error) (err error) {
|
||||||
|
if errIn != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
os.Remove(path)
|
||||||
|
return
|
||||||
|
}
|
||||||
9
zipper.code-workspace
Normal file
9
zipper.code-workspace
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
{
|
||||||
|
"folders": [
|
||||||
|
{
|
||||||
|
"name": "git.sa-roci.de/oss/go_zipper",
|
||||||
|
"path": "."
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"settings": {}
|
||||||
|
}
|
||||||
217
zipper_test.go
Normal file
217
zipper_test.go
Normal file
@ -0,0 +1,217 @@
|
|||||||
|
package zipper
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"testing"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
content = "test-content"
|
||||||
|
)
|
||||||
|
|
||||||
|
type dummyZipper struct {
|
||||||
|
Zipper
|
||||||
|
buffer map[string][]byte
|
||||||
|
multi bool
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dz dummyZipper) Flush() error {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dz *dummyZipper) Close() error {
|
||||||
|
if dz.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
dz.closed = true
|
||||||
|
return dz.Zipper.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dz dummyZipper) Compress(fileToZip *os.File, pathInArchive string) (err error) {
|
||||||
|
if dz.closed {
|
||||||
|
return fmt.Errorf("compressor closed")
|
||||||
|
}
|
||||||
|
buf := bytes.Buffer{}
|
||||||
|
if _, err = io.Copy(&buf, fileToZip); err == nil {
|
||||||
|
dz.buffer[PrependPath(fileToZip.Name(), pathInArchive)] = buf.Bytes()
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dz dummyZipper) OpenWriter(zipFile *os.File) (err error) {
|
||||||
|
dz.SetWriter(&dz)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (dz dummyZipper) MultiFileSupported() (multi bool) {
|
||||||
|
return dz.multi
|
||||||
|
}
|
||||||
|
|
||||||
|
func newZipper(archiveFilename string, multi bool) dummyZipper {
|
||||||
|
dz := dummyZipper{
|
||||||
|
Zipper: NewBaseZipper(archiveFilename),
|
||||||
|
buffer: make(map[string][]byte),
|
||||||
|
multi: multi,
|
||||||
|
closed: false,
|
||||||
|
}
|
||||||
|
dz.SetCompressor(dz)
|
||||||
|
return dz
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestZipper(t *testing.T) {
|
||||||
|
archiveFolder := "testdata"
|
||||||
|
archiveFilename := archiveFolder + "/dummy.arc"
|
||||||
|
testFileName := "testZipper.data"
|
||||||
|
if os.WriteFile(testFileName, []byte(content), 0644) == nil {
|
||||||
|
defer os.Remove(testFileName)
|
||||||
|
}
|
||||||
|
dz := newZipper(archiveFilename, false)
|
||||||
|
if dz.GetWriter() != nil {
|
||||||
|
t.Log("Found unexpected writer")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test adding files
|
||||||
|
if dz.AddZipFiles([]string{testFileName}, "testfolder") != nil {
|
||||||
|
t.Log("Unable to add a single file")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if fileContent, ok := dz.buffer["testfolder/"+testFileName]; ok {
|
||||||
|
if string(fileContent) != content {
|
||||||
|
t.Logf("Unexpected testfile content: '%s'", string(fileContent))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
t.Log("Failed to create appropriate subfolder in archive")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test modification of compressor
|
||||||
|
if dz.SetCompressor(dz) == nil {
|
||||||
|
t.Log("Unexpectedly succeeded in changing the compressor interface")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if dz.SetCompressor(nil) == nil {
|
||||||
|
t.Log("Unexpectedly succeeded in removing the compressor interface")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test adding more files than allowed
|
||||||
|
if dz.AddFileToZip(testFileName, "badFolder") == nil {
|
||||||
|
t.Log("Unexpectedly succeeded in adding more than one file")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
dz.Close()
|
||||||
|
os.Remove(archiveFilename)
|
||||||
|
os.Remove(archiveFolder)
|
||||||
|
}
|
||||||
|
|
||||||
|
type dummyUnzipper struct {
|
||||||
|
Unzipper
|
||||||
|
buffer map[string][]byte
|
||||||
|
closed bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (duz *dummyUnzipper) Close() error {
|
||||||
|
if duz.closed {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
duz.closed = true
|
||||||
|
return duz.Unzipper.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (duz *dummyUnzipper) Decompress(targetFolderPathInFileSystem string) (err error) {
|
||||||
|
if duz.buffer == nil {
|
||||||
|
return fmt.Errorf("nothing to decompress")
|
||||||
|
}
|
||||||
|
for path, data := range duz.buffer {
|
||||||
|
outputPath := PrependPath(path, targetFolderPathInFileSystem)
|
||||||
|
if err = MakeSurePathExists(outputPath, 0644); err == nil {
|
||||||
|
if err = os.WriteFile(outputPath, data, 0644); err != nil {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (duz *dummyUnzipper) OpenReader(zipFile *os.File) (err error) {
|
||||||
|
var binary []byte
|
||||||
|
if binary, err = io.ReadAll(zipFile); err == nil {
|
||||||
|
duz.SetReader(duz)
|
||||||
|
err = json.Unmarshal(binary, &duz.buffer)
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func newUnzipper(archiveFilename string) dummyUnzipper {
|
||||||
|
duz := dummyUnzipper{
|
||||||
|
Unzipper: NewBaseUnzipper(archiveFilename),
|
||||||
|
buffer: make(map[string][]byte),
|
||||||
|
}
|
||||||
|
duz.SetDecompressor(&duz)
|
||||||
|
return duz
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestUnzipper(t *testing.T) {
|
||||||
|
archiveFolder := "testdata2"
|
||||||
|
archiveFilename := archiveFolder + "/dummy.arc"
|
||||||
|
data := make(map[string][]byte)
|
||||||
|
contentFolder := "extractedTest"
|
||||||
|
contentFilename := contentFolder + "/dummy.file"
|
||||||
|
data[contentFilename] = []byte(content)
|
||||||
|
if MakeSurePathExists(archiveFilename, 0644) == nil {
|
||||||
|
if archiveContent, err := json.Marshal(data); err == nil {
|
||||||
|
if os.WriteFile(archiveFilename, archiveContent, 0644) == nil {
|
||||||
|
defer func() {
|
||||||
|
os.Remove(archiveFilename)
|
||||||
|
os.Remove(archiveFolder)
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
duz := newUnzipper(archiveFilename)
|
||||||
|
if duz.GetReader() != nil {
|
||||||
|
t.Log("Found unexpected reader")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test modification of decompressor
|
||||||
|
if duz.SetDecompressor(&duz) == nil {
|
||||||
|
t.Log("Unexpectedly succeeded in changing the decompressor interface")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
if duz.SetDecompressor(nil) == nil {
|
||||||
|
t.Log("Unexpectedly succeeded in removing the decompressor interface")
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Test decompression
|
||||||
|
if duz.ExtractFiles("") != nil {
|
||||||
|
t.Log("Decompression failed")
|
||||||
|
t.Fail()
|
||||||
|
} else {
|
||||||
|
if data, err := os.ReadFile(contentFilename); err != nil {
|
||||||
|
t.Log("Expected extracted file is missing")
|
||||||
|
t.Fail()
|
||||||
|
} else if string(data) != content {
|
||||||
|
t.Logf("Unexpected extracted file content: '%s'", string(data))
|
||||||
|
t.Fail()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Clean up
|
||||||
|
duz.Close()
|
||||||
|
os.Remove(contentFilename)
|
||||||
|
os.Remove(contentFolder)
|
||||||
|
}
|
||||||
Loading…
x
Reference in New Issue
Block a user