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] */ /** * List files in S3 bucket with optional MaxKeys / ContinuationToken pagination. * * @return array{success:bool,files:array,next_token:?string,is_truncated:bool,message:string} */ public function listFilesPage(string $prefix = '', int $maxKeys = 100, ?string $continuationToken = null, ?string $bucket = null): array { try { $targetBucket = $this->resolveBucket($bucket); $params = [ 'Bucket' => $targetBucket, 'Prefix' => $prefix, 'MaxKeys' => max(1, min(1000, $maxKeys)), ]; if ($continuationToken !== null && $continuationToken !== '') { $params['ContinuationToken'] = $continuationToken; } $result = $this->s3Client->listObjectsV2($params); $files = []; if (isset($result['Contents'])) { foreach ($result['Contents'] as $object) { $key = (string) ($object['Key'] ?? ''); if ($key === '' || substr($key, -1) === '/') { continue; } $files[] = [ 'key' => $key, 'size' => (int) ($object['Size'] ?? 0), 'last_modified' => $object['LastModified']->format('Y-m-d H:i:s'), 'url' => $this->buildBaseUrl($targetBucket) . $key, ]; } } $isTruncated = ! empty($result['IsTruncated']); $nextToken = $isTruncated ? ($result['NextContinuationToken'] ?? null) : null; return [ 'success' => true, 'files' => $files, 'next_token' => $nextToken, 'is_truncated' => $isTruncated, 'message' => 'Files retrieved successfully', ]; } catch (AwsException $e) { log_message('error', 'S3 List Page Error: ' . $e->getMessage()); return [ 'success' => false, 'files' => [], 'next_token' => null, 'is_truncated' => false, 'message' => 'Failed to list files: ' . $e->getMessage(), ]; } } /** * List top-level "folders" (common prefixes) in the bucket. * * @return array{success:bool,folders:list,message:string} */ public function listRootFolders(?string $bucket = null): array { try { $targetBucket = $this->resolveBucket($bucket); $folders = []; $token = null; do { $params = [ 'Bucket' => $targetBucket, 'Delimiter' => '/', 'MaxKeys' => 1000, ]; if ($token !== null) { $params['ContinuationToken'] = $token; } $result = $this->s3Client->listObjectsV2($params); if (! empty($result['CommonPrefixes'])) { foreach ($result['CommonPrefixes'] as $prefixRow) { $p = rtrim((string) ($prefixRow['Prefix'] ?? ''), '/'); if ($p !== '') { $folders[] = $p; } } } $token = ! empty($result['IsTruncated']) ? ($result['NextContinuationToken'] ?? null) : null; } while ($token !== null); $folders = array_values(array_unique($folders)); sort($folders); return [ 'success' => true, 'folders' => $folders, 'message' => 'Folders retrieved successfully', ]; } catch (AwsException $e) { log_message('error', 'S3 List Folders Error: ' . $e->getMessage()); return [ 'success' => false, 'folders' => [], 'message' => 'Failed to list folders: ' . $e->getMessage(), ]; } } 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(), ]; } } }