Skip to content

Commit

Permalink
Initial Commit
Browse files Browse the repository at this point in the history
  • Loading branch information
LeoAdamek committed Feb 7, 2018
0 parents commit c32509d
Show file tree
Hide file tree
Showing 5 changed files with 677 additions and 0 deletions.
45 changes: 45 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
JIRA Replicator
===============

A library (and tool) to automatically back-up your JIRA instance.

Especially handy for JIRA Cloud users.


Tool Usage
----------

### Configuration

Configuration is done using the following environment variables.

* `JIRA_URL`
* `JIRA_USERNAME`
* `JIRA_PASSWORD`

### Usage

Generate the backup using:

$ jira-backup backup

Download the backup using:

$ jira-backup download -o FILE_PATH

Copy the backup to S3:

$ jira-backup s3

Run it on a loop to make a backup every 48 hours:

$ jira-backup daemon

Please see `jira-backup help s3` for full docs on all available options and how to set defaults.

Backups on JIRA cloud are limited to once per 48 hours (after completion of previous backup).
If you exceed this you will be notified of when you can re-try the request.

### Library Usage

See godoc.
49 changes: 49 additions & 0 deletions client/backup.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,49 @@
package client

import (
"time"
"fmt"
"regexp"
"strings"
)

// BackupRateExceeded is an error which means that the
// rate limit on the backup API has been exceeded.
type BackupRateExceeded struct {
retryAt time.Time
}

func (e BackupRateExceeded) Error() string {
return fmt.Sprintf("Backup rate exceeded. Retry in %s", e.RetryIn())
}


func (e BackupRateExceeded) RetryIn() time.Duration {
return e.retryAt.Sub(time.Now())
}

func (e BackupRateExceeded) RetryAt() time.Time {
return e.retryAt
}

// FromResponse creates a BackupRateExceeded from the given text.
// It will attempt to extract the retry time from the text.
func (BackupRateExceeded) FromResponse(text string) BackupRateExceeded {

// Helpfully Atlassian gives the response time in a format easily passed
// to time.ParseDuration
exp := regexp.MustCompile("((\\d+)h)?((\\d+)m)?")

matches := exp.FindAllString(text, -1)
durationStr := strings.Join(matches, "")

dur, err := time.ParseDuration(durationStr)

if err == nil {
return BackupRateExceeded {
retryAt: time.Now().Round(time.Minute).Add(dur),
}
}

return BackupRateExceeded{retryAt: time.Now()}
}
223 changes: 223 additions & 0 deletions client/client.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,223 @@
package client

import (
"net/url"
"net/http"
"log"
"os"
"io/ioutil"
"encoding/json"
"errors"
"bytes"

"io"
"time"
"strconv"
)

// Client represents a JIRA backup client
type Client struct {
BaseURL url.URL
HTTP *http.Client
Log *log.Logger

username string
password string
}

type BackupStatus struct {
Status string `json:"status"`
Progress int `json:"progress"`
DownloadPath string `json:"result"`
}


// New creates a new JIRA Backup Client
func New(baseURL *url.URL, username, password string) *Client {

return &Client{
BaseURL: *baseURL,
HTTP: &http.Client{Transport: http.DefaultTransport},
Log: log.New(os.Stdout, "jira-client ", log.LstdFlags),

username: username,
password: password,
}
}

func (c Client) makeRequest(req *http.Request) (*http.Response, error) {

if req.Header == nil {
req.Header = http.Header{}
}

req.Header.Set("User-Agent", "Jira Replicator +https://bitbucket.org/mr-zen/jira-replicator")
req.SetBasicAuth(c.username, c.password)

st := time.Now()
res, err := c.HTTP.Do(req)
dt := time.Now().Sub(st)

if err == nil {
c.Log.Println(req.Method, req.URL, res.StatusCode, res.Header.Get("Content-Length"), dt)
}

return res, err
}

// CreateBackup creates a new JIRA backup.
func (c Client) CreateBackup(includeAttachments bool) error {

u := c.BaseURL
u.Path = "/rest/backup/1/export/runbackup"

req := &http.Request{
Method: http.MethodPost,
URL: &u,
Header: http.Header{
"Accept": []string{"application/json"},
"Content-Type": []string{"application/json"},
},
}

params := make(map[string]bool)
params["cbAttachments"] = includeAttachments
body, _ := json.Marshal(params)

req.Body = ioutil.NopCloser(bytes.NewReader(body))

res, err := c.makeRequest(req)

if err != nil {
return err
}

if res.StatusCode >= 400 {
if res.StatusCode == http.StatusPreconditionFailed /* 412 */ {
// We got us some json, let's get some deets.
body, err := ioutil.ReadAll(res.Body)

if err != nil {
return err
}

respData := make(map[string]string)

err = json.Unmarshal(body, &respData)

if err != nil {
return err
}

return BackupRateExceeded{}.FromResponse(respData["error"])
}

content, _ := ioutil.ReadAll(res.Body)

return errors.New(string(content))
}



return err
}

// Download backup downloads the current JIRA backup
func (c Client) DownloadBackup() (io.ReadCloser, int, error) {

// Check the backup is ready and waiting.
status, err := c.GetBackupStatus()

if err != nil {
return nil, 0, err
}

if status.Status != "Success" {
return nil, 0, errors.New(status.Status)
}

downloadURL, err := url.Parse(status.DownloadPath)

u := c.BaseURL
u.Path = "/plugins/servlet/"+downloadURL.Path
u.RawQuery = downloadURL.Query().Encode()

req := &http.Request{
Method: http.MethodGet,
URL: &u,
}

res, err := c.makeRequest(req)

if err != nil {
return nil, 0, err
}

length, _ := strconv.ParseInt(res.Header.Get("Content-Length"), 10, 64)


return res.Body, int(length), nil
}

func (c Client) GetBackupStatus() (BackupStatus, error) {

u := c.BaseURL
u.Path = "/rest/backup/1/export/lastTaskId"
q := u.Query()
q.Set("_", strconv.Itoa(int(time.Now().Unix())))
u.RawQuery = q.Encode()

// First determine the task ID.
req := &http.Request{
Method: http.MethodGet,
URL: &u,
Header: http.Header{
"Accept": []string{"application/json"},
},
}

res, err := c.makeRequest(req)

if err != nil {
return BackupStatus{}, err
}

if res.StatusCode >= 400 {
return BackupStatus{}, errors.New(res.Status)
}

body, _ := ioutil.ReadAll(res.Body)
taskId := string(body)

// Get the status
u = c.BaseURL
u.Path = "/rest/backup/1/export/getProgress"
q = u.Query()
q.Set("taskId", taskId)
q.Set("_", strconv.Itoa(int(time.Now().Unix())))
u.RawQuery = q.Encode()

req = &http.Request{
Method: http.MethodGet,
URL: &u,
Header: http.Header{
"Accept": []string{"application/json"},
},
}

res, err = c.makeRequest(req)

if err != nil {
return BackupStatus{}, err
}

body, _ = ioutil.ReadAll(res.Body)

var b BackupStatus
err = json.Unmarshal(body, &b)

return b, err

return b, err

}
7 changes: 7 additions & 0 deletions jira.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
[s3]
region = "eu-west-1"
bucket = "YOUR_JIRA_BACKUP_BUCKET"
storage_class = "STANDARD_IA" # Recommended for backups.

[kms]
key = "arn:aws:kms:eu-west-1:AWS_ACCOUNT_ID:key/KEY_UUID" # Set this to enable SSE.
Loading

0 comments on commit c32509d

Please sign in to comment.