1
0
Fork 0
mirror of synced 2024-07-01 20:50:49 +12:00

Merge branch '0.16.x' of github.com:appwrite/appwrite into feat-audits-label

 Conflicts:
	app/controllers/api/storage.php
	app/controllers/shared/api.php
This commit is contained in:
shimon 2022-08-17 17:08:54 +03:00
commit cc9a166146
17 changed files with 321 additions and 130 deletions

1
.env
View file

@ -72,6 +72,7 @@ OPEN_RUNTIMES_NETWORK=appwrite_runtimes
_APP_EXECUTOR_SECRET=your-secret-key _APP_EXECUTOR_SECRET=your-secret-key
_APP_EXECUTOR_HOST=http://appwrite-executor/v1 _APP_EXECUTOR_HOST=http://appwrite-executor/v1
_APP_MAINTENANCE_INTERVAL=86400 _APP_MAINTENANCE_INTERVAL=86400
_APP_MAINTENANCE_RETENTION_CACHE=2592000
_APP_MAINTENANCE_RETENTION_EXECUTION=1209600 _APP_MAINTENANCE_RETENTION_EXECUTION=1209600
_APP_MAINTENANCE_RETENTION_ABUSE=86400 _APP_MAINTENANCE_RETENTION_ABUSE=86400
_APP_MAINTENANCE_RETENTION_AUDIT=1209600 _APP_MAINTENANCE_RETENTION_AUDIT=1209600

2
.gitignore vendored
View file

@ -9,4 +9,4 @@
.php_cs.cache .php_cs.cache
debug/ debug/
app/sdks app/sdks
dev/yasd_init.php dev/yasd_init.php

View file

