Completed
Push — master ( a9ddd3...a92467 )
by Freek
16:04 queued 13:41
created

src/Tasks/Backup/BackupJob.php (1 issue)

Upgrade to new PHP Analysis Engine

These results are based on our legacy PHP analysis, consider migrating to our new PHP analysis engine instead. Learn more

1
<?php
2
3
namespace Spatie\Backup\Tasks\Backup;
4
5
use Exception;
6
use Carbon\Carbon;
7
use Spatie\DbDumper\DbDumper;
8
use Illuminate\Support\Collection;
9
use Spatie\DbDumper\Databases\Sqlite;
10
use Spatie\DbDumper\Databases\MongoDb;
11
use Spatie\Backup\Events\BackupHasFailed;
12
use Spatie\Backup\Events\BackupWasSuccessful;
13
use Spatie\Backup\Events\BackupZipWasCreated;
14
use Spatie\Backup\Exceptions\InvalidBackupJob;
15
use Spatie\DbDumper\Compressors\GzipCompressor;
16
use Spatie\TemporaryDirectory\TemporaryDirectory;
17
use Spatie\Backup\Events\BackupManifestWasCreated;
18
use Spatie\Backup\BackupDestination\BackupDestination;
19
20
class BackupJob
21
{
22
    /** @var \Spatie\Backup\Tasks\Backup\FileSelection */
23
    protected $fileSelection;
24
25
    /** @var \Illuminate\Support\Collection */
26
    protected $dbDumpers;
27
28
    /** @var \Illuminate\Support\Collection */
29
    protected $backupDestinations;
30
31
    /** @var string */
32
    protected $filename;
33
34
    /** @var \Spatie\TemporaryDirectory\TemporaryDirectory */
35
    protected $temporaryDirectory;
36
37
    /** @var bool */
38
    protected $sendNotifications = true;
39
40
    public function __construct()
41
    {
42
        $this->dontBackupFilesystem();
43
        $this->dontBackupDatabases();
44
        $this->setDefaultFilename();
45
46
        $this->backupDestinations = new Collection();
47
    }
48
49
    public function dontBackupFilesystem(): self
50
    {
51
        $this->fileSelection = FileSelection::create();
52
53
        return $this;
54
    }
55
56
    public function onlyDbName(array $allowedDbNames): self
57
    {
58
        $this->dbDumpers = $this->dbDumpers->filter(
59
            function (DbDumper $dbDumper, string $connectionName) use ($allowedDbNames) {
60
                return in_array($connectionName, $allowedDbNames);
61
            });
62
63
        return $this;
64
    }
65
66
    public function dontBackupDatabases(): self
67
    {
68
        $this->dbDumpers = new Collection();
69
70
        return $this;
71
    }
72
73
    public function disableNotifications(): self
74
    {
75
        $this->sendNotifications = false;
76
77
        return $this;
78
    }
79
80
    public function setDefaultFilename(): self
81
    {
82
        $this->filename = Carbon::now()->format('Y-m-d-H-i-s').'.zip';
83
84
        return $this;
85
    }
86
87
    public function setFileSelection(FileSelection $fileSelection): self
88
    {
89
        $this->fileSelection = $fileSelection;
90
91
        return $this;
92
    }
93
94
    public function setDbDumpers(Collection $dbDumpers): self
95
    {
96
        $this->dbDumpers = $dbDumpers;
97
98
        return $this;
99
    }
100
101
    public function setFilename(string $filename): self
102
    {
103
        $this->filename = $filename;
104
105
        return $this;
106
    }
107
108
    public function onlyBackupTo(string $diskName): self
109
    {
110
        $this->backupDestinations = $this->backupDestinations->filter(function (BackupDestination $backupDestination) use ($diskName) {
111
            return $backupDestination->diskName() === $diskName;
112
        });
113
114
        if (! count($this->backupDestinations)) {
115
            throw InvalidBackupJob::destinationDoesNotExist($diskName);
116
        }
117
118
        return $this;
119
    }
120
121
    public function setBackupDestinations(Collection $backupDestinations): self
122
    {
123
        $this->backupDestinations = $backupDestinations;
124
125
        return $this;
126
    }
127
128
    public function run()
129
    {
130
        $temporaryDirectoryPath = config('backup.backup.temporary_directory') ?? storage_path('app/backup-temp');
131
132
        $this->temporaryDirectory = (new TemporaryDirectory($temporaryDirectoryPath))
133
            ->name('temp')
134
            ->force()
135
            ->create()
136
            ->empty();
137
138
        try {
139
            if (! count($this->backupDestinations)) {
140
                throw InvalidBackupJob::noDestinationsSpecified();
141
            }
142
143
            $manifest = $this->createBackupManifest();
144
145
            if (! $manifest->count()) {
146
                throw InvalidBackupJob::noFilesToBeBackedUp();
147
            }
148
149
            $zipFile = $this->createZipContainingEveryFileInManifest($manifest);
150
151
            $this->copyToBackupDestinations($zipFile);
152
        } catch (Exception $exception) {
153
            consoleOutput()->error("Backup failed because {$exception->getMessage()}.".PHP_EOL.$exception->getTraceAsString());
154
155
            $this->sendNotification(new BackupHasFailed($exception));
156
157
            $this->temporaryDirectory->delete();
158
159
            throw $exception;
160
        }
161
162
        $this->temporaryDirectory->delete();
163
    }
164
165
    protected function createBackupManifest(): Manifest
166
    {
167
        $databaseDumps = $this->dumpDatabases();
168
169
        consoleOutput()->info('Determining files to backup...');
170
171
        $manifest = Manifest::create($this->temporaryDirectory->path('manifest.txt'))
172
            ->addFiles($databaseDumps)
173
            ->addFiles($this->filesToBeBackedUp());
174
175
        $this->sendNotification(new BackupManifestWasCreated($manifest));
176
177
        return $manifest;
178
    }
179
180
    public function filesToBeBackedUp()
181
    {
182
        $this->fileSelection->excludeFilesFrom($this->directoriesUsedByBackupJob());
183
184
        return $this->fileSelection->selectedFiles();
185
    }
186
187
    protected function directoriesUsedByBackupJob(): array
188
    {
189
        return $this->backupDestinations
190
            ->filter(function (BackupDestination $backupDestination) {
191
                return $backupDestination->filesystemType() === 'local';
192
            })
193
            ->map(function (BackupDestination $backupDestination) {
194
                return $backupDestination->disk()->getDriver()->getAdapter()->applyPathPrefix('').$backupDestination->backupName();
195
            })
196
            ->each(function (string $backupDestinationDirectory) {
197
                $this->fileSelection->excludeFilesFrom($backupDestinationDirectory);
198
            })
199
            ->push($this->temporaryDirectory->path())
200
            ->toArray();
201
    }
202
203
    protected function createZipContainingEveryFileInManifest(Manifest $manifest)
204
    {
205
        consoleOutput()->info("Zipping {$manifest->count()} files...");
206
207
        $pathToZip = $this->temporaryDirectory->path(config('backup.backup.destination.filename_prefix').$this->filename);
208
209
        $zip = Zip::createForManifest($manifest, $pathToZip);
210
211
        consoleOutput()->info("Created zip containing {$zip->count()} files. Size is {$zip->humanReadableSize()}");
212
213
        $this->sendNotification(new BackupZipWasCreated($pathToZip));
214
215
        return $pathToZip;
216
    }
217
218
    /**
219
     * Dumps the databases to the given directory.
220
     * Returns an array with paths to the dump files.
221
     *
222
     * @return array
223
     */
224
    protected function dumpDatabases(): array
225
    {
226
        return $this->dbDumpers->map(function (DbDumper $dbDumper, $key) {
227
            consoleOutput()->info("Dumping database {$dbDumper->getDbName()}...");
228
229
            $dbType = mb_strtolower(basename(str_replace('\\', '/', get_class($dbDumper))));
230
231
            $dbName = $dbDumper->getDbName();
232
            if ($dbDumper instanceof Sqlite) {
233
                $dbName = $key.'-database';
234
            }
235
236
            $fileName = "{$dbType}-{$dbName}.{$this->getExtension($dbDumper)}";
237
238
            if (config('backup.backup.gzip_database_dump')) {
239
                $dbDumper->useCompressor(new GzipCompressor());
240
                $fileName .= '.'.$dbDumper->getCompressorExtension();
241
            }
242
243
            if ($compressor = config('backup.backup.database_dump_compressor')) {
244
                $dbDumper->useCompressor(new $compressor());
245
                $fileName .= '.'.$dbDumper->getCompressorExtension();
246
            }
247
248
            $temporaryFilePath = $this->temporaryDirectory->path('db-dumps'.DIRECTORY_SEPARATOR.$fileName);
249
250
            $dbDumper->dumpToFile($temporaryFilePath);
251
252
            return $temporaryFilePath;
253
        })->toArray();
254
    }
255
256
    protected function copyToBackupDestinations(string $path)
257
    {
258
        $this->backupDestinations->each(function (BackupDestination $backupDestination) use ($path) {
259
            try {
260
                consoleOutput()->info("Copying zip to disk named {$backupDestination->diskName()}...");
261
262
                $backupDestination->write($path);
263
264
                consoleOutput()->info("Successfully copied zip to disk named {$backupDestination->diskName()}.");
265
266
                $this->sendNotification(new BackupWasSuccessful($backupDestination));
267
            } catch (Exception $exception) {
268
                consoleOutput()->error("Copying zip failed because: {$exception->getMessage()}.");
269
270
                $this->sendNotification(new BackupHasFailed($exception, $backupDestination ?? null));
271
            }
272
        });
273
    }
274
275
    protected function sendNotification($notification)
276
    {
277
        if ($this->sendNotifications) {
278
            rescue(function() use ($notification) {
279
                event($notification);
280
            }, function() {
281
                consoleOutput()->error("Sending notification failed");
0 ignored issues
show
Documentation Bug introduced by
The method error does not exist on object<Spatie\Backup\Helpers\ConsoleOutput>? Since you implemented __call, maybe consider adding a @method annotation.

If you implement __call and you know which methods are available, you can improve IDE auto-completion and static analysis by adding a @method annotation to the class.

This is often the case, when __call is implemented by a parent class and only the child class knows which methods exist:

class ParentClass {
    private $data = array();

    public function __call($method, array $args) {
        if (0 === strpos($method, 'get')) {
            return $this->data[strtolower(substr($method, 3))];
        }

        throw new \LogicException(sprintf('Unsupported method: %s', $method));
    }
}

/**
 * If this class knows which fields exist, you can specify the methods here:
 *
 * @method string getName()
 */
class SomeClass extends ParentClass { }
Loading history...
282
            });
283
        }
284
    }
285
286
    protected function getExtension(DbDumper $dbDumper): string
287
    {
288
        return $dbDumper instanceof MongoDb
289
            ? 'archive'
290
            : 'sql';
291
    }
292
}
293