11package handler
22
33import (
4- "bytes"
54 "context"
65 "errors"
76 "fmt"
@@ -18,10 +17,14 @@ import (
1817)
1918
2019type webdavFile struct {
21- // Read path (lazy loading )
20+ // Read path (streaming from storage )
2221 file * database.CloudFile
2322 fs * webdavFS
23+ ctx context.Context
2424 loadErr error
25+ s3Reader io.ReadCloser // underlying storage reader, opened lazily
26+ fileSize int64 // from Object.Size, known before fetch
27+ seekSkip int64 // bytes to discard before first Read (Range seek)
2528
2629 reader io.ReadCloser
2730 tempFile * os.File
@@ -40,21 +43,21 @@ type webdavFile struct {
4043}
4144
4245func (f * webdavFile ) Read (p []byte ) (int , error ) {
43- if f .reader != nil {
44- return f .reader .Read (p )
45- }
4646 if f .tempFile != nil {
4747 return f .tempFile .Read (p )
4848 }
49- // ponytail: lazy load from storage on first read (avoids downloading file
50- // content during PROPFIND which calls OpenFile per file but never reads)
51- if err := f .lazyLoad (); err != nil {
52- return 0 , err
53- }
5449 if f .reader != nil {
5550 return f .reader .Read (p )
5651 }
57- return 0 , io .EOF
52+ // ponytail: lazy open + stream from storage on first read.
53+ // No buffering — data flows S3 → client without an in-memory copy.
54+ if err := f .ensureOpen (); err != nil {
55+ return 0 , err
56+ }
57+ if f .s3Reader == nil {
58+ return 0 , io .EOF
59+ }
60+ return f .s3Reader .Read (p )
5861}
5962
6063func (f * webdavFile ) Write (p []byte ) (int , error ) {
@@ -71,60 +74,77 @@ func (f *webdavFile) Seek(offset int64, whence int) (int64, error) {
7174 if f .tempFile != nil {
7275 return f .tempFile .Seek (offset , whence )
7376 }
74- if f .reader != nil {
75- if s , ok := f .reader .(io.Seeker ); ok {
76- return s .Seek (offset , whence )
77+
78+ // http.ServeContent calls Seek(0, SeekEnd) for content length,
79+ // then Seek(0, SeekStart) before reading. Both are metadata-only:
80+ // we know the size from Object.Size without opening a storage reader.
81+ switch whence {
82+ case io .SeekEnd :
83+ if offset == 0 {
84+ return f .fileSize , nil
7785 }
86+ // ponytail: non-zero SeekEnd offset is never used by http.ServeContent
87+ return 0 , os .ErrInvalid
88+ case io .SeekStart :
89+ if offset == 0 {
90+ // If we already have an open reader, close + reopen on next Read.
91+ if f .s3Reader != nil {
92+ f .s3Reader .Close ()
93+ f .s3Reader = nil
94+ }
95+ return 0 , nil
96+ }
97+ // ponytail: Range requests seek to offset — re-fetch from start and
98+ // discard the skipped bytes on the next Read. Not ideal for large
99+ // offsets but acceptable (Range requests are uncommon).
100+ f .seekSkip = offset
101+ return offset , nil
102+ default :
78103 return 0 , os .ErrInvalid
79104 }
80- // ponytail: lazy load on first seek (http.ServeContent calls Seek before Read)
81- if err := f .lazyLoad (); err != nil {
82- return 0 , err
83- }
84- if s , ok := f .reader .(io.Seeker ); ok {
85- return s .Seek (offset , whence )
86- }
87- return 0 , os .ErrInvalid
88105}
89106
90- // ponytail: lazy load file content from storage on first Read/Seek .
91- // PROPFIND calls OpenFile per file but never reads — avoid the download entirely .
92- func (f * webdavFile ) lazyLoad () error {
107+ // ponytail: lazy open + stream from storage. Called by Read on first byte .
108+ // No buffering — data flows S3 → client without an in-memory copy .
109+ func (f * webdavFile ) ensureOpen () error {
93110 if f .loadErr != nil {
94111 return f .loadErr
95112 }
96- if f .reader != nil {
113+ if f .s3Reader != nil {
97114 return nil
98115 }
99116 if f .file == nil || f .fs == nil {
100117 return nil
101118 }
102119
103- // No storage key means this is a metadata-only file (empty placeholder)
104120 key := storageKeyForFile (f .file )
105121 if key == "" {
106- f .reader = io .NopCloser (bytes .NewReader (nil ))
107- return nil
122+ return nil // metadata-only file, Read returns EOF
108123 }
109124
110- reader , err := f .fs .openFileContent (context . Background () , f .file )
125+ reader , err := f .fs .openFileContent (f . ctx , f .file )
111126 if err != nil {
112- // ponytail: context canceled during content load — return ErrPermission
113- // so handlePropfindError skips gracefully
114127 if errors .Is (err , context .Canceled ) {
115128 f .loadErr = os .ErrPermission
116- return f .loadErr
129+ } else {
130+ f .loadErr = err
117131 }
118- f .loadErr = err
119- return err
132+ return f .loadErr
120133 }
121- defer reader .Close ()
122- data , err := io .ReadAll (reader )
123- if err != nil {
124- f .loadErr = err
125- return err
134+
135+ // ponytail: discard bytes for Range requests (Seek to offset before Read).
136+ // Avoids needing a storage-backend Range method.
137+ if f .seekSkip > 0 {
138+ skip := f .seekSkip
139+ f .seekSkip = 0
140+ if _ , err := io .CopyN (io .Discard , reader , skip ); err != nil {
141+ reader .Close ()
142+ f .loadErr = err
143+ return err
144+ }
126145 }
127- f .reader = io .NopCloser (bytes .NewReader (data ))
146+
147+ f .s3Reader = reader
128148 return nil
129149}
130150
@@ -153,6 +173,9 @@ func (f *webdavFile) Close() error {
153173 f .closed = true
154174
155175 if ! f .isWrite {
176+ if f .s3Reader != nil {
177+ return f .s3Reader .Close ()
178+ }
156179 if f .reader != nil {
157180 return f .reader .Close ()
158181 }
0 commit comments