Chris@0
|
1 <?php
|
Chris@0
|
2
|
Chris@0
|
3 /*
|
Chris@0
|
4 * This file is part of the Symfony package.
|
Chris@0
|
5 *
|
Chris@0
|
6 * (c) Fabien Potencier <fabien@symfony.com>
|
Chris@0
|
7 *
|
Chris@0
|
8 * For the full copyright and license information, please view the LICENSE
|
Chris@0
|
9 * file that was distributed with this source code.
|
Chris@0
|
10 */
|
Chris@0
|
11
|
Chris@0
|
12 namespace Symfony\Component\HttpKernel\Profiler;
|
Chris@0
|
13
|
Chris@0
|
14 /**
|
Chris@0
|
15 * Storage for profiler using files.
|
Chris@0
|
16 *
|
Chris@0
|
17 * @author Alexandre Salomé <alexandre.salome@gmail.com>
|
Chris@0
|
18 */
|
Chris@0
|
19 class FileProfilerStorage implements ProfilerStorageInterface
|
Chris@0
|
20 {
|
Chris@0
|
21 /**
|
Chris@0
|
22 * Folder where profiler data are stored.
|
Chris@0
|
23 *
|
Chris@0
|
24 * @var string
|
Chris@0
|
25 */
|
Chris@0
|
26 private $folder;
|
Chris@0
|
27
|
Chris@0
|
28 /**
|
Chris@0
|
29 * Constructs the file storage using a "dsn-like" path.
|
Chris@0
|
30 *
|
Chris@0
|
31 * Example : "file:/path/to/the/storage/folder"
|
Chris@0
|
32 *
|
Chris@0
|
33 * @param string $dsn The DSN
|
Chris@0
|
34 *
|
Chris@0
|
35 * @throws \RuntimeException
|
Chris@0
|
36 */
|
Chris@0
|
37 public function __construct($dsn)
|
Chris@0
|
38 {
|
Chris@0
|
39 if (0 !== strpos($dsn, 'file:')) {
|
Chris@0
|
40 throw new \RuntimeException(sprintf('Please check your configuration. You are trying to use FileStorage with an invalid dsn "%s". The expected format is "file:/path/to/the/storage/folder".', $dsn));
|
Chris@0
|
41 }
|
Chris@0
|
42 $this->folder = substr($dsn, 5);
|
Chris@0
|
43
|
Chris@0
|
44 if (!is_dir($this->folder) && false === @mkdir($this->folder, 0777, true) && !is_dir($this->folder)) {
|
Chris@0
|
45 throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $this->folder));
|
Chris@0
|
46 }
|
Chris@0
|
47 }
|
Chris@0
|
48
|
Chris@0
|
49 /**
|
Chris@0
|
50 * {@inheritdoc}
|
Chris@0
|
51 */
|
Chris@0
|
52 public function find($ip, $url, $limit, $method, $start = null, $end = null, $statusCode = null)
|
Chris@0
|
53 {
|
Chris@0
|
54 $file = $this->getIndexFilename();
|
Chris@0
|
55
|
Chris@0
|
56 if (!file_exists($file)) {
|
Chris@17
|
57 return [];
|
Chris@0
|
58 }
|
Chris@0
|
59
|
Chris@0
|
60 $file = fopen($file, 'r');
|
Chris@0
|
61 fseek($file, 0, SEEK_END);
|
Chris@0
|
62
|
Chris@17
|
63 $result = [];
|
Chris@17
|
64 while (\count($result) < $limit && $line = $this->readLineFromFile($file)) {
|
Chris@0
|
65 $values = str_getcsv($line);
|
Chris@0
|
66 list($csvToken, $csvIp, $csvMethod, $csvUrl, $csvTime, $csvParent, $csvStatusCode) = $values;
|
Chris@0
|
67 $csvTime = (int) $csvTime;
|
Chris@0
|
68
|
Chris@0
|
69 if ($ip && false === strpos($csvIp, $ip) || $url && false === strpos($csvUrl, $url) || $method && false === strpos($csvMethod, $method) || $statusCode && false === strpos($csvStatusCode, $statusCode)) {
|
Chris@0
|
70 continue;
|
Chris@0
|
71 }
|
Chris@0
|
72
|
Chris@0
|
73 if (!empty($start) && $csvTime < $start) {
|
Chris@0
|
74 continue;
|
Chris@0
|
75 }
|
Chris@0
|
76
|
Chris@0
|
77 if (!empty($end) && $csvTime > $end) {
|
Chris@0
|
78 continue;
|
Chris@0
|
79 }
|
Chris@0
|
80
|
Chris@17
|
81 $result[$csvToken] = [
|
Chris@0
|
82 'token' => $csvToken,
|
Chris@0
|
83 'ip' => $csvIp,
|
Chris@0
|
84 'method' => $csvMethod,
|
Chris@0
|
85 'url' => $csvUrl,
|
Chris@0
|
86 'time' => $csvTime,
|
Chris@0
|
87 'parent' => $csvParent,
|
Chris@0
|
88 'status_code' => $csvStatusCode,
|
Chris@17
|
89 ];
|
Chris@0
|
90 }
|
Chris@0
|
91
|
Chris@0
|
92 fclose($file);
|
Chris@0
|
93
|
Chris@0
|
94 return array_values($result);
|
Chris@0
|
95 }
|
Chris@0
|
96
|
Chris@0
|
97 /**
|
Chris@0
|
98 * {@inheritdoc}
|
Chris@0
|
99 */
|
Chris@0
|
100 public function purge()
|
Chris@0
|
101 {
|
Chris@0
|
102 $flags = \FilesystemIterator::SKIP_DOTS;
|
Chris@0
|
103 $iterator = new \RecursiveDirectoryIterator($this->folder, $flags);
|
Chris@0
|
104 $iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST);
|
Chris@0
|
105
|
Chris@0
|
106 foreach ($iterator as $file) {
|
Chris@0
|
107 if (is_file($file)) {
|
Chris@0
|
108 unlink($file);
|
Chris@0
|
109 } else {
|
Chris@0
|
110 rmdir($file);
|
Chris@0
|
111 }
|
Chris@0
|
112 }
|
Chris@0
|
113 }
|
Chris@0
|
114
|
Chris@0
|
115 /**
|
Chris@0
|
116 * {@inheritdoc}
|
Chris@0
|
117 */
|
Chris@0
|
118 public function read($token)
|
Chris@0
|
119 {
|
Chris@0
|
120 if (!$token || !file_exists($file = $this->getFilename($token))) {
|
Chris@0
|
121 return;
|
Chris@0
|
122 }
|
Chris@0
|
123
|
Chris@0
|
124 return $this->createProfileFromData($token, unserialize(file_get_contents($file)));
|
Chris@0
|
125 }
|
Chris@0
|
126
|
Chris@0
|
127 /**
|
Chris@0
|
128 * {@inheritdoc}
|
Chris@0
|
129 *
|
Chris@0
|
130 * @throws \RuntimeException
|
Chris@0
|
131 */
|
Chris@0
|
132 public function write(Profile $profile)
|
Chris@0
|
133 {
|
Chris@0
|
134 $file = $this->getFilename($profile->getToken());
|
Chris@0
|
135
|
Chris@0
|
136 $profileIndexed = is_file($file);
|
Chris@0
|
137 if (!$profileIndexed) {
|
Chris@0
|
138 // Create directory
|
Chris@17
|
139 $dir = \dirname($file);
|
Chris@0
|
140 if (!is_dir($dir) && false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
|
Chris@0
|
141 throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $dir));
|
Chris@0
|
142 }
|
Chris@0
|
143 }
|
Chris@0
|
144
|
Chris@14
|
145 $profileToken = $profile->getToken();
|
Chris@14
|
146 // when there are errors in sub-requests, the parent and/or children tokens
|
Chris@14
|
147 // may equal the profile token, resulting in infinite loops
|
Chris@14
|
148 $parentToken = $profile->getParentToken() !== $profileToken ? $profile->getParentToken() : null;
|
Chris@14
|
149 $childrenToken = array_filter(array_map(function ($p) use ($profileToken) {
|
Chris@14
|
150 return $profileToken !== $p->getToken() ? $p->getToken() : null;
|
Chris@14
|
151 }, $profile->getChildren()));
|
Chris@14
|
152
|
Chris@0
|
153 // Store profile
|
Chris@17
|
154 $data = [
|
Chris@14
|
155 'token' => $profileToken,
|
Chris@14
|
156 'parent' => $parentToken,
|
Chris@14
|
157 'children' => $childrenToken,
|
Chris@0
|
158 'data' => $profile->getCollectors(),
|
Chris@0
|
159 'ip' => $profile->getIp(),
|
Chris@0
|
160 'method' => $profile->getMethod(),
|
Chris@0
|
161 'url' => $profile->getUrl(),
|
Chris@0
|
162 'time' => $profile->getTime(),
|
Chris@0
|
163 'status_code' => $profile->getStatusCode(),
|
Chris@17
|
164 ];
|
Chris@0
|
165
|
Chris@0
|
166 if (false === file_put_contents($file, serialize($data))) {
|
Chris@0
|
167 return false;
|
Chris@0
|
168 }
|
Chris@0
|
169
|
Chris@0
|
170 if (!$profileIndexed) {
|
Chris@0
|
171 // Add to index
|
Chris@0
|
172 if (false === $file = fopen($this->getIndexFilename(), 'a')) {
|
Chris@0
|
173 return false;
|
Chris@0
|
174 }
|
Chris@0
|
175
|
Chris@17
|
176 fputcsv($file, [
|
Chris@0
|
177 $profile->getToken(),
|
Chris@0
|
178 $profile->getIp(),
|
Chris@0
|
179 $profile->getMethod(),
|
Chris@0
|
180 $profile->getUrl(),
|
Chris@0
|
181 $profile->getTime(),
|
Chris@0
|
182 $profile->getParentToken(),
|
Chris@0
|
183 $profile->getStatusCode(),
|
Chris@17
|
184 ]);
|
Chris@0
|
185 fclose($file);
|
Chris@0
|
186 }
|
Chris@0
|
187
|
Chris@0
|
188 return true;
|
Chris@0
|
189 }
|
Chris@0
|
190
|
Chris@0
|
191 /**
|
Chris@0
|
192 * Gets filename to store data, associated to the token.
|
Chris@0
|
193 *
|
Chris@0
|
194 * @param string $token
|
Chris@0
|
195 *
|
Chris@0
|
196 * @return string The profile filename
|
Chris@0
|
197 */
|
Chris@0
|
198 protected function getFilename($token)
|
Chris@0
|
199 {
|
Chris@0
|
200 // Uses 4 last characters, because first are mostly the same.
|
Chris@0
|
201 $folderA = substr($token, -2, 2);
|
Chris@0
|
202 $folderB = substr($token, -4, 2);
|
Chris@0
|
203
|
Chris@0
|
204 return $this->folder.'/'.$folderA.'/'.$folderB.'/'.$token;
|
Chris@0
|
205 }
|
Chris@0
|
206
|
Chris@0
|
207 /**
|
Chris@0
|
208 * Gets the index filename.
|
Chris@0
|
209 *
|
Chris@0
|
210 * @return string The index filename
|
Chris@0
|
211 */
|
Chris@0
|
212 protected function getIndexFilename()
|
Chris@0
|
213 {
|
Chris@0
|
214 return $this->folder.'/index.csv';
|
Chris@0
|
215 }
|
Chris@0
|
216
|
Chris@0
|
217 /**
|
Chris@0
|
218 * Reads a line in the file, backward.
|
Chris@0
|
219 *
|
Chris@0
|
220 * This function automatically skips the empty lines and do not include the line return in result value.
|
Chris@0
|
221 *
|
Chris@0
|
222 * @param resource $file The file resource, with the pointer placed at the end of the line to read
|
Chris@0
|
223 *
|
Chris@0
|
224 * @return mixed A string representing the line or null if beginning of file is reached
|
Chris@0
|
225 */
|
Chris@0
|
226 protected function readLineFromFile($file)
|
Chris@0
|
227 {
|
Chris@0
|
228 $line = '';
|
Chris@0
|
229 $position = ftell($file);
|
Chris@0
|
230
|
Chris@0
|
231 if (0 === $position) {
|
Chris@0
|
232 return;
|
Chris@0
|
233 }
|
Chris@0
|
234
|
Chris@0
|
235 while (true) {
|
Chris@0
|
236 $chunkSize = min($position, 1024);
|
Chris@0
|
237 $position -= $chunkSize;
|
Chris@0
|
238 fseek($file, $position);
|
Chris@0
|
239
|
Chris@0
|
240 if (0 === $chunkSize) {
|
Chris@0
|
241 // bof reached
|
Chris@0
|
242 break;
|
Chris@0
|
243 }
|
Chris@0
|
244
|
Chris@0
|
245 $buffer = fread($file, $chunkSize);
|
Chris@0
|
246
|
Chris@0
|
247 if (false === ($upTo = strrpos($buffer, "\n"))) {
|
Chris@0
|
248 $line = $buffer.$line;
|
Chris@0
|
249 continue;
|
Chris@0
|
250 }
|
Chris@0
|
251
|
Chris@0
|
252 $position += $upTo;
|
Chris@0
|
253 $line = substr($buffer, $upTo + 1).$line;
|
Chris@0
|
254 fseek($file, max(0, $position), SEEK_SET);
|
Chris@0
|
255
|
Chris@0
|
256 if ('' !== $line) {
|
Chris@0
|
257 break;
|
Chris@0
|
258 }
|
Chris@0
|
259 }
|
Chris@0
|
260
|
Chris@0
|
261 return '' === $line ? null : $line;
|
Chris@0
|
262 }
|
Chris@0
|
263
|
Chris@0
|
264 protected function createProfileFromData($token, $data, $parent = null)
|
Chris@0
|
265 {
|
Chris@0
|
266 $profile = new Profile($token);
|
Chris@0
|
267 $profile->setIp($data['ip']);
|
Chris@0
|
268 $profile->setMethod($data['method']);
|
Chris@0
|
269 $profile->setUrl($data['url']);
|
Chris@0
|
270 $profile->setTime($data['time']);
|
Chris@0
|
271 $profile->setStatusCode($data['status_code']);
|
Chris@0
|
272 $profile->setCollectors($data['data']);
|
Chris@0
|
273
|
Chris@0
|
274 if (!$parent && $data['parent']) {
|
Chris@0
|
275 $parent = $this->read($data['parent']);
|
Chris@0
|
276 }
|
Chris@0
|
277
|
Chris@0
|
278 if ($parent) {
|
Chris@0
|
279 $profile->setParent($parent);
|
Chris@0
|
280 }
|
Chris@0
|
281
|
Chris@0
|
282 foreach ($data['children'] as $token) {
|
Chris@0
|
283 if (!$token || !file_exists($file = $this->getFilename($token))) {
|
Chris@0
|
284 continue;
|
Chris@0
|
285 }
|
Chris@0
|
286
|
Chris@0
|
287 $profile->addChild($this->createProfileFromData($token, unserialize(file_get_contents($file)), $profile));
|
Chris@0
|
288 }
|
Chris@0
|
289
|
Chris@0
|
290 return $profile;
|
Chris@0
|
291 }
|
Chris@0
|
292 }
|