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 268 269 270 271 272
|
# tie_file.tcl --
#
# Data source: Files.
#
# Copyright (c) 2004-2021 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.
# ### ### ### ######### ######### #########
## Requisites
package require Tcl 8.5 9
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
set jchan [open $path r]
fconfigure $jchan -encoding utf-8
set data [read $jchan]
close $jchan
$ip eval $data
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}]
fconfigure $chan -buffering none -encoding utf-8
} 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.2
|