r91039 MediaWiki - Code Review archive

Repository:MediaWiki
Revision:r91038‎ | r91039 | r91040 >
Date:01:25, 29 June 2011
Author:nelson
Status:deferred
Tags:
Comment:
Relete MUCH identical code supplied by the parent class. Remove dead code.
Make an internal version of resolveVirtualURL so the external version can fetch (in the future) a temp copy of the file.
Update TODO list with things done.
Nuke SwiftFile*Match because we didn't need them to do anything different.
Remove JunkyJunk (finally).
Modified paths:
  • /trunk/extensions/SwiftMedia/LocalSettings.php (modified) (history)
  • /trunk/extensions/SwiftMedia/SwiftMedia.body.php (modified) (history)
  • /trunk/extensions/SwiftMedia/TODO (modified) (history)

Diff [purge]

Index: trunk/extensions/SwiftMedia/LocalSettings.php
@@ -129,7 +129,7 @@
130130 require_once( "$IP/extensions/SwiftMedia/SwiftMedia.php" );
131131
132132 $wgUploadDirectory = "$IP/images/swift";
133 -$wgDeletedDirectory = "{$wgUploadDirectory}/deleted";
 133+// we don't need this and will ignore it. $wgDeletedDirectory = "{$wgUploadDirectory}/deleted";
134134 $wgUploadPath = "http://alsted.wikimedia.org/images/swift";
135135
136136 $wgLocalFileRepo = array(
@@ -148,9 +148,10 @@
149149 'hashLevels' => $wgHashedUploadDirectory ? 2 : 0,
150150 'thumbScriptUrl' => $wgThumbnailScriptPath,
151151 'transformVia404' => !$wgGenerateThumbnailOnParse,
152 - 'deletedDir' => $wgDeletedDirectory,
 152+ #'deletedDir' => $wgDeletedDirectory,
153153 'deletedHashLevels' => 3
154154 );
155155
156156 $wgDebugLogFile = "/var/www/debug/abcd";
 157+$wgDebugTimestamps = true;
157158 $wgShowExceptionDetails = true;
Index: trunk/extensions/SwiftMedia/TODO
@@ -1,10 +1,83 @@
2 -5) The Upload seems to take more time than I expect, but that could be a function of generating the six thumbnails.
 2+Pending:
 3+
34 6) There's no 404 handler to generate missing thumbnails.
45 7) There's no support for remote thumbnailing.
 6+7.1) The SpecialUploadStash, when it calls the remote scaler, is actually just relying on the 404 handler on upload.wm.org.
 7+ It's just executing thumb.php (is it thumb??) to make a thumbnail on a temporary name. Note that it's using a temp/
 8+ directory in the thumbs container, not a directory in the temp container.
 9+7.2) The scaler cluster is simply some machines with access to the originals and thumb storage. The 404 handler running on the Apache
 10+ front-ends forwards the request to a thumb.php running on the scaler cluster. Thumb.php takes care of creating the thumbnail.
 11+6&7) Basically, the code which currently fetches 404 thumbs from upload.wikimedia.org needs to be changed slightly so that it inserts
 12+ thumb.php with the appropriate parameters and fetches from the scaler cluster.
513 8) Test cases (but of course that could be done until the cows come home).
614 9) Read through the code and look for anything which is insane.
715 10) Remove directory from $wgLocalFileRepo, to make sure that there's no references to it. Ditto for wgDeletedDirectory and deletedDir.
816 11) Determine what to do about the one remaining core change needed for Swift.
 17+12) Why is anybody calling resolveVirtualUrl()? It's defined in the Repo, but getPath() is defined against a file.
 18+Why is UploadStashFile() being called with a virtual URL? Once the file has been stashed() it has an object name. The container name is implicit.
 19+Should UploadStashFile *always* (in our case) be called with a virtual URL?
 20+
 21+Resolved:
 22+
 23+5) The Upload seems to take more time than I expect, but that could be a function of generating the six thumbnails.
 24+ It *is* a function of generating the seven (we generate 800x600 twice) thumbnails. Each one takes 1/2 second.
 25+10) Remove directory from $wgLocalFileRepo, to make sure that there's no references to it. Ditto for wgDeletedDirectory and deletedDir.
 26+ wgDeletedDirectory and deletedDir can be removed.
927 12) Implement repo->freeTemp() - needed by several extensions and UploadFromStash.
1028 13) Do we need $wgLocalRepo->ThumbUrl to be configurable given that the Python middleware presumes it?
 29+ We currently have no need for it to be configurable in Swift. I'll just hard-code it to .../thumb with a note saying
 30+ that if it gets changed here, it needs to be changed in the Swift middleware as well.
1131
 32+
 33+neilk_: okay, the moment when an upload passes from being a temp file into something else is at $upload->processUpload()
 34+neilk_: in the old design, in essence, all this does is move a file into an NFS directory, and creates the matching database entry which creates a wiki page.
 35+neilk_: so far I don't think this should be news?
 36+nelson____: right
 37+neilk_: So that's includes/specials/SpecialUpload.php
 38+neilk_: then there's includes/api/ApiUpload.php
 39+neilk_: which is similar but not quite the same
 40+
 41+Watch for that!
 42+
 43+neilk_: in ApiUpload.php there is the option to stash explicitly
 44+neilk_: so the path is a bit convoluted in ApiUpload.php. Also if I remember right the file is accessed a bit differently
 45+neilk_: it is possible to have stashing in ALL of these cases
 46+neilk_: but in SpecialUpload, stashing occurs if there's a recoverable error with the file, like a bad file name
 47+neilk_: in ApiUpload, stashing can happen for that reason, or it can happens if you ask for it explicitly (which is how UploadWizard works).
 48+neilk_: nelson__: anyway is this answering your question yet?
 49+nelson____: yes.
 50+neilk_: ok so that's the overview of the upload methods & stashing, what else
 51+nelson____: I think that part of the problem is that various parts of the system feel free to make the jump from "stored" to "accessible as full path".
 52+neilk_: yes
 53+neilk_: it drove me nuts too
 54+neilk_: and the code intentionally conflates a number of cases, because MediaWiki at heart just wants to throw a number of files into a directory, not manage millions of them
 55+nelson____: I gotta figure out how to mark the difference, so that something is either 1) a locally stored file, or 2) a blind token from the repo.
 56+neilk_: can't you subclass FileRepo then?
 57+nelson____: cuz if you have the blind token, then you need to turn it into a File and then call getPath() on it.
 58+nelson____: but there's times when the upload code expects to be able to access a file without creating a File first.
 59+neilk_: when does upload code access a file that isn't a File?
 60+nelson____: sec
 61+nelson____: neilk_: UploadStashFile does this in its __construct: $path = $repo->resolveVirtualUrl( $path );
 62+neilk_: yes
 63+nelson____: But maybe the key thing for me to know is that when it's SwiftMedia, $path on the right is *always* a mwrepo/
 64+nelson____: If that's the case, then I think I'm okay. I'm just having trouble following the code up and out and then back down.
 65+neilk_: there isn't any code, to my knowledge, which assumes that $path is a "physical" path. It uses the repo methods.
 66+neilk_: I don't blame you if you're having trouble.
 67+neilk_: isn't any code in UploadStashFile, I mean.
 68+nelson____: maybe ... what I should do is throw an exception if SwiftRepo::resolveVirtualUrl ever gets called without a mwrepo url, and then just go test everything.
 69+nelson____: I think maybe I'm trying to overanalyze the code.
 70+neilk_: I sympathize
 71+nelson____: I should trust the code more.
 72+neilk_: hm, I think not
 73+nelson____: Trust but verify.
 74+neilk_: also, this is sad but stashing is done in two slightly different ways, too.
 75+nelson____: I saw.
 76+neilk_: but compatible
 77+neilk_: I wanted UploadStash to absorb the other one.
 78+neilk_: We can still do that.
 79+nelson____: agreed.
 80+neilk_: When I was in the middle of Upload code, I always felt like I was cramped in some access area between two walls, with all the pipes and electrical work.
 81+nelson____: interesting metaphor.
 82+nelson____: yeah, I think part of that problem is you're always stuck between the database and the filestore.
 83+nelson____: they both have opinions about how things work, and you have to keep them consistent.
 84+nelson____: Okay, I'm gonna take some notes then go home. taking the weekend off for a bicycle road trip.
Index: trunk/extensions/SwiftMedia/SwiftMedia.body.php
@@ -87,297 +87,9 @@
8888
8989 parent::__construct( $title, $repo );
9090
91 - $this->metadata = '';
92 - $this->historyLine = 0;
93 - $this->historyRes = null;
94 - $this->dataLoaded = false;
 91+ $this->temp_path = false; // Points to our local copy.
