Skip to content

Instantly share code, notes, and snippets.

@negast
Last active November 7, 2022 06:18
Show Gist options
  • Star 1 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save negast/8d4f8b6512c4835b2d62670fbf8bdc3f to your computer and use it in GitHub Desktop.
Save negast/8d4f8b6512c4835b2d62670fbf8bdc3f to your computer and use it in GitHub Desktop.
nexus copy assets from instance to instance
module nexus_copy
go 1.14
require (
github.com/cavaliercoder/grab v2.0.0+incompatible
gopkg.in/yaml.v2 v2.4.0
)
package main
import (
"bytes"
"encoding/json"
"flag"
"fmt"
"github.com/cavaliercoder/grab"
"gopkg.in/yaml.v2"
"io"
"io/ioutil"
"log"
"mime/multipart"
"net/http"
"os"
"strings"
)
var (
conf = flag.String("conf", "config.yaml", "The path to the config file.")
)
type NexusAssetsCollection struct {
Items []NexusAsset `json:"items"`
Token string `json:"continuationToken"`
}
type NexusAsset struct {
DownloadURL string `json:"downloadUrl"`
Path string `json:"path"`
ID string `json:"id"`
Repository string `json:"repository"`
Format string `json:"format"`
Name string `json:"name"`
Checksum struct {
Sha1 string `json:"sha1"`
Sha512 string `json:"sha512"`
Sha256 string `json:"sha256"`
Md5 string `json:"md5"`
} `json:"checksum"`
}
type Conf struct {
WorkDir string `yaml:"work_dir"`
Repo1 string `yaml:"repo1"`
Url1 string `yaml:"url1"`
User1 string `yaml:"user1"`
Pass1 string `yaml:"pass1"`
Repo2 string `yaml:"repo2"`
Url2 string `yaml:"url2"`
User2 string `yaml:"user2"`
Pass2 string `yaml:"pass2"`
}
func (c *Conf) GetConf(configFile string) *Conf {
yamlFile, err := ioutil.ReadFile(configFile)
if err != nil {
log.Printf("yamlFile.Get err #%v ", err)
}
err = yaml.Unmarshal(yamlFile, c)
if err != nil {
log.Println("Unmarshal: %v", err)
}
return c
}
func downloadAssets(c Conf, assets *NexusAssetsCollection) {
for index, a :=range assets.Items {
resp, err := grab.Get(c.WorkDir, a.DownloadURL)
if err != nil {
log.Println(err.Error())
}
//assets.Items[index].Name = resp.Filename
assets.Items[index].Name = strings.TrimPrefix(resp.Filename, "assets\\")
}
}
func GetAssets(c Conf, token string) NexusAssetsCollection {
url := c.Url1 + "/service/rest/v1/assets?repository=" + c.Repo1
if token != "" {
url += "&continuationToken=" + token
}
spaceClient := http.Client{}
req, err := http.NewRequest(http.MethodGet, url, nil)
if err != nil {
log.Println(err)
}
req.Header.Set("Content-Type", "application/json; charset=UTF-8")
req.SetBasicAuth(c.User1, c.Pass1)
res, getErr := spaceClient.Do(req)
if getErr != nil {
log.Println(getErr)
}
if res.Body != nil {
defer res.Body.Close()
}
body, readErr := ioutil.ReadAll(res.Body)
if readErr != nil {
log.Fatal(readErr)
}
//Convert the body to type string
//sb := string(body)
//log.Printf(sb)
assets := NexusAssetsCollection{}
jsonErr := json.Unmarshal(body, &assets)
if jsonErr != nil {
log.Fatal(jsonErr)
}
if assets.Token != "" {
a2 := GetAssets(c, assets.Token)
assets.Items = append(assets.Items, a2.Items...)
}
return assets
}
func uploadAssets(c Conf, assets *NexusAssetsCollection) {
for _, a :=range assets.Items {
uploadRawToNexus(c, a)
}
}
func main() {
var c Conf
c.GetConf(*conf)
if c.Url1 == "" {
c.Url1 = "http://localhost:9090/nexus"
}
if c.Url2 == "" {
c.Url2 = "http://localhost:8081/nexus"
}
if c.Repo1 == "" {
c.Repo1 = "packages"
}
if c.Repo2 == "" {
c.Repo2 = c.Repo1
}
if c.User1 == "" {
c.User1 = "admin"
}
if c.User2 == "" {
c.User2 = c.User1
}
c.WorkDir = "./assets"
os.Mkdir("assets/", 0755)
assets := GetAssets(c, "")
downloadAssets(c, &assets)
uploadAssets(c, &assets)
}
func uploadRawToNexus(c Conf, a NexusAsset) {
// https://stackoverflow.com/questions/20205796/post-data-using-the-content-type-multipart-form-data
var client http.Client
u := fmt.Sprintf("%s/service/rest/v1/components?repository=%s", c.Url2, c.Repo2)
remoteURL := u
//prepare the reader instances to encode
dir := strings.TrimSuffix(a.Path, a.Name)
values := map[string]io.Reader{
a.Format + ".asset": mustOpen(c.WorkDir + "/" + a.Name), // lets assume its this file
a.Format + ".asset.filename": strings.NewReader(a.Name),
}
if dir != "" {
values = map[string]io.Reader{
a.Format + ".asset": mustOpen(c.WorkDir + "/" + a.Name), // lets assume its this file
a.Format + ".asset.filename": strings.NewReader(a.Name),
a.Format + ".directory": strings.NewReader(dir),
}
}
if a.Format == "nuget" {
values = map[string]io.Reader{
a.Format + ".asset": mustOpen(c.WorkDir + "/" + a.Name), // lets assume its this file
}
}
err := Upload(&client, remoteURL, values, c)
if err != nil {
log.Println(err.Error())
}
}
func Upload(client *http.Client, url string, values map[string]io.Reader, c Conf) (err error) {
// Prepare a form that you will submit to that URL.
var b bytes.Buffer
w := multipart.NewWriter(&b)
for key, r := range values {
var fw io.Writer
if x, ok := r.(io.Closer); ok {
defer x.Close()
}
// Add an image file
if x, ok := r.(*os.File); ok {
if fw, err = w.CreateFormFile(key, x.Name()); err != nil {
return
}
} else {
// Add other fields
if fw, err = w.CreateFormField(key); err != nil {
return
}
}
if _, err = io.Copy(fw, r); err != nil {
return err
}
}
// Don't forget to close the multipart writer.
// If you don't close it, your request will be missing the terminating boundary.
w.Close()
// Now that you have a form, you can submit it to your handler.
req, err := http.NewRequest("POST", url, &b)
if err != nil {
return
}
// Don't forget to set the content type, this will contain the boundary.
req.Header.Set("Content-Type", w.FormDataContentType())
req.SetBasicAuth(c.User2, c.Pass2)
// Submit the request
res, err := client.Do(req)
if err != nil {
return
}
// Check the response
if res.StatusCode != http.StatusOK && res.StatusCode != http.StatusNoContent {
body, _ := ioutil.ReadAll(res.Body)
err = fmt.Errorf("bad status: %s", res.Status)
log.Println(string(body))
}
return
}
func mustOpen(f string) *os.File {
r, err := os.Open(f)
if err != nil {
log.Fatal(err)
}
return r
}
@LemonChicken
Copy link

