Version 2 of A delta virtual filesystem

Updated 2004-11-09 15:01:03

SEH A delta virtual filesystem.

This virtual filesystem is designed to be used with a versioning virtual filesystem stacked on top of it. As the versioning filesystem generates a new separate file for every file edit, this filesystem will invisibly generate and manage deltas of the separate versions to save space.

It is designed to be used with the procedures posted on the tdelta page, but you can substitute the delta technology of your choice simply by overloading the tdelta and tpatch procedures.

The code includes logic to keep the new file intact if its delta with the existing file turns out to be bigger, or if the two versions are so different that the expense of generating a delta seems unwarranted (i.e., it won't waste time generating deltas of two jpeg or zip file versions if they have almost no content in common).

 # Usage: ::vfs::template::version::delta::Mount <existing directory> <intermediate virtual delta directory>
 #        ::vfs::template::version::Mount <intermediate virtual delta directory> ?-keep <number>? ?-project <project name list>? ?-time <time>? <virtual versioning directory>

 package require vfs 1

 namespace eval ::vfs::template::version::delta {}

 proc ::vfs::template::version::delta::Mount {args} {
        set volume {}
        if {[lindex $args 0] == "-volume"} {set volume "-volume"}
        set pathto [eval MountProcedure $args]
        set path [lindex $pathto 0]
        set to [lindex $pathto 1]
        eval ::vfs::filesystem mount $volume \$to \[list ::vfs::template::version::delta::handler \$path\]
        ::vfs::RegisterMount $to [list ::vfs::template::version::delta::Unmount]
        if {[trace info execution close] == {}} {
                trace add execution close leave ::vfs::template::version::delta::CloseTrace
        }
        return $to
 }

 proc ::vfs::template::version::delta::Unmount {to} {
        set to [file normalize $to]
        set path [lindex [::vfs::filesystem info $to] end]
        UnmountProcedure $path $to
        ::vfs::filesystem unmount $to
 }

 proc ::vfs::template::version::delta::CloseTrace {commandString code result op} {
        set errorCode $::errorCode
        set errorInfo $::errorInfo
        set channelID [lindex $commandString 1]
        if [regexp {::Close ([^ ]+?) } $errorInfo trash errorChannelID] {
                if [string equal $channelID $errorChannelID] {
                        if {[lindex $errorCode 0] == "POSIX"} {
                                set pError [lindex $errorCode 1]
                                ::vfs::filesystem posixerror $::vfs::posix($pError) ; return -code error $::vfs::posix($pError)
                        }
                        set message [lindex [split $errorInfo \n] 0]
                        error $message $errorInfo $errorCode
                }
        }
 }

 proc ::vfs::template::version::delta::handler {path cmd root relative actualpath args} {
        switch -- $cmd {
                access {
                        set mode [lindex $args 0]
                        set error [catch {Access $path $root $relative $actualpath $mode}]
                        if $error {::vfs::filesystem posixerror $::vfs::posix(EACCES) ; return -code error $::vfs::posix(EACCES)}
                }
                createdirectory {
                        CreateDirectory $path $root $relative $actualpath
                }
                deletefile {
                        DeleteFile $path $root $relative $actualpath
                }
                fileattributes {
                        set index [lindex $args 0]
                        set value [lindex $args 1]
                        array set attributes [FileAttributes $path $root $relative $actualpath]
                        if {$index == {}} {
                                return [lsort [array names attributes]]
                        }
                        set attribute [lindex [lsort [array names attributes]] $index]
                        if {$value == {}} {
                                return $attributes($attribute)
                        }
                        FileAttributesSet $path $root $relative $actualpath $attribute $value
                }
                matchindirectory {
                        set pattern [lindex $args 0]
                        set types [lindex $args 1]
                        return [MatchInDirectory $path $root $relative $actualpath $pattern $types]
                } open {
                        set mode [lindex $args 0]
                        if {$mode == {}} {set mode r}
                        set permissions [lindex $args 1]
                        set channelID [Open $path $root $relative $actualpath $mode $permissions]
                        switch -glob -- $mode {
                                "" -
                                "r*" -
                                "w*" {
                                        seek $channelID 0
                                }
                                "a*" {
                                            seek $channelID 0 end
                                }
                                default {
                                        ::vfs::filesystem posixerror $::vfs::posix(EINVAL)
                                        return -code error $::vfs::posix(EINVAL)
                                }
                        }
                        set result $channelID
                        lappend result [list ::vfs::template::version::delta::Close $channelID $path $root $relative $actualpath $mode]
                        return $result
                }
                removedirectory {
                        set recursive [lindex $args 0]
                        if !$recursive {
                                if {[MatchInDirectory $path $root $relative $actualpath * 0] != {}} {
                                        ::vfs::filesystem posixerror $::vfs::posix(EEXIST)
                                        return -code error $::vfs::posix(EEXIST)
                                }
                        }
                        RemoveDirectory $path $root $relative $actualpath
                }
                stat {
                        return [Stat $path $root $relative $actualpath]
                }
                utime {
                        set atime [lindex $args 0]
                        set mtime [lindex $args 1]
                        Utime $path $root $relative $actualpath $atime $mtime
                }
        }
 }

 namespace eval ::vfs::template::version::delta {

 proc MountProcedure {args} {
        if {[lindex $args 0] == "-volume"} {
                set args [lrange $args 1 end]
                set to [lindex $args 1]
        } else {
                set to [file normalize [lindex $args 1]]
        }
        set path [file normalize [lindex $args 0]]
        if [info exists ::vfs::_unmountCmd($to)] {::vfs::unmount $to}

        file mkdir $path
        array unset ::vfs::_unmountCmd $to
        lappend pathto $path
        lappend pathto $to
        return $pathto
 }

 proc UnmountProcedure {path to} {
        return
 }

 proc Access {path root relative actualpath mode} {
        set modeString [::vfs::accessMode $mode]
        if {$modeString == "F"} {set modeString RWX}
        set modeString [split $modeString {}]
        set fileName [lindex [glob -nocomplain -path [file join $path $relative] *] 0]
        set fileString {}
        if [file readable $fileName] {lappend fileString R}
        if [file writable $fileName] {lappend fileString W}
        if [file executable $fileName] {lappend fileString X}
        foreach mS $modeString {
                set errorMessage "not [string map {R readable W writable X executable} $mS]"
                if {[lsearch $fileString $mS] == -1} {error $errorMessage}
        }
 }

 proc CreateDirectory {path root relative actualpath} {
        file mkdir [file join $path $relative]
 }

 proc DeleteFile {path root relative actualpath} {
        set fileName [file join $path $relative]
        set timeStamp [lindex [split $fileName \;] 1]
        set targetFile [Reconstitute $fileName]
        foreach referenceFile [glob -directory [file dirname $fileName] -nocomplain *vfs&delta$timeStamp] {
                regsub {\;vfs&delta[0-9]*$} $referenceFile "" reconFile]
                set f [open $referenceFile r]
                fconfigure $f -translation binary
                set signature [read $f]
                close $f
                tpatch $targetFile $signature $reconFile
                file delete $referenceFile
        }
        close $targetFile
        file delete $fileName
        return
 }

 proc FileAttributes {path root relative actualpath} {
        set fileName [lindex [glob -nocomplain -path [file join $path $relative] *] 0]
        file attributes $fileName
 }

 proc FileAttributesSet {path root relative actualpath attribute value} {
        set fileName [lindex [glob -nocomplain -path [file join $path $relative] *] 0]
        file attributes $fileName $attribute $value
 }

 proc MatchInDirectory {path root relative actualpath pattern types} {
        if [::vfs::matchDirectories $types] {lappend typeString d}
        if [::vfs::matchFiles $types] {lappend typeString f}

        set globList "[glob -directory [file join $path $relative] -nocomplain -types $typeString $pattern] [glob -directory [file join $path $relative] -nocomplain -types "$typeString hidden" $pattern]"
        set pathLength [expr [string length $path] - 1]
        set newGlobList {}
        foreach gL $globList {
                set gL [string replace $gL 0 $pathLength $root]
                regsub {\;vfs&delta.*$} $gL "" gL
                lappend newGlobList $gL
        }
        return $newGlobList
 }

 proc Open {path root relative actualpath mode permissions} {
        set fileName [lindex [glob -nocomplain -path [file join $path $relative] *] 0]
        set newFile 0
        if ![file exists $fileName] {set newFile 1}
        set fileName [file join $path $relative]
        set channelID [Reconstitute $fileName]
        if [string equal $channelID {}] {set channelID [open $fileName $mode] ; close $channelID ; set channelID [::vfs::memchan]}
        if $newFile {catch {file attributes $fileName -permissions $permissions}}
        return $channelID
 }

 proc Close {channelID path root relative actualpath mode} {
 # Do not close the channel in the close callback!
 # It will crash Tcl!  Honest!
 # The core will close the channel after you've taken what info you need from it.

 #        close $channelID
        if {$mode == "r"} {return}
        set fileName [file join $path $relative]
        set f [open $fileName w]
        fconfigure $f -translation binary
        seek $f 0
        fconfigure $channelID -translation binary
        seek $channelID 0
        fcopy $channelID $f
        close $f
        Delta $fileName
        return
 }

 proc RemoveDirectory {path root relative actualpath} {
        catch {file delete [file join $path $relative]}
 }

 proc Stat {path root relative actualpath} {
        set fileName [lindex [glob -nocomplain -path [file join $path $relative] *] 0]

        set endtag [lindex [split $fileName \;] end]
        if {[string first "vfs&delta" $endtag] || [string equal "vfs&delta" $endtag]} {file stat $fileName fs ; return [array get fs]}
        set f [open $fileName r]
        fconfigure $f -translation binary
        set copyinstructions [read $f]
        array set fileStats [lindex $copyinstructions 3]
        unset copyinstructions
        close $f
        set size $fileStats(size)
        file stat $fileName fs
        set fs(size) $size
        return [array get fs]
 }

 proc Utime {path root relative actualpath atime mtime} {
        set fileName [lindex [glob -nocomplain -path [file join $path $relative] *] 0]
        file atime $fileName $atime
        file mtime $fileName $mtime
 }

 proc Delta {filename} {
        set fileRoot [lindex [split [file tail $filename] \;] 0]
        set fileNames [glob -nocomplain -path [file join [file dirname $filename] $fileRoot] *]
        set nonDeltas {}
        foreach fn $fileNames {
                set endtag [lindex [split $fn \;] end]
                if ![string first "vfs&delta" $endtag] {continue}
                lappend nonDeltas $fn
                set atimes($fn) [file atime $fn]
        }
        if {[set deltaIndex [llength $nonDeltas]] < 2} {return}
        set nonDeltas [lsort -dictionary $nonDeltas]
        incr deltaIndex -1
        set i 0
        while {$i < $deltaIndex} {
                set referenceFile [lindex $nonDeltas $i]
                set targetFile [lindex $nonDeltas [incr i]]
                set signature [tdelta $referenceFile $targetFile $::trsync::blockSize 1 1]
                set targetTimeStamp [lindex [split $targetFile \;] 1]

                file stat $referenceFile fileStats
                set signatureSize [string length $signature]
                if {$signatureSize > $fileStats(size)} {
                        set fileName $referenceFile\;vfs&delta
                        file rename $referenceFile $fileName
                        continue
                }

                array set fileStats [file attributes $referenceFile]

                set fileName $referenceFile\;vfs&delta$targetTimeStamp
                set f [open $fileName w]
                fconfigure $f -translation binary
                puts -nonewline $f $signature
                close $f
                file delete $referenceFile
                catch {file attributes $fileName -readonly 0}
                catch {file attributes $fileName -permissions rw-rw-rw-}
                catch {file attributes $fileName -owner $fileStats(uid)}
                catch {file attributes $fileName -group $fileStats(gid)}
                catch {file mtime $fileName $fileStats(mtime)}
                catch {file atime $fileName $fileStats(atime)}

                foreach attr [array names fileStats] {
                        if [string first "-" $attr] {continue}
                        if ![string first "-permissions" $attr] {continue}
                        if ![string first "-readonly" $attr] {continue}
                        catch {file attributes $fileName $attr $fileStats($attr)}
                }
                catch {file attributes $fileName -permissions $fileStats(mode)}
                catch {file attributes $fileName -readonly $fileStats(-readonly)}
        }
        foreach fn [array names atimes] {
                file atime $fn $atimes($fn)
        }
 }

 proc Reconstitute {fileName} {
        if [file isfile $fileName] {return [open $fileName r]}
        if [file isfile $fileName\;vfs&delta] {return [open $fileName\;vfs&delta r]}
        set targetFile [lindex [glob -nocomplain -path $fileName *] 0]
        if [string equal $targetFile {}] {return}
         set fileStack {}
        while {[string first "\;vfs&delta" $targetFile] > -1} {
                if ![regexp {\;vfs&delta([0-9]+)$} $targetFile trash targetTime] {break}
                set fileStack "[list $targetFile] $fileStack"
                set targetFile [lindex [glob -directory [file dirname $fileName] *\;$targetTime*] 0]
                set atimes($targetFile) [file atime $targetFile]
        }
        set targetFile [open $targetFile r]
        foreach fs $fileStack {
                set f [open $fs r]
                fconfigure $f -translation binary
                set copyInstructions [read $f]
                close $f
                set fileToConstruct [::vfs::memchan]
                tpatch $targetFile $copyInstructions $fileToConstruct
                catch {close $targetFile}
                set targetFile $fileToConstruct
        }
        foreach fn [array names atimes] {
                file atime $fn $atimes($fn)
        }
        fconfigure $targetFile -translation auto
        seek $targetFile 0
        return $targetFile
 }

 }
 # end namespace eval ::vfs::template::version::delta

Category VFS