Init
This commit is contained in:
@@ -0,0 +1,65 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Command;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
use Illuminate\Support\Facades\Hash;
|
||||
|
||||
class AnonymizeCommand extends Command
|
||||
{
|
||||
protected $signature = "boboko:anonymize";
|
||||
|
||||
protected $description = "Anonymize personal data in users and lunar_customers";
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
if (!app()->environment("local")) {
|
||||
$this->error(
|
||||
"This command can only be run in the local environment.",
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
if (
|
||||
!$this->confirm(
|
||||
"This will permanently overwrite personal data. Continue?",
|
||||
)
|
||||
) {
|
||||
return;
|
||||
}
|
||||
|
||||
DB::table("users")
|
||||
->get()
|
||||
->each(function ($user) {
|
||||
DB::table("users")
|
||||
->where("id", $user->id)
|
||||
->update([
|
||||
"name" => "User {$user->id}",
|
||||
"email" => "user_{$user->id}@example.com",
|
||||
"password" => Hash::make("password"),
|
||||
"remember_token" => null,
|
||||
]);
|
||||
});
|
||||
|
||||
$this->info("Users anonymized.");
|
||||
|
||||
DB::table("lunar_customers")
|
||||
->get()
|
||||
->each(function ($customer) {
|
||||
DB::table("lunar_customers")
|
||||
->where("id", $customer->id)
|
||||
->update([
|
||||
"title" => null,
|
||||
"first_name" => "Customer",
|
||||
"last_name" => (string) $customer->id,
|
||||
"company_name" => null,
|
||||
"tax_identifier" => null,
|
||||
"account_ref" => null,
|
||||
"meta" => null,
|
||||
]);
|
||||
});
|
||||
|
||||
$this->info("Customers anonymized.");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Command;
|
||||
|
||||
use Lunar\Admin\Console\Commands\MakeLunarAdminCommand;
|
||||
|
||||
use function Laravel\Prompts\text;
|
||||
|
||||
class CreateAdminCommand extends MakeLunarAdminCommand
|
||||
{
|
||||
protected $signature = 'lunar:create-admin
|
||||
{--firstname= : The first name of the user}
|
||||
{--lastname= : The last name of the user}
|
||||
{--email= : A valid and unique email address}';
|
||||
|
||||
protected function getUserData(): array
|
||||
{
|
||||
return [
|
||||
'first_name' => $this->options['firstname'] ?? text(
|
||||
label: 'First Name',
|
||||
required: true,
|
||||
),
|
||||
|
||||
'last_name' => $this->options['lastname'] ?? text(
|
||||
label: 'Last Name',
|
||||
required: true,
|
||||
),
|
||||
|
||||
'email' => $this->options['email'] ?? text(
|
||||
label: 'Email address',
|
||||
required: true,
|
||||
validate: fn (string $email): ?string => match (true) {
|
||||
! filter_var($email, FILTER_VALIDATE_EMAIL) => 'The email address must be valid.',
|
||||
\Lunar\Admin\Models\Staff::where('email', $email)->exists() => 'A user with this email address already exists',
|
||||
default => null,
|
||||
},
|
||||
),
|
||||
|
||||
'admin' => true,
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Command;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
|
||||
class ExportCleanupCommand extends Command
|
||||
{
|
||||
protected $signature = "boboko:export:cleanup {--keep=0 : Number of most recent exports to keep}";
|
||||
|
||||
protected $description = "Delete export archives";
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$exportDir = Storage::disk("local")->path("exports");
|
||||
$files = glob($exportDir . "/export_*.zip");
|
||||
|
||||
if (empty($files)) {
|
||||
$this->info("No export files found.");
|
||||
return;
|
||||
}
|
||||
|
||||
rsort($files);
|
||||
|
||||
$keep = max(0, (int) $this->option("keep"));
|
||||
$toDelete = array_slice($files, $keep);
|
||||
|
||||
if (empty($toDelete)) {
|
||||
$this->info("Nothing to delete.");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach ($toDelete as $file) {
|
||||
unlink($file);
|
||||
$this->line("Deleted: " . basename($file));
|
||||
}
|
||||
|
||||
$this->info("Deleted " . count($toDelete) . " export(s).");
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,112 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\Core\Command;
|
||||
|
||||
use Illuminate\Console\Command;
|
||||
use Illuminate\Support\Facades\Storage;
|
||||
use Modules\Core\ResultType\Error;
|
||||
use Modules\Core\ResultType\Result;
|
||||
use Modules\Core\ResultType\Success;
|
||||
use ZipArchive;
|
||||
|
||||
class ExportCommand extends Command
|
||||
{
|
||||
protected $signature = "boboko:export";
|
||||
|
||||
protected $description = "Export data and files";
|
||||
|
||||
public function handle(): void
|
||||
{
|
||||
$db = config("database.connections.pgsql");
|
||||
|
||||
$disk = Storage::disk("local");
|
||||
$exportDir = $disk->path("exports");
|
||||
if (!is_dir($exportDir)) {
|
||||
mkdir($exportDir, 0755, true);
|
||||
}
|
||||
|
||||
$stamp = now()->format("Y_m_d_His");
|
||||
$sqlFile = $exportDir . "/dump_{$stamp}.sql";
|
||||
$zipFile = $exportDir . "/export_{$stamp}.zip";
|
||||
$publicDisk = Storage::disk("public");
|
||||
$filesDir = $publicDisk->path("");
|
||||
|
||||
$result = $this->dumpDatabase($db, $sqlFile)->flatMap(
|
||||
fn($sql) => $this->buildZip($sql, $filesDir, $zipFile),
|
||||
);
|
||||
|
||||
if (file_exists($sqlFile)) {
|
||||
unlink($sqlFile);
|
||||
}
|
||||
|
||||
if ($result->error()->isDefined()) {
|
||||
$this->error($result->error()->get());
|
||||
return;
|
||||
}
|
||||
|
||||
$this->info("Exported to {$zipFile}");
|
||||
}
|
||||
|
||||
/** @return Result<string, string> */
|
||||
private function dumpDatabase(array $db, string $sqlFile): Result
|
||||
{
|
||||
$command = sprintf(
|
||||
"PGPASSWORD=%s pg_dump -h %s -p %s -U %s -d %s --no-owner --no-acl > %s",
|
||||
$db["password"],
|
||||
$db["host"],
|
||||
$db["port"],
|
||||
$db["username"],
|
||||
$db["database"],
|
||||
$sqlFile,
|
||||
);
|
||||
|
||||
exec($command, result_code: $code);
|
||||
|
||||
if ($code !== 0) {
|
||||
return Error::create("pg_dump failed.");
|
||||
}
|
||||
|
||||
$this->info("Database dumped.");
|
||||
|
||||
return Success::create($sqlFile);
|
||||
}
|
||||
|
||||
/** @return Result<string, string> */
|
||||
private function buildZip(
|
||||
string $sqlFile,
|
||||
string $filesDir,
|
||||
string $zipFile,
|
||||
): Result {
|
||||
$zip = new ZipArchive();
|
||||
if (
|
||||
$zip->open($zipFile, ZipArchive::CREATE | ZipArchive::OVERWRITE) !==
|
||||
true
|
||||
) {
|
||||
return Error::create("Could not create zip archive.");
|
||||
}
|
||||
|
||||
$zip->addFile($sqlFile, basename($sqlFile));
|
||||
|
||||
if (is_dir($filesDir)) {
|
||||
$iterator = new \RecursiveIteratorIterator(
|
||||
new \RecursiveDirectoryIterator(
|
||||
$filesDir,
|
||||
\FilesystemIterator::SKIP_DOTS,
|
||||
),
|
||||
);
|
||||
foreach ($iterator as $file) {
|
||||
$relativePath =
|
||||
"files/" .
|
||||
ltrim(
|
||||
str_replace($filesDir, "", $file->getRealPath()),
|
||||
DIRECTORY_SEPARATOR,
|
||||
);
|
||||
$zip->addFile($file->getRealPath(), $relativePath);
|
||||
}
|
||||
}
|
||||
|
||||
$zip->close();
|
||||
|
||||
return Success::create($zipFile);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,167 @@
|
||||
<?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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user