Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
1.59% covered (danger)
1.59%
1 / 63
11.11% covered (danger)
11.11%
1 / 9
CRAP
0.00% covered (danger)
0.00%
0 / 1
MessageBackupToCloud
1.59% covered (danger)
1.59%
1 / 63
11.11% covered (danger)
11.11%
1 / 9
174.08
0.00% covered (danger)
0.00%
0 / 1
 __construct
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
 handle
0.00% covered (danger)
0.00%
0 / 23
0.00% covered (danger)
0.00%
0 / 1
6
 createPathIfNotExists
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
6
 writeDataToFile
0.00% covered (danger)
0.00%
0 / 3
0.00% covered (danger)
0.00%
0 / 1
2
 deleteTmpFile
0.00% covered (danger)
0.00%
0 / 2
0.00% covered (danger)
0.00%
0 / 1
2
 shouldSkipBackup
0.00% covered (danger)
0.00%
0 / 6
0.00% covered (danger)
0.00%
0 / 1
6
 performLocalBackups
0.00% covered (danger)
0.00%
0 / 5
0.00% covered (danger)
0.00%
0 / 1
2
 performCloudBackups
0.00% covered (danger)
0.00%
0 / 10
0.00% covered (danger)
0.00%
0 / 1
2
 deleteOlderBackups
