Merge pull request #1299 from lohanidamodar/feat-sb-files

Feat Files with Storage Buckets
This commit is contained in:
Damodar Lohani
2021-06-29 13:19:13 +05:45
committed by GitHub
8 changed files with 1083 additions and 56 deletions
+1 -1
View File
@@ -32,7 +32,7 @@ _APP_SMTP_PORT=1025
_APP_SMTP_SECURE=
_APP_SMTP_USERNAME=
_APP_SMTP_PASSWORD=
_APP_STORAGE_LIMIT=10000000
_APP_STORAGE_LIMIT=30000000
_APP_FUNCTIONS_TIMEOUT=900
_APP_FUNCTIONS_CONTAINERS=10
_APP_FUNCTIONS_CPUS=1
+647 -8
View File
@@ -24,6 +24,7 @@ use Appwrite\Utopia\Response;
use Utopia\Config\Config;
use Utopia\Validator\Integer;
use Utopia\Database\Query;
use Utopia\Storage\Validator\FileExt;
App::post('/v1/storage/buckets')
->desc('Create storage bucket')
@@ -40,12 +41,12 @@ App::post('/v1/storage/buckets')
->param('name', '', new Text(128), 'Bucket name', false)
->param('read', [], new ArrayList(new Text(64)), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('write', [], new ArrayList(new Text(64)), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('maximumFileSize', 0, new Integer(), 'Maximum file size allowed.', true)
->param('allowedFileExtensions', ['*'], new ArrayList(new Text(64)), 'Allowed file extensions', true)
->param('maximumFileSize', (int) App::getEnv('_APP_STORAGE_LIMIT', 0) , new Integer(), 'Maximum file size allowed in bytes. Maximum allowed value is ' . App::getEnv('_APP_STORAGE_LIMIT', 0) . '. For self-hosted setups you can change the max limit by changing the `_APP_STORAGE_LIMIT` environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true)
->param('allowedFileExtensions', [], new ArrayList(new Text(64)), 'Allowed file extensions', true)
->param('enabled', true, new Boolean(), 'Is bucket enabled?', true)
->param('adapter', 'local', new WhiteList(['local']), 'Storage adapter.', true)
->param('encryption', true, new Boolean(), 'Is encryption enabled?', true)
->param('antiVirus', true, new Boolean(), 'Is virus scanning enabled?', true)
->param('encryption', true, new Boolean(), 'Is encryption enabled? For file size above ' . Storage::human(APP_LIMIT_ENCRYPTION) . ' encryption is skipped even if it\'s enabled', true)
->param('antiVirus', true, new Boolean(), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->inject('response')
->inject('dbForInternal')
->inject('audits')
@@ -151,11 +152,11 @@ App::put('/v1/storage/buckets/:bucketId')
->param('name', null, new Text(128), 'Bucket name', false)
->param('read', null, new ArrayList(new Text(64)), 'An array of strings with read permissions. By default inherits the existing read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('write', null, new ArrayList(new Text(64)), 'An array of strings with write permissions. By default inherits the existing write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('maximumFileSize', 0, new Integer(), 'Maximum file size allowed.', true)
->param('allowedFileExtensions', ['*'], new ArrayList(new Text(64)), 'Allowed file extensions', true)
->param('maximumFileSize', null, new Integer(), 'Maximum file size allowed in bytes. Maximum allowed value is ' . App::getEnv('_APP_STORAGE_LIMIT', 0) . '. For self hosted version you can change the limit by changing _APP_STORAGE_LIMIT environment variable. [Learn more about storage environment variables](docs/environment-variables#storage)', true)
->param('allowedFileExtensions', [], new ArrayList(new Text(64)), 'Allowed file extensions', true)
->param('enabled', true, new Boolean(), 'Is bucket enabled?', true)
->param('encryption', true, new Boolean(), 'Is encryption enabled?', true)
->param('antiVirus', true, new Boolean(), 'Is virus scanning enabled?', true)
->param('encryption', true, new Boolean(), 'Is encryption enabled? For file size above ' . Storage::human(APP_LIMIT_ENCRYPTION) . ' encryption is skipped even if it\'s enabled', true)
->param('antiVirus', true, new Boolean(), 'Is virus scanning enabled? For file size above ' . Storage::human(APP_LIMIT_ANTIVIRUS) . ' AntiVirus scanning is skipped even if it\'s enabled', true)
->inject('response')
->inject('dbForInternal')
->inject('audits')
@@ -245,6 +246,644 @@ App::delete('/v1/storage/buckets/:bucketId')
$response->noContent();
});
App::post('/v1/storage/buckets/:bucketId/files')
->desc('Create File')
->groups(['api', 'storage'])
->label('scope', 'files.write')
->label('event', 'storage.files.create')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'createFile')
->label('sdk.description', '/docs/references/storage/create-file.md')
->label('sdk.request.type', 'multipart/form-data')
->label('sdk.methodType', 'upload')
->label('sdk.response.code', Response::STATUS_CODE_CREATED)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FILE)
->param('bucketId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).')
->param('file', [], new File(), 'Binary file.', false)
->param('read', null, new ArrayList(new Text(64)), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->param('write', null, new ArrayList(new Text(64)), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.', true)
->inject('request')
->inject('response')
->inject('dbForInternal')
->inject('user')
->inject('audits')
->inject('usage')
->action(function ($bucketId, $file, $read, $write, $request, $response, $dbForInternal, $user, $audits, $usage) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Database\Document $user */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $usage */
$bucket = $dbForInternal->getDocument('buckets', $bucketId);
if($bucket->isEmpty()) {
throw new Exception('Bucket not found', 404);
}
$file = $request->getFiles('file');
/*
* Validators
*/
$allowedFileExtensions = $bucket->getAttribute('allowedFileExtensions', []);
$fileExt = new FileExt($allowedFileExtensions);
$maximumFileSize = $bucket->getAttribute('maximumFileSize', 0);
if($maximumFileSize > (int) App::getEnv('_APP_STORAGE_LIMIT',0)) {
throw new Exception('Error bucket maximum file size is larger than _APP_STORAGE_LIMIT', 500);
}
$fileSize = new FileSize($maximumFileSize);
$upload = new Upload();
if (empty($file)) {
throw new Exception('No file sent', 400);
}
// Make sure we handle a single file and multiple files the same way
$file['name'] = (\is_array($file['name']) && isset($file['name'][0])) ? $file['name'][0] : $file['name'];
$file['tmp_name'] = (\is_array($file['tmp_name']) && isset($file['tmp_name'][0])) ? $file['tmp_name'][0] : $file['tmp_name'];
$file['size'] = (\is_array($file['size']) && isset($file['size'][0])) ? $file['size'][0] : $file['size'];
// Check if file type is allowed (feature for project settings?)
if (!empty($allowedFileExtensions) && !$fileExt->isValid($file['name'])) {
throw new Exception('File extension not allowed', 400);
}
if (!$fileSize->isValid($file['size'])) { // Check if file size is exceeding allowed limit
throw new Exception('File size not allowed', 400);
}
$device = Storage::getDevice('files');
if (!$upload->isValid($file['tmp_name'])) {
throw new Exception('Invalid file', 403);
}
// Save to storage
$size = $device->getFileSize($file['tmp_name']);
$path = $device->getPath(\uniqid().'.'.\pathinfo($file['name'], PATHINFO_EXTENSION));
$path = $bucket->getId() . '/' . $path;
if (!$device->upload($file['tmp_name'], $path)) { // TODO deprecate 'upload' and replace with 'move'
throw new Exception('Failed moving file', 500);
}
$mimeType = $device->getFileMimeType($path); // Get mime-type before compression and encryption
if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled' && $bucket->getAttribute('antiVirus', true) && $size <= APP_LIMIT_ANTIVIRUS) {
$antiVirus = new Network(App::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'),
(int) App::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310));
if (!$antiVirus->fileScan($path)) {
$device->delete($path);
throw new Exception('Invalid file', 403);
}
}
// Compression
$data = $device->read($path);
if($size <= APP_LIMIT_COMPRESSION) {
$compressor = new GZIP();
$data = $compressor->compress($data);
}
if($bucket->getAttribute('encryption', true) && $size <= APP_LIMIT_ENCRYPTION) {
$key = App::getEnv('_APP_OPENSSL_KEY_V1');
$iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM));
$data = OpenSSL::encrypt($data, OpenSSL::CIPHER_AES_128_GCM, $key, 0, $iv, $tag);
}
if (!$device->write($path, $data, $mimeType)) {
throw new Exception('Failed to save file', 500);
}
$sizeActual = $device->getFileSize($path);
$data = [
'$read' => (is_null($read) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $read ?? [], // By default set read permissions for user
'$write' => (is_null($write) && !$user->isEmpty()) ? ['user:'.$user->getId()] : $write ?? [], // By default set write permissions for user
'dateCreated' => \time(),
'bucketId' => $bucket->getId(),
'name' => $file['name'],
'path' => $path,
'signature' => $device->getFileHash($path),
'mimeType' => $mimeType,
'sizeOriginal' => $size,
'sizeActual' => $sizeActual,
'algorithm' => empty($compressor) ? '' : $compressor->getName(),
'comment' => '',
];
if($bucket->getAttribute('encryption', true) && $size <= APP_LIMIT_ENCRYPTION) {
$data['openSSLVersion'] = '1';
$data['openSSLCipher'] = OpenSSL::CIPHER_AES_128_GCM;
$data['openSSLTag'] = \bin2hex($tag);
$data['openSSLIV'] = \bin2hex($iv);
}
$file = $dbForInternal->createDocument('files', new Document($data));
$audits
->setParam('event', 'storage.files.create')
->setParam('resource', 'storage/files/'.$file->getId())
;
$usage
->setParam('storage', $sizeActual)
;
$response->setStatusCode(Response::STATUS_CODE_CREATED);
$response->dynamic2($file, Response::MODEL_FILE);
});
App::get('/v1/storage/buckets/:bucketId/files')
->desc('List Files')
->groups(['api', 'storage'])
->label('scope', 'files.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'listFiles')
->label('sdk.description', '/docs/references/storage/list-files.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FILE_LIST)
->param('bucketId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).')
->param('search', '', new Text(256), 'Search term to filter your list results. Max length: 256 chars.', true)
->param('limit', 25, new Range(0, 100), 'Results limit value. By default will return maximum 25 results. Maximum of 100 results allowed per request.', true)
->param('offset', 0, new Range(0, 2000), 'Results offset. The default value is 0. Use this param to manage pagination.', true)
->param('orderType', 'ASC', new WhiteList(['ASC', 'DESC'], true), 'Order result by ASC or DESC order.', true)
->inject('response')
->inject('dbForInternal')
->action(function ($bucketId, $search, $limit, $offset, $orderType, $response, $dbForInternal) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
$bucket = $dbForInternal->getDocument('buckets', $bucketId);
if($bucket->isEmpty()) {
throw new Exception('Bucket not found', 404);
}
$queries = [new Query('bucketId', Query::TYPE_EQUAL, [$bucketId])];
if($search) {
$queries[] = [new Query('name', Query::TYPE_SEARCH, [$search])];
}
$response->dynamic2(new Document([
'files' => $dbForInternal->find('files', $queries, $limit, $offset, ['_id'], [$orderType]),
'sum' => $dbForInternal->count('files', $queries, APP_LIMIT_COUNT),
]), Response::MODEL_FILE_LIST);
});
App::get('/v1/storage/buckets/:bucketId/files/:fileId')
->desc('Get File')
->groups(['api', 'storage'])
->label('scope', 'files.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'getFile')
->label('sdk.description', '/docs/references/storage/get-file.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FILE)
->param('bucketId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).')
->param('fileId', '', new UID(), 'File unique ID.')
->inject('response')
->inject('dbForInternal')
->action(function ($bucketId, $fileId, $response, $dbForInternal) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
$bucket = $dbForInternal->getDocument('buckets', $bucketId);
if($bucket->isEmpty()) {
throw new Exception('Bucket not found', 404);
}
$file = $dbForInternal->getDocument('files', $fileId);
if ($file->isEmpty() || $file->getAttribute('bucketId') != $bucketId) {
throw new Exception('File not found', 404);
}
$response->dynamic2($file, Response::MODEL_FILE);
});
App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->desc('Get File Preview')
->groups(['api', 'storage'])
->label('scope', 'files.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'getFilePreview')
->label('sdk.description', '/docs/references/storage/get-file-preview.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_IMAGE)
->label('sdk.methodType', 'location')
->param('bucketId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).')
->param('fileId', '', new UID(), 'File unique ID')
->param('width', 0, new Range(0, 4000), 'Resize preview image width, Pass an integer between 0 to 4000.', true)
->param('height', 0, new Range(0, 4000), 'Resize preview image height, Pass an integer between 0 to 4000.', true)
->param('gravity', Image::GRAVITY_CENTER, new WhiteList([Image::GRAVITY_CENTER, Image::GRAVITY_NORTH, Image::GRAVITY_NORTHWEST, Image::GRAVITY_NORTHEAST, Image::GRAVITY_WEST, Image::GRAVITY_EAST, Image::GRAVITY_SOUTHWEST, Image::GRAVITY_SOUTH, Image::GRAVITY_SOUTHEAST]), 'Image crop gravity', true)
->param('quality', 100, new Range(0, 100), 'Preview image quality. Pass an integer between 0 to 100. Defaults to 100.', true)
->param('borderWidth', 0, new Range(0, 100), 'Preview image border in pixels. Pass an integer between 0 to 100. Defaults to 0.', true)
->param('borderColor', '', new HexColor(), 'Preview image border color. Use a valid HEX color, no # is needed for prefix.', true)
->param('borderRadius', 0, new Range(0, 4000), 'Preview image border radius in pixels. Pass an integer between 0 to 4000.', true)
->param('opacity', 1, new Range(0,1, Range::TYPE_FLOAT), 'Preview image opacity. Only works with images having an alpha channel (like png). Pass a number between 0 to 1.', true)
->param('rotation', 0, new Range(0,360), 'Preview image rotation in degrees. Pass an integer between 0 and 360.', true)
->param('background', '', new HexColor(), 'Preview image background color. Only works with transparent images (png). Use a valid HEX color, no # is needed for prefix.', true)
->param('output', '', new WhiteList(\array_keys(Config::getParam('storage-outputs')), true), 'Output format type (jpeg, jpg, png, gif and webp).', true)
->inject('request')
->inject('response')
->inject('project')
->inject('dbForInternal')
->action(function ($bucketId, $fileId, $width, $height, $gravity, $quality, $borderWidth, $borderColor, $borderRadius, $opacity, $rotation, $background, $output, $request, $response, $project, $dbForInternal) {
/** @var Utopia\Swoole\Request $request */
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Document $project */
/** @var Utopia\Database\Database $dbForInternal */
$storage = 'files';
if (!\extension_loaded('imagick')) {
throw new Exception('Imagick extension is missing', 500);
}
if (!Storage::exists($storage)) {
throw new Exception('No such storage device', 400);
}
$bucket = $dbForInternal->getDocument('buckets', $bucketId);
if($bucket->isEmpty()) {
throw new Exception('Bucket not found', 404);
}
if ((\strpos($request->getAccept(), 'image/webp') === false) && ('webp' == $output)) { // Fallback webp to jpeg when no browser support
$output = 'jpg';
}
$inputs = Config::getParam('storage-inputs');
$outputs = Config::getParam('storage-outputs');
$fileLogos = Config::getParam('storage-logos');
$date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT'; // 45 days cache
$key = \md5($fileId.$width.$height.$quality.$borderWidth.$borderColor.$borderRadius.$opacity.$rotation.$background.$storage.$output);
$file = $dbForInternal->getDocument('files', $fileId);
if ($file->isEmpty() || $file->getAttribute('bucketId') != $bucketId) {
throw new Exception('File not found', 404);
}
$path = $file->getAttribute('path');
$type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
$algorithm = $file->getAttribute('algorithm');
$cipher = $file->getAttribute('openSSLCipher');
$mime = $file->getAttribute('mimeType');
if (!\in_array($mime, $inputs)) {
$path = (\array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default'];
$algorithm = null;
$cipher = null;
$background = (empty($background)) ? 'eceff1' : $background;
$type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
$key = \md5($path.$width.$height.$quality.$borderWidth.$borderColor.$borderRadius.$opacity.$rotation.$background.$storage.$output);
}
$compressor = new GZIP();
$device = Storage::getDevice('files');
if (!\file_exists($path)) {
throw new Exception('File not found', 404);
}
$cache = new Cache(new Filesystem(APP_STORAGE_CACHE.'/app-'.$project->getId())); // Limit file number or size
$data = $cache->load($key, 60 * 60 * 24 * 30 * 3 /* 3 months */);
if ($data) {
$output = (empty($output)) ? $type : $output;
return $response
->setContentType((\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'])
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data)
;
}
$source = $device->read($path);
if (!empty($cipher)) { // Decrypt
$source = OpenSSL::decrypt(
$source,
$file->getAttribute('openSSLCipher'),
App::getEnv('_APP_OPENSSL_KEY_V'.$file->getAttribute('openSSLVersion')),
0,
\hex2bin($file->getAttribute('openSSLIV')),
\hex2bin($file->getAttribute('openSSLTag'))
);
}
if (!empty($algorithm)) {
$source = $compressor->decompress($source);
}
$image = new Image($source);
$image->crop((int) $width, (int) $height, $gravity);
if (!empty($opacity) || $opacity==0) {
$image->setOpacity($opacity);
}
if (!empty($background)) {
$image->setBackground('#'.$background);
}
if (!empty($borderWidth) ) {
$image->setBorder($borderWidth, '#'.$borderColor);
}
if (!empty($borderRadius)) {
$image->setBorderRadius($borderRadius);
}
if (!empty($rotation)) {
$image->setRotation($rotation);
}
$output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality);
$cache->save($key, $data);
$response
->setContentType($outputs[$output])
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'miss')
->send($data)
;
unset($image);
});
App::get('/v1/storage/buckets/:bucketId/files/:fileId/download')
->desc('Get File for Download')
->groups(['api', 'storage'])
->label('scope', 'files.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'getFileDownload')
->label('sdk.description', '/docs/references/storage/get-file-download.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', '*/*')
->label('sdk.methodType', 'location')
->param('bucketId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).')
->param('fileId', '', new UID(), 'File unique ID.')
->inject('response')
->inject('dbForInternal')
->action(function ($bucketId, $fileId, $response, $dbForInternal) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
$bucket = $dbForInternal->getDocument('buckets', $bucketId);
if($bucket->isEmpty()) {
throw new Exception('Bucket not found', 404);
}
$file = $dbForInternal->getDocument('files', $fileId);
if ($file->isEmpty() || $file->getAttribute('bucketId') != $bucketId) {
throw new Exception('File not found', 404);
}
$path = $file->getAttribute('path', '');
if (!\file_exists($path)) {
throw new Exception('File not found in '.$path, 404);
}
$device = Storage::getDevice('files');
$source = $device->read($path);
if (!empty($file->getAttribute('openSSLCipher'))) { // Decrypt
$source = OpenSSL::decrypt(
$source,
$file->getAttribute('openSSLCipher'),
App::getEnv('_APP_OPENSSL_KEY_V'.$file->getAttribute('openSSLVersion')),
0,
\hex2bin($file->getAttribute('openSSLIV')),
\hex2bin($file->getAttribute('openSSLTag'))
);
}
if(!empty($file->getAttribute('algorithm', ''))) {
$compressor = new GZIP();
$source = $compressor->decompress($source);
}
// Response
$response
->setContentType($file->getAttribute('mimeType'))
->addHeader('Content-Disposition', 'attachment; filename="'.$file->getAttribute('name', '').'"')
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache
->addHeader('X-Peak', \memory_get_peak_usage())
->send($source)
;
});
App::get('/v1/storage/buckets/:bucketId/files/:fileId/view')
->desc('Get File for View')
->groups(['api', 'storage'])
->label('scope', 'files.read')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'getFileView')
->label('sdk.description', '/docs/references/storage/get-file-view.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', '*/*')
->label('sdk.methodType', 'location')
->param('bucketId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).')
->param('fileId', '', new UID(), 'File unique ID.')
->inject('response')
->inject('dbForInternal')
->action(function ($bucketId, $fileId, $response, $dbForInternal) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
$bucket = $dbForInternal->getDocument('buckets', $bucketId);
if($bucket->isEmpty()) {
throw new Exception('Bucket not found', 404);
}
$file = $dbForInternal->getDocument('files', $fileId);
$mimes = Config::getParam('storage-mimes');
if ($file->isEmpty() || $file->getAttribute('bucketId') != $bucketId) {
throw new Exception('File not found', 404);
}
$path = $file->getAttribute('path', '');
if (!\file_exists($path)) {
throw new Exception('File not found in '.$path, 404);
}
$compressor = new GZIP();
$device = Storage::getDevice('files');
$contentType = 'text/plain';
if (\in_array($file->getAttribute('mimeType'), $mimes)) {
$contentType = $file->getAttribute('mimeType');
}
$source = $device->read($path);
if (!empty($file->getAttribute('openSSLCipher'))) { // Decrypt
$source = OpenSSL::decrypt(
$source,
$file->getAttribute('openSSLCipher'),
App::getEnv('_APP_OPENSSL_KEY_V'.$file->getAttribute('openSSLVersion')),
0,
\hex2bin($file->getAttribute('openSSLIV')),
\hex2bin($file->getAttribute('openSSLTag'))
);
}
$output = $compressor->decompress($source);
$fileName = $file->getAttribute('name', '');
// Response
$response
->setContentType($contentType)
->addHeader('Content-Security-Policy', 'script-src none;')
->addHeader('X-Content-Type-Options', 'nosniff')
->addHeader('Content-Disposition', 'inline; filename="'.$fileName.'"')
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)).' GMT') // 45 days cache
->addHeader('X-Peak', \memory_get_peak_usage())
->send($output)
;
});
App::put('/v1/storage/buckets/:bucketId/files/:fileId')
->desc('Update File')
->groups(['api', 'storage'])
->label('scope', 'files.write')
->label('event', 'storage.files.update')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'updateFile')
->label('sdk.description', '/docs/references/storage/update-file.md')
->label('sdk.response.code', Response::STATUS_CODE_OK)
->label('sdk.response.type', Response::CONTENT_TYPE_JSON)
->label('sdk.response.model', Response::MODEL_FILE)
->param('bucketId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).')
->param('fileId', '', new UID(), 'File unique ID.')
->param('read', [], new ArrayList(new Text(64)), 'An array of strings with read permissions. By default no user is granted with any read permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
->param('write', [], new ArrayList(new Text(64)), 'An array of strings with write permissions. By default no user is granted with any write permissions. [learn more about permissions](/docs/permissions) and get a full list of available permissions.')
->inject('response')
->inject('dbForInternal')
->inject('audits')
->action(function ($bucketId, $fileId, $read, $write, $response, $dbForInternal, $audits) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $audits */
$bucket = $dbForInternal->getDocument('buckets', $bucketId);
if($bucket->isEmpty()) {
throw new Exception('Bucket not found', 404);
}
$file = $dbForInternal->getDocument('files', $fileId);
if ($file->isEmpty() || $file->getAttribute('bucketId') != $bucketId) {
throw new Exception('File not found', 404);
}
$file = $dbForInternal->updateDocument('files', $fileId, $file
->setAttribute('$read', $read)
->setAttribute('$write', $write)
);
$audits
->setParam('event', 'storage.files.update')
->setParam('resource', 'storage/files/'.$file->getId())
;
$response->dynamic2($file, Response::MODEL_FILE);
});
App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->desc('Delete File')
->groups(['api', 'storage'])
->label('scope', 'files.write')
->label('event', 'storage.files.delete')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage')
->label('sdk.method', 'deleteFile')
->label('sdk.description', '/docs/references/storage/delete-file.md')
->label('sdk.response.code', Response::STATUS_CODE_NOCONTENT)
->label('sdk.response.model', Response::MODEL_NONE)
->param('bucketId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).')
->param('fileId', '', new UID(), 'File unique ID.')
->inject('response')
->inject('dbForInternal')
->inject('events')
->inject('audits')
->inject('usage')
->action(function ($bucketId, $fileId, $response, $dbForInternal, $events, $audits, $usage) {
/** @var Appwrite\Utopia\Response $response */
/** @var Utopia\Database\Database $dbForInternal */
/** @var Appwrite\Event\Event $events */
/** @var Appwrite\Event\Event $audits */
/** @var Appwrite\Event\Event $usage */
$bucket = $dbForInternal->getDocument('buckets', $bucketId);
if($bucket->isEmpty()) {
throw new Exception('Bucket not found', 404);
}
$file = $dbForInternal->getDocument('files', $fileId);
if ($file->isEmpty() || $file->getAttribute('bucketId') != $bucketId) {
throw new Exception('File not found', 404);
}
$device = Storage::getDevice('files');
if ($device->delete($file->getAttribute('path', ''))) {
if (!$dbForInternal->deleteDocument('files', $fileId)) {
throw new Exception('Failed to remove file from DB', 500);
}
}
$audits
->setParam('event', 'storage.files.delete')
->setParam('resource', 'storage/files/'.$file->getId())
;
$usage
->setParam('storage', $file->getAttribute('size', 0) * -1)
;
$events
->setParam('eventData', $response->output2($file, Response::MODEL_FILE))
;
$response->noContent();
});
App::post('/v1/storage/files')
->desc('Create File')
->groups(['api', 'storage'])
+3
View File
@@ -47,6 +47,9 @@ const APP_MODE_ADMIN = 'admin';
const APP_PAGING_LIMIT = 12;
const APP_LIMIT_COUNT = 5000;
const APP_LIMIT_USERS = 10000;
const APP_LIMIT_ANTIVIRUS = 20971520; //20MB
const APP_LIMIT_ENCRYPTION = 20971520; //20MB
const APP_LIMIT_COMPRESSION = 20971520; //20MB
const APP_CACHE_BUSTER = 148;
const APP_VERSION_STABLE = '0.8.0';
const APP_STORAGE_UPLOADS = '/storage/uploads';
Generated
+29 -28
View File
@@ -1607,7 +1607,7 @@
"source": {
"type": "git",
"url": "https://github.com/lohanidamodar/abuse",
"reference": "351dba60714321fabcd5028fdf7325f18f343018"
"reference": "8486a4f95248e5a9fb1fe9b650278b264857e929"
},
"require": {
"ext-pdo": "*",
@@ -1641,7 +1641,7 @@
"upf",
"utopia"
],
"time": "2021-06-13T07:41:20+00:00"
"time": "2021-06-18T06:21:55+00:00"
},
{
"name": "utopia-php/analytics",
@@ -1704,7 +1704,7 @@
"source": {
"type": "git",
"url": "https://github.com/lohanidamodar/audit",
"reference": "d5591321161b81043c45be3c53ce9dcfa2d81d72"
"reference": "063c1d527776c85d0ab6d8ffcde694f7813170a6"
},
"require": {
"ext-pdo": "*",
@@ -1738,7 +1738,7 @@
"upf",
"utopia"
],
"time": "2021-06-14T07:38:18+00:00"
"time": "2021-06-18T06:19:19+00:00"
},
{
"name": "utopia-php/cache",
@@ -2326,16 +2326,16 @@
},
{
"name": "utopia-php/swoole",
"version": "0.2.3",
"version": "0.2.4",
"source": {
"type": "git",
"url": "https://github.com/utopia-php/swoole.git",
"reference": "45c42aae7e7d3f9f82bf194c2cfa5499b674aefe"
"reference": "37d8c64b536d6bc7da4f0f5a934a0ec44885abf4"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/45c42aae7e7d3f9f82bf194c2cfa5499b674aefe",
"reference": "45c42aae7e7d3f9f82bf194c2cfa5499b674aefe",
"url": "https://api.github.com/repos/utopia-php/swoole/zipball/37d8c64b536d6bc7da4f0f5a934a0ec44885abf4",
"reference": "37d8c64b536d6bc7da4f0f5a934a0ec44885abf4",
"shasum": ""
},
"require": {
@@ -2376,9 +2376,9 @@
],
"support": {
"issues": "https://github.com/utopia-php/swoole/issues",
"source": "https://github.com/utopia-php/swoole/tree/0.2.3"
"source": "https://github.com/utopia-php/swoole/tree/0.2.4"
},
"time": "2021-03-22T22:39:24+00:00"
"time": "2021-06-22T10:49:24+00:00"
},
{
"name": "utopia-php/system",
@@ -5069,20 +5069,21 @@
"type": "github"
}
],
"abandoned": true,
"time": "2020-09-28T06:45:17+00:00"
},
{
"name": "sebastian/type",
"version": "2.3.2",
"version": "2.3.4",
"source": {
"type": "git",
"url": "https://github.com/sebastianbergmann/type.git",
"reference": "0d1c587401514d17e8f9258a27e23527cb1b06c1"
"reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/0d1c587401514d17e8f9258a27e23527cb1b06c1",
"reference": "0d1c587401514d17e8f9258a27e23527cb1b06c1",
"url": "https://api.github.com/repos/sebastianbergmann/type/zipball/b8cd8a1c753c90bc1a0f5372170e3e489136f914",
"reference": "b8cd8a1c753c90bc1a0f5372170e3e489136f914",
"shasum": ""
},
"require": {
@@ -5117,7 +5118,7 @@
"homepage": "https://github.com/sebastianbergmann/type",
"support": {
"issues": "https://github.com/sebastianbergmann/type/issues",
"source": "https://github.com/sebastianbergmann/type/tree/2.3.2"
"source": "https://github.com/sebastianbergmann/type/tree/2.3.4"
},
"funding": [
{
@@ -5125,7 +5126,7 @@
"type": "github"
}
],
"time": "2021-06-04T13:02:07+00:00"
"time": "2021-06-15T12:49:02+00:00"
},
{
"name": "sebastian/version",
@@ -5234,16 +5235,16 @@
},
{
"name": "symfony/console",
"version": "v5.3.0",
"version": "v5.3.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/console.git",
"reference": "058553870f7809087fa80fa734704a21b9bcaeb2"
"reference": "649730483885ff2ca99ca0560ef0e5f6b03f2ac1"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/console/zipball/058553870f7809087fa80fa734704a21b9bcaeb2",
"reference": "058553870f7809087fa80fa734704a21b9bcaeb2",
"url": "https://api.github.com/repos/symfony/console/zipball/649730483885ff2ca99ca0560ef0e5f6b03f2ac1",
"reference": "649730483885ff2ca99ca0560ef0e5f6b03f2ac1",
"shasum": ""
},
"require": {
@@ -5312,7 +5313,7 @@
"terminal"
],
"support": {
"source": "https://github.com/symfony/console/tree/v5.3.0"
"source": "https://github.com/symfony/console/tree/v5.3.2"
},
"funding": [
{
@@ -5328,7 +5329,7 @@
"type": "tidelift"
}
],
"time": "2021-05-26T17:43:10+00:00"
"time": "2021-06-12T09:42:48+00:00"
},
{
"name": "symfony/deprecation-contracts",
@@ -5802,16 +5803,16 @@
},
{
"name": "symfony/string",
"version": "v5.3.0",
"version": "v5.3.2",
"source": {
"type": "git",
"url": "https://github.com/symfony/string.git",
"reference": "a9a0f8b6aafc5d2d1c116dcccd1573a95153515b"
"reference": "0732e97e41c0a590f77e231afc16a327375d50b0"
},
"dist": {
"type": "zip",
"url": "https://api.github.com/repos/symfony/string/zipball/a9a0f8b6aafc5d2d1c116dcccd1573a95153515b",
"reference": "a9a0f8b6aafc5d2d1c116dcccd1573a95153515b",
"url": "https://api.github.com/repos/symfony/string/zipball/0732e97e41c0a590f77e231afc16a327375d50b0",
"reference": "0732e97e41c0a590f77e231afc16a327375d50b0",
"shasum": ""
},
"require": {
@@ -5865,7 +5866,7 @@
"utf8"
],
"support": {
"source": "https://github.com/symfony/string/tree/v5.3.0"
"source": "https://github.com/symfony/string/tree/v5.3.2"
},
"funding": [
{
@@ -5881,7 +5882,7 @@
"type": "tidelift"
}
],
"time": "2021-05-26T17:43:10+00:00"
"time": "2021-06-06T09:51:56+00:00"
},
{
"name": "theseer/tokenizer",
+22
View File
@@ -0,0 +1,22 @@
<?php
namespace Tests\E2E\Scopes;
trait SideConsole
{
public function getHeaders():array
{
return [
'origin' => 'http://localhost',
'cookie' => 'a_session_'.$this->getProject()['$id'].'=' . $this->getRoot()['session'],
];
}
/**
* @return string
*/
public function getSide()
{
return 'console';
}
}
+379 -17
View File
@@ -4,11 +4,374 @@ namespace Tests\E2E\Services\Storage;
use CURLFile;
use Tests\E2E\Client;
use Utopia\Image\Image;
trait StorageBase
{
public function testCreateFile():array
public function testCreateBucketFile(): array
{
/**
* Test for SUCCESS
*/
$bucket = $this->client->call(Client::METHOD_POST, '/storage/buckets', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], $this->getHeaders()), [
'name' => 'Test Bucket',
'maximumFileSize' => 2000000, //2MB
'allowedFileExtensions' => ["jpg", "png"],
'read' => ['role:all'],
'write' => ['role:all'],
]);
$this->assertEquals(201, $bucket['headers']['status-code']);
$this->assertNotEmpty($bucket['body']['$id']);
$bucketId = $bucket['body']['$id'];
$file = $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()), [
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
'read' => ['role:all'],
'write' => ['role:all'],
]);
$this->assertEquals(201, $file['headers']['status-code']);
$this->assertNotEmpty($file['body']['$id']);
$this->assertIsInt($file['body']['dateCreated']);
$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
/**
* Test for Large File above 20MB
* This should also validate the test for when Bucket encryption
* is disabled as we are using same test
*/
$bucket2 = $this->client->call(Client::METHOD_POST, '/storage/buckets', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
'x-appwrite-key' => $this->getProject()['apiKey'],
], $this->getHeaders()), [
'name' => 'Test Bucket 2',
'read' => ['role:all'],
'write' => ['role:all'],
]);
$this->assertEquals(201, $bucket2['headers']['status-code']);
$this->assertNotEmpty($bucket2['body']['$id']);
$file2 = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucket2['body']['$id'] . '/files', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/disk-a/large-file.mp4'), 'video/mp4', 'large-file.mp4'),
'read' => ['role:all'],
'write' => ['role:all'],
]);
$this->assertEquals(201, $file2['headers']['status-code']);
$this->assertNotEmpty($file2['body']['$id']);
$this->assertIsInt($file2['body']['dateCreated']);
$this->assertEquals('large-file.mp4', $file2['body']['name']);
$this->assertEquals('video/mp4', $file2['body']['mimeType']);
$this->assertEquals(23660615, $file2['body']['sizeOriginal']);
$this->assertEquals(md5_file(realpath(__DIR__ . '/../../../resources/disk-a/large-file.mp4')), $file2['body']['signature']); // should validate that the file is not encrypted
/**
* Test for FAILURE unknown Bucket
*/
$res = $this->client->call(Client::METHOD_POST, '/storage/buckets/empty/files', array_merge([
'content-type' => 'multipart/form-data',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/logo.png'), 'image/png', 'logo.png'),
'read' => ['role:all'],
'write' => ['role:all'],
]);
$this->assertEquals(404, $res['headers']['status-code']);
/**
* Test for FAILURE file above bucket's file size limit
*/
$res = $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()), [
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/disk-b/kitten-1.png'), 'image/png', 'kitten-1.png'),
'read' => ['role:all'],
'write' => ['role:all'],
]);
$this->assertEquals(400, $res['headers']['status-code']);
$this->assertEquals('File size not allowed', $res['body']['message']);
/**
* Test for FAILURE unsupported bucket file extension
*/
$res = $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()), [
'file' => new CURLFile(realpath(__DIR__ . '/../../../resources/disk-a/kitten-3.gif'), 'image/gif', 'kitten-3.gif'),
'read' => ['role:all'],
'write' => ['role:all'],
]);
$this->assertEquals(400, $res['headers']['status-code']);
$this->assertEquals('File extension not allowed', $res['body']['message']);
return ['bucketId' => $bucketId, 'fileId' => $file['body']['$id'], 'largeFileId' => $file2['body']['$id'], 'largeBucketId' => $bucket2['body']['$id']];
}
/**
* @depends testCreateBucketFile
*/
public function testListBucketFiles(array $data): array
{
/**
* Test for SUCCESS
*/
$files = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $data['bucketId'] . '/files', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $files['headers']['status-code']);
$this->assertGreaterThan(0, $files['body']['sum']);
$this->assertGreaterThan(0, count($files['body']['files']));
/**
* Test for FAILURE unknown Bucket
*/
$files = $this->client->call(Client::METHOD_GET, '/storage/buckets/empty/files', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(404, $files['headers']['status-code']);
return $data;
}
/**
* @depends testCreateBucketFile
*/
public function testGetBucketFile(array $data): array
{
$bucketId = $data['bucketId'];
/**
* Test for SUCCESS
*/
$file1 = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $data['fileId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $file1['headers']['status-code']);
$this->assertNotEmpty($file1['body']['$id']);
$this->assertIsInt($file1['body']['dateCreated']);
$this->assertEquals('logo.png', $file1['body']['name']);
$this->assertEquals('image/png', $file1['body']['mimeType']);
$this->assertEquals(47218, $file1['body']['sizeOriginal']);
//$this->assertEquals(54944, $file1['body']['sizeActual']);
//$this->assertEquals('gzip', $file1['body']['algorithm']);
//$this->assertEquals('1', $file1['body']['fileOpenSSLVersion']);
//$this->assertEquals('aes-128-gcm', $file1['body']['fileOpenSSLCipher']);
//$this->assertNotEmpty($file1['body']['fileOpenSSLTag']);
//$this->assertNotEmpty($file1['body']['fileOpenSSLIV']);
$this->assertIsArray($file1['body']['$read']);
$this->assertIsArray($file1['body']['$write']);
$this->assertCount(1, $file1['body']['$read']);
$this->assertCount(1, $file1['body']['$write']);
$file2 = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $data['fileId'] . '/preview', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $file2['headers']['status-code']);
$this->assertEquals('image/png', $file2['headers']['content-type']);
$this->assertNotEmpty($file2['body']);
//new image preview features
$file3 = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $data['fileId'] . '/preview', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'width' => 300,
'height' => 100,
'borderRadius' => '50',
'opacity' => '0.5',
'output' => 'png',
'rotation' => '45',
]);
$this->assertEquals(200, $file3['headers']['status-code']);
$this->assertEquals('image/png', $file3['headers']['content-type']);
$this->assertNotEmpty($file3['body']);
$image = new \Imagick();
$image->readImageBlob($file3['body']);
$original = new \Imagick(__DIR__ . '/../../../resources/logo-after.png');
$this->assertEquals($image->getImageWidth(), $original->getImageWidth());
$this->assertEquals($image->getImageHeight(), $original->getImageHeight());
$this->assertEquals('PNG', $image->getImageFormat());
$file4 = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $data['fileId'] . '/preview', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'width' => 200,
'height' => 80,
'borderWidth' => '5',
'borderColor' => 'ff0000',
'output' => 'jpg',
]);
$this->assertEquals(200, $file4['headers']['status-code']);
$this->assertEquals('image/jpeg', $file4['headers']['content-type']);
$this->assertNotEmpty($file4['body']);
$image = new \Imagick();
$image->readImageBlob($file4['body']);
$original = new \Imagick(__DIR__ . '/../../../resources/logo-after.jpg');
$this->assertEquals($image->getImageWidth(), $original->getImageWidth());
$this->assertEquals($image->getImageHeight(), $original->getImageHeight());
$this->assertEquals('JPEG', $image->getImageFormat());
$file5 = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $data['fileId'] . '/download', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $file5['headers']['status-code']);
$this->assertEquals('attachment; filename="logo.png"', $file5['headers']['content-disposition']);
$this->assertEquals('image/png', $file5['headers']['content-type']);
$this->assertNotEmpty($file5['body']);
$file6 = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $bucketId . '/files/' . $data['fileId'] . '/view', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(200, $file6['headers']['status-code']);
$this->assertEquals('image/png', $file6['headers']['content-type']);
$this->assertNotEmpty($file6['body']);
/**
* Test large files decompress successfully
*/
$file7 = $this->client->call(Client::METHOD_GET, '/storage/buckets/' . $data['largeBucketId'] . '/files/' . $data['largeFileId'] . '/download', array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$fileData = $file7['body'];
$this->assertEquals(200, $file7['headers']['status-code']);
$this->assertEquals('attachment; filename="large-file.mp4"', $file7['headers']['content-disposition']);
$this->assertEquals('video/mp4', $file7['headers']['content-type']);
$this->assertNotEmpty($fileData);
$this->assertEquals(md5_file(realpath(__DIR__ . '/../../../resources/disk-a/large-file.mp4')), md5($fileData)); // validate the file is downloaded correctly
/**
* Test for FAILURE unknown Bucket
*/
$file8 = $this->client->call(Client::METHOD_GET, '/storage/buckets/empty/files/' . $data['fileId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(404, $file8['headers']['status-code']);
return $data;
}
/**
* @depends testCreateBucketFile
*/
public function testUpdateBucketFile(array $data): array
{
/**
* Test for SUCCESS
*/
$file = $this->client->call(Client::METHOD_PUT, '/storage/buckets/' . $data['bucketId'] . '/files/' . $data['fileId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'read' => ['role:all', 'user:x'],
'write' => ['role:all', 'user:x'],
]);
$this->assertEquals(200, $file['headers']['status-code']);
$this->assertNotEmpty($file['body']['$id']);
$this->assertIsInt($file['body']['dateCreated']);
$this->assertEquals('logo.png', $file['body']['name']);
$this->assertEquals('image/png', $file['body']['mimeType']);
$this->assertEquals(47218, $file['body']['sizeOriginal']);
//$this->assertEquals(54944, $file['body']['sizeActual']);
//$this->assertEquals('gzip', $file['body']['algorithm']);
//$this->assertEquals('1', $file['body']['fileOpenSSLVersion']);
//$this->assertEquals('aes-128-gcm', $file['body']['fileOpenSSLCipher']);
//$this->assertNotEmpty($file['body']['fileOpenSSLTag']);
//$this->assertNotEmpty($file['body']['fileOpenSSLIV']);
$this->assertIsArray($file['body']['$read']);
$this->assertIsArray($file['body']['$write']);
$this->assertCount(2, $file['body']['$read']);
$this->assertCount(2, $file['body']['$write']);
/**
* Test for FAILURE unknown Bucket
*/
$file = $this->client->call(Client::METHOD_PUT, '/storage/buckets/empty/files/' . $data['fileId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()), [
'read' => ['role:all', 'user:x'],
'write' => ['role:all', 'user:x'],
]);
$this->assertEquals(404, $file['headers']['status-code']);
return $data;
}
/**
* @depends testUpdateBucketFile
*/
public function testDeleteBucketFile(array $data): array
{
/**
* Test for SUCCESS
*/
$file = $this->client->call(Client::METHOD_DELETE, '/storage/buckets/' . $data['bucketId'] . '/files/' . $data['fileId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(204, $file['headers']['status-code']);
$this->assertEmpty($file['body']);
$file = $this->client->call(Client::METHOD_GET, '/storage/files/' . $data['fileId'], array_merge([
'content-type' => 'application/json',
'x-appwrite-project' => $this->getProject()['$id'],
], $this->getHeaders()));
$this->assertEquals(404, $file['headers']['status-code']);
/**
* Test for FAILURE
*/
return $data;
}
public function testCreateFile(): array
{
/**
* Test for SUCCESS
@@ -34,11 +397,11 @@ trait StorageBase
*/
return ['fileId' => $file['body']['$id']];
}
/**
* @depends testCreateFile
*/
public function testGetFile(array $data):array
public function testGetFile(array $data): array
{
/**
* Test for SUCCESS
@@ -73,7 +436,7 @@ trait StorageBase
$this->assertEquals(200, $file2['headers']['status-code']);
$this->assertEquals('image/png', $file2['headers']['content-type']);
$this->assertNotEmpty($file2['body']);
//new image preview features
$file3 = $this->client->call(Client::METHOD_GET, '/storage/files/' . $data['fileId'] . '/preview', array_merge([
'content-type' => 'application/json',
@@ -86,7 +449,6 @@ trait StorageBase
'output' => 'png',
'rotation' => '45',
]);
$this->assertEquals(200, $file3['headers']['status-code']);
$this->assertEquals('image/png', $file3['headers']['content-type']);
@@ -110,11 +472,11 @@ trait StorageBase
'borderColor' => 'ff0000',
'output' => 'jpg',
]);
$this->assertEquals(200, $file4['headers']['status-code']);
$this->assertEquals('image/jpeg', $file4['headers']['content-type']);
$this->assertNotEmpty($file4['body']);
$image = new \Imagick();
$image->readImageBlob($file4['body']);
$original = new \Imagick(__DIR__ . '/../../../resources/logo-after.jpg');
@@ -148,11 +510,11 @@ trait StorageBase
return $data;
}
/**
* @depends testGetFile
*/
public function testListFiles(array $data):array
public function testListFiles(array $data): array
{
/**
* Test for SUCCESS
@@ -169,14 +531,14 @@ trait StorageBase
/**
* Test for FAILURE
*/
return $data;
}
/**
* @depends testListFiles
*/
public function testUpdateFile(array $data):array
public function testUpdateFile(array $data): array
{
/**
* Test for SUCCESS
@@ -205,7 +567,7 @@ trait StorageBase
$this->assertIsArray($file['body']['$write']);
$this->assertCount(1, $file['body']['$read']);
$this->assertCount(1, $file['body']['$write']);
/**
* Test for FAILURE
*/
@@ -216,7 +578,7 @@ trait StorageBase
/**
* @depends testUpdateFile
*/
public function testDeleteFile(array $data):array
public function testDeleteFile(array $data): array
{
/**
* Test for SUCCESS
@@ -235,11 +597,11 @@ trait StorageBase
], $this->getHeaders()));
$this->assertEquals(404, $file['headers']['status-code']);
/**
* Test for FAILURE
*/
return $data;
}
}
}
@@ -4,11 +4,11 @@ namespace Tests\E2E\Services\Storage;
use Tests\E2E\Scopes\Scope;
use Tests\E2E\Scopes\ProjectConsole;
use Tests\E2E\Scopes\SideClient;
use Tests\E2E\Scopes\SideConsole;
class StorageConsoleClientTest extends Scope
{
use StorageBase;
use ProjectConsole;
use SideClient;
use SideConsole;
}
Binary file not shown.