mirror of
				https://github.com/1Panel-dev/1Panel.git
				synced 2025-10-26 16:56:22 +08:00 
			
		
		
		
	
		
			
				
	
	
		
			620 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
			
		
		
	
	
			620 lines
		
	
	
	
		
			13 KiB
		
	
	
	
		
			Go
		
	
	
	
	
	
| package files
 | |
| 
 | |
| import (
 | |
| 	"archive/zip"
 | |
| 	"bufio"
 | |
| 	"context"
 | |
| 	"encoding/json"
 | |
| 	"fmt"
 | |
| 	"github.com/1Panel-dev/1Panel/backend/utils/cmd"
 | |
| 	http2 "github.com/1Panel-dev/1Panel/backend/utils/http"
 | |
| 	cZip "github.com/klauspost/compress/zip"
 | |
| 	"golang.org/x/text/encoding/simplifiedchinese"
 | |
| 	"golang.org/x/text/transform"
 | |
| 	"io"
 | |
| 	"io/fs"
 | |
| 	"net/http"
 | |
| 	"os"
 | |
| 	"path"
 | |
| 	"path/filepath"
 | |
| 	"strconv"
 | |
| 	"strings"
 | |
| 	"sync"
 | |
| 	"time"
 | |
| 
 | |
| 	"github.com/1Panel-dev/1Panel/backend/global"
 | |
| 	"github.com/mholt/archiver/v4"
 | |
| 	"github.com/pkg/errors"
 | |
| 	"github.com/spf13/afero"
 | |
| )
 | |
| 
 | |
| type FileOp struct {
 | |
| 	Fs afero.Fs
 | |
| }
 | |
| 
 | |
