PageRenderTime 45ms CodeModel.GetById 17ms RepoModel.GetById 0ms app.codeStats 0ms

/vendors/shells/tasks/sync.php

http://github.com/davidpersson/media
PHP | 479 lines | 306 code | 35 blank | 138 comment | 20 complexity | f20269cec9a380684776ff3d93998f48 MD5 | raw file
Possible License(s): MIT
  1. <?php
  2. /**
  3. * Sync Task 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.shells.tasks
  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. /**
  20. * Sync Task Class
  21. *
  22. * By default the task starts in interactive mode. Below you’ll find an example
  23. * for a cronjob for invoking the task on a regular basis to automate repairing
  24. * files and records which are not in sync.
  25. * {{{
  26. * #!/bin/bash
  27. *
  28. * CAKE_CORE_INCLUDE_PATH=/usr/local/share/cake-1.2.x.x
  29. * CAKE_CONSOLE=${CAKE_CORE_INCLUDE_PATH}/cake/console/cake
  30. * APP="/path/to/your/app"
  31. * MODELS="example attachments" # Models separated by one space
  32. * AUTO="-auto" # Enables automatic (destructive) repair!
  33. *
  34. * if [ ! -x $CAKE_CONSOLE ] || [ ! -x $APP ]; then
  35. * exit 1
  36. * fi
  37. *
  38. * for MODEL in $MODELS; do
  39. * $CAKE_CONSOLE media sync -app $APP -quiet $AUTO $MODEL
  40. * test $? != 0 && exit 1
  41. * done
  42. *
  43. * exit 0
  44. * }}}
  45. *
  46. * @package media
  47. * @subpackage media.shells.tasks
  48. */
  49. class SyncTask extends MediaShell {
  50. /**
  51. * model
  52. *
  53. * @var string
  54. */
  55. public $model;
  56. /**
  57. * Directory
  58. *
  59. * @var string
  60. */
  61. public $directory;
  62. /**
  63. * Default answer to use if prompted for input
  64. *
  65. * @var string
  66. */
  67. protected $_answer = 'n';
  68. /**
  69. * Verbosity of output, control via argument `-quiet`
  70. *
  71. * @var boolean
  72. */
  73. protected $_quiet;
  74. /**
  75. * Model
  76. *
  77. * @var Model
  78. */
  79. protected $_Model;
  80. /**
  81. * baseDirectory from the model's media behavior settings
  82. *
  83. * @var string
  84. */
  85. protected $_baseDirectory;
  86. /**
  87. * Folder to search
  88. *
  89. * @var Folder
  90. */
  91. protected $_Folder;
  92. /**
  93. * Current item retrieved from the model
  94. *
  95. * @var array
  96. */
  97. private $__dbItem;
  98. /**
  99. * Current item retrieved from the filesystem
  100. *
  101. * @var array
  102. */
  103. private $__fsItem;
  104. /**
  105. * Current set of items retrieved from the model
  106. *
  107. * @var array
  108. */
  109. private $__dbMap;
  110. /**
  111. * Current set of items retrieved from the filesystem
  112. *
  113. * @var array
  114. */
  115. private $__fsMap;
  116. /**
  117. * Current file object
  118. *
  119. * @var File
  120. */
  121. private $__File;
  122. /**
  123. * An alternative for the current file
  124. *
  125. * @var mixed
  126. */
  127. private $__alternativeFile;
  128. /**
  129. * Main execution method
  130. *
  131. * @return boolean
  132. */
  133. public function execute() {
  134. $this->_answer = isset($this->params['auto']) ? 'y' : 'n';
  135. $this->model = array_shift($this->args);
  136. $this->directory = array_shift($this->args);
  137. $this->_quiet = isset($this->params['quiet']);
  138. if (!isset($this->model)) {
  139. $this->model = $this->in('Name of model:', null, 'Media.Attachment');
  140. }
  141. if (!isset($this->directory)) {
  142. $this->directory = $this->in('Directory to search:', null, MEDIA_TRANSFER);
  143. }
  144. $this->_Model = ClassRegistry::init($this->model);
  145. if (!isset($this->_Model->Behaviors->Coupler)) {
  146. $this->err('CouplerBehavior is not attached to Model');
  147. return false;
  148. }
  149. $this->_baseDirectory = $this->_Model->Behaviors->Coupler->settings[$this->_Model->alias]['baseDirectory'];
  150. $this->_Folder = new Folder($this->directory);
  151. $this->interactive = !isset($this->model, $this->directory);
  152. if ($this->interactive) {
  153. $input = $this->in('Interactive?', 'y/n', 'y');
  154. if ($input == 'n') {
  155. $this->interactive = false;
  156. }
  157. }
  158. $this->out();
  159. $this->out(sprintf('%-25s: %s', 'Model', $this->_Model->name));
  160. $this->out(sprintf('%-25s: %s', 'Search directory', $this->_Folder->pwd()));
  161. $this->out(sprintf('%-25s: %s', 'Automatic repair', $this->_answer == 'y' ? 'yes' : 'no'));
  162. if ($this->in('Looks OK?', 'y,n', 'y') == 'n') {
  163. return false;
  164. }
  165. $this->_Model->Behaviors->disable('Coupler');
  166. $this->_checkFilesWithRecords();
  167. $this->_checkRecordsWithFiles();
  168. $this->_Model->Behaviors->enable('Coupler');
  169. $this->out();
  170. return true;
  171. }
  172. /**
  173. * Checks if files are in sync with records
  174. *
  175. * @return void
  176. */
  177. protected function _checkFilesWithRecords() {
  178. $this->out('');
  179. $this->out('Checking if files are in sync with records');
  180. $this->hr();
  181. list($this->__fsMap, $this->__dbMap) = $this->_generateMaps();
  182. foreach ($this->__dbMap as $dbItem) {
  183. $message = sprintf(
  184. '%-60s -> %s/%s',
  185. $this->shortPath($dbItem['file']),
  186. $this->_Model->name, $dbItem['id']
  187. );
  188. if (!$this->_quiet) {
  189. $this->out($message);
  190. }
  191. $this->__dbItem = $dbItem;
  192. $this->__File = new File($dbItem['file']);
  193. $this->__alternativeFile = $this->_findByChecksum($dbItem['checksum'], $this->__fsMap);
  194. if ($this->_findByFile($this->__alternativeFile, $this->__dbMap)) {
  195. $this->__alternativeFile = false;
  196. }
  197. if ($this->_handleNotReadable()) {
  198. continue;
  199. }
  200. if ($this->_handleOrphanedRecord()) {
  201. continue;
  202. }
  203. if ($this->_handleChecksumMismatch()) {
  204. continue;
  205. }
  206. }
  207. $this->out('Done.');
  208. }
  209. /**
  210. * Checks if records are in sync with files
  211. *
  212. * @return void
  213. */
  214. protected function _checkRecordsWithFiles() {
  215. $this->out('');
  216. $this->out('Checking if records are in sync with files');
  217. $this->hr();
  218. list($this->__fsMap, $this->__dbMap) = $this->_generateMaps();
  219. foreach ($this->__fsMap as $fsItem) {
  220. $message = sprintf(
  221. '%-60s <- %s/%s',
  222. $this->shortPath($fsItem['file']),
  223. $this->_Model->name,
  224. '?'
  225. );
  226. if (!$this->_quiet) {
  227. $this->out($message);
  228. }
  229. $this->__File = new File($fsItem['file']);
  230. $this->__fsItem = $fsItem;
  231. if ($this->_handleOrphanedFile()) {
  232. continue;
  233. }
  234. }
  235. $this->out('Done.');
  236. }
  237. /* handle methods */
  238. /**
  239. * Handles existent but not readable files
  240. *
  241. * @return mixed
  242. */
  243. protected function _handleNotReadable() {
  244. if (!$this->__File->readable() && $this->__File->exists()) {
  245. $message = 'File `' . $this->shortPath($this->__File->pwd()) . '`';
  246. $message .= ' exists but is not readable.';
  247. $this->out($message);
  248. return true;
  249. }
  250. return false;
  251. }
  252. /**
  253. * Handles orphaned records
  254. *
  255. * @return mixed
  256. */
  257. protected function _handleOrphanedRecord() {
  258. if ($this->__File->exists()) {
  259. return;
  260. }
  261. $message = "Record is orphaned. It's pointing to non-existent ";
  262. $message .= 'file `' . $this->shortPath($this->__File->pwd()) . '`.';
  263. $this->out($message);
  264. if ($this->_fixWithAlternative()) {
  265. return true;
  266. }
  267. if ($this->_fixDeleteRecord()) {
  268. return true;
  269. }
  270. return false;
  271. }
  272. /**
  273. * Handles mismatching checksums
  274. *
  275. * @return mixed
  276. */
  277. protected function _handleChecksumMismatch() {
  278. if (!$this->__File->exists()) {
  279. return;
  280. }
  281. if ($this->__dbItem['checksum'] == $this->__File->md5(true)) {
  282. return;
  283. }
  284. $message = 'The checksums for file `' . $this->shortPath($this->__File->pwd()) . '`';
  285. $message .= ' and its corresponding record mismatch.';
  286. $this->out($message);
  287. if ($this->_fixWithAlternative()) {
  288. return true;
  289. }
  290. $input = $this->in('Correct the checksum of the record?', 'y,n', $this->_answer);
  291. if ($input == 'y') {
  292. $data = array(
  293. 'id' => $this->__dbItem['id'],
  294. 'checksum' => $this->__File->md5(true),
  295. );
  296. $this->_Model->save($data);
  297. $this->out('Corrected checksum');
  298. return true;
  299. }
  300. if ($this->_fixDeleteRecord()) {
  301. return true;
  302. }
  303. }
  304. /**
  305. * Handles orphaned files
  306. *
  307. * @return mixed
  308. */
  309. protected function _handleOrphanedFile() {
  310. if ($this->_findByFile($this->__fsItem['file'], $this->__dbMap)) {
  311. return;
  312. }
  313. $message = 'File `' . $this->shortPath($this->__File->pwd()) . '`';
  314. $message .= ' is orphaned.';
  315. $this->out($message);
  316. $input = $this->in('Delete file?', 'y,n', $this->_answer);
  317. if ($input == 'y') {
  318. $this->__File->delete();
  319. $this->out('File deleted');
  320. return true;
  321. }
  322. }
  323. /* fix methods */
  324. /**
  325. * Updates a record with an alternative file
  326. *
  327. * @return boolean
  328. */
  329. protected function _fixWithAlternative() {
  330. if (!$this->__alternativeFile) {
  331. return false;
  332. }
  333. $message = sprintf(
  334. 'This file has an identical checksum: %s',
  335. $this->shortPath($this->__alternativeFile)
  336. );
  337. $this->out($message);
  338. $input = $this->in('Select this file and update record?', 'y,n', $this->_answer);
  339. if ($input == 'n') {
  340. return false;
  341. }
  342. $data = array(
  343. 'id' => $this->__dbItem['id'],
  344. 'dirname' => dirname(str_replace($this->_baseDirectory, '', $this->__alternativeFile)),
  345. 'basename' => basename($this->__alternativeFile),
  346. );
  347. $this->_Model->save($data);
  348. $this->out('Corrected dirname and basename');
  349. return true;
  350. }
  351. /**
  352. * Deletes current record
  353. *
  354. * @return booelan
  355. */
  356. protected function _fixDeleteRecord() {
  357. $input = $this->in('Delete record?', 'y,n', $this->_answer);
  358. if ($input == 'y') {
  359. $this->_Model->delete($this->__dbItem['id']);
  360. $this->out('Record deleted');
  361. return true;
  362. }
  363. return false;
  364. }
  365. /* map related methods */
  366. /**
  367. * Generates filesystem and model maps
  368. *
  369. * @return void
  370. */
  371. protected function _generateMaps() {
  372. $fsFiles = $this->_Folder->findRecursive();
  373. $results = $this->_Model->find('all');
  374. $fsMap = array();
  375. $dbMap = array();
  376. foreach ($fsFiles as $value) {
  377. $File = new File($value);
  378. $fsMap[] = array(
  379. 'file' => $File->pwd(),
  380. 'checksum' => $File->md5(true)
  381. );
  382. }
  383. foreach ($results as $result) {
  384. $dbMap[] = array(
  385. 'id' => $result[$this->_Model->name]['id'],
  386. 'file' => $this->_baseDirectory
  387. . $result[$this->_Model->name]['dirname']
  388. . DS . $result[$this->_Model->name]['basename'],
  389. 'checksum' => $result[$this->_Model->name]['checksum'],
  390. );
  391. }
  392. return array($fsMap, $dbMap);
  393. }
  394. /**
  395. * Finds an item's file by it's checksum
  396. *
  397. * @param string $checksum
  398. * @param array $map Map to use as a haystack
  399. * @return mixed
  400. */
  401. protected function _findByChecksum($checksum, $map) {
  402. foreach ($map as $item) {
  403. if ($checksum == $item['checksum']) {
  404. return $item['file'];
  405. }
  406. }
  407. return false;
  408. }
  409. /**
  410. * Finds an item's file by it's name
  411. *
  412. * @param string $file
  413. * @param array $map Map to use as a haystack
  414. * @return mixed
  415. */
  416. protected function _findByFile($file, $map) {
  417. foreach ($map as $item) {
  418. if ($file == $item['file']) {
  419. return $item['file'];
  420. }
  421. }
  422. return false;
  423. }
  424. }