0.00% covered (danger)
0.00%
0 / 11
0.00% covered (danger)
0.00%
0 / 1
6
1<?php
2
3namespace App\Console\Commands\Messages\Backups;
4
5use Illuminate\Console\Command;
6use Illuminate\Support\Carbon;
7use Illuminate\Support\Facades\DB;
8use Illuminate\Support\Facades\Log;
9use Google\Cloud\Storage\StorageClient;
10
11class MessageBackupToCloud extends Command
12{
13    /**
14     * The name and signature of the console command.
15     *
16     * @var string
17     */
18    protected $signature = 'db:messages-backup-to-cloud';
19
20    /**
21     * The console command description.
22     *
23     * @var string
24     */
25    protected $description = 'Command to get all the messages from db and create a backup to cloud';
26
27    /**
28     * Create a new command instance.
29     *
30     * @return void
31     */
32    public function __construct()
33    {
34        parent::__construct();
35    }
36
37    /**
38     * Execute the console command.
39     *
40     * @return int
41     */
42    public function handle(): int
43    {
44        // Create cloud connection
45        $storage = new StorageClient([
46            'keyFilePath' => base_path() . "/gc-" . env('GC_APP_ENV') . ".json"
47        ]);
48
49        // Create bucket instance
50        $bucket = $storage->bucket(env('GC_APP_ENV') . "-backups-bd");
51
52        // Get data from DB
53        $data = DB::table('messages')->get();
54
55        // Define the local path
56        $path = base_path() . env('GC_HOST_PATH');
57        $this->createPathIfNotExists($path);
58
59        // Write data to a local file and get the file size
60        $localFileSize = $this->writeDataToFile($data, $path);
61
62        // Delete tmp file
63        $this->deleteTmpFile($path);
64
65        // Check if backup can be skipped
66        if ($this->shouldSkipBackup($localFileSize, $bucket))
67        {
68            // Log no need message
69            Log::channel('messages-backups')
70                ->info("No need to do db backup as there's no new messages!");
71
72            // I/O
73            $this->info("No need to do db backup as there's no new messages..");
74            return 0;
75        }
76
77        // Perform local backups
78        $this->performLocalBackups($data, $path);
79
80        // Perform cloud backups
81        $timeAux = date("Y_m_d_H_i_s");
82        $this->performCloudBackups($path, $timeAux);
83
84        // Delete older backups in the cloud
85        $this->deleteOlderBackups($bucket);
86
87        // Backup path cleanup to keep the limit to 5 files + 1 ( latest )
88        exec("cd $path && ls -t | tail -n +7 | xargs -I {} rm {}");
89
90        // Log success
91        Log::channel('messages-backups')
92            ->info('Backups done with success!');
93
94        // I/O
95        $this->info("Messages backup to cloud done with success.");
96
97        // Exit
98        return 0;
99    }
100
101    /**
102     * Create a directory if it does not exist.
103     *
104     * @param string $path
105     * @return void
106     */
107    public function createPathIfNotExists(string $path): void
108    {
109        if (!file_exists($path)) {
110            mkdir($path, 775, true);
111        }
112    }
113
114    /**
115     * Write data to a temporary file and return the file size.
116     *
117     * @param mixed $data
118     * @param string $path
119     * @return int
120     */
121    public function writeDataToFile(mixed $data, string $path): int
122    {
123        $tmp_file = $path . '/tmp_file.json';
124        file_put_contents($tmp_file, json_encode($data), FILE_APPEND);
125        // unlink($tmp_file);
126        return filesize($tmp_file);
127    }
128
129    public function deleteTmpFile(string $path): void
130    {
131        $tmp_file = $path . '/tmp_file.json';
132        unlink($tmp_file);
133    }
134
135    /**
136     * Check if the backup can be skipped based on file sizes.
137     *
138     * @param int $localFileSize
139     * @param mixed $bucket
140     * @return bool
141     */
142    public function shouldSkipBackup(int $localFileSize, mixed $bucket): bool
143    {
144        $objectName = env('GC_CLOUD_PATH') . env('GC_CLOUD_FILE');
145        $latestBackupObject = $bucket->object($objectName);
146
147        // Check if the object exists
148        if ($latestBackupObject->exists())
149        {
150            // Retrieve the size of the latest backup object
151            $latestBackupFileSize = $latestBackupObject->info()['size'];
152        }
153        else
154        {
155            // If the object doesn't exist, consider it as having size 0
156            $latestBackupFileSize = 0;
157        }
158
159        // Compare sizes
160        return $localFileSize <= $latestBackupFileSize;
161    }
162
163    /**
164     * Perform local backups.
165     *
166     * @param mixed $data
167     * @param string $path
168     * @return void
169     */
170    public function performLocalBackups(mixed $data, string $path): void
171    {
172        $file    = $path . env('GC_HOST_FILE');
173        $timeAux = date("Y_m_d_H_i_s");
174        $fileLog = "$path/messages-backup-" . $timeAux . ".json";
175
176        file_put_contents($file, json_encode($data));
177        file_put_contents($fileLog, json_encode($data));
178    }
179
180    /**
181     * Perform cloud backups.
182     *
183     * @param string $path
184     * @param string $timeAux
185     * @return void
186     */
187    private function performCloudBackups(string $path, string $timeAux): void
188    {
189        // Create a new storage client for cloud backups
190        $storage = new StorageClient([
191            'keyFilePath' => base_path() . "/gc-" . env('GC_APP_ENV') . ".json"
192        ]);
193
194        // Create a bucket instance for cloud backups
195        $bucket = $storage->bucket(env('GC_APP_ENV') . "-backups-bd");
196
197        // Upload the latest backup to the cloud
198        $bucket->upload(fopen($path . env('GC_CLOUD_FILE'), 'r'), [
199            'name' => env('GC_CLOUD_PATH') . env('GC_CLOUD_FILE'),
200        ]);
201
202        // Upload a backup by hour to the cloud
203        $bucket->upload(fopen($path . "/messages-backup-" . $timeAux . ".json", 'r'), [
204            'name' => env('GC_CLOUD_PATH') . "messages-backup-" . $timeAux . ".json",
205        ]);
206    }
207
208    /**
209     * Delete older backups from the cloud bucket.
210     *
211     * @param mixed $bucket
212     * @return void
213     */
214    private function deleteOlderBackups(mixed $bucket): void
215    {
216        //$dayBeforeYesterday             = Carbon::now()->subDays(2);
217        //$dayBeforeYesterdayBackupPrefix = 'messages-backup-' . $dayBeforeYesterday->format('Y_m_d_H');
218
219        // Get the current time minus 5 hours
220        $hours = Carbon::now()->subHour(5);
221        $dayBeforeYesterdayBackupPrefix = 'messages-backup-' . $hours->format('Y_m_d_H');
222
223        // List all objects in the bucket
224        $objects = $bucket->objects();
225        $objectsArray = iterator_to_array($objects);
226
227        // Filter objects based on the name prefix of the previous day's backups
228        $oldBackups = array_filter($objectsArray, function ($object) use ($dayBeforeYesterdayBackupPrefix) {
229            return str_contains($object->name(), $dayBeforeYesterdayBackupPrefix);
230        });
231
232        // Delete all older backups
233        foreach ($oldBackups as $oldBackup) {
234            $oldBackup->delete();
235            Log::channel('messages-backups')
236                ->info('Deleted old backup ' . $oldBackup->name());
237        }
238    }
239}