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

Snapshot::getErrors()   A

Complexity

Conditions 1
Paths 1

Size

Total Lines 4

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 4
rs 10
c 0
b 0
f 0
cc 1
nc 1
nop 0
1
<?php
2
3
namespace Spatie\DbSnapshots;
4
5
use Carbon\Carbon;
6
use Exception;
7
use Illuminate\Filesystem\FilesystemAdapter as Disk;
8
use Illuminate\Support\Facades\DB;
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 Spatie\DbSnapshots\Events\SnapshotStatus;
16
use Spatie\TemporaryDirectory\TemporaryDirectory;
17
18
class Snapshot
19
{
20
    /** @var \Illuminate\Filesystem\FilesystemAdapter */
21
    public $disk;
22
23
    /** @var string */
24
    public $fileName;
25
26
    /** @var string */
27
    public $name;
28
29
    /** @var string */
30
    public $compressionExtension = null;
31
32
    /** @var bool */
33
    private $useStream = false;
34
35
    /** @var bool */
36
    private $showProgress = false;
37
38
    /** @var array */
39
    private $errors = [];
40
41
    /** @var int */
42
    const STREAM_BUFFER_SIZE = 16384;
43
44
    public function __construct(Disk $disk, string $fileName)
45
    {
46
        $this->disk = $disk;
47
48
        $this->fileName = $fileName;
49
50
        $pathinfo = pathinfo($fileName);
51
52
        if ($pathinfo['extension'] === 'gz') {
53
            $this->compressionExtension = $pathinfo['extension'];
54
            $fileName = $pathinfo['filename'];
55
        }
56
57
        $this->name = pathinfo($fileName, PATHINFO_FILENAME);
58
    }
59
60
    public function useStream()
61
    {
62
        $this->useStream = true;
63
64
        return $this;
65
    }
66
67
    public function showProgress()
68
    {
69
        $this->showProgress = true;
70
71
        return $this;
72
    }
73
74
    public function load(string $connectionName = null)
75
    {
76
        event(new LoadingSnapshot($this));
77
78
        if ($connectionName !== null) {
79
            DB::setDefaultConnection($connectionName);
80
        }
81
82
        $this->dropAllCurrentTables();
83
84
        $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...
85
86
        event(new LoadedSnapshot($this));
87
    }
88
89
    public function getErrors()
90
    {
91
        return $this->errors;
92
    }
93
94
    protected function loadAsync(string $connectionName = null)
95
    {
96
        $dbDumpContents = $this->disk->get($this->fileName);
97
98
        if ($this->compressionExtension === 'gz') {
99
            event(new SnapshotStatus($this, 'Decompressing snapshot...'));
100
            $dbDumpContents = gzdecode($dbDumpContents);
101
        }
102
103
        event(new SnapshotStatus($this, 'Importing SQL...'));
104
105
        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 96 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...
106
    }
107
108
    protected function loadStream(string $connectionName = null)
109
    {
110
        $dumpFilePath = $this->compressionExtension === 'gz' ?
111
            $this->downloadExternalSnapshort() :
112
            $this->disk->path($this->fileName);
113
114
        return $this->streamFileIntoDB($dumpFilePath, $connectionName);
115
    }
116
117
    protected function getFileHandler($path): LazyCollection
118
    {
119
        return LazyCollection::make(function () use ($path) {
120
            if ($this->compressionExtension === 'gz') {
121
                $handle = gzopen($path, 'r');
122
                while (! gzeof($handle)) {
123
                    yield gzgets($handle, self::STREAM_BUFFER_SIZE);
124
                }
125
            } else {
126
                $handle = $this->disk->readStream($path);
127
                while (($line = fgets($handle)) !== false) {
128
                    yield $line;
129
                }
130
            }
131
        });
132
    }
133
134
    protected function streamFileIntoDB($path, string $connectionName = null)
135
    {
136
        if ($connectionName !== null) {
137
            DB::setDefaultConnection($connectionName);
138
        }
139
140
        $tmpLine = '';
141
        $counter = $this->showProgress ? 0 : false;
142
143
        event(new SnapshotStatus($this, 'Importing SQL...'));
144
145
        $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...
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
                    if ($counter !== false) {
163
                        echo 'E';
164
                    }
165
166
                    preg_match_all('/INSERT INTO `(.*)`/mU', $e->getMessage(), $matches);
167
168
                    unset($matches[0]);
169
170
                    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...
171
                        if (empty($match[0])) {
172
                            continue;
173
                        }
174
                        $tableName = $match[0];
175
                        if (! isset($this->errors[$tableName])) {
176
                            $this->errors[$tableName] = 0;
177
                        }
178
                        $this->errors[$tableName]++;
179
                    }
180
                }
181
182
                $tmpLine = '';
183
            }
184
            $counter++;
185
        });
186
187
        if ($counter !== false) {
188
            echo PHP_EOL;
189
        }
190
191
        if (! empty($this->errors)) {
192
            return $this->errors;
193
        }
194
195
        return true;
196
    }
197
198
    public function downloadExternalSnapshort()
199
    {
200
        $stream = $this->disk->readStream($this->fileName);
201
        $gzFilePath = (new TemporaryDirectory(config('db-snapshots.temporary_directory_path')))
202
                           ->create()
203
                           ->path('temp-load.tmp').'.gz';
204
        $fileDest = fopen($gzFilePath, 'w');
205
        $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...
206
207
        event(new SnapshotStatus($this, 'Downloading snapshot...'));
208
209
        if (! file_exists($this->disk->path($this->fileName))) {
210
            while (feof($stream) !== true) {
211
                fwrite($fileDest, gzread($stream, self::STREAM_BUFFER_SIZE));
212
            }
213
        }
214
215
        $this->disk = Storage::disk('local');
216
217
        return $gzFilePath;
218
    }
219
220
    public function delete()
221
    {
222
        event(new DeletingSnapshot($this));
223
224
        $this->disk->delete($this->fileName);
225
226
        event(new DeletedSnapshot($this->fileName, $this->disk));
227
    }
228
229
    public function size(): int
230
    {
231
        return $this->disk->size($this->fileName);
232
    }
233
234
    public function createdAt(): Carbon
235
    {
236
        return Carbon::createFromTimestamp($this->disk->lastModified($this->fileName));
237
    }
238
239
    protected function dropAllCurrentTables()
240
    {
241
        event(new SnapshotStatus($this, 'Dropping all current database tables...'));
242
243
        DB::connection(DB::getDefaultConnection())
244
            ->getSchemaBuilder()
245
            ->dropAllTables();
246
247
        DB::reconnect();
248
    }
249
}
250