Completed
Pull Request — master (#93)
by Peter
01:27
created

Snapshot::getFileHandler()   A

Complexity

Conditions 4
Paths 1

Size

Total Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 16
rs 9.7333
c 0
b 0
f 0
cc 4
nc 1
nop 1
1
<?php
2
3
namespace Spatie\DbSnapshots;
4
5
use Carbon\Carbon;
6
use Illuminate\Filesystem\FilesystemAdapter as Disk;
7
use Illuminate\Support\Facades\DB;
8
use Illuminate\Support\Facades\File;
9
use Illuminate\Support\Facades\Storage;
10
use Illuminate\Support\LazyCollection;
11
use Spatie\DbSnapshots\Events\DeletedSnapshot;
12
use Spatie\DbSnapshots\Events\DeletingSnapshot;
13
use Spatie\DbSnapshots\Events\LoadedSnapshot;
14
use Spatie\DbSnapshots\Events\LoadingSnapshot;
15
use \Exception;
16
use Spatie\DbSnapshots\Events\SnapshotStatus;
17
use Spatie\TemporaryDirectory\TemporaryDirectory;
18
19
class Snapshot
20
{
21
    /** @var \Illuminate\Filesystem\FilesystemAdapter */
22
    public $disk;
23
24
    /** @var string */
25
    public $fileName;
26
27
    /** @var string */
28
    public $name;
29
30
    /** @var string */
31
    public $compressionExtension = null;
32
33
    /** @var bool */
34
    private $useStream = false;
35
36
    /** @var bool */
37
    private $showProgress = false;
38
39
    /** @var array */
40
    private $errors = [];
41
42
    /** @var int */
43
    CONST STREAM_BUFFER_SIZE = 16384;
44
45
    public function __construct(Disk $disk, string $fileName)
46
    {
47
        $this->disk = $disk;
48
49
        $this->fileName = $fileName;
50
51
        $pathinfo = pathinfo($fileName);
52
53
        if ($pathinfo['extension'] === 'gz') {
54
            $this->compressionExtension = $pathinfo['extension'];
55
            $fileName = $pathinfo['filename'];
56
        }
57
58
        $this->name = pathinfo($fileName, PATHINFO_FILENAME);
59
    }
60
61
    public function useStream()
62
    {
63
        $this->useStream = true;
64
        return $this;
65
    }
66
67
    public function showProgress()
68
    {
69
        $this->showProgress = true;
70
        return $this;
71
    }
72
73
    public function load(string $connectionName = null)
74
    {
75
        event(new LoadingSnapshot($this));
76
77
        if ($connectionName !== null) {
78
            DB::setDefaultConnection($connectionName);
79
        }
80
81
        $this->dropAllCurrentTables();
82
83
        $status = $this->useStream ? $this->loadStream($connectionName) : $this->loadAsync($connectionName);
0 ignored issues
show
Unused Code introduced by
$status is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
84
85
        event(new LoadedSnapshot($this));
86
    }
87
88
    public function getErrors()
89
    {
90
        return $this->errors;
91
    }
92
93
    protected function loadAsync(string $connectionName = null)
94
    {
95
        $dbDumpContents = $this->disk->get($this->fileName);
96
97
        if ($this->compressionExtension === 'gz') {
98
            event(new SnapshotStatus($this, 'Decompressing snapshot...'));
99
            $dbDumpContents = gzdecode($dbDumpContents);
100
        }
101
102
        event(new SnapshotStatus($this, 'Importing SQL...'));
103
104
        DB::connection($connectionName)->unprepared($dbDumpContents);
0 ignored issues
show
Security Bug introduced by
It seems like $dbDumpContents defined by $this->disk->get($this->fileName) on line 95 can also be of type false; however, Illuminate\Database\Conn...Interface::unprepared() does only seem to accept string, did you maybe forget to handle an error condition?

This check looks for type mismatches where the missing type is false. This is usually indicative of an error condtion.

Consider the follow example

<?php

function getDate($date)
{
    if ($date !== null) {
        return new DateTime($date);
    }

    return false;
}

This function either returns a new DateTime object or false, if there was an error. This is a typical pattern in PHP programming to show that an error has occurred without raising an exception. The calling code should check for this returned false before passing on the value to another function or method that may not be able to handle a false.

Loading history...
105
    }
106
107
    protected function loadStream(string $connectionName = null)
108
    {
109
        $dumpFilePath = $this->compressionExtension === 'gz' ?
110
            $this->downloadExternalSnapshort() :
111
            $this->disk->path($this->fileName);
112
113
        return $this->streamFileIntoDB($dumpFilePath, $connectionName);
114
    }
115
116
    protected function getFileHandler($path) : LazyCollection
117
    {
118
        return LazyCollection::make(function () use($path) {
119
            if ($this->compressionExtension === 'gz') {
120
                $handle = gzopen($path, 'r');
121
                while (!gzeof($handle)) {
122
                    yield gzgets($handle, self::STREAM_BUFFER_SIZE);
123
                }
124
            } else {
125
                $handle = $this->disk->readStream($path);
126
                while (($line = fgets($handle)) !== false) {
127
                    yield $line;
128
                }
129
            }
130
        });
131
    }
132
133
    protected function streamFileIntoDB($path, string $connectionName = null)
134
    {
135
        if ($connectionName !== null) {
136
            DB::setDefaultConnection($connectionName);
137
        }
138
139
        $tmpLine = '';
140
        $counter = $this->showProgress ? 0 : false;
141
142
        event(new SnapshotStatus($this, 'Importing SQL...'));
143
144
        $file = $this->getFileHandler($path)->each(function ($line) use(&$tmpLine, &$counter, $connectionName) {
0 ignored issues
show
Unused Code introduced by
$file is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
145
146
            if ($counter !== false && $counter % 500 === 0) {
147
                echo '.';
148
            }
149
150
            // Skip it if line is a comment
151
            if (substr($line, 0, 2) === '--' || trim($line) == '') {
152
                return;
153
            }
154
155
            $tmpLine .= $line;
156
157
            // If the line ends with a semicolon, it is the end of the query - run it
158
            if (substr(trim($line), -1, 1) === ';') {
159
                try {
160
                    DB::connection($connectionName)->unprepared($tmpLine);
161
                } catch (Exception $e) {
162
163
                    if ($counter !== false) {
164
                        echo 'E';
165
                    }
166
167
                    preg_match_all('/INSERT INTO `(.*)`/mU', $e->getMessage(), $matches);
168
169
                    unset($matches[0]);
170
171
                    foreach($matches as $match) {
0 ignored issues
show
Bug introduced by
The expression $matches of type null|array<integer,array<integer,string>> is not guaranteed to be traversable. How about adding an additional type check?

There are different options of fixing this problem.

  1. If you want to be on the safe side, you can add an additional type-check:

    $collection = json_decode($data, true);
    if ( ! is_array($collection)) {
        throw new \RuntimeException('$collection must be an array.');
    }
    
    foreach ($collection as $item) { /** ... */ }
    
  2. If you are sure that the expression is traversable, you might want to add a doc comment cast to improve IDE auto-completion and static analysis:

    /** @var array $collection */
    $collection = json_decode($data, true);
    
    foreach ($collection as $item) { /** .. */ }
    
  3. Mark the issue as a false-positive: Just hover the remove button, in the top-right corner of this issue for more options.

Loading history...
172
                        if (empty($match[0])) {
173
                            continue;
174
                        }
175
                        $tableName = $match[0];
176
                        if (!isset($this->errors[$tableName])) {
177
                            $this->errors[$tableName] = 0;
178
                        }
179
                        $this->errors[$tableName]++;
180
                    }
181
                }
182
183
                $tmpLine = '';
184
            }
185
            $counter++;
186
        });
187
188
        if ($counter !== false) {
189
            echo PHP_EOL;
190
        }
191
192
        if (!empty($this->errors)) {
193
            return $this->errors;
194
        }
195
196
        return true;
197
    }
198
199
    public function downloadExternalSnapshort()
200
    {
201
        $stream      = $this->disk->readStream($this->fileName);
202
        $gzFilePath  = (new TemporaryDirectory(config('db-snapshots.temporary_directory_path')))
203
                           ->create()
204
                           ->path('temp-load.tmp').'.gz';
205
        $fileDest    = fopen($gzFilePath, 'w');
206
        $buffer_size = 16384;
0 ignored issues
show
Unused Code introduced by
$buffer_size is not used, you could remove the assignment.

This check looks for variable assignements that are either overwritten by other assignments or where the variable is not used subsequently.

$myVar = 'Value';
$higher = false;

if (rand(1, 6) > 3) {
    $higher = true;
} else {
    $higher = false;
}

Both the $myVar assignment in line 1 and the $higher assignment in line 2 are dead. The first because $myVar is never used and the second because $higher is always overwritten for every possible time line.

Loading history...
207
208
        event(new SnapshotStatus($this, 'Downloading snapshot...'));
209
210
        if (!file_exists($this->disk->path($this->fileName))) {
211
            while (feof($stream) !== true) {
212
                fwrite($fileDest, gzread($stream, self::STREAM_BUFFER_SIZE));
213
            }
214
        }
215
216
        $this->disk = Storage::disk('local');
217
218
        return $gzFilePath;
219
    }
220
221
    public function delete()
222
    {
223
        event(new DeletingSnapshot($this));
224
225
        $this->disk->delete($this->fileName);
226
227
        event(new DeletedSnapshot($this->fileName, $this->disk));
228
    }
229
230
    public function size(): int
231
    {
232
        return $this->disk->size($this->fileName);
233
    }
234
235
    public function createdAt(): Carbon
236
    {
237
        return Carbon::createFromTimestamp($this->disk->lastModified($this->fileName));
238
    }
239
240
    protected function dropAllCurrentTables()
241
    {
242
        event(new SnapshotStatus($this, 'Dropping all current database tables...'));
243
244
        DB::connection(DB::getDefaultConnection())
245
            ->getSchemaBuilder()
246
            ->dropAllTables();
247
248
        DB::reconnect();
249
    }
250
}
251