| @@ -163,8 +163,36 @@ | ||
| 163 | 163 | * @param array $args `display_name`, `disk_name`, `size_bytes`, `mime`. |
| 164 | 164 | * @return int|WP_Error Row id. |
| 165 | 165 | */ |
| 166 | 166 | function openstation_stored_files_create( $owner_id, $args ) { |
| 167 | + $id = openstation_stored_files_locked( | |
| 168 | + static function () use ( $owner_id, $args ) { | |
| 169 | + return openstation_stored_files_create_locked( $owner_id, $args ); | |
| 170 | + } | |
| 171 | + ); | |
| 172 | + if ( ! is_wp_error( $id ) ) { | |
| 173 | + /** | |
| 174 | + * Fires after a stored-file row is created (bytes are already | |
| 175 | + * on disk at this point). | |
| 176 | + * | |
| 177 | + * @param int $id Stored-file id. | |
| 178 | + * @param int $owner_id Owner. | |
| 179 | + */ | |
| 180 | + do_action( 'openstation_stored_file_created', $id, (int) $owner_id ); | |
| 181 | + | |
| 182 | + } | |
| 183 | + return $id; | |
| 184 | +} | |
| 185 | + | |
| 186 | +/** | |
| 187 | + * Insert a stored-file row while holding the reconciliation lock. | |
| 188 | + * | |
| 189 | + * @internal | |
| 190 | + * @param int $owner_id Owner. | |
| 191 | + * @param array $args Stored-file attributes. | |
| 192 | + * @return int|WP_Error | |
| 193 | + */ | |
| 194 | +function openstation_stored_files_create_locked( $owner_id, $args ) { | |
| 167 | 195 | global $wpdb; |
| 168 | 196 | $owner_id = (int) $owner_id; |
| 169 | 197 | if ( $owner_id <= 0 ) { |
| 170 | 198 | return new WP_Error( 'openstation_stored_files_invalid_user', __( 'A user id is required.', 'desktop-mode' ), array( 'status' => 400 ) ); |
| @@ -205,17 +233,8 @@ | ||
| 205 | 233 | return new WP_Error( 'openstation_stored_files_insert_failed', __( 'Failed to record the uploaded file.', 'desktop-mode' ), array( 'status' => 500 ) ); |
| 206 | 234 | } |
| 207 | 235 | $id = (int) $wpdb->insert_id; |
| 208 | 236 | |
| 209 | - /** | |
| 210 | - * Fires after a stored-file row is created (bytes are already | |
| 211 | - * on disk at this point). | |
| 212 | - * | |
| 213 | - * @param int $id Stored-file id. | |
| 214 | - * @param int $owner_id Owner. | |
| 215 | - */ | |
| 216 | - do_action( 'openstation_stored_file_created', $id, $owner_id ); | |
| 217 | - | |
| 218 | 237 | return $id; |
| 219 | 238 | } |
| 220 | 239 | |
| 221 | 240 | /** |
| @@ -586,82 +605,9 @@ | ||
| 586 | 605 | openstation_stored_files_purge( $file_id ); |
| 587 | 606 | } |
| 588 | 607 | add_action( 'openstation_file_unplaced', 'openstation_stored_files_handle_unplaced', 10, 2 ); |
| 589 | 608 | |
| 590 | -/** | |
| 591 | - * Daily reconciliation sweep, both directions: | |
| 592 | - * | |
| 593 | - * Two classes of orphan get collected: | |
| 594 | - * | |
| 595 | - * a) Rows with no placement at all (crashed uploads, interrupted | |
| 596 | - * purges) older than the grace period → delete row + bytes. | |
| 597 | - * b) Bytes on disk with no matching row (interrupted deletes) | |
| 598 | - * whose mtime is older than the grace period → delete bytes. | |
| 599 | - * | |
| 600 | - * Rows whose bytes are missing are left alone — `exists()` still | |
| 601 | - * renders the tile so the user can see and remove it. | |
| 602 | - */ | |
| 603 | -function openstation_stored_files_reconcile() { | |
| 604 | - global $wpdb; | |
| 605 | - $tables = openstation_files_table_names(); | |
| 606 | - $grace = DAY_IN_SECONDS; | |
| 607 | 609 | |
| 608 | - // a) Placement-less rows past grace. | |
| 609 | - $cutoff_ms = openstation_files_now_ms() - ( $grace * 1000 ); | |
| 610 | - $orphans = $wpdb->get_col( | |
| 611 | - $wpdb->prepare( | |
| 612 | - "SELECT sf.id FROM {$tables['stored_files']} sf | |
| 613 | - LEFT JOIN {$tables['placements']} p | |
| 614 | - ON p.file_type = 'upload' | |
| 615 | - AND p.file_ref = CAST( sf.id AS CHAR ) | |
| 616 | - WHERE p.id IS NULL | |
| 617 | - AND sf.created_at_ms < %d", | |
| 618 | - $cutoff_ms | |
| 619 | - ) | |
| 620 | - ); | |
| 621 | - foreach ( (array) $orphans as $orphan_id ) { | |
| 622 | - openstation_stored_files_delete( (int) $orphan_id ); | |
| 623 | - } | |
| 624 | - | |
| 625 | - // b) Row-less bytes past grace. The flat layout makes this a | |
| 626 | - // two-level scan: <base>/<user_id>/<disk_name>. | |
| 627 | - $base = openstation_stored_files_dir(); | |
| 628 | - if ( ! is_dir( $base ) ) { | |
| 629 | - return; | |
| 630 | - } | |
| 631 | - $user_dirs = glob( $base . '/*', GLOB_ONLYDIR ); | |
| 632 | - foreach ( (array) $user_dirs as $user_dir ) { | |
| 633 | - $owner_id = (int) basename( $user_dir ); | |
| 634 | - if ( $owner_id <= 0 ) { | |
| 635 | - continue; | |
| 636 | - } | |
| 637 | - $known = $wpdb->get_col( | |
| 638 | - $wpdb->prepare( | |
| 639 | - "SELECT disk_name FROM {$tables['stored_files']} WHERE owner_id = %d", | |
| 640 | - $owner_id | |
| 641 | - ) | |
| 642 | - ); | |
| 643 | - $known_set = array_flip( array_map( 'strval', (array) $known ) ); | |
| 644 | - $entries = glob( $user_dir . '/*' ); | |
| 645 | - foreach ( (array) $entries as $entry ) { | |
| 646 | - $name = basename( $entry ); | |
| 647 | - if ( 'index.php' === $name || ! is_file( $entry ) ) { | |
| 648 | - continue; | |
| 649 | - } | |
| 650 | - if ( isset( $known_set[ $name ] ) ) { | |
| 651 | - continue; | |
| 652 | - } | |
| 653 | - if ( ! openstation_stored_files_valid_disk_name( $name ) ) { | |
| 654 | - continue; // Not ours — leave foreign files alone. | |
| 655 | - } | |
| 656 | - $mtime = (int) filemtime( $entry ); | |
| 657 | - if ( $mtime > 0 && ( time() - $mtime ) > $grace ) { | |
| 658 | - wp_delete_file( $entry ); | |
| 659 | - } | |
| 660 | - } | |
| 661 | - } | |
| 662 | -} | |
| 663 | -add_action( 'desktop_mode_files_daily_prune', 'openstation_stored_files_reconcile' ); | |
| 664 | 610 | |
| 665 | 611 | /** |
| 666 | 612 | * When a WordPress user is deleted, purge their stored files (rows, |
| 667 | 613 | * bytes, shares, recipient placements) and remove their directory. |