Sniff Content-Type during site update.

This isn't yet used in the code responding to GET requests because we
do not yet have a migration path for legacy code.
This commit is contained in:
Catherine
2025-10-21 03:40:29 +00:00
parent 0a111234f2
commit 25f7ea08c9
4 changed files with 57 additions and 19 deletions

View File

@@ -9,7 +9,10 @@ import (
"errors"
"fmt"
"log"
"mime"
"net/http"
"path"
"path/filepath"
"strings"
"sync"
@@ -122,6 +125,24 @@ again:
}
}
// Sniff content type using the same algorithm as `http.ServeContent`.
func DetectContentType(manifest *Manifest) {
for path, entry := range manifest.Contents {
if entry.GetType() == Type_Directory || entry.GetType() == Type_Symlink {
// no Content-Type
} else if entry.GetType() == Type_InlineFile && entry.GetTransform() == Transform_None {
contentType := mime.TypeByExtension(filepath.Ext(path))
if contentType == "" {
contentType = http.DetectContentType(entry.Data[:512])
}
entry.ContentType = proto.String(contentType)
} else {
panic(fmt.Errorf("DetectContentType encountered invalid entry: %v, %v",
entry.GetType(), entry.GetTransform()))
}
}
}
// The `clauspost/compress/zstd` package recommends reusing a compressor to avoid repeated
// allocations of internal buffers.
var zstdEncoder, _ = zstd.NewWriter(nil, zstd.WithEncoderLevel(zstd.SpeedBetterCompression))
@@ -133,13 +154,13 @@ func CompressFiles(ctx context.Context, manifest *Manifest) {
var originalSize, transformedSize int64
for _, entry := range manifest.Contents {
if entry.GetType() == Type_InlineFile && entry.GetXfrm() == Transform_None {
if entry.GetType() == Type_InlineFile && entry.GetTransform() == Transform_None {
originalSize += entry.GetSize()
compressedData := zstdEncoder.EncodeAll(entry.GetData(), make([]byte, 0, entry.GetSize()))
if len(compressedData) < int(*entry.Size) {
entry.Data = compressedData
entry.Size = proto.Int64(int64(len(entry.Data)))
entry.Xfrm = Transform_Zstandard.Enum()
entry.Transform = Transform_Zstandard.Enum()
}
transformedSize += entry.GetSize()
}
@@ -163,6 +184,8 @@ func PrepareManifest(ctx context.Context, manifest *Manifest) error {
log.Printf("redirects ok: %d rules\n", len(manifest.Redirects))
}
DetectContentType(manifest)
if config.Feature("compress") {
CompressFiles(ctx, manifest)
}
@@ -196,10 +219,11 @@ func StoreManifest(ctx context.Context, name string, manifest *Manifest) (*Manif
if cannotBeInlined {
dataHash := sha256.Sum256(entry.Data)
extManifest.Contents[name] = &Entry{
Type: Type_ExternalFile.Enum(),
Size: entry.Size,
Data: fmt.Appendf(nil, "sha256-%x", dataHash),
Xfrm: entry.Xfrm,
Type: Type_ExternalFile.Enum(),
Size: entry.Size,
Data: fmt.Appendf(nil, "sha256-%x", dataHash),
Transform: entry.Transform,
ContentType: entry.ContentType,
}
extObjectMap[string(dataHash[:])] = *entry.Size
} else {

View File

@@ -223,7 +223,7 @@ func getPage(w http.ResponseWriter, r *http.Request) error {
defer closer.Close()
}
switch entry.GetXfrm() {
switch entry.GetTransform() {
case Transform_None:
// nothing to do
case Transform_Zstandard:

View File

@@ -144,7 +144,10 @@ type Entry struct {
Data []byte `protobuf:"bytes,3,opt,name=data" json:"data,omitempty"`
// Only present for `type == InlineFile` and `type == ExternalFile` that
// have been transformed.
Xfrm *Transform `protobuf:"varint,4,opt,name=xfrm,enum=Transform" json:"xfrm,omitempty"`
Transform *Transform `protobuf:"varint,4,opt,name=transform,enum=Transform" json:"transform,omitempty"`
// Only present for `type == InlineFile` and `type == ExternalFile`.
// Currently, optional (not present on certain legacy manifests).
ContentType *string `protobuf:"bytes,5,opt,name=content_type,json=contentType" json:"content_type,omitempty"`
unknownFields protoimpl.UnknownFields
sizeCache protoimpl.SizeCache
}
@@ -200,13 +203,20 @@ func (x *Entry) GetData() []byte {
return nil
}
func (x *Entry) GetXfrm() Transform {
if x != nil && x.Xfrm != nil {
return *x.Xfrm
func (x *Entry) GetTransform() Transform {
if x != nil && x.Transform != nil {
return *x.Transform
}
return Transform_None
}
func (x *Entry) GetContentType() string {
if x != nil && x.ContentType != nil {
return *x.ContentType
}
return ""
}
// See https://docs.netlify.com/manage/routing/redirects/overview/ for details.
// Only a subset of the Netlify specification is representable here.
type Redirect struct {
@@ -337,8 +347,8 @@ type Manifest struct {
Commit *string `protobuf:"bytes,3,opt,name=commit" json:"commit,omitempty"`
// Contents
Contents map[string]*Entry `protobuf:"bytes,4,rep,name=contents" json:"contents,omitempty" protobuf_key:"bytes,1,opt,name=key" protobuf_val:"bytes,2,opt,name=value"`
TotalSize *int64 `protobuf:"varint,5,opt,name=total_size,json=totalSize" json:"total_size,omitempty"`
StoredSize *int64 `protobuf:"varint,8,opt,name=stored_size,json=storedSize" json:"stored_size,omitempty"` // after deduplication
TotalSize *int64 `protobuf:"varint,5,opt,name=total_size,json=totalSize" json:"total_size,omitempty"` // simple sum of each `entry.size`
StoredSize *int64 `protobuf:"varint,8,opt,name=stored_size,json=storedSize" json:"stored_size,omitempty"` // external objects, after deduplication
// Netlify-style `_redirects`
Redirects []*Redirect `protobuf:"bytes,6,rep,name=redirects" json:"redirects,omitempty"`
// Diagnostics for non-fatal errors
@@ -437,13 +447,14 @@ var File_schema_proto protoreflect.FileDescriptor
const file_schema_proto_rawDesc = "" +
"\n" +
"\fschema.proto\"j\n" +
"\fschema.proto\"\x97\x01\n" +
"\x05Entry\x12\x19\n" +
"\x04type\x18\x01 \x01(\x0e2\x05.TypeR\x04type\x12\x12\n" +
"\x04size\x18\x02 \x01(\x03R\x04size\x12\x12\n" +
"\x04data\x18\x03 \x01(\fR\x04data\x12\x1e\n" +
"\x04xfrm\x18\x04 \x01(\x0e2\n" +
".TransformR\x04xfrm\"\\\n" +
"\x04data\x18\x03 \x01(\fR\x04data\x12(\n" +
"\ttransform\x18\x04 \x01(\x0e2\n" +
".TransformR\ttransform\x12!\n" +
"\fcontent_type\x18\x05 \x01(\tR\vcontentType\"\\\n" +
"\bRedirect\x12\x12\n" +
"\x04from\x18\x01 \x01(\tR\x04from\x12\x0e\n" +
"\x02to\x18\x02 \x01(\tR\x02to\x12\x16\n" +
@@ -502,7 +513,7 @@ var file_schema_proto_goTypes = []any{
}
var file_schema_proto_depIdxs = []int32{
0, // 0: Entry.type:type_name -> Type
1, // 1: Entry.xfrm:type_name -> Transform
1, // 1: Entry.transform:type_name -> Transform
6, // 2: Manifest.contents:type_name -> Manifest.ContentsEntry
3, // 3: Manifest.redirects:type_name -> Redirect
4, // 4: Manifest.problems:type_name -> Problem

View File

@@ -36,7 +36,10 @@ message Entry {
bytes data = 3;
// Only present for `type == InlineFile` and `type == ExternalFile` that
// have been transformed.
Transform xfrm = 4;
Transform transform = 4;
// Only present for `type == InlineFile` and `type == ExternalFile`.
// Currently, optional (not present on certain legacy manifests).
string content_type = 5;
}
// See https://docs.netlify.com/manage/routing/redirects/overview/ for details.