mirror of
https://gitlab.com/signalytic/client-external/streamline/streamline-emr.git
synced 2026-09-12 11:11:31 +00:00
The official image from streamline does not currently work for the arm64 platform. As a temporary measure, the source code and docker build scripts have been lifted from the official images and are used to build locally. Some additional modifications are made to reduce overall image size, these are documented in docker/README.md
359 lines
10 KiB
PHP
359 lines
10 KiB
PHP
<?php
|
|
|
|
/*
|
|
* This file is part of the Symfony package.
|
|
*
|
|
* (c) Fabien Potencier <fabien@symfony.com>
|
|
*
|
|
* For the full copyright and license information, please view the LICENSE
|
|
* file that was distributed with this source code.
|
|
*/
|
|
|
|
namespace Symfony\Component\HttpKernel\Profiler;
|
|
|
|
/**
|
|
* Storage for profiler using files.
|
|
*
|
|
* @author Alexandre Salomé <alexandre.salome@gmail.com>
|
|
*/
|
|
class FileProfilerStorage implements ProfilerStorageInterface
|
|
{
|
|
/**
|
|
* Folder where profiler data are stored.
|
|
*/
|
|
private string $folder;
|
|
|
|
/**
|
|
* Constructs the file storage using a "dsn-like" path.
|
|
*
|
|
* Example : "file:/path/to/the/storage/folder"
|
|
*
|
|
* @throws \RuntimeException
|
|
*/
|
|
public function __construct(string $dsn)
|
|
{
|
|
if (!str_starts_with($dsn, 'file:')) {
|
|
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));
|
|
}
|
|
$this->folder = substr($dsn, 5);
|
|
|
|
if (!is_dir($this->folder) && false === @mkdir($this->folder, 0777, true) && !is_dir($this->folder)) {
|
|
throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $this->folder));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* @param \Closure|null $filter A filter to apply on the list of tokens
|
|
*/
|
|
public function find(?string $ip, ?string $url, ?int $limit, ?string $method, ?int $start = null, ?int $end = null, ?string $statusCode = null/* , \Closure $filter = null */): array
|
|
{
|
|
$filter = 7 < \func_num_args() ? func_get_arg(7) : null;
|
|
$file = $this->getIndexFilename();
|
|
|
|
if (!file_exists($file)) {
|
|
return [];
|
|
}
|
|
|
|
$file = fopen($file, 'r');
|
|
fseek($file, 0, \SEEK_END);
|
|
|
|
$result = [];
|
|
while (\count($result) < $limit && $line = $this->readLineFromFile($file)) {
|
|
$values = str_getcsv($line);
|
|
|
|
if (7 > \count($values)) {
|
|
// skip invalid lines
|
|
continue;
|
|
}
|
|
|
|
[$csvToken, $csvIp, $csvMethod, $csvUrl, $csvTime, $csvParent, $csvStatusCode, $csvVirtualType] = $values + [7 => null];
|
|
$csvTime = (int) $csvTime;
|
|
|
|
$urlFilter = false;
|
|
if ($url) {
|
|
$urlFilter = str_starts_with($url, '!') ? str_contains($csvUrl, substr($url, 1)) : !str_contains($csvUrl, $url);
|
|
}
|
|
|
|
if ($ip && !str_contains($csvIp, $ip) || $urlFilter || $method && !str_contains($csvMethod, $method) || $statusCode && !str_contains($csvStatusCode, $statusCode)) {
|
|
continue;
|
|
}
|
|
|
|
if (!empty($start) && $csvTime < $start) {
|
|
continue;
|
|
}
|
|
|
|
if (!empty($end) && $csvTime > $end) {
|
|
continue;
|
|
}
|
|
|
|
$profile = [
|
|
'token' => $csvToken,
|
|
'ip' => $csvIp,
|
|
'method' => $csvMethod,
|
|
'url' => $csvUrl,
|
|
'time' => $csvTime,
|
|
'parent' => $csvParent,
|
|
'status_code' => $csvStatusCode,
|
|
'virtual_type' => $csvVirtualType ?: 'request',
|
|
];
|
|
|
|
if ($filter && !$filter($profile)) {
|
|
continue;
|
|
}
|
|
|
|
$result[$csvToken] = $profile;
|
|
}
|
|
|
|
fclose($file);
|
|
|
|
return array_values($result);
|
|
}
|
|
|
|
/**
|
|
* @return void
|
|
*/
|
|
public function purge()
|
|
{
|
|
$flags = \FilesystemIterator::SKIP_DOTS;
|
|
$iterator = new \RecursiveDirectoryIterator($this->folder, $flags);
|
|
$iterator = new \RecursiveIteratorIterator($iterator, \RecursiveIteratorIterator::CHILD_FIRST);
|
|
|
|
foreach ($iterator as $file) {
|
|
if (is_file($file)) {
|
|
unlink($file);
|
|
} else {
|
|
rmdir($file);
|
|
}
|
|
}
|
|
}
|
|
|
|
public function read(string $token): ?Profile
|
|
{
|
|
return $this->doRead($token);
|
|
}
|
|
|
|
/**
|
|
* @throws \RuntimeException
|
|
*/
|
|
public function write(Profile $profile): bool
|
|
{
|
|
$file = $this->getFilename($profile->getToken());
|
|
|
|
$profileIndexed = is_file($file);
|
|
if (!$profileIndexed) {
|
|
// Create directory
|
|
$dir = \dirname($file);
|
|
if (!is_dir($dir) && false === @mkdir($dir, 0777, true) && !is_dir($dir)) {
|
|
throw new \RuntimeException(sprintf('Unable to create the storage directory (%s).', $dir));
|
|
}
|
|
}
|
|
|
|
$profileToken = $profile->getToken();
|
|
// when there are errors in sub-requests, the parent and/or children tokens
|
|
// may equal the profile token, resulting in infinite loops
|
|
$parentToken = $profile->getParentToken() !== $profileToken ? $profile->getParentToken() : null;
|
|
$childrenToken = array_filter(array_map(fn (Profile $p) => $profileToken !== $p->getToken() ? $p->getToken() : null, $profile->getChildren()));
|
|
|
|
// Store profile
|
|
$data = [
|
|
'token' => $profileToken,
|
|
'parent' => $parentToken,
|
|
'children' => $childrenToken,
|
|
'data' => $profile->getCollectors(),
|
|
'ip' => $profile->getIp(),
|
|
'method' => $profile->getMethod(),
|
|
'url' => $profile->getUrl(),
|
|
'time' => $profile->getTime(),
|
|
'status_code' => $profile->getStatusCode(),
|
|
'virtual_type' => $profile->getVirtualType() ?? 'request',
|
|
];
|
|
|
|
$data = serialize($data);
|
|
|
|
if (\function_exists('gzencode')) {
|
|
$data = gzencode($data, 3);
|
|
}
|
|
|
|
if (false === file_put_contents($file, $data, \LOCK_EX)) {
|
|
return false;
|
|
}
|
|
|
|
if (!$profileIndexed) {
|
|
// Add to index
|
|
if (false === $file = fopen($this->getIndexFilename(), 'a')) {
|
|
return false;
|
|
}
|
|
|
|
fputcsv($file, [
|
|
$profile->getToken(),
|
|
$profile->getIp(),
|
|
$profile->getMethod(),
|
|
$profile->getUrl(),
|
|
$profile->getTime() ?: time(),
|
|
$profile->getParentToken(),
|
|
$profile->getStatusCode(),
|
|
$profile->getVirtualType() ?? 'request',
|
|
]);
|
|
fclose($file);
|
|
|
|
if (1 === mt_rand(1, 10)) {
|
|
$this->removeExpiredProfiles();
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/**
|
|
* Gets filename to store data, associated to the token.
|
|
*/
|
|
protected function getFilename(string $token): string
|
|
{
|
|
// Uses 4 last characters, because first are mostly the same.
|
|
$folderA = substr($token, -2, 2);
|
|
$folderB = substr($token, -4, 2);
|
|
|
|
return $this->folder.'/'.$folderA.'/'.$folderB.'/'.$token;
|
|
}
|
|
|
|
/**
|
|
* Gets the index filename.
|
|
*/
|
|
protected function getIndexFilename(): string
|
|
{
|
|
return $this->folder.'/index.csv';
|
|
}
|
|
|
|
/**
|
|
* Reads a line in the file, backward.
|
|
*
|
|
* This function automatically skips the empty lines and do not include the line return in result value.
|
|
*
|
|
* @param resource $file The file resource, with the pointer placed at the end of the line to read
|
|
*/
|
|
protected function readLineFromFile($file): mixed
|
|
{
|
|
$line = '';
|
|
$position = ftell($file);
|
|
|
|
if (0 === $position) {
|
|
return null;
|
|
}
|
|
|
|
while (true) {
|
|
$chunkSize = min($position, 1024);
|
|
$position -= $chunkSize;
|
|
fseek($file, $position);
|
|
|
|
if (0 === $chunkSize) {
|
|
// bof reached
|
|
break;
|
|
}
|
|
|
|
$buffer = fread($file, $chunkSize);
|
|
|
|
if (false === ($upTo = strrpos($buffer, "\n"))) {
|
|
$line = $buffer.$line;
|
|
continue;
|
|
}
|
|
|
|
$position += $upTo;
|
|
$line = substr($buffer, $upTo + 1).$line;
|
|
fseek($file, max(0, $position), \SEEK_SET);
|
|
|
|
if ('' !== $line) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
return '' === $line ? null : $line;
|
|
}
|
|
|
|
/**
|
|
* @return Profile
|
|
*/
|
|
protected function createProfileFromData(string $token, array $data, ?Profile $parent = null)
|
|
{
|
|
$profile = new Profile($token);
|
|
$profile->setIp($data['ip']);
|
|
$profile->setMethod($data['method']);
|
|
$profile->setUrl($data['url']);
|
|
$profile->setTime($data['time']);
|
|
$profile->setStatusCode($data['status_code']);
|
|
$profile->setVirtualType($data['virtual_type'] ?: 'request');
|
|
$profile->setCollectors($data['data']);
|
|
|
|
if (!$parent && $data['parent']) {
|
|
$parent = $this->read($data['parent']);
|
|
}
|
|
|
|
if ($parent) {
|
|
$profile->setParent($parent);
|
|
}
|
|
|
|
foreach ($data['children'] as $token) {
|
|
if (null !== $childProfile = $this->doRead($token, $profile)) {
|
|
$profile->addChild($childProfile);
|
|
}
|
|
}
|
|
|
|
return $profile;
|
|
}
|
|
|
|
private function doRead($token, ?Profile $profile = null): ?Profile
|
|
{
|
|
if (!$token || !file_exists($file = $this->getFilename($token))) {
|
|
return null;
|
|
}
|
|
|
|
$h = fopen($file, 'r');
|
|
flock($h, \LOCK_SH);
|
|
$data = stream_get_contents($h);
|
|
flock($h, \LOCK_UN);
|
|
fclose($h);
|
|
|
|
if (\function_exists('gzdecode')) {
|
|
$data = @gzdecode($data) ?: $data;
|
|
}
|
|
|
|
if (!$data = unserialize($data)) {
|
|
return null;
|
|
}
|
|
|
|
return $this->createProfileFromData($token, $data, $profile);
|
|
}
|
|
|
|
private function removeExpiredProfiles(): void
|
|
{
|
|
$minimalProfileTimestamp = time() - 2 * 86400;
|
|
$file = $this->getIndexFilename();
|
|
$handle = fopen($file, 'r');
|
|
|
|
if ($offset = is_file($file.'.offset') ? (int) file_get_contents($file.'.offset') : 0) {
|
|
fseek($handle, $offset);
|
|
}
|
|
|
|
while ($line = fgets($handle)) {
|
|
$values = str_getcsv($line);
|
|
|
|
if (7 > \count($values)) {
|
|
// skip invalid lines
|
|
$offset += \strlen($line);
|
|
continue;
|
|
}
|
|
|
|
[$csvToken, , , , $csvTime] = $values;
|
|
|
|
if ($csvTime >= $minimalProfileTimestamp) {
|
|
break;
|
|
}
|
|
|
|
@unlink($this->getFilename($csvToken));
|
|
$offset += \strlen($line);
|
|
}
|
|
fclose($handle);
|
|
|
|
file_put_contents($file.'.offset', $offset);
|
|
}
|
|
}
|