PluginProbe
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! / 3.8.0
Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! v3.8.0
3.8.0 3.7.5 3.7.4 3.7.3 3.7.2 1-final 3.7.1 3.7.0 3.6.8 3.6.7 3.6.6 3.6.5 3.6.4 3.6.3 3.6.2 3.6.1 3.0.3 3.0.4 3.0.5 3.0.6 3.0.7 3.0.8 3.0.9 3.1.0 3.1.1 All 112 releases
templately / modules / full-site-import / Cleanup / TmpDirsTask.php

TmpDirsTask.php in Templately – Elementor & Gutenberg Template Library: 6500+ Free & Pro Ready Templates And Cloud! 3.8.0, at modules/full-site-import/Cleanup/TmpDirsTask.php

162 lines 5.1 KB
No matching file
Up and down to move Enter to open Esc to close
Raw Download Zip
1 <?php
2 /**
3 * Reclaim abandoned import working directories.
4 *
5 * @package Templately
6 */
7
8 namespace Templately\Modules\FullSiteImport\Cleanup;
9
10 use Templately\Modules\FullSiteImport\Utils\SessionData;
11 use Templately\Modules\Utilities\Cleanup\CleanupTask;
12 use Templately\Modules\Utilities\Cleanup\Context;
13 use Templately\Modules\Utilities\Cleanup\Remover;
14 use Templately\Modules\Utilities\Cleanup\RetentionKind;
15 use Templately\Modules\Utilities\Cleanup\Scanner;
16 use Templately\Modules\Utilities\Cleanup\TaskResult;
17 use WP_Error;
18
19 /**
20 * THE DISK LEAK. This is the headline defect spec 052 exists to close.
21 *
22 * `uploads/templately/tmp/{session_id}/` holds an extracted template pack —
23 * routinely 100–500 MB of XML, JSON and media. Before this task, nothing ever
24 * removed one unless an import FINISHED, and even then only on a 1-in-20
25 * random chance. An import that failed or was abandoned left its pack on disk
26 * permanently, and the scheduled cleanup made it worse by deleting the session
27 * record, so nothing referenced the directory any more.
28 *
29 * A directory is reclaimable when it is EITHER orphaned (no session record at
30 * all — the common case after the old record-only cleanup ran) OR its session
31 * has passed the retention window. An active import is never touched, and that
32 * is re-checked immediately before each removal rather than when the candidate
33 * list was built.
34 */
35 class TmpDirsTask implements CleanupTask {
36
37 const SUBDIR = 'tmp';
38
39 public function descriptor(): array {
40 return [
41 'id' => 'fsi-tmp-dirs',
42 'label' => __( 'Abandoned import files', 'templately' ),
43 'group' => 'imports',
44 'scope' => 'files',
45 'retention_kind' => RetentionKind::AGE,
46 'destructive' => true,
47 'schedulable' => true,
48 ];
49 }
50
51 public function estimate( Context $context ): TaskResult {
52 return $this->sweep( $context->with_dry_run( true ) );
53 }
54
55 public function run( Context $context ): TaskResult {
56 return $this->sweep( $context );
57 }
58
59 /**
60 * One pass over the working directories.
61 */
62 protected function sweep( Context $context ): TaskResult {
63 $result = TaskResult::empty();
64 $root = $this->root();
65
66 if ( ! is_dir( $root ) ) {
67 // Nothing has ever been imported on this site. Not an error.
68 return $result;
69 }
70
71 $entries = @scandir( $root ); // phpcs:ignore WordPress.PHP.NoSilencedErrors.Discouraged
72 if ( false === $entries ) {
73 return $result->fail( 'Could not read the import working directory.' );
74 }
75
76 $threshold = $context->policy()->age_threshold( $this->descriptor()['id'] );
77
78 // Collected ONCE. This only answers "does a record exist for this
79 // directory"; whether that record is still ACTIVE is re-read per item
80 // immediately before removal, so freshness is not traded away for it.
81 // Re-querying every session row per directory turned a sweep of a few
82 // hundred leftovers into a few hundred full-table reads.
83 $known = $this->known_session_ids();
84
85 foreach ( $entries as $entry ) {
86 if ( '.' === $entry || '..' === $entry ) {
87 continue;
88 }
89
90 $path = $root . DIRECTORY_SEPARATOR . $entry;
91 if ( ! is_dir( $path ) || is_link( $path ) ) {
92 continue;
93 }
94
95 // Budget is checked per ITEM, not per task: one of these directories
96 // can hold thousands of files, so a task that only checked once at
97 // the start would overshoot and be killed mid-delete.
98 if ( ! $context->has_budget( 2.0 ) ) {
99 return $result->stopped_on_budget();
100 }
101
102 if ( ! $this->is_reclaimable( $entry, $threshold, $known ) ) {
103 continue;
104 }
105
106 $outcome = Remover::delete_directory( $path, $context );
107
108 if ( $outcome instanceof WP_Error ) {
109 $result->fail( sprintf( '%s: %s', $entry, $outcome->get_error_message() ) );
110 continue;
111 }
112
113 $result->merge( $outcome );
114 }
115
116 return $result;
117 }
118
119 /**
120 * Whether this directory may be removed.
121 *
122 * Re-reads the session state at call time — an import that started while the
123 * sweep was walking the directory must be exempt from that moment on.
124 */
125 protected function is_reclaimable( string $entry, int $threshold, array $known = [] ): bool {
126 if ( ! isset( $known[ $entry ] ) ) {
127 // Orphaned: no record owns this directory. Reclaim it — this is the
128 // state the old record-only cleanup left behind on every site.
129 return true;
130 }
131
132 // Re-read at call time, NOT from the collected snapshot: an import that
133 // started while this sweep was walking the directory must be exempt from
134 // that moment on.
135 return ! SessionData::is_active( $entry, $threshold );
136 }
137
138 /**
139 * Session ids that currently have a record, as a lookup map.
140 *
141 * @return array<string, true>
142 */
143 protected function known_session_ids(): array {
144 $known = [];
145
146 foreach ( array_keys( SessionData::get_all_data() ) as $session_id ) {
147 $known[ (string) $session_id ] = true;
148 }
149
150 return $known;
151 }
152
153 /**
154 * The working-directory root, resolved through the engine so the base-dir
155 * test seam redirects it. Reading `wp_upload_dir()` directly here would make
156 * every test in this suite run against the developer's real uploads folder.
157 */
158 protected function root(): string {
159 return Scanner::get_base_dir() . DIRECTORY_SEPARATOR . static::SUBDIR;
160 }
161 }
162