PluginProbe ʕ •ᴥ•ʔ
WP STAGING – WordPress Backup, Restore, Migration & Clone / 4.9.2
WP STAGING – WordPress Backup, Restore, Migration & Clone v4.9.2
4.9.2 4.9.1 4.9.0 4.8.1 trunk 3.0.0 3.0.1 3.0.2 3.0.3 3.0.4 3.0.5 3.0.6 3.1.0 3.1.1 3.1.2 3.1.3 3.1.4 3.10.0 3.2.0 3.3.1 3.3.2 3.3.3 3.4.1 3.4.3 3.5.0 3.6.0 3.7.1 3.8.0 3.8.1 3.8.2 3.8.3 3.8.4 3.8.5 3.8.6 3.8.7 3.9.0 3.9.1 3.9.2 3.9.3 3.9.4 4.0.0 4.1.0 4.1.1 4.1.2 4.1.3 4.1.4 4.2.0 4.2.1 4.3.0 4.3.1 4.3.2 4.4.0 4.5.0 4.6.0 4.7.0 4.7.1 4.7.2 4.7.3 4.8.0
wp-staging / Backup / Ajax / ExploreCache.php
wp-staging / Backup / Ajax Last commit date
Backup 2 months ago FileList 4 months ago Restore 2 months ago Backup.php 2 years ago BackupDownloader.php 1 day ago BackupSizeCalculator.php 1 day ago BackupSpeedIndex.php 9 months ago BaseFileList.php 1 year ago BaseListing.php 9 months ago Delete.php 4 months ago Edit.php 4 months ago Explore.php 3 months ago ExploreCache.php 3 months ago FileInfo.php 9 months ago FileList.php 1 year ago Listing.php 7 months ago Parts.php 1 day ago Restore.php 2 years ago ScheduleList.php 1 year ago Upload.php 1 month ago
ExploreCache.php
266 lines
1 <?php
2
3 namespace WPStaging\Backup\Ajax;
4
5 use WPStaging\Backup\BackupFileIndex;
6 use WPStaging\Backup\Entity\BackupMetadata;
7 use WPStaging\Backup\FileHeader;
8 use WPStaging\Core\WPStaging;
9 use WPStaging\Framework\Filesystem\FileObject;
10 use WPStaging\Framework\Filesystem\Filesystem;
11 use WPStaging\Framework\Filesystem\PathIdentifier;
12 use WPStaging\Framework\Utils\Cache\Cache;
13
14 /**
15 * Caches the parsed backup file index as a pre-built folder tree.
16 *
17 * Parses the full index once on first request and stores a flat associative
18 * array keyed by folder path. Subsequent requests become O(1) lookups
19 * instead of full sequential index scans.
20 */
21 class ExploreCache
22 {
23 /** @var int Cache lifetime in seconds (1 hour) */
24 const LIFETIME = 3600;
25
26 /** @var int Max cache file size in bytes before falling back to direct scan (20 MB) */
27 const MAX_CACHE_SIZE = 20 * 1024 * 1024;
28
29 /** @var Cache */
30 private $cache;
31
32 /** @var PathIdentifier */
33 private $pathIdentifier;
34
35 /** @var Filesystem */
36 private $filesystem;
37
38 public function __construct(Cache $cache, PathIdentifier $pathIdentifier, Filesystem $filesystem)
39 {
40 $this->cache = $cache;
41 $this->pathIdentifier = $pathIdentifier;
42 $this->filesystem = $filesystem;
43 }
44
45 /**
46 * Get cached folder tree or build it from the backup index
47 *
48 * @param string $backupFile Absolute path to the .wpstg backup file
49 * @param BackupMetadata $metadata
50 * @return array|null Folder tree array keyed by folder path, or null on failure
51 */
52 public function getOrBuild(string $backupFile, BackupMetadata $metadata)
53 {
54 $this->configureCache($backupFile);
55
56 $cached = $this->read($backupFile);
57 if ($cached !== null) {
58 return $cached;
59 }
60
61 $tree = $this->buildTree($backupFile, $metadata);
62 if ($tree === null) {
63 return null;
64 }
65
66 $this->write($backupFile, $tree);
67
68 return $tree;
69 }
70
71 /**
72 * @param string $backupFile
73 */
74 private function configureCache(string $backupFile)
75 {
76 $this->cache->setLifetime(self::LIFETIME);
77 $this->cache->setFilename('backup_explore_' . md5($backupFile));
78 }
79
80 /**
81 * Read and validate the cached data
82 *
83 * @param string $backupFile
84 * @return array|null
85 */
86 private function read(string $backupFile)
87 {
88 if (!$this->cache->isValid(false)) {
89 return null;
90 }
91
92 $filePath = $this->cache->getFilePath();
93 if (filesize($filePath) > self::MAX_CACHE_SIZE) {
94 return null;
95 }
96
97 $data = $this->cache->get();
98 if (!is_array($data) || !isset($data['mtime'], $data['tree'])) {
99 return null;
100 }
101
102 $currentMtime = @filemtime($backupFile);
103 if ($currentMtime === false || (int)$data['mtime'] !== $currentMtime) {
104 return null;
105 }
106
107 return $data['tree'];
108 }
109
110 /**
111 * Write the folder tree to cache
112 *
113 * @param string $backupFile
114 * @param array $tree
115 */
116 private function write(string $backupFile, array $tree)
117 {
118 $mtime = @filemtime($backupFile);
119 if ($mtime === false) {
120 return;
121 }
122
123 $this->cache->save([
124 'mtime' => $mtime,
125 'tree' => $tree,
126 ]);
127 }
128
129 /**
130 * Build the full folder tree by scanning the backup index once
131 *
132 * @param string $backupFile
133 * @param BackupMetadata $metadata
134 * @return array
135 */
136 private function buildTree(string $backupFile, BackupMetadata $metadata)
137 {
138 $indexLineDto = $this->createIndexLineDto($metadata);
139 $fileObject = new FileObject($backupFile, FileObject::MODE_READ);
140 $fileObject->fseek((int)$metadata->getHeaderStart());
141
142 // tree[folderPath] = ['dirs' => [...], 'files' => [...]]
143 $tree = ['' => ['dirs' => [], 'files' => []]];
144 // Track child counts and subdirectory flags per folder per direct child dir
145 $dirChildren = [];
146 $dirHasSubdirs = [];
147
148 while ($fileObject->valid() && $fileObject->ftell() < (int)$metadata->getHeaderEnd()) {
149 $indexOffset = $fileObject->ftell();
150 $rawIndexFile = $fileObject->readAndMoveNext();
151 if (!$indexLineDto->isIndexLine($rawIndexFile)) {
152 continue;
153 }
154
155 $backupFileIndex = $indexLineDto->readIndexLine($rawIndexFile);
156 $relativePath = $this->pathIdentifier->transformIdentifiableToRelativePath($backupFileIndex->getIdentifiablePath());
157 $relativePath = $this->filesystem->normalizePath($relativePath);
158
159 if ($relativePath === '' || $relativePath === '/') {
160 continue;
161 }
162
163 $lastSlash = strrpos($relativePath, '/');
164 $parentDir = $lastSlash === false ? '' : substr($relativePath, 0, $lastSlash);
165 $fileName = $lastSlash === false ? $relativePath : substr($relativePath, $lastSlash + 1);
166
167 if ($fileName === '') {
168 continue;
169 }
170
171 // Ensure parent folder bucket exists
172 if (!isset($tree[$parentDir])) {
173 $tree[$parentDir] = ['dirs' => [], 'files' => []];
174 }
175
176 // Check if this file is nested deeper — means it belongs to a subdirectory of some ancestor
177 // Register directory entries for every ancestor level
178 $parts = explode('/', $relativePath);
179 $depth = count($parts);
180
181 if ($depth > 1) {
182 // Register this file's directory chain
183 $currentPath = '';
184 for ($i = 0; $i < $depth - 1; $i++) {
185 $dirName = $parts[$i];
186 $childPath = $currentPath === '' ? $dirName : $currentPath . '/' . $dirName;
187
188 if (!isset($tree[$currentPath])) {
189 $tree[$currentPath] = ['dirs' => [], 'files' => []];
190 }
191
192 // Track this directory as a child of the current path
193 if (!isset($tree[$currentPath]['dirs'][$dirName])) {
194 $tree[$currentPath]['dirs'][$dirName] = [
195 'name' => $dirName,
196 'path' => $childPath,
197 'hasChildren' => false,
198 ];
199 }
200
201 // Track direct children count for item counting
202 if ($i + 1 < $depth - 1) {
203 // This is an intermediate directory — mark it has subdirs
204 $dirHasSubdirs[$childPath] = true;
205 }
206
207 // Count direct children of this directory (files + immediate subdirs)
208 $nextPart = $parts[$i + 1];
209 $dirChildren[$childPath][$nextPart] = true;
210
211 $currentPath = $childPath;
212 }
213 }
214
215 // Add file to its direct parent
216 $size = (int)$backupFileIndex->getUncompressedSize();
217 $tree[$parentDir]['files'][] = [
218 'name' => $fileName,
219 'path' => $relativePath,
220 'size' => $size,
221 'offset' => (int)$indexOffset,
222 ];
223 }
224
225 $fileObject = null;
226
227 // Set hasChildren and items counts on directory entries
228 foreach ($tree as $folderPath => &$bucket) {
229 $dirArray = [];
230 foreach ($bucket['dirs'] as $dirName => $dirData) {
231 $dirPath = $dirData['path'];
232 $dirData['hasChildren'] = isset($dirChildren[$dirPath]) && count($dirChildren[$dirPath]) > 0;
233 $dirData['items'] = isset($dirChildren[$dirPath]) ? count($dirChildren[$dirPath]) : 0;
234 $dirArray[] = $dirData;
235 }
236
237 usort($dirArray, function ($a, $b) {
238 return strcasecmp($a['name'], $b['name']);
239 });
240
241 $bucket['dirs'] = $dirArray;
242
243 usort($bucket['files'], function ($a, $b) {
244 return strcasecmp($a['name'], $b['name']);
245 });
246 }
247
248 unset($bucket);
249
250 return $tree;
251 }
252
253 /**
254 * @param BackupMetadata $metadata
255 * @return \WPStaging\Backup\Interfaces\IndexLineInterface
256 */
257 private function createIndexLineDto(BackupMetadata $metadata)
258 {
259 if ($metadata->getIsBackupFormatV1()) {
260 return new BackupFileIndex();
261 }
262
263 return WPStaging::make(FileHeader::class);
264 }
265 }
266