| func NewFileOp() FileOp {
 | |
| 	return FileOp{
 | |
| 		Fs: afero.NewOsFs(),
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (f FileOp) OpenFile(dst string) (fs.File, error) {
 | |
| 	return f.Fs.Open(dst)
 | |
| }
 | |
| 
 | |
| func (f FileOp) GetContent(dst string) ([]byte, error) {
 | |
| 	afs := &afero.Afero{Fs: f.Fs}
 | |
| 	cByte, err := afs.ReadFile(dst)
 | |
| 	if err != nil {
 | |
| 		return nil, err
 | |
| 	}
 | |
| 	return cByte, nil
 | |
| }
 | |
| 
 | |
| func (f FileOp) CreateDir(dst string, mode fs.FileMode) error {
 | |
| 	return f.Fs.MkdirAll(dst, mode)
 | |
| }
 | |
| 
 | |
| func (f FileOp) CreateFile(dst string) error {
 | |
| 	if _, err := f.Fs.Create(dst); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f FileOp) LinkFile(source string, dst string, isSymlink bool) error {
 | |
| 	if isSymlink {
 | |
| 		osFs := afero.OsFs{}
 | |
| 		return osFs.SymlinkIfPossible(source, dst)
 | |
| 	} else {
 | |
| 		return os.Link(source, dst)
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (f FileOp) DeleteDir(dst string) error {
 | |
| 	return f.Fs.RemoveAll(dst)
 | |
| }
 | |
| 
 | |
| func (f FileOp) Stat(dst string) bool {
 | |
| 	info, _ := f.Fs.Stat(dst)
 | |
| 	return info != nil
 | |
| }
 | |
| 
 | |
| func (f FileOp) DeleteFile(dst string) error {
 | |
| 	return f.Fs.Remove(dst)
 | |
| }
 | |
| 
 | |
| func (f FileOp) WriteFile(dst string, in io.Reader, mode fs.FileMode) error {
 | |
| 	file, err := f.Fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	defer file.Close()
 | |
| 
 | |
| 	if _, err = io.Copy(file, in); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	if _, err = file.Stat(); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f FileOp) SaveFile(dst string, content string, mode fs.FileMode) error {
 | |
| 	if !f.Stat(path.Dir(dst)) {
 | |
| 		_ = f.CreateDir(path.Dir(dst), mode.Perm())
 | |
| 	}
 | |
| 	file, err := f.Fs.OpenFile(dst, os.O_RDWR|os.O_CREATE|os.O_TRUNC, mode)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	defer file.Close()
 | |
| 	write := bufio.NewWriter(file)
 | |
| 	_, _ = write.WriteString(content)
 | |
| 	write.Flush()
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f FileOp) Chmod(dst string, mode fs.FileMode) error {
 | |
| 	return f.Fs.Chmod(dst, mode)
 | |
| }
 | |
| 
 | |
| func (f FileOp) Chown(dst string, uid int, gid int) error {
 | |
| 	return f.Fs.Chown(dst, uid, gid)
 | |
| }
 | |
| 
 | |
| func (f FileOp) ChownR(dst string, uid string, gid string, sub bool) error {
 | |
| 	cmdStr := fmt.Sprintf("chown %s:%s %s", uid, gid, dst)
 | |
| 	if sub {
 | |
| 		cmdStr = fmt.Sprintf("chown -R %s:%s %s", uid, gid, dst)
 | |
| 	}
 | |
| 	if cmd.HasNoPasswordSudo() {
 | |
| 		cmdStr = fmt.Sprintf("sudo %s", cmdStr)
 | |
| 	}
 | |
| 	if msg, err := cmd.ExecWithTimeOut(cmdStr, 2*time.Second); err != nil {
 | |
| 		if msg != "" {
 | |
| 			return errors.New(msg)
 | |
| 		}
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f FileOp) ChmodR(dst string, mode int64) error {
 | |
| 	cmdStr := fmt.Sprintf("chmod -R %v %s", fmt.Sprintf("%04o", mode), dst)
 | |
| 	if cmd.HasNoPasswordSudo() {
 | |
| 		cmdStr = fmt.Sprintf("sudo %s", cmdStr)
 | |
| 	}
 | |
| 	if msg, err := cmd.ExecWithTimeOut(cmdStr, 2*time.Second); err != nil {
 | |
| 		if msg != "" {
 | |
| 			return errors.New(msg)
 | |
| 		}
 | |
| 		return err
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f FileOp) Rename(oldName string, newName string) error {
 | |
| 	return f.Fs.Rename(oldName, newName)
 | |
| }
 | |
| 
 | |
| type WriteCounter struct {
 | |
| 	Total   uint64
 | |
| 	Written uint64
 | |
| 	Key     string
 | |
| 	Name    string
 | |
| }
 | |
| 
 | |
| type Process struct {
 | |
| 	Total   uint64  `json:"total"`
 | |
| 	Written uint64  `json:"written"`
 | |
| 	Percent float64 `json:"percent"`
 | |
| 	Name    string  `json:"name"`
 | |
| }
 | |
| 
 | |
| func (w *WriteCounter) Write(p []byte) (n int, err error) {
 | |
| 	n = len(p)
 | |
| 	w.Written += uint64(n)
 | |
| 	w.SaveProcess()
 | |
| 	return n, nil
 | |
| }
 | |
| 
 | |
| func (w *WriteCounter) SaveProcess() {
 | |
| 	percentValue := 0.0
 | |
| 	if w.Total > 0 {
 | |
| 		percent := float64(w.Written) / float64(w.Total) * 100
 | |
| 		percentValue, _ = strconv.ParseFloat(fmt.Sprintf("%.2f", percent), 64)
 | |
| 	}
 | |
| 	process := Process{
 | |
| 		Total:   w.Total,
 | |
| 		Written: w.Written,
 | |
| 		Percent: percentValue,
 | |
| 		Name:    w.Name,
 | |
| 	}
 | |
| 	by, _ := json.Marshal(process)
 | |
| 	if percentValue < 100 {
 | |
| 		if err := global.CACHE.Set(w.Key, string(by)); err != nil {
 | |
| 			global.LOG.Errorf("save cache error, err %s", err.Error())
 | |
| 		}
 | |
| 	} else {
 | |
| 		if err := global.CACHE.SetWithTTL(w.Key, string(by), time.Second*time.Duration(10)); err != nil {
 | |
| 			global.LOG.Errorf("save cache error, err %s", err.Error())
 | |
| 		}
 | |
| 	}
 | |
| }
 | |
| 
 | |
| func (f FileOp) DownloadFileWithProcess(url, dst, key string) error {
 | |
| 	client := &http.Client{}
 | |
| 	request, err := http.NewRequest("GET", url, nil)
 | |
| 	if err != nil {
 | |
| 		return nil
 | |
| 	}
 | |
| 	request.Header.Set("Accept-Encoding", "identity")
 | |
| 	resp, err := client.Do(request)
 | |
| 	if err != nil {
 | |
| 		global.LOG.Errorf("get download file [%s] error, err %s", dst, err.Error())
 | |
| 		return err
 | |
| 	}
 | |
| 	out, err := os.Create(dst)
 | |
| 	if err != nil {
 | |
| 		global.LOG.Errorf("create download file [%s] error, err %s", dst, err.Error())
 | |
| 		return err
 | |
| 	}
 | |
| 	go func() {
 | |
| 		counter := &WriteCounter{}
 | |
| 		counter.Key = key
 | |
| 		if resp.ContentLength > 0 {
 | |
| 			counter.Total = uint64(resp.ContentLength)
 | |
| 		}
 | |
| 		counter.Name = filepath.Base(dst)
 | |
| 		if _, err = io.Copy(out, io.TeeReader(resp.Body, counter)); err != nil {
 | |
| 			global.LOG.Errorf("save download file [%s] error, err %s", dst, err.Error())
 | |
| 		}
 | |
| 		out.Close()
 | |
| 		resp.Body.Close()
 | |
| 
 | |
| 		value, err := global.CACHE.Get(counter.Key)
 | |
| 		if err != nil {
 | |
| 			global.LOG.Errorf("get cache error,err %s", err.Error())
 | |
| 			return
 | |
| 		}
 | |
| 		process := &Process{}
 | |
| 		_ = json.Unmarshal(value, process)
 | |
| 		process.Percent = 100
 | |
| 		process.Name = counter.Name
 | |
| 		process.Total = process.Written
 | |
| 		by, _ := json.Marshal(process)
 | |
| 		if err := global.CACHE.SetWithTTL(counter.Key, string(by), time.Second*time.Duration(10)); err != nil {
 | |
| 			global.LOG.Errorf("save cache error, err %s", err.Error())
 | |
| 		}
 | |
| 	}()
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f FileOp) DownloadFile(url, dst string) error {
 | |
| 	resp, err := http2.GetHttpRes(url)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	out, err := os.Create(dst)
 | |
| 	if err != nil {
 | |
| 		return fmt.Errorf("create download file [%s] error, err %s", dst, err.Error())
 | |
| 	}
 | |
| 	defer out.Close()
 | |
| 
 | |
| 	if _, err = io.Copy(out, resp.Body); err != nil {
 | |
| 		return fmt.Errorf("save download file [%s] error, err %s", dst, err.Error())
 | |
| 	}
 | |
| 	out.Close()
 | |
| 	resp.Body.Close()
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f FileOp) Cut(oldPaths []string, dst string) error {
 | |
| 	for _, p := range oldPaths {
 | |
| 		base := filepath.Base(p)
 | |
| 		dstPath := filepath.Join(dst, base)
 | |
| 		if err := f.Fs.Rename(p, dstPath); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f FileOp) Copy(src, dst string) error {
 | |
| 	if src = path.Clean("/" + src); src == "" {
 | |
| 		return os.ErrNotExist
 | |
| 	}
 | |
| 	if dst = path.Clean("/" + dst); dst == "" {
 | |
| 		return os.ErrNotExist
 | |
| 	}
 | |
| 	if src == "/" || dst == "/" {
 | |
| 		return os.ErrInvalid
 | |
| 	}
 | |
| 	if dst == src {
 | |
| 		return os.ErrInvalid
 | |
| 	}
 | |
| 	info, err := f.Fs.Stat(src)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if info.IsDir() {
 | |
| 		return f.CopyDir(src, dst)
 | |
| 	}
 | |
| 	return f.CopyFile(src, dst)
 | |
| }
 | |
| 
 | |
| func (f FileOp) CopyDir(src, dst string) error {
 | |
| 	srcInfo, err := f.Fs.Stat(src)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	dstDir := filepath.Join(dst, srcInfo.Name())
 | |
| 	if err := f.Fs.MkdirAll(dstDir, srcInfo.Mode()); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	dir, _ := f.Fs.Open(src)
 | |
| 	obs, err := dir.Readdir(-1)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	var errs []error
 | |
| 
 | |
| 	for _, obj := range obs {
 | |
| 		fSrc := filepath.Join(src, obj.Name())
 | |
| 		if obj.IsDir() {
 | |
| 			err = f.CopyDir(fSrc, dstDir)
 | |
| 			if err != nil {
 | |
| 				errs = append(errs, err)
 | |
| 			}
 | |
| 		} else {
 | |
| 			err = f.CopyFile(fSrc, dstDir)
 | |
| 			if err != nil {
 | |
| 				errs = append(errs, err)
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 
 | |
| 	var errString string
 | |
| 	for _, err := range errs {
 | |
| 		errString += err.Error() + "\n"
 | |
| 	}
 | |
| 
 | |
| 	if errString != "" {
 | |
| 		return errors.New(errString)
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f FileOp) CopyFile(src, dst string) error {
 | |
| 	srcFile, err := f.Fs.Open(src)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	defer srcFile.Close()
 | |
| 
 | |
| 	srcInfo, err := f.Fs.Stat(src)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	dstPath := path.Join(dst, srcInfo.Name())
 | |
| 	if src == dstPath {
 | |
| 		return nil
 | |
| 	}
 | |
| 
 | |
| 	err = f.Fs.MkdirAll(filepath.Dir(dst), 0666)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	dstFile, err := f.Fs.OpenFile(dstPath, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0775)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	defer dstFile.Close()
 | |
| 
 | |
| 	if _, err = io.Copy(dstFile, srcFile); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	info, err := f.Fs.Stat(src)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	if err = f.Fs.Chmod(dstFile.Name(), info.Mode()); err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func (f FileOp) GetDirSize(path string) (float64, error) {
 | |
| 	var m sync.Map
 | |
| 	var wg sync.WaitGroup
 | |
| 
 | |
| 	wg.Add(1)
 | |
| 	go ScanDir(f.Fs, path, &m, &wg)
 | |
| 	wg.Wait()
 | |
| 
 | |
| 	var dirSize float64
 | |
| 	m.Range(func(k, v interface{}) bool {
 | |
| 		dirSize = dirSize + v.(float64)
 | |
| 		return true
 | |
| 	})
 | |
| 
 | |
| 	return dirSize, nil
 | |
| }
 | |
| 
 | |
| type CompressType string
 | |
| 
 | |
| const (
 | |
| 	Zip   CompressType = "zip"
 | |
| 	Gz    CompressType = "gz"
 | |
| 	Bz2   CompressType = "bz2"
 | |
| 	Tar   CompressType = "tar"
 | |
| 	TarGz CompressType = "tar.gz"
 | |
| 	Xz    CompressType = "xz"
 | |
| )
 | |
| 
 | |
| func getFormat(cType CompressType) archiver.CompressedArchive {
 | |
| 
 | |
| 	format := archiver.CompressedArchive{}
 | |
| 	switch cType {
 | |
| 	case Tar:
 | |
| 		format.Archival = archiver.Tar{}
 | |
| 	case TarGz, Gz:
 | |
| 		format.Compression = archiver.Gz{}
 | |
| 		format.Archival = archiver.Tar{}
 | |
| 	case Zip:
 | |
| 		format.Archival = archiver.Zip{
 | |
| 			Compression: zip.Deflate,
 | |
| 		}
 | |
| 	case Bz2:
 | |
| 		format.Compression = archiver.Bz2{}
 | |
| 		format.Archival = archiver.Tar{}
 | |
| 	case Xz:
 | |
| 		format.Compression = archiver.Xz{}
 | |
| 		format.Archival = archiver.Tar{}
 | |
| 	}
 | |
| 	return format
 | |
| }
 | |
| 
 | |
| func (f FileOp) Compress(srcRiles []string, dst string, name string, cType CompressType) error {
 | |
| 	format := getFormat(cType)
 | |
| 
 | |
| 	fileMaps := make(map[string]string, len(srcRiles))
 | |
| 	for _, s := range srcRiles {
 | |
| 		base := filepath.Base(s)
 | |
| 		fileMaps[s] = base
 | |
| 	}
 | |
| 
 | |
| 	if !f.Stat(dst) {
 | |
| 		_ = f.CreateDir(dst, 0755)
 | |
| 	}
 | |
| 
 | |
| 	files, err := archiver.FilesFromDisk(nil, fileMaps)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	dstFile := filepath.Join(dst, name)
 | |
| 	out, err := f.Fs.Create(dstFile)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 
 | |
| 	switch cType {
 | |
| 	case Zip:
 | |
| 		if err := ZipFile(files, out); err != nil {
 | |
| 			_ = f.DeleteFile(dstFile)
 | |
| 			return err
 | |
| 		}
 | |
| 	default:
 | |
| 		err = format.Archive(context.Background(), out, files)
 | |
| 		if err != nil {
 | |
| 			_ = f.DeleteFile(dstFile)
 | |
| 			return err
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 | |
| 
 | |
| func isIgnoreFile(name string) bool {
 | |
| 	return strings.HasPrefix(name, "__MACOSX") || strings.HasSuffix(name, ".DS_Store") || strings.HasPrefix(name, "._")
 | |
| }
 | |
| 
 | |
| func decodeGBK(input string) (string, error) {
 | |
| 	decoder := simplifiedchinese.GBK.NewDecoder()
 | |
| 	decoded, _, err := transform.String(decoder, input)
 | |
| 	if err != nil {
 | |
| 		return "", err
 | |
| 	}
 | |
| 	return decoded, nil
 | |
| }
 | |
| 
 | |
| func (f FileOp) Decompress(srcFile string, dst string, cType CompressType) error {
 | |
| 	format := getFormat(cType)
 | |
| 
 | |
| 	handler := func(ctx context.Context, archFile archiver.File) error {
 | |
| 		info := archFile.FileInfo
 | |
| 		if isIgnoreFile(archFile.Name()) {
 | |
| 			return nil
 | |
| 		}
 | |
| 		fileName := archFile.NameInArchive
 | |
| 		var err error
 | |
| 		if header, ok := archFile.Header.(cZip.FileHeader); ok {
 | |
| 			if header.NonUTF8 && header.Flags == 0 {
 | |
| 				fileName, err = decodeGBK(fileName)
 | |
| 				if err != nil {
 | |
| 					return err
 | |
| 				}
 | |
| 			}
 | |
| 		}
 | |
| 		filePath := filepath.Join(dst, fileName)
 | |
| 		if archFile.FileInfo.IsDir() {
 | |
| 			if err := f.Fs.MkdirAll(filePath, info.Mode()); err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			return nil
 | |
| 		}
 | |
| 		fr, err := archFile.Open()
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer fr.Close()
 | |
| 		fw, err := f.Fs.OpenFile(filePath, os.O_CREATE|os.O_RDWR|os.O_TRUNC, info.Mode())
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		defer fw.Close()
 | |
| 		if _, err := io.Copy(fw, fr); err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 
 | |
| 		return nil
 | |
| 	}
 | |
| 	input, err := f.Fs.Open(srcFile)
 | |
| 	if err != nil {
 | |
| 		return err
 | |
| 	}
 | |
| 	return format.Extract(context.Background(), input, nil, handler)
 | |
| }
 | |
| 
 | |
| func (f FileOp) Backup(srcFile string) (string, error) {
 | |
| 	backupPath := srcFile + "_bak"
 | |
| 	info, _ := f.Fs.Stat(backupPath)
 | |
| 	if info != nil {
 | |
| 		if info.IsDir() {
 | |
| 			_ = f.DeleteDir(backupPath)
 | |
| 		} else {
 | |
| 			_ = f.DeleteFile(backupPath)
 | |
| 		}
 | |
| 	}
 | |
| 	if err := f.Rename(srcFile, backupPath); err != nil {
 | |
| 		return backupPath, err
 | |
| 	}
 | |
| 
 | |
| 	return backupPath, nil
 | |
| }
 | |
| 
 | |
| func (f FileOp) CopyAndBackup(src string) (string, error) {
 | |
| 	backupPath := src + "_bak"
 | |
| 	info, _ := f.Fs.Stat(backupPath)
 | |
| 	if info != nil {
 | |
| 		if info.IsDir() {
 | |
| 			_ = f.DeleteDir(backupPath)
 | |
| 		} else {
 | |
| 			_ = f.DeleteFile(backupPath)
 | |
| 		}
 | |
| 	}
 | |
| 	_ = f.CreateDir(backupPath, 0755)
 | |
| 	if err := f.Copy(src, backupPath); err != nil {
 | |
| 		return backupPath, err
 | |
| 	}
 | |
| 	return backupPath, nil
 | |
| }
 | |
| 
 | |
| func ZipFile(files []archiver.File, dst afero.File) error {
 | |
| 	zw := zip.NewWriter(dst)
 | |
| 	defer zw.Close()
 | |
| 
 | |
| 	for _, file := range files {
 | |
| 		hdr, err := zip.FileInfoHeader(file)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		hdr.Name = file.NameInArchive
 | |
| 		if file.IsDir() {
 | |
| 			if !strings.HasSuffix(hdr.Name, "/") {
 | |
| 				hdr.Name += "/"
 | |
| 			}
 | |
| 			hdr.Method = zip.Store
 | |
| 		}
 | |
| 		w, err := zw.CreateHeader(hdr)
 | |
| 		if err != nil {
 | |
| 			return err
 | |
| 		}
 | |
| 		if file.IsDir() {
 | |
| 			continue
 | |
| 		}
 | |
| 
 | |
| 		if file.LinkTarget != "" {
 | |
| 			_, err = w.Write([]byte(filepath.ToSlash(file.LinkTarget)))
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		} else {
 | |
| 			fileReader, err := file.Open()
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 			_, err = io.Copy(w, fileReader)
 | |
| 			if err != nil {
 | |
| 				return err
 | |
| 			}
 | |
| 		}
 | |
| 	}
 | |
| 	return nil
 | |
| }
 |