Browse Source

api: `GET /repos/:owner/:repo/contents/:path` (#5963)

* support API `GET /repos/:owner/:repo/contents/:path`

This PR adds support to #5949:
`GET /repos/:owner/:repo/contents/:path`

Curl:

```bash
curl -H "Authorization: token REDACTED"
http://localhost:3000/api/v1/repos/root/testrepo/contents//master/README.md
-X GET  | jq .
```

Curl  Response:

```bash
{
  "type": "blob",
  "size": 12,
  "name": "README.md",
  "path": "README.md",
  "sha": "70fcb456d436f08462602f26df6fb7e167e7a916",
  "url": "http://localhost:3000/api/v1/repos/root/testrepo/contents/README.md",
  "git_url": "http://localhost:3000/api/v1/repos/root/testrepo/trees/70fcb456d436f08462602f26df6fb7e167e7a916",
  "html_url": "http://localhost:3000/api/v1/repos/root/testrepo/tree/70fcb456d436f08462602f26df6fb7e167e7a916",
  "download_url": "http://localhost:3000/api/v1/root/testrepo/raw/README.md",
  "_links": {
    "git": "http://localhost:3000/api/v1/repos/root/testrepo/trees/70fcb456d436f08462602f26df6fb7e167e7a916",
    "self": "http://localhost:3000/api/v1/repos/root/testrepo/contents/README.md",
    "html": "http://localhost:3000/api/v1/repos/root/testrepo/tree/70fcb456d436f08462602f26df6fb7e167e7a916"
  },
  "content": "IyB0ZXN0cmVwbwoK"
}

```

* rename - path.go to contents.go

* reorder imports

Co-Authored-By: ᴜɴᴋɴᴡᴏɴ <u@gogs.io>

* rename struct to repoContents and fix field order

Co-Authored-By: ᴜɴᴋɴᴡᴏɴ <u@gogs.io>

* rename variable

Co-Authored-By: ᴜɴᴋɴᴡᴏɴ <u@gogs.io>

* rename GetPathContents to GetContents

Co-Authored-By: ᴜɴᴋɴᴡᴏɴ <u@gogs.io>

* return on server error

Co-Authored-By: ᴜɴᴋɴᴡᴏɴ <u@gogs.io>

* resolve conflicts introduced via git web ui

* make constants as method variables

* handle dir type case last

* fix func and var names

* implement suggested changes in review

* refactor smaller funcs to be part of GetContent

* fix content type check for blob after refactoring

* changes based on suggestions

* read full file, return empty json array

* don't set submoduleURL

* set server err msg to method name

* set target to be blob data for symlinks

* Update contents.go

Co-authored-by: ᴜɴᴋɴᴡᴏɴ <u@gogs.io>
ᴜɴᴋɴᴡᴏɴ 4 years ago
parent
commit
5ad2fdcf0b
2 changed files with 204 additions and 0 deletions
  1. 1 0
      internal/route/api/v1/api.go
  2. 203 0
      internal/route/api/v1/repo/contents.go

+ 1 - 0
internal/route/api/v1/api.go

@@ -271,6 +271,7 @@ func RegisterRoutes(m *macaron.Macaron) {
 				}, reqRepoAdmin())
 
 				m.Get("/raw/*", context.RepoRef(), repo2.GetRawFile)
