diff --git a/resources/sql/patches/20130215.phabricatorfileaddttl.sql b/resources/sql/patches/20130215.phabricatorfileaddttl.sql new file mode 100644 index 0000000000..d8c3cbf8b3 --- /dev/null +++ b/resources/sql/patches/20130215.phabricatorfileaddttl.sql @@ -0,0 +1,3 @@ +ALTER TABLE {$NAMESPACE}_file.file + ADD ttl INT(10) UNSIGNED DEFAULT NULL, + ADD KEY key_ttl (ttl); diff --git a/src/applications/diffusion/controller/DiffusionBrowseFileController.php b/src/applications/diffusion/controller/DiffusionBrowseFileController.php index 474605c44c..cb285fda77 100644 --- a/src/applications/diffusion/controller/DiffusionBrowseFileController.php +++ b/src/applications/diffusion/controller/DiffusionBrowseFileController.php @@ -1,951 +1,952 @@ getRequest(); $drequest = $this->getDiffusionRequest(); $before = $request->getStr('before'); if ($before) { return $this->buildBeforeResponse($before); } $path = $drequest->getPath(); $selected = $request->getStr('view'); $preferences = $request->getUser()->loadPreferences(); if (!$selected) { $selected = $preferences->getPreference( PhabricatorUserPreferences::PREFERENCE_DIFFUSION_VIEW, 'highlighted'); } else if ($request->isFormPost() && $selected != 'raw') { $preferences->setPreference( PhabricatorUserPreferences::PREFERENCE_DIFFUSION_VIEW, $selected); $preferences->save(); return id(new AphrontRedirectResponse()) ->setURI($request->getRequestURI()->alter('view', $selected)); } $needs_blame = ($selected == 'blame' || $selected == 'plainblame'); $file_query = DiffusionFileContentQuery::newFromDiffusionRequest( $this->diffusionRequest); $file_query->setViewer($request->getUser()); $file_query->setNeedsBlame($needs_blame); $file_query->loadFileContent(); $data = $file_query->getRawData(); if ($selected === 'raw') { return $this->buildRawResponse($path, $data); } $this->loadLintMessages(); // Build the content of the file. $corpus = $this->buildCorpus( $selected, $file_query, $needs_blame, $drequest, $path, $data); require_celerity_resource('diffusion-source-css'); if ($this->corpusType == 'text') { $view_select_panel = $this->renderViewSelectPanel($selected); } else { $view_select_panel = null; } // Render the page. $content = array(); $follow = $request->getStr('follow'); if ($follow) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_WARNING); $notice->setTitle('Unable to Continue'); switch ($follow) { case 'first': $notice->appendChild( "Unable to continue tracing the history of this file because ". "this commit is the first commit in the repository."); break; case 'created': $notice->appendChild( "Unable to continue tracing the history of this file because ". "this commit created the file."); break; } $content[] = $notice; } $renamed = $request->getStr('renamed'); if ($renamed) { $notice = new AphrontErrorView(); $notice->setSeverity(AphrontErrorView::SEVERITY_NOTICE); $notice->setTitle('File Renamed'); $notice->appendChild( "File history passes through a rename from '".$drequest->getPath(). "' to '".$renamed."'."); $content[] = $notice; } $content[] = $view_select_panel; $content[] = $corpus; $content[] = $this->buildOpenRevisions(); $nav = $this->buildSideNav('browse', true); $nav->appendChild($content); $crumbs = $this->buildCrumbs( array( 'branch' => true, 'path' => true, 'view' => 'browse', )); $nav->setCrumbs($crumbs); $basename = basename($this->getDiffusionRequest()->getPath()); return $this->buildApplicationPage( $nav, array( 'title' => $basename, )); } private function loadLintMessages() { $drequest = $this->getDiffusionRequest(); $branch = $drequest->loadBranch(); if (!$branch || !$branch->getLintCommit()) { return; } $this->lintCommit = $branch->getLintCommit(); $conn = id(new PhabricatorRepository())->establishConnection('r'); $where = ''; if ($drequest->getLint()) { $where = qsprintf( $conn, 'AND code = %s', $drequest->getLint()); } $this->lintMessages = queryfx_all( $conn, 'SELECT * FROM %T WHERE branchID = %d %Q AND path = %s', PhabricatorRepository::TABLE_LINTMESSAGE, $branch->getID(), $where, '/'.$drequest->getPath()); } private function buildCorpus($selected, DiffusionFileContentQuery $file_query, $needs_blame, DiffusionRequest $drequest, $path, $data) { if (ArcanistDiffUtils::isHeuristicBinaryFile($data)) { $file = $this->loadFileForData($path, $data); $file_uri = $file->getBestURI(); if ($file->isViewableImage()) { $this->corpusType = 'image'; return $this->buildImageCorpus($file_uri); } else { $this->corpusType = 'binary'; return $this->buildBinaryCorpus($file_uri, $data); } } switch ($selected) { case 'plain': $style = "margin: 1em 2em; width: 90%; height: 80em; font-family: monospace"; $corpus = phutil_tag( 'textarea', array( 'style' => $style, ), $file_query->getRawData()); break; case 'plainblame': $style = "margin: 1em 2em; width: 90%; height: 80em; font-family: monospace"; list($text_list, $rev_list, $blame_dict) = $file_query->getBlameData(); $rows = array(); foreach ($text_list as $k => $line) { $rev = $rev_list[$k]; if (isset($blame_dict[$rev]['handle'])) { $author = $blame_dict[$rev]['handle']->getName(); } else { $author = $blame_dict[$rev]['author']; } $rows[] = sprintf("%-10s %-20s %s", substr($rev, 0, 7), $author, $line); } $corpus = phutil_tag( 'textarea', array( 'style' => $style, ), implode("\n", $rows)); break; case 'highlighted': case 'blame': default: require_celerity_resource('syntax-highlighting-css'); list($text_list, $rev_list, $blame_dict) = $file_query->getBlameData(); $text_list = implode("\n", $text_list); $text_list = PhabricatorSyntaxHighlighter::highlightWithFilename( $path, $text_list); $text_list = explode("\n", $text_list); $rows = $this->buildDisplayRows($text_list, $rev_list, $blame_dict, $needs_blame, $drequest, $file_query, $selected); $id = celerity_generate_unique_node_id(); $projects = $drequest->loadArcanistProjects(); $langs = array(); foreach ($projects as $project) { $ls = $project->getSymbolIndexLanguages(); if (!$ls) { continue; } $dep_projects = $project->getSymbolIndexProjects(); $dep_projects[] = $project->getPHID(); foreach ($ls as $lang) { if (!isset($langs[$lang])) { $langs[$lang] = array(); } $langs[$lang] += $dep_projects + array($project); } } $lang = last(explode('.', $drequest->getPath())); if (isset($langs[$lang])) { Javelin::initBehavior( 'repository-crossreference', array( 'container' => $id, 'lang' => $lang, 'projects' => $langs[$lang], )); } $corpus_table = javelin_tag( 'table', array( 'class' => "diffusion-source remarkup-code PhabricatorMonospaced", 'sigil' => 'diffusion-source', ), $rows); $corpus = phutil_tag( 'div', array( 'style' => 'padding: 0 2em;', 'id' => $id, ), $corpus_table); break; } return $corpus; } private function renderViewSelectPanel($selected) { $toggle_blame = array( 'highlighted' => 'blame', 'blame' => 'highlighted', 'plain' => 'plainblame', 'plainblame' => 'plain', 'raw' => 'raw', // not a real case. ); $toggle_highlight = array( 'highlighted' => 'plain', 'blame' => 'plainblame', 'plain' => 'highlighted', 'plainblame' => 'blame', 'raw' => 'raw', // not a real case. ); $user = $this->getRequest()->getUser(); $base_uri = $this->getRequest()->getRequestURI(); $blame_on = ($selected == 'blame' || $selected == 'plainblame'); if ($blame_on) { $blame_text = pht('Disable Blame'); } else { $blame_text = pht('Enable Blame'); } $blame_button = $this->createViewAction( $blame_text, $base_uri->alter('view', $toggle_blame[$selected]), $user); $highlight_on = ($selected == 'blame' || $selected == 'highlighted'); if ($highlight_on) { $highlight_text = pht('Disable Highlighting'); } else { $highlight_text = pht('Enable Highlighting'); } $highlight_button = $this->createViewAction( $highlight_text, $base_uri->alter('view', $toggle_highlight[$selected]), $user); $href = null; if ($this->getRequest()->getStr('lint') !== null) { $lint_text = pht('Hide %d Lint Message(s)', count($this->lintMessages)); $href = $base_uri->alter('lint', null); } else if ($this->lintCommit === null) { $lint_text = pht('Lint not Available'); } else { $lint_text = pht( 'Show %d Lint Message(s)', count($this->lintMessages)); $href = $this->getDiffusionRequest()->generateURI(array( 'action' => 'browse', 'commit' => $this->lintCommit, ))->alter('lint', ''); } $lint_button = $this->createViewAction( $lint_text, $href, $user); if (!$href) { $lint_button->setDisabled(true); } $raw_button = $this->createViewAction( pht('View Raw File'), $base_uri->alter('view', 'raw'), $user, 'file'); $edit_button = $this->createEditAction(); return id(new PhabricatorActionListView()) ->setUser($user) ->addAction($blame_button) ->addAction($highlight_button) ->addAction($lint_button) ->addAction($raw_button) ->addAction($edit_button); } private function createViewAction( $localized_text, $href, $user, $icon = null) { return id(new PhabricatorActionView()) ->setName($localized_text) ->setIcon($icon) ->setUser($user) ->setRenderAsForm(true) ->setHref($href); } private function createEditAction() { $request = $this->getRequest(); $user = $request->getUser(); $drequest = $this->getDiffusionRequest(); $repository = $drequest->getRepository(); $path = $drequest->getPath(); $line = nonempty((int)$drequest->getLine(), 1); $callsign = $repository->getCallsign(); $editor_link = $user->loadEditorLink($path, $line, $callsign); $action = id(new PhabricatorActionView()) ->setName(pht('Open in Editor')) ->setIcon('edit'); $action->setHref($editor_link); $action->setDisabled(!$editor_link); return $action; } private function buildDisplayRows( array $text_list, array $rev_list, array $blame_dict, $needs_blame, DiffusionRequest $drequest, DiffusionFileContentQuery $file_query, $selected) { if ($blame_dict) { $epoch_list = ipull(ifilter($blame_dict, 'epoch'), 'epoch'); $epoch_min = min($epoch_list); $epoch_max = max($epoch_list); $epoch_range = ($epoch_max - $epoch_min) + 1; } $line_arr = array(); $line_str = $drequest->getLine(); $ranges = explode(',', $line_str); foreach ($ranges as $range) { if (strpos($range, '-') !== false) { list($min, $max) = explode('-', $range, 2); $line_arr[] = array( 'min' => min($min, $max), 'max' => max($min, $max), ); } else if (strlen($range)) { $line_arr[] = array( 'min' => $range, 'max' => $range, ); } } $display = array(); $line_number = 1; $last_rev = null; $color = null; foreach ($text_list as $k => $line) { $display_line = array( 'color' => null, 'epoch' => null, 'commit' => null, 'author' => null, 'target' => null, 'highlighted' => null, 'line' => $line_number, 'data' => $line, ); if ($needs_blame) { // If the line's rev is same as the line above, show empty content // with same color; otherwise generate blame info. The newer a change // is, the more saturated the color. $rev = idx($rev_list, $k, $last_rev); if ($last_rev == $rev) { $display_line['color'] = $color; } else { $blame = $blame_dict[$rev]; if (!isset($blame['epoch'])) { $color = '#ffd'; // Render as warning. } else { $color_ratio = ($blame['epoch'] - $epoch_min) / $epoch_range; $color_value = 0xF6 * (1.0 - $color_ratio); $color = sprintf( '#%02x%02x%02x', $color_value, 0xF6, $color_value); } $display_line['epoch'] = idx($blame, 'epoch'); $display_line['color'] = $color; $display_line['commit'] = $rev; if (isset($blame['handle'])) { $author_link = $blame['handle']->renderLink(); } else { $author_link = phutil_tag( 'span', array( ), $blame['author']); } $display_line['author'] = $author_link; $last_rev = $rev; } } if ($line_arr) { if ($line_number == $line_arr[0]['min']) { $display_line['target'] = true; } foreach ($line_arr as $range) { if ($line_number >= $range['min'] && $line_number <= $range['max']) { $display_line['highlighted'] = true; } } } $display[] = $display_line; ++$line_number; } $commits = array_filter(ipull($display, 'commit')); if ($commits) { $commits = id(new PhabricatorAuditCommitQuery()) ->withIdentifiers($drequest->getRepository()->getID(), $commits) ->needCommitData(true) ->execute(); $commits = mpull($commits, null, 'getCommitIdentifier'); } $revision_ids = id(new DifferentialRevision()) ->loadIDsByCommitPHIDs(mpull($commits, 'getPHID')); $revisions = array(); if ($revision_ids) { $revisions = id(new DifferentialRevision())->loadAllWhere( 'id IN (%Ld)', $revision_ids); } $request = $this->getRequest(); $user = $request->getUser(); Javelin::initBehavior('phabricator-oncopy', array()); $engine = null; $inlines = array(); if ($this->getRequest()->getStr('lint') !== null && $this->lintMessages) { $engine = new PhabricatorMarkupEngine(); $engine->setViewer($user); foreach ($this->lintMessages as $message) { $inline = id(new PhabricatorAuditInlineComment()) ->setID($message['id']) ->setSyntheticAuthor( ArcanistLintSeverity::getStringForSeverity($message['severity']). ' '.$message['code'].' ('.$message['name'].')') ->setLineNumber($message['line']) ->setContent($message['description']); $inlines[$message['line']][] = $inline; $engine->addObject( $inline, PhabricatorInlineCommentInterface::MARKUP_FIELD_BODY); } $engine->process(); require_celerity_resource('differential-changeset-view-css'); } $rows = $this->renderInlines( idx($inlines, 0, array()), $needs_blame, $engine); foreach ($display as $line) { $line_href = $drequest->generateURI( array( 'action' => 'browse', 'line' => $line['line'], 'stable' => true, )); $blame = array(); if ($line['color']) { $color = $line['color']; $before_link = null; $commit_link = null; $revision_link = null; if (idx($line, 'commit')) { $commit = $line['commit']; $summary = 'Unknown'; if (idx($commits, $commit)) { $summary = $commits[$commit]->getCommitData()->getSummary(); } $tooltip = phabricator_date( $line['epoch'], $user)." \xC2\xB7 ".$summary; Javelin::initBehavior('phabricator-tooltips', array()); require_celerity_resource('aphront-tooltip-css'); $commit_link = javelin_tag( 'a', array( 'href' => $drequest->generateURI( array( 'action' => 'commit', 'commit' => $line['commit'], )), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tooltip, 'align' => 'E', 'size' => 600, ), ), phutil_utf8_shorten($line['commit'], 9, '')); $revision_id = null; if (idx($commits, $commit)) { $revision_id = idx($revision_ids, $commits[$commit]->getPHID()); } if ($revision_id) { $revision = idx($revisions, $revision_id); if (!$revision) { $tooltip = '(Invalid revision)'; } else { $tooltip = phabricator_date($revision->getDateModified(), $user). " \xC2\xB7 ". $revision->getTitle(); } $revision_link = javelin_tag( 'a', array( 'href' => '/D'.$revision_id, 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => $tooltip, 'align' => 'E', 'size' => 600, ), ), 'D'.$revision_id); } $uri = $line_href->alter('before', $commit); $before_link = javelin_tag( 'a', array( 'href' => $uri->setQueryParam('view', 'blame'), 'sigil' => 'has-tooltip', 'meta' => array( 'tip' => 'Skip Past This Commit', 'align' => 'E', 'size' => 300, ), ), "\xC2\xAB"); } $blame[] = phutil_tag( 'th', array( 'class' => 'diffusion-blame-link', 'style' => 'background: '.$color, ), $before_link); $blame[] = phutil_tag( 'th', array( 'class' => 'diffusion-rev-link', 'style' => 'background: '.$color, ), $commit_link); $blame[] = phutil_tag( 'th', array( 'class' => 'diffusion-rev-link', 'style' => 'background: '.$color, ), $revision_link); $blame[] = phutil_tag( 'th', array( 'class' => 'diffusion-author-link', 'style' => 'background: '.$color, ), idx($line, 'author')); } $line_link = phutil_tag( 'a', array( 'href' => $line_href, ), $line['line']); $blame[] = javelin_tag( 'th', array( 'class' => 'diffusion-line-link', 'sigil' => 'diffusion-line-link', 'style' => isset($color) ? 'background: '.$color : null, ), $line_link); Javelin::initBehavior('diffusion-line-linker'); if ($line['target']) { Javelin::initBehavior( 'diffusion-jump-to', array( 'target' => 'scroll_target', )); $anchor_text = phutil_tag( 'a', array( 'id' => 'scroll_target', ), ''); } else { $anchor_text = null; } $blame[] = phutil_tag( 'td', array( ), array( $anchor_text, // NOTE: See phabricator-oncopy behavior. "\xE2\x80\x8B", // TODO: [HTML] Not ideal. phutil_safe_html($line['data']), )); $rows[] = phutil_tag( 'tr', array( 'class' => ($line['highlighted'] ? 'highlighted' : null), ), $blame); $rows = array_merge($rows, $this->renderInlines( idx($inlines, $line['line'], array()), $needs_blame, $engine)); } return $rows; } private function renderInlines(array $inlines, $needs_blame, $engine) { $rows = array(); foreach ($inlines as $inline) { $inline_view = id(new DifferentialInlineCommentView()) ->setMarkupEngine($engine) ->setInlineComment($inline) ->render(); $row = array_fill(0, ($needs_blame ? 5 : 1), phutil_tag('th')); $row[] = phutil_tag('td', array(), $inline_view); $rows[] = phutil_tag('tr', array('class' => 'inline'), $row); } return $rows; } private function loadFileForData($path, $data) { return PhabricatorFile::buildFromFileDataOrHash( $data, array( 'name' => basename($path), + 'ttl' => time() + 60 * 60 * 24, )); } private function buildRawResponse($path, $data) { $file = $this->loadFileForData($path, $data); return id(new AphrontRedirectResponse())->setURI($file->getBestURI()); } private function buildImageCorpus($file_uri) { $properties = new PhabricatorPropertyListView(); $properties->addProperty( pht('Image'), phutil_tag( 'img', array( 'src' => $file_uri, ))); $actions = id(new PhabricatorActionListView()) ->setUser($this->getRequest()->getUser()) ->addAction($this->createEditAction()); return array($actions, $properties); } private function buildBinaryCorpus($file_uri, $data) { $properties = new PhabricatorPropertyListView(); $size = strlen($data); $properties->addTextContent( pht( 'This is a binary file. It is %s byte(s) in length.', new PhutilNumber($size))); $actions = id(new PhabricatorActionListView()) ->setUser($this->getRequest()->getUser()) ->addAction($this->createEditAction()) ->addAction( id(new PhabricatorActionView()) ->setName(pht('Download Binary File...')) ->setIcon('download') ->setHref($file_uri)); return array($actions, $properties); } private function buildBeforeResponse($before) { $request = $this->getRequest(); $drequest = $this->getDiffusionRequest(); // NOTE: We need to get the grandparent so we can capture filename changes // in the parent. $parent = $this->loadParentRevisionOf($before); $old_filename = null; $was_created = false; if ($parent) { $grandparent = $this->loadParentRevisionOf( $parent->getCommitIdentifier()); if ($grandparent) { $rename_query = new DiffusionRenameHistoryQuery(); $rename_query->setRequest($drequest); $rename_query->setOldCommit($grandparent->getCommitIdentifier()); $old_filename = $rename_query->loadOldFilename(); $was_created = $rename_query->getWasCreated(); } } $follow = null; if ($was_created) { // If the file was created in history, that means older commits won't // have it. Since we know it existed at 'before', it must have been // created then; jump there. $target_commit = $before; $follow = 'created'; } else if ($parent) { // If we found a parent, jump to it. This is the normal case. $target_commit = $parent->getCommitIdentifier(); } else { // If there's no parent, this was probably created in the initial commit? // And the "was_created" check will fail because we can't identify the // grandparent. Keep the user at 'before'. $target_commit = $before; $follow = 'first'; } $path = $drequest->getPath(); $renamed = null; if ($old_filename !== null && $old_filename !== '/'.$path) { $renamed = $path; $path = $old_filename; } $line = null; // If there's a follow error, drop the line so the user sees the message. if (!$follow) { $line = $this->getBeforeLineNumber($target_commit); } $before_uri = $drequest->generateURI( array( 'action' => 'browse', 'commit' => $target_commit, 'line' => $line, 'path' => $path, )); $before_uri->setQueryParams($request->getRequestURI()->getQueryParams()); $before_uri = $before_uri->alter('before', null); $before_uri = $before_uri->alter('renamed', $renamed); $before_uri = $before_uri->alter('follow', $follow); return id(new AphrontRedirectResponse())->setURI($before_uri); } private function getBeforeLineNumber($target_commit) { $drequest = $this->getDiffusionRequest(); $line = $drequest->getLine(); if (!$line) { return null; } $diff_query = DiffusionRawDiffQuery::newFromDiffusionRequest($drequest); $diff_query->setAgainstCommit($target_commit); try { $raw_diff = $diff_query->loadRawDiff(); $old_line = 0; $new_line = 0; foreach (explode("\n", $raw_diff) as $text) { if ($text[0] == '-' || $text[0] == ' ') { $old_line++; } if ($text[0] == '+' || $text[0] == ' ') { $new_line++; } if ($new_line == $line) { return $old_line; } } // We didn't find the target line. return $line; } catch (Exception $ex) { return $line; } } private function loadParentRevisionOf($commit) { $drequest = $this->getDiffusionRequest(); $before_req = DiffusionRequest::newFromDictionary( array( 'repository' => $drequest->getRepository(), 'commit' => $commit, )); $query = DiffusionCommitParentsQuery::newFromDiffusionRequest($before_req); $parents = $query->loadParents(); return head($parents); } } diff --git a/src/applications/files/PhabricatorImageTransformer.php b/src/applications/files/PhabricatorImageTransformer.php index 80ff562bdb..c3a10e4816 100644 --- a/src/applications/files/PhabricatorImageTransformer.php +++ b/src/applications/files/PhabricatorImageTransformer.php @@ -1,419 +1,420 @@ applyMemeTo($file, $upper_text, $lower_text); return PhabricatorFile::newFromFileData( $image, array( 'name' => 'meme-'.$file->getName(), + 'ttl' => time() + 60 * 60 * 24, )); } public function executeThumbTransform( PhabricatorFile $file, $x, $y) { $image = $this->crudelyScaleTo($file, $x, $y); return PhabricatorFile::newFromFileData( $image, array( 'name' => 'thumb-'.$file->getName(), )); } public function executeProfileTransform( PhabricatorFile $file, $x, $min_y, $max_y) { $image = $this->crudelyCropTo($file, $x, $min_y, $max_y); return PhabricatorFile::newFromFileData( $image, array( 'name' => 'profile-'.$file->getName(), )); } public function executePreviewTransform( PhabricatorFile $file, $size) { $image = $this->generatePreview($file, $size); return PhabricatorFile::newFromFileData( $image, array( 'name' => 'preview-'.$file->getName(), )); } public function executeConpherenceTransform( PhabricatorFile $file, $top, $left, $width, $height) { $image = $this->crasslyCropTo( $file, $top, $left, $width, $height); return PhabricatorFile::newFromFileData( $image, array( 'name' => 'conpherence-'.$file->getName(), )); } private function crudelyCropTo(PhabricatorFile $file, $x, $min_y, $max_y) { $data = $file->loadFileData(); $img = imagecreatefromstring($data); $sx = imagesx($img); $sy = imagesy($img); $scaled_y = ($x / $sx) * $sy; if ($scaled_y > $max_y) { // This image is very tall and thin. $scaled_y = $max_y; } else if ($scaled_y < $min_y) { // This image is very short and wide. $scaled_y = $min_y; } $cropped = $this->applyScaleWithImagemagick($file, $x, $scaled_y); if ($cropped != null) { return $cropped; } $img = $this->applyScaleTo( $file, $x, $scaled_y); return $this->saveImageDataInAnyFormat($img, $file->getMimeType()); } private function crasslyCropTo(PhabricatorFile $file, $top, $left, $w, $h) { $data = $file->loadFileData(); $src = imagecreatefromstring($data); $dst = $this->getBlankDestinationFile($w, $h); $scale = self::getScaleForCrop($file, $w, $h); $orig_x = $left / $scale; $orig_y = $top / $scale; $orig_w = $w / $scale; $orig_h = $h / $scale; imagecopyresampled( $dst, $src, 0, 0, $orig_x, $orig_y, $w, $h, $orig_w, $orig_h); return $this->saveImageDataInAnyFormat($dst, $file->getMimeType()); } /** * Very crudely scale an image up or down to an exact size. */ private function crudelyScaleTo(PhabricatorFile $file, $dx, $dy) { $scaled = $this->applyScaleWithImagemagick($file, $dx, $dy); if ($scaled != null) { return $scaled; } $dst = $this->applyScaleTo($file, $dx, $dy); return $this->saveImageDataInAnyFormat($dst, $file->getMimeType()); } private function getBlankDestinationFile($dx, $dy) { $dst = imagecreatetruecolor($dx, $dy); imagesavealpha($dst, true); imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127)); return $dst; } private function applyScaleTo(PhabricatorFile $file, $dx, $dy) { $data = $file->loadFileData(); $src = imagecreatefromstring($data); $x = imagesx($src); $y = imagesy($src); $scale = min(($dx / $x), ($dy / $y), 1); $sdx = $scale * $x; $sdy = $scale * $y; $dst = $this->getBlankDestinationFile($dx, $dy); imagesavealpha($dst, true); imagefill($dst, 0, 0, imagecolorallocatealpha($dst, 255, 255, 255, 127)); imagecopyresampled( $dst, $src, ($dx - $sdx) / 2, ($dy - $sdy) / 2, 0, 0, $sdx, $sdy, $x, $y); return $dst; } public static function getPreviewDimensions(PhabricatorFile $file, $size) { $data = $file->loadFileData(); $src = imagecreatefromstring($data); $x = imagesx($src); $y = imagesy($src); $scale = min($size / $x, $size / $y, 1); $dx = max($size / 4, $scale * $x); $dy = max($size / 4, $scale * $y); $sdx = $scale * $x; $sdy = $scale * $y; return array( 'x' => $x, 'y' => $y, 'dx' => $dx, 'dy' => $dy, 'sdx' => $sdx, 'sdy' => $sdy ); } public static function getScaleForCrop( PhabricatorFile $file, $des_width, $des_height) { $metadata = $file->getMetadata(); $width = $metadata[PhabricatorFile::METADATA_IMAGE_WIDTH]; $height = $metadata[PhabricatorFile::METADATA_IMAGE_HEIGHT]; if ($height < $des_height) { $scale = $height / $des_height; } else if ($width < $des_width) { $scale = $width / $des_width; } else { $scale_x = $des_width / $width; $scale_y = $des_height / $height; $scale = max($scale_x, $scale_y); } return $scale; } private function generatePreview(PhabricatorFile $file, $size) { $data = $file->loadFileData(); $src = imagecreatefromstring($data); $dimensions = self::getPreviewDimensions($file, $size); $x = $dimensions['x']; $y = $dimensions['y']; $dx = $dimensions['dx']; $dy = $dimensions['dy']; $sdx = $dimensions['sdx']; $sdy = $dimensions['sdy']; $dst = $this->getBlankDestinationFile($dx, $dy); imagecopyresampled( $dst, $src, ($dx - $sdx) / 2, ($dy - $sdy) / 2, 0, 0, $sdx, $sdy, $x, $y); return $this->saveImageDataInAnyFormat($dst, $file->getMimeType()); } private function applyMemeTo( PhabricatorFile $file, $upper_text, $lower_text) { $data = $file->loadFileData(); $img = imagecreatefromstring($data); $phabricator_root = dirname(phutil_get_library_root('phabricator')); $font_root = $phabricator_root.'/resources/font/'; $font_path = $font_root.'tuffy.ttf'; if (Filesystem::pathExists($font_root.'impact.ttf')) { $font_path = $font_root.'impact.ttf'; } $text_color = imagecolorallocate($img, 255, 255, 255); $border_color = imagecolorallocatealpha($img, 0, 0, 0, 110); $border_width = 4; $font_max = 200; $font_min = 5; for ($i = $font_max; $i > $font_min; $i--) { $fit = $this->doesTextBoundingBoxFitInImage( $img, $upper_text, $i, $font_path); if ($fit['doesfit']) { $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2; $y = $fit['txtheight'] + 10; $this->makeImageWithTextBorder($img, $i, $x, $y, $text_color, $border_color, $border_width, $font_path, $upper_text); break; } } for ($i = $font_max; $i > $font_min; $i--) { $fit = $this->doesTextBoundingBoxFitInImage($img, $lower_text, $i, $font_path); if ($fit['doesfit']) { $x = ($fit['imgwidth'] - $fit['txtwidth']) / 2; $y = $fit['imgheight'] - 10; $this->makeImageWithTextBorder( $img, $i, $x, $y, $text_color, $border_color, $border_width, $font_path, $lower_text); break; } } return $this->saveImageDataInAnyFormat($img, $file->getMimeType()); } private function makeImageWithTextBorder($img, $font_size, $x, $y, $color, $stroke_color, $bw, $font, $text) { $angle = 0; $bw = abs($bw); for ($c1 = $x - $bw; $c1 <= $x + $bw; $c1++) { for ($c2 = $y - $bw; $c2 <= $y + $bw; $c2++) { if (!(($c1 == $x - $bw || $x + $bw) && $c2 == $y - $bw || $c2 == $y + $bw)) { $bg = imagettftext($img, $font_size, $angle, $c1, $c2, $stroke_color, $font, $text); } } } imagettftext($img, $font_size, $angle, $x , $y, $color , $font, $text); } private function doesTextBoundingBoxFitInImage($img, $text, $font_size, $font_path) { // Default Angle = 0 $angle = 0; $bbox = imagettfbbox($font_size, $angle, $font_path, $text); $text_height = abs($bbox[3] - $bbox[5]); $text_width = abs($bbox[0] - $bbox[2]); return array( "doesfit" => ($text_height * 1.05 <= imagesy($img) / 2 && $text_width * 1.05 <= imagesx($img)), "txtwidth" => $text_width, "txtheight" => $text_height, "imgwidth" => imagesx($img), "imgheight" => imagesy($img), ); } private function saveImageDataInAnyFormat($data, $preferred_mime = '') { switch ($preferred_mime) { case 'image/gif': // Gif doesn't support true color case 'image/png': if (function_exists('imagepng')) { ob_start(); imagepng($data); return ob_get_clean(); } break; } $img = null; if (function_exists('imagejpeg')) { ob_start(); imagejpeg($data); $img = ob_get_clean(); } else if (function_exists('imagepng')) { ob_start(); imagepng($data); $img = ob_get_clean(); } else if (function_exists('imagegif')) { ob_start(); imagegif($data); $img = ob_get_clean(); } else { throw new Exception("No image generation functions exist!"); } return $img; } private function applyScaleWithImagemagick(PhabricatorFile $file, $dx, $dy) { $img_type = $file->getMimeType(); $imagemagick = PhabricatorEnv::getEnvConfig('files.enable-imagemagick'); if ($img_type != 'image/gif' || $imagemagick == false) { return null; } $data = $file->loadFileData(); $src = imagecreatefromstring($data); $x = imagesx($src); $y = imagesy($src); $scale = min(($dx / $x), ($dy / $y), 1); $sdx = $scale * $x; $sdy = $scale * $y; $input = new TempFile(); Filesystem::writeFile($input, $data); $resized = new TempFile(); list($err) = exec_manual( 'convert %s -coalesce -resize %sX%s\! %s' , $input, $sdx, $sdy, $resized); if (!$err) { $new_data = Filesystem::readFile($resized); return $new_data; } else { return null; } } } diff --git a/src/applications/files/controller/PhabricatorFileInfoController.php b/src/applications/files/controller/PhabricatorFileInfoController.php index faeac9c154..fe031222b7 100644 --- a/src/applications/files/controller/PhabricatorFileInfoController.php +++ b/src/applications/files/controller/PhabricatorFileInfoController.php @@ -1,165 +1,173 @@ phid = $data['phid']; } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $file = id(new PhabricatorFileQuery()) ->setViewer($user) ->withPHIDs(array($this->phid)) ->executeOne(); if (!$file) { return new Aphront404Response(); } $this->loadHandles(array($file->getAuthorPHID())); $phid = $file->getPHID(); $crumbs = $this->buildApplicationCrumbs(); $crumbs->addCrumb( id(new PhabricatorCrumbView()) ->setName('F'.$file->getID()) ->setHref($this->getApplicationURI("/info/{$phid}/"))); $header = id(new PhabricatorHeaderView()) ->setObjectName('F'.$file->getID()) ->setHeader($file->getName()); + $ttl = $file->getTTL(); + if ($ttl !== null) { + $ttl_tag = id(new PhabricatorTagView()) + ->setType(PhabricatorTagView::TYPE_OBJECT) + ->setName(pht("Temporary")); + $header->addTag($ttl_tag); + } + $actions = $this->buildActionView($file); $properties = $this->buildPropertyView($file); return $this->buildApplicationPage( array( $crumbs, $header, $actions, $properties, ), array( 'title' => $file->getName(), 'device' => true, )); } private function buildActionView(PhabricatorFile $file) { $request = $this->getRequest(); $user = $request->getUser(); $id = $file->getID(); $view = id(new PhabricatorActionListView()) ->setUser($user) ->setObject($file); if ($file->isViewableInBrowser()) { $view->addAction( id(new PhabricatorActionView()) ->setName(pht('View File')) ->setIcon('preview') ->setHref($file->getViewURI())); } else { $view->addAction( id(new PhabricatorActionView()) ->setUser($user) ->setRenderAsForm(true) ->setDownload(true) ->setName(pht('Download File')) ->setIcon('download') ->setHref($file->getViewURI())); } $view->addAction( id(new PhabricatorActionView()) ->setName(pht('Delete File')) ->setIcon('delete') ->setHref($this->getApplicationURI("/delete/{$id}/")) ->setWorkflow(true)); return $view; } private function buildPropertyView(PhabricatorFile $file) { $request = $this->getRequest(); $user = $request->getUser(); $view = id(new PhabricatorPropertyListView()); if ($file->getAuthorPHID()) { $view->addProperty( pht('Author'), $this->getHandle($file->getAuthorPHID())->renderLink()); } $view->addProperty( pht('Created'), phabricator_datetime($file->getDateCreated(), $user)); $view->addProperty( pht('Size'), phabricator_format_bytes($file->getByteSize())); $view->addSectionHeader(pht('Technical Details')); $view->addProperty( pht('Mime Type'), $file->getMimeType()); $view->addProperty( pht('Engine'), $file->getStorageEngine()); $view->addProperty( pht('Format'), $file->getStorageFormat()); $view->addProperty( pht('Handle'), $file->getStorageHandle()); $metadata = $file->getMetadata(); if (!empty($metadata)) { $view->addSectionHeader(pht('Metadata')); foreach ($metadata as $key => $value) { $view->addProperty( PhabricatorFile::getMetadataName($key), $value); } } if ($file->isViewableImage()) { // TODO: Clean this up after Pholio (dark backgrounds, standardization, // etc.) $image = phutil_tag( 'img', array( 'src' => $file->getViewURI(), 'class' => 'phabricator-property-list-image', )); $linked_image = phutil_tag( 'a', array( 'href' => $file->getViewURI(), ), $image); $view->addTextContent($linked_image); } return $view; } } diff --git a/src/applications/files/controller/PhabricatorFileListController.php b/src/applications/files/controller/PhabricatorFileListController.php index 5e31eb3554..a2dd69e08d 100644 --- a/src/applications/files/controller/PhabricatorFileListController.php +++ b/src/applications/files/controller/PhabricatorFileListController.php @@ -1,123 +1,126 @@ filter = $filter; return $this; } private function getFilter() { return $this->filter; } public function willProcessRequest(array $data) { $this->setFilter(idx($data, 'filter', 'my')); } public function processRequest() { $request = $this->getRequest(); $user = $request->getUser(); $pager = id(new AphrontCursorPagerView()) ->readFromRequest($request); $query = id(new PhabricatorFileQuery()) ->setViewer($user); switch ($this->getFilter()) { case 'my': $query->withAuthorPHIDs(array($user->getPHID())); $header = pht('Files You Uploaded'); break; case 'all': default: $header = pht('All Files'); break; } $files = $query->executeWithCursorPager($pager); $this->loadHandles(mpull($files, 'getAuthorPHID')); $highlighted = $request->getStrList('h'); $file_list = $this->buildFileList($files, $highlighted); $side_nav = $this->buildSideNavView(); $side_nav->selectFilter($this->getFilter()); $header_view = id(new PhabricatorHeaderView()) ->setHeader($header); $side_nav->appendChild( array( $header_view, $file_list, $pager, new PhabricatorGlobalUploadTargetView(), )); $side_nav->setCrumbs( $this ->buildApplicationCrumbs() ->addCrumb( id(new PhabricatorCrumbView()) ->setName($header) ->setHref($request->getRequestURI()))); return $this->buildApplicationPage( $side_nav, array( 'title' => 'Files', 'device' => true, )); } private function buildFileList(array $files, array $highlighted_ids) { assert_instances_of($files, 'PhabricatorFile'); $request = $this->getRequest(); $user = $request->getUser(); $highlighted_ids = array_fill_keys($highlighted_ids, true); $list_view = id(new PhabricatorObjectItemListView()) ->setUser($user); foreach ($files as $file) { $id = $file->getID(); $phid = $file->getPHID(); $name = $file->getName(); - $file_name = "F{$id} {$name}"; $file_uri = $this->getApplicationURI("/info/{$phid}/"); $date_created = phabricator_date($file->getDateCreated(), $user); - $author_phid = $file->getAuthorPHID(); if ($author_phid) { $author_link = $this->getHandle($author_phid)->renderLink(); $uploaded = pht('Uploaded by %s on %s', $author_link, $date_created); } else { $uploaded = pht('Uploaded on %s', $date_created); } $item = id(new PhabricatorObjectItemView()) ->setObject($file) ->setHeader($file_name) ->setHref($file_uri) ->addAttribute($uploaded) ->addIcon('none', phabricator_format_bytes($file->getByteSize())); + $ttl = $file->getTTL(); + if ($ttl !== null) { + $item->addIcon('blame', pht('Temporary')); + } + if (isset($highlighted_ids[$id])) { $item->setEffect('highlighted'); } $list_view->addItem($item); } return $list_view; } } diff --git a/src/applications/files/storage/PhabricatorFile.php b/src/applications/files/storage/PhabricatorFile.php index d74f995b42..fee670dd66 100644 --- a/src/applications/files/storage/PhabricatorFile.php +++ b/src/applications/files/storage/PhabricatorFile.php @@ -1,633 +1,647 @@ true, self::CONFIG_SERIALIZATION => array( 'metadata' => self::SERIALIZATION_JSON, ), ) + parent::getConfiguration(); } public function generatePHID() { return PhabricatorPHID::generateNewPHID( PhabricatorPHIDConstants::PHID_TYPE_FILE); } public static function readUploadedFileData($spec) { if (!$spec) { throw new Exception("No file was uploaded!"); } $err = idx($spec, 'error'); if ($err) { throw new PhabricatorFileUploadException($err); } $tmp_name = idx($spec, 'tmp_name'); $is_valid = @is_uploaded_file($tmp_name); if (!$is_valid) { throw new Exception("File is not an uploaded file."); } $file_data = Filesystem::readFile($tmp_name); $file_size = idx($spec, 'size'); if (strlen($file_data) != $file_size) { throw new Exception("File size disagrees with uploaded size."); } self::validateFileSize(strlen($file_data)); return $file_data; } public static function newFromPHPUpload($spec, array $params = array()) { $file_data = self::readUploadedFileData($spec); $file_name = nonempty( idx($params, 'name'), idx($spec, 'name')); $params = array( 'name' => $file_name, ) + $params; return self::newFromFileData($file_data, $params); } public static function newFromXHRUpload($data, array $params = array()) { self::validateFileSize(strlen($data)); return self::newFromFileData($data, $params); } private static function validateFileSize($size) { $limit = PhabricatorEnv::getEnvConfig('storage.upload-size-limit'); if (!$limit) { return; } $limit = phabricator_parse_bytes($limit); if ($size > $limit) { throw new PhabricatorFileUploadException(-1000); } } /** * Given a block of data, try to load an existing file with the same content * if one exists. If it does not, build a new file. * * This method is generally used when we have some piece of semi-trusted data * like a diff or a file from a repository that we want to show to the user. * We can't just dump it out because it may be dangerous for any number of * reasons; instead, we need to serve it through the File abstraction so it * ends up on the CDN domain if one is configured and so on. However, if we * simply wrote a new file every time we'd potentially end up with a lot * of redundant data in file storage. * * To solve these problems, we use file storage as a cache and reuse the * same file again if we've previously written it. * * NOTE: This method unguards writes. * * @param string Raw file data. * @param dict Dictionary of file information. */ public static function buildFromFileDataOrHash( $data, array $params = array()) { $file = id(new PhabricatorFile())->loadOneWhere( 'name = %s AND contentHash = %s LIMIT 1', self::normalizeFileName(idx($params, 'name')), self::hashFileContent($data)); if (!$file) { $unguarded = AphrontWriteGuard::beginScopedUnguardedWrites(); $file = PhabricatorFile::newFromFileData($data, $params); unset($unguarded); } return $file; } public static function newFileFromContentHash($hash, $params) { // Check to see if a file with same contentHash exist $file = id(new PhabricatorFile())->loadOneWhere( 'contentHash = %s LIMIT 1', $hash); if ($file) { // copy storageEngine, storageHandle, storageFormat $copy_of_storage_engine = $file->getStorageEngine(); $copy_of_storage_handle = $file->getStorageHandle(); $copy_of_storage_format = $file->getStorageFormat(); $copy_of_byteSize = $file->getByteSize(); $copy_of_mimeType = $file->getMimeType(); $file_name = idx($params, 'name'); $file_name = self::normalizeFileName($file_name); + $file_ttl = idx($params, 'ttl'); $authorPHID = idx($params, 'authorPHID'); $new_file = new PhabricatorFile(); $new_file->setName($file_name); $new_file->setByteSize($copy_of_byteSize); $new_file->setAuthorPHID($authorPHID); + $new_file->setTtl($file_ttl); $new_file->setContentHash($hash); $new_file->setStorageEngine($copy_of_storage_engine); $new_file->setStorageHandle($copy_of_storage_handle); $new_file->setStorageFormat($copy_of_storage_format); $new_file->setMimeType($copy_of_mimeType); $new_file->save(); return $new_file; } return $file; } private static function buildFromFileData($data, array $params = array()) { $selector = PhabricatorEnv::newObjectFromConfig('storage.engine-selector'); if (isset($params['storageEngines'])) { $engines = $params['storageEngines']; } else { $selector = PhabricatorEnv::newObjectFromConfig( 'storage.engine-selector'); $engines = $selector->selectStorageEngines($data, $params); } assert_instances_of($engines, 'PhabricatorFileStorageEngine'); if (!$engines) { throw new Exception("No valid storage engines are available!"); } $file = new PhabricatorFile(); $data_handle = null; $engine_identifier = null; $exceptions = array(); foreach ($engines as $engine) { $engine_class = get_class($engine); try { list($engine_identifier, $data_handle) = $file->writeToEngine( $engine, $data, $params); // We stored the file somewhere so stop trying to write it to other // places. break; } catch (PhabricatorFileStorageConfigurationException $ex) { // If an engine is outright misconfigured (or misimplemented), raise // that immediately since it probably needs attention. throw $ex; } catch (Exception $ex) { phlog($ex); // If an engine doesn't work, keep trying all the other valid engines // in case something else works. $exceptions[$engine_class] = $ex; } } if (!$data_handle) { throw new PhutilAggregateException( "All storage engines failed to write file:", $exceptions); } $file_name = idx($params, 'name'); $file_name = self::normalizeFileName($file_name); + $file_ttl = idx($params, 'ttl'); // If for whatever reason, authorPHID isn't passed as a param // (always the case with newFromFileDownload()), store a '' $authorPHID = idx($params, 'authorPHID'); $file->setName($file_name); $file->setByteSize(strlen($data)); $file->setAuthorPHID($authorPHID); + $file->setTtl($file_ttl); $file->setContentHash(self::hashFileContent($data)); $file->setStorageEngine($engine_identifier); $file->setStorageHandle($data_handle); // TODO: This is probably YAGNI, but allows for us to do encryption or // compression later if we want. $file->setStorageFormat(self::STORAGE_FORMAT_RAW); if (isset($params['mime-type'])) { $file->setMimeType($params['mime-type']); } else { $tmp = new TempFile(); Filesystem::writeFile($tmp, $data); $file->setMimeType(Filesystem::getMimeType($tmp)); } try { $file->updateDimensions(false); } catch (Exception $ex) { // Do nothing } $file->save(); return $file; } public static function newFromFileData($data, array $params = array()) { $hash = self::hashFileContent($data); $file = self::newFileFromContentHash($hash, $params); if ($file) { return $file; } return self::buildFromFileData($data, $params); } public function migrateToEngine(PhabricatorFileStorageEngine $engine) { if (!$this->getID() || !$this->getStorageHandle()) { throw new Exception( "You can not migrate a file which hasn't yet been saved."); } $data = $this->loadFileData(); $params = array( 'name' => $this->getName(), ); list($new_identifier, $new_handle) = $this->writeToEngine( $engine, $data, $params); $old_engine = $this->instantiateStorageEngine(); $old_handle = $this->getStorageHandle(); $this->setStorageEngine($new_identifier); $this->setStorageHandle($new_handle); $this->save(); $old_engine->deleteFile($old_handle); return $this; } private function writeToEngine( PhabricatorFileStorageEngine $engine, $data, array $params) { $engine_class = get_class($engine); $data_handle = $engine->writeFile($data, $params); if (!$data_handle || strlen($data_handle) > 255) { // This indicates an improperly implemented storage engine. throw new PhabricatorFileStorageConfigurationException( "Storage engine '{$engine_class}' executed writeFile() but did ". "not return a valid handle ('{$data_handle}') to the data: it ". "must be nonempty and no longer than 255 characters."); } $engine_identifier = $engine->getEngineIdentifier(); if (!$engine_identifier || strlen($engine_identifier) > 32) { throw new PhabricatorFileStorageConfigurationException( "Storage engine '{$engine_class}' returned an improper engine ". "identifier '{$engine_identifier}': it must be nonempty ". "and no longer than 32 characters."); } return array($engine_identifier, $data_handle); } public static function newFromFileDownload($uri, array $params) { $uri = new PhutilURI($uri); $protocol = $uri->getProtocol(); switch ($protocol) { case 'http': case 'https': break; default: // Make sure we are not accessing any file:// URIs or similar. return null; } $timeout = 5; list($file_data) = id(new HTTPSFuture($uri)) ->setTimeout($timeout) ->resolvex(); return self::newFromFileData($file_data, $params); } public static function normalizeFileName($file_name) { return preg_replace('/[^a-zA-Z0-9.~_-]/', '_', $file_name); } public function delete() { + // delete all records of this file in transformedfile + $trans_files = id(new PhabricatorTransformedFile())->loadAllWhere( + 'TransformedPHID = %s', $this->getPHID()); + + $this->openTransaction(); + foreach ($trans_files as $trans_file) { + $trans_file->delete(); + } + $ret = parent::delete(); + $this->saveTransaction(); + // Check to see if other files are using storage $other_file = id(new PhabricatorFile())->loadAllWhere( 'storageEngine = %s AND storageHandle = %s AND storageFormat = %s AND id != %d LIMIT 1', $this->getStorageEngine(), $this->getStorageHandle(), $this->getStorageFormat(), $this->getID()); // If this is the only file using the storage, delete storage if (count($other_file) == 0) { $engine = $this->instantiateStorageEngine(); $engine->deleteFile($this->getStorageHandle()); } - - $ret = parent::delete(); - return $ret; } public static function hashFileContent($data) { return sha1($data); } public function loadFileData() { $engine = $this->instantiateStorageEngine(); $data = $engine->readFile($this->getStorageHandle()); switch ($this->getStorageFormat()) { case self::STORAGE_FORMAT_RAW: $data = $data; break; default: throw new Exception("Unknown storage format."); } return $data; } public function getViewURI() { if (!$this->getPHID()) { throw new Exception( "You must save a file before you can generate a view URI."); } $name = phutil_escape_uri($this->getName()); $path = '/file/data/'.$this->getSecretKey().'/'.$this->getPHID().'/'.$name; return PhabricatorEnv::getCDNURI($path); } public function getInfoURI() { return '/file/info/'.$this->getPHID().'/'; } public function getBestURI() { if ($this->isViewableInBrowser()) { return $this->getViewURI(); } else { return $this->getInfoURI(); } } public function getDownloadURI() { $uri = id(new PhutilURI($this->getViewURI())) ->setQueryParam('download', true); return (string) $uri; } public function getThumb60x45URI() { $path = '/file/xform/thumb-60x45/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function getThumb160x120URI() { $path = '/file/xform/thumb-160x120/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function getPreview220URI() { $path = '/file/xform/preview-220/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function getThumb220x165URI() { $path = '/file/xform/thumb-220x165/'.$this->getPHID().'/' .$this->getSecretKey().'/'; return PhabricatorEnv::getCDNURI($path); } public function isViewableInBrowser() { return ($this->getViewableMimeType() !== null); } public function isViewableImage() { if (!$this->isViewableInBrowser()) { return false; } $mime_map = PhabricatorEnv::getEnvConfig('files.image-mime-types'); $mime_type = $this->getMimeType(); return idx($mime_map, $mime_type); } public function isTransformableImage() { // NOTE: The way the 'gd' extension works in PHP is that you can install it // with support for only some file types, so it might be able to handle // PNG but not JPEG. Try to generate thumbnails for whatever we can. Setup // warns you if you don't have complete support. $matches = null; $ok = preg_match( '@^image/(gif|png|jpe?g)@', $this->getViewableMimeType(), $matches); if (!$ok) { return false; } switch ($matches[1]) { case 'jpg'; case 'jpeg': return function_exists('imagejpeg'); break; case 'png': return function_exists('imagepng'); break; case 'gif': return function_exists('imagegif'); break; default: throw new Exception('Unknown type matched as image MIME type.'); } } public static function getTransformableImageFormats() { $supported = array(); if (function_exists('imagejpeg')) { $supported[] = 'jpg'; } if (function_exists('imagepng')) { $supported[] = 'png'; } if (function_exists('imagegif')) { $supported[] = 'gif'; } return $supported; } protected function instantiateStorageEngine() { return self::buildEngine($this->getStorageEngine()); } public static function buildEngine($engine_identifier) { $engines = self::buildAllEngines(); foreach ($engines as $engine) { if ($engine->getEngineIdentifier() == $engine_identifier) { return $engine; } } throw new Exception( "Storage engine '{$engine_identifier}' could not be located!"); } public static function buildAllEngines() { $engines = id(new PhutilSymbolLoader()) ->setType('class') ->setConcreteOnly(true) ->setAncestorClass('PhabricatorFileStorageEngine') ->selectAndLoadSymbols(); $results = array(); foreach ($engines as $engine_class) { $results[] = newv($engine_class['name'], array()); } return $results; } public function getViewableMimeType() { $mime_map = PhabricatorEnv::getEnvConfig('files.viewable-mime-types'); $mime_type = $this->getMimeType(); $mime_parts = explode(';', $mime_type); $mime_type = trim(reset($mime_parts)); return idx($mime_map, $mime_type); } public function validateSecretKey($key) { return ($key == $this->getSecretKey()); } public function save() { if (!$this->getSecretKey()) { $this->setSecretKey($this->generateSecretKey()); } return parent::save(); } public function generateSecretKey() { return Filesystem::readRandomCharacters(20); } public function updateDimensions($save = true) { if (!$this->isViewableImage()) { throw new Exception( "This file is not a viewable image."); } if (!function_exists("imagecreatefromstring")) { throw new Exception( "Cannot retrieve image information."); } $data = $this->loadFileData(); $img = imagecreatefromstring($data); if ($img === false) { throw new Exception( "Error when decoding image."); } $this->metadata[self::METADATA_IMAGE_WIDTH] = imagesx($img); $this->metadata[self::METADATA_IMAGE_HEIGHT] = imagesy($img); if ($save) { $this->save(); } return $this; } public static function getMetadataName($metadata) { switch ($metadata) { case self::METADATA_IMAGE_WIDTH: $name = pht('Width'); break; case self::METADATA_IMAGE_HEIGHT: $name = pht('Height'); break; default: $name = ucfirst($metadata); break; } return $name; } /* -( PhabricatorPolicyInterface Implementation )-------------------------- */ public function getCapabilities() { return array( PhabricatorPolicyCapability::CAN_VIEW, ); } public function getPolicy($capability) { // TODO: Implement proper per-object policies. return PhabricatorPolicies::POLICY_USER; } public function hasAutomaticCapability($capability, PhabricatorUser $viewer) { return false; } } diff --git a/src/applications/files/storage/__tests__/PhabricatorFileTestCase.php b/src/applications/files/storage/__tests__/PhabricatorFileTestCase.php index f794585080..d2b2112c0f 100644 --- a/src/applications/files/storage/__tests__/PhabricatorFileTestCase.php +++ b/src/applications/files/storage/__tests__/PhabricatorFileTestCase.php @@ -1,129 +1,148 @@ true, ); } public function testFileStorageReadWrite() { $engine = new PhabricatorTestStorageEngine(); $data = Filesystem::readRandomCharacters(64); $params = array( 'name' => 'test.dat', 'storageEngines' => array( $engine, ), ); $file = PhabricatorFile::newFromFileData($data, $params); // Test that the storage engine worked, and was the target of the write. We // don't actually care what the data is (future changes may compress or // encrypt it), just that it exists in the test storage engine. $engine->readFile($file->getStorageHandle()); // Now test that we get the same data back out. $this->assertEqual($data, $file->loadFileData()); } public function testFileStorageUploadDifferentFiles() { $engine = new PhabricatorTestStorageEngine(); $data = Filesystem::readRandomCharacters(64); $other_data = Filesystem::readRandomCharacters(64); $params = array( 'name' => 'test.dat', 'storageEngines' => array( $engine, ), ); $first_file = PhabricatorFile::newFromFileData($data, $params); $second_file = PhabricatorFile::newFromFileData($other_data, $params); // Test that the the second file uses different storage handle from // the first file. $first_handle = $first_file->getStorageHandle(); $second_handle = $second_file->getStorageHandle(); $this->assertEqual(true, ($first_handle != $second_handle)); } public function testFileStorageUploadSameFile() { $engine = new PhabricatorTestStorageEngine(); $data = Filesystem::readRandomCharacters(64); $params = array( 'name' => 'test.dat', 'storageEngines' => array( $engine, ), ); $first_file = PhabricatorFile::newFromFileData($data, $params); $second_file = PhabricatorFile::newFromFileData($data, $params); // Test that the the second file uses the same storage handle as // the first file. $handle = $first_file->getStorageHandle(); $second_handle = $second_file->getStorageHandle(); $this->assertEqual($handle, $second_handle); } public function testFileStorageDelete() { $engine = new PhabricatorTestStorageEngine(); $data = Filesystem::readRandomCharacters(64); $params = array( 'name' => 'test.dat', 'storageEngines' => array( $engine, ), ); $file = PhabricatorFile::newFromFileData($data, $params); $handle = $file->getStorageHandle(); $file->delete(); $caught = null; try { $engine->readFile($handle); } catch (Exception $ex) { $caught = $ex; } $this->assertEqual(true, $caught instanceof Exception); } public function testFileStorageDeleteSharedHandle() { $engine = new PhabricatorTestStorageEngine(); $data = Filesystem::readRandomCharacters(64); $params = array( 'name' => 'test.dat', 'storageEngines' => array( $engine, ), ); $first_file = PhabricatorFile::newFromFileData($data, $params); $second_file = PhabricatorFile::newFromFileData($data, $params); $first_file->delete(); $this->assertEqual($data, $second_file->loadFileData()); } + public function testReadWriteTtlFiles() { + $engine = new PhabricatorTestStorageEngine(); + + $data = Filesystem::readRandomCharacters(64); + + $ttl = (time() + 60 * 60 * 24); + + $params = array( + 'name' => 'test.dat', + 'ttl' => ($ttl), + 'storageEngines' => array( + $engine, + ), + ); + + $file = PhabricatorFile::newFromFileData($data, $params); + $this->assertEqual($ttl, $file->getTTL()); + } + } diff --git a/src/infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php b/src/infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php index f4f678b14c..dc736e8982 100644 --- a/src/infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php +++ b/src/infrastructure/daemon/PhabricatorGarbageCollectorDaemon.php @@ -1,210 +1,222 @@ collectHeraldTranscripts(); $n_daemon = $this->collectDaemonLogs(); $n_parse = $this->collectParseCaches(); $n_markup = $this->collectMarkupCaches(); $n_tasks = $this->collectArchivedTasks(); $n_cache_ttl = $this->collectGeneralCacheTTL(); $n_cache = $this->collectGeneralCaches(); + $n_files = $this->collectExpiredFiles(); $collected = array( 'Herald Transcript' => $n_herald, 'Daemon Log' => $n_daemon, 'Differential Parse Cache' => $n_parse, 'Markup Cache' => $n_markup, 'Archived Tasks' => $n_tasks, 'General Cache TTL' => $n_cache_ttl, 'General Cache Entries' => $n_cache, + 'Temporary Files' => $n_files, ); $collected = array_filter($collected); foreach ($collected as $thing => $count) { $count = number_format($count); $this->log("Garbage collected {$count} '{$thing}' objects."); } $total = array_sum($collected); if ($total < 100) { // We didn't max out any of the GCs so we're basically caught up. Ease // off the GC loop so we don't keep doing table scans just to delete // a handful of rows; wake up in a few hours. $this->sleep(4 * (60 * 60)); } else { $this->stillWorking(); } } while (true); } private function collectHeraldTranscripts() { $ttl = PhabricatorEnv::getEnvConfig('gcdaemon.ttl.herald-transcripts'); if ($ttl <= 0) { return 0; } $table = new HeraldTranscript(); $conn_w = $table->establishConnection('w'); queryfx( $conn_w, 'UPDATE %T SET objectTranscript = "", ruleTranscripts = "", conditionTranscripts = "", applyTranscripts = "", garbageCollected = 1 WHERE garbageCollected = 0 AND `time` < %d LIMIT 100', $table->getTableName(), time() - $ttl); return $conn_w->getAffectedRows(); } private function collectDaemonLogs() { $ttl = PhabricatorEnv::getEnvConfig('gcdaemon.ttl.daemon-logs'); if ($ttl <= 0) { return 0; } $table = new PhabricatorDaemonLogEvent(); $conn_w = $table->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE epoch < %d LIMIT 100', $table->getTableName(), time() - $ttl); return $conn_w->getAffectedRows(); } private function collectParseCaches() { $key = 'gcdaemon.ttl.differential-parse-cache'; $ttl = PhabricatorEnv::getEnvConfig($key); if ($ttl <= 0) { return 0; } $table = new DifferentialChangeset(); $conn_w = $table->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE dateCreated < %d LIMIT 100', DifferentialChangeset::TABLE_CACHE, time() - $ttl); return $conn_w->getAffectedRows(); } private function collectMarkupCaches() { $key = 'gcdaemon.ttl.markup-cache'; $ttl = PhabricatorEnv::getEnvConfig($key); if ($ttl <= 0) { return 0; } $table = new PhabricatorMarkupCache(); $conn_w = $table->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE dateCreated < %d LIMIT 100', $table->getTableName(), time() - $ttl); return $conn_w->getAffectedRows(); } private function collectArchivedTasks() { $key = 'gcdaemon.ttl.task-archive'; $ttl = PhabricatorEnv::getEnvConfig($key); if ($ttl <= 0) { return 0; } $table = new PhabricatorWorkerArchiveTask(); $data_table = new PhabricatorWorkerTaskData(); $conn_w = $table->establishConnection('w'); $rows = queryfx_all( $conn_w, 'SELECT id, dataID FROM %T WHERE dateCreated < %d LIMIT 100', $table->getTableName(), time() - $ttl); if (!$rows) { return 0; } $data_ids = array_filter(ipull($rows, 'dataID')); $task_ids = ipull($rows, 'id'); $table->openTransaction(); if ($data_ids) { queryfx( $conn_w, 'DELETE FROM %T WHERE id IN (%Ld)', $data_table->getTableName(), $data_ids); } queryfx( $conn_w, 'DELETE FROM %T WHERE id IN (%Ld)', $table->getTableName(), $task_ids); $table->saveTransaction(); return count($task_ids); } private function collectGeneralCacheTTL() { $cache = new PhabricatorKeyValueDatabaseCache(); $conn_w = $cache->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE cacheExpires < %d ORDER BY cacheExpires ASC LIMIT 100', $cache->getTableName(), time()); return $conn_w->getAffectedRows(); } private function collectGeneralCaches() { $key = 'gcdaemon.ttl.general-cache'; $ttl = PhabricatorEnv::getEnvConfig($key); if ($ttl <= 0) { return 0; } $cache = new PhabricatorKeyValueDatabaseCache(); $conn_w = $cache->establishConnection('w'); queryfx( $conn_w, 'DELETE FROM %T WHERE cacheCreated < %d ORDER BY cacheCreated ASC LIMIT 100', $cache->getTableName(), time() - $ttl); return $conn_w->getAffectedRows(); } + private function collectExpiredFiles() { + $files = id(new PhabricatorFile())->loadAllWhere('ttl < %d LIMIT 100', + time()); + + foreach ($files as $file) { + $file->delete(); + } + + return count($files); + } } diff --git a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php index dd677403b1..f678f7c4cb 100644 --- a/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php +++ b/src/infrastructure/storage/patch/PhabricatorBuiltinPatchList.php @@ -1,1143 +1,1147 @@ array( 'type' => 'db', 'name' => 'audit', 'after' => array( /* First Patch */ ), ), 'db.calendar' => array( 'type' => 'db', 'name' => 'calendar', ), 'db.chatlog' => array( 'type' => 'db', 'name' => 'chatlog', ), 'db.conduit' => array( 'type' => 'db', 'name' => 'conduit', ), 'db.countdown' => array( 'type' => 'db', 'name' => 'countdown', ), 'db.daemon' => array( 'type' => 'db', 'name' => 'daemon', ), 'db.differential' => array( 'type' => 'db', 'name' => 'differential', ), 'db.draft' => array( 'type' => 'db', 'name' => 'draft', ), 'db.drydock' => array( 'type' => 'db', 'name' => 'drydock', ), 'db.feed' => array( 'type' => 'db', 'name' => 'feed', ), 'db.file' => array( 'type' => 'db', 'name' => 'file', ), 'db.flag' => array( 'type' => 'db', 'name' => 'flag', ), 'db.harbormaster' => array( 'type' => 'db', 'name' => 'harbormaster', ), 'db.herald' => array( 'type' => 'db', 'name' => 'herald', ), 'db.maniphest' => array( 'type' => 'db', 'name' => 'maniphest', ), 'db.meta_data' => array( 'type' => 'db', 'name' => 'meta_data', ), 'db.metamta' => array( 'type' => 'db', 'name' => 'metamta', ), 'db.oauth_server' => array( 'type' => 'db', 'name' => 'oauth_server', ), 'db.owners' => array( 'type' => 'db', 'name' => 'owners', ), 'db.pastebin' => array( 'type' => 'db', 'name' => 'pastebin', ), 'db.phame' => array( 'type' => 'db', 'name' => 'phame', ), 'db.phriction' => array( 'type' => 'db', 'name' => 'phriction', ), 'db.project' => array( 'type' => 'db', 'name' => 'project', ), 'db.repository' => array( 'type' => 'db', 'name' => 'repository', ), 'db.search' => array( 'type' => 'db', 'name' => 'search', ), 'db.slowvote' => array( 'type' => 'db', 'name' => 'slowvote', ), 'db.timeline' => array( 'type' => 'db', 'name' => 'timeline', ), 'db.user' => array( 'type' => 'db', 'name' => 'user', ), 'db.worker' => array( 'type' => 'db', 'name' => 'worker', ), 'db.xhpastview' => array( 'type' => 'db', 'name' => 'xhpastview', ), 'db.cache' => array( 'type' => 'db', 'name' => 'cache', ), 'db.fact' => array( 'type' => 'db', 'name' => 'fact', ), 'db.ponder' => array( 'type' => 'db', 'name' => 'ponder', ), 'db.xhprof' => array( 'type' => 'db', 'name' => 'xhprof', ), 'db.pholio' => array( 'type' => 'db', 'name' => 'pholio', ), 'db.conpherence' => array( 'type' => 'db', 'name' => 'conpherence', ), 'db.config' => array( 'type' => 'db', 'name' => 'config', ), 'db.token' => array( 'type' => 'db', 'name' => 'token', ), '0000.legacy.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('0000.legacy.sql'), 'legacy' => 0, ), '000.project.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('000.project.sql'), 'legacy' => 0, ), '001.maniphest_projects.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('001.maniphest_projects.sql'), 'legacy' => 1, ), '002.oauth.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('002.oauth.sql'), 'legacy' => 2, ), '003.more_oauth.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('003.more_oauth.sql'), 'legacy' => 3, ), '004.daemonrepos.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('004.daemonrepos.sql'), 'legacy' => 4, ), '005.workers.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('005.workers.sql'), 'legacy' => 5, ), '006.repository.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('006.repository.sql'), 'legacy' => 6, ), '007.daemonlog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('007.daemonlog.sql'), 'legacy' => 7, ), '008.repoopt.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('008.repoopt.sql'), 'legacy' => 8, ), '009.repo_summary.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('009.repo_summary.sql'), 'legacy' => 9, ), '010.herald.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('010.herald.sql'), 'legacy' => 10, ), '011.badcommit.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('011.badcommit.sql'), 'legacy' => 11, ), '012.dropphidtype.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('012.dropphidtype.sql'), 'legacy' => 12, ), '013.commitdetail.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('013.commitdetail.sql'), 'legacy' => 13, ), '014.shortcuts.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('014.shortcuts.sql'), 'legacy' => 14, ), '015.preferences.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('015.preferences.sql'), 'legacy' => 15, ), '016.userrealnameindex.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('016.userrealnameindex.sql'), 'legacy' => 16, ), '017.sessionkeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('017.sessionkeys.sql'), 'legacy' => 17, ), '018.owners.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('018.owners.sql'), 'legacy' => 18, ), '019.arcprojects.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('019.arcprojects.sql'), 'legacy' => 19, ), '020.pathcapital.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('020.pathcapital.sql'), 'legacy' => 20, ), '021.xhpastview.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('021.xhpastview.sql'), 'legacy' => 21, ), '022.differentialcommit.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('022.differentialcommit.sql'), 'legacy' => 22, ), '023.dxkeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('023.dxkeys.sql'), 'legacy' => 23, ), '024.mlistkeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('024.mlistkeys.sql'), 'legacy' => 24, ), '025.commentopt.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('025.commentopt.sql'), 'legacy' => 25, ), '026.diffpropkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('026.diffpropkey.sql'), 'legacy' => 26, ), '027.metamtakeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('027.metamtakeys.sql'), 'legacy' => 27, ), '028.systemagent.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('028.systemagent.sql'), 'legacy' => 28, ), '029.cursors.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('029.cursors.sql'), 'legacy' => 29, ), '030.imagemacro.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('030.imagemacro.sql'), 'legacy' => 30, ), '031.workerrace.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('031.workerrace.sql'), 'legacy' => 31, ), '032.viewtime.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('032.viewtime.sql'), 'legacy' => 32, ), '033.privtest.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('033.privtest.sql'), 'legacy' => 33, ), '034.savedheader.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('034.savedheader.sql'), 'legacy' => 34, ), '035.proxyimage.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('035.proxyimage.sql'), 'legacy' => 35, ), '036.mailkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('036.mailkey.sql'), 'legacy' => 36, ), '037.setuptest.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('037.setuptest.sql'), 'legacy' => 37, ), '038.admin.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('038.admin.sql'), 'legacy' => 38, ), '039.userlog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('039.userlog.sql'), 'legacy' => 39, ), '040.transform.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('040.transform.sql'), 'legacy' => 40, ), '041.heraldrepetition.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('041.heraldrepetition.sql'), 'legacy' => 41, ), '042.commentmetadata.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('042.commentmetadata.sql'), 'legacy' => 42, ), '043.pastebin.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('043.pastebin.sql'), 'legacy' => 43, ), '044.countdown.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('044.countdown.sql'), 'legacy' => 44, ), '045.timezone.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('045.timezone.sql'), 'legacy' => 45, ), '046.conduittoken.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('046.conduittoken.sql'), 'legacy' => 46, ), '047.projectstatus.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('047.projectstatus.sql'), 'legacy' => 47, ), '048.relationshipkeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('048.relationshipkeys.sql'), 'legacy' => 48, ), '049.projectowner.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('049.projectowner.sql'), 'legacy' => 49, ), '050.taskdenormal.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('050.taskdenormal.sql'), 'legacy' => 50, ), '051.projectfilter.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('051.projectfilter.sql'), 'legacy' => 51, ), '052.pastelanguage.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('052.pastelanguage.sql'), 'legacy' => 52, ), '053.feed.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('053.feed.sql'), 'legacy' => 53, ), '054.subscribers.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('054.subscribers.sql'), 'legacy' => 54, ), '055.add_author_to_files.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('055.add_author_to_files.sql'), 'legacy' => 55, ), '056.slowvote.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('056.slowvote.sql'), 'legacy' => 56, ), '057.parsecache.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('057.parsecache.sql'), 'legacy' => 57, ), '058.missingkeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('058.missingkeys.sql'), 'legacy' => 58, ), '059.engines.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('059.engines.php'), 'legacy' => 59, ), '060.phriction.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('060.phriction.sql'), 'legacy' => 60, ), '061.phrictioncontent.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('061.phrictioncontent.sql'), 'legacy' => 61, ), '062.phrictionmenu.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('062.phrictionmenu.sql'), 'legacy' => 62, ), '063.pasteforks.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('063.pasteforks.sql'), 'legacy' => 63, ), '064.subprojects.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('064.subprojects.sql'), 'legacy' => 64, ), '065.sshkeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('065.sshkeys.sql'), 'legacy' => 65, ), '066.phrictioncontent.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('066.phrictioncontent.sql'), 'legacy' => 66, ), '067.preferences.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('067.preferences.sql'), 'legacy' => 67, ), '068.maniphestauxiliarystorage.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('068.maniphestauxiliarystorage.sql'), 'legacy' => 68, ), '069.heraldxscript.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('069.heraldxscript.sql'), 'legacy' => 69, ), '070.differentialaux.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('070.differentialaux.sql'), 'legacy' => 70, ), '071.contentsource.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('071.contentsource.sql'), 'legacy' => 71, ), '072.blamerevert.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('072.blamerevert.sql'), 'legacy' => 72, ), '073.reposymbols.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('073.reposymbols.sql'), 'legacy' => 73, ), '074.affectedpath.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('074.affectedpath.sql'), 'legacy' => 74, ), '075.revisionhash.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('075.revisionhash.sql'), 'legacy' => 75, ), '076.indexedlanguages.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('076.indexedlanguages.sql'), 'legacy' => 76, ), '077.originalemail.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('077.originalemail.sql'), 'legacy' => 77, ), '078.nametoken.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('078.nametoken.sql'), 'legacy' => 78, ), '079.nametokenindex.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('079.nametokenindex.php'), 'legacy' => 79, ), '080.filekeys.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('080.filekeys.sql'), 'legacy' => 80, ), '081.filekeys.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('081.filekeys.php'), 'legacy' => 81, ), '082.xactionkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('082.xactionkey.sql'), 'legacy' => 82, ), '083.dxviewtime.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('083.dxviewtime.sql'), 'legacy' => 83, ), '084.pasteauthorkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('084.pasteauthorkey.sql'), 'legacy' => 84, ), '085.packagecommitrelationship.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('085.packagecommitrelationship.sql'), 'legacy' => 85, ), '086.formeraffil.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('086.formeraffil.sql'), 'legacy' => 86, ), '087.phrictiondelete.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('087.phrictiondelete.sql'), 'legacy' => 87, ), '088.audit.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('088.audit.sql'), 'legacy' => 88, ), '089.projectwiki.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('089.projectwiki.sql'), 'legacy' => 89, ), '090.forceuniqueprojectnames.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('090.forceuniqueprojectnames.php'), 'legacy' => 90, ), '091.uniqueslugkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('091.uniqueslugkey.sql'), 'legacy' => 91, ), '092.dropgithubnotification.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('092.dropgithubnotification.sql'), 'legacy' => 92, ), '093.gitremotes.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('093.gitremotes.php'), 'legacy' => 93, ), '094.phrictioncolumn.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('094.phrictioncolumn.sql'), 'legacy' => 94, ), '095.directory.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('095.directory.sql'), 'legacy' => 95, ), '096.filename.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('096.filename.sql'), 'legacy' => 96, ), '097.heraldruletypes.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('097.heraldruletypes.sql'), 'legacy' => 97, ), '098.heraldruletypemigration.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('098.heraldruletypemigration.php'), 'legacy' => 98, ), '099.drydock.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('099.drydock.sql'), 'legacy' => 99, ), '100.projectxaction.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('100.projectxaction.sql'), 'legacy' => 100, ), '101.heraldruleapplied.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('101.heraldruleapplied.sql'), 'legacy' => 101, ), '102.heraldcleanup.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('102.heraldcleanup.php'), 'legacy' => 102, ), '103.heraldedithistory.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('103.heraldedithistory.sql'), 'legacy' => 103, ), '104.searchkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('104.searchkey.sql'), 'legacy' => 104, ), '105.mimetype.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('105.mimetype.sql'), 'legacy' => 105, ), '106.chatlog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('106.chatlog.sql'), 'legacy' => 106, ), '107.oauthserver.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('107.oauthserver.sql'), 'legacy' => 107, ), '108.oauthscope.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('108.oauthscope.sql'), 'legacy' => 108, ), '109.oauthclientphidkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('109.oauthclientphidkey.sql'), 'legacy' => 109, ), '110.commitaudit.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('110.commitaudit.sql'), 'legacy' => 110, ), '111.commitauditmigration.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('111.commitauditmigration.php'), 'legacy' => 111, ), '112.oauthaccesscoderedirecturi.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('112.oauthaccesscoderedirecturi.sql'), 'legacy' => 112, ), '113.lastreviewer.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('113.lastreviewer.sql'), 'legacy' => 113, ), '114.auditrequest.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('114.auditrequest.sql'), 'legacy' => 114, ), '115.prepareutf8.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('115.prepareutf8.sql'), 'legacy' => 115, ), '116.utf8-backup-first-expect-wait.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('116.utf8-backup-first-expect-wait.sql'), 'legacy' => 116, ), '117.repositorydescription.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('117.repositorydescription.php'), 'legacy' => 117, ), '118.auditinline.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('118.auditinline.sql'), 'legacy' => 118, ), '119.filehash.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('119.filehash.sql'), 'legacy' => 119, ), '120.noop.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('120.noop.sql'), 'legacy' => 120, ), '121.drydocklog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('121.drydocklog.sql'), 'legacy' => 121, ), '122.flag.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('122.flag.sql'), 'legacy' => 122, ), '123.heraldrulelog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('123.heraldrulelog.sql'), 'legacy' => 123, ), '124.subpriority.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('124.subpriority.sql'), 'legacy' => 124, ), '125.ipv6.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('125.ipv6.sql'), 'legacy' => 125, ), '126.edges.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('126.edges.sql'), 'legacy' => 126, ), '127.userkeybody.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('127.userkeybody.sql'), 'legacy' => 127, ), '128.phabricatorcom.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('128.phabricatorcom.sql'), 'legacy' => 128, ), '129.savedquery.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('129.savedquery.sql'), 'legacy' => 129, ), '130.denormalrevisionquery.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('130.denormalrevisionquery.sql'), 'legacy' => 130, ), '131.migraterevisionquery.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('131.migraterevisionquery.php'), 'legacy' => 131, ), '132.phame.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('132.phame.sql'), 'legacy' => 132, ), '133.imagemacro.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('133.imagemacro.sql'), 'legacy' => 133, ), '134.emptysearch.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('134.emptysearch.sql'), 'legacy' => 134, ), '135.datecommitted.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('135.datecommitted.sql'), 'legacy' => 135, ), '136.sex.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('136.sex.sql'), 'legacy' => 136, ), '137.auditmetadata.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('137.auditmetadata.sql'), 'legacy' => 137, ), '138.notification.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('138.notification.sql'), ), 'holidays.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('holidays.sql'), ), 'userstatus.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('userstatus.sql'), ), 'emailtable.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('emailtable.sql'), ), 'emailtableport.sql' => array( 'type' => 'php', 'name' => $this->getPatchPath('emailtableport.php'), ), 'emailtableremove.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('emailtableremove.sql'), ), 'phiddrop.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('phiddrop.sql'), ), 'testdatabase.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('testdatabase.sql'), ), 'ldapinfo.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('ldapinfo.sql'), ), 'threadtopic.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('threadtopic.sql'), ), 'usertranslation.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('usertranslation.sql'), ), 'differentialbookmarks.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('differentialbookmarks.sql'), ), 'harbormasterobject.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('harbormasterobject.sql'), ), 'markupcache.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('markupcache.sql'), ), 'maniphestxcache.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('maniphestxcache.sql'), ), 'migrate-maniphest-dependencies.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('migrate-maniphest-dependencies.php'), ), 'migrate-differential-dependencies.php' => array( 'type' => 'php', 'name' => $this->getPatchPath( 'migrate-differential-dependencies.php'), ), 'phameblog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('phameblog.sql'), ), 'migrate-maniphest-revisions.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('migrate-maniphest-revisions.php'), ), 'daemonstatus.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('daemonstatus.sql'), ), 'symbolcontexts.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('symbolcontexts.sql'), ), 'migrate-project-edges.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('migrate-project-edges.php'), ), 'fact-raw.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('fact-raw.sql'), ), 'ponder.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('ponder.sql') ), 'policy-project.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('policy-project.sql'), ), 'daemonstatuskey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('daemonstatuskey.sql'), ), 'edgetype.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('edgetype.sql'), ), 'ponder-comments.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('ponder-comments.sql'), ), 'pastepolicy.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('pastepolicy.sql'), ), 'xhprof.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('xhprof.sql'), ), 'draft-metadata.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('draft-metadata.sql'), ), 'phamedomain.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('phamedomain.sql'), ), 'ponder-mailkey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('ponder-mailkey.sql'), ), 'ponder-mailkey-populate.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('ponder-mailkey-populate.php'), ), 'phamepolicy.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('phamepolicy.sql'), ), 'phameoneblog.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('phameoneblog.sql'), ), 'statustxt.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('statustxt.sql'), ), 'daemontaskarchive.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('daemontaskarchive.sql'), ), 'drydocktaskid.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('drydocktaskid.sql'), ), 'drydockresoucetype.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('drydockresourcetype.sql'), ), 'liskcounters.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('liskcounters.sql'), ), 'liskcounters.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('liskcounters.php'), ), 'dropfileproxyimage.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('dropfileproxyimage.sql'), ), 'repository-lint.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('repository-lint.sql'), ), 'liskcounters-task.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('liskcounters-task.sql'), ), 'pholio.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('pholio.sql'), ), 'owners-exclude.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('owners-exclude.sql'), ), '20121209.pholioxactions.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20121209.pholioxactions.sql'), ), '20121209.xmacroadd.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20121209.xmacroadd.sql'), ), '20121209.xmacromigrate.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20121209.xmacromigrate.php'), ), '20121209.xmacromigratekey.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20121209.xmacromigratekey.sql'), ), '20121220.generalcache.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20121220.generalcache.sql'), ), '20121226.config.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20121226.config.sql'), ), '20130101.confxaction.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130101.confxaction.sql'), ), '20130102.metamtareceivedmailmessageidhash.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130102.metamtareceivedmailmessageidhash.sql'), ), '20130103.filemetadata.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130103.filemetadata.sql'), ), '20130111.conpherence.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130111.conpherence.sql'), ), '20130127.altheraldtranscript.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130127.altheraldtranscript.sql'), ), '20130201.revisionunsubscribed.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130201.revisionunsubscribed.php'), ), '20130201.revisionunsubscribed.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130201.revisionunsubscribed.sql'), ), '20130131.conpherencepics.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130131.conpherencepics.sql'), ), '20130214.chatlogchannel.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130214.chatlogchannel.sql'), ), '20130214.chatlogchannelid.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130214.chatlogchannelid.sql'), ), '20130214.token.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130214.token.sql'), ), '20130217.cachettl.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130217.cachettl.sql'), ), '20130218.updatechannelid.php' => array( 'type' => 'php', 'name' => $this->getPatchPath('20130218.updatechannelid.php'), ), '20130218.longdaemon.sql' => array( 'type' => 'sql', 'name' => $this->getPatchPath('20130218.longdaemon.sql'), ), + '20130215.phabricatorfileaddttl.sql' => array( + 'type' => 'sql', + 'name' => $this->getPatchPath('20130215.phabricatorfileaddttl.sql'), + ), ); } }