~ubuntu-branches/ubuntu/wily/phabricator/wily

« back to all changes in this revision

Viewing changes to arcanist/src/upload/ArcanistFileUploader.php

  • Committer: Package Import Robot
  • Author(s): Richard Sellam
  • Date: 2015-06-13 10:52:10 UTC
  • mfrom: (0.30.1) (0.29.1) (0.17.4) (2.1.9 sid)
  • Revision ID: package-import@ubuntu.com-20150613105210-5uirr7tvnk0n6e6y
Tags: 0~git20150613-1
* New snapshot release (closes: #787805)
* fixed typo in logrotate script (closes: #787645)

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
<?php
 
2
 
 
3
/**
 
4
 * Upload a list of @{class:ArcanistFileDataRef} objects over Conduit.
 
5
 *
 
6
 *   // Create a new uploader.
 
7
 *   $uploader = id(new ArcanistFileUploader())
 
8
 *     ->setConduitClient($conduit);
 
9
 *
 
10
 *   // Queue one or more files to be uploaded.
 
11
 *   $file = id(new ArcanistFileDataRef())
 
12
 *     ->setName('example.jpg')
 
13
 *     ->setPath('/path/to/example.jpg');
 
14
 *   $uploader->addFile($file);
 
15
 *
 
16
 *   // Upload the files.
 
17
 *   $files = $uploader->uploadFiles();
 
18
 *
 
19
 * For details about building file references, see @{class:ArcanistFileDataRef}.
 
20
 *
 
21
 * @task config Configuring the Uploader
 
22
 * @task add Adding Files
 
23
 * @task upload Uploading Files
 
24
 * @task internal Internals
 
25
 */
 
26
final class ArcanistFileUploader extends Phobject {
 
27
 
 
28
  private $conduit;
 
29
  private $files;
 
30
 
 
31
 
 
32
/* -(  Configuring the Uploader  )------------------------------------------- */
 
33
 
 
34
 
 
35
  /**
 
36
   * Provide a Conduit client to choose which server to upload files to.
 
37
   *
 
38
   * @param ConduitClient Configured client.
 
39
   * @return this
 
40
   * @task config
 
41
   */
 
42
  public function setConduitClient(ConduitClient $conduit) {
 
43
    $this->conduit = $conduit;
 
44
    return $this;
 
45
  }
 
46
 
 
47
 
 
48
/* -(  Adding Files  )------------------------------------------------------- */
 
49
 
 
50
 
 
51
  /**
 
52
   * Add a file to the list of files to be uploaded.
 
53
   *
 
54
   * You can optionally provide an explicit key which will be used to identify
 
55
   * the file. After adding files, upload them with @{method:uploadFiles}.
 
56
   *
 
57
   * @param ArcanistFileDataRef File data to upload.
 
58
   * @param null|string Optional key to use to identify this file.
 
59
   * @return this
 
60
   * @task add
 
61
   */
 
62
  public function addFile(ArcanistFileDataRef $file, $key = null) {
 
63
 
 
64
    if ($key === null) {
 
65
      $this->files[] = $file;
 
66
    } else {
 
67
      if (isset($this->files[$key])) {
 
68
        throw new Exception(
 
69
          pht(
 
70
            'Two files were added with identical explicit keys ("%s"); each '.
 
71
            'explicit key must be unique.',
 
72
            $key));
 
73
      }
 
74
      $this->files[$key] = $file;
 
75
    }
 
76
 
 
77
    return $this;
 
78
  }
 
79
 
 
80
 
 
81
/* -(  Uploading Files  )---------------------------------------------------- */
 
82
 
 
83
 
 
84
  /**
 
85
   * Upload files to the server.
 
86
   *
 
87
   * This transfers all files which have been queued with @{method:addFiles}
 
88
   * over the Conduit link configured with @{method:setConduitClient}.
 
89
   *
 
90
   * This method returns a map of all file data references. If references were
 
91
   * added with an explicit key when @{method:addFile} was called, the key is
 
92
   * retained in the result map.
 
93
   *
 
94
   * On return, files are either populated with a PHID (indicating a successful
 
95
   * upload) or a list of errors. See @{class:ArcanistFileDataRef} for
 
96
   * details.
 
97
   *
 
98
   * @return map<string, ArcanistFileDataRef> Files with results populated.
 
99
   * @task upload
 
100
   */
 
101
  public function uploadFiles() {
 
102
    if (!$this->conduit) {
 
103
      throw new PhutilInvalidStateException('setConduitClient');
 
104
    }
 
105
 
 
106
    $files = $this->files;
 
107
    foreach ($files as $key => $file) {
 
108
      try {
 
109
        $file->willUpload();
 
110
      } catch (Exception $ex) {
 
111
        $file->didFail($ex->getMessage());
 
112
        unset($files[$key]);
 
113
      }
 
114
    }
 
115
 
 
116
    $conduit = $this->conduit;
 
117
    $futures = array();
 
118
    foreach ($files as $key => $file) {
 
119
      $futures[$key] = $conduit->callMethod(
 
120
        'file.allocate',
 
121
        array(
 
122
          'name' => $file->getName(),
 
123
          'contentLength' => $file->getByteSize(),
 
124
          'contentHash' => $file->getContentHash(),
 
125
        ));
 
126
    }
 
127
 
 
128
    $iterator = id(new FutureIterator($futures))->limit(4);
 
129
    $chunks = array();
 
130
    foreach ($iterator as $key => $future) {
 
131
      try {
 
132
        $result = $future->resolve();
 
133
      } catch (Exception $ex) {
 
134
        // The most likely cause for a failure here is that the server does
 
135
        // not support `file.allocate`. In this case, we'll try the older
 
136
        // upload method below.
 
137
        continue;
 
138
      }
 
139
 
 
140
      $phid = $result['filePHID'];
 
141
      $file = $files[$key];
 
142
 
 
143
      // We don't need to upload any data. Figure out why not: this can either
 
144
      // be because of an error (server can't accept the data) or because the
 
145
      // server already has the data.
 
146
      if (!$result['upload']) {
 
147
        if (!$phid) {
 
148
          $file->didFail(
 
149
            pht(
 
150
              'Unable to upload file: the server refused to accept file '.
 
151
              '"%s". This usually means it is too large.',
 
152
              $file->getName()));
 
153
        } else {
 
154
          // These server completed the upload by creating a reference to known
 
155
          // file data. We don't need to transfer the actual data, and are all
 
156
          // set.
 
157
          $file->setPHID($phid);
 
158
        }
 
159
        unset($files[$key]);
 
160
        continue;
 
161
      }
 
162
 
 
163
      // The server wants us to do an upload.
 
164
      if ($phid) {
 
165
        $chunks[$key] = array(
 
166
          'file' => $file,
 
167
          'phid' => $phid,
 
168
        );
 
169
      }
 
170
    }
 
171
 
 
172
    foreach ($chunks as $key => $chunk) {
 
173
      $file = $chunk['file'];
 
174
      $phid = $chunk['phid'];
 
175
      try {
 
176
        $this->uploadChunks($file, $phid);
 
177
        $file->setPHID($phid);
 
178
      } catch (Exception $ex) {
 
179
        $file->didFail(
 
180
          pht(
 
181
            'Unable to upload file chunks: %s',
 
182
            $ex->getMessage()));
 
183
      }
 
184
      unset($files[$key]);
 
185
    }
 
186
 
 
187
    foreach ($files as $key => $file) {
 
188
      try {
 
189
        $phid = $this->uploadData($file);
 
190
        $file->setPHID($phid);
 
191
      } catch (Exception $ex) {
 
192
        $file->didFail(
 
193
          pht(
 
194
            'Unable to upload file data: %s',
 
195
            $ex->getMessage()));
 
196
      }
 
197
      unset($files[$key]);
 
198
    }
 
199
 
 
200
    foreach ($this->files as $file) {
 
201
      $file->didUpload();
 
202
    }
 
203
 
 
204
    return $this->files;
 
205
  }
 
206
 
 
207
 
 
208
/* -(  Internals  )---------------------------------------------------------- */
 
209
 
 
210
 
 
211
  /**
 
212
   * Upload missing chunks of a large file by calling `file.uploadchunk` over
 
213
   * Conduit.
 
214
   *
 
215
   * @task internal
 
216
   */
 
217
  private function uploadChunks(ArcanistFileDataRef $file, $file_phid) {
 
218
    $conduit = $this->conduit;
 
219
 
 
220
    $chunks = $conduit->callMethodSynchronous(
 
221
      'file.querychunks',
 
222
      array(
 
223
        'filePHID' => $file_phid,
 
224
      ));
 
225
 
 
226
    $remaining = array();
 
227
    foreach ($chunks as $chunk) {
 
228
      if (!$chunk['complete']) {
 
229
        $remaining[] = $chunk;
 
230
      }
 
231
    }
 
232
 
 
233
    $done = (count($chunks) - count($remaining));
 
234
 
 
235
    if ($done) {
 
236
      $this->writeStatus(
 
237
        pht(
 
238
          'Resuming upload (%d of %d chunks remain).',
 
239
          new PhutilNumber(count($remaining)),
 
240
          new PhutilNumber(count($chunks))));
 
241
    } else {
 
242
      $this->writeStatus(
 
243
        pht(
 
244
          'Uploading chunks (%d chunks to upload).',
 
245
          new PhutilNumber(count($remaining))));
 
246
    }
 
247
 
 
248
    $progress = new PhutilConsoleProgressBar();
 
249
    $progress->setTotal(count($chunks));
 
250
 
 
251
    for ($ii = 0; $ii < $done; $ii++) {
 
252
      $progress->update(1);
 
253
    }
 
254
 
 
255
    $progress->draw();
 
256
 
 
257
    // TODO: We could do these in parallel to improve upload performance.
 
258
    foreach ($remaining as $chunk) {
 
259
      $data = $file->readBytes($chunk['byteStart'], $chunk['byteEnd']);
 
260
 
 
261
      $conduit->callMethodSynchronous(
 
262
        'file.uploadchunk',
 
263
        array(
 
264
          'filePHID' => $file_phid,
 
265
          'byteStart' => $chunk['byteStart'],
 
266
          'dataEncoding' => 'base64',
 
267
          'data' => base64_encode($data),
 
268
        ));
 
269
 
 
270
      $progress->update(1);
 
271
    }
 
272
  }
 
273
 
 
274
 
 
275
  /**
 
276
   * Upload an entire file by calling `file.upload` over Conduit.
 
277
   *
 
278
   * @task internal
 
279
   */
 
280
  private function uploadData(ArcanistFileDataRef $file) {
 
281
    $conduit = $this->conduit;
 
282
 
 
283
    $data = $file->readBytes(0, $file->getByteSize());
 
284
 
 
285
    return $conduit->callMethodSynchronous(
 
286
      'file.upload',
 
287
      array(
 
288
        'name' => $file->getName(),
 
289
        'data_base64' => base64_encode($data),
 
290
      ));
 
291
  }
 
292
 
 
293
 
 
294
  /**
 
295
   * Write a status message.
 
296
   *
 
297
   * @task internal
 
298
   */
 
299
  private function writeStatus($message) {
 
300
    fwrite(STDERR, $message."\n");
 
301
  }
 
302
 
 
303
}