summaryrefslogtreecommitdiffstats
path: root/tcllib/modules/tie/tie_file.tcl
diff options
context:
space:
mode:
Diffstat (limited to 'tcllib/modules/tie/tie_file.tcl')
-rw-r--r--tcllib/modules/tie/tie_file.tcl273
1 files changed, 273 insertions, 0 deletions
diff --git a/tcllib/modules/tie/tie_file.tcl b/tcllib/modules/tie/tie_file.tcl
new file mode 100644
index 0000000..5592e42
--- /dev/null
+++ b/tcllib/modules/tie/tie_file.tcl
@@ -0,0 +1,273 @@
+# 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.11 2008/02/28 06:19:56 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
+
+ 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.0.4