@recaptime-dev's working patches + fork for Phorge, a community fork of Phabricator. (Upstream dev and stable branches are at upstream/main and upstream/stable respectively.) hq.recaptime.dev/wiki/Phorge
phorge phabricator
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

When deleting a file, delete all transformations of the file

Summary:
Fixes T3143. When a user deletes a file, delete all transforms of the file too. In particular, this means that deleting an image deletes all the thumbnails of the image.

In most cases, this aligns with user expectations. The only sort of weird case I can come up with is that memes are transformations of the source macro image, so deleting grumpycat will delete all the hilarious grumpycat memes. This seems not-too-unreasonable, though, and desirable if someone accidentally uploads an inappropriate image which is promptly turned into a meme.

Test Plan:
Added a unit test which covers both inbound and outbound transformations.

Uploaded a file and deleted it, verified its thumbnail was also deleted.

Reviewers: chad, btrahan, joseph.kampf

Reviewed By: btrahan

CC: aran, joseph.kampf

Maniphest Tasks: T3143

Differential Revision: https://secure.phabricator.com/D5879

+183 -16
+45 -6
src/applications/files/query/PhabricatorFileQuery.php
··· 24 24 return $this; 25 25 } 26 26 27 + /** 28 + * Select files which are transformations of some other file. For example, 29 + * you can use this query to find previously generated thumbnails of an image 30 + * file. 31 + * 32 + * As a parameter, provide a list of transformation specifications. Each 33 + * specification is a dictionary with the keys `originalPHID` and `transform`. 34 + * The `originalPHID` is the PHID of the original file (the file which was 35 + * transformed) and the `transform` is the name of the transform to query 36 + * for. If you pass `true` as the `transform`, all transformations of the 37 + * file will be selected. 38 + * 39 + * For example: 40 + * 41 + * array( 42 + * array( 43 + * 'originalPHID' => 'PHID-FILE-aaaa', 44 + * 'transform' => 'sepia', 45 + * ), 46 + * array( 47 + * 'originalPHID' => 'PHID-FILE-bbbb', 48 + * 'transform' => true, 49 + * ), 50 + * ) 51 + * 52 + * This selects the `"sepia"` transformation of the file with PHID 53 + * `PHID-FILE-aaaa` and all transformations of the file with PHID 54 + * `PHID-FILE-bbbb`. 55 + * 56 + * @param list<dict> List of transform specifications, described above. 57 + * @return this 58 + */ 27 59 public function withTransforms(array $specs) { 28 60 foreach ($specs as $spec) { 29 61 if (!is_array($spec) || ··· 50 82 51 83 $data = queryfx_all( 52 84 $conn_r, 53 - 'SELECT * FROM %T f %Q %Q %Q %Q', 85 + 'SELECT f.* FROM %T f %Q %Q %Q %Q', 54 86 $table->getTableName(), 55 87 $this->buildJoinClause($conn_r), 56 88 $this->buildWhereClause($conn_r), ··· 108 140 if ($this->transforms) { 109 141 $clauses = array(); 110 142 foreach ($this->transforms as $transform) { 111 - $clauses[] = qsprintf( 112 - $conn_r, 113 - '(t.originalPHID = %s AND t.transform = %s)', 114 - $transform['originalPHID'], 115 - $transform['transform']); 143 + if ($transform['transform'] === true) { 144 + $clauses[] = qsprintf( 145 + $conn_r, 146 + '(t.originalPHID = %s)', 147 + $transform['originalPHID']); 148 + } else { 149 + $clauses[] = qsprintf( 150 + $conn_r, 151 + '(t.originalPHID = %s AND t.transform = %s)', 152 + $transform['originalPHID'], 153 + $transform['transform']); 154 + } 116 155 } 117 156 $where[] = qsprintf($conn_r, '(%Q)', implode(') OR (', $clauses)); 118 157 }
+35 -10
src/applications/files/storage/PhabricatorFile.php
··· 384 384 } 385 385 386 386 public function delete() { 387 - // delete all records of this file in transformedfile 388 - $trans_files = id(new PhabricatorTransformedFile())->loadAllWhere( 389 - 'TransformedPHID = %s', $this->getPHID()); 387 + 388 + // We want to delete all the rows which mark this file as the transformation 389 + // of some other file (since we're getting rid of it). We also delete all 390 + // the transformations of this file, so that a user who deletes an image 391 + // doesn't need to separately hunt down and delete a bunch of thumbnails and 392 + // resizes of it. 393 + 394 + $outbound_xforms = id(new PhabricatorFileQuery()) 395 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 396 + ->withTransforms( 397 + array( 398 + array( 399 + 'originalPHID' => $this->getPHID(), 400 + 'transform' => true, 401 + ), 402 + )) 403 + ->execute(); 404 + 405 + foreach ($outbound_xforms as $outbound_xform) { 406 + $outbound_xform->delete(); 407 + } 408 + 409 + $inbound_xforms = id(new PhabricatorTransformedFile())->loadAllWhere( 410 + 'transformedPHID = %s', 411 + $this->getPHID()); 390 412 391 413 $this->openTransaction(); 392 - foreach ($trans_files as $trans_file) { 393 - $trans_file->delete(); 394 - } 395 - $ret = parent::delete(); 414 + foreach ($inbound_xforms as $inbound_xform) { 415 + $inbound_xform->delete(); 416 + } 417 + $ret = parent::delete(); 396 418 $this->saveTransaction(); 397 419 398 420 // Check to see if other files are using storage 399 421 $other_file = id(new PhabricatorFile())->loadAllWhere( 400 422 'storageEngine = %s AND storageHandle = %s AND 401 - storageFormat = %s AND id != %d LIMIT 1', $this->getStorageEngine(), 402 - $this->getStorageHandle(), $this->getStorageFormat(), 423 + storageFormat = %s AND id != %d LIMIT 1', 424 + $this->getStorageEngine(), 425 + $this->getStorageHandle(), 426 + $this->getStorageFormat(), 403 427 $this->getID()); 404 428 405 429 // If this is the only file using the storage, delete storage 406 - if (count($other_file) == 0) { 430 + if (!$other_file) { 407 431 $engine = $this->instantiateStorageEngine(); 408 432 $engine->deleteFile($this->getStorageHandle()); 409 433 } 434 + 410 435 return $ret; 411 436 } 412 437
+103
src/applications/files/storage/__tests__/PhabricatorFileTestCase.php
··· 145 145 $this->assertEqual($ttl, $file->getTTL()); 146 146 } 147 147 148 + public function testFileTransformDelete() { 149 + // We want to test that a file deletes all its inbound transformation 150 + // records and outbound transformed derivatives when it is deleted. 151 + 152 + // First, we create a chain of transforms, A -> B -> C. 153 + 154 + $engine = new PhabricatorTestStorageEngine(); 155 + 156 + $params = array( 157 + 'name' => 'test.txt', 158 + 'storageEngines' => array( 159 + $engine, 160 + ), 161 + ); 162 + 163 + $a = PhabricatorFile::newFromFileData('a', $params); 164 + $b = PhabricatorFile::newFromFileData('b', $params); 165 + $c = PhabricatorFile::newFromFileData('c', $params); 166 + 167 + id(new PhabricatorTransformedFile()) 168 + ->setOriginalPHID($a->getPHID()) 169 + ->setTransform('test:a->b') 170 + ->setTransformedPHID($b->getPHID()) 171 + ->save(); 172 + 173 + id(new PhabricatorTransformedFile()) 174 + ->setOriginalPHID($b->getPHID()) 175 + ->setTransform('test:b->c') 176 + ->setTransformedPHID($c->getPHID()) 177 + ->save(); 178 + 179 + // Now, verify that A -> B and B -> C exist. 180 + 181 + $xform_a = id(new PhabricatorFileQuery()) 182 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 183 + ->withTransforms( 184 + array( 185 + array( 186 + 'originalPHID' => $a->getPHID(), 187 + 'transform' => true, 188 + ), 189 + )) 190 + ->execute(); 191 + 192 + $this->assertEqual(1, count($xform_a)); 193 + $this->assertEqual($b->getPHID(), head($xform_a)->getPHID()); 194 + 195 + $xform_b = id(new PhabricatorFileQuery()) 196 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 197 + ->withTransforms( 198 + array( 199 + array( 200 + 'originalPHID' => $b->getPHID(), 201 + 'transform' => true, 202 + ), 203 + )) 204 + ->execute(); 205 + 206 + $this->assertEqual(1, count($xform_b)); 207 + $this->assertEqual($c->getPHID(), head($xform_b)->getPHID()); 208 + 209 + // Delete "B". 210 + 211 + $b->delete(); 212 + 213 + // Now, verify that the A -> B and B -> C links are gone. 214 + 215 + $xform_a = id(new PhabricatorFileQuery()) 216 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 217 + ->withTransforms( 218 + array( 219 + array( 220 + 'originalPHID' => $a->getPHID(), 221 + 'transform' => true, 222 + ), 223 + )) 224 + ->execute(); 225 + 226 + $this->assertEqual(0, count($xform_a)); 227 + 228 + $xform_b = id(new PhabricatorFileQuery()) 229 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 230 + ->withTransforms( 231 + array( 232 + array( 233 + 'originalPHID' => $b->getPHID(), 234 + 'transform' => true, 235 + ), 236 + )) 237 + ->execute(); 238 + 239 + $this->assertEqual(0, count($xform_b)); 240 + 241 + // Also verify that C has been deleted. 242 + 243 + $alternate_c = id(new PhabricatorFileQuery()) 244 + ->setViewer(PhabricatorUser::getOmnipotentUser()) 245 + ->withPHIDs(array($c->getPHID())) 246 + ->execute(); 247 + 248 + $this->assertEqual(array(), $alternate_c); 249 + } 250 + 148 251 }