TbBackupRestoreCommand   A
last analyzed

Complexity

Total Complexity 36

Size/Duplication

Total Lines 298
Duplicated Lines 0 %

Importance

Changes 1
Bugs 0 Features 0
Metric Value
wmc 36
eloc 128
c 1
b 0
f 0
dl 0
loc 298
rs 9.52

10 Methods

Rating   Name   Duplication   Size   Complexity  
A validateFile() 0 32 6
A assignBackupFile() 0 24 4
A loadDatabase() 0 29 4
A loadFiles() 0 12 2
A wipeDisk() 0 18 4
A __construct() 0 3 1
A cleanup() 0 4 1
B handle() 0 79 10
A copyDisk() 0 18 2
A checkVersion() 0 21 2
1
<?php
2
3
namespace App\Console\Commands;
4
5
use Zip;
6
use PragmaRX\Version\Package\Version;
7
8
use Illuminate\Console\Command;
9
use Illuminate\Support\Facades\DB;
10
use Illuminate\Support\Facades\Log;
11
use Illuminate\Support\Facades\File;
12
use Illuminate\Support\Facades\Storage;
13
use Illuminate\Database\QueryException;
14
15
class TbBackupRestoreCommand extends Command
16
{
17
    protected $signature   = 'tb_backup:restore {filename?} {--confirmed}';
18
    protected $description = 'Restore the Tech Bench from a previously saved backup';
19
    protected $filename;
20
    protected $basename;
21
22
    /**
23
     * Create a new command instance
24
     */
25
    public function __construct()
26
    {
27
        parent::__construct();
28
    }
29
30
    /**
31
     * Execute the console command
32
     */
33
    public function handle()
34
    {
35
        $this->newLine();
36
        $this->info('Starting Tech Bench Restore');
37
38
        //  If there was not a filename supplied, give the choice of what file to restore
39
        if(is_null($this->argument('filename')))
40
        {
41
            if(!$this->assignBackupFile())
42
            {
43
                return 0;
44
            }
45
        }
46
        else
47
        {
48
            $this->filename = $this->argument('filename');
49
        }
50
51
        //  Verify that the backup file exists
52
        if(!Storage::disk('backups')->exists($this->filename))
0 ignored issues
show
Bug introduced by
It seems like $this->filename can also be of type array; however, parameter $path of Illuminate\Filesystem\FilesystemAdapter::exists() does only seem to accept string, maybe add an additional type check? ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-type  annotation

52
        if(!Storage::disk('backups')->exists(/** @scrutinizer ignore-type */ $this->filename))
Loading history...
53
        {
54
            $this->error('     The filename entered does not exist                                           ');
55
            $this->error('                  Exiting...                                                       ');
56
            return 0;
57
        }
58
59
        //  Verify a proper backup file
60
        if(!$this->validateFile())
61
        {
62
            $this->error('     The backup file specified is not a Tech Bench Backup                          ');
63
            $this->error('                  Exiting...                                                       ');
64
            return 0;
65
        }
66
67
        if(!$this->checkVersion())
68
        {
69
            $this->cleanup();
70
            return 0;
71
        }
72
73
        //  Verify that the user wants to run this process
74
        if(!$this->option('confirmed'))
75
        {
76
            $this->warn(' ___________________________________________________________________ ');
77
            $this->warn('|                      IMPORTANT NOTE:                              |');
78
            $this->warn('|   ALL EXISTING DATA WILL BE ERASED AND REPLACED WITH THE BACKUP   |');
79
            $this->warn('|___________________________________________________________________|');
80
            $this->warn('                                                                     ');
81
        }
82
83
        if(!$this->option('confirmed') && !$this->confirm('Are you sure?'))
84
        {
85
            $this->cleanup();
86
            $this->line('Operation Canceled');
87
            return 0;
88
        }
89
90
        //  Start the restore process
91
        Log::critical('Restoring backup from filename - '.$this->filename);
92
        $this->newLine();
93
        $this->warn('Restoring backup from filename - '.$this->filename);
94
95
        $this->call('down');
96
        $this->newLine();
97
        if(!$this->loadDatabase())
0 ignored issues
show
Bug Best Practice introduced by
The expression $this->loadDatabase() of type boolean|null is loosely compared to false; this is ambiguous if the boolean can be false. You might want to explicitly use !== null instead.

If an expression can have both false, and null as possible values. It is generally a good practice to always use strict comparison to clearly distinguish between those two values.

$a = canBeFalseAndNull();

// Instead of
if ( ! $a) { }

// Better use one of the explicit versions:
if ($a !== null) { }
if ($a !== false) { }
if ($a !== null && $a !== false) { }
Loading history...
98
        {
99
            $this->error('     Unable to modify database structure                                             ');
100
            $this->error('     Please verify that the database user in the .env file has write permissions     ');
101
            $this->error('     Exiting....                                                                     ');
102
            $this->cleanup();
103
            return 0;
104
        }
105
        $this->loadFiles();
106
107
        // $this->cleanup();
108
        $this->newLine();
109
        $this->info('Tech Bench has been restored');
110
        $this->call('up');
111
        return 0;
112
    }
113
114
    /**
115
     * If no filename was specified, give user a list of available files to select from
116
     */
117
    protected function assignBackupFile()
118
    {
119
        $backupList = Storage::disk('backups')->files();
120
        $backups    = ['Cancel'];
121
        foreach($backupList as $b)
122
        {
123
            $parts = pathinfo($b);
124
            if($parts['extension'] === 'zip')
125
            {
126
                $backups[] = $b;
127
            }
128
        }
129
130
        $this->line('Please select which backup file to restore');
131
        $choice = $this->choice('Select Number:', $backups);
132
133
        if($choice === 'Cancel')
134
        {
135
            $this->line('Canceling...');
136
            return false;
137
        }
138
139
        $this->filename = $choice;
140
        return true;
141
    }
142
143
    /**
144
     * Verify that the file is in fact a valid Tech Bench backup file (to the best of our abilities)
145
     */
146
    protected function validateFile()
147
    {
148
        $this->line('Checking backup file');
149
150
        //  This must be a .zip file
151
        $fileParts = pathinfo($this->filename);
152
        $this->basename = $fileParts['filename'].DIRECTORY_SEPARATOR;
153
        if($fileParts['extension'] !== 'zip')
154
        {
155
            return false;
156
        }
157
158
        //  Open and extract the archive file
159
        $archive = Zip::open(config('filesystems.disks.backups.root').DIRECTORY_SEPARATOR.$this->filename);
160
        $archive->extract(config('filesystems.disks.backups.root').DIRECTORY_SEPARATOR.$this->basename);
161
        $archive->close();
162
163
        //  Make sure that the version, .env, and module files are there
164
        if(Storage::disk('backups')->missing($this->basename.'.env')
165
                    || Storage::disk('backups')->missing($this->basename.'modules.txt')
166
                    || Storage::disk('backups')->missing($this->basename.'version.txt'))
167
        {
168
            return false;
169
        }
170
171
        //  Verify that the .env file is writeable
172
        if(!is_writable(base_path().DIRECTORY_SEPARATOR.'.env'))
173
        {
174
            return false;
175
        }
176
177
        return true;
178
    }
179
180
    /**
181
     * Remove any files created by this process
182
     */
183
    protected function cleanup()
184
    {
185
        $this->line('Cleaning up...');
186
        Storage::disk('backups')->deleteDirectory($this->basename);
187
    }
188
189
    /**
190
     * Make sure that the Tech Bench backup is the same version as the Tech Bench
191
     */
192
    protected function checkVersion()
193
    {
194
        //  Backup file version
195
        $verText = Storage::disk('backups')->get($this->basename.DIRECTORY_SEPARATOR.'version.txt');
196
        $verArr  = explode(' ', $verText);
197
        $bkVer   = floatval($verArr[1]);
198
        //  Tech Bench Application version
199
        $verObj = new Version;
200
        $appVer = floatval($verObj->major().'.'.$verObj->minor());
201
202
        if($appVer < $bkVer)
203
        {
204
            $this->newLine();
205
            $this->error('|     This Backup is from a newer version of Tech Bench                              |');
206
            $this->error('|     Please install the version '.$bkVer.' before trying to restore this backup            |');
207
            $this->error('|     Exiting...                                                                     |');
208
            $this->newLine();
209
            return false;
210
        }
211
212
        return true;
213
    }
214
215
    /**
216
     * Load the files from the backup
217
     */
218
    protected function loadFiles()
219
    {
220
        //  Start with the .env file
221
        $env = Storage::disk('backups')->get($this->basename.'.env');
222
        File::put(base_path().DIRECTORY_SEPARATOR.'.env', $env);
223
224
        //  Load application files if they are part of the backup
225
        if(Storage::disk('backups')->exists($this->basename.'app'))
226
        {
227
            $this->line('Restoring files');
228
            $this->wipeDisk('local');
229
            $this->copyDisk('local');
230
        }
231
232
    }
233
234
    /**
235
     * Load the database from the backup
236
     */
237
    protected function loadDatabase()
238
    {
239
        if(Storage::disk('backups')->exists($this->basename.'backup.sql'))
240
        {
241
            $this->line('Restoring database');
242
            try{
243
244
                DB::connection(DB::getDefaultConnection())
245
                ->getSchemaBuilder()
246
                ->dropAllTables();
247
                DB::reconnect();
248
            }
249
            catch(QueryException $e)
250
            {
251
                report($e);
252
                return false;
253
            }
254
255
            //  Input the database information one line at a time
256
            $dbFile = file(Storage::disk('backups')->path($this->basename.'backup.sql'));
257
            foreach($dbFile as $line)
258
            {
259
                DB::unprepared(str_replace('\r\n', '', $line));
260
            }
261
262
            //  Run any migrations in case the application is newer than the backup
263
            $this->callSilently('migrate');
264
265
            return true;
266
        }
267
    }
268
269
    /**
270
     * Copy all of the files from a folder into a disk instance
271
     */
272
    protected function copyDisk($disk)
273
    {
274
        //  Get the root folder name
275
        $folder = config('filesystems.disks.'.$disk.'.base_folder');
276
        $files  = Storage::disk('backups')->allFiles($this->basename.$folder);
277
278
        foreach($files as $file)
279
        {
280
            $data    = Storage::disk('backups')->get($file);
281
            //  Trim the file path to the correct new path
282
            //  If this is a Windows server, the directory separator will be incorrect
283
            $rename = str_replace(str_replace('\\', '/', $this->basename).$folder, '', $file);
284
285
            Storage::disk($disk)->put($rename, $data);
286
        }
287
288
        //  Make sure that the symbolic link for the public folder exists
289
        $this->callSilently('storage:link');
290
    }
291
292
    /**
293
     * Wipe a directory along with all sub-directories
294
     */
295
    protected function wipeDisk($disk)
296
    {
297
        //  Clear all files from the disk
298
        $files = Storage::disk($disk)->allFiles();
299
300
        foreach($files as $file)
301
        {
302
            if($file != '.gitignore')
303
            {
304
                Storage::disk($disk)->delete($file);
305
            }
306
        }
307
308
        //  Clear all sub directories from the disk
309
        $folders = Storage::disk($disk)->directories();
310
        foreach($folders as $folder)
311
        {
312
            Storage::disk($disk)->deleteDirectory($folder);
313
        }
314
    }
315
}
316