fix: Buffer simple uploads for R2 Content-Length requirement

R2 PutObject requires Content-Length header. Buffer files <100MB into
memory before uploading so the SDK can set the header. Also fix HTML
pattern regex escaping for slug input.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
Jeff Emmett 2026-03-23 11:55:03 -07:00
parent e938f6d9a9
commit 4c22439574
2 changed files with 24 additions and 32 deletions

View File

@ -1,6 +1,7 @@
package r2
import (
"bytes"
"context"
"fmt"
"io"
@ -48,11 +49,19 @@ func (c *Client) Upload(ctx context.Context, key, contentType string, size int64
}
func (c *Client) uploadSimple(ctx context.Context, key, contentType string, body io.Reader) error {
_, err := c.s3.PutObject(ctx, &s3.PutObjectInput{
Bucket: &c.bucket,
Key: &key,
Body: body,
ContentType: &contentType,
// R2 requires Content-Length; buffer the body so we can set it.
// Only used for files < 100MB, so memory usage is bounded.
data, err := io.ReadAll(body)
if err != nil {
return fmt.Errorf("read body: %w", err)
}
size := int64(len(data))
_, err = c.s3.PutObject(ctx, &s3.PutObjectInput{
Bucket: &c.bucket,
Key: &key,
Body: bytes.NewReader(data),
ContentType: &contentType,
ContentLength: &size,
})
return err
}
@ -78,12 +87,16 @@ func (c *Client) uploadMultipart(ctx context.Context, key, contentType string, b
break
}
partData := make([]byte, n)
copy(partData, buf[:n])
partLen := int64(n)
upload, err := c.s3.UploadPart(ctx, &s3.UploadPartInput{
Bucket: &c.bucket,
Key: &key,
UploadId: uploadID,
PartNumber: &partNum,
Body: io.NopCloser(io.LimitReader(bytesReader(buf[:n]), int64(n))),
Bucket: &c.bucket,
Key: &key,
UploadId: uploadID,
PartNumber: &partNum,
Body: bytes.NewReader(partData),
ContentLength: &partLen,
})
if err != nil {
c.s3.AbortMultipartUpload(ctx, &s3.AbortMultipartUploadInput{
@ -147,24 +160,3 @@ func (c *Client) Delete(ctx context.Context, key string) error {
return err
}
// bytesReader wraps a byte slice as an io.Reader.
type bytesReaderImpl struct {
data []byte
pos int
}
func bytesReader(b []byte) io.Reader {
// Make a copy so the buffer can be reused
cp := make([]byte, len(b))
copy(cp, b)
return &bytesReaderImpl{data: cp}
}
func (r *bytesReaderImpl) Read(p []byte) (int, error) {
if r.pos >= len(r.data) {
return 0, io.EOF
}
n := copy(p, r.data[r.pos:])
r.pos += n
return n, nil
}

View File

@ -28,7 +28,7 @@
<label for="slug">Custom URL</label>
<div class="slug-input">
<span class="slug-prefix">/f/</span>
<input type="text" id="slug" placeholder="my-file" pattern="[a-zA-Z0-9][a-zA-Z0-9._-]*" maxlength="64">
<input type="text" id="slug" placeholder="my-file" pattern="[a-zA-Z0-9][a-zA-Z0-9.\-_]*" maxlength="64">
</div>
</div>
<div class="option-group">