diff --git a/DAV-requirements.md b/DAV-requirements.md index b8b4594..1eedc80 100644 --- a/DAV-requirements.md +++ b/DAV-requirements.md @@ -25,9 +25,8 @@ Renaming files, creating directories, removing files/directories ## For writing files. Writing files is a delicate operation, we should take care to do it -correctly. Right now, the driver checks if we're talking to an Apache -or SabreDAV implementation because they are the only ones that implement -partial put. +correctly. The driver checks if we're talking to an Apache or SabreDAV +implementation because they are the only ones that implement partial updates. - If-Match: * / If-None-Match: * support (RFC2616). If-Match: * is used with the PUT method to prevent files being written @@ -36,14 +35,20 @@ partial put. if it already exists. Though this is very basic, there are servers that do not implement this, or not correctly. -- Partial PUT support. +- Partial PUT support (preferred). This means writing just a part of a file, updating it in-place, instead - of replacing an existing file. webdavFS detects what webserver it is - talking to. If it's Apache it uses PUT + Content-Range, if it's + of replacing an existing file. webdavfs detects what webserver it is + talking to. If it's Apache it uses PUT + Content-Range, if it's SabreDAV it uses PATCH + X-Update-Range. For more info, see: https://blog.sphere.chronosempire.org.uk/2012/11/21/webdav-and-the-http-patch-nightmare http://sabre.io/dav/http-patch/ +- Fallback: full PUT on close. + If partial PUT is not available, webdavfs supports read–write by using a + per‑open in‑RAM buffer. The full file is uploaded with a normal PUT on + fsync/close or after a short inactivity period. Servers only need to support + standard `PUT`, `GET`, and conditional `If-Match`/`If-None-Match`. + ## Partial PUT support as a standard There seems to be some movement in this space. RFC9110 mentions diff --git a/README.md b/README.md index 517ad43..a3eacfb 100644 --- a/README.md +++ b/README.md @@ -2,15 +2,13 @@ ## A FUSE filesystem for WEBDAV shares. -Most filesystem drivers for Webdav shares act somewhat like a mirror; -if a file is read it's first downloaded then cached in its entirety -on a local drive, then read from there. Writing files is similar or -even worse- a partial update to a file might involve downloading it first, -modifying it, then uploading it again. In many cases that is not optimal. +Most filesystem drivers for WebDAV shares mirror files locally and operate on +on-disk caches. Partial updates often involve downloading the full file, +modifying it, then uploading it again. -This filesystem driver behaves like a network filesystem. It doesn't -cache anything locally, it just sends out partial reads/writes over the -network. +webdavfs behaves like a network filesystem and supports efficient partial I/O +when the server allows it. It also has a robust RAM-backed mode for servers +without partial write support. For that to work, you need partial write support- and unfortunately, there is no standard for that. See @@ -22,13 +20,11 @@ used by e.g. NextCloud) for partial writes. So we detect if it's Apache or SabreDav we're talking to and then use their specific methods to partially update files. -If no support for partial writes is detected, mount.webdavfs will -print a warning and mount the filesystem read-only. In that case you can -also use the `rwdirops` mount option, this will make metadata writable -(i.e. you can use rm / mv / mkdir / rmdir) but you still won't be able -to write to files. - -But if you only need to read files it's still way faster than davfs2 :) +If no support for partial writes is detected, webdavfs still allows full +read–write operation by buffering file content in RAM per open handle, and +flushing the entire file with a single PUT on close (or fsync). There is also +an inactivity auto‑flush (default 10s) to push changes even if the application +keeps the file open. ## What is working @@ -38,6 +34,10 @@ Basic filesystem operations. - directories: mkdir rmdir readdir - query filesystem size (df / vfsstat) +Small files (by default ≤64 MiB) use a per‑open in‑RAM cache for reads/writes, +and upload on close/fsync/auto‑flush. Large files use ranged I/O when the +server supports partial updates. + ## What is not yet working - locking @@ -47,7 +47,9 @@ Basic filesystem operations. - change permissions (all files are 644, all dirs are 755) - change user/group - devices / fifos / chardev / blockdev etc -- truncate(2) / ftruncate(2) for lengths between 1 .. currentfilesize - 1 +- truncate(2) / ftruncate(2) shrinking a file while using ranged I/O. + Shrinking is supported for handles using the RAM cache (small files or + when the server lacks partial write support). This is basically because these are mostly just missing properties from webdav. @@ -96,6 +98,7 @@ Using it is simple as: ``` # mount -t webdavfs -ousername=you,password=pass https://webdav.where.ever/subdir /mnt ``` +On exit (Ctrl+C or SIGTERM), webdavfs unmounts the mountpoint automatically. ## Command line options @@ -133,6 +136,15 @@ Using it is simple as: | maxidleconns | Maximum number of idle connections (default 8) | sabredav_partialupdate | Use the sabredav partialupdate protocol even when | | the remote server doesn't advertise support (DANGEROUS) +| cache_threshold | Size in bytes above which files use non‑cached ranged +| | I/O (if available). Default 67108864 (64 MiB). +| cache_threshold_mb | Same as cache_threshold but specified in MiB. | + +Notes: +- If the server does not support partial writes, all files use the in‑RAM + per‑open cache regardless of size. +- The in‑RAM cache auto‑flushes after 10 seconds of inactivity per open handle, + or on fsync/close. If the webdavfs program is called via `mount -t webdavfs` or as `mount.webdav`, it will fork, re-exec and run in the background. In that case it will remove @@ -168,4 +180,3 @@ experience and better performance, here are a few ideas: - DELETE Depth 0 for collections (no delete if non-empty) - return updated PROPSTAT information after operations like PUT / DELETE / MKCOL / MOVE - diff --git a/cache.go b/cache.go index 388bdd4..d1b7f88 100644 --- a/cache.go +++ b/cache.go @@ -9,7 +9,6 @@ func (nd *Node) statInfoFresh() bool { return nd.LastStat.Add(statCacheTime).After(now) } -func (nd* Node) statInfoTouch() { +func (nd *Node) statInfoTouch() { nd.LastStat = time.Now() } - diff --git a/daemon.go b/daemon.go index 2877d11..7245b51 100644 --- a/daemon.go +++ b/daemon.go @@ -1,4 +1,3 @@ - package main import ( @@ -83,8 +82,8 @@ func Daemonize() error { // now re-exec ourselves. attrs := os.ProcAttr{ - Files: []*os.File{ devnull, wout, werr }, - Sys: &syscall.SysProcAttr{ Setsid: true }, + Files: []*os.File{devnull, wout, werr}, + Sys: &syscall.SysProcAttr{Setsid: true}, } os.Setenv(isDaemonEnv, "YES") proc, err := os.StartProcess(binary, os.Args, &attrs) @@ -126,7 +125,7 @@ func Daemonize() error { } // Returns true when this process is a daemonized child. -func IsDaemon() (bool) { +func IsDaemon() bool { return os.Getenv(isDaemonEnv) != "" } @@ -141,4 +140,3 @@ func Detach() error { fh.Close() return nil } - diff --git a/debug.go b/debug.go index 5a5738e..5d0ec99 100644 --- a/debug.go +++ b/debug.go @@ -1,4 +1,3 @@ - package main import ( @@ -8,7 +7,8 @@ import ( ) var dbgChan = make(chan string, 8) -func init () { + +func init() { go func() { for { line := <-dbgChan @@ -28,4 +28,3 @@ func dbgJson(obj interface{}) string { } return fmt.Sprintf("%+v", obj) } - diff --git a/fuse.go b/fuse.go index d1d57ce..ff54207 100644 --- a/fuse.go +++ b/fuse.go @@ -1,17 +1,16 @@ - package main import ( "os" "runtime" + "strconv" "strings" "syscall" - "strconv" "time" - "golang.org/x/net/context" "bazil.org/fuse" "bazil.org/fuse/fs" + "golang.org/x/net/context" ) const ( @@ -22,14 +21,16 @@ const ( ) type WebdavFS struct { - Uid uint32 - Gid uint32 - Mode uint32 - dirMode os.FileMode - fileMode os.FileMode - blockSize uint32 - root *Node + Uid uint32 + Gid uint32 + Mode uint32 + dirMode os.FileMode + fileMode os.FileMode + blockSize uint32 + root *Node + CacheThreshold uint64 } + var FS *WebdavFS var dav *DavClient @@ -70,13 +71,13 @@ func NewFS(d *DavClient, config WebdavFS) *WebdavFS { FS.fileMode = os.FileMode(FS.Mode &^ uint32(0111)) FS.dirMode = os.FileMode(FS.Mode) - if FS.dirMode & 0007 > 0 { + if FS.dirMode&0007 > 0 { FS.dirMode |= 0001 } - if FS.dirMode & 0070 > 0 { + if FS.dirMode&0070 > 0 { FS.dirMode |= 0010 } - if FS.dirMode & 0700 > 0 { + if FS.dirMode&0700 > 0 { FS.dirMode |= 0100 } FS.dirMode |= os.ModeDir @@ -100,13 +101,13 @@ func (fs *WebdavFS) Statfs(ctx context.Context, req *fuse.StatfsRequest, resp *f tPrintf("%d Statfs()", req.Header.ID) defer func() { if err != nil { - tPrintf("%d Statfs(): %v",req.Header.ID, err) + tPrintf("%d Statfs(): %v", req.Header.ID, err) } else { tPrintf("%d Statfs(): %v", req.Header.ID, resp) } }() } - wanted := []string{ "quota-available-bytes", "quota-used-bytes" } + wanted := []string{"quota-available-bytes", "quota-used-bytes"} props, err := dav.PropFind("/", 0, wanted) if err != nil { return @@ -121,7 +122,7 @@ func (fs *WebdavFS) Statfs(ctx context.Context, req *fuse.StatfsRequest, resp *f spaceFree, _ := strconv.ParseUint(props[0].SpaceFree, 10, 64) if spaceUsed > 0 || spaceFree > 0 { used := (spaceUsed + 4095) / 4096 - free = (spaceFree + 4095) / 4096 + free = (spaceFree + 4095) / 4096 if free > 0 { total = used + free } @@ -129,11 +130,11 @@ func (fs *WebdavFS) Statfs(ctx context.Context, req *fuse.StatfsRequest, resp *f } data := fuse.StatfsResponse{ - Blocks: total, - Bfree: free, - Bavail: free, - Bsize: 4096, - Frsize: 4096, + Blocks: total, + Bfree: free, + Bavail: free, + Bsize: 4096, + Frsize: 4096, Namelen: 255, } *resp = data @@ -159,7 +160,7 @@ func (nd *Node) Mkdir(ctx context.Context, req *fuse.MkdirRequest) (ret fs.Node, if err == nil { now := time.Now() nn := Dnode{ - Name: req.Name, + Name: req.Name, Mtime: now, Ctime: now, IsDir: true, @@ -241,6 +242,19 @@ func (nd *Node) Rename(ctx context.Context, req *fuse.RenameRequest, destDir fs. isDir = dnode.IsDir } else { isDir = node.IsDir + // If the source is a regular file with open mem-handles or not yet uploaded, perform a + // fast local rename and defer remote operations to handle flush/close. + if !isDir && (len(node.OpenMem) > 0 || !node.RemoteExists) { + // Drop any existing dest node locally to emulate overwrite semantics. + if req.OldName != req.NewName { + if destNode.getNode(req.NewName) != nil { + destNode.delNode(req.NewName) + } + nd.moveNode(destNode, req.OldName, req.NewName) + } + nd.Unlock() + return nil + } nd.Unlock() } @@ -277,6 +291,20 @@ func (nd *Node) Remove(ctx context.Context, req *fuse.RemoveRequest) (err error) } nd.incMetaRefThenLock(req.Header.ID) path := joinPath(nd.getPath(), req.Name) + // If the node exists only locally (not uploaded), remove locally and cancel pending uploads. + if n := nd.getNode(req.Name); n != nil && !n.RemoteExists { + // mark deleted and prevent memhandles from uploading + n.Deleted = true + for _, mh := range n.OpenMem { + mh.mu.Lock() + mh.dirty = false + mh.mu.Unlock() + } + nd.delNode(req.Name) + nd.decMetaRef() + nd.Unlock() + return nil + } nd.Unlock() props, err := dav.PropFindWithRedirect(path, 1, nil) if err == nil { @@ -377,18 +405,18 @@ func (nd *Node) Getattr(ctx context.Context, req *fuse.GetattrRequest, resp *fus mode = os.ModeSymlink | 0777 } resp.Attr = fuse.Attr{ - Valid: attrValidTime, - Inode: nd.Inode, - Size: nd.Size, - Blocks: (nd.Size + 511) / 512, - Atime: atime, - Mtime: mtime, - Ctime: ctime, - Crtime: ctime, - Mode: mode, - Nlink: 1, - Uid: FS.Uid, - Gid: FS.Gid, + Valid: attrValidTime, + Inode: nd.Inode, + Size: nd.Size, + Blocks: (nd.Size + 511) / 512, + Atime: atime, + Mtime: mtime, + Ctime: ctime, + Crtime: ctime, + Mode: mode, + Nlink: 1, + Uid: FS.Uid, + Gid: FS.Gid, BlockSize: FS.blockSize, } } @@ -427,6 +455,7 @@ func (nd *Node) Lookup(ctx context.Context, req *fuse.LookupRequest, resp *fuse. if err == nil { node := nd.addNode(dnode, true) + node.RemoteExists = true rn = node } nd.decIoRef() @@ -461,20 +490,21 @@ func (nd *Node) ReadDirAll(ctx context.Context) (dd []fuse.Dirent, err error) { ino := nd.Inode if d.Name != "" && d.Name != "." { nn := nd.addNode(d, false) + nn.RemoteExists = true ino = nn.Inode } tp := fuse.DT_File - if (d.IsDir) { - tp =fuse.DT_Dir + if d.IsDir { + tp = fuse.DT_Dir } - if (d.IsLink) { - tp =fuse.DT_Link + if d.IsLink { + tp = fuse.DT_Link } dd = append(dd, fuse.Dirent{ - Name: d.Name, + Name: d.Name, Inode: ino, - Type: tp, + Type: tp, }) seen[d.Name] = true @@ -492,9 +522,9 @@ func (nd *Node) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse. path := nd.getPath() nd.Unlock() trunc := flagSet(req.Flags, fuse.OpenTruncate) - read := req.Flags.IsReadWrite() || req.Flags.IsReadOnly() + read := req.Flags.IsReadWrite() || req.Flags.IsReadOnly() write := req.Flags.IsReadWrite() || req.Flags.IsWriteOnly() - excl := flagSet(req.Flags, fuse.OpenExclusive) + excl := flagSet(req.Flags, fuse.OpenExclusive) if trace(T_FUSE) { tPrintf("%d Create(%s): trunc=%v read=%v write=%v excl=%v", req.Header.ID, req.Name, trunc, read, write, excl) @@ -507,29 +537,20 @@ func (nd *Node) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse. }() } path = joinPath(path, req.Name) - created := false - if trunc { - // A simple put with no body creates and truncates the - // file if it's not there. - created, err = dav.Put(path, []byte{}, true, excl) - } else { - // A Put-Range at offset 0 with an empty body - // creates the file if not present, but doesn't - // truncate it. - created, err = dav.PutRange(path, []byte{}, 0, true, excl) - } - if err == nil && excl && !created { - err = fuse.EEXIST + // Enforce exclusivity: if the target exists, fail. + if excl { + if _, statErr := dav.Stat(path); statErr == nil { + err = fuse.EEXIST + } } if err == nil { - dnode, err := dav.Stat(path) - if err == nil { - n := nd.addNode(dnode, true) - node = n - handle = n - } else { - nd.invalidateNode(req.Name) - } + now := time.Now() + dn := Dnode{Name: req.Name, IsDir: false, Size: 0, Mtime: now, Ctime: now} + n := nd.addNode(dn, true) + node = n + // In-memory buffer starts empty (trunc semantics). Content will be uploaded on flush/close. + mh := newMemHandle(n, []byte{}, req.Flags.IsReadWrite() || req.Flags.IsWriteOnly()) + handle = mh } nd.Lock() nd.decMetaRef() @@ -537,7 +558,6 @@ func (nd *Node) Create(ctx context.Context, req *fuse.CreateRequest, resp *fuse. return } - func (nd *Node) Forget() { if trace(T_FUSE) { tPrintf("Forget(%s)", nd.Name) @@ -552,19 +572,51 @@ func (nd *Node) Forget() { func (nd *Node) ftruncate(ctx context.Context, size uint64, id fuse.RequestID) (err error) { nd.incMetaRefThenLock(id) path := nd.getPath() + // If there are open in-memory handles, resize their buffers and defer PUT to close/flush + if len(nd.OpenMem) > 0 { + for _, mh := range nd.OpenMem { + mh.resize(size) + } + nd.Size = size + nd.decMetaRef() + nd.Unlock() + return nil + } nd.Unlock() - if size == 0 { - if nd.Size > 0 { - _, err = dav.Put(path, []byte{}, false, false) + if !dav.CanPutRange() { + // Fallback: fetch, resize in-memory, and PUT full file. + var data []byte + if size == 0 { + data = []byte{} + } else { + data, err = dav.Get(path) + if err == nil { + if uint64(len(data)) < size { + nb := make([]byte, size) + copy(nb, data) + data = nb + } else { + data = data[:size] + } + } + } + if err == nil { + _, err = dav.Put(path, data, false, false) + } + } else { + if size == 0 { + if nd.Size > 0 { + _, err = dav.Put(path, []byte{}, false, false) + } + } else if size > nd.Size { + _, err = dav.PutRange(path, []byte{0}, int64(size-1), false, false) + } else if size != nd.Size { + err = fuse.ERANGE } - } else if size > nd.Size { - _, err = dav.PutRange(path, []byte{0}, int64(size - 1), false, false) - } else if size != nd.Size { - err = fuse.ERANGE } nd.Lock() if err == nil { - nd.Size= size + nd.Size = size } nd.decMetaRef() nd.Unlock() @@ -586,7 +638,7 @@ func (nd *Node) Setattr(ctx context.Context, req *fuse.SetattrRequest, resp *fus err = fuse.Errno(syscall.ESTALE) return } - invalid := fuse.SetattrMode | fuse. SetattrUid | fuse.SetattrGid | + invalid := fuse.SetattrMode | fuse.SetattrUid | fuse.SetattrGid | fuse.SetattrBkuptime | fuse.SetattrCrtime | fuse.SetattrChgtime | fuse.SetattrFlags | fuse.SetattrHandle v := req.Valid @@ -611,8 +663,8 @@ func (nd *Node) Setattr(ctx context.Context, req *fuse.SetattrRequest, resp *fus // fake setting mtime if it is roughly unchanged. if attrSet(v, fuse.SetattrMtime) { if nd.LastStat.Add(time.Second).Before(time.Now()) || - req.Mtime.Before(nd.Mtime.Add(-500 * time.Millisecond)) || - req.Mtime.After(nd.Mtime.Add(500 * time.Millisecond)) { + req.Mtime.Before(nd.Mtime.Add(-500*time.Millisecond)) || + req.Mtime.After(nd.Mtime.Add(500*time.Millisecond)) { return fuse.EPERM } } @@ -639,18 +691,18 @@ func (nd *Node) Setattr(ctx context.Context, req *fuse.SetattrRequest, resp *fus atime = mtime } attr := fuse.Attr{ - Valid: attrValidTime, - Inode: nd.Inode, - Size: nd.Size, - Blocks: nd.Size / 512, - Atime: atime, - Mtime: mtime, - Ctime: ctime, - Crtime: ctime, - Mode: mode, - Nlink: 1, - Uid: FS.Uid, - Gid: FS.Gid, + Valid: attrValidTime, + Inode: nd.Inode, + Size: nd.Size, + Blocks: nd.Size / 512, + Atime: atime, + Mtime: mtime, + Ctime: ctime, + Crtime: ctime, + Mode: mode, + Nlink: 1, + Uid: FS.Uid, + Gid: FS.Gid, BlockSize: 4096, } resp.Attr = attr @@ -752,7 +804,7 @@ func (nf *Node) Write(ctx context.Context, req *fuse.WriteRequest, resp *fuse.Wr func (nf *Node) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.OpenResponse) (handle fs.Handle, err error) { trunc := flagSet(req.Flags, fuse.OpenTruncate) - read := req.Flags.IsReadWrite() || req.Flags.IsReadOnly() + read := req.Flags.IsReadWrite() || req.Flags.IsReadOnly() write := req.Flags.IsReadWrite() || req.Flags.IsWriteOnly() if trace(T_FUSE) { @@ -765,7 +817,8 @@ func (nf *Node) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.Open } }() } - if nf.IsDir { + // Treat root or directory nodes as directories. + if nf.IsDir || nf.Parent == nil { handle = nf return } @@ -773,31 +826,47 @@ func (nf *Node) Open(ctx context.Context, req *fuse.OpenRequest, resp *fuse.Open nf.incIoRef(req.Header.ID) path := nf.getPath() - // See if kernel cache is still valid. - dnode, err := dav.Stat(path) - if err == nil { - nf.Lock() - nf.Dnode = dnode - nf.statInfoTouch() - if dnode.Size == nf.Size && dnode.Mtime == nf.Mtime { - resp.Flags = fuse.OpenKeepCache - } - nf.Unlock() - - // This is actually not called, truncating is - // done by calling Setattr with 0 size. - if trunc { - _, err = dav.Put(path, []byte{}, false, false) - if err == nil { - nf.Size = 0 + // Decide whether to use RAM cache or ranged I/O based on file size and threshold + useMem := true + if dav.CanPutRange() && !trunc && FS.CacheThreshold > 0 { + if dnode, stErr := dav.Stat(path); stErr == nil { + nf.Lock() + nf.Dnode = dnode + nf.Size = dnode.Size + nf.RemoteExists = true + nf.statInfoTouch() + nf.Unlock() + if uint64(dnode.Size) > FS.CacheThreshold { + useMem = false } } } - nf.decIoRef() - if err == nil { + if !useMem { + // Non-cached handle using ranged I/O + nf.decIoRef() handle = nf + return + } + + // Use RAM buffer: load full file content + var data []byte + if trunc { + data = []byte{} + } else { + var gerr error + data, gerr = dav.Get(path) + if gerr != nil { + // If not found or other error, start with empty buffer; it will be created on flush by writer. + data = []byte{} + } else { + nf.Lock() + nf.RemoteExists = true + nf.Unlock() + } } + nf.decIoRef() + mh := newMemHandle(nf, data, req.Flags.IsReadWrite() || req.Flags.IsWriteOnly()) + handle = mh return } - diff --git a/main.go b/main.go index ad5547b..5c807fc 100644 --- a/main.go +++ b/main.go @@ -1,29 +1,31 @@ - package main import ( + "bazil.org/fuse" + "bazil.org/fuse/fs" "fmt" + "github.com/pborman/getopt/v2" "os" + "os/signal" "path" "strings" - "bazil.org/fuse" - "bazil.org/fuse/fs" - "github.com/pborman/getopt/v2" + "syscall" ) const VERSION = "1.0" type Opts struct { - Type string - TraceOpts string - TraceFile string - Daemonize bool - Fake bool - NoMtab bool - Sloppy bool - Verbose bool - RawOptions string + Type string + TraceOpts string + TraceFile string + Daemonize bool + Fake bool + NoMtab bool + Sloppy bool + Verbose bool + RawOptions string } + var opts = Opts{} var mountOpts MountOptions var progname = path.Base(os.Args[0]) @@ -53,7 +55,7 @@ func fatal(err string) { // rebuild the os.Args array, string username/password. func rebuildOptions(url, path string) { - args := []string{ os.Args[0], url, path } + args := []string{os.Args[0], url, path} bools := "" if opts.NoMtab { bools += "n" @@ -68,16 +70,16 @@ func rebuildOptions(url, path string) { bools += "v" } if bools != "" { - args = append(args, "-" + bools) + args = append(args, "-"+bools) } if opts.Type != "" { - args = append(args, "-t" + opts.Type) + args = append(args, "-t"+opts.Type) } if opts.TraceOpts != "" { - args = append(args, "-T" + opts.TraceOpts) + args = append(args, "-T"+opts.TraceOpts) } if opts.TraceFile != "" { - args = append(args, "-F" + opts.TraceFile) + args = append(args, "-F"+opts.TraceFile) } stropts := []string{} for _, o := range strings.Split(opts.RawOptions, ",") { @@ -95,7 +97,7 @@ func rebuildOptions(url, path string) { } } if len(stropts) > 0 { - args = append(args, "-o" + strings.Join(stropts, ",")) + args = append(args, "-o"+strings.Join(stropts, ",")) } os.Args = args } @@ -108,7 +110,7 @@ func main() { var err error for fd < 3 { file, err = os.OpenFile("/dev/null", os.O_RDWR, 0666) - if err != nil { + if err != nil { fatal(err.Error()) } fd = int(file.Fd()) @@ -133,7 +135,7 @@ func main() { // put non-option arguments last. l := len(os.Args) if l > 2 && !strings.HasPrefix(os.Args[1], "-") && - !strings.HasPrefix(os.Args[2], "-") { + !strings.HasPrefix(os.Args[2], "-") { // os.Args = append([]string{}, os.Args[:1]..., os.Args[3:]..., os.Args[1:3]...) args := []string{} args = append(args, os.Args[0]) @@ -152,13 +154,13 @@ func main() { usage(nil, 0) } if version { - fmt.Printf("webdavfs %s\n", VERSION); + fmt.Printf("webdavfs %s\n", VERSION) os.Exit(0) } // check that we have two non-option args at the end if l < 3 || strings.HasPrefix(os.Args[l-2], "-") || - strings.HasPrefix(os.Args[l-1], "-") { + strings.HasPrefix(os.Args[l-1], "-") { usage(nil, 1) } @@ -197,6 +199,12 @@ func main() { } config.Mode = mountOpts.Mode + // Default cache threshold: 64 MiB if not configured + if mountOpts.CacheThreshold == 0 { + mountOpts.CacheThreshold = 64 * 1024 * 1024 + } + config.CacheThreshold = mountOpts.CacheThreshold + // if running from fstab with "uid=123,gid=456" set some reasonable // defaults so that that uid can actually access the files. if os.Getuid() == 0 && mountOpts.Uid != 0 && mountOpts.Mode == 0 { @@ -246,7 +254,7 @@ func main() { username := os.Getenv("WEBDAV_USERNAME") password := os.Getenv("WEBDAV_PASSWORD") - cookie := os.Getenv("WEBDAV_COOKIE") + cookie := os.Getenv("WEBDAV_COOKIE") if mountOpts.Username != "" { username = mountOpts.Username } @@ -266,22 +274,24 @@ func main() { } dav := &DavClient{ - Url: url, - MaxConns: int(mountOpts.MaxConns), + Url: url, + MaxConns: int(mountOpts.MaxConns), MaxIdleConns: int(mountOpts.MaxIdleConns), - Username: username, - Password: password, - Cookie: cookie, - PutDisabled: mountOpts.ReadWriteDirOps, - IsSabre: mountOpts.SabreDavPartialUpdate, + Username: username, + Password: password, + Cookie: cookie, + PutDisabled: mountOpts.ReadWriteDirOps, + IsSabre: mountOpts.SabreDavPartialUpdate, } err = dav.Mount() if err != nil { fatal(err.Error()) } + // Allow mounting even without PUT Range support. + // When range writes are unavailable, the filesystem will buffer + // per-open writes in RAM and flush with a full PUT on close. if !dav.CanPutRange() && !mountOpts.ReadOnly && !mountOpts.ReadWriteDirOps { - fmt.Fprintf(os.Stderr, "%s: no PUT Range support, mounting read-only\n", url) - mountOpts.ReadOnly = true + fmt.Fprintf(os.Stderr, "%s: no PUT Range support, using in-RAM buffering with full PUT on close\n", url) } if opts.Fake { return @@ -320,7 +330,33 @@ func main() { if err != nil { fatal(err.Error()) } - defer c.Close() + // Ensure we always attempt to unmount and close the FUSE session on exit + defer func() { + // Try graceful unmount + _ = fuse.Unmount(mountpoint) + // Close the connection regardless + _ = c.Close() + // As a last resort on Linux, try a lazy unmount + syscall.Unmount(mountpoint, syscall.MNT_DETACH) + }() + + // Unmount on exit signals for clean shutdown + sigc := make(chan os.Signal, 1) + signal.Notify(sigc, os.Interrupt, syscall.SIGTERM, syscall.SIGHUP) + go func() { + s := <-sigc + fmt.Fprintf(os.Stderr, "received %s, unmounting %s\n", s, mountpoint) + // Try graceful unmount first + if err := fuse.Unmount(mountpoint); err != nil { + fmt.Fprintf(os.Stderr, "unmount error: %v\n", err) + // Fallback to lazy unmount on Linux if needed + _ = syscall.Unmount(mountpoint, syscall.MNT_DETACH) + } + // Close the FUSE connection to stop fs.Serve + _ = c.Close() + // Exit to ensure process terminates promptly + os.Exit(0) + }() if IsDaemon() { Detach() @@ -338,4 +374,3 @@ func main() { fatal(err.Error()) } } - diff --git a/memhandle.go b/memhandle.go new file mode 100644 index 0000000..6aa3bf5 --- /dev/null +++ b/memhandle.go @@ -0,0 +1,261 @@ +package main + +import ( + "sync" + "time" + + "bazil.org/fuse" + "golang.org/x/net/context" +) + +// MemHandle provides a per-open-file in-RAM buffer for servers +// without PUT Range support. It loads the full file on open and +// flushes back with a single PUT on close if modified. +type MemHandle struct { + n *Node + buf []byte + dirty bool + writable bool + mu sync.Mutex + timer *time.Timer + closed bool + flushing bool +} + +func newMemHandle(n *Node, initial []byte, writable bool) *MemHandle { + mh := &MemHandle{n: n, buf: initial, dirty: false, writable: writable} + // Update node size from buffer for consistency. + n.Lock() + n.Size = uint64(len(mh.buf)) + n.OpenMem = append(n.OpenMem, mh) + n.Unlock() + return mh +} + +// Read reads from the in-memory buffer. +func (h *MemHandle) Read(ctx context.Context, req *fuse.ReadRequest, resp *fuse.ReadResponse) error { + h.mu.Lock() + defer h.mu.Unlock() + h.n.incIoRef(req.Header.ID) + defer h.n.decIoRef() + + off := req.Offset + if off >= int64(len(h.buf)) { + resp.Data = []byte{} + return nil + } + // compute slice bounds + end := off + int64(req.Size) + if end > int64(len(h.buf)) { + end = int64(len(h.buf)) + } + resp.Data = h.buf[off:end] + return nil +} + +// Write writes into the in-memory buffer and marks it dirty. +func (h *MemHandle) Write(ctx context.Context, req *fuse.WriteRequest, resp *fuse.WriteResponse) error { + if !h.writable { + return fuse.EPERM + } + h.mu.Lock() + defer h.mu.Unlock() + h.n.incIoRef(req.Header.ID) + defer h.n.decIoRef() + + // Ensure buffer is large enough + need := int(req.Offset) + len(req.Data) + if need > len(h.buf) { + // grow and zero-fill new region + nb := make([]byte, need) + copy(nb, h.buf) + h.buf = nb + } + copy(h.buf[int(req.Offset):int(req.Offset)+len(req.Data)], req.Data) + h.dirty = true + h.scheduleFlushLocked() + resp.Size = len(req.Data) + + // Update node size + h.n.Lock() + if uint64(need) > h.n.Size { + h.n.Size = uint64(need) + } + h.n.Unlock() + return nil +} + +// Release flushes the buffer back to the server on close if dirty. +func (h *MemHandle) Release(ctx context.Context, req *fuse.ReleaseRequest) error { + h.mu.Lock() + h.closed = true + if h.timer != nil { + h.timer.Stop() + h.timer = nil + } + defer h.mu.Unlock() + // If not dirty, nothing to do. + if !h.dirty || h.n.Deleted { + return nil + } + + h.n.incIoRef(req.Header.ID) + path := h.n.getPath() + // send full PUT with entire buffer + // If the file does not exist remotely, create it on first upload. + _, err := dav.Put(path, h.buf, !h.n.RemoteExists, false) + h.n.decIoRef() + if err != nil { + return err + } + + // Refresh node metadata afterwards (best effort) + dnode, err2 := dav.Stat(path) + h.n.Lock() + if err2 == nil { + h.n.Dnode = dnode + h.n.Mtime = dnode.Mtime + h.n.Ctime = dnode.Ctime + h.n.Size = dnode.Size + h.n.RemoteExists = true + } else { + // at least touch mtime + h.n.Mtime = time.Now() + } + // remove from open list + for i, mh := range h.n.OpenMem { + if mh == h { + h.n.OpenMem = append(h.n.OpenMem[:i], h.n.OpenMem[i+1:]...) + break + } + } + h.n.Unlock() + return nil +} + +// Flush pushes data like Release but keeps the handle open. +func (h *MemHandle) Flush(ctx context.Context, req *fuse.FlushRequest) error { + h.mu.Lock() + if h.timer != nil { + h.timer.Stop() + } + defer h.mu.Unlock() + if !h.dirty { + return nil + } + h.n.incIoRef(req.Header.ID) + path := h.n.getPath() + _, err := dav.Put(path, h.buf, !h.n.RemoteExists, false) + h.n.decIoRef() + if err != nil { + return err + } + h.dirty = false + h.n.Lock() + h.n.RemoteExists = true + h.n.Unlock() + return nil +} + +// Fsync also flushes to remote. +func (h *MemHandle) Fsync(ctx context.Context, req *fuse.FsyncRequest) error { + h.mu.Lock() + if h.timer != nil { + h.timer.Stop() + } + defer h.mu.Unlock() + if !h.dirty { + return nil + } + h.n.incIoRef(req.Header.ID) + path := h.n.getPath() + _, err := dav.Put(path, h.buf, !h.n.RemoteExists, false) + h.n.decIoRef() + if err != nil { + return err + } + h.dirty = false + h.n.Lock() + h.n.RemoteExists = true + h.n.Unlock() + return nil +} + +// resize adjusts the in-memory buffer size. +func (h *MemHandle) resize(newSize uint64) { + h.mu.Lock() + defer h.mu.Unlock() + cur := uint64(len(h.buf)) + if newSize == cur { + return + } + if newSize < cur { + h.buf = h.buf[:newSize] + } else { + nb := make([]byte, newSize) + copy(nb, h.buf) + h.buf = nb + } + h.dirty = true + h.scheduleFlushLocked() +} + +// scheduleFlushLocked (re)arms the inactivity timer. Caller must hold h.mu. +func (h *MemHandle) scheduleFlushLocked() { + if h.closed { + return + } + if h.timer == nil { + h.timer = time.AfterFunc(10*time.Second, func() { + h.inactivityFlush() + }) + } else { + h.timer.Reset(10 * time.Second) + } +} + +// inactivityFlush performs a flush after the inactivity window if needed. +func (h *MemHandle) inactivityFlush() { + h.mu.Lock() + if h.closed || !h.dirty || h.flushing { + h.mu.Unlock() + return + } + h.flushing = true + data := make([]byte, len(h.buf)) + copy(data, h.buf) + create := !h.n.RemoteExists + // optimistic clear; if new writes happen, dirty will be set again. + h.dirty = false + h.mu.Unlock() + + path := h.n.getPath() + // background flush; no specific RequestID + _, err := dav.Put(path, data, create, false) + + h.mu.Lock() + h.flushing = false + if err != nil { + // mark dirty again to retry later + h.dirty = true + // leave timer nil so a future write will rearm; or rearm now + h.scheduleFlushLocked() + h.mu.Unlock() + return + } + // success: mark remote existence + h.n.Lock() + h.n.RemoteExists = true + h.n.Unlock() + h.mu.Unlock() +} + +// ReadAll allows cat-like reads to fetch the full buffer efficiently. +func (h *MemHandle) ReadAll(ctx context.Context) ([]byte, error) { + // Provide a copy to avoid exposing internal buffer to mutation + h.mu.Lock() + defer h.mu.Unlock() + cp := make([]byte, len(h.buf)) + copy(cp, h.buf) + return cp, nil +} diff --git a/mountoptions.go b/mountoptions.go index 7c51dbb..8532369 100644 --- a/mountoptions.go +++ b/mountoptions.go @@ -1,4 +1,3 @@ - package main import ( @@ -8,28 +7,29 @@ import ( ) type MountOptions struct { - AllowRoot bool - AllowOther bool - DefaultPermissions bool - NoDefaultPermissions bool - ReadOnly bool - ReadWrite bool - ReadWriteDirOps bool - Uid uint32 - Gid uint32 - Mode uint32 - Cookie string - Password string - Username string - AsyncRead bool - NonEmpty bool - MaxConns uint32 - MaxIdleConns uint32 - SabreDavPartialUpdate bool + AllowRoot bool + AllowOther bool + DefaultPermissions bool + NoDefaultPermissions bool + ReadOnly bool + ReadWrite bool + ReadWriteDirOps bool + Uid uint32 + Gid uint32 + Mode uint32 + Cookie string + Password string + Username string + AsyncRead bool + NonEmpty bool + MaxConns uint32 + MaxIdleConns uint32 + SabreDavPartialUpdate bool + CacheThreshold uint64 // bytes; 0 => default } func parseUInt32(v string, base int, name string, loc *uint32) (err error) { - n, err := strconv.ParseUint(v , base, 32) + n, err := strconv.ParseUint(v, base, 32) if err == nil { *loc = uint32(n) } @@ -84,6 +84,23 @@ func parseMountOptions(n string, sloppy bool) (mo MountOptions, err error) { err = parseUInt32(v, 10, "maxidleconns", &mo.MaxIdleConns) case "sabredav_partialupdate": mo.SabreDavPartialUpdate = true + case "cache_threshold": + // bytes + var v64 uint64 + v64, err = strconv.ParseUint(v, 10, 64) + if err == nil { + mo.CacheThreshold = v64 + } + case "cache_threshold_mb": + // megabytes + var v64 uint64 + v64u32, err2 := strconv.ParseUint(v, 10, 32) + if err2 == nil { + v64 = uint64(v64u32) * 1024 * 1024 + mo.CacheThreshold = v64 + } else { + err = err2 + } default: if !sloppy { err = errors.New(a[0] + ": unknown option") diff --git a/node.go b/node.go index f5e770b..348911b 100644 --- a/node.go +++ b/node.go @@ -1,7 +1,8 @@ - package main import ( + "bazil.org/fuse" + "bazil.org/fuse/fs" "os" "runtime/debug" "runtime/pprof" @@ -9,8 +10,6 @@ import ( "sync" "syscall" "time" - "bazil.org/fuse" - "bazil.org/fuse/fs" ) const ( @@ -20,21 +19,23 @@ const ( type Node struct { Dnode - Atime time.Time - LastStat time.Time - DirCache map[string]Dnode - DirCacheTime time.Time - Inode uint64 - RefCount [2]int - Deleted bool - Parent *Node - Child map[string]*Node - InUse bool + Atime time.Time + LastStat time.Time + DirCache map[string]Dnode + DirCacheTime time.Time + Inode uint64 + RefCount [2]int + Deleted bool + Parent *Node + Child map[string]*Node + InUse bool + OpenMem []*MemHandle + RemoteExists bool } var rootNode = &Node{ - Inode: 1, - Child: make(map[string]*Node), + Inode: 1, + Child: make(map[string]*Node), } var EBUSY = fuse.Errno(syscall.EBUSY) @@ -47,7 +48,7 @@ func (nd *Node) Lock() { if trace(T_LOCK) { name := nd.Name stack := debug.Stack() - lockTimer = time.AfterFunc(2 * time.Second, func() { + lockTimer = time.AfterFunc(2*time.Second, func() { tPrintf("LOCKERR (%s) Lock held longer than 2 seconds:\n%s", name, stack) tPrintf("== dump of all goroutines:") @@ -87,11 +88,11 @@ func (nd *Node) addNode(d Dnode, really bool) *Node { n.Dnode = d return n } - nn := &Node { - Inode: fs.GenerateDynamicInode(nd.Inode, d.Name), - Dnode: d, - Parent: nd, - InUse: really, + nn := &Node{ + Inode: fs.GenerateDynamicInode(nd.Inode, d.Name), + Dnode: d, + Parent: nd, + InUse: really, LastStat: time.Now(), } if d.IsDir { @@ -173,7 +174,7 @@ func lookupNode(path string) (de *Node) { d := rootNode if path != "/" { pelem := strings.Split(path[1:], "/") - for _, n := range(pelem) { + for _, n := range pelem { if d.Child == nil || d.Child[n] == nil { return } @@ -193,7 +194,7 @@ func (de *Node) getPath() string { a = append(a, d.Name) } // reverse - for i, j := 0, len(a) - 1; i < j; i, j = i + 1, j - 1 { + for i, j := 0, len(a)-1; i < j; i, j = i+1, j-1 { a[i], a[j] = a[j], a[i] } path := "/" + strings.Join(a, "/") @@ -305,4 +306,3 @@ func (de *Node) decMetaRef() { de.RefCount[RefMeta]-- // dbgPrintf("node: decMetaRef %s@%p: ref now %d\n", de.Name, de, de.RefCount[RefMeta]) } - diff --git a/osdep_default.go b/osdep_default.go index dc8e60e..d0b4ded 100644 --- a/osdep_default.go +++ b/osdep_default.go @@ -1,12 +1,12 @@ +//go:build !linux // +build !linux package main import ( - "syscall" + "syscall" ) func Dup2(oldfd int, newfd int) (err error) { return syscall.Dup2(oldfd, newfd) } - diff --git a/osdep_linux.go b/osdep_linux.go index ad1862f..85971d2 100644 --- a/osdep_linux.go +++ b/osdep_linux.go @@ -1,10 +1,9 @@ package main import ( - "syscall" + "syscall" ) func Dup2(oldfd int, newfd int) (err error) { return syscall.Dup3(oldfd, newfd, 0) } - diff --git a/trace.go b/trace.go index 3eecf91..ee7007b 100644 --- a/trace.go +++ b/trace.go @@ -1,12 +1,11 @@ - package main import ( - "errors" "encoding/json" + "errors" "fmt" - "os" "net/http" + "os" "runtime" "sort" "strings" @@ -15,7 +14,7 @@ import ( ) const ( - T_WEBDAV = 1 << iota + T_WEBDAV = 1 << iota T_HTTP_REQUEST T_HTTP_HEADERS T_FUSE @@ -28,7 +27,7 @@ var traceFile *os.File var traceChan = make(chan string, 8) -func startLogger (file *os.File, fileName string) { +func startLogger(file *os.File, fileName string) { go func() { for { line := <-traceChan @@ -76,7 +75,7 @@ func tPrintf(format string, args ...interface{}) { lines := strings.Split(s, "\n") l2 := []string{} for _, l := range lines { - l2 = append(l2, t + l + "\n") + l2 = append(l2, t+l+"\n") } s = strings.Join(l2, "") } @@ -99,7 +98,7 @@ func tHeaders(hdrs http.Header, prefix string) string { } sort.Strings(h) for _, m := range h { - r = append(r, prefix + m + ": " + strings.Join(hdrs[m], "\n") + "\n") + r = append(r, prefix+m+": "+strings.Join(hdrs[m], "\n")+"\n") } return strings.Join(r, "") } @@ -136,12 +135,12 @@ func traceredirectStdoutErr() { Dup2(int(traceFile.Fd()), 2) } -func traceOpts(opt string, fn string) (err error) { +func traceOpts(opt string, fn string) (err error) { if opt == "" { return } opts := strings.Split(opt, ",") - for _, o := range(opts) { + for _, o := range opts { switch o { case "webdav": traceOptions |= T_WEBDAV @@ -170,4 +169,3 @@ func traceOpts(opt string, fn string) (err error) { } return } - diff --git a/webdav.go b/webdav.go index fdb0f72..25e4578 100644 --- a/webdav.go +++ b/webdav.go @@ -1,10 +1,11 @@ -package main; +package main import ( - "fmt" + "bazil.org/fuse" "bytes" "encoding/xml" "errors" + "fmt" "io" "io/ioutil" "net/http" @@ -14,80 +15,79 @@ import ( "strings" "syscall" "time" - "bazil.org/fuse" ) -type davEmpty struct {} +type davEmpty struct{} type davSem chan davEmpty type DavClient struct { - Url string - Username string - Password string - Cookie string - Methods map[string]bool - DavSupport map[string]bool - IsSabre bool - IsApache bool - PutDisabled bool - MaxConns int - MaxIdleConns int - base string - cc *http.Client - davSem davSem + Url string + Username string + Password string + Cookie string + Methods map[string]bool + DavSupport map[string]bool + IsSabre bool + IsApache bool + PutDisabled bool + MaxConns int + MaxIdleConns int + base string + cc *http.Client + davSem davSem } type DavError struct { - Code int - Message string - Location string - Errnum syscall.Errno + Code int + Message string + Location string + Errnum syscall.Errno } type Dnode struct { - Name string - Target string - IsDir bool - IsLink bool - Mtime time.Time - Ctime time.Time - Size uint64 + Name string + Target string + IsDir bool + IsLink bool + Mtime time.Time + Ctime time.Time + Size uint64 } type Props struct { - Name string `xml:"-"` - ResourceType_ ResourceType `xml:"resourcetype"` - RefTarget_ RefTarget `xml:"reftarget"` - ResourceType string `xml:"-"` - RefTarget string `xml:"-"` - CreationDate string `xml:"creationdate"` - LastModified string `xml:"getlastmodified"` - Etag string `xml:"getetag"` - ContentLength string `xml:"getcontentlength"` - SpaceUsed string `xml:"quota-used-bytes"` - SpaceFree string `xml:"quota-available-bytes"` + Name string `xml:"-"` + ResourceType_ ResourceType `xml:"resourcetype"` + RefTarget_ RefTarget `xml:"reftarget"` + ResourceType string `xml:"-"` + RefTarget string `xml:"-"` + CreationDate string `xml:"creationdate"` + LastModified string `xml:"getlastmodified"` + Etag string `xml:"getetag"` + ContentLength string `xml:"getcontentlength"` + SpaceUsed string `xml:"quota-used-bytes"` + SpaceFree string `xml:"quota-available-bytes"` } type ResourceType struct { - Collection *struct{} `xml:"collection"` - RedirectRef *struct{} `xml:"redirectref"` + Collection *struct{} `xml:"collection"` + RedirectRef *struct{} `xml:"redirectref"` } type RefTarget struct { - Href *string `xml:"href"` + Href *string `xml:"href"` } type Propstat struct { - Props *Props `xml:"prop"` + Props *Props `xml:"prop"` } type Response struct { - Href string `xml:"href"` - Propstat *Propstat `xml:"propstat"` + Href string `xml:"href"` + Propstat *Propstat `xml:"propstat"` } type MultiStatus struct { - Responses []Response `xml:"response"` + Responses []Response `xml:"response"` } var mostProps = "" @@ -95,13 +95,13 @@ var mostProps = " 1 && s[0] == '"' && s [l-1] == '"' { - return s[1:l-1] + if l > 1 && s[0] == '"' && s[l-1] == '"' { + return s[1 : l-1] } return s } @@ -162,7 +162,7 @@ func dirName(s string) string { return "/" } -func parseTime (s string) (t time.Time) { +func parseTime(s string) (t time.Time) { if len(s) > 0 && s[0] >= '0' && s[0] <= '9' { t, _ = time.Parse(davTimeFormat, s) } else { @@ -173,7 +173,7 @@ func parseTime (s string) (t time.Time) { func joinPath(s1, s2 string) string { if (len(s1) > 0 && s1[len(s1)-1] == '/') || - (len(s2) > 0 && s2[0] == '/') { + (len(s2) > 0 && s2[0] == '/') { return s1 + s2 } return s1 + "/" + s2 @@ -189,7 +189,7 @@ func stripHrefPrefix(href string, prefix string) (string, bool) { name = name[len(prefix):] } i := strings.Index(name, "/") - if i >= 0 && i < len(name) - 1 { + if i >= 0 && i < len(name)-1 { return "", false } return name, true @@ -266,12 +266,12 @@ func (d *DavClient) buildRequest(method string, path string, b ...interface{}) ( blen = -1 } } - u := url.URL{ Path: path } - req, err = http.NewRequest(method, d.Url + u.EscapedPath(), body) + u := url.URL{Path: path} + req, err = http.NewRequest(method, d.Url+u.EscapedPath(), body) if err != nil { return } - if (blen >= 0) { + if blen >= 0 { if blen == 0 { // Need this to FORCE the http client to send a // Content-Length header for size 0. @@ -319,8 +319,8 @@ func (d *DavClient) do(req *http.Request) (resp *http.Response, err error) { resp, err = d.cc.Do(req) if err == nil && !statusIsValid(resp) { err = davToErrno(&DavError{ - Message: resp.Status, - Code: resp.StatusCode, + Message: resp.Status, + Code: resp.StatusCode, Location: resp.Header.Get("Location"), }) } @@ -346,7 +346,7 @@ func (d *DavClient) Mount() (err error) { tr.DisableCompression = true d.cc = &http.Client{ - Timeout: 60 * time.Second, + Timeout: 60 * time.Second, Transport: &tr, CheckRedirect: func(req *http.Request, via []*http.Request) error { return errors.New("400 Will not follow redirect") @@ -428,7 +428,7 @@ func (d *DavClient) PropFind(path string, depth int, props []string) (ret []*Pro } else { a = append(a, "") for _, s := range props { - a = append(a, "") + a = append(a, "") } a = append(a, "") } @@ -519,7 +519,7 @@ func (d *DavClient) PropFindWithRedirect(path string, depth int, props []string) // did we get a redirect? if daverr, ok := err.(*DavError); ok { - if daverr.Code / 100 != 3 || daverr.Location == "" { + if daverr.Code/100 != 3 || daverr.Location == "" { return } url, err2 := url.ParseRequestURI(daverr.Location) @@ -527,8 +527,8 @@ func (d *DavClient) PropFindWithRedirect(path string, depth int, props []string) return } // if it's just a "this is a directory" redirect, retry. - if url.Path == d.base + path + "/" { - ret, err = d.PropFind(path + "/", depth, props) + if url.Path == d.base+path+"/" { + ret, err = d.PropFind(path+"/", depth, props) } } return @@ -564,8 +564,8 @@ func (d *DavClient) Readdir(path string, detail bool) (ret []Dnode, err error) { continue } n := Dnode{ - Name: name, - IsDir: p.ResourceType == "collection", + Name: name, + IsDir: p.ResourceType == "collection", IsLink: p.ResourceType == "redirectref", Target: p.RefTarget, } @@ -607,12 +607,12 @@ func (d *DavClient) Stat(path string) (ret Dnode, err error) { p := props[0] size, _ := strconv.ParseUint(p.ContentLength, 10, 64) ret = Dnode{ - Name: stripLastSlash(p.Name), + Name: stripLastSlash(p.Name), IsDir: p.ResourceType == "collection", Mtime: parseTime(p.LastModified), Ctime: parseTime(p.CreationDate), - Size: size, - } + Size: size, + } return } @@ -650,9 +650,9 @@ func (d *DavClient) GetRange(path string, offset int64, length int) (data []byte return } partial := false - if (offset >= 0 && length >= 0 ) { + if offset >= 0 && length >= 0 { partial = true - req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset + int64(length) - 1)) + req.Header.Set("Range", fmt.Sprintf("bytes=%d-%d", offset, offset+int64(length)-1)) } resp, err := d.do(req) if err != nil { @@ -667,12 +667,12 @@ func (d *DavClient) GetRange(path string, offset int64, length int) (data []byte if partial && resp.StatusCode != 206 { err = davToErrno(&DavError{ Message: "416 Range Not Satisfiable", - Code: 416, + Code: 416, }) return } data, err = ioutil.ReadAll(resp.Body) - if len(data) > length { + if length >= 0 && len(data) > length { data = data[:length] } return @@ -763,7 +763,7 @@ func (d *DavClient) Move(oldPath, newPath string) (err error) { // multipart response means there were errors. err = davToErrno(&DavError{ Message: "500 unexpected error during MOVE", - Code: 500, + Code: 500, }) } return @@ -851,7 +851,7 @@ func (d *DavClient) PutRange(path string, data []byte, offset int64, create bool } err = davToErrno(&DavError{ Message: "405 Method Not Allowed", - Code: 405, + Code: 405, }) return } @@ -864,14 +864,6 @@ func (d *DavClient) Put(path string, data []byte, create bool, excl bool) (creat d.semAcquire() defer d.semRelease() - if !d.CanPutRange() { - err = davToErrno(&DavError{ - Message: "405 Method Not Allowed", - Code: 405, - }) - return - } - req, err := d.buildRequest("PUT", path, data) if create { if excl { @@ -888,4 +880,3 @@ func (d *DavClient) Put(path string, data []byte, create bool, excl bool) (creat defer resp.Body.Close() return } - diff --git a/webdavfs b/webdavfs new file mode 100755 index 0000000..753c95c Binary files /dev/null and b/webdavfs differ