seaweedfs/weed/server/webdav_server.go

617 lines
15 KiB
Go
Raw Normal View History

package weed_server
import (
2020-12-02 07:32:27 +08:00
"bytes"
"context"
2019-05-03 15:24:35 +08:00
"fmt"
"io"
2020-03-22 16:30:27 +08:00
"math"
"os"
"path"
"strings"
"time"
2020-12-02 07:32:27 +08:00
"github.com/chrislusf/seaweedfs/weed/util/buffered_writer"
2019-12-07 23:56:05 +08:00
"golang.org/x/net/webdav"
"google.golang.org/grpc"
2019-05-03 15:55:52 +08:00
"github.com/chrislusf/seaweedfs/weed/operation"
2020-03-04 16:39:47 +08:00
"github.com/chrislusf/seaweedfs/weed/pb"
2019-05-03 15:24:35 +08:00
"github.com/chrislusf/seaweedfs/weed/pb/filer_pb"
"github.com/chrislusf/seaweedfs/weed/util"
2020-04-12 03:45:24 +08:00
"github.com/chrislusf/seaweedfs/weed/util/chunk_cache"
2019-12-07 23:56:05 +08:00
2020-09-01 15:21:19 +08:00
"github.com/chrislusf/seaweedfs/weed/filer"
"github.com/chrislusf/seaweedfs/weed/glog"
"github.com/chrislusf/seaweedfs/weed/security"
)
type WebDavOption struct {
Filer string
FilerGrpcAddress string
DomainName string
BucketsPath string
GrpcDialOption grpc.DialOption
2019-05-03 15:24:35 +08:00
Collection string
Uid uint32
Gid uint32
Cipher bool
2020-04-12 12:12:41 +08:00
CacheDir string
CacheSizeMB int64
}
type WebDavServer struct {
option *WebDavOption
secret security.SigningKey
2020-09-01 15:21:19 +08:00
filer *filer.Filer
grpcDialOption grpc.DialOption
Handler *webdav.Handler
}
func NewWebDavServer(option *WebDavOption) (ws *WebDavServer, err error) {
2019-05-03 15:24:35 +08:00
fs, _ := NewWebDavFileSystem(option)
ws = &WebDavServer{
option: option,
grpcDialOption: security.LoadClientTLS(util.GetViper(), "grpc.filer"),
Handler: &webdav.Handler{
FileSystem: fs,
LockSystem: webdav.NewMemLS(),
},
}
return ws, nil
}
// adapted from https://github.com/mattn/davfs/blob/master/plugin/mysql/mysql.go
type WebDavFileSystem struct {
2019-05-03 15:24:35 +08:00
option *WebDavOption
secret security.SigningKey
2020-09-01 15:21:19 +08:00
filer *filer.Filer
2019-05-03 15:24:35 +08:00
grpcDialOption grpc.DialOption
2020-08-18 11:15:53 +08:00
chunkCache *chunk_cache.TieredChunkCache
signature int32
}
type FileInfo struct {
2019-05-03 15:24:35 +08:00
name string
size int64
mode os.FileMode
modifiledTime time.Time
isDirectory bool
}
func (fi *FileInfo) Name() string { return fi.name }
func (fi *FileInfo) Size() int64 { return fi.size }
func (fi *FileInfo) Mode() os.FileMode { return fi.mode }
2019-05-03 15:24:35 +08:00
func (fi *FileInfo) ModTime() time.Time { return fi.modifiledTime }
func (fi *FileInfo) IsDir() bool { return fi.isDirectory }
func (fi *FileInfo) Sys() interface{} { return nil }
type WebDavFile struct {
2019-05-03 15:24:35 +08:00
fs *WebDavFileSystem
name string
isDirectory bool
off int64
entry *filer_pb.Entry
2020-09-01 15:21:19 +08:00
entryViewCache []filer.VisibleInterval
reader io.ReaderAt
2020-12-02 07:32:27 +08:00
bufWriter *buffered_writer.BufferedWriteCloser
collection string
replication string
}
2019-05-03 15:24:35 +08:00
func NewWebDavFileSystem(option *WebDavOption) (webdav.FileSystem, error) {
2020-04-12 12:12:41 +08:00
cacheUniqueId := util.Md5String([]byte("webdav" + option.FilerGrpcAddress + util.Version()))[0:8]
cacheDir := path.Join(option.CacheDir, cacheUniqueId)
chunkCache := chunk_cache.NewTieredChunkCache(256, cacheDir, option.CacheSizeMB, 1024*1024)
2019-05-03 15:24:35 +08:00
return &WebDavFileSystem{
option: option,
2020-04-12 12:12:41 +08:00
chunkCache: chunkCache,
signature: util.RandomInt32(),
2019-05-03 15:24:35 +08:00
}, nil
}
2020-04-30 04:26:02 +08:00
var _ = filer_pb.FilerClient(&WebDavFileSystem{})
func (fs *WebDavFileSystem) WithFilerClient(fn func(filer_pb.SeaweedFilerClient) error) error {
2019-05-03 15:24:35 +08:00
2020-03-04 16:39:47 +08:00
return pb.WithCachedGrpcClient(func(grpcConnection *grpc.ClientConn) error {
2019-05-03 15:24:35 +08:00
client := filer_pb.NewSeaweedFilerClient(grpcConnection)
return fn(client)
2019-05-03 15:24:35 +08:00
}, fs.option.FilerGrpcAddress, fs.option.GrpcDialOption)
}
2021-01-29 06:36:29 +08:00
func (fs *WebDavFileSystem) AdjustedUrl(location *filer_pb.Location) string {
return location.Url
}
func clearName(name string) (string, error) {
slashed := strings.HasSuffix(name, "/")
name = path.Clean(name)
if !strings.HasSuffix(name, "/") && slashed {
name += "/"
}
if !strings.HasPrefix(name, "/") {
return "", os.ErrInvalid
}
return name, nil
}
2019-05-03 15:24:35 +08:00
func (fs *WebDavFileSystem) Mkdir(ctx context.Context, fullDirPath string, perm os.FileMode) error {
2019-05-03 15:24:35 +08:00
glog.V(2).Infof("WebDavFileSystem.Mkdir %v", fullDirPath)
2019-05-03 15:24:35 +08:00
if !strings.HasSuffix(fullDirPath, "/") {
fullDirPath += "/"
}
var err error
2019-05-03 15:24:35 +08:00
if fullDirPath, err = clearName(fullDirPath); err != nil {
return err
}
2019-05-03 15:24:35 +08:00
_, err = fs.stat(ctx, fullDirPath)
if err == nil {
return os.ErrExist
}
return fs.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
2020-03-23 15:01:34 +08:00
dir, name := util.FullPath(fullDirPath).DirAndName()
2019-05-04 04:13:08 +08:00
request := &filer_pb.CreateEntryRequest{
Directory: dir,
Entry: &filer_pb.Entry{
Name: name,
IsDirectory: true,
Attributes: &filer_pb.FuseAttributes{
Mtime: time.Now().Unix(),
Crtime: time.Now().Unix(),
FileMode: uint32(perm | os.ModeDir),
Uid: fs.option.Uid,
Gid: fs.option.Gid,
2019-05-03 15:24:35 +08:00
},
2019-05-04 04:13:08 +08:00
},
Signatures: []int32{fs.signature},
2019-05-04 04:13:08 +08:00
}
2019-05-03 15:24:35 +08:00
2019-05-04 04:13:08 +08:00
glog.V(1).Infof("mkdir: %v", request)
if err := filer_pb.CreateEntry(client, request); err != nil {
2019-05-04 04:13:08 +08:00
return fmt.Errorf("mkdir %s/%s: %v", dir, name, err)
}
2019-05-04 04:13:08 +08:00
return nil
})
}
2019-05-03 15:24:35 +08:00
func (fs *WebDavFileSystem) OpenFile(ctx context.Context, fullFilePath string, flag int, perm os.FileMode) (webdav.File, error) {
2019-12-07 23:56:05 +08:00
glog.V(2).Infof("WebDavFileSystem.OpenFile %v %x", fullFilePath, flag)
var err error
2019-05-03 15:24:35 +08:00
if fullFilePath, err = clearName(fullFilePath); err != nil {
return nil, err
}
if flag&os.O_CREATE != 0 {
// file should not have / suffix.
2019-05-03 15:24:35 +08:00
if strings.HasSuffix(fullFilePath, "/") {
return nil, os.ErrInvalid
}
2019-05-03 15:24:35 +08:00
_, err = fs.stat(ctx, fullFilePath)
if err == nil {
if flag&os.O_EXCL != 0 {
return nil, os.ErrExist
}
2019-05-03 15:24:35 +08:00
fs.removeAll(ctx, fullFilePath)
}
2019-05-03 15:24:35 +08:00
2020-03-23 15:01:34 +08:00
dir, name := util.FullPath(fullFilePath).DirAndName()
err = fs.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
if err := filer_pb.CreateEntry(client, &filer_pb.CreateEntryRequest{
2019-05-03 15:24:35 +08:00
Directory: dir,
Entry: &filer_pb.Entry{
Name: name,
IsDirectory: perm&os.ModeDir > 0,
Attributes: &filer_pb.FuseAttributes{
Mtime: time.Now().Unix(),
Crtime: time.Now().Unix(),
FileMode: uint32(perm),
Uid: fs.option.Uid,
Gid: fs.option.Gid,
Collection: fs.option.Collection,
Replication: "000",
TtlSec: 0,
},
},
Signatures: []int32{fs.signature},
2019-05-03 15:24:35 +08:00
}); err != nil {
return fmt.Errorf("create %s: %v", fullFilePath, err)
}
return nil
})
if err != nil {
return nil, err
}
2019-05-03 15:24:35 +08:00
return &WebDavFile{
fs: fs,
name: fullFilePath,
isDirectory: false,
2020-12-02 07:32:27 +08:00
bufWriter: buffered_writer.NewBufferedWriteCloser(4 * 1024 * 1024),
2019-05-03 15:24:35 +08:00
}, nil
}
2019-05-03 15:24:35 +08:00
fi, err := fs.stat(ctx, fullFilePath)
if err != nil {
return nil, os.ErrNotExist
}
2019-05-03 15:24:35 +08:00
if !strings.HasSuffix(fullFilePath, "/") && fi.IsDir() {
fullFilePath += "/"
}
2019-05-03 15:24:35 +08:00
return &WebDavFile{
fs: fs,
name: fullFilePath,
isDirectory: false,
2020-12-02 07:32:27 +08:00
bufWriter: buffered_writer.NewBufferedWriteCloser(4 * 1024 * 1024),
2019-05-03 15:24:35 +08:00
}, nil
}
2019-05-03 15:24:35 +08:00
func (fs *WebDavFileSystem) removeAll(ctx context.Context, fullFilePath string) error {
var err error
2019-05-03 15:24:35 +08:00
if fullFilePath, err = clearName(fullFilePath); err != nil {
return err
}
2020-03-23 15:01:34 +08:00
dir, name := util.FullPath(fullFilePath).DirAndName()
2019-05-03 15:24:35 +08:00
2020-09-10 02:21:23 +08:00
return filer_pb.Remove(fs, dir, name, true, false, false, false, []int32{fs.signature})
2019-05-03 15:24:35 +08:00
}
func (fs *WebDavFileSystem) RemoveAll(ctx context.Context, name string) error {
glog.V(2).Infof("WebDavFileSystem.RemoveAll %v", name)
2019-05-03 15:24:35 +08:00
return fs.removeAll(ctx, name)
}
func (fs *WebDavFileSystem) Rename(ctx context.Context, oldName, newName string) error {
glog.V(2).Infof("WebDavFileSystem.Rename %v to %v", oldName, newName)
var err error
if oldName, err = clearName(oldName); err != nil {
return err
}
if newName, err = clearName(newName); err != nil {
return err
}
2019-05-03 15:24:35 +08:00
of, err := fs.stat(ctx, oldName)
if err != nil {
return os.ErrExist
}
2019-05-04 05:12:51 +08:00
if of.IsDir() {
if strings.HasSuffix(oldName, "/") {
oldName = strings.TrimRight(oldName, "/")
}
if strings.HasSuffix(newName, "/") {
newName = strings.TrimRight(newName, "/")
}
}
2019-05-03 15:24:35 +08:00
_, err = fs.stat(ctx, newName)
if err == nil {
return os.ErrExist
}
2020-03-23 15:01:34 +08:00
oldDir, oldBaseName := util.FullPath(oldName).DirAndName()
newDir, newBaseName := util.FullPath(newName).DirAndName()
2019-05-03 15:24:35 +08:00
return fs.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
2019-05-03 15:24:35 +08:00
request := &filer_pb.AtomicRenameEntryRequest{
OldDirectory: oldDir,
OldName: oldBaseName,
NewDirectory: newDir,
NewName: newBaseName,
}
_, err := client.AtomicRenameEntry(ctx, request)
if err != nil {
return fmt.Errorf("renaming %s/%s => %s/%s: %v", oldDir, oldBaseName, newDir, newBaseName, err)
}
return nil
})
}
2019-05-03 15:24:35 +08:00
func (fs *WebDavFileSystem) stat(ctx context.Context, fullFilePath string) (os.FileInfo, error) {
var err error
2019-05-03 15:24:35 +08:00
if fullFilePath, err = clearName(fullFilePath); err != nil {
return nil, err
}
2020-03-23 15:01:34 +08:00
fullpath := util.FullPath(fullFilePath)
2020-01-20 15:59:46 +08:00
var fi FileInfo
2020-03-23 15:01:34 +08:00
entry, err := filer_pb.GetEntry(fs, fullpath)
2019-05-04 05:12:51 +08:00
if entry == nil {
return nil, os.ErrNotExist
}
if err != nil {
return nil, err
}
2020-09-01 15:21:19 +08:00
fi.size = int64(filer.FileSize(entry))
2020-01-20 15:59:46 +08:00
fi.name = string(fullpath)
2019-05-03 15:24:35 +08:00
fi.mode = os.FileMode(entry.Attributes.FileMode)
fi.modifiledTime = time.Unix(entry.Attributes.Mtime, 0)
fi.isDirectory = entry.IsDirectory
2020-01-20 15:59:46 +08:00
if fi.name == "/" {
2019-05-03 15:24:35 +08:00
fi.modifiledTime = time.Now()
fi.isDirectory = true
}
return &fi, nil
}
func (fs *WebDavFileSystem) Stat(ctx context.Context, name string) (os.FileInfo, error) {
glog.V(2).Infof("WebDavFileSystem.Stat %v", name)
2019-05-03 15:24:35 +08:00
return fs.stat(ctx, name)
}
2020-12-02 07:32:27 +08:00
func (f *WebDavFile) saveDataAsChunk(reader io.Reader, name string, offset int64) (chunk *filer_pb.FileChunk, collection, replication string, err error) {
2019-05-03 15:55:52 +08:00
var fileId, host string
var auth security.EncodedJwt
2020-12-02 07:32:27 +08:00
if flushErr := f.fs.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
ctx := context.Background()
2019-05-03 15:55:52 +08:00
request := &filer_pb.AssignVolumeRequest{
Count: 1,
Replication: "",
2019-05-03 15:55:52 +08:00
Collection: f.fs.option.Collection,
2020-12-02 07:32:27 +08:00
Path: name,
2019-05-03 15:55:52 +08:00
}
resp, err := client.AssignVolume(ctx, request)
if err != nil {
glog.V(0).Infof("assign volume failure %v: %v", request, err)
return err
}
if resp.Error != "" {
return fmt.Errorf("assign volume failure %v: %v", request, resp.Error)
}
2019-05-03 15:55:52 +08:00
fileId, host, auth = resp.FileId, resp.Url, security.EncodedJwt(resp.Auth)
2020-12-02 07:32:27 +08:00
f.collection, f.replication = resp.Collection, resp.Replication
2019-05-03 15:55:52 +08:00
return nil
2020-12-02 07:32:27 +08:00
}); flushErr != nil {
return nil, f.collection, f.replication, fmt.Errorf("filerGrpcAddress assign volume: %v", flushErr)
2019-05-03 15:55:52 +08:00
}
fileUrl := fmt.Sprintf("http://%s/%s", host, fileId)
2020-12-02 07:32:27 +08:00
uploadResult, flushErr, _ := operation.Upload(fileUrl, f.name, f.fs.option.Cipher, reader, false, "", nil, auth)
if flushErr != nil {
glog.V(0).Infof("upload data %v to %s: %v", f.name, fileUrl, flushErr)
return nil, f.collection, f.replication, fmt.Errorf("upload data: %v", flushErr)
2019-05-03 15:55:52 +08:00
}
if uploadResult.Error != "" {
2020-12-02 07:32:27 +08:00
glog.V(0).Infof("upload failure %v to %s: %v", f.name, fileUrl, flushErr)
return nil, f.collection, f.replication, fmt.Errorf("upload result: %v", uploadResult.Error)
}
return uploadResult.ToPbFileChunk(fileId, offset), f.collection, f.replication, nil
}
func (f *WebDavFile) Write(buf []byte) (int, error) {
glog.V(2).Infof("WebDavFileSystem.Write %v", f.name)
dir, _ := util.FullPath(f.name).DirAndName()
var getErr error
ctx := context.Background()
if f.entry == nil {
f.entry, getErr = filer_pb.GetEntry(f.fs, util.FullPath(f.name))
}
if f.entry == nil {
return 0, getErr
}
if getErr != nil {
return 0, getErr
2019-05-03 15:55:52 +08:00
}
2020-12-02 07:32:27 +08:00
if f.bufWriter.FlushFunc == nil {
f.bufWriter.FlushFunc = func(data []byte, offset int64) (flushErr error) {
2019-05-03 15:55:52 +08:00
2020-12-02 07:32:27 +08:00
var chunk *filer_pb.FileChunk
chunk, f.collection, f.replication, flushErr = f.saveDataAsChunk(bytes.NewReader(data), f.name, offset)
2019-05-03 15:55:52 +08:00
2020-12-02 07:32:27 +08:00
if flushErr != nil {
return fmt.Errorf("%s upload result: %v", f.name, flushErr)
}
f.entry.Content = nil
f.entry.Chunks = append(f.entry.Chunks, chunk)
return flushErr
2019-05-03 15:55:52 +08:00
}
2020-12-02 07:32:27 +08:00
f.bufWriter.CloseFunc = func() error {
manifestedChunks, manifestErr := filer.MaybeManifestize(f.saveDataAsChunk, f.entry.Chunks)
if manifestErr != nil {
// not good, but should be ok
2020-12-02 11:37:21 +08:00
glog.V(0).Infof("file %s close MaybeManifestize: %v", f.name, manifestErr)
2020-12-02 07:32:27 +08:00
} else {
f.entry.Chunks = manifestedChunks
}
flushErr := f.fs.WithFilerClient(func(client filer_pb.SeaweedFilerClient) error {
f.entry.Attributes.Mtime = time.Now().Unix()
f.entry.Attributes.Collection = f.collection
f.entry.Attributes.Replication = f.replication
request := &filer_pb.UpdateEntryRequest{
Directory: dir,
Entry: f.entry,
Signatures: []int32{f.fs.signature},
}
2019-05-03 15:55:52 +08:00
2020-12-02 07:32:27 +08:00
if _, err := client.UpdateEntry(ctx, request); err != nil {
return fmt.Errorf("update %s: %v", f.name, err)
}
return nil
})
return flushErr
2019-05-03 15:55:52 +08:00
}
2020-12-02 07:32:27 +08:00
}
2019-05-03 15:55:52 +08:00
2020-12-02 07:32:27 +08:00
written, err := f.bufWriter.Write(buf)
2019-05-03 15:55:52 +08:00
if err == nil {
2019-12-07 23:56:05 +08:00
glog.V(3).Infof("WebDavFileSystem.Write %v: written [%d,%d)", f.name, f.off, f.off+int64(len(buf)))
2020-12-02 07:32:27 +08:00
f.off += int64(written)
2019-05-03 15:55:52 +08:00
}
2019-12-07 23:56:05 +08:00
2020-12-02 07:32:27 +08:00
return written, err
}
func (f *WebDavFile) Close() error {
glog.V(2).Infof("WebDavFileSystem.Close %v", f.name)
2020-12-02 07:32:27 +08:00
err := f.bufWriter.Close()
2019-05-03 15:24:35 +08:00
if f.entry != nil {
f.entry = nil
f.entryViewCache = nil
}
2020-12-02 07:32:27 +08:00
return err
}
2019-05-03 15:24:35 +08:00
func (f *WebDavFile) Read(p []byte) (readSize int, err error) {
glog.V(2).Infof("WebDavFileSystem.Read %v", f.name)
2019-05-03 15:24:35 +08:00
if f.entry == nil {
2020-03-23 15:01:34 +08:00
f.entry, err = filer_pb.GetEntry(f.fs, util.FullPath(f.name))
2019-05-03 15:24:35 +08:00
}
2019-05-04 05:12:51 +08:00
if f.entry == nil {
return 0, err
}
if err != nil {
return 0, err
}
2020-09-01 15:21:19 +08:00
fileSize := int64(filer.FileSize(f.entry))
2020-08-16 15:49:08 +08:00
if fileSize == 0 {
2019-05-03 15:24:35 +08:00
return 0, io.EOF
}
if f.entryViewCache == nil {
2020-09-01 15:21:19 +08:00
f.entryViewCache, _ = filer.NonOverlappingVisibleIntervals(filer.LookupFn(f.fs), f.entry.Chunks)
2020-03-22 16:30:27 +08:00
f.reader = nil
2019-05-03 15:24:35 +08:00
}
2020-03-22 16:30:27 +08:00
if f.reader == nil {
chunkViews := filer.ViewFromVisibleIntervals(f.entryViewCache, 0, math.MaxInt64)
f.reader = filer.NewChunkReaderAtFromClient(filer.LookupFn(f.fs), chunkViews, f.fs.chunkCache, fileSize)
2019-05-03 15:24:35 +08:00
}
readSize, err = f.reader.ReadAt(p, f.off)
2019-12-07 23:56:05 +08:00
2020-03-22 16:39:08 +08:00
glog.V(3).Infof("WebDavFileSystem.Read %v: [%d,%d)", f.name, f.off, f.off+int64(readSize))
2020-03-22 16:30:27 +08:00
f.off += int64(readSize)
if err != nil && err != io.EOF {
2020-03-22 16:30:27 +08:00
glog.Errorf("file read %s: %v", f.name, err)
2019-05-03 15:24:35 +08:00
}
2019-12-07 23:56:05 +08:00
2019-05-03 15:24:35 +08:00
return
2020-03-22 16:30:27 +08:00
}
2019-05-03 15:24:35 +08:00
func (f *WebDavFile) Readdir(count int) (ret []os.FileInfo, err error) {
2019-05-03 15:24:35 +08:00
glog.V(2).Infof("WebDavFileSystem.Readdir %v count %d", f.name, count)
2020-03-23 15:01:34 +08:00
dir, _ := util.FullPath(f.name).DirAndName()
2019-05-03 15:24:35 +08:00
err = filer_pb.ReadDirAllEntries(f.fs, util.FullPath(dir), "", func(entry *filer_pb.Entry, isLast bool) error {
2019-05-03 15:24:35 +08:00
fi := FileInfo{
2020-09-01 15:21:19 +08:00
size: int64(filer.FileSize(entry)),
2019-05-03 15:24:35 +08:00
name: entry.Name,
mode: os.FileMode(entry.Attributes.FileMode),
modifiledTime: time.Unix(entry.Attributes.Mtime, 0),
isDirectory: entry.IsDirectory,
}
if !strings.HasSuffix(fi.name, "/") && fi.IsDir() {
fi.name += "/"
}
glog.V(4).Infof("entry: %v", fi.name)
ret = append(ret, &fi)
return nil
2019-05-03 15:24:35 +08:00
})
old := f.off
if old >= int64(len(ret)) {
if count > 0 {
return nil, io.EOF
}
return nil, nil
}
if count > 0 {
f.off += int64(count)
if f.off > int64(len(ret)) {
f.off = int64(len(ret))
}
} else {
f.off = int64(len(ret))
old = 0
}
return ret[old:f.off], nil
}
func (f *WebDavFile) Seek(offset int64, whence int) (int64, error) {
glog.V(2).Infof("WebDavFile.Seek %v %v %v", f.name, offset, whence)
2019-05-03 15:24:35 +08:00
ctx := context.Background()
var err error
switch whence {
2020-09-16 15:37:57 +08:00
case io.SeekStart:
f.off = 0
2020-09-16 15:37:57 +08:00
case io.SeekEnd:
2019-05-03 15:24:35 +08:00
if fi, err := f.fs.stat(ctx, f.name); err != nil {
return 0, err
} else {
f.off = fi.Size()
}
}
f.off += offset
return f.off, err
}
func (f *WebDavFile) Stat() (os.FileInfo, error) {
glog.V(2).Infof("WebDavFile.Stat %v", f.name)
2019-05-03 15:24:35 +08:00
ctx := context.Background()
return f.fs.stat(ctx, f.name)
}