[FEAT] Show follow symlink button
- When a user goes opens a symlink file in Forgejo, the file would be rendered with the path of the symlink as content. - Add a button that is shown when the user opens a *valid* symlink file, which means that the symlink must have an valid path to an existent file and after 999 follows isn't a symlink anymore. - Return the relative path from the `FollowLink` functions, because Git really doesn't want to tell where an file is located based on the blob ID. - Adds integration tests.
This commit is contained in:
		
							parent
							
								
									0bba571f5b
								
							
						
					
					
						commit
						c63b52c126
					
				
					 6 changed files with 84 additions and 18 deletions
				
			
		| 
						 | 
				
			
			@ -174,7 +174,7 @@ func Int64sToStrings(ints []int64) []string {
 | 
			
		|||
func EntryIcon(entry *git.TreeEntry) string {
 | 
			
		||||
	switch {
 | 
			
		||||
	case entry.IsLink():
 | 
			
		||||
		te, err := entry.FollowLink()
 | 
			
		||||
		te, _, err := entry.FollowLink()
 | 
			
		||||
		if err != nil {
 | 
			
		||||
			log.Debug(err.Error())
 | 
			
		||||
			return "file-symlink-file"
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -23,15 +23,15 @@ func (te *TreeEntry) Type() string {
 | 
			
		|||
}
 | 
			
		||||
 | 
			
		||||
// FollowLink returns the entry pointed to by a symlink
 | 
			
		||||
func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
 | 
			
		||||
func (te *TreeEntry) FollowLink() (*TreeEntry, string, error) {
 | 
			
		||||
	if !te.IsLink() {
 | 
			
		||||
		return nil, ErrBadLink{te.Name(), "not a symlink"}
 | 
			
		||||
		return nil, "", ErrBadLink{te.Name(), "not a symlink"}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	// read the link
 | 
			
		||||
	r, err := te.Blob().DataAsync()
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
		return nil, "", err
 | 
			
		||||
	}
 | 
			
		||||
	closed := false
 | 
			
		||||
	defer func() {
 | 
			
		||||
| 
						 | 
				
			
			@ -42,7 +42,7 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
 | 
			
		|||
	buf := make([]byte, te.Size())
 | 
			
		||||
	_, err = io.ReadFull(r, buf)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		return nil, err
 | 
			
		||||
		return nil, "", err
 | 
			
		||||
	}
 | 
			
		||||
	_ = r.Close()
 | 
			
		||||
	closed = true
 | 
			
		||||
| 
						 | 
				
			
			@ -56,33 +56,35 @@ func (te *TreeEntry) FollowLink() (*TreeEntry, error) {
 | 
			
		|||
	}
 | 
			
		||||
 | 
			
		||||
	if t == nil {
 | 
			
		||||
		return nil, ErrBadLink{te.Name(), "points outside of repo"}
 | 
			
		||||
		return nil, "", ErrBadLink{te.Name(), "points outside of repo"}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	target, err := t.GetTreeEntryByPath(lnk)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		if IsErrNotExist(err) {
 | 
			
		||||
			return nil, ErrBadLink{te.Name(), "broken link"}
 | 
			
		||||
			return nil, "", ErrBadLink{te.Name(), "broken link"}
 | 
			
		||||
		}
 | 
			
		||||
		return nil, err
 | 
			
		||||
		return nil, "", err
 | 
			
		||||
	}
 | 
			
		||||
	return target, nil
 | 
			
		||||
	return target, lnk, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// FollowLinks returns the entry ultimately pointed to by a symlink
 | 
			
		||||
func (te *TreeEntry) FollowLinks() (*TreeEntry, error) {
 | 
			
		||||
func (te *TreeEntry) FollowLinks() (*TreeEntry, string, error) {
 | 
			
		||||
	if !te.IsLink() {
 | 
			
		||||
		return nil, ErrBadLink{te.Name(), "not a symlink"}
 | 
			
		||||
		return nil, "", ErrBadLink{te.Name(), "not a symlink"}
 | 
			
		||||
	}
 | 
			
		||||
	entry := te
 | 
			
		||||
	entryLink := ""
 | 
			
		||||
	for i := 0; i < 999; i++ {
 | 
			
		||||
		if entry.IsLink() {
 | 
			
		||||
			next, err := entry.FollowLink()
 | 
			
		||||
			next, link, err := entry.FollowLink()
 | 
			
		||||
			entryLink = link
 | 
			
		||||
			if err != nil {
 | 
			
		||||
				return nil, err
 | 
			
		||||
				return nil, "", err
 | 
			
		||||
			}
 | 
			
		||||
			if next.ID == entry.ID {
 | 
			
		||||
				return nil, ErrBadLink{
 | 
			
		||||
				return nil, "", ErrBadLink{
 | 
			
		||||
					entry.Name(),
 | 
			
		||||
					"recursive link",
 | 
			
		||||
				}
 | 
			
		||||
| 
						 | 
				
			
			@ -93,12 +95,12 @@ func (te *TreeEntry) FollowLinks() (*TreeEntry, error) {
 | 
			
		|||
		}
 | 
			
		||||
	}
 | 
			
		||||
	if entry.IsLink() {
 | 
			
		||||
		return nil, ErrBadLink{
 | 
			
		||||
		return nil, "", ErrBadLink{
 | 
			
		||||
			te.Name(),
 | 
			
		||||
			"too many levels of symbolic links",
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
	return entry, nil
 | 
			
		||||
	return entry, entryLink, nil
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
// returns the Tree pointed to by this TreeEntry, or nil if this is not a tree
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -1205,6 +1205,7 @@ tag = Tag
 | 
			
		|||
released_this = released this
 | 
			
		||||
file.title = %s at %s
 | 
			
		||||
file_raw = Raw
 | 
			
		||||
file_follow = Follow Symlink
 | 
			
		||||
file_history = History
 | 
			
		||||
file_view_source = View Source
 | 
			
		||||
file_view_rendered = View Rendered
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -114,7 +114,7 @@ func findReadmeFileInEntries(ctx *context.Context, entries []*git.TreeEntry, try
 | 
			
		|||
			log.Debug("Potential readme file: %s", entry.Name())
 | 
			
		||||
			if readmeFiles[i] == nil || base.NaturalSortLess(readmeFiles[i].Name(), entry.Blob().Name()) {
 | 
			
		||||
				if entry.IsLink() {
 | 
			
		||||
					target, err := entry.FollowLinks()
 | 
			
		||||
					target, _, err := entry.FollowLinks()
 | 
			
		||||
					if err != nil && !git.IsErrBadLink(err) {
 | 
			
		||||
						return "", nil, err
 | 
			
		||||
					} else if target != nil && (target.IsExecutable() || target.IsRegular()) {
 | 
			
		||||
| 
						 | 
				
			
			@ -267,7 +267,7 @@ func getFileReader(ctx gocontext.Context, repoID int64, blob *git.Blob) ([]byte,
 | 
			
		|||
func renderReadmeFile(ctx *context.Context, subfolder string, readmeFile *git.TreeEntry) {
 | 
			
		||||
	target := readmeFile
 | 
			
		||||
	if readmeFile != nil && readmeFile.IsLink() {
 | 
			
		||||
		target, _ = readmeFile.FollowLinks()
 | 
			
		||||
		target, _, _ = readmeFile.FollowLinks()
 | 
			
		||||
	}
 | 
			
		||||
	if target == nil {
 | 
			
		||||
		// if findReadmeFile() failed and/or gave us a broken symlink (which it shouldn't)
 | 
			
		||||
| 
						 | 
				
			
			@ -391,6 +391,15 @@ func renderFile(ctx *context.Context, entry *git.TreeEntry) {
 | 
			
		|||
	ctx.Data["FileName"] = blob.Name()
 | 
			
		||||
	ctx.Data["RawFileLink"] = ctx.Repo.RepoLink + "/raw/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(ctx.Repo.TreePath)
 | 
			
		||||
 | 
			
		||||
	if entry.IsLink() {
 | 
			
		||||
		_, link, err := entry.FollowLinks()
 | 
			
		||||
		// Errors should be allowed, because this shouldn't
 | 
			
		||||
		// block rendering invalid symlink files.
 | 
			
		||||
		if err == nil {
 | 
			
		||||
			ctx.Data["SymlinkURL"] = ctx.Repo.RepoLink + "/src/" + ctx.Repo.BranchNameSubURL() + "/" + util.PathEscapeSegments(link)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	commit, err := ctx.Repo.Commit.GetCommitByPath(ctx.Repo.TreePath)
 | 
			
		||||
	if err != nil {
 | 
			
		||||
		ctx.ServerError("GetCommitByPath", err)
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -43,6 +43,9 @@
 | 
			
		|||
			{{end}}
 | 
			
		||||
			{{if not .ReadmeInList}}
 | 
			
		||||
				<div class="ui buttons gt-mr-2">
 | 
			
		||||
					{{if .SymlinkURL}}
 | 
			
		||||
						<a class="ui mini basic button" href="{{$.SymlinkURL}}" data-kind="follow-symlink">{{ctx.Locale.Tr "repo.file_follow"}}</a>
 | 
			
		||||
					{{end}}
 | 
			
		||||
					<a class="ui mini basic button" href="{{$.RawFileLink}}">{{ctx.Locale.Tr "repo.file_raw"}}</a>
 | 
			
		||||
					{{if not .IsViewCommit}}
 | 
			
		||||
						<a class="ui mini basic button" href="{{.RepoLink}}/src/commit/{{PathEscape .CommitID}}/{{PathEscapeSegments .TreePath}}">{{ctx.Locale.Tr "repo.file_permalink"}}</a>
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -961,3 +961,54 @@ func TestRepoFilesList(t *testing.T) {
 | 
			
		|||
		assert.EqualValues(t, []string{"Charlie", "alpha", "Beta", "delta", "licensa", "LICENSE", "licensz", "README.md", "zEta"}, filesList)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
func TestRepoFollowSymlink(t *testing.T) {
 | 
			
		||||
	defer tests.PrepareTestEnv(t)()
 | 
			
		||||
	session := loginUser(t, "user2")
 | 
			
		||||
 | 
			
		||||
	assertCase := func(t *testing.T, url, expectedSymlinkURL string, shouldExist bool) {
 | 
			
		||||
		t.Helper()
 | 
			
		||||
 | 
			
		||||
		req := NewRequest(t, "GET", url)
 | 
			
		||||
		resp := session.MakeRequest(t, req, http.StatusOK)
 | 
			
		||||
 | 
			
		||||
		htmlDoc := NewHTMLParser(t, resp.Body)
 | 
			
		||||
		symlinkURL, ok := htmlDoc.Find(".file-actions .button[data-kind='follow-symlink']").Attr("href")
 | 
			
		||||
		if shouldExist {
 | 
			
		||||
			assert.True(t, ok)
 | 
			
		||||
			assert.EqualValues(t, expectedSymlinkURL, symlinkURL)
 | 
			
		||||
		} else {
 | 
			
		||||
			assert.False(t, ok)
 | 
			
		||||
		}
 | 
			
		||||
	}
 | 
			
		||||
 | 
			
		||||
	t.Run("Normal", func(t *testing.T) {
 | 
			
		||||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
		assertCase(t, "/user2/readme-test/src/branch/symlink/README.md?display=source", "/user2/readme-test/src/branch/symlink/some/other/path/awefulcake.txt", true)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Normal", func(t *testing.T) {
 | 
			
		||||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
		assertCase(t, "/user2/readme-test/src/branch/symlink/some/README.txt", "/user2/readme-test/src/branch/symlink/some/other/path/awefulcake.txt", true)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Normal", func(t *testing.T) {
 | 
			
		||||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
		assertCase(t, "/user2/readme-test/src/branch/symlink/up/back/down/down/README.md", "/user2/readme-test/src/branch/symlink/down/side/../left/right/../reelmein", true)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Broken symlink", func(t *testing.T) {
 | 
			
		||||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
		assertCase(t, "/user2/readme-test/src/branch/fallbacks-broken-symlinks/docs/README", "", false)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Loop symlink", func(t *testing.T) {
 | 
			
		||||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
		assertCase(t, "/user2/readme-test/src/branch/symlink-loop/README.md", "", false)
 | 
			
		||||
	})
 | 
			
		||||
 | 
			
		||||
	t.Run("Not a symlink", func(t *testing.T) {
 | 
			
		||||
		defer tests.PrintCurrentTest(t)()
 | 
			
		||||
		assertCase(t, "/user2/readme-test/src/branch/master/README.md", "", false)
 | 
			
		||||
	})
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue