code.gitea.io/gitea@v1.21.7/services/repository/files/content.go (about)

     1  // Copyright 2019 The Gitea Authors. All rights reserved.
     2  // SPDX-License-Identifier: MIT
     3  
     4  package files
     5  
     6  import (
     7  	"context"
     8  	"fmt"
     9  	"net/url"
    10  	"path"
    11  	"strings"
    12  
    13  	"code.gitea.io/gitea/models"
    14  	repo_model "code.gitea.io/gitea/models/repo"
    15  	"code.gitea.io/gitea/modules/git"
    16  	"code.gitea.io/gitea/modules/setting"
    17  	api "code.gitea.io/gitea/modules/structs"
    18  	"code.gitea.io/gitea/modules/util"
    19  )
    20  
    21  // ContentType repo content type
    22  type ContentType string
    23  
    24  // The string representations of different content types
    25  const (
    26  	// ContentTypeRegular regular content type (file)
    27  	ContentTypeRegular ContentType = "file"
    28  	// ContentTypeDir dir content type (dir)
    29  	ContentTypeDir ContentType = "dir"
    30  	// ContentLink link content type (symlink)
    31  	ContentTypeLink ContentType = "symlink"
    32  	// ContentTag submodule content type (submodule)
    33  	ContentTypeSubmodule ContentType = "submodule"
    34  )
    35  
    36  // String gets the string of ContentType
    37  func (ct *ContentType) String() string {
    38  	return string(*ct)
    39  }
    40  
    41  // GetContentsOrList gets the meta data of a file's contents (*ContentsResponse) if treePath not a tree
    42  // directory, otherwise a listing of file contents ([]*ContentsResponse). Ref can be a branch, commit or tag
    43  func GetContentsOrList(ctx context.Context, repo *repo_model.Repository, treePath, ref string) (any, error) {
    44  	if repo.IsEmpty {
    45  		return make([]any, 0), nil
    46  	}
    47  	if ref == "" {
    48  		ref = repo.DefaultBranch
    49  	}
    50  	origRef := ref
    51  
    52  	// Check that the path given in opts.treePath is valid (not a git path)
    53  	cleanTreePath := CleanUploadFileName(treePath)
    54  	if cleanTreePath == "" && treePath != "" {
    55  		return nil, models.ErrFilenameInvalid{
    56  			Path: treePath,
    57  		}
    58  	}
    59  	treePath = cleanTreePath
    60  
    61  	gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath())
    62  	if err != nil {
    63  		return nil, err
    64  	}
    65  	defer closer.Close()
    66  
    67  	// Get the commit object for the ref
    68  	commit, err := gitRepo.GetCommit(ref)
    69  	if err != nil {
    70  		return nil, err
    71  	}
    72  
    73  	entry, err := commit.GetTreeEntryByPath(treePath)
    74  	if err != nil {
    75  		return nil, err
    76  	}
    77  
    78  	if entry.Type() != "tree" {
    79  		return GetContents(ctx, repo, treePath, origRef, false)
    80  	}
    81  
    82  	// We are in a directory, so we return a list of FileContentResponse objects
    83  	var fileList []*api.ContentsResponse
    84  
    85  	gitTree, err := commit.SubTree(treePath)
    86  	if err != nil {
    87  		return nil, err
    88  	}
    89  	entries, err := gitTree.ListEntries()
    90  	if err != nil {
    91  		return nil, err
    92  	}
    93  	for _, e := range entries {
    94  		subTreePath := path.Join(treePath, e.Name())
    95  		fileContentResponse, err := GetContents(ctx, repo, subTreePath, origRef, true)
    96  		if err != nil {
    97  			return nil, err
    98  		}
    99  		fileList = append(fileList, fileContentResponse)
   100  	}
   101  	return fileList, nil
   102  }
   103  
   104  // GetObjectTypeFromTreeEntry check what content is behind it
   105  func GetObjectTypeFromTreeEntry(entry *git.TreeEntry) ContentType {
   106  	switch {
   107  	case entry.IsDir():
   108  		return ContentTypeDir
   109  	case entry.IsSubModule():
   110  		return ContentTypeSubmodule
   111  	case entry.IsExecutable(), entry.IsRegular():
   112  		return ContentTypeRegular
   113  	case entry.IsLink():
   114  		return ContentTypeLink
   115  	default:
   116  		return ""
   117  	}
   118  }
   119  
   120  // GetContents gets the meta data on a file's contents. Ref can be a branch, commit or tag
   121  func GetContents(ctx context.Context, repo *repo_model.Repository, treePath, ref string, forList bool) (*api.ContentsResponse, error) {
   122  	if ref == "" {
   123  		ref = repo.DefaultBranch
   124  	}
   125  	origRef := ref
   126  
   127  	// Check that the path given in opts.treePath is valid (not a git path)
   128  	cleanTreePath := CleanUploadFileName(treePath)
   129  	if cleanTreePath == "" && treePath != "" {
   130  		return nil, models.ErrFilenameInvalid{
   131  			Path: treePath,
   132  		}
   133  	}
   134  	treePath = cleanTreePath
   135  
   136  	gitRepo, closer, err := git.RepositoryFromContextOrOpen(ctx, repo.RepoPath())
   137  	if err != nil {
   138  		return nil, err
   139  	}
   140  	defer closer.Close()
   141  
   142  	// Get the commit object for the ref
   143  	commit, err := gitRepo.GetCommit(ref)
   144  	if err != nil {
   145  		return nil, err
   146  	}
   147  	commitID := commit.ID.String()
   148  	if len(ref) >= 4 && strings.HasPrefix(commitID, ref) {
   149  		ref = commit.ID.String()
   150  	}
   151  
   152  	entry, err := commit.GetTreeEntryByPath(treePath)
   153  	if err != nil {
   154  		return nil, err
   155  	}
   156  
   157  	refType := gitRepo.GetRefType(ref)
   158  	if refType == "invalid" {
   159  		return nil, fmt.Errorf("no commit found for the ref [ref: %s]", ref)
   160  	}
   161  
   162  	selfURL, err := url.Parse(repo.APIURL() + "/contents/" + util.PathEscapeSegments(treePath) + "?ref=" + url.QueryEscape(origRef))
   163  	if err != nil {
   164  		return nil, err
   165  	}
   166  	selfURLString := selfURL.String()
   167  
   168  	err = gitRepo.AddLastCommitCache(repo.GetCommitsCountCacheKey(ref, refType != git.ObjectCommit), repo.FullName(), commitID)
   169  	if err != nil {
   170  		return nil, err
   171  	}
   172  
   173  	lastCommit, err := commit.GetCommitByPath(treePath)
   174  	if err != nil {
   175  		return nil, err
   176  	}
   177  
   178  	// All content types have these fields in populated
   179  	contentsResponse := &api.ContentsResponse{
   180  		Name:          entry.Name(),
   181  		Path:          treePath,
   182  		SHA:           entry.ID.String(),
   183  		LastCommitSHA: lastCommit.ID.String(),
   184  		Size:          entry.Size(),
   185  		URL:           &selfURLString,
   186  		Links: &api.FileLinksResponse{
   187  			Self: &selfURLString,
   188  		},
   189  	}
   190  
   191  	// Now populate the rest of the ContentsResponse based on entry type
   192  	if entry.IsRegular() || entry.IsExecutable() {
   193  		contentsResponse.Type = string(ContentTypeRegular)
   194  		if blobResponse, err := GetBlobBySHA(ctx, repo, gitRepo, entry.ID.String()); err != nil {
   195  			return nil, err
   196  		} else if !forList {
   197  			// We don't show the content if we are getting a list of FileContentResponses
   198  			contentsResponse.Encoding = &blobResponse.Encoding
   199  			contentsResponse.Content = &blobResponse.Content
   200  		}
   201  	} else if entry.IsDir() {
   202  		contentsResponse.Type = string(ContentTypeDir)
   203  	} else if entry.IsLink() {
   204  		contentsResponse.Type = string(ContentTypeLink)
   205  		// The target of a symlink file is the content of the file
   206  		targetFromContent, err := entry.Blob().GetBlobContent(1024)
   207  		if err != nil {
   208  			return nil, err
   209  		}
   210  		contentsResponse.Target = &targetFromContent
   211  	} else if entry.IsSubModule() {
   212  		contentsResponse.Type = string(ContentTypeSubmodule)
   213  		submodule, err := commit.GetSubModule(treePath)
   214  		if err != nil {
   215  			return nil, err
   216  		}
   217  		if submodule != nil && submodule.URL != "" {
   218  			contentsResponse.SubmoduleGitURL = &submodule.URL
   219  		}
   220  	}
   221  	// Handle links
   222  	if entry.IsRegular() || entry.IsLink() {
   223  		downloadURL, err := url.Parse(repo.HTMLURL() + "/raw/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath))
   224  		if err != nil {
   225  			return nil, err
   226  		}
   227  		downloadURLString := downloadURL.String()
   228  		contentsResponse.DownloadURL = &downloadURLString
   229  	}
   230  	if !entry.IsSubModule() {
   231  		htmlURL, err := url.Parse(repo.HTMLURL() + "/src/" + url.PathEscape(string(refType)) + "/" + util.PathEscapeSegments(ref) + "/" + util.PathEscapeSegments(treePath))
   232  		if err != nil {
   233  			return nil, err
   234  		}
   235  		htmlURLString := htmlURL.String()
   236  		contentsResponse.HTMLURL = &htmlURLString
   237  		contentsResponse.Links.HTMLURL = &htmlURLString
   238  
   239  		gitURL, err := url.Parse(repo.APIURL() + "/git/blobs/" + url.PathEscape(entry.ID.String()))
   240  		if err != nil {
   241  			return nil, err
   242  		}
   243  		gitURLString := gitURL.String()
   244  		contentsResponse.GitURL = &gitURLString
   245  		contentsResponse.Links.GitURL = &gitURLString
   246  	}
   247  
   248  	return contentsResponse, nil
   249  }
   250  
   251  // GetBlobBySHA get the GitBlobResponse of a repository using a sha hash.
   252  func GetBlobBySHA(ctx context.Context, repo *repo_model.Repository, gitRepo *git.Repository, sha string) (*api.GitBlobResponse, error) {
   253  	gitBlob, err := gitRepo.GetBlob(sha)
   254  	if err != nil {
   255  		return nil, err
   256  	}
   257  	content := ""
   258  	if gitBlob.Size() <= setting.API.DefaultMaxBlobSize {
   259  		content, err = gitBlob.GetBlobContentBase64()
   260  		if err != nil {
   261  			return nil, err
   262  		}
   263  	}
   264  	return &api.GitBlobResponse{
   265  		SHA:      gitBlob.ID.String(),
   266  		URL:      repo.APIURL() + "/git/blobs/" + url.PathEscape(gitBlob.ID.String()),
   267  		Size:     gitBlob.Size(),
   268  		Encoding: "base64",
   269  		Content:  content,
   270  	}, nil
   271  }