github.com/vmware/govmomi@v0.51.0/object/datastore_file.go (about) 1 // © Broadcom. All Rights Reserved. 2 // The term “Broadcom” refers to Broadcom Inc. and/or its subsidiaries. 3 // SPDX-License-Identifier: Apache-2.0 4 5 package object 6 7 import ( 8 "bytes" 9 "context" 10 "errors" 11 "fmt" 12 "io" 13 "net/http" 14 "os" 15 "path" 16 "sync" 17 "time" 18 19 "github.com/vmware/govmomi/vim25/soap" 20 ) 21 22 // DatastoreFile implements io.Reader, io.Seeker and io.Closer interfaces for datastore file access. 23 type DatastoreFile struct { 24 d Datastore 25 ctx context.Context 26 name string 27 28 buf io.Reader 29 body io.ReadCloser 30 length int64 31 offset struct { 32 read, seek int64 33 } 34 } 35 36 // Open opens the named file relative to the Datastore. 37 func (d Datastore) Open(ctx context.Context, name string) (*DatastoreFile, error) { 38 return &DatastoreFile{ 39 d: d, 40 name: name, 41 length: -1, 42 ctx: ctx, 43 }, nil 44 } 45 46 // Read reads up to len(b) bytes from the DatastoreFile. 47 func (f *DatastoreFile) Read(b []byte) (int, error) { 48 if f.offset.read != f.offset.seek { 49 // A Seek() call changed the offset, we need to issue a new GET 50 _ = f.Close() 51 52 f.offset.read = f.offset.seek 53 } else if f.buf != nil { 54 // f.buf + f behaves like an io.MultiReader 55 n, err := f.buf.Read(b) 56 if err == io.EOF { 57 f.buf = nil // buffer has been drained 58 } 59 if n > 0 { 60 return n, nil 61 } 62 } 63 64 body, err := f.get() 65 if err != nil { 66 return 0, err 67 } 68 69 n, err := body.Read(b) 70 71 f.offset.read += int64(n) 72 f.offset.seek += int64(n) 73 74 return n, err 75 } 76 77 // Close closes the DatastoreFile. 78 func (f *DatastoreFile) Close() error { 79 var err error 80 81 if f.body != nil { 82 err = f.body.Close() 83 f.body = nil 84 } 85 86 f.buf = nil 87 88 return err 89 } 90 91 // Seek sets the offset for the next Read on the DatastoreFile. 92 func (f *DatastoreFile) Seek(offset int64, whence int) (int64, error) { 93 switch whence { 94 case io.SeekStart: 95 case io.SeekCurrent: 96 offset += f.offset.seek 97 case io.SeekEnd: 98 if f.length < 0 { 99 _, err := f.Stat() 100 if err != nil { 101 return 0, err 102 } 103 } 104 offset += f.length 105 default: 106 return 0, errors.New("Seek: invalid whence") 107 } 108 109 // allow negative SeekStart for initial Range request 110 if offset < 0 { 111 return 0, errors.New("Seek: invalid offset") 112 } 113 114 f.offset.seek = offset 115 116 return offset, nil 117 } 118 119 type fileStat struct { 120 file *DatastoreFile 121 header http.Header 122 } 123 124 func (s *fileStat) Name() string { 125 return path.Base(s.file.name) 126 } 127 128 func (s *fileStat) Size() int64 { 129 return s.file.length 130 } 131 132 func (s *fileStat) Mode() os.FileMode { 133 return 0 134 } 135 136 func (s *fileStat) ModTime() time.Time { 137 return time.Now() // no Last-Modified 138 } 139 140 func (s *fileStat) IsDir() bool { 141 return false 142 } 143 144 func (s *fileStat) Sys() any { 145 return s.header 146 } 147 148 func statusError(res *http.Response) error { 149 if res.StatusCode == http.StatusNotFound { 150 return os.ErrNotExist 151 } 152 return errors.New(res.Status) 153 } 154 155 // Stat returns the os.FileInfo interface describing file. 156 func (f *DatastoreFile) Stat() (os.FileInfo, error) { 157 // TODO: consider using Datastore.Stat() instead 158 u, p, err := f.d.downloadTicket(f.ctx, f.name, &soap.Download{Method: "HEAD"}) 159 if err != nil { 160 return nil, err 161 } 162 163 res, err := f.d.Client().DownloadRequest(f.ctx, u, p) 164 if err != nil { 165 return nil, err 166 } 167 168 if res.StatusCode != http.StatusOK { 169 return nil, statusError(res) 170 } 171 172 f.length = res.ContentLength 173 174 return &fileStat{f, res.Header}, nil 175 } 176 177 func (f *DatastoreFile) get() (io.Reader, error) { 178 if f.body != nil { 179 return f.body, nil 180 } 181 182 u, p, err := f.d.downloadTicket(f.ctx, f.name, nil) 183 if err != nil { 184 return nil, err 185 } 186 187 if f.offset.read != 0 { 188 p.Headers = map[string]string{ 189 "Range": fmt.Sprintf("bytes=%d-", f.offset.read), 190 } 191 } 192 193 res, err := f.d.Client().DownloadRequest(f.ctx, u, p) 194 if err != nil { 195 return nil, err 196 } 197 198 switch res.StatusCode { 199 case http.StatusOK: 200 f.length = res.ContentLength 201 case http.StatusPartialContent: 202 var start, end int 203 cr := res.Header.Get("Content-Range") 204 _, err = fmt.Sscanf(cr, "bytes %d-%d/%d", &start, &end, &f.length) 205 if err != nil { 206 f.length = -1 207 } 208 case http.StatusRequestedRangeNotSatisfiable: 209 // ok: Read() will return io.EOF 210 default: 211 return nil, statusError(res) 212 } 213 214 if f.length < 0 { 215 _ = res.Body.Close() 216 return nil, errors.New("unable to determine file size") 217 } 218 219 f.body = res.Body 220 221 return f.body, nil 222 } 223 224 func lastIndexLines(s []byte, line *int, include func(l int, m string) bool) (int64, bool) { 225 i := len(s) - 1 226 done := false 227 228 for i > 0 { 229 o := bytes.LastIndexByte(s[:i], '\n') 230 if o < 0 { 231 break 232 } 233 234 msg := string(s[o+1 : i+1]) 235 if !include(*line, msg) { 236 done = true 237 break 238 } else { 239 i = o 240 *line++ 241 } 242 } 243 244 return int64(i), done 245 } 246 247 // Tail seeks to the position of the last N lines of the file. 248 func (f *DatastoreFile) Tail(n int) error { 249 return f.TailFunc(n, func(line int, _ string) bool { return n > line }) 250 } 251 252 // TailFunc will seek backwards in the datastore file until it hits a line that does 253 // not satisfy the supplied `include` function. 254 func (f *DatastoreFile) TailFunc(lines int, include func(line int, message string) bool) error { 255 // Read the file in reverse using bsize chunks 256 const bsize = int64(1024 * 16) 257 258 fsize, err := f.Seek(0, io.SeekEnd) 259 if err != nil { 260 return err 261 } 262 263 if lines == 0 { 264 return nil 265 } 266 267 chunk := int64(-1) 268 269 buf := bytes.NewBuffer(make([]byte, 0, bsize)) 270 line := 0 271 272 for { 273 var eof bool 274 var pos int64 275 276 nread := bsize 277 278 offset := chunk * bsize 279 remain := fsize + offset 280 281 if remain < 0 { 282 if pos, err = f.Seek(0, io.SeekStart); err != nil { 283 return err 284 } 285 286 nread = bsize + remain 287 eof = true 288 } else if pos, err = f.Seek(offset, io.SeekEnd); err != nil { 289 return err 290 } 291 292 if _, err = io.CopyN(buf, f, nread); err != nil { 293 if err != io.EOF { 294 return err 295 } 296 } 297 298 b := buf.Bytes() 299 idx, done := lastIndexLines(b, &line, include) 300 301 if done { 302 if chunk == -1 { 303 // We found all N lines in the last chunk of the file. 304 // The seek offset is also now at the current end of file. 305 // Save this buffer to avoid another GET request when Read() is called. 306 buf.Next(int(idx + 1)) 307 f.buf = buf 308 return nil 309 } 310 311 if _, err = f.Seek(pos+idx+1, io.SeekStart); err != nil { 312 return err 313 } 314 315 break 316 } 317 318 if eof { 319 if remain < 0 { 320 // We found < N lines in the entire file, so seek to the start. 321 _, _ = f.Seek(0, io.SeekStart) 322 } 323 break 324 } 325 326 chunk-- 327 buf.Reset() 328 } 329 330 return nil 331 } 332 333 type followDatastoreFile struct { 334 r *DatastoreFile 335 c chan struct{} 336 i time.Duration 337 o sync.Once 338 } 339 340 // Read reads up to len(b) bytes from the DatastoreFile being followed. 341 // This method will block until data is read, an error other than io.EOF is returned or Close() is called. 342 func (f *followDatastoreFile) Read(p []byte) (int, error) { 343 offset := f.r.offset.seek 344 stop := false 345 346 for { 347 n, err := f.r.Read(p) 348 if err != nil && err == io.EOF { 349 _ = f.r.Close() // GET request body has been drained. 350 if stop { 351 return n, err 352 } 353 err = nil 354 } 355 356 if n > 0 { 357 return n, err 358 } 359 360 select { 361 case <-f.c: 362 // Wake up and stop polling once the body has been drained 363 stop = true 364 case <-time.After(f.i): 365 } 366 367 info, serr := f.r.Stat() 368 if serr != nil { 369 // Return EOF rather than 404 if the file goes away 370 if serr == os.ErrNotExist { 371 _ = f.r.Close() 372 return 0, io.EOF 373 } 374 return 0, serr 375 } 376 377 if info.Size() < offset { 378 // assume file has be truncated 379 offset, err = f.r.Seek(0, io.SeekStart) 380 if err != nil { 381 return 0, err 382 } 383 } 384 } 385 } 386 387 // Close will stop Follow polling and close the underlying DatastoreFile. 388 func (f *followDatastoreFile) Close() error { 389 f.o.Do(func() { close(f.c) }) 390 return nil 391 } 392 393 // Follow returns an io.ReadCloser to stream the file contents as data is appended. 394 func (f *DatastoreFile) Follow(interval time.Duration) io.ReadCloser { 395 return &followDatastoreFile{ 396 r: f, 397 c: make(chan struct{}), 398 i: interval, 399 } 400 }