Commits

Liam Staskawicz committed 79220a2

initial commit: uploads working, and plausible error handling

Comments (0)

Files changed (5)

+
+# executables
+bbpost
+*.exe
+
+# bbpost
+
+(hopefully temporary) workaround for posting download artifacts to bitbucket projects.
+
+This is a bit terrible, but bbpost scrapes the contents of the 'downloads' page for a project on bitbucket, and uploads a given file to be hosted there. As a result, the format of the form upload may change and bbpost may stop working.
+
+With any luck, a proper API will be provided for this, rendering bbpost obsolete. This is being tracked in [bitbucket issue #1315](https://bitbucket.org/site/master/issue/3251/add-custom-file-uploads-to-rest-api-bb).
+
+### install
+
+Requires a Go installation for now. Maybe host binaries if it's worth it?
+
+    $ go get bitbucket.org/liamstask/bbpost
+
+This will install the `bbpost` binary to your `$GOPATH/bin` directory.
+
+### usage
+
+    $ bbpost -user username -pass password -proj projectname -post /path/to/file
+package main
+
+import (
+	"errors"
+	"flag"
+	"fmt"
+	"log"
+	"os"
+	"path/filepath"
+)
+
+var (
+	flagUser = flag.String("user", "", "bitbucket username")
+	flagPass = flag.String("pass", "", "bitbucket password")
+	flagProj = flag.String("proj", "", "bitbucket project name")
+	flagPost = flag.String("post", "", "path to the resource to post")
+)
+
+func usage() {
+	log.Println("usage: bbpost -user username -pass password -proj projectname -post /path/to/resource")
+}
+
+type Options struct {
+	User, Pass, Proj, Post string
+}
+
+// default to flags, and override from env
+func getOptions() (*Options, error) {
+
+	flag.Parse()
+
+	opts := &Options{
+		User: *flagUser,
+		Pass: *flagPass,
+		Proj: *flagProj,
+		Post: *flagPost,
+	}
+
+	// XXX: read from env as well
+
+	for k, v := range map[string]string{"user": opts.User, "pass": opts.Pass, "proj": opts.Proj, "post": opts.Post} {
+		if v == "" {
+			return opts, errors.New(fmt.Sprintf("bbpost: %s not specified", k))
+		}
+	}
+
+	return opts, nil
+}
+
+func main() {
+
+	opts, err := getOptions()
+	if err != nil {
+		log.Println(err)
+		flag.Usage()
+		os.Exit(1)
+	}
+
+	csrftoken, err := scrapeCRSFToken(opts)
+	if err != nil {
+		log.Fatal(err)
+	}
+
+	if err = upload(opts, csrftoken); err != nil {
+		log.Fatal(err)
+	}
+
+	log.Println(fmt.Sprintf("bbpost: uploaded %s to bitbucket.org/%s/%s", opts.Post, opts.User, filepath.Base(opts.Proj)))
+}
+package main
+
+import (
+	"fmt"
+	"net/http"
+
+	"code.google.com/p/go.net/html"
+)
+
+func scrapeCRSFToken(o *Options) (string, error) {
+
+	downloadsURL := fmt.Sprintf("https://bitbucket.org/%s/%s/downloads", o.User, o.Proj)
+	req, err := http.NewRequest("GET", downloadsURL, nil)
+	if err != nil {
+		return "", err
+	}
+
+	req.SetBasicAuth(o.User, o.Pass)
+
+	resp, err := http.DefaultClient.Do(req)
+	if err != nil {
+		return "", err
+	}
+	defer resp.Body.Close()
+
+	doc, err := html.Parse(resp.Body)
+	if err != nil {
+		return "", err
+	}
+
+	return extractCRSFToken(doc), nil
+}
+
+func extractCRSFToken(n *html.Node) string {
+
+	crsftoken := ""
+
+	var f func(*html.Node)
+	f = func(n *html.Node) {
+		if n.Type == html.ElementNode && n.Data == "input" {
+
+			token := ""
+			sawtoken := false
+
+			for _, a := range n.Attr {
+				switch a.Key {
+				case "name":
+					if a.Val == "csrfmiddlewaretoken" {
+						sawtoken = true
+					}
+				case "value":
+					token = a.Val
+				}
+			}
+
+			if sawtoken {
+				crsftoken = token
+				return
+			}
+		}
+		for c := n.FirstChild; c != nil; c = c.NextSibling {
+			f(c)
+		}
+	}
+	f(n)
+
+	return crsftoken
+}
+package main
+
+import (
+	"bytes"
+	"errors"
+	"fmt"
+	"io"
+	"mime/multipart"
+	"net/http"
+	"os"
+	"path/filepath"
+	"strings"
+)
+
+func upload(o *Options, crsftoken string) error {
+
+	req, err := createMultipartPostReq(o, crsftoken)
+	if err != nil {
+		return err
+	}
+
+	client := &http.Client{}
+	// see if we're being redirected to sign in - indicates auth is not correct
+	client.CheckRedirect = func(req *http.Request, via []*http.Request) error {
+		if strings.HasPrefix(req.URL.Path, "/account/signin") {
+			return errors.New("user/pass appear to be incorrect")
+		}
+		return nil
+	}
+
+	res, err := client.Do(req)
+	if err != nil {
+		return err
+	}
+
+	if res.StatusCode != http.StatusOK {
+		return errors.New(fmt.Sprintf("%s: %s", res.Request.URL, res.Status))
+	}
+
+	return nil
+}
+
+func createMultipartPostReq(o *Options, crsftoken string) (*http.Request, error) {
+	var b bytes.Buffer
+	w := multipart.NewWriter(&b)
+
+	f, err := os.Open(o.Post)
+	if err != nil {
+		return nil, err
+	}
+
+	fw, err := w.CreateFormFile("file", filepath.Base(o.Post))
+	if err != nil {
+		return nil, err
+	}
+
+	// pr, pw := io.Pipe()
+
+	if _, err = io.Copy(fw, f); err != nil {
+		return nil, err
+	}
+
+	// add any other fields
+	params := map[string]string{
+		"csrfmiddlewaretoken": crsftoken,
+	}
+
+	for k, v := range params {
+		if err = w.WriteField(k, v); err != nil {
+			return nil, err
+		}
+	}
+	w.Close()
+
+	uploadURL := fmt.Sprintf("https://bitbucket.org/%s/%s/downloads", o.User, o.Proj)
+	req, err := http.NewRequest("POST", uploadURL, &b)
+	if err != nil {
+		return nil, err
+	}
+	req.Header.Set("Content-Type", w.FormDataContentType())
+	req.SetBasicAuth(o.User, o.Pass)
+
+	return req, nil
+}