Version 1
[yaffs-website] / web / core / modules / migrate / src / Plugin / migrate / process / FileCopy.php
1 <?php
2
3 namespace Drupal\migrate\Plugin\migrate\process;
4
5 use Drupal\Core\File\FileSystemInterface;
6 use Drupal\Core\Plugin\ContainerFactoryPluginInterface;
7 use Drupal\Core\StreamWrapper\LocalStream;
8 use Drupal\Core\StreamWrapper\StreamWrapperManagerInterface;
9 use Drupal\migrate\MigrateException;
10 use Drupal\migrate\MigrateExecutableInterface;
11 use Drupal\migrate\Plugin\MigrateProcessInterface;
12 use Drupal\migrate\ProcessPluginBase;
13 use Drupal\migrate\Row;
14 use Symfony\Component\DependencyInjection\ContainerInterface;
15
16 /**
17  * Copies or moves a local file from one place into another.
18  *
19  * The file can be moved, reused, or set to be automatically renamed if a
20  * duplicate exists.
21  *
22  * The source value is an array of two values:
23  * - source: The source path or URI, e.g. '/path/to/foo.txt' or
24  *   'public://bar.txt'.
25  * - destination: The destination path or URI, e.g. '/path/to/bar.txt' or
26  *   'public://foo.txt'.
27  *
28  * Available configuration keys:
29  * - move: (optional) Boolean, if TRUE, move the file, otherwise copy the file.
30  *   Defaults to FALSE.
31  * - rename: (optional) Boolean, if TRUE, rename the file by appending a number
32  *   until the name is unique. Defaults to FALSE.
33  * - reuse: (optional) Boolean, if TRUE, reuse the current file in its existing
34  *   location rather than move/copy/rename the file. Defaults to FALSE.
35  *
36  * Examples:
37  *
38  * @code
39  * process:
40  *   path_to_file:
41  *     plugin: file_copy
42  *     source: /path/to/file.png
43  *     destination: /new/path/to/file.png
44  * @endcode
45  *
46  * @see \Drupal\migrate\Plugin\MigrateProcessInterface
47  *
48  * @MigrateProcessPlugin(
49  *   id = "file_copy"
50  * )
51  */
52 class FileCopy extends ProcessPluginBase implements ContainerFactoryPluginInterface {
53
54   /**
55    * The stream wrapper manager service.
56    *
57    * @var \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface
58    */
59   protected $streamWrapperManager;
60
61   /**
62    * The file system service.
63    *
64    * @var \Drupal\Core\File\FileSystemInterface
65    */
66   protected $fileSystem;
67
68   /**
69    * An instance of the download process plugin.
70    *
71    * @var \Drupal\migrate\Plugin\MigrateProcessInterface
72    */
73   protected $downloadPlugin;
74
75   /**
76    * Constructs a file_copy process plugin.
77    *
78    * @param array $configuration
79    *   The plugin configuration.
80    * @param string $plugin_id
81    *   The plugin ID.
82    * @param mixed $plugin_definition
83    *   The plugin definition.
84    * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrappers
85    *   The stream wrapper manager service.
86    * @param \Drupal\Core\File\FileSystemInterface $file_system
87    *   The file system service.
88    * @param \Drupal\migrate\Plugin\MigrateProcessInterface $download_plugin
89    *   An instance of the download plugin for handling remote URIs.
90    */
91   public function __construct(array $configuration, $plugin_id, array $plugin_definition, StreamWrapperManagerInterface $stream_wrappers, FileSystemInterface $file_system, MigrateProcessInterface $download_plugin) {
92     $configuration += [
93       'move' => FALSE,
94       'rename' => FALSE,
95       'reuse' => FALSE,
96     ];
97     parent::__construct($configuration, $plugin_id, $plugin_definition);
98     $this->streamWrapperManager = $stream_wrappers;
99     $this->fileSystem = $file_system;
100     $this->downloadPlugin = $download_plugin;
101   }
102
103   /**
104    * {@inheritdoc}
105    */
106   public static function create(ContainerInterface $container, array $configuration, $plugin_id, $plugin_definition) {
107     return new static(
108       $configuration,
109       $plugin_id,
110       $plugin_definition,
111       $container->get('stream_wrapper_manager'),
112       $container->get('file_system'),
113       $container->get('plugin.manager.migrate.process')->createInstance('download')
114     );
115   }
116
117   /**
118    * {@inheritdoc}
119    */
120   public function transform($value, MigrateExecutableInterface $migrate_executable, Row $row, $destination_property) {
121     // If we're stubbing a file entity, return a URI of NULL so it will get
122     // stubbed by the general process.
123     if ($row->isStub()) {
124       return NULL;
125     }
126     list($source, $destination) = $value;
127
128     // If the source path or URI represents a remote resource, delegate to the
129     // download plugin.
130     if (!$this->isLocalUri($source)) {
131       return $this->downloadPlugin->transform($value, $migrate_executable, $row, $destination_property);
132     }
133
134     // Ensure the source file exists, if it's a local URI or path.
135     if (!file_exists($source)) {
136       throw new MigrateException("File '$source' does not exist");
137     }
138
139     // If the start and end file is exactly the same, there is nothing to do.
140     if ($this->isLocationUnchanged($source, $destination)) {
141       return $destination;
142     }
143
144     // Check if a writable directory exists, and if not try to create it.
145     $dir = $this->getDirectory($destination);
146     // If the directory exists and is writable, avoid file_prepare_directory()
147     // call and write the file to destination.
148     if (!is_dir($dir) || !is_writable($dir)) {
149       if (!file_prepare_directory($dir, FILE_CREATE_DIRECTORY | FILE_MODIFY_PERMISSIONS)) {
150         throw new MigrateException("Could not create or write to directory '$dir'");
151       }
152     }
153
154     $final_destination = $this->writeFile($source, $destination, $this->getOverwriteMode());
155     if ($final_destination) {
156       return $final_destination;
157     }
158     throw new MigrateException("File $source could not be copied to $destination");
159   }
160
161   /**
162    * Tries to move or copy a file.
163    *
164    * @param string $source
165    *   The source path or URI.
166    * @param string $destination
167    *   The destination path or URI.
168    * @param int $replace
169    *   (optional) FILE_EXISTS_REPLACE (default) or FILE_EXISTS_RENAME.
170    *
171    * @return string|bool
172    *   File destination on success, FALSE on failure.
173    */
174   protected function writeFile($source, $destination, $replace = FILE_EXISTS_REPLACE) {
175     // Check if there is a destination available for copying. If there isn't,
176     // it already exists at the destination and the replace flag tells us to not
177     // replace it. In that case, return the original destination.
178     if (!($final_destination = file_destination($destination, $replace))) {
179       return $destination;
180     }
181     $function = 'file_unmanaged_' . ($this->configuration['move'] ? 'move' : 'copy');
182     return $function($source, $destination, $replace);
183   }
184
185   /**
186    * Determines how to handle file conflicts.
187    *
188    * @return int
189    *   FILE_EXISTS_REPLACE (default), FILE_EXISTS_RENAME, or FILE_EXISTS_ERROR
190    *   depending on the current configuration.
191    */
192   protected function getOverwriteMode() {
193     if (!empty($this->configuration['rename'])) {
194       return FILE_EXISTS_RENAME;
195     }
196     if (!empty($this->configuration['reuse'])) {
197       return FILE_EXISTS_ERROR;
198     }
199
200     return FILE_EXISTS_REPLACE;
201   }
202
203   /**
204    * Returns the directory component of a URI or path.
205    *
206    * For URIs like public://foo.txt, the full physical path of public://
207    * will be returned, since a scheme by itself will trip up certain file
208    * API functions (such as file_prepare_directory()).
209    *
210    * @param string $uri
211    *   The URI or path.
212    *
213    * @return string|false
214    *   The directory component of the path or URI, or FALSE if it could not
215    *   be determined.
216    */
217   protected function getDirectory($uri) {
218     $dir = $this->fileSystem->dirname($uri);
219     if (substr($dir, -3) == '://') {
220       return $this->fileSystem->realpath($dir);
221     }
222     return $dir;
223   }
224
225   /**
226    * Determines if the source and destination URIs represent identical paths.
227    *
228    * @param string $source
229    *   The source URI.
230    * @param string $destination
231    *   The destination URI.
232    *
233    * @return bool
234    *   TRUE if the source and destination URIs refer to the same physical path,
235    *   otherwise FALSE.
236    */
237   protected function isLocationUnchanged($source, $destination) {
238     return $this->fileSystem->realpath($source) === $this->fileSystem->realpath($destination);
239   }
240
241   /**
242    * Determines if the given URI or path is considered local.
243    *
244    * A URI or path is considered local if it either has no scheme component,
245    * or the scheme is implemented by a stream wrapper which extends
246    * \Drupal\Core\StreamWrapper\LocalStream.
247    *
248    * @param string $uri
249    *   The URI or path to test.
250    *
251    * @return bool
252    */
253   protected function isLocalUri($uri) {
254     $scheme = $this->fileSystem->uriScheme($uri);
255
256     // The vfs scheme is vfsStream, which is used in testing. vfsStream is a
257     // simulated file system that exists only in memory, but should be treated
258     // as a local resource.
259     if ($scheme == 'vfs') {
260       $scheme = FALSE;
261     }
262     return $scheme === FALSE || $this->streamWrapperManager->getViaScheme($scheme) instanceof LocalStream;
263   }
264
265 }