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