feat(storage): support signed read options and local byte ranges
This commit is contained in:
@@ -151,14 +151,31 @@ func (s *Store) Get(ctx context.Context, key string) (storage.Object, error) {
|
||||
}
|
||||
info.ModifiedAt = stat.ModTime()
|
||||
ok = true
|
||||
return storage.Object{Info: info, Body: &body{Reader: storage.ContextReader{Context: ctx, Reader: io.LimitReader(f, info.Size)}, Closer: f}}, nil
|
||||
// Expose only the payload as a seekable section. Seeks are relative to the
|
||||
// object, never to our private metadata header or other disk bytes.
|
||||
return storage.Object{Info: info, Body: &body{section: io.NewSectionReader(f, int64(length)+4, info.Size), file: f, ctx: ctx}}, nil
|
||||
}
|
||||
|
||||
type body struct {
|
||||
io.Reader
|
||||
io.Closer
|
||||
section *io.SectionReader
|
||||
file *os.File
|
||||
ctx context.Context
|
||||
}
|
||||
|
||||
func (b *body) Read(p []byte) (int, error) {
|
||||
if err := b.ctx.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return b.section.Read(p)
|
||||
}
|
||||
func (b *body) Seek(offset int64, whence int) (int64, error) {
|
||||
if err := b.ctx.Err(); err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return b.section.Seek(offset, whence)
|
||||
}
|
||||
func (b *body) Close() error { return b.file.Close() }
|
||||
|
||||
func (s *Store) Delete(ctx context.Context, key string) error {
|
||||
if err := storage.Check(ctx, key); err != nil {
|
||||
return err
|
||||
|
||||
125
storage/local/read_test.go
Normal file
125
storage/local/read_test.go
Normal file
@@ -0,0 +1,125 @@
|
||||
package local
|
||||
|
||||
import (
|
||||
"context"
|
||||
"io"
|
||||
"mime"
|
||||
"mime/multipart"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"git.yuntuo.hk/k2safe/go-omni/storage"
|
||||
)
|
||||
|
||||
func TestSignedPayloadRanges(t *testing.T) {
|
||||
store, err := New(t.TempDir(), 1<<20)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
data := "%PDF-1.4\n" + strings.Repeat("0123456789", 100) + "\n%%EOF\n"
|
||||
ctx := context.Background()
|
||||
if _, err = store.Put(ctx, "private/document", strings.NewReader(data), int64(len(data)), "application/pdf"); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
signer, _ := storage.NewSigner(store, "https://files.example/read", []byte(strings.Repeat("s", 32)))
|
||||
link, err := signer.PresignGetWithOptions(ctx, "private/document", 30*time.Minute, storage.ReadOptions{Disposition: storage.DispositionInline, FileName: "示例.pdf"})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
for _, tc := range []struct {
|
||||
method, span string
|
||||
status int
|
||||
want string
|
||||
}{
|
||||
{"GET", "", 200, data}, {"HEAD", "", 200, ""}, {"HEAD", "bytes=0-3", 200, ""},
|
||||
{"GET", "bytes=0-7", 206, data[:8]}, {"GET", "bytes=20-", 206, data[20:]}, {"GET", "bytes=-6", 206, data[len(data)-6:]},
|
||||
{"GET", "bytes=1000-999999", 206, data[1000:]}, {"GET", "bytes=999999-", 416, ""}, {"GET", "bytes=bad", 416, ""},
|
||||
} {
|
||||
t.Run(tc.method+tc.span, func(t *testing.T) {
|
||||
r := httptest.NewRequest(tc.method, link.URL, nil)
|
||||
if tc.span != "" {
|
||||
r.Header.Set("Range", tc.span)
|
||||
}
|
||||
w := httptest.NewRecorder()
|
||||
signer.ServeHTTP(w, r)
|
||||
if w.Code != tc.status {
|
||||
t.Fatal(w.Code)
|
||||
}
|
||||
if tc.status != 416 && w.Body.String() != tc.want {
|
||||
t.Fatal("wrong payload slice")
|
||||
}
|
||||
if tc.status == 206 && (w.Header().Get("Content-Length") != strconv.Itoa(len(tc.want)) || !strings.HasSuffix(w.Header().Get("Content-Range"), "/"+strconv.Itoa(len(data)))) {
|
||||
t.Fatal(w.Header())
|
||||
}
|
||||
if tc.status == 200 && w.Header().Get("Content-Length") != strconv.Itoa(len(data)) {
|
||||
t.Fatal("metadata included in length")
|
||||
}
|
||||
if tc.span == "bytes=999999-" && w.Header().Get("Content-Range") != "bytes */"+strconv.Itoa(len(data)) {
|
||||
t.Fatal(w.Header())
|
||||
}
|
||||
if w.Header().Get("Cache-Control") != "private, no-store" {
|
||||
t.Fatal("cache policy")
|
||||
}
|
||||
if tc.status != 416 {
|
||||
kind, params, err := mime.ParseMediaType(w.Header().Get("Content-Disposition"))
|
||||
if err != nil || kind != "inline" || params["filename"] != "示例.pdf" || w.Header().Get("Content-Type") != "application/pdf" {
|
||||
t.Fatal(w.Header(), err)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
r := httptest.NewRequest("GET", link.URL, nil)
|
||||
r.Header.Set("Range", "bytes=0-3,10-13")
|
||||
w := httptest.NewRecorder()
|
||||
signer.ServeHTTP(w, r)
|
||||
kind, params, err := mime.ParseMediaType(w.Header().Get("Content-Type"))
|
||||
if err != nil || w.Code != 206 || kind != "multipart/byteranges" {
|
||||
t.Fatal(w.Header(), err)
|
||||
}
|
||||
reader := multipart.NewReader(w.Body, params["boundary"])
|
||||
for _, want := range []string{data[:4], data[10:14]} {
|
||||
p, err := reader.NextPart()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
got, err := io.ReadAll(p)
|
||||
if err != nil || string(got) != want {
|
||||
t.Fatal("multipart bytes", err)
|
||||
}
|
||||
}
|
||||
if _, err := reader.NextPart(); err != io.EOF {
|
||||
t.Fatal(err)
|
||||
}
|
||||
// Random access cannot expose the managed header and still observes context.
|
||||
ctx, cancel := context.WithCancel(ctx)
|
||||
defer cancel()
|
||||
object, err := store.Get(ctx, "private/document")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer object.Body.Close()
|
||||
seeker := object.Body.(io.ReadSeeker)
|
||||
if size, err := seeker.Seek(0, io.SeekEnd); err != nil || size != int64(len(data)) {
|
||||
t.Fatal(size, err)
|
||||
}
|
||||
if _, err := seeker.Seek(-1, io.SeekStart); err == nil {
|
||||
t.Fatal("seek into header")
|
||||
}
|
||||
if _, err := seeker.Seek(0, io.SeekStart); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
prefix := make([]byte, 8)
|
||||
if _, err := io.ReadFull(seeker, prefix); err != nil || string(prefix) != data[:8] {
|
||||
t.Fatal("metadata exposed", err)
|
||||
}
|
||||
cancel()
|
||||
if _, err := seeker.Seek(0, io.SeekStart); err != context.Canceled {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if _, err := seeker.Read(prefix); err != context.Canceled {
|
||||
t.Fatal(err)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user