git/blob: GetContentBase64 with fewer allocations and no goroutine (#8297)

See #8222 for context.i

`GetBlobContentBase64` was using a pipe and a goroutine to read the blob content as base64. This can be replace by a pre-allocated buffer and a direct copy.

Note that although similar to `GetBlobContent`, it does not truncate the content if the blob size is over the limit (but returns an error). I think that `GetBlobContent` should adopt the same behavior at some point (error instead of truncating).

### Tests

- I added test coverage for Go changes...
  - [x] in their respective `*_test.go` for unit tests.
- [x] I did not document these changes and I do not expect someone else to do it.

### Release notes

- [x] I do not want this change to show in the release notes.

Reviewed-on: https://codeberg.org/forgejo/forgejo/pulls/8297
Reviewed-by: Earl Warren <earl-warren@noreply.codeberg.org>
Co-authored-by: oliverpool <git@olivier.pfad.fr>
Co-committed-by: oliverpool <git@olivier.pfad.fr>
This commit is contained in:
oliverpool 2025-06-27 11:22:10 +02:00 committed by Earl Warren
parent 184e068f37
commit 7ad20a2730
4 changed files with 57 additions and 31 deletions

View file

@ -8,6 +8,7 @@ import (
"bufio"
"bytes"
"encoding/base64"
"fmt"
"io"
"forgejo.org/modules/log"
@ -172,33 +173,43 @@ func (b *Blob) GetBlobContent(limit int64) (string, error) {
return string(buf), err
}
// GetBlobContentBase64 Reads the content of the blob with a base64 encode and returns the encoded string
func (b *Blob) GetBlobContentBase64() (string, error) {
dataRc, err := b.DataAsync()
if err != nil {
return "", err
}
defer dataRc.Close()
type BlobTooLargeError struct {
Size, Limit int64
}
pr, pw := io.Pipe()
encoder := base64.NewEncoder(base64.StdEncoding, pw)
func (b BlobTooLargeError) Error() string {
return fmt.Sprintf("blob: content larger than limit (%d > %d)", b.Size, b.Limit)
}
go func() {
_, err := io.Copy(encoder, dataRc)
_ = encoder.Close()
if err != nil {
_ = pw.CloseWithError(err)
} else {
_ = pw.Close()
// GetContentBase64 Reads the content of the blob and returns it as base64 encoded string.
// Returns [BlobTooLargeError] if the (unencoded) content is larger than the limit.
func (b *Blob) GetContentBase64(limit int64) (string, error) {
if b.Size() > limit {
return "", BlobTooLargeError{
Size: b.Size(),
Limit: limit,
}
}()
}
out, err := io.ReadAll(pr)
rc, size, err := b.NewTruncatedReader(limit)
if err != nil {
return "", err
}
return string(out), nil
defer rc.Close()
encoding := base64.StdEncoding
buf := bytes.NewBuffer(make([]byte, 0, encoding.EncodedLen(int(size))))
encoder := base64.NewEncoder(encoding, buf)
if _, err := io.Copy(encoder, rc); err != nil {
return "", err
}
if err := encoder.Close(); err != nil {
return "", err
}
return buf.String(), nil
}
// GuessContentType guesses the content type of the blob.