Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
13 changes: 11 additions & 2 deletions src/Migration/Sources/Appwrite.php
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@
namespace Utopia\Migration\Sources;

use Appwrite\AppwriteException;
use Appwrite\Client;
use Appwrite\Enums\ProjectAuthMethodId;
use Appwrite\Enums\ProjectPolicyId;
use Appwrite\Enums\ProjectProtocolId;
Expand Down Expand Up @@ -85,6 +84,7 @@
use Utopia\Migration\Resources\Storage\File;
use Utopia\Migration\Resources\Templates\EmailTemplate;
use Utopia\Migration\Source;
use Utopia\Migration\Sources\Appwrite\Client;
use Utopia\Migration\Sources\Appwrite\Reader;
use Utopia\Migration\Sources\Appwrite\Reader\API as APIReader;
use Utopia\Migration\Sources\Appwrite\Reader\Database as DatabaseReader;
Expand Down Expand Up @@ -139,7 +139,8 @@ public function __construct(
protected ?UtopiaDatabase $dbForProject = null,
protected array $queries = [],
) {
$this->client = (new Client())
$this->client = new Client();
$this->client
->setEndpoint($endpoint)
->setProject($projectId)
->setKey($key)
Expand Down Expand Up @@ -187,6 +188,14 @@ public static function getName(): string
return 'Appwrite';
}

#[Override]
public function setResolver(?\Closure $resolver): static
{
$this->client->setResolver($resolver);

return parent::setResolver($resolver);
}

