168 lines
4.7 KiB
PHP
168 lines
4.7 KiB
PHP
|
|
<?php
|
||
|
|
|
||
|
|
namespace Modules\Core\Command;
|
||
|
|
|
||
|
|
use Illuminate\Console\Command;
|
||
|
|
use Illuminate\Contracts\Filesystem\Filesystem;
|
||
|
|
use Illuminate\Support\Facades\Storage;
|
||
|
|
use Modules\Core\ResultType\Error;
|
||
|
|
use Modules\Core\ResultType\Result;
|
||
|
|
use Modules\Core\ResultType\Success;
|
||
|
|
use ZipArchive;
|
||
|
|
|
||
|
|
class ImportCommand extends Command
|
||
|
|
{
|
||
|
|
protected $signature = "boboko:import";
|
||
|
|
|
||
|
|
protected $description = "Import data and files from an export archive";
|
||
|
|
|
||
|
|
public function handle(): void
|
||
|
|
{
|
||
|
|
if (!app()->environment("local")) {
|
||
|
|
$this->error(
|
||
|
|
"This command can only be run in the local environment.",
|
||
|
|
);
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$localDisk = Storage::disk("local");
|
||
|
|
$exports = array_values(array_filter(
|
||
|
|
$localDisk->files("exports"),
|
||
|
|
fn($f) => fnmatch("exports/export_*.zip", $f),
|
||
|
|
));
|
||
|
|
|
||
|
|
if (empty($exports)) {
|
||
|
|
$this->error("No export files found.");
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
rsort($exports);
|
||
|
|
$choices = array_map(fn($f) => basename($f), $exports);
|
||
|
|
$chosen = $this->choice("Select an export file to import:", $choices, 0);
|
||
|
|
$zipFile = $localDisk->path("exports/" . $chosen);
|
||
|
|
|
||
|
|
$db = config("database.connections.pgsql");
|
||
|
|
$publicDisk = Storage::disk("public");
|
||
|
|
$tempPath = "boboko_import_" . uniqid();
|
||
|
|
|
||
|
|
$result = $this->extractZip($zipFile, $localDisk, $tempPath)
|
||
|
|
->flatMap(fn($path) => $this->restoreDatabase($db, $localDisk, $path))
|
||
|
|
->flatMap(fn($path) => $this->restoreFiles($localDisk, $path, $publicDisk));
|
||
|
|
|
||
|
|
$localDisk->deleteDirectory($tempPath);
|
||
|
|
|
||
|
|
if ($result->error()->isDefined()) {
|
||
|
|
$this->error($result->error()->get());
|
||
|
|
return;
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->info("Import complete.");
|
||
|
|
$this->call("boboko:anonymize");
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @return Result<string, string> */
|
||
|
|
private function extractZip(
|
||
|
|
string $zipFile,
|
||
|
|
Filesystem $localDisk,
|
||
|
|
string $tempPath,
|
||
|
|
): Result {
|
||
|
|
$zip = new ZipArchive();
|
||
|
|
if ($zip->open($zipFile) !== true) {
|
||
|
|
return Error::create("Could not open zip archive.");
|
||
|
|
}
|
||
|
|
|
||
|
|
$localDisk->makeDirectory($tempPath);
|
||
|
|
$zip->extractTo($localDisk->path($tempPath));
|
||
|
|
$zip->close();
|
||
|
|
|
||
|
|
$this->info("Archive extracted.");
|
||
|
|
|
||
|
|
return Success::create($tempPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @return Result<string, string> */
|
||
|
|
private function restoreDatabase(
|
||
|
|
array $db,
|
||
|
|
Filesystem $localDisk,
|
||
|
|
string $tempPath,
|
||
|
|
): Result {
|
||
|
|
$sqlFiles = array_values(array_filter(
|
||
|
|
$localDisk->files($tempPath),
|
||
|
|
fn($f) => fnmatch("dump_*.sql", basename($f)),
|
||
|
|
));
|
||
|
|
|
||
|
|
if (empty($sqlFiles)) {
|
||
|
|
return Error::create("No SQL dump found in archive.");
|
||
|
|
}
|
||
|
|
|
||
|
|
$dropCommand = sprintf(
|
||
|
|
"PGPASSWORD=%s psql -h %s -p %s -U %s -d %s -c 'DROP SCHEMA public CASCADE; CREATE SCHEMA public;'",
|
||
|
|
$db["password"],
|
||
|
|
$db["host"],
|
||
|
|
$db["port"],
|
||
|
|
$db["username"],
|
||
|
|
$db["database"],
|
||
|
|
);
|
||
|
|
|
||
|
|
exec($dropCommand, result_code: $dropCode);
|
||
|
|
|
||
|
|
if ($dropCode !== 0) {
|
||
|
|
return Error::create("Failed to reset database schema.");
|
||
|
|
}
|
||
|
|
|
||
|
|
$command = sprintf(
|
||
|
|
"PGPASSWORD=%s psql -h %s -p %s -U %s -d %s < %s",
|
||
|
|
$db["password"],
|
||
|
|
$db["host"],
|
||
|
|
$db["port"],
|
||
|
|
$db["username"],
|
||
|
|
$db["database"],
|
||
|
|
$localDisk->path($sqlFiles[0]),
|
||
|
|
);
|
||
|
|
|
||
|
|
exec($command, result_code: $code);
|
||
|
|
|
||
|
|
if ($code !== 0) {
|
||
|
|
return Error::create("psql restore failed.");
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->info("Database restored.");
|
||
|
|
|
||
|
|
return Success::create($tempPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
/** @return Result<string, string> */
|
||
|
|
private function restoreFiles(
|
||
|
|
Filesystem $localDisk,
|
||
|
|
string $tempPath,
|
||
|
|
Filesystem $publicDisk,
|
||
|
|
): Result {
|
||
|
|
$filesSubPath = $tempPath . "/files";
|
||
|
|
|
||
|
|
if (!$localDisk->exists($filesSubPath)) {
|
||
|
|
$this->info("No files to restore.");
|
||
|
|
return Success::create($tempPath);
|
||
|
|
}
|
||
|
|
|
||
|
|
foreach ($publicDisk->allFiles() as $file) {
|
||
|
|
$publicDisk->delete($file);
|
||
|
|
}
|
||
|
|
foreach ($publicDisk->directories() as $dir) {
|
||
|
|
$publicDisk->deleteDirectory($dir);
|
||
|
|
}
|
||
|
|
|
||
|
|
$tempDisk = Storage::build([
|
||
|
|
"driver" => "local",
|
||
|
|
"root" => $localDisk->path($filesSubPath),
|
||
|
|
]);
|
||
|
|
|
||
|
|
foreach ($tempDisk->allFiles() as $relativePath) {
|
||
|
|
$publicDisk->put($relativePath, $tempDisk->readStream($relativePath));
|
||
|
|
}
|
||
|
|
|
||
|
|
$this->info("Files restored.");
|
||
|
|
|
||
|
|
return Success::create($tempPath);
|
||
|
|
}
|
||
|
|
}
|