# ACTIVESTATE TEAPOT-PKG BEGIN TM -*- tcl -*-
# -- Tcl Module
# @@ Meta Begin
# Package vfs::webdav 0.1
# Meta as::author {Vincent Darley}
# Meta as::build::date 2015-03-13
# Meta as::origin http://sf.net/projects/tclvfs
# Meta category Virtual filesystems
# Meta description Tclvfs allows Virtual Filesystems to be built using
# Meta description Tcl scripts only. It is also a repository of such
# Meta description Tcl-implemented filesystems (metakit, zip, ftp, tar,
# Meta description http, webdav, namespace, url)
# Meta license BSD
# Meta platform tcl
# Meta require {Tcl 8.4}
# Meta require vfs
# Meta require base64
# Meta subject zip ftp tar http webdav vfs filesystem metakit
# Meta subject namespace url
# Meta summary Extra virtual filesystems for Tcl.
# @@ Meta End
# ACTIVESTATE TEAPOT-PKG BEGIN REQUIREMENTS
package require Tcl 8.4
package require vfs
package require base64
# ACTIVESTATE TEAPOT-PKG END REQUIREMENTS
# ACTIVESTATE TEAPOT-PKG BEGIN DECLARE
package provide vfs::webdav 0.1
# ACTIVESTATE TEAPOT-PKG END DECLARE
# ACTIVESTATE TEAPOT-PKG END TM
package provide vfs::webdav 0.1
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"} {
eval [list $cmd $dirurl $extraHeadersList $relative $actualpath] $args
} else {
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"
# get information on the type of this file.
if {$name == ""} {
set mtime 0
lappend res type directory
lappend res dev -1 uid -1 gid -1 nlink 1 depth 0 \
atime $mtime ctime $mtime mtime $mtime mode 0777
return $res
}
# This is a bit of a hack. We really want to do a 'PROPFIND'
# request with depth 0, I believe. I don't think Tcl's http
# package supports that.
set token [::http::geturl $dirurl$name -method PROPFIND \
-headers [concat $extraHeadersList [list Depth 0]] -protocol 1.1]
upvar #0 $token state
if {![regexp " (OK|Multi\\-Status)$" $state(http)]} {
::vfs::log "No good: $state(http)"
#parray state
::http::cleanup $token
error "Not found"
}
regexp {(.*)} [::http::data $token] -> properties
if {[regexp {} $properties]} {
set type directory
} else {
set type file
}
#parray state
set mtime 0
lappend res type $type
lappend res dev -1 uid -1 gid -1 nlink 1 depth 0 \
atime $mtime ctime $mtime mtime $mtime mode 0777 \
size $state(totalsize)
::http::cleanup $token
return $res
}
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
if {![regexp " (OK|Moved Permanently)$" $state(http)]} {
::vfs::log "No good: $state(http)"
::http::cleanup $token
error "Not found"
} 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 mode permissions} {
::vfs::log "open $name $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.
switch -glob -- $mode {
"" -
"r" {
set token [::http::geturl $dirurl$name -headers $extraHeadersList]
upvar #0 $token state
set filed [vfs::memchan]
fconfigure $filed -encoding binary -translation binary
puts -nonewline $filed [::http::data $token]
seek $filed 0
::http::cleanup $token
return [list $filed]
}
"a" -
"w*" {
error "Can't open $name for writing"
}
default {
return -code error "illegal access mode \"$mode\""
}
}
}
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
#parray state
set body [::http::data $token]
::http::cleanup $token
#::vfs::log $body
while {1} {
set start [string first "(.*)" $item -> name]} {
continue
}
# Get tail of name (don't use 'file tail' since it isn't a file).
vfs::log "checking: $name"
regexp {[^/]+/?$} $name name
if {$name == ""} { continue }
if {[string match $pattern $name]} {
vfs::log "check: $name"
if {$type == 0} {
lappend res [file join $actualpath $name]
} else {
eval lappend res [_matchtypes $item \
[file join $actualpath $name] $type]
}
}
#vfs::log "got: $res"
}
} else {
# single file
set token [::http::geturl $dirurl$path -method PROPFIND \
-headers [concat $extraHeadersList [list Depth 0]]]
upvar #0 $token state
if {![regexp " (OK|Multi\\-Status)$" $state(http)]} {
::vfs::log "No good: $state(http)"
#parray state
::http::cleanup $token
return ""
}
set body [::http::data $token]
::http::cleanup $token
#::vfs::log $body
eval lappend res [_matchtypes $body $actualpath $type]
}
return $res
}
# Helper function
proc vfs::webdav::_matchtypes {item actualpath type} {
#::vfs::log [list $item $actualpath $type]
if {[regexp {} $item]} {
if {![::vfs::matchDirectories $type]} {
return ""
}
} else {
if {![::vfs::matchFiles $type]} {
return ""
}
}
return [list $actualpath]
}
proc vfs::webdav::createdirectory {dirurl extraHeadersList name} {
::vfs::log "createdirectory $name"
error "write access not implemented"
}
proc vfs::webdav::removedirectory {dirurl extraHeadersList name recursive} {
::vfs::log "removedirectory $name"
error "write access not implemented"
}
proc vfs::webdav::deletefile {dirurl extraHeadersList name} {
::vfs::log "deletefile $name"
error "write access not implemented"
}
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]
error "write access not implemented"
}
}
}
proc vfs::webdav::utime {dirurl extraHeadersList path actime mtime} {
error "write access not implemented"
}