Skip to content

Commit 336410f

Browse files
committed
Enhance ChunkedBackupService and ConcurrentUploadManager with improved error handling, database connection detection, and file path management. Introduce temporary config file for secure password handling in backups and streamline retention policy for successful backups.
1 parent cec6659 commit 336410f

4 files changed

Lines changed: 106 additions & 51 deletions

File tree

composer.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
"description": "A Laravel package for backing up databases and files to external sources with notifications",
44
"type": "library",
55
"license": "MIT",
6-
"version": "0.1.1",
6+
"version": "0.1.5",
77
"authors": [{
88
"name": "Nathan Langer",
99
"email": "nathanlanger@googlemail.com"

src/Services/ChunkedBackupService.php

Lines changed: 49 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -158,8 +158,14 @@ protected function createChunkedBackup(): void
158158

159159
protected function createChunkedDatabaseBackup(string $timestamp): void
160160
{
161-
$connections = config('capsule.database.connections', 'default');
162-
$connections = is_string($connections) ? [$connections] : $connections;
161+
$connections = config('capsule.database.connections');
162+
163+
// Auto-detect current database if not specified
164+
if ($connections === null) {
165+
$connections = [config('database.default')];
166+
} else {
167+
$connections = is_string($connections) ? [$connections] : $connections;
168+
}
163169

164170
foreach ($connections as $connection) {
165171
$this->log("Streaming database '{$connection}' to chunks...");
@@ -192,16 +198,28 @@ protected function streamDatabaseToChunks(string $connection, string $timestamp)
192198

193199
protected function streamMysqlToChunks(array $config, string $connection, string $timestamp, int $chunkSize, string $tempPrefix): void
194200
{
201+
// Create a temporary config file for secure password handling
202+
$configFile = tempnam(sys_get_temp_dir(), 'mysql_config_');
203+
$configContent = sprintf(
204+
"[mysqldump]\nuser=%s\npassword=%s\nhost=%s\nport=%s\n",
205+
$config['username'],
206+
$config['password'],
207+
$config['host'],
208+
$config['port'] ?? 3306
209+
);
210+
file_put_contents($configFile, $configContent);
211+
chmod($configFile, 0600);
212+
195213
$command = sprintf(
196-
'mysqldump --host=%s --port=%s --user=%s --password=%s --single-transaction --routines --triggers %s',
197-
escapeshellarg($config['host']),
198-
escapeshellarg($config['port'] ?? 3306),
199-
escapeshellarg($config['username']),
200-
escapeshellarg($config['password']),
214+
'mysqldump --defaults-extra-file=%s --single-transaction --routines --triggers %s',
215+
escapeshellarg($configFile),
201216
escapeshellarg($config['database'])
202217
);
203218

204219
$this->streamCommandToChunks($command, "db_{$connection}_{$timestamp}", $chunkSize, $tempPrefix);
220+
221+
// Clean up the temporary config file
222+
@unlink($configFile);
205223
}
206224

207225
protected function streamPostgresToChunks(array $config, string $connection, string $timestamp, int $chunkSize, string $tempPrefix): void
@@ -399,7 +417,7 @@ protected function uploadChunksConcurrently(): array
399417
'stats' => $stats,
400418
'backup_id' => $this->backupId,
401419
]);
402-
$this->log("Concurrent upload complete. Time taken: {$stats['total_time']}s");
420+
$this->log("Concurrent upload complete. Total: {$stats['total']}, Successful: {$stats['successful']}, Failed: {$stats['failed']}");
403421

404422
// Check if any uploads failed
405423
$failedUploads = $this->uploadManager->getFailedUploads();
@@ -481,32 +499,29 @@ protected function cleanup(): void
481499
$retentionDays = config('capsule.retention.days', 30);
482500
$retentionCount = config('capsule.retention.count', 10);
483501

484-
BackupLog::where('status', 'success')
485-
->where('created_at', '<', now()->subDays($retentionDays))
486-
->orWhere(function ($query) use ($retentionCount) {
487-
$query->where('status', 'success')
488-
->whereNotIn('id', function ($subQuery) use ($retentionCount) {
489-
$subQuery->select('id')
490-
->from('backup_logs')
491-
->where('status', 'success')
492-
->orderBy('created_at', 'desc')
493-
->limit($retentionCount);
494-
});
495-
})
496-
->each(function (BackupLog $backup) {
497-
if ($backup->file_path) {
498-
if (isset($backup->metadata['chunked']) && $backup->metadata['chunked']) {
499-
// For chunked backups, delete from remote storage
500-
$fileName = basename($backup->file_path);
501-
$this->storageManager->delete($fileName);
502-
} else {
503-
// For regular backups, use storage manager for consistent handling
504-
$fileName = basename($backup->file_path);
505-
$this->storageManager->delete($fileName);
506-
}
507-
}
508-
$backup->delete();
509-
});
502+
// Get IDs of backups to keep (latest N successful backups)
503+
$keepIds = BackupLog::where('status', 'success')
504+
->orderBy('created_at', 'desc')
505+
->limit($retentionCount)
506+
->pluck('id')
507+
->toArray();
508+
509+
// Find backups to delete
510+
$query = BackupLog::where('status', 'success')
511+
->where('created_at', '<', now()->subDays($retentionDays));
512+
513+
if (!empty($keepIds)) {
514+
$query->whereNotIn('id', $keepIds);
515+
}
516+
517+
// Use eachById to avoid memory issues with large result sets
518+
$query->eachById(function (BackupLog $backup) {
519+
if ($backup->file_path) {
520+
$fileName = basename($backup->file_path);
521+
$this->storageManager->delete($fileName);
522+
}
523+
$backup->delete();
524+
});
510525
}
511526
}
512527
}

