mirror of
https://github.com/tailscale/tailscale.git
synced 2025-10-27 03:32:03 +00:00
tailfs: clean up naming and package structure
- Restyles tailfs -> tailFS - Defines interfaces for main TailFS types - Moves implemenatation of TailFS into tailfsimpl package Updates tailscale/corp#16827 Signed-off-by: Percy Wegmann <percy@tailscale.com>
This commit is contained in:
committed by
Percy Wegmann
parent
79b547804b
commit
abab0d4197
192
tailfs/tailfsimpl/webdavfs/readonly_file.go
Normal file
192
tailfs/tailfsimpl/webdavfs/readonly_file.go
Normal file
@@ -0,0 +1,192 @@
|
||||
// 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
|
||||
}
|
||||
71
tailfs/tailfsimpl/webdavfs/stat_cache.go
Normal file
71
tailfs/tailfsimpl/webdavfs/stat_cache.go
Normal file
@@ -0,0 +1,71 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package webdavfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"path/filepath"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/jellydator/ttlcache/v3"
|
||||
)
|
||||
|
||||
// statCache provides a cache for file directory and file metadata. Especially
|
||||
// when used from the command-line, mapped WebDAV drives can generate
|
||||
// repetitive requests for the same file metadata. This cache helps reduce the
|
||||
// number of round-trips to the WebDAV server for such requests.
|
||||
type statCache struct {
|
||||
// mu guards the below values.
|
||||
mu sync.Mutex
|
||||
cache *ttlcache.Cache[string, fs.FileInfo]
|
||||
}
|
||||
|
||||
func newStatCache(ttl time.Duration) *statCache {
|
||||
cache := ttlcache.New(
|
||||
ttlcache.WithTTL[string, fs.FileInfo](ttl),
|
||||
)
|
||||
go cache.Start()
|
||||
return &statCache{cache: cache}
|
||||
}
|
||||
|
||||
func (c *statCache) getOrFetch(name string, fetch func(string) (fs.FileInfo, error)) (fs.FileInfo, error) {
|
||||
c.mu.Lock()
|
||||
item := c.cache.Get(name)
|
||||
c.mu.Unlock()
|
||||
|
||||
if item != nil {
|
||||
return item.Value(), nil
|
||||
}
|
||||
|
||||
fi, err := fetch(name)
|
||||
if err == nil {
|
||||
c.mu.Lock()
|
||||
c.cache.Set(name, fi, ttlcache.DefaultTTL)
|
||||
c.mu.Unlock()
|
||||
}
|
||||
|
||||
return fi, err
|
||||
}
|
||||
|
||||
func (c *statCache) set(parentPath string, infos []fs.FileInfo) {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
for _, info := range infos {
|
||||
path := filepath.Join(parentPath, filepath.Base(info.Name()))
|
||||
c.cache.Set(path, info, ttlcache.DefaultTTL)
|
||||
}
|
||||
}
|
||||
|
||||
func (c *statCache) invalidate() {
|
||||
c.mu.Lock()
|
||||
defer c.mu.Unlock()
|
||||
|
||||
c.cache.DeleteAll()
|
||||
}
|
||||
|
||||
func (c *statCache) stop() {
|
||||
c.cache.Stop()
|
||||
}
|
||||
104
tailfs/tailfsimpl/webdavfs/stat_cache_test.go
Normal file
104
tailfs/tailfsimpl/webdavfs/stat_cache_test.go
Normal file
@@ -0,0 +1,104 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package webdavfs
|
||||
|
||||
import (
|
||||
"io/fs"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/tstest"
|
||||
)
|
||||
|
||||
func TestStatCache(t *testing.T) {
|
||||
// Make sure we don't leak goroutines
|
||||
tstest.ResourceCheck(t)
|
||||
|
||||
dir, err := os.MkdirTemp("", "")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// create file of size 1
|
||||
filename := filepath.Join(dir, "thefile")
|
||||
err = os.WriteFile(filename, []byte("1"), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
stat := func(name string) (os.FileInfo, error) {
|
||||
return os.Stat(name)
|
||||
}
|
||||
ttl := 1 * time.Second
|
||||
c := newStatCache(ttl)
|
||||
|
||||
// fetch new stat
|
||||
fi, err := c.getOrFetch(filename, stat)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fi.Size() != 1 {
|
||||
t.Errorf("got size %d, want 1", fi.Size())
|
||||
}
|
||||
// save original FileInfo as a StaticFileInfo so we can reuse it later
|
||||
// without worrying about the underlying FileInfo changing.
|
||||
originalFI := &shared.StaticFileInfo{
|
||||
Named: fi.Name(),
|
||||
Sized: fi.Size(),
|
||||
Moded: fi.Mode(),
|
||||
ModdedTime: fi.ModTime(),
|
||||
Dir: fi.IsDir(),
|
||||
}
|
||||
|
||||
// update file to size 2
|
||||
err = os.WriteFile(filename, []byte("12"), 0644)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
// fetch stat again, should still be cached
|
||||
fi, err = c.getOrFetch(filename, stat)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fi.Size() != 1 {
|
||||
t.Errorf("got size %d, want 1", fi.Size())
|
||||
}
|
||||
|
||||
// wait for cache to expire and refetch stat, size should reflect new size
|
||||
time.Sleep(ttl * 2)
|
||||
|
||||
fi, err = c.getOrFetch(filename, stat)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fi.Size() != 2 {
|
||||
t.Errorf("got size %d, want 2", fi.Size())
|
||||
}
|
||||
|
||||
// explicitly set the original FileInfo and make sure it's returned
|
||||
c.set(dir, []fs.FileInfo{originalFI})
|
||||
fi, err = c.getOrFetch(filename, stat)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fi.Size() != 1 {
|
||||
t.Errorf("got size %d, want 1", fi.Size())
|
||||
}
|
||||
|
||||
// invalidate the cache and make sure the new size is returned
|
||||
c.invalidate()
|
||||
fi, err = c.getOrFetch(filename, stat)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if fi.Size() != 2 {
|
||||
t.Errorf("got size %d, want 2", fi.Size())
|
||||
}
|
||||
|
||||
c.stop()
|
||||
}
|
||||
256
tailfs/tailfsimpl/webdavfs/webdavfs.go
Normal file
256
tailfs/tailfsimpl/webdavfs/webdavfs.go
Normal file
@@ -0,0 +1,256 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
// Package webdavfs provides an implementation of webdav.FileSystem backed by
|
||||
// a gowebdav.Client.
|
||||
package webdavfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"time"
|
||||
|
||||
"github.com/tailscale/gowebdav"
|
||||
"github.com/tailscale/xnet/webdav"
|
||||
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
"tailscale.com/tstime"
|
||||
"tailscale.com/types/logger"
|
||||
)
|
||||
|
||||
const (
|
||||
// keep requests from taking too long if the server is down or slow to respond
|
||||
opTimeout = 2 * time.Second // TODO(oxtoacart): tune this
|
||||
)
|
||||
|
||||
type Options struct {
|
||||
// Logf us a logging function to use for debug and error logging.
|
||||
Logf logger.Logf
|
||||
// URL is the base URL of the remote WebDAV server.
|
||||
URL string
|
||||
// Transport is the http.Transport to use for connecting to the WebDAV
|
||||
// server.
|
||||
Transport http.RoundTripper
|
||||
// StatRoot, if true, will cause this filesystem to actually stat its own
|
||||
// root via the remote server. If false, it will use a static directory
|
||||
// info for the root to avoid a round-trip.
|
||||
StatRoot bool
|
||||
// StatCacheTTL, when greater than 0, enables caching of file metadata
|
||||
StatCacheTTL time.Duration
|
||||
// Clock, if specified, determines the current time. If not specified, we
|
||||
// default to time.Now().
|
||||
Clock tstime.Clock
|
||||
}
|
||||
|
||||
// webdavFS adapts gowebdav.Client to webdav.FileSystem
|
||||
type webdavFS struct {
|
||||
logf logger.Logf
|
||||
transport http.RoundTripper
|
||||
*gowebdav.Client
|
||||
now func() time.Time
|
||||
statRoot bool
|
||||
statCache *statCache
|
||||
}
|
||||
|
||||
// New creates a new webdav.FileSystem backed by the given gowebdav.Client.
|
||||
// If cacheTTL is greater than zero, the filesystem will cache results from
|
||||
// Stat calls for the given duration.
|
||||
func New(opts Options) webdav.FileSystem {
|
||||
if opts.Logf == nil {
|
||||
opts.Logf = log.Printf
|
||||
}
|
||||
wfs := &webdavFS{
|
||||
logf: opts.Logf,
|
||||
transport: opts.Transport,
|
||||
Client: gowebdav.New(&gowebdav.Opts{URI: opts.URL, Transport: opts.Transport}),
|
||||
statRoot: opts.StatRoot,
|
||||
}
|
||||
if opts.StatCacheTTL > 0 {
|
||||
wfs.statCache = newStatCache(opts.StatCacheTTL)
|
||||
}
|
||||
if opts.Clock != nil {
|
||||
wfs.now = opts.Clock.Now
|
||||
} else {
|
||||
wfs.now = time.Now
|
||||
}
|
||||
return wfs
|
||||
}
|
||||
|
||||
// Mkdir implements webdav.FileSystem.
|
||||
func (wfs *webdavFS) Mkdir(ctx context.Context, name string, perm os.FileMode) error {
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, opTimeout)
|
||||
defer cancel()
|
||||
|
||||
if wfs.statCache != nil {
|
||||
wfs.statCache.invalidate()
|
||||
}
|
||||
return translateWebDAVError(wfs.Client.Mkdir(ctxWithTimeout, name, perm))
|
||||
}
|
||||
|
||||
// OpenFile implements webdav.FileSystem.
|
||||
func (wfs *webdavFS) OpenFile(ctx context.Context, name string, flag int, perm os.FileMode) (webdav.File, error) {
|
||||
if hasFlag(flag, os.O_APPEND) {
|
||||
return nil, &os.PathError{
|
||||
Op: "open",
|
||||
Path: name,
|
||||
Err: errors.New("mode APPEND not supported"),
|
||||
}
|
||||
}
|
||||
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, opTimeout)
|
||||
defer cancel()
|
||||
|
||||
if hasFlag(flag, os.O_WRONLY) || hasFlag(flag, os.O_RDWR) {
|
||||
if wfs.statCache != nil {
|
||||
wfs.statCache.invalidate()
|
||||
}
|
||||
|
||||
fi, err := wfs.Stat(ctxWithTimeout, name)
|
||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
||||
return nil, err
|
||||
}
|
||||
if err == nil && fi.IsDir() {
|
||||
return nil, &os.PathError{
|
||||
Op: "open",
|
||||
Path: name,
|
||||
Err: errors.New("is a directory"),
|
||||
}
|
||||
}
|
||||
|
||||
pipeReader, pipeWriter := io.Pipe()
|
||||
f := &writeOnlyFile{
|
||||
WriteCloser: pipeWriter,
|
||||
name: name,
|
||||
perm: perm,
|
||||
fs: wfs,
|
||||
finalError: make(chan error, 1),
|
||||
}
|
||||
go func() {
|
||||
defer pipeReader.Close()
|
||||
err := wfs.Client.WriteStream(context.Background(), name, pipeReader, perm)
|
||||
f.finalError <- err
|
||||
close(f.finalError)
|
||||
}()
|
||||
|
||||
return f, nil
|
||||
}
|
||||
|
||||
// Assume reading
|
||||
fi, err := wfs.Stat(ctxWithTimeout, name)
|
||||
if err != nil {
|
||||
return nil, translateWebDAVError(err)
|
||||
}
|
||||
if fi.IsDir() {
|
||||
return wfs.dirWithChildren(name, fi), nil
|
||||
}
|
||||
|
||||
return &readOnlyFile{
|
||||
client: wfs.Client,
|
||||
name: name,
|
||||
initialFI: fi,
|
||||
rewindBuffer: make([]byte, 0, MaxRewindBuffer),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (wfs *webdavFS) dirWithChildren(name string, fi fs.FileInfo) webdav.File {
|
||||
return &shared.DirFile{
|
||||
Info: fi,
|
||||
LoadChildren: func() ([]fs.FileInfo, error) {
|
||||
ctxWithTimeout, cancel := context.WithTimeout(context.Background(), opTimeout)
|
||||
defer cancel()
|
||||
|
||||
dirInfos, err := wfs.Client.ReadDir(ctxWithTimeout, name)
|
||||
if err != nil {
|
||||
wfs.logf("encountered error reading children of '%v', returning empty list: %v", name, err)
|
||||
// We do not return the actual error here because some WebDAV clients
|
||||
// will take that as an invitation to retry, hanging in the process.
|
||||
return dirInfos, nil
|
||||
}
|
||||
if wfs.statCache != nil {
|
||||
wfs.statCache.set(name, dirInfos)
|
||||
}
|
||||
return dirInfos, nil
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// RemoveAll implements webdav.FileSystem.
|
||||
func (wfs *webdavFS) RemoveAll(ctx context.Context, name string) error {
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, opTimeout)
|
||||
defer cancel()
|
||||
|
||||
if wfs.statCache != nil {
|
||||
wfs.statCache.invalidate()
|
||||
}
|
||||
return wfs.Client.RemoveAll(ctxWithTimeout, name)
|
||||
}
|
||||
|
||||
// Rename implements webdav.FileSystem.
|
||||
func (wfs *webdavFS) Rename(ctx context.Context, oldName, newName string) error {
|
||||
ctxWithTimeout, cancel := context.WithTimeout(ctx, opTimeout)
|
||||
defer cancel()
|
||||
|
||||
if wfs.statCache != nil {
|
||||
wfs.statCache.invalidate()
|
||||
}
|
||||
return wfs.Client.Rename(ctxWithTimeout, oldName, newName, false)
|
||||
}
|
||||
|
||||
// Stat implements webdav.FileSystem.
|
||||
func (wfs *webdavFS) Stat(ctx context.Context, name string) (fs.FileInfo, error) {
|
||||
if wfs.statCache != nil {
|
||||
return wfs.statCache.getOrFetch(name, wfs.doStat)
|
||||
}
|
||||
return wfs.doStat(name)
|
||||
}
|
||||
|
||||
// Close implements webdav.FileSystem.
|
||||
func (wfs *webdavFS) Close() error {
|
||||
if wfs.statCache != nil {
|
||||
wfs.statCache.stop()
|
||||
}
|
||||
tr, ok := wfs.transport.(*http.Transport)
|
||||
if ok {
|
||||
tr.CloseIdleConnections()
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (wfs *webdavFS) doStat(name string) (fs.FileInfo, error) {
|
||||
ctxWithTimeout, cancel := context.WithTimeout(context.Background(), opTimeout)
|
||||
defer cancel()
|
||||
|
||||
if !wfs.statRoot && shared.IsRoot(name) {
|
||||
// use a static directory info for the root
|
||||
// always use now() as the modified time to bust caches
|
||||
return shared.ReadOnlyDirInfo(name, wfs.now()), nil
|
||||
}
|
||||
fi, err := wfs.Client.Stat(ctxWithTimeout, name)
|
||||
return fi, translateWebDAVError(err)
|
||||
}
|
||||
|
||||
func translateWebDAVError(err error) error {
|
||||
if err == nil {
|
||||
return nil
|
||||
}
|
||||
var se gowebdav.StatusError
|
||||
if errors.As(err, &se) {
|
||||
if se.Status == http.StatusNotFound {
|
||||
return os.ErrNotExist
|
||||
}
|
||||
}
|
||||
// Note, we intentionally don't wrap the error because we don't want
|
||||
// github.com/tailscale/xnet/webdav to try to interpret the underlying
|
||||
// error.
|
||||
return fmt.Errorf("unexpected WebDAV error: %v", err)
|
||||
}
|
||||
|
||||
func hasFlag(flags int, flag int) bool {
|
||||
return (flags & flag) == flag
|
||||
}
|
||||
89
tailfs/tailfsimpl/webdavfs/writeonly_file.go
Normal file
89
tailfs/tailfsimpl/webdavfs/writeonly_file.go
Normal file
@@ -0,0 +1,89 @@
|
||||
// Copyright (c) Tailscale Inc & AUTHORS
|
||||
// SPDX-License-Identifier: BSD-3-Clause
|
||||
|
||||
package webdavfs
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"io"
|
||||
"io/fs"
|
||||
"os"
|
||||
|
||||
"tailscale.com/tailfs/tailfsimpl/shared"
|
||||
)
|
||||
|
||||
type writeOnlyFile struct {
|
||||
io.WriteCloser
|
||||
name string
|
||||
perm os.FileMode
|
||||
fs *webdavFS
|
||||
finalError chan error
|
||||
}
|
||||
|
||||
// Readdir implements webdav.File. As this is a file, this always fails with an
|
||||
// os.PathError.
|
||||
func (f *writeOnlyFile) Readdir(count int) ([]fs.FileInfo, error) {
|
||||
return nil, &os.PathError{
|
||||
Op: "readdir",
|
||||
Path: f.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. This always fails with an os.PathError.
|
||||
func (f *writeOnlyFile) Seek(offset int64, whence int) (int64, error) {
|
||||
return 0, &os.PathError{
|
||||
Op: "seek",
|
||||
Path: f.name,
|
||||
Err: errors.New("seek not supported"),
|
||||
}
|
||||
}
|
||||
|
||||
// Stat implements webdav.File.
|
||||
func (f *writeOnlyFile) Stat() (fs.FileInfo, error) {
|
||||
fi, err := f.fs.Stat(context.Background(), f.name)
|
||||
if err != nil {
|
||||
// use static info for newly created file
|
||||
now := f.fs.now()
|
||||
fi = &shared.StaticFileInfo{
|
||||
Named: f.name,
|
||||
Sized: 0,
|
||||
Moded: f.perm,
|
||||
BirthedTime: now,
|
||||
ModdedTime: now,
|
||||
Dir: false,
|
||||
}
|
||||
}
|
||||
return fi, nil
|
||||
}
|
||||
|
||||
// Read implements webdav.File. As this is a write-only file, it always fails
|
||||
// with an os.PathError.
|
||||
func (f *writeOnlyFile) Read(p []byte) (int, error) {
|
||||
return 0, &os.PathError{
|
||||
Op: "write",
|
||||
Path: f.name,
|
||||
Err: errors.New("write-only"),
|
||||
}
|
||||
}
|
||||
|
||||
// Write implements webdav.File.
|
||||
func (f *writeOnlyFile) Write(p []byte) (int, error) {
|
||||
select {
|
||||
case err := <-f.finalError:
|
||||
return 0, err
|
||||
default:
|
||||
return f.WriteCloser.Write(p)
|
||||
}
|
||||
}
|
||||
|
||||
// Close implements webdav.File.
|
||||
func (f *writeOnlyFile) Close() error {
|
||||
err := f.WriteCloser.Close()
|
||||
writeErr := <-f.finalError
|
||||
if writeErr != nil {
|
||||
return writeErr
|
||||
}
|
||||
return err
|
||||
}
|
||||
Reference in New Issue
Block a user