diff --git a/cmd/cmd_pkg.go b/cmd/cmd_pkg.go index 6c7e75cc..0c08dc7e 100644 --- a/cmd/cmd_pkg.go +++ b/cmd/cmd_pkg.go @@ -5,6 +5,7 @@ import ( "crypto/rand" "encoding/hex" "fmt" + "net/http" "os" "path" "path/filepath" @@ -12,9 +13,11 @@ import ( "sort" "strings" + "github.com/go-errors/errors" "github.com/olekukonko/tablewriter" "github.com/spf13/cobra" + "github.com/nanovms/ops/lepton" api "github.com/nanovms/ops/lepton" "github.com/nanovms/ops/log" ) @@ -70,6 +73,13 @@ func PackageCommands() *cobra.Command { Run: cmdPkgLogin, } + var cmdPkgPush = &cobra.Command{ + Use: "push [local-package]", + Short: "push the local package to packagehub", + Args: cobra.ExactArgs(1), + Run: cmdPkgPush, + } + var cmdPkg = &cobra.Command{ Use: "pkg", Short: "Package related commands", @@ -98,6 +108,7 @@ func PackageCommands() *cobra.Command { cmdPkg.AddCommand(cmdAddPackage) cmdPkg.AddCommand(cmdFromDockerPackage) cmdPkg.AddCommand(cmdPkgLogin) + cmdPkg.AddCommand(cmdPkgPush) cmdPkg.AddCommand(LoadCommand()) return cmdPkg } @@ -290,6 +301,59 @@ func cmdPkgLogin(cmd *cobra.Command, args []string) { fmt.Printf("Login Successful as user %s\n", resp.Username) } +func cmdPkgPush(cmd *cobra.Command, args []string) { + packageName := args[0] + creds, err := api.ReadCredsFromLocal() + if err != nil { + if err == api.ErrCredentialsNotExist { + // for a better error message + log.Fatal(errors.New("user is not logged in. use 'ops pkg login' first")) + } else { + log.Fatal(err) + } + } + pkgList, err := api.GetLocalPackageList() + if err != nil { + log.Fatal(err) + } + localPackages := filepath.Join(api.GetOpsHome(), "local_packages") + var foundPkg api.Package + for name, pkg := range *pkgList { + if name == packageName { + foundPkg = pkg + break + } + } + if foundPkg.SHA256 == "" { + log.Fatalf("no local package with the name %s found", packageName) + } + + // build the archive here + archiveName := filepath.Join(localPackages, packageName) + ".tar.gz" + err = lepton.CreateTarGz(filepath.Join(localPackages, packageName), archiveName) + if err != nil { + log.Fatal(err) + } + defer os.RemoveAll(archiveName) + + req, err := lepton.BuildRequestForArchiveUpload(packageName, foundPkg, archiveName) + if err != nil { + log.Fatal(err) + } + req.Header.Set(lepton.APIKeyHeader, creds.APIKey) + resp, err := http.DefaultClient.Do(req) + if err != nil { + log.Fatal(err) + } + // if the package is uploaded successfully then pkghub redirects to home page + if resp.StatusCode != http.StatusOK { + log.Fatal(errors.New("there was as an error while uploading the archive")) + } else { + fmt.Println("Package was uploaded successfully.") + } + +} + func randomToken(n int) (string, error) { bytes := make([]byte, n) diff --git a/lepton/push.go b/lepton/push.go new file mode 100644 index 00000000..14aac4a0 --- /dev/null +++ b/lepton/push.go @@ -0,0 +1,145 @@ +package lepton + +import ( + "archive/tar" + "bytes" + "compress/gzip" + "fmt" + "io" + "io/ioutil" + "mime/multipart" + "net/http" + "os" + "path/filepath" +) + +// BuildRequestForArchiveUpload builds the request to upload a package with the provided metadata +func BuildRequestForArchiveUpload(name string, pkg Package, archiveLocation string) (*http.Request, error) { + params := map[string]string{ + "name": name, + "description": pkg.Description, + "language": pkg.Language, + "runtime": pkg.Runtime, + "version": pkg.Version, + } + return newfileUploadRequest(PkghubBaseURL+"/packages/create", params, "package", archiveLocation) + +} + +func newfileUploadRequest(uri string, params map[string]string, fileParamName, path string) (*http.Request, error) { + file, err := os.Open(path) + if err != nil { + return nil, err + } + fileContents, err := ioutil.ReadAll(file) + if err != nil { + return nil, err + } + fi, err := file.Stat() + if err != nil { + return nil, err + } + file.Close() + + body := new(bytes.Buffer) + writer := multipart.NewWriter(body) + part, err := writer.CreateFormFile(fileParamName, fi.Name()) + if err != nil { + return nil, err + } + part.Write(fileContents) + + for key, val := range params { + _ = writer.WriteField(key, val) + } + err = writer.Close() + if err != nil { + return nil, err + } + + req, err := http.NewRequest("POST", uri, body) + if err != nil { + return nil, err + } + req.Header.Set("Content-Type", fmt.Sprintf("multipart/form-data; boundary=%s", writer.Boundary())) + return req, nil +} + +// CreateTarGz builds a .tar.gz archive with the directory of the source as the root of the archive +func CreateTarGz(src string, destination string) error { + fd, err := os.Create(destination) + if err != nil { + return err + } + // tar > gzip > buf + zr := gzip.NewWriter(fd) + tw := tar.NewWriter(zr) + + // is file a folder? + fi, err := os.Stat(src) + if err != nil { + return err + } + mode := fi.Mode() + if mode.IsRegular() { + // get header + header, err := tar.FileInfoHeader(fi, src) + if err != nil { + return err + } + // write header + if err := tw.WriteHeader(header); err != nil { + return err + } + // get content + data, err := os.Open(src) + if err != nil { + return err + } + if _, err := io.Copy(tw, data); err != nil { + return err + } + } else if mode.IsDir() { // folder + + // walk through every file in the folder + filepath.Walk(src, func(file string, fi os.FileInfo, err error) error { + // generate tar header + header, err := tar.FileInfoHeader(fi, file) + if err != nil { + return err + } + + filename, _ := filepath.Rel(filepath.Dir(src), filepath.ToSlash(file)) + header.Name = filename + + // write header + if err := tw.WriteHeader(header); err != nil { + return err + } + // if not a dir, write file content + if !fi.IsDir() { + data, err := os.Open(file) + if err != nil { + return err + } + if _, err := io.Copy(tw, data); err != nil { + return err + } + } + return nil + }) + } else { + return fmt.Errorf("error: file type not supported") + } + + // produce tar + if err := tw.Close(); err != nil { + return err + } + // produce gzip + if err := zr.Close(); err != nil { + return err + } + + return nil +}