Skip to content

Commit

Permalink
Refactor, and add framework that will allow me to add BitBucket and G…
Browse files Browse the repository at this point in the history
…itLab support easily
  • Loading branch information
tempor1s committed Mar 10, 2020
1 parent 2007220 commit 111753e
Show file tree
Hide file tree
Showing 6 changed files with 192 additions and 151 deletions.
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -28,3 +28,4 @@ gobackup
main
~./.vimdid/
~.
tempor1s/
2 changes: 2 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -28,6 +28,8 @@ TODO
- [x] Clone a single repository to your computer through CLI.
- [x] Clone multiple repositories to local computer
- [x] Clone multiple reposiories using concurrency
- [ ] Add support for cloning GitLab repos
- [ ] Add support for cloning BitBucket repos
- [ ] Upload cloned repositories to other services like GitLab or BitBucket
- [ ] Do the above concurrently
- [ ] Do the reverse, pull repos from BitBucket / GitLab and clone them or upload them to GitHub. Ideally want to be platform agnostic.
Expand Down
182 changes: 32 additions & 150 deletions backup/backup.go
Original file line number Diff line number Diff line change
@@ -1,190 +1,72 @@
package backup

import (
"context"
"fmt"
"io/ioutil"
"log"
"os"
"path"
"sync"

"github.com/google/go-github/github"
"github.com/schollz/progressbar/v2"
"golang.org/x/oauth2"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing/transport/http"
)

