Completed
Pull Request — master (#19)
by Şəhriyar
10:40
created

FileStream::handleUpload()   F

Complexity

Conditions 24
Paths 2926

Size

Total Lines 98
Code Lines 47

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 600

Importance

Changes 2
Bugs 0 Features 1
Metric Value
c 2
b 0
f 1
dl 0
loc 98
ccs 0
cts 64
cp 0
rs 2
cc 24
eloc 47
nc 2926
nop 1
crap 600

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php namespace App\Services;
2
3
use App\Events\Files\Uploaded;
4
use App\Exceptions\FileStream as FileStreamExceptions;
5
use Illuminate\Http\Request;
6
use Symfony\Component\HttpFoundation\File\UploadedFile;
7
8
class FileStream
9
{
10
    /**
11
     * @var \Illuminate\Contracts\Filesystem\Filesystem
12
     */
13
    public $filesystem;
14
15
    /**
16
     * Folder to hold uploaded chunks.
17
     *
18
     * @var string
19
     */
20
    public $temporaryChunksFolder;
21
22
    /**
23
     * Chunks will be cleaned once in 1000 requests on average.
24
     *
25
     * @var float
26
     */
27
    public $chunksCleanupProbability = 0.001;
28
29
    /**
30
     * By default, chunks are considered loose and deletable, in 1 week.
31
     *
32
     * @var int
33
     */
34
    public $chunksExpireIn = 604800;
35
36
    /**
37
     * Upload size limit.
38
     *
39
     * @var int
40
     */
41
    public $sizeLimit;
42
43
    public function __construct()
44
    {
45
        $this->filesystem = app('filesystem')->disk();
46
        $this->temporaryChunksFolder = DIRECTORY_SEPARATOR . '_chunks';
47
        if (app('config')->has('filesystems.chunks_ttl') && is_int(config('filesystems.chunks_ttl'))) {
48
            $this->chunksExpireIn = config('filesystems.chunks_ttl');
49
        }
50
        if (app('config')->has('filesystems.size_limit') && is_int(config('filesystems.size_limit'))) {
51
            $this->sizeLimit = config('filesystems.size_limit');
52
        }
53
    }
54
55
    /**
56
     * Write the uploaded file to the local filesystem.
57
     *
58
     * @param \Illuminate\Http\Request $request
59
     *
60
     * @return \Illuminate\Http\JsonResponse
61
     */
62
    public function handleUpload(Request $request)
63
    {
64
        $fineUploaderUuid = null;
65
        if ($request->has('qquuid')) {
66
            $fineUploaderUuid = $request->get('qquuid');
67
        }
68
69
        //------------------------------
70
        // Is it Post-processing?
71
        //------------------------------
72
73
        if ($request->has('post-process') && $request->get('post-process') == 1) {
74
            # Combine chunks.
75
            $this->combineChunks($request);
76
77
            return collect(event(new Uploaded($fineUploaderUuid, $request)))->last(); // Return the result of the second event listener.
78
        }
79
80
        //----------------
81
        // Prelim work.
82
        //----------------
83
84
        $filesystem = app('filesystem')->disk();
85
86
        if (!file_exists($this->temporaryChunksFolder) || !is_dir($this->temporaryChunksFolder)) {
87
            $filesystem->makeDirectory($this->temporaryChunksFolder);
88
        }
89
90
        # Temp folder writable?
91
        if (!is_writable($absolutePathToTemporaryChunksFolder = config('filesystems.disks.local.root') . $this->temporaryChunksFolder) || !is_executable($absolutePathToTemporaryChunksFolder)) {
92
            throw new FileStreamExceptions\TemporaryUploadFolderNotWritableException;
93
        }
94
95
        # Cleanup chunks.
96
        if (1 === mt_rand(1, 1 / $this->chunksCleanupProbability)) {
97
            $this->cleanupChunks();
98
        }
99
100
        # Check upload size against the size-limit, if any.
101
        if (!empty($this->sizeLimit)) {
102
            $uploadIsTooLarge = false;
103
            $request->has('qqtotalfilesize') && intval($request->get('qqtotalfilesize')) > $this->sizeLimit && $uploadIsTooLarge = true;
104
            $this->filesizeFromHumanReadableToBytes(ini_get('post_max_size')) < $this->sizeLimit && $uploadIsTooLarge = true;
105
            $this->filesizeFromHumanReadableToBytes(ini_get('upload_max_filesize')) < $this->sizeLimit && $uploadIsTooLarge = true;
106
            if ($uploadIsTooLarge) {
107
                throw new FileStreamExceptions\UploadTooLargeException;
108
            }
109
        }
110
111
        # Is there attempt for multiple file uploads?
112
        $collectionOfUploadedFiles = collect($request->file());
113
        if ($collectionOfUploadedFiles->count() > 1) {
114
            throw new FileStreamExceptions\MultipleSimultaneousUploadsNotAllowedException;
115
        }
116
117
        /** @var UploadedFile $file */
118
        $file = $collectionOfUploadedFiles->first();
119
120
        //--------------------
121
        // Upload handling.
122
        //--------------------
123
124
        if ($file->getSize() == 0) {
125
            throw new FileStreamExceptions\UploadIsEmptyException;
126
        }
127
128
        $name = $file->getClientOriginalName();
129
        if ($request->has('qqfilename')) {
130
            $name = $request->get('qqfilename');
131
        }
132
        if (empty($name)) {
133
            throw new FileStreamExceptions\UploadFilenameIsEmptyException;
134
        }
135
136
        $totalNumberOfChunks = $request->has('qqtotalparts') ? $request->get('qqtotalparts') : 1;
137
138
        if ($totalNumberOfChunks > 1) {
139
            $chunkIndex = intval($request->get('qqpartindex'));
140
            $targetFolder = $this->temporaryChunksFolder . DIRECTORY_SEPARATOR . $fineUploaderUuid;
141
            if (!$filesystem->exists($targetFolder)) {
142
                $filesystem->makeDirectory($targetFolder);
143
            }
144
145
            if (!$file->isValid()) {
146
                throw new FileStreamExceptions\UploadAttemptFailedException;
147
            }
148
            $file->move(storage_path('app' . $targetFolder), $chunkIndex);
149
150
            return response()->json(['success' => true, 'uuid' => $fineUploaderUuid]);
151
        } else {
152
            if (!$file->isValid()) {
153
                throw new FileStreamExceptions\UploadAttemptFailedException;
154
            }
155
            $file->move(storage_path('app'), $fineUploaderUuid);
156
157
            return collect(event(new Uploaded($fineUploaderUuid, $request)))->last(); // Return the result of the second event listener.
158
        }
159
    }
160
161
    /**
162
     * @param \Illuminate\Http\Request $request
163
     *
164
     * @return bool
165
     */
166
    public function isUploadResumable(Request $request)
167
    {
168
        $filesystem = app('filesystem')->disk();
169
        $fineUploaderUuid = $request->get('qquuid');
170
        $chunkIndex = intval($request->get('qqpartindex'));
171
        $numberOfExistingChunks = count($filesystem->files($this->temporaryChunksFolder . DIRECTORY_SEPARATOR . $fineUploaderUuid));
172
        if ($numberOfExistingChunks < $chunkIndex) {
173
            throw new FileStreamExceptions\UploadIncompleteException;
174
        }
175
176
        return true;
177
    }
178
179
    /**
180
     * @param string $size
181
     *
182
     * @return false|string
183
     */
184
    public function filesizeFromHumanReadableToBytes($size)
185
    {
186
        if (preg_match('/^([\d,.]+)\s?([kmgtpezy]?i?b)$/i', $size, $matches) !== 1) {
187
            return false;
188
        }
189
        $coefficient = $matches[1];
190
        $prefix = strtolower($matches[2]);
191
192
        $binaryPrefices = ['b', 'kib', 'mib', 'gib', 'tib', 'pib', 'eib', 'zib', 'yib'];
193
        $decimalPrefices = ['b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'];
194
195
        $base = in_array($prefix, $binaryPrefices) ? 1024 : 1000;
196
        $flippedPrefixMap = $base == 1024 ? array_flip($binaryPrefices) : array_flip($decimalPrefices);
197
        $factor = array_pull($flippedPrefixMap, $prefix);
198
199
        return sprintf("%d", bcmul(str_replace(',', '', $coefficient), bcpow($base, $factor)));
200
    }
201
202
    /**
203
     * @param int  $bytes
204
     * @param int  $decimals
205
     * @param bool $binary
206
     *
207
     * @return string
208
     */
209
    public function filesizeFromBytesToHumanReadable($bytes, $decimals = 2, $binary = true)
210
    {
211
        $binaryPrefices = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
212
        $decimalPrefices = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
213
        $factor = intval(floor((strlen($bytes) - 1) / 3));
214
215
        return sprintf("%.{$decimals}f", $bytes / pow($binary ? 1024 : 1000, $factor)) . ' ' . $binary ? $binaryPrefices[$factor] : $decimalPrefices[$factor];
216
    }
217
218
    /**
219
     * @param string $path
220
     *
221
     * @return string
222
     * @throws \App\Exceptions\FileStream\NotFoundException
223
     */
224
    public function getAbsolutePath($path)
225
    {
226
        return config('filesystems.disks.local.root') . DIRECTORY_SEPARATOR . trim($path, DIRECTORY_SEPARATOR);
227
    }
228
229
    private function cleanupChunks()
230
    {
231
        $filesystem = app('filesystem')->disk('local');
232
        foreach ($filesystem->directories($this->temporaryChunksFolder) as $file) {
233
            if (time() - $filesystem->lastModified($file) > $this->chunksExpireIn) {
234
                $filesystem->deleteDirectory($file);
235
            }
236
        }
237
    }
238
239
    /**
240
     * @param \Illuminate\Http\Request $request
241
     *
242
     * @return void
243
     */
244
    private function combineChunks(Request $request)
245
    {
246
        # Prelim
247
        $filesystem = app('filesystem')->disk();
248
        $fineUploaderUuid = $request->get('qquuid');
249
        $chunksFolder = $this->temporaryChunksFolder . DIRECTORY_SEPARATOR . $fineUploaderUuid;
250
        $totalNumberOfChunks = $request->has('qqtotalparts') ? intval($request->get('qqtotalparts')) : 1;
251
252
        # Do we have all chunks?
253
        $numberOfExistingChunks = count($filesystem->files($chunksFolder));
254
        if ($numberOfExistingChunks != $totalNumberOfChunks) {
255
            throw new FileStreamExceptions\UploadIncompleteException;
256
        }
257
258
        # We have all chunks, proceed with combine.
259
        $targetStream = fopen($this->getAbsolutePath($fineUploaderUuid), 'wb');
260
        for ($i = 0; $i < $totalNumberOfChunks; $i++) {
261
            $chunkStream = fopen($this->getAbsolutePath($chunksFolder . DIRECTORY_SEPARATOR . $i), 'rb');
262
            stream_copy_to_stream($chunkStream, $targetStream);
263
            fclose($chunkStream);
264
        }
265
        fclose($targetStream);
266
        $filesystem->deleteDirectory($chunksFolder);
267
    }
268
}
269