drone/internal/api/controller/repo/get_content.go

334 lines
8.5 KiB
Go

// Copyright 2022 Harness Inc. All rights reserved.
// Use of this source code is governed by the Polyform Free Trial License
// that can be found in the LICENSE.md file for this repository.
package repo
import (
"context"
"encoding/base64"
"fmt"
"time"
"github.com/harness/gitness/gitrpc"
apiauth "github.com/harness/gitness/internal/api/auth"
"github.com/harness/gitness/internal/auth"
"github.com/harness/gitness/types/enum"
)
const (
// maxGetContentFileSize specifies the maximum number of bytes a file content response contains.
// If a file is any larger, the content is truncated.
maxGetContentFileSize = 1 << 27 // 128 MB
)
type ContentType string
const (
ContentTypeFile ContentType = "file"
ContentTypeDir ContentType = "dir"
ContentTypeSymlink ContentType = "symlink"
ContentTypeSubmodule ContentType = "submodule"
)
type ContentInfo struct {
Type ContentType `json:"type"`
SHA string `json:"sha"`
Name string `json:"name"`
Path string `json:"path"`
LatestCommit *Commit `json:"latestCommit,omitempty"`
}
type Commit struct {
SHA string `json:"sha"`
Title string `json:"title"`
Message string `json:"message"`
Author Signature `json:"author"`
Committer Signature `json:"committer"`
}
type Signature struct {
Identity Identity `json:"identity"`
When time.Time `json:"when"`
}
type Identity struct {
Name string `json:"name"`
Email string `json:"email"`
}
type GetContentOutput struct {
ContentInfo
Content Content `json:"content"`
}
type FileEncodingType string
const (
FileEncodingTypeBase64 FileEncodingType = "base64"
)
// Content restricts the possible types of content returned by the api.
type Content interface {
isContent()
}
type FileContent struct {
Encoding FileEncodingType `json:"encoding"`
Data string `json:"data"`
Size int64 `json:"size"`
}
func (c *FileContent) isContent() {}
type SymlinkContent struct {
Target string `json:"target"`
Size int64 `json:"size"`
}
func (c *SymlinkContent) isContent() {}
type DirContent struct {
Entries []ContentInfo `json:"entries"`
}
func (c *DirContent) isContent() {}
type SubmoduleContent struct {
URL string `json:"url"`
CommitSHA string `json:"commitSha"`
}
func (c *SubmoduleContent) isContent() {}
/*
* GetContent finds the content of the repo at the given path.
* If no gitRef is provided, the content is retrieved from the default branch.
* If includeLatestCommit is enabled, the response contains information of the latest commit that changed the object.
*/
func (c *Controller) GetContent(ctx context.Context, session *auth.Session, repoRef string,
gitRef string, repoPath string, includeLatestCommit bool) (*GetContentOutput, error) {
repo, err := c.repoStore.FindRepoFromRef(ctx, repoRef)
if err != nil {
return nil, err
}
if err = apiauth.CheckRepo(ctx, c.authorizer, session, repo, enum.PermissionRepoView, true); err != nil {
return nil, err
}
// set gitRef to default branch in case an empty reference was provided
if gitRef == "" {
gitRef = repo.DefaultBranch
}
treeNodeOutput, err := c.gitRPCClient.GetTreeNode(ctx, &gitrpc.GetTreeNodeParams{
RepoUID: repo.GitUID,
GitREF: gitRef,
Path: repoPath,
IncludeLatestCommit: includeLatestCommit,
})
if err != nil {
return nil, err
}
info, err := mapToContentInfo(&treeNodeOutput.Node, treeNodeOutput.Commit)
if err != nil {
return nil, err
}
var content Content
switch info.Type {
case ContentTypeDir:
// for getContent we don't want any recursiveness for dir content.
content, err = c.getDirContent(ctx, repo.GitUID, gitRef, repoPath, includeLatestCommit, false)
case ContentTypeFile:
content, err = c.getFileContent(ctx, repo.GitUID, info.SHA)
case ContentTypeSymlink:
content, err = c.getSymlinkContent(ctx, repo.GitUID, info.SHA)
case ContentTypeSubmodule:
content, err = c.getSubmoduleContent(ctx, repo.GitUID, gitRef, repoPath, info.SHA)
default:
err = fmt.Errorf("unknown tree node type '%s'", treeNodeOutput.Node.Type)
}
if err != nil {
return nil, err
}
return &GetContentOutput{
ContentInfo: *info,
Content: content,
}, nil
}
func (c *Controller) getSubmoduleContent(ctx context.Context, gitRepoUID string, gitRef string,
repoPath string, commitSHA string) (*SubmoduleContent, error) {
output, err := c.gitRPCClient.GetSubmodule(ctx, &gitrpc.GetSubmoduleParams{
RepoUID: gitRepoUID,
GitREF: gitRef,
Path: repoPath,
})
if err != nil {
// TODO: handle not found error
// This requires gitrpc to also return notfound though!
return nil, fmt.Errorf("failed to get submodule: %w", err)
}
return &SubmoduleContent{
URL: output.Submodule.URL,
CommitSHA: commitSHA,
}, nil
}
func (c *Controller) getFileContent(ctx context.Context, gitRepoUID string, blobSHA string) (*FileContent, error) {
output, err := c.gitRPCClient.GetBlob(ctx, &gitrpc.GetBlobParams{
RepoUID: gitRepoUID,
SHA: blobSHA,
SizeLimit: maxGetContentFileSize,
})
if err != nil {
// TODO: handle not found error
// This requires gitrpc to also return notfound though!
return nil, fmt.Errorf("failed to get file content: %w", err)
}
return &FileContent{
Size: output.Blob.Size,
Encoding: FileEncodingTypeBase64,
Data: base64.StdEncoding.EncodeToString(output.Blob.Content),
}, nil
}
func (c *Controller) getSymlinkContent(ctx context.Context, gitRepoUID string,
blobSHA string) (*SymlinkContent, error) {
output, err := c.gitRPCClient.GetBlob(ctx, &gitrpc.GetBlobParams{
RepoUID: gitRepoUID,
SHA: blobSHA,
SizeLimit: maxGetContentFileSize,
})
if err != nil {
// TODO: handle not found error
// This requires gitrpc to also return notfound though!
return nil, fmt.Errorf("failed to get symlink: %w", err)
}
return &SymlinkContent{
Size: output.Blob.Size,
Target: string(output.Blob.Content),
}, nil
}
func (c *Controller) getDirContent(ctx context.Context, gitRepoUID string, gitRef string,
repoPath string, includeLatestCommit bool, recursive bool) (*DirContent, error) {
output, err := c.gitRPCClient.ListTreeNodes(ctx, &gitrpc.ListTreeNodeParams{
RepoUID: gitRepoUID,
GitREF: gitRef,
Path: repoPath,
IncludeLatestCommit: includeLatestCommit,
Recursive: recursive,
})
if err != nil {
// TODO: handle not found error
// This requires gitrpc to also return notfound though!
return nil, fmt.Errorf("failed to get content of dir: %w", err)
}
entries := make([]ContentInfo, len(output.Nodes))
for i := range output.Nodes {
node := output.Nodes[i]
var entry *ContentInfo
entry, err = mapToContentInfo(&node.TreeNode, node.Commit)
if err != nil {
return nil, err
}
entries[i] = *entry
}
return &DirContent{
Entries: entries,
}, nil
}
func mapToContentInfo(node *gitrpc.TreeNode, commit *gitrpc.Commit) (*ContentInfo, error) {
// node data is expected
if node == nil {
return nil, fmt.Errorf("node can't be nil")
}
typ, err := mapNodeModeToContentType(node.Mode)
if err != nil {
return nil, err
}
res := &ContentInfo{
Type: typ,
SHA: node.SHA,
Name: node.Name,
Path: node.Path,
}
// parse commit only if available
if commit != nil {
res.LatestCommit, err = mapCommit(commit)
if err != nil {
return nil, err
}
}
return res, nil
}
func mapCommit(c *gitrpc.Commit) (*Commit, error) {
if c == nil {
return nil, fmt.Errorf("commit is nil")
}
author, err := mapSignature(&c.Author)
if err != nil {
return nil, fmt.Errorf("failed to map author: %w", err)
}
committer, err := mapSignature(&c.Committer)
if err != nil {
return nil, fmt.Errorf("failed to map committer: %w", err)
}
return &Commit{
SHA: c.SHA,
Title: c.Title,
Message: c.Message,
Author: *author,
Committer: *committer,
}, nil
}
func mapSignature(s *gitrpc.Signature) (*Signature, error) {
if s == nil {
return nil, fmt.Errorf("signature is nil")
}
return &Signature{
Identity: Identity{
Name: s.Identity.Name,
Email: s.Identity.Email,
},
When: s.When,
}, nil
}
func mapNodeModeToContentType(m gitrpc.TreeNodeMode) (ContentType, error) {
switch m {
case gitrpc.TreeNodeModeFile, gitrpc.TreeNodeModeExec:
return ContentTypeFile, nil
case gitrpc.TreeNodeModeSymlink:
return ContentTypeSymlink, nil
case gitrpc.TreeNodeModeCommit:
return ContentTypeSubmodule, nil
case gitrpc.TreeNodeModeTree:
return ContentTypeDir, nil
default:
return ContentTypeFile, fmt.Errorf("unsupported tree node mode '%s'", m)
}
}