region = $config['region'] ?? ''; $this->bucket = $config['bucket'] ?? ''; $this->endpoint = $config['endpoint'] ?? null; if ($this->region === '' || $this->bucket === '') { throw new Exception('OSS config missing: region/bucket'); } if (!empty($config['access_key_id']) && !empty($config['access_key_secret'])) { $credentialsProvider = new FixedCredentialsProvider( (string)$config['access_key_id'], (string)$config['access_key_secret'], $config['session_token'] ?? null ); } else { $credentialsProvider = new Oss\Credentials\EnvironmentVariableCredentialsProvider(); } $cfg = Oss\Config::loadDefault(); $cfg->setCredentialsProvider($credentialsProvider); $cfg->setRegion($this->region); if ($this->endpoint) { $cfg->setEndpoint($this->endpoint); if ($this->shouldUseCname($this->endpoint)) { $cfg->setUseCname(true); } } $this->client = new Oss\Client($cfg); $this->assertCredentialsVisible($config); } public function upload(string $source, string $key, ?string $bucket = null, array $options = []): array { if ($this->isBase64DataUri($source)) { return $this->uploadFromBase64($source, $key, $bucket, $options); } if ($this->isUrl($source)) { return $this->uploadFromUrl($source, $key, $bucket, $options); } return $this->uploadFromFile($source, $key, $bucket, $options); } public function uploadFromBase64(string $base64, string $key, ?string $bucket = null, array $options = []): array { $contentType = null; $payload = $base64; if (preg_match('/^data:([a-zA-Z0-9.+\\/-]+);base64,(.*)$/s', $base64, $m)) { $contentType = strtolower((string)$m[1]); $payload = (string)$m[2]; } $payload = preg_replace('/\\s+/', '', $payload ?? ''); if ($payload === '') { throw new Exception('Base64 payload is empty'); } $binary = base64_decode($payload, true); if ($binary === false) { throw new Exception('Invalid base64 payload'); } if ($contentType && empty($options['contentType'])) { $options['contentType'] = $contentType; } return $this->uploadFromBinary($binary, $key, $bucket, $options); } public function uploadFromBinary(string $binary, string $key, ?string $bucket = null, array $options = []): array { if ($binary === '') { throw new Exception('Binary payload is empty'); } $stream = fopen('php://temp', 'wb+'); if ($stream === false) { throw new Exception('Unable to create temporary stream for binary upload'); } fwrite($stream, $binary); rewind($stream); return $this->putObjectFromResource($stream, $key, $bucket, $options); } public function uploadFromFile(string $filePath, string $key, ?string $bucket = null, array $options = []): array { if (!is_file($filePath)) { throw new Exception("Local file not found: {$filePath}"); } $fp = fopen($filePath, 'rb'); if ($fp === false) { throw new Exception("Unable to open local file: {$filePath}"); } return $this->putObjectFromResource($fp, $key, $bucket, $options); } public function uploadFromUrl(string $url, string $key, ?string $bucket = null, array $options = []): array { $fp = @fopen($url, 'rb'); if ($fp !== false) { return $this->putObjectFromResource($fp, $key, $bucket, $options); } $tmp = tmpfile(); if ($tmp === false) { throw new Exception('Unable to create tmpfile() for remote download'); } $this->downloadToStreamByCurl($url, $tmp); rewind($tmp); return $this->putObjectFromResource($tmp, $key, $bucket, $options); } public function buildObjectUrl(string $key, ?string $bucket = null, bool $useHttps = true): string { $bucket = $bucket ?: $this->bucket; $key = ltrim($key, '/'); $endpoint = $this->endpoint ?: sprintf('oss-%s.aliyuncs.com', $this->region); $host = $this->extractEndpointHost($endpoint); $scheme = $useHttps ? 'https' : 'http'; $encodedKey = implode('/', array_map('rawurlencode', explode('/', $key))); if ($this->shouldUseCname($endpoint)) { return sprintf('%s://%s/%s', $scheme, $host, $encodedKey); } return sprintf('%s://%s.%s/%s', $scheme, $bucket, $host, $encodedKey); } private function putObjectFromResource($resource, string $key, ?string $bucket = null, array $options = []): array { $bucket = $bucket ?: $this->bucket; $key = ltrim($key, '/'); $metadata = $options['metadata'] ?? []; $contentType = $options['contentType'] ?? null; $acl = $options['acl'] ?? null; $body = Oss\Utils::streamFor($resource); $request = new Oss\Models\PutObjectRequest( bucket: $bucket, key: $key, metadata: $metadata ?: null, contentType: $contentType, objectAcl: $acl ); $request->body = $body; $result = $this->client->putObject($request); return [ 'statusCode' => $result->statusCode, 'requestId' => $result->requestId, 'etag' => $result->etag, 'bucket' => $bucket, 'key' => $key, ]; } private function downloadToStreamByCurl(string $url, $destStream): void { if (!function_exists('curl_init')) { throw new Exception('cURL extension is required when allow_url_fopen is disabled.'); } $ch = curl_init($url); curl_setopt_array($ch, [ CURLOPT_FILE => $destStream, CURLOPT_FOLLOWLOCATION => true, CURLOPT_MAXREDIRS => 5, CURLOPT_CONNECTTIMEOUT => 10, CURLOPT_TIMEOUT => 60, CURLOPT_FAILONERROR => true, CURLOPT_USERAGENT => 'OssService/1.0', ]); $ok = curl_exec($ch); if ($ok !== true) { $err = curl_error($ch); $code = curl_getinfo($ch, CURLINFO_RESPONSE_CODE); curl_close($ch); throw new Exception("Failed to download remote file. HTTP={$code}, error={$err}"); } curl_close($ch); } private function isUrl(string $value): bool { return (bool)filter_var($value, FILTER_VALIDATE_URL); } private function isBase64DataUri(string $value): bool { return (bool)preg_match('/^data:[a-zA-Z0-9.+\\/-]+;base64,/i', $value); } private function assertCredentialsVisible(array $config): void { if (!empty($config['access_key_id']) && !empty($config['access_key_secret'])) { return; } $ak = getenv('OSS_ACCESS_KEY_ID'); $sk = getenv('OSS_ACCESS_KEY_SECRET'); if (empty($ak) || empty($sk)) { throw new Exception( "OSS credentials missing in PHP process env. " . "getenv('OSS_ACCESS_KEY_ID')/getenv('OSS_ACCESS_KEY_SECRET') is empty. " . "Fix: make sure the PHP runtime process can see env vars, or pass access_key_id/access_key_secret into OssService config." ); } } private function shouldUseCname(string $endpoint): bool { $host = strtolower($this->extractEndpointHost($endpoint)); if ($host === '' || str_contains($host, 'aliyuncs.com') || str_contains($host, '.oss-')) { return false; } return true; } private function extractEndpointHost(string $endpoint): string { $normalized = preg_match('#^https?://#i', $endpoint) ? $endpoint : 'https://' . ltrim($endpoint, '/'); return (string)(parse_url($normalized, PHP_URL_HOST) ?: ''); } public function presignGetObjectUrl(string $key, ?string $bucket = null, int $expireSeconds = 900): array { $bucket = $bucket ?: $this->bucket; $key = ltrim($key, '/'); $request = new Oss\Models\GetObjectRequest(bucket: $bucket, key: $key); $result = $this->client->presign($request, [ 'expires' => new \DateInterval("PT{$expireSeconds}S"), ]); return [ 'url' => $result->url, 'expire' => $expireSeconds, 'bucket' => $bucket, 'key' => $key, ]; } } final class FixedCredentialsProvider implements Oss\Credentials\CredentialsProvider { private string $ak; private string $sk; private ?string $token; public function __construct(string $ak, string $sk, ?string $token = null) { $this->ak = $ak; $this->sk = $sk; $this->token = $token; } public function getCredentials(): Oss\Credentials\Credentials { return new Oss\Credentials\Credentials($this->ak, $this->sk, $this->token); } }