1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267
  
     | 
    
      # tie_file.tcl --
#
#	Data source: Files.
#
# Copyright (c) 2004 Andreas Kupries <andreas_kupries@users.sourceforge.net>
#
# See the file "license.terms" for information on usage and redistribution
# of this file, and for a DISCLAIMER OF ALL WARRANTIES.
# 
# RCS: @(#) $Id: tie_file.tcl,v 1.5 2005/09/30 05:36:39 andreas_kupries Exp $
# ### ### ### ######### ######### #########
## Requisites
package require snit
package require tie
# ### ### ### ######### ######### #########
## Implementation
snit::type ::tie::std::file {
    # ### ### ### ######### ######### #########
    ## Notes
    ## This data source maintains an internal cache for higher
    ## efficiency, i.e. to avoid having to go out to the slow file.
    ## This cache is handled as follows
    ##
    ## - All write operations invalidate the cache and write directly
    ##   to the file.
    ##
    ## - All read operations load from the file if the cache is
    ##   invalid, and from the cache otherwise
    ## This scheme works well in the following situations:
    ## (a) The data source is created, and then only read from.
    ## (b) The data source is created, and then only written to.
    ## (c) The data source is created, read once, and then only
    ##     written to.
    ## This scheme works badly if the data source is opened and then
    ## randomly read from and written to. The cache is useless, as it
    ## is continuously invalidated and reloaded.
    ## This no problem from this developers POV of view however.
    ## Consider the context. If you have this situation just tie the
    ## DS to an array A after creation. The tie framework operates on
    ## the DS in mode (c) and A becomes an explicit cache for the DS
    ## which is not invalidated by writing to it. IOW this covers
    ## exactly the situation the DS by itself is not working well for.
    # ### ### ### ######### ######### #########
    ## Specials
    pragma -hastypemethods no
    pragma -hasinfo        no
    pragma -simpledispatch yes
    # ### ### ### ######### ######### #########
    ## API : Construction & Destruction
    constructor {thepath} {
	# Locate and open the journal file.
	set path [file normalize $thepath]
	if {[file exists $path]} {
	    set chan [open $path {RDWR EXCL APPEND}]
	} else {
	    set chan [open $path {RDWR EXCL CREAT APPEND}]
	}
	fconfigure $chan -buffering none -encoding utf-8
	return
    }
    destructor {
	# Release the channel to the journal file, should it be open.
	if {$chan ne ""} {close $chan}
	return
    }
    # ### ### ### ######### ######### #########
    ## API : Data source methods
    method get {} {
	if {![file size $path]} {return {}}
	$self LoadJournal
	return [array get cache]
    }
    method set {dict} {
	puts $chan [list array set $dict]
	$self Invalidate
	return
    }
    method unset {{pattern *}} {
	puts $chan [list array unset $pattern]
	$self Invalidate
	return
    }
    method names {} {
	if {![file size $path]} {return {}}
	$self LoadJournal
	return [array names cache]
    }
    method size {} {
	if {![file size $path]} {return 0}
	$self LoadJournal
	return [array size cache]
    }
    method getv {index} {
	if {![file size $path]} {
	    return -code error "can't read \"$index\": no such variable"
	}
	$self LoadJournal
	return $cache($index)
    }
    method setv {index value} {
	puts $chan [list set $index $value]
	$self Invalidate
	return
    }
    method unsetv {index} {
	puts $chan [list unset $index]
	$self Invalidate
	return
    }
    # ### ### ### ######### ######### #########
    ## Internal : Instance data
    variable chan {} ; # Channel to write the journal.
    variable path {} ; # Path to journal file.
    # Journal loading, and cache.
    variable count 0         ; # #Operations in the journal.
    variable cvalid 0        ; # Validity of the cache.
    variable cache -array {} ; # Cache for journal
    # Management of the cache: See notes at beginning.
    # ### ### ### ######### ######### #########
    ## Internal: Loading from the journal.
    method LoadJournal {} {
	if {$cvalid} return
	$self Replay
	$self Compact
	return
    }
    method Replay {} {
	# Use a safe interp for the evaluation of the journal file.
	# (Empty safe for the hidden commands and the aliases we insert).
	# Called for !cvalid, implies cache does not exist
	set ip [interp create -safe]
	foreach c [$ip eval {info commands}] {
	    if {$c eq "rename"} continue
	    $ip eval [list rename $c {}]
	}
	$ip eval {rename rename {}}
	interp alias $ip set   {} $self Set
	interp alias $ip unset {} $self Unset
	interp alias $ip array {} $self Array
	array set cache {}
	set       count 0
	$ip invokehidden -global source $path
	interp delete $ip
	set cvalid 1
	return
    }
    method Compact {} {
	# Compact the journal
	#puts @@/2*$count/3*[array size temp]/=/[expr {2*$count >= 3*[array size temp]}]
	# ASSERT cvalid
	# do not compact <=>
	# 2*ops < 3*size <=>
	# ops < 3/2*size <=>
	# ops < 1.5*size
	if {(2*$count) < (3*[array size cache])} return
	file delete -force ${path}.new
	set new [open ${path}.new {RDWR EXCL CREAT APPEND}]
	fconfigure $new -buffering none -encoding utf-8
	# Compress current contents into a single multi-key load operation.
	puts $new [list array set [array get cache]]
	if {$::tcl_platform(platform) eq "windows"} {
	    # For windows the open channels prevent us from
	    # overwriting the old file. We have to leave
	    # attackers a (small) window of opportunity for
	    # replacing the file with something they own :(
	    close $chan
	    close $new
	    file rename -force ${path}.new $path
	    set chan [open ${path} {RDWR EXCL APPEND}]
	} else {
	    # Copy compacted journal over the existing one.
	    file rename -force ${path}.new $path
	    close $chan
	    set    chan $new
	}
	return
    }
    method Set {index value} {
	set cache($index) $value
	incr count
	return
    }
    method Unset {index} {
	unset cache($index)
	incr count
	return
    }
    method Array {cmd detail} {
	# syntax : set   dict
	# ...... : unset pattern
	if {$cmd eq "set"} {
	    array set cache $detail
	} elseif {$cmd eq "unset"} {
	    array unset cache $detail
	} else {
	    return -code error "Illegal command \"$cmd\""
	}
	incr count
	return
    }
    method Invalidate {} {
	if {!$cvalid} return
	set cvalid 0
	unset cache
	return
    }
    # ### ### ### ######### ######### #########
}
# ### ### ### ######### ######### #########
## Ready to go
::tie::register ::tie::std::file as file
package provide   tie::std::file 1.0.1
 
     |