Skip to content

Commit

Permalink
registry: support installing zip
Browse files Browse the repository at this point in the history
  • Loading branch information
ImSingee committed Nov 10, 2023
1 parent a33102a commit 1fbfa2a
Show file tree
Hide file tree
Showing 8 changed files with 365 additions and 9 deletions.
2 changes: 2 additions & 0 deletions internal/extension-registry/install.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@ import (
"github.com/ImSingee/kitty/internal/extension-registry/installer"
"github.com/ImSingee/kitty/internal/extension-registry/installer/impl/bininstaller"
"github.com/ImSingee/kitty/internal/extension-registry/installer/impl/goinstaller"
"github.com/ImSingee/kitty/internal/extension-registry/installer/impl/zipinstaller"
eroptions "github.com/ImSingee/kitty/internal/extension-registry/options"
)

Expand Down Expand Up @@ -103,6 +104,7 @@ func factoryIdl(factory installer.Factory) idl {

var installers = []idl{
factoryIdl(&bininstaller.Factory{}),
factoryIdl(&zipinstaller.Factory{}),
factoryIdl(&goinstaller.Factory{}),
}

Expand Down
119 changes: 119 additions & 0 deletions internal/extension-registry/installer/impl/zipinstaller/installer.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,119 @@
package zipinstaller

import (
"net/url"
"os"
"path"
"strings"

"github.com/ImSingee/go-ex/ee"
"github.com/ImSingee/go-ex/pp"

"github.com/ImSingee/kitty/internal/extension-registry/binkey"
"github.com/ImSingee/kitty/internal/extension-registry/installer"
"github.com/ImSingee/kitty/internal/extension-registry/installer/tmpl"
eroptions "github.com/ImSingee/kitty/internal/extension-registry/options"
erutils "github.com/ImSingee/kitty/internal/extension-registry/utils"
)

type Factory struct{}

func (Factory) Key() string {
return "dist-zip"
}

func (Factory) GetInstaller(o eroptions.AnyOptions) (installer.Installer, error) {
object := eroptions.Get(o, string(binkey.GetCurrentBinKey()))
object = eroptions.RenameDollarKey(object, "url", false)

if eroptions.Exists(o, "default") {
defaultOptions := eroptions.Get(o, "default")
defaultOptions = eroptions.RenameDollarKey(defaultOptions, "url", false)

object = eroptions.Merge(object, defaultOptions, false)
}

url, _ := object["url"].Val().(string)
bin, _ := object["bin"].Val().(string)

if url == "" || bin == "" {
return nil, installer.ErrInstallerNotApplicable
}

return &Installer{url, bin}, nil
}

type Installer struct {
url string
bin string
}

func (i *Installer) Install(o *installer.InstallOptions) error {
url, err := tmpl.Render(i.url, o)
if err != nil {
return ee.Wrap(err, "invalid url")
}
bin, err := tmpl.Render(i.bin, o)
if err != nil {
return ee.Wrap(err, "invalid bin")
}

filenameWithoutExt, ext, err := getFilenamePartsOfUrl(url)
if err != nil {
return err
}
filename := filenameWithoutExt + ext

zipDownloadedTo, err := erutils.DownloadFileToTemp(url, "kdl-*-"+filename, o.ShowProgress)
if err != nil {
return ee.Wrap(err, "cannot download zip file")
}

if o.ShowProgress {
pp.Println("zip file downloaded to:", zipDownloadedTo)
}

unzipToDir, err := os.MkdirTemp("", "kunzip-*")
if err != nil {
return ee.Wrap(err, "cannot create temp dir")
}

if o.ShowProgress {
pp.Println("Extract ...")
}
err = erutils.Unzip(zipDownloadedTo, unzipToDir)
if err != nil {
return ee.Wrap(err, "cannot unzip file")
}

distFrom := path.Join(unzipToDir, bin)
if _, err := os.Lstat(distFrom); err != nil {
return ee.Wrapf(err, "cannot find bin file %s in zip", bin)
}

_, _ = erutils.MkdirFor(o.To)
err = erutils.Rename(distFrom, o.To)
if err != nil {
return ee.Wrapf(err, "failed to install bin file %s from zip", bin)
}

return nil
}

func getFilenamePartsOfUrl(u string) (name string, ext string, err error) {
parsed, err := url.Parse(u)
if err != nil {
return "", "", ee.Wrapf(err, "url %s is invalid", u)
}

paths := strings.Split(parsed.Path, "/")
pathLastPart := paths[len(paths)-1]

dotIndex := strings.Index(pathLastPart, ".")

if dotIndex == -1 {
return pathLastPart, "", nil
} else {
return pathLastPart[:dotIndex], pathLastPart[dotIndex:], nil
}
}
37 changes: 37 additions & 0 deletions internal/extension-registry/installer/tmpl/tmpl.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package tmpl

import (
"strings"
"text/template"

"github.com/ImSingee/go-ex/ee"
"github.com/ImSingee/semver"

"github.com/ImSingee/kitty/internal/extension-registry/installer"
)

func Render(tmpl string, options *installer.InstallOptions) (string, error) {
t, err := template.New("").Parse(tmpl)
if err != nil {
return "", ee.Wrap(err, "invalid template")
}

w := strings.Builder{}
err = t.Execute(&w, toTemplateOptions(options))
if err != nil {
return "", ee.Wrap(err, "cannot render template")
}
return w.String(), nil
}

func toTemplateOptions(options *installer.InstallOptions) map[string]interface{} {
v := ""
if sv, _ := semver.NewVersion(options.Version); sv != nil {
v = sv.String()
}

return map[string]interface{}{
"version": options.Version,
"semver": v,
}
}
24 changes: 24 additions & 0 deletions internal/extension-registry/options/options.go
Original file line number Diff line number Diff line change
Expand Up @@ -106,3 +106,27 @@ func Get(o AnyOptions, key string) AnyOptions {
return wrappedOptions(v)
}
}

