FileStream::handleUpload()   F
last analyzed

Complexity

Conditions 24
Paths 2926

Size

Total Lines 96
Code Lines 46

Duplication

Lines 0
Ratio 0 %

Code Coverage

Tests 0
CRAP Score 600

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 96
rs 2
ccs 0
cts 45
cp 0
cc 24
eloc 46
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 App\Models\File;
6
use Illuminate\Http\Request;
7
use Symfony\Component\HttpFoundation\File\UploadedFile;
8
9
class FileStream
10
{
11
    /**
12
     * @var \Illuminate\Contracts\Filesystem\Filesystem
13
     */
14
    public $filesystem;
15
16
    /**
17
     * Folder to hold uploaded chunks.
18
     *
19
     * @var string
20
     */
21
    public $temporaryChunksFolder;
22
23
    /**
24
     * Chunks will be cleaned once in 1000 requests on average.
25
     *
26
     * @var float
27
     */
28
    public $chunksCleanupProbability = 0.001;
29
30
    /**
31
     * By default, chunks are considered loose and deletable, in 1 week.
32
     *
33
     * @var int
34
     */
35
    public $chunksExpireIn;
36
37
    /**
38
     * Upload size limit.
39
     *
40
     * @var int
41
     */
42
    public $sizeLimit;
43
44
    public function __construct()
45
    {
46
        $this->filesystem = app('filesystem')->disk();
47
        $this->chunksExpireIn = config('filesystems.disks.local.chunks_expire_in');
48
        $this->temporaryChunksFolder = DIRECTORY_SEPARATOR . '_chunks';
49
        if (app('config')->has('filesystems.chunks_ttl') && is_int(config('filesystems.chunks_ttl'))) {
50
            $this->chunksExpireIn = config('filesystems.chunks_ttl');
51
        }
52
        if (app('config')->has('filesystems.size_limit') && is_int(config('filesystems.size_limit'))) {
53
            $this->sizeLimit = config('filesystems.size_limit');
54
        }
55
    }
56
57
    /**
58
     * Write the uploaded file to the local filesystem.
59
     *
60
     * @param \Illuminate\Http\Request $request
61
     *
62
     * @return \Illuminate\Http\JsonResponse
63
     */
64
    public function handleUpload(Request $request)
65
    {
66
        $fineUploaderUuid = null;
67
        if ($request->has('qquuid')) {
68
            $fineUploaderUuid = $request->get('qquuid');
69
        }
70
71
        //------------------------------
72
        // Is it Post-processing?
73
        //------------------------------
74
75
        if ($request->has('post-process') && $request->get('post-process') == 1) {
76
            # Combine chunks.
77
            $this->combineChunks($request);
78
79
            return collect(event(new Uploaded($fineUploaderUuid, $request)))->last(); // Return the result of the second event listener.
80
        }
81
82
        //----------------
83
        // Prelim work.
84
        //----------------
85
86
        if (!file_exists($this->temporaryChunksFolder) || !is_dir($this->temporaryChunksFolder)) {
87
            $this->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 (!$this->filesystem->exists($targetFolder)) {
142
                $this->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
        $fineUploaderUuid = $request->get('qquuid');
169
        $chunkIndex = intval($request->get('qqpartindex'));
170
        $numberOfExistingChunks = count($this->filesystem->files($this->temporaryChunksFolder . DIRECTORY_SEPARATOR . $fineUploaderUuid));
171
        if ($numberOfExistingChunks < $chunkIndex) {
172
            throw new FileStreamExceptions\UploadIncompleteException;
173
        }
174
175
        return true;
176
    }
177
178
    /**
179
     * @param string $size
180
     *
181
     * @return false|string
182
     */
183
    public function filesizeFromHumanReadableToBytes($size)
184
    {
185
        if (preg_match('/^([\d,.]+)\s?([kmgtpezy]?i?b)$/i', $size, $matches) !== 1) {
186
            return false;
187
        }
188
        $coefficient = $matches[1];
189
        $prefix = strtolower($matches[2]);
190
191
        $binaryPrefices = ['b', 'kib', 'mib', 'gib', 'tib', 'pib', 'eib', 'zib', 'yib'];
192
        $decimalPrefices = ['b', 'kb', 'mb', 'gb', 'tb', 'pb', 'eb', 'zb', 'yb'];
193
194
        $base = in_array($prefix, $binaryPrefices) ? 1024 : 1000;
195
        $flippedPrefixMap = $base == 1024 ? array_flip($binaryPrefices) : array_flip($decimalPrefices);
196
        $factor = array_pull($flippedPrefixMap, $prefix);
197
198
        return sprintf("%d", bcmul(str_replace(',', '', $coefficient), bcpow($base, $factor)));
199
    }
200
201
    /**
202
     * @param int  $bytes
203
     * @param int  $decimals
204
     * @param bool $binary
205
     *
206
     * @return string
207
     */
208
    public function filesizeFromBytesToHumanReadable($bytes, $decimals = 2, $binary = true)
209
    {
210
        $binaryPrefices = ['B', 'KiB', 'MiB', 'GiB', 'TiB', 'PiB', 'EiB', 'ZiB', 'YiB'];
211
        $decimalPrefices = ['B', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
212
        $factor = intval(floor((strlen($bytes) - 1) / 3));
213
214
        return sprintf("%.{$decimals}f", $bytes / pow($binary ? 1024 : 1000, $factor)) . ' ' . $binary ? $binaryPrefices[$factor] : $decimalPrefices[$factor];
215
    }
216
217
    /**
218
     * @param string $path
219
     *
220
     * @return string
221
     */
222
    public function getAbsolutePath($path)
223
    {
224
        return config('filesystems.disks.local.root') . DIRECTORY_SEPARATOR . trim($path, DIRECTORY_SEPARATOR);
225
    }
226
227
    /**
228
     * @param string $hash
229
     * @param string $tag
230
     *
231
     * @return bool
232
     * @throws \Exception
233
     */
234
    public function deleteFile($hash, $tag = '')
235
    {
236
        /** @var \App\Models\File $file */
237
        $file = File::findOrFail($hash);
238
        if ($file->load('uploaders')->count()) {
0 ignored issues
show
Documentation Bug introduced by
The method count does not exist on object<App\Models\File>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
239
            /** @var \App\Models\User $me */
240
            $me = app('sentinel')->getUser();
241
            if ($file->uploaders->contains('id', $me->id)) {
242
                $pivotToDelete = $file->uploaders()->newPivotStatement()->where('user_id', '=', $me->id);
243
                if (!empty($tag)) {
244
                    $pivotToDelete->where('tag', '=', $tag);
245
                }
246
                $pivotToDelete->delete();
247
                $file->load('uploaders');
248
            }
249
            !$file->uploaders->count() && app('filesystem')->disk($file->disk)->delete($file->path) && $file->delete();
250
        }
251
252
        return true;
253
    }
254
255
    private function cleanupChunks()
256
    {
257
        foreach ($this->filesystem->directories($this->temporaryChunksFolder) as $file) {
258
            if (time() - $this->filesystem->lastModified($file) > $this->chunksExpireIn) {
259
                $this->filesystem->deleteDirectory($file);
260
            }
261
        }
262
    }
263
264
    /**
265
     * @param \Illuminate\Http\Request $request
266
     *
267
     * @return void
268
     */
269
    private function combineChunks(Request $request)
270
    {
271
        # Prelim
272
        $fineUploaderUuid = $request->get('qquuid');
273
        $chunksFolder = $this->temporaryChunksFolder . DIRECTORY_SEPARATOR . $fineUploaderUuid;
274
        $totalNumberOfChunks = $request->has('qqtotalparts') ? intval($request->get('qqtotalparts')) : 1;
275
276
        # Do we have all chunks?
277
        $numberOfExistingChunks = count($this->filesystem->files($chunksFolder));
278
        if ($numberOfExistingChunks != $totalNumberOfChunks) {
279
            throw new FileStreamExceptions\UploadIncompleteException;
280
        }
281
282
        # We have all chunks, proceed with combine.
283
        $targetStream = fopen($this->getAbsolutePath($fineUploaderUuid), 'wb');
284
        for ($i = 0; $i < $totalNumberOfChunks; $i++) {
285
            $chunkStream = fopen($this->getAbsolutePath($chunksFolder . DIRECTORY_SEPARATOR . $i), 'rb');
286
            stream_copy_to_stream($chunkStream, $targetStream);
287
            fclose($chunkStream);
288
        }
289
        fclose($targetStream);
290
        $this->filesystem->deleteDirectory($chunksFolder);
291
    }
292
}
293