src/Services/ConcurrentUploadManager.php

Lines changed: 29 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,16 +69,22 @@ protected function createUploadProcess(array $chunk): array
6969
'completed' => false,
7070
'success' => false,
7171
'error' => null,
72+
'pid' => null,
73+
'error_log' => $tempScript . '.error.log', // Add an error log file
7274
];
7375

74-
// Start the background process
76+
// Start the background process and redirect stderr
7577
$command = sprintf(
76-
'php %s > /dev/null 2>&1 & echo $!',
77-
escapeshellarg($tempScript)
78+
'(php %s > /dev/null 2> %s) & echo $!',
79+
escapeshellarg($tempScript),
80+
escapeshellarg($process['error_log'])
7881
);
7982

8083
$process['handle'] = popen($command, 'r');
81-
$process['pid'] = trim(fgets($process['handle']));
84+
$pid = trim(fgets($process['handle']));
85+
if (!empty($pid)) {
86+
$process['pid'] = $pid;
87+
}
8288
pclose($process['handle']);
8389

8490
return $process;
@@ -87,6 +93,8 @@ protected function createUploadProcess(array $chunk): array
8793
protected function createTempUploadScript(array $chunk): string
8894
{
8995
$tempFile = tempnam(sys_get_temp_dir(), 'capsule_upload_');
96+
$dataFile = $tempFile . '.data';
97+
file_put_contents($dataFile, $chunk['data']);
9098

9199
$script = '<?php
92100
require_once "' . base_path('vendor/autoload.php') . '";
@@ -97,7 +105,7 @@ protected function createTempUploadScript(array $chunk): string
97105
98106
$storageManager = app(\Dgtlss\Capsule\Storage\StorageManager::class);
99107
100-
$chunkData = base64_decode("' . base64_encode($chunk['data']) . '");
108+
$chunkData = file_get_contents("' . $dataFile . '");
101109
$chunkName = "' . $chunk['name'] . '";
102110
103111
$tempStream = fopen("php://temp", "r+");
@@ -110,6 +118,9 @@ protected function createTempUploadScript(array $chunk): string
110118
file_put_contents("' . $tempFile . '.success", "SUCCESS");
111119
} catch (Exception $e) {
112120
file_put_contents("' . $tempFile . '.error", $e->getMessage());
121+
} finally {
122+
// Clean up data file
123+
@unlink("' . $dataFile . '");
113124
}
114125
';
115126

@@ -132,23 +143,33 @@ protected function processActiveUploads(): void
132143
}
133144
}
134145

