347 lines
12 KiB
PHP
347 lines
12 KiB
PHP
<?php
|
|
|
|
namespace App\Libraries;
|
|
|
|
use Aws\S3\S3Client;
|
|
use Aws\Exception\AwsException;
|
|
|
|
class S3Service
|
|
{
|
|
protected $s3Client;
|
|
protected $bucket;
|
|
protected $region;
|
|
protected $baseUrl;
|
|
|
|
public function __construct()
|
|
{
|
|
$this->bucket = getenv('AWS_BUCKET');
|
|
$this->region = getenv('AWS_DEFAULT_REGION');
|
|
|
|
$this->s3Client = new S3Client([
|
|
'version' => 'latest',
|
|
'region' => $this->region,
|
|
'credentials' => [
|
|
'key' => getenv('AWS_ACCESS_KEY_ID'),
|
|
'secret' => getenv('AWS_SECRET_ACCESS_KEY'),
|
|
],
|
|
]);
|
|
|
|
$this->baseUrl = $this->buildBaseUrl($this->bucket);
|
|
}
|
|
|
|
protected function resolveBucket(?string $bucket = null): string
|
|
{
|
|
return $bucket ?: $this->bucket;
|
|
}
|
|
|
|
protected function buildBaseUrl(string $bucket): string
|
|
{
|
|
return "https://{$bucket}.s3.{$this->region}.amazonaws.com/";
|
|
}
|
|
|
|
/**
|
|
* Upload file to S3
|
|
*
|
|
* @param mixed $file File object or file path
|
|
* @param string $folder Folder path in S3 bucket (optional)
|
|
* @param string $fileName Custom file name (optional)
|
|
* @param string|null $bucket Override destination bucket (optional)
|
|
* @return array ['success' => bool, 'url' => string, 'key' => string, 'message' => string]
|
|
*/
|
|
public function upload($file, string $folder = '', string $fileName = null, ?string $bucket = null): array
|
|
{
|
|
$targetBucket = $this->resolveBucket($bucket);
|
|
$startLog = [
|
|
'bucket' => $targetBucket,
|
|
'region' => $this->region,
|
|
'folder' => $folder,
|
|
'file_name' => $fileName,
|
|
'file_type' => is_object($file) ? get_class($file) : gettype($file),
|
|
];
|
|
log_message('info', 'S3 Upload started | ' . json_encode($startLog, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
|
|
|
try {
|
|
// Handle CodeIgniter file upload object
|
|
if (is_object($file) && method_exists($file, 'isValid')) {
|
|
if (!$file->isValid()) {
|
|
$fail = [
|
|
'success' => false,
|
|
'message' => 'Invalid file upload',
|
|
'url' => null,
|
|
'key' => null
|
|
];
|
|
log_message('error', 'S3 Upload validation failed | ' . json_encode(array_merge($startLog, [
|
|
'result' => $fail,
|
|
'error' => method_exists($file, 'getErrorString') ? $file->getErrorString() : null,
|
|
]), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
|
return $fail;
|
|
}
|
|
$filePath = $file->getTempName();
|
|
$originalName = $fileName ?? $file->getClientName();
|
|
$sizeBytes = method_exists($file, 'getSize') ? $file->getSize() : null;
|
|
} else {
|
|
// Handle file path string
|
|
$filePath = $file;
|
|
$originalName = $fileName ?? basename($file);
|
|
$sizeBytes = is_string($filePath) && is_file($filePath) ? filesize($filePath) : null;
|
|
}
|
|
|
|
// Generate unique file name
|
|
$extension = pathinfo($originalName, PATHINFO_EXTENSION);
|
|
// $uniqueName = pathinfo($originalName, PATHINFO_FILENAME) . '_' . time() . '.' . $extension;
|
|
$uniqueName = pathinfo($originalName, PATHINFO_FILENAME) . '.' . $extension;
|
|
|
|
// Build S3 key (path)
|
|
$key = $folder ? rtrim($folder, '/') . '/' . $uniqueName : $uniqueName;
|
|
|
|
// Upload to S3
|
|
// NEW - Faster with streaming and proper content type
|
|
$finfo = finfo_open(FILEINFO_MIME_TYPE);
|
|
$mimeType = finfo_file($finfo, $filePath);
|
|
finfo_close($finfo);
|
|
|
|
$result = $this->s3Client->putObject([
|
|
'Bucket' => $targetBucket,
|
|
'Key' => $key,
|
|
'Body' => fopen($filePath, 'rb'), // Stream instead of loading into memory
|
|
'ContentType' => $mimeType,
|
|
// 'ACL' => 'public-read',
|
|
]);
|
|
|
|
$success = [
|
|
'success' => true,
|
|
'url' => $this->buildBaseUrl($targetBucket) . $key,
|
|
'key' => $key,
|
|
'message' => 'File uploaded successfully'
|
|
];
|
|
|
|
log_message('info', 'S3 Upload successful | ' . json_encode(array_merge($startLog, [
|
|
'tmp_path' => $filePath,
|
|
'original_name' => $originalName,
|
|
'unique_name' => $uniqueName,
|
|
'key' => $key,
|
|
'mime_type' => $mimeType,
|
|
'size_bytes' => $sizeBytes,
|
|
'etag' => $result['ETag'] ?? null,
|
|
'object_url' => $result['ObjectURL'] ?? null,
|
|
'result' => $success,
|
|
]), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
|
|
|
return $success;
|
|
|
|
} catch (AwsException $e) {
|
|
log_message('error', 'S3 Upload Error | ' . json_encode(array_merge($startLog, [
|
|
'error_message' => $e->getMessage(),
|
|
'aws_error_code' => $e->getAwsErrorCode(),
|
|
'aws_error_type' => $e->getAwsErrorType(),
|
|
'status_code' => $e->getStatusCode(),
|
|
'request_id' => $e->getAwsRequestId(),
|
|
]), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Upload failed: ' . $e->getMessage(),
|
|
'url' => null,
|
|
'key' => null
|
|
];
|
|
} catch (\Throwable $e) {
|
|
log_message('error', 'S3 Upload Exception | ' . json_encode(array_merge($startLog, [
|
|
'error_message' => $e->getMessage(),
|
|
'exception_class' => get_class($e),
|
|
'error_file' => $e->getFile(),
|
|
'error_line' => $e->getLine(),
|
|
]), JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE));
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Upload failed: ' . $e->getMessage(),
|
|
'url' => null,
|
|
'key' => null
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Download file from S3
|
|
*
|
|
* @param string $key S3 object key (file path in bucket)
|
|
* @param string $savePath Local path to save the file (optional)
|
|
* @param string|null $bucket Override source bucket (optional)
|
|
* @return array ['success' => bool, 'path' => string, 'content' => string, 'message' => string]
|
|
*/
|
|
public function download(string $key, string $savePath = null, ?string $bucket = null): array
|
|
{
|
|
try {
|
|
$result = $this->s3Client->getObject([
|
|
'Bucket' => $this->resolveBucket($bucket),
|
|
'Key' => $key,
|
|
]);
|
|
|
|
$content = (string) $result['Body'];
|
|
|
|
// If save path is provided, save to local file
|
|
if ($savePath) {
|
|
$directory = dirname($savePath);
|
|
if (!is_dir($directory)) {
|
|
mkdir($directory, 0755, true);
|
|
}
|
|
file_put_contents($savePath, $content);
|
|
|
|
return [
|
|
'success' => true,
|
|
'path' => $savePath,
|
|
'content' => $content,
|
|
'message' => 'File downloaded successfully'
|
|
];
|
|
}
|
|
|
|
return [
|
|
'success' => true,
|
|
'path' => null,
|
|
'content' => $content,
|
|
'message' => 'File content retrieved successfully'
|
|
];
|
|
|
|
} catch (AwsException $e) {
|
|
log_message('error', 'S3 Download Error: ' . $e->getMessage());
|
|
return [
|
|
'success' => false,
|
|
'path' => null,
|
|
'content' => null,
|
|
'message' => 'Download failed: ' . $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get a pre-signed URL for temporary access to a private file
|
|
*
|
|
* @param string $key S3 object key
|
|
* @param int $expiration Expiration time in minutes (default: 60)
|
|
* @param string|null $bucket Override source bucket (optional)
|
|
* @return array ['success' => bool, 'url' => string, 'message' => string]
|
|
*/
|
|
public function getPresignedUrl(string $key, int $expiration = 60, ?string $bucket = null): array
|
|
{
|
|
try {
|
|
$cmd = $this->s3Client->getCommand('GetObject', [
|
|
'Bucket' => $this->resolveBucket($bucket),
|
|
'Key' => $key
|
|
]);
|
|
|
|
$request = $this->s3Client->createPresignedRequest($cmd, "+{$expiration} minutes");
|
|
$presignedUrl = (string) $request->getUri();
|
|
|
|
return [
|
|
'success' => true,
|
|
'url' => $presignedUrl,
|
|
'message' => 'Pre-signed URL generated successfully'
|
|
];
|
|
|
|
} catch (AwsException $e) {
|
|
log_message('error', 'S3 Presigned URL Error: ' . $e->getMessage());
|
|
return [
|
|
'success' => false,
|
|
'url' => null,
|
|
'message' => 'Failed to generate URL: ' . $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Delete file from S3
|
|
*
|
|
* @param string $key S3 object key
|
|
* @param string|null $bucket Override source bucket (optional)
|
|
* @return array ['success' => bool, 'message' => string]
|
|
*/
|
|
public function delete(string $key, ?string $bucket = null): array
|
|
{
|
|
try {
|
|
$this->s3Client->deleteObject([
|
|
'Bucket' => $this->resolveBucket($bucket),
|
|
'Key' => $key,
|
|
]);
|
|
|
|
return [
|
|
'success' => true,
|
|
'message' => 'File deleted successfully'
|
|
];
|
|
|
|
} catch (AwsException $e) {
|
|
log_message('error', 'S3 Delete Error: ' . $e->getMessage());
|
|
return [
|
|
'success' => false,
|
|
'message' => 'Delete failed: ' . $e->getMessage()
|
|
];
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Check if file exists in S3
|
|
*
|
|
* @param string $key S3 object key
|
|
* @param string|null $bucket Override source bucket (optional)
|
|
* @return bool
|
|
*/
|
|
public function exists(string $key, ?string $bucket = null): bool
|
|
{
|
|
return $this->s3Client->doesObjectExist($this->resolveBucket($bucket), $key);
|
|
}
|
|
|
|
/**
|
|
* List files in S3 bucket
|
|
*
|
|
* @param string $prefix Folder prefix (optional)
|
|
* @param string|null $bucket Override source bucket (optional)
|
|
* @return array ['success' => bool, 'files' => array, 'message' => string]
|
|
*/
|
|
public function listFiles(string $prefix = '', ?string $bucket = null): array
|
|
{
|
|
try {
|
|
$targetBucket = $this->resolveBucket($bucket);
|
|
$files = [];
|
|
$token = null;
|
|
|
|
do {
|
|
$params = [
|
|
'Bucket' => $targetBucket,
|
|
'Prefix' => $prefix,
|
|
];
|
|
if ($token !== null) {
|
|
$params['ContinuationToken'] = $token;
|
|
}
|
|
|
|
$result = $this->s3Client->listObjectsV2($params);
|
|
|
|
if (isset($result['Contents'])) {
|
|
foreach ($result['Contents'] as $object) {
|
|
$files[] = [
|
|
'key' => $object['Key'],
|
|
'size' => $object['Size'],
|
|
'last_modified' => $object['LastModified']->format('Y-m-d H:i:s'),
|
|
'url' => $this->buildBaseUrl($targetBucket) . $object['Key'],
|
|
];
|
|
}
|
|
}
|
|
|
|
$token = ! empty($result['IsTruncated'])
|
|
? ($result['NextContinuationToken'] ?? null)
|
|
: null;
|
|
} while ($token !== null);
|
|
|
|
return [
|
|
'success' => true,
|
|
'files' => $files,
|
|
'message' => 'Files retrieved successfully',
|
|
];
|
|
|
|
} catch (AwsException $e) {
|
|
log_message('error', 'S3 List Error: ' . $e->getMessage());
|
|
return [
|
|
'success' => false,
|
|
'files' => [],
|
|
'message' => 'Failed to list files: ' . $e->getMessage(),
|
|
];
|
|
}
|
|
}
|
|
}
|