feat: implement MySQL driver and API schema services for database management
This commit is contained in:
@@ -33,4 +33,19 @@ interface DatabaseDriverInterface
|
||||
* Get the underlying connection instance.
|
||||
*/
|
||||
public function getConnection();
|
||||
|
||||
/**
|
||||
* Export the database.
|
||||
*/
|
||||
public function export(array $config): string;
|
||||
|
||||
/**
|
||||
* Import the database.
|
||||
*/
|
||||
public function import(array $config, string $filePath): bool;
|
||||
|
||||
/**
|
||||
* Get database metadata (charset, collation, size, etc.)
|
||||
*/
|
||||
public function getDatabaseMetadata(string $database): array;
|
||||
}
|
||||
|
||||
@@ -104,4 +104,54 @@ class SchemaController extends Controller
|
||||
return Response::json(['error' => $e->getMessage()], 400);
|
||||
}
|
||||
}
|
||||
|
||||
public function export(Request $request)
|
||||
{
|
||||
try {
|
||||
$this->initializeDriver($request);
|
||||
$config = $request->only(['host', 'username', 'password', 'database', 'port']);
|
||||
$filePath = $this->databaseService->export($config);
|
||||
|
||||
return Response::download($filePath)->deleteFileAfterSend(true);
|
||||
} catch (\Exception $e) {
|
||||
return Response::json(['error' => $e->getMessage()], 400);
|
||||
}
|
||||
}
|
||||
|
||||
public function import(Request $request)
|
||||
{
|
||||
try {
|
||||
if (!$request->hasFile('file')) {
|
||||
return Response::json(['error' => 'No file uploaded'], 400);
|
||||
}
|
||||
|
||||
$this->initializeDriver($request);
|
||||
$config = $request->only(['host', 'username', 'password', 'database', 'port']);
|
||||
|
||||
$file = $request->file('file');
|
||||
$tempPath = $file->storeAs('temp', $file->getClientOriginalName());
|
||||
$fullPath = storage_path('app/' . $tempPath);
|
||||
|
||||
$this->databaseService->import($config, $fullPath);
|
||||
|
||||
if (file_exists($fullPath)) {
|
||||
unlink($fullPath);
|
||||
}
|
||||
|
||||
return Response::json(['message' => 'Database imported successfully']);
|
||||
} catch (\Exception $e) {
|
||||
return Response::json(['error' => $e->getMessage()], 400);
|
||||
}
|
||||
}
|
||||
|
||||
public function metadata(Request $request, $database)
|
||||
{
|
||||
try {
|
||||
$request->merge(['database' => $database]);
|
||||
$this->initializeDriver($request);
|
||||
return Response::json($this->databaseService->getDatabaseMetadata($database));
|
||||
} catch (\Exception $e) {
|
||||
return Response::json(['error' => $e->getMessage()], 400);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -93,4 +93,120 @@ class MySqlDriver implements DatabaseDriverInterface, SchemaDiscoveryInterface
|
||||
$dbName = DB::connection($this->connectionName)->getDatabaseName();
|
||||
return $this->query($sql, [$table, $dbName]);
|
||||
}
|
||||
|
||||
public function export(array $config): string
|
||||
{
|
||||
$filename = 'backup-' . ($config['database'] ?? 'all') . '-' . date('Y-m-d-H-i-s') . '.sql';
|
||||
$directory = storage_path('app/backups');
|
||||
|
||||
if (!is_dir($directory)) {
|
||||
mkdir($directory, 0755, true);
|
||||
}
|
||||
|
||||
$path = $directory . DIRECTORY_SEPARATOR . $filename;
|
||||
$errorPath = $directory . DIRECTORY_SEPARATOR . $filename . '.err';
|
||||
|
||||
// Ensure we have a username
|
||||
$username = $config['username'] ?? 'root';
|
||||
$password = $config['password'] ?? '';
|
||||
$host = $config['host'] ?? '127.0.0.1';
|
||||
$port = $config['port'] ?? '3306';
|
||||
$database = $config['database'] ?? '';
|
||||
|
||||
// Build command
|
||||
$passwordPart = !empty($password) ? "-p" . escapeshellarg($password) : "";
|
||||
$dbPart = !empty($database) ? escapeshellarg($database) : "--all-databases";
|
||||
|
||||
// On Windows, we might need to handle double quotes in escapeshellarg
|
||||
// Let's use a more robust way to execute and capture errors
|
||||
$command = sprintf(
|
||||
'mysqldump -u %s %s -h %s -P %s %s > %s 2> %s',
|
||||
escapeshellarg($username),
|
||||
$passwordPart,
|
||||
escapeshellarg($host),
|
||||
escapeshellarg($port),
|
||||
$dbPart,
|
||||
escapeshellarg($path),
|
||||
escapeshellarg($errorPath)
|
||||
);
|
||||
|
||||
shell_exec($command);
|
||||
|
||||
if (file_exists($errorPath)) {
|
||||
$errors = file_get_contents($errorPath);
|
||||
unlink($errorPath);
|
||||
if (!empty(trim($errors))) {
|
||||
// If the file is 0 bytes and there are errors, it definitely failed
|
||||
if (!file_exists($path) || filesize($path) === 0) {
|
||||
throw new \Exception("Export failed: " . $errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (!file_exists($path) || filesize($path) === 0) {
|
||||
throw new \Exception("Export failed: Resulting file is empty. Ensure mysqldump is installed and in the system PATH.");
|
||||
}
|
||||
|
||||
return $path;
|
||||
}
|
||||
|
||||
public function import(array $config, string $filePath): bool
|
||||
{
|
||||
$directory = storage_path('app/backups');
|
||||
if (!is_dir($directory)) {
|
||||
mkdir($directory, 0755, true);
|
||||
}
|
||||
$errorPath = $directory . DIRECTORY_SEPARATOR . 'import_error.err';
|
||||
|
||||
$username = $config['username'] ?? 'root';
|
||||
$password = $config['password'] ?? '';
|
||||
$host = $config['host'] ?? '127.0.0.1';
|
||||
$port = $config['port'] ?? '3306';
|
||||
$database = $config['database'] ?? '';
|
||||
|
||||
$passwordPart = !empty($password) ? "-p" . escapeshellarg($password) : "";
|
||||
$dbPart = !empty($database) ? escapeshellarg($database) : "";
|
||||
|
||||
$command = sprintf(
|
||||
'mysql -u %s %s -h %s -P %s %s < %s 2> %s',
|
||||
escapeshellarg($username),
|
||||
$passwordPart,
|
||||
escapeshellarg($host),
|
||||
escapeshellarg($port),
|
||||
$dbPart,
|
||||
escapeshellarg($filePath),
|
||||
escapeshellarg($errorPath)
|
||||
);
|
||||
|
||||
shell_exec($command);
|
||||
|
||||
if (file_exists($errorPath)) {
|
||||
$errors = file_get_contents($errorPath);
|
||||
unlink($errorPath);
|
||||
if (!empty(trim($errors))) {
|
||||
throw new \Exception("Import failed: " . $errors);
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
public function getDatabaseMetadata(string $database): array
|
||||
{
|
||||
$sql = "
|
||||
SELECT
|
||||
s.DEFAULT_CHARACTER_SET_NAME as charset,
|
||||
s.DEFAULT_COLLATION_NAME as collation,
|
||||
(SELECT SUM(data_length + index_length) FROM information_schema.TABLES WHERE table_schema = s.SCHEMA_NAME) as size_bytes,
|
||||
(SELECT COUNT(*) FROM information_schema.TABLES WHERE table_schema = s.SCHEMA_NAME) as table_count,
|
||||
(SELECT COUNT(*) FROM information_schema.VIEWS WHERE table_schema = s.SCHEMA_NAME) as view_count
|
||||
FROM
|
||||
information_schema.SCHEMATA s
|
||||
WHERE
|
||||
s.SCHEMA_NAME = ?
|
||||
";
|
||||
|
||||
$results = $this->query($sql, [$database]);
|
||||
return (array) ($results[0] ?? []);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -98,4 +98,28 @@ class DatabaseService
|
||||
{
|
||||
return $this->getDriver()->query($sql, $bindings);
|
||||
}
|
||||
|
||||
/**
|
||||
* Export the database.
|
||||
*/
|
||||
public function export(array $config): string
|
||||
{
|
||||
return $this->getDriver()->export($config);
|
||||
}
|
||||
|
||||
/**
|
||||
* Import the database.
|
||||
*/
|
||||
public function import(array $config, string $filePath): bool
|
||||
{
|
||||
return $this->getDriver()->import($config, $filePath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get database metadata.
|
||||
*/
|
||||
public function getDatabaseMetadata(string $database): array
|
||||
{
|
||||
return $this->getDriver()->getDatabaseMetadata($database);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user