@ -2798,6 +2798,62 @@ $collections = [
], ],
] ]
], ],
'cache' => [
'$collection' => Database::METADATA,
'$id' => 'cache',
'name' => 'Cache',
'attributes' => [
[
'$id' => 'resource',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 255,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'accessedAt',
'type' => Database::VAR_INTEGER,
'format' => '',
'size' => 0,
'signed' => false,
'required' => true,
'default' => null,
'array' => false,
'filters' => [],
],
[
'$id' => 'signature',
'type' => Database::VAR_STRING,
'format' => '',
'size' => 255,
'signed' => true,
'required' => false,
'default' => null,
'array' => false,
'filters' => [],
],
],
'indexes' => [
[
'$id' => '_key_accessedAt',
'type' => Database::INDEX_KEY,
'attributes' => ['accessedAt'],
'lengths' => [],
'orders' => [],
],
[
'$id' => '_key_resource',
'type' => Database::INDEX_KEY,
'attributes' => ['resource'],
'lengths' => [],
'orders' => [],
],
],
],
'files' => [ 'files' => [
'$collection' => 'buckets', '$collection' => 'buckets',
'$id' => 'files', '$id' => 'files',

View file

@ -804,6 +804,15 @@ return [
'question' => '', 'question' => '',
'filter' => '' 'filter' => ''
], ],
[
'name' => '_APP_MAINTENANCE_RETENTION_CACHE',
'description' => 'The maximum duration (in seconds) upto which to retain cached files. The default value is 2592000 seconds (30 days).',
'introduction' => '0.16.0',
'default' => '2592000',
'required' => false,
'question' => '',
'filter' => ''
],
[ [
'name' => '_APP_MAINTENANCE_RETENTION_EXECUTION', 'name' => '_APP_MAINTENANCE_RETENTION_EXECUTION',
'description' => 'The maximum duration (in seconds) upto which to retain execution logs. The default value is 1209600 seconds (14 days).', 'description' => 'The maximum duration (in seconds) upto which to retain execution logs. The default value is 1209600 seconds (14 days).',

View file

@ -7,8 +7,6 @@ use Appwrite\Utopia\Response;
use chillerlan\QRCode\QRCode; use chillerlan\QRCode\QRCode;
use chillerlan\QRCode\QROptions; use chillerlan\QRCode\QROptions;
use Utopia\App; use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Config\Config; use Utopia\Config\Config;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Image\Image; use Utopia\Image\Image;
@ -37,8 +35,6 @@ $avatarCallback = function (string $type, string $code, int $width, int $height,
} }
$output = 'png'; $output = 'png';
$date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache
$key = \md5('/v1/avatars/' . $type . '/:code-' . $code . $width . $height . $quality . $output);
$path = $set[$code]; $path = $set[$code];
$type = 'png'; $type = 'png';
@ -46,35 +42,15 @@ $avatarCallback = function (string $type, string $code, int $width, int $height,
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'File not readable in ' . $path); throw new Exception(Exception::GENERAL_SERVER_ERROR, 'File not readable in ' . $path);
} }
$cache = new Cache(new Filesystem(APP_STORAGE_CACHE . '/app-0')); // 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('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data);
}
$image = new Image(\file_get_contents($path)); $image = new Image(\file_get_contents($path));
$image->crop((int) $width, (int) $height); $image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output; $output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality); $data = $image->output($output, $quality);
$cache->save($key, $data);
$response $response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
->setContentType('image/png') ->setContentType('image/png')
->addHeader('Expires', $date) ->file($data)
->addHeader('X-Appwrite-Cache', 'miss') ;
->send($data, null);
unset($image); unset($image);
}; };
@ -82,6 +58,8 @@ App::get('/v1/avatars/credit-cards/:code')
->desc('Get Credit Card Icon') ->desc('Get Credit Card Icon')
->groups(['api', 'avatars']) ->groups(['api', 'avatars'])
->label('scope', 'avatars.read') ->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/credit-card')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars') ->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getCreditCard') ->label('sdk.method', 'getCreditCard')
@ -100,6 +78,8 @@ App::get('/v1/avatars/browsers/:code')
->desc('Get Browser Icon') ->desc('Get Browser Icon')
->groups(['api', 'avatars']) ->groups(['api', 'avatars'])
->label('scope', 'avatars.read') ->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/browser')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars') ->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getBrowser') ->label('sdk.method', 'getBrowser')
@ -118,6 +98,8 @@ App::get('/v1/avatars/flags/:code')
->desc('Get Country Flag') ->desc('Get Country Flag')
->groups(['api', 'avatars']) ->groups(['api', 'avatars'])
->label('scope', 'avatars.read') ->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/flag')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars') ->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getFlag') ->label('sdk.method', 'getFlag')
@ -136,6 +118,8 @@ App::get('/v1/avatars/image')
->desc('Get Image from URL') ->desc('Get Image from URL')
->groups(['api', 'avatars']) ->groups(['api', 'avatars'])
->label('scope', 'avatars.read') ->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/image')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars') ->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getImage') ->label('sdk.method', 'getImage')
@ -151,19 +135,7 @@ App::get('/v1/avatars/image')
$quality = 80; $quality = 80;
$output = 'png'; $output = 'png';
$date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache
$key = \md5('/v2/avatars/images-' . $url . '-' . $width . '/' . $height . '/' . $quality);
$type = 'png'; $type = 'png';
$cache = new Cache(new Filesystem(APP_STORAGE_CACHE . '/app-0')); // Limit file number or size
$data = $cache->load($key, 60 * 60 * 24 * 7/* 1 week */);
if ($data) {
return $response
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data);
}
if (!\extension_loaded('imagick')) { if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
@ -182,19 +154,14 @@ App::get('/v1/avatars/image')
} }
$image->crop((int) $width, (int) $height); $image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output; $output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality); $data = $image->output($output, $quality);
$cache->save($key, $data);
$response $response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
->setContentType('image/png') ->setContentType('image/png')
->addHeader('Expires', $date) ->file($data)
->addHeader('X-Appwrite-Cache', 'miss') ;
->send($data);
unset($image); unset($image);
}); });
@ -202,6 +169,8 @@ App::get('/v1/avatars/favicon')
->desc('Get Favicon') ->desc('Get Favicon')
->groups(['api', 'avatars']) ->groups(['api', 'avatars'])
->label('scope', 'avatars.read') ->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/favicon')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars') ->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getFavicon') ->label('sdk.method', 'getFavicon')
@ -217,19 +186,7 @@ App::get('/v1/avatars/favicon')
$height = 56; $height = 56;
$quality = 80; $quality = 80;
$output = 'png'; $output = 'png';
$date = \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT'; // 45 days cache
$key = \md5('/v2/avatars/favicon-' . $url);
$type = 'png'; $type = 'png';
$cache = new Cache(new Filesystem(APP_STORAGE_CACHE . '/app-0')); // Limit file number or size
$data = $cache->load($key, 60 * 60 * 24 * 30 * 3/* 3 months */);
if ($data) {
return $response
->setContentType('image/png')
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data);
}
if (!\extension_loaded('imagick')) { if (!\extension_loaded('imagick')) {
throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing');
@ -314,14 +271,11 @@ App::get('/v1/avatars/favicon')
if (empty($data) || (\mb_substr($data, 0, 5) === '<html') || \mb_substr($data, 0, 5) === '<!doc') { if (empty($data) || (\mb_substr($data, 0, 5) === '<html') || \mb_substr($data, 0, 5) === '<!doc') {
throw new Exception(Exception::AVATAR_ICON_NOT_FOUND, 'Favicon not found'); throw new Exception(Exception::AVATAR_ICON_NOT_FOUND, 'Favicon not found');
} }
$response
$cache->save($key, $data); ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
return $response
->setContentType('image/x-icon') ->setContentType('image/x-icon')
->addHeader('Expires', $date) ->file($data)
->addHeader('X-Appwrite-Cache', 'miss') ;
->send($data);
} }
$fetch = @\file_get_contents($outputHref, false); $fetch = @\file_get_contents($outputHref, false);
@ -331,21 +285,15 @@ App::get('/v1/avatars/favicon')
} }
$image = new Image($fetch); $image = new Image($fetch);
$image->crop((int) $width, (int) $height); $image->crop((int) $width, (int) $height);
$output = (empty($output)) ? $type : $output; $output = (empty($output)) ? $type : $output;
$data = $image->output($output, $quality); $data = $image->output($output, $quality);
$cache->save($key, $data);
$response $response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
->setContentType('image/png') ->setContentType('image/png')
->addHeader('Expires', $date) ->file($data)
->addHeader('X-Appwrite-Cache', 'miss') ;
->send($data);
unset($image); unset($image);
}); });
@ -353,6 +301,8 @@ App::get('/v1/avatars/qr')
->desc('Get QR Code') ->desc('Get QR Code')
->groups(['api', 'avatars']) ->groups(['api', 'avatars'])
->label('scope', 'avatars.read') ->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/qr')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars') ->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getQR') ->label('sdk.method', 'getQR')
@ -381,19 +331,21 @@ App::get('/v1/avatars/qr')
} }
$image = new Image($qrcode->render($text)); $image = new Image($qrcode->render($text));
$image->crop((int) $size, (int) $size); $image->crop((int) $size, (int) $size);
$response $response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
->setContentType('image/png') ->setContentType('image/png')
->send($image->output('png', 9)); ->file($image->output('png', 9))
;
}); });
App::get('/v1/avatars/initials') App::get('/v1/avatars/initials')
->desc('Get User Initials') ->desc('Get User Initials')
->groups(['api', 'avatars']) ->groups(['api', 'avatars'])
->label('scope', 'avatars.read') ->label('scope', 'avatars.read')
->label('cache', true)
->label('cache.resource', 'avatar/initials')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'avatars') ->label('sdk.namespace', 'avatars')
->label('sdk.method', 'getInitials') ->label('sdk.method', 'getInitials')
@ -468,5 +420,6 @@ App::get('/v1/avatars/initials')
$response $response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache ->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
->setContentType('image/png') ->setContentType('image/png')
->send($image->getImageBlob()); ->file($image->getImageBlob())
;
}); });

