Skip to content

Commit 96e96d7

Browse files
committed
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 f46585c commit 96e96d7

File tree

2 files changed

+156
-19
lines changed

2 files changed

+156
-19
lines changed

routers/web/repo/download.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import (
1010
"time"
1111

1212
git_model "code.gitea.io/gitea/models/git"
13+
"code.gitea.io/gitea/modules/annex"
1314
"code.gitea.io/gitea/modules/context"
1415
"code.gitea.io/gitea/modules/git"
1516
"code.gitea.io/gitea/modules/httpcache"
@@ -79,6 +80,34 @@ func ServeBlobOrLFS(ctx *context.Context, blob *git.Blob, lastModified time.Time
7980
}
8081
closed = true
8182

83+
// check for git-annex files
84+
// re-grab the TreeEntry, since annex needs to work on that, not blobs
85+
// (this code is weirdly redundant because I'm trying not to delete any lines in order to make merges easier)
86+
entry, _ := ctx.Repo.Commit.GetTreeEntryByPath(ctx.Repo.TreePath) // NB: ignoring error because it should have been handled by getBlobForEntry()
87+
isAnnexed, err := annex.IsAnnexed(entry)
88+
if err != nil {
89+
ctx.ServerError("annex.IsAnnexed", err)
90+
return err
91+
}
92+
if isAnnexed {
93+
content, err := annex.Content(entry)
94+
if err != nil {
95+
// XXX are there any other possible failure cases here?
96+
// there are, there could be unrelated io errors; those should be ctx.ServerError()s
97+
ctx.NotFound("annex.Content", err)
98+
return err
99+
}
100+
defer content.Close()
101+
102+
stat, err := content.Stat()
103+
if err != nil {
104+
ctx.ServerError("stat", err)
105+
return err
106+
}
107+
108+
return common.ServeData(ctx, ctx.Repo.TreePath, stat.Size(), content)
109+
}
110+
82111
return common.ServeBlob(ctx, blob, lastModified)
83112
}
84113

tests/integration/git_annex_test.go

