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>
113 lines
3.2 KiB
Go
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())
|
|
}
|
|
}
|