-
-
Notifications
You must be signed in to change notification settings - Fork 14
Expand file tree
/
Copy pathFileStorage.php
More file actions
135 lines (113 loc) · 3.91 KB
/
FileStorage.php
File metadata and controls
135 lines (113 loc) · 3.91 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
<?php
declare(strict_types=1);
namespace Yiisoft\Yii\Debug\Storage;
use Yiisoft\Files\FileHelper;
use function array_filter;
use function array_slice;
use function count;
use function dirname;
use function file_exists;
use function filemtime;
use function glob;
use function json_decode;
use function json_encode;
use function sprintf;
use function strlen;
use function substr;
final class FileStorage implements StorageInterface
{
private int $historySize = 50;
public function __construct(
private readonly string $path,
) {
}
public function setHistorySize(int $historySize): void
{
$this->historySize = $historySize;
}
public function read(string $type, ?string $id = null): array
{
clearstatcache();
$dataFiles = $this->findFilesOrderedByModifiedTime(
sprintf(
'%s/**/%s/%s.json',
$this->path,
$id ?? '**',
$type,
)
);
$data = [];
foreach ($dataFiles as $file) {
$dir = dirname($file);
$id = substr($dir, strlen(dirname($file, 2)) + 1);
$content = file_get_contents($file);
$data[$id] = $content === '' ? '' : json_decode($content, true, flags: JSON_THROW_ON_ERROR);
}
return $data;
}
public function write(string $id, array $data, array $objectsMap, array $summary): void
{
$basePath = $this->path . '/' . date('Y-m-d') . '/' . $id . '/';
try {
FileHelper::ensureDirectory($basePath);
file_put_contents($basePath . self::TYPE_DATA . '.json', $this->encode($data));
file_put_contents($basePath . self::TYPE_OBJECTS . '.json', $this->encode($objectsMap));
file_put_contents($basePath . self::TYPE_SUMMARY . '.json', $this->encode($summary));
} finally {
$this->gc();
}
}
public function clear(): void
{
FileHelper::removeDirectory($this->path);
}
/**
* Removes obsolete data files
*/
private function gc(): void
{
$summaryFiles = $this->findFilesOrderedByModifiedTime($this->path . '/**/**/summary.json');
if (empty($summaryFiles) || count($summaryFiles) <= $this->historySize) {
return;
}
$excessFiles = array_slice($summaryFiles, $this->historySize);
foreach ($excessFiles as $file) {
$path1 = dirname($file);
$path2 = dirname($file, 2);
$path3 = dirname($file, 3);
$resource = substr($path1, strlen($path3));
FileHelper::removeDirectory($this->path . $resource);
// Clean empty group directories
$group = substr($path2, strlen($path3));
if (FileHelper::isEmptyDirectory($this->path . $group)) {
FileHelper::removeDirectory($this->path . $group);
}
}
}
/**
* @return string[]
*/
private function findFilesOrderedByModifiedTime(string $pattern): array
{
$files = glob($pattern, GLOB_NOSORT);
if ($files === false) {
return [];
}
usort(
$files,
static function (string $a, string $b): int {
// Use @ to suppress warnings when files are deleted concurrently by another process
$mtimeA = @filemtime($a);
$mtimeB = @filemtime($b);
// filemtime() returns false if file doesn't exist, treat as 0 for sorting
return ($mtimeB ?: 0) <=> ($mtimeA ?: 0);
}
);
// Filter out files that no longer exist (due to concurrent deletion)
return array_filter($files, static fn(string $file): bool => @file_exists($file));
}
private function encode(mixed $value): string
{
return json_encode($value, JSON_THROW_ON_ERROR | JSON_UNESCAPED_UNICODE | JSON_INVALID_UTF8_SUBSTITUTE);
}
}