6
6
package repo
7
7
8
8
import (
9
+ "bytes"
9
10
"encoding/base64"
10
11
"fmt"
12
+ "io"
11
13
"net/http"
12
14
"path"
13
15
"time"
@@ -18,7 +20,11 @@ import (
18
20
"code.gitea.io/gitea/modules/cache"
19
21
"code.gitea.io/gitea/modules/context"
20
22
"code.gitea.io/gitea/modules/git"
23
+ "code.gitea.io/gitea/modules/httpcache"
24
+ "code.gitea.io/gitea/modules/lfs"
25
+ "code.gitea.io/gitea/modules/log"
21
26
"code.gitea.io/gitea/modules/setting"
27
+ "code.gitea.io/gitea/modules/storage"
22
28
api "code.gitea.io/gitea/modules/structs"
23
29
"code.gitea.io/gitea/modules/web"
24
30
"code.gitea.io/gitea/routers/common"
@@ -75,6 +81,142 @@ func GetRawFile(ctx *context.APIContext) {
75
81
}
76
82
}
77
83
84
+ // GetRawFileOrLFS get a file by repo's path, redirecting to LFS if necessary.
85
+ func GetRawFileOrLFS (ctx * context.APIContext ) {
86
+ // swagger:operation GET /repos/{owner}/{repo}/media/{filepath} repository repoGetRawFileOrLFS
87
+ // ---
88
+ // summary: Get a file or it's LFS object from a repository
89
+ // parameters:
90
+ // - name: owner
91
+ // in: path
92
+ // description: owner of the repo
93
+ // type: string
94
+ // required: true
95
+ // - name: repo
96
+ // in: path
97
+ // description: name of the repo
98
+ // type: string
99
+ // required: true
100
+ // - name: filepath
101
+ // in: path
102
+ // description: filepath of the file to get
103
+ // type: string
104
+ // required: true
105
+ // - name: ref
106
+ // in: query
107
+ // description: "The name of the commit/branch/tag. Default the repository’s default branch (usually master)"
108
+ // type: string
109
+ // required: false
110
+ // responses:
111
+ // 200:
112
+ // description: Returns raw file content.
113
+ // "404":
114
+ // "$ref": "#/responses/notFound"
115
+
116
+ if ctx .Repo .Repository .IsEmpty {
117
+ ctx .NotFound ()
118
+ return
119
+ }
120
+
121
+ blob , lastModified := getBlobForEntry (ctx )
122
+ if ctx .Written () {
123
+ return
124
+ }
125
+
126
+ // LFS Pointer files are at most 1024 bytes - so any blob greater than 1024 bytes cannot be an LFS file
127
+ if blob .Size () > 1024 {
128
+ // First handle caching for the blob
129
+ if httpcache .HandleGenericETagTimeCache (ctx .Req , ctx .Resp , `"` + blob .ID .String ()+ `"` , lastModified ) {
130
+ return
131
+ }
132
+
133
+ // OK not cached - serve!
134
+ if err := common .ServeBlob (ctx .Context , blob , lastModified ); err != nil {
135
+ ctx .ServerError ("ServeBlob" , err )
136
+ }
137
+ return
138
+ }
139
+
140
+ // OK, now the blob is known to have at most 1024 bytes we can simply read this in in one go (This saves reading it twice)
141
+ dataRc , err := blob .DataAsync ()
142
+ if err != nil {
143
+ ctx .ServerError ("DataAsync" , err )
144
+ return
145
+ }
146
+
147
+ buf , err := io .ReadAll (dataRc )
148
+ if err != nil {
149
+ _ = dataRc .Close ()
150
+ ctx .ServerError ("DataAsync" , err )
151
+ return
152
+ }
153
+
154
+ if err := dataRc .Close (); err != nil {
155
+ log .Error ("Error whilst closing blob %s reader in %-v. Error: %v" , blob .ID , ctx .Context .Repo .Repository , err )
156
+ }
157
+
158
+ // Check if the blob represents a pointer
159
+ pointer , _ := lfs .ReadPointer (bytes .NewReader (buf ))
160
+
161
+ // if its not a pointer just serve the data directly
162
+ if ! pointer .IsValid () {
163
+ // First handle caching for the blob
164
+ if httpcache .HandleGenericETagTimeCache (ctx .Req , ctx .Resp , `"` + blob .ID .String ()+ `"` , lastModified ) {
165
+ return
166
+ }
167
+
168
+ // OK not cached - serve!
169
+ if err := common .ServeData (ctx .Context , ctx .Repo .TreePath , blob .Size (), bytes .NewReader (buf )); err != nil {
170
+ ctx .ServerError ("ServeBlob" , err )
171
+ }
172
+ return
173
+ }
174
+
175
+ // Now check if there is a meta object for this pointer
176
+ meta , err := models .GetLFSMetaObjectByOid (ctx .Repo .Repository .ID , pointer .Oid )
177
+
178
+ // If there isn't one just serve the data directly
179
+ if err == models .ErrLFSObjectNotExist {
180
+ // Handle caching for the blob SHA (not the LFS object OID)
181
+ if httpcache .HandleGenericETagTimeCache (ctx .Req , ctx .Resp , `"` + blob .ID .String ()+ `"` , lastModified ) {
182
+ return
183
+ }
184
+
185
+ if err := common .ServeData (ctx .Context , ctx .Repo .TreePath , blob .Size (), bytes .NewReader (buf )); err != nil {
186
+ ctx .ServerError ("ServeBlob" , err )
187
+ }
188
+ return
189
+ } else if err != nil {
190
+ ctx .ServerError ("GetLFSMetaObjectByOid" , err )
191
+ return
192
+ }
193
+
194
+ // Handle caching for the LFS object OID
195
+ if httpcache .HandleGenericETagCache (ctx .Req , ctx .Resp , `"` + pointer .Oid + `"` ) {
196
+ return
197
+ }
198
+
199
+ if setting .LFS .ServeDirect {
200
+ // If we have a signed url (S3, object storage), redirect to this directly.
201
+ u , err := storage .LFS .URL (pointer .RelativePath (), blob .Name ())
202
+ if u != nil && err == nil {
203
+ ctx .Redirect (u .String ())
204
+ return
205
+ }
206
+ }
207
+
208
+ lfsDataRc , err := lfs .ReadMetaObject (meta .Pointer )
209
+ if err != nil {
210
+ ctx .ServerError ("ReadMetaObject" , err )
211
+ return
212
+ }
213
+ defer lfsDataRc .Close ()
214
+
215
+ if err := common .ServeData (ctx .Context , ctx .Repo .TreePath , meta .Size , lfsDataRc ); err != nil {
216
+ ctx .ServerError ("ServeData" , err )
217
+ }
218
+ }
219
+
78
220
func getBlobForEntry (ctx * context.APIContext ) (blob * git.Blob , lastModified time.Time ) {
79
221
entry , err := ctx .Repo .Commit .GetTreeEntryByPath (ctx .Repo .TreePath )
80
222
if err != nil {
0 commit comments