← Index
NYTProf Performance Profile   « line view »
For ./view
  Run on Fri Jul 31 19:05:14 2015
Reported on Fri Jul 31 19:08:09 2015

Filename/var/www/foswiki11/lib/Foswiki/Store/VC/Handler.pm
StatementsExecuted 721909 statements in 1.65s
Subroutines
Calls P F Exclusive
Time
Inclusive
Time
Subroutine
4735211590ms690msFoswiki::Store::VC::Handler::::newFoswiki::Store::VC::Handler::new
4678131427ms427msFoswiki::Store::VC::Handler::::storedDataExistsFoswiki::Store::VC::Handler::storedDataExists
4111315ms315msFoswiki::Store::VC::Handler::::getTopicNamesFoswiki::Store::VC::Handler::getTopicNames
1021181.7ms83.8msFoswiki::Store::VC::Handler::::getWebNamesFoswiki::Store::VC::Handler::getWebNames
4261137.7ms38.1msFoswiki::Store::VC::Handler::::readFileFoswiki::Store::VC::Handler::readFile
4734310.6ms10.6msFoswiki::Store::VC::Handler::::noCheckinPendingFoswiki::Store::VC::Handler::noCheckinPending
426115.70ms43.7msFoswiki::Store::VC::Handler::::getRevisionFoswiki::Store::VC::Handler::getRevision
12112.58ms4.45msFoswiki::Store::VC::Handler::::_getTOPICINFOFoswiki::Store::VC::Handler::_getTOPICINFO
1112.13ms2.24msFoswiki::Store::VC::Handler::::BEGIN@31Foswiki::Store::VC::Handler::BEGIN@31
22111.58ms2.49sFoswiki::Store::VC::Handler::::getInfoFoswiki::Store::VC::Handler::getInfo
1111656µs743µsFoswiki::Store::VC::Handler::::getTimestampFoswiki::Store::VC::Handler::getTimestamp
111333µs405µsFoswiki::Store::VC::Handler::::BEGIN@38Foswiki::Store::VC::Handler::BEGIN@38
322101µs394msFoswiki::Store::VC::Handler::::getLatestRevisionIDFoswiki::Store::VC::Handler::getLatestRevisionID
21182µs394msFoswiki::Store::VC::Handler::::getRevisionHistoryFoswiki::Store::VC::Handler::getRevisionHistory
11118µs31µsFoswiki::Store::VC::Handler::::BEGIN@26Foswiki::Store::VC::Handler::BEGIN@26
11112µs17µsFoswiki::Store::VC::Handler::::BEGIN@27Foswiki::Store::VC::Handler::BEGIN@27
11112µs16µsFoswiki::Store::VC::Handler::::BEGIN@41Foswiki::Store::VC::Handler::BEGIN@41
11111µs351µsFoswiki::Store::VC::Handler::::BEGIN@34Foswiki::Store::VC::Handler::BEGIN@34
1119µs23µsFoswiki::Store::VC::Handler::::BEGIN@28Foswiki::Store::VC::Handler::BEGIN@28
1114µs4µsFoswiki::Store::VC::Handler::::BEGIN@32Foswiki::Store::VC::Handler::BEGIN@32
1114µs4µsFoswiki::Store::VC::Handler::::BEGIN@36Foswiki::Store::VC::Handler::BEGIN@36
1114µs4µsFoswiki::Store::VC::Handler::::BEGIN@30Foswiki::Store::VC::Handler::BEGIN@30
1113µs3µsFoswiki::Store::VC::Handler::::BEGIN@33Foswiki::Store::VC::Handler::BEGIN@33
1113µs3µsFoswiki::Store::VC::Handler::::BEGIN@37Foswiki::Store::VC::Handler::BEGIN@37
0000s0sFoswiki::Store::VC::Handler::::_constructAttributesForAutoAttachedFoswiki::Store::VC::Handler::_constructAttributesForAutoAttached
0000s0sFoswiki::Store::VC::Handler::::_controlFileNameFoswiki::Store::VC::Handler::_controlFileName
0000s0sFoswiki::Store::VC::Handler::::_dirForTopicAttachmentsFoswiki::Store::VC::Handler::_dirForTopicAttachments
0000s0sFoswiki::Store::VC::Handler::::_epochToRcsDateTimeFoswiki::Store::VC::Handler::_epochToRcsDateTime
0000s0sFoswiki::Store::VC::Handler::::_getAttachmentStatsFoswiki::Store::VC::Handler::_getAttachmentStats
0000s0sFoswiki::Store::VC::Handler::::_mktempFoswiki::Store::VC::Handler::_mktemp
0000s0sFoswiki::Store::VC::Handler::::_rmtreeFoswiki::Store::VC::Handler::_rmtree
0000s0sFoswiki::Store::VC::Handler::::_saveDamageFoswiki::Store::VC::Handler::_saveDamage
0000s0sFoswiki::Store::VC::Handler::::addRevisionFromStreamFoswiki::Store::VC::Handler::addRevisionFromStream
0000s0sFoswiki::Store::VC::Handler::::addRevisionFromTextFoswiki::Store::VC::Handler::addRevisionFromText
0000s0sFoswiki::Store::VC::Handler::::ciFoswiki::Store::VC::Handler::ci
0000s0sFoswiki::Store::VC::Handler::::copyAttachmentFoswiki::Store::VC::Handler::copyAttachment
0000s0sFoswiki::Store::VC::Handler::::copyFileFoswiki::Store::VC::Handler::copyFile
0000s0sFoswiki::Store::VC::Handler::::copyTopicFoswiki::Store::VC::Handler::copyTopic
0000s0sFoswiki::Store::VC::Handler::::eachChangeFoswiki::Store::VC::Handler::eachChange
0000s0sFoswiki::Store::VC::Handler::::finishFoswiki::Store::VC::Handler::finish
0000s0sFoswiki::Store::VC::Handler::::getAttachmentListFoswiki::Store::VC::Handler::getAttachmentList
0000s0sFoswiki::Store::VC::Handler::::getLatestRevisionTimeFoswiki::Store::VC::Handler::getLatestRevisionTime
0000s0sFoswiki::Store::VC::Handler::::getLeaseFoswiki::Store::VC::Handler::getLease
0000s0sFoswiki::Store::VC::Handler::::getNextRevisionIDFoswiki::Store::VC::Handler::getNextRevisionID
0000s0sFoswiki::Store::VC::Handler::::hidePathFoswiki::Store::VC::Handler::hidePath
0000s0sFoswiki::Store::VC::Handler::::initFoswiki::Store::VC::Handler::init
0000s0sFoswiki::Store::VC::Handler::::isAsciiDefaultFoswiki::Store::VC::Handler::isAsciiDefault
0000s0sFoswiki::Store::VC::Handler::::isLockedFoswiki::Store::VC::Handler::isLocked
0000s0sFoswiki::Store::VC::Handler::::mkPathToFoswiki::Store::VC::Handler::mkPathTo
0000s0sFoswiki::Store::VC::Handler::::mkTmpFilenameFoswiki::Store::VC::Handler::mkTmpFilename
0000s0sFoswiki::Store::VC::Handler::::moveAttachmentFoswiki::Store::VC::Handler::moveAttachment
0000s0sFoswiki::Store::VC::Handler::::moveFileFoswiki::Store::VC::Handler::moveFile
0000s0sFoswiki::Store::VC::Handler::::moveTopicFoswiki::Store::VC::Handler::moveTopic
0000s0sFoswiki::Store::VC::Handler::::moveWebFoswiki::Store::VC::Handler::moveWeb
0000s0sFoswiki::Store::VC::Handler::::openStreamFoswiki::Store::VC::Handler::openStream
0000s0sFoswiki::Store::VC::Handler::::recordChangeFoswiki::Store::VC::Handler::recordChange
0000s0sFoswiki::Store::VC::Handler::::removeFoswiki::Store::VC::Handler::remove
0000s0sFoswiki::Store::VC::Handler::::removeSpuriousLeasesFoswiki::Store::VC::Handler::removeSpuriousLeases
0000s0sFoswiki::Store::VC::Handler::::repRevFoswiki::Store::VC::Handler::repRev
0000s0sFoswiki::Store::VC::Handler::::replaceRevisionFoswiki::Store::VC::Handler::replaceRevision
0000s0sFoswiki::Store::VC::Handler::::restoreLatestRevisionFoswiki::Store::VC::Handler::restoreLatestRevision
0000s0sFoswiki::Store::VC::Handler::::revisionExistsFoswiki::Store::VC::Handler::revisionExists
0000s0sFoswiki::Store::VC::Handler::::saveFileFoswiki::Store::VC::Handler::saveFile
0000s0sFoswiki::Store::VC::Handler::::saveStreamFoswiki::Store::VC::Handler::saveStream
0000s0sFoswiki::Store::VC::Handler::::setLeaseFoswiki::Store::VC::Handler::setLease
0000s0sFoswiki::Store::VC::Handler::::setLockFoswiki::Store::VC::Handler::setLock
0000s0sFoswiki::Store::VC::Handler::::stringifyFoswiki::Store::VC::Handler::stringify
0000s0sFoswiki::Store::VC::Handler::::synchroniseAttachmentsListFoswiki::Store::VC::Handler::synchroniseAttachmentsList
0000s0sFoswiki::Store::VC::Handler::::testFoswiki::Store::VC::Handler::test
0000s0sFoswiki::Store::_MemoryFile::::CLOSEFoswiki::Store::_MemoryFile::CLOSE
0000s0sFoswiki::Store::_MemoryFile::::READFoswiki::Store::_MemoryFile::READ
0000s0sFoswiki::Store::_MemoryFile::::READLINEFoswiki::Store::_MemoryFile::READLINE
0000s0sFoswiki::Store::_MemoryFile::::TIEHANDLEFoswiki::Store::_MemoryFile::TIEHANDLE
Call graph for these subroutines as a Graphviz dot language file.
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
7This class is PACKAGE PRIVATE to Store::VC, and should never be
8used from anywhere else. It is the base class of implementations of
9individual file handler objects used with stores that manipulate
10files stored in a version control system (phew!).
11
12The general contract of the methods on this class and its subclasses
13calls for errors to be signalled by Error::Simple exceptions.
14
15There are a number of references to RCS below; however this class is
16useful as a base class for handlers for all kinds of version control
17systems which use files on disk.
18
19For readers who are familiar with Foswiki version 1.0.0, this class
20is analagous to =Foswiki::Store::RcsFile=.
21
22=cut
23
24package Foswiki::Store::VC::Handler;
25
26232µs244µ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
use strict;
# spent 31µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@26 # spent 13µs making 1 call to strict::import
27226µs222µ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
use warnings;
# spent 17µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@27 # spent 5µs making 1 call to warnings::import
28224µs238µ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
use Assert;
# spent 23µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@28 # spent 14µs making 1 call to Assert::import
29
30218µs14µ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
use IO::File ();
# spent 4µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@30
312106µs12.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
use File::Copy ();
# spent 2.24ms making 1 call to Foswiki::Store::VC::Handler::BEGIN@31
32220µs14µ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
use File::Spec ();
# spent 4µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@32
33224µs13µ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
use File::Path ();
# spent 3µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@33
34232µs2690µ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
use Fcntl qw( :DEFAULT :flock SEEK_SET );
# spent 351µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@34 # spent 340µs making 1 call to Exporter::import
35
36218µs14µ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
use Foswiki::Store ();
# spent 4µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@36
37218µs13µ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
use Foswiki::Sandbox ();
# spent 3µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@37
382131µs1405µ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
use Foswiki::Iterator::NumberRangeIterator ();
# 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
BEGIN {
4214µs if ( $Foswiki::cfg{UseLocale} ) {
431300ns require locale;
4413µs14µs import locale();
# spent 4µs making 1 call to locale::import
45 }
4614.86ms116µ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
52Constructor. There is one object per stored file.
53
54$store is the Foswiki::VC::Store object that contains the cache for
55objects of this type. A cache is used because at some point we'll be
56smarter about the number of calls to RCS code we make.
57
58Note 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
sub new {
634735240.4ms my ( $class, $store, $web, $topic, $attachment ) = @_;
64
654735243.0ms4735235.9ms ASSERT( $store->isa('Foswiki::Store') ) if DEBUG;
# spent 35.9ms making 47352 calls to Assert::ASSERTS_OFF, avg 758ns/call
66
6747352205ms4735260.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
70470176µs $attachment = $topic;
71470793µs4701.08ms $topic = $web->topic();
# spent 1.08ms making 470 calls to Foswiki::Meta::topic, avg 2µs/call
72470746µs470886µs $web = $web->web();
# spent 886µs making 470 calls to Foswiki::Meta::web, avg 2µs/call
73 }
74
75 # Reuse is good
764735244.5ms my $id = ( $web || 0 ) . '/' . ( $topic || 0 ) . '/' . ( $attachment || 0 );
7747352255ms if ( $store->{handler_cache} && $store->{handler_cache}->{$id} ) {
78 return $store->{handler_cache}->{$id};
79 }
80
8114774.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)
8714772.19ms $store->{handler_cache}->{$id} = $this;
88
891477603µs if ( $web && $topic ) {
901380944µs my $rcsSubDir = ( $Foswiki::cfg{RCS}{useSubDir} ? '/RCS' : '' );
91
9213801.58ms13801.40ms ASSERT( UNTAINTED($web), "web $web is tainted!" ) if DEBUG;
# spent 1.40ms making 1380 calls to Assert::ASSERTS_OFF, avg 1µs/call
9313801.28ms13801.04ms ASSERT( UNTAINTED($topic), "topic $topic is tainted!" ) if DEBUG;
# spent 1.04ms making 1380 calls to Assert::ASSERTS_OFF, avg 753ns/call
941380868µs if ($attachment) {
9512µs11µs ASSERT( UNTAINTED($attachment) ) if DEBUG;
# spent 1µs making 1 call to Assert::ASSERTS_OFF
9614µs $this->{file} =
97 $Foswiki::cfg{PubDir} . '/'
98 . $web . '/'
99 . $topic . '/'
100 . $attachment;
10113µs $this->{rcsFile} =
102 $Foswiki::cfg{PubDir} . '/'
103 . $web . '/'
104 . $topic
105 . $rcsSubDir . '/'
106 . $attachment . ',v';
107
108 }
109 else {
11013792.23ms $this->{file} =
111 $Foswiki::cfg{DataDir} . '/' . $web . '/' . $topic . '.txt';
11213791.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
1221477686µs $Foswiki::cfg{Store}{RememberChangesFor} ||= 31 * 24 * 60 * 60;
123
12414774.09ms return $this;
125}
126
127=begin TML
128
129---++ ObjectMethod finish()
130Break 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.
137sub 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)
148sub 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
164sub 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
185sub _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
198sub _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
212Returns 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
214Designed to be overridden by subclasses, which can call up to this method
215if 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
sub getInfo {
2202250µ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
2242271µ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.
2322285µs my $info = {};
23322335µs22876µ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
24210198µs102.48s $info->{version} = $this->_numRevisions() + 1;
# spent 2.48s making 10 calls to Foswiki::Store::VC::RcsWrapHandler::_numRevisions, avg 248ms/call
2431038µ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.
2481268µs124.45ms $this->_getTOPICINFO($info);
# spent 4.45ms making 12 calls to Foswiki::Store::VC::Handler::_getTOPICINFO, avg 370µs/call
2491218µs $info->{version} = 1;
2501218µs $info->{comment} = "pending";
251 }
25222343µs11743µ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
2532222µs $info->{version} = 1 unless defined $info->{version};
2542216µs $info->{comment} = '' unless defined $info->{comment};
2552239µs $info->{author} ||= $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID;
25622197µ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
sub _getTOPICINFO {
2611225µs my ( $this, $info ) = @_;
262123µs my $f;
263
26412488µs if ( open( $f, '<', $this->{file} ) ) {
2651254µs local $/ = "\n";
26612158µs my $ti = <$f>;
2671283µs close($f);
26812257µs if ( defined $ti && $ti =~ /^%META:TOPICINFO{(.*)}%/ ) {
26911156µs require Foswiki::Attrs;
27011237µs111.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.
27411128µs11206µs $info->{version} = Foswiki::Store::cleanUpRevID( $a->{version} )
# spent 206µs making 11 calls to Foswiki::Store::cleanUpRevID, avg 19µs/call
275 || 1;
2761119µs $info->{date} = $a->{date};
2771122µs $info->{author} = $a->{author};
2781173µ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
sub noCheckinPending {
288473152µs my $this = shift;
289473123µs my $isValid = 0;
290
2914733.47ms if ( !-e $this->{file} ) {
292 $isValid = 1; # Hmmmm......
293 }
294 else {
2954732.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.
300436268µs local ${^WIN32_SLOPPY_STAT} =
301 1; # don't need to open the file on Win32
3024361.84ms my $rcsTime = ( stat( $this->{rcsFile} ) )[9];
3034361.42ms my $fileTime = ( stat( $this->{file} ) )[9];
304436359µs $isValid = ( $rcsTime < $fileTime ) ? 0 : 1;
305 }
306 }
3074731.63ms return $isValid;
308}
309
310# Must be implemented by subclasses
311sub 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.
317sub _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
345Add 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
353sub 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
366Add 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
373sub 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
387Replace 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
395sub replaceRevision {
396 my $this = shift;
397 $this->_saveDamage();
398 $this->repRev(@_);
399}
400
401# Signature as for replaceRevision
402sub repRev {
403 die "Pure virtual method";
404}
405
406=begin TML
407
408---++ ObjectMethod getRevisionHistory() -> $iterator
409
410Get an iterator over the identifiers of revisions. Returns the most
411recent revision first.
412
413The default is to return an iterator from the current version number
414down to 1. Return rev 1 if the file exists without history. Return
415an 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
sub getRevisionHistory {
42021µs my $this = shift;
42122µs21µs ASSERT( $this->{file} ) if DEBUG;
# spent 1µs making 2 calls to Assert::ASSERTS_OFF, avg 700ns/call
422215µ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?
43329µs2394ms my $maxRev = $this->getLatestRevisionID();
# spent 394ms making 2 calls to Foswiki::Store::VC::Handler::getLatestRevisionID, avg 197ms/call
434240µs251µ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
441Get the ID of the most recent revision. This may return undef if there have
442been 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
sub getLatestRevisionID {
44732µs my $this = shift;
448315µs return 0 unless -e $this->{file};
449318µs3394ms 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
453330µs382µs $rev++ unless $this->noCheckinPending() || !-e $this->{rcsFile};
# spent 82µs making 3 calls to Foswiki::Store::VC::Handler::noCheckinPending, avg 27µs/call
454327µs return $rev;
455}
456
457=begin TML
458
459---++ ObjectMethod getNextRevisionID() -> $id
460
461Get the ID of the next (as yet uncreated) revision. The handler is required
462to implement this because the store has to be able to embed the revision
463ID into TOPICINFO before the revision is actually created.
464
465If the file exists without revisions, then rev 1 does exist, so next rev
466should be rev 2, not rev 1, so the first change with missing history
467doesn't get merged into rev 1.
468
469=cut
470
471sub getNextRevisionID {
472 my $this = shift;
473 return $this->getLatestRevisionID() + 1;
474}
475
476=begin TML
477
478---++ ObjectMethod getLatestRevisionTime() -> $text
479
480Get the time of the most recent revision
481
482=cut
483
484sub getLatestRevisionTime {
485 my @e = stat( shift->{file} );
486 return $e[9] || 0;
487}
488
489=begin TML
490
491---++ ObjectMethod getTopicNames() -> @topics
492
493Get list of all topics in a web
494 * =$web= - Web name, required, e.g. ='Sandbox'=
495Return 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
sub getTopicNames {
5004122µs my $this = shift;
501419µs my $dh;
50241959µ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.
5075248323.2ms my @topicList =
508157189156ms map { /^(.*)\.txt$/; $1; }
509 sort
51041129ms grep { !/$Foswiki::cfg{NameFilter}/ && /\.txt$/ } readdir($dh);
51141246µs closedir($dh);
5124110.9ms return @topicList;
513}
514
515=begin TML
516
517---++ ObjectMethod revisionExists($rev) -> $boolean
518
519Determine if the identified revision actually exists in the object
520history.
521
522=cut
523
524sub 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
535Gets 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
sub getWebNames {
54010226µs my $this = shift;
54110253µs my $dir = $Foswiki::cfg{DataDir};
54210293µs $dir .= '/' . $this->{web} if defined $this->{web};
54310215µs my @tmpList;
54410216µs my $dh;
545
5461023.07ms if ( opendir( $dh, $dir ) ) {
547100227µs1002.14ms @tmpList = map {
# spent 2.14ms making 100 calls to Foswiki::Sandbox::untaint, avg 21µs/call
54810663626.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.
55510250.9ms grep { !/\./ && -e "$dir/$_/$Foswiki::cfg{WebPrefsTopicName}.txt" }
556 readdir($dh);
557102327µs closedir($dh);
558 }
559
560102636µs return @tmpList;
561}
562
563=begin TML
564
565---++ ObjectMethod moveWeb( $newWeb )
566
567Move a web.
568
569=cut
570
571sub 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
591Get the text of the given revision, and a flag indicating if this is the
592most recent revision.
593
594Designed to be overridden by subclasses, which can call up to this method
595if 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
sub getRevision {
600426174µs my ($this) = @_;
6014264.88ms42638.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
611Establishes 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
sub storedDataExists {
6164678112.1ms my $this = shift;
6174678128.3ms return 0 unless $this->{file};
61846781537ms return -e $this->{file};
619}
620
621=begin TML
622
623---++ ObjectMethod restoreLatestRevision( $cUID )
624
625Restore the plaintext file from the revision at the head.
626
627=cut
628
629sub 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
648Destroy, utterly. Remove the data and attachments in the web.
649
650Use with great care! No backup is taken!
651
652=cut
653
654sub 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
680Move/rename a topic.
681
682=cut
683
684sub 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
715Copy a topic.
716
717=cut
718
719sub 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
752Move an attachment from one topic to another. The name is retained.
753
754=cut
755
756sub 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
775Copy an attachment from one topic to another. The name is retained unless
776$newAttachment is defined.
777
778=cut
779
780sub 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
802Check if this file type is known to be an ascii type file.
803
804=cut
805
806sub 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
815Set a lock on the topic, if $lock, otherwise clear it.
816$cUID is a cUID.
817
818SMELL: there is a tremendous amount of potential for race
819conditions using this locking approach.
820
821It 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
826sub 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
845See if a lock exists. Return the lock user and lock time if it does.
846
847=cut
848
849sub 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
866Set an lease on the topic.
867
868=cut
869
870sub 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
888Get the current lease on the topic.
889
890=cut
891
892sub 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
908Remove leases that are not related to a topic. These can get left behind in
909some store implementations when a topic is created, but never saved.
910
911=cut
912
913sub 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
929sub test {
930 my ( $this, $test ) = @_;
931 return eval "-$test '$this->{file}'";
932}
933
934# Used by subclasses
935sub 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
959sub 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
969sub 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
980sub 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
sub readFile {
1005426255µs my ( $this, $name ) = @_;
1006426417µs426395µs ASSERT($name) if DEBUG;
# spent 395µs making 426 calls to Assert::ASSERTS_OFF, avg 927ns/call
100742670µs my $data;
100842643µs my $IN_FILE;
10094267.42ms if ( open( $IN_FILE, '<', $name ) ) {
1010426485µs binmode($IN_FILE);
10114261.18ms local $/ = undef;
101242621.5ms $data = <$IN_FILE>;
10134263.18ms close($IN_FILE);
1014 }
1015426111µs $data ||= '';
10164263.39ms return $data;
1017}
1018
1019# Used by subclasses
1020sub 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
1027sub _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.
1074sub _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
11191700ns 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
1163Opens a file handle onto the store. This method is primarily to
1164support virtual file systems.
1165
1166=$mode= can be '&lt;', '&gt;' or '&gt;&gt;' for read, write, and append
1167respectively. %
1168
1169=%opts= can take different settings depending on =$mode=.
1170 * =$mode='&lt;'=
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='&gt;'= or ='&gt;&gt;'
1176 * no options
1177
1178=cut
1179
1180sub 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.
1208sub _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
1236Synchronise the attachment list from meta-data with what's actually
1237stored in the DB. Returns an ARRAY of FILEATTACHMENTs. These can be
1238put in the new tom.
1239
1240This function is only called when the {RCS}{AutoAttachPubFiles} configuration
1241option 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
1250sub 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
1290Get list of attachment names actually stored for topic.
1291
1292=cut
1293
1294sub 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
1306sub _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
1318sub _dirForTopicAttachments {
1319 my ( $web, $topic ) = @_;
1320}
1321
1322=begin TML
1323
1324---++ ObjectMethod stringify()
1325
1326Generate string representation for debugging
1327
1328=cut
1329
1330sub 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
1343sub 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)
1352Record that the file changed, and who changed it
1353
1354=cut
1355
1356sub 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
1394Return iterator over changes - see Store for details
1395
1396=cut
1397
1398sub 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
sub getTimestamp {
14481126µs my ($this) = @_;
14491180µs1187µs ASSERT( $this->{file} ) if DEBUG;
# spent 87µs making 11 calls to Assert::ASSERTS_OFF, avg 8µs/call
1450
1451117µs my $date = 0;
145211259µ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 }
14581183µs return $date;
1459}
1460
146113µs1;
1462
1463__END__