Skip to content

Commit a782b88

Browse files
kousuactions-user
authored andcommitted
git-annex: make /media/ download annexed content (#20)
Previously, Gitea's LFS support allowed direct-downloads of LFS content, via http://$HOSTNAME:$PORT/$USER/$REPO/media/branch/$BRANCH/$FILE Expand that grace to git-annex too. Now /media should provide the relevant *content* from the .git/annex/objects/ folder. This adds tests too. And expands the tests to try symlink-based annexing, since /media implicitly supports both that and pointer-file-based annexing.
1 parent 77547cc commit a782b88

File tree

2 files changed

+147
-18
lines changed

2 files changed

+147
-18
lines changed

routers/web/repo/download.go

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ import (
99
"time"
1010

1111
git_model "code.gitea.io/gitea/models/git"
12+
"code.gitea.io/gitea/modules/annex"
1213
"code.gitea.io/gitea/modules/context"
1314
"code.gitea.io/gitea/modules/git"
1415
"code.gitea.io/gitea/modules/httpcache"
@@ -79,6 +80,26 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified *time.Tim
7980
}
8081
closed = true
8182

83+
// check for git-annex files
84+
// (this code is weirdly redundant because I'm trying not to delete any lines in order to make merges easier)
85+
isAnnexed, err := annex.IsAnnexed(blob)
86+
if err != nil {
87+
ctx.ServerError("annex.IsAnnexed", err)
88+
return err
89+
}
90+
if isAnnexed {
91+
content, err := annex.Content(blob)
92+
if err != nil {
93+
// XXX are there any other possible failure cases here?
94+
// there are, there could be unrelated io errors; those should be ctx.ServerError()s
95+
ctx.NotFound("annex.Content", err)
96+
return err
97+
}
98+
defer content.Close()
99+
common.ServeContentByReadSeeker(ctx.Base, ctx.Repo.TreePath, lastModified, content)
100+
return nil
101+
}
102+
82103
return common.ServeBlob(ctx.Base, ctx.Repo.TreePath, blob, lastModified)
83104
}
84105

tests/integration/git_annex_test.go