View file

@ -127,6 +127,7 @@ App::post('/v1/projects')
if (($collection['$collection'] ?? '') !== Database::METADATA) { if (($collection['$collection'] ?? '') !== Database::METADATA) {
continue; continue;
} }
$attributes = []; $attributes = [];
$indexes = []; $indexes = [];
@ -153,7 +154,6 @@ App::post('/v1/projects')
'orders' => $index['orders'], 'orders' => $index['orders'],
]); ]);
} }
$dbForProject->createCollection($key, $attributes, $indexes); $dbForProject->createCollection($key, $attributes, $indexes);
} }

View file

@ -9,8 +9,6 @@ use Appwrite\OpenSSL\OpenSSL;
use Appwrite\Stats\Stats; use Appwrite\Stats\Stats;
use Appwrite\Utopia\Response; use Appwrite\Utopia\Response;
use Utopia\App; use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Config\Config; use Utopia\Config\Config;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
@ -34,6 +32,7 @@ use Utopia\Storage\Validator\Upload;
use Utopia\Validator\ArrayList; use Utopia\Validator\ArrayList;
use Utopia\Validator\Boolean; use Utopia\Validator\Boolean;
use Utopia\Validator\HexColor; use Utopia\Validator\HexColor;
use Utopia\Validator\Integer;
use Utopia\Validator\Range; use Utopia\Validator\Range;
use Utopia\Validator\Text; use Utopia\Validator\Text;
use Utopia\Validator\WhiteList; use Utopia\Validator\WhiteList;
@ -337,7 +336,9 @@ App::post('/v1/storage/buckets/:bucketId/files')
->inject('mode') ->inject('mode')
->inject('deviceFiles') ->inject('deviceFiles')
->inject('deviceLocal') ->inject('deviceLocal')
->action(function (string $bucketId, string $fileId, mixed $file, ?array $read, ?array $write, Request $request, Response $response, Database $dbForProject, Document $user, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal) { ->inject('deletes')
->action(function (string $bucketId, string $fileId, mixed $file, ?array $read, ?array $write, Request $request, Response $response, Database $dbForProject, Document $user, Stats $usage, Event $events, string $mode, Device $deviceFiles, Device $deviceLocal, Delete $deletes) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ( if (
@ -632,6 +633,11 @@ App::post('/v1/storage/buckets/:bucketId/files')
->setContext('bucket', $bucket) ->setContext('bucket', $bucket)
; ;
$deletes
->setType(DELETE_TYPE_CACHE_BY_RESOURCE)
->setResource('file/' . $file->getId())
;
$metadata = null; // was causing leaks as it was passed by reference $metadata = null; // was causing leaks as it was passed by reference
$response->setStatusCode(Response::STATUS_CODE_CREATED); $response->setStatusCode(Response::STATUS_CODE_CREATED);
@ -781,6 +787,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
->desc('Get File Preview') ->desc('Get File Preview')
->groups(['api', 'storage']) ->groups(['api', 'storage'])
->label('scope', 'files.read') ->label('scope', 'files.read')
->label('cache', true)
->label('cache.resource', 'file/{request.fileId}')
->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT])
->label('sdk.namespace', 'storage') ->label('sdk.namespace', 'storage')
->label('sdk.method', 'getFilePreview') ->label('sdk.method', 'getFilePreview')
@ -840,9 +848,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$outputs = Config::getParam('storage-outputs'); $outputs = Config::getParam('storage-outputs');
$fileLogos = Config::getParam('storage-logos'); $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 . $gravity . $quality . $borderWidth . $borderColor . $borderRadius . $opacity . $rotation . $background . $output);
if ($bucket->getAttribute('permission') === 'bucket') { if ($bucket->getAttribute('permission') === 'bucket') {
// skip authorization // skip authorization
$file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId));
@ -859,7 +864,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$algorithm = $file->getAttribute('algorithm'); $algorithm = $file->getAttribute('algorithm');
$cipher = $file->getAttribute('openSSLCipher'); $cipher = $file->getAttribute('openSSLCipher');
$mime = $file->getAttribute('mimeType'); $mime = $file->getAttribute('mimeType');
if (!\in_array($mime, $inputs) || $file->getAttribute('sizeActual') > (int) App::getEnv('_APP_STORAGE_PREVIEW_LIMIT', 20000000)) { if (!\in_array($mime, $inputs) || $file->getAttribute('sizeActual') > (int) App::getEnv('_APP_STORAGE_PREVIEW_LIMIT', 20000000)) {
if (!\in_array($mime, $inputs)) { if (!\in_array($mime, $inputs)) {
$path = (\array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default']; $path = (\array_key_exists($mime, $fileLogos)) ? $fileLogos[$mime] : $fileLogos['default'];
@ -872,7 +876,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$cipher = null; $cipher = null;
$background = (empty($background)) ? 'eceff1' : $background; $background = (empty($background)) ? 'eceff1' : $background;
$type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
$key = \md5($path . $width . $height . $gravity . $quality . $borderWidth . $borderColor . $borderRadius . $opacity . $rotation . $background . $output);
$deviceFiles = $deviceLocal; $deviceFiles = $deviceLocal;
} }
@ -883,23 +886,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); throw new Exception(Exception::STORAGE_FILE_NOT_FOUND);
} }
$cache = new Cache(new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId() . DIRECTORY_SEPARATOR . $bucketId . DIRECTORY_SEPARATOR . $fileId)); // Limit file number or size
$data = $cache->load($key, 60 * 60 * 24 * 30 * 3/* 3 months */);
if (empty($output)) { if (empty($output)) {
// when file extension is not provided and the mime type is not one of our supported outputs // when file extension is not provided and the mime type is not one of our supported outputs
// we fallback to `jpg` output format // we fallback to `jpg` output format
$output = empty($type) ? (array_search($mime, $outputs) ?? 'jpg') : $type; $output = empty($type) ? (array_search($mime, $outputs) ?? 'jpg') : $type;
} }
if ($data) {
return $response
->setContentType((\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'])
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'hit')
->send($data)
;
}
$source = $deviceFiles->read($path); $source = $deviceFiles->read($path);
@ -944,20 +936,18 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
$data = $image->output($output, $quality); $data = $image->output($output, $quality);
$cache->save($key, $data);
$usage $usage
->setParam('storage.files.read', 1) ->setParam('storage.files.read', 1)
->setParam('bucketId', $bucketId) ->setParam('bucketId', $bucketId)
; ;
$response $contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
->setContentType((\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'])
->addHeader('Expires', $date)
->addHeader('X-Appwrite-Cache', 'miss')
->send($data)
;
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
->setContentType($contentType)
->file($data)
;
unset($image); unset($image);
}); });
@ -1363,13 +1353,15 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
->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('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 ID.') ->param('fileId', '', new UID(), 'File ID.')
->inject('response') ->inject('response')
->inject('request')
->inject('dbForProject') ->inject('dbForProject')
->inject('events') ->inject('events')
->inject('usage') ->inject('usage')
->inject('mode') ->inject('mode')
->inject('deviceFiles') ->inject('deviceFiles')
->inject('project') ->inject('project')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $events, Stats $usage, string $mode, Device $deviceFiles, Document $project) { ->inject('deletes')
->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $events, Stats $usage, string $mode, Device $deviceFiles, Document $project, Delete $deletes) {
$bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId));
if ( if (
@ -1408,10 +1400,10 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
} }
if ($deviceDeleted) { if ($deviceDeleted) {
//delete related cache $deletes
$cacheDir = APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId() . DIRECTORY_SEPARATOR . $bucketId . DIRECTORY_SEPARATOR . $fileId; ->setType(DELETE_TYPE_CACHE_BY_RESOURCE)
$deviceLocal = new Local($cacheDir); ->setResource('file/' . $fileId)
$deviceLocal->delete($cacheDir, true); ;
if ($bucket->getAttribute('permission') === 'bucket') { if ($bucket->getAttribute('permission') === 'bucket') {
$deleted = Authorization::skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId)); $deleted = Authorization::skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId));