// GitHub will clone all users github repos to your local machine. Puts them in a <github_username>/ folder.
func GitHub(token string, args []string) {
// Start will start the command, handle arguments, and dispatch the correct handler
func Start(token string, args []string) {
if len(args) == 0 {
fmt.Println("Please enter a URL for a GitHub user. Example: `gobackup backup github.com/tempor1s`")
fmt.Println("Please pass the provider that you want to clone from. (github/gitlab/bitbucket) Example: `backup github`")
return
} else if len(args) == 1 {
fmt.Println("Please pass in the username that you would like to clone from. Example: `backup github tempor1s`")
return
}

if token == "" {
// TODO: Support non-access token based request
fmt.Println("WARNING: Personal token was not passed in. Please pass in a token using --token - Support for NON-token download coming soon.")
fmt.Println("WARNING: Personal token was not passed in, only cloning public repos. If you want to clone private repos, please supply a token using --token")
}

fmt.Printf("Backing up your repos... Please wait - Don't worry if the bar freezes, this could take a few minutes :)\n\n")

// Get the URL to clone
userURL := args[0]

// Get the users github name for the directory
dirName := path.Base(userURL)
// Check the service to backup and then dispatch to the correct handler with token and username
switch args[0] {
case "github":
gitHub(token, args[1])
case "gitlab":
gitLab(token, args[1])
case "bitbucket":
bitBucket(token, args[1])
}
}

// gitHub will clone all of a users github repos to your local machine. Puts them in a <github_username>/ folder.
func gitHub(token string, username string) {
// Start timer, create wait group so we dont exit early, and create channel for URL's
repos := make(chan string)
repoChan := make(chan int)
var wg sync.WaitGroup

// Get all repos for the user
go getRepos(token, dirName, repos, repoChan, &wg)
go getGithubRepos(token, username, repos, repoChan, &wg)

// Get length of repos for the max of our progress bar
repoCount := <-repoChan
bar := progressbar.NewOptions(repoCount, progressbar.OptionSetRenderBlankState(true))

// Clone all repos
cloneRepos(repos, bar, dirName, token, &wg)
cloneRepos(repos, bar, username, token, &wg)
// Wait until all repos have been cloned before printing time and exiting
wg.Wait()

// TODO: Refactor this out
size := dirSize(dirName)
sizeStr := fmt.Sprintf("%.2f MB", size)

if size > 1000 {
size = size / 1000
sizeStr = fmt.Sprintf("%.2f GB", size)
}
// Get the total size of all the cloned directories and print information
size := getDirSizeStr(username)

fmt.Printf("\n\nCloning repos complete. Cloned %d repos with a total size of %s\n", repoCount+1, sizeStr)
fmt.Printf("\n\nCloning repos complete. Cloned %d repos with a total size of %s\n", repoCount+1, size)
}

// getRepos will get all the repos for a user
func getRepos(token, userName string, c chan string, count chan int, wg *sync.WaitGroup) {
// Set up OAuth token stuff
ctx := context.Background()
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)

tc := oauth2.NewClient(ctx, ts)

// Create a new github client using the OAuth2 token or no token
var client *github.Client
if token != "" {
client = github.NewClient(tc)
} else {
client = github.NewClient(nil)
}

// Options for our request to GitHub. This will have the API only return repos that we own, and with no pagination
opt := &github.RepositoryListOptions{
Affiliation: "owner",
ListOptions: github.ListOptions{
PerPage: 100000,
},
}

// Get all repos that the user owns
var repos []*github.Repository
var err error

if token != "" {
// Get private repos if we have a token
repos, _, err = client.Repositories.List(ctx, "", opt)
} else {
// Get only public repos if we have no token, allowing us to clone other peoples repos as well
repos, _, err = client.Repositories.List(ctx, userName, opt)
}

if err != nil {
log.Fatal(err)
}

// Send length of bar to channel to use for ProgressBar
count <- len(repos)

// Add all repos to the channel
for _, repo := range repos {
c <- *repo.HTMLURL
wg.Add(1)
}

// Close the channel, our other function will still be able to receive what is already inside of it, and this prevents a deadlock
close(c)
// gitLab will clone all of a users gitlab repos to your local machine. Puts them in a <gitlab_username>/ folder.
func gitLab(token string, username string) {
// TODO
fmt.Println("Coming soon!")
}

// cloneRepos will clone all the repos in a given string slice of repo URLS
func cloneRepos(repos chan string, bar *progressbar.ProgressBar, dirName, token string, wg *sync.WaitGroup) {
// Create username dir to put all cloned repos in.
err := os.MkdirAll(dirName, os.ModePerm)

if err != nil {
log.Fatal(err)
}

// Clone each repo in the channel
for repo := range repos {
go cloneWorker(repo, dirName, token, wg, bar)
}
}

// cloneWorker will clone the given repository
func cloneWorker(repo, dirName, token string, wg *sync.WaitGroup, bar *progressbar.ProgressBar) {
// fmt.Printf("[gobackup] cloning %s\n", repo)

// Decrement the waitgroup count when we are finished cloning the repository
defer bar.Add(1)
defer wg.Done()
// Get the name of the repo we are cloning
repoName := path.Base(repo)
// Dirname which will be <github_username>/<repo_name>
dirName = dirName + "/" + repoName

// Setup auth
var auth *http.BasicAuth
if token != "" {
// If we have a token
auth = &http.BasicAuth{
Username: "gobackup",
Password: token,
}
} else {
// If we have no token, we dont want to use any auth
auth = nil
}
// Clone the repository
_, err := git.PlainClone(dirName, false, &git.CloneOptions{
Auth: auth,
URL: repo,
})

if err != nil {
log.Fatal(err)
}
}

var fileSize float64

func dirSize(dirName string) float64 {
allFiles, err := ioutil.ReadDir(dirName)

if err != nil {
log.Fatal(err)
}

for _, file := range allFiles {
if file.IsDir() {
dirSize(dirName + "/" + file.Name())
}
fileSize += float64(file.Size()) / 1000000.0
}

return fileSize
// bitBucket will clone all of a users bitbucket repos to your local machine. Puts them in a <bitbucket_username>/ folder.
func bitBucket(token string, username string) {
// TODO
fmt.Println("Coming soon!")
}
111 changes: 111 additions & 0 deletions backup/repos.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
package backup

import (
"context"
"os"
"path"
"sync"

"github.com/google/go-github/github"
"github.com/schollz/progressbar/v2"
"golang.org/x/oauth2"
"gopkg.in/src-d/go-git.v4"
"gopkg.in/src-d/go-git.v4/plumbing/transport/http"
)

// getGithubRepos will get all a users github repos
func getGithubRepos(token, username string, repoChan chan string, totalRepos chan int, wg *sync.WaitGroup) {
// Set up OAuth token stuff
ctx := context.Background()
ts := oauth2.StaticTokenSource(
&oauth2.Token{AccessToken: token},
)

tc := oauth2.NewClient(ctx, ts)

// Create a new github client using the OAuth2 token or no token
var client *github.Client
if token != "" {
client = github.NewClient(tc)
} else {
client = github.NewClient(nil)
}

// Options for our request to GitHub. This will have the API only return repos that we own, and with no pagination
opt := &github.RepositoryListOptions{
Affiliation: "owner",
ListOptions: github.ListOptions{
PerPage: 100000,
},
}

// Get all repos that the user owns
var repos []*github.Repository
var err error

if token != "" {
// Get private repos if we have a token
repos, _, err = client.Repositories.List(ctx, "", opt)
} else {
// Get only public repos if we have no token, allowing us to clone other peoples repos as well
repos, _, err = client.Repositories.List(ctx, username, opt)
}
checkIfError(err)

// Send length of bar to channel to use for ProgressBar
totalRepos <- len(repos)

// Add all repos to the channel
for _, repo := range repos {
repoChan <- *repo.HTMLURL
wg.Add(1)
}

// Close repo list chanel, our other function will still be able to receive what is already inside of it, and this prevents a deadlock or infinite loop
close(repoChan)
}

// cloneRepos will clone all the repos in a given string slice of repo URLS
func cloneRepos(repos chan string, bar *progressbar.ProgressBar, dirName, token string, wg *sync.WaitGroup) {
// Create username dir to put all cloned repos in.
err := os.MkdirAll(dirName, os.ModePerm)
checkIfError(err)

// Clone each repo in the channel
for repo := range repos {
go cloneWorker(repo, dirName, token, wg, bar)
}
}

// cloneWorker will clone the given repository
func cloneWorker(repo, dirName, token string, wg *sync.WaitGroup, bar *progressbar.ProgressBar) {
// fmt.Printf("[gobackup] cloning %s\n", repo)

// Decrement the waitgroup count when we are finished cloning the repository and increment our progress bar
defer bar.Add(1)
defer wg.Done()
// Get the name of the repo we are cloning
repoName := path.Base(repo)
// Dirname which will be <github_username>/<repo_name>
dirName = dirName + "/" + repoName

// Setup auth if we have a token
var auth *http.BasicAuth
if token != "" {
// If we have a token
auth = &http.BasicAuth{
Username: "gobackup",
Password: token,
}
} else {
// If we have no token, we dont want to use any auth
auth = nil
}
// Clone the repository
_, err := git.PlainClone(dirName, false, &git.CloneOptions{
Auth: auth,
URL: repo,
})

checkIfError(err)
}
45 changes: 45 additions & 0 deletions backup/utils.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
package backup

import (
"fmt"
"io/ioutil"
"log"
)

// Get the size of a directory in the format of a string.
func getDirSizeStr(dirName string) string {
size := dirSize(dirName)
sizeStr := fmt.Sprintf("%.2f MB", size)

if size > 1000 {
size = size / 1000
sizeStr = fmt.Sprintf("%.2f GB", size)
}

return sizeStr
}

// fileSize is a var we keep because our other method is recursive, and it just makes life easier
var fileSize float64

// Get the size of a directory in MB, returns a float
func dirSize(dirName string) float64 {
allFiles, err := ioutil.ReadDir(dirName)
checkIfError(err)

for _, file := range allFiles {
if file.IsDir() {
dirSize(dirName + "/" + file.Name())
}
fileSize += float64(file.Size()) / 1000000.0
}

return fileSize
}

// Just a simple check to see if there is an error, because I was re-writing this code too much
func checkIfError(e error) {
if e != nil {
log.Fatal(e)
}
}

0 comments on commit 111753e

Please sign in to comment.