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]] }