Your IP : 216.73.216.86


Current Path : /var/www/homesaver/www/bitrix/modules/clouds/classes/general/
Upload File :
Current File : /var/www/homesaver/www/bitrix/modules/clouds/classes/general/storage_service_openstack.php

<?php
IncludeModuleLangFile(__FILE__);

class CCloudStorageService_OpenStackStorage extends CCloudStorageService
{
	public function GetObject()
	{
		return new CCloudStorageService_OpenStackStorage();
	}

	public function GetID()
	{
		return 'openstack_storage';
	}

	public function GetName()
	{
		return 'OpenStack Object Storage';
	}

	public function GetLocationList()
	{
		return [
			'' => 'N/A',
		];
	}

	public function GetSettingsHTML($arBucket, $bServiceSet, $cur_SERVICE_ID, $bVarsFromForm)
	{
		if ($bVarsFromForm)
		{
			$arSettings = $_POST['SETTINGS'][$this->GetID()];
		}
		else
		{
			$arSettings = unserialize($arBucket['SETTINGS'], ['allowed_classes' => false]);
		}

		if (!is_array($arSettings))
		{
			$arSettings = [
				'HOST' => '',
				'USER' => '',
				'KEY' => '',
				'FORCE_HTTP' => 'N',
			];
		}

		$htmlID = htmlspecialcharsbx($this->GetID());
		$show = (($cur_SERVICE_ID == $this->GetID()) || !$bServiceSet) ? '' : 'none';

		$result = '
		<tr id="SETTINGS_2_' . $htmlID . '" style="display:' . $show . '" class="settings-tr adm-detail-required-field">
			<td>' . GetMessage('CLO_STORAGE_OPENSTACK_EDIT_HOST') . ':</td>
			<td><input type="hidden" name="SETTINGS[' . $htmlID . '][HOST]" id="' . $htmlID . 'HOST" value="' . htmlspecialcharsbx($arSettings['HOST']) . '"><input type="text" size="55" name="' . $htmlID . 'INP_HOST" id="' . $htmlID . 'INP_HOST" value="' . htmlspecialcharsbx($arSettings['HOST']) . '" ' . ($arBucket['READ_ONLY'] == 'Y' ? '"disabled"' : '') . ' onchange="BX(\'' . $htmlID . 'HOST\').value = this.value"></td>
		</tr>
		<tr id="SETTINGS_0_' . $htmlID . '" style="display:' . $show . '" class="settings-tr adm-detail-required-field">
			<td>' . GetMessage('CLO_STORAGE_OPENSTACK_EDIT_USER') . ':</td>
			<td><input type="hidden" name="SETTINGS[' . $htmlID . '][USER]" id="' . $htmlID . 'USER" value="' . htmlspecialcharsbx($arSettings['USER']) . '"><input type="text" size="55" name="' . $htmlID . 'INP_" id="' . $htmlID . 'INP_USER" value="' . htmlspecialcharsbx($arSettings['USER']) . '" ' . ($arBucket['READ_ONLY'] == 'Y' ? '"disabled"' : '') . ' onchange="BX(\'' . $htmlID . 'USER\').value = this.value"></td>
		</tr>
		<tr id="SETTINGS_1_' . $htmlID . '" style="display:' . $show . '" class="settings-tr adm-detail-required-field">
			<td>' . GetMessage('CLO_STORAGE_OPENSTACK_EDIT_KEY') . ':</td>
			<td><input type="hidden" name="SETTINGS[' . $htmlID . '][KEY]" id="' . $htmlID . 'KEY" value="' . htmlspecialcharsbx($arSettings['KEY']) . '"><input type="text" size="55" name="' . $htmlID . 'INP_KEY" id="' . $htmlID . 'INP_KEY" value="' . htmlspecialcharsbx($arSettings['KEY']) . '" autocomplete="off" ' . ($arBucket['READ_ONLY'] == 'Y' ? '"disabled"' : '') . ' onchange="BX(\'' . $htmlID . 'KEY\').value = this.value"></td>
		</tr>
		<tr id="SETTINGS_3_' . $htmlID . '" style="display:' . $show . '" class="settings-tr">
			<td>' . GetMessage('CLO_STORAGE_OPENSTACK_FORCE_HTTP') . ':</td>
			<td><input type="hidden" name="SETTINGS[' . $htmlID . '][FORCE_HTTP]" id="' . $htmlID . 'KEY" value="N"><input type="checkbox" name="SETTINGS[' . $htmlID . '][FORCE_HTTP]" id="' . $htmlID . 'FORCE_HTTP" value="Y" ' . ($arSettings['FORCE_HTTP'] == 'Y' ? 'checked="checked"' : '') . '></td>
		</tr>
		';
		return $result;
	}

