diff --git a/src/__phutil_library_map__.php b/src/__phutil_library_map__.php index ef3dd661a7..2cf06ba8a8 100644 --- a/src/__phutil_library_map__.php +++ b/src/__phutil_library_map__.php @@ -1578,6 +1578,7 @@ phutil_register_library_map(array( 'PhabricatorFileUploadException' => 'applications/files/exception/PhabricatorFileUploadException.php', 'PhabricatorFilesApplication' => 'applications/files/application/PhabricatorFilesApplication.php', 'PhabricatorFilesConfigOptions' => 'applications/files/config/PhabricatorFilesConfigOptions.php', + 'PhabricatorFilesManagementCompactWorkflow' => 'applications/files/management/PhabricatorFilesManagementCompactWorkflow.php', 'PhabricatorFilesManagementEnginesWorkflow' => 'applications/files/management/PhabricatorFilesManagementEnginesWorkflow.php', 'PhabricatorFilesManagementMigrateWorkflow' => 'applications/files/management/PhabricatorFilesManagementMigrateWorkflow.php', 'PhabricatorFilesManagementPurgeWorkflow' => 'applications/files/management/PhabricatorFilesManagementPurgeWorkflow.php', @@ -4430,6 +4431,7 @@ phutil_register_library_map(array( 'PhabricatorFileUploadException' => 'Exception', 'PhabricatorFilesApplication' => 'PhabricatorApplication', 'PhabricatorFilesConfigOptions' => 'PhabricatorApplicationConfigOptions', + 'PhabricatorFilesManagementCompactWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementEnginesWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementMigrateWorkflow' => 'PhabricatorFilesManagementWorkflow', 'PhabricatorFilesManagementPurgeWorkflow' => 'PhabricatorFilesManagementWorkflow', diff --git a/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php b/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php new file mode 100644 index 0000000000..63f44228fe --- /dev/null +++ b/src/applications/files/management/PhabricatorFilesManagementCompactWorkflow.php @@ -0,0 +1,133 @@ +setName('compact') + ->setSynopsis( + pht( + 'Merge identical files to share the same storage. In some cases, '. + 'this can repair files with missing data.')) + ->setArguments( + array( + array( + 'name' => 'dry-run', + 'help' => pht('Show what would be compacted.'), + ), + array( + 'name' => 'all', + 'help' => pht('Compact all files.'), + ), + array( + 'name' => 'names', + 'wildcard' => true, + ), + )); + } + + public function execute(PhutilArgumentParser $args) { + $console = PhutilConsole::getConsole(); + + $iterator = $this->buildIterator($args); + if (!$iterator) { + throw new PhutilArgumentUsageException( + pht( + 'Either specify a list of files to compact, or use `--all` '. + 'to compact all files.')); + } + + $is_dry_run = $args->getArg('dry-run'); + + foreach ($iterator as $file) { + $monogram = $file->getMonogram(); + + $hash = $file->getContentHash(); + if (!$hash) { + $console->writeOut( + "%s\n", + pht('%s: No content hash.', $monogram)); + continue; + } + + // Find other files with the same content hash. We're going to point + // them at the data for this file. + $similar_files = id(new PhabricatorFile())->loadAllWhere( + 'contentHash = %s AND id != %d AND + (storageEngine != %s OR storageHandle != %s)', + $hash, + $file->getID(), + $file->getStorageEngine(), + $file->getStorageHandle()); + if (!$similar_files) { + $console->writeOut( + "%s\n", + pht('%s: No other files with the same content hash.', $monogram)); + continue; + } + + // Only compact files into this one if we can load the data. This + // prevents us from breaking working files if we're missing some data. + try { + $data = $file->loadFileData(); + } catch (Exception $ex) { + $data = null; + } + + if ($data === null) { + $console->writeOut( + "%s\n", + pht( + '%s: Unable to load file data; declining to compact.', + $monogram)); + continue; + } + + foreach ($similar_files as $similar_file) { + if ($is_dry_run) { + $console->writeOut( + "%s\n", + pht( + '%s: Would compact storage with %s.', + $monogram, + $similar_file->getMonogram())); + continue; + } + + $console->writeOut( + "%s\n", + pht( + '%s: Compacting storage with %s.', + $monogram, + $similar_file->getMonogram())); + + $old_instance = null; + try { + $old_instance = $similar_file->instantiateStorageEngine(); + $old_engine = $similar_file->getStorageEngine(); + $old_handle = $similar_file->getStorageHandle(); + } catch (Exception $ex) { + // If the old stuff is busted, we just won't try to delete the + // old data. + phlog($ex); + } + + $similar_file + ->setStorageEngine($file->getStorageEngine()) + ->setStorageHandle($file->getStorageHandle()) + ->save(); + + if ($old_instance) { + $similar_file->deleteFileDataIfUnused( + $old_instance, + $old_engine, + $old_handle); + } + } + } + + return 0; + } + +} diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index 133da30415..44ba9ca21f 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -455,7 +455,7 @@ final class PhabricatorFile extends PhabricatorFileDAO * Destroy stored file data if there are no remaining files which reference * it. */ - private function deleteFileDataIfUnused( + public function deleteFileDataIfUnused( PhabricatorFileStorageEngine $engine, $engine_identifier, $handle) { @@ -644,7 +644,7 @@ final class PhabricatorFile extends PhabricatorFileDAO return $supported; } - protected function instantiateStorageEngine() { + public function instantiateStorageEngine() { return self::buildEngine($this->getStorageEngine()); }