Hi, any tutorials on how to use this? i managed to get the dependencies down, but it still wont run, i noticed it needed a config.yaml how would the structure be for this?

@negast
Copy link
Author

negast commented Nov 7, 2022

I ran this from my IDEA the config file looks something like this for me:

---
pass1: 'pass1'
pass2: 'pass2'

You can see in the code on line 138 to ... that it sets some default config that was ok for me. The same is repeated on line 44-54
if c.Url1 == "" {
c.Url1 = "http://localhost:9090/nexus"
}
if c.Url2 == "" {
c.Url2 = "http://localhost:8081/nexus"
}
if c.Repo1 == "" {
c.Repo1 = "packages"
}
if c.Repo2 == "" {
c.Repo2 = c.Repo1
}
if c.User1 == "" {
c.User1 = "admin"
}
if c.User2 == "" {
c.User2 = c.User1
}

this would translate in config.yaml to

---
url1: 'value'
url2: 'value'
user1: 'value'
user2: 'value'
repo1:  'value'
repo2: 'value'
pass1: 'pass1'
pass2: 'pass2'
work_dir: 'assets'

In theory I guess you run go build to build this script as well. And then just run it from the local directory. The config.yaml should be in the local dir. I adjusted on line of code my IDEA was giving trouble on.

I only tested this for raw assets though as uploading rpm's/etc might be different upload forms. I do also have a script somewhere to recreate my yum repos etc from instances. But I don't reupload my local ones.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment