diff --git a/app/config/specs/open-api3-latest-client.json b/app/config/specs/open-api3-latest-client.json index 052fe536c9..8038b0f061 100644 --- a/app/config/specs/open-api3-latest-client.json +++ b/app/config/specs/open-api3-latest-client.json @@ -1,7 +1,7 @@ { "openapi": "3.0.0", "info": { - "version": "1.8.0", + "version": "1.8.1", "title": "Appwrite", "description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)", "termsOfService": "https:\/\/appwrite.io\/policy\/terms", diff --git a/app/config/specs/open-api3-latest-console.json b/app/config/specs/open-api3-latest-console.json index de68c4db48..4d6b513e6e 100644 --- a/app/config/specs/open-api3-latest-console.json +++ b/app/config/specs/open-api3-latest-console.json @@ -1,7 +1,7 @@ { "openapi": "3.0.0", "info": { - "version": "1.8.0", + "version": "1.8.1", "title": "Appwrite", "description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)", "termsOfService": "https:\/\/appwrite.io\/policy\/terms", @@ -55086,6 +55086,12 @@ "type": "boolean", "description": "Image transformations are enabled.", "x-example": false + }, + "totalSize": { + "type": "integer", + "description": "Total size of this bucket in bytes.", + "x-example": 128, + "format": "int32" } }, "required": [ @@ -55101,7 +55107,8 @@ "compression", "encryption", "antivirus", - "transformations" + "transformations", + "totalSize" ], "example": { "$id": "5e5ea5c16897e", @@ -55121,7 +55128,8 @@ "compression": "gzip", "encryption": false, "antivirus": false, - "transformations": false + "transformations": false, + "totalSize": 128 } }, "resourceToken": { diff --git a/app/config/specs/open-api3-latest-server.json b/app/config/specs/open-api3-latest-server.json index 2a0081b378..cb46b564ae 100644 --- a/app/config/specs/open-api3-latest-server.json +++ b/app/config/specs/open-api3-latest-server.json @@ -1,7 +1,7 @@ { "openapi": "3.0.0", "info": { - "version": "1.8.0", + "version": "1.8.1", "title": "Appwrite", "description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)", "termsOfService": "https:\/\/appwrite.io\/policy\/terms", @@ -43237,6 +43237,12 @@ "type": "boolean", "description": "Image transformations are enabled.", "x-example": false + }, + "totalSize": { + "type": "integer", + "description": "Total size of this bucket in bytes.", + "x-example": 128, + "format": "int32" } }, "required": [ @@ -43252,7 +43258,8 @@ "compression", "encryption", "antivirus", - "transformations" + "transformations", + "totalSize" ], "example": { "$id": "5e5ea5c16897e", @@ -43272,7 +43279,8 @@ "compression": "gzip", "encryption": false, "antivirus": false, - "transformations": false + "transformations": false, + "totalSize": 128 } }, "resourceToken": { diff --git a/app/config/specs/swagger2-latest-client.json b/app/config/specs/swagger2-latest-client.json index e11d5053a4..ea83ad8d1f 100644 --- a/app/config/specs/swagger2-latest-client.json +++ b/app/config/specs/swagger2-latest-client.json @@ -1,7 +1,7 @@ { "swagger": "2.0", "info": { - "version": "1.8.0", + "version": "1.8.1", "title": "Appwrite", "description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)", "termsOfService": "https:\/\/appwrite.io\/policy\/terms", diff --git a/app/config/specs/swagger2-latest-console.json b/app/config/specs/swagger2-latest-console.json index 21f8513e16..2761a040c0 100644 --- a/app/config/specs/swagger2-latest-console.json +++ b/app/config/specs/swagger2-latest-console.json @@ -1,7 +1,7 @@ { "swagger": "2.0", "info": { - "version": "1.8.0", + "version": "1.8.1", "title": "Appwrite", "description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)", "termsOfService": "https:\/\/appwrite.io\/policy\/terms", @@ -54918,6 +54918,12 @@ "type": "boolean", "description": "Image transformations are enabled.", "x-example": false + }, + "totalSize": { + "type": "integer", + "description": "Total size of this bucket in bytes.", + "x-example": 128, + "format": "int32" } }, "required": [ @@ -54933,7 +54939,8 @@ "compression", "encryption", "antivirus", - "transformations" + "transformations", + "totalSize" ], "example": { "$id": "5e5ea5c16897e", @@ -54953,7 +54960,8 @@ "compression": "gzip", "encryption": false, "antivirus": false, - "transformations": false + "transformations": false, + "totalSize": 128 } }, "resourceToken": { diff --git a/app/config/specs/swagger2-latest-server.json b/app/config/specs/swagger2-latest-server.json index a3d51a703d..8096164cca 100644 --- a/app/config/specs/swagger2-latest-server.json +++ b/app/config/specs/swagger2-latest-server.json @@ -1,7 +1,7 @@ { "swagger": "2.0", "info": { - "version": "1.8.0", + "version": "1.8.1", "title": "Appwrite", "description": "Appwrite backend as a service cuts up to 70% of the time and costs required for building a modern application. We abstract and simplify common development tasks behind a REST APIs, to help you develop your app in a fast and secure way. For full API documentation and tutorials go to [https:\/\/appwrite.io\/docs](https:\/\/appwrite.io\/docs)", "termsOfService": "https:\/\/appwrite.io\/policy\/terms", @@ -43165,6 +43165,12 @@ "type": "boolean", "description": "Image transformations are enabled.", "x-example": false + }, + "totalSize": { + "type": "integer", + "description": "Total size of this bucket in bytes.", + "x-example": 128, + "format": "int32" } }, "required": [ @@ -43180,7 +43186,8 @@ "compression", "encryption", "antivirus", - "transformations" + "transformations", + "totalSize" ], "example": { "$id": "5e5ea5c16897e", @@ -43200,7 +43207,8 @@ "compression": "gzip", "encryption": false, "antivirus": false, - "transformations": false + "transformations": false, + "totalSize": 128 } }, "resourceToken": { diff --git a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php index dd14feef6e..61954c0a00 100644 --- a/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php +++ b/src/Appwrite/Platform/Modules/Storage/Http/Buckets/Get.php @@ -8,6 +8,9 @@ use Appwrite\SDK\Method; use Appwrite\SDK\Response as SDKResponse; use Appwrite\Utopia\Response; use Utopia\Database\Database; +use Utopia\Database\Document; +use Utopia\Database\Query; +use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\UID; use Utopia\Platform\Action; use Utopia\Platform\Scope\HTTP; @@ -46,20 +49,55 @@ class Get extends Action ->param('bucketId', '', new UID(), 'Bucket unique ID.') ->inject('response') ->inject('dbForProject') + ->inject('project') + ->inject('getLogsDB') ->callback($this->action(...)); } public function action( string $bucketId, Response $response, - Database $dbForProject - ) { + Database $dbForProject, + Document $project, + callable $getLogsDB + ): void { $bucket = $dbForProject->getDocument('buckets', $bucketId); if ($bucket->isEmpty()) { throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); } + $dbForLogs = call_user_func($getLogsDB, $project); + $this->addBucketStorageSize($dbForLogs, $bucket); + $response->dynamic($bucket, Response::MODEL_BUCKET); } + + /** + * Adds the latest aggregated bucket storage size from logs DB stats. + */ + private function addBucketStorageSize(Database $dbForLogs, Document $bucket): void + { + $metric = str_replace( + '{bucketInternalId}', + $bucket->getSequence(), + METRIC_BUCKET_ID_FILES_STORAGE + ); + + $statsDocId = md5('_inf_' . $metric); + $storageStats = Authorization::skip( + fn () => $dbForLogs->getDocument( + 'stats', + $statsDocId, + [Query::select(['value'])] + ) + ); + + /** + * The value can be 0 if stats were not aggregated when this request was made! + */ + $totalSize = $storageStats->isEmpty() ? 0 : $storageStats->getAttribute('value', 0); + + $bucket->setAttribute('totalSize', $totalSize); + } } diff --git a/src/Appwrite/Utopia/Response/Model/Bucket.php b/src/Appwrite/Utopia/Response/Model/Bucket.php index f51c8b6527..707815eff0 100644 --- a/src/Appwrite/Utopia/Response/Model/Bucket.php +++ b/src/Appwrite/Utopia/Response/Model/Bucket.php @@ -92,6 +92,12 @@ class Bucket extends Model 'default' => true, 'example' => false, ]) + ->addRule('totalSize', [ + 'type' => self::TYPE_INTEGER, + 'description' => 'Total size of this bucket in bytes.', + 'default' => 0, + 'example' => 128, + ]) ; } diff --git a/tests/e2e/Services/GraphQL/Base.php b/tests/e2e/Services/GraphQL/Base.php index 10a6efd8e8..2468fe0424 100644 --- a/tests/e2e/Services/GraphQL/Base.php +++ b/tests/e2e/Services/GraphQL/Base.php @@ -2344,6 +2344,7 @@ trait Base _id name enabled + totalSize } }'; case self::UPDATE_BUCKET: diff --git a/tests/e2e/Services/GraphQL/StorageServerTest.php b/tests/e2e/Services/GraphQL/StorageServerTest.php index 37dba77ab3..f54b4fa63a 100644 --- a/tests/e2e/Services/GraphQL/StorageServerTest.php +++ b/tests/e2e/Services/GraphQL/StorageServerTest.php @@ -110,7 +110,9 @@ class StorageServerTest extends Scope /** * @depends testCreateBucket + * @depends testCreateFile * @param $bucket + * @param $file * @return array * @throws \Exception */ @@ -134,6 +136,7 @@ class StorageServerTest extends Scope $this->assertArrayNotHasKey('errors', $bucket['body']); $bucket = $bucket['body']['data']['storageGetBucket']; $this->assertEquals('Actors', $bucket['name']); + $this->assertArrayHasKey('totalSize', $bucket); return $bucket; } diff --git a/tests/e2e/Services/Storage/StorageBase.php b/tests/e2e/Services/Storage/StorageBase.php index c67cfcc99a..d0130eb3d0 100644 --- a/tests/e2e/Services/Storage/StorageBase.php +++ b/tests/e2e/Services/Storage/StorageBase.php @@ -951,4 +951,69 @@ trait StorageBase return $data; } + + public function testBucketTotalSize(): void + { + $bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ], [ + 'bucketId' => ID::unique(), + 'name' => 'Test Bucket Size', + 'permissions' => [ + Permission::read(Role::any()), + Permission::create(Role::any()), + ], + ]); + + $this->assertEquals(201, $bucket['headers']['status-code']); + $bucketId = $bucket['body']['$id']; + + // bucket should have totalSize = 0 (no files) + $emptyBucket = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $emptyBucket['headers']['status-code']); + $this->assertArrayHasKey('totalSize', $emptyBucket['body']); + $this->assertEquals(0, $emptyBucket['body']['totalSize']); + + // upload first file + $file1 = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'), + ]); + + $this->assertEquals(201, $file1['headers']['status-code']); + + // upload second file + $file2 = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ + 'content-type' => 'multipart/form-data', + 'x-appwrite-project' => $this->getProject()['$id'], + ], $this->getHeaders()), [ + 'fileId' => ID::unique(), + 'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/image.webp'), 'image/webp', 'image.webp'), + ]); + + $this->assertEquals(201, $file2['headers']['status-code']); + + $bucket = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId, [ + 'content-type' => 'application/json', + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ]); + + $this->assertEquals(200, $bucket['headers']['status-code']); + $this->assertArrayHasKey('totalSize', $bucket['body']); + $this->assertIsInt($bucket['body']['totalSize']); + + /* will always be 0 in tests because the worker runs hourly! */ + $this->assertGreaterThanOrEqual(0, $bucket['body']['totalSize']); + } } diff --git a/tests/e2e/Services/Storage/StorageCustomServerTest.php b/tests/e2e/Services/Storage/StorageCustomServerTest.php index 1dafd8ca06..5aa9010601 100644 --- a/tests/e2e/Services/Storage/StorageCustomServerTest.php +++ b/tests/e2e/Services/Storage/StorageCustomServerTest.php @@ -186,6 +186,7 @@ class StorageCustomServerTest extends Scope $this->assertNotEmpty($response['body']); $this->assertEquals($id, $response['body']['$id']); $this->assertEquals('Test Bucket', $response['body']['name']); + $this->assertArrayHasKey('totalSize', $response['body']); /** * Test for FAILURE