From 4894af9792cd7aa3048aa03c9ce65a4e61a22659 Mon Sep 17 00:00:00 2001 From: helei Date: Tue, 4 Aug 2026 20:03:00 +0800 Subject: [PATCH 1/2] =?UTF-8?q?func=20(ro=20*Route)=20loadFile(path=20stri?= =?UTF-8?q?ng)=20bug=E5=A4=84=E7=90=86?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Signed-off-by: helei --- net/http/route/file.go | 7 ++----- net/http/route/types.go | 7 +++++++ 2 files changed, 9 insertions(+), 5 deletions(-) diff --git a/net/http/route/file.go b/net/http/route/file.go index b7f8d21..61669f7 100644 --- a/net/http/route/file.go +++ b/net/http/route/file.go @@ -171,13 +171,10 @@ func (ro *Route) loadFile(path string) (http.FileSystem, bool) { hfs http.FileSystem enableEmbed = false ) - for _, cache := range ro.embed { if strings.HasPrefix(path, cache.Search) { - if cache.Prefix != "" { - path = filepath.Join(cache.Prefix, path) - } - hfs = http.FS(cache.FS) + _hfs, _ := fs.Sub(cache.FS, cache.Prefix) + hfs = http.FS(_hfs) enableEmbed = true break } diff --git a/net/http/route/types.go b/net/http/route/types.go index f97721a..c73d3f4 100644 --- a/net/http/route/types.go +++ b/net/http/route/types.go @@ -52,6 +52,13 @@ type ErrorResp struct { Error error } +// EmbedInfo 使用方式 +// +// .AddEmbed(&route.EmbedInfo{ +// Search: "/", +// Prefix: "build", +// FS: &html.HTMLFS, +// }) type EmbedInfo struct { Search string // 用search 去匹配请求path的前面部分,看是否包含 string.HasPrefix(path,search) From db69cd42c427ce76bc12d40a7793059e98592a49 Mon Sep 17 00:00:00 2001 From: helei Date: Sat, 22 Aug 2026 13:30:29 +0800 Subject: [PATCH 2/2] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=20GzipRaw=20/=20?= =?UTF-8?q?GzipAuto=20=E6=95=B0=E6=8D=AE=E5=BA=93=E8=87=AA=E5=8A=A8?= =?UTF-8?q?=E5=8E=8B=E7=BC=A9=E5=AD=98=E5=82=A8=E7=B1=BB=E5=9E=8B?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- dataType/gzip.go | 279 ++++++++++++++++++++++++++++++++++++++++++ dataType/gzip_test.go | 231 ++++++++++++++++++++++++++++++++++ 2 files changed, 510 insertions(+) create mode 100644 dataType/gzip.go create mode 100644 dataType/gzip_test.go diff --git a/dataType/gzip.go b/dataType/gzip.go new file mode 100644 index 0000000..6b5c0f5 --- /dev/null +++ b/dataType/gzip.go @@ -0,0 +1,279 @@ +package dataType + +import ( + "bytes" + "compress/gzip" + "database/sql/driver" + "encoding/base64" + "encoding/json" + "io" + + "gorm.io/gorm" + "gorm.io/gorm/schema" + "helay.net/go/utils/v3/tools" +) + +// GzipRaw 定义一个 Gzip 存储类型:写入数据库时对原始数据自动 Gzip 压缩, +// 读取(Scan)时原样返回数据库中的压缩字节,不解压。 +// 底层列类型由 BlobDbDataType 自动判断。 +// noinspection all +type GzipRaw struct { + data []byte + compressLevel int +} + +// NewGzipRaw 构造一个 GzipRaw,data 为待压缩的原始字节 +func NewGzipRaw(data []byte) GzipRaw { + return GzipRaw{ + data: data, + compressLevel: gzip.DefaultCompression, + } +} + +// Value 实现了 driver.Valuer 接口,写入数据库时将原始数据 Gzip 压缩 +// noinspection all +func (g GzipRaw) Value() (driver.Value, error) { + if g.data == nil { + return nil, nil + } + return gzipCompressLevel(g.data, g.compressLevel) +} + +// Scan 实现了 sql.Scanner 接口,读取数据库时原样保留压缩字节,不解压 +// noinspection all +func (g *GzipRaw) Scan(val any) error { + if val == nil { + g.data = nil + return nil + } + b, err := tools.Any2bytes(val) + if err != nil { + return err + } + g.data = b + return nil +} + +// GormDataType gorm common data type +// noinspection all +func (GzipRaw) GormDataType() string { + return "blob" +} + +// GormDBDataType gorm db data type,由 BlobDbDataType 自动判断 +// noinspection all +func (GzipRaw) GormDBDataType(db *gorm.DB, field *schema.Field) string { + return BlobDbDataType(db, field) +} + +// SetCompressLevel 设置压缩级别,取值同 compress/gzip(如 gzip.BestSpeed、 +// gzip.DefaultCompression、gzip.BestCompression)。0 表示使用默认压缩级别。 +// noinspection all +func (g *GzipRaw) SetCompressLevel(level int) { + g.compressLevel = level +} + +// GetCompressLevel 返回当前压缩级别 +// noinspection all +func (g GzipRaw) GetCompressLevel() int { + return g.compressLevel +} + +// SetValue 设置原始数据 +// noinspection all +func (g *GzipRaw) SetValue(v []byte) { + g.data = v +} + +// GetValue 返回原始数据(对于 GzipRaw,即调用方设置/扫描进来的字节) +// noinspection all +func (g GzipRaw) GetValue() []byte { + return g.data +} + +// IsNil 判断是否为空值 +// noinspection all +func (g GzipRaw) IsNil() bool { + return g.data == nil +} + +// MarshalJSON 以 base64 输出原始数据 +// noinspection all +func (g GzipRaw) MarshalJSON() ([]byte, error) { + if g.data == nil { + return []byte("null"), nil + } + return json.Marshal(base64.StdEncoding.EncodeToString(g.data)) +} + +// UnmarshalJSON 从 base64 还原原始数据 +// noinspection all +func (g *GzipRaw) UnmarshalJSON(b []byte) error { + if b == nil || len(b) == 0 || string(b) == "null" { + g.data = nil + return nil + } + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + data, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return err + } + g.data = data + return nil +} + +// GzipAuto 定义一个 Gzip 存储类型:写入数据库时对原始数据自动 Gzip 压缩, +// 读取(Scan)时自动解压,还原出压缩前的原始数据。 +// 底层列类型由 BlobDbDataType 自动判断。 +// noinspection all +type GzipAuto struct { + data []byte + compressLevel int +} + +// NewGzipAuto 构造一个 GzipAuto,data 为待压缩的原始字节 +func NewGzipAuto(data []byte) GzipAuto { + return GzipAuto{ + data: data, + compressLevel: gzip.DefaultCompression, + } +} + +// Value 实现了 driver.Valuer 接口,写入数据库时将原始数据 Gzip 压缩 +// noinspection all +func (g GzipAuto) Value() (driver.Value, error) { + if g.data == nil { + return nil, nil + } + return gzipCompressLevel(g.data, g.compressLevel) +} + +// Scan 实现了 sql.Scanner 接口,读取数据库时自动解压还原原始数据 +// noinspection all +func (g *GzipAuto) Scan(val any) error { + if val == nil { + g.data = nil + return nil + } + b, err := tools.Any2bytes(val) + if err != nil { + return err + } + decoded, err := gzipDecompress(b) + if err != nil { + return err + } + g.data = decoded + return nil +} + +// GormDataType gorm common data type +// noinspection all +func (GzipAuto) GormDataType() string { + return "blob" +} + +// GormDBDataType gorm db data type,由 BlobDbDataType 自动判断 +// noinspection all +func (GzipAuto) GormDBDataType(db *gorm.DB, field *schema.Field) string { + return BlobDbDataType(db, field) +} + +// SetCompressLevel 设置压缩级别,取值同 compress/gzip(如 gzip.BestSpeed、 +// gzip.DefaultCompression、gzip.BestCompression)。0 表示使用默认压缩级别。 +// noinspection all +func (g *GzipAuto) SetCompressLevel(level int) { + g.compressLevel = level +} + +// GetCompressLevel 返回当前压缩级别 +// noinspection all +func (g GzipAuto) GetCompressLevel() int { + return g.compressLevel +} + +// SetValue 设置原始数据 +// noinspection all +func (g *GzipAuto) SetValue(v []byte) { + g.data = v +} + +// GetValue 返回解压后的原始数据 +// noinspection all +func (g GzipAuto) GetValue() []byte { + return g.data +} + +// IsNil 判断是否为空值 +// noinspection all +func (g GzipAuto) IsNil() bool { + return g.data == nil +} + +// MarshalJSON 以 base64 输出原始数据 +// noinspection all +func (g GzipAuto) MarshalJSON() ([]byte, error) { + if g.data == nil { + return []byte("null"), nil + } + return json.Marshal(base64.StdEncoding.EncodeToString(g.data)) +} + +// UnmarshalJSON 从 base64 还原原始数据 +// noinspection all +func (g *GzipAuto) UnmarshalJSON(b []byte) error { + if b == nil || len(b) == 0 || string(b) == "null" { + g.data = nil + return nil + } + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + data, err := base64.StdEncoding.DecodeString(s) + if err != nil { + return err + } + g.data = data + return nil +} + +// 使用标准库 compress/gzip 压缩数据,level 为压缩级别 +func gzipCompressLevel(data []byte, level int) ([]byte, error) { + var buf bytes.Buffer + zw, err := gzip.NewWriterLevel(&buf, level) + if err != nil { + return nil, err + } + if _, err = zw.Write(data); err != nil { + return nil, err + } + if err = zw.Close(); err != nil { + return nil, err + } + return buf.Bytes(), nil +} + +// 解压 gzip 数据;若数据不是合法 gzip(兼容旧数据/未压缩数据), +// 则回退返回原字节且不报错。 +func gzipDecompress(data []byte) ([]byte, error) { + if len(data) == 0 { + return data, nil + } + r, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + // 兼容未压缩数据,原样返回 + return data, nil + } + defer func() { + _ = r.Close() + }() + out, err := io.ReadAll(r) + if err != nil { + return nil, err + } + return out, nil +} diff --git a/dataType/gzip_test.go b/dataType/gzip_test.go new file mode 100644 index 0000000..b2771cc --- /dev/null +++ b/dataType/gzip_test.go @@ -0,0 +1,231 @@ +package dataType + +import ( + "bytes" + "compress/gzip" + "encoding/json" + "io" + "testing" +) + +func TestGzipRawRoundTrip(t *testing.T) { + raw := bytes.Repeat([]byte("hello gzip raw "), 100) + g := NewGzipRaw(raw) + + // Value: 写入数据库时应为合法 gzip 压缩字节 + val, err := g.Value() + if err != nil { + t.Fatalf("Value error: %v", err) + } + compressed, ok := val.([]byte) + if !ok { + t.Fatalf("Value expected []byte, got %T", val) + } + if !isValidGzip(compressed) { + t.Fatalf("Value output is not valid gzip data") + } + + // Scan: 读取时原样返回压缩字节,不解压 + var dst GzipRaw + if err := dst.Scan(compressed); err != nil { + t.Fatalf("Scan error: %v", err) + } + if !bytes.Equal(dst.GetValue(), compressed) { + t.Fatalf("GzipRaw.Scan should keep compressed bytes as-is, got %v want %v", dst.GetValue(), compressed) + } + if bytes.Equal(dst.GetValue(), raw) { + t.Fatalf("GzipRaw should NOT decompress on Scan, but result equals raw data") + } +} + +func TestGzipAutoRoundTrip(t *testing.T) { + raw := bytes.Repeat([]byte("hello gzip auto "), 100) + g := NewGzipAuto(raw) + + val, err := g.Value() + if err != nil { + t.Fatalf("Value error: %v", err) + } + compressed, ok := val.([]byte) + if !ok { + t.Fatalf("Value expected []byte, got %T", val) + } + if !isValidGzip(compressed) { + t.Fatalf("Value output is not valid gzip data") + } + + // Scan: 读取时自动解压,还原原始数据 + var dst GzipAuto + if err := dst.Scan(compressed); err != nil { + t.Fatalf("Scan error: %v", err) + } + if !bytes.Equal(dst.GetValue(), raw) { + t.Fatalf("GzipAuto.Scan should decompress to original, got %v want %v", dst.GetValue(), raw) + } +} + +func TestGzipNilHandling(t *testing.T) { + var raw GzipRaw + v, err := raw.Value() + if err != nil { + t.Fatalf("GzipRaw.Value nil error: %v", err) + } + if v != nil { + t.Fatalf("GzipRaw nil Value expected nil, got %v", v) + } + if err := raw.Scan(nil); err != nil { + t.Fatalf("GzipRaw.Scan nil error: %v", err) + } + if !raw.IsNil() { + t.Fatalf("GzipRaw nil IsNil should be true") + } + + var auto GzipAuto + v, err = auto.Value() + if err != nil { + t.Fatalf("GzipAuto.Value nil error: %v", err) + } + if v != nil { + t.Fatalf("GzipAuto nil Value expected nil, got %v", v) + } + if err := auto.Scan(nil); err != nil { + t.Fatalf("GzipAuto.Scan nil error: %v", err) + } + if !auto.IsNil() { + t.Fatalf("GzipAuto nil IsNil should be true") + } +} + +func TestGzipStringScan(t *testing.T) { + // Scan 支持 string 类型输入 + var raw GzipRaw + str := "not-gzipped-plain-string" + if err := raw.Scan(str); err != nil { + t.Fatalf("GzipRaw.Scan string error: %v", err) + } + if raw.GetValue() == nil || string(raw.GetValue()) != str { + t.Fatalf("GzipRaw Scan string mismatch, got %v", raw.GetValue()) + } +} + +func TestGzipAutoToleratesUncompressedData(t *testing.T) { + // GzipAuto 读取到未压缩数据时应原样返回(容错) + var auto GzipAuto + str := []byte("plain uncompressed data") + if err := auto.Scan(str); err != nil { + t.Fatalf("GzipAuto.Scan plain error: %v", err) + } + if !bytes.Equal(auto.GetValue(), str) { + t.Fatalf("GzipAuto should tolerate uncompressed data, got %v want %v", auto.GetValue(), str) + } +} + +func TestGzipJSONRoundTrip(t *testing.T) { + raw := bytes.Repeat([]byte("json round trip "), 20) + + // GzipRaw JSON + var rawG GzipRaw + rawG = NewGzipRaw(raw) + b, err := json.Marshal(rawG) + if err != nil { + t.Fatalf("GzipRaw MarshalJSON error: %v", err) + } + var rawG2 GzipRaw + if err := json.Unmarshal(b, &rawG2); err != nil { + t.Fatalf("GzipRaw UnmarshalJSON error: %v", err) + } + if !bytes.Equal(rawG2.GetValue(), raw) { + t.Fatalf("GzipRaw JSON round trip mismatch") + } + + // GzipAuto JSON + var autoG GzipAuto + autoG = NewGzipAuto(raw) + b, err = json.Marshal(autoG) + if err != nil { + t.Fatalf("GzipAuto MarshalJSON error: %v", err) + } + var autoG2 GzipAuto + if err := json.Unmarshal(b, &autoG2); err != nil { + t.Fatalf("GzipAuto UnmarshalJSON error: %v", err) + } + if !bytes.Equal(autoG2.GetValue(), raw) { + t.Fatalf("GzipAuto JSON round trip mismatch") + } +} + +func TestGzipJSONNull(t *testing.T) { + // null 应还原为空值 + b, _ := json.Marshal(GzipRaw{}) + var raw GzipRaw + if err := json.Unmarshal(b, &raw); err != nil { + t.Fatalf("GzipRaw UnmarshalJSON null error: %v", err) + } + if !raw.IsNil() { + t.Fatalf("GzipRaw JSON null should be nil") + } +} + +func TestGzipCompressLevel(t *testing.T) { + raw := bytes.Repeat([]byte("compress level test data "), 200) + + // GzipRaw 设置压缩级别后往返仍正确 + var rawG GzipRaw + rawG = NewGzipRaw(raw) + rawG.SetCompressLevel(gzip.BestCompression) + if rawG.GetCompressLevel() != gzip.BestCompression { + t.Fatalf("GzipRaw.GetCompressLevel mismatch, got %d", rawG.GetCompressLevel()) + } + val, err := rawG.Value() + if err != nil { + t.Fatalf("GzipRaw compressed Value error: %v", err) + } + if !isValidGzip(val.([]byte)) { + t.Fatalf("GzipRaw compressed output invalid") + } + + // GzipAuto 设置压缩级别后往返还原原始数据 + var autoG GzipAuto + autoG = NewGzipAuto(raw) + autoG.SetCompressLevel(gzip.BestSpeed) + if autoG.GetCompressLevel() != gzip.BestSpeed { + t.Fatalf("GzipAuto.GetCompressLevel mismatch, got %d", autoG.GetCompressLevel()) + } + val, err = autoG.Value() + if err != nil { + t.Fatalf("GzipAuto compressed Value error: %v", err) + } + var back GzipAuto + if err := back.Scan(val); err != nil { + t.Fatalf("GzipAuto compressed Scan error: %v", err) + } + if !bytes.Equal(back.GetValue(), raw) { + t.Fatalf("GzipAuto compressed round trip mismatch") + } + + // 默认压缩级别 + d := NewGzipAuto(nil) + if d.GetCompressLevel() != gzip.DefaultCompression { + t.Fatalf("default compress level mismatch, got %d", d.GetCompressLevel()) + } +} + +func TestGzipInvalidCompressLevel(t *testing.T) { + raw := []byte("invalid level check") + var autoG GzipAuto + autoG = NewGzipAuto(raw) + autoG.SetCompressLevel(12345) // 非法级别,应导致 NewWriterLevel 报错 + if _, err := autoG.Value(); err == nil { + t.Fatalf("invalid compress level should error") + } +} + +func isValidGzip(data []byte) bool { + r, err := gzip.NewReader(bytes.NewReader(data)) + if err != nil { + return false + } + defer func() { _ = r.Close() }() + out, err := io.ReadAll(r) + return err == nil && len(out) > 0 +} \ No newline at end of file