Files
hamlab/internal/files/seek_test.go
Mitchell-JohnsonandClaude Fable 5.1 a9bcb88212 Files: play video and audio in the viewer, as the file is; reads answer Range requests
The viewer hands a video or audio file to the browser untouched — container, codec and
all — and the browser plays what it can; when it can't, the viewer says so and offers the
download. What the server adds is the part a player needs: /api/files/read now goes
through http.ServeContent for a real file of known size, so Range requests (seeking) and
HEAD work, and a download can resume. The root backend's helper takes an offset for that,
and a Seeker opens the backend once, at the range asked for, instead of streaming from
the top. Audio files are a preview kind too: their cover art is the thumbnail.

Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
2026-10-11 16:40:42 -07:00

113 lines
3.2 KiB
Go

package files
import (
"context"
"io"
"os"
"path/filepath"
"testing"
)
import "strings"
type stringsBuilder = strings.Builder
type countingFS struct {
Local
opens []int64
}
func (c *countingFS) OpenAt(ctx context.Context, path string, off int64) (io.ReadCloser, error) {
c.opens = append(c.opens, off)
return c.Local.OpenAt(ctx, path, off)
}
// ServeContent's moves — size from the end, seek to the range, read forward — open the
// backend once, at the range; a seek to where the stream already is costs nothing.
func TestSeeker(t *testing.T) {
p := filepath.Join(t.TempDir(), "f")
data := make([]byte, 1000)
for i := range data {
data[i] = byte(i)
}
if err := os.WriteFile(p, data, 0o644); err != nil {
t.Fatal(err)
}
fs := &countingFS{}
s := NewSeeker(context.Background(), fs, p, int64(len(data)))
if n, _ := s.Seek(0, io.SeekEnd); n != 1000 {
t.Fatalf("end = %d", n)
}
if _, err := s.Seek(100, io.SeekStart); err != nil {
t.Fatal(err)
}
buf := make([]byte, 50)
if n, err := io.ReadFull(s, buf); err != nil || n != 50 || buf[0] != 100 || buf[49] != 149 {
t.Fatalf("read at 100: %d %v %v", n, err, buf[:2])
}
if _, err := s.Seek(150, io.SeekStart); err != nil { // where it already is
t.Fatal(err)
}
if n, _ := io.ReadFull(s, buf); n != 50 || buf[0] != 150 {
t.Fatal("read after a no-op seek")
}
if len(fs.opens) != 1 || fs.opens[0] != 100 {
t.Fatalf("opens = %v, want [100]", fs.opens)
}
if _, err := s.Seek(0, io.SeekStart); err != nil {
t.Fatal(err)
}
rest, err := io.ReadAll(s)
if err != nil || len(rest) != 1000 || rest[999] != data[999] {
t.Fatalf("read all from the start: %d %v", len(rest), err)
}
if len(fs.opens) != 2 || fs.opens[1] != 0 {
t.Fatalf("opens = %v, want [100 0]", fs.opens)
}
if n, err := s.Read(buf); n != 0 || err != io.EOF {
t.Fatal("past the end is EOF")
}
if _, err := s.Seek(-1, io.SeekStart); err == nil {
t.Fatal("before the start")
}
if err := s.Close(); err != nil {
t.Fatal(err)
}
// Check opens at the start and keeps that stream for a read from the start
s2 := NewSeeker(context.Background(), fs, p, 1000)
if err := s2.Check(); err != nil {
t.Fatal(err)
}
s2.Seek(0, io.SeekEnd)
s2.Seek(0, io.SeekStart)
if n, _ := io.ReadFull(s2, buf); n != 50 || buf[0] != 0 {
t.Fatal("read from the start after Check")
}
if len(fs.opens) != 3 {
t.Fatalf("Check's open was not reused: %v", fs.opens)
}
s2.Close()
if err := NewSeeker(context.Background(), fs, filepath.Join(t.TempDir(), "nope"), 5).Check(); err == nil {
t.Fatal("Check must report a file that can't be opened")
}
}
// The helper's read takes an offset, for the Sudo backend.
func TestHelperReadOffset(t *testing.T) {
p := filepath.Join(t.TempDir(), "f")
if err := os.WriteFile(p, []byte("0123456789"), 0o644); err != nil {
t.Fatal(err)
}
var out, errb stringsBuilder
if rc := Helper([]string{"read", "--offset", "7", p}, nil, &out, &errb); rc != 0 {
t.Fatalf("rc %d: %s", rc, errb.String())
}
if out.String() != "789" {
t.Fatalf("got %q", out.String())
}
out.Reset()
if rc := Helper([]string{"read", p}, nil, &out, &errb); rc != 0 || out.String() != "0123456789" {
t.Fatalf("plain read: rc %d %q", rc, out.String())
}
}