Files
api/services/nginx/app/traits/minio_t.php
T

442 lines
14 KiB
PHP

<?php
namespace traits;
use Aws\S3\S3Client;
use classes\wash_certificate_store;
trait minio_t
{
/**
* The bucket to store the files in
* @return string
*/
private string $bucket;
/**
* The S3 client to interact with the Minio server
* @return S3Client
*/
private s3Client $s3Client;
/**
* List all the files in the bucket
* @return array
*/
public function listFiles(): array
{
if ($this->shouldUseLocalTestStorage()) {
return $this->listLocalTestObjects();
}
$objects = self::getS3Client()->listObjects([
'Bucket' => self::getBucket()
]);
// If there are no files, return an empty array
if (!isset($objects['Contents'])) {
return [];
}
return $objects['Contents'];
}
/**
* Returns the S3 client to interact with the Minio server
* @return S3Client
*/
public function getS3Client(): S3Client
{
// If the S3 client is not set, create a new one
if (!isset($this->s3Client)) {
$this->connect();
}
return $this->s3Client;
}
/**
* Connect to the Minio server
*/
private function connect(): self
{
$this->s3Client = new S3Client([
'version' => 'latest',
'region' => 'us-east-1',
'endpoint' => $this->getEndpoint(),
'use_path_style_endpoint' => true,
'credentials' => [
'key' => $this->getAccessKey(),
'secret' => $this->getSecretKey(),
],
]);
return $this;
}
/**
* Returns the endpoint of the Minio server
* @return string
*/
public function getEndpoint(): string
{
global $MINIO;
return is_array($MINIO ?? null) ? (string)($MINIO['endpoint'] ?? '') : '';
}
/**
* Returns the access key of the Minio server
* @return string
*/
public function getAccessKey(): string
{
global $MINIO;
return is_array($MINIO ?? null) ? (string)($MINIO['access_key'] ?? '') : '';
}
/**
* Returns the secret key of the Minio server
* @return string
*/
public function getSecretKey(): string
{
global $MINIO;
return is_array($MINIO ?? null) ? (string)($MINIO['secret_key'] ?? '') : '';
}
/**
* Returns the bucket to store the files in
* @return string
*/
public function getBucket(): string
{
return $this->bucket;
}
/**
* Sets the bucket to store the files in
* @param string $bucket
* @return wash_certificate_store|minio_t
*/
public function setBucket(string $bucket): self
{
$this->bucket = $bucket;
return $this;
}
/**
* Create a new object in the bucket
* @param string $key The key of the object (file)
* @param string $body The content of the object (file)
* @return bool
*/
public function createObject(string $key, string $body): bool
{
if ($this->shouldUseLocalTestStorage()) {
return file_put_contents($this->getLocalTestObjectPath($key), $body) !== false;
}
$result = self::getS3Client()->putObject([
'Bucket' => self::getBucket(),
'Key' => $key,
'Body' => $body
]);
return $result['@metadata']['statusCode'] == 200;
}
/**
* Get the URL of the object in the bucket
* @param string $key The key of the object (file)
* @return string
*/
public function getObjectUrl(string $key): string
{
if ($this->shouldUseLocalTestStorage()) {
return $this->getLocalTestObjectPath($key);
}
return self::getS3Client()->getObjectUrl(self::getBucket(), $key);
}
/**
* Generate a presigned URL for the object in the bucket (valid for 20 minutes)
* @param string $key The key of the object (file)
* @param int|null $expires The expiration time in seconds (default is not set, which means 20 minutes)
* @param bool $isUpload Whether the URL is for uploading (true) or downloading (false)
* @note If $expires is null, it defaults to 1200 seconds (20 minutes).
* @note If $isUpload is true, the URL will be for uploading the object.
* @note If $isUpload is false, the URL will be for downloading the object
* @return string
*/
public function getPresignedUrl(string $key, ?int $expires = null, bool $isUpload = false): string
{
if ($this->shouldUseLocalTestStorage()) {
return $this->getLocalTestObjectPath($key);
}
$commandAction = $isUpload ? 'PutObject' : 'GetObject';
$command = self::getS3Client()->getCommand($commandAction, [
'Bucket' => self::getBucket(),
'Key' => $key,
]);
$duration_seconds = $expires ?? 1200; // Default to 20 minutes if not specified
$duration_attribute = '+' . ($duration_seconds / 60) . ' minutes';
return (string)self::getS3Client()->createPresignedRequest($command, $duration_attribute)->getUri();
}
/**
* Upload a file to the bucket
* @param string $key The key of the object (file)
* @param string $file The path to the file to upload
* @return bool
*/
public function uploadFile(string $key, string $file): bool
{
if ($this->shouldUseLocalTestStorage()) {
return copy($file, $this->getLocalTestObjectPath($key));
}
$result = self::getS3Client()->putObject([
'Bucket' => self::getBucket(),
'Key' => $key,
'SourceFile' => $file
]);
return $result['@metadata']['statusCode'] == 200;
}
/**
* Check if the object exists in the bucket
* @param string $key The key of the object (file or folder)
* @return bool
*/
public function doesObjectExist(string $key): bool
{
if ($this->shouldUseLocalTestStorage()) {
return file_exists($this->getLocalTestObjectPath($key));
}
return self::getS3Client()->doesObjectExist(self::getBucket(), $key);
}
public function downloadToTemporaryFile(string $key): string
{
if (trim($key) === '') {
throw new \InvalidArgumentException('Object key cannot be empty');
}
$path = tempnam(sys_get_temp_dir(), 'stored_object_');
if ($path === false) {
throw new \RuntimeException('Unable to create temporary object file');
}
try {
if ($this->shouldUseLocalTestStorage()) {
if (!copy($this->getLocalTestObjectPath($key), $path)) {
throw new \RuntimeException('Unable to copy object from local storage');
}
return $path;
}
self::getS3Client()->getObject([
'Bucket' => self::getBucket(),
'Key' => $key,
'SaveAs' => $path,
]);
return $path;
} catch (\Throwable $e) {
if (file_exists($path)) {
unlink($path);
}
throw $e;
}
}
/**
* Store temp file from base64 string
* @param string $base64 The base64 encoded string
* @param string $fileExtension The file extension (e.g., 'txt', 'jpg')
* @return string|bool The path to the temporary file if successful, false otherwise
*/
public function storeTempFileFromBase64(string $base64, string $fileExtension = 'txt'): string|bool
{
// Check if the base64 string is valid
if (empty($base64)) {
return false; // Invalid base64 string
}
// Check if the base64 string has a data URI prefix
if (preg_match('/^data:.*;base64,/', $base64)) {
$base64 = preg_replace('/^data:.*;base64,/', '', $base64);
}
// Decode the base64 data
$fileData = base64_decode($base64);
if ($fileData === false) {
return false; // Failed to decode base64 data
}
// Temporary file path
$tempFileName = uniqid('temp_file_', true) . '.' . $fileExtension;
$tempFilePath = '/tmp/' . $tempFileName;
// Create a temporary file
$tempFile = fopen($tempFilePath, 'wb');
if ($tempFile === false) {
return false; // Failed to create temp file
}
// Write the file data to the temporary file
if (fwrite($tempFile, $fileData) === false) {
fclose($tempFile);
return false; // Failed to write to temp file
}
fclose($tempFile);
// Upload the temporary file to the bucket
$result = self::uploadFile($tempFileName, $tempFilePath);
// Clean up the temporary file
unlink($tempFilePath);
return $result ? $tempFileName : false;
}
/**
* Store temp image file from base64 string
* @param string $base64 The base64 encoded image string (E.g. data:image/jpeg;base64,/9j/4AAQSkZJRgABAQAAA)
* @return string|bool The path to the temporary file if successful, false otherwise
*/
public function storeTempImageFromBase64(string $base64): string|bool
{
// Check if the base64 string is valid
if (empty($base64)) {
return false; // Invalid base64 string
}
// Extract the base64 data from the string if it has a data URI prefix
$imageData = $base64;
if (preg_match('/^data:image\/(\w+);base64,/', $base64)) {
$imageData = preg_replace('/^data:image\/\w+;base64,/', '', $base64);
}
// Decode the base64 data
$imageData = base64_decode($imageData);
if ($imageData === false) {
return false; // Failed to decode base64 data
}
// Temporary file path
$tempFileName = uniqid('temp_image_', true) . '.' . $this->getImageExtension($base64);
$tempFilePath = '/tmp/' . $tempFileName;
// Create a temporary file
$tempFile = fopen($tempFilePath, 'wb');
if ($tempFile === false) {
return false; // Failed to create temp file
}
// Write the image data to the temporary file
if (fwrite($tempFile, $imageData) === false) {
fclose($tempFile);
return false; // Failed to write to temp file
}
fclose($tempFile);
// Upload the temporary file to the bucket
$result = self::uploadFile($tempFileName, $tempFilePath);
// Clean up the temporary file
unlink($tempFilePath);
return $result ? $tempFileName : false;
}
/**
* Get the image extension from the base64 string
* @param string $base64 The base64 encoded image string
* @return string The image extension (e.g., 'jpg', 'png')
*/
private function getImageExtension(string $base64): string
{
// Check if the base64 string contains a data URL prefix
if (preg_match('/^data:image\/(\w+);base64,/', $base64, $matches)) {
// Return the image extension
switch ($matches[1]) {
case 'png':
return 'png';
case 'gif':
return 'gif';
case 'webp':
return 'webp';
case 'bmp':
return 'bmp';
default:
return 'jpg'; // Default to jpg if unknown
}
}
// If no match, return a default extension (e.g., 'jpg')
return 'jpg';
}
private function shouldUseLocalTestStorage(): bool
{
if (getenv('RUN_API_TESTS') !== '1') {
return false;
}
return trim((string)$this->getEndpoint()) === ''
|| trim((string)$this->getAccessKey()) === ''
|| trim((string)$this->getSecretKey()) === '';
}
/**
* @return array<int, array{Key:string}>
*/
private function listLocalTestObjects(): array
{
$directory = $this->getLocalTestStorageDirectory();
$objects = [];
$iterator = new \RecursiveIteratorIterator(
new \RecursiveDirectoryIterator($directory, \FilesystemIterator::SKIP_DOTS)
);
foreach ($iterator as $fileInfo) {
if (!$fileInfo->isFile()) {
continue;
}
$pathname = $fileInfo->getPathname();
$relativePath = substr($pathname, strlen($directory) + 1);
$objects[] = [
'Key' => str_replace('\\', '/', $relativePath),
];
}
return $objects;
}
private function getLocalTestStorageDirectory(): string
{
$bucket = preg_replace('/[^a-zA-Z0-9_.-]/', '_', $this->getBucket()) ?: 'default';
$directory = rtrim(sys_get_temp_dir(), DIRECTORY_SEPARATOR)
. DIRECTORY_SEPARATOR
. 'truckwash-test-object-store'
. DIRECTORY_SEPARATOR
. $bucket;
if (!is_dir($directory)) {
mkdir($directory, 0777, true);
}
return $directory;
}
private function getLocalTestObjectPath(string $key): string
{
$normalizedKey = str_replace('\\', '/', $key);
$normalizedKey = preg_replace('#(^|/)\\.\\.(?=/|$)#', '', $normalizedKey) ?? $normalizedKey;
$normalizedKey = ltrim($normalizedKey, '/');
if ($normalizedKey === '') {
$normalizedKey = 'object';
}
$path = $this->getLocalTestStorageDirectory()
. DIRECTORY_SEPARATOR
. str_replace('/', DIRECTORY_SEPARATOR, $normalizedKey);
$directory = dirname($path);
if (!is_dir($directory)) {
mkdir($directory, 0777, true);
}
return $path;
}
}