PageRenderTime 48ms CodeModel.GetById 19ms RepoModel.GetById 1ms app.codeStats 0ms

/models/behaviors/generator.php

http://github.com/davidpersson/media
PHP | 371 lines | 150 code | 23 blank | 198 comment | 28 complexity | 727d5d0b8c4ac088c47976326f6667c8 MD5 | raw file
Possible License(s): MIT
  1. <?php
  2. /**
  3. * Generator Behavior File
  4. *
  5. * Copyright (c) 2007-2013 David Persson
  6. *
  7. * Distributed under the terms of the MIT License.
  8. * Redistributions of files must retain the above copyright notice.
  9. *
  10. * PHP version 5
  11. * CakePHP version 1.3
  12. *
  13. * @package media
  14. * @subpackage media.models.behaviors
  15. * @copyright 2007-2013 David Persson <nperson@gmx.de>
  16. * @license http://www.opensource.org/licenses/mit-license.php The MIT License
  17. * @link http://github.com/davidpersson/media
  18. */
  19. require_once 'Media/Process.php';
  20. require_once 'Mime/Type.php';
  21. /**
  22. * Generator Behavior Class
  23. *
  24. * The generation of files is handled by the make method. It is either manually
  25. * triggered or when a to-be-saved record contains a file field with an absolute
  26. * path to a file. The model the behavior is attached to doesn’t necessarily need
  27. * to be bound to a table.
  28. *
  29. * To connect TransferBehavior and GeneratorBehavior with each other it is important
  30. * to specify TransferBehavior before GeneratorBehavior:
  31. * {{{
  32. * public $actAs = array(
  33. * 'Media.Transfer',
  34. * 'Media.Generator'
  35. * );
  36. * }}}
  37. *
  38. * Please note that this behavior *will not* delete generated versions
  39. * automatically. See docs/FAQ for an in depth explanation and docs/TUTORIAL
  40. * for a snippet you can use to implement this functionality.
  41. *
  42. * @package media
  43. * @subpackage media.models.behaviors
  44. */
  45. class GeneratorBehavior extends ModelBehavior {
  46. /**
  47. * Default settings
  48. *
  49. * baseDirectory
  50. * An absolute path (with trailing slash) to a directory which will be stripped off the file path
  51. *
  52. * filterDirectory
  53. * An absolute path (with trailing slash) to a directory to use for storing generated versions
  54. *
  55. * createDirectory
  56. * false - Fail on missing directories
  57. * true - Recursively create missing directories
  58. *
  59. * createDirectoryMode
  60. * Octal mode value to use when creating directories
  61. *
  62. * mode
  63. * Octal mode value to use for resulting files of `make()`.
  64. *
  65. * overwrite
  66. * false - Will fail if a version with the same already exists
  67. * true - Overwrites existing versions with the same name
  68. *
  69. * guessExtension
  70. * false - When making media use extension of source file regardless of MIME
  71. * type of the destination file.
  72. * true - Try to guess extension by looking at the MIME type of the resulting file.
  73. *
  74. * instructions
  75. * Holds instruction sets for generating versions. Allow you to control how and when
  76. * versions are generated. For more detailed information see the documentation of
  77. * `GeneratorBehavior::makeVersion`.
  78. *
  79. * {{{
  80. * // ...
  81. * 'instructions' => array(
  82. * 'image' => array(
  83. * 'fix0' => array('convert' => 'image/png', 'fit' => array(300, 400)),
  84. * 'fix1' => array('convert' => 'image/png', 'fit' => array(500, 600))
  85. * // ...
  86. * }}}
  87. *
  88. * If you want to compile your instruction sets dynamically you can do so by adding an
  89. * `instructions` method to your model. The Generator behavior will then call that method
  90. * in order get the value for this setting.
  91. *
  92. * @var array
  93. */
  94. protected $_defaultSettings = array(
  95. 'baseDirectory' => MEDIA_TRANSFER,
  96. 'filterDirectory' => MEDIA_FILTER,
  97. 'createDirectory' => true,
  98. 'createDirectoryMode' => 0755,
  99. 'mode' => 0644,
  100. 'overwrite' => false,
  101. 'guessExtension' => true,
  102. 'instructions' => array()
  103. );
  104. /**
  105. * Setup
  106. *
  107. * @param Model $Model
  108. * @param array $settings See defaultSettings for configuration options
  109. * @return void
  110. */
  111. public function setup(&$Model, $settings = array()) {
  112. $settings = (array)$settings;
  113. $this->settings[$Model->alias] = array_merge($this->_defaultSettings, $settings);
  114. if (method_exists($Model, 'instructions')) {
  115. $this->settings[$Model->alias]['instructions'] = $Model->instructions();
  116. }
  117. }
  118. /**
  119. * Callback
  120. *
  121. * Triggers `make()` if both `dirname` and `basename` fields are present.
  122. * Otherwise skips and returns `true` to continue the save operation.
  123. *
  124. * @param Model $Model
  125. * @param boolean $created
  126. * @return boolean
  127. */
  128. public function afterSave(&$Model, $created) {
  129. $item = $Model->data[$Model->alias];
  130. if (isset($item['dirname'], $item['basename'])) {
  131. $file = $item['dirname'] . DS . $item['basename'];
  132. } elseif (isset($item['file'])) {
  133. $file = $item['file'];
  134. } else {
  135. return false;
  136. }
  137. return $this->make($Model, $file);
  138. }
  139. /**
  140. * Parses instruction sets and invokes `makeVersion()` for each version on a file.
  141. * Also creates the destination directory if enabled by settings.
  142. *
  143. * If the `makeVersion()` method is implemented in the current model it'll be used
  144. * for generating a specifc version of the file (i.e. `s`, `m` or `l`) otherwise
  145. * the method within this behavior is going to be used.
  146. *
  147. * If you already have generated versions of files and change the filter
  148. * configuration afterwards you may want to recreate those files with the new
  149. * settings.
  150. *
  151. * You can achieve that by removing already generated files first (optional), than
  152. * invoking the task from the shell:
  153. * $ cake media make
  154. *
  155. * For more information on options and arguments for the task call:
  156. * $ cake media help
  157. *
  158. * @param Model $Model
  159. * @param string $file Path to a file relative to `baseDirectory` or an absolute path to a file
  160. * @return array An array of absolute paths to version files which successfully have been made
  161. */
  162. public function make(&$Model, $file) {
  163. extract($this->settings[$Model->alias]);
  164. if (!$instructions) {
  165. return array();
  166. }
  167. list($file, $relativeFile) = $this->_file($Model, $file);
  168. $relativeDirectory = DS . rtrim(dirname($relativeFile), '.');
  169. $filter = $instructions[Mime_Type::guessName($file)];
  170. $results = array();
  171. foreach ($filter as $version => $instructions) {
  172. $directory = Folder::slashTerm($filterDirectory . $version . $relativeDirectory);
  173. $Folder = new Folder($directory, $createDirectory, $createDirectoryMode);
  174. if (!$Folder->pwd()) {
  175. $message = "GeneratorBehavior::make - Directory `{$directory}` ";
  176. $message .= "could not be created or is not writable. ";
  177. $message .= "Please check the permissions.";
  178. trigger_error($message, E_USER_WARNING);
  179. continue;
  180. }
  181. try {
  182. $result = $Model->makeVersion($file, compact('version', 'directory', 'instructions'));
  183. } catch (Exception $E) {
  184. $result = false;
  185. $message = "GeneratorBehavior::make - While making version `{$version}` ";
  186. $message .= "of file `{$file}` an exception was thrown, the message provided ";
  187. $message .= 'was `' . $E->getMessage() . '`. Skipping version.';
  188. trigger_error($message, E_USER_WARNING);
  189. }
  190. if ($result) {
  191. $results[] = $result;
  192. } else {
  193. $message = "GeneratorBehavior::make - The method responsible for making version ";
  194. $message .= "`{$version}` of file `{$file}` returned `false`. Skipping version.";
  195. trigger_error($message, E_USER_WARNING);
  196. }
  197. }
  198. return $results;
  199. }
  200. /**
  201. * Generate a version of a file. If this method is reimplemented in the
  202. * model, than that one is used by `make()` instead of the implementation
  203. * below.
  204. *
  205. * $process an array with the following contents:
  206. * - `directory`: The destination directory (If this method was called
  207. * by `make()` the directory is already created)
  208. * - `version`: The version requested to be processed (e.g. `'l'`)
  209. * - `instructions`: An array specifying processing steps to execute on $file
  210. * in order to get to the desired transformed file.
  211. *
  212. * Each instruction is either a key/value pair where the key
  213. * can be thought of the method and the value the arguments
  214. * passed to that method. Whenever a value appears without a
  215. * corresponding string key it is used as the method instead.
  216. *
  217. * `array('name of method', 'name of other method')`
  218. * `array('name of method' => array('arg1', 'arg2'))`
  219. *
  220. * Most methods are made available through the `Media_Process_*`
  221. * classes. The class is chosen depending on the type of media
  222. * being processed. Since each one of those classes exposes
  223. * different methods the availaibility of those depends on the
  224. * type of media being processed.
  225. *
  226. * Please see the documentation for the mm library for further
  227. * information on the `Media_Process_*` classes mentioned above.
  228. *
  229. * However some methods are builtin and made available directly
  230. * through this method here. One of them being the `clone` method.
  231. * Cloning allows instructions which don't actually modify a file
  232. * but represent just a copy of it. Available clone types are `copy`,
  233. * `link` and `symlink`.
  234. *
  235. * `array('clone' => <type>)`
  236. *
  237. * In case an instruction method is neither builtin nor available
  238. * through one of the `Media_Proces_*` classes, the `passthru()`
  239. * method is invoked on that media object. The concrete implementation
  240. * of `passthru()` and therefore how it deals with the data passed
  241. * to it *highly* depends on the adapter in use.
  242. *
  243. * @link https://github.com/davidpersson/mm The PHP media library.
  244. * @param Model $Model
  245. * @param string $file Absolute path to the source file
  246. * @param array $process directory, version, instructions
  247. * @return string|boolean Absolute path to the version file, `false` on error
  248. */
  249. public function makeVersion(&$Model, $file, $process) {
  250. extract($this->settings[$Model->alias]);
  251. /* Process builtin instructions */
  252. if (isset($process['instructions']['clone'])) {
  253. $action = $process['instructions']['clone'];
  254. if (!in_array($action, array('copy', 'link', 'symlink'))) {
  255. return false;
  256. }
  257. $destination = $this->_destinationFile(
  258. $file, $process['directory'], null, $overwrite
  259. );
  260. if (!$destination) {
  261. return false;
  262. }
  263. if (!call_user_func($action, $file, $destination)) {
  264. return false;
  265. }
  266. return $action != 'copy' || chmod($destination, $mode) ? $destination : false;
  267. }
  268. /* Process `Media_Process_*` instructions */
  269. $Media = Media_Process::factory(array('source' => $file));
  270. foreach ($process['instructions'] as $method => $args) {
  271. if (is_int($method)) {
  272. $method = $args;
  273. $args = null;
  274. }
  275. if (method_exists($Media, $method)) {
  276. $result = call_user_func_array(array($Media, $method), (array) $args);
  277. } else {
  278. $result = $Media->passthru($method, $args);
  279. }
  280. if ($result === false) {
  281. return false;
  282. } elseif (is_a($result, 'Media_Process_Generic')) {
  283. $Media = $result;
  284. }
  285. }
  286. /* Determine destination file */
  287. $extension = null;
  288. if ($guessExtension) {
  289. if (isset($process['instructions']['convert'])) {
  290. $extension = Mime_Type::guessExtension($process['instructions']['convert']);
  291. } else {
  292. $extension = Mime_Type::guessExtension($file);
  293. }
  294. }
  295. $destination = $this->_destinationFile(
  296. $file, $process['directory'], $extension, $overwrite
  297. );
  298. if (!$destination) {
  299. return false;
  300. }
  301. return $Media->store($destination) && chmod($destination, $mode) ? $destination : false;
  302. }
  303. /**
  304. * Helper method to determine path to destination file and delete
  305. * it if necessary.
  306. *
  307. * @param string $source Path to or name of source file.
  308. * @param string $directory Path to directory.
  309. * @param string $extension Optionally an extension to append to the final path.
  310. * @param boolean $overwrite If true will unlink destination if it exists, defaults to false.
  311. * @return string Path to destination file.
  312. */
  313. protected function _destinationFile($source, $directory, $extension = null, $overwrite = false) {
  314. $destination = $directory;
  315. if ($extension) {
  316. $destination .= pathinfo($source, PATHINFO_FILENAME) . '.' . $extension;
  317. } else {
  318. $destination .= basename($source);
  319. }
  320. if (file_exists($destination) || is_link($destination)) {
  321. if (!$overwrite) {
  322. return false;
  323. }
  324. unlink($destination);
  325. }
  326. return $destination;
  327. }
  328. /**
  329. * Returns relative and absolute path to a file
  330. *
  331. * @param Model $Model
  332. * @param string $file
  333. * @return array
  334. */
  335. protected function _file(&$Model, $file) {
  336. extract($this->settings[$Model->alias]);
  337. $file = str_replace(array('\\', '/'), DS, $file);
  338. if (!is_file($file)) {
  339. $file = ltrim($file, DS);
  340. $relativeFile = $file;
  341. $file = $baseDirectory . $file;
  342. } else {
  343. $relativeFile = str_replace($baseDirectory, null, $file);
  344. }
  345. return array($file, $relativeFile);
  346. }
  347. }