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