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); } /** * 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 */ 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; } }