mirror of
https://gitee.com/energye/energy.git
synced 2024-12-05 13:17:54 +08:00
712 lines
17 KiB
Go
712 lines
17 KiB
Go
//----------------------------------------
|
|
//
|
|
// Copyright © yanghy. All Rights Reserved.
|
|
//
|
|
// Licensed under Apache License Version 2.0, January 2004
|
|
//
|
|
// https://www.apache.org/licenses/LICENSE-2.0
|
|
//
|
|
|
|
package install
|
|
|
|
import (
|
|
"archive/tar"
|
|
"archive/zip"
|
|
"compress/bzip2"
|
|
"compress/gzip"
|
|
"errors"
|
|
"fmt"
|
|
"github.com/energye/energy/v2/cmd/internal/command"
|
|
"github.com/energye/energy/v2/cmd/internal/consts"
|
|
"github.com/energye/energy/v2/cmd/internal/env"
|
|
"github.com/energye/energy/v2/cmd/internal/term"
|
|
"github.com/energye/energy/v2/cmd/internal/tools"
|
|
"github.com/pterm/pterm"
|
|
"io"
|
|
"io/fs"
|
|
"net/http"
|
|
"os"
|
|
"path/filepath"
|
|
"runtime"
|
|
"strconv"
|
|
"strings"
|
|
)
|
|
|
|
type downloadInfo struct {
|
|
fileName string
|
|
frameworkPath string
|
|
downloadPath string
|
|
url string
|
|
success bool
|
|
isSupport bool
|
|
module string
|
|
}
|
|
type softEnf struct {
|
|
name string
|
|
desc string
|
|
installed bool
|
|
yes func()
|
|
}
|
|
|
|
func Install(c *command.Config) error {
|
|
// 初始配置和安装目录
|
|
initInstall(c)
|
|
// 检查环境
|
|
willInstall := checkInstallEnv(c)
|
|
var (
|
|
goRoot string
|
|
goSuccessCallback func()
|
|
cefFrameworkRoot string
|
|
cefFrameworkSuccessCallback func()
|
|
nsisRoot string
|
|
nsisSuccessCallback func()
|
|
upxRoot string
|
|
upxSuccessCallback func()
|
|
)
|
|
if len(willInstall) > 0 {
|
|
// energy 依赖的软件框架
|
|
tableData := pterm.TableData{
|
|
{"Software", "Installed", "Support Platform Description"},
|
|
}
|
|
var options []string
|
|
for _, se := range willInstall {
|
|
var installed string
|
|
if se.installed {
|
|
installed = "Yes"
|
|
} else {
|
|
installed = "No"
|
|
options = append(options, se.name)
|
|
//se.yes()
|
|
}
|
|
var data = []string{se.name, installed, se.desc}
|
|
tableData = append(tableData, data)
|
|
}
|
|
term.Section.Println("Energy Development Environment Framework Dependencies")
|
|
err := pterm.DefaultTable.WithHasHeader().WithHeaderRowSeparator("-").WithBoxed().WithData(tableData).Render()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// 选择
|
|
if len(options) > 0 {
|
|
printer := term.DefaultInteractiveMultiselect.WithOnInterruptFunc(func() {
|
|
os.Exit(1)
|
|
}).WithOptions(options)
|
|
printer.Checkmark.Checked = "+"
|
|
printer.Checkmark.Unchecked = "-"
|
|
printer.DefaultText = "Optional Installation"
|
|
printer.Filter = false
|
|
selectedOptions, err := printer.Show()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
pterm.Info.Printfln("Selected options: %s", pterm.Green(selectedOptions))
|
|
for _, option := range selectedOptions {
|
|
for _, wi := range willInstall {
|
|
if option == wi.name {
|
|
wi.yes()
|
|
break
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
// 安装Go开发环境
|
|
goRoot, goSuccessCallback = installGolang(c)
|
|
// 设置 go 环境变量
|
|
if goRoot != "" {
|
|
env.SetGoEnv(goRoot)
|
|
}
|
|
// 安装nsis安装包制作工具, 仅windows - amd64
|
|
nsisRoot, nsisSuccessCallback = installNSIS(c)
|
|
// 设置nsis环境变量
|
|
if nsisRoot != "" {
|
|
env.SetNSISEnv(nsisRoot)
|
|
}
|
|
// 安装upx, 内置, 仅windows, linux
|
|
upxRoot, upxSuccessCallback = installUPX(c)
|
|
// 设置upx环境变量
|
|
if upxRoot != "" {
|
|
env.SetUPXEnv(upxRoot)
|
|
}
|
|
// 安装CEF二进制框架
|
|
cefFrameworkRoot, cefFrameworkSuccessCallback = installCEFFramework(c)
|
|
// 设置 energy cef 环境变量
|
|
if cefFrameworkRoot != "" && c.Install.IsSame {
|
|
env.SetEnergyHomeEnv(cefFrameworkRoot)
|
|
}
|
|
// success 输出
|
|
if nsisSuccessCallback != nil || goSuccessCallback != nil || upxSuccessCallback != nil || cefFrameworkSuccessCallback != nil {
|
|
term.BoxPrintln("Hint: Reopen the cmd window for command to take effect.")
|
|
}
|
|
if nsisSuccessCallback != nil {
|
|
nsisSuccessCallback()
|
|
}
|
|
if goSuccessCallback != nil {
|
|
goSuccessCallback()
|
|
}
|
|
if upxSuccessCallback != nil {
|
|
upxSuccessCallback()
|
|
}
|
|
if cefFrameworkSuccessCallback != nil {
|
|
cefFrameworkSuccessCallback()
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func cefInstallPathName(c *command.Config) string {
|
|
if c.Install.IsSame {
|
|
return filepath.Join(c.Install.Path, consts.ENERGY, c.Install.Name)
|
|
} else {
|
|
return filepath.Join(c.Install.Path, consts.ENERGY, fmt.Sprintf("%s_%s%s", c.Install.Name, c.Install.OS, c.Install.Arch))
|
|
}
|
|
}
|
|
|
|
func goInstallPathName(c *command.Config) string {
|
|
return filepath.Join(c.Install.Path, consts.ENERGY, "go")
|
|
}
|
|
|
|
func nsisInstallPathName(c *command.Config) string {
|
|
return filepath.Join(c.Install.Path, consts.ENERGY, "nsis")
|
|
}
|
|
|
|
func upxInstallPathName(c *command.Config) string {
|
|
return filepath.Join(c.Install.Path, consts.ENERGY, "upx")
|
|
}
|
|
|
|
func nsisIsInstall() bool {
|
|
return consts.IsWindows && !consts.IsARM64
|
|
}
|
|
|
|
func upxIsInstall() bool {
|
|
return (consts.IsWindows && !consts.IsARM64) || (consts.IsLinux)
|
|
}
|
|
|
|
// 检查当前环境
|
|
// golang, nsis, cef, upx
|
|
// golang: all os
|
|
// nsis: windows
|
|
// cef: all os
|
|
// upx: windows amd64, 386, linux amd64, arm64
|
|
func checkInstallEnv(c *command.Config) (result []*softEnf) {
|
|
result = make([]*softEnf, 0)
|
|
var check = func(chkInstall func() (string, bool), name string, yes func()) {
|
|
desc, ok := chkInstall()
|
|
result = append(result, &softEnf{name: name, desc: desc, installed: ok, yes: yes})
|
|
}
|
|
|
|
// go
|
|
check(func() (string, bool) {
|
|
return "All", tools.CommandExists("go")
|
|
}, "Golang", func() {
|
|
c.Install.IGolang = true
|
|
})
|
|
|
|
// cef
|
|
var cefName = "CEF Framework"
|
|
if !c.Install.IsSame {
|
|
cefName = fmt.Sprintf("CEF Framework %s%s", c.Install.OS, c.Install.Arch)
|
|
}
|
|
check(func() (string, bool) {
|
|
if c.Install.IsSame {
|
|
// 检查环境变量是否配置
|
|
return "All", tools.CheckCEFDir()
|
|
}
|
|
// 非当月系统架构时检查一下目标安装路径是否已经存在
|
|
var lib = func() string {
|
|
if c.Install.OS.IsWindows() {
|
|
return "libcef.dll"
|
|
} else if c.Install.OS.IsLinux() {
|
|
return "libcef.so"
|
|
} else if c.Install.OS.IsDarwin() {
|
|
return "cef_sandbox.a"
|
|
}
|
|
return ""
|
|
}()
|
|
if lib != "" {
|
|
s := filepath.Join(cefInstallPathName(c), lib)
|
|
return "All", tools.IsExist(s)
|
|
} else {
|
|
return "Unsupported Platform", true
|
|
}
|
|
}, cefName, func() {
|
|
c.Install.ICEF = true
|
|
})
|
|
if consts.IsWindows {
|
|
// nsis
|
|
check(func() (string, bool) {
|
|
if nsisIsInstall() {
|
|
return "Windows AMD", tools.CommandExists("makensis")
|
|
} else {
|
|
return "Non Windows AMD skipping NSIS.", true
|
|
}
|
|
}, "NSIS", func() {
|
|
c.Install.INSIS = true
|
|
})
|
|
}
|
|
if !consts.IsDarwin {
|
|
// upx
|
|
check(func() (string, bool) {
|
|
if upxIsInstall() {
|
|
return "Windows AMD, Linux", tools.CommandExists("upx")
|
|
} else {
|
|
return "Non Windows and Linux skipping UPX.", true
|
|
}
|
|
}, "UPX", func() {
|
|
c.Install.IUPX = true
|
|
})
|
|
}
|
|
if consts.IsLinux {
|
|
// gtk2
|
|
// gtk3
|
|
// dpkg
|
|
}
|
|
if consts.IsDarwin {
|
|
//
|
|
}
|
|
return
|
|
}
|
|
|
|
func initInstall(c *command.Config) {
|
|
if c.Install.Path == "" {
|
|
// current dir
|
|
c.Install.Path = c.Wd
|
|
}
|
|
if c.Install.Version == "" {
|
|
// latest
|
|
c.Install.Version = "latest"
|
|
}
|
|
if c.Install.OS == "" {
|
|
c.Install.OS = command.OS(runtime.GOOS)
|
|
}
|
|
if c.Install.Arch == "" {
|
|
c.Install.Arch = command.Arch(runtime.GOARCH)
|
|
}
|
|
if string(c.Install.OS) == runtime.GOOS && string(c.Install.Arch) == runtime.GOARCH {
|
|
c.Install.IsSame = true
|
|
}
|
|
// 创建安装目录
|
|
os.MkdirAll(c.Install.Path, fs.ModePerm)
|
|
os.MkdirAll(cefInstallPathName(c), fs.ModePerm)
|
|
os.MkdirAll(goInstallPathName(c), fs.ModePerm)
|
|
if nsisIsInstall() {
|
|
os.MkdirAll(nsisInstallPathName(c), fs.ModePerm)
|
|
}
|
|
if upxIsInstall() {
|
|
os.MkdirAll(upxInstallPathName(c), fs.ModePerm)
|
|
}
|
|
os.MkdirAll(filepath.Join(c.Install.Path, consts.FrameworkCache), fs.ModePerm)
|
|
}
|
|
|
|
func filePathInclude(compressPath string, files ...any) (string, bool) {
|
|
if len(files) == 0 {
|
|
return compressPath, true
|
|
} else {
|
|
for _, file := range files {
|
|
f := file.(string)
|
|
tIdx := strings.LastIndex(f, "/*")
|
|
if tIdx != -1 {
|
|
f = f[:tIdx]
|
|
if f[0] == '/' {
|
|
if strings.Index(compressPath, f[1:]) == 0 {
|
|
return compressPath, true
|
|
}
|
|
}
|
|
if strings.Index(compressPath, f) == 0 {
|
|
return strings.Replace(compressPath, f, "", 1), true
|
|
}
|
|
} else {
|
|
if f[0] == '/' {
|
|
if compressPath == f[1:] {
|
|
return f, true
|
|
}
|
|
}
|
|
if compressPath == f {
|
|
f = f[strings.Index(f, "/")+1:]
|
|
return f, true
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return "", false
|
|
}
|
|
|
|
func dir(path string) string {
|
|
path = strings.ReplaceAll(path, "\\", string(filepath.Separator))
|
|
lastSep := strings.LastIndex(path, string(filepath.Separator))
|
|
return path[:lastSep]
|
|
}
|
|
|
|
func tarFileReader(filePath string) (*tar.Reader, func(), error) {
|
|
reader, err := os.Open(filePath)
|
|
if err != nil {
|
|
fmt.Printf("error: cannot open file, error=[%v]\n", err)
|
|
return nil, nil, err
|
|
}
|
|
if filepath.Ext(filePath) == ".gz" {
|
|
gr, err := gzip.NewReader(reader)
|
|
if err != nil {
|
|
fmt.Printf("error: cannot open gzip file, error=[%v]\n", err)
|
|
return nil, nil, err
|
|
}
|
|
return tar.NewReader(gr), func() {
|
|
gr.Close()
|
|
reader.Close()
|
|
}, nil
|
|
} else {
|
|
return tar.NewReader(reader), func() {
|
|
reader.Close()
|
|
}, nil
|
|
}
|
|
}
|
|
|
|
func tarFileCount(filePath string) (int, error) {
|
|
tarReader, clos, err := tarFileReader(filePath)
|
|
if err != nil {
|
|
return 0, err
|
|
}
|
|
defer clos()
|
|
var count int
|
|
for {
|
|
_, err := tarReader.Next()
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return 0, err
|
|
}
|
|
count++
|
|
}
|
|
return count, nil
|
|
}
|
|
|
|
func ExtractUnTar(filePath, targetPath string, files ...any) error {
|
|
term.Logger.Info("Read Files Number")
|
|
fileCount, err := tarFileCount(filePath)
|
|
println(fileCount)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tarReader, clos, err := tarFileReader(filePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer clos()
|
|
multi := pterm.DefaultMultiPrinter
|
|
defer multi.Stop()
|
|
fileTotalProcessBar := pterm.DefaultProgressbar.WithWriter(multi.NewWriter())
|
|
writeFileProcessBar := pterm.DefaultProgressbar.WithWriter(multi.NewWriter())
|
|
extractFilesProcessBar, err := fileTotalProcessBar.WithTotal(fileCount).Start("Extract File")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
multi.Start()
|
|
for {
|
|
header, err := tarReader.Next()
|
|
if err == io.EOF {
|
|
break
|
|
} else if err != nil {
|
|
return err
|
|
}
|
|
extractFilesProcessBar.Increment()
|
|
// 去除压缩包内的一级目录
|
|
compressPath := filepath.Clean(header.Name[strings.Index(header.Name, "/")+1:])
|
|
includePath, isInclude := filePathInclude(compressPath, files...)
|
|
if !isInclude {
|
|
continue
|
|
}
|
|
info := header.FileInfo()
|
|
targetFile := filepath.Join(targetPath, includePath)
|
|
if info.IsDir() {
|
|
if err = os.MkdirAll(targetFile, info.Mode()); err != nil {
|
|
return err
|
|
}
|
|
} else {
|
|
fDir := dir(targetFile)
|
|
_, err = os.Stat(fDir)
|
|
if os.IsNotExist(err) {
|
|
if err = os.MkdirAll(fDir, info.Mode()); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
file, err := os.OpenFile(targetFile, os.O_CREATE|os.O_TRUNC|os.O_WRONLY, info.Mode())
|
|
if err != nil {
|
|
return err
|
|
}
|
|
var (
|
|
total = 100
|
|
c int
|
|
cn int
|
|
)
|
|
_, fileName := filepath.Split(targetFile)
|
|
wfpb, err := writeFileProcessBar.WithCurrent(0).WithTotal(total).Start("Write File " + fileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writeFile(tarReader, file, header.Size, func(totalLength, processLength int64) {
|
|
process := int((float64(processLength) / float64(totalLength)) * 100)
|
|
if process > c {
|
|
c = process
|
|
wfpb.Add(process)
|
|
cn++
|
|
}
|
|
})
|
|
if cn < total {
|
|
wfpb.Add(total - cn)
|
|
}
|
|
wfpb.Stop()
|
|
file.Sync()
|
|
file.Close()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func ExtractUnZip(filePath, targetPath string, rmRootDir bool, files ...any) error {
|
|
if rc, err := zip.OpenReader(filePath); err == nil {
|
|
defer rc.Close()
|
|
multi := pterm.DefaultMultiPrinter
|
|
defer multi.Stop()
|
|
fileTotalProcessBar := pterm.DefaultProgressbar.WithWriter(multi.NewWriter())
|
|
writeFileProcessBar := pterm.DefaultProgressbar.WithWriter(multi.NewWriter())
|
|
var createWriteFile = func(info fs.FileInfo, path string, file io.Reader) error {
|
|
targetFileName := filepath.Join(targetPath, path)
|
|
if info.IsDir() {
|
|
os.MkdirAll(targetFileName, info.Mode())
|
|
return nil
|
|
}
|
|
fDir, name := filepath.Split(targetFileName)
|
|
if !tools.IsExist(fDir) {
|
|
os.MkdirAll(fDir, 0755)
|
|
}
|
|
if targetFile, err := os.Create(targetFileName); err == nil {
|
|
defer targetFile.Close()
|
|
var (
|
|
total = 100
|
|
c int
|
|
cn int
|
|
)
|
|
wfpb, err := writeFileProcessBar.WithCurrent(0).WithTotal(total).Start("Write File " + name)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
writeFile(file, targetFile, info.Size(), func(totalLength, processLength int64) {
|
|
process := int((float64(processLength) / float64(totalLength)) * 100)
|
|
if process > c {
|
|
c = process
|
|
wfpb.Add(process)
|
|
cn++
|
|
}
|
|
})
|
|
if cn < total {
|
|
wfpb.Add(total - cn)
|
|
}
|
|
wfpb.Stop()
|
|
return nil
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
// 所有文件
|
|
if len(files) == 0 {
|
|
zipFiles := rc.File
|
|
extractFilesProcessBar, err := fileTotalProcessBar.WithTotal(len(zipFiles)).Start("Extract File")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
multi.Start()
|
|
for _, f := range zipFiles {
|
|
extractFilesProcessBar.Increment() // +1
|
|
r, _ := f.Open()
|
|
var name string
|
|
if rmRootDir {
|
|
// 移除压缩包内的根文件夹名
|
|
name = filepath.Clean(f.Name[strings.Index(f.Name, "/")+1:])
|
|
} else {
|
|
name = filepath.Clean(f.Name)
|
|
}
|
|
if err := createWriteFile(f.FileInfo(), name, r.(io.Reader)); err != nil {
|
|
return err
|
|
}
|
|
_ = r.Close()
|
|
}
|
|
return nil
|
|
} else {
|
|
extractFilesProcessBar, err := fileTotalProcessBar.WithTotal(len(files)).Start("Extract File")
|
|
if err != nil {
|
|
return err
|
|
}
|
|
multi.Start()
|
|
// 指定名字的文件
|
|
for i := 0; i < len(files); i++ {
|
|
extractFilesProcessBar.Increment() // +1
|
|
if f, err := rc.Open(files[i].(string)); err == nil {
|
|
info, _ := f.Stat()
|
|
if err := createWriteFile(info, files[i].(string), f); err != nil {
|
|
return err
|
|
}
|
|
_ = f.Close()
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
} else {
|
|
return err
|
|
}
|
|
}
|
|
|
|
// 释放bz2文件到tar
|
|
func UnBz2ToTar(name string, callback func(totalLength, processLength int64)) (string, error) {
|
|
fileBz2, err := os.Open(name)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer fileBz2.Close()
|
|
dirName := fileBz2.Name()
|
|
dirName = dirName[:strings.LastIndex(dirName, ".")]
|
|
if !tools.IsExist(dirName) {
|
|
r := bzip2.NewReader(fileBz2)
|
|
w, err := os.Create(dirName)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
defer w.Close()
|
|
writeFile(r, w, 0, callback)
|
|
} else {
|
|
term.Section.Println("File already exists")
|
|
}
|
|
return dirName, nil
|
|
}
|
|
|
|
func writeFile(r io.Reader, w *os.File, totalLength int64, callback func(totalLength, processLength int64)) {
|
|
buf := make([]byte, 1024*10)
|
|
var written int64
|
|
for {
|
|
nr, err := r.Read(buf)
|
|
if nr > 0 {
|
|
nw, err := w.Write(buf[0:nr])
|
|
if nw > 0 {
|
|
written += int64(nw)
|
|
}
|
|
callback(totalLength, written)
|
|
if err != nil {
|
|
break
|
|
}
|
|
if nr != nw {
|
|
err = io.ErrShortWrite
|
|
break
|
|
}
|
|
}
|
|
if err != nil {
|
|
break
|
|
}
|
|
}
|
|
}
|
|
|
|
func isFileExist(filename string, filesize int64) bool {
|
|
info, err := os.Stat(filename)
|
|
if os.IsNotExist(err) {
|
|
return false
|
|
}
|
|
if filesize == info.Size() {
|
|
return true
|
|
}
|
|
os.Remove(filename)
|
|
return false
|
|
}
|
|
|
|
// DownloadFile 下载文件
|
|
// 如果文件存在大小一样不再下载
|
|
func DownloadFile(url string, localPath string, callback func(totalLength, processLength int64)) error {
|
|
var (
|
|
fsize int64
|
|
buf = make([]byte, 1024*10)
|
|
written int64
|
|
)
|
|
tmpFilePath := localPath + ".download"
|
|
client := new(http.Client)
|
|
resp, err := client.Get(url)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
fsize, err = strconv.ParseInt(resp.Header.Get("Content-Length"), 10, 32)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if isFileExist(localPath, fsize) {
|
|
term.Section.Println("File already exists")
|
|
return nil
|
|
}
|
|
file, err := os.Create(tmpFilePath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer file.Close()
|
|
if resp.Body == nil {
|
|
return errors.New("body is null")
|
|
}
|
|
defer resp.Body.Close()
|
|
total := 100
|
|
_, fileName := filepath.Split(localPath)
|
|
p, err := pterm.DefaultProgressbar.WithTotal(total).WithTitle("Download " + fileName).Start()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
defer func() {
|
|
p.Stop()
|
|
}()
|
|
var (
|
|
count int
|
|
cn int
|
|
)
|
|
var nw int
|
|
for {
|
|
nr, er := resp.Body.Read(buf)
|
|
if nr > 0 {
|
|
nw, err = file.Write(buf[0:nr])
|
|
if nw > 0 {
|
|
written += int64(nw)
|
|
}
|
|
if callback != nil {
|
|
callback(fsize, written)
|
|
} else {
|
|
process := int((float64(written) / float64(fsize)) * 100)
|
|
if process > count {
|
|
count = process
|
|
p.Add(1)
|
|
cn++
|
|
}
|
|
}
|
|
if err != nil {
|
|
break
|
|
}
|
|
if nr != nw {
|
|
err = io.ErrShortWrite
|
|
break
|
|
}
|
|
}
|
|
if er != nil {
|
|
if er != io.EOF {
|
|
err = er
|
|
}
|
|
break
|
|
}
|
|
}
|
|
if cn < total {
|
|
p.Add(total - cn)
|
|
}
|
|
p.Stop()
|
|
if err == nil {
|
|
file.Sync()
|
|
file.Close()
|
|
err = os.Rename(tmpFilePath, localPath)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return err
|
|
}
|