PluginProbe
Media Cloud Sync / trunk
Media Cloud Sync vtrunk
1.4.1 1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 All 35 releases
media-cloud-sync / includes / sdk / s3 / Aws / Multipart / AbstractUploadManager.php

AbstractUploadManager.php in Media Cloud Sync trunk, at includes/sdk/s3/Aws/Multipart/AbstractUploadManager.php

258 lines 10.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2
3 namespace Dudlewebs\WPMCS\s3\Aws\Multipart;
4
5 use Dudlewebs\WPMCS\s3\Aws\AwsClientInterface as Client;
6 use Dudlewebs\WPMCS\s3\Aws\CommandInterface;
7 use Dudlewebs\WPMCS\s3\Aws\CommandPool;
8 use Dudlewebs\WPMCS\s3\Aws\Exception\AwsException;
9 use Dudlewebs\WPMCS\s3\Aws\Exception\MultipartUploadException;
10 use Dudlewebs\WPMCS\s3\Aws\Result;
11 use Dudlewebs\WPMCS\s3\Aws\ResultInterface;
12 use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise;
13 use Dudlewebs\WPMCS\s3\GuzzleHttp\Promise\PromiseInterface;
14 use InvalidArgumentException as IAE;
15 use Dudlewebs\WPMCS\s3\Psr\Http\Message\RequestInterface;
16 /**
17 * Encapsulates the execution of a multipart upload to S3 or Glacier.
18 *
19 * @internal
20 */
21 abstract class AbstractUploadManager implements Promise\PromisorInterface
22 {
23 const DEFAULT_CONCURRENCY = 5;
24 /** @var array Default values for base multipart configuration */
25 private static $defaultConfig = ['part_size' => null, 'state' => null, 'concurrency' => self::DEFAULT_CONCURRENCY, 'prepare_data_source' => null, 'before_initiate' => null, 'before_upload' => null, 'before_complete' => null, 'exception_class' => MultipartUploadException::class];
26 /** @var Client Client used for the upload. */
27 protected $client;
28 /** @var array Configuration used to perform the upload. */
29 protected $config;
30 /** @var array Service-specific information about the upload workflow. */
31 protected $info;
32 /** @var PromiseInterface Promise that represents the multipart upload. */
33 protected $promise;
34 /** @var UploadState State used to manage the upload. */
35 protected $state;
36 /** @var bool Configuration used to indicate if upload progress will be displayed. */
37 protected $displayProgress;
38 /**
39 * @param Client $client
40 * @param array $config
41 */
42 public function __construct(Client $client, array $config = [])
43 {
44 $this->client = $client;
45 $this->info = $this->loadUploadWorkflowInfo();
46 $this->config = $config + self::$defaultConfig;
47 $this->state = $this->determineState();
48 if (isset($config['display_progress']) && \is_bool($config['display_progress'])) {
49 $this->displayProgress = $config['display_progress'];
50 }
51 }
52 /**
53 * Returns the current state of the upload
54 *
55 * @return UploadState
56 */
57 public function getState()
58 {
59 return $this->state;
60 }
61 /**
62 * Upload the source using multipart upload operations.
63 *
64 * @return Result The result of the CompleteMultipartUpload operation.
65 * @throws \LogicException if the upload is already complete or aborted.
66 * @throws MultipartUploadException if an upload operation fails.
67 */
68 public function upload()
69 {
70 return $this->promise()->wait();
71 }
72 /**
73 * Upload the source asynchronously using multipart upload operations.
74 *
75 * @return PromiseInterface
76 */
77 public function promise() : PromiseInterface
78 {
79 if ($this->promise) {
80 return $this->promise;
81 }
82 return $this->promise = Promise\Coroutine::of(function () {
83 // Initiate the upload.
84 if ($this->state->isCompleted()) {
85 throw new \LogicException('This multipart upload has already ' . 'been completed or aborted.');
86 }
87 if (!$this->state->isInitiated()) {
88 // Execute the prepare callback.
89 if (\is_callable($this->config["prepare_data_source"])) {
90 $this->config["prepare_data_source"]();
91 }
92 $result = (yield $this->execCommand('initiate', $this->getInitiateParams()));
93 $this->state->setUploadId($this->info['id']['upload_id'], $result[$this->info['id']['upload_id']]);
94 $this->state->setStatus(UploadState::INITIATED);
95 }
96 // Create a command pool from a generator that yields UploadPart
97 // commands for each upload part.
98 $resultHandler = $this->getResultHandler($errors);
99 $commands = new CommandPool($this->client, $this->getUploadCommands($resultHandler), ['concurrency' => $this->config['concurrency'], 'before' => $this->config['before_upload']]);
100 // Execute the pool of commands concurrently, and process errors.
101 (yield $commands->promise());
102 if ($errors) {
103 throw new $this->config['exception_class']($this->state, $errors);
104 }
105 // Complete the multipart upload.
106 (yield $this->execCommand('complete', $this->getCompleteParams()));
107 $this->state->setStatus(UploadState::COMPLETED);
108 })->otherwise($this->buildFailureCatch());
109 }
110 private function transformException($e)
111 {
112 // Throw errors from the operations as a specific Multipart error.
113 if ($e instanceof AwsException) {
114 $e = new $this->config['exception_class']($this->state, $e);
115 }
116 throw $e;
117 }
118 private function buildFailureCatch()
119 {
120 if (\interface_exists("Throwable")) {
121 return function (\Throwable $e) {
122 return $this->transformException($e);
123 };
124 } else {
125 return function (\Exception $e) {
126 return $this->transformException($e);
127 };
128 }
129 }
130 protected function getConfig()
131 {
132 return $this->config;
133 }
134 /**
135 * Provides service-specific information about the multipart upload
136 * workflow.
137 *
138 * This array of data should include the keys: 'command', 'id', and 'part_num'.
139 *
140 * @return array
141 */
142 protected abstract function loadUploadWorkflowInfo();
143 /**
144 * Determines the part size to use for upload parts.
145 *
146 * Examines the provided partSize value and the source to determine the
147 * best possible part size.
148 *
149 * @throws \InvalidArgumentException if the part size is invalid.
150 *
151 * @return int
152 */
153 protected abstract function determinePartSize();
154 /**
155 * Uses information from the Command and Result to determine which part was
156 * uploaded and mark it as uploaded in the upload's state.
157 *
158 * @param CommandInterface $command
159 * @param ResultInterface $result
160 */
161 protected abstract function handleResult(CommandInterface $command, ResultInterface $result);
162 /**
163 * Gets the service-specific parameters used to initiate the upload.
164 *
165 * @return array
166 */
167 protected abstract function getInitiateParams();
168 /**
169 * Gets the service-specific parameters used to complete the upload.
170 *
171 * @return array
172 */
173 protected abstract function getCompleteParams();
174 /**
175 * Based on the config and service-specific workflow info, creates a
176 * `Promise` for an `UploadState` object.
177 */
178 private function determineState() : UploadState
179 {
180 // If the state was provided via config, then just use it.
181 if ($this->config['state'] instanceof UploadState) {
182 return $this->config['state'];
183 }
184 // Otherwise, construct a new state from the provided identifiers.
185 $required = $this->info['id'];
186 $id = [$required['upload_id'] => null];
187 unset($required['upload_id']);
188 foreach ($required as $key => $param) {
189 if (!$this->config[$key]) {
190 throw new IAE('You must provide a value for "' . $key . '" in ' . 'your config for the MultipartUploader for ' . $this->client->getApi()->getServiceFullName() . '.');
191 }
192 $id[$param] = $this->config[$key];
193 }
194 $state = new UploadState($id, $this->config);
195 $state->setPartSize($this->determinePartSize());
196 return $state;
197 }
198 /**
199 * Executes a MUP command with all of the parameters for the operation.
200 *
201 * @param string $operation Name of the operation.
202 * @param array $params Service-specific params for the operation.
203 *
204 * @return PromiseInterface
205 */
206 protected function execCommand($operation, array $params)
207 {
208 // Create the command.
209 $command = $this->client->getCommand($this->info['command'][$operation], $params + $this->state->getId());
210 // Execute the before callback.
211 if (\is_callable($this->config["before_{$operation}"])) {
212 $this->config["before_{$operation}"]($command);
213 }
214 // Execute the command asynchronously and return the promise.
215 return $this->client->executeAsync($command);
216 }
217 /**
218 * Returns a middleware for processing responses of part upload operations.
219 *
220 * - Adds an onFulfilled callback that calls the service-specific
221 * handleResult method on the Result of the operation.
222 * - Adds an onRejected callback that adds the error to an array of errors.
223 * - Has a passedByRef $errors arg that the exceptions get added to. The
224 * caller should use that &$errors array to do error handling.
225 *
226 * @param array $errors Errors from upload operations are added to this.
227 *
228 * @return callable
229 */
230 protected function getResultHandler(&$errors = [])
231 {
232 return function (callable $handler) use(&$errors) {
233 return function (CommandInterface $command, ?RequestInterface $request = null) use($handler, &$errors) {
234 return $handler($command, $request)->then(function (ResultInterface $result) use($command) {
235 $this->handleResult($command, $result);
236 return $result;
237 }, function (AwsException $e) use(&$errors) {
238 $errors[$e->getCommand()[$this->info['part_num']]] = $e;
239 return new Result();
240 });
241 };
242 };
243 }
244 /**
245 * Creates a generator that yields part data for the upload's source.
246 *
247 * Yields associative arrays of parameters that are ultimately merged in
248 * with others to form the complete parameters of a command. This can
249 * include the Body parameter, which is a limited stream (i.e., a Stream
250 * object, decorated with a LimitStream).
251 *
252 * @param callable $resultHandler
253 *
254 * @return \Generator
255 */
256 protected abstract function getUploadCommands(callable $resultHandler);
257 }
258