	public function CheckSettings($arBucket, &$arSettings)
	{
		global $APPLICATION;
		$aMsg = [];

		$result = [
			'HOST' => is_array($arSettings) ? trim($arSettings['HOST']) : '',
			'USER' => is_array($arSettings) ? trim($arSettings['USER']) : '',
			'KEY' => is_array($arSettings) ? trim($arSettings['KEY']) : '',
			'FORCE_HTTP' => is_array($arSettings) && $arSettings['FORCE_HTTP'] == 'Y' ? 'Y' : 'N',
		];

		if ($arBucket['READ_ONLY'] !== 'Y' && !mb_strlen($result['HOST']))
		{
			$aMsg[] = ['id' => $this->GetID() . 'INP_HOST', 'text' => GetMessage('CLO_STORAGE_OPENSTACK_EMPTY_HOST')];
		}

		if ($arBucket['READ_ONLY'] !== 'Y' && !mb_strlen($result['USER']))
		{
			$aMsg[] = ['id' => $this->GetID() . 'INP_USER', 'text' => GetMessage('CLO_STORAGE_OPENSTACK_EMPTY_USER')];
		}

		if ($arBucket['READ_ONLY'] !== 'Y' && !mb_strlen($result['KEY']))
		{
			$aMsg[] = ['id' => $this->GetID() . 'INP_KEY', 'text' => GetMessage('CLO_STORAGE_OPENSTACK_EMPTY_KEY')];
		}

		if (empty($aMsg))
		{
			if (!$this->_GetToken($result['HOST'], $result['USER'], $result['KEY']))
			{
				$aMsg[] = ['text' => GetMessage('CLO_STORAGE_OPENSTACK_ERROR_GET_TOKEN')];
			}
		}

		if (!empty($aMsg))
		{
			$e = new CAdminException($aMsg);
			$APPLICATION->ThrowException($e);
			return false;
		}
		else
		{
			$arSettings = $result;
		}

		return true;
	}

