Filename | /usr/local/src/github.com/foswiki/core/lib/Foswiki/Store/VC/Handler.pm |
Statements | Executed 20739 statements in 222ms |
Calls | P | F | Exclusive Time |
Inclusive Time |
Subroutine |
---|---|---|---|---|---|
1 | 1 | 1 | 108ms | 169ms | getTopicNames | Foswiki::Store::VC::Handler::
17231 | 3 | 1 | 33.0ms | 33.0ms | CORE:match (opcode) | Foswiki::Store::VC::Handler::
347 | 1 | 1 | 15.7ms | 20.0ms | new | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 10.3ms | 10.3ms | CORE:readdir (opcode) | Foswiki::Store::VC::Handler::
6896 | 1 | 1 | 8.81ms | 8.81ms | CORE:regcomp (opcode) | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 8.52ms | 8.52ms | CORE:sort (opcode) | Foswiki::Store::VC::Handler::
70 | 1 | 1 | 4.58ms | 8.41ms | readFile | Foswiki::Store::VC::Handler::
117 | 4 | 3 | 4.18ms | 6.36ms | noCheckinPending | Foswiki::Store::VC::Handler::
617 | 9 | 1 | 4.05ms | 4.05ms | CORE:ftis (opcode) | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 3.76ms | 3.96ms | BEGIN@31 | Foswiki::Store::VC::Handler::
14 | 1 | 1 | 3.28ms | 8.62ms | _getTOPICINFO | Foswiki::Store::VC::Handler::
274 | 2 | 1 | 3.27ms | 5.19ms | storedDataExists | Foswiki::Store::VC::Handler::
70 | 1 | 1 | 1.93ms | 10.8ms | getRevision | Foswiki::Store::VC::Handler::
84 | 2 | 1 | 1.83ms | 1.83ms | CORE:open (opcode) | Foswiki::Store::VC::Handler::
84 | 2 | 1 | 1.65ms | 1.65ms | CORE:readline (opcode) | Foswiki::Store::VC::Handler::
21 | 1 | 1 | 1.57ms | 22.5s | getInfo | Foswiki::Store::VC::Handler::
156 | 3 | 1 | 804µs | 804µs | CORE:stat (opcode) | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 545µs | 688µs | BEGIN@38 | Foswiki::Store::VC::Handler::
84 | 2 | 1 | 449µs | 449µs | CORE:close (opcode) | Foswiki::Store::VC::Handler::
8 | 1 | 1 | 386µs | 652µs | getTimestamp | Foswiki::Store::VC::Handler::
5 | 2 | 2 | 270µs | 1.41s | getLatestRevisionID | Foswiki::Store::VC::Handler::
70 | 1 | 1 | 200µs | 200µs | CORE:binmode (opcode) | Foswiki::Store::VC::Handler::
2 | 1 | 1 | 134µs | 620ms | getRevisionHistory | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 83µs | 83µs | CORE:open_dir (opcode) | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 42µs | 42µs | CORE:closedir (opcode) | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 25µs | 1.86ms | BEGIN@34 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 24µs | 31µs | BEGIN@26 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 16µs | 33µs | BEGIN@27 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 16µs | 56µs | BEGIN@28 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 10µs | 10µs | BEGIN@36 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 10µs | 10µs | BEGIN@41 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 10µs | 10µs | BEGIN@32 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 9µs | 9µs | BEGIN@33 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 9µs | 9µs | BEGIN@37 | Foswiki::Store::VC::Handler::
1 | 1 | 1 | 9µs | 9µs | BEGIN@30 | 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 | getWebNames | 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 | 45µs | 2 | 38µs | # spent 31µs (24+7) within Foswiki::Store::VC::Handler::BEGIN@26 which was called:
# once (24µs+7µ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 7µs making 1 call to strict::import |
27 | 2 | 42µs | 2 | 50µs | # spent 33µs (16+17) within Foswiki::Store::VC::Handler::BEGIN@27 which was called:
# once (16µs+17µs) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 27 # spent 33µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@27
# spent 17µs making 1 call to warnings::import |
28 | 2 | 45µs | 2 | 96µs | # spent 56µs (16+40) within Foswiki::Store::VC::Handler::BEGIN@28 which was called:
# once (16µs+40µs) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 28 # spent 56µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@28
# spent 40µs making 1 call to Assert::import |
29 | |||||
30 | 2 | 43µs | 1 | 9µs | # spent 9µs within Foswiki::Store::VC::Handler::BEGIN@30 which was called:
# once (9µs+0s) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 30 # spent 9µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@30 |
31 | 2 | 182µs | 1 | 3.96ms | # spent 3.96ms (3.76+204µs) within Foswiki::Store::VC::Handler::BEGIN@31 which was called:
# once (3.76ms+204µs) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 31 # spent 3.96ms making 1 call to Foswiki::Store::VC::Handler::BEGIN@31 |
32 | 2 | 39µs | 1 | 10µs | # spent 10µs within Foswiki::Store::VC::Handler::BEGIN@32 which was called:
# once (10µs+0s) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 32 # spent 10µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@32 |
33 | 2 | 44µs | 1 | 9µs | # spent 9µs within Foswiki::Store::VC::Handler::BEGIN@33 which was called:
# once (9µs+0s) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 33 # spent 9µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@33 |
34 | 2 | 54µs | 2 | 3.69ms | # spent 1.86ms (25µs+1.83) within Foswiki::Store::VC::Handler::BEGIN@34 which was called:
# once (25µs+1.83ms) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 34 # spent 1.86ms making 1 call to Foswiki::Store::VC::Handler::BEGIN@34
# spent 1.83ms making 1 call to Exporter::import |
35 | |||||
36 | 2 | 40µs | 1 | 10µs | # spent 10µs within Foswiki::Store::VC::Handler::BEGIN@36 which was called:
# once (10µs+0s) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 36 # spent 10µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@36 |
37 | 2 | 39µs | 1 | 9µs | # spent 9µs within Foswiki::Store::VC::Handler::BEGIN@37 which was called:
# once (9µs+0s) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 37 # spent 9µs making 1 call to Foswiki::Store::VC::Handler::BEGIN@37 |
38 | 2 | 193µs | 1 | 688µs | # spent 688µs (545+143) within Foswiki::Store::VC::Handler::BEGIN@38 which was called:
# once (545µs+143µs) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 38 # spent 688µ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 10µs within Foswiki::Store::VC::Handler::BEGIN@41 which was called:
# once (10µs+0s) by Foswiki::Store::VC::RcsWrapHandler::BEGIN@22 at line 46 | ||||
42 | 1 | 10µs | if ( $Foswiki::cfg{UseLocale} ) { | ||
43 | require locale; | ||||
44 | import locale(); | ||||
45 | } | ||||
46 | 1 | 9.05ms | 1 | 10µs | } # spent 10µ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 20.0ms (15.7+4.29) within Foswiki::Store::VC::Handler::new which was called 347 times, avg 58µs/call:
# 347 times (15.7ms+4.29ms) by Foswiki::Store::VC::RcsWrapHandler::new at line 28 of /usr/local/src/github.com/foswiki/core/lib/Foswiki/Store/VC/RcsWrapHandler.pm, avg 58µs/call | ||||
63 | 3775 | 15.7ms | my ( $class, $store, $web, $topic, $attachment ) = @_; | ||
64 | |||||
65 | 347 | 1.20ms | ASSERT( $store->isa('Foswiki::Store') ) if DEBUG; # spent 1.20ms making 347 calls to Assert::ASSERTS_OFF, avg 3µs/call | ||
66 | |||||
67 | 347 | 1.03ms | if ( UNIVERSAL::isa( $web, 'Foswiki::Meta' ) ) { # spent 1.03ms making 347 calls to UNIVERSAL::isa, avg 3µs/call | ||
68 | |||||
69 | # $web refers to a meta object | ||||
70 | $attachment = $topic; | ||||
71 | 73 | 538µs | $topic = $web->topic(); # spent 538µs making 73 calls to Foswiki::Meta::topic, avg 7µs/call | ||
72 | 73 | 526µs | $web = $web->web(); # spent 526µs making 73 calls to Foswiki::Meta::web, avg 7µs/call | ||
73 | } | ||||
74 | |||||
75 | # Reuse is good | ||||
76 | my $id = ( $web || 0 ) . '/' . ( $topic || 0 ) . '/' . ( $attachment || 0 ); | ||||
77 | if ( $store->{handler_cache} && $store->{handler_cache}->{$id} ) { | ||||
78 | return $store->{handler_cache}->{$id}; | ||||
79 | } | ||||
80 | |||||
81 | 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 | $store->{handler_cache}->{$id} = $this; | ||||
88 | |||||
89 | if ( $web && $topic ) { | ||||
90 | my $rcsSubDir = ( $Foswiki::cfg{RCS}{useSubDir} ? '/RCS' : '' ); | ||||
91 | |||||
92 | 147 | 539µs | ASSERT( UNTAINTED($web), "web $web is tainted!" ) if DEBUG; # spent 539µs making 147 calls to Assert::ASSERTS_OFF, avg 4µs/call | ||
93 | 147 | 456µs | ASSERT( UNTAINTED($topic), "topic $topic is tainted!" ) if DEBUG; # spent 456µs making 147 calls to Assert::ASSERTS_OFF, avg 3µs/call | ||
94 | if ($attachment) { | ||||
95 | ASSERT( UNTAINTED($attachment) ) if DEBUG; | ||||
96 | $this->{file} = | ||||
97 | $Foswiki::cfg{PubDir} . '/' | ||||
98 | . $web . '/' | ||||
99 | . $topic . '/' | ||||
100 | . $attachment; | ||||
101 | $this->{rcsFile} = | ||||
102 | $Foswiki::cfg{PubDir} . '/' | ||||
103 | . $web . '/' | ||||
104 | . $topic | ||||
105 | . $rcsSubDir . '/' | ||||
106 | . $attachment . ',v'; | ||||
107 | |||||
108 | } | ||||
109 | else { | ||||
110 | $this->{file} = | ||||
111 | $Foswiki::cfg{DataDir} . '/' . $web . '/' . $topic . '.txt'; | ||||
112 | $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 | $Foswiki::cfg{Store}{RememberChangesFor} ||= 31 * 24 * 60 * 60; | ||||
123 | |||||
124 | 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 22.5s (1.57ms+22.5) within Foswiki::Store::VC::Handler::getInfo which was called 21 times, avg 1.07s/call:
# 21 times (1.57ms+22.5s) by Foswiki::Store::VC::RcsWrapHandler::getInfo at line 352 of /usr/local/src/github.com/foswiki/core/lib/Foswiki/Store/VC/RcsWrapHandler.pm, avg 1.07s/call | ||||
220 | 245 | 1.59ms | 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 | 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 | my $info = {}; | ||||
233 | 42 | 1.41ms | if ( $this->noCheckinPending() ) { # spent 1.33ms making 21 calls to Foswiki::Store::VC::Handler::noCheckinPending, avg 63µs/call
# spent 83µs making 21 calls to Foswiki::Store::VC::Handler::CORE:ftis, avg 4µ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 | 7 | 22.5s | $info->{version} = $this->_numRevisions() + 1; # spent 22.5s making 7 calls to Foswiki::Store::VC::RcsWrapHandler::_numRevisions, avg 3.21s/call | ||
243 | $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 | 14 | 8.62ms | $this->_getTOPICINFO($info); # spent 8.62ms making 14 calls to Foswiki::Store::VC::Handler::_getTOPICINFO, avg 616µs/call | ||
249 | $info->{version} = 1; | ||||
250 | $info->{comment} = "pending"; | ||||
251 | } | ||||
252 | 8 | 652µs | $info->{date} = $this->getTimestamp() unless defined $info->{date}; # spent 652µs making 8 calls to Foswiki::Store::VC::Handler::getTimestamp, avg 81µs/call | ||
253 | $info->{version} = 1 unless defined $info->{version}; | ||||
254 | $info->{comment} = '' unless defined $info->{comment}; | ||||
255 | $info->{author} ||= $Foswiki::Users::BaseUserMapping::UNKNOWN_USER_CUID; | ||||
256 | return $info; | ||||
257 | } | ||||
258 | |||||
259 | # Try and read TOPICINFO | ||||
260 | # spent 8.62ms (3.28+5.34) within Foswiki::Store::VC::Handler::_getTOPICINFO which was called 14 times, avg 616µs/call:
# 14 times (3.28ms+5.34ms) by Foswiki::Store::VC::Handler::getInfo at line 248, avg 616µs/call | ||||
261 | 176 | 2.59ms | my ( $this, $info ) = @_; | ||
262 | my $f; | ||||
263 | |||||
264 | 14 | 268µs | if ( open( $f, '<', $this->{file} ) ) { # spent 268µs making 14 calls to Foswiki::Store::VC::Handler::CORE:open, avg 19µs/call | ||
265 | local $/ = "\n"; | ||||
266 | 14 | 190µs | my $ti = <$f>; # spent 190µs making 14 calls to Foswiki::Store::VC::Handler::CORE:readline, avg 14µs/call | ||
267 | 14 | 75µs | close($f); # spent 75µs making 14 calls to Foswiki::Store::VC::Handler::CORE:close, avg 5µs/call | ||
268 | 14 | 258µs | if ( defined $ti && $ti =~ /^%META:TOPICINFO{(.*)}%/ ) { # spent 258µs making 14 calls to Foswiki::Store::VC::Handler::CORE:match, avg 18µs/call | ||
269 | require Foswiki::Attrs; | ||||
270 | 13 | 3.82ms | my $a = Foswiki::Attrs->new($1); # spent 3.82ms making 13 calls to Foswiki::Attrs::new, avg 294µ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 | 13 | 468µs | $info->{version} = Foswiki::Store::cleanUpRevID( $a->{version} ) # spent 468µs making 13 calls to Foswiki::Store::cleanUpRevID, avg 36µs/call | ||
275 | || 1; | ||||
276 | $info->{date} = $a->{date}; | ||||
277 | $info->{author} = $a->{author}; | ||||
278 | $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 6.36ms (4.18+2.18) within Foswiki::Store::VC::Handler::noCheckinPending which was called 117 times, avg 54µs/call:
# 70 times (2.59ms+1.31ms) by Foswiki::Store::VC::Store::readTopic at line 112 of /usr/local/src/github.com/foswiki/core/lib/Foswiki/Store/VC/Store.pm, avg 56µs/call
# 21 times (822µs+508µs) by Foswiki::Store::VC::Handler::getInfo at line 233, avg 63µs/call
# 21 times (511µs+232µs) by Foswiki::Store::VC::RcsWrapHandler::getInfo at line 320 of /usr/local/src/github.com/foswiki/core/lib/Foswiki/Store/VC/RcsWrapHandler.pm, avg 35µs/call
# 5 times (262µs+132µs) by Foswiki::Store::VC::Handler::getLatestRevisionID at line 453, avg 79µs/call | ||||
288 | 881 | 6.40ms | my $this = shift; | ||
289 | my $isValid = 0; | ||||
290 | |||||
291 | 117 | 934µs | if ( !-e $this->{file} ) { # spent 934µs making 117 calls to Foswiki::Store::VC::Handler::CORE:ftis, avg 8µs/call | ||
292 | $isValid = 1; # Hmmmm...... | ||||
293 | } | ||||
294 | else { | ||||
295 | 117 | 510µs | if ( -e $this->{rcsFile} ) { # spent 510µs making 117 calls to Foswiki::Store::VC::Handler::CORE:ftis, avg 4µs/call | ||
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 | local ${^WIN32_SLOPPY_STAT} = | ||||
301 | 1; # don't need to open the file on Win32 | ||||
302 | 74 | 411µs | my $rcsTime = ( stat( $this->{rcsFile} ) )[9]; # spent 411µs making 74 calls to Foswiki::Store::VC::Handler::CORE:stat, avg 6µs/call | ||
303 | 74 | 328µs | my $fileTime = ( stat( $this->{file} ) )[9]; # spent 328µs making 74 calls to Foswiki::Store::VC::Handler::CORE:stat, avg 4µs/call | ||
304 | $isValid = ( $rcsTime < $fileTime ) ? 0 : 1; | ||||
305 | } | ||||
306 | } | ||||
307 | 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 620ms (134µs+620) within Foswiki::Store::VC::Handler::getRevisionHistory which was called 2 times, avg 310ms/call:
# 2 times (134µs+620ms) by Foswiki::Store::VC::Store::getRevisionHistory at line 289 of /usr/local/src/github.com/foswiki/core/lib/Foswiki/Store/VC/Store.pm, avg 310ms/call | ||||
420 | 10 | 142µs | my $this = shift; | ||
421 | 2 | 6µs | ASSERT( $this->{file} ) if DEBUG; # spent 6µs making 2 calls to Assert::ASSERTS_OFF, avg 3µs/call | ||
422 | 2 | 22µs | unless ( -e $this->{rcsFile} ) { # spent 22µs making 2 calls to Foswiki::Store::VC::Handler::CORE:ftis, avg 11µs/call | ||
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 | 620ms | my $maxRev = $this->getLatestRevisionID(); # spent 620ms making 2 calls to Foswiki::Store::VC::Handler::getLatestRevisionID, avg 310ms/call | ||
434 | 2 | 82µs | return Foswiki::Iterator::NumberRangeIterator->new( $maxRev, 1 ); # spent 82µs making 2 calls to Foswiki::Iterator::NumberRangeIterator::new, avg 41µ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 1.41s (270µs+1.41) within Foswiki::Store::VC::Handler::getLatestRevisionID which was called 5 times, avg 282ms/call:
# 3 times (184µs+790ms) by Foswiki::Store::VC::Store::readTopic at line 132 of /usr/local/src/github.com/foswiki/core/lib/Foswiki/Store/VC/Store.pm, avg 263ms/call
# 2 times (87µs+620ms) by Foswiki::Store::VC::Handler::getRevisionHistory at line 433, avg 310ms/call | ||||
447 | 25 | 319µs | my $this = shift; | ||
448 | 5 | 23µs | return 0 unless -e $this->{file}; # spent 23µs making 5 calls to Foswiki::Store::VC::Handler::CORE:ftis, avg 5µs/call | ||
449 | 5 | 1.41s | my $rev = $this->_numRevisions() || 1; # spent 1.41s making 5 calls to Foswiki::Store::VC::RcsWrapHandler::_numRevisions, avg 282ms/call | ||
450 | |||||
451 | # If there is a pending pseudo-revision, need n+1, but only if there is | ||||
452 | # an existing history | ||||
453 | 8 | 411µs | $rev++ unless $this->noCheckinPending() || !-e $this->{rcsFile}; # spent 393µs making 5 calls to Foswiki::Store::VC::Handler::noCheckinPending, avg 79µs/call
# spent 18µs making 3 calls to Foswiki::Store::VC::Handler::CORE:ftis, avg 6µs/call | ||
454 | 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 169ms (108+60.5) within Foswiki::Store::VC::Handler::getTopicNames which was called:
# once (108ms+60.5ms) by Foswiki::Store::VC::Store::eachTopic at line 493 of /usr/local/src/github.com/foswiki/core/lib/Foswiki/Store/VC/Store.pm | ||||
500 | 13752 | 169ms | my $this = shift; | ||
501 | my $dh; | ||||
502 | 1 | 83µs | opendir( $dh, "$Foswiki::cfg{DataDir}/$this->{web}" ) # spent 83µs making 1 call to Foswiki::Store::VC::Handler::CORE:open_dir | ||
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 | 3425 | 9.73ms | my @topicList = # spent 9.73ms making 3425 calls to Foswiki::Store::VC::Handler::CORE:match, avg 3µs/call | ||
508 | 20688 | 31.8ms | map { /^(.*)\.txt$/; $1; } # spent 23.0ms making 13792 calls to Foswiki::Store::VC::Handler::CORE:match, avg 2µs/call
# spent 8.81ms making 6896 calls to Foswiki::Store::VC::Handler::CORE:regcomp, avg 1µs/call | ||
509 | sort | ||||
510 | 2 | 18.9ms | grep { !/$Foswiki::cfg{NameFilter}/ && /\.txt$/ } readdir($dh); # spent 10.3ms making 1 call to Foswiki::Store::VC::Handler::CORE:readdir
# spent 8.52ms making 1 call to Foswiki::Store::VC::Handler::CORE:sort | ||
511 | 1 | 42µs | closedir($dh); # spent 42µs making 1 call to Foswiki::Store::VC::Handler::CORE:closedir | ||
512 | 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 | sub getWebNames { | ||||
540 | my $this = shift; | ||||
541 | my $dir = $Foswiki::cfg{DataDir}; | ||||
542 | $dir .= '/' . $this->{web} if defined $this->{web}; | ||||
543 | my @tmpList; | ||||
544 | my $dh; | ||||
545 | |||||
546 | if ( opendir( $dh, $dir ) ) { | ||||
547 | @tmpList = map { | ||||
548 | 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 | grep { !/\./ && -e "$dir/$_/$Foswiki::cfg{WebPrefsTopicName}.txt" } | ||||
556 | readdir($dh); | ||||
557 | closedir($dh); | ||||
558 | } | ||||
559 | |||||
560 | 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 10.8ms (1.93+8.84) within Foswiki::Store::VC::Handler::getRevision which was called 70 times, avg 154µs/call:
# 70 times (1.93ms+8.84ms) by Foswiki::Store::VC::RcsWrapHandler::getRevision at line 250 of /usr/local/src/github.com/foswiki/core/lib/Foswiki/Store/VC/RcsWrapHandler.pm, avg 154µs/call | ||||
600 | 210 | 1.91ms | my ($this) = @_; | ||
601 | 70 | 430µs | if ( defined $this->{file} && -e $this->{file} ) { # spent 430µs making 70 calls to Foswiki::Store::VC::Handler::CORE:ftis, avg 6µs/call | ||
602 | 70 | 8.41ms | return ( readFile( $this, $this->{file} ), 1 ); # spent 8.41ms making 70 calls to Foswiki::Store::VC::Handler::readFile, avg 120µs/call | ||
603 | } | ||||
604 | return ( undef, 0 ); | ||||
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 5.19ms (3.27+1.92) within Foswiki::Store::VC::Handler::storedDataExists which was called 274 times, avg 19µs/call:
# 272 times (3.23ms+1.90ms) by Foswiki::Store::VC::Store::topicExists at line 463 of /usr/local/src/github.com/foswiki/core/lib/Foswiki/Store/VC/Store.pm, avg 19µs/call
# 2 times (35µs+26µs) by Foswiki::Store::VC::Store::webExists at line 452 of /usr/local/src/github.com/foswiki/core/lib/Foswiki/Store/VC/Store.pm, avg 30µs/call | ||||
616 | 822 | 5.40ms | my $this = shift; | ||
617 | return 0 unless $this->{file}; | ||||
618 | 274 | 1.92ms | return -e $this->{file}; # spent 1.92ms making 274 calls to Foswiki::Store::VC::Handler::CORE:ftis, avg 7µs/call | ||
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 | if ( $f =~ /^(.*)\.lease$/ ) { | ||||
919 | if ( !-e "$1.txt,v" ) { | ||||
920 | unlink($f); | ||||
921 | } | ||||
922 | } | ||||
923 | } | ||||
924 | closedir($W); | ||||
925 | } | ||||
926 | } | ||||
927 | |||||
928 | sub test { | ||||
929 | my ( $this, $test ) = @_; | ||||
930 | return eval "-$test '$this->{file}'"; | ||||
931 | } | ||||
932 | |||||
933 | # Used by subclasses | ||||
934 | sub saveStream { | ||||
935 | my ( $this, $fh ) = @_; | ||||
936 | |||||
937 | ASSERT($fh) if DEBUG; | ||||
938 | |||||
939 | $this->mkPathTo( $this->{file} ); | ||||
940 | my $F; | ||||
941 | open( $F, '>', $this->{file} ) | ||||
942 | || throw Error::Simple( | ||||
943 | 'VC::Handler: open ' . $this->{file} . ' failed: ' . $! ); | ||||
944 | binmode($F) | ||||
945 | || throw Error::Simple( | ||||
946 | 'VC::Handler: failed to binmode ' . $this->{file} . ': ' . $! ); | ||||
947 | my $text; | ||||
948 | while ( read( $fh, $text, 1024 ) ) { | ||||
949 | print $F $text; | ||||
950 | } | ||||
951 | close($F) | ||||
952 | || throw Error::Simple( | ||||
953 | 'VC::Handler: close ' . $this->{file} . ' failed: ' . $! ); | ||||
954 | |||||
955 | chmod( $Foswiki::cfg{RCS}{filePermission}, $this->{file} ); | ||||
956 | } | ||||
957 | |||||
958 | sub copyFile { | ||||
959 | my ( $this, $from, $to ) = @_; | ||||
960 | |||||
961 | $this->mkPathTo($to); | ||||
962 | unless ( File::Copy::copy( $from, $to ) ) { | ||||
963 | throw Error::Simple( | ||||
964 | 'VC::Handler: copy ' . $from . ' to ' . $to . ' failed: ' . $! ); | ||||
965 | } | ||||
966 | } | ||||
967 | |||||
968 | sub moveFile { | ||||
969 | my ( $this, $from, $to ) = @_; | ||||
970 | ASSERT( -e $from ) if DEBUG; | ||||
971 | $this->mkPathTo($to); | ||||
972 | unless ( File::Copy::move( $from, $to ) ) { | ||||
973 | throw Error::Simple( | ||||
974 | 'VC::Handler: move ' . $from . ' to ' . $to . ' failed: ' . $! ); | ||||
975 | } | ||||
976 | } | ||||
977 | |||||
978 | # Used by subclasses | ||||
979 | sub saveFile { | ||||
980 | my ( $this, $name, $text ) = @_; | ||||
981 | |||||
982 | $this->mkPathTo($name); | ||||
983 | my $fh; | ||||
984 | open( $fh, '>', $name ) | ||||
985 | or throw Error::Simple( | ||||
986 | 'VC::Handler: failed to create file ' . $name . ': ' . $! ); | ||||
987 | flock( $fh, LOCK_EX ) | ||||
988 | or throw Error::Simple( | ||||
989 | 'VC::Handler: failed to lock file ' . $name . ': ' . $! ); | ||||
990 | binmode($fh) | ||||
991 | or throw Error::Simple( | ||||
992 | 'VC::Handler: failed to binmode ' . $name . ': ' . $! ); | ||||
993 | print $fh $text | ||||
994 | or throw Error::Simple( | ||||
995 | 'VC::Handler: failed to print into ' . $name . ': ' . $! ); | ||||
996 | close($fh) | ||||
997 | or throw Error::Simple( | ||||
998 | 'VC::Handler: failed to close file ' . $name . ': ' . $! ); | ||||
999 | return; | ||||
1000 | } | ||||
1001 | |||||
1002 | # Used by subclasses | ||||
1003 | # spent 8.41ms (4.58+3.83) within Foswiki::Store::VC::Handler::readFile which was called 70 times, avg 120µs/call:
# 70 times (4.58ms+3.83ms) by Foswiki::Store::VC::Handler::getRevision at line 602, avg 120µs/call | ||||
1004 | 770 | 8.57ms | my ( $this, $name ) = @_; | ||
1005 | 70 | 238µs | ASSERT($name) if DEBUG; # spent 238µs making 70 calls to Assert::ASSERTS_OFF, avg 3µs/call | ||
1006 | my $data; | ||||
1007 | my $IN_FILE; | ||||
1008 | 70 | 1.56ms | if ( open( $IN_FILE, '<', $name ) ) { # spent 1.56ms making 70 calls to Foswiki::Store::VC::Handler::CORE:open, avg 22µs/call | ||
1009 | 70 | 200µs | binmode($IN_FILE); # spent 200µs making 70 calls to Foswiki::Store::VC::Handler::CORE:binmode, avg 3µs/call | ||
1010 | local $/ = undef; | ||||
1011 | 70 | 1.46ms | $data = <$IN_FILE>; # spent 1.46ms making 70 calls to Foswiki::Store::VC::Handler::CORE:readline, avg 21µs/call | ||
1012 | 70 | 374µs | close($IN_FILE); # spent 374µs making 70 calls to Foswiki::Store::VC::Handler::CORE:close, avg 5µs/call | ||
1013 | } | ||||
1014 | $data ||= ''; | ||||
1015 | return $data; | ||||
1016 | } | ||||
1017 | |||||
1018 | # Used by subclasses | ||||
1019 | sub mkTmpFilename { | ||||
1020 | my $tmpdir = File::Spec->tmpdir(); | ||||
1021 | my $file = _mktemp( 'foswikiAttachmentXXXXXX', $tmpdir ); | ||||
1022 | return File::Spec->catfile( $tmpdir, $file ); | ||||
1023 | } | ||||
1024 | |||||
1025 | # Adapted from CPAN - File::MkTemp | ||||
1026 | sub _mktemp { | ||||
1027 | my ( $template, $dir, $ext, $keepgen, $lookup ); | ||||
1028 | my ( @template, @letters ); | ||||
1029 | |||||
1030 | ASSERT( @_ == 1 || @_ == 2 || @_ == 3 ) if DEBUG; | ||||
1031 | |||||
1032 | ( $template, $dir, $ext ) = @_; | ||||
1033 | @template = split //, $template; | ||||
1034 | |||||
1035 | ASSERT( $template =~ /XXXXXX$/ ) if DEBUG; | ||||
1036 | |||||
1037 | if ($dir) { | ||||
1038 | ASSERT( -e $dir ) if DEBUG; | ||||
1039 | } | ||||
1040 | |||||
1041 | @letters = | ||||
1042 | split( //, 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ' ); | ||||
1043 | |||||
1044 | $keepgen = 1; | ||||
1045 | |||||
1046 | while ($keepgen) { | ||||
1047 | for ( my $i = $#template ; $i >= 0 && ( $template[$i] eq 'X' ) ; $i-- ) | ||||
1048 | { | ||||
1049 | $template[$i] = $letters[ int( rand 52 ) ]; | ||||
1050 | } | ||||
1051 | |||||
1052 | undef $template; | ||||
1053 | |||||
1054 | $template = pack 'a' x @template, @template; | ||||
1055 | |||||
1056 | $template = $template . $ext if ($ext); | ||||
1057 | |||||
1058 | if ($dir) { | ||||
1059 | $lookup = File::Spec->catfile( $dir, $template ); | ||||
1060 | $keepgen = 0 unless ( -e $lookup ); | ||||
1061 | } | ||||
1062 | else { | ||||
1063 | $keepgen = 0; | ||||
1064 | } | ||||
1065 | |||||
1066 | next if $keepgen == 0; | ||||
1067 | } | ||||
1068 | |||||
1069 | return ($template); | ||||
1070 | } | ||||
1071 | |||||
1072 | # remove a directory and all subdirectories. | ||||
1073 | sub _rmtree { | ||||
1074 | my $root = shift; | ||||
1075 | my $D; | ||||
1076 | |||||
1077 | if ( opendir( $D, $root ) ) { | ||||
1078 | foreach my $entry ( grep { !/^\.+$/ } readdir($D) ) { | ||||
1079 | $entry =~ /^(.*)$/; | ||||
1080 | $entry = $root . '/' . $1; | ||||
1081 | if ( -d $entry ) { | ||||
1082 | _rmtree($entry); | ||||
1083 | } | ||||
1084 | elsif ( !unlink($entry) && -e $entry ) { | ||||
1085 | if ( $Foswiki::cfg{OS} ne 'WINDOWS' ) { | ||||
1086 | throw Error::Simple( 'VC::Handler: Failed to delete file ' | ||||
1087 | . $entry . ': ' | ||||
1088 | . $! ); | ||||
1089 | } | ||||
1090 | else { | ||||
1091 | |||||
1092 | # Windows sometimes fails to delete files when | ||||
1093 | # subprocesses haven't exited yet, because the | ||||
1094 | # subprocess still has the file open. Live with it. | ||||
1095 | print STDERR 'WARNING: Failed to delete file ', | ||||
1096 | $entry, ": $!\n"; | ||||
1097 | } | ||||
1098 | } | ||||
1099 | } | ||||
1100 | closedir($D); | ||||
1101 | |||||
1102 | if ( !rmdir($root) ) { | ||||
1103 | if ( $Foswiki::cfg{OS} ne 'WINDOWS' ) { | ||||
1104 | throw Error::Simple( | ||||
1105 | 'VC::Handler: Failed to delete ' . $root . ': ' . $! ); | ||||
1106 | } | ||||
1107 | else { | ||||
1108 | print STDERR 'WARNING: Failed to delete ' . $root . ': ' . $!, | ||||
1109 | "\n"; | ||||
1110 | } | ||||
1111 | } | ||||
1112 | } | ||||
1113 | } | ||||
1114 | |||||
1115 | { | ||||
1116 | |||||
1117 | # Package that ties a filehandle to a memory string for reading | ||||
1118 | 1 | 3µs | package Foswiki::Store::_MemoryFile; | ||
1119 | |||||
1120 | sub TIEHANDLE { | ||||
1121 | my ( $class, $data ) = @_; | ||||
1122 | return | ||||
1123 | bless( { data => $data, size => length($data), ptr => 0 }, $class ); | ||||
1124 | } | ||||
1125 | |||||
1126 | sub READ { | ||||
1127 | my $this = shift; | ||||
1128 | my ( undef, $len, $offset ) = @_; | ||||
1129 | if ( $this->{size} - $this->{ptr} < $len ) { | ||||
1130 | $len = $this->{size} - $this->{ptr}; | ||||
1131 | } | ||||
1132 | return 0 unless $len; | ||||
1133 | $_[0] = substr( $this->{data}, $this->{ptr}, $len ); | ||||
1134 | $this->{ptr} += $len; | ||||
1135 | return $len; | ||||
1136 | } | ||||
1137 | |||||
1138 | sub READLINE { | ||||
1139 | my $this = shift; | ||||
1140 | return if $this->{ptr} == $this->{size}; | ||||
1141 | return substr( $this->{data}, $this->{ptr} ) if !defined $/; | ||||
1142 | my $start = $this->{ptr}; | ||||
1143 | while ( $this->{ptr} < $this->{size} | ||||
1144 | && substr( $this->{data}, $this->{ptr}, 1 ) ne $/ ) | ||||
1145 | { | ||||
1146 | $this->{ptr}++; | ||||
1147 | } | ||||
1148 | $this->{ptr}++ if $this->{ptr} < $this->{size}; | ||||
1149 | return substr( $this->{data}, $start, $this->{ptr} - $start ); | ||||
1150 | } | ||||
1151 | |||||
1152 | sub CLOSE { | ||||
1153 | my $this = shift; | ||||
1154 | $this->{data} = undef; | ||||
1155 | } | ||||
1156 | } | ||||
1157 | |||||
1158 | =begin TML | ||||
1159 | |||||
1160 | ---++ ObjectMethod openStream($mode, %opts) -> $fh | ||||
1161 | |||||
1162 | Opens a file handle onto the store. This method is primarily to | ||||
1163 | support virtual file systems. | ||||
1164 | |||||
1165 | =$mode= can be '<', '>' or '>>' for read, write, and append | ||||
1166 | respectively. % | ||||
1167 | |||||
1168 | =%opts= can take different settings depending on =$mode=. | ||||
1169 | * =$mode='<'= | ||||
1170 | * =version= - revision of the object to open e.g. =version => 6= | ||||
1171 | Default behaviour is to return the latest revision. Note that it is | ||||
1172 | much more efficient to pass undef than to pass the number of the | ||||
1173 | latest revision. | ||||
1174 | * =$mode='>'= or ='>>' | ||||
1175 | * no options | ||||
1176 | |||||
1177 | =cut | ||||
1178 | |||||
1179 | sub openStream { | ||||
1180 | my ( $this, $mode, %opts ) = @_; | ||||
1181 | my $stream; | ||||
1182 | if ( $mode eq '<' && $opts{version} ) { | ||||
1183 | |||||
1184 | # Bulk load the revision and tie a filehandle | ||||
1185 | require Symbol; | ||||
1186 | $stream = Symbol::gensym; # create an anonymous glob | ||||
1187 | tie( *$stream, 'Foswiki::Store::_MemoryFile', | ||||
1188 | $this->getRevision( $opts{version} ) ); | ||||
1189 | } | ||||
1190 | else { | ||||
1191 | if ( $mode =~ />/ ) { | ||||
1192 | $this->mkPathTo( $this->{file} ); | ||||
1193 | } | ||||
1194 | unless ( open( $stream, $mode, $this->{file} ) ) { | ||||
1195 | throw Error::Simple( 'VC::Handler: stream open ' | ||||
1196 | . $this->{file} | ||||
1197 | . ' failed: ' | ||||
1198 | . $! ); | ||||
1199 | } | ||||
1200 | binmode $stream; | ||||
1201 | } | ||||
1202 | return $stream; | ||||
1203 | } | ||||
1204 | |||||
1205 | # as long as stat is defined, return an emulated set of attributes for that | ||||
1206 | # attachment. | ||||
1207 | sub _constructAttributesForAutoAttached { | ||||
1208 | my ( $file, $stat ) = @_; | ||||
1209 | |||||
1210 | my %pairs = ( | ||||
1211 | name => $file, | ||||
1212 | version => '', | ||||
1213 | path => $file, | ||||
1214 | size => $stat->[7], | ||||
1215 | date => $stat->[9], | ||||
1216 | |||||
1217 | # user => 'UnknownUser', #safer _not_ to default - Foswiki will fill it in when it needs to | ||||
1218 | comment => '', | ||||
1219 | attr => '', | ||||
1220 | autoattached => '1' | ||||
1221 | ); | ||||
1222 | |||||
1223 | if ( $#$stat > 0 ) { | ||||
1224 | return \%pairs; | ||||
1225 | } | ||||
1226 | else { | ||||
1227 | return; | ||||
1228 | } | ||||
1229 | } | ||||
1230 | |||||
1231 | =begin TML | ||||
1232 | |||||
1233 | ---++ ObjectMethod synchroniseAttachmentsList(\@old) -> @new | ||||
1234 | |||||
1235 | Synchronise the attachment list from meta-data with what's actually | ||||
1236 | stored in the DB. Returns an ARRAY of FILEATTACHMENTs. These can be | ||||
1237 | put in the new tom. | ||||
1238 | |||||
1239 | This function is only called when the {RCS}{AutoAttachPubFiles} configuration | ||||
1240 | option is set. | ||||
1241 | |||||
1242 | =cut | ||||
1243 | |||||
1244 | # IDEA On Windows machines where the underlying filesystem can store arbitary | ||||
1245 | # meta data against files, this might replace/fulfil the COMMENT purpose | ||||
1246 | # | ||||
1247 | # TODO consider logging when things are added to metadata | ||||
1248 | |||||
1249 | sub synchroniseAttachmentsList { | ||||
1250 | my ( $this, $attachmentsKnownInMeta ) = @_; | ||||
1251 | |||||
1252 | my %filesListedInPub = $this->_getAttachmentStats(); | ||||
1253 | my %filesListedInMeta = (); | ||||
1254 | |||||
1255 | # You need the following lines if you want metadata to supplement | ||||
1256 | # the filesystem | ||||
1257 | if ( defined $attachmentsKnownInMeta ) { | ||||
1258 | %filesListedInMeta = | ||||
1259 | map { $_->{name} => $_ } @$attachmentsKnownInMeta; | ||||
1260 | } | ||||
1261 | |||||
1262 | foreach my $file ( keys %filesListedInPub ) { | ||||
1263 | if ( $filesListedInMeta{$file} ) { | ||||
1264 | |||||
1265 | # Bring forward any missing yet wanted attributes | ||||
1266 | foreach my $field (qw(comment attr user version)) { | ||||
1267 | if ( $filesListedInMeta{$file}{$field} ) { | ||||
1268 | $filesListedInPub{$file}{$field} = | ||||
1269 | $filesListedInMeta{$file}{$field}; | ||||
1270 | } | ||||
1271 | } | ||||
1272 | } | ||||
1273 | } | ||||
1274 | |||||
1275 | # A comparison of the keys of the $filesListedInMeta and %filesListedInPub | ||||
1276 | # would show files that were in Meta but have disappeared from Pub. | ||||
1277 | |||||
1278 | # Do not change this from array to hash, you would lose the | ||||
1279 | # proper attachment sequence | ||||
1280 | my @deindexedBecauseMetaDoesnotIndexAttachments = values(%filesListedInPub); | ||||
1281 | |||||
1282 | return @deindexedBecauseMetaDoesnotIndexAttachments; | ||||
1283 | } | ||||
1284 | |||||
1285 | =begin TML | ||||
1286 | |||||
1287 | ---++ ObjectMethod getAttachmentList() -> @list | ||||
1288 | |||||
1289 | Get list of attachment names actually stored for topic. | ||||
1290 | |||||
1291 | =cut | ||||
1292 | |||||
1293 | sub getAttachmentList { | ||||
1294 | my $this = shift; | ||||
1295 | my $dir = "$Foswiki::cfg{PubDir}/$this->{web}/$this->{topic}"; | ||||
1296 | my $dh; | ||||
1297 | opendir( $dh, $dir ) || return (); | ||||
1298 | my @files = grep { !/^[.*_]/ && !/,v$/ } readdir($dh); | ||||
1299 | closedir($dh); | ||||
1300 | return @files; | ||||
1301 | } | ||||
1302 | |||||
1303 | # returns {} of filename => { key => value, key2 => value } | ||||
1304 | # for any given web, topic | ||||
1305 | sub _getAttachmentStats { | ||||
1306 | my $this = shift; | ||||
1307 | my %attachmentList = (); | ||||
1308 | my $dir = "$Foswiki::cfg{PubDir}/$this->{web}/$this->{topic}"; | ||||
1309 | foreach my $attachment ( $this->getAttachmentList() ) { | ||||
1310 | my @stat = stat( $dir . "/" . $attachment ); | ||||
1311 | $attachmentList{$attachment} = | ||||
1312 | _constructAttributesForAutoAttached( $attachment, \@stat ); | ||||
1313 | } | ||||
1314 | return %attachmentList; | ||||
1315 | } | ||||
1316 | |||||
1317 | sub _dirForTopicAttachments { | ||||
1318 | my ( $web, $topic ) = @_; | ||||
1319 | } | ||||
1320 | |||||
1321 | =begin TML | ||||
1322 | |||||
1323 | ---++ ObjectMethod stringify() | ||||
1324 | |||||
1325 | Generate string representation for debugging | ||||
1326 | |||||
1327 | =cut | ||||
1328 | |||||
1329 | sub stringify { | ||||
1330 | my $this = shift; | ||||
1331 | my @reply; | ||||
1332 | foreach my $key (qw(web topic attachment file rcsFile)) { | ||||
1333 | if ( defined $this->{$key} ) { | ||||
1334 | push( @reply, "$key=$this->{$key}" ); | ||||
1335 | } | ||||
1336 | } | ||||
1337 | return join( ',', @reply ); | ||||
1338 | } | ||||
1339 | |||||
1340 | # Chop out recognisable path components to prevent hacking based on error | ||||
1341 | # messages | ||||
1342 | sub hidePath { | ||||
1343 | my ( $this, $erf ) = @_; | ||||
1344 | $erf =~ s#.*(/\w+/\w+\.[\w,]*)$#...$1#; | ||||
1345 | return $erf; | ||||
1346 | } | ||||
1347 | |||||
1348 | =begin TML | ||||
1349 | |||||
1350 | ---++ ObjectMethod recordChange($cUID, $rev, $more) | ||||
1351 | Record that the file changed, and who changed it | ||||
1352 | |||||
1353 | =cut | ||||
1354 | |||||
1355 | sub recordChange { | ||||
1356 | my ( $this, $cUID, $rev, $more ) = @_; | ||||
1357 | $more ||= ''; | ||||
1358 | ASSERT($cUID) if DEBUG; | ||||
1359 | |||||
1360 | my $file = $Foswiki::cfg{DataDir} . '/' . $this->{web} . '/.changes'; | ||||
1361 | |||||
1362 | my @changes = | ||||
1363 | map { | ||||
1364 | my @row = split( /\t/, $_, 5 ); | ||||
1365 | \@row | ||||
1366 | } | ||||
1367 | split( /[\r\n]+/, readFile( $this, $file ) ); | ||||
1368 | |||||
1369 | # Forget old stuff | ||||
1370 | my $cutoff = time() - $Foswiki::cfg{Store}{RememberChangesFor}; | ||||
1371 | while ( scalar(@changes) && $changes[0]->[2] < $cutoff ) { | ||||
1372 | shift(@changes); | ||||
1373 | } | ||||
1374 | |||||
1375 | # Add the new change to the end of the file | ||||
1376 | push( @changes, [ $this->{topic} || '.', $cUID, time(), $rev, $more ] ); | ||||
1377 | |||||
1378 | # Doing this using a Schwartzian transform sometimes causes a mysterious | ||||
1379 | # undefined value, so had to unwrap it to a for loop. | ||||
1380 | for ( my $i = 0 ; $i <= $#changes ; $i++ ) { | ||||
1381 | $changes[$i] = join( "\t", @{ $changes[$i] } ); | ||||
1382 | } | ||||
1383 | |||||
1384 | my $text = join( "\n", @changes ); | ||||
1385 | |||||
1386 | saveFile( $this, $file, $text ); | ||||
1387 | } | ||||
1388 | |||||
1389 | =begin TML | ||||
1390 | |||||
1391 | ---++ ObjectMethod eachChange($since) -> $iterator | ||||
1392 | |||||
1393 | Return iterator over changes - see Store for details | ||||
1394 | |||||
1395 | =cut | ||||
1396 | |||||
1397 | sub eachChange { | ||||
1398 | my ( $this, $since ) = @_; | ||||
1399 | my $file = $Foswiki::cfg{DataDir} . '/' . $this->{web} . '/.changes'; | ||||
1400 | require Foswiki::ListIterator; | ||||
1401 | |||||
1402 | if ( -r $file ) { | ||||
1403 | |||||
1404 | # Could use a LineIterator to avoid reading the whole | ||||
1405 | # file, but it hardly seems worth it. | ||||
1406 | my @changes = | ||||
1407 | map { | ||||
1408 | |||||
1409 | # Create a hash for this line | ||||
1410 | { | ||||
1411 | topic => Foswiki::Sandbox::untaint( | ||||
1412 | $_->[0], \&Foswiki::Sandbox::validateTopicName | ||||
1413 | ), | ||||
1414 | user => $_->[1], | ||||
1415 | time => $_->[2], | ||||
1416 | revision => $_->[3], | ||||
1417 | more => $_->[4] | ||||
1418 | }; | ||||
1419 | } | ||||
1420 | grep { | ||||
1421 | |||||
1422 | # Filter on time | ||||
1423 | $_->[2] && $_->[2] >= $since | ||||
1424 | } | ||||
1425 | map { | ||||
1426 | |||||
1427 | # Split line into an array | ||||
1428 | my @row = split( /\t/, $_, 5 ); | ||||
1429 | \@row; | ||||
1430 | } | ||||
1431 | reverse split( /[\r\n]+/, readFile( $this, $file ) ); | ||||
1432 | |||||
1433 | return Foswiki::ListIterator->new( \@changes ); | ||||
1434 | } | ||||
1435 | else { | ||||
1436 | my $changes = []; | ||||
1437 | return Foswiki::ListIterator->new($changes); | ||||
1438 | } | ||||
1439 | } | ||||
1440 | |||||
1441 | # ObjectMethod getTimestamp() -> $integer | ||||
1442 | # Get the timestamp of the file | ||||
1443 | # Returns 0 if no file, otherwise epoch seconds | ||||
1444 | # Used in subclasses | ||||
1445 | |||||
1446 | # spent 652µs (386+266) within Foswiki::Store::VC::Handler::getTimestamp which was called 8 times, avg 81µs/call:
# 8 times (386µs+266µs) by Foswiki::Store::VC::Handler::getInfo at line 252, avg 81µs/call | ||||
1447 | 48 | 527µs | my ($this) = @_; | ||
1448 | 8 | 88µs | ASSERT( $this->{file} ) if DEBUG; # spent 88µs making 8 calls to Assert::ASSERTS_OFF, avg 11µs/call | ||
1449 | |||||
1450 | my $date = 0; | ||||
1451 | 8 | 112µs | if ( -e $this->{file} ) { # spent 112µs making 8 calls to Foswiki::Store::VC::Handler::CORE:ftis, avg 14µs/call | ||
1452 | |||||
1453 | # If the stat fails, stamp it with some arbitrary static | ||||
1454 | # time in the past (00:40:05 on 5th Jan 1989) | ||||
1455 | 8 | 66µs | $date = ( stat $this->{file} )[9] || 600000000; # spent 66µs making 8 calls to Foswiki::Store::VC::Handler::CORE:stat, avg 8µs/call | ||
1456 | } | ||||
1457 | return $date; | ||||
1458 | } | ||||
1459 | |||||
1460 | 1 | 5µs | 1; | ||
1461 | |||||
1462 | __END__ | ||||
# spent 200µs within Foswiki::Store::VC::Handler::CORE:binmode which was called 70 times, avg 3µs/call:
# 70 times (200µs+0s) by Foswiki::Store::VC::Handler::readFile at line 1009, avg 3µs/call | |||||
sub Foswiki::Store::VC::Handler::CORE:close; # opcode | |||||
# spent 42µs within Foswiki::Store::VC::Handler::CORE:closedir which was called:
# once (42µs+0s) by Foswiki::Store::VC::Handler::getTopicNames at line 511 | |||||
# spent 4.05ms within Foswiki::Store::VC::Handler::CORE:ftis which was called 617 times, avg 7µs/call:
# 274 times (1.92ms+0s) by Foswiki::Store::VC::Handler::storedDataExists at line 618, avg 7µs/call
# 117 times (934µs+0s) by Foswiki::Store::VC::Handler::noCheckinPending at line 291, avg 8µs/call
# 117 times (510µs+0s) by Foswiki::Store::VC::Handler::noCheckinPending at line 295, avg 4µs/call
# 70 times (430µs+0s) by Foswiki::Store::VC::Handler::getRevision at line 601, avg 6µs/call
# 21 times (83µs+0s) by Foswiki::Store::VC::Handler::getInfo at line 233, avg 4µs/call
# 8 times (112µs+0s) by Foswiki::Store::VC::Handler::getTimestamp at line 1451, avg 14µs/call
# 5 times (23µs+0s) by Foswiki::Store::VC::Handler::getLatestRevisionID at line 448, avg 5µs/call
# 3 times (18µs+0s) by Foswiki::Store::VC::Handler::getLatestRevisionID at line 453, avg 6µs/call
# 2 times (22µs+0s) by Foswiki::Store::VC::Handler::getRevisionHistory at line 422, avg 11µs/call | |||||
# spent 33.0ms within Foswiki::Store::VC::Handler::CORE:match which was called 17231 times, avg 2µs/call:
# 13792 times (23.0ms+0s) by Foswiki::Store::VC::Handler::getTopicNames at line 508, avg 2µs/call
# 3425 times (9.73ms+0s) by Foswiki::Store::VC::Handler::getTopicNames at line 507, avg 3µs/call
# 14 times (258µs+0s) by Foswiki::Store::VC::Handler::_getTOPICINFO at line 268, avg 18µs/call | |||||
sub Foswiki::Store::VC::Handler::CORE:open; # opcode | |||||
# spent 83µs within Foswiki::Store::VC::Handler::CORE:open_dir which was called:
# once (83µs+0s) by Foswiki::Store::VC::Handler::getTopicNames at line 502 | |||||
# spent 10.3ms within Foswiki::Store::VC::Handler::CORE:readdir which was called:
# once (10.3ms+0s) by Foswiki::Store::VC::Handler::getTopicNames at line 510 | |||||
sub Foswiki::Store::VC::Handler::CORE:readline; # opcode | |||||
# spent 8.81ms within Foswiki::Store::VC::Handler::CORE:regcomp which was called 6896 times, avg 1µs/call:
# 6896 times (8.81ms+0s) by Foswiki::Store::VC::Handler::getTopicNames at line 508, avg 1µs/call | |||||
# spent 8.52ms within Foswiki::Store::VC::Handler::CORE:sort which was called:
# once (8.52ms+0s) by Foswiki::Store::VC::Handler::getTopicNames at line 510 | |||||
# spent 804µs within Foswiki::Store::VC::Handler::CORE:stat which was called 156 times, avg 5µs/call:
# 74 times (411µs+0s) by Foswiki::Store::VC::Handler::noCheckinPending at line 302, avg 6µs/call
# 74 times (328µs+0s) by Foswiki::Store::VC::Handler::noCheckinPending at line 303, avg 4µs/call
# 8 times (66µs+0s) by Foswiki::Store::VC::Handler::getTimestamp at line 1455, avg 8µs/call |