/** * Run the transaction, except the cleanup batch. * The cleanup batch should be run in a separate transaction, because it locks different * rows and there's no need to keep the image row locked while it's acquiring those locks * The caller may have its own transaction open. * So we save the batch and let the caller call cleanup() */ function execute() { global $wgLang; if (!$this->all && !$this->ids) { // Do nothing return $this->file->repo->newGood(); } $exists = $this->file->lock(); $dbw = $this->file->repo->getMasterDB(); $status = $this->file->repo->newGood(); // Fetch all or selected archived revisions for the file, // sorted from the most recent to the oldest. $conditions = array('fa_name' => $this->file->getName()); if (!$this->all) { $conditions[] = 'fa_id IN (' . $dbw->makeList($this->ids) . ')'; } $result = $dbw->select('filearchive', '*', $conditions, __METHOD__, array('ORDER BY' => 'fa_timestamp DESC')); $idsPresent = array(); $storeBatch = array(); $insertBatch = array(); $insertCurrent = false; $deleteIds = array(); $first = true; $archiveNames = array(); foreach ($result as $row) { $idsPresent[] = $row->fa_id; if ($row->fa_name != $this->file->getName()) { $status->error('undelete-filename-mismatch', $wgLang->timeanddate($row->fa_timestamp)); $status->failCount++; continue; } if ($row->fa_storage_key == '') { // Revision was missing pre-deletion $status->error('undelete-bad-store-key', $wgLang->timeanddate($row->fa_timestamp)); $status->failCount++; continue; } $deletedRel = $this->file->repo->getDeletedHashPath($row->fa_storage_key) . $row->fa_storage_key; $deletedUrl = $this->file->repo->getVirtualUrl() . '/deleted/' . $deletedRel; $sha1 = substr($row->fa_storage_key, 0, strcspn($row->fa_storage_key, '.')); # Fix leading zero if (strlen($sha1) == 32 && $sha1[0] == '0') { $sha1 = substr($sha1, 1); } if (is_null($row->fa_major_mime) || $row->fa_major_mime == 'unknown' || is_null($row->fa_minor_mime) || $row->fa_minor_mime == 'unknown' || is_null($row->fa_media_type) || $row->fa_media_type == 'UNKNOWN' || is_null($row->fa_metadata)) { // Refresh our metadata // Required for a new current revision; nice for older ones too. :) $props = RepoGroup::singleton()->getFileProps($deletedUrl); } else { $props = array('minor_mime' => $row->fa_minor_mime, 'major_mime' => $row->fa_major_mime, 'media_type' => $row->fa_media_type, 'metadata' => $row->fa_metadata); } if ($first && !$exists) { // This revision will be published as the new current version $destRel = $this->file->getRel(); $insertCurrent = array('img_name' => $row->fa_name, 'img_size' => $row->fa_size, 'img_width' => $row->fa_width, 'img_height' => $row->fa_height, 'img_metadata' => $props['metadata'], 'img_bits' => $row->fa_bits, 'img_media_type' => $props['media_type'], 'img_major_mime' => $props['major_mime'], 'img_minor_mime' => $props['minor_mime'], 'img_description' => $row->fa_description, 'img_user' => $row->fa_user, 'img_user_text' => $row->fa_user_text, 'img_timestamp' => $row->fa_timestamp, 'img_sha1' => $sha1); // The live (current) version cannot be hidden! if (!$this->unsuppress && $row->fa_deleted) { $storeBatch[] = array($deletedUrl, 'public', $destRel); $this->cleanupBatch[] = $row->fa_storage_key; } } else { $archiveName = $row->fa_archive_name; if ($archiveName == '') { // This was originally a current version; we // have to devise a new archive name for it. // Format is <timestamp of archiving>!<name> $timestamp = wfTimestamp(TS_UNIX, $row->fa_deleted_timestamp); do { $archiveName = wfTimestamp(TS_MW, $timestamp) . '!' . $row->fa_name; $timestamp++; } while (isset($archiveNames[$archiveName])); } $archiveNames[$archiveName] = true; $destRel = $this->file->getArchiveRel($archiveName); $insertBatch[] = array('oi_name' => $row->fa_name, 'oi_archive_name' => $archiveName, 'oi_size' => $row->fa_size, 'oi_width' => $row->fa_width, 'oi_height' => $row->fa_height, 'oi_bits' => $row->fa_bits, 'oi_description' => $row->fa_description, 'oi_user' => $row->fa_user, 'oi_user_text' => $row->fa_user_text, 'oi_timestamp' => $row->fa_timestamp, 'oi_metadata' => $props['metadata'], 'oi_media_type' => $props['media_type'], 'oi_major_mime' => $props['major_mime'], 'oi_minor_mime' => $props['minor_mime'], 'oi_deleted' => $this->unsuppress ? 0 : $row->fa_deleted, 'oi_sha1' => $sha1); } $deleteIds[] = $row->fa_id; if (!$this->unsuppress && $row->fa_deleted & File::DELETED_FILE) { // private files can stay where they are $status->successCount++; } else { $storeBatch[] = array($deletedUrl, 'public', $destRel); $this->cleanupBatch[] = $row->fa_storage_key; } $first = false; } unset($result); // Add a warning to the status object for missing IDs $missingIds = array_diff($this->ids, $idsPresent); foreach ($missingIds as $id) { $status->error('undelete-missing-filearchive', $id); } // Remove missing files from batch, so we don't get errors when undeleting them $storeBatch = $this->removeNonexistentFiles($storeBatch); // Run the store batch // Use the OVERWRITE_SAME flag to smooth over a common error $storeStatus = $this->file->repo->storeBatch($storeBatch, FileRepo::OVERWRITE_SAME); $status->merge($storeStatus); if (!$status->isGood()) { // Even if some files could be copied, fail entirely as that is the // easiest thing to do without data loss $this->cleanupFailedBatch($storeStatus, $storeBatch); $status->ok = false; $this->file->unlock(); return $status; } // Run the DB updates // Because we have locked the image row, key conflicts should be rare. // If they do occur, we can roll back the transaction at this time with // no data loss, but leaving unregistered files scattered throughout the // public zone. // This is not ideal, which is why it's important to lock the image row. if ($insertCurrent) { $dbw->insert('image', $insertCurrent, __METHOD__); } if ($insertBatch) { $dbw->insert('oldimage', $insertBatch, __METHOD__); } if ($deleteIds) { $dbw->delete('filearchive', array('fa_id IN (' . $dbw->makeList($deleteIds) . ')'), __METHOD__); } // If store batch is empty (all files are missing), deletion is to be considered successful if ($status->successCount > 0 || !$storeBatch) { if (!$exists) { wfDebug(__METHOD__ . " restored {$status->successCount} items, creating a new current\n"); // Update site_stats $site_stats = $dbw->tableName('site_stats'); $dbw->query("UPDATE {$site_stats} SET ss_images=ss_images+1", __METHOD__); $this->file->purgeEverything(); } else { wfDebug(__METHOD__ . " restored {$status->successCount} as archived versions\n"); $this->file->purgeDescription(); $this->file->purgeHistory(); } } $this->file->unlock(); return $status; }
/** * Perform the move. * @return FileRepoStatus */ function execute() { $repo = $this->file->repo; $status = $repo->newGood(); $triplets = $this->getMoveTriplets(); $triplets = $this->removeNonexistentFiles($triplets); $destFile = wfLocalFile($this->target); $this->file->lock(); // begin $destFile->lock(); // quickly fail if destination is not available // Rename the file versions metadata in the DB. // This implicitly locks the destination file, which avoids race conditions. // If we moved the files from A -> C before DB updates, another process could // move files from B -> C at this point, causing storeBatch() to fail and thus // cleanupTarget() to trigger. It would delete the C files and cause data loss. $statusDb = $this->doDBUpdates(); if (!$statusDb->isGood()) { $this->file->unlockAndRollback(); $statusDb->ok = false; return $statusDb; } wfDebugLog('imagemove', "Renamed {$this->file->getName()} in database: " . "{$statusDb->successCount} successes, {$statusDb->failCount} failures"); // Copy the files into their new location. // If a prior process fataled copying or cleaning up files we tolerate any // of the existing files if they are identical to the ones being stored. $statusMove = $repo->storeBatch($triplets, FileRepo::OVERWRITE_SAME); wfDebugLog('imagemove', "Moved files for {$this->file->getName()}: " . "{$statusMove->successCount} successes, {$statusMove->failCount} failures"); if (!$statusMove->isGood()) { // Delete any files copied over (while the destination is still locked) $this->cleanupTarget($triplets); $this->file->unlockAndRollback(); // unlocks the destination wfDebugLog('imagemove', "Error in moving files: " . $statusMove->getWikiText()); $statusMove->ok = false; return $statusMove; } $destFile->unlock(); $this->file->unlock(); // done // Everything went ok, remove the source files $this->cleanupSource($triplets); $status->merge($statusDb); $status->merge($statusMove); return $status; }
/** * Perform the move. * @return Status */ public function execute() { $repo = $this->file->repo; $status = $repo->newGood(); $destFile = wfLocalFile($this->target); $this->file->lock(); // begin $destFile->lock(); // quickly fail if destination is not available $triplets = $this->getMoveTriplets(); $checkStatus = $this->removeNonexistentFiles($triplets); if (!$checkStatus->isGood()) { $destFile->unlock(); $this->file->unlock(); $status->merge($checkStatus); // couldn't talk to file backend return $status; } $triplets = $checkStatus->value; // Verify the file versions metadata in the DB. $statusDb = $this->verifyDBUpdates(); if (!$statusDb->isGood()) { $destFile->unlock(); $this->file->unlock(); $statusDb->setOK(false); return $statusDb; } if (!$repo->hasSha1Storage()) { // Copy the files into their new location. // If a prior process fataled copying or cleaning up files we tolerate any // of the existing files if they are identical to the ones being stored. $statusMove = $repo->storeBatch($triplets, FileRepo::OVERWRITE_SAME); wfDebugLog('imagemove', "Moved files for {$this->file->getName()}: " . "{$statusMove->successCount} successes, {$statusMove->failCount} failures"); if (!$statusMove->isGood()) { // Delete any files copied over (while the destination is still locked) $this->cleanupTarget($triplets); $destFile->unlock(); $this->file->unlock(); wfDebugLog('imagemove', "Error in moving files: " . $statusMove->getWikiText(false, false, 'en')); $statusMove->setOK(false); return $statusMove; } $status->merge($statusMove); } // Rename the file versions metadata in the DB. $this->doDBUpdates(); wfDebugLog('imagemove', "Renamed {$this->file->getName()} in database: " . "{$statusDb->successCount} successes, {$statusDb->failCount} failures"); $destFile->unlock(); $this->file->unlock(); // done // Everything went ok, remove the source files $this->cleanupSource($triplets); $status->merge($statusDb); return $status; }