	public function _GetToken($host, $user, $key)
	{
		global $APPLICATION;
		static $results = [];
		$cache_id = 'v0|' . $host . '|' . $user . '|' . $key;

		if (array_key_exists($cache_id, $results))
		{
			$result = $results[$cache_id];
		}
		else
		{
			$result = false;
			$obCache = new CPHPCache;

			if ($obCache->InitCache(600, $cache_id, '/')) /*TODO make setting*/
			{
				$result = $obCache->GetVars();
			}
			else
			{
				$this->status = 0;
				$this->host = $host;
				$this->verb = 'GET';
				$this->url = 'http://' . $host . '/v1.0';
				$this->headers = [];
				$this->errno = 0;
				$this->errstr = '';
				$this->result = '';

				$stime = 0;
				$logRequest = false;
				if (defined('BX_CLOUDS_TRACE') && $this->verb !== 'GET' && $this->verb !== 'HEAD')
				{
					$stime = microtime(1);
					$logRequest = [
						'request_id' => md5((string)mt_rand()),
						'portal' => $_SERVER['HTTP_HOST'],
						'verb' => $this->verb,
						'url' => $this->url,
					];
					AddMessage2Log(json_encode($logRequest), 'clouds', 20);
				}

				$request = new Bitrix\Main\Web\HttpClient([
					'redirect' => false,
					'streamTimeout' => $this->streamTimeout,
				]);
				$request->setHeader('X-Auth-User', $user);
				$request->setHeader('X-Auth-Key', $key);
				$request->query($this->verb, $this->url);

				$this->status = $request->getStatus();
				foreach ($request->getHeaders() as $headerKey => $headerValue)
				{
					$this->headers[$headerKey] = is_array($headerValue) ? $headerValue[0] : $headerValue;
				}
				$this->errstr = implode("\n", $request->getError());
				$this->errno = $this->errstr ? 255 : 0;
				$this->result = $request->getResult();

				if ($logRequest)
				{
					$logRequest['status'] = $this->status;
					$logRequest['time'] = round(microtime(true) - $stime, 6);
					$logRequest['headers'] = $this->headers;
					AddMessage2Log(json_encode($logRequest), 'clouds', 0);
				}

				if ($this->status == 412)
				{
					$APPLICATION->ResetException();

					$this->status = 0;
					$this->host = $host;
					$this->verb = 'GET';
					$this->url = 'http://' . $host . '/auth/v1.0';
					$this->headers = [];
					$this->errno = 0;
					$this->errstr = '';
					$this->result = '';

					$logRequest = false;
					if (defined('BX_CLOUDS_TRACE') && $this->verb !== 'GET' && $this->verb !== 'HEAD')
					{
						$stime = microtime(1);
						$logRequest = [
							'request_id' => md5((string)mt_rand()),
							'portal' => $_SERVER['HTTP_HOST'],
							'verb' => $this->verb,
							'url' => $this->url,
						];
						AddMessage2Log(json_encode($logRequest), 'clouds', 20);
					}

					$request = new Bitrix\Main\Web\HttpClient([
						'redirect' => false,
						'streamTimeout' => $this->streamTimeout,
					]);
					$request->setHeader('X-Auth-User', $user);
					$request->setHeader('X-Auth-Key', $key);
					$request->query($this->verb, $this->url);

					$this->status = $request->getStatus();
					foreach ($request->getHeaders() as $headerKey => $headerValue)
					{
						$this->headers[$headerKey] = is_array($headerValue) ? $headerValue[0] : $headerValue;
					}
					$this->errstr = implode("\n", $request->getError());
					$this->errno = $this->errstr ? 255 : 0;
					$this->result = $request->getResult();

					if ($logRequest)
					{
						$logRequest['status'] = $this->status;
						$logRequest['time'] = round(microtime(true) - $stime, 6);
						$logRequest['headers'] = $this->headers;
						AddMessage2Log(json_encode($logRequest), 'clouds', 0);
					}
				}

				if ($this->status == 204 || $this->status == 200)
				{
					$arStorage = [];
					if (preg_match('#^http://(.*?)(|:\d+)(/.*)$#', $this->headers['X-Storage-Url'], $arStorage))
					{
						$result = $this->headers;
						$result['X-Storage-NoProtoUrl'] = $arStorage[1] . ($arStorage[2] === ':80' ? '' : $arStorage[2]) . $arStorage[3];
						$result['X-Storage-Host'] = $arStorage[1];
						$result['X-Storage-Port'] = $arStorage[2] ? mb_substr($arStorage[2], 1) : 80;
						$result['X-Storage-Urn'] = $arStorage[3];
						$result['X-Storage-Proto'] = '';
					}
				}
			}

			if (is_array($result))
			{
				if ($obCache->StartDataCache())
				{
					$obCache->EndDataCache($result);
				}
			}

			$results[$cache_id] = $result;
		}

		return $result;
	}

	public function SendRequest($settings, $verb, $bucket, $file_name='', $params='', $content=false, $additional_headers=[])
	{
		$arToken = $this->_GetToken($settings['HOST'], $settings['USER'], $settings['KEY']);
		if (!$arToken)
		{
			return false;
		}

		$request = new Bitrix\Main\Web\HttpClient([
			'redirect' => false,
			'streamTimeout' => $this->streamTimeout,
		]);
		if (isset($additional_headers['option-file-result']))
		{
			$request->setOutputStream($additional_headers['option-file-result']);
		}

		$RequestURI = $file_name;

		$ContentType = 'N';
		$request->setHeader('X-Auth-Token', $arToken['X-Auth-Token']);
		foreach ($additional_headers as $key => $value)
		{
			if ($key == 'Content-Type')
			{
				$ContentType = $value;
			}
			else
			{
				$request->setHeader($key, $value);
			}
		}

		$this->status = 0;
		$this->host = $arToken['X-Storage-Host'];
		$this->port = $arToken['X-Storage-Port'];
		$this->verb = $verb;
		$this->url = rtrim($arToken['X-Storage-Url'], '/') . '/' . $bucket . $RequestURI . $params;
		$this->headers = [];
		$this->errno = 0;
		$this->errstr = '';
		$this->result = '';

		$stime = 0;
		$logRequest = false;
		if (defined('BX_CLOUDS_TRACE') && $this->verb !== 'GET' && $this->verb !== 'HEAD')
		{
			$stime = microtime(1);
			$logRequest = [
				'request_id' => md5((string)mt_rand()),
				'portal' => $_SERVER['HTTP_HOST'],
				'verb' => $this->verb,
				'url' => $this->url,
			];
			AddMessage2Log(json_encode($logRequest), 'clouds', 20);
		}

		$request->setHeader('Content-type', $ContentType);
		$request->query($this->verb, $this->url, $content);

		$this->status = $request->getStatus();
		foreach ($request->getHeaders() as $key => $value)
		{
			$this->headers[$key] = is_array($value) ? $value[0] : $value;
		}
		$this->errstr = implode("\n", $request->getError());
		$this->errno = $this->errstr ? 255 : 0;
		$this->result = $request->getResult();

		if ($logRequest)
		{
			$logRequest['status'] = $this->status;
			$logRequest['time'] = round(microtime(true) - $stime, 6);
			$logRequest['headers'] = $this->headers;
			AddMessage2Log(json_encode($logRequest), 'clouds', 0);
		}

		return $request;
	}

