// Copyright (c) Tailscale Inc & AUTHORS // SPDX-License-Identifier: BSD-3-Clause package webdavfs import ( "context" "errors" "io" "io/fs" "os" "sync" "github.com/tailscale/gowebdav" ) const ( // MaxRewindBuffer specifies the size of the rewind buffer for reading // from files. For some files, net/http performs content type detection // by reading up to the first 512 bytes of a file, then seeking back to the // beginning before actually transmitting the file. To support this, we // maintain a rewind buffer of 512 bytes. MaxRewindBuffer = 512 ) type readOnlyFile struct { name string client *gowebdav.Client rewindBuffer []byte position int // mu guards the below values. Acquire a write lock before updating any of // them, acquire a read lock before reading any of them. mu sync.RWMutex io.ReadCloser initialFI fs.FileInfo fi fs.FileInfo } // Readdir implements webdav.File. Since this is a file, it always failes with // an os.PathError. func (f *readOnlyFile) Readdir(count int) ([]fs.FileInfo, error) { return nil, &os.PathError{ Op: "readdir", Path: f.fi.Name(), Err: errors.New("is a file"), // TODO(oxtoacart): make sure this and below errors match what a regular os.File does } } // Seek implements webdav.File. Only the specific types of seek used by the // webdav package are implemented, namely: // // - Seek to 0 from end of file // - Seek to 0 from beginning of file, provided that fewer than 512 bytes // have already been read. // - Seek to n from beginning of file, provided that no bytes have already // been read. // // Any other type of seek will fail with an os.PathError. func (f *readOnlyFile) Seek(offset int64, whence int) (int64, error) { err := f.statIfNecessary() if err != nil { return 0, err } switch whence { case io.SeekEnd: if offset == 0 { // seek to end is usually done to check size, let's play along size := f.fi.Size() return size, nil } case io.SeekStart: if offset == 0 { // this is usually done to start reading after getting size if f.position > MaxRewindBuffer { return 0, errors.New("attempted seek after having read past rewind buffer") } f.position = 0 return 0, nil } else if f.position == 0 { // this is usually done to perform a range request to skip the head of the file f.position = int(offset) return offset, nil } } // unknown seek scenario, error out return 0, &os.PathError{ Op: "seek", Path: f.fi.Name(), Err: errors.New("seek not supported"), } } // Stat implements webdav.File, returning either the FileInfo with which this // file was initialized, or the more recently fetched FileInfo if available. func (f *readOnlyFile) Stat() (fs.FileInfo, error) { f.mu.RLock() defer f.mu.RUnlock() if f.fi != nil { return f.fi, nil } return f.initialFI, nil } // Read implements webdav.File. func (f *readOnlyFile) Read(p []byte) (int, error) { err := f.initReaderIfNecessary() if err != nil { return 0, err } amountToReadFromBuffer := len(f.rewindBuffer) - f.position if amountToReadFromBuffer > 0 { n := copy(p, f.rewindBuffer) f.position += n return n, nil } n, err := f.ReadCloser.Read(p) if n > 0 && f.position < MaxRewindBuffer { amountToReadIntoBuffer := MaxRewindBuffer - f.position if amountToReadIntoBuffer > n { amountToReadIntoBuffer = n } f.rewindBuffer = append(f.rewindBuffer, p[:amountToReadIntoBuffer]...) } f.position += n return n, err } // Write implements webdav.File. As this file is read-only, it always fails // with an os.PathError. func (f *readOnlyFile) Write(p []byte) (int, error) { return 0, &os.PathError{ Op: "write", Path: f.fi.Name(), Err: errors.New("read-only"), } } // Close implements webdav.File. func (f *readOnlyFile) Close() error { f.mu.Lock() defer f.mu.Unlock() if f.ReadCloser == nil { return nil } return f.ReadCloser.Close() } // statIfNecessary lazily initializes the FileInfo, bypassing the stat cache to // make sure we have fresh info before trying to read the file. func (f *readOnlyFile) statIfNecessary() error { f.mu.Lock() defer f.mu.Unlock() if f.fi == nil { ctxWithTimeout, cancel := context.WithTimeout(context.Background(), opTimeout) defer cancel() var err error f.fi, err = f.client.Stat(ctxWithTimeout, f.name) if err != nil { return translateWebDAVError(err) } } return nil } // initReaderIfNecessary initializes the Reader if it hasn't been opened yet. We // do this lazily because github.com/tailscale/xnet/webdav often opens files in // read-only mode without ever actually reading from them, so we can improve // performance by avoiding the round-trip to the server. func (f *readOnlyFile) initReaderIfNecessary() error { f.mu.Lock() defer f.mu.Unlock() if f.ReadCloser == nil { var err error f.ReadCloser, err = f.client.ReadStreamOffset(context.Background(), f.name, f.position) if err != nil { return translateWebDAVError(err) } } return nil }