Related #18543 Co-authored-by: Lunny Xiao <xiaolunwen@gmail.com>tags/v1.19.0-rc0
@@ -349,9 +349,11 @@ func (ctx *Context) RespHeader() http.Header { | |||
type ServeHeaderOptions struct { | |||
ContentType string // defaults to "application/octet-stream" | |||
ContentTypeCharset string | |||
ContentLength *int64 | |||
Disposition string // defaults to "attachment" | |||
Filename string | |||
CacheDuration time.Duration // defaults to 5 minutes | |||
LastModified time.Time | |||
} | |||
// SetServeHeaders sets necessary content serve headers | |||
@@ -369,6 +371,10 @@ func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) { | |||
header.Set("Content-Type", contentType) | |||
header.Set("X-Content-Type-Options", "nosniff") | |||
if opts.ContentLength != nil { | |||
header.Set("Content-Length", strconv.FormatInt(*opts.ContentLength, 10)) | |||
} | |||
if opts.Filename != "" { | |||
disposition := opts.Disposition | |||
if disposition == "" { | |||
@@ -385,14 +391,16 @@ func (ctx *Context) SetServeHeaders(opts *ServeHeaderOptions) { | |||
duration = 5 * time.Minute | |||
} | |||
httpcache.AddCacheControlToHeader(header, duration) | |||
if !opts.LastModified.IsZero() { | |||
header.Set("Last-Modified", opts.LastModified.UTC().Format(http.TimeFormat)) | |||
} | |||
} | |||
// ServeContent serves content to http request | |||
func (ctx *Context) ServeContent(name string, r io.ReadSeeker, modTime time.Time) { | |||
ctx.SetServeHeaders(&ServeHeaderOptions{ | |||
Filename: name, | |||
}) | |||
http.ServeContent(ctx.Resp, ctx.Req, name, modTime, r) | |||
func (ctx *Context) ServeContent(r io.ReadSeeker, opts *ServeHeaderOptions) { | |||
ctx.SetServeHeaders(opts) | |||
http.ServeContent(ctx.Resp, ctx.Req, opts.Filename, opts.LastModified, r) | |||
} | |||
// UploadStream returns the request body or the first form file |
@@ -181,6 +181,7 @@ func CommonRoutes(ctx gocontext.Context) *web.Route { | |||
r.Group("/maven", func() { | |||
r.Put("/*", reqPackageAccess(perm.AccessModeWrite), maven.UploadPackageFile) | |||
r.Get("/*", maven.DownloadPackageFile) | |||
r.Head("/*", maven.ProvidePackageFileHeader) | |||
}, reqPackageAccess(perm.AccessModeRead)) | |||
r.Group("/nuget", func() { | |||
r.Group("", func() { // Needs to be unauthenticated for the NuGet client. |
@@ -184,7 +184,10 @@ func DownloadPackageFile(ctx *context.Context) { | |||
} | |||
defer s.Close() | |||
ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(s, &context.ServeHeaderOptions{ | |||
Filename: pf.Name, | |||
LastModified: pf.CreatedUnix.AsLocalTime(), | |||
}) | |||
} | |||
// UploadPackage creates a new package |
@@ -477,7 +477,10 @@ func downloadFile(ctx *context.Context, fileFilter container.Set[string], fileKe | |||
} | |||
defer s.Close() | |||
ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(s, &context.ServeHeaderOptions{ | |||
Filename: pf.Name, | |||
LastModified: pf.CreatedUnix.AsLocalTime(), | |||
}) | |||
} | |||
// DeleteRecipeV1 deletes the requested recipe(s) |
@@ -53,7 +53,10 @@ func DownloadPackageFile(ctx *context.Context) { | |||
} | |||
defer s.Close() | |||
ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(s, &context.ServeHeaderOptions{ | |||
Filename: pf.Name, | |||
LastModified: pf.CreatedUnix.AsLocalTime(), | |||
}) | |||
} | |||
// UploadPackage uploads the specific generic package. |
@@ -138,7 +138,10 @@ func DownloadPackageFile(ctx *context.Context) { | |||
} | |||
defer s.Close() | |||
ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(s, &context.ServeHeaderOptions{ | |||
Filename: pf.Name, | |||
LastModified: pf.CreatedUnix.AsLocalTime(), | |||
}) | |||
} | |||
// UploadPackage creates a new package |
@@ -6,7 +6,6 @@ package maven | |||
import ( | |||
"encoding/xml" | |||
"sort" | |||
"strings" | |||
packages_model "code.gitea.io/gitea/models/packages" | |||
@@ -23,12 +22,8 @@ type MetadataResponse struct { | |||
Version []string `xml:"versioning>versions>version"` | |||
} | |||
// pds is expected to be sorted ascending by CreatedUnix | |||
func createMetadataResponse(pds []*packages_model.PackageDescriptor) *MetadataResponse { | |||
sort.Slice(pds, func(i, j int) bool { | |||
// Maven and Gradle order packages by their creation timestamp and not by their version string | |||
return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix | |||
}) | |||
var release *packages_model.PackageDescriptor | |||
versions := make([]string, 0, len(pds)) |
@@ -16,6 +16,8 @@ import ( | |||
"net/http" | |||
"path/filepath" | |||
"regexp" | |||
"sort" | |||
"strconv" | |||
"strings" | |||
packages_model "code.gitea.io/gitea/models/packages" | |||
@@ -34,6 +36,10 @@ const ( | |||
extensionSHA1 = ".sha1" | |||
extensionSHA256 = ".sha256" | |||
extensionSHA512 = ".sha512" | |||
extensionPom = ".pom" | |||
extensionJar = ".jar" | |||
contentTypeJar = "application/java-archive" | |||
contentTypeXML = "text/xml" | |||
) | |||
var ( | |||
@@ -49,6 +55,15 @@ func apiError(ctx *context.Context, status int, obj interface{}) { | |||
// DownloadPackageFile serves the content of a package | |||
func DownloadPackageFile(ctx *context.Context) { | |||
handlePackageFile(ctx, true) | |||
} | |||
// ProvidePackageFileHeader provides only the headers describing a package | |||
func ProvidePackageFileHeader(ctx *context.Context) { | |||
handlePackageFile(ctx, false) | |||
} | |||
func handlePackageFile(ctx *context.Context, serveContent bool) { | |||
params, err := extractPathParameters(ctx) | |||
if err != nil { | |||
apiError(ctx, http.StatusBadRequest, err) | |||
@@ -58,7 +73,7 @@ func DownloadPackageFile(ctx *context.Context) { | |||
if params.IsMeta && params.Version == "" { | |||
serveMavenMetadata(ctx, params) | |||
} else { | |||
servePackageFile(ctx, params) | |||
servePackageFile(ctx, params, serveContent) | |||
} | |||
} | |||
@@ -82,6 +97,11 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { | |||
return | |||
} | |||
sort.Slice(pds, func(i, j int) bool { | |||
// Maven and Gradle order packages by their creation timestamp and not by their version string | |||
return pds[i].Version.CreatedUnix < pds[j].Version.CreatedUnix | |||
}) | |||
xmlMetadata, err := xml.Marshal(createMetadataResponse(pds)) | |||
if err != nil { | |||
apiError(ctx, http.StatusInternalServerError, err) | |||
@@ -89,6 +109,9 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { | |||
} | |||
xmlMetadataWithHeader := append([]byte(xml.Header), xmlMetadata...) | |||
latest := pds[len(pds)-1] | |||
ctx.Resp.Header().Set("Last-Modified", latest.Version.CreatedUnix.Format(http.TimeFormat)) | |||
ext := strings.ToLower(filepath.Ext(params.Filename)) | |||
if isChecksumExtension(ext) { | |||
var hash []byte | |||
@@ -110,10 +133,15 @@ func serveMavenMetadata(ctx *context.Context, params parameters) { | |||
return | |||
} | |||
ctx.PlainTextBytes(http.StatusOK, xmlMetadataWithHeader) | |||
ctx.Resp.Header().Set("Content-Length", strconv.Itoa(len(xmlMetadataWithHeader))) | |||
ctx.Resp.Header().Set("Content-Type", contentTypeXML) | |||
if _, err := ctx.Resp.Write(xmlMetadataWithHeader); err != nil { | |||
log.Error("write bytes failed: %v", err) | |||
} | |||
} | |||
func servePackageFile(ctx *context.Context, params parameters) { | |||
func servePackageFile(ctx *context.Context, params parameters, serveContent bool) { | |||
packageName := params.GroupID + "-" + params.ArtifactID | |||
pv, err := packages_model.GetVersionByNameAndVersion(ctx, ctx.Package.Owner.ID, packages_model.TypeMaven, packageName, params.Version) | |||
@@ -165,6 +193,23 @@ func servePackageFile(ctx *context.Context, params parameters) { | |||
return | |||
} | |||
opts := &context.ServeHeaderOptions{ | |||
ContentLength: &pb.Size, | |||
LastModified: pf.CreatedUnix.AsLocalTime(), | |||
} | |||
switch ext { | |||
case extensionJar: | |||
opts.ContentType = contentTypeJar | |||
case extensionPom: | |||
opts.ContentType = contentTypeXML | |||
} | |||
if !serveContent { | |||
ctx.SetServeHeaders(opts) | |||
ctx.Status(http.StatusOK) | |||
return | |||
} | |||
s, err := packages_module.NewContentStore().Get(packages_module.BlobHash256Key(pb.HashSHA256)) | |||
if err != nil { | |||
apiError(ctx, http.StatusInternalServerError, err) | |||
@@ -177,7 +222,9 @@ func servePackageFile(ctx *context.Context, params parameters) { | |||
} | |||
} | |||
ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) | |||
opts.Filename = pf.Name | |||
ctx.ServeContent(s, opts) | |||
} | |||
// UploadPackageFile adds a file to the package. If the package does not exist, it gets created. | |||
@@ -273,7 +320,7 @@ func UploadPackageFile(ctx *context.Context) { | |||
} | |||
// If it's the package pom file extract the metadata | |||
if ext == ".pom" { | |||
if ext == extensionPom { | |||
pfci.IsLead = true | |||
var err error |
@@ -103,7 +103,10 @@ func DownloadPackageFile(ctx *context.Context) { | |||
} | |||
defer s.Close() | |||
ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(s, &context.ServeHeaderOptions{ | |||
Filename: pf.Name, | |||
LastModified: pf.CreatedUnix.AsLocalTime(), | |||
}) | |||
} | |||
// DownloadPackageFileByName finds the version and serves the contents of a package | |||
@@ -146,7 +149,10 @@ func DownloadPackageFileByName(ctx *context.Context) { | |||
} | |||
defer s.Close() | |||
ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(s, &context.ServeHeaderOptions{ | |||
Filename: pf.Name, | |||
LastModified: pf.CreatedUnix.AsLocalTime(), | |||
}) | |||
} | |||
// UploadPackage creates a new package |
@@ -342,7 +342,10 @@ func DownloadPackageFile(ctx *context.Context) { | |||
} | |||
defer s.Close() | |||
ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(s, &context.ServeHeaderOptions{ | |||
Filename: pf.Name, | |||
LastModified: pf.CreatedUnix.AsLocalTime(), | |||
}) | |||
} | |||
// UploadPackage creates a new package with the metadata contained in the uploaded nupgk file | |||
@@ -562,7 +565,10 @@ func DownloadSymbolFile(ctx *context.Context) { | |||
} | |||
defer s.Close() | |||
ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(s, &context.ServeHeaderOptions{ | |||
Filename: pf.Name, | |||
LastModified: pf.CreatedUnix.AsLocalTime(), | |||
}) | |||
} | |||
// DeletePackage hard deletes the package |
@@ -275,5 +275,8 @@ func DownloadPackageFile(ctx *context.Context) { | |||
} | |||
defer s.Close() | |||
ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(s, &context.ServeHeaderOptions{ | |||
Filename: pf.Name, | |||
LastModified: pf.CreatedUnix.AsLocalTime(), | |||
}) | |||
} |
@@ -95,7 +95,10 @@ func DownloadPackageFile(ctx *context.Context) { | |||
} | |||
defer s.Close() | |||
ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(s, &context.ServeHeaderOptions{ | |||
Filename: pf.Name, | |||
LastModified: pf.CreatedUnix.AsLocalTime(), | |||
}) | |||
} | |||
// UploadPackageFile adds a file to the package. If the package does not exist, it gets created. |
@@ -192,7 +192,10 @@ func DownloadPackageFile(ctx *context.Context) { | |||
} | |||
defer s.Close() | |||
ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(s, &context.ServeHeaderOptions{ | |||
Filename: pf.Name, | |||
LastModified: pf.CreatedUnix.AsLocalTime(), | |||
}) | |||
} | |||
// UploadPackageFile adds a file to the package. If the package does not exist, it gets created. |
@@ -239,5 +239,8 @@ func DownloadPackageFile(ctx *context.Context) { | |||
} | |||
defer s.Close() | |||
ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(s, &context.ServeHeaderOptions{ | |||
Filename: pf.Name, | |||
LastModified: pf.CreatedUnix.AsLocalTime(), | |||
}) | |||
} |
@@ -341,7 +341,11 @@ func download(ctx *context.APIContext, archiveName string, archiver *repo_model. | |||
return | |||
} | |||
defer fr.Close() | |||
ctx.ServeContent(downloadName, fr, archiver.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(fr, &context.ServeHeaderOptions{ | |||
Filename: downloadName, | |||
LastModified: archiver.CreatedUnix.AsLocalTime(), | |||
}) | |||
} | |||
// GetEditorconfig get editor config of a repository |
@@ -5,7 +5,6 @@ | |||
package common | |||
import ( | |||
"fmt" | |||
"io" | |||
"path" | |||
"path/filepath" | |||
@@ -52,16 +51,16 @@ func ServeData(ctx *context.Context, filePath string, size int64, reader io.Read | |||
buf = buf[:n] | |||
} | |||
opts := &context.ServeHeaderOptions{ | |||
Filename: path.Base(filePath), | |||
} | |||
if size >= 0 { | |||
ctx.Resp.Header().Set("Content-Length", fmt.Sprintf("%d", size)) | |||
opts.ContentLength = &size | |||
} else { | |||
log.Error("ServeData called to serve data: %s with size < 0: %d", filePath, size) | |||
} | |||
opts := &context.ServeHeaderOptions{ | |||
Filename: path.Base(filePath), | |||
} | |||
sniffedType := typesniffer.DetectContentType(buf) | |||
isPlain := sniffedType.IsText() || ctx.FormBool("render") | |||
@@ -426,7 +426,10 @@ func download(ctx *context.Context, archiveName string, archiver *repo_model.Rep | |||
} | |||
defer fr.Close() | |||
ctx.ServeContent(downloadName, fr, archiver.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(fr, &context.ServeHeaderOptions{ | |||
Filename: downloadName, | |||
LastModified: archiver.CreatedUnix.AsLocalTime(), | |||
}) | |||
} | |||
// InitiateDownload will enqueue an archival request, as needed. It may submit |
@@ -402,5 +402,8 @@ func DownloadPackageFile(ctx *context.Context) { | |||
} | |||
defer s.Close() | |||
ctx.ServeContent(pf.Name, s, pf.CreatedUnix.AsLocalTime()) | |||
ctx.ServeContent(s, &context.ServeHeaderOptions{ | |||
Filename: pf.Name, | |||
LastModified: pf.CreatedUnix.AsLocalTime(), | |||
}) | |||
} |
@@ -7,6 +7,7 @@ package integration | |||
import ( | |||
"fmt" | |||
"net/http" | |||
"strconv" | |||
"strings" | |||
"testing" | |||
@@ -39,6 +40,12 @@ func TestPackageMaven(t *testing.T) { | |||
MakeRequest(t, req, expectedStatus) | |||
} | |||
checkHeaders := func(t *testing.T, h http.Header, contentType string, contentLength int64) { | |||
assert.Equal(t, contentType, h.Get("Content-Type")) | |||
assert.Equal(t, strconv.FormatInt(contentLength, 10), h.Get("Content-Length")) | |||
assert.NotEmpty(t, h.Get("Last-Modified")) | |||
} | |||
t.Run("Upload", func(t *testing.T) { | |||
defer tests.PrintCurrentTest(t)() | |||
@@ -77,10 +84,18 @@ func TestPackageMaven(t *testing.T) { | |||
t.Run("Download", func(t *testing.T) { | |||
defer tests.PrintCurrentTest(t)() | |||
req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)) | |||
req := NewRequest(t, "HEAD", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)) | |||
req = AddBasicAuthHeader(req, user.Name) | |||
resp := MakeRequest(t, req, http.StatusOK) | |||
checkHeaders(t, resp.Header(), "application/java-archive", 4) | |||
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s", root, packageVersion, filename)) | |||
req = AddBasicAuthHeader(req, user.Name) | |||
resp = MakeRequest(t, req, http.StatusOK) | |||
checkHeaders(t, resp.Header(), "application/java-archive", 4) | |||
assert.Equal(t, []byte("test"), resp.Body.Bytes()) | |||
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven) | |||
@@ -150,10 +165,18 @@ func TestPackageMaven(t *testing.T) { | |||
t.Run("DownloadPOM", func(t *testing.T) { | |||
defer tests.PrintCurrentTest(t)() | |||
req := NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)) | |||
req := NewRequest(t, "HEAD", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)) | |||
req = AddBasicAuthHeader(req, user.Name) | |||
resp := MakeRequest(t, req, http.StatusOK) | |||
checkHeaders(t, resp.Header(), "text/xml", int64(len(pomContent))) | |||
req = NewRequest(t, "GET", fmt.Sprintf("%s/%s/%s.pom", root, packageVersion, filename)) | |||
req = AddBasicAuthHeader(req, user.Name) | |||
resp = MakeRequest(t, req, http.StatusOK) | |||
checkHeaders(t, resp.Header(), "text/xml", int64(len(pomContent))) | |||
assert.Equal(t, []byte(pomContent), resp.Body.Bytes()) | |||
pvs, err := packages.GetVersionsByPackageType(db.DefaultContext, user.ID, packages.TypeMaven) | |||
@@ -191,6 +214,9 @@ func TestPackageMaven(t *testing.T) { | |||
resp := MakeRequest(t, req, http.StatusOK) | |||
expectedMetadata := `<?xml version="1.0" encoding="UTF-8"?>` + "\n<metadata><groupId>com.gitea</groupId><artifactId>test-project</artifactId><versioning><release>1.0.1</release><latest>1.0.1</latest><versions><version>1.0.1</version></versions></versioning></metadata>" | |||
checkHeaders(t, resp.Header(), "text/xml", int64(len(expectedMetadata))) | |||
assert.Equal(t, expectedMetadata, resp.Body.String()) | |||
for key, checksum := range map[string]string{ |