mirror of
https://github.com/appwrite/appwrite.git
synced 2026-05-26 13:51:13 +00:00
feat: per bucket image transformations flag
This commit is contained in:
@@ -1527,6 +1527,17 @@ return [
|
||||
'required' => true,
|
||||
'array' => false,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('transformations'),
|
||||
'type' => Database::VAR_BOOLEAN,
|
||||
'signed' => true,
|
||||
'size' => 0,
|
||||
'format' => '',
|
||||
'filters' => [],
|
||||
'required' => false,
|
||||
'array' => false,
|
||||
'default' => true,
|
||||
],
|
||||
[
|
||||
'$id' => ID::custom('search'),
|
||||
'type' => Database::VAR_STRING,
|
||||
|
||||
@@ -522,6 +522,11 @@ return [
|
||||
'description' => 'The requested file is not publicly readable.',
|
||||
'code' => 403,
|
||||
],
|
||||
Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED => [
|
||||
'name' => Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED,
|
||||
'description' => 'Image transformations are disabled for the requested bucket.',
|
||||
'code' => 403,
|
||||
],
|
||||
|
||||
/** Tokens */
|
||||
Exception::TOKEN_NOT_FOUND => [
|
||||
|
||||
@@ -85,10 +85,11 @@ App::post('/v1/storage/buckets')
|
||||
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
|
||||
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
|
||||
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
|
||||
->param('transformations', true, new Boolean(true), 'Are image transformations enabled?', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, bool $transformations, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
|
||||
$bucketId = $bucketId === 'unique()' ? ID::unique() : $bucketId;
|
||||
|
||||
@@ -141,6 +142,7 @@ App::post('/v1/storage/buckets')
|
||||
'compression' => $compression,
|
||||
'encryption' => $encryption,
|
||||
'antivirus' => $antivirus,
|
||||
'transformations' => $transformations,
|
||||
'search' => implode(' ', [$bucketId, $name]),
|
||||
]));
|
||||
|
||||
@@ -297,10 +299,11 @@ App::put('/v1/storage/buckets/:bucketId')
|
||||
->param('compression', Compression::NONE, new WhiteList([Compression::NONE, Compression::GZIP, Compression::ZSTD], true), 'Compression algorithm choosen for compression. Can be one of ' . Compression::NONE . ', [' . Compression::GZIP . '](https://en.wikipedia.org/wiki/Gzip), or [' . Compression::ZSTD . '](https://en.wikipedia.org/wiki/Zstd), For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' compression is skipped even if it\'s enabled', true)
|
||||
->param('encryption', true, new Boolean(true), 'Is encryption enabled? For file size above ' . Storage::human(APP_STORAGE_READ_BUFFER, 0) . ' encryption is skipped even if it\'s enabled', true)
|
||||
->param('antivirus', true, new Boolean(true), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS, 0) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
|
||||
->param('transformations', true, new Boolean(true), 'Are image transformations enabled?', true)
|
||||
->inject('response')
|
||||
->inject('dbForProject')
|
||||
->inject('queueForEvents')
|
||||
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
->action(function (string $bucketId, string $name, ?array $permissions, bool $fileSecurity, bool $enabled, ?int $maximumFileSize, array $allowedFileExtensions, ?string $compression, ?bool $encryption, bool $antivirus, bool $transformations, Response $response, Database $dbForProject, Event $queueForEvents) {
|
||||
$bucket = $dbForProject->getDocument('buckets', $bucketId);
|
||||
|
||||
if ($bucket->isEmpty()) {
|
||||
@@ -314,6 +317,7 @@ App::put('/v1/storage/buckets/:bucketId')
|
||||
$encryption ??= $bucket->getAttribute('encryption', true);
|
||||
$antivirus ??= $bucket->getAttribute('antivirus', true);
|
||||
$compression ??= $bucket->getAttribute('compression', Compression::NONE);
|
||||
$transformations ??= $bucket->getAttribute('transformations', true);
|
||||
|
||||
// Map aggregate permissions into the multiple permissions they represent.
|
||||
$permissions = Permission::aggregate($permissions);
|
||||
@@ -327,7 +331,8 @@ App::put('/v1/storage/buckets/:bucketId')
|
||||
->setAttribute('enabled', $enabled)
|
||||
->setAttribute('encryption', $encryption)
|
||||
->setAttribute('compression', $compression)
|
||||
->setAttribute('antivirus', $antivirus));
|
||||
->setAttribute('antivirus', $antivirus)
|
||||
->setAttribute('transformations', $transformations));
|
||||
|
||||
$dbForProject->updateCollection('bucket_' . $bucket->getSequence(), $permissions, $fileSecurity);
|
||||
|
||||
@@ -974,13 +979,17 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
||||
/* @type Document $bucket */
|
||||
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
|
||||
|
||||
$isAPIKey = Auth::isAppUser(Authorization::getRoles());
|
||||
$isAppUser = Auth::isAppUser(Authorization::getRoles());
|
||||
$isPrivilegedUser = Auth::isPrivilegedUser(Authorization::getRoles());
|
||||
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAPIKey && !$isPrivilegedUser)) {
|
||||
if ($bucket->isEmpty() || (!$bucket->getAttribute('enabled') && !$isAppUser && !$isPrivilegedUser)) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$bucket->getAttribute('transformations', true) && !$isAppUser && !$isPrivilegedUser) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED);
|
||||
}
|
||||
|
||||
$isToken = !$resourceToken->isEmpty() && $resourceToken->getAttribute('bucketInternalId') === $bucket->getSequence();
|
||||
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
|
||||
$validator = new Authorization(Database::PERMISSION_READ);
|
||||
|
||||
@@ -594,6 +594,10 @@ App::init()
|
||||
throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND);
|
||||
}
|
||||
|
||||
if (!$bucket->getAttribute('transformations', true) && !$isAppUser && !$isPrivilegedUser) {
|
||||
throw new Exception(Exception::STORAGE_BUCKET_TRANSFORMATIONS_DISABLED);
|
||||
}
|
||||
|
||||
$fileSecurity = $bucket->getAttribute('fileSecurity', false);
|
||||
$validator = new Authorization(Database::PERMISSION_READ);
|
||||
$valid = $validator->isValid($bucket->getRead());
|
||||
|
||||
@@ -150,6 +150,7 @@ class Exception extends \Exception
|
||||
public const string STORAGE_INVALID_RANGE = 'storage_invalid_range';
|
||||
public const string STORAGE_INVALID_APPWRITE_ID = 'storage_invalid_appwrite_id';
|
||||
public const string STORAGE_FILE_NOT_PUBLIC = 'storage_file_not_public';
|
||||
public const string STORAGE_BUCKET_TRANSFORMATIONS_DISABLED = 'storage_bucket_transformations_disabled';
|
||||
|
||||
/** VCS */
|
||||
public const string INSTALLATION_NOT_FOUND = 'installation_not_found';
|
||||
|
||||
@@ -136,6 +136,16 @@ class V23 extends Migration
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
$this->createAttributeFromCollection(
|
||||
$this->dbForPlatform,
|
||||
'buckets',
|
||||
'transformations',
|
||||
);
|
||||
} catch (Throwable $th) {
|
||||
Console::warning("'transformations' from 'buckets': {$th->getMessage()}");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -10,7 +10,8 @@ class Buckets extends Base
|
||||
'fileSecurity',
|
||||
'maximumFileSize',
|
||||
'encryption',
|
||||
'antivirus'
|
||||
'antivirus',
|
||||
'transformations',
|
||||
];
|
||||
|
||||
/**
|
||||
|
||||
@@ -86,6 +86,12 @@ class Bucket extends Model
|
||||
'default' => true,
|
||||
'example' => false,
|
||||
])
|
||||
->addRule('transformations', [
|
||||
'type' => self::TYPE_BOOLEAN,
|
||||
'description' => 'Image transformations are enabled.',
|
||||
'default' => true,
|
||||
'example' => false,
|
||||
])
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
namespace Tests\E2E\Services\Storage;
|
||||
|
||||
use CURLFile;
|
||||
use Tests\E2E\Client;
|
||||
use Tests\E2E\Scopes\ProjectCustom;
|
||||
use Tests\E2E\Scopes\Scope;
|
||||
@@ -107,4 +108,56 @@ class StorageConsoleClientTest extends Scope
|
||||
$this->assertIsArray($response['body']['imageTransformations']);
|
||||
$this->assertIsNumeric($response['body']['imageTransformationsTotal']);
|
||||
}
|
||||
public function testCreateBucketTransformationsDisabledConsole(): void
|
||||
{
|
||||
// Create a bucket with default settings
|
||||
$bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'bucketId' => ID::unique(),
|
||||
'name' => 'Test Console Bucket Transformations Disabled',
|
||||
]);
|
||||
$this->assertEquals(201, $bucket['headers']['status-code']);
|
||||
|
||||
// Create a file in the bucket
|
||||
$file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucket['body']['$id'] . '/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', 'transformations.png'),
|
||||
]);
|
||||
$this->assertEquals(201, $file['headers']['status-code']);
|
||||
|
||||
// Try to get the file preview
|
||||
$preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucket['body']['$id'] . '/files/' . $file['body']['$id'] . '/preview', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals(200, $preview['headers']['status-code']);
|
||||
|
||||
// Update the bucket to disable transformations
|
||||
$bucket = $this->client->call(Client::METHOD_PUT, '/storage/buckets/' . $bucket['body']['$id'], array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()), [
|
||||
'name' => 'Test Bucket Transformations Disabled',
|
||||
'transformations' => false,
|
||||
]);
|
||||
|
||||
// Try to get the file preview again
|
||||
$preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucket['body']['$id'] . '/files/' . $file['body']['$id'] . '/preview', array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals(200, $preview['headers']['status-code']); // Returns 200 since image transformations are not counted for console requests
|
||||
|
||||
// Delete the bucket
|
||||
$this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucket['body']['$id'], array_merge([
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
], $this->getHeaders()));
|
||||
$this->assertEquals(200, $bucket['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1386,4 +1386,65 @@ class StorageCustomClientTest extends Scope
|
||||
$this->assertStringContainsString('users', $file['body']['message']);
|
||||
$this->assertStringContainsString('user:' . $this->getUser()['$id'], $file['body']['message']);
|
||||
}
|
||||
|
||||
public function testCreateBucketTransformationsDisabled(): void
|
||||
{
|
||||
// Create a bucket with default settings
|
||||
$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 Transformations Disabled',
|
||||
'permissions' => [
|
||||
Permission::read(Role::any())
|
||||
],
|
||||
]);
|
||||
$this->assertEquals(201, $bucket['headers']['status-code']);
|
||||
|
||||
// Create a file in the bucket
|
||||
$file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucket['body']['$id'] . '/files', [
|
||||
'content-type' => 'multipart/form-data',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'fileId' => ID::unique(),
|
||||
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'transformations.png'),
|
||||
]);
|
||||
$this->assertEquals(201, $file['headers']['status-code']);
|
||||
|
||||
// Try to get the file preview
|
||||
$preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucket['body']['$id'] . '/files/' . $file['body']['$id'] . '/preview', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]);
|
||||
$this->assertEquals(200, $preview['headers']['status-code']);
|
||||
|
||||
// Update the bucket to disable transformations
|
||||
$bucket = $this->client->call(Client::METHOD_PUT, '/storage/buckets/' . $bucket['body']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
], [
|
||||
'name' => 'Test Bucket Transformations Disabled',
|
||||
'transformations' => false,
|
||||
]);
|
||||
|
||||
// Try to get the file preview again
|
||||
$preview = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucket['body']['$id'] . '/files/' . $file['body']['$id'] . '/preview', [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
]);
|
||||
$this->assertEquals(403, $preview['headers']['status-code']);
|
||||
$this->assertStringContainsString('Image transformations are disabled for the requested bucket.', $preview['body']['message']);
|
||||
|
||||
// Delete the bucket
|
||||
$this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $bucket['body']['$id'], [
|
||||
'content-type' => 'application/json',
|
||||
'x-appwrite-project' => $this->getProject()['$id'],
|
||||
'x-appwrite-key' => $this->getProject()['apiKey'],
|
||||
]);
|
||||
$this->assertEquals(200, $bucket['headers']['status-code']);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user