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

Snapshot::streamFileIntoDB()   C

Complexity

Conditions 16
Paths 16

Size

Total Lines 65

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
dl 0
loc 65
rs 5.5666
c 0
b 0
f 0
cc 16
nc 16
nop 2

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
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
        $this->useStream ? $this->loadStream($connectionName) : $this->loadAsync($connectionName);
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
        $this->getFileHandler($path)->each(function ($line) use (&$tmpLine, &$counter, $connectionName) {
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
                    if (is_array($matches)) {
169
                        unset($matches[0]);
170
171
                        foreach ($matches as $match) {
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
184
                $tmpLine = '';
185
            }
186
            $counter++;
187
        });
188
189
        if ($counter !== false) {
190
            echo PHP_EOL;
191
        }
192
193
        if (! empty($this->errors)) {
194
            return $this->errors;
195
        }
196
197
        return true;
198
    }
199
200
    public function downloadExternalSnapshort()
201
    {
202
        $stream = $this->disk->readStream($this->fileName);
203
        $gzFilePath = (new TemporaryDirectory(config('db-snapshots.temporary_directory_path')))
204
                           ->create()
205
                           ->path('temp-load.tmp').'.gz';
206
        $fileDest = fopen($gzFilePath, 'w');
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