mirror of
https://github.com/snipe/snipe-it.git
synced 2024-11-19 12:04:21 -08:00
250 lines
10 KiB
PHP
250 lines
10 KiB
PHP
|
<?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"}
|
||
|
{filename : The zip file to be migrated}';
|
||
|
|
||
|
/**
|
||
|
* 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 = [
|
||
|
'storage/private_uploads/assets',
|
||
|
'storage/private_uploads/audits',
|
||
|
'storage/private_uploads/imports',
|
||
|
'storage/private_uploads/assetmodels',
|
||
|
'storage/private_uploads/users',
|
||
|
'storage/private_uploads/licenses',
|
||
|
'storage/private_uploads/signatures' // We probably don't want this in the first place.
|
||
|
];
|
||
|
$private_files = [
|
||
|
'storage/oauth-private.key',
|
||
|
'storage/oauth-public.key'
|
||
|
];
|
||
|
$public_dirs = [
|
||
|
'public/uploads/companies',
|
||
|
'public/uploads/components',
|
||
|
'public/uploads/categories',
|
||
|
'public/uploads/manufacturers',
|
||
|
'public/uploads/barcodes', // don't want this neither
|
||
|
'public/uploads/consumables',
|
||
|
'public/uploads/departments',
|
||
|
'public/uploads/avatars',
|
||
|
'public/uploads/suppliers',
|
||
|
'public/uploads/assets', //wait, what?!
|
||
|
'public/uploads/locations',
|
||
|
'public/uploads/accessories',
|
||
|
'public/uploads/models',
|
||
|
];
|
||
|
|
||
|
$public_files = [
|
||
|
'public/uploads/logo.png'
|
||
|
];
|
||
|
|
||
|
$all_files = $private_dirs + $public_dirs;
|
||
|
|
||
|
$sqlfiles = [];
|
||
|
$sqlfile_indices = [];
|
||
|
|
||
|
$interesting_files = [];
|
||
|
|
||
|
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'];
|
||
|
// skip macOS resource fork files (?!?!?!)
|
||
|
if(strpos($raw_path,"__MACOSX")!==false && strpos($raw_path,"._") !== false) {
|
||
|
//print "SKIPPING macOS Resource fork file: $raw_path\n";
|
||
|
continue;
|
||
|
}
|
||
|
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";
|
||
|
}
|
||
|
|
||
|
if(@pathinfo($raw_path)['extension'] == "sql") {
|
||
|
print "Found a sql file!\n";
|
||
|
$sqlfiles[] = $raw_path;
|
||
|
$sqlfile_indices[] = $i;
|
||
|
}
|
||
|
|
||
|
foreach($private_dirs+$public_dirs as $dir) {
|
||
|
$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");
|
||
|
print("We would copy $raw_path to $dir.\n"); //FIXME append to a path?
|
||
|
$interesting_files[$raw_path] = ['dest' =>$dir, 'index' => $i];
|
||
|
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");
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
foreach($private_files+$public_files as $file) {
|
||
|
$last_pos = strrpos($raw_path,$file); // no trailing slash!
|
||
|
if($last_pos !== false ) {
|
||
|
//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");
|
||
|
if($last_pos + strlen($file) == strlen($raw_path)) { //again, no trailing slash
|
||
|
print("FOUND THE EXACT FILE: $file AT: $raw_path!!!\n"); //we *do* care about this, though.
|
||
|
$interesting_files[$raw_path] = ['dest' => dirname($file),'index' => $i];
|
||
|
}
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|
||
|
|
||
|
// 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 = [];
|
||
|
|
||
|
// FIXME - absolutely can *NOT* be hardcoding paths like this!!!!!!! But I don't know how to do it right? (Maybe get the user's ENV and append the MYSQL_PWD to it?)
|
||
|
$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]);
|
||
|
$this->info("SQL Stat is: ".print_r($sql_stat,true));
|
||
|
$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!!!!
|
||
|
foreach($interesting_files AS $pretty_file_name => $file_details) {
|
||
|
$ugly_file_name = $za->statIndex($file_details['index'])['name'];
|
||
|
$fp = $za->getStream($ugly_file_name);
|
||
|
$this->info("Weird problem, here are file details? ".print_r($file_details,true));
|
||
|
$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);
|
||
|
$this->info("Wrote $ugly_file_name to $pretty_file_name");
|
||
|
}
|
||
|
|
||
|
}
|
||
|
}
|