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