setLogController($logController); if($key) $this->setAccessKey($key); if($secret) $this->setSecretKey($secret); if($region) $this->setRegion($region); if($bucket) $this->setBucket($bucket); if($endpoint) $this->setEndpoint($endpoint); if($verifyssl) $this->setVerifySSL($verifyssl); if($chunk_size) $this->setChunkSize($chunk_size); if($keepalive_timeout) $this->setKeepAliveTimeout($keepalive_timeout); if($keepalive_requests) $this->setKeepAliveRequests($keepalive_requests); if($retries) $this->setRetries($retries); $this->_retry_upload = 0; } /** * @return Client */ public function getClient():Client { if (!$this->_client) $this->_client = new Client($this->getAccessKey(), $this->getSecretKey(), $this->getRegion(), $this->getBucket(), $this->getEndpoint(), $this->getVerifySSL(), $this->getKeepAliveTimeout(), $this->getKeepAliveRequests()); return $this->_client; } /** * @return LogController */ public function getLogController():LogController { return $this->_logController; } /** * @param LogController $logController * * @return void */ public function setLogController(LogController $logController):void { $this->_logController = $logController; } /** * @return string|null */ public function getAccessKey():?string { return $this->_key; } /** * @param string $key * * @return void */ public function setAccessKey(string $key):void { $this->_key = $key; } /** * @return string|null */ public function getSecretKey():?string { return $this->_secret; } /** * @param string $key * * @return void */ public function setSecretKey(string $key):void { $this->_secret = $key; } /** * @return string|null */ public function getRegion():?string { return $this->_region; } /** * @param string $region * * @return void */ public function setRegion(string $region):void { $this->_region = $region; } /** * @return string|null */ public function getBucket():?string { return $this->_bucket; } /** * @param string $bucket * * @return void */ public function setBucket(string $bucket):void { $this->_bucket = $bucket; } /** * @return string|null */ public function getEndpoint():?string { return $this->_endpoint; } /** * @param string $endpoint * * @return void */ public function setEndpoint(string $endpoint):void { $this->_endpoint = $endpoint; } /** * @return bool */ public function getVerifySSL():bool { return !!$this->_verifyssl; } /** * @param bool $verifyssl * * @return void */ public function setVerifySSL(bool $verifyssl):void { $this->_verifyssl = !!$verifyssl; } /** * @return int */ public function getChunkSize():int { return $this->_chunk_size ?: self::PUT_CHUNK_SIZE; } /** * @param int $size * * @return void */ public function setChunkSize(int $size):void { $this->_chunk_size = $size; } /** * @return int */ public function getKeepAliveTimeout():int { return $this->_keepalive_timeout?: self::KEEP_ALIVE_TIMEOUT; } /** * @param int $timeout * * @return void */ public function setKeepAliveTimeout(int $timeout):void { $this->_keepalive_timeout = $timeout; } /** * @return int */ public function getKeepAliveRequests():int { return $this->_keepalive_requests?: self::KEEP_ALIVE_REQUESTS; } /** * @param int $queries * * @return void */ public function setKeepAliveRequests(int $queries):void { $this->_keepalive_requests = $queries; } /** * @return int */ public function getRetries():int { return $this->_retries?: self::RETRIES; } /** * @param int $retries * * @return void */ public function setRetries(int $retries):void { $this->_retries = $retries; } /** * @return ClientRetry */ public static function client():ClientRetry { return new ClientRetry(); } /** * @param string $prefix A string used to group keys. When specified, the response will only contain objects with keys beginning with the string. * @param int $limit The maximum number of objects to return. Defaults to 1,000. * @param string $token The key (object name) to start with when listing objects. For use with pagination (e.g. when then number of objects in the result exceeds the specified max-keys). * @param string $delimiter A single character used to group keys. When specified, the response will only contain keys up to its first occurrence. (E.g. Using a slash as the delimiter can allow you to list keys as if they were folders, especially in combination with a prefix.) * * @return ListObjects * @throws ClientException */ public function listObjects(string $prefix='', int $limit=0, string $token='', string $delimiter='/'):ListObjects { // https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html //encoding-type=EncodingType&fetch-owner=FetchOwner&max-keys=MaxKeys&start-after=StartAfter $params = [ 'list-type' => 2 ]; if($delimiter) $params['delimiter'] = $delimiter; if($token) $params['continuation-token'] = $token; if($limit) $params['max-keys'] = $limit; if($prefix) { if(!str_ends_with($prefix, '/')) $prefix .= '/'; $params['prefix'] = $prefix; } $listObjects = new ListObjects(); $result = self::client()->func('get')->args('/', $params)->exec($this); $output = []; // NextContinuationToken is sent when isTruncated is true, which means there are more keys in the bucket that can be listed. // The next list requests to Amazon S3 can be continued with this NextContinuationToken. if(isset($result->Body->IsTruncated)) $listObjects->setIsTruncated($result->Body->IsTruncated); if(isset($result->Body->NextContinuationToken)) $listObjects->setNextContinuationToken($result->Body->NextContinuationToken); foreach($result->Body->CommonPrefixes as $objectData) { $key = (string) $objectData->Prefix; $object = new ObjectData(); $object->setKey($key); $output[] = $object; } foreach($result->Body->Contents as $objectData) { if($prefix == (string) $objectData->Key) continue; $object = new ObjectData(); $object->setKey($objectData->Key); //$object->setEtag($objectData->ETag); $object->setSize(intval($objectData->Size)); //$object->setType($objectData->Type); $object->setMtime($objectData->{"LastModified"} ? strtotime($objectData->{"LastModified"}) : 0); $output[] = $object; } $listObjects->setObjectsList($output); return $listObjects; } /** * @return string|null * @throws ClientException */ public function getBucketRegion():?string { $result = self::client()->func('get')->args('', [ 'location' => 1 ])->exec($this); if(!isset($result->Body->{0})) return null; return $result->Body->{0}; } /** * @param string $key * @param string|null $destination * @param int $start * @param int $end * * @return ObjectData * @throws ClientException */ public function getObject(string $key, ?string $destination=null, int $start=0, int $end=0):ObjectData { if($destination) { if($start || $end) $result = self::client()->func('getObjectRange')->args($key, $destination, $start, $end)->exec($this); else $result = self::client()->func('getObject')->args($key, $destination)->exec($this); } else $result = self::client()->func('head')->args($key)->exec($this); $object = new ObjectData(); $object->setKey($key); //$object->setEtag($result->Headers->{"etag"}); $object->setSize(intval($result->Headers->{"content-length"})); //$object->setType($result->Headers->{"x-rgw-object-type"}); $object->setMtime($result->Headers->{"last-modified"} ? strtotime($result->Headers->{"last-modified"}) : 0); return $object; } /** * @param string $object * * @return void * @throws ClientException */ public function deleteObject(string $object):void { self::client()->func('delete')->args($object)->exec($this); } /** * @param string $directory * * @return void * @throws ClientException */ public function createObject(string $directory):void { self::client()->func('putString')->args('', $directory)->exec($this); } /** * @param string $destination * * @return string * @throws ClientException */ public function createUploadID(string $destination):string { $result = self::client()->func('post')->args($destination, ['uploads' => 1])->exec($this); return (string) $result->Body->UploadId; } /** * @param string $destination * @param string $upload_id * * @return object|mixed * @throws ClientException */ public function listUploadParts(string $destination, string $upload_id, ?int $partNumberMarker=null):object { $params = ['uploadId' => $upload_id]; if($partNumberMarker) $params['part-number-marker'] = $partNumberMarker; return self::client()->func('get')->args($destination, $params)->exec($this); } /** * @param string $source * @param string $destination * * @return void * @throws ClientException */ public function putObject(string $source, string $destination):void { if(!file_exists($source) || !is_file($source)) throw new ClientException("Invalid source provided"); try { $file = new FileStream($source); } catch(FileException $e) { throw new ClientException($e->getMessage(), $e->getCode(), $e); } try { $chunk = new FileChunk($file, $this->getChunkSize()); $this->getLogController()->logDebug('[_putObjectSingle]'); $this->getLogController()->logDebug('File: ' . $file->getFile()); $this->getLogController()->logDebug('ChunkSize: ' . $chunk->getSize()); } catch(IOException $e) { throw new ClientException($e->getMessage(), $e->getCode(), $e); } self::client() ->func('putChunk') ->args($chunk, $destination) ->retryCallback(function() use ($chunk) { // rewind the chunk on each retry, we need to send the data from the strat $chunk->rewind(); }) ->exec($this); } /** * @throws ClientException */ public function putChunk(FileChunk $chunk, string $destination, string $upload_id, int $part_number) { return self::client()->func('putChunk') ->args($chunk, $destination, $upload_id, $part_number) ->exec($this); } /** * @throws ClientException */ public function closeChunkedUpload(string $destination, string $upload_id, string $document) { self::client() ->func('post') ->args('/' . $destination, [ 'uploadId' => $upload_id ], $document, 'application/xml') ->exec($this); } public function __destruct() { if($this->_client) $this->_client->close(); } }