1
0
Fork 0
mirror of synced 2024-06-02 10:54:44 +12:00

preview, view, download, update, delete endpoints and tests

This commit is contained in:
Damodar Lohani 2021-06-20 17:05:35 +05:45
parent 18a8392c9b
commit fe0bdf0799
2 changed files with 482 additions and 1 deletions

View file

@ -461,13 +461,424 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId')
$file = $dbForInternal->getDocument('files', $fileId);
if ($file->isEmpty()) {
if (empty($file->getId()) || $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 */
$bucket = $dbForInternal->getDocument('buckets', $bucketId);
if($bucket->isEmpty()) {
throw new Exception("Bucket not found", 404);
}
$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);
}
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 (empty($file->getId()) || $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 (empty($file->getId()) || $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');
$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'))
);
}
$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 (empty($file->getId()) || $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 (empty($file->getId()) || $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 (empty($file->getId()) || $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'])

View file

@ -184,6 +184,76 @@ trait StorageBase
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
*/
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
{