Exception
1 year ago
.htaccess
1 year ago
Client.php
1 year ago
ClientManager.php
2 months ago
ClientRetry.php
1 year ago
ListObjects.php
1 year ago
ObjectData.php
1 year ago
index.html
1 year ago
web.config
1 year ago
ClientManager.php
410 lines
| 1 | <?php |
| 2 | /* |
| 3 | * |
| 4 | * JetBackup @ package |
| 5 | * Created By Idan Ben-Ezra |
| 6 | * |
| 7 | * Copyrights @ JetApps |
| 8 | * https://www.jetapps.com |
| 9 | * |
| 10 | **/ |
| 11 | namespace JetBackup\Destination\Vendors\S3\Client; |
| 12 | |
| 13 | use Exception; |
| 14 | use JetBackup\Destination\Vendors\S3\Client\Exception\ClientException; |
| 15 | use JetBackup\Exception\IOException; |
| 16 | use JetBackup\Log\LogController; |
| 17 | use JetBackup\Web\File\FileChunk; |
| 18 | use JetBackup\Web\File\FileChunkIterator; |
| 19 | use JetBackup\Web\File\FileException; |
| 20 | use JetBackup\Web\File\FileStream; |
| 21 | use SimpleXMLElement; |
| 22 | |
| 23 | defined( '__JETBACKUP__' ) or die( 'Restricted access' ); |
| 24 | |
| 25 | class ClientManager { |
| 26 | |
| 27 | const PUT_CHUNK_SIZE = 1073741824; // 1GB |
| 28 | const KEEP_ALIVE_TIMEOUT = 60; |
| 29 | const KEEP_ALIVE_REQUESTS = 100; |
| 30 | const RETRIES = 0; |
| 31 | |
| 32 | private LogController $_logController; |
| 33 | private ?string $_key; |
| 34 | private ?string $_secret; |
| 35 | private ?string $_region; |
| 36 | private ?string $_bucket; |
| 37 | private ?string $_endpoint; |
| 38 | private ?bool $_verifyssl; |
| 39 | private ?int $_retries; |
| 40 | private ?bool $_verifyupload; |
| 41 | private ?int $_chunk_size; |
| 42 | private ?int $_keepalive_timeout; |
| 43 | private ?int $_keepalive_requests; |
| 44 | private int $_retry_upload; |
| 45 | private ?Client $_client = null; |
| 46 | |
| 47 | /** |
| 48 | * @param LogController $logController |
| 49 | * @param string|null $key |
| 50 | * @param string|null $secret |
| 51 | * @param string|null $region |
| 52 | * @param string|null $bucket |
| 53 | * @param string|null $endpoint |
| 54 | * @param bool|null $verifyssl |
| 55 | * @param int|null $chunk_size |
| 56 | * @param int|null $keepalive_timeout |
| 57 | * @param int|null $keepalive_requests |
| 58 | * @param int|null $retries |
| 59 | */ |
| 60 | public function __construct(LogController $logController, ?string $key=null, ?string $secret=null, ?string $region=null, ?string $bucket=null, ?string $endpoint=null, ?bool $verifyssl=true, ?int $chunk_size=null, ?int $keepalive_timeout=null, ?int $keepalive_requests=null, ?int $retries=null) { |
| 61 | $this->setLogController($logController); |
| 62 | if($key) $this->setAccessKey($key); |
| 63 | if($secret) $this->setSecretKey($secret); |
| 64 | if($region) $this->setRegion($region); |
| 65 | if($bucket) $this->setBucket($bucket); |
| 66 | if($endpoint) $this->setEndpoint($endpoint); |
| 67 | if($verifyssl) $this->setVerifySSL($verifyssl); |
| 68 | if($chunk_size) $this->setChunkSize($chunk_size); |
| 69 | if($keepalive_timeout) $this->setKeepAliveTimeout($keepalive_timeout); |
| 70 | if($keepalive_requests) $this->setKeepAliveRequests($keepalive_requests); |
| 71 | if($retries) $this->setRetries($retries); |
| 72 | $this->_retry_upload = 0; |
| 73 | } |
| 74 | |
| 75 | /** |
| 76 | * @return Client |
| 77 | */ |
| 78 | public function getClient():Client { |
| 79 | if (!$this->_client) $this->_client = new Client($this->getAccessKey(), $this->getSecretKey(), $this->getRegion(), $this->getBucket(), $this->getEndpoint(), $this->getVerifySSL(), $this->getKeepAliveTimeout(), $this->getKeepAliveRequests()); |
| 80 | return $this->_client; |
| 81 | } |
| 82 | |
| 83 | /** |
| 84 | * @return LogController |
| 85 | */ |
| 86 | public function getLogController():LogController { return $this->_logController; } |
| 87 | |
| 88 | /** |
| 89 | * @param LogController $logController |
| 90 | * |
| 91 | * @return void |
| 92 | */ |
| 93 | public function setLogController(LogController $logController):void { $this->_logController = $logController; } |
| 94 | |
| 95 | /** |
| 96 | * @return string|null |
| 97 | */ |
| 98 | public function getAccessKey():?string { return $this->_key; } |
| 99 | |
| 100 | /** |
| 101 | * @param string $key |
| 102 | * |
| 103 | * @return void |
| 104 | */ |
| 105 | public function setAccessKey(string $key):void { $this->_key = $key; } |
| 106 | |
| 107 | /** |
| 108 | * @return string|null |
| 109 | */ |
| 110 | public function getSecretKey():?string { return $this->_secret; } |
| 111 | |
| 112 | /** |
| 113 | * @param string $key |
| 114 | * |
| 115 | * @return void |
| 116 | */ |
| 117 | public function setSecretKey(string $key):void { $this->_secret = $key; } |
| 118 | |
| 119 | /** |
| 120 | * @return string|null |
| 121 | */ |
| 122 | public function getRegion():?string { return $this->_region; } |
| 123 | |
| 124 | /** |
| 125 | * @param string $region |
| 126 | * |
| 127 | * @return void |
| 128 | */ |
| 129 | public function setRegion(string $region):void { $this->_region = $region; } |
| 130 | |
| 131 | /** |
| 132 | * @return string|null |
| 133 | */ |
| 134 | public function getBucket():?string { return $this->_bucket; } |
| 135 | |
| 136 | /** |
| 137 | * @param string $bucket |
| 138 | * |
| 139 | * @return void |
| 140 | */ |
| 141 | public function setBucket(string $bucket):void { $this->_bucket = $bucket; } |
| 142 | |
| 143 | /** |
| 144 | * @return string|null |
| 145 | */ |
| 146 | public function getEndpoint():?string { return $this->_endpoint; } |
| 147 | |
| 148 | /** |
| 149 | * @param string $endpoint |
| 150 | * |
| 151 | * @return void |
| 152 | */ |
| 153 | public function setEndpoint(string $endpoint):void { $this->_endpoint = $endpoint; } |
| 154 | |
| 155 | /** |
| 156 | * @return bool |
| 157 | */ |
| 158 | public function getVerifySSL():bool { return !!$this->_verifyssl; } |
| 159 | |
| 160 | /** |
| 161 | * @param bool $verifyssl |
| 162 | * |
| 163 | * @return void |
| 164 | */ |
| 165 | public function setVerifySSL(bool $verifyssl):void { $this->_verifyssl = !!$verifyssl; } |
| 166 | |
| 167 | /** |
| 168 | * @return int |
| 169 | */ |
| 170 | public function getChunkSize():int { return $this->_chunk_size ?: self::PUT_CHUNK_SIZE; } |
| 171 | |
| 172 | /** |
| 173 | * @param int $size |
| 174 | * |
| 175 | * @return void |
| 176 | */ |
| 177 | public function setChunkSize(int $size):void { $this->_chunk_size = $size; } |
| 178 | |
| 179 | /** |
| 180 | * @return int |
| 181 | */ |
| 182 | public function getKeepAliveTimeout():int { return $this->_keepalive_timeout?: self::KEEP_ALIVE_TIMEOUT; } |
| 183 | |
| 184 | /** |
| 185 | * @param int $timeout |
| 186 | * |
| 187 | * @return void |
| 188 | */ |
| 189 | public function setKeepAliveTimeout(int $timeout):void { $this->_keepalive_timeout = $timeout; } |
| 190 | |
| 191 | /** |
| 192 | * @return int |
| 193 | */ |
| 194 | public function getKeepAliveRequests():int { return $this->_keepalive_requests?: self::KEEP_ALIVE_REQUESTS; } |
| 195 | |
| 196 | /** |
| 197 | * @param int $queries |
| 198 | * |
| 199 | * @return void |
| 200 | */ |
| 201 | public function setKeepAliveRequests(int $queries):void { $this->_keepalive_requests = $queries; } |
| 202 | |
| 203 | /** |
| 204 | * @return int |
| 205 | */ |
| 206 | public function getRetries():int { return $this->_retries?: self::RETRIES; } |
| 207 | |
| 208 | /** |
| 209 | * @param int $retries |
| 210 | * |
| 211 | * @return void |
| 212 | */ |
| 213 | public function setRetries(int $retries):void { $this->_retries = $retries; } |
| 214 | |
| 215 | /** |
| 216 | * @return ClientRetry |
| 217 | */ |
| 218 | public static function client():ClientRetry { return new ClientRetry(); } |
| 219 | |
| 220 | /** |
| 221 | * @param string $prefix A string used to group keys. When specified, the response will only contain objects with keys beginning with the string. |
| 222 | * @param int $limit The maximum number of objects to return. Defaults to 1,000. |
| 223 | * @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). |
| 224 | * @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.) |
| 225 | * |
| 226 | * @return ListObjects |
| 227 | * @throws ClientException |
| 228 | */ |
| 229 | public function listObjects(string $prefix='', int $limit=0, string $token='', string $delimiter='/'):ListObjects { |
| 230 | |
| 231 | // https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html |
| 232 | //encoding-type=EncodingType&fetch-owner=FetchOwner&max-keys=MaxKeys&start-after=StartAfter |
| 233 | |
| 234 | $params = [ 'list-type' => 2 ]; |
| 235 | if($delimiter) $params['delimiter'] = $delimiter; |
| 236 | if($token) $params['continuation-token'] = $token; |
| 237 | if($limit) $params['max-keys'] = $limit; |
| 238 | if($prefix) { |
| 239 | if(!str_ends_with($prefix, '/')) $prefix .= '/'; |
| 240 | $params['prefix'] = $prefix; |
| 241 | } |
| 242 | |
| 243 | $listObjects = new ListObjects(); |
| 244 | |
| 245 | $result = self::client()->func('get')->args('/', $params)->exec($this); |
| 246 | $output = []; |
| 247 | |
| 248 | // NextContinuationToken is sent when isTruncated is true, which means there are more keys in the bucket that can be listed. |
| 249 | // The next list requests to Amazon S3 can be continued with this NextContinuationToken. |
| 250 | if(isset($result->Body->IsTruncated)) $listObjects->setIsTruncated($result->Body->IsTruncated); |
| 251 | if(isset($result->Body->NextContinuationToken)) $listObjects->setNextContinuationToken($result->Body->NextContinuationToken); |
| 252 | |
| 253 | foreach($result->Body->CommonPrefixes as $objectData) { |
| 254 | $key = (string) $objectData->Prefix; |
| 255 | $object = new ObjectData(); |
| 256 | $object->setKey($key); |
| 257 | $output[] = $object; |
| 258 | } |
| 259 | |
| 260 | foreach($result->Body->Contents as $objectData) { |
| 261 | if($prefix == (string) $objectData->Key) continue; |
| 262 | $object = new ObjectData(); |
| 263 | $object->setKey($objectData->Key); |
| 264 | //$object->setEtag($objectData->ETag); |
| 265 | $object->setSize(intval($objectData->Size)); |
| 266 | //$object->setType($objectData->Type); |
| 267 | $object->setMtime($objectData->{"LastModified"} ? strtotime($objectData->{"LastModified"}) : 0); |
| 268 | $output[] = $object; |
| 269 | } |
| 270 | |
| 271 | $listObjects->setObjectsList($output); |
| 272 | |
| 273 | return $listObjects; |
| 274 | } |
| 275 | |
| 276 | /** |
| 277 | * @return string|null |
| 278 | * @throws ClientException |
| 279 | */ |
| 280 | public function getBucketRegion():?string { |
| 281 | $result = self::client()->func('get')->args('', [ 'location' => 1 ])->exec($this); |
| 282 | if(!isset($result->Body->{0})) return null; |
| 283 | return $result->Body->{0}; |
| 284 | } |
| 285 | |
| 286 | /** |
| 287 | * @param string $key |
| 288 | * @param string|null $destination |
| 289 | * @param int $start |
| 290 | * @param int $end |
| 291 | * |
| 292 | * @return ObjectData |
| 293 | * @throws ClientException |
| 294 | */ |
| 295 | public function getObject(string $key, ?string $destination=null, int $start=0, int $end=0):ObjectData { |
| 296 | if($destination) { |
| 297 | if($start || $end) $result = self::client()->func('getObjectRange')->args($key, $destination, $start, $end)->exec($this); |
| 298 | else $result = self::client()->func('getObject')->args($key, $destination)->exec($this); |
| 299 | } else $result = self::client()->func('head')->args($key)->exec($this); |
| 300 | $object = new ObjectData(); |
| 301 | $object->setKey($key); |
| 302 | //$object->setEtag($result->Headers->{"etag"}); |
| 303 | $object->setSize(intval($result->Headers->{"content-length"})); |
| 304 | //$object->setType($result->Headers->{"x-rgw-object-type"}); |
| 305 | $object->setMtime($result->Headers->{"last-modified"} ? strtotime($result->Headers->{"last-modified"}) : 0); |
| 306 | return $object; |
| 307 | } |
| 308 | |
| 309 | /** |
| 310 | * @param string $object |
| 311 | * |
| 312 | * @return void |
| 313 | * @throws ClientException |
| 314 | */ |
| 315 | public function deleteObject(string $object):void { |
| 316 | self::client()->func('delete')->args($object)->exec($this); |
| 317 | } |
| 318 | |
| 319 | /** |
| 320 | * @param string $directory |
| 321 | * |
| 322 | * @return void |
| 323 | * @throws ClientException |
| 324 | */ |
| 325 | public function createObject(string $directory):void { |
| 326 | self::client()->func('putString')->args('', $directory)->exec($this); |
| 327 | } |
| 328 | |
| 329 | /** |
| 330 | * @param string $destination |
| 331 | * |
| 332 | * @return string |
| 333 | * @throws ClientException |
| 334 | */ |
| 335 | public function createUploadID(string $destination):string { |
| 336 | $result = self::client()->func('post')->args($destination, ['uploads' => 1])->exec($this); |
| 337 | return (string) $result->Body->UploadId; |
| 338 | } |
| 339 | |
| 340 | /** |
| 341 | * @param string $destination |
| 342 | * @param string $upload_id |
| 343 | * |
| 344 | * @return object|mixed |
| 345 | * @throws ClientException |
| 346 | */ |
| 347 | public function listUploadParts(string $destination, string $upload_id, ?int $partNumberMarker=null):object { |
| 348 | $params = ['uploadId' => $upload_id]; |
| 349 | if($partNumberMarker) $params['part-number-marker'] = $partNumberMarker; |
| 350 | return self::client()->func('get')->args($destination, $params)->exec($this); |
| 351 | } |
| 352 | |
| 353 | /** |
| 354 | * @param string $source |
| 355 | * @param string $destination |
| 356 | * |
| 357 | * @return void |
| 358 | * @throws ClientException |
| 359 | */ |
| 360 | public function putObject(string $source, string $destination):void { |
| 361 | if(!file_exists($source) || !is_file($source)) throw new ClientException("Invalid source provided"); |
| 362 | |
| 363 | try { |
| 364 | $file = new FileStream($source); |
| 365 | } catch(FileException $e) { |
| 366 | throw new ClientException($e->getMessage(), $e->getCode(), $e); |
| 367 | } |
| 368 | |
| 369 | try { |
| 370 | $chunk = new FileChunk($file, $this->getChunkSize()); |
| 371 | $this->getLogController()->logDebug('[_putObjectSingle]'); |
| 372 | $this->getLogController()->logDebug('File: ' . $file->getFile()); |
| 373 | $this->getLogController()->logDebug('ChunkSize: ' . $chunk->getSize()); |
| 374 | } catch(IOException $e) { |
| 375 | throw new ClientException($e->getMessage(), $e->getCode(), $e); |
| 376 | } |
| 377 | |
| 378 | self::client() |
| 379 | ->func('putChunk') |
| 380 | ->args($chunk, $destination) |
| 381 | ->retryCallback(function() use ($chunk) { |
| 382 | // rewind the chunk on each retry, we need to send the data from the strat |
| 383 | $chunk->rewind(); |
| 384 | }) |
| 385 | ->exec($this); |
| 386 | } |
| 387 | |
| 388 | /** |
| 389 | * @throws ClientException |
| 390 | */ |
| 391 | public function putChunk(FileChunk $chunk, string $destination, string $upload_id, int $part_number) { |
| 392 | return self::client()->func('putChunk') |
| 393 | ->args($chunk, $destination, $upload_id, $part_number) |
| 394 | ->exec($this); |
| 395 | } |
| 396 | |
| 397 | /** |
| 398 | * @throws ClientException |
| 399 | */ |
| 400 | public function closeChunkedUpload(string $destination, string $upload_id, string $document) { |
| 401 | self::client() |
| 402 | ->func('post') |
| 403 | ->args('/' . $destination, [ 'uploadId' => $upload_id ], $document, 'application/xml') |
| 404 | ->exec($this); |
| 405 | } |
| 406 | |
| 407 | public function __destruct() { |
| 408 | if($this->_client) $this->_client->close(); |
| 409 | } |
| 410 | } |