UploaderTrait::getProgressHandler()   B
last analyzed

Complexity

Conditions 9
Paths 1

Size

Total Lines 28
Code Lines 18

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 18
CRAP Score 9.0117

Importance

Changes 0
Metric Value
cc 9
eloc 18
nc 1
nop 5
dl 0
loc 28
ccs 18
cts 19
cp 0.9474
crap 9.0117
rs 8.0555
c 0
b 0
f 0
1
<?php
2
3
namespace mpyw\Cowitter\Traits;
4
5
use mpyw\Cowitter\HttpException;
6
use mpyw\Co\Co;
7
use mpyw\Co\CURLException;
8
use mpyw\Cowitter\ResponseInterface;
9
use mpyw\Cowitter\Helpers\RequestParamValidator;
10
11
trait UploaderTrait
12
{
13
    abstract public function getAsync($endpoint, array $params = [], $return_response_object = false);
14
    abstract public function postAsync($endpoint, array $params = [], $return_response_object = false);
15
    abstract public function postMultipartAsync($endpoint, array $params = [], $return_response_object = false);
16
    abstract public function withOptions(array $options);
17
18 12
    protected static function validateChunkSize($value)
19 12
    {
20 12
        if (false === $value = filter_var($value, FILTER_VALIDATE_INT)) {
21 1
            throw new \InvalidArgumentException('Chunk size must be integer.');
22
        }
23 11
        if ($value < 10000) {
24 1
            throw new \LengthException('Chunk size must be no less than 10000 bytes.');
25
        }
26 10
        return $value;
27
    }
28
29 10
    protected static function getMimeType(\SplFileObject $file)
30 10
    {
31 10
        $finfo = new \finfo(FILEINFO_MIME_TYPE);
32 10
        $type = $finfo->buffer($file->fread(1024));
33 10
        $file->rewind();
34 10
        return $type;
35
    }
36
37 12
    public function uploadAsync(\SplFileObject $file, $media_category = null, callable $on_uploading = null, callable $on_processing = null, $chunk_size = 300000)
38 12
    {
39 12
        $response = (yield $this->uploadStep1($file, $media_category, $chunk_size, $on_uploading));
40 10
        if (!$response instanceof ResponseInterface) {
41 2
            yield Co::RETURN_WITH => $response;
42
        }
43 8
        if (!isset($response->getContent()->processing_info)) {
44 1
            yield Co::RETURN_WITH => $response->getContent();
45
        }
46 7
        yield Co::RETURN_WITH => (yield $this->uploadStep2($response, $on_processing));
47
        // @codeCoverageIgnoreStart
48
    }
49
    // @codeCoverageIgnoreEnd
50
51 12
    protected function uploadStep1(\SplFileObject $file, $media_category = null, $chunk_size = 300000, callable $on_uploading = null)
52 12
    {
53 12
        $chunk_size = static::validateChunkSize($chunk_size);
54 10
        $info = (yield $this->postAsync('media/upload', [
55 10
            'command' => 'INIT',
56 10
            'media_type' => static::getMimeType($file),
57 10
            'total_bytes' => $file->getSize(),
58 10
            'media_category' => $media_category,
59
        ]));
60
        try {
61 10
            yield $this->uploadBuffers($file, $info, $chunk_size, $on_uploading);
62 2
        } catch (CURLException $e) {
63 2
            if ($e->getCode() === CURLE_ABORTED_BY_CALLBACK) {
64 2
                yield Co::RETURN_WITH => $info;
65
            }
66
            // @codeCoverageIgnoreStart
67
            throw $e;
68
            // @codeCoverageIgnoreEnd
69
        }
70 8
        yield Co::RETURN_WITH => (yield $this->postAsync('media/upload', [
71 8
            'command' => 'FINALIZE',
72 8
            'media_id' => $info->media_id_string,
73 8
        ], true));
74
        // @codeCoverageIgnoreStart
75
    }
76
    // @codeCoverageIgnoreEnd
77
78 10
    protected function uploadBuffers(\SplFileObject $file, \stdClass $info, $chunk_size, callable $on_uploading = null)
79 10
    {
80 10
        $tasks = [];
81 10
        $whole_uploaded = 0;
82 10
        $first = true;
83 10
        $canceled = false;
84 10
        for ($i = 0; '' !== $buffer = $file->fread($chunk_size); ++$i) {
85 10
            $client = $on_uploading ? $this->withOptions([
86 3
                CURLOPT_NOPROGRESS => false,
87 3
                CURLOPT_PROGRESSFUNCTION => static::getProgressHandler($file, $on_uploading, $whole_uploaded, $first, $canceled),
88 10
            ]) : $this;
89 10
            $tasks[] = $client->postMultipartAsync('media/upload', [
90 10
                'command' => 'APPEND',
91 10
                'media_id' => $info->media_id_string,
92 10
                'segment_index' => $i,
93 10
                'media' => $buffer,
94
            ]);
95
        }
96 10
        yield $tasks;
97 8
    }
98
99 3
    protected static function getProgressHandler(\SplFileObject $file, callable $on_uploading, &$whole_uploaded, &$first, &$canceled)
100 3
    {
101
        // NOTE: File size doesn't include other parameters' size.
102
        //       It is calculated for approximate usage.
103
        return function ($ch, $dl_total, $dl_now, $up_total, $up_now) use ($file, $on_uploading, &$whole_uploaded, &$first, &$canceled) {
104 3
            static $previous_up_now = 0;
105 3
            if ($canceled) {
106 1
                return 1;
107
            }
108 3
            if ($dl_now !== 0 || ($up_now === $previous_up_now && !$first)) {
109 2
                return 0;
110
            }
111 3
            $whole_uploaded_percent_before = (int)(min(100, (int)(($whole_uploaded / $file->getSize()) * 100)) / 5) * 5;
112 3
            $whole_uploaded += $up_now - $previous_up_now;
113 3
            $previous_up_now = $up_now;
114 3
            $whole_uploaded_percent_after  = (int)(min(100, (int)(($whole_uploaded / $file->getSize()) * 100)) / 5) * 5;
115 3
            if ($whole_uploaded_percent_before === $whole_uploaded_percent_after && !$first) {
116
                return 0;
117
            }
118 3
            $first = false;
119 3
            if (RequestParamValidator::isGenerator($on_uploading)) {
120
                Co::async(function () use ($on_uploading, $whole_uploaded_percent_after, &$canceled) {
121 1
                    if (false === (yield $on_uploading($whole_uploaded_percent_after))) {
122 1
                        $canceled = true;
123
                    }
124 1
                });
125
            }
126 3
            return (int)(false === $on_uploading($whole_uploaded_percent_after));
127 3
        };
128
    }
129
130 7
    protected function uploadStep2(ResponseInterface $response, callable $on_processing = null)
131 7
    {
132 7
        $info = $response->getContent();
133 7
        $canceled = false;
134 7
        $previous_percent = 0;
135 7
        while ($info->processing_info->state === 'pending' || $info->processing_info->state === 'in_progress') {
136 7
            $percent = isset($info->processing_info->progress_percent)
137 5
                ? $info->processing_info->progress_percent
138 7
                : $previous_percent
139
            ;
140 7
            $previous_percent = $percent;
141 7
            if ($on_processing) {
142 4
                if (RequestParamValidator::isGenerator($on_processing)) {
143
                    Co::async(function () use ($on_processing, $percent, $response, &$canceled) {
144 2
                        if (false === (yield $on_processing($percent, $response))) {
145 2
                            $canceled = true;
146
                        }
147 2
                    });
148 2
                } elseif (false === $on_processing($percent, $response)) {
149 1
                    yield Co::RETURN_WITH => $info;
150
                }
151
            }
152 6
            if ($canceled) yield Co::RETURN_WITH => $info;
153 5
            yield Co::DELAY => $info->processing_info->check_after_secs;
154 5
            if ($canceled) yield Co::RETURN_WITH => $info;
155 5
            $response = (yield $this->getAsync('media/upload', [
156 5
                'command' => 'STATUS',
157 5
                'media_id' => $info->media_id_string,
158 5
            ], true));
159 5
            $info = $response->getContent();
160
        }
161
162 4
        if ($info->processing_info->state === 'failed') {
163 2
            $message = isset($info->processing_info->error->message)
164 1
                ? $info->processing_info->error->message
165 2
                : $info->processing_info->error->name;
166 2
            throw new HttpException(
167 2
                $message,
168 2
                $info->processing_info->error->code,
169 2
                $response
170
            );
171
        }
172
173 2
        yield Co::RETURN_WITH => $info;
174
        // @codeCoverageIgnoreStart
175
    }
176
    // @codeCoverageIgnoreEnd
177
178 9
    public function uploadImageAsync(\SplFileObject $file, callable $on_uploading = null, callable $on_processing = null, $chunk_size = 300000)
179 9
    {
180 9
        yield Co::RETURN_WITH => ($this->uploadAsync($file, 'tweet_image', $on_uploading, $on_processing, $chunk_size));
181
        // @codeCoverageIgnoreStart
182
    }
183
    // @codeCoverageIgnoreEnd
184
185 1
    public function uploadAnimeGifAsync(\SplFileObject $file, callable $on_uploading = null, callable $on_processing = null, $chunk_size = 300000)
186 1
    {
187 1
        yield Co::RETURN_WITH => ($this->uploadAsync($file, 'tweet_gif', $on_uploading, $on_processing, $chunk_size));
188
        // @codeCoverageIgnoreStart
189
    }
190
    // @codeCoverageIgnoreEnd
191
192 1
    public function uploadVideoAsync(\SplFileObject $file, $media_category, callable $on_uploading = null, callable $on_processing = null, $chunk_size = 300000)
193 1
    {
194 1
        yield Co::RETURN_WITH => ($this->uploadAsync($file, $media_category, $on_uploading, $on_processing, $chunk_size));
195
        // @codeCoverageIgnoreStart
196
    }
197
    // @codeCoverageIgnoreEnd
198
}
199