tailscale/tailfs/webdavfs/readonly_file.go
Percy Wegmann 993acf4475 tailfs: initial implementation
Add a WebDAV-based folder sharing mechanism that is exposed to local clients at
100.100.100.100:8080 and to remote peers via a new peerapi endpoint at
/v0/tailfs.

Add the ability to manage folder sharing via the new 'share' CLI sub-command.

Updates tailscale/corp#16827

Signed-off-by: Percy Wegmann <percy@tailscale.com>
2024-02-09 09:13:51 -06:00

193 lines
4.9 KiB
Go

// 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
}