-
-
Notifications
You must be signed in to change notification settings - Fork 5.9k
WIP: Serve LFS/attachment with http.ServeContent to Support Range-Request #20480
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
55241a3
34fa2ce
f2ee607
d085944
0f7f583
1953d84
37813a6
c6630dd
6743d56
8deb747
a456cde
ea62aab
7c60fc8
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1 +1 @@ | ||
1032bbf17fbc0d9c95bb5418dabe8f8c99278700 | ||
808eedf6b8dd519aa89b59af2d815ed668580fc2 |
Original file line number | Diff line number | Diff line change | ||||||||
---|---|---|---|---|---|---|---|---|---|---|
|
@@ -7,7 +7,9 @@ package typesniffer | |||||||||
import ( | ||||||||||
"fmt" | ||||||||||
"io" | ||||||||||
"mime" | ||||||||||
"net/http" | ||||||||||
"path/filepath" | ||||||||||
"regexp" | ||||||||||
"strings" | ||||||||||
|
||||||||||
|
@@ -36,32 +38,32 @@ type SniffedType struct { | |||||||||
|
||||||||||
// IsText etects if content format is plain text. | ||||||||||
func (ct SniffedType) IsText() bool { | ||||||||||
return strings.Contains(ct.contentType, "text/") | ||||||||||
return strings.HasPrefix(ct.contentType, "text/") | ||||||||||
} | ||||||||||
|
||||||||||
// IsImage detects if data is an image format | ||||||||||
func (ct SniffedType) IsImage() bool { | ||||||||||
return strings.Contains(ct.contentType, "image/") | ||||||||||
return strings.HasPrefix(ct.contentType, "image/") | ||||||||||
} | ||||||||||
|
||||||||||
// IsSvgImage detects if data is an SVG image format | ||||||||||
func (ct SniffedType) IsSvgImage() bool { | ||||||||||
return strings.Contains(ct.contentType, SvgMimeType) | ||||||||||
return strings.HasPrefix(ct.contentType, SvgMimeType) | ||||||||||
} | ||||||||||
|
||||||||||
// IsPDF detects if data is a PDF format | ||||||||||
func (ct SniffedType) IsPDF() bool { | ||||||||||
return strings.Contains(ct.contentType, "application/pdf") | ||||||||||
return strings.HasPrefix(ct.contentType, "application/pdf") | ||||||||||
} | ||||||||||
|
||||||||||
// IsVideo detects if data is an video format | ||||||||||
func (ct SniffedType) IsVideo() bool { | ||||||||||
return strings.Contains(ct.contentType, "video/") | ||||||||||
return strings.HasPrefix(ct.contentType, "video/") | ||||||||||
} | ||||||||||
|
||||||||||
// IsAudio detects if data is an video format | ||||||||||
func (ct SniffedType) IsAudio() bool { | ||||||||||
return strings.Contains(ct.contentType, "audio/") | ||||||||||
return strings.HasPrefix(ct.contentType, "audio/") | ||||||||||
} | ||||||||||
|
||||||||||
// IsRepresentableAsText returns true if file content can be represented as | ||||||||||
|
@@ -70,6 +72,11 @@ func (ct SniffedType) IsRepresentableAsText() bool { | |||||||||
return ct.IsText() || ct.IsSvgImage() | ||||||||||
} | ||||||||||
|
||||||||||
// Mime return the mime | ||||||||||
func (ct SniffedType) Mime() string { | ||||||||||
return strings.Split(ct.contentType, ";")[0] | ||||||||||
} | ||||||||||
|
||||||||||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Note to self: this is a better version than I have in #20464, will incorporate there. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. we should use SplitN so go can skip split after first ; ... just some tiny optimization nits There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. @silverwind I'm going to delete the mime stuff as it's unrelated to the pull topic! - so feel free to pick it for your pull :) There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. sure, for reference, code was: func (ct SniffedType) Mime() string {
return strings.Split(ct.contentType, ";")[0]
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. If we are talking about micro-optimization, Edit: Done, added it as
Comment on lines
+75
to
+79
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
Suggested change
Delete as discussed, it's unused. |
||||||||||
// DetectContentType extends http.DetectContentType with more content types. Defaults to text/unknown if input is empty. | ||||||||||
func DetectContentType(data []byte) SniffedType { | ||||||||||
if len(data) == 0 { | ||||||||||
|
@@ -82,15 +89,35 @@ func DetectContentType(data []byte) SniffedType { | |||||||||
data = data[:sniffLen] | ||||||||||
} | ||||||||||
|
||||||||||
if (strings.Contains(ct, "text/plain") || strings.Contains(ct, "text/html")) && svgTagRegex.Match(data) || | ||||||||||
strings.Contains(ct, "text/xml") && svgTagInXMLRegex.Match(data) { | ||||||||||
if (strings.HasPrefix(ct, "text/plain") || strings.HasPrefix(ct, "text/html")) && svgTagRegex.Match(data) || | ||||||||||
strings.HasPrefix(ct, "text/xml") && svgTagInXMLRegex.Match(data) { | ||||||||||
// SVG is unsupported. https://github.com/golang/go/issues/15888 | ||||||||||
ct = SvgMimeType | ||||||||||
} | ||||||||||
|
||||||||||
return SniffedType{ct} | ||||||||||
} | ||||||||||
|
||||||||||
// DetectContentTypeExtFirst | ||||||||||
// detect content type by `name` first, if not found, detect by `reader` | ||||||||||
// Note: you may need `reader.Seek(0, io.SeekStart)` to reset the offset | ||||||||||
func DetectContentTypeExtFirst(name string, bytesOrReader interface{}) (SniffedType, error) { | ||||||||||
ct := mime.TypeByExtension(filepath.Ext(name)) | ||||||||||
// FIXME: Not sure if it's necessary to keep the old behavior. | ||||||||||
// if ct != "" && !strings.HasPrefix(ct, "text/") { | ||||||||||
if ct != "" { | ||||||||||
return SniffedType{ct}, nil | ||||||||||
} | ||||||||||
if r, ok := bytesOrReader.(io.Reader); ok { | ||||||||||
st, err := DetectContentTypeFromReader(r) | ||||||||||
if nil != err { | ||||||||||
return SniffedType{}, err | ||||||||||
} | ||||||||||
return st, nil | ||||||||||
} | ||||||||||
return DetectContentType(bytesOrReader.([]byte)), nil | ||||||||||
} | ||||||||||
|
||||||||||
// DetectContentTypeFromReader guesses the content type contained in the reader. | ||||||||||
func DetectContentTypeFromReader(r io.Reader) (SniffedType, error) { | ||||||||||
buf := make([]byte, sniffLen) | ||||||||||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -7,8 +7,9 @@ package common | |
import ( | ||
"fmt" | ||
"io" | ||
"path" | ||
"net/http" | ||
"path/filepath" | ||
"strconv" | ||
"strings" | ||
"time" | ||
|
||
|
@@ -22,7 +23,8 @@ import ( | |
"code.gitea.io/gitea/modules/util" | ||
) | ||
|
||
// ServeBlob download a git.Blob | ||
// ServeBlob serve git.Blob which represents a normal(non-lfs) file stored in repositories | ||
// todo: implement io.Seeker for git.Blob.blobReader to support Range-Request | ||
func ServeBlob(ctx *context.Context, blob *git.Blob, lastModified time.Time) error { | ||
if httpcache.HandleGenericETagTimeCache(ctx.Req, ctx.Resp, `"`+blob.ID.String()+`"`, lastModified) { | ||
return nil | ||
|
@@ -38,49 +40,75 @@ func ServeBlob(ctx *context.Context, blob *git.Blob, lastModified time.Time) err | |
} | ||
}() | ||
|
||
return ServeData(ctx, ctx.Repo.TreePath, blob.Size(), dataRc) | ||
} | ||
|
||
// ServeData download file from io.Reader | ||
func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) error { | ||
buf := make([]byte, 1024) | ||
n, err := util.ReadAtMost(reader, buf) | ||
n, err := util.ReadAtMost(dataRc, buf) | ||
if err != nil { | ||
return err | ||
} | ||
if n >= 0 { | ||
buf = buf[:n] | ||
} | ||
|
||
ctx.Resp.Header().Set("Cache-Control", "public,max-age=86400") | ||
|
||
size := blob.Size() | ||
if size >= 0 { | ||
ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size)) | ||
ctx.Resp.Header().Set("Content-Length", strconv.FormatInt(size, 10)) | ||
} else { | ||
log.Error("ServeData called to serve data: %s with size < 0: %d", name, size) | ||
log.Error("ServeData called to serve data: %s with size < 0: %d", ctx.Repo.TreePath, size) | ||
} | ||
|
||
if err := setCommonHeaders(ctx, ctx.Repo.TreePath, buf); err != nil { | ||
return err | ||
} | ||
|
||
_, err = ctx.Resp.Write(buf) | ||
if err != nil { | ||
return err | ||
} | ||
name = path.Base(name) | ||
_, err = io.Copy(ctx.Resp, dataRc) | ||
return err | ||
} | ||
|
||
func setCommonHeaders(ctx *context.Context, name string, data interface{}) error { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Not sure this additional function is of any benefit. Cache header can be set like in #20484 via |
||
// Google Chrome dislike commas in filenames, so let's change it to a space | ||
name = strings.ReplaceAll(name, ",", " ") | ||
|
||
st := typesniffer.DetectContentType(buf) | ||
ctx.Resp.Header().Set("Cache-Control", "public, max-age=300") | ||
|
||
// reset the offset to the start of served file | ||
if seeker, ok := data.(io.ReadSeeker); ok { | ||
_, _ = seeker.Seek(0, io.SeekStart) | ||
} | ||
|
||
st, err := typesniffer.DetectContentTypeExtFirst(name, data) | ||
if nil != err { | ||
return err | ||
} | ||
|
||
mappedMimeType := "" | ||
if setting.MimeTypeMap.Enabled { | ||
fileExtension := strings.ToLower(filepath.Ext(name)) | ||
mappedMimeType = setting.MimeTypeMap.Map[fileExtension] | ||
} | ||
|
||
if st.IsText() || ctx.FormBool("render") { | ||
cs, err := charset.DetectEncoding(buf) | ||
var cs string | ||
var err error | ||
if reader, ok := data.(io.ReadSeeker); ok { | ||
cs, err = charset.DetectEncodingFromReader(reader) | ||
_, _ = reader.Seek(0, io.SeekStart) | ||
} else { | ||
cs, err = charset.DetectEncoding(data.([]byte)) | ||
} | ||
if err != nil { | ||
log.Error("Detect raw file %s charset failed: %v, using by default utf-8", name, err) | ||
cs = "utf-8" | ||
} | ||
|
||
if mappedMimeType == "" { | ||
mappedMimeType = "text/plain" | ||
} | ||
ctx.Resp.Header().Set("Content-Type", mappedMimeType+"; charset="+strings.ToLower(cs)) | ||
|
||
} else { | ||
ctx.Resp.Header().Set("Access-Control-Expose-Headers", "Content-Disposition") | ||
if mappedMimeType != "" { | ||
|
@@ -102,10 +130,14 @@ func ServeData(ctx *context.Context, name string, size int64, reader io.Reader) | |
} | ||
} | ||
|
||
_, err = ctx.Resp.Write(buf) | ||
if err != nil { | ||
return nil | ||
} | ||
|
||
// ServeLargeFile Serve files stored with Git LFS and attachments uploaded on the Releases page | ||
func ServeLargeFile(ctx *context.Context, name string, time time.Time, reader io.ReadSeeker) error { | ||
if err := setCommonHeaders(ctx, name, reader); err != nil { | ||
return err | ||
} | ||
_, err = io.Copy(ctx.Resp, reader) | ||
return err | ||
http.ServeContent(ctx.Resp, ctx.Req, name, time, reader) | ||
return nil | ||
} |
Uh oh!
There was an error while loading. Please reload this page.