Filename | /var/www/foswiki11/lib/Foswiki/Store/VC/Handler.pm |
Statements | Executed 721909 statements in 1.65s |
Calls | P | F | Exclusive Time |
Inclusive Time |
Subroutine |
---|---|---|---|---|---|
47352 | 1 | 1 | 590ms | 690ms | new | Foswiki::Store::VC::Handler::
46781 | 3 | 1 | 427ms | 427ms | storedDataExists | Foswiki::Store::VC::Handler::
41 | 1 | 1 | 315ms | 315ms | getTopicNames | Foswiki::Store::VC::Handler::
102 | 1 | 1 | 81.7ms | 83.8ms | getWebNames | Foswiki::Store::VC::Handler::
426 | 1 | 1 | 37.7ms | 38.1ms | readFile | Foswiki::Store::VC::Handler::
473 | 4 | 3 | 10.6ms | 10.6ms | noCheckinPending | Foswiki::Store::VC::Handler::
426 | 1 | 1 | 5.70ms | 43.7ms | getRevision | Foswiki::Store::VC::Handler::
12 | 1 | 1 | 2.58ms | 4.45ms | _getTOPICINFO | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 2.13ms | 2.24ms | BEGIN@31 | Foswiki::Store::VC::Handler::
22 | 1 | 1 | 1.58ms | 2.49s | getInfo | Foswiki::Store::VC::Handler::
11 | 1 | 1 | 656µs | 743µs | getTimestamp | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 333µs | 405µs | BEGIN@38 | Foswiki::Store::VC::Handler::
3 | 2 | 2 | 101µs | 394ms | getLatestRevisionID | Foswiki::Store::VC::Handler::
2 | 1 | 1 | 82µs | 394ms | getRevisionHistory | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 18µs | 31µs | BEGIN@26 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 12µs | 17µs | BEGIN@27 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 12µs | 16µs | BEGIN@41 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 11µs | 351µs | BEGIN@34 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 9µs | 23µs | BEGIN@28 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 4µs | 4µs | BEGIN@32 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 4µs | 4µs | BEGIN@36 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 4µs | 4µs | BEGIN@30 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 3µs | 3µs | BEGIN@33 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 3µs | 3µs | BEGIN@37 | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | _constructAttributesForAutoAttached | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | _controlFileName | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | _dirForTopicAttachments | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | _epochToRcsDateTime | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | _getAttachmentStats | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | _mktemp | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | _rmtree | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | _saveDamage | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | addRevisionFromStream | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | addRevisionFromText | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | ci | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | copyAttachment | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | copyFile | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | copyTopic | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | eachChange | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | finish | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | getAttachmentList | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | getLatestRevisionTime | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | getLease | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | getNextRevisionID | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | hidePath | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | init | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | isAsciiDefault | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | isLocked | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | mkPathTo | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | mkTmpFilename | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | moveAttachment | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | moveFile | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | moveTopic | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | moveWeb | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | openStream | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | recordChange | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | remove | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | removeSpuriousLeases | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | repRev | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | replaceRevision | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | restoreLatestRevision | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | revisionExists | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | saveFile | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | saveStream | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | setLease | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | setLock | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | stringify | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | synchroniseAttachmentsList | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | test | Foswiki::Store::VC::Handler::
0 | 0 | 0 | 0s | 0s | CLOSE | Foswiki::Store::_MemoryFile::
0 | 0 | 0 | 0s | 0s | READ | Foswiki::Store::_MemoryFile::
0 | 0 | 0 | 0s | 0s | READLINE | Foswiki::Store::_MemoryFile::
0 | 0 | 0 | 0s | 0s | TIEHANDLE | Foswiki::Store::_MemoryFile::
Line | State ments |
Time on line |
Calls | Time in subs |
Code |
---|---|---|---|---|---|
1 | # See bottom of file for license and copyright information | ||||
2 | |||||
3 | =begin TML | ||||
4 | |||||
5 | ---+ package Foswiki::Store::VC::Handler | ||||
6 | |||||
7 | This class is PACKAGE PRIVATE to Store::VC, and should never be | ||||
8 | used from anywhere else. It is the base class of implementations of | ||||
9 | individual file handler objects used with stores that manipulate | ||||
10 | files stored in a version control system (phew!). | ||||
11 | |||||
12 | The general contract of the methods on this class and its subclasses | ||||
13 | calls for errors to be signalled by Error::Simple exceptions. | ||||
14 | |||||
15 | There are a number of references to RCS below; however this class is | ||||
16 | useful as a base class for handlers for all kinds of version control | ||||
17 | systems which use files on disk. | ||||
18 | |||||
19 | For readers who are familiar with Foswiki version 1.0.0, this class | ||||
20 | is analagous to =Foswiki::Store::RcsFile=. | ||||
21 | |||||
22 | =cut | ||||
23 | |||||
24 | package Foswiki::Store::VC::Handler; | ||||
25 | |||||
26 | 2 | 32µs | 2 | 44µs | # spent 31µs (18+13) within Foswiki::Store::VC::Handler::BEGIN@26 which was called:
# once (18µs+13µs) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 26 # spent 31µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@26
# spent 13µs making 1 call to strict::import |
27 | 2 | 26µs | 2 | 22µs | # spent 17µs (12+5) within Foswiki::Store::VC::Handler::BEGIN@27 which was called:
# once (12µs+5µs) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 27 # spent 17µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@27
# spent 5µs making 1 call to warnings::import |
28 | 2 | 24µs | 2 | 38µs | # spent 23µs (9+14) within Foswiki::Store::VC::Handler::BEGIN@28 which was called:
# once (9µs+14µs) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 28 # spent 23µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@28
# spent 14µs making 1 call to Assert::import |
29 | |||||
30 | 2 | 18µs | 1 | 4µs | # spent 4µs within Foswiki::Store::VC::Handler::BEGIN@30 which was called:
# once (4µs+0s) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 30 # spent 4µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@30 |
31 | 2 | 106µs | 1 | 2.24ms | # spent 2.24ms (2.13+111µs) within Foswiki::Store::VC::Handler::BEGIN@31 which was called:
# once (2.13ms+111µs) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 31 # spent 2.24ms making 1 call to Foswiki::Store::VC::Handler::BEGIN@31 |
32 | 2 | 20µs | 1 | 4µs | # spent 4µs within Foswiki::Store::VC::Handler::BEGIN@32 which was called:
# once (4µs+0s) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 32 # spent 4µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@32 |
33 | 2 | 24µs | 1 | 3µs | # spent 3µs within Foswiki::Store::VC::Handler::BEGIN@33 which was called:
# once (3µs+0s) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 33 # spent 3µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@33 |
34 | 2 | 32µs | 2 | 690µs | # spent 351µs (11+340) within Foswiki::Store::VC::Handler::BEGIN@34 which was called:
# once (11µs+340µs) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 34 # spent 351µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@34
# spent 340µs making 1 call to Exporter::import |
35 | |||||
36 | 2 | 18µs | 1 | 4µs | # spent 4µs within Foswiki::Store::VC::Handler::BEGIN@36 which was called:
# once (4µs+0s) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 36 # spent 4µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@36 |
37 | 2 | 18µs | 1 | 3µs | # spent 3µs within Foswiki::Store::VC::Handler::BEGIN@37 which was called:
# once (3µs+0s) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 37 # spent 3µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@37 |
38 | 2 | 131µs | 1 | 405µs | # spent 405µs (333+72) within Foswiki::Store::VC::Handler::BEGIN@38 which was called:
# once (333µs+72µs) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 38 # spent 405µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@38 |
39 | |||||
40 | # use the locale if required to ensure sort order is correct | ||||
41 | # spent 16µs (12+4) within Foswiki::Store::VC::Handler::BEGIN@41 which was called:
# once (12µs+4µs) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 46 | ||||
42 | 1 | 4µs | if ( $Foswiki::cfg{UseLocale} ) { | ||
43 | 1 | 300ns | require locale; | ||
44 | 1 | 3µs | 1 | 4µs | import locale(); # spent 4µs making 1 call to locale::import |
45 | } | ||||
46 | 1 | 4.86ms | 1 | 16µs | } # spent 16µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@41 |
47 | |||||
48 | =begin TML | ||||
49 | |||||
50 | ---++ ClassMethod new($store, $web, $topic, $attachment) | ||||
51 | |||||
52 | Constructor. There is one object per stored file. | ||||
53 | |||||
54 | $store is the Foswiki::VC::Store object that contains the cache for | ||||
55 | objects of this type. A cache is used because at some point we'll be | ||||
56 | smarter about the number of calls to RCS code we make. | ||||
57 | |||||
58 | Note that $web, $topic and $attachment must be untainted! | ||||
59 | |||||
60 | =cut | ||||
61 | |||||
62 | # spent 690ms (590+100) within Foswiki::Store::VC::Handler::new which was called 47352 times, avg 15µs/call:
# 47352 times (590ms+100ms) by Foswiki::Store::VC::RcsWrapHandler::new at line 28 of /var/www/foswiki11/lib/Foswiki/Store/VC/RcsWrapHandler.pm, avg 15µs/call | ||||
63 | 47352 | 40.4ms | my ( $class, $store, $web, $topic, $attachment ) = @_; | ||
64 | |||||
65 | 47352 | 43.0ms | 47352 | 35.9ms | ASSERT( $store->isa('Foswiki::Store') ) if DEBUG; # spent 35.9ms making 47352 calls to Assert::ASSERTS_OFF, avg 758ns/call |
66 | |||||
67 | 47352 | 205ms | 47352 | 60.0ms | if ( UNIVERSAL::isa( $web, 'Foswiki::Meta' ) ) { # spent 60.0ms making 47352 calls to UNIVERSAL::isa, avg 1µs/call |
68 | |||||
69 | # $web refers to a meta object | ||||
70 | 470 | 176µs | $attachment = $topic; | ||
71 | 470 | 793µs | 470 | 1.08ms | $topic = $web->topic(); # spent 1.08ms making 470 calls to Foswiki::Meta::topic, avg 2µs/call |
72 | 470 | 746µs | 470 | 886µs | $web = $web->web(); # spent 886µs making 470 calls to Foswiki::Meta::web, avg 2µs/call |
73 | } | ||||
74 | |||||
75 | # Reuse is good | ||||
76 | 47352 | 44.5ms | my $id = ( $web || 0 ) . '/' . ( $topic || 0 ) . '/' . ( $attachment || 0 ); | ||
77 | 47352 | 255ms | if ( $store->{handler_cache} && $store->{handler_cache}->{$id} ) { | ||
78 | return $store->{handler_cache}->{$id}; | ||||
79 | } | ||||
80 | |||||
81 | 1477 | 4.77ms | my $this = | ||
82 | bless( { web => $web, topic => $topic, attachment => $attachment }, | ||||
83 | $class ); | ||||
84 | |||||
85 | # Cache so we can re-use this object (it has no internal state | ||||
86 | # so can safely be reused) | ||||
87 | 1477 | 2.19ms | $store->{handler_cache}->{$id} = $this; | ||
88 | |||||
89 | 1477 | 603µs | if ( $web && $topic ) { | ||
90 | 1380 | 944µs | my $rcsSubDir = ( $Foswiki::cfg{RCS}{useSubDir} ? '/RCS' : '' ); | ||
91 | |||||
92 | 1380 | 1.58ms | 1380 | 1.40ms | ASSERT( UNTAINTED($web), "web $web is tainted!" ) if DEBUG; # spent 1.40ms making 1380 calls to Assert::ASSERTS_OFF, avg 1µs/call |
93 | 1380 | 1.28ms | 1380 | 1.04ms | ASSERT( UNTAINTED($topic), "topic $topic is tainted!" ) if DEBUG; # spent 1.04ms making 1380 calls to Assert::ASSERTS_OFF, avg 753ns/call |
94 | 1380 | 868µs | if ($attachment) { | ||
95 | 1 | 2µs | 1 | 1µs | ASSERT( UNTAINTED($attachment) ) if DEBUG; # spent 1µs making 1 call to Assert::ASSERTS_OFF |
96 | 1 | 4µs | $this->{file} = | ||
97 | $Foswiki::cfg{PubDir} . '/' | ||||
98 | . $web . '/' | ||||
99 | . $topic . '/' | ||||
100 | . $attachment; | ||||
101 | 1 | 3µs | $this->{rcsFile} = | ||
102 | $Foswiki::cfg{PubDir} . '/' | ||||
103 | . $web . '/' | ||||
104 | . $topic | ||||
105 | . $rcsSubDir . '/' | ||||
106 | . $attachment . ',v'; | ||||
107 | |||||
108 | } | ||||
109 | else { | ||||
110 | 1379 | 2.23ms | $this->{file} = | ||
111 | $Foswiki::cfg{DataDir} . '/' . $web . '/' . $topic . '.txt'; | ||||
112 | 1379 | 1.66ms | $this->{rcsFile} = | ||
113 | $Foswiki::cfg{DataDir} . '/' | ||||
114 | . $web | ||||
115 | . $rcsSubDir . '/' | ||||
116 | . $topic | ||||
117 | . '.txt,v'; | ||||
118 | } | ||||
119 | } | ||||
120 | |||||
121 | # Default to remembering changes for a month | ||||
122 | 1477 | 686µs | $Foswiki::cfg{Store}{RememberChangesFor} ||= 31 * 24 * 60 * 60; | ||
123 | |||||
124 | 1477 | 4.09ms | return $this; | ||
125 | } | ||||
126 | |||||
127 | =begin TML | ||||
128 | |||||
129 | ---++ ObjectMethod finish() | ||||
130 | Break circular references. | ||||
131 | |||||
132 | =cut | ||||
133 | |||||
134 | # Note to developers; please undef *all* fields in the object explicitly, | ||||
135 | # whether they are references or not. That way this method is "golden | ||||
136 | # documentation" of the live fields in the object. | ||||
137 | sub finish { | ||||
138 | my $this = shift; | ||||
139 | undef $this->{file}; | ||||
140 | undef $this->{rcsFile}; | ||||
141 | undef $this->{web}; | ||||
142 | undef $this->{topic}; | ||||
143 | undef $this->{attachment}; | ||||
144 | } | ||||
145 | |||||
146 | # Used in subclasses for late initialisation during object creation | ||||
147 | # (after the object is blessed into the subclass) | ||||
148 | sub init { | ||||
149 | my $this = shift; | ||||
150 | |||||
151 | return unless $this->{topic}; | ||||
152 | |||||
153 | unless ( -e $this->{file} ) { | ||||
154 | if ( $this->{attachment} && !$this->isAsciiDefault() ) { | ||||
155 | $this->initBinary(); | ||||
156 | } | ||||
157 | else { | ||||
158 | $this->initText(); | ||||
159 | } | ||||
160 | } | ||||
161 | } | ||||
162 | |||||
163 | # Make any missing paths on the way to this file | ||||
164 | sub mkPathTo { | ||||
165 | |||||
166 | my ( $this, $file ) = @_; | ||||
167 | |||||
168 | $file = Foswiki::Sandbox::untaintUnchecked($file); | ||||
169 | |||||
170 | ASSERT( File::Spec->file_name_is_absolute($file) ) if DEBUG; | ||||
171 | |||||
172 | my ( $volume, $path, undef ) = File::Spec->splitpath($file); | ||||
173 | $path = File::Spec->catpath( $volume, $path, '' ); | ||||
174 | |||||
175 | # SMELL: Sites running Apache with SuexecUserGroup will have a forced "safe" umask | ||||
176 | # Override umask here to allow correct dirPermissions to be applied | ||||
177 | umask( oct(777) - $Foswiki::cfg{RCS}{dirPermission} ); | ||||
178 | |||||
179 | eval { File::Path::mkpath( $path, 0, $Foswiki::cfg{RCS}{dirPermission} ); }; | ||||
180 | if ($@) { | ||||
181 | throw Error::Simple("VC::Handler: failed to create ${path}: $!"); | ||||
182 | } | ||||
183 | } | ||||
184 | |||||
185 | sub _epochToRcsDateTime { | ||||
186 | my ($dateTime) = @_; | ||||
187 | |||||
188 | # TODO: should this be gmtime or local time? | ||||
189 | my ( $sec, $min, $hour, $mday, $mon, $year, $wday, $yday ) = | ||||
190 | gmtime($dateTime); | ||||
191 | $year += 1900 if ( $year > 99 ); | ||||
192 | my $rcsDateTime = sprintf '%d.%02d.%02d.%02d.%02d.%02d', | ||||
193 | ( $year, $mon + 1, $mday, $hour, $min, $sec ); | ||||
194 | return $rcsDateTime; | ||||
195 | } | ||||
196 | |||||
197 | # filenames for lock and lease files | ||||
198 | sub _controlFileName { | ||||
199 | my ( $this, $type ) = @_; | ||||
200 | |||||
201 | my $fn = $this->{file} || ''; | ||||
202 | $fn =~ s/txt$/$type/; | ||||
203 | return $fn; | ||||
204 | } | ||||
205 | |||||
206 | =begin TML | ||||
207 | |||||
208 | ---++ ObjectMethod getInfo($version) -> \%info | ||||
209 | |||||
210 | * =$version= if 0 or undef, or out of range (version number > number of revs) will return info about the latest revision. | ||||
211 | |||||
212 | Returns info where version is the number of the rev for which the info was recovered, date is the date of that rev (epoch s), user is the canonical user ID of the user who saved that rev, and comment is the comment associated with the rev. | ||||
213 | |||||
214 | Designed to be overridden by subclasses, which can call up to this method | ||||
215 | if simple file-based rev info is required. | ||||
216 | |||||
217 | =cut | ||||
218 | |||||
219 | # spent 2.49s (1.58ms+2.49) within Foswiki::Store::VC::Handler::getInfo which was called 22 times, avg 113ms/call:
# 22 times (1.58ms+2.49s) by Foswiki::Store::VC::RcsWrapHandler::getInfo at line 346 of /var/www/foswiki11/lib/Foswiki/Store/VC/RcsWrapHandler.pm, avg 113ms/call | ||||
220 | 22 | 50µs | my $this = | ||
221 | shift; # $version is not useful here, as we have no way to record history | ||||
222 | |||||
223 | # SMELL: this is only required for the constant | ||||
224 | 22 | 71µs | require Foswiki::Users::BaseUserMapping; | ||
225 | |||||
226 | # We only arrive here if the implementation getInfo can't serve the info; this | ||||
227 | # will usually be because the ,v is missing or the topic cache is newer. | ||||
228 | |||||
229 | # If there is a .txt file, grab the TOPICINFO from it. | ||||
230 | # Note that we only peek at the first line of the file, | ||||
231 | # which is where a "proper" save will have left the tag. | ||||
232 | 22 | 85µs | my $info = {}; | ||
233 | 22 | 335µs | 22 | 876µs | if ( $this->noCheckinPending() ) { # spent 876µs making 22 calls to Foswiki::Store::VC::Handler::noCheckinPending, avg 40µs/call |
234 | |||||
235 | # TOPICINFO may be OK | ||||
236 | $this->_getTOPICINFO($info); | ||||
237 | } | ||||
238 | elsif ( -e $this->{rcsFile} ) { | ||||
239 | |||||
240 | # There is a checkin pending, and there is an rcs file. | ||||
241 | # Ignore TOPICINFO | ||||
242 | 10 | 198µs | 10 | 2.48s | $info->{version} = $this->_numRevisions() + 1; # spent 2.48s making 10 calls to Foswiki::Store::VC::RcsWrapHandler::_numRevisions, avg 248ms/call |
243 | 10 | 38µs | $info->{comment} = "pending"; | ||
244 | } | ||||
245 | else { | ||||
246 | |||||
247 | # There is a checkin pending, but no RCS file. Make the best we can of TOPICINFO. | ||||
248 | 12 | 68µs | 12 | 4.45ms | $this->_getTOPICINFO($info); # spent 4.45ms making 12 calls to Foswiki::Store::VC::Handler::_getTOPICINFO, avg 370µs/call |
249 | 12 | 18µs | $info->{version} = 1; | ||
250 | 12 | 18µs | $info->{comment} = "pending"; | ||
251 | } | ||||
252 | 22 | 343µs | 11 | 743µs | $info->{date} = $this->getTimestamp() unless defined $info->{date}; # spent 743µs making 11 calls to Foswiki::Store::VC::Handler::getTimestamp, avg 68µs/call |
253 | 22 | 22µs | $info->{version} = 1 unless defined $info->{version}; | ||
254 | 22 | 16µs | $info->{comment} = '' unless defined $info->{comment}; | ||
255 | 22 | 39µs | $info->{author} ||= $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID; | ||
256 | 22 | 197µs | return $info; | ||
257 | } | ||||
258 | |||||
259 | # Try and read TOPICINFO | ||||
260 | # spent 4.45ms (2.58+1.87) within Foswiki::Store::VC::Handler::_getTOPICINFO which was called 12 times, avg 370µs/call:
# 12 times (2.58ms+1.87ms) by Foswiki::Store::VC::Handler::getInfo at line 248, avg 370µs/call | ||||
261 | 12 | 25µs | my ( $this, $info ) = @_; | ||
262 | 12 | 3µs | my $f; | ||
263 | |||||
264 | 12 | 488µs | if ( open( $f, '<', $this->{file} ) ) { | ||
265 | 12 | 54µs | local $/ = "\n"; | ||
266 | 12 | 158µs | my $ti = <$f>; | ||
267 | 12 | 83µs | close($f); | ||
268 | 12 | 257µs | if ( defined $ti && $ti =~ /^%META:TOPICINFO{(.*)}%/ ) { | ||
269 | 11 | 156µs | require Foswiki::Attrs; | ||
270 | 11 | 237µs | 11 | 1.53ms | my $a = Foswiki::Attrs->new($1); # spent 1.53ms making 11 calls to Foswiki::Attrs::new, avg 139µs/call |
271 | |||||
272 | # Default bad revs to 1, not 0, because this is coming from | ||||
273 | # a topic on disk, so we know it's a "real" rev. | ||||
274 | 11 | 128µs | 11 | 206µs | $info->{version} = Foswiki::Store::cleanUpRevID( $a->{version} ) # spent 206µs making 11 calls to Foswiki::Store::cleanUpRevID, avg 19µs/call |
275 | || 1; | ||||
276 | 11 | 19µs | $info->{date} = $a->{date}; | ||
277 | 11 | 22µs | $info->{author} = $a->{author}; | ||
278 | 11 | 73µs | $info->{comment} = $a->{comment}; | ||
279 | } | ||||
280 | } | ||||
281 | } | ||||
282 | |||||
283 | # Check to see if there is a newer non-,v file waiting to be checked in. If there is, then | ||||
284 | # all rev numbers have to be incremented, as they will auto-increment when it is finally | ||||
285 | # checked in (usually as the result of a save). This is also used to test the validity of | ||||
286 | # TOPICINFO, as a pending checkin does not contain valid TOPICINFO. | ||||
287 | # spent 10.6ms within Foswiki::Store::VC::Handler::noCheckinPending which was called 473 times, avg 22µs/call:
# 426 times (9.33ms+0s) by Foswiki::Store::VC::Store::readTopic at line 96 of /var/www/foswiki11/lib/Foswiki/Store/VC/Store.pm, avg 22µs/call
# 22 times (876µs+0s) by Foswiki::Store::VC::Handler::getInfo at line 233, avg 40µs/call
# 22 times (276µs+0s) by Foswiki::Store::VC::RcsWrapHandler::getInfo at line 314 of /var/www/foswiki11/lib/Foswiki/Store/VC/RcsWrapHandler.pm, avg 13µs/call
# 3 times (82µs+0s) by Foswiki::Store::VC::Handler::getLatestRevisionID at line 453, avg 27µs/call | ||||
288 | 473 | 152µs | my $this = shift; | ||
289 | 473 | 123µs | my $isValid = 0; | ||
290 | |||||
291 | 473 | 3.47ms | if ( !-e $this->{file} ) { | ||
292 | $isValid = 1; # Hmmmm...... | ||||
293 | } | ||||
294 | else { | ||||
295 | 473 | 2.03ms | if ( -e $this->{rcsFile} ) { | ||
296 | |||||
297 | # Check the time on the rcs file; is the .txt newer? | ||||
298 | # Danger, Will Robinson! stat isn't reliable on all file systems, though [9] is claimed to be OK | ||||
299 | # See perldoc perlport for more on this. | ||||
300 | 436 | 268µs | local ${^WIN32_SLOPPY_STAT} = | ||
301 | 1; # don't need to open the file on Win32 | ||||
302 | 436 | 1.84ms | my $rcsTime = ( stat( $this->{rcsFile} ) )[9]; | ||
303 | 436 | 1.42ms | my $fileTime = ( stat( $this->{file} ) )[9]; | ||
304 | 436 | 359µs | $isValid = ( $rcsTime < $fileTime ) ? 0 : 1; | ||
305 | } | ||||
306 | } | ||||
307 | 473 | 1.63ms | return $isValid; | ||
308 | } | ||||
309 | |||||
310 | # Must be implemented by subclasses | ||||
311 | sub ci { | ||||
312 | die "Pure virtual method"; | ||||
313 | } | ||||
314 | |||||
315 | # Protected for use only in subclasses. Check that the object has a history | ||||
316 | # and the .txt is consistent with that history. | ||||
317 | sub _saveDamage { | ||||
318 | my $this = shift; | ||||
319 | return if $this->noCheckinPending(); | ||||
320 | |||||
321 | # the version in the TOPICINFO may not be correct. We need | ||||
322 | # to check the change in and update the TOPICINFO accordingly | ||||
323 | my $t = $this->readFile( $this->{file} ); | ||||
324 | |||||
325 | # If this is a topic, adjust the TOPICINFO | ||||
326 | if ( defined $this->{topic} && !defined $this->{attachment} ) { | ||||
327 | my $rev = -e $this->{rcsFile} ? $this->getLatestRevisionID() : 1; | ||||
328 | $t =~ s/^%META:TOPICINFO{(.*)}%$//m; | ||||
329 | $t = | ||||
330 | '%META:TOPICINFO{author="' | ||||
331 | . $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID | ||||
332 | . '" comment="autosave" date="' | ||||
333 | . time() | ||||
334 | . '" format="1.1" version="' | ||||
335 | . $rev . '"}%' . "\n$t"; | ||||
336 | } | ||||
337 | $this->ci( 0, $t, 'autosave', | ||||
338 | $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID, time() ); | ||||
339 | } | ||||
340 | |||||
341 | =begin TML | ||||
342 | |||||
343 | ---++ ObjectMethod addRevisionFromText($text, $comment, $cUID, $date) | ||||
344 | |||||
345 | Add new revision. Replace file with text. | ||||
346 | * =$text= of new revision | ||||
347 | * =$comment= checkin comment | ||||
348 | * =$cUID= is a cUID. | ||||
349 | * =$date= in epoch seconds; may be ignored | ||||
350 | |||||
351 | =cut | ||||
352 | |||||
353 | sub addRevisionFromText { | ||||
354 | my ( $this, $text, $comment, $user, $date ) = @_; | ||||
355 | $this->init(); | ||||
356 | |||||
357 | # Commit any out-of-band damage to .txt | ||||
358 | $this->_saveDamage(); | ||||
359 | $this->ci( 0, $text, $comment, $user, $date ); | ||||
360 | } | ||||
361 | |||||
362 | =begin TML | ||||
363 | |||||
364 | ---++ ObjectMethod addRevisionFromStream($fh, $comment, $cUID, $date) | ||||
365 | |||||
366 | Add new revision. Replace file with contents of stream. | ||||
367 | * =$fh= filehandle for contents of new revision | ||||
368 | * =$cUID= is a cUID. | ||||
369 | * =$date= in epoch seconds; may be ignored | ||||
370 | |||||
371 | =cut | ||||
372 | |||||
373 | sub addRevisionFromStream { | ||||
374 | my ( $this, $stream, $comment, $user, $date ) = @_; | ||||
375 | $this->init(); | ||||
376 | |||||
377 | # Commit any out-of-band damage to .txt | ||||
378 | $this->_saveDamage(); | ||||
379 | |||||
380 | $this->ci( 1, $stream, $comment, $user, $date ); | ||||
381 | } | ||||
382 | |||||
383 | =begin TML | ||||
384 | |||||
385 | ---++ ObjectMethod replaceRevision($text, $comment, $cUID, $date) | ||||
386 | |||||
387 | Replace the top revision. | ||||
388 | * =$text= is the new revision | ||||
389 | * =$date= is in epoch seconds. | ||||
390 | * =$cUID= is a cUID. | ||||
391 | * =$comment= is a string | ||||
392 | |||||
393 | =cut | ||||
394 | |||||
395 | sub replaceRevision { | ||||
396 | my $this = shift; | ||||
397 | $this->_saveDamage(); | ||||
398 | $this->repRev(@_); | ||||
399 | } | ||||
400 | |||||
401 | # Signature as for replaceRevision | ||||
402 | sub repRev { | ||||
403 | die "Pure virtual method"; | ||||
404 | } | ||||
405 | |||||
406 | =begin TML | ||||
407 | |||||
408 | ---++ ObjectMethod getRevisionHistory() -> $iterator | ||||
409 | |||||
410 | Get an iterator over the identifiers of revisions. Returns the most | ||||
411 | recent revision first. | ||||
412 | |||||
413 | The default is to return an iterator from the current version number | ||||
414 | down to 1. Return rev 1 if the file exists without history. Return | ||||
415 | an empty iterator if the file does not exist. | ||||
416 | |||||
417 | =cut | ||||
418 | |||||
419 | # spent 394ms (82µs+394) within Foswiki::Store::VC::Handler::getRevisionHistory which was called 2 times, avg 197ms/call:
# 2 times (82µs+394ms) by Foswiki::Store::VC::Store::getRevisionHistory at line 237 of /var/www/foswiki11/lib/Foswiki/Store/VC/Store.pm, avg 197ms/call | ||||
420 | 2 | 1µs | my $this = shift; | ||
421 | 2 | 2µs | 2 | 1µs | ASSERT( $this->{file} ) if DEBUG; # spent 1µs making 2 calls to Assert::ASSERTS_OFF, avg 700ns/call |
422 | 2 | 15µs | unless ( -e $this->{rcsFile} ) { | ||
423 | require Foswiki::ListIterator; | ||||
424 | if ( -e $this->{file} ) { | ||||
425 | return Foswiki::ListIterator->new( [1] ); | ||||
426 | } | ||||
427 | else { | ||||
428 | return Foswiki::ListIterator->new( [] ); | ||||
429 | } | ||||
430 | } | ||||
431 | |||||
432 | # SMELL: what happens with the working file? | ||||
433 | 2 | 9µs | 2 | 394ms | my $maxRev = $this->getLatestRevisionID(); # spent 394ms making 2 calls to Foswiki::Store::VC::Handler::getLatestRevisionID, avg 197ms/call |
434 | 2 | 40µs | 2 | 51µs | return Foswiki::Iterator::NumberRangeIterator->new( $maxRev, 1 ); # spent 51µs making 2 calls to Foswiki::Iterator::NumberRangeIterator::new, avg 25µs/call |
435 | } | ||||
436 | |||||
437 | =begin TML | ||||
438 | |||||
439 | ---++ ObjectMethod getLatestRevisionID() -> $id | ||||
440 | |||||
441 | Get the ID of the most recent revision. This may return undef if there have | ||||
442 | been no revisions committed to the store. | ||||
443 | |||||
444 | =cut | ||||
445 | |||||
446 | # spent 394ms (101µs+394) within Foswiki::Store::VC::Handler::getLatestRevisionID which was called 3 times, avg 131ms/call:
# 2 times (54µs+394ms) by Foswiki::Store::VC::Handler::getRevisionHistory at line 433, avg 197ms/call
# once (47µs+32µs) by Foswiki::Store::VC::Store::readTopic at line 116 of /var/www/foswiki11/lib/Foswiki/Store/VC/Store.pm | ||||
447 | 3 | 2µs | my $this = shift; | ||
448 | 3 | 15µs | return 0 unless -e $this->{file}; | ||
449 | 3 | 18µs | 3 | 394ms | my $rev = $this->_numRevisions() || 1; # spent 394ms making 3 calls to Foswiki::Store::VC::RcsWrapHandler::_numRevisions, avg 131ms/call |
450 | |||||
451 | # If there is a pending pseudo-revision, need n+1, but only if there is | ||||
452 | # an existing history | ||||
453 | 3 | 30µs | 3 | 82µs | $rev++ unless $this->noCheckinPending() || !-e $this->{rcsFile}; # spent 82µs making 3 calls to Foswiki::Store::VC::Handler::noCheckinPending, avg 27µs/call |
454 | 3 | 27µs | return $rev; | ||
455 | } | ||||
456 | |||||
457 | =begin TML | ||||
458 | |||||
459 | ---++ ObjectMethod getNextRevisionID() -> $id | ||||
460 | |||||
461 | Get the ID of the next (as yet uncreated) revision. The handler is required | ||||
462 | to implement this because the store has to be able to embed the revision | ||||
463 | ID into TOPICINFO before the revision is actually created. | ||||
464 | |||||
465 | If the file exists without revisions, then rev 1 does exist, so next rev | ||||
466 | should be rev 2, not rev 1, so the first change with missing history | ||||
467 | doesn't get merged into rev 1. | ||||
468 | |||||
469 | =cut | ||||
470 | |||||
471 | sub getNextRevisionID { | ||||
472 | my $this = shift; | ||||
473 | return $this->getLatestRevisionID() + 1; | ||||
474 | } | ||||
475 | |||||
476 | =begin TML | ||||
477 | |||||
478 | ---++ ObjectMethod getLatestRevisionTime() -> $text | ||||
479 | |||||
480 | Get the time of the most recent revision | ||||
481 | |||||
482 | =cut | ||||
483 | |||||
484 | sub getLatestRevisionTime { | ||||
485 | my @e = stat( shift->{file} ); | ||||
486 | return $e[9] || 0; | ||||
487 | } | ||||
488 | |||||
489 | =begin TML | ||||
490 | |||||
491 | ---++ ObjectMethod getTopicNames() -> @topics | ||||
492 | |||||
493 | Get list of all topics in a web | ||||
494 | * =$web= - Web name, required, e.g. ='Sandbox'= | ||||
495 | Return a topic list, e.g. =( 'WebChanges', 'WebHome', 'WebIndex', 'WebNotify' )= | ||||
496 | |||||
497 | =cut | ||||
498 | |||||
499 | # spent 315ms within Foswiki::Store::VC::Handler::getTopicNames which was called 41 times, avg 7.68ms/call:
# 41 times (315ms+0s) by Foswiki::Store::VC::Store::eachTopic at line 415 of /var/www/foswiki11/lib/Foswiki/Store/VC/Store.pm, avg 7.68ms/call | ||||
500 | 41 | 22µs | my $this = shift; | ||
501 | 41 | 9µs | my $dh; | ||
502 | 41 | 959µs | opendir( $dh, "$Foswiki::cfg{DataDir}/$this->{web}" ) | ||
503 | or return (); | ||||
504 | |||||
505 | # the name filter is used to ensure we don't return filenames | ||||
506 | # that contain illegal characters as topic names. | ||||
507 | 52483 | 23.2ms | my @topicList = | ||
508 | 157189 | 156ms | map { /^(.*)\.txt$/; $1; } | ||
509 | sort | ||||
510 | 41 | 129ms | grep { !/$Foswiki::cfg{NameFilter}/ && /\.txt$/ } readdir($dh); | ||
511 | 41 | 246µs | closedir($dh); | ||
512 | 41 | 10.9ms | return @topicList; | ||
513 | } | ||||
514 | |||||
515 | =begin TML | ||||
516 | |||||
517 | ---++ ObjectMethod revisionExists($rev) -> $boolean | ||||
518 | |||||
519 | Determine if the identified revision actually exists in the object | ||||
520 | history. | ||||
521 | |||||
522 | =cut | ||||
523 | |||||
524 | sub revisionExists { | ||||
525 | my ( $this, $rev ) = @_; | ||||
526 | |||||
527 | # Rev numbers run from 1 to numRevisions | ||||
528 | return $rev && $rev <= $this->_numRevisions(); | ||||
529 | } | ||||
530 | |||||
531 | =begin TML | ||||
532 | |||||
533 | ---++ ObjectMethod getWebNames() -> @webs | ||||
534 | |||||
535 | Gets a list of names of subwebs in the current web | ||||
536 | |||||
537 | =cut | ||||
538 | |||||
539 | # spent 83.8ms (81.7+2.14) within Foswiki::Store::VC::Handler::getWebNames which was called 102 times, avg 822µs/call:
# 102 times (81.7ms+2.14ms) by Foswiki::Store::VC::Store::eachWeb at line 429 of /var/www/foswiki11/lib/Foswiki/Store/VC/Store.pm, avg 822µs/call | ||||
540 | 102 | 26µs | my $this = shift; | ||
541 | 102 | 53µs | my $dir = $Foswiki::cfg{DataDir}; | ||
542 | 102 | 93µs | $dir .= '/' . $this->{web} if defined $this->{web}; | ||
543 | 102 | 15µs | my @tmpList; | ||
544 | 102 | 16µs | my $dh; | ||
545 | |||||
546 | 102 | 3.07ms | if ( opendir( $dh, $dir ) ) { | ||
547 | 100 | 227µs | 100 | 2.14ms | @tmpList = map { # spent 2.14ms making 100 calls to Foswiki::Sandbox::untaint, avg 21µs/call |
548 | 106636 | 26.4ms | Foswiki::Sandbox::untaint( $_, \&Foswiki::Sandbox::validateWebName ) | ||
549 | } | ||||
550 | |||||
551 | # The -e on the web preferences is used in preference to a | ||||
552 | # -d to avoid having to validate the web name each time. Since | ||||
553 | # the definition of a Web in this handler is "a directory with a | ||||
554 | # WebPreferences.txt in it", this works. | ||||
555 | 102 | 50.9ms | grep { !/\./ && -e "$dir/$_/$Foswiki::cfg{WebPrefsTopicName}.txt" } | ||
556 | readdir($dh); | ||||
557 | 102 | 327µs | closedir($dh); | ||
558 | } | ||||
559 | |||||
560 | 102 | 636µs | return @tmpList; | ||
561 | } | ||||
562 | |||||
563 | =begin TML | ||||
564 | |||||
565 | ---++ ObjectMethod moveWeb( $newWeb ) | ||||
566 | |||||
567 | Move a web. | ||||
568 | |||||
569 | =cut | ||||
570 | |||||
571 | sub moveWeb { | ||||
572 | my ( $this, $newWeb ) = @_; | ||||
573 | $this->moveFile( | ||||
574 | $Foswiki::cfg{DataDir} . '/' . $this->{web}, | ||||
575 | $Foswiki::cfg{DataDir} . '/' . $newWeb | ||||
576 | ); | ||||
577 | if ( -d $Foswiki::cfg{PubDir} . '/' . $this->{web} ) { | ||||
578 | $this->moveFile( | ||||
579 | $Foswiki::cfg{PubDir} . '/' . $this->{web}, | ||||
580 | $Foswiki::cfg{PubDir} . '/' . $newWeb | ||||
581 | ); | ||||
582 | } | ||||
583 | } | ||||
584 | |||||
585 | =begin TML | ||||
586 | |||||
587 | ---++ ObjectMethod getRevision($version) -> ($text, $isLatest) | ||||
588 | |||||
589 | * =$version= if 0 or undef, or out of range (version number > number of revs) will return the latest revision. | ||||
590 | |||||
591 | Get the text of the given revision, and a flag indicating if this is the | ||||
592 | most recent revision. | ||||
593 | |||||
594 | Designed to be overridden by subclasses, which can call up to this method | ||||
595 | if the main file revision is required. | ||||
596 | |||||
597 | =cut | ||||
598 | |||||
599 | # spent 43.7ms (5.70+38.1) within Foswiki::Store::VC::Handler::getRevision which was called 426 times, avg 103µs/call:
# 426 times (5.70ms+38.1ms) by Foswiki::Store::VC::RcsWrapHandler::getRevision at line 243 of /var/www/foswiki11/lib/Foswiki/Store/VC/RcsWrapHandler.pm, avg 103µs/call | ||||
600 | 426 | 174µs | my ($this) = @_; | ||
601 | 426 | 4.88ms | 426 | 38.1ms | if ( defined $this->{file} && -e $this->{file} ) { # spent 38.1ms making 426 calls to Foswiki::Store::VC::Handler::readFile, avg 89µs/call |
602 | return ( readFile( $this, $this->{file} ), 1 ); | ||||
603 | } | ||||
604 | return ( undef, 1 ); | ||||
605 | } | ||||
606 | |||||
607 | =begin TML | ||||
608 | |||||
609 | ---++ ObjectMethod storedDataExists() -> $boolean | ||||
610 | |||||
611 | Establishes if there is stored data associated with this handler. | ||||
612 | |||||
613 | =cut | ||||
614 | |||||
615 | # spent 427ms within Foswiki::Store::VC::Handler::storedDataExists which was called 46781 times, avg 9µs/call:
# 46604 times (425ms+0s) by Foswiki::Store::VC::Store::topicExists at line 385 of /var/www/foswiki11/lib/Foswiki/Store/VC/Store.pm, avg 9µs/call
# 176 times (2.12ms+0s) by Foswiki::Store::VC::Store::webExists at line 374 of /var/www/foswiki11/lib/Foswiki/Store/VC/Store.pm, avg 12µs/call
# once (14µs+0s) by Foswiki::Store::VC::Store::attachmentExists at line 185 of /var/www/foswiki11/lib/Foswiki/Store/VC/Store.pm | ||||
616 | 46781 | 12.1ms | my $this = shift; | ||
617 | 46781 | 28.3ms | return 0 unless $this->{file}; | ||
618 | 46781 | 537ms | return -e $this->{file}; | ||
619 | } | ||||
620 | |||||
621 | =begin TML | ||||
622 | |||||
623 | ---++ ObjectMethod restoreLatestRevision( $cUID ) | ||||
624 | |||||
625 | Restore the plaintext file from the revision at the head. | ||||
626 | |||||
627 | =cut | ||||
628 | |||||
629 | sub restoreLatestRevision { | ||||
630 | my ( $this, $cUID ) = @_; | ||||
631 | |||||
632 | my $rev = $this->getLatestRevisionID(); | ||||
633 | my ($text) = $this->getRevision($rev); | ||||
634 | |||||
635 | # If there is no ,v, create it | ||||
636 | unless ( -e $this->{rcsFile} ) { | ||||
637 | $this->addRevisionFromText( $text, "restored", $cUID, time() ); | ||||
638 | } | ||||
639 | else { | ||||
640 | saveFile( $this, $this->{file}, $text ); | ||||
641 | } | ||||
642 | } | ||||
643 | |||||
644 | =begin TML | ||||
645 | |||||
646 | ---++ ObjectMethod remove() | ||||
647 | |||||
648 | Destroy, utterly. Remove the data and attachments in the web. | ||||
649 | |||||
650 | Use with great care! No backup is taken! | ||||
651 | |||||
652 | =cut | ||||
653 | |||||
654 | sub remove { | ||||
655 | my $this = shift; | ||||
656 | |||||
657 | if ( !$this->{topic} ) { | ||||
658 | |||||
659 | # Web | ||||
660 | _rmtree( $Foswiki::cfg{DataDir} . '/' . $this->{web} ); | ||||
661 | _rmtree( $Foswiki::cfg{PubDir} . '/' . $this->{web} ); | ||||
662 | } | ||||
663 | else { | ||||
664 | |||||
665 | # Topic or attachment | ||||
666 | unlink( $this->{file} ); | ||||
667 | unlink( $this->{rcsFile} ); | ||||
668 | if ( !$this->{attachment} ) { | ||||
669 | _rmtree($Foswiki::cfg{PubDir} . '/' | ||||
670 | . $this->{web} . '/' | ||||
671 | . $this->{topic} ); | ||||
672 | } | ||||
673 | } | ||||
674 | } | ||||
675 | |||||
676 | =begin TML | ||||
677 | |||||
678 | ---++ ObjectMethod moveTopic( $store, $newWeb, $newTopic ) | ||||
679 | |||||
680 | Move/rename a topic. | ||||
681 | |||||
682 | =cut | ||||
683 | |||||
684 | sub moveTopic { | ||||
685 | my ( $this, $store, $newWeb, $newTopic ) = @_; | ||||
686 | |||||
687 | ASSERT( $store->isa('Foswiki::Store') ) if DEBUG; | ||||
688 | |||||
689 | my $oldWeb = $this->{web}; | ||||
690 | my $oldTopic = $this->{topic}; | ||||
691 | |||||
692 | # Move data file | ||||
693 | my $new = $store->getHandler( $newWeb, $newTopic ); | ||||
694 | $this->moveFile( $this->{file}, $new->{file} ); | ||||
695 | |||||
696 | # Move history | ||||
697 | $this->mkPathTo( $new->{rcsFile} ); | ||||
698 | if ( -e $this->{rcsFile} ) { | ||||
699 | $this->moveFile( $this->{rcsFile}, $new->{rcsFile} ); | ||||
700 | } | ||||
701 | |||||
702 | # Move attachments | ||||
703 | my $from = | ||||
704 | $Foswiki::cfg{PubDir} . '/' . $this->{web} . '/' . $this->{topic}; | ||||
705 | if ( -e $from ) { | ||||
706 | my $to = $Foswiki::cfg{PubDir} . '/' . $newWeb . '/' . $newTopic; | ||||
707 | $this->moveFile( $from, $to ); | ||||
708 | } | ||||
709 | } | ||||
710 | |||||
711 | =begin TML | ||||
712 | |||||
713 | ---++ ObjectMethod copyTopic( $store, $newWeb, $newTopic ) | ||||
714 | |||||
715 | Copy a topic. | ||||
716 | |||||
717 | =cut | ||||
718 | |||||
719 | sub copyTopic { | ||||
720 | my ( $this, $store, $newWeb, $newTopic ) = @_; | ||||
721 | |||||
722 | ASSERT( $store->isa('Foswiki::Store') ) if DEBUG; | ||||
723 | |||||
724 | my $oldWeb = $this->{web}; | ||||
725 | my $oldTopic = $this->{topic}; | ||||
726 | |||||
727 | my $new = $store->getHandler( $newWeb, $newTopic ); | ||||
728 | |||||
729 | $this->copyFile( $this->{file}, $new->{file} ); | ||||
730 | if ( -e $this->{rcsFile} ) { | ||||
731 | $this->copyFile( $this->{rcsFile}, $new->{rcsFile} ); | ||||
732 | } | ||||
733 | |||||
734 | my $dh; | ||||
735 | if ( opendir( $dh, "$Foswiki::cfg{PubDir}/$this->{web}/$this->{topic}" ) ) { | ||||
736 | for my $att ( grep { !/^\./ } readdir $dh ) { | ||||
737 | $att = Foswiki::Sandbox::untaint( $att, | ||||
738 | \&Foswiki::Sandbox::validateAttachmentName ); | ||||
739 | my $oldAtt = | ||||
740 | $store->getHandler( $this->{web}, $this->{topic}, $att ); | ||||
741 | $oldAtt->copyAttachment( $store, $newWeb, $newTopic ); | ||||
742 | } | ||||
743 | |||||
744 | closedir $dh; | ||||
745 | } | ||||
746 | } | ||||
747 | |||||
748 | =begin TML | ||||
749 | |||||
750 | ---++ ObjectMethod moveAttachment( $store, $newWeb, $newTopic, $newAttachment ) | ||||
751 | |||||
752 | Move an attachment from one topic to another. The name is retained. | ||||
753 | |||||
754 | =cut | ||||
755 | |||||
756 | sub moveAttachment { | ||||
757 | my ( $this, $store, $newWeb, $newTopic, $newAttachment ) = @_; | ||||
758 | |||||
759 | ASSERT( $store->isa('Foswiki::Store') ) if DEBUG; | ||||
760 | |||||
761 | # FIXME might want to delete old directories if empty | ||||
762 | my $new = $store->getHandler( $newWeb, $newTopic, $newAttachment ); | ||||
763 | |||||
764 | $this->moveFile( $this->{file}, $new->{file} ); | ||||
765 | |||||
766 | if ( -e $this->{rcsFile} ) { | ||||
767 | $this->moveFile( $this->{rcsFile}, $new->{rcsFile} ); | ||||
768 | } | ||||
769 | } | ||||
770 | |||||
771 | =begin TML | ||||
772 | |||||
773 | ---++ ObjectMethod copyAttachment( $store, $newWeb, $newTopic, $newAttachment ) | ||||
774 | |||||
775 | Copy an attachment from one topic to another. The name is retained unless | ||||
776 | $newAttachment is defined. | ||||
777 | |||||
778 | =cut | ||||
779 | |||||
780 | sub copyAttachment { | ||||
781 | my ( $this, $store, $newWeb, $newTopic, $attachment ) = @_; | ||||
782 | |||||
783 | ASSERT( $store->isa('Foswiki::Store') ) if DEBUG; | ||||
784 | |||||
785 | my $oldWeb = $this->{web}; | ||||
786 | my $oldTopic = $this->{topic}; | ||||
787 | $attachment ||= $this->{attachment}; | ||||
788 | |||||
789 | my $new = $store->getHandler( $newWeb, $newTopic, $attachment ); | ||||
790 | |||||
791 | $this->copyFile( $this->{file}, $new->{file} ); | ||||
792 | |||||
793 | if ( -e $this->{rcsFile} ) { | ||||
794 | $this->copyFile( $this->{rcsFile}, $new->{rcsFile} ); | ||||
795 | } | ||||
796 | } | ||||
797 | |||||
798 | =begin TML | ||||
799 | |||||
800 | ---++ ObjectMethod isAsciiDefault ( ) -> $boolean | ||||
801 | |||||
802 | Check if this file type is known to be an ascii type file. | ||||
803 | |||||
804 | =cut | ||||
805 | |||||
806 | sub isAsciiDefault { | ||||
807 | my $this = shift; | ||||
808 | return ( $this->{attachment} =~ /$Foswiki::cfg{RCS}{asciiFileSuffixes}/ ); | ||||
809 | } | ||||
810 | |||||
811 | =begin TML | ||||
812 | |||||
813 | ---++ ObjectMethod setLock($lock, $cUID) | ||||
814 | |||||
815 | Set a lock on the topic, if $lock, otherwise clear it. | ||||
816 | $cUID is a cUID. | ||||
817 | |||||
818 | SMELL: there is a tremendous amount of potential for race | ||||
819 | conditions using this locking approach. | ||||
820 | |||||
821 | It would be nice to use flock to do this, but the API is unreliable | ||||
822 | (doesn't work on all platforms) | ||||
823 | |||||
824 | =cut | ||||
825 | |||||
826 | sub setLock { | ||||
827 | my ( $this, $lock, $cUID ) = @_; | ||||
828 | |||||
829 | my $filename = _controlFileName( $this, 'lock' ); | ||||
830 | if ($lock) { | ||||
831 | my $lockTime = time(); | ||||
832 | saveFile( $this, $filename, $cUID . "\n" . $lockTime ); | ||||
833 | } | ||||
834 | else { | ||||
835 | unlink $filename | ||||
836 | || throw Error::Simple( | ||||
837 | 'VC::Handler: failed to delete ' . $filename . ': ' . $! ); | ||||
838 | } | ||||
839 | } | ||||
840 | |||||
841 | =begin TML | ||||
842 | |||||
843 | ---++ ObjectMethod isLocked( ) -> ($cUID, $time) | ||||
844 | |||||
845 | See if a lock exists. Return the lock user and lock time if it does. | ||||
846 | |||||
847 | =cut | ||||
848 | |||||
849 | sub isLocked { | ||||
850 | my $this = shift; | ||||
851 | |||||
852 | my $filename = _controlFileName( $this, 'lock' ); | ||||
853 | if ( -e $filename ) { | ||||
854 | my $t = readFile( $this, $filename ); | ||||
855 | return split( /\s+/, $t, 2 ); | ||||
856 | } | ||||
857 | return ( undef, undef ); | ||||
858 | } | ||||
859 | |||||
860 | =begin TML | ||||
861 | |||||
862 | ---++ ObjectMethod setLease( $lease ) | ||||
863 | |||||
864 | * =$lease= reference to lease hash, or undef if the existing lease is to be cleared. | ||||
865 | |||||
866 | Set an lease on the topic. | ||||
867 | |||||
868 | =cut | ||||
869 | |||||
870 | sub setLease { | ||||
871 | my ( $this, $lease ) = @_; | ||||
872 | |||||
873 | my $filename = _controlFileName( $this, 'lease' ); | ||||
874 | if ($lease) { | ||||
875 | saveFile( $this, $filename, join( "\n", %$lease ) ); | ||||
876 | } | ||||
877 | elsif ( -e $filename ) { | ||||
878 | unlink $filename | ||||
879 | || throw Error::Simple( | ||||
880 | 'VC::Handler: failed to delete ' . $filename . ': ' . $! ); | ||||
881 | } | ||||
882 | } | ||||
883 | |||||
884 | =begin TML | ||||
885 | |||||
886 | ---++ ObjectMethod getLease() -> $lease | ||||
887 | |||||
888 | Get the current lease on the topic. | ||||
889 | |||||
890 | =cut | ||||
891 | |||||
892 | sub getLease { | ||||
893 | my ($this) = @_; | ||||
894 | |||||
895 | my $filename = _controlFileName( $this, 'lease' ); | ||||
896 | if ( -e $filename ) { | ||||
897 | my $t = readFile( $this, $filename ); | ||||
898 | my $lease = { split( /\r?\n/, $t ) }; | ||||
899 | return $lease; | ||||
900 | } | ||||
901 | return; | ||||
902 | } | ||||
903 | |||||
904 | =begin TML | ||||
905 | |||||
906 | ---++ ObjectMethod removeSpuriousLeases( $web ) | ||||
907 | |||||
908 | Remove leases that are not related to a topic. These can get left behind in | ||||
909 | some store implementations when a topic is created, but never saved. | ||||
910 | |||||
911 | =cut | ||||
912 | |||||
913 | sub removeSpuriousLeases { | ||||
914 | my ($this) = @_; | ||||
915 | my $web = $Foswiki::cfg{DataDir} . '/' . $this->{web} . '/'; | ||||
916 | if ( opendir( my $W, $web ) ) { | ||||
917 | foreach my $f ( readdir($W) ) { | ||||
918 | my $file = $web . $f; | ||||
919 | if ( $file =~ /^(.*)\.lease$/ ) { | ||||
920 | if ( !-e "$1.txt,v" ) { | ||||
921 | unlink("$1.lease"); # $file is tainted | ||||
922 | } | ||||
923 | } | ||||
924 | } | ||||
925 | closedir($W); | ||||
926 | } | ||||
927 | } | ||||
928 | |||||
929 | sub test { | ||||
930 | my ( $this, $test ) = @_; | ||||
931 | return eval "-$test '$this->{file}'"; | ||||
932 | } | ||||
933 | |||||
934 | # Used by subclasses | ||||
935 | sub saveStream { | ||||
936 | my ( $this, $fh ) = @_; | ||||
937 | |||||
938 | ASSERT($fh) if DEBUG; | ||||
939 | |||||
940 | $this->mkPathTo( $this->{file} ); | ||||
941 | my $F; | ||||
942 | open( $F, '>', $this->{file} ) | ||||
943 | || throw Error::Simple( | ||||
944 | 'VC::Handler: open ' . $this->{file} . ' failed: ' . $! ); | ||||
945 | binmode($F) | ||||
946 | || throw Error::Simple( | ||||
947 | 'VC::Handler: failed to binmode ' . $this->{file} . ': ' . $! ); | ||||
948 | my $text; | ||||
949 | while ( read( $fh, $text, 1024 ) ) { | ||||
950 | print $F $text; | ||||
951 | } | ||||
952 | close($F) | ||||
953 | || throw Error::Simple( | ||||
954 | 'VC::Handler: close ' . $this->{file} . ' failed: ' . $! ); | ||||
955 | |||||
956 | chmod( $Foswiki::cfg{RCS}{filePermission}, $this->{file} ); | ||||
957 | } | ||||
958 | |||||
959 | sub copyFile { | ||||
960 | my ( $this, $from, $to ) = @_; | ||||
961 | |||||
962 | $this->mkPathTo($to); | ||||
963 | unless ( File::Copy::copy( $from, $to ) ) { | ||||
964 | throw Error::Simple( | ||||
965 | 'VC::Handler: copy ' . $from . ' to ' . $to . ' failed: ' . $! ); | ||||
966 | } | ||||
967 | } | ||||
968 | |||||
969 | sub moveFile { | ||||
970 | my ( $this, $from, $to ) = @_; | ||||
971 | ASSERT( -e $from ) if DEBUG; | ||||
972 | $this->mkPathTo($to); | ||||
973 | unless ( File::Copy::move( $from, $to ) ) { | ||||
974 | throw Error::Simple( | ||||
975 | 'VC::Handler: move ' . $from . ' to ' . $to . ' failed: ' . $! ); | ||||
976 | } | ||||
977 | } | ||||
978 | |||||
979 | # Used by subclasses | ||||
980 | sub saveFile { | ||||
981 | my ( $this, $name, $text ) = @_; | ||||
982 | |||||
983 | $this->mkPathTo($name); | ||||
984 | my $fh; | ||||
985 | open( $fh, '>', $name ) | ||||
986 | or throw Error::Simple( | ||||
987 | 'VC::Handler: failed to create file ' . $name . ': ' . $! ); | ||||
988 | flock( $fh, LOCK_EX ) | ||||
989 | or throw Error::Simple( | ||||
990 | 'VC::Handler: failed to lock file ' . $name . ': ' . $! ); | ||||
991 | binmode($fh) | ||||
992 | or throw Error::Simple( | ||||
993 | 'VC::Handler: failed to binmode ' . $name . ': ' . $! ); | ||||
994 | print $fh $text | ||||
995 | or throw Error::Simple( | ||||
996 | 'VC::Handler: failed to print into ' . $name . ': ' . $! ); | ||||
997 | close($fh) | ||||
998 | or throw Error::Simple( | ||||
999 | 'VC::Handler: failed to close file ' . $name . ': ' . $! ); | ||||
1000 | return; | ||||
1001 | } | ||||
1002 | |||||
1003 | # Used by subclasses | ||||
1004 | # spent 38.1ms (37.7+395µs) within Foswiki::Store::VC::Handler::readFile which was called 426 times, avg 89µs/call:
# 426 times (37.7ms+395µs) by Foswiki::Store::VC::Handler::getRevision at line 601, avg 89µs/call | ||||
1005 | 426 | 255µs | my ( $this, $name ) = @_; | ||
1006 | 426 | 417µs | 426 | 395µs | ASSERT($name) if DEBUG; # spent 395µs making 426 calls to Assert::ASSERTS_OFF, avg 927ns/call |
1007 | 426 | 70µs | my $data; | ||
1008 | 426 | 43µs | my $IN_FILE; | ||
1009 | 426 | 7.42ms | if ( open( $IN_FILE, '<', $name ) ) { | ||
1010 | 426 | 485µs | binmode($IN_FILE); | ||
1011 | 426 | 1.18ms | local $/ = undef; | ||
1012 | 426 | 21.5ms | $data = <$IN_FILE>; | ||
1013 | 426 | 3.18ms | close($IN_FILE); | ||
1014 | } | ||||
1015 | 426 | 111µs | $data ||= ''; | ||
1016 | 426 | 3.39ms | return $data; | ||
1017 | } | ||||
1018 | |||||
1019 | # Used by subclasses | ||||
1020 | sub mkTmpFilename { | ||||
1021 | my $tmpdir = File::Spec->tmpdir(); | ||||
1022 | my $file = _mktemp( 'foswikiAttachmentXXXXXX', $tmpdir ); | ||||
1023 | return File::Spec->catfile( $tmpdir, $file ); | ||||
1024 | } | ||||
1025 | |||||
1026 | # Adapted from CPAN - File::MkTemp | ||||
1027 | sub _mktemp { | ||||
1028 | my ( $template, $dir, $ext, $keepgen, $lookup ); | ||||
1029 | my ( @template, @letters ); | ||||
1030 | |||||
1031 | ASSERT( @_ == 1 || @_ == 2 || @_ == 3 ) if DEBUG; | ||||
1032 | |||||
1033 | ( $template, $dir, $ext ) = @_; | ||||
1034 | @template = split //, $template; | ||||
1035 | |||||
1036 | ASSERT( $template =~ /XXXXXX$/ ) if DEBUG; | ||||
1037 | |||||
1038 | if ($dir) { | ||||
1039 | ASSERT( -e $dir ) if DEBUG; | ||||
1040 | } | ||||
1041 | |||||
1042 | @letters = | ||||
1043 | split( //, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ); | ||||
1044 | |||||
1045 | $keepgen = 1; | ||||
1046 | |||||
1047 | while ($keepgen) { | ||||
1048 | for ( my $i = $#template ; $i >= 0 && ( $template[$i] eq 'X' ) ; $i-- ) | ||||
1049 | { | ||||
1050 | $template[$i] = $letters[ int( rand 52 ) ]; | ||||
1051 | } | ||||
1052 | |||||
1053 | undef $template; | ||||
1054 | |||||
1055 | $template = pack 'a' x @template, @template; | ||||
1056 | |||||
1057 | $template = $template . $ext if ($ext); | ||||
1058 | |||||
1059 | if ($dir) { | ||||
1060 | $lookup = File::Spec->catfile( $dir, $template ); | ||||
1061 | $keepgen = 0 unless ( -e $lookup ); | ||||
1062 | } | ||||
1063 | else { | ||||
1064 | $keepgen = 0; | ||||
1065 | } | ||||
1066 | |||||
1067 | next if $keepgen == 0; | ||||
1068 | } | ||||
1069 | |||||
1070 | return ($template); | ||||
1071 | } | ||||
1072 | |||||
1073 | # remove a directory and all subdirectories. | ||||
1074 | sub _rmtree { | ||||
1075 | my $root = shift; | ||||
1076 | my $D; | ||||
1077 | |||||
1078 | if ( opendir( $D, $root ) ) { | ||||
1079 | foreach my $entry ( grep { !/^\.+$/ } readdir($D) ) { | ||||
1080 | $entry =~ /^(.*)$/; | ||||
1081 | $entry = $root . '/' . $1; | ||||
1082 | if ( -d $entry ) { | ||||
1083 | _rmtree($entry); | ||||
1084 | } | ||||
1085 | elsif ( !unlink($entry) && -e $entry ) { | ||||
1086 | if ( $Foswiki::cfg{OS} ne 'WINDOWS' ) { | ||||
1087 | throw Error::Simple( 'VC::Handler: Failed to delete file ' | ||||
1088 | . $entry . ': ' | ||||
1089 | . $! ); | ||||
1090 | } | ||||
1091 | else { | ||||
1092 | |||||
1093 | # Windows sometimes fails to delete files when | ||||
1094 | # subprocesses haven't exited yet, because the | ||||
1095 | # subprocess still has the file open. Live with it. | ||||
1096 | print STDERR 'WARNING: Failed to delete file ', | ||||
1097 | $entry, ": $!\n"; | ||||
1098 | } | ||||
1099 | } | ||||
1100 | } | ||||
1101 | closedir($D); | ||||
1102 | |||||
1103 | if ( !rmdir($root) ) { | ||||
1104 | if ( $Foswiki::cfg{OS} ne 'WINDOWS' ) { | ||||
1105 | throw Error::Simple( | ||||
1106 | 'VC::Handler: Failed to delete ' . $root . ': ' . $! ); | ||||
1107 | } | ||||
1108 | else { | ||||
1109 | print STDERR 'WARNING: Failed to delete ' . $root . ': ' . $!, | ||||
1110 | "\n"; | ||||
1111 | } | ||||
1112 | } | ||||
1113 | } | ||||
1114 | } | ||||
1115 | |||||
1116 | { | ||||
1117 | |||||
1118 | # Package that ties a filehandle to a memory string for reading | ||||
1119 | 1 | 700ns | package Foswiki::Store::_MemoryFile; | ||
1120 | |||||
1121 | sub TIEHANDLE { | ||||
1122 | my ( $class, $data ) = @_; | ||||
1123 | return | ||||
1124 | bless( { data => $data, size => length($data), ptr => 0 }, $class ); | ||||
1125 | } | ||||
1126 | |||||
1127 | sub READ { | ||||
1128 | my $this = shift; | ||||
1129 | my ( undef, $len, $offset ) = @_; | ||||
1130 | if ( $this->{size} - $this->{ptr} < $len ) { | ||||
1131 | $len = $this->{size} - $this->{ptr}; | ||||
1132 | } | ||||
1133 | return 0 unless $len; | ||||
1134 | $_[0] = substr( $this->{data}, $this->{ptr}, $len ); | ||||
1135 | $this->{ptr} += $len; | ||||
1136 | return $len; | ||||
1137 | } | ||||
1138 | |||||
1139 | sub READLINE { | ||||
1140 | my $this = shift; | ||||
1141 | return if $this->{ptr} == $this->{size}; | ||||
1142 | return substr( $this->{data}, $this->{ptr} ) if !defined $/; | ||||
1143 | my $start = $this->{ptr}; | ||||
1144 | while ( $this->{ptr} < $this->{size} | ||||
1145 | && substr( $this->{data}, $this->{ptr}, 1 ) ne $/ ) | ||||
1146 | { | ||||
1147 | $this->{ptr}++; | ||||
1148 | } | ||||
1149 | $this->{ptr}++ if $this->{ptr} < $this->{size}; | ||||
1150 | return substr( $this->{data}, $start, $this->{ptr} - $start ); | ||||
1151 | } | ||||
1152 | |||||
1153 | sub CLOSE { | ||||
1154 | my $this = shift; | ||||
1155 | $this->{data} = undef; | ||||
1156 | } | ||||
1157 | } | ||||
1158 | |||||
1159 | =begin TML | ||||
1160 | |||||
1161 | ---++ ObjectMethod openStream($mode, %opts) -> $fh | ||||
1162 | |||||
1163 | Opens a file handle onto the store. This method is primarily to | ||||
1164 | support virtual file systems. | ||||
1165 | |||||
1166 | =$mode= can be '<', '>' or '>>' for read, write, and append | ||||
1167 | respectively. % | ||||
1168 | |||||
1169 | =%opts= can take different settings depending on =$mode=. | ||||
1170 | * =$mode='<'= | ||||
1171 | * =version= - revision of the object to open e.g. =version => 6= | ||||
1172 | Default behaviour is to return the latest revision. Note that it is | ||||
1173 | much more efficient to pass undef than to pass the number of the | ||||
1174 | latest revision. | ||||
1175 | * =$mode='>'= or ='>>' | ||||
1176 | * no options | ||||
1177 | |||||
1178 | =cut | ||||
1179 | |||||
1180 | sub openStream { | ||||
1181 | my ( $this, $mode, %opts ) = @_; | ||||
1182 | my $stream; | ||||
1183 | if ( $mode eq '<' && $opts{version} ) { | ||||
1184 | |||||
1185 | # Bulk load the revision and tie a filehandle | ||||
1186 | require Symbol; | ||||
1187 | $stream = Symbol::gensym; # create an anonymous glob | ||||
1188 | tie( *$stream, 'Foswiki::Store::_MemoryFile', | ||||
1189 | $this->getRevision( $opts{version} ) ); | ||||
1190 | } | ||||
1191 | else { | ||||
1192 | if ( $mode =~ />/ ) { | ||||
1193 | $this->mkPathTo( $this->{file} ); | ||||
1194 | } | ||||
1195 | unless ( open( $stream, $mode, $this->{file} ) ) { | ||||
1196 | throw Error::Simple( 'VC::Handler: stream open ' | ||||
1197 | . $this->{file} | ||||
1198 | . ' failed: ' | ||||
1199 | . $! ); | ||||
1200 | } | ||||
1201 | binmode $stream; | ||||
1202 | } | ||||
1203 | return $stream; | ||||
1204 | } | ||||
1205 | |||||
1206 | # as long as stat is defined, return an emulated set of attributes for that | ||||
1207 | # attachment. | ||||
1208 | sub _constructAttributesForAutoAttached { | ||||
1209 | my ( $file, $stat ) = @_; | ||||
1210 | |||||
1211 | my %pairs = ( | ||||
1212 | name => $file, | ||||
1213 | version => '', | ||||
1214 | path => $file, | ||||
1215 | size => $stat->[7], | ||||
1216 | date => $stat->[9], | ||||
1217 | |||||
1218 | # user => 'UnknownUser', #safer _not_ to default - Foswiki will fill it in when it needs to | ||||
1219 | comment => '', | ||||
1220 | attr => '', | ||||
1221 | autoattached => '1' | ||||
1222 | ); | ||||
1223 | |||||
1224 | if ( $#$stat > 0 ) { | ||||
1225 | return \%pairs; | ||||
1226 | } | ||||
1227 | else { | ||||
1228 | return; | ||||
1229 | } | ||||
1230 | } | ||||
1231 | |||||
1232 | =begin TML | ||||
1233 | |||||
1234 | ---++ ObjectMethod synchroniseAttachmentsList(\@old) -> @new | ||||
1235 | |||||
1236 | Synchronise the attachment list from meta-data with what's actually | ||||
1237 | stored in the DB. Returns an ARRAY of FILEATTACHMENTs. These can be | ||||
1238 | put in the new tom. | ||||
1239 | |||||
1240 | This function is only called when the {RCS}{AutoAttachPubFiles} configuration | ||||
1241 | option is set. | ||||
1242 | |||||
1243 | =cut | ||||
1244 | |||||
1245 | # IDEA On Windows machines where the underlying filesystem can store arbitary | ||||
1246 | # meta data against files, this might replace/fulfil the COMMENT purpose | ||||
1247 | # | ||||
1248 | # TODO consider logging when things are added to metadata | ||||
1249 | |||||
1250 | sub synchroniseAttachmentsList { | ||||
1251 | my ( $this, $attachmentsKnownInMeta ) = @_; | ||||
1252 | |||||
1253 | my %filesListedInPub = $this->_getAttachmentStats(); | ||||
1254 | my %filesListedInMeta = (); | ||||
1255 | |||||
1256 | # You need the following lines if you want metadata to supplement | ||||
1257 | # the filesystem | ||||
1258 | if ( defined $attachmentsKnownInMeta ) { | ||||
1259 | %filesListedInMeta = | ||||
1260 | map { $_->{name} => $_ } @$attachmentsKnownInMeta; | ||||
1261 | } | ||||
1262 | |||||
1263 | foreach my $file ( keys %filesListedInPub ) { | ||||
1264 | if ( $filesListedInMeta{$file} ) { | ||||
1265 | |||||
1266 | # Bring forward any missing yet wanted attributes | ||||
1267 | foreach my $field (qw(comment attr user version)) { | ||||
1268 | if ( $filesListedInMeta{$file}{$field} ) { | ||||
1269 | $filesListedInPub{$file}{$field} = | ||||
1270 | $filesListedInMeta{$file}{$field}; | ||||
1271 | } | ||||
1272 | } | ||||
1273 | } | ||||
1274 | } | ||||
1275 | |||||
1276 | # A comparison of the keys of the $filesListedInMeta and %filesListedInPub | ||||
1277 | # would show files that were in Meta but have disappeared from Pub. | ||||
1278 | |||||
1279 | # Do not change this from array to hash, you would lose the | ||||
1280 | # proper attachment sequence | ||||
1281 | my @deindexedBecauseMetaDoesnotIndexAttachments = values(%filesListedInPub); | ||||
1282 | |||||
1283 | return @deindexedBecauseMetaDoesnotIndexAttachments; | ||||
1284 | } | ||||
1285 | |||||
1286 | =begin TML | ||||
1287 | |||||
1288 | ---++ ObjectMethod getAttachmentList() -> @list | ||||
1289 | |||||
1290 | Get list of attachment names actually stored for topic. | ||||
1291 | |||||
1292 | =cut | ||||
1293 | |||||
1294 | sub getAttachmentList { | ||||
1295 | my $this = shift; | ||||
1296 | my $dir = "$Foswiki::cfg{PubDir}/$this->{web}/$this->{topic}"; | ||||
1297 | my $dh; | ||||
1298 | opendir( $dh, $dir ) || return (); | ||||
1299 | my @files = grep { !/^[.*_]/ && !/,v$/ } readdir($dh); | ||||
1300 | closedir($dh); | ||||
1301 | return @files; | ||||
1302 | } | ||||
1303 | |||||
1304 | # returns {} of filename => { key => value, key2 => value } | ||||
1305 | # for any given web, topic | ||||
1306 | sub _getAttachmentStats { | ||||
1307 | my $this = shift; | ||||
1308 | my %attachmentList = (); | ||||
1309 | my $dir = "$Foswiki::cfg{PubDir}/$this->{web}/$this->{topic}"; | ||||
1310 | foreach my $attachment ( $this->getAttachmentList() ) { | ||||
1311 | my @stat = stat( $dir . "/" . $attachment ); | ||||
1312 | $attachmentList{$attachment} = | ||||
1313 | _constructAttributesForAutoAttached( $attachment, \@stat ); | ||||
1314 | } | ||||
1315 | return %attachmentList; | ||||
1316 | } | ||||
1317 | |||||
1318 | sub _dirForTopicAttachments { | ||||
1319 | my ( $web, $topic ) = @_; | ||||
1320 | } | ||||
1321 | |||||
1322 | =begin TML | ||||
1323 | |||||
1324 | ---++ ObjectMethod stringify() | ||||
1325 | |||||
1326 | Generate string representation for debugging | ||||
1327 | |||||
1328 | =cut | ||||
1329 | |||||
1330 | sub stringify { | ||||
1331 | my $this = shift; | ||||
1332 | my @reply; | ||||
1333 | foreach my $key (qw(web topic attachment file rcsFile)) { | ||||
1334 | if ( defined $this->{$key} ) { | ||||
1335 | push( @reply, "$key=$this->{$key}" ); | ||||
1336 | } | ||||
1337 | } | ||||
1338 | return join( ',', @reply ); | ||||
1339 | } | ||||
1340 | |||||
1341 | # Chop out recognisable path components to prevent hacking based on error | ||||
1342 | # messages | ||||
1343 | sub hidePath { | ||||
1344 | my ( $this, $erf ) = @_; | ||||
1345 | $erf =~ s#.*(/\w+/\w+\.[\w,]*)$#...$1#; | ||||
1346 | return $erf; | ||||
1347 | } | ||||
1348 | |||||
1349 | =begin TML | ||||
1350 | |||||
1351 | ---++ ObjectMethod recordChange($cUID, $rev, $more) | ||||
1352 | Record that the file changed, and who changed it | ||||
1353 | |||||
1354 | =cut | ||||
1355 | |||||
1356 | sub recordChange { | ||||
1357 | my ( $this, $cUID, $rev, $more ) = @_; | ||||
1358 | $more ||= ''; | ||||
1359 | ASSERT($cUID) if DEBUG; | ||||
1360 | |||||
1361 | my $file = $Foswiki::cfg{DataDir} . '/' . $this->{web} . '/.changes'; | ||||
1362 | |||||
1363 | my @changes = | ||||
1364 | map { | ||||
1365 | my @row = split( /\t/, $_, 5 ); | ||||
1366 | \@row | ||||
1367 | } | ||||
1368 | split( /[\r\n]+/, readFile( $this, $file ) ); | ||||
1369 | |||||
1370 | # Forget old stuff | ||||
1371 | my $cutoff = time() - $Foswiki::cfg{Store}{RememberChangesFor}; | ||||
1372 | while ( scalar(@changes) && $changes[0]->[2] < $cutoff ) { | ||||
1373 | shift(@changes); | ||||
1374 | } | ||||
1375 | |||||
1376 | # Add the new change to the end of the file | ||||
1377 | push( @changes, [ $this->{topic} || '.', $cUID, time(), $rev, $more ] ); | ||||
1378 | |||||
1379 | # Doing this using a Schwartzian transform sometimes causes a mysterious | ||||
1380 | # undefined value, so had to unwrap it to a for loop. | ||||
1381 | for ( my $i = 0 ; $i <= $#changes ; $i++ ) { | ||||
1382 | $changes[$i] = join( "\t", @{ $changes[$i] } ); | ||||
1383 | } | ||||
1384 | |||||
1385 | my $text = join( "\n", @changes ); | ||||
1386 | |||||
1387 | saveFile( $this, $file, $text ); | ||||
1388 | } | ||||
1389 | |||||
1390 | =begin TML | ||||
1391 | |||||
1392 | ---++ ObjectMethod eachChange($since) -> $iterator | ||||
1393 | |||||
1394 | Return iterator over changes - see Store for details | ||||
1395 | |||||
1396 | =cut | ||||
1397 | |||||
1398 | sub eachChange { | ||||
1399 | my ( $this, $since ) = @_; | ||||
1400 | my $file = $Foswiki::cfg{DataDir} . '/' . $this->{web} . '/.changes'; | ||||
1401 | require Foswiki::ListIterator; | ||||
1402 | |||||
1403 | if ( -r $file ) { | ||||
1404 | |||||
1405 | # Could use a LineIterator to avoid reading the whole | ||||
1406 | # file, but it hardly seems worth it. | ||||
1407 | my @changes = | ||||
1408 | map { | ||||
1409 | |||||
1410 | # Create a hash for this line | ||||
1411 | { | ||||
1412 | topic => Foswiki::Sandbox::untaint( | ||||
1413 | $_->[0], \&Foswiki::Sandbox::validateTopicName | ||||
1414 | ), | ||||
1415 | user => $_->[1], | ||||
1416 | time => $_->[2], | ||||
1417 | revision => $_->[3], | ||||
1418 | more => $_->[4] | ||||
1419 | }; | ||||
1420 | } | ||||
1421 | grep { | ||||
1422 | |||||
1423 | # Filter on time | ||||
1424 | $_->[2] && $_->[2] >= $since | ||||
1425 | } | ||||
1426 | map { | ||||
1427 | |||||
1428 | # Split line into an array | ||||
1429 | my @row = split( /\t/, $_, 5 ); | ||||
1430 | \@row; | ||||
1431 | } | ||||
1432 | reverse split( /[\r\n]+/, readFile( $this, $file ) ); | ||||
1433 | |||||
1434 | return Foswiki::ListIterator->new( \@changes ); | ||||
1435 | } | ||||
1436 | else { | ||||
1437 | my $changes = []; | ||||
1438 | return Foswiki::ListIterator->new($changes); | ||||
1439 | } | ||||
1440 | } | ||||
1441 | |||||
1442 | # ObjectMethod getTimestamp() -> $integer | ||||
1443 | # Get the timestamp of the file | ||||
1444 | # Returns 0 if no file, otherwise epoch seconds | ||||
1445 | # Used in subclasses | ||||
1446 | |||||
1447 | # spent 743µs (656+87) within Foswiki::Store::VC::Handler::getTimestamp which was called 11 times, avg 68µs/call:
# 11 times (656µs+87µs) by Foswiki::Store::VC::Handler::getInfo at line 252, avg 68µs/call | ||||
1448 | 11 | 26µs | my ($this) = @_; | ||
1449 | 11 | 80µs | 11 | 87µs | ASSERT( $this->{file} ) if DEBUG; # spent 87µs making 11 calls to Assert::ASSERTS_OFF, avg 8µs/call |
1450 | |||||
1451 | 11 | 7µs | my $date = 0; | ||
1452 | 11 | 259µs | if ( -e $this->{file} ) { | ||
1453 | |||||
1454 | # If the stat fails, stamp it with some arbitrary static | ||||
1455 | # time in the past (00:40:05 on 5th Jan 1989) | ||||
1456 | $date = ( stat $this->{file} )[9] || 600000000; | ||||
1457 | } | ||||
1458 | 11 | 83µs | return $date; | ||
1459 | } | ||||
1460 | |||||
1461 | 1 | 3µs | 1; | ||
1462 | |||||
1463 | __END__ |