From 5ad2fdcf0b3334a7b7d5aff8cf57ae49d7f911f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E1=B4=9C=C9=B4=E1=B4=8B=C9=B4=E1=B4=A1=E1=B4=8F=C9=B4?= <u@gogs.io> Date: Thu, 5 Mar 2020 16:15:38 +0800 Subject: [PATCH] api: `GET /repos/:owner/:repo/contents/:path` (#5963) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * 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> --- internal/route/api/v1/api.go | 1 + internal/route/api/v1/repo/contents.go | 203 +++++++++++++++++++++++++ 2 files changed, 204 insertions(+) create mode 100644 internal/route/api/v1/repo/contents.go diff --git a/internal/route/api/v1/api.go b/internal/route/api/v1/api.go index 6f5ebdb55..afc50191b 100644 --- a/internal/route/api/v1/api.go +++ b/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) diff --git a/internal/route/api/v1/repo/contents.go b/internal/route/api/v1/repo/contents.go new file mode 100644 index 000000000..7f7ec18e4 --- /dev/null +++ b/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) +}