From 127520b96113ef3cf9e89ea60faf52edf87d879d Mon Sep 17 00:00:00 2001 From: Torsten Dittmann Date: Thu, 21 May 2026 17:13:54 +0400 Subject: [PATCH] feat: support concurrent chunk uploads --- CHANGELOG.md | 8 - docs/account.md | 63 ++++++ .../account/create-o-auth-2-session.md | 20 ++ docs/examples/account/create-push-target.md | 18 ++ docs/examples/account/delete-push-target.md | 16 ++ docs/examples/account/update-push-target.md | 17 ++ docs/examples/avatars/get-screenshot.md | 2 +- docs/project.md | 20 +- src/Appwrite/Enums/BuildRuntime.php | 27 --- src/Appwrite/Enums/Runtime.php | 27 --- src/Appwrite/Services/Account.php | 184 +++++++++++++++++ src/Appwrite/Services/Functions.php | 185 +++++++++++++++-- src/Appwrite/Services/Sites.php | 185 +++++++++++++++-- src/Appwrite/Services/Storage.php | 186 ++++++++++++++++-- tests/Appwrite/Services/AccountTest.php | 80 ++++++++ tests/Appwrite/Services/ProjectTest.php | 4 +- 16 files changed, 916 insertions(+), 126 deletions(-) create mode 100644 docs/examples/account/create-o-auth-2-session.md create mode 100644 docs/examples/account/create-push-target.md create mode 100644 docs/examples/account/delete-push-target.md create mode 100644 docs/examples/account/update-push-target.md diff --git a/CHANGELOG.md b/CHANGELOG.md index 07f15349..04807029 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,13 +1,5 @@ # Change Log -## 24.1.0 - -* Added `sizeActual` property to `File` model for actual stored size after compression/encryption -* Updated `BillingLimits` properties to be nullable to match the server's sparse "limits crossed" response -* Updated `Project.billingLimits` to be nullable -* Updated advisor example docs to use API key authentication -* Removed orphaned `Prompt` enum (already unused; superseded by `ProjectOAuth2GooglePrompt` in 24.0.0) - ## 24.0.0 * Breaking: Renamed `AuthMethod` enum to `ProjectAuthMethodId` diff --git a/docs/account.md b/docs/account.md index 9b74014c..57de12d6 100644 --- a/docs/account.md +++ b/docs/account.md @@ -440,6 +440,27 @@ PUT https://cloud.appwrite.io/v1/account/sessions/magic-url | secret | string | Valid verification token. | | +```http request +GET https://cloud.appwrite.io/v1/account/sessions/oauth2/{provider} +``` + +** Allow the user to login to their account using the OAuth2 provider of their choice. Each OAuth2 provider should be enabled from the Appwrite console first. Use the success and failure arguments to provide a redirect URL's back to your app when login is completed. + +If there is already an active session, the new session will be attached to the logged-in account. If there are no active sessions, the server will attempt to look for a user with the same email address as the email received from the OAuth2 provider and attach the new session to the existing user. If no matching user is found - the server will create a new user. + +A user is limited to 10 active sessions at a time by default. [Learn more about session limits](https://appwrite.io/docs/authentication-security#limits). + ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| provider | string | **Required** OAuth2 Provider. Currently, supported providers are: amazon, apple, auth0, authentik, autodesk, bitbucket, bitly, box, dailymotion, discord, disqus, dropbox, etsy, facebook, figma, fusionauth, github, gitlab, google, keycloak, kick, linkedin, microsoft, notion, oidc, okta, paypal, paypalSandbox, podio, salesforce, slack, spotify, stripe, tradeshift, tradeshiftBox, twitch, wordpress, x, yahoo, yammer, yandex, zoho, zoom. | | +| success | string | URL to redirect back to your app after a successful login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. | | +| failure | string | URL to redirect back to your app after a failed login attempt. Only URLs from hostnames in your project's platform list are allowed. This requirement helps to prevent an [open redirect](https://cheatsheetseries.owasp.org/cheatsheets/Unvalidated_Redirects_and_Forwards_Cheat_Sheet.html) attack against your project API. | | +| scopes | array | A list of custom OAuth2 scopes. Check each provider internal docs for a list of supported scopes. Maximum of 100 scopes are allowed, each 4096 characters long. | [] | + + ```http request PUT https://cloud.appwrite.io/v1/account/sessions/phone ``` @@ -514,6 +535,48 @@ PATCH https://cloud.appwrite.io/v1/account/status ** Block the currently logged in user account. Behind the scene, the user record is not deleted but permanently blocked from any access. To completely delete a user, use the Users API instead. ** +```http request +POST https://cloud.appwrite.io/v1/account/targets/push +``` + +** Use this endpoint to register a device for push notifications. Provide a target ID (custom or generated using ID.unique()), a device identifier (usually a device token), and optionally specify which provider should send notifications to this target. The target is automatically linked to the current session and includes device information like brand and model. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| targetId | string | Target ID. Choose a custom ID or generate a random ID with `ID.unique()`. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can't start with a special char. Max length is 36 chars. | | +| identifier | string | The target identifier (token, email, phone etc.) | | +| providerId | string | Provider ID. Message will be sent to this target from the specified provider ID. If no provider ID is set the first setup provider will be used. | | + + +```http request +PUT https://cloud.appwrite.io/v1/account/targets/{targetId}/push +``` + +** Update the currently logged in user's push notification target. You can modify the target's identifier (device token) and provider ID (token, email, phone etc.). The target must exist and belong to the current user. If you change the provider ID, notifications will be sent through the new messaging provider instead. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| targetId | string | **Required** Target ID. | | +| identifier | string | The target identifier (token, email, phone etc.) | | + + +```http request +DELETE https://cloud.appwrite.io/v1/account/targets/{targetId}/push +``` + +** Delete a push notification target for the currently logged in user. After deletion, the device will no longer receive push notifications. The target must exist and belong to the current user. ** + +### Parameters + +| Field Name | Type | Description | Default | +| --- | --- | --- | --- | +| targetId | string | **Required** Target ID. | | + + ```http request POST https://cloud.appwrite.io/v1/account/tokens/email ``` diff --git a/docs/examples/account/create-o-auth-2-session.md b/docs/examples/account/create-o-auth-2-session.md new file mode 100644 index 00000000..ee2e5b52 --- /dev/null +++ b/docs/examples/account/create-o-auth-2-session.md @@ -0,0 +1,20 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setSession(''); // The user session to authenticate with + +$account = new Account($client); + +$result = $account->createOAuth2Session( + provider: OAuthProvider::AMAZON(), + success: 'https://example.com', // optional + failure: 'https://example.com', // optional + scopes: [] // optional +);``` diff --git a/docs/examples/account/create-push-target.md b/docs/examples/account/create-push-target.md new file mode 100644 index 00000000..8eaa55a7 --- /dev/null +++ b/docs/examples/account/create-push-target.md @@ -0,0 +1,18 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setSession(''); // The user session to authenticate with + +$account = new Account($client); + +$result = $account->createPushTarget( + targetId: '', + identifier: '', + providerId: '' // optional +);``` diff --git a/docs/examples/account/delete-push-target.md b/docs/examples/account/delete-push-target.md new file mode 100644 index 00000000..206e0d97 --- /dev/null +++ b/docs/examples/account/delete-push-target.md @@ -0,0 +1,16 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setSession(''); // The user session to authenticate with + +$account = new Account($client); + +$result = $account->deletePushTarget( + targetId: '' +);``` diff --git a/docs/examples/account/update-push-target.md b/docs/examples/account/update-push-target.md new file mode 100644 index 00000000..a3677b99 --- /dev/null +++ b/docs/examples/account/update-push-target.md @@ -0,0 +1,17 @@ +```php +setEndpoint('https://.cloud.appwrite.io/v1') // Your API Endpoint + ->setProject('') // Your project ID + ->setSession(''); // The user session to authenticate with + +$account = new Account($client); + +$result = $account->updatePushTarget( + targetId: '', + identifier: '' +);``` diff --git a/docs/examples/avatars/get-screenshot.md b/docs/examples/avatars/get-screenshot.md index 9db8b3cd..8d9f9575 100644 --- a/docs/examples/avatars/get-screenshot.md +++ b/docs/examples/avatars/get-screenshot.md @@ -28,7 +28,7 @@ $result = $avatars->getScreenshot( userAgent: 'Mozilla/5.0 (iPhone; CPU iPhone OS 14_0 like Mac OS X) AppleWebKit/605.1.15', // optional fullpage: true, // optional locale: 'en-US', // optional - timezone: Timezone::AMERICANEWYORK(), // optional + timezone: Timezone::AFRICAABIDJAN(), // optional latitude: 37.7749, // optional longitude: -122.4194, // optional accuracy: 100, // optional diff --git a/docs/project.md b/docs/project.md index 7ef92d74..cfb294a3 100644 --- a/docs/project.md +++ b/docs/project.md @@ -501,7 +501,7 @@ PATCH https://cloud.appwrite.io/v1/project/oauth2/google | Field Name | Type | Description | Default | | --- | --- | --- | --- | | clientId | string | 'Client ID' of Google OAuth2 app. For example: 120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com | | -| clientSecret | string | 'Client Secret' of Google OAuth2 app. For example: example-google-client-secret | | +| clientSecret | string | 'Client Secret' of Google OAuth2 app. For example: GOCSPX-2k8gsR0000000000000000VNahJj | | | prompt | array | Array of Google OAuth2 prompt values. If "none" is included, it must be the only element. "none" means: don't display any authentication or consent screens. Must not be specified with other values. "consent" means: prompt the user for consent. "select_account" means: prompt the user to select an account. | | | enabled | boolean | OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid. | | @@ -549,7 +549,7 @@ PATCH https://cloud.appwrite.io/v1/project/oauth2/linkedin | Field Name | Type | Description | Default | | --- | --- | --- | --- | | clientId | string | 'Client ID' of Linkedin OAuth2 app. For example: 770000000000dv | | -| primaryClientSecret | string | 'Primary Client Secret or Secondary Client Secret' of Linkedin OAuth2 app. For example: example-linkedin-client-secret | | +| primaryClientSecret | string | 'Primary Client Secret or Secondary Client Secret' of Linkedin OAuth2 app. For example: WPL_AP1.2Bf0000000000000./HtlYw== | | | enabled | boolean | OAuth2 sign-in method status. Set to true to enable new session creation. Setting to true will trigger end-to-end credentials validation, and will throw if the credentials are invalid. | | @@ -1292,12 +1292,12 @@ PATCH https://cloud.appwrite.io/v1/project/smtp | --- | --- | --- | --- | | host | string | SMTP server hostname (domain) | | | port | integer | SMTP server port | | -| username | string | SMTP server username. Leave empty for no authorization. | | -| password | string | SMTP server password. Leave empty for no authorization. This property is stored securely and cannot be read in future (write-only). | | -| senderEmail | string | Email address shown in inbox as the sender of the email. | | -| senderName | string | Name shown in inbox as the sender of the email. | | -| replyToEmail | string | Email used when user replies to the email. | | -| replyToName | string | Name used when user replies to the email. | | +| username | string | SMTP server username. Pass an empty string to clear a previously set value. | | +| password | string | SMTP server password. Pass an empty string to clear a previously set value. This property is stored securely and cannot be read in future (write-only). | | +| senderEmail | string | Email address shown in inbox as the sender of the email. Pass an empty string to clear a previously set value. | | +| senderName | string | Name shown in inbox as the sender of the email. Pass an empty string to clear a previously set value. | | +| replyToEmail | string | Email used when user replies to the email. Pass an empty string to clear a previously set value. | | +| replyToName | string | Name used when user replies to the email. Pass an empty string to clear a previously set value. | | | secure | string | Configures if communication with SMTP server is encrypted. Allowed values are: tls, ssl. Leave empty for no encryption. | | | enabled | boolean | Enable or disable custom SMTP. Custom SMTP is useful for branding purposes, but also allows use of custom email templates. | | @@ -1344,8 +1344,8 @@ PATCH https://cloud.appwrite.io/v1/project/templates/email | subject | string | Subject of the email template. Can be up to 255 characters. | | | message | string | Plain or HTML body of the email template message. Can be up to 10MB of content. | | | senderName | string | Name of the email sender. | | -| senderEmail | string | Email of the sender. | | -| replyToEmail | string | Reply to email. | | +| senderEmail | string | Email of the sender. Pass an empty string to clear a previously set value. | | +| replyToEmail | string | Reply to email. Pass an empty string to clear a previously set value. | | | replyToName | string | Reply to name. | | diff --git a/src/Appwrite/Enums/BuildRuntime.php b/src/Appwrite/Enums/BuildRuntime.php index 7b5369c1..901d5f98 100644 --- a/src/Appwrite/Enums/BuildRuntime.php +++ b/src/Appwrite/Enums/BuildRuntime.php @@ -37,9 +37,6 @@ class BuildRuntime implements JsonSerializable private static BuildRuntime $PYTHONML311; private static BuildRuntime $PYTHONML312; private static BuildRuntime $PYTHONML313; - private static BuildRuntime $DENO121; - private static BuildRuntime $DENO124; - private static BuildRuntime $DENO135; private static BuildRuntime $DENO140; private static BuildRuntime $DENO146; private static BuildRuntime $DENO20; @@ -333,27 +330,6 @@ public static function PYTHONML313(): BuildRuntime } return self::$PYTHONML313; } - public static function DENO121(): BuildRuntime - { - if (!isset(self::$DENO121)) { - self::$DENO121 = new BuildRuntime('deno-1.21'); - } - return self::$DENO121; - } - public static function DENO124(): BuildRuntime - { - if (!isset(self::$DENO124)) { - self::$DENO124 = new BuildRuntime('deno-1.24'); - } - return self::$DENO124; - } - public static function DENO135(): BuildRuntime - { - if (!isset(self::$DENO135)) { - self::$DENO135 = new BuildRuntime('deno-1.35'); - } - return self::$DENO135; - } public static function DENO140(): BuildRuntime { if (!isset(self::$DENO140)) { @@ -795,9 +771,6 @@ public static function from(string $value): self 'python-ml-3.11' => self::PYTHONML311(), 'python-ml-3.12' => self::PYTHONML312(), 'python-ml-3.13' => self::PYTHONML313(), - 'deno-1.21' => self::DENO121(), - 'deno-1.24' => self::DENO124(), - 'deno-1.35' => self::DENO135(), 'deno-1.40' => self::DENO140(), 'deno-1.46' => self::DENO146(), 'deno-2.0' => self::DENO20(), diff --git a/src/Appwrite/Enums/Runtime.php b/src/Appwrite/Enums/Runtime.php index 667d0d3f..5bf14b0c 100644 --- a/src/Appwrite/Enums/Runtime.php +++ b/src/Appwrite/Enums/Runtime.php @@ -37,9 +37,6 @@ class Runtime implements JsonSerializable private static Runtime $PYTHONML311; private static Runtime $PYTHONML312; private static Runtime $PYTHONML313; - private static Runtime $DENO121; - private static Runtime $DENO124; - private static Runtime $DENO135; private static Runtime $DENO140; private static Runtime $DENO146; private static Runtime $DENO20; @@ -333,27 +330,6 @@ public static function PYTHONML313(): Runtime } return self::$PYTHONML313; } - public static function DENO121(): Runtime - { - if (!isset(self::$DENO121)) { - self::$DENO121 = new Runtime('deno-1.21'); - } - return self::$DENO121; - } - public static function DENO124(): Runtime - { - if (!isset(self::$DENO124)) { - self::$DENO124 = new Runtime('deno-1.24'); - } - return self::$DENO124; - } - public static function DENO135(): Runtime - { - if (!isset(self::$DENO135)) { - self::$DENO135 = new Runtime('deno-1.35'); - } - return self::$DENO135; - } public static function DENO140(): Runtime { if (!isset(self::$DENO140)) { @@ -795,9 +771,6 @@ public static function from(string $value): self 'python-ml-3.11' => self::PYTHONML311(), 'python-ml-3.12' => self::PYTHONML312(), 'python-ml-3.13' => self::PYTHONML313(), - 'deno-1.21' => self::DENO121(), - 'deno-1.24' => self::DENO124(), - 'deno-1.35' => self::DENO135(), 'deno-1.40' => self::DENO140(), 'deno-1.46' => self::DENO146(), 'deno-2.0' => self::DENO20(), diff --git a/src/Appwrite/Services/Account.php b/src/Appwrite/Services/Account.php index 42ceabbb..5ae34903 100644 --- a/src/Appwrite/Services/Account.php +++ b/src/Appwrite/Services/Account.php @@ -1156,6 +1156,67 @@ public function updateMagicURLSession(string $userId, string $secret): \Appwrite } + /** + * Allow the user to login to their account using the OAuth2 provider of their + * choice. Each OAuth2 provider should be enabled from the Appwrite console + * first. Use the success and failure arguments to provide a redirect URL's + * back to your app when login is completed. + * + * If there is already an active session, the new session will be attached to + * the logged-in account. If there are no active sessions, the server will + * attempt to look for a user with the same email address as the email + * received from the OAuth2 provider and attach the new session to the + * existing user. If no matching user is found - the server will create a new + * user. + * + * A user is limited to 10 active sessions at a time by default. [Learn more + * about session + * limits](https://appwrite.io/docs/authentication-security#limits). + * + * + * @param OAuthProvider $provider + * @param ?string $success + * @param ?string $failure + * @param ?array $scopes + * @throws AppwriteException + * @return string + */ + public function createOAuth2Session(OAuthProvider $provider, ?string $success = null, ?string $failure = null, ?array $scopes = null): string + { + $apiPath = str_replace( + ['{provider}'], + [$provider], + '/account/sessions/oauth2/{provider}' + ); + + $apiParams = []; + $apiParams['provider'] = $provider; + + if (!is_null($success)) { + $apiParams['success'] = $success; + } + + if (!is_null($failure)) { + $apiParams['failure'] = $failure; + } + + if (!is_null($scopes)) { + $apiParams['scopes'] = $scopes; + } + + $apiHeaders = []; + + $response = $this->client->call( + Client::METHOD_GET, + $apiPath, + $apiHeaders, + $apiParams, 'location' + ); + + return $response; + + } + /** * Use this endpoint to create a session from token. Provide the **userId** * and **secret** parameters from the successful response of authentication @@ -1385,6 +1446,129 @@ public function updateStatus(): \Appwrite\Models\User } + /** + * Use this endpoint to register a device for push notifications. Provide a + * target ID (custom or generated using ID.unique()), a device identifier + * (usually a device token), and optionally specify which provider should send + * notifications to this target. The target is automatically linked to the + * current session and includes device information like brand and model. + * + * @param string $targetId + * @param string $identifier + * @param ?string $providerId + * @throws AppwriteException + * @return \Appwrite\Models\Target + */ + public function createPushTarget(string $targetId, string $identifier, ?string $providerId = null): \Appwrite\Models\Target + { + $apiPath = str_replace( + [], + [], + '/account/targets/push' + ); + + $apiParams = []; + $apiParams['targetId'] = $targetId; + $apiParams['identifier'] = $identifier; + + if (!is_null($providerId)) { + $apiParams['providerId'] = $providerId; + } + + $apiHeaders = []; + $apiHeaders['content-type'] = 'application/json'; + + $response = $this->client->call( + Client::METHOD_POST, + $apiPath, + $apiHeaders, + $apiParams + ); + + if (!is_array($response)) { + throw new \UnexpectedValueException('Expected array response when hydrating a response model.'); + } + + return \Appwrite\Models\Target::from($response); + + } + + /** + * Update the currently logged in user's push notification target. You can + * modify the target's identifier (device token) and provider ID (token, + * email, phone etc.). The target must exist and belong to the current user. + * If you change the provider ID, notifications will be sent through the new + * messaging provider instead. + * + * @param string $targetId + * @param string $identifier + * @throws AppwriteException + * @return \Appwrite\Models\Target + */ + public function updatePushTarget(string $targetId, string $identifier): \Appwrite\Models\Target + { + $apiPath = str_replace( + ['{targetId}'], + [$targetId], + '/account/targets/{targetId}/push' + ); + + $apiParams = []; + $apiParams['targetId'] = $targetId; + $apiParams['identifier'] = $identifier; + + $apiHeaders = []; + $apiHeaders['content-type'] = 'application/json'; + + $response = $this->client->call( + Client::METHOD_PUT, + $apiPath, + $apiHeaders, + $apiParams + ); + + if (!is_array($response)) { + throw new \UnexpectedValueException('Expected array response when hydrating a response model.'); + } + + return \Appwrite\Models\Target::from($response); + + } + + /** + * Delete a push notification target for the currently logged in user. After + * deletion, the device will no longer receive push notifications. The target + * must exist and belong to the current user. + * + * @param string $targetId + * @throws AppwriteException + * @return string + */ + public function deletePushTarget(string $targetId): string + { + $apiPath = str_replace( + ['{targetId}'], + [$targetId], + '/account/targets/{targetId}/push' + ); + + $apiParams = []; + $apiParams['targetId'] = $targetId; + + $apiHeaders = []; + $apiHeaders['content-type'] = 'application/json'; + + $response = $this->client->call( + Client::METHOD_DELETE, + $apiPath, + $apiHeaders, + $apiParams + ); + + return $response; + + } + /** * Sends the user an email with a secret key for creating a session. If the * email address has never been used, a **new account is created** using the diff --git a/src/Appwrite/Services/Functions.php b/src/Appwrite/Services/Functions.php index 7d44d391..344b9eca 100644 --- a/src/Appwrite/Services/Functions.php +++ b/src/Appwrite/Services/Functions.php @@ -635,35 +635,186 @@ public function createDeployment(string $functionId, InputFile $code, bool $acti $handle = @fopen($code->getPath(), "rb"); } + $uploadId = ''; + $totalChunks = (int) ceil($size / Client::CHUNK_SIZE); + $chunks = []; $start = $counter * Client::CHUNK_SIZE; while ($start < $size) { - $chunk = ''; + $chunks[] = [ + 'index' => $counter, + 'start' => $start, + 'end' => min($start + Client::CHUNK_SIZE, $size), + ]; + $counter++; + $start += Client::CHUNK_SIZE; + } + + $readChunk = function(int $start, int $end) use ($handle, $code) { if(!empty($handle)) { fseek($handle, $start); - $chunk = @fread($handle, Client::CHUNK_SIZE); - } else { - $chunk = substr($code->getData(), $start, Client::CHUNK_SIZE); + return @fread($handle, $end - $start); } - $apiParams['code'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($chunk), $mimeType, $postedName); - $apiHeaders['content-range'] = 'bytes ' . ($counter * Client::CHUNK_SIZE) . '-' . min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE) - 1), $size - 1) . '/' . $size; - if(!empty($id)) { - $apiHeaders['x-appwrite-id'] = $id; + + return substr($code->getData(), $start, $end - $start); + }; + + $uploadChunk = function(array $chunk, string $currentUploadId = '') use ($readChunk, $apiPath, $apiHeaders, $apiParams, $mimeType, $postedName, $size) { + $chunkParams = $apiParams; + $chunkHeaders = $apiHeaders; + $data = $readChunk($chunk['start'], $chunk['end']); + $chunkParams['code'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($data), $mimeType, $postedName); + $chunkHeaders['content-range'] = 'bytes ' . $chunk['start'] . '-' . ($chunk['end'] - 1) . '/' . $size; + if(!empty($currentUploadId)) { + $chunkHeaders['x-appwrite-id'] = $currentUploadId; } - $response = $this->client->call(Client::METHOD_POST, $apiPath, $apiHeaders, $apiParams); - $counter++; - $start += Client::CHUNK_SIZE; - if(empty($id)) { - $id = $response['$id']; + + return $this->client->call(Client::METHOD_POST, $apiPath, $chunkHeaders, $chunkParams); + }; + + $isUploadComplete = function($chunkResponse) use ($totalChunks): bool { + if(!is_array($chunkResponse) || !isset($chunkResponse['chunksUploaded'])) { + return false; + } + + return (int) $chunkResponse['chunksUploaded'] >= (int) ($chunkResponse['chunksTotal'] ?? $totalChunks); + }; + + if (!empty($chunks)) { + $response = $uploadChunk($chunks[0], $uploadId); + if(empty($uploadId)) { + $uploadId = $response['$id']; } + $completedCount = $chunks[0]['index'] + 1; + $uploadedSize = $chunks[0]['end']; if($onProgress !== null) { $onProgress([ '$id' => $response['$id'], - 'progress' => min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE)), $size) / $size * 100, - 'sizeUploaded' => min($counter * Client::CHUNK_SIZE), - 'chunksTotal' => $response['chunksTotal'], - 'chunksUploaded' => $response['chunksUploaded'], + 'progress' => $uploadedSize / $size * 100, + 'sizeUploaded' => $uploadedSize, + 'chunksTotal' => $totalChunks, + 'chunksUploaded' => $completedCount, ]); } + + $remainingChunks = array_slice($chunks, 1); + $clientConfig = \Closure::bind(function() { + if (property_exists($this, 'key') && $this->key !== null) { + $this->headers['authorization'] = $this->getAuthorization(); + } + + return [$this->endpoint, $this->headers, $this->selfSigned, $this->timeout, $this->connectTimeout]; + }, $this->client, Client::class); + $flattenParams = \Closure::bind(function(array $params): array { + return $this->flatten($params); + }, $this->client, Client::class); + [$endpoint, $globalHeaders, $selfSigned, $timeout, $connectTimeout] = $clientConfig(); + $responseHeaders = []; + + $makeHandle = function(array $chunk) use ($readChunk, $apiPath, $apiHeaders, $apiParams, $mimeType, $postedName, $size, $uploadId, $endpoint, $globalHeaders, $selfSigned, $timeout, $connectTimeout, $flattenParams, &$responseHeaders) { + $chunkParams = $apiParams; + $chunkHeaders = array_merge($globalHeaders, $apiHeaders); + $data = $readChunk($chunk['start'], $chunk['end']); + $chunkParams['code'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($data), $mimeType, $postedName); + $chunkHeaders['content-range'] = 'bytes ' . $chunk['start'] . '-' . ($chunk['end'] - 1) . '/' . $size; + if(!empty($uploadId)) { + $chunkHeaders['x-appwrite-id'] = $uploadId; + } + + $headers = []; + foreach ($chunkHeaders as $key => $value) { + $headers[] = $key . ':' . $value; + } + + $ch = curl_init($endpoint . $apiPath); + $responseHeaders[spl_object_id($ch)] = []; + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, Client::METHOD_POST); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_USERAGENT, php_uname('s') . '-' . php_uname('r') . ':php-' . phpversion()); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $flattenParams($chunkParams)); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$responseHeaders) { + $length = strlen($header); + $header = explode(':', strtolower($header), 2); + if (count($header) >= 2) { + $responseHeaders[spl_object_id($curl)][strtolower(trim($header[0]))] = trim($header[1]); + } + + return $length; + }); + if($selfSigned) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + } + if($timeout !== null) { + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + } + if($connectTimeout !== null) { + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $connectTimeout); + } + + return $ch; + }; + + $nextChunk = 0; + while ($nextChunk < count($remainingChunks)) { + $multiHandle = curl_multi_init(); + $handles = []; + for ($i = 0; $i < 8 && $nextChunk < count($remainingChunks); $i++, $nextChunk++) { + $chunk = $remainingChunks[$nextChunk]; + $ch = $makeHandle($chunk); + $handles[spl_object_id($ch)] = ['handle' => $ch, 'chunk' => $chunk]; + curl_multi_add_handle($multiHandle, $ch); + } + + do { + $status = curl_multi_exec($multiHandle, $active); + if ($active) { + curl_multi_select($multiHandle); + } + } while ($active && $status == CURLM_OK); + + foreach ($handles as $handleInfo) { + $ch = $handleInfo['handle']; + $body = curl_multi_getcontent($ch); + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = $responseHeaders[spl_object_id($ch)]['content-type'] ?? ''; + + if (curl_errno($ch)) { + throw new AppwriteException(curl_error($ch), $statusCode, '', $body); + } + + $chunkResponse = str_starts_with($contentType, 'application/json') ? json_decode($body, true) : $body; + + if($statusCode >= 400) { + if(is_array($chunkResponse)) { + throw new AppwriteException($chunkResponse['message'], $statusCode, $chunkResponse['type'] ?? '', json_encode($chunkResponse)); + } + + throw new AppwriteException($chunkResponse, $statusCode, '', $chunkResponse); + } + + $completedCount++; + $uploadedSize += $handleInfo['chunk']['end'] - $handleInfo['chunk']['start']; + if($isUploadComplete($chunkResponse)) { + $response = $chunkResponse; + } + if($onProgress !== null) { + $onProgress([ + '$id' => $uploadId, + 'progress' => $uploadedSize / $size * 100, + 'sizeUploaded' => $uploadedSize, + 'chunksTotal' => $totalChunks, + 'chunksUploaded' => $completedCount, + ]); + } + + curl_multi_remove_handle($multiHandle, $ch); + } + + curl_multi_close($multiHandle); + } + } if(!empty($handle)) { @fclose($handle); diff --git a/src/Appwrite/Services/Sites.php b/src/Appwrite/Services/Sites.php index 660a1bbc..253bb3ee 100644 --- a/src/Appwrite/Services/Sites.php +++ b/src/Appwrite/Services/Sites.php @@ -641,35 +641,186 @@ public function createDeployment(string $siteId, InputFile $code, ?string $insta $handle = @fopen($code->getPath(), "rb"); } + $uploadId = ''; + $totalChunks = (int) ceil($size / Client::CHUNK_SIZE); + $chunks = []; $start = $counter * Client::CHUNK_SIZE; while ($start < $size) { - $chunk = ''; + $chunks[] = [ + 'index' => $counter, + 'start' => $start, + 'end' => min($start + Client::CHUNK_SIZE, $size), + ]; + $counter++; + $start += Client::CHUNK_SIZE; + } + + $readChunk = function(int $start, int $end) use ($handle, $code) { if(!empty($handle)) { fseek($handle, $start); - $chunk = @fread($handle, Client::CHUNK_SIZE); - } else { - $chunk = substr($code->getData(), $start, Client::CHUNK_SIZE); + return @fread($handle, $end - $start); } - $apiParams['code'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($chunk), $mimeType, $postedName); - $apiHeaders['content-range'] = 'bytes ' . ($counter * Client::CHUNK_SIZE) . '-' . min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE) - 1), $size - 1) . '/' . $size; - if(!empty($id)) { - $apiHeaders['x-appwrite-id'] = $id; + + return substr($code->getData(), $start, $end - $start); + }; + + $uploadChunk = function(array $chunk, string $currentUploadId = '') use ($readChunk, $apiPath, $apiHeaders, $apiParams, $mimeType, $postedName, $size) { + $chunkParams = $apiParams; + $chunkHeaders = $apiHeaders; + $data = $readChunk($chunk['start'], $chunk['end']); + $chunkParams['code'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($data), $mimeType, $postedName); + $chunkHeaders['content-range'] = 'bytes ' . $chunk['start'] . '-' . ($chunk['end'] - 1) . '/' . $size; + if(!empty($currentUploadId)) { + $chunkHeaders['x-appwrite-id'] = $currentUploadId; } - $response = $this->client->call(Client::METHOD_POST, $apiPath, $apiHeaders, $apiParams); - $counter++; - $start += Client::CHUNK_SIZE; - if(empty($id)) { - $id = $response['$id']; + + return $this->client->call(Client::METHOD_POST, $apiPath, $chunkHeaders, $chunkParams); + }; + + $isUploadComplete = function($chunkResponse) use ($totalChunks): bool { + if(!is_array($chunkResponse) || !isset($chunkResponse['chunksUploaded'])) { + return false; + } + + return (int) $chunkResponse['chunksUploaded'] >= (int) ($chunkResponse['chunksTotal'] ?? $totalChunks); + }; + + if (!empty($chunks)) { + $response = $uploadChunk($chunks[0], $uploadId); + if(empty($uploadId)) { + $uploadId = $response['$id']; } + $completedCount = $chunks[0]['index'] + 1; + $uploadedSize = $chunks[0]['end']; if($onProgress !== null) { $onProgress([ '$id' => $response['$id'], - 'progress' => min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE)), $size) / $size * 100, - 'sizeUploaded' => min($counter * Client::CHUNK_SIZE), - 'chunksTotal' => $response['chunksTotal'], - 'chunksUploaded' => $response['chunksUploaded'], + 'progress' => $uploadedSize / $size * 100, + 'sizeUploaded' => $uploadedSize, + 'chunksTotal' => $totalChunks, + 'chunksUploaded' => $completedCount, ]); } + + $remainingChunks = array_slice($chunks, 1); + $clientConfig = \Closure::bind(function() { + if (property_exists($this, 'key') && $this->key !== null) { + $this->headers['authorization'] = $this->getAuthorization(); + } + + return [$this->endpoint, $this->headers, $this->selfSigned, $this->timeout, $this->connectTimeout]; + }, $this->client, Client::class); + $flattenParams = \Closure::bind(function(array $params): array { + return $this->flatten($params); + }, $this->client, Client::class); + [$endpoint, $globalHeaders, $selfSigned, $timeout, $connectTimeout] = $clientConfig(); + $responseHeaders = []; + + $makeHandle = function(array $chunk) use ($readChunk, $apiPath, $apiHeaders, $apiParams, $mimeType, $postedName, $size, $uploadId, $endpoint, $globalHeaders, $selfSigned, $timeout, $connectTimeout, $flattenParams, &$responseHeaders) { + $chunkParams = $apiParams; + $chunkHeaders = array_merge($globalHeaders, $apiHeaders); + $data = $readChunk($chunk['start'], $chunk['end']); + $chunkParams['code'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($data), $mimeType, $postedName); + $chunkHeaders['content-range'] = 'bytes ' . $chunk['start'] . '-' . ($chunk['end'] - 1) . '/' . $size; + if(!empty($uploadId)) { + $chunkHeaders['x-appwrite-id'] = $uploadId; + } + + $headers = []; + foreach ($chunkHeaders as $key => $value) { + $headers[] = $key . ':' . $value; + } + + $ch = curl_init($endpoint . $apiPath); + $responseHeaders[spl_object_id($ch)] = []; + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, Client::METHOD_POST); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_USERAGENT, php_uname('s') . '-' . php_uname('r') . ':php-' . phpversion()); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $flattenParams($chunkParams)); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$responseHeaders) { + $length = strlen($header); + $header = explode(':', strtolower($header), 2); + if (count($header) >= 2) { + $responseHeaders[spl_object_id($curl)][strtolower(trim($header[0]))] = trim($header[1]); + } + + return $length; + }); + if($selfSigned) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + } + if($timeout !== null) { + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + } + if($connectTimeout !== null) { + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $connectTimeout); + } + + return $ch; + }; + + $nextChunk = 0; + while ($nextChunk < count($remainingChunks)) { + $multiHandle = curl_multi_init(); + $handles = []; + for ($i = 0; $i < 8 && $nextChunk < count($remainingChunks); $i++, $nextChunk++) { + $chunk = $remainingChunks[$nextChunk]; + $ch = $makeHandle($chunk); + $handles[spl_object_id($ch)] = ['handle' => $ch, 'chunk' => $chunk]; + curl_multi_add_handle($multiHandle, $ch); + } + + do { + $status = curl_multi_exec($multiHandle, $active); + if ($active) { + curl_multi_select($multiHandle); + } + } while ($active && $status == CURLM_OK); + + foreach ($handles as $handleInfo) { + $ch = $handleInfo['handle']; + $body = curl_multi_getcontent($ch); + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = $responseHeaders[spl_object_id($ch)]['content-type'] ?? ''; + + if (curl_errno($ch)) { + throw new AppwriteException(curl_error($ch), $statusCode, '', $body); + } + + $chunkResponse = str_starts_with($contentType, 'application/json') ? json_decode($body, true) : $body; + + if($statusCode >= 400) { + if(is_array($chunkResponse)) { + throw new AppwriteException($chunkResponse['message'], $statusCode, $chunkResponse['type'] ?? '', json_encode($chunkResponse)); + } + + throw new AppwriteException($chunkResponse, $statusCode, '', $chunkResponse); + } + + $completedCount++; + $uploadedSize += $handleInfo['chunk']['end'] - $handleInfo['chunk']['start']; + if($isUploadComplete($chunkResponse)) { + $response = $chunkResponse; + } + if($onProgress !== null) { + $onProgress([ + '$id' => $uploadId, + 'progress' => $uploadedSize / $size * 100, + 'sizeUploaded' => $uploadedSize, + 'chunksTotal' => $totalChunks, + 'chunksUploaded' => $completedCount, + ]); + } + + curl_multi_remove_handle($multiHandle, $ch); + } + + curl_multi_close($multiHandle); + } + } if(!empty($handle)) { @fclose($handle); diff --git a/src/Appwrite/Services/Storage.php b/src/Appwrite/Services/Storage.php index a8661140..6af6be96 100644 --- a/src/Appwrite/Services/Storage.php +++ b/src/Appwrite/Services/Storage.php @@ -443,35 +443,187 @@ public function createFile(string $bucketId, string $fileId, InputFile $file, ?a $handle = @fopen($file->getPath(), "rb"); } + $uploadId = ''; + $uploadId = $fileId ?? ''; + $totalChunks = (int) ceil($size / Client::CHUNK_SIZE); + $chunks = []; $start = $counter * Client::CHUNK_SIZE; while ($start < $size) { - $chunk = ''; + $chunks[] = [ + 'index' => $counter, + 'start' => $start, + 'end' => min($start + Client::CHUNK_SIZE, $size), + ]; + $counter++; + $start += Client::CHUNK_SIZE; + } + + $readChunk = function(int $start, int $end) use ($handle, $file) { if(!empty($handle)) { fseek($handle, $start); - $chunk = @fread($handle, Client::CHUNK_SIZE); - } else { - $chunk = substr($file->getData(), $start, Client::CHUNK_SIZE); + return @fread($handle, $end - $start); } - $apiParams['file'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($chunk), $mimeType, $postedName); - $apiHeaders['content-range'] = 'bytes ' . ($counter * Client::CHUNK_SIZE) . '-' . min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE) - 1), $size - 1) . '/' . $size; - if(!empty($id)) { - $apiHeaders['x-appwrite-id'] = $id; + + return substr($file->getData(), $start, $end - $start); + }; + + $uploadChunk = function(array $chunk, string $currentUploadId = '') use ($readChunk, $apiPath, $apiHeaders, $apiParams, $mimeType, $postedName, $size) { + $chunkParams = $apiParams; + $chunkHeaders = $apiHeaders; + $data = $readChunk($chunk['start'], $chunk['end']); + $chunkParams['file'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($data), $mimeType, $postedName); + $chunkHeaders['content-range'] = 'bytes ' . $chunk['start'] . '-' . ($chunk['end'] - 1) . '/' . $size; + if(!empty($currentUploadId)) { + $chunkHeaders['x-appwrite-id'] = $currentUploadId; } - $response = $this->client->call(Client::METHOD_POST, $apiPath, $apiHeaders, $apiParams); - $counter++; - $start += Client::CHUNK_SIZE; - if(empty($id)) { - $id = $response['$id']; + + return $this->client->call(Client::METHOD_POST, $apiPath, $chunkHeaders, $chunkParams); + }; + + $isUploadComplete = function($chunkResponse) use ($totalChunks): bool { + if(!is_array($chunkResponse) || !isset($chunkResponse['chunksUploaded'])) { + return false; + } + + return (int) $chunkResponse['chunksUploaded'] >= (int) ($chunkResponse['chunksTotal'] ?? $totalChunks); + }; + + if (!empty($chunks)) { + $response = $uploadChunk($chunks[0], $uploadId); + if(empty($uploadId)) { + $uploadId = $response['$id']; } + $completedCount = $chunks[0]['index'] + 1; + $uploadedSize = $chunks[0]['end']; if($onProgress !== null) { $onProgress([ '$id' => $response['$id'], - 'progress' => min(((($counter * Client::CHUNK_SIZE) + Client::CHUNK_SIZE)), $size) / $size * 100, - 'sizeUploaded' => min($counter * Client::CHUNK_SIZE), - 'chunksTotal' => $response['chunksTotal'], - 'chunksUploaded' => $response['chunksUploaded'], + 'progress' => $uploadedSize / $size * 100, + 'sizeUploaded' => $uploadedSize, + 'chunksTotal' => $totalChunks, + 'chunksUploaded' => $completedCount, ]); } + + $remainingChunks = array_slice($chunks, 1); + $clientConfig = \Closure::bind(function() { + if (property_exists($this, 'key') && $this->key !== null) { + $this->headers['authorization'] = $this->getAuthorization(); + } + + return [$this->endpoint, $this->headers, $this->selfSigned, $this->timeout, $this->connectTimeout]; + }, $this->client, Client::class); + $flattenParams = \Closure::bind(function(array $params): array { + return $this->flatten($params); + }, $this->client, Client::class); + [$endpoint, $globalHeaders, $selfSigned, $timeout, $connectTimeout] = $clientConfig(); + $responseHeaders = []; + + $makeHandle = function(array $chunk) use ($readChunk, $apiPath, $apiHeaders, $apiParams, $mimeType, $postedName, $size, $uploadId, $endpoint, $globalHeaders, $selfSigned, $timeout, $connectTimeout, $flattenParams, &$responseHeaders) { + $chunkParams = $apiParams; + $chunkHeaders = array_merge($globalHeaders, $apiHeaders); + $data = $readChunk($chunk['start'], $chunk['end']); + $chunkParams['file'] = new \CURLFile('data://' . $mimeType . ';base64,' . base64_encode($data), $mimeType, $postedName); + $chunkHeaders['content-range'] = 'bytes ' . $chunk['start'] . '-' . ($chunk['end'] - 1) . '/' . $size; + if(!empty($uploadId)) { + $chunkHeaders['x-appwrite-id'] = $uploadId; + } + + $headers = []; + foreach ($chunkHeaders as $key => $value) { + $headers[] = $key . ':' . $value; + } + + $ch = curl_init($endpoint . $apiPath); + $responseHeaders[spl_object_id($ch)] = []; + curl_setopt($ch, CURLOPT_CUSTOMREQUEST, Client::METHOD_POST); + curl_setopt($ch, CURLOPT_RETURNTRANSFER, 1); + curl_setopt($ch, CURLOPT_USERAGENT, php_uname('s') . '-' . php_uname('r') . ':php-' . phpversion()); + curl_setopt($ch, CURLOPT_HTTPHEADER, $headers); + curl_setopt($ch, CURLOPT_FOLLOWLOCATION, true); + curl_setopt($ch, CURLOPT_POSTFIELDS, $flattenParams($chunkParams)); + curl_setopt($ch, CURLOPT_HEADERFUNCTION, function($curl, $header) use (&$responseHeaders) { + $length = strlen($header); + $header = explode(':', strtolower($header), 2); + if (count($header) >= 2) { + $responseHeaders[spl_object_id($curl)][strtolower(trim($header[0]))] = trim($header[1]); + } + + return $length; + }); + if($selfSigned) { + curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, false); + curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, false); + } + if($timeout !== null) { + curl_setopt($ch, CURLOPT_TIMEOUT, $timeout); + } + if($connectTimeout !== null) { + curl_setopt($ch, CURLOPT_CONNECTTIMEOUT, $connectTimeout); + } + + return $ch; + }; + + $nextChunk = 0; + while ($nextChunk < count($remainingChunks)) { + $multiHandle = curl_multi_init(); + $handles = []; + for ($i = 0; $i < 8 && $nextChunk < count($remainingChunks); $i++, $nextChunk++) { + $chunk = $remainingChunks[$nextChunk]; + $ch = $makeHandle($chunk); + $handles[spl_object_id($ch)] = ['handle' => $ch, 'chunk' => $chunk]; + curl_multi_add_handle($multiHandle, $ch); + } + + do { + $status = curl_multi_exec($multiHandle, $active); + if ($active) { + curl_multi_select($multiHandle); + } + } while ($active && $status == CURLM_OK); + + foreach ($handles as $handleInfo) { + $ch = $handleInfo['handle']; + $body = curl_multi_getcontent($ch); + $statusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE); + $contentType = $responseHeaders[spl_object_id($ch)]['content-type'] ?? ''; + + if (curl_errno($ch)) { + throw new AppwriteException(curl_error($ch), $statusCode, '', $body); + } + + $chunkResponse = str_starts_with($contentType, 'application/json') ? json_decode($body, true) : $body; + + if($statusCode >= 400) { + if(is_array($chunkResponse)) { + throw new AppwriteException($chunkResponse['message'], $statusCode, $chunkResponse['type'] ?? '', json_encode($chunkResponse)); + } + + throw new AppwriteException($chunkResponse, $statusCode, '', $chunkResponse); + } + + $completedCount++; + $uploadedSize += $handleInfo['chunk']['end'] - $handleInfo['chunk']['start']; + if($isUploadComplete($chunkResponse)) { + $response = $chunkResponse; + } + if($onProgress !== null) { + $onProgress([ + '$id' => $uploadId, + 'progress' => $uploadedSize / $size * 100, + 'sizeUploaded' => $uploadedSize, + 'chunksTotal' => $totalChunks, + 'chunksUploaded' => $completedCount, + ]); + } + + curl_multi_remove_handle($multiHandle, $ch); + } + + curl_multi_close($multiHandle); + } + } if(!empty($handle)) { @fclose($handle); diff --git a/tests/Appwrite/Services/AccountTest.php b/tests/Appwrite/Services/AccountTest.php index da86553d..24fbf7d8 100644 --- a/tests/Appwrite/Services/AccountTest.php +++ b/tests/Appwrite/Services/AccountTest.php @@ -929,6 +929,21 @@ public function testMethodUpdateMagicURLSession(): void $this->assertInstanceOf(\Appwrite\Models\Session::class, $response); } + public function testMethodCreateOAuth2Session(): void + { + $data = ''; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + + $response = $this->account->createOAuth2Session( + OAuthProvider::AMAZON() + ); + + $this->assertSame($data, $response); + } + public function testMethodUpdatePhoneSession(): void { $data = array( @@ -1167,6 +1182,71 @@ public function testMethodUpdateStatus(): void $this->assertInstanceOf(\Appwrite\Models\User::class, $response); } + public function testMethodCreatePushTarget(): void + { + $data = array( + "\$id" => "259125845563242502", + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "\$updatedAt" => "2020-10-15T06:38:00.000+00:00", + "name" => "Apple iPhone 12", + "userId" => "259125845563242502", + "providerType" => "email", + "identifier" => "token", + "expired" => true + ); + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + + $response = $this->account->createPushTarget( + "", + "" + ); + + $this->assertInstanceOf(\Appwrite\Models\Target::class, $response); + } + + public function testMethodUpdatePushTarget(): void + { + $data = array( + "\$id" => "259125845563242502", + "\$createdAt" => "2020-10-15T06:38:00.000+00:00", + "\$updatedAt" => "2020-10-15T06:38:00.000+00:00", + "name" => "Apple iPhone 12", + "userId" => "259125845563242502", + "providerType" => "email", + "identifier" => "token", + "expired" => true + ); + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + + $response = $this->account->updatePushTarget( + "", + "" + ); + + $this->assertInstanceOf(\Appwrite\Models\Target::class, $response); + } + + public function testMethodDeletePushTarget(): void + { + $data = ''; + + $this->client + ->allows()->call(Mockery::any(), Mockery::any(), Mockery::any(), Mockery::any()) + ->andReturn($data); + + $response = $this->account->deletePushTarget( + "" + ); + + $this->assertSame($data, $response); + } + public function testMethodCreateEmailToken(): void { $data = array( diff --git a/tests/Appwrite/Services/ProjectTest.php b/tests/Appwrite/Services/ProjectTest.php index 4b859821..315e4c88 100644 --- a/tests/Appwrite/Services/ProjectTest.php +++ b/tests/Appwrite/Services/ProjectTest.php @@ -877,7 +877,7 @@ public function testMethodUpdateOAuth2Google(): void "\$id" => "github", "enabled" => true, "clientId" => "120000000095-92ifjb00000000000000000000g7ijfb.apps.googleusercontent.com", - "clientSecret" => "example-google-client-secret", + "clientSecret" => "GOCSPX-2k8gsR0000000000000000VNahJj", "prompt" => array() ); @@ -934,7 +934,7 @@ public function testMethodUpdateOAuth2Linkedin(): void "\$id" => "github", "enabled" => true, "clientId" => "770000000000dv", - "primaryClientSecret" => "example-linkedin-client-secret" + "primaryClientSecret" => "WPL_AP1.2Bf0000000000000./HtlYw==" ); $this->client