#[Override]
public function run(
array $resources,
Expand Down
69 changes: 69 additions & 0 deletions src/Migration/Sources/Appwrite/Client.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
<?php

namespace Utopia\Migration\Sources\Appwrite;

use Appwrite\Client as Base;
use Override;
use Utopia\Client\Adapter\Curl\Client as Curl;
use Utopia\Client\Client as HttpClient;
use Utopia\Migration\Sources\Appwrite\Client\RedirectRefusal;

class Client extends Base
{
private const string PROTOCOLS = 'http,https';

/**
* @var (\Closure(string): array<string>)|null
*/
private ?\Closure $resolver = null;

/**
* The resolver receives the endpoint URL and returns the CURLOPT_RESOLVE
* entries ("host:port:address[,address]") every connection must use. It
* throws to refuse the endpoint. While a resolver is set, requests never go
* through a proxy, which would resolve the host itself.
*
* @param (\Closure(string): array<string>)|null $resolver
*/
public function setResolver(?\Closure $resolver): static
{
$this->resolver = $resolver;
$this->resetHttpClient();

return $this;
}

#[Override]
protected function createHttpClient(bool $followRedirects): HttpClient
{
$options = [
CURLOPT_TIMEOUT_MS => 0,
CURLOPT_USERAGENT => php_uname('s').'-'.php_uname('r').':php-'.phpversion(),
CURLOPT_PROTOCOLS_STR => self::PROTOCOLS,
CURLOPT_REDIR_PROTOCOLS_STR => self::PROTOCOLS,
];

if ($this->resolver !== null) {
$options[CURLOPT_RESOLVE] = ($this->resolver)($this->endpoint);
$options[CURLOPT_PROXY] = '';
}

$adapter = (new Curl(options: $options))->withConnectionReuse();

$client = new HttpClient($followRedirects ? new RedirectRefusal($adapter) : $adapter);

if ($this->selfSigned) {
$client = $client->withSslVerification(false);
}

if ($this->timeout !== null) {
$client = $client->withTimeout($this->timeout);
}

if ($this->connectTimeout !== null) {
$client = $client->withConnectTimeout($this->connectTimeout);
}

return $client;
}
}
35 changes: 35 additions & 0 deletions src/Migration/Sources/Appwrite/Client/RedirectRefusal.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
<?php

namespace Utopia\Migration\Sources\Appwrite\Client;

use Override;
use Psr\Http\Message\RequestInterface;
use Psr\Http\Message\ResponseInterface;
use Utopia\Client\Decorator;
use Utopia\Client\Exception\RequestException;

final class RedirectRefusal extends Decorator
{
#[Override]
public function sendRequest(RequestInterface $request): ResponseInterface
{
return $this->refuse($request, $this->adapter->sendRequest($request));
}

#[Override]
public function stream(RequestInterface $request, callable $sink): ResponseInterface
{
return $this->refuse($request, $this->adapter->stream($request, $sink));
}

private function refuse(RequestInterface $request, ResponseInterface $response): ResponseInterface
{
$status = $response->getStatusCode();

if ($status >= 300 && $status < 400) {
throw new RequestException($request, $status.': Redirects are not followed', $status);
}

return $response;
}
}
70 changes: 65 additions & 5 deletions src/Migration/Target.php
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,8 @@

abstract class Target
{
private const string PROTOCOLS = 'http,https';

/**
* Seconds to wait for a remote to accept the connection.
*/
Expand Down Expand Up @@ -56,6 +58,13 @@ abstract class Target

private ?Client $client = null;

protected bool $followRedirects = false;

/**
* @var (\Closure(string): array<string>)|null
*/
protected ?\Closure $resolver = null;

protected string $rootResourceId = '';

protected string $rootResourceType = '';
Expand All @@ -69,6 +78,29 @@ public function registerCache(Cache &$cache): void
$this->cache = &$cache;
}

public function setFollowRedirects(bool $followRedirects): static
{
$this->followRedirects = $followRedirects;
$this->client = null;

return $this;
}

/**
* The resolver receives each request URL and returns the CURLOPT_RESOLVE
* entries ("host:port:address[,address]") the connection must use. It
* throws to refuse the URL. While a resolver is set, requests never go
* through a proxy, which would resolve the host itself.
*
* @param (\Closure(string): array<string>)|null $resolver
*/
public function setResolver(?\Closure $resolver): static
{
$this->resolver = $resolver;

return $this;
}

/**
* Run Transfer
*
Expand Down Expand Up @@ -129,8 +161,12 @@ protected function call(
default => $requests->form($method, $uri, $params, $headers),
};

$client = $this->resolver === null
? $this->client()
: $this->client(($this->resolver)((string) $request->getUri()));

try {
$response = $this->client()->sendRequest($request);
$response = $client->sendRequest($request);
} catch (ClientExceptionInterface $error) {
throw new \Exception($error->getMessage(), Exception::CODE_INTERNAL, $error);
}
Expand All @@ -146,6 +182,10 @@ protected function call(
$responseBody = \json_decode($responseBody, true);
}

if (! $this->followRedirects && $responseStatus >= 300 && $responseStatus < 400) {
throw new \Exception($responseStatus.': Redirects are not followed', $responseStatus);
}

if ($responseStatus >= 400) {
if (\is_array($responseBody)) {
throw new \Exception(\json_encode($responseBody), $responseStatus);
Expand All @@ -158,16 +198,36 @@ protected function call(
}

/**
* The HTTP client for {@see call()}, built once per target so connections are reused.
* The HTTP client for {@see call()}, built once per target so connections are
* reused. A request pinned to resolved addresses gets a client of its own.
*
* @param array<string>|null $resolve
*/
protected function client(?array $resolve = null): Client
{
if ($resolve === null) {
return $this->client ??= $this->createClient([]);
}

return $this->createClient([
\CURLOPT_RESOLVE => $resolve,
\CURLOPT_PROXY => '',
]);
}

/**
* @param array<int, mixed> $options
*/
protected function client(): Client
private function createClient(array $options): Client
{
return $this->client ??= (new Client(new CurlAdapter(options: [
return (new Client(new CurlAdapter(options: $options + [
\CURLOPT_TIMEOUT_MS => 0,
\CURLOPT_LOW_SPEED_LIMIT => 1,
\CURLOPT_LOW_SPEED_TIME => static::STALL_TIMEOUT,
\CURLOPT_PROTOCOLS_STR => self::PROTOCOLS,
\CURLOPT_REDIR_PROTOCOLS_STR => self::PROTOCOLS,
])))
->withFollowRedirects()
->withFollowRedirects($this->followRedirects)
->withConnectTimeout(static::CONNECT_TIMEOUT);
}

Expand Down
46 changes: 46 additions & 0 deletions tests/Migration/Unit/General/RequestTarget.php
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
<?php

namespace Utopia\Tests\Unit\General;

use Override;
use Utopia\Migration\Target;

final class RequestTarget extends Target
{
public function __construct(string $endpoint)
{
$this->endpoint = $endpoint;
}

#[Override]
public static function getName(): string
{
return 'Request';
}

#[Override]
public static function getSupportedResources(): array
{
return [];
}

#[Override]
public function run(array $resources, callable $callback, string $rootResourceId = ''): void
{
}

#[Override]
public function report(array $resources = [], array $resourceIds = []): array
{
return [];
}

/**
* @param array<string, mixed> $params
* @return array<mixed>|string
*/
public function request(string $method, string $path, array $params = []): array|string
{
return $this->call($method, $path, [], $params);
}
}
Loading
Loading