// Merge two AnyOptions, return a new merged object
//
// override: if there's same key in o and with, whether to override it (make o[key] = with[key])
func Merge(o AnyOptions, with AnyOptions, override bool) AnyOptions {
result := make(AnyOptions)
for k, v := range o {
result[k] = v
}

if override {
for k, v := range with {
result[k] = v
}
} else {
for k, v := range with {
if _, exists := result[k]; !exists {
result[k] = v
}
}
}

return result
}
55 changes: 55 additions & 0 deletions internal/extension-registry/utils/copy.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,55 @@
package erutils

import (
"io"
"os"
"path/filepath"
"syscall"

"github.com/ImSingee/go-ex/ee"
)

func copyFile(from, to string, force bool) error {
from = filepath.Clean(from)
to = filepath.Clean(to)
if from == to {
return nil
}

fromF, err := os.Open(from)
if err != nil {
return ee.Wrapf(err, "cannot open source file %s", from)
}
defer fromF.Close()

stat, err := fromF.Stat()
if err != nil {
return ee.Wrapf(err, "cannot stat source file %s", from)
}

var toF *os.File
if force { // 存在时覆盖
toF, err = os.OpenFile(to, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, stat.Mode())
} else { // 存在时报错
toF, err = os.OpenFile(to, os.O_WRONLY|os.O_CREATE|os.O_EXCL, stat.Mode())
}

if err != nil {
return ee.Wrapf(err, "cannot open destination file %s", to)
}
defer toF.Close()

_, err = io.Copy(toF, fromF)
if err != nil {
return ee.Wrap(err, "cannot copy data")
}

// change owner
sysStat := stat.Sys().(*syscall.Stat_t) //nolint:forcetypeassert
err = os.Chown(to, int(sysStat.Uid), int(sysStat.Gid))
if err != nil {
return ee.Wrap(err, "cannot change destination file owner")
}

return nil
}
46 changes: 37 additions & 9 deletions internal/extension-registry/utils/download.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,16 +9,11 @@ import (
"github.com/ImSingee/go-ex/pp"
)

func DownloadFileTo(url string, dst string, perm os.FileMode, showProgress bool) error {
func downloadFileTo(url string, w io.Writer, showProgress bool) error {
if showProgress { // TODO progress bar
pp.Println("Download", url, "...")
}

_, err := MkdirFor(dst)
if err != nil {
return err
}

resp, err := http.Get(url)
if err != nil {
return ee.Wrapf(err, "cannot download file from %s", url)
Expand All @@ -29,17 +24,30 @@ func DownloadFileTo(url string, dst string, perm os.FileMode, showProgress bool)
return ee.Errorf("cannot download file from %s: status code = %d", url, resp.StatusCode)
}

_ = os.Remove(dst)
_, err = io.Copy(w, resp.Body)
if err != nil {
return ee.Wrap(err, "cannot write data")
}

return nil
}

func DownloadFileTo(url string, dst string, perm os.FileMode, showProgress bool) error {
_, err := MkdirFor(dst)
if err != nil {
return err
}

_ = os.Remove(dst)
f, err := os.OpenFile(dst, os.O_WRONLY|os.O_CREATE|os.O_EXCL, perm)
if err != nil {
return ee.Wrapf(err, "cannot create file %s", dst)
}
defer f.Close()

_, err = io.Copy(f, resp.Body)
err = downloadFileTo(url, f, showProgress)
if err != nil {
return ee.Wrapf(err, "cannot write data to %s", dst)
return ee.Wrapf(err, "cannot download file to %s", dst)
}

err = f.Close()
Expand All @@ -49,3 +57,23 @@ func DownloadFileTo(url string, dst string, perm os.FileMode, showProgress bool)

return nil
}

func DownloadFileToTemp(url string, temppattern string, showProgress bool) (string, error) {
f, err := os.CreateTemp("", temppattern)
if err != nil {
return "", ee.Wrap(err, "cannot create temp file")
}
defer f.Close()

err = downloadFileTo(url, f, showProgress)
if err != nil {
return "", ee.Wrap(err, "cannot download file to [tempfile]")
}

err = f.Close()
if err != nil {
return "", ee.Wrap(err, "cannot save and close file [tempfile]")
}

return f.Name(), nil
}
36 changes: 36 additions & 0 deletions internal/extension-registry/utils/move.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
package erutils

import (
"os"
"syscall"

"github.com/ImSingee/go-ex/ee"
)

func Rename(oldpath, newpath string) error {
err := os.Rename(oldpath, newpath)
if err == nil {
return nil
}

// os.Rename guarantees it returns an *os.LinkError
if err.(*os.LinkError).Err != syscall.EXDEV { //nolint:forcetypeassert
return err
}

// fallback to copy and remove
return moveFile(oldpath, newpath)
}

func moveFile(oldpath, newpath string) error {
err := copyFile(oldpath, newpath, true)
if err != nil {
return ee.Wrapf(err, "rename %s -> %s: cannot copy file", oldpath, newpath)
}
err = os.Remove(oldpath)
if err != nil {
return ee.Wrapf(err, "rename %s -> %s: cannot remove old file", oldpath, newpath)
}

return nil
}
Loading

0 comments on commit 1fbfa2a

Please sign in to comment.