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  }