View file

@ -14,6 +14,8 @@ use Utopia\App;
use Appwrite\Extend\Exception; use Appwrite\Extend\Exception;
use Utopia\Abuse\Abuse; use Utopia\Abuse\Abuse;
use Utopia\Abuse\Adapters\TimeLimit; use Utopia\Abuse\Adapters\TimeLimit;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Validator\Authorization; use Utopia\Database\Validator\Authorization;
@ -149,6 +151,31 @@ App::init()
$deletes->setProject($project); $deletes->setProject($project);
$database->setProject($project); $database->setProject($project);
$useCache = $route->getLabel('cache', false);
if ($useCache) {
$key = md5($request->getURI() . implode('*', $request->getParams()));
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
$timestamp = 60 * 60 * 24 * 30;
$data = $cache->load($key, $timestamp);
if (!empty($data)) {
$data = json_decode($data, true);
$response
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + $timestamp) . ' GMT')
->addHeader('X-Appwrite-Cache', 'hit')
->setContentType($data['content-type'])
->send(base64_decode($data['payload']))
;
$route->setIsActive(false);
} else {
$response->addHeader('X-Appwrite-Cache', 'miss');
}
}
}); });
App::init() App::init()
@ -219,6 +246,7 @@ App::shutdown()
->inject('mode') ->inject('mode')
->inject('dbForProject') ->inject('dbForProject')
->action(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject) use ($parseLabel) { ->action(function (App $utopia, Request $request, Response $response, Document $project, Event $events, Audit $audits, Stats $usage, Delete $deletes, EventDatabase $database, string $mode, Database $dbForProject) use ($parseLabel) {
$responsePayload = $response->getPayload(); $responsePayload = $response->getPayload();
if (!empty($events->getEvent())) { if (!empty($events->getEvent())) {
@ -319,15 +347,59 @@ App::shutdown()
$database->trigger(); $database->trigger();
} }
$route = $utopia->match($request);
$requestParams = $route->getParamsValues();
$user = $audits->getUser();
$useCache = $route->getLabel('cache', false);
if ($useCache) {
$resource = null;
$data = $response->getPayload();
if (!empty($data['payload'])) {
$pattern = $route->getLabel('cache.resource', null);
if (!empty($pattern)) {
$resource = $parseLabel($pattern, $responsePayload, $requestParams, $user);
}
$key = md5($request->getURI() . implode('*', $request->getParams()));
$data = json_encode([
'content-type' => $response->getContentType(),
'payload' => base64_encode($data['payload']),
]) ;
$signature = md5($data);
$cacheLog = $dbForProject->getDocument('cache', $key);
if ($cacheLog->isEmpty()) {
Authorization::skip(fn () => $dbForProject->createDocument('cache', new Document([
'$id' => $key,
'resource' => $resource,
'accessedAt' => \time(),
'signature' => $signature,
])));
} elseif (date('Y/m/d', \time()) > date('Y/m/d', $cacheLog->getAttribute('accessedAt'))) {
$cacheLog->setAttribute('accessedAt', \time());
Authorization::skip(fn () => $dbForProject->updateDocument('cache', $cacheLog->getId(), $cacheLog));
}
if ($signature !== $cacheLog->getAttribute('signature')) {
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId())
);
$cache->save($key, $data);
}
}
}
if ( if (
App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled' App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled'
&& $project->getId() && $project->getId()
&& $mode !== APP_MODE_ADMIN // TODO: add check to make sure user is admin && $mode !== APP_MODE_ADMIN // TODO: add check to make sure user is admin
&& !empty($route->getLabel('sdk.namespace', null)) && !empty($route->getLabel('sdk.namespace', null))
) { // Don't calculate console usage on admin mode ) {
$usage $usage
->setParam('networkRequestSize', $request->getSize() + $usage->getParam('storage')) ->setParam('networkRequestSize', $request->getSize() + $usage->getParam('storage'))
->setParam('networkResponseSize', $response->getSize()) ->setParam('networkResponseSize', $response->getSize())
->submit(); ->submit();
} }
}); });

