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