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>
This commit is contained in:
Mitchell-Johnson
2026-10-11 16:40:42 -07:00
co-authored by Claude Fable 5.1
parent c24f6f5cde
commit a9bcb88212
9 changed files with 343 additions and 19 deletions
+30 -2
View File
@@ -59,6 +59,7 @@ type FS interface {
List(ctx context.Context, path string) (Listing, error)
Stat(ctx context.Context, path string) (Info, error)
Open(ctx context.Context, path string) (io.ReadCloser, error)
OpenAt(ctx context.Context, path string, offset int64) (io.ReadCloser, error) // Open from a byte offset: a range request, a player seeking
Write(ctx context.Context, path string, r io.Reader) error
Mkdir(ctx context.Context, path string) error
Touch(ctx context.Context, path string) error
@@ -231,6 +232,20 @@ func (Local) Open(_ context.Context, path string) (io.ReadCloser, error) {
return os.Open(path)
}
func (l Local) OpenAt(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
f, err := l.Open(ctx, path)
if err != nil {
return nil, err
}
if offset > 0 {
if _, err := f.(*os.File).Seek(offset, io.SeekStart); err != nil {
f.Close()
return nil, err
}
}
return f, nil
}
// Write replaces the file's contents, keeping its mode; new files get 0644 (umask).
func (Local) Write(_ context.Context, path string, r io.Reader) error {
f, err := os.OpenFile(path, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o644)
@@ -336,7 +351,15 @@ func (r *cmdReader) Close() error {
}
func (s Sudo) Open(ctx context.Context, path string) (io.ReadCloser, error) {
c := s.cmd(ctx, "read", path)
return s.open(ctx, "read", path)
}
func (s Sudo) OpenAt(ctx context.Context, path string, offset int64) (io.ReadCloser, error) {
return s.open(ctx, "read", "--offset", strconv.FormatInt(offset, 10), path)
}
func (s Sudo) open(ctx context.Context, args ...string) (io.ReadCloser, error) {
c := s.cmd(ctx, args...)
errb := &strings.Builder{}
c.Stderr = errb
out, err := c.StdoutPipe()
@@ -430,6 +453,11 @@ func Helper(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer)
ctx := context.Background()
var l Local
op := args[0]
var offset int64
if op == "read" && len(args) >= 4 && args[1] == "--offset" { // read --offset N <path>
offset, _ = strconv.ParseInt(args[2], 10, 64)
args = append([]string{"read"}, args[3:]...)
}
clean := func(i int) (string, error) {
if i >= len(args) {
return "", errors.New("path required")
@@ -456,7 +484,7 @@ func Helper(args []string, stdin io.Reader, stdout io.Writer, stderr io.Writer)
}
return emit(stdout, out, stderr)
case "read":
f, err := l.Open(ctx, p)
f, err := l.OpenAt(ctx, p, offset)
if err != nil {
return fail(err)
}
+89
View File
@@ -0,0 +1,89 @@
package files
import (
"context"
"errors"
"io"
)
// Seeker is an FS file as http.ServeContent wants one — something to seek in — for a file
// whose size is already known from a stat. ServeContent seeks to the end for the size, to
// the start of the range asked for, then reads: so the backend is opened once, at that
// position, and only ever read forward. The Sudo backend's helper is started at the offset
// rather than streamed from the top and skipped.
type Seeker struct {
ctx context.Context
fsys FS
path string
size int64
pos int64 // where the next Read reads from
at int64 // where cur is positioned, when open
cur io.ReadCloser
}
func NewSeeker(ctx context.Context, fsys FS, path string, size int64) *Seeker {
return &Seeker{ctx: ctx, fsys: fsys, path: path, size: size}
}
// Check opens the file now, from the start, so a file that cannot be read is known before
// any headers go out. A later seek elsewhere simply reopens.
func (s *Seeker) Check() error {
if s.cur != nil {
return nil
}
f, err := s.fsys.OpenAt(s.ctx, s.path, 0)
if err != nil {
return err
}
s.cur, s.at = f, 0
return nil
}
func (s *Seeker) Seek(offset int64, whence int) (int64, error) {
var pos int64
switch whence {
case io.SeekStart:
pos = offset
case io.SeekCurrent:
pos = s.pos + offset
case io.SeekEnd:
pos = s.size + offset
default:
return 0, errors.New("bad whence")
}
if pos < 0 {
return 0, errors.New("seek before start")
}
if s.cur != nil && pos != s.at {
s.cur.Close()
s.cur = nil
}
s.pos = pos
return pos, nil
}
func (s *Seeker) Read(p []byte) (int, error) {
if s.pos >= s.size {
return 0, io.EOF
}
if s.cur == nil {
f, err := s.fsys.OpenAt(s.ctx, s.path, s.pos)
if err != nil {
return 0, err
}
s.cur, s.at = f, s.pos
}
n, err := s.cur.Read(p)
s.pos += int64(n)
s.at += int64(n)
return n, err
}
func (s *Seeker) Close() error {
if s.cur == nil {
return nil
}
err := s.cur.Close()
s.cur = nil
return err
}
+112
View File
@@ -0,0 +1,112 @@
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())
}
}