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 }