	public function CreateBucket($arBucket)
	{
		$this->SendRequest(
			$arBucket['SETTINGS'],
			'PUT',
			$arBucket['BUCKET'],
			'', //filename
			'', //params
			false, //content
			[
				'X-Container-Read' => '.r:*',
				'X-Container-Meta-Web-Listings' => 'false',
				'X-Container-Meta-Type' => 'public',
			]
		);

		return ($this->status == 201)/*Created*/ || ($this->status == 202) /*Accepted*/;
	}

	public function DeleteBucket($arBucket)
	{
		if ($arBucket['PREFIX'])
		{
			//Do not delete bucket if there is some files left
			if (!$this->IsEmptyBucket($arBucket))
			{
				return false;
			}

			//Do not delete bucket if there is some files left in other prefixes
			$arAllBucket = $arBucket;
			$arBucket['PREFIX'] = '';
			if (!$this->IsEmptyBucket($arAllBucket))
			{
				return true;
			}
		}

		$this->SendRequest(
			$arBucket['SETTINGS'],
			'DELETE',
			$arBucket['BUCKET']
		);

		if (
			$this->status == 204/*No Content*/
			|| $this->status == 404/*Not Found*/
		)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	public function IsEmptyBucket($arBucket)
	{
		$this->SendRequest(
			$arBucket['SETTINGS'],
			'GET',
			$arBucket['BUCKET'],
			'',
			'?limit=1&format=xml' . ($arBucket['PREFIX'] ? '&prefix=' . $arBucket['PREFIX'] : '')
		);

		$arXML = false;
		if ($this->status && $this->result)
		{
			$obXML = new CDataXML;
			$text = preg_replace('/<' . '\\?XML.*?\\?' . '>/i', '', $this->result);
			if ($obXML->LoadString($text))
			{
				$arXML = $obXML->GetArray();
			}
		}

		if ($this->status == 404)
		{
			return true;
		}
		elseif (is_array($arXML))
		{
			return
				!isset($arXML['container'])
				|| !is_array($arXML['container'])
				|| !isset($arXML['container']['#'])
				|| !is_array($arXML['container']['#'])
				|| !isset($arXML['container']['#']['object'])
				|| !is_array($arXML['container']['#']['object']);
		}
		else
		{
			return false;
		}
	}

	/**
	 * @param array[string]string $arBucket
	 * @param mixed $arFile
	 * @param bool $encoded
	 * @return string
	*/
	public function GetFileSRC($arBucket, $arFile, $encoded = true)
	{
		$forceHttp = $arBucket['SETTINGS']['FORCE_HTTP'] ?? 'N';
		if ($forceHttp === 'Y')
		{
			$proto = 'http';
		}
		else
		{
			/* @var \Bitrix\Main\HttpRequest $request */
			$request = \Bitrix\Main\Context::getCurrent()->getRequest();
			$proto = $request->isHttps() ? 'https' : 'http';
		}

		if ($arBucket['CNAME'])
		{
			$host = $proto . '://' . $arBucket['CNAME'];
		}
		else
		{
			$arToken = $this->_GetToken(
				$arBucket['SETTINGS']['HOST'],
				$arBucket['SETTINGS']['USER'],
				$arBucket['SETTINGS']['KEY']
			);

			if (is_array($arToken))
			{
				if ($arToken['X-Storage-NoProtoUrl'])
				{
					$host = $proto . '://' . $arToken['X-Storage-NoProtoUrl'] . '/' . $arBucket['BUCKET'];
				}
				else
				{
					$host = $arToken['X-Storage-Url'] . '/' . $arBucket['BUCKET'];
				}
			}
			else
			{
				return '/404.php';
			}
		}

		if (is_array($arFile))
		{
			$URI = ltrim($arFile['SUBDIR'] . '/' . $arFile['FILE_NAME'], '/');
		}
		else
		{
			$URI = ltrim($arFile, '/');
		}

		if ($arBucket['PREFIX'])
		{
			if (mb_substr($URI, 0, mb_strlen($arBucket['PREFIX']) + 1) !== $arBucket['PREFIX'] . '/')
			{
				$URI = $arBucket['PREFIX'] . '/' . $URI;
			}
		}

		if ($encoded)
		{
			return $host . '/' . CCloudUtil::URLEncode($URI, 'UTF-8', true);
		}
		else
		{
			return $host . '/' . $URI;
		}
	}

	public function FileExists($arBucket, $filePath)
	{
		global $APPLICATION;

		if ($arBucket['PREFIX'])
		{
			if (mb_substr($filePath, 0, mb_strlen($arBucket['PREFIX']) + 2) !== '/' . $arBucket['PREFIX'] . '/')
			{
				$filePath = '/' . $arBucket['PREFIX'] . '/' . ltrim($filePath, '/');
			}
		}
		$filePath = CCloudUtil::URLEncode($filePath, 'UTF-8', true);

		$this->SendRequest(
			$arBucket['SETTINGS'],
			'HEAD',
			$arBucket['BUCKET'],
			$filePath
		);

		if ($this->status == 200)
		{
			if (isset($this->headers['Content-Length']) && $this->headers['Content-Length'] > 0)
			{
				return $this->headers['Content-Length'];
			}
			else
			{
				return true;
			}
		}
		elseif ($this->status == 206)
		{
			$APPLICATION->ResetException();
			return true;
		}
		else//if($this->status == 404)
		{
			$APPLICATION->ResetException();
			return false;
		}
	}

	public function FileCopy($arBucket, $arFile, $filePath)
	{
		if ($arBucket['PREFIX'])
		{
			if (mb_substr($filePath, 0, mb_strlen($arBucket['PREFIX']) + 2) !== '/' . $arBucket['PREFIX'] . '/')
			{
				$filePath = '/' . $arBucket['PREFIX'] . '/' . ltrim($filePath, '/');
			}
		}
		$fileSource = CCloudUtil::URLEncode('/' . $arBucket['BUCKET'] . '/' . ($arBucket['PREFIX'] ? $arBucket['PREFIX'] . '/' : '') . ($arFile['SUBDIR'] ? $arFile['SUBDIR'] . '/' : '') . $arFile['FILE_NAME'], 'UTF-8', true);

		$this->SendRequest(
			$arBucket['SETTINGS'],
			'PUT',
			$arBucket['BUCKET'],
			CCloudUtil::URLEncode($filePath, 'UTF-8', true),
			'',
			false,
			[
				'X-Copy-From' => $fileSource,
				'Content-Type' => $arFile['CONTENT_TYPE'],
			]
		);

		if ($this->status == 200 || $this->status == 201)
		{
			return $this->GetFileSRC($arBucket, $filePath);
		}
		else
		{
			return false;
		}
	}

	public function DeleteFile($arBucket, $filePath)
	{
		global $APPLICATION;

		if ($arBucket['PREFIX'])
		{
			if (mb_substr($filePath, 0, mb_strlen($arBucket['PREFIX']) + 2) !== '/' . $arBucket['PREFIX'] . '/')
			{
				$filePath = '/' . $arBucket['PREFIX'] . '/' . ltrim($filePath, '/');
			}
		}
		$filePath = CCloudUtil::URLEncode($filePath, 'UTF-8', true);

		$this->SendRequest(
			$arBucket['SETTINGS'],
			'DELETE',
			$arBucket['BUCKET'],
			$filePath
		);

		//Try to fix space in the path
		if ($this->status == 404 && mb_strpos($filePath, '+') !== false)
		{
			$filePath = str_replace('+', '%20', $filePath);
			$this->SendRequest(
				$arBucket['SETTINGS'],
				'DELETE',
				$arBucket['BUCKET'],
				$filePath
			);
		}

		if ($this->status == 204 || $this->status == 404)
		{
			$APPLICATION->ResetException();
			return true;
		}
		else
		{
			$APPLICATION->ResetException();
			return false;
		}
	}

	public function SaveFile($arBucket, $filePath, $arFile)
	{
		if ($arBucket['PREFIX'])
		{
			if (mb_substr($filePath, 0, mb_strlen($arBucket['PREFIX']) + 2) !== '/' . $arBucket['PREFIX'] . '/')
			{
				$filePath = '/' . $arBucket['PREFIX'] . '/' . ltrim($filePath, '/');
			}
		}
		$filePath = CCloudUtil::URLEncode($filePath, 'UTF-8', true);

		if (array_key_exists('content', $arFile))
		{
			$this->SendRequest(
				$arBucket['SETTINGS'],
				'PUT',
				$arBucket['BUCKET'],
				$filePath,
				'',
				$arFile['content'],
				[
					'Content-Type' => $arFile['type'],
					'Content-Length' => strlen($arFile['content']),
				]
			);
		}
		else
		{
			$this->SendRequest(
				$arBucket['SETTINGS'],
				'PUT',
				$arBucket['BUCKET'],
				$filePath,
				'',
				fopen($arFile['tmp_name'], 'rb'),
				[
					'Content-Type' => $arFile['type'],
					'Content-Length' => filesize($arFile['tmp_name']),
				]
			);
		}

		if ($this->status == 201)
		{
			return true;
		}
		else
		{
			return false;
		}
	}

	public function ListFiles($arBucket, $filePath, $bRecursive = false)
	{
		$result = [
			'dir' => [],
			'file' => [],
			'file_size' => [],
			'file_mtime' => [],
			'file_hash' => [],
			'last_key' => '',
		];

		$filePath = trim($filePath, '/');
		if ($filePath !== '')
		{
			$filePath .= '/';
		}

		if ($arBucket['PREFIX'])
		{
			if (mb_substr($filePath, 0, mb_strlen($arBucket['PREFIX']) + 2) !== '/' . $arBucket['PREFIX'] . '/')
			{
				$filePath = $arBucket['PREFIX'] . '/' . ltrim($filePath, '/');
			}
		}
		$filePath = str_replace(' ', '+', $filePath);

		$marker = '';
		$new_marker = false;
		while (true)
		{
			$this->SendRequest(
				$arBucket['SETTINGS'],
				'GET',
				$arBucket['BUCKET'],
				'/',
				$s = '?format=xml&' . ($bRecursive ? '' : '&delimiter=/') . '&prefix=' . urlencode($filePath) . '&marker=' . urlencode($marker)
			);
			$bFound = false;
			if ($this->result && $this->status == 200)
			{
				$obXML = new CDataXML;
				$text = preg_replace('/<' . '\\?XML.*?\\?' . '>/i', '', $this->result);
				if ($obXML->LoadString($text))
				{
					$arXML = $obXML->GetArray();
					if (
						isset($arXML['container'])
						&& is_array($arXML['container'])
						&& isset($arXML['container']['#'])
						&& is_array($arXML['container']['#'])
						&& !empty($arXML['container']['#'])
					)
					{
						if (
							isset($arXML['container']['#']['object'])
							&& is_array($arXML['container']['#']['object'])
							&& !empty($arXML['container']['#']['object'])
						)
						{
							$bFound = true;
							foreach ($arXML['container']['#']['object'] as $a)
							{
								$new_marker = $a['#']['name'][0]['#'];
								if ($a['#']['content_type'][0]['#'] === 'application/directory')
								{
									$dir_name = trim(mb_substr($a['#']['name'][0]['#'], mb_strlen($filePath)), '/');
									$result['dir'][$dir_name] = true;
								}
								else
								{
									$file_name = mb_substr($a['#']['name'][0]['#'], mb_strlen($filePath));
									if (!in_array($file_name, $result['file'], true))
									{
										$result['file'][] = $file_name;
										$result['file_size'][] = $a['#']['bytes'][0]['#'];
										$result['file_mtime'][] = mb_substr($a['#']['last_modified'][0]['#'], 0, 19);
										$result['file_hash'][] = $a['#']['hash'][0]['#'];
										$result['last_key'] = $file_name;
									}
								}
							}
						}

						if (
							isset($arXML['container']['#']['subdir'])
							&& is_array($arXML['container']['#']['subdir'])
							&& !empty($arXML['container']['#']['subdir'])
						)
						{
							$bFound = true;
							foreach ($arXML['container']['#']['subdir'] as $a)
							{
								$new_marker = $a['@']['name'];
								$dir_name = trim(mb_substr($a['@']['name'], mb_strlen($filePath)), '/');
								$result['dir'][rawurldecode($dir_name)] = true;
							}
						}
					}
				}
			}
			else
			{
				return false;
			}

			if ($new_marker === $marker)
			{
				break;
			}

			if (!$bFound)
			{
				break;
			}

			$marker = $new_marker;
		}

		$result['dir'] = array_keys($result['dir']);

		return $result;
	}

	public function InitiateMultipartUpload($arBucket, &$NS, $filePath, $fileSize, $ContentType)
	{
		$filePath = '/' . trim($filePath, '/');
		if ($arBucket['PREFIX'])
		{
			if (mb_substr($filePath, 0, mb_strlen($arBucket['PREFIX']) + 2) !== '/' . $arBucket['PREFIX'] . '/')
			{
				$filePath = '/' . $arBucket['PREFIX'] . $filePath;
			}
		}

		$NS = [
			'filePath' => $filePath,
			'fileTemp' => CCloudStorage::translit('/tmp' . str_replace(' ', '_', $filePath), '/'),
			'partsCount' => 0,
			'Parts' => [],
			'Content-Type' => $ContentType,
		];

		return true;
	}

	public function GetMinUploadPartSize()
	{
		return 5 * 1024 * 1024; //5MB
	}

	public function UploadPartNo($arBucket, &$NS, $data, $part_no)
	{
		$filePath = $NS['fileTemp'] . '/' . sprintf('%06d', $part_no + 1);
		$filePath = CCloudUtil::URLEncode($filePath, 'UTF-8', true);

		$this->SendRequest(
			$arBucket['SETTINGS'],
			'PUT',
			$arBucket['BUCKET'],
			$filePath,
			'',
			$data
		);

		if ($this->status == 201)
		{
			$NS['partsCount']++;
			$NS['Parts'][$part_no] = $filePath;
			return true;
		}
		else
		{
			return false;
		}
	}

	public function UploadPart($arBucket, &$NS, $data)
	{
		return $this->UploadPartNo($arBucket, $NS, $data, count($NS['Parts']));
	}

	public function CompleteMultipartUpload($arBucket, &$NS)
	{
		$filePath = CCloudUtil::URLEncode($NS['fileTemp'], 'UTF-8', true);

		$this->SendRequest(
			$arBucket['SETTINGS'],
			'PUT',
			$arBucket['BUCKET'],
			$filePath,
			'',
			false,
			[
				'Content-Length' => 0,
				'Content-Type' => $NS['Content-Type'],
				'X-Object-Manifest' => $arBucket['BUCKET'] . $filePath . '/',
			]
		);

		if ($this->status == 201)
		{
			$fileSource = CCloudUtil::URLEncode('/' . $arBucket['BUCKET'] . $NS['fileTemp'], 'UTF-8', true);

			$this->SendRequest(
				$arBucket['SETTINGS'],
				'PUT',
				$arBucket['BUCKET'],
				CCloudUtil::URLEncode($NS['filePath'], 'UTF-8', true),
				'',
				false,
				[
					'Content-Type' => $NS['Content-Type'],
					'X-Copy-From' => $fileSource,
				]
			);

			if (
				$this->status == 201
				|| $this->status == 200
			)
			{
				$result = true;
			}
			else
			{
				$result = false;
			}

			$this->DeleteFile($arBucket, $NS['fileTemp']);
			ksort($NS['Parts']);
			foreach ($NS['Parts'] as $tmpPath)
			{
				$this->DeleteFile($arBucket, $tmpPath);
			}

			return $result;
		}
		else
		{
			//May be delete uploaded tmp file?
			AddMessage2Log($this);
			return false;
		}
	}
}