implemented FileStorage (gemini) as step 02
This commit is contained in:
106
src/Services/FileStorage.php
Normal file
106
src/Services/FileStorage.php
Normal file
@ -0,0 +1,106 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use Exception;
|
||||
|
||||
class FileStorage
|
||||
{
|
||||
private string $basePath;
|
||||
|
||||
public function __construct(?string $basePath = null)
|
||||
{
|
||||
// Default base path is the 'data' directory in the project root
|
||||
if ($basePath === null) {
|
||||
$this->basePath = realpath(__DIR__ . '/../../data');
|
||||
} else {
|
||||
$this->basePath = realpath($basePath);
|
||||
}
|
||||
|
||||
if (!$this->basePath || !is_dir($this->basePath)) {
|
||||
throw new Exception("Invalid base path for FileStorage.");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Safely resolves and validates a path within the base directory.
|
||||
* Prevents path traversal attacks.
|
||||
*/
|
||||
private function resolvePath(string $path): string
|
||||
{
|
||||
$fullPath = $this->basePath . DIRECTORY_SEPARATOR . ltrim($path, DIRECTORY_SEPARATOR);
|
||||
$realPath = realpath(dirname($fullPath));
|
||||
|
||||
if ($realPath === false || strpos($realPath, $this->basePath) !== 0) {
|
||||
throw new Exception("Security Alert: Path traversal attempt detected or invalid path: " . $path);
|
||||
}
|
||||
|
||||
return $fullPath;
|
||||
}
|
||||
|
||||
/**
|
||||
* Saves an array as a JSON file.
|
||||
*/
|
||||
public function put(string $path, array $data): bool
|
||||
{
|
||||
$fullPath = $this->resolvePath($path);
|
||||
|
||||
$json = json_encode($data, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
|
||||
|
||||
if (file_put_contents($fullPath, $json) === false) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Reads a JSON file and returns it as an associative array.
|
||||
*/
|
||||
public function get(string $path): ?array
|
||||
{
|
||||
$fullPath = $this->resolvePath($path);
|
||||
|
||||
if (!file_exists($fullPath)) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
$content = file_get_contents($fullPath);
|
||||
if ($content === false) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return json_decode($content, true, 512, JSON_THROW_ON_ERROR);
|
||||
} catch (Exception $e) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks if a file exists.
|
||||
*/
|
||||
public function exists(string $path): bool
|
||||
{
|
||||
try {
|
||||
$fullPath = $this->resolvePath($path);
|
||||
return file_exists($fullPath);
|
||||
} catch (Exception $e) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Deletes a file.
|
||||
*/
|
||||
public function delete(string $path): bool
|
||||
{
|
||||
$fullPath = $this->resolvePath($path);
|
||||
|
||||
if (file_exists($fullPath)) {
|
||||
return unlink($fullPath);
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user