vfs::webdav

tclvfs now contains the first vague attempts at a 'webdav' implementation in Tcl.


Gotisch - 2011-12-17 20:02:43

/updated 2011-12-18/

I updated the webdavvfs.tcl and post it here in case someelse wants the updated version.

I started by using the tclxml package and redoing some of the already implemented features which didnt work for me anymore. Based on that i added file writing abilities. Still missing is utime (i dont think webdav has something like last accessed).

I also started adding the posix errors, but they turn out to be not correct for the system im on (at least i added "Function not implemented" but tcl kept throwing "File exists")

attached the updated code.

package provide vfs::webdav 0.2

package require dom
package require vfs 1.0
package require http 2.6
# part of tcllib
package require base64

# This works for very basic operations.
# It has been put together, so far, largely by trial and error!
# What it really needs is to be filled in with proper xml support,
# using the tclxml package.

namespace eval vfs::webdav {}

proc vfs::webdav::Mount {dirurl local} {
    ::vfs::log "http-vfs: attempt to mount $dirurl at $local"
    if {[string index $dirurl end] != "/"} {
        append dirurl "/"
    }
    if {[string range $dirurl 0 6] == "http://"} {
        set rest [string range $dirurl 7 end]
    } else {
        set rest $dirurl
        set dirurl "http://${dirurl}"
    }
    
    if {![regexp {(([^:]*)(:([^@]*))?@)?([^/]*)(/(.*/)?([^/]*))?$} $rest \
            junk junk user junk pass host junk path file]} {
        return -code error "Sorry I didn't understand\
          the url address \"$dirurl\""
    }
    
    if {[string length $file]} {
        return -code error "Can only mount directories, not\
          files (perhaps you need a trailing '/' - I understood\
          a path '$path' and file '$file')"
    }
    
    if {![string length $user]} {
        set user anonymous
    }
    
    set dirurl "http://$host/$path"
    
    set extraHeadersList [list Authorization \
            [list Basic [base64::encode ${user}:${pass}]]]

    set token [::http::geturl $dirurl -headers $extraHeadersList -validate 1]
    http::cleanup $token
    
    if {![catch {vfs::filesystem info $dirurl}]} {
        # unmount old mount
        ::vfs::log "ftp-vfs: unmounted old mount point at $dirurl"
        vfs::unmount $dirurl
    }
    ::vfs::log "http $host, $path mounted at $local"
    vfs::filesystem mount $local [list vfs::webdav::handler \
            $dirurl $extraHeadersList $path]
    # Register command to unmount
    vfs::RegisterMount $local [list ::vfs::webdav::Unmount $dirurl]
    return $dirurl
}

proc vfs::webdav::Unmount {dirurl local} {
    vfs::filesystem unmount $local
}

proc vfs::webdav::handler {dirurl extraHeadersList path cmd root relative actualpath args} {
    ::vfs::log "handler $dirurl $path $cmd"
    if {$cmd == "matchindirectory" || $cmd == "open"} {
        eval [list $cmd $dirurl $extraHeadersList $relative $actualpath] $args
    } else {
    ::vfs::log "[list $cmd $dirurl $extraHeadersList $relative] $args"
        eval [list $cmd $dirurl $extraHeadersList $relative] $args
    }
}

# If we implement the commands below, we will have a perfect
# virtual file system for remote http sites.

proc vfs::webdav::stat {dirurl extraHeadersList name} {
    ::vfs::log "stat $name"
#    ::vfs::log "geturl $dirurl$name"
    set token [::http::geturl $dirurl$name -method PROPFIND \
      -headers [concat $extraHeadersList [list Depth 0]] -protocol 1.1]
    upvar #0 $token state
    set httpcode [lindex [split $state(http) " "] 1]
        if {$httpcode != 200 && $httpcode != 207} {
                ::vfs::log "No good: $state(http)"
                #parray state
                ::http::cleanup $token
                return [vfs::filesystem posixerror [::vfs::posixError ENOENT]]
        }
    set data [::http::data $token]
    ::http::cleanup $token
    ::vfs::log $data
        set xmldoc [::dom::parse $data]
        #TODO other stat info
        set resourcetype [::dom::selectNode $xmldoc {/d:multistatus/d:response/d:propstat/d:prop/d:resourcetype/d:collection} -namespaces {d DAV:}]
        if {$resourcetype != ""} {
                set type "directory"
        } else {
                set type "file" 
        }
        set filesize [::dom::selectNode $xmldoc {/d:multistatus/d:response/d:propstat/d:getcontentlength} -namespaces {d DAV:}]
        if {$filesize != ""} {
                set filesize [$filesize stringValue]
        } else {
                set filesize 0
        }
    return [list dev -1 uid -1 gid -1 nlink 1 depth 0 size $filesize atime 0 mtime 0 ctime 0 mode 777 type $type]
}