Lines changed: 126 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,9 @@ package integration
77
import (
88
"errors"
99
"fmt"
10+
"io"
1011
"math/rand"
12+
"net/http"
1113
"net/url"
1214
"os"
1315
"path"
@@ -56,6 +58,63 @@ func doCreateRemoteAnnexRepository(t *testing.T, u *url.URL, ctx APITestContext,
5658
return nil
5759
}
5860

61+
func TestGitAnnexMedia(t *testing.T) {
62+
if !setting.Annex.Enabled {
63+
t.Skip("Skipping since annex support is disabled.")
64+
}
65+
66+
onGiteaRun(t, func(t *testing.T, u *url.URL) {
67+
ctx := NewAPITestContext(t, "user2", "annex-media-test", auth_model.AccessTokenScopeWriteRepository)
68+
69+
// create a public repo
70+
require.NoError(t, doCreateRemoteAnnexRepository(t, u, ctx, false))
71+
72+
// the filenames here correspond to specific cases defined in doInitAnnexRepository()
73+
t.Run("AnnexSymlink", func(t *testing.T) {
74+
defer tests.PrintCurrentTest(t)()
75+
doAnnexMediaTest(t, ctx, "annexed.tiff")
76+
})
77+
t.Run("AnnexPointer", func(t *testing.T) {
78+
defer tests.PrintCurrentTest(t)()
79+
doAnnexMediaTest(t, ctx, "annexed.bin")
80+
})
81+
})
82+
}
83+
84+
func doAnnexMediaTest(t *testing.T, ctx APITestContext, file string) {
85+
// Make sure that downloading via /media on the website recognizes it should give the annexed content
86+
87+
// TODO:
88+
// - [ ] roll this into TestGitAnnexPermissions to ensure that permission enforcement works correctly even on /media?
89+
90+
session := loginUser(t, ctx.Username) // logs in to the http:// site/API, storing a cookie;
91+
// this is a different auth method than the git+ssh:// or git+http:// protocols TestGitAnnexPermissions uses!
92+
93+
// compute server-side path of the annexed file
94+
remoteRepoPath := path.Join(setting.RepoRootPath, ctx.GitPath())
95+
remoteObjectPath, err := contentLocation(remoteRepoPath, file)
96+
require.NoError(t, err)
97+
98+
// download annexed file
99+
localObjectPath := path.Join(t.TempDir(), file)
100+
fd, err := os.OpenFile(localObjectPath, os.O_CREATE|os.O_WRONLY, 0o777)
101+
defer fd.Close()
102+
require.NoError(t, err)
103+
104+
mediaLink := path.Join("/", ctx.Username, ctx.Reponame, "/media/branch/master", file)
105+
req := NewRequest(t, "GET", mediaLink)
106+
resp := session.MakeRequest(t, req, http.StatusOK)
107+
108+
_, err = io.Copy(fd, resp.Body)
109+
require.NoError(t, err)
110+
fd.Close()
111+
112+
// verify the download
113+
match, err := util.FileCmp(localObjectPath, remoteObjectPath, 0)
114+
require.NoError(t, err)
115+
require.True(t, match, "Annexed files should be the same")
116+
}
117+
59118
/*
60119
Test that permissions are enforced on git-annex-shell commands.
61120
@@ -763,16 +822,16 @@ func doAnnexInitTest(remoteRepoPath, repoPath string) (err error) {
763822
}
764823

765824
// - method 1: 'git annex whereis'.
766-
// Demonstrates that git-annex understands the annexed file can be found in the remote annex.
767-
annexWhereis, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "whereis", "large.bin").RunStdString(&git.RunOpts{Dir: repoPath})
825+
// Demonstrates that git-annex understands annexed files can be found in the remote annex.
826+
annexWhereis, _, err := git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "whereis", "annexed.bin").RunStdString(&git.RunOpts{Dir: repoPath})
768827
if err != nil {
769-
return fmt.Errorf("Couldn't `git annex whereis large.bin`: %w", err)
828+
return fmt.Errorf("Couldn't `git annex whereis`: %w", err)
770829
}
771830
// Note: this regex is unanchored because 'whereis' outputs multiple lines containing
772831
// headers and 1+ remotes and we just want to find one of them.
773832
match = regexp.MustCompile(regexp.QuoteMeta(remoteAnnexUUID) + " -- .* \\[origin\\]\n").MatchString(annexWhereis)
774833
if !match {
775-
return errors.New("'git annex whereis' should report large.bin is known to be in [origin]")
834+
return errors.New("'git annex whereis' should report files are known to be in [origin]")
776835
}
777836

778837
return nil
@@ -788,27 +847,56 @@ func doAnnexDownloadTest(remoteRepoPath, repoPath string) (err error) {
788847
return err
789848
}
790849

791-
// verify the file was downloaded
792-
localObjectPath, err := contentLocation(repoPath, "large.bin")
793-
if err != nil {
794-
return err
850+
// verify the files downloaded
851+
852+
cmp := func(filename string) error {
853+
localObjectPath, err := contentLocation(repoPath, filename)
854+
if err != nil {
855+
return err
856+
}
857+
// localObjectPath := path.Join(repoPath, filename) // or, just compare against the checked-out file
858+
859+
remoteObjectPath, err := contentLocation(remoteRepoPath, filename)
860+
if err != nil {
861+
return err
862+
}
863+
864+
match, err := util.FileCmp(localObjectPath, remoteObjectPath, 0)
865+
if err != nil {
866+
return err
867+
}
868+
if !match {
869+
return errors.New("Annexed files should be the same")
870+
}
871+
872+
return nil
795873
}
796-
// localObjectPath := path.Join(repoPath, "large.bin") // or, just compare against the checked-out file
797874

798-
remoteObjectPath, err := contentLocation(remoteRepoPath, "large.bin")
875+
// this is the annex-symlink file
876+
stat, err := os.Lstat(path.Join(repoPath, "annexed.tiff"))
799877
if err != nil {
878+
return fmt.Errorf("Lstat: %w", err)
879+
}
880+
if !((stat.Mode() & os.ModeSymlink) != 0) {
881+
// this line is really just double-checking that the text fixture is set up correctly
882+
return errors.New("*.tiff should be a symlink")
883+
}
884+
if err = cmp("annexed.tiff"); err != nil {
800885
return err
801886
}
802887

803-
match, err := util.FileCmp(localObjectPath, remoteObjectPath, 0)
888+
// this is the annex-pointer file
889+
stat, err = os.Lstat(path.Join(repoPath, "annexed.bin"))
804890
if err != nil {
805-
return err
891+
return fmt.Errorf("Lstat: %w", err)
806892
}
807-
if !match {
808-
return errors.New("Annexed files should be the same")
893+
if !((stat.Mode() & os.ModeSymlink) == 0) {
894+
// this line is really just double-checking that the text fixture is set up correctly
895+
return errors.New("*.bin should not be a symlink")
809896
}
897+
err = cmp("annexed.bin")
810898

811-
return nil
899+
return err
812900
}
813901

814902
func doAnnexUploadTest(remoteRepoPath, repoPath string) (err error) {
@@ -953,16 +1041,36 @@ func doInitAnnexRepository(repoPath string) error {
9531041
return err
9541042
}
9551043

956-
// add a file to the annex
957-
err = generateRandomFile(1024*1024/4, path.Join(repoPath, "large.bin"))
1044+
// add files to the annex, stored via annex symlinks
1045+
// // a binary file
1046+
err = generateRandomFile(1024*1024/4, path.Join(repoPath, "annexed.tiff"))
1047+
if err != nil {
1048+
return err
1049+
}
1050+
1051+
err = git.NewCommandContextNoGlobals(git.DefaultContext, "annex", "add", ".").Run(&git.RunOpts{Dir: repoPath})
1052+
if err != nil {
1053+
return err
1054+
}
1055+
1056+
// add files to the annex, stored via git-annex-smudge
1057+
// // a binary file
1058+
err = generateRandomFile(1024*1024/4, path.Join(repoPath, "annexed.bin"))
1059+
if err != nil {
1060+
return err
1061+
}
1062+
9581063
if err != nil {
9591064
return err
9601065
}
1066+
9611067
err = git.AddChanges(repoPath, false, ".")
9621068
if err != nil {
9631069
return err
9641070
}
965-
err = git.CommitChanges(repoPath, git.CommitChangesOptions{Message: "Annex a file"})
1071+
1072+
// save everything
1073+
err = git.CommitChanges(repoPath, git.CommitChangesOptions{Message: "Annex files"})
9661074
if err != nil {
9671075
return err
9681076
}

0 commit comments

Comments
 (0)