file.inc 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588
  1. <?php
  2. /**
  3. * @file
  4. * Support for file entity as destination. Note that File Fields have their
  5. * own destination in fields.inc
  6. */
  7. /**
  8. * Interface for taking some value representing a file and returning
  9. * a Drupal file entity (creating the entity if necessary).
  10. */
  11. interface MigrateFileInterface {
  12. /**
  13. * Return a list of subfields and options specific to this implementation,
  14. * keyed by name.
  15. */
  16. public static function fields();
  17. /**
  18. * Create or link to a Drupal file entity.
  19. *
  20. * @param $value
  21. * A class-specific value (URI, pre-existing file ID, file blob, ...)
  22. * representing file content.
  23. *
  24. * @param $owner
  25. * uid of an account to be recorded as the file owner.
  26. *
  27. * @return object
  28. * File entity being created or referenced.
  29. */
  30. public function processFile($value, $owner);
  31. }
  32. /**
  33. * Handle the degenerate case where we already have a file ID.
  34. */
  35. class MigrateFileFid implements MigrateFileInterface {
  36. /**
  37. * Implementation of MigrateFileInterface::fields().
  38. *
  39. * @return array
  40. */
  41. static public function fields() {
  42. return array();
  43. }
  44. /**
  45. * Implementation of MigrateFileInterface::processFile().
  46. *
  47. * @param $value
  48. * An existing file entity ID (fid).
  49. * @param $owner
  50. * User ID (uid) to be the owner of the file. Ignored in this case.
  51. * @return int
  52. * The file entity corresponding to the fid that was passed in.
  53. */
  54. public function processFile($value, $owner) {
  55. return file_load($value);
  56. }
  57. }
  58. /**
  59. * Base class for creating core file entities.
  60. */
  61. abstract class MigrateFile implements MigrateFileInterface {
  62. /**
  63. * Extension of the core FILE_EXISTS_* constants, offering an alternative to
  64. * reuse the existing file if present as-is (core only offers the options of
  65. * replacing it or renaming to avoid collision).
  66. */
  67. const FILE_EXISTS_REUSE = -1;
  68. /**
  69. * The destination directory within Drupal.
  70. *
  71. * @var string
  72. */
  73. protected $destinationDir = 'public://';
  74. /**
  75. * The filename relative to destinationDir to which to save the current file.
  76. *
  77. * @var string
  78. */
  79. protected $destinationFile = '';
  80. /**
  81. * How to handle destination filename collisions.
  82. *
  83. * @var int
  84. */
  85. protected $fileReplace = FILE_EXISTS_RENAME;
  86. /**
  87. * Set to TRUE to prevent file deletion on rollback.
  88. *
  89. * @var bool
  90. */
  91. protected $preserveFiles = FALSE;
  92. /**
  93. * An optional file object to use as a default starting point for building the
  94. * file entity.
  95. *
  96. * @var stdClass
  97. */
  98. protected $defaultFile;
  99. public function __construct($arguments = array(), $default_file = NULL) {
  100. if (isset($arguments['destination_dir'])) {
  101. $this->destinationDir = $arguments['destination_dir'];
  102. }
  103. if (isset($arguments['destination_file'])) {
  104. $this->destinationFile = $arguments['destination_file'];
  105. }
  106. if (isset($arguments['file_replace'])) {
  107. $this->fileReplace = $arguments['file_replace'];
  108. }
  109. if (isset($arguments['preserve_files'])) {
  110. $this->preserveFiles = $arguments['preserve_files'];
  111. }
  112. if ($default_file) {
  113. $this->defaultFile = $default_file;
  114. }
  115. else {
  116. $this->defaultFile = new stdClass;
  117. }
  118. }
  119. /**
  120. * Implementation of MigrateFileInterface::fields().
  121. *
  122. * @return array
  123. */
  124. static public function fields() {
  125. return array(
  126. 'destination_dir' => t('Subfield: <a href="@doc">Path within Drupal files directory to store file</a>',
  127. array('@doc' => 'http://drupal.org/node/1540106#destination_dir')),
  128. 'destination_file' => t('Subfield: <a href="@doc">Path within destination_dir to store the file.</a>',
  129. array('@doc' => 'http://drupal.org/node/1540106#destination_file')),
  130. 'file_replace' => t('Option: <a href="@doc">Value of $replace in that file function. Does not apply to file_fast(). Defaults to FILE_EXISTS_RENAME.</a>',
  131. array('@doc' => 'http://drupal.org/node/1540106#file_replace')),
  132. 'preserve_files' => t('Option: <a href="@doc">Boolean indicating whether files should be preserved or deleted on rollback</a>',
  133. array('@doc' => 'http://drupal.org/node/1540106#preserve_files')),
  134. );
  135. }
  136. /**
  137. * Setup a file entity object suitable for saving.
  138. *
  139. * @param $destination
  140. * Path to the Drupal copy of the file.
  141. * @param $owner
  142. * Uid of the file owner.
  143. * @return stdClass
  144. * A file object ready to be saved.
  145. */
  146. protected function createFileEntity($destination, $owner) {
  147. $file = clone $this->defaultFile;
  148. $file->uri = $destination;
  149. $file->uid = $owner;
  150. if (!isset($file->filename)) {
  151. $file->filename = drupal_basename($destination);
  152. }
  153. if (!isset($file->filemime)) {
  154. $file->filemime = file_get_mimetype($destination);
  155. }
  156. if (!isset($file->status)) {
  157. $file->status = FILE_STATUS_PERMANENT;
  158. }
  159. // If we are replacing an existing file re-use its database record.
  160. if ($this->fileReplace == FILE_EXISTS_REPLACE) {
  161. $existing_files = file_load_multiple(array(), array('uri' => $destination));
  162. if (count($existing_files)) {
  163. $existing = reset($existing_files);
  164. $file->fid = $existing->fid;
  165. $file->filename = $existing->filename;
  166. }
  167. }
  168. return $file;
  169. }
  170. /**
  171. * By whatever appropriate means, put the file in the right place.
  172. *
  173. * @param $destination
  174. * Destination path within Drupal.
  175. * @return bool
  176. * TRUE if the file is successfully saved, FALSE otherwise.
  177. */
  178. abstract protected function copyFile($destination);
  179. /**
  180. * Default implementation of MigrateFileInterface::processFiles().
  181. *
  182. * @param $value
  183. * The URI or local filespec of a file to be imported.
  184. * @param $owner
  185. * User ID (uid) to be the owner of the file.
  186. * @return object
  187. * The file entity being created or referenced.
  188. */
  189. public function processFile($value, $owner) {
  190. $migration = Migration::currentMigration();
  191. // Determine the final path we want in Drupal - start with our preferred path.
  192. $destination = file_stream_wrapper_uri_normalize(
  193. $this->destinationDir . '/' .
  194. ltrim($this->destinationFile, "/\\"));
  195. // Our own file_replace behavior - if the file exists, use it without
  196. // replacing it
  197. if ($this->fileReplace == self::FILE_EXISTS_REUSE) {
  198. // See if we this file already (we'll reuse a file entity if it exists).
  199. if (file_exists($destination)) {
  200. $file = $this->createFileEntity($destination, $owner);
  201. // File entity didn't already exist, create it
  202. if (empty($file->fid)) {
  203. $file = file_save($file);
  204. }
  205. return $file;
  206. }
  207. // No existing one to reuse, reset to REPLACE
  208. $this->fileReplace = FILE_EXISTS_REPLACE;
  209. }
  210. // Prepare the destination directory.
  211. if (!file_prepare_directory(drupal_dirname($destination),
  212. FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
  213. $migration->saveMessage(t('Could not create destination directory for !dest',
  214. array('!dest' => $destination)));
  215. return FALSE;
  216. }
  217. // Determine whether we can perform this operation based on overwrite rules.
  218. $destination = file_destination($destination, $this->fileReplace);
  219. if ($destination === FALSE) {
  220. $migration->saveMessage(t('The file could not be copied because ' .
  221. 'file %dest already exists in the destination directory.',
  222. array('%dest' => $destination)));
  223. return FALSE;
  224. }
  225. // Make sure the .htaccess files are present.
  226. file_ensure_htaccess();
  227. // Put the file where it needs to be.
  228. if (!$this->copyFile($destination)) {
  229. return FALSE;
  230. }
  231. // Set the permissions on the new file.
  232. drupal_chmod($destination);
  233. // Create and save the file entity.
  234. $file = file_save($this->createFileEntity($destination, $owner));
  235. // Prevent deletion of the file on rollback if requested.
  236. if (is_object($file)) {
  237. if (!empty($this->preserveFiles)) {
  238. // We do this directly instead of calling file_usage_add, to force the
  239. // count to 1 - otherwise, updates will increment the counter and the file
  240. // will never be deletable
  241. db_merge('file_usage')
  242. ->key(array(
  243. 'fid' => $file->fid,
  244. 'module' => 'migrate',
  245. 'type' => 'file',
  246. 'id' => $file->fid,
  247. ))
  248. ->fields(array('count' => 1))
  249. ->execute();
  250. }
  251. return $file;
  252. }
  253. else {
  254. return FALSE;
  255. }
  256. }
  257. }
  258. /**
  259. * Handle cases where we're handed a URI, or local filespec, representing a file
  260. * to be imported to Drupal.
  261. */
  262. class MigrateFileUri extends MigrateFile {
  263. /**
  264. * The source directory for the file, relative to which the value (source
  265. * file) will be taken.
  266. *
  267. * @var string
  268. */
  269. protected $sourceDir = '';
  270. /**
  271. * The full path to the source file.
  272. *
  273. * @var string
  274. */
  275. protected $sourcePath = '';
  276. public function __construct($arguments = array(), $default_file = NULL) {
  277. parent::__construct($arguments, $default_file);
  278. if (isset($arguments['source_dir'])) {
  279. $this->sourceDir = rtrim($arguments['source_dir'], "/\\");
  280. }
  281. }
  282. /**
  283. * Implementation of MigrateFileInterface::fields().
  284. *
  285. * @return array
  286. */
  287. static public function fields() {
  288. return parent::fields() +
  289. array(
  290. 'source_dir' => t('Subfield: <a href="@doc">Path to source file.</a>',
  291. array('@doc' => 'http://drupal.org/node/1540106#source_dir')),
  292. );
  293. }
  294. /**
  295. * Implementation of MigrateFile::copyFile().
  296. *
  297. * @param $destination
  298. * Destination within Drupal.
  299. *
  300. * @return bool
  301. * TRUE if the copy succeeded, FALSE otherwise.
  302. */
  303. protected function copyFile($destination) {
  304. // Perform the copy operation.
  305. if (!@copy($this->sourcePath, $destination)) {
  306. throw new MigrateException(t('The specified file %file could not be copied to ' .
  307. '%destination.',
  308. array('%file' => $this->sourcePath, '%destination' => $destination)));
  309. }
  310. else {
  311. return TRUE;
  312. }
  313. }
  314. /**
  315. * Implementation of MigrateFileInterface::processFiles().
  316. *
  317. * @param $value
  318. * The URI or local filespec of a file to be imported.
  319. * @param $owner
  320. * User ID (uid) to be the owner of the file.
  321. * @return object
  322. * The file entity being created or referenced.
  323. */
  324. public function processFile($value, $owner) {
  325. // Identify the full path to the source file
  326. if (!empty($this->sourceDir)) {
  327. $this->sourcePath = rtrim($this->sourceDir, "/\\") . '/' . ltrim($value, "/\\");
  328. }
  329. else {
  330. $this->sourcePath = $value;
  331. }
  332. if (empty($this->destinationFile)) {
  333. $this->destinationFile = basename($this->sourcePath);
  334. }
  335. // MigrateFile has most of the smarts - the key is that it will call back
  336. // to our copyFile() implementation.
  337. $file = parent::processFile($value, $owner);
  338. return $file;
  339. }
  340. }
  341. /**
  342. * Handle cases where we're handed a blob (i.e., the actual contents of a file,
  343. * such as image data) to be stored as a real file in Drupal.
  344. */
  345. class MigrateFileBlob extends MigrateFile {
  346. /**
  347. * The file contents we will be writing to a real file.
  348. *
  349. * @var
  350. */
  351. protected $fileContents;
  352. /**
  353. * Implementation of MigrateFile::copyFile().
  354. *
  355. * @param $destination
  356. * Drupal destination path.
  357. * @return bool
  358. * TRUE if the file contents were successfully written, FALSE otherwise.
  359. */
  360. protected function copyFile($destination) {
  361. if (file_put_contents($destination, $this->fileContents)) {
  362. return TRUE;
  363. }
  364. else {
  365. $migration = Migration::currentMigration();
  366. $migration->saveMessage(t('Failed to write blob data to %destination',
  367. array('%destination' => $destination)));
  368. return FALSE;
  369. }
  370. }
  371. /**
  372. * Implementation of MigrateFileInterface::processFile().
  373. *
  374. * @param $value
  375. * The file contents to be saved as a file.
  376. * @param $owner
  377. * User ID (uid) to be the owner of the file.
  378. * @return object
  379. * File entity being created or referenced.
  380. */
  381. public function processFile($value, $owner) {
  382. $this->fileContents = $value;
  383. $file = parent::processFile($value, $owner);
  384. return $file;
  385. }
  386. }
  387. /**
  388. * Destination class implementing migration into the files table.
  389. */
  390. class MigrateDestinationFile extends MigrateDestinationEntity {
  391. /**
  392. * File class (MigrateFileUri etc.) doing the dirty wrk.
  393. *
  394. * @var string
  395. */
  396. protected $fileClass;
  397. /**
  398. * Implementation of MigrateDestination::getKeySchema().
  399. *
  400. * @return array
  401. */
  402. static public function getKeySchema() {
  403. return array(
  404. 'fid' => array(
  405. 'type' => 'int',
  406. 'unsigned' => TRUE,
  407. 'description' => 'file_managed ID',
  408. ),
  409. );
  410. }
  411. /**
  412. * Basic initialization
  413. *
  414. * @param array $options
  415. * Options applied to files.
  416. */
  417. public function __construct($bundle = 'file', $file_class = 'MigrateFileUri',
  418. $options = array()) {
  419. parent::__construct('file', $bundle, $options);
  420. $this->fileClass = $file_class;
  421. }
  422. /**
  423. * Returns a list of fields available to be mapped for the entity type (bundle)
  424. *
  425. * @param Migration $migration
  426. * Optionally, the migration containing this destination.
  427. * @return array
  428. * Keys: machine names of the fields (to be passed to addFieldMapping)
  429. * Values: Human-friendly descriptions of the fields.
  430. */
  431. public function fields($migration = NULL) {
  432. $fields = array();
  433. // First the core properties
  434. $fields['fid'] = t('File: Existing file ID');
  435. $fields['uid'] = t('File: Uid of user associated with file');
  436. $fields['value'] = t('File: Representation of the source file (usually a URI)');
  437. $fields['timestamp'] = t('File: UNIX timestamp for the date the file was added');
  438. // Then add in anything provided by handlers
  439. $fields += migrate_handler_invoke_all('Entity', 'fields', $this->entityType, $this->bundle, $migration);
  440. $fields += migrate_handler_invoke_all('File', 'fields', $this->entityType, $this->bundle, $migration);
  441. // Plus anything provided by the file class
  442. $fields += call_user_func(array($this->fileClass, 'fields'));
  443. return $fields;
  444. }
  445. /**
  446. * Delete a file entry.
  447. *
  448. * @param array $fid
  449. * Fid to delete, arrayed.
  450. */
  451. public function rollback(array $fid) {
  452. migrate_instrument_start('file_load');
  453. $file = file_load(reset($fid));
  454. migrate_instrument_stop('file_load');
  455. if ($file) {
  456. // If we're not preserving the file, make sure we do the job completely.
  457. migrate_instrument_start('file_delete');
  458. file_delete($file, TRUE);
  459. migrate_instrument_stop('file_delete');
  460. }
  461. }
  462. /**
  463. * Import a single file record.
  464. *
  465. * @param $file
  466. * File object to build. Prefilled with any fields mapped in the Migration.
  467. * @param $row
  468. * Raw source data object - passed through to prepare/complete handlers.
  469. * @return array
  470. * Array of key fields (fid only in this case) of the file that was saved if
  471. * successful. FALSE on failure.
  472. */
  473. public function import(stdClass $file, stdClass $row) {
  474. // Updating previously-migrated content?
  475. $migration = Migration::currentMigration();
  476. if (isset($row->migrate_map_destid1)) {
  477. if (isset($file->fid)) {
  478. if ($file->fid != $row->migrate_map_destid1) {
  479. throw new MigrateException(t("Incoming fid !fid and map destination fid !destid1 don't match",
  480. array('!fid' => $file->fid, '!destid1' => $row->migrate_map_destid1)));
  481. }
  482. }
  483. else {
  484. $file->fid = $row->migrate_map_destid1;
  485. }
  486. }
  487. if ($migration->getSystemOfRecord() == Migration::DESTINATION) {
  488. if (!isset($file->fid)) {
  489. throw new MigrateException(t('System-of-record is DESTINATION, but no destination fid provided'));
  490. }
  491. $old_file = file_load($file->fid);
  492. }
  493. // Invoke migration prepare handlers
  494. $this->prepare($file, $row);
  495. if (isset($file->fid)) {
  496. $updating = TRUE;
  497. }
  498. else {
  499. $updating = FALSE;
  500. }
  501. if (!isset($file->uid)) {
  502. $file->uid = 1;
  503. }
  504. // file_save() unconditionally sets timestamp - if we have an explicit
  505. // value we want, we need to set it manually after file_save.
  506. if (isset($file->timestamp)) {
  507. $timestamp = MigrationBase::timestamp($file->timestamp);
  508. }
  509. $file_class = $this->fileClass;
  510. $source = new $file_class((array)$file, $file);
  511. $file = $source->processFile($file->value, $file->uid);
  512. if (is_object($file) && isset($file->fid)) {
  513. $this->complete($file, $row);
  514. if (isset($timestamp)) {
  515. db_update('file_managed')
  516. ->fields(array('timestamp' => $timestamp))
  517. ->condition('fid', $file->fid)
  518. ->execute();
  519. $file->timestamp = $timestamp;
  520. }
  521. $return = array($file->fid);
  522. if ($updating) {
  523. $this->numUpdated++;
  524. }
  525. else {
  526. $this->numCreated++;
  527. }
  528. }
  529. else {
  530. $return = FALSE;
  531. }
  532. return $return;
  533. }
  534. }