mirror of
https://github.com/tailscale/tailscale.git
synced 2024-11-29 21:15:39 +00:00
993acf4475
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>
193 lines
4.9 KiB
Go
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
|
|
}
|