nikoksr/proji

View on GitHub
pkg/remote/github/github.go

Summary

Maintainability
B
4 hrs
Test Coverage
package github

import (
    "context"
    "fmt"
    "net/http"
    "net/url"
    "os"
    "path/filepath"
    "regexp"
    "strings"
    "time"

    gh "github.com/google/go-github/v31/github"
    "github.com/nikoksr/proji/internal/util"
    "github.com/nikoksr/proji/pkg/domain"
    "github.com/pkg/errors"
    "golang.org/x/oauth2"
)

const defaultTimeout = time.Second * 10

// Service struct holds important data about a github remote.
type Service struct {
    isAuthenticated bool
    client          *gh.Client
    repo            *repo
}

// New creates a new github remote instance.
func New(authToken string) (*Service, error) {
    s := &Service{}
    err := s.setClient(authToken)
    if err != nil {
        return nil, errors.Wrap(err, "github client")
    }
    return s, nil
}

func (s Service) getRepository(url *url.URL) (*repo, error) {
    if url.Hostname() != "github.com" {
        return nil, fmt.Errorf("invalid host %s", url.Hostname())
    }

    // Extract owner, remote and branch if given
    // Examples:
    //  - /[nikoksr]/[proji]                -> extracts owner and remote name; no branch name
    //  - /[nikoksr]/[proji]/tree/[master]    -> extracts owner, remote and branch name
    regex := regexp.MustCompile(`/([^/]+)/([^/]+)(?:/tree/([^/]+))?`)
    specs := regex.FindStringSubmatch(url.Path)
    if specs == nil {
        return nil, fmt.Errorf("could not parse url")
    }

    owner := specs[1]
    repoName := specs[2]
    if owner == "" || repoName == "" {
        return nil, fmt.Errorf("could not extract user and/or repository name")
    }

    currentRepo := &repo{
        url:    url,
        name:   repoName,
        owner:  owner,
        branch: specs[3],
        client: s.client,
    }

    // Set a branch if none was given
    ctx := context.Background()
    err := currentRepo.setBranch(ctx, s.isAuthenticated)
    if err != nil {
        return nil, errors.Wrap(err, "set branch name")
    }

    // Set the repositories sha equal to the latest commit sha of the active branch. This is required by the gh api
    // library.
    err = currentRepo.setSHA(ctx)
    if err != nil {
        return nil, errors.Wrap(err, "repository commit sha")
    }

    return currentRepo, nil
}

func (s *Service) setClient(authToken string) error {
    if len(strings.TrimSpace(authToken)) <= 0 {
        // Create an unauthenticated client
        s.client = gh.NewClient(&http.Client{Timeout: defaultTimeout})
        s.isAuthenticated = false
        return nil
    }
    // Create oauth token
    oauthToken := &oauth2.Token{AccessToken: authToken}
    if !oauthToken.Valid() {
        return fmt.Errorf("couldn't validate github access token")
    }

    // Create an authenticated client
    authenticatedClient := oauth2.NewClient(context.Background(), oauth2.StaticTokenSource(oauthToken))
    authenticatedClient.Timeout = defaultTimeout
    s.client = gh.NewClient(authenticatedClient)
    s.isAuthenticated = true
    return nil
}

func (s Service) GetTreeEntriesAsTemplates(url *url.URL, exclude *regexp.Regexp) ([]*domain.Template, error) {
    repo, err := s.getRepository(url)
    if err != nil {
        return nil, errors.Wrap(err, "github service")
    }

    treeEntries, err := repo.getTreeEntries()
    if err != nil {
        return nil, errors.Wrap(err, "github repository")
    }
    templates := make([]*domain.Template, 0, len(treeEntries))

    for _, entry := range treeEntries {
        path := entry.GetPath()

        // Check if exclude matches the path
        doesMatch := exclude.MatchString(path)
        if doesMatch {
            continue
        }

        // Parse to template
        isFile := false
        if entry.GetType() == "blob" {
            isFile = true
        }
        templates = append(templates, &domain.Template{
            IsFile:      isFile,
            Path:        "",
            Destination: path,
        })
    }
    return templates, nil
}

func (s Service) GetPackageConfig(url *url.URL) (string, error) {
    repo, err := s.getRepository(url)
    if err != nil {
        return "", errors.Wrap(err, "github service")
    }

    configPath := filepath.Join(os.TempDir(), "proji/configs")
    configPath, err = repo.downloadPackageConfig(configPath, url)
    if err != nil {
        return "", errors.Wrap(err, "download package config")
    }
    return configPath, nil
}

func (r repo) downloadCollectionConfigs(treeEntries []*gh.TreeEntry, exclude *regexp.Regexp) ([]string, error) {
    var configPaths []string
    configsBasePath := filepath.Join(os.TempDir(), "proji/configs")
    for _, entry := range treeEntries {
        path := entry.GetPath()
        // Check if exclude matches the path
        doesMatch := exclude.MatchString(path)
        if doesMatch {
            continue
        }

        // Parse package url from base url
        packageURL := r.url
        packageURL.Path = filepath.Join(packageURL.Path, entry.GetPath())

        // Download config
        configPath, err := r.downloadPackageConfig(configsBasePath, packageURL)
        if err != nil {
            return nil, err
        }

        // Add config path to list
        configPaths = append(configPaths, configPath)
    }
    return configPaths, nil
}

func (s Service) GetCollectionConfigs(url *url.URL, exclude *regexp.Regexp) ([]string, error) {
    // Setup repo
    repo, err := s.getRepository(url)
    if err != nil {
        return []string{}, errors.Wrap(err, "github service")
    }

    // Get tree entries from repo
    treeEntries, err := repo.getTreeEntries()
    if err != nil {
        return []string{}, errors.Wrap(err, "repo tree entries")
    }

    // Filter out only files under configs/ path
    return repo.downloadCollectionConfigs(treeEntries, exclude)
}

// GetBaseURI returns the base URI of the remote
// You can pass the relative path to a file of that remote to receive the complete raw url for said file.
// Or you pass an empty string resulting in the base of the raw url for files of this remote.
func (r repo) getRawFileURL(filePath string) string {
    filePath = strings.TrimPrefix(filePath, "/")
    return fmt.Sprintf("https://raw.githubusercontent.com/%s/%s/%s/%s", r.owner, r.name, r.branch, filePath)
}

// downloadPackageConfig downloads the config file from the given url to the given destination. Returns an error if not
// a valid download source and returns the full path of the downloaded file on success.
func (r repo) downloadPackageConfig(destination string, source *url.URL) (string, error) {
    // Extract file name
    fileName := filepath.Base(source.Path)

    // download file to temporary directory
    destination = filepath.Join(destination, fileName)

    // get raw file url ready for download
    rawSource := r.getRawFileURL(filepath.Join("configs", fileName))
    err := util.DownloadFileIfNotExists(destination, rawSource)
    if err != nil {
        return "", errors.Wrap(err, "download config file")
    }
    return destination, nil
}