gitea/modules/zstd/zstd.go

164 lines
3.5 KiB
Go
Raw Normal View History

Support compression for Actions logs (#31761) Support compression for Actions logs to save storage space and bandwidth. Inspired by https://github.com/go-gitea/gitea/issues/24256#issuecomment-1521153015 The biggest challenge is that the compression format should support [seekable](https://github.com/facebook/zstd/blob/dev/contrib/seekable_format/zstd_seekable_compression_format.md). So when users are viewing a part of the log lines, Gitea doesn't need to download the whole compressed file and decompress it. That means gzip cannot help here. And I did research, there aren't too many choices, like bgzip and xz, but I think zstd is the most popular one. It has an implementation in Golang with [zstd](https://github.com/klauspost/compress/tree/master/zstd) and [zstd-seekable-format-go](https://github.com/SaveTheRbtz/zstd-seekable-format-go), and what is better is that it has good compatibility: a seekable format zstd file can be read by a regular zstd reader. This PR introduces a new package `zstd` to combine and wrap the two packages, to provide a unified and easy-to-use API. And a new setting `LOG_COMPRESSION` is added to the config, although I don't see any reason why not to use compression, I think's it's a good idea to keep the default with `none` to be consistent with old versions. `LOG_COMPRESSION` takes effect for only new log files, it adds `.zst` as an extension to the file name, so Gitea can determine if it needs decompression according to the file name when reading. Old files will keep the format since it's not worth converting them, as they will be cleared after #31735. <img width="541" alt="image" src="https://github.com/user-attachments/assets/e9598764-a4e0-4b68-8c2b-f769265183c9">
2024-08-09 10:10:30 +08:00
// Copyright 2024 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
// Package zstd provides a high-level API for reading and writing zstd-compressed data.
// It supports both regular and seekable zstd streams.
// It's not a new wheel, but a wrapper around the zstd and zstd-seekable-format-go packages.
package zstd
import (
"errors"
"io"
seekable "github.com/SaveTheRbtz/zstd-seekable-format-go/pkg"
"github.com/klauspost/compress/zstd"
)
type Writer zstd.Encoder
var _ io.WriteCloser = (*Writer)(nil)
// NewWriter returns a new zstd writer.
func NewWriter(w io.Writer, opts ...WriterOption) (*Writer, error) {
zstdW, err := zstd.NewWriter(w, opts...)
if err != nil {
return nil, err
}
return (*Writer)(zstdW), nil
}
func (w *Writer) Write(p []byte) (int, error) {
return (*zstd.Encoder)(w).Write(p)
}
func (w *Writer) Close() error {
return (*zstd.Encoder)(w).Close()
}
type Reader zstd.Decoder
var _ io.ReadCloser = (*Reader)(nil)
// NewReader returns a new zstd reader.
func NewReader(r io.Reader, opts ...ReaderOption) (*Reader, error) {
zstdR, err := zstd.NewReader(r, opts...)
if err != nil {
return nil, err
}
return (*Reader)(zstdR), nil
}
func (r *Reader) Read(p []byte) (int, error) {
return (*zstd.Decoder)(r).Read(p)
}
func (r *Reader) Close() error {
(*zstd.Decoder)(r).Close() // no error returned
return nil
}
type SeekableWriter struct {
buf []byte
n int
w seekable.Writer
}
var _ io.WriteCloser = (*SeekableWriter)(nil)
// NewSeekableWriter returns a zstd writer to compress data to seekable format.
// blockSize is an important parameter, it should be decided according to the actual business requirements.
// If it's too small, the compression ratio could be very bad, even no compression at all.
// If it's too large, it could cost more traffic when reading the data partially from underlying storage.
func NewSeekableWriter(w io.Writer, blockSize int, opts ...WriterOption) (*SeekableWriter, error) {
zstdW, err := zstd.NewWriter(nil, opts...)
if err != nil {
return nil, err
}
seekableW, err := seekable.NewWriter(w, zstdW)
if err != nil {
return nil, err
}
return &SeekableWriter{
buf: make([]byte, blockSize),
w: seekableW,
}, nil
}
func (w *SeekableWriter) Write(p []byte) (int, error) {
written := 0
for len(p) > 0 {
n := copy(w.buf[w.n:], p)
w.n += n
written += n
p = p[n:]
if w.n == len(w.buf) {
if _, err := w.w.Write(w.buf); err != nil {
return written, err
}
w.n = 0
}
}
return written, nil
}
func (w *SeekableWriter) Close() error {
if w.n > 0 {
if _, err := w.w.Write(w.buf[:w.n]); err != nil {
return err
}
}
return w.w.Close()
}
type SeekableReader struct {
r seekable.Reader
c func() error
}
var _ io.ReadSeekCloser = (*SeekableReader)(nil)
// NewSeekableReader returns a zstd reader to decompress data from seekable format.
func NewSeekableReader(r io.ReadSeeker, opts ...ReaderOption) (*SeekableReader, error) {
zstdR, err := zstd.NewReader(nil, opts...)
if err != nil {
return nil, err
}
seekableR, err := seekable.NewReader(r, zstdR)
if err != nil {
return nil, err
}
ret := &SeekableReader{
r: seekableR,
}
if closer, ok := r.(io.Closer); ok {
ret.c = closer.Close
}
return ret, nil
}
func (r *SeekableReader) Read(p []byte) (int, error) {
return r.r.Read(p)
}
func (r *SeekableReader) Seek(offset int64, whence int) (int64, error) {
return r.r.Seek(offset, whence)
}
func (r *SeekableReader) Close() error {
return errors.Join(
func() error {
if r.c != nil {
return r.c()
}
return nil
}(),
r.r.Close(),
)
}