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:
commit
cc9a166146
17 changed files with 321 additions and 130 deletions
1
.env
1
.env
|
@ -72,6 +72,7 @@ OPEN_RUNTIMES_NETWORK=appwrite_runtimes
|
|||
_APP_EXECUTOR_SECRET=your-secret-key
|
||||
_APP_EXECUTOR_HOST=http://appwrite-executor/v1
|
||||
_APP_MAINTENANCE_INTERVAL=86400
|
||||
_APP_MAINTENANCE_RETENTION_CACHE=2592000
|
||||
_APP_MAINTENANCE_RETENTION_EXECUTION=1209600
|
||||
_APP_MAINTENANCE_RETENTION_ABUSE=86400
|
||||
_APP_MAINTENANCE_RETENTION_AUDIT=1209600
|
||||
|
|
|
@ -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' => [
|
||||
'$collection' => 'buckets',
|
||||
'$id' => 'files',
|
||||
|
|
|
@ -804,6 +804,15 @@ return [
|
|||
'question' => '',
|
||||
'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',
|
||||
'description' => 'The maximum duration (in seconds) upto which to retain execution logs. The default value is 1209600 seconds (14 days).',
|
||||
|
|
|
@ -7,8 +7,6 @@ use Appwrite\Utopia\Response;
|
|||
use chillerlan\QRCode\QRCode;
|
||||
use chillerlan\QRCode\QROptions;
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Adapter\Filesystem;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Image\Image;
|
||||
|
@ -37,8 +35,6 @@ $avatarCallback = function (string $type, string $code, int $width, int $height,
|
|||
}
|
||||
|
||||
$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];
|
||||
$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);
|
||||
}
|
||||
|
||||
$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->crop((int) $width, (int) $height);
|
||||
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
|
||||
$data = $image->output($output, $quality);
|
||||
|
||||
$cache->save($key, $data);
|
||||
|
||||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
|
||||
->setContentType('image/png')
|
||||
->addHeader('Expires', $date)
|
||||
->addHeader('X-Appwrite-Cache', 'miss')
|
||||
->send($data, null);
|
||||
|
||||
->file($data)
|
||||
;
|
||||
unset($image);
|
||||
};
|
||||
|
||||
|
@ -82,6 +58,8 @@ App::get('/v1/avatars/credit-cards/:code')
|
|||
->desc('Get Credit Card Icon')
|
||||
->groups(['api', 'avatars'])
|
||||
->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.namespace', 'avatars')
|
||||
->label('sdk.method', 'getCreditCard')
|
||||
|
@ -100,6 +78,8 @@ App::get('/v1/avatars/browsers/:code')
|
|||
->desc('Get Browser Icon')
|
||||
->groups(['api', 'avatars'])
|
||||
->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.namespace', 'avatars')
|
||||
->label('sdk.method', 'getBrowser')
|
||||
|
@ -118,6 +98,8 @@ App::get('/v1/avatars/flags/:code')
|
|||
->desc('Get Country Flag')
|
||||
->groups(['api', 'avatars'])
|
||||
->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.namespace', 'avatars')
|
||||
->label('sdk.method', 'getFlag')
|
||||
|
@ -136,6 +118,8 @@ App::get('/v1/avatars/image')
|
|||
->desc('Get Image from URL')
|
||||
->groups(['api', 'avatars'])
|
||||
->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.namespace', 'avatars')
|
||||
->label('sdk.method', 'getImage')
|
||||
|
@ -151,19 +135,7 @@ App::get('/v1/avatars/image')
|
|||
|
||||
$quality = 80;
|
||||
$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';
|
||||
$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')) {
|
||||
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);
|
||||
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
|
||||
$data = $image->output($output, $quality);
|
||||
|
||||
$cache->save($key, $data);
|
||||
|
||||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
|
||||
->setContentType('image/png')
|
||||
->addHeader('Expires', $date)
|
||||
->addHeader('X-Appwrite-Cache', 'miss')
|
||||
->send($data);
|
||||
|
||||
->file($data)
|
||||
;
|
||||
unset($image);
|
||||
});
|
||||
|
||||
|
@ -202,6 +169,8 @@ App::get('/v1/avatars/favicon')
|
|||
->desc('Get Favicon')
|
||||
->groups(['api', 'avatars'])
|
||||
->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.namespace', 'avatars')
|
||||
->label('sdk.method', 'getFavicon')
|
||||
|
@ -217,19 +186,7 @@ App::get('/v1/avatars/favicon')
|
|||
$height = 56;
|
||||
$quality = 80;
|
||||
$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';
|
||||
$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')) {
|
||||
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') {
|
||||
throw new Exception(Exception::AVATAR_ICON_NOT_FOUND, 'Favicon not found');
|
||||
}
|
||||
|
||||
$cache->save($key, $data);
|
||||
|
||||
return $response
|
||||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
|
||||
->setContentType('image/x-icon')
|
||||
->addHeader('Expires', $date)
|
||||
->addHeader('X-Appwrite-Cache', 'miss')
|
||||
->send($data);
|
||||
->file($data)
|
||||
;
|
||||
}
|
||||
|
||||
$fetch = @\file_get_contents($outputHref, false);
|
||||
|
@ -331,21 +285,15 @@ App::get('/v1/avatars/favicon')
|
|||
}
|
||||
|
||||
$image = new Image($fetch);
|
||||
|
||||
$image->crop((int) $width, (int) $height);
|
||||
|
||||
$output = (empty($output)) ? $type : $output;
|
||||
|
||||
$data = $image->output($output, $quality);
|
||||
|
||||
$cache->save($key, $data);
|
||||
|
||||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
|
||||
->setContentType('image/png')
|
||||
->addHeader('Expires', $date)
|
||||
->addHeader('X-Appwrite-Cache', 'miss')
|
||||
->send($data);
|
||||
|
||||
->file($data)
|
||||
;
|
||||
unset($image);
|
||||
});
|
||||
|
||||
|
@ -353,6 +301,8 @@ App::get('/v1/avatars/qr')
|
|||
->desc('Get QR Code')
|
||||
->groups(['api', 'avatars'])
|
||||
->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.namespace', 'avatars')
|
||||
->label('sdk.method', 'getQR')
|
||||
|
@ -381,19 +331,21 @@ App::get('/v1/avatars/qr')
|
|||
}
|
||||
|
||||
$image = new Image($qrcode->render($text));
|
||||
|
||||
$image->crop((int) $size, (int) $size);
|
||||
|
||||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
|
||||
->setContentType('image/png')
|
||||
->send($image->output('png', 9));
|
||||
->file($image->output('png', 9))
|
||||
;
|
||||
});
|
||||
|
||||
App::get('/v1/avatars/initials')
|
||||
->desc('Get User Initials')
|
||||
->groups(['api', 'avatars'])
|
||||
->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.namespace', 'avatars')
|
||||
->label('sdk.method', 'getInitials')
|
||||
|
@ -468,5 +420,6 @@ App::get('/v1/avatars/initials')
|
|||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + (60 * 60 * 24 * 45)) . ' GMT') // 45 days cache
|
||||
->setContentType('image/png')
|
||||
->send($image->getImageBlob());
|
||||
->file($image->getImageBlob())
|
||||
;
|
||||
});
|
||||
|
|
|
@ -127,6 +127,7 @@ App::post('/v1/projects')
|
|||
if (($collection['$collection'] ?? '') !== Database::METADATA) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$attributes = [];
|
||||
$indexes = [];
|
||||
|
||||
|
@ -153,7 +154,6 @@ App::post('/v1/projects')
|
|||
'orders' => $index['orders'],
|
||||
]);
|
||||
}
|
||||
|
||||
$dbForProject->createCollection($key, $attributes, $indexes);
|
||||
}
|
||||
|
||||
|
|
|
@ -9,8 +9,6 @@ use Appwrite\OpenSSL\OpenSSL;
|
|||
use Appwrite\Stats\Stats;
|
||||
use Appwrite\Utopia\Response;
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Adapter\Filesystem;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Config\Config;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
|
@ -34,6 +32,7 @@ use Utopia\Storage\Validator\Upload;
|
|||
use Utopia\Validator\ArrayList;
|
||||
use Utopia\Validator\Boolean;
|
||||
use Utopia\Validator\HexColor;
|
||||
use Utopia\Validator\Integer;
|
||||
use Utopia\Validator\Range;
|
||||
use Utopia\Validator\Text;
|
||||
use Utopia\Validator\WhiteList;
|
||||
|
@ -337,7 +336,9 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
|||
->inject('mode')
|
||||
->inject('deviceFiles')
|
||||
->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));
|
||||
|
||||
if (
|
||||
|
@ -632,6 +633,11 @@ App::post('/v1/storage/buckets/:bucketId/files')
|
|||
->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
|
||||
|
||||
$response->setStatusCode(Response::STATUS_CODE_CREATED);
|
||||
|
@ -781,6 +787,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
|||
->desc('Get File Preview')
|
||||
->groups(['api', 'storage'])
|
||||
->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.namespace', 'storage')
|
||||
->label('sdk.method', 'getFilePreview')
|
||||
|
@ -840,9 +848,6 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
|||
$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 . $gravity . $quality . $borderWidth . $borderColor . $borderRadius . $opacity . $rotation . $background . $output);
|
||||
|
||||
if ($bucket->getAttribute('permission') === 'bucket') {
|
||||
// skip authorization
|
||||
$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');
|
||||
$cipher = $file->getAttribute('openSSLCipher');
|
||||
$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)) {
|
||||
$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;
|
||||
$background = (empty($background)) ? 'eceff1' : $background;
|
||||
$type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION));
|
||||
$key = \md5($path . $width . $height . $gravity . $quality . $borderWidth . $borderColor . $borderRadius . $opacity . $rotation . $background . $output);
|
||||
$deviceFiles = $deviceLocal;
|
||||
}
|
||||
|
||||
|
@ -883,23 +886,12 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
|||
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)) {
|
||||
// when file extension is not provided and the mime type is not one of our supported outputs
|
||||
// we fallback to `jpg` output format
|
||||
$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);
|
||||
|
||||
|
@ -944,20 +936,18 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview')
|
|||
|
||||
$data = $image->output($output, $quality);
|
||||
|
||||
$cache->save($key, $data);
|
||||
|
||||
$usage
|
||||
->setParam('storage.files.read', 1)
|
||||
->setParam('bucketId', $bucketId)
|
||||
;
|
||||
|
||||
$response
|
||||
->setContentType((\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'])
|
||||
->addHeader('Expires', $date)
|
||||
->addHeader('X-Appwrite-Cache', 'miss')
|
||||
->send($data)
|
||||
;
|
||||
$contentType = (\array_key_exists($output, $outputs)) ? $outputs[$output] : $outputs['jpg'];
|
||||
|
||||
$response
|
||||
->addHeader('Expires', \date('D, d M Y H:i:s', \time() + 60 * 60 * 24 * 30) . ' GMT')
|
||||
->setContentType($contentType)
|
||||
->file($data)
|
||||
;
|
||||
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('fileId', '', new UID(), 'File ID.')
|
||||
->inject('response')
|
||||
->inject('request')
|
||||
->inject('dbForProject')
|
||||
->inject('events')
|
||||
->inject('usage')
|
||||
->inject('mode')
|
||||
->inject('deviceFiles')
|
||||
->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));
|
||||
|
||||
if (
|
||||
|
@ -1408,10 +1400,10 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId')
|
|||
}
|
||||
|
||||
if ($deviceDeleted) {
|
||||
//delete related cache
|
||||
$cacheDir = APP_STORAGE_CACHE . DIRECTORY_SEPARATOR . 'app-' . $project->getId() . DIRECTORY_SEPARATOR . $bucketId . DIRECTORY_SEPARATOR . $fileId;
|
||||
$deviceLocal = new Local($cacheDir);
|
||||
$deviceLocal->delete($cacheDir, true);
|
||||
$deletes
|
||||
->setType(DELETE_TYPE_CACHE_BY_RESOURCE)
|
||||
->setResource('file/' . $fileId)
|
||||
;
|
||||
|
||||
if ($bucket->getAttribute('permission') === 'bucket') {
|
||||
$deleted = Authorization::skip(fn () => $dbForProject->deleteDocument('bucket_' . $bucket->getInternalId(), $fileId));
|
||||
|
|
|
@ -14,6 +14,8 @@ use Utopia\App;
|
|||
use Appwrite\Extend\Exception;
|
||||
use Utopia\Abuse\Abuse;
|
||||
use Utopia\Abuse\Adapters\TimeLimit;
|
||||
use Utopia\Cache\Adapter\Filesystem;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Validator\Authorization;
|
||||
|
@ -149,6 +151,31 @@ App::init()
|
|||
|
||||
$deletes->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()
|
||||
|
@ -219,6 +246,7 @@ App::shutdown()
|
|||
->inject('mode')
|
||||
->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) {
|
||||
|
||||
$responsePayload = $response->getPayload();
|
||||
|
||||
if (!empty($events->getEvent())) {
|
||||
|
@ -319,12 +347,56 @@ App::shutdown()
|
|||
$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 (
|
||||
App::getEnv('_APP_USAGE_STATS', 'enabled') == 'enabled'
|
||||
&& $project->getId()
|
||||
&& $mode !== APP_MODE_ADMIN // TODO: add check to make sure user is admin
|
||||
&& !empty($route->getLabel('sdk.namespace', null))
|
||||
) { // Don't calculate console usage on admin mode
|
||||
) {
|
||||
$usage
|
||||
->setParam('networkRequestSize', $request->getSize() + $usage->getParam('storage'))
|
||||
->setParam('networkResponseSize', $response->getSize())
|
||||
|
|
|
@ -143,6 +143,8 @@ const DELETE_TYPE_USAGE = 'usage';
|
|||
const DELETE_TYPE_REALTIME = 'realtime';
|
||||
const DELETE_TYPE_BUCKETS = 'buckets';
|
||||
const DELETE_TYPE_SESSIONS = 'sessions';
|
||||
const DELETE_TYPE_CACHE_BY_TIMESTAMP = 'cacheByTimeStamp';
|
||||
const DELETE_TYPE_CACHE_BY_RESOURCE = 'cacheByResource';
|
||||
// Mail Types
|
||||
const MAIL_TYPE_VERIFICATION = 'verification';
|
||||
const MAIL_TYPE_MAGIC_SESSION = 'magicSession';
|
||||
|
|
|
@ -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)
|
||||
$interval = (int) App::getEnv('_APP_MAINTENANCE_INTERVAL', '86400');
|
||||
$executionLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_EXECUTION', '1209600');
|
||||
|
@ -134,8 +143,9 @@ $cli
|
|||
$abuseLogsRetention = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_ABUSE', '86400');
|
||||
$usageStatsRetention30m = (int) App::getEnv('_APP_MAINTENANCE_RETENTION_USAGE_30M', '129600'); //36 hours
|
||||
$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();
|
||||
|
||||
$time = date('d-m-Y H:i:s', time());
|
||||
|
@ -147,5 +157,6 @@ $cli
|
|||
notifyDeleteConnections();
|
||||
notifyDeleteExpiredSessions();
|
||||
renewCertificates($database);
|
||||
notifyDeleteCache($cacheRetention);
|
||||
}, $interval);
|
||||
});
|
||||
|
|
|
@ -147,6 +147,7 @@ services:
|
|||
- _APP_STATSD_PORT
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
- _APP_MAINTENANCE_RETENTION_ABUSE
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT
|
||||
- _APP_SMS_PROVIDER
|
||||
|
@ -545,6 +546,7 @@ services:
|
|||
- _APP_DB_PASS
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
- _APP_MAINTENANCE_RETENTION_ABUSE
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT
|
||||
|
||||
|
|
|
@ -1,6 +1,8 @@
|
|||
<?php
|
||||
|
||||
use Utopia\App;
|
||||
use Utopia\Cache\Adapter\Filesystem;
|
||||
use Utopia\Cache\Cache;
|
||||
use Utopia\Database\Database;
|
||||
use Utopia\Database\Document;
|
||||
use Utopia\Database\Query;
|
||||
|
@ -33,7 +35,6 @@ class DeletesV1 extends Worker
|
|||
{
|
||||
$project = new Document($this->args['project'] ?? []);
|
||||
$type = $this->args['type'] ?? '';
|
||||
|
||||
switch (strval($type)) {
|
||||
case DELETE_TYPE_DOCUMENT:
|
||||
$document = new Document($this->args['document'] ?? []);
|
||||
|
@ -107,6 +108,13 @@ class DeletesV1 extends Worker
|
|||
case DELETE_TYPE_USAGE:
|
||||
$this->deleteUsageStats($this->args['timestamp1d'], $this->args['timestamp30m']);
|
||||
break;
|
||||
|
||||
case DELETE_TYPE_CACHE_BY_RESOURCE:
|
||||
$this->deleteCacheByResource($project->getId());
|
||||
break;
|
||||
case DELETE_TYPE_CACHE_BY_TIMESTAMP:
|
||||
$this->deleteCacheByTimestamp();
|
||||
break;
|
||||
default:
|
||||
Console::error('No delete operation for type: ' . $type);
|
||||
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 string $projectId
|
||||
|
|
|
@ -103,11 +103,9 @@ services:
|
|||
- ./phpunit.xml:/usr/src/code/phpunit.xml
|
||||
- ./tests:/usr/src/code/tests
|
||||
- ./app:/usr/src/code/app
|
||||
# - ./vendor/utopia/database:/usr/src/code/vendor/utopia/database
|
||||
- ./docs:/usr/src/code/docs
|
||||
- ./public:/usr/src/code/public
|
||||
- ./src:/usr/src/code/src
|
||||
# - ./debug:/tmp
|
||||
- ./dev:/usr/local/dev
|
||||
depends_on:
|
||||
- mariadb
|
||||
|
@ -173,6 +171,7 @@ services:
|
|||
- _APP_STATSD_PORT
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
- _APP_MAINTENANCE_RETENTION_ABUSE
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT
|
||||
- _APP_SMS_PROVIDER
|
||||
|
@ -207,7 +206,6 @@ services:
|
|||
volumes:
|
||||
- ./app:/usr/src/code/app
|
||||
- ./src:/usr/src/code/src
|
||||
# - ./vendor:/usr/src/code/vendor
|
||||
depends_on:
|
||||
- mariadb
|
||||
- redis
|
||||
|
@ -579,6 +577,7 @@ services:
|
|||
- _APP_DB_PASS
|
||||
- _APP_MAINTENANCE_INTERVAL
|
||||
- _APP_MAINTENANCE_RETENTION_EXECUTION
|
||||
- _APP_MAINTENANCE_RETENTION_CACHE
|
||||
- _APP_MAINTENANCE_RETENTION_ABUSE
|
||||
- _APP_MAINTENANCE_RETENTION_AUDIT
|
||||
|
||||
|
|
|
@ -12,6 +12,7 @@ class Delete extends Event
|
|||
protected ?int $timestamp1d = null;
|
||||
protected ?int $timestamp30m = null;
|
||||
protected ?Document $document = null;
|
||||
protected ?string $resource = null;
|
||||
|
||||
public function __construct()
|
||||
{
|
||||
|
@ -93,6 +94,29 @@ class Delete extends Event
|
|||
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.
|
||||
*
|
||||
|
@ -103,6 +127,7 @@ class Delete extends Event
|
|||
return $this->document;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Executes this event and sends it to the deletes worker.
|
||||
*
|
||||
|
@ -117,7 +142,8 @@ class Delete extends Event
|
|||
'document' => $this->document,
|
||||
'timestamp' => $this->timestamp,
|
||||
'timestamp1d' => $this->timestamp1d,
|
||||
'timestamp30m' => $this->timestamp30m
|
||||
'timestamp30m' => $this->timestamp30m,
|
||||
'resource' => $this->resource,
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -475,6 +475,24 @@ class Response extends SwooleResponse
|
|||
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
|
||||
*
|
||||
|
@ -506,7 +524,6 @@ class Response extends SwooleResponse
|
|||
return $this->payload;
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Function to set a response filter
|
||||
*
|
||||
|
|
|
@ -490,7 +490,7 @@ trait StorageBase
|
|||
|
||||
$this->assertEquals(204, $file['headers']['status-code']);
|
||||
$this->assertEmpty($file['body']);
|
||||
|
||||
sleep(1);
|
||||
//upload again using the same ID
|
||||
$file = $this->client->call(Client::METHOD_POST, '/storage/buckets/' . $bucketId . '/files', array_merge([
|
||||
'content-type' => 'multipart/form-data',
|
||||
|
|
Loading…
Reference in a new issue