Merge pull request #11069 from appwrite/storage-size

This commit is contained in:
Darshan 2026-01-07 16:36:06 +05:30 committed by GitHub
commit 8bb6f01e22
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
12 changed files with 162 additions and 16 deletions

View file

@ -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",

View file

@ -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": {

View file

@ -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": {

View file

@ -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",

View file

@ -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": {

View file

@ -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": {

View file

@ -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);
}
}

View file

@ -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,
])
;
}

View file

@ -2344,6 +2344,7 @@ trait Base
_id
name
enabled
totalSize
}
}';
case self::UPDATE_BUCKET:

View file

@ -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;
}

View file

@ -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']);
}
}

View file

@ -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