2021-05-12 17:44:39 -07:00
< ? php
namespace App\Console\Commands ;
use Illuminate\Console\Command ;
use ZipArchive ;
class RestoreFromBackup extends Command
{
/**
* The name and signature of the console command .
*
* @ var string
*/
protected $signature = ' snipeit : restore
{ -- force : Skip the danger prompt ; assuming you hit " y " }
2021-05-21 15:23:23 -07:00
{ filename : The zip file to be migrated }
{ -- no - progress : Don\ 't show a progress bar}' ;
2021-05-12 17:44:39 -07:00
/**
* The console command description .
*
* @ var string
*/
protected $description = 'Restore from a previously created backup' ;
/**
* Create a new command instance .
*
* @ return void
*/
public function __construct ()
{
parent :: __construct ();
}
/**
* Execute the console command .
*
* @ return mixed
*/
public function handle ()
{
$dir = getcwd ();
print " Current working directory is: $dir\n " ;
//
$filename = $this -> argument ( 'filename' );
if ( ! $filename ) {
return $this -> error ( " Missing required filename " );
}
if ( ! $this -> option ( 'force' ) && ! $this -> confirm ( 'Are you sure you wish to restore from the given backup file? This can lead to MASSIVE DATA LOSS!' )) {
return $this -> error ( " Data loss not confirmed " );
}
if ( config ( 'database.default' ) != 'mysql' ) {
return $this -> error ( " DB_CONNECTION must be MySQL in order to perform a restore. Detected: " . config ( 'database.default' ));
}
$za = new ZipArchive ();
$errcode = $za -> open ( $filename , ZipArchive :: RDONLY );
if ( $errcode !== true ) {
$errors = [
ZipArchive :: ER_EXISTS => " File already exists. " ,
ZipArchive :: ER_INCONS => " Zip archive inconsistent. " ,
ZipArchive :: ER_INVAL => " Invalid argument. " ,
ZipArchive :: ER_MEMORY => " Malloc failure. " ,
ZipArchive :: ER_NOENT => " No such file. " ,
ZipArchive :: ER_NOZIP => " Not a zip archive. " ,
ZipArchive :: ER_OPEN => " Can't open file. " ,
ZipArchive :: ER_READ => " Read error. " ,
ZipArchive :: ER_SEEK => " Seek error. "
];
return $this -> error ( " Could not access file: " . $filename . " - " . array_key_exists ( $errcode , $errors ) ? $errors [ $errcode ] : " Unknown reason: $errcode " );
}
$private_dirs = [
2021-05-21 15:23:23 -07:00
'storage/private_uploads/assets' , // these are asset _files_, not the pictures.
2021-05-12 17:44:39 -07:00
'storage/private_uploads/audits' ,
'storage/private_uploads/imports' ,
'storage/private_uploads/assetmodels' ,
'storage/private_uploads/users' ,
'storage/private_uploads/licenses' ,
2021-05-21 15:23:23 -07:00
'storage/private_uploads/signatures'
2021-05-12 17:44:39 -07:00
];
$private_files = [
'storage/oauth-private.key' ,
'storage/oauth-public.key'
];
$public_dirs = [
'public/uploads/companies' ,
'public/uploads/components' ,
'public/uploads/categories' ,
'public/uploads/manufacturers' ,
2021-05-21 15:23:23 -07:00
//'public/uploads/barcodes', // we don't want this, let the barcodes be regenerated
2021-05-12 17:44:39 -07:00
'public/uploads/consumables' ,
'public/uploads/departments' ,
'public/uploads/avatars' ,
'public/uploads/suppliers' ,
2021-05-21 15:23:23 -07:00
'public/uploads/assets' , // these are asset _pictures_, not asset files
2021-05-12 17:44:39 -07:00
'public/uploads/locations' ,
'public/uploads/accessories' ,
'public/uploads/models' ,
2021-05-21 15:23:23 -07:00
'public/uploads/categories' ,
'public/uploads/avatars' ,
'public/uploads/manufacturers'
2021-05-12 17:44:39 -07:00
];
$public_files = [
2021-05-21 15:23:23 -07:00
'public/uploads/logo.png' ,
'public/uploads/setting-email_logo*' ,
'public/uploads/setting-label_logo*' ,
'public/uploads/setting-logo*' ,
'public/uploads/favicon.*' ,
'public/uploads/favicon-uploaded.*'
2021-05-12 17:44:39 -07:00
];
$all_files = $private_dirs + $public_dirs ;
$sqlfiles = [];
$sqlfile_indices = [];
$interesting_files = [];
2021-05-21 15:23:23 -07:00
$boring_files = [];
2021-05-12 17:44:39 -07:00
for ( $i = 0 ; $i < $za -> numFiles ; $i ++ ) {
$stat_results = $za -> statIndex ( $i );
// echo "index: $i\n";
// print_r($stat_results);
$raw_path = $stat_results [ 'name' ];
if ( strpos ( $raw_path , '\\' ) !== false ) { //found a backslash, swap it to forward-slash
$raw_path = strtr ( $raw_path , '\\' , '/' );
//print "Translating file: ".$stat_results['name']." to: ".$raw_path."\n";
}
2021-05-21 15:23:23 -07:00
// skip macOS resource fork files (?!?!?!)
if ( strpos ( $raw_path , " __MACOSX " ) !== false && strpos ( $raw_path , " ._ " ) !== false ) {
//print "SKIPPING macOS Resource fork file: $raw_path\n";
$boring_files [] = $raw_path ;
continue ;
}
2021-05-12 17:44:39 -07:00
if ( @ pathinfo ( $raw_path )[ 'extension' ] == " sql " ) {
print " Found a sql file! \n " ;
$sqlfiles [] = $raw_path ;
$sqlfile_indices [] = $i ;
2021-05-21 15:23:23 -07:00
continue ;
2021-05-12 17:44:39 -07:00
}
2021-05-21 15:23:23 -07:00
foreach ( array_merge ( $private_dirs , $public_dirs ) as $dir ) {
2021-05-12 17:44:39 -07:00
$last_pos = strrpos ( $raw_path , $dir . '/' );
if ( $last_pos !== false ) {
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $dir - last_pos+strlen(\$dir) is: ".($last_pos+strlen($dir))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
2021-05-21 15:23:23 -07:00
//print("We would copy $raw_path to $dir.\n"); //FIXME append to a path?
2021-05-12 17:44:39 -07:00
$interesting_files [ $raw_path ] = [ 'dest' => $dir , 'index' => $i ];
2021-05-21 15:23:23 -07:00
continue 2 ;
2021-05-12 17:44:39 -07:00
if ( $last_pos + strlen ( $dir ) + 1 == strlen ( $raw_path )) {
// we don't care about that; we just want files with the appropriate prefix
//print("FOUND THE EXACT DIRECTORY: $dir AT: $raw_path!!!\n");
}
}
}
2021-05-21 15:23:23 -07:00
$good_extensions = [ " png " , " gif " , " jpg " , " svg " , " jpeg " , " doc " , " docx " , " pdf " , " txt " ,
" zip " , " rar " , " xls " , " xlsx " , " lic " , " xml " , " rtf " , " webp " , " key " , " ico " ];
foreach ( array_merge ( $private_files , $public_files ) as $file ) {
$has_wildcard = ( strpos ( $file , " * " ) !== false );
if ( $has_wildcard ) {
$file = substr ( $file , 0 , - 1 ); //trim last character (which should be the wildcard)
}
2021-05-12 17:44:39 -07:00
$last_pos = strrpos ( $raw_path , $file ); // no trailing slash!
if ( $last_pos !== false ) {
2021-05-21 15:23:23 -07:00
$extension = strtolower ( pathinfo ( $raw_path , PATHINFO_EXTENSION ));
if ( ! in_array ( $extension , $good_extensions )) {
$this -> warn ( " Potentially unsafe file " . $raw_path . " is being skipped " );
$boring_files [] = $raw_path ;
continue 2 ;
}
2021-05-12 17:44:39 -07:00
//print("INTERESTING - last_pos is $last_pos when searching $raw_path for $file - last_pos+strlen(\$file) is: ".($last_pos+strlen($file))." and strlen(\$rawpath) is: ".strlen($raw_path)."\n");
2021-05-21 15:23:23 -07:00
//no wildcards found in $file, process 'normally'
if ( $last_pos + strlen ( $file ) == strlen ( $raw_path ) || $has_wildcard ) { //again, no trailing slash. or this is a wildcard and we just take it.
// print("FOUND THE EXACT FILE: $file AT: $raw_path!!!\n"); //we *do* care about this, though.
2021-05-12 17:44:39 -07:00
$interesting_files [ $raw_path ] = [ 'dest' => dirname ( $file ), 'index' => $i ];
2021-05-21 15:23:23 -07:00
continue 2 ;
2021-05-12 17:44:39 -07:00
}
}
}
2021-05-21 15:23:23 -07:00
$boring_files [] = $raw_path ; //if we've gotten to here and haven't continue'ed our way into the next iteration, we don't want this file
} // end of pre-processing the ZIP file for-loop
2021-05-12 17:44:39 -07:00
// print_r($interesting_files);exit(-1);
if ( count ( $sqlfiles ) != 1 ) {
return $this -> error ( " There should be exactly *one* sql backup file found, found: " . ( count ( $sqlfiles ) == 0 ? " None " : implode ( " , " , $sqlfiles )));
}
if ( strpos ( $sqlfiles [ 0 ], " db-dumps " ) === false ) {
//return $this->error("SQL backup file is missing 'db-dumps' component of full pathname: ".$sqlfiles[0]);
//older Snipe-IT installs don't have the db-dumps subdirectory component
}
//how to invoke the restore?
$pipes = [];
$env_vars = getenv ();
$env_vars [ 'MYSQL_PWD' ] = config ( " database.connections.mysql.password " );
$proc_results = proc_open ( " mysql -h " . escapeshellarg ( config ( 'database.connections.mysql.host' )) . " -u " . escapeshellarg ( config ( 'database.connections.mysql.username' )) . " " . escapeshellarg ( config ( 'database.connections.mysql.database' )), // yanked -p since we pass via ENV
[ 0 => [ 'pipe' , 'r' ], 1 => [ 'pipe' , 'w' ], 2 => [ 'pipe' , 'w' ]],
$pipes ,
null ,
$env_vars ); // this is not super-duper awesome-secure, but definitely more secure than showing it on the CLI, or dropping temporary files with passwords in them.
if ( $proc_results === false ) {
return $this -> error ( " Unable to invoke mysql via CLI " );
}
// $this->info("Stdout says? ".fgets($pipes[1])); //FIXME: I think we might need to set non-blocking mode to use this properly?
// $this->info("Stderr says? ".fgets($pipes[2])); //FIXME: ditto, same.
// should we read stdout?
// fwrite($pipes[0],config("database.connections.mysql.password")."\n"); //this doesn't work :(
//$sql_contents = fopen($sqlfiles[0], "r"); //NOPE! This isn't a real file yet, silly-billy!
$sql_stat = $za -> statIndex ( $sqlfile_indices [ 0 ]);
2021-05-21 15:23:23 -07:00
//$this->info("SQL Stat is: ".print_r($sql_stat,true));
2021-05-12 17:44:39 -07:00
$sql_contents = $za -> getStream ( $sql_stat [ 'name' ]);
if ( $sql_contents === false ) {
$stdout = fgets ( $pipes [ 1 ]);
$this -> info ( $stdout );
$stderr = fgets ( $pipes [ 2 ]);
$this -> info ( $stderr );
return false ;
}
while (( $buffer = fgets ( $sql_contents )) !== false ) {
//$this->info("Buffer is: '$buffer'");
$bytes_written = fwrite ( $pipes [ 0 ], $buffer );
if ( $bytes_written === false ) {
$stdout = fgets ( $pipes [ 1 ]);
$this -> info ( $stdout );
$stderr = fgets ( $pipes [ 2 ]);
$this -> info ( $stderr );
return false ;
}
}
fclose ( $pipes [ 0 ]);
fclose ( $sql_contents );
fclose ( $pipes [ 1 ]);
fclose ( $pipes [ 2 ]);
//wait, have to do fclose() on all pipes first?
$close_results = proc_close ( $proc_results );
if ( $close_results != 0 ) {
return $this -> error ( " There may have been a problem with the database import: Error number " . $close_results );
}
//and now copy the files over too (right?)
//FIXME - we don't prune the filesystem space yet!!!!
2021-05-21 15:23:23 -07:00
if ( $this -> option ( 'no-progress' )) {
$bar = null ;
} else {
$bar = $this -> output -> createProgressBar ( count ( $interesting_files ));
}
2021-05-12 17:44:39 -07:00
foreach ( $interesting_files AS $pretty_file_name => $file_details ) {
$ugly_file_name = $za -> statIndex ( $file_details [ 'index' ])[ 'name' ];
$fp = $za -> getStream ( $ugly_file_name );
2021-05-21 15:23:23 -07:00
//$this->info("Weird problem, here are file details? ".print_r($file_details,true));
2021-05-12 17:44:39 -07:00
$migrated_file = fopen ( $file_details [ 'dest' ] . " / " . basename ( $pretty_file_name ), " w " );
while (( $buffer = fgets ( $fp )) !== false ) {
fwrite ( $migrated_file , $buffer );
}
fclose ( $migrated_file );
fclose ( $fp );
2021-05-21 15:23:23 -07:00
//$this->info("Wrote $ugly_file_name to $pretty_file_name");
if ( $bar ) {
$bar -> advance ();
}
}
if ( $bar ) {
$bar -> finish ();
$this -> line ( " " );
} else {
$this -> info ( count ( $interesting_files ) . " files were succesfully transferred " );
}
foreach ( $boring_files as $boring_file ) {
$this -> warn ( $boring_file . " was skipped. " );
2021-05-12 17:44:39 -07:00
}
}
}