diff --git a/Dockerfile b/Dockerfile new file mode 100755 index 0000000..38e3088 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,9 @@ +FROM golang:1.9 + +WORKDIR /go/src/github.com/sjeandeaux/nexus-cli +COPY . . + +RUN go-wrapper download # "go get -d -v ./..." +RUN go-wrapper install # "go install -v ./..." + +ENTRYPOINT ["go-wrapper" , "run"] \ No newline at end of file diff --git a/README.md b/README.md new file mode 100755 index 0000000..5698367 --- /dev/null +++ b/README.md @@ -0,0 +1,29 @@ +# nexus-cli + +## upload + +I share a volume where i have my upload.jar file. + +```bash + +#run your own nexus +docker run -d -P --name nexus sonatype/nexus3:3.5.1 +docker build --tag sjeandeaux/nexus-cli . +docker run --link nexus:nexus -ti -v $(pwd):$(pwd):ro sjeandeaux/nexus-cli \ + -repo=http://nexus:8081/repository/maven-releases \ + -user=admin \ + -password=admin123 \ + -file=$(pwd)/upload.jar \ + -groupID=com.jeandeaux \ + -artifactID=elyne \ + -version=0.1.0 \ + -hash md5 \ + -hash sha1 + +``` + +## TODOs + +- [ ] interface for artifact +- [ ] interface for repository +- [ ] integration tests diff --git a/log/log.go b/log/log.go new file mode 100755 index 0000000..2a6115f --- /dev/null +++ b/log/log.go @@ -0,0 +1,9 @@ +package log + +import ( + "io/ioutil" + "log" +) + +// Logger logger of application. +var Logger *log.Logger = log.New(ioutil.Discard, "[☯ nexus-cli ☯] ⇒ ", log.Ldate|log.Ltime|log.LUTC) diff --git a/main.go b/main.go new file mode 100755 index 0000000..6c660d9 --- /dev/null +++ b/main.go @@ -0,0 +1,72 @@ +package main + +import ( + "flag" + + "fmt" + + "github.com/sjeandeaux/nexus-cli/log" + "github.com/sjeandeaux/nexus-cli/repositorymanager" + + "os" +) + +type enableHash []string + +func (i *enableHash) String() string { + return fmt.Sprintf("%s", *i) +} + +func (i *enableHash) Set(value string) error { + *i = append(*i, value) + return nil +} + +//commandLineArgs all parameters in command line +type commandLineArgs struct { + //url of repository to upload file + urlOfRepository string + user string + password string + //file to upload + file string + //groupID of artifact + groupID string + //artifactID of artifact + artifactID string + //version of artifact + version string + //hashs the hashs which you want to upload + hash enableHash +} + +var commandLine = &commandLineArgs{} + +//init configuration +func init() { + log.Logger.SetOutput(os.Stdout) + flag.StringVar(&commandLine.urlOfRepository, "repo", "http://localhost/repository/third-party", "url of repository") + flag.StringVar(&commandLine.user, "user", "", "user for repository") + flag.StringVar(&commandLine.password, "password", "", "password for repository") + flag.StringVar(&commandLine.file, "file", "", "your file to upload on repository") + flag.StringVar(&commandLine.groupID, "groupID", "com.jeandeaux", "groupid of artifact") + flag.StringVar(&commandLine.artifactID, "artifactID", "elyne", "artifactID of artifact") + flag.StringVar(&commandLine.version, "version", "0.1.0-SNAPSHOT", "version of artifact") + flag.Var(&commandLine.hash, "hash", "md5 or/and sha1") + flag.Parse() +} + +//main upload artifact +func main() { + + repo := repositorymanager.NewRepository(commandLine.urlOfRepository, commandLine.user, commandLine.password) + + artifact, err := repositorymanager.NewArtifact(commandLine.groupID, commandLine.artifactID, commandLine.version, commandLine.file) + if err != nil { + log.Logger.Fatal(err) + } + + if err := repo.UploadArtifact(artifact, commandLine.hash...); err != nil { + log.Logger.Fatal(err) + } +} diff --git a/repositorymanager/repositorymanager.go b/repositorymanager/repositorymanager.go new file mode 100755 index 0000000..196f3d6 --- /dev/null +++ b/repositorymanager/repositorymanager.go @@ -0,0 +1,264 @@ +//Package repositorymanager upload a file in this repository +package repositorymanager + +import ( + "bytes" + "crypto/md5" + "crypto/sha1" + "encoding/hex" + "fmt" + "hash" + "html/template" + "io" + "net/http" + "os" + "path/filepath" + "strings" + + "net/url" + + "github.com/sjeandeaux/nexus-cli/log" + + "errors" +) + +const ( + dot = "." + suffixPom = "pom" +) + +//Repository where we want put the file +type Repository struct { + url string + user string + password string + client *http.Client + hash map[string]*repositoryHash +} + +// repositoryHash create hash and has the suffix for the file on repository +type repositoryHash struct { + suffix string + //TODO see if we need to a func or variable + hash func() hash.Hash +} + +//NewRepository create a Repository with default client HTTP. +func NewRepository(url, user, password string) *Repository { + const ( + nameMd5 = "md5" + nameSha1 = "sha1" + ) + + shaOneAndMdFive := make(map[string]*repositoryHash) + + shaOneAndMdFive[nameMd5] = &repositoryHash{ + suffix: nameMd5, + hash: func() hash.Hash { return md5.New() }, + } + + shaOneAndMdFive[nameSha1] = &repositoryHash{ + suffix: nameSha1, + hash: func() hash.Hash { return sha1.New() }, + } + + return &Repository{ + url: url, + user: user, + password: password, + client: &http.Client{}, + hash: shaOneAndMdFive} + +} + +//UploadArtifact upload ar on repository TODO goroutine to upload +//ar the artifact to upload +//hashs list of hash to send +func (n *Repository) UploadArtifact(ar *Artifact, hashs ...string) error { + pomURL := n.generateURL(ar, suffixPom) + if err := n.upload(pomURL, bytes.NewReader(ar.Pom)); err != nil { + return err + } + + fOpen, err := os.Open(ar.File) + if err != nil { + return err + } + + fileURL := n.generateURL(ar, ar.extension()) + if err := n.upload(fileURL, fOpen); err != nil { + return err + } + + for _, h := range hashs { + if iGetIt := n.hash[h]; iGetIt != nil { + n.uploadHash(ar, iGetIt) + } else { + urlIssue := generateURLIssue(h) + log.Logger.Printf("%q is not managed by the client %q", h, urlIssue) + } + } + + return nil +} + +//generateURLIssue generate the URL on github to create issue on hash method +func generateURLIssue(h string) string { + const ( + title = "Move your ass" + urlFormat = "https://github.com/sjeandeaux/nexus-cli/issues/new?title=%s&body=%s" + bodyFormat = "Could you add the hash %q lazy man?" + ) + escapedTitle := url.QueryEscape(title) + body := fmt.Sprintf(bodyFormat, h) + escapedBody := url.QueryEscape(body) + urlIssue := fmt.Sprintf(urlFormat, escapedTitle, escapedBody) + return urlIssue +} + +//generateURL generate the url of ar +// ///-. +func (n *Repository) generateURL(ar *Artifact, endOfFile string) string { + const ( + allReplacement = -1 + slash = "/" + dash = "-" + ) + + g := strings.Replace(ar.GroupID, dot, slash, allReplacement) + nameOfFile := fmt.Sprint(slash, ar.ArtifactID, dash, ar.Version, dot, endOfFile) + return fmt.Sprint(n.url, slash, g, slash, ar.ArtifactID, slash, ar.Version, nameOfFile) +} + +//uploadHash upload the hash +func (n *Repository) uploadHash(ar *Artifact, h *repositoryHash) error { + p, f, err := generateHash(ar.Pom, ar.File, h.hash) + if err != nil { + return err + } + + hashedPom := n.generateURL(ar, fmt.Sprint(suffixPom, dot, h.suffix)) + if err = n.upload(hashedPom, p); err != nil { + return err + } + + hashedFile := n.generateURL(ar, fmt.Sprint(ar.extension(), dot, h.suffix)) + return n.upload(hashedFile, f) +} + +func (n *Repository) upload(url string, data io.Reader) error { + const PUT = "PUT" + log.Logger.Print(url) + req, _ := http.NewRequest(PUT, url, data) + if n.user != "" && n.password != "" { + req.SetBasicAuth(n.user, n.password) + } + + res, err := n.client.Do(req) + if err != nil { + return err + } + defer res.Body.Close() + + if res.StatusCode != 201 { + return fmt.Errorf(res.Status) + } + return nil +} + +//generateHash generate the +func generateHash(pom []byte, file string, h func() hash.Hash) (io.Reader, io.Reader, error) { + hashedPom, err := hashSum(bytes.NewReader(pom), h()) + if err != nil { + return nil, nil, err + } + + f, errOnFile := os.Open(file) + defer f.Close() + if errOnFile != nil { + return nil, nil, errOnFile + } + + hashedFile, err := hashSum(f, h()) + if err != nil { + return nil, nil, err + } + + return hashedPom, hashedFile, nil +} + +//generate the hash of io.Reader +func hashSum(data io.Reader, h hash.Hash) (io.Reader, error) { + if _, err := io.Copy(h, data); err != nil { + return nil, err + } + return strings.NewReader(hex.EncodeToString(h.Sum(nil))), nil +} + +//Artifact the artifact +type Artifact struct { + //GroupID of artifact + GroupID string + //ArtifactID of artifact + ArtifactID string + //Version of artifact + Version string + //file to upload + File string + //pom of this artifact + Pom []byte +} + +//NewArtifact create a artifact with this own pom +func NewArtifact(groupID, artifactID, version, file string) (*Artifact, error) { + if file == "" { + return nil, errors.New("You must specify a file") + + } + + if _, err := os.Stat(file); os.IsNotExist(err) { + return nil, err + } + + a := &Artifact{ + GroupID: groupID, + ArtifactID: artifactID, + Version: version, + File: file, + } + + pom, err := a.writePom() + if err != nil { + return nil, err + } + a.Pom = pom + return a, nil +} + +// extension extension of file +func (artifact *Artifact) extension() string { + const unknown = "unknown" + + if ex := filepath.Ext(artifact.File); ex != "" { + return ex[1:] + } + return unknown +} + +// writePom write a wonderful pom +func (artifact *Artifact) writePom() ([]byte, error) { + const templateName = "pom" + const templateValue = `4.0.0{{.GroupID}}{{.ArtifactID}}{{.Version}}` + + pomTemplate, err := template.New(templateName).Parse(templateValue) + if err != nil { + return nil, err + } + + var buf bytes.Buffer + err = pomTemplate.Execute(&buf, artifact) + if err != nil { + return nil, err + } + return buf.Bytes(), nil +} diff --git a/repositorymanager/repositorymanager_test.go b/repositorymanager/repositorymanager_test.go new file mode 100755 index 0000000..f73dc7e --- /dev/null +++ b/repositorymanager/repositorymanager_test.go @@ -0,0 +1,213 @@ +package repositorymanager + +import ( + "io/ioutil" + "net/http" + "net/http/httptest" + "os" + "testing" + + lognexuscli "github.com/sjeandeaux/nexus-cli/log" + + "fmt" + "path/filepath" +) + +func init() { + lognexuscli.Logger.SetOutput(os.Stdout) +} + +const ( + expectedPom = `4.0.0com.jeandeauxelyne0.1.0-SNAPSHOT` + groupID = "com.jeandeaux" + artifactID = "elyne" + version = "0.1.0-SNAPSHOT" +) + +func TestRepository_generateURL(t *testing.T) { + repo := NewRepository("http://in.your.anus.fr/third-party", "", "") + actual := repo.generateURL(&Artifact{GroupID: groupID, ArtifactID: artifactID, Version: version}, "jar") + expected := "http://in.your.anus.fr/third-party/com/jeandeaux/elyne/0.1.0-SNAPSHOT/elyne-0.1.0-SNAPSHOT.jar" + if actual != expected { + t.Fatal("actual", actual, "expected", expected) + } +} + +type call struct { + called bool + calledSha1 bool + calledMd5 bool + expected string + expectedSha1 string + expectedMd5 string +} + +func (c *call) allIsCalled() bool { + return c.called && c.calledMd5 && c.calledSha1 +} + +func TestRepository_UploadArtifact(t *testing.T) { + forPom := call{ + called: false, + calledSha1: false, + calledMd5: false, + expected: expectedPom, + expectedSha1: "1f396c7604363c787362e5916005a0cad72701c0", + expectedMd5: "649de9004a8b0e95a7ed1592bcf1ba8c", + } + + forFile := call{ + called: false, + calledSha1: false, + calledMd5: false, + expected: "Commodores Nightshift", + expectedSha1: "3db2e83d419582fbc443067426a6f3cf7b793bcb", + expectedMd5: "b32bc418e831a8311b25a455f584879a", + } + + file, err := ioutil.TempFile(os.TempDir(), ".jar") + file.WriteString(forFile.expected) + extension := filepath.Ext(file.Name()) + + ts := repositoryManagerImplementation(t, extension, &forPom, &forFile) + defer ts.Close() + repo := NewRepository(ts.URL, "bob", "thesponge") + + if err != nil { + t.Fatal(err) + } + a, _ := NewArtifact(groupID, artifactID, version, file.Name()) + err = repo.UploadArtifact(a, "sha1", "md5", "not-found") + if err != nil { + t.Fatal(err) + } + + if !forFile.allIsCalled() { + t.Errorf("Problem we are waiting more calls %v", forFile) + } + + if !forPom.allIsCalled() { + t.Errorf("Problem we are waiting more calls %v", forPom) + } + +} + +//repositoryManagerImplementation TODO too big for what is it +func repositoryManagerImplementation(t *testing.T, extension string, forPom *call, forFile *call) *httptest.Server { + ts := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + w.WriteHeader(201) + bodyBytes, err := ioutil.ReadAll(r.Body) + if err != nil { + t.Fatal(err) + } + actual := string(bodyBytes) + + path := "/com/jeandeaux/elyne/0.1.0-SNAPSHOT/elyne-0.1.0-SNAPSHOT" + switch r.URL.Path { + case fmt.Sprint(path, ".pom"): + forPom.called = true + if actual != forPom.expected { + if actual != forPom.expected { + t.Error("actual", actual, "expected", forPom.expected) + } + } + case fmt.Sprint(path, ".pom.sha1"): + forPom.calledSha1 = true + if actual != forPom.expectedSha1 { + if actual != forPom.expectedSha1 { + t.Error("actual", actual, "expected", forPom.expectedSha1) + } + } + + case fmt.Sprint(path, ".pom.md5"): + forPom.calledMd5 = true + if actual != forPom.expectedMd5 { + if actual != forPom.expectedMd5 { + t.Error("actual", actual, "expected", forPom.expectedMd5) + } + } + + case fmt.Sprint(path, extension): + forFile.called = true + if actual != forFile.expected { + if actual != forFile.expected { + t.Error("actual", actual, "expected", forFile.expected) + } + } + case fmt.Sprint(path, extension, ".sha1"): + forFile.calledSha1 = true + if actual != forFile.expectedSha1 { + if actual != forFile.expectedSha1 { + t.Error("actual", actual, "expected", forFile.expectedSha1) + } + } + case fmt.Sprint(path, extension, ".md5"): + forFile.calledMd5 = true + if actual != forFile.expectedMd5 { + if actual != forFile.expectedMd5 { + t.Error("actual", actual, "expected", forFile.expectedMd5) + } + } + } + + })) + return ts +} + +func TestArtifact_writePom(t *testing.T) { + a := &Artifact{GroupID: groupID, ArtifactID: artifactID, Version: version} + actualBytes, err := a.writePom() + if err != nil { + t.Fatal(err) + } + actual := string(actualBytes) + + if actual != expectedPom { + t.Fatal("actual", actual, "expected", expectedPom) + } +} + +func TestNewArtifact_Ok(t *testing.T) { + a := &Artifact{GroupID: groupID, ArtifactID: artifactID, Version: version} + if a.File != "" { + t.Fatal("actual", a.File, "expected", "nil") + } + if a.Pom != nil { + t.Fatal("actual", a.Pom, "expected", "nil") + } + + file, err := ioutil.TempFile(os.TempDir(), ".jar") + if err != nil { + t.Fatal(err) + } + + a, err = NewArtifact(groupID, artifactID, version, file.Name()) + if err != nil { + t.Fatal(err) + } + + actual := string(a.Pom) + + if actual != expectedPom { + t.Fatal("actual", actual, "expected", expectedPom) + } +} + +func TestNewArtifact_Ko_Because_Not_Found(t *testing.T) { + _, err := NewArtifact(groupID, artifactID, version, "") + if err == nil { + t.Fatal("I want a error") + } +} + +func TestNewArtifact_Ko_Because_Name_Empty(t *testing.T) { + _, err := NewArtifact(groupID, artifactID, version, "") + if err == nil { + t.Fatal("I want a error") + } + actual := err.Error() + expected := "You must specify a file" + if actual != expected { + t.Fatal("actual", actual, "expected", expected) + } +}