Subversion Repositories cheapmusic

Rev

Details | Last modification | View Log | RSS feed

Rev Author Line No. Line
103 - 1
<?php
2
/**
3
 * Copyright 2012 Google Inc.
4
 *
5
 * Licensed under the Apache License, Version 2.0 (the "License");
6
 * you may not use this file except in compliance with the License.
7
 * You may obtain a copy of the License at
8
 *
9
 *     http://www.apache.org/licenses/LICENSE-2.0
10
 *
11
 * Unless required by applicable law or agreed to in writing, software
12
 * distributed under the License is distributed on an "AS IS" BASIS,
13
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
14
 * See the License for the specific language governing permissions and
15
 * limitations under the License.
16
 */
17
 
18
use GuzzleHttp\Psr7;
19
use GuzzleHttp\Psr7\Request;
20
use GuzzleHttp\Psr7\Uri;
21
use Psr\Http\Message\RequestInterface;
22
 
23
/**
24
 * Manage large file uploads, which may be media but can be any type
25
 * of sizable data.
26
 */
27
class Google_Http_MediaFileUpload
28
{
29
  const UPLOAD_MEDIA_TYPE = 'media';
30
  const UPLOAD_MULTIPART_TYPE = 'multipart';
31
  const UPLOAD_RESUMABLE_TYPE = 'resumable';
32
 
33
  /** @var string $mimeType */
34
  private $mimeType;
35
 
36
  /** @var string $data */
37
  private $data;
38
 
39
  /** @var bool $resumable */
40
  private $resumable;
41
 
42
  /** @var int $chunkSize */
43
  private $chunkSize;
44
 
45
  /** @var int $size */
46
  private $size;
47
 
48
  /** @var string $resumeUri */
49
  private $resumeUri;
50
 
51
  /** @var int $progress */
52
  private $progress;
53
 
54
  /** @var Google_Client */
55
  private $client;
56
 
57
  /** @var Psr\Http\Message\RequestInterface */
58
  private $request;
59
 
60
  /** @var string */
61
  private $boundary;
62
 
63
  /**
64
   * Result code from last HTTP call
65
   * @var int
66
   */
67
  private $httpResultCode;
68
 
69
  /**
70
   * @param $mimeType string
71
   * @param $data string The bytes you want to upload.
72
   * @param $resumable bool
73
   * @param bool $chunkSize File will be uploaded in chunks of this many bytes.
74
   * only used if resumable=True
75
   */
76
  public function __construct(
77
      Google_Client $client,
78
      RequestInterface $request,
79
      $mimeType,
80
      $data,
81
      $resumable = false,
82
      $chunkSize = false
83
  ) {
84
    $this->client = $client;
85
    $this->request = $request;
86
    $this->mimeType = $mimeType;
87
    $this->data = $data;
88
    $this->resumable = $resumable;
89
    $this->chunkSize = $chunkSize;
90
    $this->progress = 0;
91
 
92
    $this->process();
93
  }
94
 
95
  /**
96
   * Set the size of the file that is being uploaded.
97
   * @param $size - int file size in bytes
98
   */
99
  public function setFileSize($size)
100
  {
101
    $this->size = $size;
102
  }
103
 
104
  /**
105
   * Return the progress on the upload
106
   * @return int progress in bytes uploaded.
107
   */
108
  public function getProgress()
109
  {
110
    return $this->progress;
111
  }
112
 
113
  /**
114
   * Send the next part of the file to upload.
115
   * @param [$chunk] the next set of bytes to send. If false will used $data passed
116
   * at construct time.
117
   */
118
  public function nextChunk($chunk = false)
119
  {
120
    $resumeUri = $this->getResumeUri();
121
 
122
    if (false == $chunk) {
123
      $chunk = substr($this->data, $this->progress, $this->chunkSize);
124
    }
125
 
126
    $lastBytePos = $this->progress + strlen($chunk) - 1;
127
    $headers = array(
128
      'content-range' => "bytes $this->progress-$lastBytePos/$this->size",
129
      'content-length' => strlen($chunk),
130
      'expect' => '',
131
    );
132
 
133
    $request = new Request(
134
        'PUT',
135
        $resumeUri,
136
        $headers,
137
        Psr7\stream_for($chunk)
138
    );
139
 
140
    return $this->makePutRequest($request);
141
  }
142
 
143
  /**
144
   * Return the HTTP result code from the last call made.
145
   * @return int code
146
   */
147
  public function getHttpResultCode()
148
  {
149
    return $this->httpResultCode;
150
  }
151
 
152
  /**
153
  * Sends a PUT-Request to google drive and parses the response,
154
  * setting the appropiate variables from the response()
155
  *
156
  * @param Google_Http_Request $httpRequest the Reuqest which will be send
157
  *
158
  * @return false|mixed false when the upload is unfinished or the decoded http response
159
  *
160
  */
161
  private function makePutRequest(RequestInterface $request)
162
  {
163
    $response = $this->client->execute($request);
164
    $this->httpResultCode = $response->getStatusCode();
165
 
166
    if (308 == $this->httpResultCode) {
167
      // Track the amount uploaded.
168
      $range = explode('-', $response->getHeaderLine('range'));
169
      $this->progress = $range[1] + 1;
170
 
171
      // Allow for changing upload URLs.
172
      $location = $response->getHeaderLine('location');
173
      if ($location) {
174
        $this->resumeUri = $location;
175
      }
176
 
177
      // No problems, but upload not complete.
178
      return false;
179
    }
180
 
181
    return Google_Http_REST::decodeHttpResponse($response, $this->request);
182
  }
183
 
184
  /**
185
   * Resume a previously unfinished upload
186
   * @param $resumeUri the resume-URI of the unfinished, resumable upload.
187
   */
188
  public function resume($resumeUri)
189
  {
190
     $this->resumeUri = $resumeUri;
191
     $headers = array(
192
       'content-range' => "bytes */$this->size",
193
       'content-length' => 0,
194
     );
195
     $httpRequest = new Request(
196
         'PUT',
197
         $this->resumeUri,
198
         $headers
199
     );
200
 
201
     return $this->makePutRequest($httpRequest);
202
  }
203
 
204
  /**
205
   * @return Psr\Http\Message\RequestInterface $request
206
   * @visible for testing
207
   */
208
  private function process()
209
  {
210
    $this->transformToUploadUrl();
211
    $request = $this->request;
212
 
213
    $postBody = '';
214
    $contentType = false;
215
 
216
    $meta = (string) $request->getBody();
217
    $meta = is_string($meta) ? json_decode($meta, true) : $meta;
218
 
219
    $uploadType = $this->getUploadType($meta);
220
    $request = $request->withUri(
221
        Uri::withQueryValue($request->getUri(), 'uploadType', $uploadType)
222
    );
223
 
224
    $mimeType = $this->mimeType ?: $request->getHeaderLine('content-type');
225
 
226
    if (self::UPLOAD_RESUMABLE_TYPE == $uploadType) {
227
      $contentType = $mimeType;
228
      $postBody = is_string($meta) ? $meta : json_encode($meta);
229
    } else if (self::UPLOAD_MEDIA_TYPE == $uploadType) {
230
      $contentType = $mimeType;
231
      $postBody = $this->data;
232
    } else if (self::UPLOAD_MULTIPART_TYPE == $uploadType) {
233
      // This is a multipart/related upload.
234
      $boundary = $this->boundary ?: mt_rand();
235
      $boundary = str_replace('"', '', $boundary);
236
      $contentType = 'multipart/related; boundary=' . $boundary;
237
      $related = "--$boundary\r\n";
238
      $related .= "Content-Type: application/json; charset=UTF-8\r\n";
239
      $related .= "\r\n" . json_encode($meta) . "\r\n";
240
      $related .= "--$boundary\r\n";
241
      $related .= "Content-Type: $mimeType\r\n";
242
      $related .= "Content-Transfer-Encoding: base64\r\n";
243
      $related .= "\r\n" . base64_encode($this->data) . "\r\n";
244
      $related .= "--$boundary--";
245
      $postBody = $related;
246
    }
247
 
248
    $request = $request->withBody(Psr7\stream_for($postBody));
249
 
250
    if (isset($contentType) && $contentType) {
251
      $request = $request->withHeader('content-type', $contentType);
252
    }
253
 
254
    return $this->request = $request;
255
  }
256
 
257
  /**
258
   * Valid upload types:
259
   * - resumable (UPLOAD_RESUMABLE_TYPE)
260
   * - media (UPLOAD_MEDIA_TYPE)
261
   * - multipart (UPLOAD_MULTIPART_TYPE)
262
   * @param $meta
263
   * @return string
264
   * @visible for testing
265
   */
266
  public function getUploadType($meta)
267
  {
268
    if ($this->resumable) {
269
      return self::UPLOAD_RESUMABLE_TYPE;
270
    }
271
 
272
    if (false == $meta && $this->data) {
273
      return self::UPLOAD_MEDIA_TYPE;
274
    }
275
 
276
    return self::UPLOAD_MULTIPART_TYPE;
277
  }
278
 
279
  public function getResumeUri()
280
  {
281
    if (null === $this->resumeUri) {
282
      $this->resumeUri = $this->fetchResumeUri();
283
    }
284
 
285
    return $this->resumeUri;
286
  }
287
 
288
  private function fetchResumeUri()
289
  {
290
    $body = $this->request->getBody();
291
    if ($body) {
292
      $headers = array(
293
        'content-type' => 'application/json; charset=UTF-8',
294
        'content-length' => $body->getSize(),
295
        'x-upload-content-type' => $this->mimeType,
296
        'x-upload-content-length' => $this->size,
297
        'expect' => '',
298
      );
299
      foreach ($headers as $key => $value) {
300
        $this->request = $this->request->withHeader($key, $value);
301
      }
302
    }
303
 
304
    $response = $this->client->execute($this->request, false);
305
    $location = $response->getHeaderLine('location');
306
    $code = $response->getStatusCode();
307
 
308
    if (200 == $code && true == $location) {
309
      return $location;
310
    }
311
 
312
    $message = $code;
313
    $body = json_decode((string) $this->request->getBody(), true);
314
    if (isset($body['error']['errors'])) {
315
      $message .= ': ';
316
      foreach ($body['error']['errors'] as $error) {
317
        $message .= "{$error[domain]}, {$error[message]};";
318
      }
319
      $message = rtrim($message, ';');
320
    }
321
 
322
    $error = "Failed to start the resumable upload (HTTP {$message})";
323
    $this->client->getLogger()->error($error);
324
 
325
    throw new Google_Exception($error);
326
  }
327
 
328
  private function transformToUploadUrl()
329
  {
330
    $parts = parse_url((string) $this->request->getUri());
331
    if (!isset($parts['path'])) {
332
      $parts['path'] = '';
333
    }
334
    $parts['path'] = '/upload' . $parts['path'];
335
    $uri = Uri::fromParts($parts);
336
    $this->request = $this->request->withUri($uri);
337
  }
338
 
339
  public function setChunkSize($chunkSize)
340
  {
341
    $this->chunkSize = $chunkSize;
342
  }
343
 
344
  public function getRequest()
345
  {
346
    return $this->request;
347
  }
348
}