View file

@ -143,6 +143,8 @@ const DELETE_TYPE_USAGE = 'usage';
const DELETE_TYPE_REALTIME = 'realtime'; const DELETE_TYPE_REALTIME = 'realtime';
const DELETE_TYPE_BUCKETS = 'buckets'; const DELETE_TYPE_BUCKETS = 'buckets';
const DELETE_TYPE_SESSIONS = 'sessions'; const DELETE_TYPE_SESSIONS = 'sessions';
const DELETE_TYPE_CACHE_BY_TIMESTAMP = 'cacheByTimeStamp';
const DELETE_TYPE_CACHE_BY_RESOURCE = 'cacheByResource';
// Mail Types // Mail Types
const MAIL_TYPE_VERIFICATION = 'verification'; const MAIL_TYPE_VERIFICATION = 'verification';
const MAIL_TYPE_MAGIC_SESSION = 'magicSession'; const MAIL_TYPE_MAGIC_SESSION = 'magicSession';

View file

@ -127,6 +127,15 @@ $cli
} }
} }
function notifyDeleteCache($interval)
{
(new Delete())
->setType(DELETE_TYPE_CACHE_BY_TIMESTAMP)
->setTimestamp(time() - $interval)
->trigger();
}
// # of days in seconds (1 day = 86400s) // # of days in seconds (1 day = 86400s)
$interval = (int) App::getEnv('_APP_MAINTENANCE_INTERVAL', '86400'); $interval = (int) App::getEnv('_APP_MAINTENANCE_INTERVAL', '86400');
$executionLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', '1209600'); $executionLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', '1209600');
@ -134,8 +143,9 @@ $cli
$abuseLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', '86400'); $abuseLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', '86400');
$usageStatsRetention30m = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_30M', '129600'); //36 hours $usageStatsRetention30m = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_30M', '129600'); //36 hours
$usageStatsRetention1d = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_1D', '8640000'); // 100 days $usageStatsRetention1d = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_1D', '8640000'); // 100 days
$cacheRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_CACHE', '2592000'); // 30 days
Console::loop(function () use ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d) { Console::loop(function () use ($interval, $executionLogsRetention, $abuseLogsRetention, $auditLogRetention, $usageStatsRetention30m, $usageStatsRetention1d, $cacheRetention) {
$database = getConsoleDB(); $database = getConsoleDB();
$time = date('d-m-Y H:i:s', time()); $time = date('d-m-Y H:i:s', time());
@ -147,5 +157,6 @@ $cli
notifyDeleteConnections(); notifyDeleteConnections();
notifyDeleteExpiredSessions(); notifyDeleteExpiredSessions();
renewCertificates($database); renewCertificates($database);
notifyDeleteCache($cacheRetention);
}, $interval); }, $interval);
}); });