proc vfs::webdav::access {dirurl extraHeadersList name mode} {
    ::vfs::log "access $name $mode"
    if {$name == ""} { return 1 }
    set token [::http::geturl $dirurl$name -headers $extraHeadersList]
    upvar #0 $token state
    set httpcode [lindex [split $state(http) " "] 1]
        if {$httpcode != 200 && $httpcode != 207} {
                ::vfs::log "No good: $state(http)"
                #parray state
                ::http::cleanup $token
                return 0
        } else {
        ::http::cleanup $token
        return 1
    }
}

# We've chosen to implement these channels by using a memchan.
# The alternative would be to use temporary files.
proc vfs::webdav::open {dirurl extraHeadersList name actualpath mode permissions} {
    ::vfs::log "open $name $actualpath $mode $permissions"
    # return a list of two elements:
    # 1. first element is the Tcl channel name which has been opened
    # 2. second element (optional) is a command to evaluate when
    #    the channel is closed.
    set resultchannel [vfs::memchan]
    fconfigure $resultchannel -encoding binary -translation binary
    if {$mode == ""} { set mode "r" }
    if {[file isdirectory $actualpath]} {
            ::vfs::log "can not read/write to directory."
            return [vfs::filesystem posixerror [::vfs::posixError EISDIR]]
    }
    # There is a tricky part. we need to download the file from server in all cases where it must exists
    # r, r+ and a absolutly need the file
    # w, w+ do not need the file at all
    # a+ works with file and without
    if {[string match "\[ar\]*" $mode ]} {
            # we should at least try to download the file.
            set token [::http::geturl $dirurl$name -headers $extraHeadersList]
            upvar #0 $token state
            set httpcode [lindex [split $state(http) " "] 1]
            if {$httpcode != 200} {
                    # TODO give better error message.
                    if {[string match "r*" $mode] || [string equal $mode "a"]} {
                            ::vfs::log "File not found"
                            ::http::cleanup $token
                            close $resultchannel
                            return [vfs::filesystem posixerror [::vfs::posixError ENOENT]]
                    }
            } else {
                    # load file into buffer
                    puts -nonewline $resultchannel [::http::data $token]
                    ::http::cleanup $token
            }
    }
    # we need to handle uploading of the file back if writing is permitted.
    # r+, w, w+, a, a+
    # The pointer is at the beginning for: r+, w, w+
    switch -glob -- $mode {
                "" -
                "r" {
                        seek $resultchannel 0
                        return [list $resultchannel]
                }
                "r+" -
                "w" -
                "w+" {
                        seek $resultchannel 0
                }
                "a" -
                "a+" {
                        # reading and writing file created if it does not exist. position at end of file
                        # seeking does not work but we should already be at the end.
                }
                default {
                        close $resultchannel
                        return -code error "illegal access mode \"$mode\""
                }
    }
    ::vfs::log "[list $resultchannel [list ::vfs::webdav::_closing $dirurl$name $resultchannel $extraHeadersList]]"
    return [list $resultchannel [list ::vfs::webdav::_closing $dirurl$name $resultchannel $extraHeadersList]]
}
proc vfs::webdav::_closing {url channel extraHeadersList} {
        ::vfs::log "_closing $url $channel $extraHeadersList"
        seek $channel 0
        set token [::http::geturl $url -headers $extraHeadersList -method PUT -querychannel $channel]
        upvar #0 $token state
    set httpcode [lindex [split $state(http) " "] 1]
    if {$httpcode > 204} {
                    # TODO give better error message.
                    ::vfs::log "Error Writing: $httpcode"
                    ::http::cleanup $token
                        return [vfs::filesystem posixerror [::vfs::posixError EIO]]
        }
        ::vfs::log "Upload successfull"
        ::http::cleanup $token
}
proc vfs::webdav::matchindirectory {dirurl extraHeadersList path actualpath pattern type} {
    ::vfs::log "matchindirectory $dirurl $path $actualpath $pattern $type"
    set res [list]
    if {[string length $pattern]} {
                # need to match all files in a given remote http site.
                set token [::http::geturl $dirurl$path -method PROPFIND \
                  -headers [concat $extraHeadersList [list Depth 1]]]
                upvar #0 $token state
                set httpcode [lindex [split $state(http) " "] 1]
                if {$httpcode != 200 && $httpcode != 207} {
                        ::vfs::log "No good: $state(http)"
                        ::http::cleanup $token
                        return [vfs::filesystem posixerror [::vfs::posixError ENOENT]]
                }
                set body [::http::data $token]
                ::http::cleanup $token
                set xmldoc [::dom::parse $body]
                set data [::dom::selectNode $xmldoc {/d:multistatus/d:response/d:href} -namespaces {d DAV:}]
                set currentdir [lindex $data 0]
                set content [lrange $data 1 end]
                foreach node $content {
                        # strip path
                        set itemname [string map [list [$currentdir stringValue] ""] [$node stringValue]]
                        if {[string index $itemname end] == "/"} {
                                # Directories should not be show with slash at the end but without.
                                set itemname [string range $itemname 0 end-1]
                        }
                        if {[string match $pattern $itemname]} {
                                if {$type == 0} {
                                        lappend res [file join $actualpath $itemname]
                                } else {
                                        eval lappend res [_matchtypes [$node parent] [file join $actualpath $itemname] $type]        
                                }
                                #vfs::log "match: $itemname"
                        }
                }
    } else {
                # single file
                set token [::http::geturl $dirurl$path -method PROPFIND \
                  -headers [concat $extraHeadersList [list Depth 0]]]
        
                upvar #0 $token state
                set httpcode [lindex [split $state(http) " "] 1]
                if {$httpcode != 200 && $httpcode != 207} {
                        ::vfs::log "No good: $state(http)"s
                        ::http::cleanup $token
                        return [vfs::filesystem posixerror [::vfs::posixError ENOENT]]
                }
                set body [::http::data $token]
                ::http::cleanup $token
                set xmldoc [::dom::parse $body]
                set response [::dom::selectNode $xmldoc {/d:multistatus/d:response} -namespaces {d DAV:}]
                #::vfs::log $body
                eval lappend res [_matchtypes $response $actualpath $type]
    } 
    return $res
}

