mirror of
https://github.com/appwrite/appwrite
synced 2026-05-23 00:49:02 +00:00
Merge remote-tracking branch 'origin/master' into feat-dotnet-sdk
This commit is contained in:
commit
257c58f438
19 changed files with 164 additions and 29 deletions
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
|
|
@ -6,7 +6,7 @@ on:
|
|||
|
||||
jobs:
|
||||
tests:
|
||||
name: Unit & E2E
|
||||
name: Release
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
|
|
@ -37,9 +37,9 @@ jobs:
|
|||
with:
|
||||
images: appwrite/appwrite
|
||||
tags: |
|
||||
type=semver,pattern={{major}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}.{{minor}}.{{patch}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=semver,pattern={{major}}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v4
|
||||
|
|
|
|||
|
|
@ -1,3 +1,9 @@
|
|||
# Version 1.3.4
|
||||
|
||||
## Bugs
|
||||
|
||||
- Update migration to properly migrate bucket permissiosn [#5497](https://github.com/appwrite/appwrite/pull/5497)
|
||||
|
||||
# Version 1.3.3
|
||||
|
||||
## Bugs
|
||||
|
|
@ -9,6 +15,7 @@
|
|||
- Fixed auto-setting custom ID on nested documents [#5363](https://github.com/appwrite/appwrite/pull/5363)
|
||||
- Fixed listDocuments not returning all the documents [#5395](https://github.com/appwrite/appwrite/pull/5395)
|
||||
- Fixed deleting keys, webhooks, platforms and domains after deleting project [#5395](https://github.com/appwrite/appwrite/pull/5395)
|
||||
- Fixed empty team prefs returning as JSON object rather array [#5361](https://github.com/appwrite/appwrite/pull/5361)
|
||||
|
||||
# Version 1.3.1
|
||||
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ docker run -it --rm \
|
|||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.3.3
|
||||
appwrite/appwrite:1.3.4
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
|
@ -78,7 +78,7 @@ docker run -it --rm ^
|
|||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.3.3
|
||||
appwrite/appwrite:1.3.4
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
|
@ -88,7 +88,7 @@ docker run -it --rm `
|
|||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.3.3
|
||||
appwrite/appwrite:1.3.4
|
||||
```
|
||||
|
||||
运行后,可以在浏览器上访问 http://localhost 找到 Appwrite 控制台。在非 Linux 的本机主机上完成安装后,服务器可能需要几分钟才能启动。
|
||||
|
|
|
|||
|
|
@ -75,7 +75,7 @@ docker run -it --rm \
|
|||
--volume /var/run/docker.sock:/var/run/docker.sock \
|
||||
--volume "$(pwd)"/appwrite:/usr/src/code/appwrite:rw \
|
||||
--entrypoint="install" \
|
||||
appwrite/appwrite:1.3.3
|
||||
appwrite/appwrite:1.3.4
|
||||
```
|
||||
|
||||
### Windows
|
||||
|
|
@ -87,7 +87,7 @@ docker run -it --rm ^
|
|||
--volume //var/run/docker.sock:/var/run/docker.sock ^
|
||||
--volume "%cd%"/appwrite:/usr/src/code/appwrite:rw ^
|
||||
--entrypoint="install" ^
|
||||
appwrite/appwrite:1.3.3
|
||||
appwrite/appwrite:1.3.4
|
||||
```
|
||||
|
||||
#### PowerShell
|
||||
|
|
@ -97,7 +97,7 @@ docker run -it --rm `
|
|||
--volume /var/run/docker.sock:/var/run/docker.sock `
|
||||
--volume ${pwd}/appwrite:/usr/src/code/appwrite:rw `
|
||||
--entrypoint="install" `
|
||||
appwrite/appwrite:1.3.3
|
||||
appwrite/appwrite:1.3.4
|
||||
```
|
||||
|
||||
Once the Docker installation is complete, go to http://localhost to access the Appwrite console from your browser. Please note that on non-Linux native hosts, the server might take a few minutes to start after completing the installation.
|
||||
|
|
|
|||
|
|
@ -88,6 +88,11 @@ return [
|
|||
'description' => 'The request cannot be fulfilled with the current protocol. Please check the value of the _APP_OPTIONS_FORCE_HTTPS environment variable.',
|
||||
'code' => 500,
|
||||
],
|
||||
Exception::GENERAL_USAGE_DISABLED => [
|
||||
'name' => Exception::GENERAL_USAGE_DISABLED,
|
||||
'description' => 'Usage stats is not configured. Please check the value of the _APP_USAGE_STATS environment variable of your Appwrite server.',
|
||||
'code' => 501,
|
||||
],
|
||||
|
||||
/** User Errors */
|
||||
Exception::USER_COUNT_EXCEEDED => [
|
||||
|
|
|
|||
|
|
@ -152,7 +152,7 @@ return [
|
|||
],
|
||||
[
|
||||
'name' => '_APP_LOGGING_PROVIDER',
|
||||
'description' => 'This variable allows you to enable logging errors to 3rd party providers. This value is empty by default, to enable the logger set the value to one of \'sentry\', \'raygun\', \'appsignal\', \'logowl\'',
|
||||
'description' => 'This variable allows you to enable logging errors to 3rd party providers. This value is empty by default, to enable the logger set the value to one of \'sentry\', \'raygun\', \'appSignal\', \'logOwl\'',
|
||||
'introduction' => '0.12.0',
|
||||
'default' => '',
|
||||
'required' => false,
|
||||
|
|
|
|||
|
|
@ -3279,6 +3279,7 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
|
|||
$permissions = $document->getPermissions() ?? [];
|
||||
}
|
||||
|
||||
$data = \array_merge($document->getArrayCopy(), $data); // Merge existing data with new data
|
||||
$data['$collection'] = $collection->getId(); // Make sure user doesn't switch collectionID
|
||||
$data['$createdAt'] = $document->getCreatedAt(); // Make sure user doesn't switch createdAt
|
||||
$data['$id'] = $document->getId(); // Make sure user doesn't switch document unique ID
|
||||
|
|
@ -3368,8 +3369,6 @@ App::patch('/v1/databases/:databaseId/collections/:collectionId/documents/:docum
|
|||
|
||||
$checkPermissions($collection, $newDocument, $document, Database::PERMISSION_UPDATE);
|
||||
|
||||
$newDocument = new Document(\array_merge($document->getArrayCopy(), $data));
|
||||
|
||||
try {
|
||||
$document = $dbForProject->withRequestTimestamp(
|
||||
$requestTimestamp,
|
||||
|
|
@ -3603,7 +3602,7 @@ App::delete('/v1/databases/:databaseId/collections/:collectionId/documents/:docu
|
|||
|
||||
App::get('/v1/databases/usage')
|
||||
->desc('Get usage stats for the database')
|
||||
->groups(['api', 'database'])
|
||||
->groups(['api', 'database', 'usage'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'databases')
|
||||
|
|
@ -3721,7 +3720,7 @@ App::get('/v1/databases/usage')
|
|||
|
||||
App::get('/v1/databases/:databaseId/usage')
|
||||
->desc('Get usage stats for the database')
|
||||
->groups(['api', 'database'])
|
||||
->groups(['api', 'database', 'usage'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'databases')
|
||||
|
|
@ -3831,7 +3830,7 @@ App::get('/v1/databases/:databaseId/usage')
|
|||
App::get('/v1/databases/:databaseId/collections/:collectionId/usage')
|
||||
->alias('/v1/database/:collectionId/usage', ['databaseId' => 'default'])
|
||||
->desc('Get usage stats for a collection')
|
||||
->groups(['api', 'database'])
|
||||
->groups(['api', 'database', 'usage'])
|
||||
->label('scope', 'collections.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'databases')
|
||||
|
|
|
|||
|
|
@ -211,7 +211,7 @@ App::get('/v1/functions/:functionId')
|
|||
|
||||
App::get('/v1/functions/:functionId/usage')
|
||||
->desc('Get Function Usage')
|
||||
->groups(['api', 'functions'])
|
||||
->groups(['api', 'functions', 'usage'])
|
||||
->label('scope', 'functions.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'functions')
|
||||
|
|
@ -321,7 +321,7 @@ App::get('/v1/functions/:functionId/usage')
|
|||
|
||||
App::get('/v1/functions/usage')
|
||||
->desc('Get Functions Usage')
|
||||
->groups(['api', 'functions'])
|
||||
->groups(['api', 'functions', 'usage'])
|
||||
->label('scope', 'functions.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'functions')
|
||||
|
|
|
|||
|
|
@ -245,7 +245,7 @@ App::get('/v1/projects/:projectId')
|
|||
|
||||
App::get('/v1/projects/:projectId/usage')
|
||||
->desc('Get usage stats for a project')
|
||||
->groups(['api', 'projects'])
|
||||
->groups(['api', 'projects', 'usage'])
|
||||
->label('scope', 'projects.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'projects')
|
||||
|
|
|
|||
|
|
@ -128,7 +128,7 @@ App::post('/v1/storage/buckets')
|
|||
|
||||
$bucket = $dbForProject->getDocument('buckets', $bucketId);
|
||||
|
||||
$dbForProject->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes);
|
||||
$dbForProject->createCollection('bucket_' . $bucket->getInternalId(), $attributes, $indexes, permissions: $permissions ?? [], documentSecurity: $fileSecurity);
|
||||
} catch (Duplicate) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_ALREADY_EXISTS);
|
||||
}
|
||||
|
|
@ -274,6 +274,7 @@ App::put('/v1/storage/buckets/:bucketId')
|
|||
->setAttribute('encryption', $encryption)
|
||||
->setAttribute('compression', $compression)
|
||||
->setAttribute('antivirus', $antivirus));
|
||||
$dbForProject->updateCollection('bucket_' . $bucket->getInternalId(), $permissions, $fileSecurity);
|
||||
|
||||
$events
|
||||
->setParam('bucketId', $bucket->getId())
|
||||
|
|
@ -514,6 +515,7 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
|||
}
|
||||
|
||||
$mimeType = $deviceFiles->getFileMimeType($path); // Get mime-type before compression and encryption
|
||||
$fileHash = $deviceFiles->getFileHash($path); // Get file hash before compression and encryption
|
||||
$data = '';
|
||||
// Compression
|
||||
$algorithm = $bucket->getAttribute('compression', COMPRESSION_TYPE_NONE);
|
||||
|
|
@ -547,7 +549,6 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
|||
}
|
||||
|
||||
$sizeActual = $deviceFiles->getFileSize($path);
|
||||
$fileHash = $deviceFiles->getFileHash($path);
|
||||
|
||||
$openSSLVersion = null;
|
||||
$openSSLCipher = null;
|
||||
|
|
@ -1447,7 +1448,7 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
|
|||
|
||||
App::get('/v1/storage/usage')
|
||||
->desc('Get usage stats for storage')
|
||||
->groups(['api', 'storage'])
|
||||
->groups(['api', 'storage', 'usage'])
|
||||
->label('scope', 'files.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'storage')
|
||||
|
|
@ -1557,7 +1558,7 @@ App::get('/v1/storage/usage')
|
|||
|
||||
App::get('/v1/storage/:bucketId/usage')
|
||||
->desc('Get usage stats for a storage bucket')
|
||||
->groups(['api', 'storage'])
|
||||
->groups(['api', 'storage', 'usage'])
|
||||
->label('scope', 'files.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'storage')
|
||||
|
|
|
|||
|
|
@ -1129,7 +1129,7 @@ App::delete('/v1/users/:userId')
|
|||
|
||||
App::get('/v1/users/usage')
|
||||
->desc('Get usage stats for the users API')
|
||||
->groups(['api', 'users'])
|
||||
->groups(['api', 'users', 'usage'])
|
||||
->label('scope', 'users.read')
|
||||
->label('sdk.auth', [APP_AUTH_TYPE_ADMIN])
|
||||
->label('sdk.namespace', 'users')
|
||||
|
|
|
|||
|
|
@ -552,3 +552,11 @@ App::shutdown()
|
|||
->submit();
|
||||
}
|
||||
});
|
||||
|
||||
App::init()
|
||||
->groups(['usage'])
|
||||
->action(function () {
|
||||
if (App::getEnv('_APP_USAGE_STATS', 'enabled') !== 'enabled') {
|
||||
throw new Exception(Exception::GENERAL_USAGE_DISABLED);
|
||||
}
|
||||
});
|
||||
|
|
|
|||
|
|
@ -101,7 +101,7 @@ const APP_LIMIT_LIST_DEFAULT = 25; // Default maximum number of items to return
|
|||
const APP_KEY_ACCCESS = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_UPDATE = 24 * 60 * 60; // 24 hours
|
||||
const APP_CACHE_BUSTER = 503;
|
||||
const APP_VERSION_STABLE = '1.3.3';
|
||||
const APP_VERSION_STABLE = '1.3.4';
|
||||
const APP_DATABASE_ATTRIBUTE_EMAIL = 'email';
|
||||
const APP_DATABASE_ATTRIBUTE_ENUM = 'enum';
|
||||
const APP_DATABASE_ATTRIBUTE_IP = 'ip';
|
||||
|
|
|
|||
|
|
@ -224,9 +224,9 @@ $cli
|
|||
}
|
||||
}
|
||||
|
||||
Console::log("Running \"docker compose -f {$path}/docker-compose.yml up -d --remove-orphans --renew-anon-volumes\"");
|
||||
Console::log("Running \"docker compose up -d --remove-orphans --renew-anon-volumes\"");
|
||||
|
||||
$exit = Console::execute("${env} docker compose -f {$path}/docker-compose.yml up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr);
|
||||
$exit = Console::execute("${env} docker compose --project-directory {$path} up -d --remove-orphans --renew-anon-volumes", '', $stdout, $stderr);
|
||||
|
||||
if ($exit !== 0) {
|
||||
$message = 'Failed to install Appwrite dockers';
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ class Exception extends \Exception
|
|||
public const GENERAL_CURSOR_NOT_FOUND = 'general_cursor_not_found';
|
||||
public const GENERAL_SERVER_ERROR = 'general_server_error';
|
||||
public const GENERAL_PROTOCOL_UNSUPPORTED = 'general_protocol_unsupported';
|
||||
public const GENERAL_USAGE_DISABLED = 'general_usage_disabled';
|
||||
|
||||
/** Users */
|
||||
public const USER_COUNT_EXCEEDED = 'user_count_exceeded';
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ abstract class Migration
|
|||
'1.3.1' => 'V18',
|
||||
'1.3.2' => 'V18',
|
||||
'1.3.3' => 'V18',
|
||||
'1.3.4' => 'V18',
|
||||
];
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -182,6 +182,19 @@ class V18 extends Migration
|
|||
*/
|
||||
$document->setAttribute('options', $document->getAttribute('options', new \stdClass()));
|
||||
break;
|
||||
case 'buckets':
|
||||
/**
|
||||
* Set the bucket permission in the metadata table
|
||||
*/
|
||||
try {
|
||||
$internalBucketId = "bucket_{$this->project->getInternalId()}";
|
||||
$permissions = $document->getPermissions();
|
||||
$fileSecurity = $document->getAttribute('fileSecurity', false);
|
||||
$this->projectDB->updateCollection($internalBucketId, $permissions, $fileSecurity);
|
||||
} catch (\Throwable $th) {
|
||||
Console::warning($th->getMessage());
|
||||
}
|
||||
break;
|
||||
}
|
||||
|
||||
return $document;
|
||||
|
|
|
|||
|
|
@ -3968,4 +3968,105 @@ trait DatabasesBase
|
|||
$this->assertArrayHasKey('fullName', $response['body']);
|
||||
$this->assertArrayNotHasKey('libraries', $response['body']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @depends testCreateDatabase
|
||||
* @param array $data
|
||||
* @return void
|
||||
* @throws \Exception
|
||||
*/
|
||||
public function testUpdateWithExistingRelationships(array $data): void
|
||||
{
|
||||
$databaseId = $data['databaseId'];
|
||||
|
||||
$collection1 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'Collection1',
|
||||
'documentSecurity' => true,
|
||||
'permissions' => [
|
||||
Permission::create(Role::user($this->getUser()['$id'])),
|
||||
Permission::read(Role::user($this->getUser()['$id'])),
|
||||
],
|
||||
]);
|
||||
|
||||
$collection2 = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'collectionId' => ID::unique(),
|
||||
'name' => 'Collection2',
|
||||
'documentSecurity' => true,
|
||||
'permissions' => [
|
||||
Permission::create(Role::user($this->getUser()['$id'])),
|
||||
Permission::read(Role::user($this->getUser()['$id'])),
|
||||
],
|
||||
]);
|
||||
|
||||
$collection1 = $collection1['body']['$id'];
|
||||
$collection2 = $collection2['body']['$id'];
|
||||
|
||||
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection1 . '/attributes/string', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'key' => 'name',
|
||||
'size' => '49',
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection2 . '/attributes/string', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'key' => 'name',
|
||||
'size' => '49',
|
||||
'required' => true,
|
||||
]);
|
||||
|
||||
$this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection1 . '/attributes/relationship', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey']
|
||||
]), [
|
||||
'relatedCollectionId' => $collection2,
|
||||
'type' => Database::RELATION_ONE_TO_MANY,
|
||||
'twoWay' => true,
|
||||
'key' => 'collection2'
|
||||
]);
|
||||
|
||||
sleep(1);
|
||||
|
||||
$document = $this->client->call(Client::METHOD_POST, '/databases/' . $databaseId . '/collections/' . $collection1 . '/documents', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id']
|
||||
], $this->getHeaders()), [
|
||||
'documentId' => ID::unique(),
|
||||
'data' => [
|
||||
'name' => 'Document 1',
|
||||
'collection2' => [
|
||||
[
|
||||
'name' => 'Document 2',
|
||||
],
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
$update = $this->client->call(Client::METHOD_PATCH, '/databases/' . $databaseId . '/collections/' . $collection1 . '/documents/' . $document['body']['$id'], array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id']
|
||||
], $this->getHeaders()), [
|
||||
'data' => [
|
||||
'name' => 'Document 1 Updated',
|
||||
],
|
||||
]);
|
||||
|
||||
$this->assertEquals(200, $update['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -57,8 +57,7 @@ trait StorageBase
|
|||
$this->assertEquals('logo.png', $file['body']['name']);
|
||||
$this->assertEquals('image/png', $file['body']['mimeType']);
|
||||
$this->assertEquals(47218, $file['body']['sizeOriginal']);
|
||||
$this->assertTrue(md5_file(realpath(__DIR__ . '/../../../resources/logo.png')) != $file['body']['signature']); // should validate that the file is encrypted
|
||||
|
||||
$this->assertTrue(md5_file(realpath(__DIR__ . '/../../../resources/logo.png')) == $file['body']['signature']);
|
||||
/**
|
||||
* Test for Large File above 20MB
|
||||
* This should also validate the test for when Bucket encryption
|
||||
|
|
@ -289,7 +288,7 @@ trait StorageBase
|
|||
$this->assertEquals('logo.png', $file['body']['name']);
|
||||
$this->assertEquals('image/png', $file['body']['mimeType']);
|
||||
$this->assertEquals(47218, $file['body']['sizeOriginal']);
|
||||
$this->assertTrue(md5_file(realpath(__DIR__ . '/../../../resources/logo.png')) != $file['body']['signature']); // should validate that the file is encrypted
|
||||
$this->assertTrue(md5_file(realpath(__DIR__ . '/../../../resources/logo.png')) == $file['body']['signature']);
|
||||
|
||||
return ['bucketId' => $bucketId];
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue