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
2022-03-29 08:28:43 -07:00
{ -- force : Skip the danger prompt ; assuming you enter " y " }
{ filename : The zip file to be migrated }
2021-05-21 15:23:23 -07:00
{ -- no - progress : Don\ 't show a progress bar}' ;
2021-05-12 17:44:39 -07:00
/**
* The console command description .
*
* @ var string
*/
2021-11-10 00:07:32 -08:00
protected $description = 'Restore from a previously created Snipe-IT backup file' ;
2021-05-12 17:44:39 -07:00
/**
* Create a new command instance .
*
* @ return void
*/
public function __construct ()
{
parent :: __construct ();
}
2021-11-10 12:44:19 -08:00
public static $buffer_size = 1024 * 1024 ; // use a 1MB buffer, ought to work fine for most cases?
2021-05-12 17:44:39 -07:00
/**
* Execute the console command .
*
* @ return mixed
*/
public function handle ()
{
$dir = getcwd ();
2021-11-10 12:44:19 -08:00
if ( $dir != base_path () ) { // usually only the case when running via webserver, not via command-line
\Log :: debug ( " Current working directory is: $dir , changing directory to: " . base_path ());
chdir ( base_path ()); // TODO - is this *safe* to change on a running script?!
}
2021-05-12 17:44:39 -07:00
//
$filename = $this -> argument ( 'filename' );
2021-06-10 13:15:52 -07:00
if ( ! $filename ) {
return $this -> error ( 'Missing required filename' );
2021-05-12 17:44:39 -07:00
}
2021-06-10 13:15:52 -07:00
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' );
2021-05-12 17:44:39 -07:00
}
if ( config ( 'database.default' ) != 'mysql' ) {
2021-06-10 13:15:52 -07:00
return $this -> error ( 'DB_CONNECTION must be MySQL in order to perform a restore. Detected: ' . config ( 'database.default' ));
2021-05-12 17:44:39 -07:00
}
$za = new ZipArchive ();
2021-06-30 14:53:08 -07:00
$errcode = $za -> open ( $filename /* , ZipArchive::RDONLY */ ); // that constant only exists in PHP 7.4 and higher
2021-05-12 17:44:39 -07:00
if ( $errcode !== true ) {
$errors = [
2021-06-10 13:15:52 -07:00
ZipArchive :: ER_EXISTS => 'File already exists.' ,
ZipArchive :: ER_INCONS => 'Zip archive inconsistent.' ,
ZipArchive :: ER_INVAL => 'Invalid argument.' ,
ZipArchive :: ER_MEMORY => 'Malloc failure.' ,
2021-11-10 00:07:32 -08:00
ZipArchive :: ER_NOENT => 'No such file (' . $filename . ') in directory ' . $dir . '.' ,
2021-06-10 13:15:52 -07:00
ZipArchive :: ER_NOZIP => 'Not a zip archive.' ,
2021-05-12 17:44:39 -07:00
ZipArchive :: ER_OPEN => " Can't open file. " ,
2021-06-10 13:15:52 -07:00
ZipArchive :: ER_READ => 'Read error.' ,
ZipArchive :: ER_SEEK => 'Seek error.' ,
2021-05-12 17:44:39 -07:00
];
2021-06-10 13:15:52 -07:00
return $this -> error ( 'Could not access file: ' . $filename . ' - ' . array_key_exists ( $errcode , $errors ) ? $errors [ $errcode ] : " Unknown reason: $errcode " );
2021-05-12 17:44:39 -07:00
}
2022-03-16 11:02:07 -07:00
2021-05-12 17:44:39 -07:00
$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-06-10 13:15:52 -07:00
'storage/private_uploads/signatures' ,
2021-05-12 17:44:39 -07:00
];
$private_files = [
'storage/oauth-private.key' ,
2021-06-10 13:15:52 -07:00
'storage/oauth-public.key' ,
2021-05-12 17:44:39 -07:00
];
$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' ,
2021-06-10 13:15:52 -07:00
'public/uploads/manufacturers' ,
2021-05-12 17:44:39 -07:00
];
2021-06-10 13:15:52 -07:00
2021-05-12 17:44:39 -07:00
$public_files = [
2021-05-21 15:55:37 -07:00
'public/uploads/logo.*' ,
2021-05-21 15:23:23 -07:00
'public/uploads/setting-email_logo*' ,
'public/uploads/setting-label_logo*' ,
'public/uploads/setting-logo*' ,
'public/uploads/favicon.*' ,
2021-06-10 13:15:52 -07:00
'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-06-10 13:15:52 -07:00
for ( $i = 0 ; $i < $za -> numFiles ; $i ++ ) {
2021-05-12 17:44:39 -07:00
$stat_results = $za -> statIndex ( $i );
// echo "index: $i\n";
// print_r($stat_results);
2021-06-10 13:15:52 -07:00
2021-05-12 17:44:39 -07:00
$raw_path = $stat_results [ 'name' ];
2021-06-10 13:15:52 -07:00
if ( strpos ( $raw_path , '\\' ) !== false ) { //found a backslash, swap it to forward-slash
$raw_path = strtr ( $raw_path , '\\' , '/' );
2021-05-12 17:44:39 -07:00
//print "Translating file: ".$stat_results['name']." to: ".$raw_path."\n";
}
2021-06-10 13:15:52 -07:00
2021-05-21 15:23:23 -07:00
// skip macOS resource fork files (?!?!?!)
2021-06-10 13:15:52 -07:00
if ( strpos ( $raw_path , '__MACOSX' ) !== false && strpos ( $raw_path , '._' ) !== false ) {
2021-05-21 15:23:23 -07:00
//print "SKIPPING macOS Resource fork file: $raw_path\n";
$boring_files [] = $raw_path ;
continue ;
}
2023-01-24 18:19:26 -08:00
if ( @ pathinfo ( $raw_path , PATHINFO_EXTENSION ) == 'sql' ) {
2021-11-10 17:08:04 -08:00
\Log :: debug ( " Found a sql file! " );
2021-05-12 17:44:39 -07:00
$sqlfiles [] = $raw_path ;
$sqlfile_indices [] = $i ;
2021-05-21 15:23:23 -07:00
continue ;
2021-05-12 17:44:39 -07:00
}
2021-06-10 13:15:52 -07:00
foreach ( array_merge ( $private_dirs , $public_dirs ) as $dir ) {
$last_pos = strrpos ( $raw_path , $dir . '/' );
if ( $last_pos !== false ) {
2021-05-12 17:44:39 -07:00
//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-06-10 13:15:52 -07:00
if ( $last_pos + strlen ( $dir ) + 1 == strlen ( $raw_path )) {
2021-05-12 17:44:39 -07:00
// 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-06-10 13:15:52 -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-21 15:23:23 -07:00
}
2021-06-10 13:15:52 -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 ));
2021-06-10 13:15:52 -07:00
if ( ! in_array ( $extension , $good_extensions )) {
$this -> warn ( 'Potentially unsafe file ' . $raw_path . ' is being skipped' );
2021-05-21 15:23:23 -07:00
$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'
2021-06-10 13:15:52 -07:00
if ( $last_pos + strlen ( $file ) == strlen ( $raw_path ) || $has_wildcard ) { //again, no trailing slash. or this is a wildcard and we just take it.
2021-05-21 15:23:23 -07:00
// print("FOUND THE EXACT FILE: $file AT: $raw_path!!!\n"); //we *do* care about this, though.
2021-06-10 13:15:52 -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);
2021-06-10 13:15:52 -07:00
if ( count ( $sqlfiles ) != 1 ) {
return $this -> error ( 'There should be exactly *one* sql backup file found, found: ' . ( count ( $sqlfiles ) == 0 ? 'None' : implode ( ', ' , $sqlfiles )));
2021-05-12 17:44:39 -07:00
}
2021-06-10 13:15:52 -07:00
if ( strpos ( $sqlfiles [ 0 ], 'db-dumps' ) === false ) {
2021-05-12 17:44:39 -07:00
//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 ();
2021-06-10 13:15:52 -07:00
$env_vars [ 'MYSQL_PWD' ] = config ( 'database.connections.mysql.password' );
2021-11-10 12:44:19 -08:00
// TODO notes: we are stealing the dump_binary_path (which *probably* also has your copy of the mysql binary in it. But it might not, so we might need to extend this)
// we unilaterally prepend a slash to the `mysql` command. This might mean your path could look like /blah/blah/blah//mysql - which should be fine. But maybe in some environments it isn't?
2023-01-10 13:06:47 -08:00
$mysql_binary = config ( 'database.connections.mysql.dump.dump_binary_path' ) . \DIRECTORY_SEPARATOR . 'mysql' . ( \DIRECTORY_SEPARATOR == '\\' ? " .exe " : " " );
2021-11-10 17:08:04 -08:00
if ( ! file_exists ( $mysql_binary ) ) {
return $this -> error ( " mysql tool at: ' $mysql_binary ' does not exist, cannot restore. Please edit DB_DUMP_PATH in your .env to point to a directory that contains the mysqldump and mysql binary " );
}
$proc_results = proc_open ( " $mysql_binary -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
2021-06-10 13:15:52 -07:00
[ 0 => [ 'pipe' , 'r' ], 1 => [ 'pipe' , 'w' ], 2 => [ 'pipe' , 'w' ]],
2021-05-12 17:44:39 -07:00
$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.
2021-06-10 13:15:52 -07:00
if ( $proc_results === false ) {
return $this -> error ( 'Unable to invoke mysql via CLI' );
2021-05-12 17:44:39 -07:00
}
2022-05-17 17:01:23 -07:00
stream_set_blocking ( $pipes [ 1 ], false ); // use non-blocking reads for stdout
stream_set_blocking ( $pipes [ 2 ], false ); // use non-blocking reads for stderr
2021-05-12 17:44:39 -07:00
// $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 );
2021-06-10 13:15:52 -07:00
2021-05-12 17:44:39 -07:00
return false ;
}
2021-11-10 17:08:04 -08:00
$bytes_read = 0 ;
2022-03-16 11:05:47 -07:00
2022-05-17 17:01:23 -07:00
try {
while (( $buffer = fgets ( $sql_contents , self :: $buffer_size )) !== false ) {
$bytes_read += strlen ( $buffer );
// \Log::debug("Buffer is: '$buffer'");
$bytes_written = fwrite ( $pipes [ 0 ], $buffer );
if ( $bytes_written === false ) {
throw new Exception ( " Unable to write to pipe " );
}
2022-03-16 11:05:47 -07:00
}
2022-05-17 17:01:23 -07:00
} catch ( \Exception $e ) {
\Log :: error ( " Error during restore!!!! " . $e -> getMessage ());
$err_out = fgets ( $pipes [ 1 ]);
$err_err = fgets ( $pipes [ 2 ]);
\Log :: error ( " Error OUTPUT: " . $err_out );
$this -> info ( $err_out );
\Log :: error ( " Error ERROR : " . $err_err );
$this -> error ( $err_err );
throw $e ;
2021-05-12 17:44:39 -07:00
}
2022-03-16 11:05:47 -07:00
2021-11-10 17:08:04 -08:00
if ( ! feof ( $sql_contents ) || $bytes_read == 0 ) {
return $this -> error ( " Not at end of file for sql file, or zero bytes read. aborting! " );
}
2021-05-12 17:44:39 -07:00
fclose ( $pipes [ 0 ]);
fclose ( $sql_contents );
2021-06-30 14:53:08 -07:00
$this -> line ( stream_get_contents ( $pipes [ 1 ]));
2021-05-12 17:44:39 -07:00
fclose ( $pipes [ 1 ]);
2021-06-30 14:53:08 -07:00
$this -> error ( stream_get_contents ( $pipes [ 2 ]));
2021-05-12 17:44:39 -07:00
fclose ( $pipes [ 2 ]);
2021-06-30 14:53:08 -07:00
2021-05-12 17:44:39 -07:00
//wait, have to do fclose() on all pipes first?
$close_results = proc_close ( $proc_results );
2021-06-10 13:15:52 -07:00
if ( $close_results != 0 ) {
return $this -> error ( 'There may have been a problem with the database import: Error number ' . $close_results );
2021-05-12 17:44:39 -07:00
}
2021-06-10 13:15:52 -07:00
2021-05-12 17:44:39 -07:00
//and now copy the files over too (right?)
//FIXME - we don't prune the filesystem space yet!!!!
2021-06-10 13:15:52 -07:00
if ( $this -> option ( 'no-progress' )) {
2021-05-21 15:23:23 -07:00
$bar = null ;
} else {
$bar = $this -> output -> createProgressBar ( count ( $interesting_files ));
}
2021-06-10 13:15:52 -07:00
foreach ( $interesting_files as $pretty_file_name => $file_details ) {
2021-05-12 17:44:39 -07:00
$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-06-10 13:15:52 -07:00
$migrated_file = fopen ( $file_details [ 'dest' ] . '/' . basename ( $pretty_file_name ), 'w' );
2021-11-10 12:44:19 -08:00
while (( $buffer = fgets ( $fp , self :: $buffer_size )) !== false ) {
2021-06-10 13:15:52 -07:00
fwrite ( $migrated_file , $buffer );
2021-05-12 17:44:39 -07:00
}
fclose ( $migrated_file );
fclose ( $fp );
2021-05-21 15:23:23 -07:00
//$this->info("Wrote $ugly_file_name to $pretty_file_name");
2021-06-10 13:15:52 -07:00
if ( $bar ) {
2021-05-21 15:23:23 -07:00
$bar -> advance ();
}
}
2021-06-10 13:15:52 -07:00
if ( $bar ) {
2021-05-21 15:23:23 -07:00
$bar -> finish ();
2021-06-10 13:15:52 -07:00
$this -> line ( '' );
2021-05-21 15:23:23 -07:00
} else {
2021-06-10 13:15:52 -07:00
$this -> info ( count ( $interesting_files ) . ' files were succesfully transferred' );
2021-05-21 15:23:23 -07:00
}
2021-06-10 13:15:52 -07:00
foreach ( $boring_files as $boring_file ) {
$this -> warn ( $boring_file . ' was skipped.' );
2021-05-12 17:44:39 -07:00
}
}
}