View file

@ -147,6 +147,7 @@ services:
- _APP_STATSD_PORT - _APP_STATSD_PORT
- _APP_MAINTENANCE_INTERVAL - _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION - _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
- _APP_MAINTENANCE_RETENTION_ABUSE - _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT - _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_SMS_PROVIDER - _APP_SMS_PROVIDER
@ -545,6 +546,7 @@ services:
- _APP_DB_PASS - _APP_DB_PASS
- _APP_MAINTENANCE_INTERVAL - _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION - _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
- _APP_MAINTENANCE_RETENTION_ABUSE - _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT - _APP_MAINTENANCE_RETENTION_AUDIT

View file

@ -1,6 +1,8 @@
<?php <?php
use Utopia\App; use Utopia\App;
use Utopia\Cache\Adapter\Filesystem;
use Utopia\Cache\Cache;
use Utopia\Database\Database; use Utopia\Database\Database;
use Utopia\Database\Document; use Utopia\Database\Document;
use Utopia\Database\Query; use Utopia\Database\Query;
@ -33,7 +35,6 @@ class DeletesV1 extends Worker
{ {
$project = new Document($this->args['project'] ?? []); $project = new Document($this->args['project'] ?? []);
$type = $this->args['type'] ?? ''; $type = $this->args['type'] ?? '';
switch (strval($type)) { switch (strval($type)) {
case DELETE_TYPE_DOCUMENT: case DELETE_TYPE_DOCUMENT:
$document = new Document($this->args['document'] ?? []); $document = new Document($this->args['document'] ?? []);
@ -107,6 +108,13 @@ class DeletesV1 extends Worker
case DELETE_TYPE_USAGE: case DELETE_TYPE_USAGE:
$this->deleteUsageStats($this->args['timestamp1d'], $this->args['timestamp30m']); $this->deleteUsageStats($this->args['timestamp1d'], $this->args['timestamp30m']);
break; break;
case DELETE_TYPE_CACHE_BY_RESOURCE:
$this->deleteCacheByResource($project->getId());
break;
case DELETE_TYPE_CACHE_BY_TIMESTAMP:
$this->deleteCacheByTimestamp();
break;
default: default:
Console::error('No delete operation for type: ' . $type); Console::error('No delete operation for type: ' . $type);
break; break;
@ -117,6 +125,49 @@ class DeletesV1 extends Worker
{ {
} }
/**
* @param string $projectId
*/
protected function deleteCacheByResource(string $projectId): void
{
$this->deleteCacheFiles([
new Query('resource', Query::TYPE_EQUAL, [$this->args['resource']])
]);
}
protected function deleteCacheByTimestamp(): void
{
$this->deleteCacheFiles([
new Query('accessedAt', Query::TYPE_LESSER, [$this->args['timestamp']])
]);
}
protected function deleteCacheFiles($query): void
{
$this->deleteForProjectIds(function (string $projectId) use ($query) {
$dbForProject = $this->getProjectDB($projectId);
$cache = new Cache(
new Filesystem(APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId)
);
$this->deleteByGroup(
'cache',
$query,
$dbForProject,
function (Document $document) use ($cache, $projectId) {
$path = APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $projectId . DIRECTORY_SEPARATOR . $document->getId();
if ($cache->purge($document->getId())) {
Console::success('Deleting cache file: ' . $path);
} else {
Console::error('Failed to delete cache file: ' . $path);
}
}
);
});
}
/** /**
* @param Document $document database document * @param Document $document database document
* @param string $projectId * @param string $projectId
@ -276,10 +327,10 @@ class DeletesV1 extends Worker
{ {
$this->deleteForProjectIds(function (string $projectId) use ($timestamp) { $this->deleteForProjectIds(function (string $projectId) use ($timestamp) {
$dbForProject = $this->getProjectDB($projectId); $dbForProject = $this->getProjectDB($projectId);
// Delete Sessions // Delete Sessions
$this->deleteByGroup('sessions', [ $this->deleteByGroup('sessions', [
new Query('expire', Query::TYPE_LESSER, [$timestamp]) new Query('expire', Query::TYPE_LESSER, [$timestamp])
], $dbForProject); ], $dbForProject);
}); });
} }

View file

@ -103,11 +103,9 @@ services:
- ./phpunit.xml:/usr/src/code/phpunit.xml - ./phpunit.xml:/usr/src/code/phpunit.xml
- ./tests:/usr/src/code/tests - ./tests:/usr/src/code/tests
- ./app:/usr/src/code/app - ./app:/usr/src/code/app
# - ./vendor/utopia/database:/usr/src/code/vendor/utopia/database
- ./docs:/usr/src/code/docs - ./docs:/usr/src/code/docs
- ./public:/usr/src/code/public - ./public:/usr/src/code/public
- ./src:/usr/src/code/src - ./src:/usr/src/code/src
# - ./debug:/tmp
- ./dev:/usr/local/dev - ./dev:/usr/local/dev
depends_on: depends_on:
- mariadb - mariadb
@ -173,6 +171,7 @@ services:
- _APP_STATSD_PORT - _APP_STATSD_PORT
- _APP_MAINTENANCE_INTERVAL - _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION - _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
- _APP_MAINTENANCE_RETENTION_ABUSE - _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT - _APP_MAINTENANCE_RETENTION_AUDIT
- _APP_SMS_PROVIDER - _APP_SMS_PROVIDER
@ -207,7 +206,6 @@ services:
volumes: volumes:
- ./app:/usr/src/code/app - ./app:/usr/src/code/app
- ./src:/usr/src/code/src - ./src:/usr/src/code/src
# - ./vendor:/usr/src/code/vendor
depends_on: depends_on:
- mariadb - mariadb
- redis - redis
@ -330,7 +328,7 @@ services:
volumes: volumes:
- ./app:/usr/src/code/app - ./app:/usr/src/code/app
- ./src:/usr/src/code/src - ./src:/usr/src/code/src
# - ./vendor/utopia-php/database:/usr/src/code/vendor/utopia-php/database #- ./vendor/utopia-php/database:/usr/src/code/vendor/utopia-php/database
depends_on: depends_on:
- redis - redis
- mariadb - mariadb
@ -579,6 +577,7 @@ services:
- _APP_DB_PASS - _APP_DB_PASS
- _APP_MAINTENANCE_INTERVAL - _APP_MAINTENANCE_INTERVAL
- _APP_MAINTENANCE_RETENTION_EXECUTION - _APP_MAINTENANCE_RETENTION_EXECUTION
- _APP_MAINTENANCE_RETENTION_CACHE
- _APP_MAINTENANCE_RETENTION_ABUSE - _APP_MAINTENANCE_RETENTION_ABUSE
- _APP_MAINTENANCE_RETENTION_AUDIT - _APP_MAINTENANCE_RETENTION_AUDIT

View file

@ -7,7 +7,7 @@
convertWarningsToExceptions="true" convertWarningsToExceptions="true"
processIsolation="false" processIsolation="false"
stopOnFailure="false" stopOnFailure="false"
> >
<extensions> <extensions>
<extension class="Appwrite\Tests\TestHook" /> <extension class="Appwrite\Tests\TestHook" />
</extensions> </extensions>
@ -35,4 +35,4 @@
<file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file> <file>./tests/e2e/Services/Functions/FunctionsCustomClientTest.php</file>
</testsuite> </testsuite>
</testsuites> </testsuites>
</phpunit> </phpunit>

View file

@ -12,6 +12,7 @@ class Delete extends Event
protected ?int $timestamp1d = null; protected ?int $timestamp1d = null;
protected ?int $timestamp30m = null; protected ?int $timestamp30m = null;
protected ?Document $document = null; protected ?Document $document = null;
protected ?string $resource = null;
public function __construct() public function __construct()
{ {
@ -93,6 +94,29 @@ class Delete extends Event
return $this; return $this;
} }
/**
* Returns the resource for the delete event.
*
* @return string
*/
public function getResource(): string
{
return $this->resource;
}
/**
* Sets the resource for the delete event.
*
* @param string $resource
* @return self
*/
public function setResource(string $resource): self
{
$this->resource = $resource;
return $this;
}
/** /**
* Returns the set document for the delete event. * Returns the set document for the delete event.
* *
@ -103,6 +127,7 @@ class Delete extends Event
return $this->document; return $this->document;
} }
/** /**
* Executes this event and sends it to the deletes worker. * Executes this event and sends it to the deletes worker.
* *
@ -117,7 +142,8 @@ class Delete extends Event
'document' => $this->document, 'document' => $this->document,
'timestamp' => $this->timestamp, 'timestamp' => $this->timestamp,
'timestamp1d' => $this->timestamp1d, 'timestamp1d' => $this->timestamp1d,
'timestamp30m' => $this->timestamp30m 'timestamp30m' => $this->timestamp30m,
'resource' => $this->resource,
]); ]);
} }
} }

View file

@ -475,6 +475,24 @@ class Response extends SwooleResponse
return $this->payload; return $this->payload;
} }
/**
* Output response
*
* Generate HTTP response output including the response header (+cookies) and body and prints them.
*
* @param string $body
*
* @return void
*/
public function file(string $body = ''): void
{
$this->payload = [
'payload' => $body
];
$this->send($body);
}
/** /**
* YAML * YAML
* *
@ -506,7 +524,6 @@ class Response extends SwooleResponse
return $this->payload; return $this->payload;
} }
/** /**
* Function to set a response filter * Function to set a response filter
* *

View file

@ -490,7 +490,7 @@ trait StorageBase
$this->assertEquals(204, $file['headers']['status-code']); $this->assertEquals(204, $file['headers']['status-code']);
$this->assertEmpty($file['body']); $this->assertEmpty($file['body']);
sleep(1);
//upload again using the same ID //upload again using the same ID
$file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([ $file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([
'content-type' => 'multipart/form-data', 'content-type' => 'multipart/form-data',