9592 }
9693
97 - /**
98 - * Get the memcached key for the main data for this file, or false if
99 - * there is no access to the shared cache.
100 - */
101 - function getCacheKey() {
102 - $hashedName = md5( $this->getName() );
103 -
104 - return $this->repo->getSharedCacheKey( 'file', $hashedName );
105 - }
106 -
107 - /**
108 - * Try to load file metadata from memcached. Returns true on success.
109 - */
110 - function loadFromCache() {
111 - global $wgMemc;
112 -
113 - wfProfileIn( __METHOD__ );
114 - $this->dataLoaded = false;
115 - $key = $this->getCacheKey();
116 -
117 - if ( !$key ) {
118 - wfProfileOut( __METHOD__ );
119 - return false;
120 - }
121 -
122 - $cachedValues = $wgMemc->get( $key );
123 -
124 - // Check if the key existed and belongs to this version of MediaWiki
125 - if ( isset( $cachedValues['version'] ) && ( $cachedValues['version'] == MW_FILE_VERSION ) ) {
126 - wfDebug( "Pulling file metadata from cache key $key\n" );
127 - $this->fileExists = $cachedValues['fileExists'];
128 - if ( $this->fileExists ) {
129 - $this->setProps( $cachedValues );
130 - }
131 - $this->dataLoaded = true;
132 - }
133 -
134 - if ( $this->dataLoaded ) {
135 - wfIncrStats( 'image_cache_hit' );
136 - } else {
137 - wfIncrStats( 'image_cache_miss' );
138 - }
139 -
140 - wfProfileOut( __METHOD__ );
141 - return $this->dataLoaded;
142 - }
143 -
144 - /**
145 - * Save the file metadata to memcached
146 - */
147 - function saveToCache() {
148 - global $wgMemc;
149 -
150 - $this->load();
151 - $key = $this->getCacheKey();
152 -
153 - if ( !$key ) {
154 - return;
155 - }
156 -
157 - $fields = $this->getCacheFields( '' );
158 - $cache = array( 'version' => MW_FILE_VERSION );
159 - $cache['fileExists'] = $this->fileExists;
160 -
161 - if ( $this->fileExists ) {
162 - foreach ( $fields as $field ) {
163 - $cache[$field] = $this->$field;
164 - }
165 - }
166 -
167 - $wgMemc->set( $key, $cache, 60 * 60 * 24 * 7 ); // A week
168 - }
169 -
170 - /**
171 - * Load metadata from the file itself
172 - */
173 - function loadFromFile() {
174 - wfDebug( __METHOD__ . $this->getPath() . "\n" );
175 - $this->setProps( self::getPropsFromPath( $this->getPath() ) );
176 - }
177 -
178 - function getCacheFields( $prefix = 'img_' ) {
179 - static $fields = array( 'size', 'width', 'height', 'bits', 'media_type',
180 - 'major_mime', 'minor_mime', 'metadata', 'timestamp', 'sha1', 'user', 'user_text', 'description' );
181 - static $results = array();
182 -
183 - if ( $prefix == '' ) {
184 - return $fields;
185 - }
186 -
187 - if ( !isset( $results[$prefix] ) ) {
188 - $prefixedFields = array();
189 - foreach ( $fields as $field ) {
190 - $prefixedFields[] = $prefix . $field;
191 - }
192 - $results[$prefix] = $prefixedFields;
193 - }
194 -
195 - return $results[$prefix];
196 - }
197 -
198 - /**
199 - * Load file metadata from the DB
200 - */
201 - function loadFromDB() {
202 - # Polymorphic function name to distinguish foreign and local fetches
203 - $fname = get_class( $this ) . '::' . __FUNCTION__;
204 - wfProfileIn( $fname );
205 -
206 - # Unconditionally set loaded=true, we don't want the accessors constantly rechecking
207 - $this->dataLoaded = true;
208 -
209 - $dbr = $this->repo->getMasterDB();
210 -
211 - $row = $dbr->selectRow( 'image', $this->getCacheFields( 'img_' ),
212 - array( 'img_name' => $this->getName() ), $fname );
213 -
214 - if ( $row ) {
215 - $this->loadFromRow( $row );
216 - } else {
217 - $this->fileExists = false;
218 - }
219 -
220 - wfProfileOut( $fname );
221 - }
222 -
223 - /**
224 - * Decode a row from the database (either object or array) to an array
225 - * with timestamps and MIME types decoded, and the field prefix removed.
226 - */
227 - function decodeRow( $row, $prefix = 'img_' ) {
228 - $array = (array)$row;
229 - $prefixLength = strlen( $prefix );
230 -
231 - // Sanity check prefix once
232 - if ( substr( key( $array ), 0, $prefixLength ) !== $prefix ) {
233 - throw new MWException( __METHOD__ . ": incorrect $prefix parameter" );
234 - }
235 -
236 - $decoded = array();
237 -
238 - foreach ( $array as $name => $value ) {
239 - $decoded[substr( $name, $prefixLength )] = $value;
240 - }
241 -
242 - $decoded['timestamp'] = wfTimestamp( TS_MW, $decoded['timestamp'] );
243 -
244 - if ( empty( $decoded['major_mime'] ) ) {
245 - $decoded['mime'] = 'unknown/unknown';
246 - } else {
247 - if ( !$decoded['minor_mime'] ) {
248 - $decoded['minor_mime'] = 'unknown';
249 - }
250 - $decoded['mime'] = $decoded['major_mime'] . '/' . $decoded['minor_mime'];
251 - }
252 -
253 - # Trim zero padding from char/binary field
254 - $decoded['sha1'] = rtrim( $decoded['sha1'], "\0" );
255 -
256 - return $decoded;
257 - }
258 -
259 - /**
260 - * Load file metadata from a DB result row
261 - */
262 - function loadFromRow( $row, $prefix = 'img_' ) {
263 - $this->dataLoaded = true;
264 - $array = $this->decodeRow( $row, $prefix );
265 -
266 - foreach ( $array as $name => $value ) {
267 - $this->$name = $value;
268 - }
269 -
270 - $this->fileExists = true;
271 - $this->maybeUpgradeRow();
272 - }
273 -
274 - /**
275 - * Load file metadata from cache or DB, unless already loaded
276 - */
277 - function load() {
278 - if ( !$this->dataLoaded ) {
279 - if ( !$this->loadFromCache() ) {
280 - $this->loadFromDB();
281 - $this->saveToCache();
282 - }
283 - $this->dataLoaded = true;
284 - }
285 - }
286 -
287 - /**
288 - * Upgrade a row if it needs it
289 - */
290 - function maybeUpgradeRow() {
291 - if ( wfReadOnly() ) {
292 - return;
293 - }
294 -
295 - if ( is_null( $this->media_type ) ||
296 - $this->mime == 'image/svg'
297 - ) {
298 - $this->upgradeRow();
299 - $this->upgraded = true;
300 - } else {
301 - $handler = $this->getHandler();
302 - if ( $handler && !$handler->isMetadataValid( $this, $this->metadata ) ) {
303 - $this->upgradeRow();
304 - $this->upgraded = true;
305 - }
306 - }
307 - }
308 -
309 - function getUpgraded() {
310 - return $this->upgraded;
311 - }
312 -
313 - /**
314 - * Fix assorted version-related problems with the image row by reloading it from the file
315 - */
316 - function upgradeRow() {
317 - wfProfileIn( __METHOD__ );
318 -
319 - $this->loadFromFile();
320 -
321 - # Don't destroy file info of missing files
322 - if ( !$this->fileExists ) {
323 - wfDebug( __METHOD__ . ": file does not exist, aborting\n" );
324 - wfProfileOut( __METHOD__ );
325 - return;
326 - }
327 -
328 - $dbw = $this->repo->getMasterDB();
329 - list( $major, $minor ) = self::splitMime( $this->mime );
330 -
331 - if ( wfReadOnly() ) {
332 - wfProfileOut( __METHOD__ );
333 - return;
334 - }
335 - wfDebug( __METHOD__ . ': upgrading ' . $this->getName() . " to the current schema\n" );
336 -
337 - $dbw->update( 'image',
338 - array(
339 - 'img_width' => $this->width,
340 - 'img_height' => $this->height,
341 - 'img_bits' => $this->bits,
342 - 'img_media_type' => $this->media_type,
343 - 'img_major_mime' => $major,
344 - 'img_minor_mime' => $minor,
345 - 'img_metadata' => $this->metadata,
346 - 'img_sha1' => $this->sha1,
347 - ), array( 'img_name' => $this->getName() ),
348 - __METHOD__
349 - );
350 -
351 - $this->saveToCache();
352 - wfProfileOut( __METHOD__ );
353 - }
354 -
355 - /**
356 - * Set properties in this object to be equal to those given in the
357 - * associative array $info. Only cacheable fields can be set.
358 - *
359 - * If 'mime' is given, it will be split into major_mime/minor_mime.
360 - * If major_mime/minor_mime are given, $this->mime will also be set.
361 - */
362 - function setProps( $info ) {
363 - $this->dataLoaded = true;
364 - $fields = $this->getCacheFields( '' );
365 - $fields[] = 'fileExists';
366 -
367 - foreach ( $fields as $field ) {
368 - if ( isset( $info[$field] ) ) {
369 - $this->$field = $info[$field];
370 - }
371 - }
372 -
373 - // Fix up mime fields
374 - if ( isset( $info['major_mime'] ) ) {
375 - $this->mime = "{$info['major_mime']}/{$info['minor_mime']}";
376 - } elseif ( isset( $info['mime'] ) ) {
377 - $this->mime = $info['mime'];
378 - list( $this->major_mime, $this->minor_mime ) = self::splitMime( $this->mime );
379 - }
380 - }
381 -
38294 /** splitMime inherited */
38395 /** getName inherited */
38496 /** getTitle inherited */
@@ -403,6 +115,39 @@
404116 return $this->getLocalCopy($this->repo->getZoneContainer('thumb'), $path);
405117 }
406118
 119+ /**
 120+ * Get a local path corresponding to a virtual URL
 121+ */
 122+ function getContainerRel( $url ) {
 123+ if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
 124+ throw new MWException( __METHOD__.": unknown protocol" );
 125+ }
 126+
 127+ $bits = explode( '/', substr( $url, 9 ), 3 );
 128+ if ( count( $bits ) != 3 ) {
 129+ throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
 130+ }
 131+ list( $repo, $zone, $rel ) = $bits;
 132+ if ( $repo !== $this->name ) {
 133+ throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" );
 134+ }
 135+ $container = $this->getZoneContainer( $zone );
 136+ if ( $container === false) {
 137+ throw new MWException( __METHOD__.": invalid zone: $zone" );
 138+ }
 139+ return array($container, rawurldecode( $rel ));
 140+ }
 141+
 142+ /**
 143+ * Called from elsewhere to turn a virtual URL into a path.
 144+ */
 145+ function resolveVirtualUrl( $url ) {
 146+ $path = getContainerRel( $url );
 147+ list($c, $r) = $path;
 148+ return $this->getLocalCopy($c, $r);
 149+ }
 150+
 151+
