PluginProbe
Media Cloud Sync / trunk
Media Cloud Sync vtrunk
1.4.0 1.3.12 1.3.11 1.3.10 trunk 1.0.0 1.0.1 1.0.2 1.0.3 1.1.0 1.1.1 1.2.0 1.2.10 1.2.11 1.2.12 1.2.13 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.2.7 1.2.8 1.2.9 1.3.0 All 34 releases
media-cloud-sync / includes / upgrade / upgrade.php

upgrade.php in Media Cloud Sync trunk, at includes/upgrade/upgrade.php

384 lines 11.9 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 namespace Dudlewebs\WPMCS;
3
4 defined('ABSPATH') || exit;
5
6 /**
7 * Upgrade class
8 *
9 * Stateless batch-wise DB upgrade runner
10 *
11 * @since 1.3.6
12 */
13 class Upgrade {
14
15 private static $instance = null;
16
17 private $token;
18 private $current_version = false;
19 private $latest_version = false;
20 private $upgrade_queue = [];
21 private $upgrade_queue_key = '';
22
23 /**
24 * Constructor
25 */
26 public function __construct() {
27 $this->token = WPMCS_TOKEN;
28 $this->latest_version = WPMCS_DB_UPGRADE_VERSION;
29 $this->upgrade_queue_key = $this->token . '_upgrade_queue';
30
31 $this->current_version = get_option( $this->token . '_db_upgrade_version', false );
32 $this->upgrade_queue = get_transient( $this->upgrade_queue_key ) ?: [];
33
34 $this->register_upgrades();
35 }
36
37
38 /**
39 * Registers upgrades to be performed on the database.
40 *
41 * Upgrades are registered in the format of [$version, $callback] where
42 * $version is the target version to upgrade to and $callback is a
43 * callable that will be executed when the upgrade is performed.
44 *
45 * The upgrades are registered in the order in which they should be
46 * performed. The upgrade process will only be performed if the current
47 * version is less than the latest version.
48 */
49 private function register_upgrades() {
50 // Force DB upgrade for fresh installs prior to 1.0.0
51 if ( $this->latest_version === '1.0.0' && $this->current_version === '0.0.0' ) {
52 $this->add_upgrade( '1.0.0', [ $this, 'upgrade_to_1_0_0' ] );
53 }
54
55 // If no version is set, set to latest version to avoid upgrades
56 if( $this->current_version === false ) {
57 $this->current_version = $this->latest_version;
58 update_option(
59 $this->token . '_db_upgrade_version',
60 $this->latest_version,
61 false
62 );
63 }
64
65 // Add future upgrades here
66 // if ( version_compare( $this->current_version, '1.0.1', '<' ) ) {
67 // $this->add_upgrade( '1.0.1', [ $this, 'upgrade_to_1_0_1' ] );
68 // }
69
70 }
71
72 /**
73 * Add upgrade
74 */
75 public function add_upgrade( $version, $callback ) {
76 if ( isset( $this->upgrade_queue[ $version ] ) ) {
77 return;
78 }
79
80 $this->upgrade_queue[ $version ] = $callback;
81
82 // Update queue in DB
83 set_transient( $this->token . '_upgrade_queue', $this->upgrade_queue, DAY_IN_SECONDS );
84 }
85
86 /**
87 * Start upgrade
88 */
89 public function start_upgrade() {
90 // Reset previous upgrade data
91 delete_transient( $this->upgrade_queue_key );
92 delete_transient( $this->token . '_upgrade_total' );
93 delete_transient( $this->token . '_upgrade_completed' );
94
95 set_transient( $this->token . '_upgrade_started', true, DAY_IN_SECONDS );
96
97 return $this->run();
98 }
99
100 /**
101 * Get Progress
102 */
103 public function get_progress() {
104 if( get_transient( $this->token . '_upgrade_started' ) === false ) {
105 return [
106 'status' => 'not_started',
107 'percentage' => 0,
108 ];
109 }
110
111 return $this->run();
112 }
113
114 /**
115 * Run the upgrade process.
116 *
117 * This function will check if all upgrades are finished and return
118 * the result of the upgrade process. If not all upgrades are finished,
119 * it will run the first task in the upgrade queue and return the progress
120 * of the upgrade process.
121 *
122 * @return array|string The result of the upgrade process or the progress of the upgrade process.
123 */
124 private function run() {
125 $total_key = $this->token . '_upgrade_total';
126 $completed_key = $this->token . '_upgrade_completed';
127
128 // Initialize counters once
129 if ( get_transient( $total_key ) === false ) {
130
131 if ( empty( $this->upgrade_queue ) ) {
132 return $this->finish_upgrade();
133 }
134
135 set_transient( $this->upgrade_queue_key, $this->upgrade_queue, DAY_IN_SECONDS );
136 set_transient( $total_key, count( $this->upgrade_queue ), DAY_IN_SECONDS );
137 set_transient( $completed_key, 0, DAY_IN_SECONDS );
138 }
139
140 // All tasks finished
141 if ( empty( $this->upgrade_queue ) ) {
142 return $this->finish_upgrade();
143 }
144
145 // Get first task (PHP-safe)
146 foreach ( $this->upgrade_queue as $version => $callback ) {
147 break;
148 }
149
150 $total = max( 1, (int) get_transient( $total_key ) );
151 $completed = (int) get_transient( $completed_key );
152
153 $result = is_callable( $callback ) ? call_user_func( $callback ) : true;
154
155 // Task still running
156 if ( is_array( $result ) && empty( $result['done'] ) ) {
157 return $this->progress_response(
158 $completed + ( (int) ( $result['percentage'] ?? 0 ) / 100 ),
159 $total
160 );
161 }
162
163 // Task completed
164 unset( $this->upgrade_queue[ $version ] );
165 $completed++;
166
167 set_transient( $this->upgrade_queue_key, $this->upgrade_queue, DAY_IN_SECONDS );
168 set_transient( $completed_key, $completed, DAY_IN_SECONDS );
169
170 return empty( $this->upgrade_queue )
171 ? $this->finish_upgrade()
172 : $this->progress_response( $completed, $total );
173 }
174
175
176 /**
177 * Finish the upgrade process by updating the database version and deleting the upgrade queue and counters.
178 *
179 * @return array The result of the upgrade process with status 'completed' and percentage 100.
180 */
181 private function finish_upgrade() {
182 update_option(
183 $this->token . '_db_upgrade_version',
184 $this->latest_version,
185 false
186 );
187
188 delete_transient( $this->upgrade_queue_key );
189 delete_transient( $this->token . '_upgrade_total' );
190 delete_transient( $this->token . '_upgrade_completed' );
191 delete_transient( $this->token . '_upgrade_started' );
192
193 $this->current_version = $this->latest_version;
194
195 return [
196 'status' => 'completed',
197 'percentage' => 100,
198 ];
199 }
200
201
202 /**
203 * Return a response for the progress of the upgrade process.
204 *
205 * @param int $completed_units Number of completed units.
206 * @param int $total Total number of units.
207 *
208 * @return array {
209 * 'status' => string Status of the upgrade process (running/completed).
210 * 'percentage' => int Percentage of completed units (integer between 0 and 100).
211 * }
212 */
213 private function progress_response( $completed_units, $total ) {
214
215 $percentage = ( $completed_units / max( 1, $total ) ) * 100;
216
217 return [
218 'status' => 'running',
219 'percentage' => (int) floor( $percentage ),
220 ];
221 }
222
223
224
225 /* --------------------------------------------------------------------
226 * Batch Upgrade
227 * ------------------------------------------------------------------ */
228
229
230 /**
231 * Upgrades the database from version 0.0.0 to version 1.0.0.
232 *
233 * This upgrade does the following:
234 * - Populate the `original_source_path` and `original_key` columns with the relevant data from the `extra` column.
235 * - Set the `original_source_path` and `original_key` columns to NULL if the `extra` column does not contain the relevant data.
236 *
237 * This upgrade is done in chunks of 200 rows at a time, and keeps track of its progress in a transient.
238 * The progress is returned as a percentage, with a hard stop at 99% to avoid repeated stalls.
239 *
240 * @return array {
241 * 'done' => bool Whether the upgrade is completed.
242 * 'percentage' => int Percentage of completed upgrade (integer between 0 and 100).
243 * }
244 */
245 private function upgrade_to_1_0_0() {
246 global $wpdb;
247
248 $table = Db::get_table_name();
249 $limit = 200;
250 $key = $this->token . '_upgrade_1_0_0_state';
251
252 $state = get_transient( $key ) ?: [
253 'last_id' => 0,
254 'total' => null,
255 'done' => 0,
256 'stall' => 0,
257 ];
258
259 // Hard stop
260 if ( $state['stall'] >= 5 ) {
261 delete_transient( $key );
262 return [ 'done' => true, 'percentage' => 100 ];
263 }
264
265 // Total count (once)
266 if ( $state['total'] === null ) {
267 $state['total'] = (int) $wpdb->get_var(
268 "SELECT COUNT(*) FROM {$table}
269 WHERE (original_source_path IS NULL OR original_source_path = '')
270 AND extra IS NOT NULL"
271 );
272
273 if ( ! $state['total'] ) {
274 delete_transient( $key );
275 return [ 'done' => true, 'percentage' => 100 ];
276 }
277 }
278
279 // Cursor-based fetch
280 $rows = $wpdb->get_results(
281 $wpdb->prepare(
282 "SELECT id, extra FROM {$table}
283 WHERE id > %d
284 AND (original_source_path IS NULL OR original_source_path = '')
285 AND extra IS NOT NULL
286 ORDER BY id ASC
287 LIMIT %d",
288 $state['last_id'],
289 $limit
290 ),
291 ARRAY_A
292 );
293
294 if ( ! $rows ) {
295 delete_transient( $key );
296 return [ 'done' => true, 'percentage' => 100 ];
297 }
298
299 $case_sp = [];
300 $case_k = [];
301 $case_ext = [];
302 $ids = [];
303 $max_id = $state['last_id'];
304
305 foreach ( $rows as $r ) {
306 $max_id = max( $max_id, (int) $r['id'] );
307 $e = Utils::maybe_unserialize( $r['extra'] );
308
309 if (
310 empty( $e['original']['source_path'] ) ||
311 empty( $e['original']['key'] )
312 ) {
313 continue;
314 }
315
316 $id = (int) $r['id'];
317
318 $ids[] = $id;
319 $case_sp[] = $wpdb->prepare( "WHEN id=%d THEN %s", $id, $r['extra'] ? $e['original']['source_path'] ?? '' : '' );
320 $case_k[] = $wpdb->prepare( "WHEN id=%d THEN %s", $id, $r['extra'] ? $e['original']['key'] ?? '' : '' );
321
322 // Cleanup extra
323 unset( $e['original'] );
324 $new_extra = Utils::maybe_serialize( $e );
325
326 $case_ext[] = $wpdb->prepare( "WHEN id=%d THEN %s", $id, $new_extra );
327 }
328
329 if ( ! $ids ) {
330 $state['last_id'] = $max_id;
331 $state['stall']++;
332 set_transient( $key, $state, DAY_IN_SECONDS );
333
334 return [
335 'done' => false,
336 'percentage' => (int) ceil( $state['done'] / $state['total'] * 100 ),
337 ];
338 }
339
340 // Single UPDATE (migration + cleanup)
341 $wpdb->query(
342 "UPDATE {$table}
343 SET
344 original_source_path = CASE " . implode( ' ', $case_sp ) . " ELSE original_source_path END,
345 original_key = CASE " . implode( ' ', $case_k ) . " ELSE original_key END,
346 extra = CASE " . implode( ' ', $case_ext ) . " ELSE extra END
347 WHERE id IN (" . implode( ',', $ids ) . ")
348 AND (original_source_path IS NULL OR original_source_path = '')"
349 );
350
351 $state['done'] += count( $ids );
352 $state['last_id'] = $max_id;
353 $state['stall'] = 0;
354
355 set_transient( $key, $state, DAY_IN_SECONDS );
356
357 return [
358 'done' => false,
359 'percentage' => min( 99, (int) ceil( $state['done'] / $state['total'] * 100 ) ),
360 ];
361 }
362
363
364
365
366 /**
367 * Is upgrade needed
368 */
369 public function is_upgrade_needed() {
370 return version_compare( $this->current_version, $this->latest_version, '<' ) ||
371 ($this->current_version === false && $this->latest_version === '1.0.0');
372 }
373
374 /**
375 * Singleton
376 */
377 public static function instance() {
378 if ( self::$instance === null ) {
379 self::$instance = new self();
380 }
381 return self::$instance;
382 }
383 }
384