Lines changed: 127 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@ import (
1111

1212
"errors"
1313
"fmt"
14+
"io"
1415
"math/rand"
16+
"net/http"
1517
"net/url"
1618
"os"
1719
"path"
@@ -49,6 +51,62 @@ func doCreateRemoteAnnexRepository(t *testing.T, u *url.URL, ctx APITestContext,
4951
return nil
5052
}
5153

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

717775
/* test that 'git annex init' works
718776
719-
precondition: repoPath contains a pre-cloned git repo with an annex: a valid git-annex branch,
720-
and a file 'large.bin' in its origin's annex. See doInitAnnexRepository().
777+
precondition: repoPath contains a pre-cloned repo set up by doInitAnnexRepository().
721778
722779
*/
723780
func doAnnexInitTest(remoteRepoPath string, repoPath string) (err error) {
@@ -756,16 +813,16 @@ func doAnnexInitTest(remoteRepoPath string, repoPath string) (err error) {
756813
}
757814

758815
// - method 1: 'git annex whereis'.
759-
// Demonstrates that git-annex understands the annexed file can be found in the remote annex.
760-
annexWhereis, _, err := git.NewCommandNoGlobals("annex", "whereis", "large.bin").RunStdString(&git.RunOpts{Dir: repoPath})
816+
// Demonstrates that git-annex understands annexed files can be found in the remote annex.
817+
annexWhereis, _, err := git.NewCommandNoGlobals("annex", "whereis", "annexed.bin").RunStdString(&git.RunOpts{Dir: repoPath})
761818
if err != nil {
762-
return fmt.Errorf("Couldn't `git annex whereis large.bin`: %w", err)
819+
return fmt.Errorf("Couldn't `git annex whereis`: %w", err)
763820
}
764821
// Note: this regex is unanchored because 'whereis' outputs multiple lines containing
765822
// headers and 1+ remotes and we just want to find one of them.
766823
match = regexp.MustCompile(regexp.QuoteMeta(remoteAnnexUUID) + " -- .* \\[origin\\]\n").MatchString(annexWhereis)
767824
if !match {
768-
return errors.New("'git annex whereis' should report large.bin is known to be in [origin]")
825+
return errors.New("'git annex whereis' should report files are known to be in [origin]")
769826
}
770827

771828
return nil
@@ -781,24 +838,55 @@ func doAnnexDownloadTest(remoteRepoPath string, repoPath string) (err error) {
781838
return err
782839
}
783840

784-
// verify the file was downloaded
785-
localObjectPath, err := contentLocation(repoPath, "large.bin")
786-
if err != nil {
787-
return err
841+
// verify the files downloaded
842+
843+
cmp := func(filename string) error {
844+
localObjectPath, err := contentLocation(repoPath, filename)
845+
if err != nil {
846+
return err
847+
}
848+
//localObjectPath := path.Join(repoPath, filename) // or, just compare against the checked-out file
849+
850+
remoteObjectPath, err := contentLocation(remoteRepoPath, filename)
851+
if err != nil {
852+
return err
853+
}
854+
855+
match, err := util.FileCmp(localObjectPath, remoteObjectPath, 0)
856+
if err != nil {
857+
return err
858+
}
859+
if !match {
860+
return errors.New("Annexed files should be the same")
861+
}
862+
863+
return nil
788864
}
789-
//localObjectPath := path.Join(repoPath, "large.bin") // or, just compare against the checked-out file
790865

791-
remoteObjectPath, err := contentLocation(remoteRepoPath, "large.bin")
866+
// this is the annex-symlink file
867+
stat, err := os.Lstat(path.Join(repoPath, "annexed.tiff"))
792868
if err != nil {
869+
return fmt.Errorf("Lstat: %w", err)
870+
}
871+
if !((stat.Mode() & os.ModeSymlink) != 0) {
872+
// this line is really just double-checking that the text fixture is set up correctly
873+
return errors.New("*.tiff should be a symlink")
874+
}
875+
if err = cmp("annexed.tiff"); err != nil {
793876
return err
794877
}
795878

796-
match, err := util.FileCmp(localObjectPath, remoteObjectPath, 0)
879+
// this is the annex-pointer file
880+
stat, err = os.Lstat(path.Join(repoPath, "annexed.bin"))
797881
if err != nil {
798-
return err
882+
return fmt.Errorf("Lstat: %w", err)
799883
}
800-
if !match {
801-
return errors.New("Annexed files should be the same")
884+
if !((stat.Mode() & os.ModeSymlink) == 0) {
885+
// this line is really just double-checking that the text fixture is set up correctly
886+
return errors.New("*.bin should not be a symlink")
887+
}
888+
if err = cmp("annexed.bin"); err != nil {
889+
return err
802890
}
803891

804892
return nil
@@ -946,16 +1034,36 @@ func doInitAnnexRepository(repoPath string) error {
9461034
return err
9471035
}
9481036

949-
// add a file to the annex
950-
err = generateRandomFile(1024*1024/4, path.Join(repoPath, "large.bin"))
1037+
// add files to the annex, stored via annex symlinks
1038+
// // a binary file
1039+
err = generateRandomFile(1024*1024/4, path.Join(repoPath, "annexed.tiff"))
1040+
if err != nil {
1041+
return err
1042+
}
1043+
1044+
err = git.NewCommandNoGlobals("annex", "add", ".").Run(&git.RunOpts{Dir: repoPath})
1045+
if err != nil {
1046+
return err
1047+
}
1048+
1049+
// add files to the annex, stored via git-annex-smudge
1050+
// // a binary file
1051+
err = generateRandomFile(1024*1024/4, path.Join(repoPath, "annexed.bin"))
1052+
if err != nil {
1053+
return err
1054+
}
1055+
9511056
if err != nil {
9521057
return err
9531058
}
1059+
9541060
err = git.AddChanges(repoPath, false, ".")
9551061
if err != nil {
9561062
return err
9571063
}
958-
err = git.CommitChanges(repoPath, git.CommitChangesOptions{Message: "Annex a file"})
1064+
1065+
// save everything
1066+
err = git.CommitChanges(repoPath, git.CommitChangesOptions{Message: "Annex files"})
9591067
if err != nil {
9601068
return err
9611069
}

0 commit comments

Comments
 (0)