[TASK] Do not generate new processed file if it already exists
[Packages/TYPO3.CMS.git] / typo3 / sysext / core / Classes / Resource / ProcessedFile.php
1 <?php
2 namespace TYPO3\CMS\Core\Resource;
3
4 /*
5 * This file is part of the TYPO3 CMS project.
6 *
7 * It is free software; you can redistribute it and/or modify it under
8 * the terms of the GNU General Public License, either version 2
9 * of the License, or any later version.
10 *
11 * For the full copyright and license information, please read the
12 * LICENSE.txt file that was distributed with this source code.
13 *
14 * The TYPO3 project - inspiring people to share!
15 */
16
17 /**
18 * Representation of a specific processed version of a file. These are created by the FileProcessingService,
19 * which in turn uses helper classes for doing the actual file processing. See there for a detailed description.
20 *
21 * Objects of this class may be freshly created during runtime or being fetched from the database. The latter
22 * indicates that the file has been processed earlier and was then cached.
23 *
24 * Each processed file—besides belonging to one file—has been created for a certain task (context) and
25 * configuration. All these won't change during the lifetime of a processed file; the only thing
26 * that can change is the original file, or rather it's contents. In that case, the processed file has to
27 * be processed again. Detecting this is done via comparing the current SHA1 hash of the original file against
28 * the one it had at the time the file was processed.
29 * The configuration of a processed file indicates what should be done to the original file to create the
30 * processed version. This may include things like cropping, scaling, rotating, flipping or using some special
31 * magic.
32 * A file may also meet the expectations set in the configuration without any processing. In that case, the
33 * ProcessedFile object still exists, but there is no physical file directly linked to it. Instead, it then
34 * redirects most method calls to the original file object. The data of these objects are also stored in the
35 * database, to indicate that no processing is required. With such files, the identifier and name fields in the
36 * database are empty to show this.
37 *
38 * @author Benjamin Mack <benni@typo3.org>
39 */
40 class ProcessedFile extends AbstractFile {
41
42 /*********************************************
43 * FILE PROCESSING CONTEXTS
44 *********************************************/
45 /**
46 * Basic processing context to get a processed image with smaller
47 * width/height to render a preview
48 */
49 const CONTEXT_IMAGEPREVIEW = 'Image.Preview';
50 /**
51 * Standard processing context for the frontend, that was previously
52 * in \TYPO3\CMS\Frontend\ContentObject\ContentObjectRenderer::getImgResource which only takes cropping, masking and scaling
53 * into account
54 */
55 const CONTEXT_IMAGECROPSCALEMASK = 'Image.CropScaleMask';
56
57 /**
58 * Processing context, i.e. the type of processing done
59 *
60 * @var string
61 */
62 protected $taskType;
63
64 /**
65 * @var Processing\TaskInterface
66 */
67 protected $task;
68
69 /**
70 * @var Processing\TaskTypeRegistry
71 */
72 protected $taskTypeRegistry;
73
74 /**
75 * Processing configuration
76 *
77 * @var array
78 */
79 protected $processingConfiguration;
80
81 /**
82 * Reference to the original file this processed file has been created from.
83 *
84 * @var File
85 */
86 protected $originalFile;
87
88 /**
89 * The SHA1 hash of the original file this processed version has been created for.
90 * Is used for detecting changes if the original file has been changed and thus
91 * we have to recreate this processed file.
92 *
93 * @var string
94 */
95 protected $originalFileSha1;
96
97 /**
98 * A flag that shows if this object has been updated during its lifetime, i.e. the file has been
99 * replaced with a new one.
100 *
101 * @var bool
102 */
103 protected $updated = FALSE;
104
105 /**
106 * Constructor for a processed file object. Should normally not be used
107 * directly, use the corresponding factory methods instead.
108 *
109 * @param File $originalFile
110 * @param string $taskType
111 * @param array $processingConfiguration
112 * @param array $databaseRow
113 */
114 public function __construct(File $originalFile, $taskType, array $processingConfiguration, array $databaseRow = NULL) {
115 $this->originalFile = $originalFile;
116 $this->originalFileSha1 = $this->originalFile->getSha1();
117 $this->storage = $originalFile->getStorage()->getProcessingFolder()->getStorage();
118 $this->taskType = $taskType;
119 $this->processingConfiguration = $processingConfiguration;
120 if (is_array($databaseRow)) {
121 $this->reconstituteFromDatabaseRecord($databaseRow);
122 }
123 $this->taskTypeRegistry = \TYPO3\CMS\Core\Utility\GeneralUtility::makeInstance(\TYPO3\CMS\Core\Resource\Processing\TaskTypeRegistry::class);
124 }
125
126 /**
127 * Creates a ProcessedFile object from a database record.
128 *
129 * @param array $databaseRow
130 * @return ProcessedFile
131 */
132 protected function reconstituteFromDatabaseRecord(array $databaseRow) {
133 $this->taskType = $this->taskType ?: $databaseRow['task_type'];
134 $this->processingConfiguration = $this->processingConfiguration ?: unserialize($databaseRow['configuration']);
135
136 $this->originalFileSha1 = $databaseRow['originalfilesha1'];
137 $this->identifier = $databaseRow['identifier'];
138 $this->name = $databaseRow['name'];
139 $this->properties = $databaseRow;
140 }
141
142 /********************************
143 * VARIOUS FILE PROPERTY GETTERS
144 ********************************/
145
146 /**
147 * Returns a unique checksum for this file's processing configuration and original file.
148 *
149 * @return string
150 */
151 // @todo replace these usages with direct calls to the task object
152 public function calculateChecksum() {
153 return $this->getTask()->getConfigurationChecksum();
154 }
155
156 /*******************
157 * CONTENTS RELATED
158 *******************/
159 /**
160 * Replace the current file contents with the given string
161 *
162 * @param string $contents The contents to write to the file.
163 * @return File The file object (allows chaining).
164 * @throws \BadMethodCallException
165 */
166 public function setContents($contents) {
167 throw new \BadMethodCallException('Setting contents not possible for processed file.', 1305438528);
168 }
169
170 /**
171 * Injects a local file, which is a processing result into the object.
172 *
173 * @param string $filePath
174 * @return void
175 * @throws \RuntimeException
176 */
177 public function updateWithLocalFile($filePath) {
178 if ($this->identifier === NULL) {
179 throw new \RuntimeException('Cannot update original file!', 1350582054);
180 }
181 $processingFolder = $this->originalFile->getStorage()->getProcessingFolder();
182 $addedFile = $this->storage->updateProcessedFile($filePath, $this, $processingFolder);
183
184 // Update some related properties
185 $this->identifier = $addedFile->getIdentifier();
186 $this->originalFileSha1 = $this->originalFile->getSha1();
187 $this->updateProperties($addedFile->getProperties());
188 $this->deleted = FALSE;
189 $this->updated = TRUE;
190 }
191
192 /*****************************************
193 * STORAGE AND MANAGEMENT RELATED METHDOS
194 *****************************************/
195 /**
196 * Returns TRUE if this file is indexed
197 *
198 * @return bool
199 */
200 public function isIndexed() {
201 // Processed files are never indexed; instead you might be looking for isPersisted()
202 return FALSE;
203 }
204
205 /**
206 * Checks whether the ProcessedFile already has an entry in sys_file_processedfile table
207 *
208 * @return bool
209 */
210 public function isPersisted() {
211 return is_array($this->properties) && array_key_exists('uid', $this->properties) && $this->properties['uid'] > 0;
212 }
213
214 /**
215 * Checks whether the ProcessedFile Object is newly created
216 *
217 * @return bool
218 */
219 public function isNew() {
220 return !$this->isPersisted();
221 }
222
223 /**
224 * Checks whether the object since last reconstitution, and therefore
225 * needs persistence again
226 *
227 * @return bool
228 */
229 public function isUpdated() {
230 return $this->updated;
231 }
232
233 /**
234 * Sets a new file name
235 *
236 * @param string $name
237 */
238 public function setName($name) {
239 // Remove the existing file
240 if ($this->name !== $name && $this->name !== '' && $this->exists()) {
241 $this->delete();
242 }
243
244 $this->name = $name;
245 // @todo this is a *weird* hack that will fail if the storage is non-hierarchical!
246 $this->identifier = $this->storage->getProcessingFolder()->getIdentifier() . $this->name;
247
248 $this->updated = TRUE;
249 }
250
251 /******************
252 * SPECIAL METHODS
253 ******************/
254
255 /**
256 * Returns TRUE if this file is already processed.
257 *
258 * @return bool
259 */
260 public function isProcessed() {
261 return $this->updated || ($this->isPersisted() && !$this->needsReprocessing());
262 }
263
264 /**
265 * Getter for the Original, unprocessed File
266 *
267 * @return File
268 */
269 public function getOriginalFile() {
270 return $this->originalFile;
271 }
272
273 /**
274 * Get the identifier of the file
275 *
276 * If there is no processed file in the file system (as the original file did not have to be modified e.g.
277 * when the original image is in the boundaries of the maxW/maxH stuff), then just return the identifier of
278 * the original file
279 *
280 * @return string
281 */
282 public function getIdentifier() {
283 return (!$this->usesOriginalFile()) ? $this->identifier : $this->getOriginalFile()->getIdentifier();
284 }
285
286 /**
287 * Get the name of the file
288 *
289 * If there is no processed file in the file system (as the original file did not have to be modified e.g.
290 * when the original image is in the boundaries of the maxW/maxH stuff)
291 * then just return the name of the original file
292 *
293 * @return string
294 */
295 public function getName() {
296 if ($this->usesOriginalFile()) {
297 return $this->originalFile->getName();
298 } else {
299 return $this->name;
300 }
301 }
302
303 /**
304 * Updates properties of this object. Do not use this to reconstitute an object from the database; use
305 * reconstituteFromDatabaseRecord() instead!
306 *
307 * @param array $properties
308 */
309 public function updateProperties(array $properties) {
310 if (!is_array($this->properties)) {
311 $this->properties = array();
312 }
313
314 if (array_key_exists('uid', $properties) && \TYPO3\CMS\Core\Utility\MathUtility::canBeInterpretedAsInteger($properties['uid'])) {
315 $this->properties['uid'] = $properties['uid'];
316 }
317
318 // @todo we should have a blacklist of properties that might not be updated
319 $this->properties = array_merge($this->properties, $properties);
320
321 // @todo when should this update be done?
322 if (!$this->isUnchanged() && $this->exists()) {
323 $this->properties = array_merge($this->properties, $this->storage->getFileInfo($this));
324 }
325
326 }
327
328 /**
329 * Basic array function for the DB update
330 *
331 * @return array
332 */
333 public function toArray() {
334 if ($this->usesOriginalFile()) {
335 $properties = $this->originalFile->getProperties();
336 unset($properties['uid']);
337 unset($properties['pid']);
338 unset($properties['identifier']);
339 unset($properties['name']);
340
341 // Use width + height set in processed file
342 $properties['width'] = $this->properties['width'];
343 $properties['height'] = $this->properties['height'];
344 } else {
345 $properties = $this->properties;
346 $properties['identifier'] = $this->getIdentifier();
347 $properties['name'] = $this->getName();
348 }
349
350 $properties['configuration'] = serialize($this->processingConfiguration);
351
352 return array_merge($properties, array(
353 'storage' => $this->getStorage()->getUid(),
354 'checksum' => $this->calculateChecksum(),
355 'task_type' => $this->taskType,
356 'configurationsha1' => sha1($properties['configuration']),
357 'original' => $this->originalFile->getUid(),
358 'originalfilesha1' => $this->originalFileSha1
359 ));
360 }
361
362 /**
363 * Returns TRUE if this file has not been changed during processing (i.e., we just deliver the original file)
364 *
365 * @return bool
366 */
367 protected function isUnchanged() {
368 return !$this->properties['width'] && $this->usesOriginalFile();
369 }
370
371 /**
372 * @return void
373 */
374 public function setUsesOriginalFile() {
375 // @todo check if some of these properties can/should be set in a generic update method
376 $this->identifier = $this->originalFile->getIdentifier();
377 $this->updated = TRUE;
378 $this->originalFileSha1 = $this->originalFile->getSha1();
379 }
380
381 /**
382 * @return bool
383 */
384 public function usesOriginalFile() {
385 return $this->identifier == NULL || $this->identifier === $this->originalFile->getIdentifier();
386 }
387
388 /**
389 * Returns TRUE if the original file of this file changed and the file should be processed again.
390 *
391 * @return bool
392 */
393 public function isOutdated() {
394 return $this->needsReprocessing();
395 }
396
397 /**
398 * Delete processed file
399 *
400 * @param bool $force
401 * @return bool
402 */
403 public function delete($force = FALSE) {
404 if (!$force && $this->isUnchanged()) {
405 return FALSE;
406 }
407 // Only delete file when original isn't used
408 if (!$this->usesOriginalFile()) {
409 return parent::delete();
410 } else {
411 return TRUE;
412 }
413 }
414
415 /**
416 * Getter for file-properties
417 *
418 * @param string $key
419 *
420 * @return mixed
421 */
422 public function getProperty($key) {
423 // The uid always (!) has to come from this file and never the original file (see getOriginalFile() to get this)
424 if ($this->isUnchanged() && $key !== 'uid') {
425 return $this->originalFile->getProperty($key);
426 } else {
427 return $this->properties[$key];
428 }
429 }
430
431 /**
432 * Returns the uid of this file
433 *
434 * @return int
435 */
436 public function getUid() {
437 return $this->properties['uid'];
438 }
439
440
441 /**
442 * Checks if the ProcessedFile needs reprocessing
443 *
444 * @return bool
445 */
446 public function needsReprocessing() {
447 $fileMustBeRecreated = FALSE;
448
449 // if original is missing we can not reprocess the file
450 if ($this->originalFile->isMissing()) {
451 return FALSE;
452 }
453
454 // processedFile does not exist
455 if (!$this->usesOriginalFile() && !$this->exists()) {
456 $fileMustBeRecreated = TRUE;
457 }
458
459 // hash does not match
460 if (array_key_exists('checksum', $this->properties) && $this->calculateChecksum() !== $this->properties['checksum']) {
461 $fileMustBeRecreated = TRUE;
462 }
463
464 // original file changed
465 if ($this->originalFile->getSha1() !== $this->originalFileSha1) {
466 $fileMustBeRecreated = TRUE;
467 }
468
469 if (!array_key_exists('uid', $this->properties)) {
470 $fileMustBeRecreated = TRUE;
471 }
472
473 // remove outdated file
474 if ($fileMustBeRecreated && $this->exists()) {
475 $this->delete();
476 }
477 return $fileMustBeRecreated;
478 }
479
480 /**
481 * Returns the processing information
482 *
483 * @return array
484 */
485 public function getProcessingConfiguration() {
486 return $this->processingConfiguration;
487 }
488
489 /**
490 * Getter for the task identifier.
491 *
492 * @return string
493 */
494 public function getTaskIdentifier() {
495 return $this->taskType;
496 }
497
498 /**
499 * Returns the task object associated with this processed file.
500 *
501 * @return Processing\TaskInterface
502 * @throws \RuntimeException
503 */
504 public function getTask() {
505 if ($this->task == NULL) {
506 $this->task = $this->taskTypeRegistry->getTaskForType($this->taskType, $this, $this->processingConfiguration);
507 }
508
509 return $this->task;
510 }
511
512 /**
513 * Generate the name of of the new File
514 *
515 * @return string
516 */
517 public function generateProcessedFileNameWithoutExtension() {
518 $name = $this->originalFile->getNameWithoutExtension();
519 $name .= '_' . $this->originalFile->getUid();
520 $name .= '_' . $this->calculateChecksum();
521
522 return $name;
523 }
524
525 }