407152 /** Given a container and relative path, return an absolute path pointing at a copy of the file */
408153 function getLocalCopy($container, $rel) {
409154 // if we already have a local copy, return it.
@@ -441,122 +186,6 @@
442187 }
443188 }
444189
445 - function isMissing() {
446 - if ( $this->missing === null ) {
447 - list( $fileExists ) = $this->repo->fileExistsBatch( array( $this->getVirtualUrl() ), FileRepo::FILES_ONLY );
448 - $this->missing = !$fileExists;
449 - }
450 - return $this->missing;
451 - }
452 -
453 - /**
454 - * Return the width of the image
455 - *
456 - * Returns false on error
457 - */
458 - public function getWidth( $page = 1 ) {
459 - $this->load();
460 -
461 - if ( $this->isMultipage() ) {
462 - $dim = $this->getHandler()->getPageDimensions( $this, $page );
463 - if ( $dim ) {
464 - return $dim['width'];
465 - } else {
466 - return false;
467 - }
468 - } else {
469 - return $this->width;
470 - }
471 - }
472 -
473 - /**
474 - * Return the height of the image
475 - *
476 - * Returns false on error
477 - */
478 - public function getHeight( $page = 1 ) {
479 - $this->load();
480 -
481 - if ( $this->isMultipage() ) {
482 - $dim = $this->getHandler()->getPageDimensions( $this, $page );
483 - if ( $dim ) {
484 - return $dim['height'];
485 - } else {
486 - return false;
487 - }
488 - } else {
489 - return $this->height;
490 - }
491 - }
492 -
493 - /**
494 - * Returns ID or name of user who uploaded the file
495 - *
496 - * @param $type string 'text' or 'id'
497 - */
498 - function getUser( $type = 'text' ) {
499 - $this->load();
500 -
501 - if ( $type == 'text' ) {
502 - return $this->user_text;
503 - } elseif ( $type == 'id' ) {
504 - return $this->user;
505 - }
506 - }
507 -
508 - /**
509 - * Get handler-specific metadata
510 - */
511 - function getMetadata() {
512 - $this->load();
513 - return $this->metadata;
514 - }
515 -
516 - function getBitDepth() {
517 - $this->load();
518 - return $this->bits;
519 - }
520 -
521 - /**
522 - * Return the size of the image file, in bytes
523 - */
524 - public function getSize() {
525 - $this->load();
526 - return $this->size;
527 - }
528 -
529 - /**
530 - * Returns the mime type of the file.
531 - */
532 - function getMimeType() {
533 - $this->load();
534 - return $this->mime;
535 - }
536 -
537 - /**
538 - * Return the type of the media in the file.
539 - * Use the value returned by this function with the MEDIATYPE_xxx constants.
540 - */
541 - function getMediaType() {
542 - $this->load();
543 - return $this->media_type;
544 - }
545 -
546 - /** canRender inherited */
547 - /** mustRender inherited */
548 - /** allowInlineDisplay inherited */
549 - /** isSafeFile inherited */
550 - /** isTrustedFile inherited */
551 -
552 - /**
553 - * Returns true if the file file exists on disk.
554 - * @return boolean Whether file file exist on disk.
555 - */
556 - public function exists() {
557 - $this->load();
558 - return $this->fileExists;
559 - }
560 -
561190 /** getTransformScript inherited */
562191 /** getUnscaledThumb inherited */
563192 /** thumbName inherited */
@@ -651,43 +280,6 @@
652281 }
653282
654283 /**
655 - * Refresh metadata in memcached, but don't touch thumbnails or squid
656 - */
657 - function purgeMetadataCache() {
658 - $this->loadFromDB();
659 - $this->saveToCache();
660 - $this->purgeHistory();
661 - }
662 -
663 - /**
664 - * Purge the shared history (OldLocalFile) cache
665 - */
666 - function purgeHistory() {
667 - global $wgMemc;
668 -
669 - $hashedName = md5( $this->getName() );
670 - $oldKey = $this->repo->getSharedCacheKey( 'oldfile', $hashedName );
671 -
672 - if ( $oldKey ) {
673 - $wgMemc->delete( $oldKey );
674 - }
675 - }
676 -
677 - /**
678 - * Delete all previously generated thumbnails, refresh metadata in memcached and purge the squid
679 - */
680 - function purgeCache() {
681 - // Refresh metadata cache
682 - $this->purgeMetadataCache();
683 -
684 - // Delete thumbnails
685 - $this->purgeThumbnails();
686 -
687 - // Purge squid cache for this file
688 - SquidUpdate::purge( array( $this->getURL() ) );
689 - }
690 -
691 - /**
692284 * Delete cached transformed files
693285 */
694286 function purgeThumbnails() {
@@ -717,1517 +309,11 @@
718310 }
719311 }
720312
721 - /** purgeDescription inherited */
722 - /** purgeEverything inherited */
723 -
724 - function getHistory( $limit = null, $start = null, $end = null, $inc = true ) {
725 - $dbr = $this->repo->getSlaveDB();
726 - $tables = array( 'oldimage' );
727 - $fields = OldSwiftFile::selectFields();
728 - $conds = $opts = $join_conds = array();
729 - $eq = $inc ? '=' : '';
730 - $conds[] = "oi_name = " . $dbr->addQuotes( $this->title->getDBkey() );
731 -
732 - if ( $start ) {
733 - $conds[] = "oi_timestamp <$eq " . $dbr->addQuotes( $dbr->timestamp( $start ) );
734 - }
735 -
736 - if ( $end ) {
737 - $conds[] = "oi_timestamp >$eq " . $dbr->addQuotes( $dbr->timestamp( $end ) );
738 - }
739 -
740 - if ( $limit ) {
741 - $opts['LIMIT'] = $limit;
742 - }
743 -
744 - // Search backwards for time > x queries
745 - $order = ( !$start && $end !== null ) ? 'ASC' : 'DESC';
746 - $opts['ORDER BY'] = "oi_timestamp $order";
747 - $opts['USE INDEX'] = array( 'oldimage' => 'oi_name_timestamp' );
748 -
749 - wfRunHooks( 'SwiftFile::getHistory', array( &$this, &$tables, &$fields,
750 - &$conds, &$opts, &$join_conds ) );
751 -
752 - $res = $dbr->select( $tables, $fields, $conds, __METHOD__, $opts, $join_conds );
753 - $r = array();
754 -
755 - foreach ( $res as $row ) {
756 - if ( $this->repo->oldFileFromRowFactory ) {
757 - $r[] = call_user_func( $this->repo->oldFileFromRowFactory, $row, $this->repo );
758 - } else {
759 - $r[] = OldSwiftFile::newFromRow( $row, $this->repo );
760 - }
761 - }
762 -
763 - if ( $order == 'ASC' ) {
764 - $r = array_reverse( $r ); // make sure it ends up descending
765 - }
766 -
767 - return $r;
768 - }
769 -
770 - /**
771 - * Return the history of this file, line by line.
772 - * starts with current version, then old versions.
773 - * uses $this->historyLine to check which line to return:
774 - * 0 return line for current version
775 - * 1 query for old versions, return first one
776 - * 2, ... return next old version from above query
777 - */
778 - public function nextHistoryLine() {
779 - # Polymorphic function name to distinguish foreign and local fetches
780 - $fname = get_class( $this ) . '::' . __FUNCTION__;
781 -
782 - $dbr = $this->repo->getSlaveDB();
783 -
784 - if ( $this->historyLine == 0 ) {// called for the first time, return line from cur
785 - $this->historyRes = $dbr->select( 'image',
786 - array(
787 - '*',
788 - "'' AS oi_archive_name",
789 - '0 as oi_deleted',
790 - 'img_sha1'
791 - ),
792 - array( 'img_name' => $this->title->getDBkey() ),
793 - $fname
794 - );
795 -
796 - if ( 0 == $dbr->numRows( $this->historyRes ) ) {
797 - $this->historyRes = null;
798 - return false;
799 - }
800 - } elseif ( $this->historyLine == 1 ) {
801 - $this->historyRes = $dbr->select( 'oldimage', '*',
802 - array( 'oi_name' => $this->title->getDBkey() ),
803 - $fname,
804 - array( 'ORDER BY' => 'oi_timestamp DESC' )
805 - );
806 - }
807 - $this->historyLine ++;
808 -
809 - return $dbr->fetchObject( $this->historyRes );
810 - }
811 -
812 - /**
813 - * Reset the history pointer to the first element of the history
814 - */
815 - public function resetHistory() {
816 - $this->historyLine = 0;
817 -
818 - if ( !is_null( $this->historyRes ) ) {
819 - $this->historyRes = null;
820 - }
821 - }
822 -
823 - /** getFullPath inherited */
824 - /** getHashPath inherited */
825 - /** getRel inherited */
826 - /** getUrlRel inherited */
827 - /** getArchiveRel inherited */
828 - /** getArchiveUrl inherited */
829 - /** getThumbUrl inherited */
830 - /** getArchiveVirtualUrl inherited */
831 - /** getThumbVirtualUrl inherited */
832 - /** isHashed inherited */
833 -
834 - /**
835 - * Upload a file and record it in the DB
836 - * @param $srcPath String: source path or virtual URL
837 - * @param $comment String: upload description
838 - * @param $pageText String: text to use for the new description page,
839 - * if a new description page is created
840 - * @param $flags Integer: flags for publish()
841 - * @param $props Array: File properties, if known. This can be used to reduce the
842 - * upload time when uploading virtual URLs for which the file info
843 - * is already known
844 - * @param $timestamp String: timestamp for img_timestamp, or false to use the current time
845 - * @param $user Mixed: User object or null to use $wgUser
846 - *
847 - * @return FileRepoStatus object. On success, the value member contains the
848 - * archive name, or an empty string if it was a new file.
849 - */
850 - function upload( $srcPath, $comment, $pageText, $flags = 0, $props = false, $timestamp = false, $user = null ) {
851 - $this->lock();
852 - $status = $this->publish( $srcPath, $flags );
853 -
854 - if ( $status->ok ) {
855 - if ( !$this->recordUpload2( $status->value, $comment, $pageText, $props, $timestamp, $user ) ) {
856 - $status->fatal( 'filenotfound', $srcPath );
857 - }
858 - }
859 -
860 - $this->unlock();
861 -
862 - return $status;
863 - }
864 -
865 - /**
866 - * Record a file upload in the upload log and the image table
867 - * @deprecated use upload()
868 - */
869 - function recordUpload( $oldver, $desc, $license = '', $copyStatus = '', $source = '',
870 - $watch = false, $timestamp = false )
871 - {
872 - $pageText = SpecialUpload::getInitialPageText( $desc, $license, $copyStatus, $source );
873 -
874 - if ( !$this->recordUpload2( $oldver, $desc, $pageText ) ) {
875 - return false;
876 - }
877 -
878 - if ( $watch ) {
879 - global $wgUser;
880 - $wgUser->addWatch( $this->getTitle() );
881 - }
882 - return true;
883 -
884 - }
885 -
886 - /**
887 - * Record a file upload in the upload log and the image table
888 - */
889 - function recordUpload2( $oldver, $comment, $pageText, $props = false, $timestamp = false, $user = null )
890 - {
891 - if ( is_null( $user ) ) {
892 - global $wgUser;
893 - $user = $wgUser;
894 - }
895 -
896 - $dbw = $this->repo->getMasterDB();
897 - $dbw->begin();
898 -
899 - if ( !$props ) {
900 - $props = $this->repo->getFileProps( $this->getVirtualUrl() );
901 - }
902 -
903 - $props['description'] = $comment;
904 - $props['user'] = $user->getId();
905 - $props['user_text'] = $user->getName();
906 - $props['timestamp'] = wfTimestamp( TS_MW );
907 - $this->setProps( $props );
908 -
909 - # Delete thumbnails
910 - $this->purgeThumbnails();
911 -
912 - # The file is already on its final location, remove it from the squid cache
913 - SquidUpdate::purge( array( $this->getURL() ) );
914 -
915 - # Fail now if the file isn't there
916 - if ( !$this->fileExists ) {
917 - wfDebug( __METHOD__ . ": File " . $this->getRel() . " went missing!\n" );
918 - return false;
919 - }
920 -
921 - $reupload = false;
922 -
923 - if ( $timestamp === false ) {
924 - $timestamp = $dbw->timestamp();
925 - }
926 -
927 - # Test to see if the row exists using INSERT IGNORE
928 - # This avoids race conditions by locking the row until the commit, and also
929 - # doesn't deadlock. SELECT FOR UPDATE causes a deadlock for every race condition.
930 - $dbw->insert( 'image',
931 - array(
932 - 'img_name' => $this->getName(),
933 - 'img_size' => $this->size,
934 - 'img_width' => intval( $this->width ),
935 - 'img_height' => intval( $this->height ),
936 - 'img_bits' => $this->bits,
937 - 'img_media_type' => $this->media_type,
938 - 'img_major_mime' => $this->major_mime,
939 - 'img_minor_mime' => $this->minor_mime,
940 - 'img_timestamp' => $timestamp,
941 - 'img_description' => $comment,
942 - 'img_user' => $user->getId(),
943 - 'img_user_text' => $user->getName(),
944 - 'img_metadata' => $this->metadata,
945 - 'img_sha1' => $this->sha1
946 - ),
947 - __METHOD__,
948 - 'IGNORE'
949 - );
950 -
951 - if ( $dbw->affectedRows() == 0 ) {
952 - $reupload = true;
953 -
954 - # Collision, this is an update of a file
955 - # Insert previous contents into oldimage
956 - $dbw->insertSelect( 'oldimage', 'image',
957 - array(
958 - 'oi_name' => 'img_name',
959 - 'oi_archive_name' => $dbw->addQuotes( $oldver ),
960 - 'oi_size' => 'img_size',
961 - 'oi_width' => 'img_width',
962 - 'oi_height' => 'img_height',
963 - 'oi_bits' => 'img_bits',
964 - 'oi_timestamp' => 'img_timestamp',
965 - 'oi_description' => 'img_description',
966 - 'oi_user' => 'img_user',
967 - 'oi_user_text' => 'img_user_text',
968 - 'oi_metadata' => 'img_metadata',
969 - 'oi_media_type' => 'img_media_type',
970 - 'oi_major_mime' => 'img_major_mime',
971 - 'oi_minor_mime' => 'img_minor_mime',
972 - 'oi_sha1' => 'img_sha1'
973 - ), array( 'img_name' => $this->getName() ), __METHOD__
974 - );
975 -
976 - # Update the current image row
977 - $dbw->update( 'image',
978 - array( /* SET */
979 - 'img_size' => $this->size,
980 - 'img_width' => intval( $this->width ),
981 - 'img_height' => intval( $this->height ),
982 - 'img_bits' => $this->bits,
983 - 'img_media_type' => $this->media_type,
984 - 'img_major_mime' => $this->major_mime,
985 - 'img_minor_mime' => $this->minor_mime,
986 - 'img_timestamp' => $timestamp,
987 - 'img_description' => $comment,
988 - 'img_user' => $user->getId(),
989 - 'img_user_text' => $user->getName(),
990 - 'img_metadata' => $this->metadata,
991 - 'img_sha1' => $this->sha1
992 - ), array( /* WHERE */
993 - 'img_name' => $this->getName()
994 - ), __METHOD__
995 - );
996 - } else {
997 - # This is a new file
998 - # Update the image count
999 - $site_stats = $dbw->tableName( 'site_stats' );
1000 - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
1001 - }
1002 -
1003 - $descTitle = $this->getTitle();
1004 - $article = new ImagePage( $descTitle );
1005 - $article->setFile( $this );
1006 -
1007 - # Add the log entry
1008 - $log = new LogPage( 'upload' );
1009 - $action = $reupload ? 'overwrite' : 'upload';
1010 - $log->addEntry( $action, $descTitle, $comment, array(), $user );
1011 -
1012 - if ( $descTitle->exists() ) {
1013 - # Create a null revision
1014 - $latest = $descTitle->getLatestRevID();
1015 - $nullRevision = Revision::newNullRevision(
1016 - $dbw,
1017 - $descTitle->getArticleId(),
1018 - $log->getRcComment(),
1019 - false
1020 - );
1021 - $nullRevision->insertOn( $dbw );
1022 -
1023 - wfRunHooks( 'NewRevisionFromEditComplete', array( $article, $nullRevision, $latest, $user ) );
1024 - $article->updateRevisionOn( $dbw, $nullRevision );
1025 -
1026 - # Invalidate the cache for the description page
1027 - $descTitle->invalidateCache();
1028 - $descTitle->purgeSquid();
1029 - } else {
1030 - # New file; create the description page.
1031 - # There's already a log entry, so don't make a second RC entry
1032 - # Squid and file cache for the description page are purged by doEdit.
1033 - $article->doEdit( $pageText, $comment, EDIT_NEW | EDIT_SUPPRESS_RC );
1034 - }
1035 -
1036 - # Commit the transaction now, in case something goes wrong later
1037 - # The most important thing is that files don't get lost, especially archives
1038 - $dbw->commit();
1039 -
1040 - # Save to cache and purge the squid
1041 - # We shall not saveToCache before the commit since otherwise
1042 - # in case of a rollback there is an usable file from memcached
1043 - # which in fact doesn't really exist (bug 24978)
1044 - $this->saveToCache();
1045 -
1046 - # Hooks, hooks, the magic of hooks...
1047 - wfRunHooks( 'FileUpload', array( $this, $reupload, $descTitle->exists() ) );
1048 -
1049 - # Invalidate cache for all pages using this file
1050 - $update = new HTMLCacheUpdate( $this->getTitle(), 'imagelinks' );
1051 - $update->doUpdate();
1052 -
1053 - # Invalidate cache for all pages that redirects on this page
1054 - $redirs = $this->getTitle()->getRedirectsHere();
1055 -
1056 - foreach ( $redirs as $redir ) {
1057 - $update = new HTMLCacheUpdate( $redir, 'imagelinks' );
1058 - $update->doUpdate();
1059 - }
1060 -
1061 - return true;
1062 - }
1063 -
1064 - /**
1065 - * Move or copy a file to its public location. If a file exists at the
1066 - * destination, move it to an archive. Returns a FileRepoStatus object with
1067 - * the archive name in the "value" member on success.
1068 - *
1069 - * The archive name should be passed through to recordUpload for database
1070 - * registration.
1071 - *
1072 - * @param $srcPath String: local filesystem path to the source image
1073 - * @param $flags Integer: a bitwise combination of:
1074 - * File::DELETE_SOURCE Delete the source file, i.e. move
1075 - * rather than copy
1076 - * @return FileRepoStatus object. On success, the value member contains the
1077 - * archive name, or an empty string if it was a new file.
1078 - */
1079 - function publish( $srcPath, $flags = 0 ) {
1080 - $this->lock();
1081 -
1082 - $dstRel = $this->getRel();
1083 - $archiveName = gmdate( 'YmdHis' ) . '!' . $this->getName();
1084 - $archiveRel = 'archive/' . $this->getHashPath() . $archiveName;
1085 - $flags = $flags & File::DELETE_SOURCE ? LocalRepo::DELETE_SOURCE : 0;
1086 - $status = $this->repo->publish( $srcPath, $dstRel, $archiveRel, $flags );
1087 -
1088 - if ( $status->value == 'new' ) {
1089 - $status->value = '';
1090 - } else {
1091 - $status->value = $archiveName;
1092 - }
1093 -
1094 - $this->unlock();
1095 -
1096 - return $status;
1097 - }
1098 -
1099 - /** getLinksTo inherited */
1100 - /** getExifData inherited */
1101 - /** isLocal inherited */
1102 - /** wasDeleted inherited */
1103 -
1104 - /**
1105 - * Move file to the new title
1106 - *
1107 - * Move current, old version and all thumbnails
1108 - * to the new filename. Old file is deleted.
1109 - *
1110 - * Cache purging is done; checks for validity
1111 - * and logging are caller's responsibility
1112 - *
1113 - * @param $target Title New file name
1114 - * @return FileRepoStatus object.
1115 - */
1116 - function move( $target ) {
1117 - wfDebugLog( 'imagemove', "Got request to move {$this->name} to " . $target->getText() );
1118 - $this->lock();
1119 -
1120 - $batch = new SwiftFileMoveBatch( $this, $target );
1121 - $batch->addCurrent();
1122 - $batch->addOlds();
1123 -
1124 - $status = $batch->execute();
1125 - wfDebugLog( 'imagemove', "Finished moving {$this->name}" );
1126 -
1127 - $this->purgeEverything();
1128 - $this->unlock();
1129 -
1130 - if ( $status->isOk() ) {
1131 - // Now switch the object
1132 - $this->title = $target;
1133 - // Force regeneration of the name and hashpath
1134 - unset( $this->name );
1135 - unset( $this->hashPath );
1136 - // Purge the new image
1137 - $this->purgeEverything();
1138 - }
1139 -
1140 - return $status;
1141 - }
1142 -
1143 - /**
1144 - * Delete all versions of the file.
1145 - *
1146 - * Moves the files into an archive directory (or deletes them)
1147 - * and removes the database rows.
1148 - *
1149 - * Cache purging is done; logging is caller's responsibility.
1150 - *
1151 - * @param $reason
1152 - * @param $suppress
1153 - * @return FileRepoStatus object.
1154 - */
1155 - function delete( $reason, $suppress = false ) {
1156 - $this->lock();
1157 -
1158 - $batch = new SwiftFileDeleteBatch( $this, $reason, $suppress );
1159 - $batch->addCurrent();
1160 -
1161 - # Get old version relative paths
1162 - $dbw = $this->repo->getMasterDB();
1163 - $result = $dbw->select( 'oldimage',
1164 - array( 'oi_archive_name' ),
1165 - array( 'oi_name' => $this->getName() ) );
1166 - foreach ( $result as $row ) {
1167 - $batch->addOld( $row->oi_archive_name );
1168 - }
1169 - //wfDebug(__METHOD__ . " deleting these files: " . var_export($batch, true) . "\n");
1170 - $status = $batch->execute();
1171 -
1172 - if ( $status->ok ) {
1173 - // Update site_stats
1174 - $site_stats = $dbw->tableName( 'site_stats' );
1175 - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images-1", __METHOD__ );
1176 - $this->purgeEverything();
1177 - }
1178 -
1179 - $this->unlock();
1180 -
1181 - return $status;
1182 - }
1183 -
1184 - /**
1185 - * Delete an old version of the file.
1186 - *
1187 - * Moves the file into an archive directory (or deletes it)
1188 - * and removes the database row.
1189 - *
1190 - * Cache purging is done; logging is caller's responsibility.
1191 - *
1192 - * @param $archiveName String
1193 - * @param $reason String
1194 - * @param $suppress Boolean
1195 - * @throws MWException or FSException on database or file store failure
1196 - * @return FileRepoStatus object.
1197 - */
1198 - function deleteOld( $archiveName, $reason, $suppress = false ) {
1199 - $this->lock();
1200 -
1201 - $batch = new SwiftFileDeleteBatch( $this, $reason, $suppress );
1202 - $batch->addOld( $archiveName );
1203 - $status = $batch->execute();
1204 -
1205 - $this->unlock();
1206 -
1207 - if ( $status->ok ) {
1208 - $this->purgeDescription();
1209 - $this->purgeHistory();
1210 - }
1211 -
1212 - return $status;
1213 - }
1214 -
1215 - /**
1216 - * Restore all or specified deleted revisions to the given file.
1217 - * Permissions and logging are left to the caller.
1218 - *
1219 - * May throw database exceptions on error.
1220 - *
1221 - * @param $versions set of record ids of deleted items to restore,
1222 - * or empty to restore all revisions.
1223 - * @param $unsuppress Boolean
1224 - * @return FileRepoStatus
1225 - */
1226 - function restore( $versions = array(), $unsuppress = false ) {
1227 - $batch = new SwiftFileRestoreBatch( $this, $unsuppress );
1228 -
1229 - if ( !$versions ) {
1230 - $batch->addAll();
1231 - } else {
1232 - $batch->addIds( $versions );
1233 - }
1234 -
1235 - $status = $batch->execute();
1236 -
1237 - if ( !$status->isGood() ) {
1238 - return $status;
1239 - }
1240 -
1241 - $cleanupStatus = $batch->cleanup();
1242 - $cleanupStatus->successCount = 0;
1243 - $cleanupStatus->failCount = 0;
1244 - $status->merge( $cleanupStatus );
1245 -
1246 - return $status;
1247 - }
1248 -
1249 - /** isMultipage inherited */
1250 - /** pageCount inherited */
1251 - /** scaleHeight inherited */
1252 - /** getImageSize inherited */
1253 -
1254 - /**
1255 - * Get the URL of the file description page.
1256 - */
1257 - function getDescriptionUrl() {
1258 - return $this->title->getLocalUrl();
1259 - }
1260 -
1261 - /**
1262 - * Get the HTML text of the description page
1263 - * This is not used by ImagePage for local files, since (among other things)
1264 - * it skips the parser cache.
1265 - */
1266 - function getDescriptionText() {
1267 - global $wgParser;
1268 - $revision = Revision::newFromTitle( $this->title );
1269 - if ( !$revision ) return false;
1270 - $text = $revision->getText();
1271 - if ( !$text ) return false;
1272 - $pout = $wgParser->parse( $text, $this->title, new ParserOptions() );
1273 - return $pout->getText();
1274 - }
1275 -
1276 - function getDescription() {
1277 - $this->load();
1278 - return $this->description;
1279 - }
1280 -
1281 - function getTimestamp() {
1282 - $this->load();
1283 - return $this->timestamp;
1284 - }
1285 -
1286 - function getSha1() {
1287 - $this->load();
1288 - // Initialise now if necessary
1289 - if ( $this->sha1 == '' && $this->fileExists ) {
1290 - $this->sha1 = File::sha1Base36( $this->getPath() );
1291 - if ( !wfReadOnly() && strval( $this->sha1 ) != '' ) {
1292 - $dbw = $this->repo->getMasterDB();
1293 - $dbw->update( 'image',
1294 - array( 'img_sha1' => $this->sha1 ),
1295 - array( 'img_name' => $this->getName() ),
1296 - __METHOD__ );
1297 - $this->saveToCache();
1298 - }
1299 - }
1300 -
1301 - return $this->sha1;
1302 - }
1303 -
1304 - /**
1305 - * Start a transaction and lock the image for update
1306 - * Increments a reference counter if the lock is already held
1307 - * @return boolean True if the image exists, false otherwise
1308 - */
1309 - function lock() {
1310 - $dbw = $this->repo->getMasterDB();
1311 -
1312 - if ( !$this->locked ) {
1313 - $dbw->begin();
1314 - $this->locked++;
1315 - }
1316 -
1317 - return $dbw->selectField( 'image', '1', array( 'img_name' => $this->getName() ), __METHOD__ );
1318 - }
1319 -
1320 - /**
1321 - * Decrement the lock reference count. If the reference count is reduced to zero, commits
1322 - * the transaction and thereby releases the image lock.
1323 - */
1324 - function unlock() {
1325 - if ( $this->locked ) {
1326 - --$this->locked;
1327 - if ( !$this->locked ) {
1328 - $dbw = $this->repo->getMasterDB();
1329 - $dbw->commit();
1330 - }
1331 - }
1332 - }
1333 -
1334 - /**
1335 - * Roll back the DB transaction and mark the image unlocked
1336 - */
1337 - function unlockAndRollback() {
1338 - $this->locked = false;
1339 - $dbw = $this->repo->getMasterDB();
1340 - $dbw->rollback();
1341 - }
1342313 } // SwiftFile class
1343314
1344315 # ------------------------------------------------------------------------------
1345316
1346317 /**
1347 - * Helper class for file deletion
1348 - * @ingroup FileRepo
1349 - */
1350 -class SwiftFileDeleteBatch {
1351 -
1352 - /**
1353 - * @var SwiftFile
1354 - */
1355 - var $file;
1356 -
1357 - var $reason, $srcRels = array(), $archiveUrls = array(), $deletionBatch, $suppress;
1358 - var $status;
1359 -
1360 - function __construct( File $file, $reason = '', $suppress = false ) {
1361 - $this->file = $file;
1362 - $this->reason = $reason;
1363 - $this->suppress = $suppress;
1364 - $this->status = $file->repo->newGood();
1365 - }
1366 -
1367 - function addCurrent() {
1368 - $this->srcRels['.'] = $this->file->getRel();
1369 - }
1370 -
1371 - function addOld( $oldName ) {
1372 - $this->srcRels[$oldName] = $this->file->getArchiveRel( $oldName );
1373 - $this->archiveUrls[] = $this->file->getArchiveUrl( $oldName );
1374 - }
1375 -
1376 - function getOldRels() {
1377 - if ( !isset( $this->srcRels['.'] ) ) {
1378 - $oldRels =& $this->srcRels;
1379 - $deleteCurrent = false;
1380 - } else {
1381 - $oldRels = $this->srcRels;
1382 - unset( $oldRels['.'] );
1383 - $deleteCurrent = true;
1384 - }
1385 -
1386 - return array( $oldRels, $deleteCurrent );
1387 - }
1388 -
1389 - /*protected*/ function getHashes() {
1390 - $hashes = array();
1391 - list( $oldRels, $deleteCurrent ) = $this->getOldRels();
1392 -
1393 - if ( $deleteCurrent ) {
1394 - $hashes['.'] = $this->file->getSha1();
1395 - }
1396 -
1397 - if ( count( $oldRels ) ) {
1398 - $dbw = $this->file->repo->getMasterDB();
1399 - $res = $dbw->select(
1400 - 'oldimage',
1401 - array( 'oi_archive_name', 'oi_sha1' ),
1402 - 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
1403 - __METHOD__
1404 - );
1405 -
1406 - foreach ( $res as $row ) {
1407 - if ( rtrim( $row->oi_sha1, "\0" ) === '' ) {
1408 - // Get the hash from the file
1409 - $oldUrl = $this->file->getArchiveVirtualUrl( $row->oi_archive_name );
1410 - $props = $this->file->repo->getFileProps( $oldUrl );
1411 -
1412 - if ( $props['fileExists'] ) {
1413 - // Upgrade the oldimage row
1414 - $dbw->update( 'oldimage',
1415 - array( 'oi_sha1' => $props['sha1'] ),
1416 - array( 'oi_name' => $this->file->getName(), 'oi_archive_name' => $row->oi_archive_name ),
1417 - __METHOD__ );
1418 - $hashes[$row->oi_archive_name] = $props['sha1'];
1419 - } else {
1420 - $hashes[$row->oi_archive_name] = false;
1421 - }
1422 - } else {
1423 - $hashes[$row->oi_archive_name] = $row->oi_sha1;
1424 - }
1425 - }
1426 - }
1427 -
1428 - $missing = array_diff_key( $this->srcRels, $hashes );
1429 -
1430 - foreach ( $missing as $name => $rel ) {
1431 - $this->status->error( 'filedelete-old-unregistered', $name );
1432 - }
1433 -
1434 - foreach ( $hashes as $name => $hash ) {
1435 - if ( !$hash ) {
1436 - $this->status->error( 'filedelete-missing', $this->srcRels[$name] );
1437 - unset( $hashes[$name] );
1438 - }
1439 - }
1440 -
1441 - return $hashes;
1442 - }
1443 -
1444 - function doDBInserts() {
1445 - global $wgUser;
1446 -
1447 - $dbw = $this->file->repo->getMasterDB();
1448 - $encTimestamp = $dbw->addQuotes( $dbw->timestamp() );
1449 - $encUserId = $dbw->addQuotes( $wgUser->getId() );
1450 - $encReason = $dbw->addQuotes( $this->reason );
1451 - $encGroup = $dbw->addQuotes( 'deleted' );
1452 - $ext = $this->file->getExtension();
1453 - $dotExt = $ext === '' ? '' : ".$ext";
1454 - $encExt = $dbw->addQuotes( $dotExt );
1455 - list( $oldRels, $deleteCurrent ) = $this->getOldRels();
1456 -
1457 - // Bitfields to further suppress the content
1458 - if ( $this->suppress ) {
1459 - $bitfield = 0;
1460 - // This should be 15...
1461 - $bitfield |= Revision::DELETED_TEXT;
1462 - $bitfield |= Revision::DELETED_COMMENT;
1463 - $bitfield |= Revision::DELETED_USER;
1464 - $bitfield |= Revision::DELETED_RESTRICTED;
1465 - } else {
1466 - $bitfield = 'oi_deleted';
1467 - }
1468 -
1469 - if ( $deleteCurrent ) {
1470 - $concat = $dbw->buildConcat( array( "img_sha1", $encExt ) );
1471 - $where = array( 'img_name' => $this->file->getName() );
1472 - $dbw->insertSelect( 'filearchive', 'image',
1473 - array(
1474 - 'fa_storage_group' => $encGroup,
1475 - 'fa_storage_key' => "CASE WHEN img_sha1='' THEN '' ELSE $concat END",
1476 - 'fa_deleted_user' => $encUserId,
1477 - 'fa_deleted_timestamp' => $encTimestamp,
1478 - 'fa_deleted_reason' => $encReason,
1479 - 'fa_deleted' => $this->suppress ? $bitfield : 0,
1480 -
1481 - 'fa_name' => 'img_name',
1482 - 'fa_archive_name' => 'NULL',
1483 - 'fa_size' => 'img_size',
1484 - 'fa_width' => 'img_width',
1485 - 'fa_height' => 'img_height',
1486 - 'fa_metadata' => 'img_metadata',
1487 - 'fa_bits' => 'img_bits',
1488 - 'fa_media_type' => 'img_media_type',
1489 - 'fa_major_mime' => 'img_major_mime',
1490 - 'fa_minor_mime' => 'img_minor_mime',
1491 - 'fa_description' => 'img_description',
1492 - 'fa_user' => 'img_user',
1493 - 'fa_user_text' => 'img_user_text',
1494 - 'fa_timestamp' => 'img_timestamp'
1495 - ), $where, __METHOD__ );
1496 - }
1497 -
1498 - if ( count( $oldRels ) ) {
1499 - $concat = $dbw->buildConcat( array( "oi_sha1", $encExt ) );
1500 - $where = array(
1501 - 'oi_name' => $this->file->getName(),
1502 - 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')' );
1503 - $dbw->insertSelect( 'filearchive', 'oldimage',
1504 - array(
1505 - 'fa_storage_group' => $encGroup,
1506 - 'fa_storage_key' => "CASE WHEN oi_sha1='' THEN '' ELSE $concat END",
1507 - 'fa_deleted_user' => $encUserId,
1508 - 'fa_deleted_timestamp' => $encTimestamp,
1509 - 'fa_deleted_reason' => $encReason,
1510 - 'fa_deleted' => $this->suppress ? $bitfield : 'oi_deleted',
1511 -
1512 - 'fa_name' => 'oi_name',
1513 - 'fa_archive_name' => 'oi_archive_name',
1514 - 'fa_size' => 'oi_size',
1515 - 'fa_width' => 'oi_width',
1516 - 'fa_height' => 'oi_height',
1517 - 'fa_metadata' => 'oi_metadata',
1518 - 'fa_bits' => 'oi_bits',
1519 - 'fa_media_type' => 'oi_media_type',
1520 - 'fa_major_mime' => 'oi_major_mime',
1521 - 'fa_minor_mime' => 'oi_minor_mime',
1522 - 'fa_description' => 'oi_description',
1523 - 'fa_user' => 'oi_user',
1524 - 'fa_user_text' => 'oi_user_text',
1525 - 'fa_timestamp' => 'oi_timestamp',
1526 - 'fa_deleted' => $bitfield
1527 - ), $where, __METHOD__ );
1528 - }
1529 - }
1530 -
1531 - function doDBDeletes() {
1532 - $dbw = $this->file->repo->getMasterDB();
1533 - list( $oldRels, $deleteCurrent ) = $this->getOldRels();
1534 -
1535 - if ( count( $oldRels ) ) {
1536 - $dbw->delete( 'oldimage',
1537 - array(
1538 - 'oi_name' => $this->file->getName(),
1539 - 'oi_archive_name' => array_keys( $oldRels )
1540 - ), __METHOD__ );
1541 - }
1542 -
1543 - if ( $deleteCurrent ) {
1544 - $dbw->delete( 'image', array( 'img_name' => $this->file->getName() ), __METHOD__ );
1545 - }
1546 - }
1547 -
1548 - /**
1549 - * Run the transaction
1550 - */
1551 - function execute() {
1552 - global $wgUseSquid;
1553 - wfProfileIn( __METHOD__ );
1554 -
1555 - $this->file->lock();
1556 - // Leave private files alone
1557 - $privateFiles = array();
1558 - list( $oldRels, $deleteCurrent ) = $this->getOldRels();
1559 - $dbw = $this->file->repo->getMasterDB();
1560 -
1561 - if ( !empty( $oldRels ) ) {
1562 - $res = $dbw->select( 'oldimage',
1563 - array( 'oi_archive_name' ),
1564 - array( 'oi_name' => $this->file->getName(),
1565 - 'oi_archive_name IN (' . $dbw->makeList( array_keys( $oldRels ) ) . ')',
1566 - $dbw->bitAnd( 'oi_deleted', File::DELETED_FILE ) => File::DELETED_FILE ),
1567 - __METHOD__ );
1568 -
1569 - foreach ( $res as $row ) {
1570 - $privateFiles[$row->oi_archive_name] = 1;
1571 - }
1572 - }
1573 - // Prepare deletion batch
1574 - $hashes = $this->getHashes();
1575 - $this->deletionBatch = array();
1576 - $ext = $this->file->getExtension();
1577 - $dotExt = $ext === '' ? '' : ".$ext";
1578 -
1579 - foreach ( $this->srcRels as $name => $srcRel ) {
1580 - // Skip files that have no hash (missing source).
1581 - // Keep private files where they are.
1582 - if ( isset( $hashes[$name] ) && !array_key_exists( $name, $privateFiles ) ) {
1583 - $hash = $hashes[$name];
1584 - $key = $hash . $dotExt;
1585 - $dstRel = $this->file->repo->getDeletedHashPath( $key ) . $key;
1586 - $this->deletionBatch[$name] = array( $srcRel, $dstRel );
1587 - }
1588 - }
1589 -
1590 - // Lock the filearchive rows so that the files don't get deleted by a cleanup operation
1591 - // We acquire this lock by running the inserts now, before the file operations.
1592 - //
1593 - // This potentially has poor lock contention characteristics -- an alternative
1594 - // scheme would be to insert stub filearchive entries with no fa_name and commit
1595 - // them in a separate transaction, then run the file ops, then update the fa_name fields.
1596 - $this->doDBInserts();
1597 -
1598 - // Removes non-existent file from the batch, so we don't get errors.
1599 - $this->deletionBatch = $this->removeNonexistentFiles( $this->deletionBatch );
1600 -
1601 - // Execute the file deletion batch
1602 - $status = $this->file->repo->deleteBatch( $this->deletionBatch );
1603 -
1604 - if ( !$status->isGood() ) {
1605 - $this->status->merge( $status );
1606 - }
1607 -
1608 - if ( !$this->status->ok ) {
1609 - // Critical file deletion error
1610 - // Roll back inserts, release lock and abort
1611 - // TODO: delete the defunct filearchive rows if we are using a non-transactional DB
1612 - $this->file->unlockAndRollback();
1613 - wfProfileOut( __METHOD__ );
1614 - return $this->status;
1615 - }
1616 -
1617 - // Purge squid
1618 - if ( $wgUseSquid ) {
1619 - $urls = array();
1620 -
1621 - foreach ( $this->srcRels as $srcRel ) {
1622 - $urlRel = str_replace( '%2F', '/', rawurlencode( $srcRel ) );
1623 - $urls[] = $this->file->repo->getZoneUrl( 'public' ) . '/' . $urlRel;
1624 - }
1625 - SquidUpdate::purge( $urls );
1626 - }
1627 -
1628 - // Delete image/oldimage rows
1629 - $this->doDBDeletes();
1630 -
1631 - // Commit and return
1632 - $this->file->unlock();
1633 - wfProfileOut( __METHOD__ );
1634 -
1635 - return $this->status;
1636 - }
1637 -
1638 - /**
1639 - * Removes non-existent files from a deletion batch.
1640 - */
1641 - function removeNonexistentFiles( $batch ) {
1642 - $files = $newBatch = array();
1643 -
1644 - foreach ( $batch as $batchItem ) {
1645 - list( $src, $dest ) = $batchItem;
1646 - $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
1647 - }
1648 -
1649 - $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
1650 -
1651 - foreach ( $batch as $batchItem ) {
1652 - if ( $result[$batchItem[0]] ) {
1653 - $newBatch[] = $batchItem;
1654 - }
1655 - }
1656 -
1657 - return $newBatch;
1658 - }
1659 -}
1660 -
1661 -# ------------------------------------------------------------------------------
1662 -
1663 -/**
1664 - * Helper class for file undeletion
1665 - * @ingroup FileRepo
1666 - */
1667 -class SwiftFileRestoreBatch {
1668 - /**
1669 - * @var SwiftFile
1670 - */
1671 - var $file;
1672 -
1673 - var $cleanupBatch, $ids, $all, $unsuppress = false;
1674 -
1675 - function __construct( File $file, $unsuppress = false ) {
1676 - $this->file = $file;
1677 - $this->cleanupBatch = $this->ids = array();
1678 - $this->ids = array();
1679 - $this->unsuppress = $unsuppress;
1680 - }
1681 -
1682 - /**
1683 - * Add a file by ID
1684 - */
1685 - function addId( $fa_id ) {
1686 - $this->ids[] = $fa_id;
1687 - }
1688 -
1689 - /**
1690 - * Add a whole lot of files by ID
1691 - */
1692 - function addIds( $ids ) {
1693 - $this->ids = array_merge( $this->ids, $ids );
1694 - }
1695 -
1696 - /**
1697 - * Add all revisions of the file
1698 - */
1699 - function addAll() {
1700 - $this->all = true;
1701 - }
1702 -
1703 - /**
1704 - * Run the transaction, except the cleanup batch.
1705 - * The cleanup batch should be run in a separate transaction, because it locks different
1706 - * rows and there's no need to keep the image row locked while it's acquiring those locks
1707 - * The caller may have its own transaction open.
1708 - * So we save the batch and let the caller call cleanup()
1709 - */
1710 - function execute() {
1711 - global $wgLang;
1712 -
1713 - if ( !$this->all && !$this->ids ) {
1714 - // Do nothing
1715 - return $this->file->repo->newGood();
1716 - }
1717 -
1718 - $exists = $this->file->lock();
1719 - $dbw = $this->file->repo->getMasterDB();
1720 - $status = $this->file->repo->newGood();
1721 -
1722 - // Fetch all or selected archived revisions for the file,
1723 - // sorted from the most recent to the oldest.
1724 - $conditions = array( 'fa_name' => $this->file->getName() );
1725 -
1726 - if ( !$this->all ) {
1727 - $conditions[] = 'fa_id IN (' . $dbw->makeList( $this->ids ) . ')';
1728 - }
1729 -
1730 - $result = $dbw->select( 'filearchive', '*',
1731 - $conditions,
1732 - __METHOD__,
1733 - array( 'ORDER BY' => 'fa_timestamp DESC' )
1734 - );
1735 -
1736 - $idsPresent = array();
1737 - $storeBatch = array();
1738 - $insertBatch = array();
1739 - $insertCurrent = false;
1740 - $deleteIds = array();
1741 - $first = true;
1742 - $archiveNames = array();
1743 -
1744 - foreach ( $result as $row ) {
1745 - $idsPresent[] = $row->fa_id;
1746 -
1747 - if ( $row->fa_name != $this->file->getName() ) {
1748 - $status->error( 'undelete-filename-mismatch', $wgLang->timeanddate( $row->fa_timestamp ) );
1749 - $status->failCount++;
1750 - continue;
1751 - }
1752 -
1753 - if ( $row->fa_storage_key == '' ) {
1754 - // Revision was missing pre-deletion
1755 - $status->error( 'undelete-bad-store-key', $wgLang->timeanddate( $row->fa_timestamp ) );
1756 - $status->failCount++;
1757 - continue;
1758 - }
1759 -
1760 - $deletedRel = $this->file->repo->getDeletedHashPath( $row->fa_storage_key ) . $row->fa_storage_key;
1761 - $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel;
1762 -
1763 - $sha1 = substr( $row->fa_storage_key, 0, strcspn( $row->fa_storage_key, '.' ) );
1764 -
1765 - # Fix leading zero
1766 - if ( strlen( $sha1 ) == 32 && $sha1[0] == '0' ) {
1767 - $sha1 = substr( $sha1, 1 );
1768 - }
1769 -
1770 - if ( is_null( $row->fa_major_mime ) || $row->fa_major_mime == 'unknown'
1771 - || is_null( $row->fa_minor_mime ) || $row->fa_minor_mime == 'unknown'
1772 - || is_null( $row->fa_media_type ) || $row->fa_media_type == 'UNKNOWN'
1773 - || is_null( $row->fa_metadata ) ) {
1774 - // Refresh our metadata
1775 - // Required for a new current revision; nice for older ones too. :)
1776 - $props = RepoGroup::singleton()->getFileProps( $deletedUrl );
1777 - } else {
1778 - $props = array(
1779 - 'minor_mime' => $row->fa_minor_mime,
1780 - 'major_mime' => $row->fa_major_mime,
1781 - 'media_type' => $row->fa_media_type,
1782 - 'metadata' => $row->fa_metadata
1783 - );
1784 - }
1785 -
1786 - if ( $first && !$exists ) {
1787 - // This revision will be published as the new current version
1788 - $destRel = $this->file->getRel();
1789 - $insertCurrent = array(
1790 - 'img_name' => $row->fa_name,
1791 - 'img_size' => $row->fa_size,
1792 - 'img_width' => $row->fa_width,
1793 - 'img_height' => $row->fa_height,
1794 - 'img_metadata' => $props['metadata'],
1795 - 'img_bits' => $row->fa_bits,
1796 - 'img_media_type' => $props['media_type'],
1797 - 'img_major_mime' => $props['major_mime'],
1798 - 'img_minor_mime' => $props['minor_mime'],
1799 - 'img_description' => $row->fa_description,
1800 - 'img_user' => $row->fa_user,
1801 - 'img_user_text' => $row->fa_user_text,
1802 - 'img_timestamp' => $row->fa_timestamp,
1803 - 'img_sha1' => $sha1
1804 - );
1805 -
1806 - // The live (current) version cannot be hidden!
1807 - if ( !$this->unsuppress && $row->fa_deleted ) {
1808 - $storeBatch[] = array( $deletedUrl, 'public', $destRel );
1809 - $this->cleanupBatch[] = $row->fa_storage_key;
1810 - }
1811 - } else {
1812 - $archiveName = $row->fa_archive_name;
1813 -
1814 - if ( $archiveName == '' ) {
1815 - // This was originally a current version; we
1816 - // have to devise a new archive name for it.
1817 - // Format is <timestamp of archiving>!<name>
1818 - $timestamp = wfTimestamp( TS_UNIX, $row->fa_deleted_timestamp );
1819 -
1820 - do {
1821 - $archiveName = wfTimestamp( TS_MW, $timestamp ) . '!' . $row->fa_name;
1822 - $timestamp++;
1823 - } while ( isset( $archiveNames[$archiveName] ) );
1824 - }
1825 -
1826 - $archiveNames[$archiveName] = true;
1827 - $destRel = $this->file->getArchiveRel( $archiveName );
1828 - $insertBatch[] = array(
1829 - 'oi_name' => $row->fa_name,
1830 - 'oi_archive_name' => $archiveName,
1831 - 'oi_size' => $row->fa_size,
1832 - 'oi_width' => $row->fa_width,
1833 - 'oi_height' => $row->fa_height,
1834 - 'oi_bits' => $row->fa_bits,
1835 - 'oi_description' => $row->fa_description,
1836 - 'oi_user' => $row->fa_user,
1837 - 'oi_user_text' => $row->fa_user_text,
1838 - 'oi_timestamp' => $row->fa_timestamp,
1839 - 'oi_metadata' => $props['metadata'],
1840 - 'oi_media_type' => $props['media_type'],
1841 - 'oi_major_mime' => $props['major_mime'],
1842 - 'oi_minor_mime' => $props['minor_mime'],
1843 - 'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted,
1844 - 'oi_sha1' => $sha1 );
1845 - }
1846 -
1847 - $deleteIds[] = $row->fa_id;
1848 -
1849 - if ( !$this->unsuppress && $row->fa_deleted & File::DELETED_FILE ) {
1850 - // private files can stay where they are
1851 - $status->successCount++;
1852 - } else {
1853 - $storeBatch[] = array( $deletedUrl, 'public', $destRel );
1854 - $this->cleanupBatch[] = $row->fa_storage_key;
1855 - }
1856 -
1857 - $first = false;
1858 - }
1859 -
1860 - unset( $result );
1861 -
1862 - // Add a warning to the status object for missing IDs
1863 - $missingIds = array_diff( $this->ids, $idsPresent );
1864 -
1865 - foreach ( $missingIds as $id ) {
1866 - $status->error( 'undelete-missing-filearchive', $id );
1867 - }
1868 -
1869 - // Remove missing files from batch, so we don't get errors when undeleting them
1870 - $storeBatch = $this->removeNonexistentFiles( $storeBatch );
1871 -
1872 - // Run the store batch
1873 - // Use the OVERWRITE_SAME flag to smooth over a common error
1874 - $storeStatus = $this->file->repo->storeBatch( $storeBatch, FileRepo::OVERWRITE_SAME );
1875 - $status->merge( $storeStatus );
1876 -
1877 - if ( !$status->isGood() ) {
1878 - // Even if some files could be copied, fail entirely as that is the
1879 - // easiest thing to do without data loss
1880 - $this->cleanupFailedBatch( $storeStatus, $storeBatch );
1881 - $status->ok = false;
1882 - $this->file->unlock();
1883 -
1884 - return $status;
1885 - }
1886 -
1887 - // Run the DB updates
1888 - // Because we have locked the image row, key conflicts should be rare.
1889 - // If they do occur, we can roll back the transaction at this time with
1890 - // no data loss, but leaving unregistered files scattered throughout the
1891 - // public zone.
1892 - // This is not ideal, which is why it's important to lock the image row.
1893 - if ( $insertCurrent ) {
1894 - $dbw->insert( 'image', $insertCurrent, __METHOD__ );
1895 - }
1896 -
1897 - if ( $insertBatch ) {
1898 - $dbw->insert( 'oldimage', $insertBatch, __METHOD__ );
1899 - }
1900 -
1901 - if ( $deleteIds ) {
1902 - $dbw->delete( 'filearchive',
1903 - array( 'fa_id IN (' . $dbw->makeList( $deleteIds ) . ')' ),
1904 - __METHOD__ );
1905 - }
1906 -
1907 - // If store batch is empty (all files are missing), deletion is to be considered successful
1908 - if ( $status->successCount > 0 || !$storeBatch ) {
1909 - if ( !$exists ) {
1910 - wfDebug( __METHOD__ . " restored {$status->successCount} items, creating a new current\n" );
1911 -
1912 - // Update site_stats
1913 - $site_stats = $dbw->tableName( 'site_stats' );
1914 - $dbw->query( "UPDATE $site_stats SET ss_images=ss_images+1", __METHOD__ );
1915 -
1916 - $this->file->purgeEverything();
1917 - } else {
1918 - wfDebug( __METHOD__ . " restored {$status->successCount} as archived versions\n" );
1919 - $this->file->purgeDescription();
1920 - $this->file->purgeHistory();
1921 - }
1922 - }
1923 -
1924 - $this->file->unlock();
1925 -
1926 - return $status;
1927 - }
1928 -
1929 - /**
1930 - * Removes non-existent files from a store batch.
1931 - */
1932 - function removeNonexistentFiles( $triplets ) {
1933 - $files = $filteredTriplets = array();
1934 - foreach ( $triplets as $file )
1935 - $files[$file[0]] = $file[0];
1936 -
1937 - $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
1938 -
1939 - foreach ( $triplets as $file ) {
1940 - if ( $result[$file[0]] ) {
1941 - $filteredTriplets[] = $file;
1942 - }
1943 - }
1944 -
1945 - return $filteredTriplets;
1946 - }
1947 -
1948 - /**
1949 - * Removes non-existent files from a cleanup batch.
1950 - */
1951 - function removeNonexistentFromCleanup( $batch ) {
1952 - $files = $newBatch = array();
1953 - $repo = $this->file->repo;
1954 -
1955 - foreach ( $batch as $file ) {
1956 - $files[$file] = $repo->getVirtualUrl( 'deleted' ) . '/' .
1957 - rawurlencode( $repo->getDeletedHashPath( $file ) . $file );
1958 - }
1959 -
1960 - $result = $repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
1961 -
1962 - foreach ( $batch as $file ) {
1963 - if ( $result[$file] ) {
1964 - $newBatch[] = $file;
1965 - }
1966 - }
1967 -
1968 - return $newBatch;
1969 - }
1970 -
1971 - /**
1972 - * Delete unused files in the deleted zone.
1973 - * This should be called from outside the transaction in which execute() was called.
1974 - */
1975 - function cleanup() {
1976 - if ( !$this->cleanupBatch ) {
1977 - return $this->file->repo->newGood();
1978 - }
1979 -
1980 - $this->cleanupBatch = $this->removeNonexistentFromCleanup( $this->cleanupBatch );
1981 -
1982 - $status = $this->file->repo->cleanupDeletedBatch( $this->cleanupBatch );
1983 -
1984 - return $status;
1985 - }
1986 -
1987 - function cleanupFailedBatch( $storeStatus, $storeBatch ) {
1988 - $cleanupBatch = array();
1989 -
1990 - foreach ( $storeStatus->success as $i => $success ) {
1991 - if ( $success ) {
1992 - $cleanupBatch[] = array( $storeBatch[$i][1], $storeBatch[$i][1] );
1993 - }
1994 - }
1995 - $this->file->repo->cleanupBatch( $cleanupBatch );
1996 - }
1997 -}
1998 -
1999 -# ------------------------------------------------------------------------------
2000 -
2001 -/**
2002 - * Helper class for file movement
2003 - * @ingroup FileRepo
2004 - */
2005 -class SwiftFileMoveBatch {
2006 - var $file, $cur, $olds, $oldCount, $archive, $target, $db;
2007 -
2008 - function __construct( File $file, Title $target ) {
2009 - $this->file = $file;
2010 - $this->target = $target;
2011 - $this->oldHash = $this->file->repo->getHashPath( $this->file->getName() );
2012 - $this->newHash = $this->file->repo->getHashPath( $this->target->getDBkey() );
2013 - $this->oldName = $this->file->getName();
2014 - $this->newName = $this->file->repo->getNameFromTitle( $this->target );
2015 - $this->oldRel = $this->oldHash . $this->oldName;
2016 - $this->newRel = $this->newHash . $this->newName;
2017 - $this->db = $file->repo->getMasterDb();
2018 - }
2019 -
2020 - /**
2021 - * Add the current image to the batch
2022 - */
2023 - function addCurrent() {
2024 - $this->cur = array( $this->oldRel, $this->newRel );
2025 - }
2026 -
2027 - /**
2028 - * Add the old versions of the image to the batch
2029 - */
2030 - function addOlds() {
2031 - $archiveBase = 'archive';
2032 - $this->olds = array();
2033 - $this->oldCount = 0;
2034 -
2035 - $result = $this->db->select( 'oldimage',
2036 - array( 'oi_archive_name', 'oi_deleted' ),
2037 - array( 'oi_name' => $this->oldName ),
2038 - __METHOD__
2039 - );
2040 -
2041 - foreach ( $result as $row ) {
2042 - $oldName = $row->oi_archive_name;
2043 - $bits = explode( '!', $oldName, 2 );
2044 -
2045 - if ( count( $bits ) != 2 ) {
2046 - wfDebug( "Old file name missing !: '$oldName' \n" );
2047 - continue;
2048 - }
2049 -
2050 - list( $timestamp, $filename ) = $bits;
2051 -
2052 - if ( $this->oldName != $filename ) {
2053 - wfDebug( "Old file name doesn't match: '$oldName' \n" );
2054 - continue;
2055 - }
2056 -
2057 - $this->oldCount++;
2058 -
2059 - // Do we want to add those to oldCount?
2060 - if ( $row->oi_deleted & File::DELETED_FILE ) {
2061 - continue;
2062 - }
2063 -
2064 - $this->olds[] = array(
2065 - "{$archiveBase}/{$this->oldHash}{$oldName}",
2066 - "{$archiveBase}/{$this->newHash}{$timestamp}!{$this->newName}"
2067 - );
2068 - }
2069 - }
2070 -
2071 - /**
2072 - * Perform the move.
2073 - */
2074 - function execute() {
2075 - $repo = $this->file->repo;
2076 - $status = $repo->newGood();
2077 - $triplets = $this->getMoveTriplets();
2078 -
2079 - $triplets = $this->removeNonexistentFiles( $triplets );
2080 -
2081 - // Copy the files into their new location
2082 - $statusMove = $repo->storeBatch( $triplets );
2083 - wfDebugLog( 'imagemove', "Moved files for {$this->file->name}: {$statusMove->successCount} successes, {$statusMove->failCount} failures" );
2084 - if ( !$statusMove->isGood() ) {
2085 - wfDebugLog( 'imagemove', "Error in moving files: " . $statusMove->getWikiText() );
2086 - $this->cleanupTarget( $triplets );
2087 - $statusMove->ok = false;
2088 - return $statusMove;
2089 - }
2090 -
2091 - $this->db->begin();
2092 - $statusDb = $this->doDBUpdates();
2093 - wfDebugLog( 'imagemove', "Renamed {$this->file->name} in database: {$statusDb->successCount} successes, {$statusDb->failCount} failures" );
2094 - if ( !$statusDb->isGood() ) {
2095 - $this->db->rollback();
2096 - // Something went wrong with the DB updates, so remove the target files
2097 - $this->cleanupTarget( $triplets );
2098 - $statusDb->ok = false;
2099 - return $statusDb;
2100 - }
2101 - $this->db->commit();
2102 -
2103 - // Everything went ok, remove the source files
2104 - $this->cleanupSource( $triplets );
2105 -
2106 - $status->merge( $statusDb );
2107 - $status->merge( $statusMove );
2108 -
2109 - return $status;
2110 - }
2111 -
2112 - /**
2113 - * Do the database updates and return a new FileRepoStatus indicating how
2114 - * many rows where updated.
2115 - *
2116 - * @return FileRepoStatus
2117 - */
2118 - function doDBUpdates() {
2119 - $repo = $this->file->repo;
2120 - $status = $repo->newGood();
2121 - $dbw = $this->db;
2122 -
2123 - // Update current image
2124 - $dbw->update(
2125 - 'image',
2126 - array( 'img_name' => $this->newName ),
2127 - array( 'img_name' => $this->oldName ),
2128 - __METHOD__
2129 - );
2130 -
2131 - if ( $dbw->affectedRows() ) {
2132 - $status->successCount++;
2133 - } else {
2134 - $status->failCount++;
2135 - $status->fatal( 'imageinvalidfilename' );
2136 - return $status;
2137 - }
2138 -
2139 - // Update old images
2140 - $dbw->update(
2141 - 'oldimage',
2142 - array(
2143 - 'oi_name' => $this->newName,
2144 - 'oi_archive_name = ' . $dbw->strreplace( 'oi_archive_name', $dbw->addQuotes( $this->oldName ), $dbw->addQuotes( $this->newName ) ),
2145 - ),
2146 - array( 'oi_name' => $this->oldName ),
2147 - __METHOD__
2148 - );
2149 -
2150 - $affected = $dbw->affectedRows();
2151 - $total = $this->oldCount;
2152 - $status->successCount += $affected;
2153 - $status->failCount += $total - $affected;
2154 - if ( $status->failCount ) {
2155 - $status->error( 'imageinvalidfilename' );
2156 - }
2157 -
2158 - return $status;
2159 - }
2160 -
2161 - /**
2162 - * Generate triplets for storeBatch().
2163 - */
2164 - function getMoveTriplets() {
2165 - $moves = array_merge( array( $this->cur ), $this->olds );
2166 - $triplets = array(); // The format is: (srcUrl, destZone, destUrl)
2167 -
2168 - foreach ( $moves as $move ) {
2169 - // $move: (oldRelativePath, newRelativePath)
2170 - $srcUrl = $this->file->repo->getVirtualUrl() . '/public/' . rawurlencode( $move[0] );
2171 - $triplets[] = array( $srcUrl, 'public', $move[1] );
2172 - wfDebugLog( 'imagemove', "Generated move triplet for {$this->file->name}: {$srcUrl} :: public :: {$move[1]}" );
2173 - }
2174 -
2175 - return $triplets;
2176 - }
2177 -
2178 - /**
2179 - * Removes non-existent files from move batch.
2180 - */
2181 - function removeNonexistentFiles( $triplets ) {
2182 - $files = array();
2183 -
2184 - foreach ( $triplets as $file ) {
2185 - $files[$file[0]] = $file[0];
2186 - }
2187 -
2188 - $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
2189 - $filteredTriplets = array();
2190 -
2191 - foreach ( $triplets as $file ) {
2192 - if ( $result[$file[0]] ) {
2193 - $filteredTriplets[] = $file;
2194 - } else {
2195 - wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
2196 - }
2197 - }
2198 -
2199 - return $filteredTriplets;
2200 - }
2201 -
2202 - /**
2203 - * Cleanup a partially moved array of triplets by deleting the target
2204 - * files. Called if something went wrong half way.
2205 - */
2206 - function cleanupTarget( $triplets ) {
2207 - // Create dest pairs from the triplets
2208 - $pairs = array();
2209 - foreach ( $triplets as $triplet ) {
2210 - $pairs[] = array( $triplet[1], $triplet[2] );
2211 - }
2212 -
2213 - $this->file->repo->cleanupBatch( $pairs );
2214 - }
2215 -
2216 - /**
2217 - * Cleanup a fully moved array of triplets by deleting the source files.
2218 - * Called at the end of the move process if everything else went ok.
2219 - */
2220 - function cleanupSource( $triplets ) {
2221 - // Create source file names from the triplets
2222 - $files = array();
2223 - foreach ( $triplets as $triplet ) {
2224 - $files[] = $triplet[0];
2225 - }
2226 -
2227 - $this->file->repo->cleanupBatch( $files );
2228 - }
2229 -}
2230 -
2231 -/**
2232318 * Repository that stores files in Swift and registers them
2233319 * in the wiki's own database.
2234320 *
@@ -2236,6 +322,9 @@
2237323 */
2238324
2239325 class SwiftRepo extends LocalRepo {
 326+ // The public interface to SwiftFile is through SwiftRepo's findFile and
 327+ // newFile. They call into the repo's NewFile and FindFile, which call
 328+ // one of these factories to create the File object.
2240329 var $fileFactory = array( 'SwiftFile', 'newFromTitle' );
2241330 var $fileFactoryKey = array( 'SwiftFile', 'newFromKey' );
2242331 var $fileFromRowFactory = array( 'SwiftFile', 'newFromRow' );
@@ -2244,6 +333,8 @@
2245334 var $oldFileFromRowFactory = array( 'OldSwiftFile', 'newFromRow' );
2246335
2247336 function __construct( $info ) {
 337+ // We don't call parent::_construct because it requires $this->directory,
 338+ // which doesn't exist in Swift.
2248339 FileRepo::__construct( $info );
2249340
2250341 // Required settings
@@ -2254,12 +345,14 @@
2255346 $this->deletedHashLevels = isset( $info['deletedHashLevels'] ) ?
2256347 $info['deletedHashLevels'] : $this->hashLevels;
2257348
2258 - if ( isset( $info['thumbUrl'] ) ) {
2259 - $this->thumbUrl = $info['thumbUrl'];
2260 - } else {
2261 - $this->thumbUrl = "{$this->url}/thumb";
2262 - }
 349+ // This relationship is also hard-coded in rewrite.py, another part of this
 350+ // extension. If you want to change this here, you might have to change it
 351+ // there, too.
 352+ $this->thumbUrl = "{$this->url}/thumb";
2263353
 354+ // we don't have directories
 355+ $this->deletedDir = false;
 356+
2264357 // Required settings
2265358 $this->swiftuser= $info['user'];
2266359 $this->swiftkey= $info['key'];
@@ -2399,7 +492,7 @@
2400493
2401494 // Where are we copying this from?
2402495 if (self::isVirtualUrl( $srcPath )) {
2403 - $src = $this->resolveVirtualUrl( $srcPath );
 496+ $src = $this->getContainerRel( $srcPath );
2404497 list ($srcContainer, $srcRel) = $src;
2405498 $srcc = $this->get_container($conn, $srcContainer);
2406499
@@ -2417,7 +510,7 @@
2418511
2419512 if ( !( $flags & self::SKIP_VALIDATION ) ) {
2420513 // FIXME: Swift will return the MD5 of the data written.
2421 - if (0) { // ( $hashDest === false || $hashSource !== $hashDest ) {
 514+ if (0) { // ( $hashDest === false || $hashSource !== $hashDest )
2422515 wfDebug( __METHOD__ . ': File copy validation failed: ' .
2423516 "$srcPath ($hashSource) to $dstPath ($hashDest)\n" );
2424517
@@ -2436,25 +529,6 @@
2437530 }
2438531
2439532 /**
2440 - * Pick a random name in the temp zone and store a file to it.
2441 - * @param $originalName String: the base name of the file as specified
2442 - * by the user. The file extension will be maintained.
2443 - * @param $srcPath String: the current location of the file.
2444 - * @return FileRepoStatus object with the URL in the value.
2445 - */
2446 - function storeTemp( $originalName, $srcPath ) {
2447 - $date = gmdate( "YmdHis" );
2448 - $hashPath = $this->getHashPath( $originalName );
2449 - $dstRel = "$hashPath$date!$originalName";
2450 - $dstUrlRel = $hashPath . $date . '!' . rawurlencode( $originalName );
2451 -
2452 - $result = $this->store( $srcPath, 'temp', $dstRel );
2453 - $result->value = $this->getVirtualUrl( 'temp' ) . '/' . $dstUrlRel;
2454 - return $result;
2455 - }
2456 -
2457 -
2458 - /**
2459533 * Append the contents of the source path to the given file, OR queue
2460534 * the appending operation in anticipation of a later appendFinish() call.
2461535 * @param $srcPath String: location of the source file
@@ -2562,7 +636,7 @@
2563637 if ( !self::isVirtualUrl( $file ) ) {
2564638 throw new MWException( __METHOD__ . " requires a virtual URL, not '$file'");
2565639 }
2566 - $rvu = $this->resolveVirtualUrl( $file );
 640+ $rvu = $this->getContainerRel( $file );
2567641 list ($cont, $rel) = $rvu;
2568642 $container = $this->get_container($conn,$cont);
2569643 try {
@@ -2707,7 +781,7 @@
2708782 } else {
2709783 if ( self::isVirtualUrl( $file ) ) {
2710784 // This is a virtual url, resolve it
2711 - $path = $this->resolveVirtualUrl( $file );
 785+ $path = $this->getContainerRel( $file );
2712786 list( $cont, $rel) = $path;
2713787 } else {
2714788 // FIXME: This is a full file name
@@ -2790,31 +864,37 @@
2791865 }
2792866 }
2793867 /**
2794 - * Get the ($container, $object) corresponding to a virtual URL
 868+ * Remove a temporary file or mark it for garbage collection
 869+ * @param $virtualUrl String: the virtual URL returned by storeTemp
 870+ * @return Boolean: true on success, false on failure
2795871 */
2796 - function resolveVirtualUrl( $url ) {
2797 - if ( substr( $url, 0, 9 ) != 'mwrepo://' ) {
2798 - throw new MWException( __METHOD__.": unknown protocol" );
 872+ function freeTemp( $virtualUrl ) {
 873+ $temp = "mwrepo://{$this->name}/temp";
 874+ if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
 875+ wfDebug( __METHOD__.": Invalid virtual URL\n" );
 876+ return false;
2799877 }
 878+ $path = $this->getContainerRel( $virtualUrl );
 879+ list ($c, $r) = $path;
 880+ $conn = $this->connect();
 881+ $container = $this->get_container($conn,$c);
 882+ $this->swift_delete($container, $r);
 883+ }
2800884
2801 - $bits = explode( '/', substr( $url, 9 ), 3 );
2802 - if ( count( $bits ) != 3 ) {
2803 - throw new MWException( __METHOD__.": invalid mwrepo URL: $url" );
2804 - }
2805 - list( $repo, $zone, $rel ) = $bits;
2806 - if ( $repo !== $this->name ) {
2807 - throw new MWException( __METHOD__.": fetching from a foreign repo is not supported" );
2808 - }
2809 - $container = $this->getZoneContainer( $zone );
2810 - if ( $container === false) {
2811 - throw new MWException( __METHOD__.": invalid zone: $zone" );
2812 - }
2813 - return array($container, rawurldecode( $rel ));
 885+ /**
 886+ * Get an UploadStash associated with this repo.
 887+ *
 888+ * @return UploadStash
 889+ */
 890+ function getUploadStash() {
 891+ return new SwiftStash( $this );
2814892 }
 893+}
2815894
 895+class SwiftStash extends UploadStash {
 896+}
2816897
2817 -
2818 -
 898+class SwiftStashFile extends UploadStashFile {
2819899 }
2820900
2821901 /**
@@ -3031,90 +1111,3 @@
30321112 return Revision::userCanBitfield( $this->deleted, $field );
30331113 }
30341114 }
3035 -
3036 -
3037 -class Junkyjunk {
3038 - /**
3039 - * Remove a temporary file or mark it for garbage collection
3040 - * @param $virtualUrl String: the virtual URL returned by storeTemp
3041 - * @return Boolean: true on success, false on failure
3042 - */
3043 - function freeTemp( $virtualUrl ) {
3044 - $temp = "mwrepo://{$this->name}/temp";
3045 - if ( substr( $virtualUrl, 0, strlen( $temp ) ) != $temp ) {
3046 - wfDebug( __METHOD__.": Invalid virtual URL\n" );
3047 - return false;
3048 - }
3049 - $path = $this->resolveVirtualUrl( $virtualUrl );
3050 - $success = unlink( $path );
3051 - return $success;
3052 - }
3053 -
3054 -
3055 -
3056 - /**
3057 - * Removes non-existent files from move batch.
3058 - */
3059 - function move_removeNonexistentFiles( $triplets ) {
3060 - $files = array();
3061 -
3062 - foreach ( $triplets as $file ) {
3063 - $files[$file[0]] = $file[0];
3064 - }
3065 -
3066 - $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
3067 - $filteredTriplets = array();
3068 -
3069 - foreach ( $triplets as $file ) {
3070 - if ( $result[$file[0]] ) {
3071 - $filteredTriplets[] = $file;
3072 - } else {
3073 - wfDebugLog( 'imagemove', "File {$file[0]} does not exist" );
3074 - }
3075 - }
3076 -
3077 - return $filteredTriplets;
3078 - }
3079 - /**
3080 - * Removes non-existent files from a store batch.
3081 - */
3082 - function store_removeNonexistentFiles( $triplets ) {
3083 - $files = $filteredTriplets = array();
3084 -
3085 - foreach ( $triplets as $file )
3086 - $files[$file[0]] = $file[0];
3087 -
3088 - $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
3089 -
3090 - foreach ( $triplets as $file ) {
3091 - if ( $result[$file[0]] ) {
3092 - $filteredTriplets[] = $file;
3093 - }
3094 - }
3095 -
3096 - return $filteredTriplets;
3097 - }
3098 -
3099 - /**
3100 - * Removes non-existent files from a deletion batch.
3101 - */
3102 - function deletion_removeNonexistentFiles( $triplets ) {
3103 - $files = $filteredTriplets = array();
3104 -
3105 - foreach ( $triplets as $file) {
3106 - list( $src, $dest ) = $file;
3107 - $files[$src] = $this->file->repo->getVirtualUrl( 'public' ) . '/' . rawurlencode( $src );
3108 - }
3109 -
3110 - $result = $this->file->repo->fileExistsBatch( $files, FSRepo::FILES_ONLY );
3111 -
3112 - foreach ( $triplets as $file ) {
3113 - if ( $result[$file[0]] ) {
3114 - $filteredTriplets[] = $file;
3115 - }
3116 - }
3117 -
3118 - return $filteredTriplets;
3119 - }
3120 -
3121 -}

Status & tagging log