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