135-
protected function checkUploadComplete(array $upload): bool
146+
protected function checkUploadComplete(array &$upload): bool
136147
{
137148
$script = $upload['process']['script'];
149+
$errorLog = $upload['process']['error_log'];
138150

139-
// Check if success or error file exists
151+
// Check if success file exists
140152
if (file_exists($script . '.success')) {
141153
$upload['process']['completed'] = true;
142154
$upload['process']['success'] = true;
143155
return true;
144156
}
145157

158+
// Check if an error was logged by the script
146159
if (file_exists($script . '.error')) {
147160
$upload['process']['completed'] = true;
148161
$upload['process']['success'] = false;
149162
$upload['process']['error'] = file_get_contents($script . '.error');
150163
return true;
151164
}
165+
166+
// Check if the stderr log has content
167+
if (file_exists($errorLog) && filesize($errorLog) > 0) {
168+
$upload['process']['completed'] = true;
169+
$upload['process']['success'] = false;
170+
$upload['process']['error'] = file_get_contents($errorLog);
171+
return true;
172+
}
152173

153174
// Check if process is still running (timeout after 60 seconds)
154175
if (microtime(true) - $upload['started_at'] > 60) {
@@ -170,6 +191,7 @@ protected function completeUpload(string $uploadId, array $upload): void
170191
@unlink($process['script']);
171192
@unlink($process['script'] . '.success');
172193
@unlink($process['script'] . '.error');
194+
@unlink($process['error_log']);
173195

174196
$this->results[$uploadId] = [
175197
'chunk' => $chunk,

src/Storage/StorageManager.php

Lines changed: 27 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ public function __construct()
2222
public function store(string $filePath): string
2323
{
2424
$fileName = basename($filePath);
25-
$remotePath = $this->backupPath . '/' . $fileName;
25+
$remotePath = $this->getPrefixedPath($fileName);
2626

2727
$this->disk->putFileAs($this->backupPath, $filePath, $fileName);
2828

@@ -31,7 +31,7 @@ public function store(string $filePath): string
3131

3232
public function storeStream($stream, string $fileName): string
3333
{
34-
$remotePath = $this->backupPath . '/' . $fileName;
34+
$remotePath = $this->getPrefixedPath($fileName);
3535

3636
$this->disk->put($remotePath, $stream);
3737

@@ -40,21 +40,25 @@ public function storeStream($stream, string $fileName): string
4040

4141
public function delete(string $fileName): bool
4242
{
43-
$remotePath = $this->backupPath . '/' . $fileName;
43+
$remotePath = $this->getPrefixedPath($fileName);
4444

4545
return $this->disk->delete($remotePath);
4646
}
4747

4848
public function exists(string $fileName): bool
4949
{
50-
$remotePath = $this->backupPath . '/' . $fileName;
50+
$remotePath = $this->getPrefixedPath($fileName);
5151

5252
return $this->disk->exists($remotePath);
5353
}
5454

5555
public function getFileSize(string $fileName): int
5656
{
57-
$remotePath = $this->backupPath . '/' . $fileName;
57+
$remotePath = $this->getPrefixedPath($fileName);
58+
59+
if (!$this->disk->exists($remotePath)) {
60+
throw new Exception("Unable to retrieve the file_size for file at location: {$remotePath}.");
61+
}
5862

5963
return $this->disk->size($remotePath) ?? 0;
6064
}
@@ -76,14 +80,14 @@ public function listFiles(string $path = null): array
7680

7781
public function size(string $fileName): int
7882
{
79-
$remotePath = $this->backupPath . '/' . $fileName;
83+
$remotePath = $this->getPrefixedPath($fileName);
8084

8185
return $this->disk->size($remotePath) ?? 0;
8286
}
8387

8488
public function collateChunks(array $chunkGroups, string $finalFileName): string
8589
{
86-
$finalPath = $this->backupPath . '/' . $finalFileName;
90+
$finalPath = $this->getPrefixedPath($finalFileName);
8791

8892
// Create a temporary zip file
8993
$tempZip = tmpfile();
@@ -99,7 +103,7 @@ public function collateChunks(array $chunkGroups, string $finalFileName): string
99103
$combinedData = '';
100104

101105
foreach ($chunks as $chunk) {
102-
$chunkPath = $this->backupPath . '/' . $chunk['name'];
106+
$chunkPath = $this->getPrefixedPath($chunk['name']);
103107

104108
if ($this->disk->exists($chunkPath)) {
105109
$combinedData .= $this->disk->get($chunkPath);
@@ -129,4 +133,18 @@ public function getBackupPath(): string
129133
{
130134
return $this->backupPath;
131135
}
132-
}
136+
137+
protected function getPrefixedPath(string $fileName): string
138+
{
139+
// Normalize slashes and remove leading/trailing slashes
140+
$backupPath = trim($this->backupPath, '/');
141+
$fileName = trim($fileName, '/');
142+
143+
// Check if the filename already starts with the backup path
144+
if (strpos($fileName, $backupPath) === 0) {
145+
return $fileName;
146+
}
147+
148+
return $backupPath . '/' . $fileName;
149+
}
150+
}

0 commit comments

Comments
 (0)