+				m.Get("/contents/*", context.RepoRef(), repo2.GetContents)
 				m.Get("/archive/*", repo2.GetArchive)
 				m.Group("/git/trees", func() {
 					m.Get("/:sha", context.RepoRef(), repo2.GetRepoGitTree)

+ 203 - 0
internal/route/api/v1/repo/contents.go

@@ -0,0 +1,203 @@
+// Copyright 2020 The Gogs Authors. All rights reserved.
+// Use of this source code is governed by a MIT-style
+// license that can be found in the LICENSE file.
+
+package repo
+
+import (
+	"encoding/base64"
+	"fmt"
+	"io/ioutil"
+
+	"github.com/gogs/git-module"
+
+	"gogs.io/gogs/internal/context"
+)
+
+type repoContent struct {
+	Type            string `json:"type"`
+	Target          string `json:"target,omitempty"`
+	SubmoduleGitURL string `json:"submodule_git_url,omitempty"`
+	Encoding        string `json:"encoding,omitempty"`
+	Size            int64  `json:"size"`
+	Name            string `json:"name"`
+	Path            string `json:"path"`
+	Content         string `json:"content,omitempty"`
+	Sha             string `json:"sha"`
+	URL             string `json:"url"`
+	GitURL          string `json:"git_url"`
+	HTMLURL         string `json:"html_url"`
+	DownloadURL     string `json:"download_url"`
+	Links           Links  `json:"_links"`
+}
+
+type Links struct {
+	Git  string `json:"git"`
+	Self string `json:"self"`
+	HTML string `json:"html"`
+}
+
+func GetContents(c *context.APIContext) {
+	treeEntry, err := c.Repo.Commit.GetTreeEntryByPath(c.Repo.TreePath)
+	if err != nil {
+		c.NotFoundOrServerError("GetTreeEntryByPath", git.IsErrNotExist, err)
+		return
+	}
+	username := c.Params(":username")
+	reponame := c.Params(":reponame")
+
+	// TODO: figure out the best way to do this
+	// :base-url/:username/:project/raw/:refs/:path
+	templateDownloadURL := "%s/%s/%s/raw/%s"
+	// :base-url/repos/:username/:project/contents/:path
+	templateSelfLink := "%s/repos/%s/%s/contents/%s"
+	// :baseurl/repos/:username/:project/git/trees/:sha
+	templateGitURLLink := "%s/repos/%s/%s/trees/%s"
+	// :baseurl/repos/:username/:project/tree/:sha
+	templateHTMLLLink := "%s/repos/%s/%s/tree/%s"
+
+	gitURL := fmt.Sprintf(templateGitURLLink, c.BaseURL, username, reponame, treeEntry.ID.String())
+	htmlURL := fmt.Sprintf(templateHTMLLLink, c.BaseURL, username, reponame, treeEntry.ID.String())
+	selfURL := fmt.Sprintf(templateSelfLink, c.BaseURL, username, reponame, c.Repo.TreePath)
+
+	// TODO(unknwon): Make a treeEntryToRepoContent helper.
+	contents := &repoContent{
+		Size:        treeEntry.Size(),
+		Name:        treeEntry.Name(),
+		Path:        c.Repo.TreePath,
+		Sha:         treeEntry.ID.String(),
+		URL:         selfURL,
+		GitURL:      gitURL,
+		HTMLURL:     htmlURL,
+		DownloadURL: fmt.Sprintf(templateDownloadURL, c.BaseURL, username, reponame, c.Repo.TreePath),
+		Links: Links{
+			Git:  gitURL,
+			Self: selfURL,
+			HTML: htmlURL,
+		},
+	}
+
+	// A tree entry can only be one of the following types:
+	//   1. Tree (directory)
+	//   2. SubModule
+	//   3. SymLink
+	//   4. Blob (file)
+	if treeEntry.IsSubModule() {
+		// TODO(unknwon): submoduleURL is not set as current git-module doesn't handle it properly
+		contents.Type = "submodule"
+		c.JSONSuccess(contents)
+		return
+
+	} else if treeEntry.IsLink() {
+		contents.Type = "symlink"
+		blob, err := c.Repo.Commit.GetBlobByPath(c.Repo.TreePath)
+		if err != nil {
+			c.ServerError("GetBlobByPath", err)
+			return
+		}
+		b, err := blob.Data()
+		if err != nil {
+			c.ServerError("Data", err)
+			return
+		}
+		buf, err := ioutil.ReadAll(b)
+		if err != nil {
+			c.ServerError("ReadAll", err)
+			return
+		}
+		contents.Target = string(buf)
+		c.JSONSuccess(contents)
+		return
+
+	} else if treeEntry.Type == "blob" {
+		blob, err := c.Repo.Commit.GetBlobByPath(c.Repo.TreePath)
+		if err != nil {
+			c.ServerError("GetBlobByPath", err)
+			return
+		}
+		b, err := blob.Data()
+		if err != nil {
+			c.ServerError("Data", err)
+			return
+		}
+		buf, err := ioutil.ReadAll(b)
+		if err != nil {
+			c.ServerError("ReadAll", err)
+			return
+		}
+		contents.Content = base64.StdEncoding.EncodeToString(buf)
+		contents.Type = "file"
+		c.JSONSuccess(contents)
+		return
+	}
+
+	// treeEntry is a directory
+	dirTree, err := c.Repo.GitRepo.GetTree(treeEntry.ID.String())
+	if err != nil {
+		c.NotFoundOrServerError("GetTree", git.IsErrNotExist, err)
+		return
+	}
+
+	entries, err := dirTree.ListEntries()
+	if err != nil {
+		c.NotFoundOrServerError("ListEntries", git.IsErrNotExist, err)
+		return
+	}
+
+	if len(entries) == 0 {
+		c.JSONSuccess([]string{})
+		return
+	}
+
+	var results = make([]*repoContent, 0, len(entries))
+	for _, entry := range entries {
+		gitURL := fmt.Sprintf(templateGitURLLink, c.BaseURL, username, reponame, entry.ID.String())
+		htmlURL := fmt.Sprintf(templateHTMLLLink, c.BaseURL, username, reponame, entry.ID.String())
+		selfURL := fmt.Sprintf(templateSelfLink, c.BaseURL, username, reponame, c.Repo.TreePath)
+		var contentType string
+		if entry.IsDir() {
+			contentType = "dir"
+		} else if entry.IsSubModule() {
+			// TODO(unknwon): submoduleURL is not set as current git-module doesn't handle it properly
+			contentType = "submodule"
+		} else if entry.IsLink() {
+			contentType = "symlink"
+			blob, err := c.Repo.Commit.GetBlobByPath(c.Repo.TreePath)
+			if err != nil {
+				c.ServerError("GetBlobByPath", err)
+				return
+			}
+			b, err := blob.Data()
+			if err != nil {
+				c.ServerError("Data", err)
+				return
+			}
+			buf, err := ioutil.ReadAll(b)
+			if err != nil {
+				c.ServerError("ReadAll", err)
+				return
+			}
+			contents.Target = string(buf)
+		} else {
+			contentType = "file"
+		}
+
+		results = append(results, &repoContent{
+			Type:        contentType,
+			Size:        entry.Size(),
+			Name:        entry.Name(),
+			Path:        c.Repo.TreePath,
+			Sha:         entry.ID.String(),
+			URL:         selfURL,
+			GitURL:      gitURL,
+			HTMLURL:     htmlURL,
+			DownloadURL: fmt.Sprintf(templateDownloadURL, c.BaseURL, username, reponame, c.Repo.TreePath),
+			Links: Links{
+				Git:  gitURL,
+				Self: selfURL,
+				HTML: htmlURL,
+			},
+		})
+	}
+	c.JSONSuccess(results)
+}