# Helper function
proc vfs::webdav::_matchtypes {item actualpath type} {
    #::vfs::log [list $item $actualpath $type]
    if {[$item selectNode $item {d:propstat/d:prop/d:resourcetype/d:collection} -namespaces {d DAV:}] != ""} {
                if {![::vfs::matchDirectories $type]} {
                        return ""
                }
    } else {
                if {![::vfs::matchFiles $type]} {
                        return ""
                }
    }
    return [list $actualpath]
}

proc vfs::webdav::createdirectory {dirurl extraHeadersList name} {
    ::vfs::log "createdirectory $dirurl $extraHeadersList $name"
    set token [::http::geturl $dirurl$name -method MKCOL \
                  -headers [concat $extraHeadersList [list Depth 0]]]
        upvar #0 $token state
        set httpcode [lindex [split $state(http) " "] 1]
        ::http::cleanup $token
        if {$httpcode == 201} {
                return 1
        }
        ::vfs::log "No good: $state(http)"
        switch -- $httpcode {
                403 {
                        return [vfs::filesystem posixerror [::vfs::posixError EACCES]]
                }
                507 {
                        return [vfs::filesystem posixerror [::vfs::posixError ENOSPC]]
                }
                409 {
                        return [vfs::filesystem posixerror [::vfs::posixError ENOENT]]
                }
                405 {
                        return [vfs::filesystem posixerror [::vfs::posixError EPERM]]
                }
        }
        return [vfs::filesystem posixerror [::vfs::posixError ENODEV]]
}

proc vfs::webdav::removedirectory {dirurl extraHeadersList name recursive} {
    ::vfs::log "removedirectory $dirurl $name $recursive"
    # deletion is always recursive.
    
    set token [::http::geturl $dirurl$name -method DELETE -headers $extraHeadersList]
        upvar #0 $token state
        set httpcode [lindex [split $state(http) " "] 1]
        set body [::http::data $token]
        ::vfs::log "$state(http)"
        ::http::cleanup $token
        switch -- $httpcode {
                404 {
                        return [vfs::filesystem posixerror [::vfs::posixError ENOENT]]
                }
                204 -
                200 {
                        return 1
                }
                - {
                    return [vfs::filesystem posixerror [::vfs::posixError ENOTEMPTY]]
                }
        }
}

proc vfs::webdav::deletefile {dirurl extraHeadersList name} {
    ::vfs::log "deletefile $name"
    removedirectory $dirurl $extraHeadersList $name 0
}

proc vfs::webdav::fileattributes {dirurl extraHeadersList path args} {
    ::vfs::log "fileattributes $args"
    switch -- [llength $args] {
        0 {
            # list strings
            return [list]
        }
        1 {
            # get value
            set index [lindex $args 0]
        }
        2 {
            # set value
            set index [lindex $args 0]
            set val [lindex $args 1]
             return [vfs::filesystem posixerror [::vfs::posixError ENOSYS]]
        }
    }
}

proc vfs::webdav::utime {dirurl extraHeadersList path actime mtime} {
     return [vfs::filesystem posixerror [::vfs::posixError ENOSYS]]
}