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/**
19 * @author Chirag Shah <chirags@google.com>
20 *
21 */
22class Google_MediaFileUpload {
23  const UPLOAD_MEDIA_TYPE = 'media';
24  const UPLOAD_MULTIPART_TYPE = 'multipart';
25  const UPLOAD_RESUMABLE_TYPE = 'resumable';
26
27  /** @var string $mimeType */
28  public $mimeType;
29
30  /** @var string $data */
31  public $data;
32
33  /** @var bool $resumable */
34  public $resumable;
35
36  /** @var int $chunkSize */
37  public $chunkSize;
38
39  /** @var int $size */
40  public $size;
41
42  /** @var string $resumeUri */
43  public $resumeUri;
44
45  /** @var int $progress */
46  public $progress;
47
48  /**
49   * @param $mimeType string
50   * @param $data string The bytes you want to upload.
51   * @param $resumable bool
52   * @param bool $chunkSize File will be uploaded in chunks of this many bytes.
53   * only used if resumable=True
54   */
55  public function __construct($mimeType, $data, $resumable=false, $chunkSize=false) {
56    $this->mimeType = $mimeType;
57    $this->data = $data;
58    $this->size = strlen($this->data);
59    $this->resumable = $resumable;
60    if(!$chunkSize) {
61      $chunkSize = 256 * 1024;
62    }
63    $this->chunkSize = $chunkSize;
64    $this->progress = 0;
65  }
66
67  public function setFileSize($size) {
68    $this->size = $size;
69  }
70
71  /**
72   * @static
73   * @param $meta
74   * @param $params
75   * @return array|bool
76   */
77  public static function process($meta, &$params) {
78    $payload = array();
79    $meta = is_string($meta) ? json_decode($meta, true) : $meta;
80    $uploadType = self::getUploadType($meta, $payload, $params);
81    if (!$uploadType) {
82      // Process as a normal API request.
83      return false;
84    }
85
86    // Process as a media upload request.
87    $params['uploadType'] = array(
88        'type' => 'string',
89        'location' => 'query',
90        'value' => $uploadType,
91    );
92
93    $mimeType = isset($params['mimeType'])
94        ? $params['mimeType']['value']
95        : false;
96    unset($params['mimeType']);
97
98    if (!$mimeType) {
99      $mimeType = $payload['content-type'];
100    }
101
102    if (isset($params['file'])) {
103      // This is a standard file upload with curl.
104      $file = $params['file']['value'];
105      unset($params['file']);
106      return self::processFileUpload($file, $mimeType);
107    }
108
109    $data = isset($params['data'])
110        ? $params['data']['value']
111        : false;
112    unset($params['data']);
113
114    if (self::UPLOAD_RESUMABLE_TYPE == $uploadType) {
115      $payload['content-type'] = $mimeType;
116      $payload['postBody'] = is_string($meta) ? $meta : json_encode($meta);
117
118    } elseif (self::UPLOAD_MEDIA_TYPE == $uploadType) {
119      // This is a simple media upload.
120      $payload['content-type'] = $mimeType;
121      $payload['postBody'] = $data;
122    }
123
124    elseif (self::UPLOAD_MULTIPART_TYPE == $uploadType) {
125      // This is a multipart/related upload.
126      $boundary = isset($params['boundary']['value']) ? $params['boundary']['value'] : mt_rand();
127      $boundary = str_replace('"', '', $boundary);
128      $payload['content-type'] = 'multipart/related; boundary=' . $boundary;
129      $related = "--$boundary\r\n";
130      $related .= "Content-Type: application/json; charset=UTF-8\r\n";
131      $related .= "\r\n" . json_encode($meta) . "\r\n";
132      $related .= "--$boundary\r\n";
133      $related .= "Content-Type: $mimeType\r\n";
134      $related .= "Content-Transfer-Encoding: base64\r\n";
135      $related .= "\r\n" . base64_encode($data) . "\r\n";
136      $related .= "--$boundary--";
137      $payload['postBody'] = $related;
138    }
139
140    return $payload;
141  }
142
143  /**
144   * Prepares a standard file upload via cURL.
145   * @param $file
146   * @param $mime
147   * @return array Includes the processed file name.
148   * @visible For testing.
149   */
150  public static function processFileUpload($file, $mime) {
151    if (!$file) return array();
152    if (substr($file, 0, 1) != '@') {
153      $file = '@' . $file;
154    }
155
156    // This is a standard file upload with curl.
157    $params = array('postBody' => array('file' => $file));
158    if ($mime) {
159      $params['content-type'] = $mime;
160    }
161
162    return $params;
163  }
164
165  /**
166   * Valid upload types:
167   * - resumable (UPLOAD_RESUMABLE_TYPE)
168   * - media (UPLOAD_MEDIA_TYPE)
169   * - multipart (UPLOAD_MULTIPART_TYPE)
170   * - none (false)
171   * @param $meta
172   * @param $payload
173   * @param $params
174   * @return bool|string
175   */
176  public static function getUploadType($meta, &$payload, &$params) {
177    if (isset($params['mediaUpload'])
178        && get_class($params['mediaUpload']['value']) == 'Google_MediaFileUpload') {
179      $upload = $params['mediaUpload']['value'];
180      unset($params['mediaUpload']);
181      $payload['content-type'] = $upload->mimeType;
182      if (isset($upload->resumable) && $upload->resumable) {
183        return self::UPLOAD_RESUMABLE_TYPE;
184      }
185    }
186
187    // Allow the developer to override the upload type.
188    if (isset($params['uploadType'])) {
189      return $params['uploadType']['value'];
190    }
191
192    $data = isset($params['data']['value'])
193        ? $params['data']['value'] : false;
194
195    if (false == $data && false == isset($params['file'])) {
196      // No upload data available.
197      return false;
198    }
199
200    if (isset($params['file'])) {
201      return self::UPLOAD_MEDIA_TYPE;
202    }
203
204    if (false == $meta) {
205      return self::UPLOAD_MEDIA_TYPE;
206    }
207
208    return self::UPLOAD_MULTIPART_TYPE;
209  }
210
211
212  public function nextChunk(Google_HttpRequest $req, $chunk=false) {
213    if (false == $this->resumeUri) {
214      $this->resumeUri = $this->getResumeUri($req);
215    }
216
217    if (false == $chunk) {
218      $chunk = substr($this->data, $this->progress, $this->chunkSize);
219    }
220
221    $lastBytePos = $this->progress + strlen($chunk) - 1;
222    $headers = array(
223      'content-range' => "bytes $this->progress-$lastBytePos/$this->size",
224      'content-type' => $req->getRequestHeader('content-type'),
225      'content-length' => $this->chunkSize,
226      'expect' => '',
227    );
228
229    $httpRequest = new Google_HttpRequest($this->resumeUri, 'PUT', $headers, $chunk);
230    $response = Google_Client::$io->authenticatedRequest($httpRequest);
231    $code = $response->getResponseHttpCode();
232    if (308 == $code) {
233      $range = explode('-', $response->getResponseHeader('range'));
234      $this->progress = $range[1] + 1;
235      return false;
236    } else {
237      return Google_REST::decodeHttpResponse($response);
238    }
239  }
240
241  private function getResumeUri(Google_HttpRequest $httpRequest) {
242    $result = null;
243    $body = $httpRequest->getPostBody();
244    if ($body) {
245      $httpRequest->setRequestHeaders(array(
246        'content-type' => 'application/json; charset=UTF-8',
247        'content-length' => Google_Utils::getStrLen($body),
248        'x-upload-content-type' => $this->mimeType,
249        'x-upload-content-length' => $this->size,
250        'expect' => '',
251      ));
252    }
253
254    $response = Google_Client::$io->makeRequest($httpRequest);
255    $location = $response->getResponseHeader('location');
256    $code = $response->getResponseHttpCode();
257    if (200 == $code && true == $location) {
258      return $location;
259    }
260    throw new Google_Exception("Failed to start the resumable upload");
261  }
262}