diff --git a/app/controllers/api/functions.php b/app/controllers/api/functions.php index 42e95e27a3..b129d88993 100644 --- a/app/controllers/api/functions.php +++ b/app/controllers/api/functions.php @@ -845,8 +845,8 @@ App::get('/v1/functions/:functionId/deployments/:deploymentId/download') ->inject('response') ->inject('request') ->inject('dbForProject') - ->inject('deviceFunctions') - ->action(function (string $functionId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceFunctions) { + ->inject('deviceForFunctions') + ->action(function (string $functionId, string $deploymentId, Response $response, Request $request, Database $dbForProject, Device $deviceForFunctions) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -863,7 +863,7 @@ App::get('/v1/functions/:functionId/deployments/:deploymentId/download') } $path = $deployment->getAttribute('path', ''); - if (!$deviceFunctions->exists($path)) { + if (!$deviceForFunctions->exists($path)) { throw new Exception(Exception::DEPLOYMENT_NOT_FOUND); } @@ -873,7 +873,7 @@ App::get('/v1/functions/:functionId/deployments/:deploymentId/download') ->addHeader('X-Peak', \memory_get_peak_usage()) ->addHeader('Content-Disposition', 'attachment; filename="' . $deploymentId . '.tar.gz"'); - $size = $deviceFunctions->getFileSize($path); + $size = $deviceForFunctions->getFileSize($path); $rangeHeader = $request->getHeader('range'); if (!empty($rangeHeader)) { @@ -895,13 +895,13 @@ App::get('/v1/functions/:functionId/deployments/:deploymentId/download') ->addHeader('Content-Length', $end - $start + 1) ->setStatusCode(Response::STATUS_CODE_PARTIALCONTENT); - $response->send($deviceFunctions->read($path, $start, ($end - $start + 1))); + $response->send($deviceForFunctions->read($path, $start, ($end - $start + 1))); } if ($size > APP_STORAGE_READ_BUFFER) { for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) { $response->chunk( - $deviceFunctions->read( + $deviceForFunctions->read( $path, ($i * MAX_OUTPUT_CHUNK_SIZE), min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE)) @@ -910,7 +910,7 @@ App::get('/v1/functions/:functionId/deployments/:deploymentId/download') ); } } else { - $response->send($deviceFunctions->read($path)); + $response->send($deviceForFunctions->read($path)); } }); @@ -1049,10 +1049,10 @@ App::post('/v1/functions/:functionId/deployments') ->inject('dbForProject') ->inject('queueForEvents') ->inject('project') - ->inject('deviceFunctions') - ->inject('deviceLocal') + ->inject('deviceForFunctions') + ->inject('deviceForLocal') ->inject('queueForBuilds') - ->action(function (string $functionId, ?string $entrypoint, ?string $commands, mixed $code, bool $activate, Request $request, Response $response, Database $dbForProject, Event $queueForEvents, Document $project, Device $deviceFunctions, Device $deviceLocal, Build $queueForBuilds) { + ->action(function (string $functionId, ?string $entrypoint, ?string $commands, mixed $code, bool $activate, Request $request, Response $response, Database $dbForProject, Event $queueForEvents, Document $project, Device $deviceForFunctions, Device $deviceForLocal, Build $queueForBuilds) { $activate = filter_var($activate, FILTER_VALIDATE_BOOLEAN); @@ -1133,11 +1133,11 @@ App::post('/v1/functions/:functionId/deployments') } // Save to storage - $fileSize ??= $deviceLocal->getFileSize($fileTmpName); - $path = $deviceFunctions->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION)); + $fileSize ??= $deviceForLocal->getFileSize($fileTmpName); + $path = $deviceForFunctions->getPath($deploymentId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION)); $deployment = $dbForProject->getDocument('deployments', $deploymentId); - $metadata = ['content_type' => $deviceLocal->getFileMimeType($fileTmpName)]; + $metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)]; if (!$deployment->isEmpty()) { $chunks = $deployment->getAttribute('chunksTotal', 1); $metadata = $deployment->getAttribute('metadata', []); @@ -1146,7 +1146,7 @@ App::post('/v1/functions/:functionId/deployments') } } - $chunksUploaded = $deviceFunctions->upload($fileTmpName, $path, $chunk, $chunks, $metadata); + $chunksUploaded = $deviceForFunctions->upload($fileTmpName, $path, $chunk, $chunks, $metadata); if (empty($chunksUploaded)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed moving file'); @@ -1169,7 +1169,7 @@ App::post('/v1/functions/:functionId/deployments') } } - $fileSize = $deviceFunctions->getFileSize($path); + $fileSize = $deviceForFunctions->getFileSize($path); if ($deployment->isEmpty()) { $deployment = $dbForProject->createDocument('deployments', new Document([ @@ -1378,8 +1378,8 @@ App::delete('/v1/functions/:functionId/deployments/:deploymentId') ->inject('dbForProject') ->inject('queueForDeletes') ->inject('queueForEvents') - ->inject('deviceFunctions') - ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Device $deviceFunctions) { + ->inject('deviceForFunctions') + ->action(function (string $functionId, string $deploymentId, Response $response, Database $dbForProject, Delete $queueForDeletes, Event $queueForEvents, Device $deviceForFunctions) { $function = $dbForProject->getDocument('functions', $functionId); if ($function->isEmpty()) { @@ -1400,7 +1400,7 @@ App::delete('/v1/functions/:functionId/deployments/:deploymentId') } if (!empty($deployment->getAttribute('path', ''))) { - if (!($deviceFunctions->delete($deployment->getAttribute('path', '')))) { + if (!($deviceForFunctions->delete($deployment->getAttribute('path', '')))) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to remove deployment from storage'); } } diff --git a/app/controllers/api/health.php b/app/controllers/api/health.php index 4e0b094926..3ea96c9744 100644 --- a/app/controllers/api/health.php +++ b/app/controllers/api/health.php @@ -792,8 +792,8 @@ App::get('/v1/health/stats') // Currently only used internally ->label('docs', false) ->inject('response') ->inject('register') - ->inject('deviceFiles') - ->action(function (Response $response, Registry $register, Device $deviceFiles) { + ->inject('deviceForFiles') + ->action(function (Response $response, Registry $register, Device $deviceForFiles) { $cache = $register->get('cache'); @@ -802,9 +802,9 @@ App::get('/v1/health/stats') // Currently only used internally $response ->json([ 'storage' => [ - 'used' => Storage::human($deviceFiles->getDirectorySize($deviceFiles->getRoot() . '/')), - 'partitionTotal' => Storage::human($deviceFiles->getPartitionTotalSpace()), - 'partitionFree' => Storage::human($deviceFiles->getPartitionFreeSpace()), + 'used' => Storage::human($deviceForFiles->getDirectorySize($deviceForFiles->getRoot() . '/')), + 'partitionTotal' => Storage::human($deviceForFiles->getPartitionTotalSpace()), + 'partitionFree' => Storage::human($deviceForFiles->getPartitionFreeSpace()), ], 'cache' => [ 'uptime' => $cacheStats['uptime_in_seconds'] ?? 0, diff --git a/app/controllers/api/messaging.php b/app/controllers/api/messaging.php index d8af44f7fd..f07a2c0d5f 100644 --- a/app/controllers/api/messaging.php +++ b/app/controllers/api/messaging.php @@ -10,6 +10,7 @@ use Appwrite\Messaging\Status as MessageStatus; use Appwrite\Network\Validator\Email; use Appwrite\Permission; use Appwrite\Role; +use Appwrite\Utopia\Database\Validator\CompoundUID; use Appwrite\Utopia\Database\Validator\CustomId; use Appwrite\Utopia\Database\Validator\Queries\Messages; use Appwrite\Utopia\Database\Validator\Queries\Providers; @@ -2573,6 +2574,7 @@ App::post('/v1/messaging/messages/email') ->param('targets', [], new ArrayList(new UID()), 'List of Targets IDs.', true) ->param('cc', [], new ArrayList(new UID()), 'Array of target IDs to be added as CC.', true) ->param('bcc', [], new ArrayList(new UID()), 'Array of target IDs to be added as BCC.', true) + ->param('attachments', [], new ArrayList(new CompoundUID()), 'Array of compound bucket IDs to file IDs to be attached to the email.', true) ->param('status', MessageStatus::DRAFT, new WhiteList([MessageStatus::DRAFT, MessageStatus::SCHEDULED, MessageStatus::PROCESSING]), 'Message Status. Value must be one of: ' . implode(', ', [MessageStatus::DRAFT, MessageStatus::SCHEDULED, MessageStatus::PROCESSING]) . '.', true) ->param('html', false, new Boolean(), 'Is content of type HTML', true) ->param('scheduledAt', null, new DatetimeValidator(requireDateInFuture: true), 'Scheduled delivery time for message in [ISO 8601](https://www.iso.org/iso-8601-date-and-time-format.html) format. DateTime value must be in future.', true) @@ -2582,7 +2584,7 @@ App::post('/v1/messaging/messages/email') ->inject('project') ->inject('queueForMessaging') ->inject('response') - ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, array $cc, array $bcc, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) { + ->action(function (string $messageId, string $subject, string $content, array $topics, array $users, array $targets, array $cc, array $bcc, array $attachments, string $status, bool $html, ?string $scheduledAt, Event $queueForEvents, Database $dbForProject, Database $dbForConsole, Document $project, Messaging $queueForMessaging, Response $response) { $messageId = $messageId == 'unique()' ? ID::unique() : $messageId; @@ -2615,6 +2617,29 @@ App::post('/v1/messaging/messages/email') } } + if (!empty($attachments)) { + foreach ($attachments as &$attachment) { + [$bucketId, $fileId] = CompoundUID::parse($attachment); + + $bucket = $dbForProject->getDocument('buckets', $bucketId); + + if ($bucket->isEmpty()) { + throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); + } + + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + + if ($file->isEmpty()) { + throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); + } + + $attachment = [ + 'bucketId' => $bucketId, + 'fileId' => $fileId, + ]; + } + } + $message = $dbForProject->createDocument('messages', new Document([ '$id' => $messageId, 'providerType' => MESSAGE_TYPE_EMAIL, @@ -2628,6 +2653,7 @@ App::post('/v1/messaging/messages/email') 'html' => $html, 'cc' => $cc, 'bcc' => $bcc, + 'attachments' => $attachments, ], 'status' => $status, ])); diff --git a/app/controllers/api/storage.php b/app/controllers/api/storage.php index 7fe2580aec..4968fc381d 100644 --- a/app/controllers/api/storage.php +++ b/app/controllers/api/storage.php @@ -360,9 +360,9 @@ App::post('/v1/storage/buckets/:bucketId/files') ->inject('user') ->inject('queueForEvents') ->inject('mode') - ->inject('deviceFiles') - ->inject('deviceLocal') - ->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, string $mode, Device $deviceFiles, Device $deviceLocal) { + ->inject('deviceForFiles') + ->inject('deviceForLocal') + ->action(function (string $bucketId, string $fileId, mixed $file, ?array $permissions, Request $request, Response $response, Database $dbForProject, Document $user, Event $queueForEvents, string $mode, Device $deviceForFiles, Device $deviceForLocal) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); @@ -493,13 +493,13 @@ App::post('/v1/storage/buckets/:bucketId/files') } // Save to storage - $fileSize ??= $deviceLocal->getFileSize($fileTmpName); - $path = $deviceFiles->getPath($fileId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION)); - $path = str_ireplace($deviceFiles->getRoot(), $deviceFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root + $fileSize ??= $deviceForLocal->getFileSize($fileTmpName); + $path = $deviceForFiles->getPath($fileId . '.' . \pathinfo($fileName, PATHINFO_EXTENSION)); + $path = str_ireplace($deviceForFiles->getRoot(), $deviceForFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $path); // Add bucket id to path after root $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); - $metadata = ['content_type' => $deviceLocal->getFileMimeType($fileTmpName)]; + $metadata = ['content_type' => $deviceForLocal->getFileMimeType($fileTmpName)]; if (!$file->isEmpty()) { $chunks = $file->getAttribute('chunksTotal', 1); $uploaded = $file->getAttribute('chunksUploaded', 0); @@ -514,32 +514,32 @@ App::post('/v1/storage/buckets/:bucketId/files') } } - $chunksUploaded = $deviceFiles->upload($fileTmpName, $path, $chunk, $chunks, $metadata); + $chunksUploaded = $deviceForFiles->upload($fileTmpName, $path, $chunk, $chunks, $metadata); if (empty($chunksUploaded)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed uploading file'); } if ($chunksUploaded === $chunks) { - if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled' && $bucket->getAttribute('antivirus', true) && $fileSize <= APP_LIMIT_ANTIVIRUS && $deviceFiles->getType() === Storage::DEVICE_LOCAL) { + if (App::getEnv('_APP_STORAGE_ANTIVIRUS') === 'enabled' && $bucket->getAttribute('antivirus', true) && $fileSize <= APP_LIMIT_ANTIVIRUS && $deviceForFiles->getType() === Storage::DEVICE_LOCAL) { $antivirus = new Network( App::getEnv('_APP_STORAGE_ANTIVIRUS_HOST', 'clamav'), (int) App::getEnv('_APP_STORAGE_ANTIVIRUS_PORT', 3310) ); if (!$antivirus->fileScan($path)) { - $deviceFiles->delete($path); + $deviceForFiles->delete($path); throw new Exception(Exception::STORAGE_INVALID_FILE); } } - $mimeType = $deviceFiles->getFileMimeType($path); // Get mime-type before compression and encryption - $fileHash = $deviceFiles->getFileHash($path); // Get file hash before compression and encryption + $mimeType = $deviceForFiles->getFileMimeType($path); // Get mime-type before compression and encryption + $fileHash = $deviceForFiles->getFileHash($path); // Get file hash before compression and encryption $data = ''; // Compression $algorithm = $bucket->getAttribute('compression', Compression::NONE); if ($fileSize <= APP_STORAGE_READ_BUFFER && $algorithm != Compression::NONE) { - $data = $deviceFiles->read($path); + $data = $deviceForFiles->read($path); switch ($algorithm) { case Compression::ZSTD: $compressor = new Zstd(); @@ -559,7 +559,7 @@ App::post('/v1/storage/buckets/:bucketId/files') if ($bucket->getAttribute('encryption', true) && $fileSize <= APP_STORAGE_READ_BUFFER) { if (empty($data)) { - $data = $deviceFiles->read($path); + $data = $deviceForFiles->read($path); } $key = App::getEnv('_APP_OPENSSL_KEY_V1'); $iv = OpenSSL::randomPseudoBytes(OpenSSL::cipherIVLength(OpenSSL::CIPHER_AES_128_GCM)); @@ -567,12 +567,12 @@ App::post('/v1/storage/buckets/:bucketId/files') } if (!empty($data)) { - if (!$deviceFiles->write($path, $data, $mimeType)) { + if (!$deviceForFiles->write($path, $data, $mimeType)) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Failed to save file'); } } - $sizeActual = $deviceFiles->getFileSize($path); + $sizeActual = $deviceForFiles->getFileSize($path); $openSSLVersion = null; $openSSLCipher = null; @@ -872,9 +872,9 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') ->inject('project') ->inject('dbForProject') ->inject('mode') - ->inject('deviceFiles') - ->inject('deviceLocal') - ->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceFiles, Device $deviceLocal) { + ->inject('deviceForFiles') + ->inject('deviceForLocal') + ->action(function (string $bucketId, string $fileId, int $width, int $height, string $gravity, int $quality, int $borderWidth, string $borderColor, int $borderRadius, float $opacity, int $rotation, string $background, string $output, Request $request, Response $response, Document $project, Database $dbForProject, string $mode, Device $deviceForFiles, Device $deviceForLocal) { if (!\extension_loaded('imagick')) { throw new Exception(Exception::GENERAL_SERVER_ERROR, 'Imagick extension is missing'); @@ -931,10 +931,10 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $cipher = null; $background = (empty($background)) ? 'eceff1' : $background; $type = \strtolower(\pathinfo($path, PATHINFO_EXTENSION)); - $deviceFiles = $deviceLocal; + $deviceForFiles = $deviceForLocal; } - if (!$deviceFiles->exists($path)) { + if (!$deviceForFiles->exists($path)) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); } @@ -950,7 +950,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/preview') $output = empty($type) ? (array_search($mime, $outputs) ?? 'jpg') : $type; } - $source = $deviceFiles->read($path); + $source = $deviceForFiles->read($path); if (!empty($cipher)) { // Decrypt $source = OpenSSL::decrypt( @@ -1033,8 +1033,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') ->inject('response') ->inject('dbForProject') ->inject('mode') - ->inject('deviceFiles') - ->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode, Device $deviceFiles) { + ->inject('deviceForFiles') + ->action(function (string $bucketId, string $fileId, Request $request, Response $response, Database $dbForProject, string $mode, Device $deviceForFiles) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); @@ -1064,7 +1064,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') $path = $file->getAttribute('path', ''); - if (!$deviceFiles->exists($path)) { + if (!$deviceForFiles->exists($path)) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path); } @@ -1100,7 +1100,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') $source = ''; if (!empty($file->getAttribute('openSSLCipher'))) { // Decrypt - $source = $deviceFiles->read($path); + $source = $deviceForFiles->read($path); $source = OpenSSL::decrypt( $source, $file->getAttribute('openSSLCipher'), @@ -1114,14 +1114,14 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') switch ($file->getAttribute('algorithm', Compression::NONE)) { case Compression::ZSTD: if (empty($source)) { - $source = $deviceFiles->read($path); + $source = $deviceForFiles->read($path); } $compressor = new Zstd(); $source = $compressor->decompress($source); break; case Compression::GZIP: if (empty($source)) { - $source = $deviceFiles->read($path); + $source = $deviceForFiles->read($path); } $compressor = new GZIP(); $source = $compressor->decompress($source); @@ -1136,13 +1136,13 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') } if (!empty($rangeHeader)) { - $response->send($deviceFiles->read($path, $start, ($end - $start + 1))); + $response->send($deviceForFiles->read($path, $start, ($end - $start + 1))); } if ($size > APP_STORAGE_READ_BUFFER) { for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) { $response->chunk( - $deviceFiles->read( + $deviceForFiles->read( $path, ($i * MAX_OUTPUT_CHUNK_SIZE), min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE)) @@ -1151,7 +1151,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/download') ); } } else { - $response->send($deviceFiles->read($path)); + $response->send($deviceForFiles->read($path)); } }); @@ -1173,8 +1173,8 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') ->inject('request') ->inject('dbForProject') ->inject('mode') - ->inject('deviceFiles') - ->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Device $deviceFiles) { + ->inject('deviceForFiles') + ->action(function (string $bucketId, string $fileId, Response $response, Request $request, Database $dbForProject, string $mode, Device $deviceForFiles) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); @@ -1205,7 +1205,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') $path = $file->getAttribute('path', ''); - if (!$deviceFiles->exists($path)) { + if (!$deviceForFiles->exists($path)) { throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path); } @@ -1249,7 +1249,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') $source = ''; if (!empty($file->getAttribute('openSSLCipher'))) { // Decrypt - $source = $deviceFiles->read($path); + $source = $deviceForFiles->read($path); $source = OpenSSL::decrypt( $source, $file->getAttribute('openSSLCipher'), @@ -1263,14 +1263,14 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') switch ($file->getAttribute('algorithm', Compression::NONE)) { case Compression::ZSTD: if (empty($source)) { - $source = $deviceFiles->read($path); + $source = $deviceForFiles->read($path); } $compressor = new Zstd(); $source = $compressor->decompress($source); break; case Compression::GZIP: if (empty($source)) { - $source = $deviceFiles->read($path); + $source = $deviceForFiles->read($path); } $compressor = new GZIP(); $source = $compressor->decompress($source); @@ -1286,15 +1286,15 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') } if (!empty($rangeHeader)) { - $response->send($deviceFiles->read($path, $start, ($end - $start + 1))); + $response->send($deviceForFiles->read($path, $start, ($end - $start + 1))); return; } - $size = $deviceFiles->getFileSize($path); + $size = $deviceForFiles->getFileSize($path); if ($size > APP_STORAGE_READ_BUFFER) { for ($i = 0; $i < ceil($size / MAX_OUTPUT_CHUNK_SIZE); $i++) { $response->chunk( - $deviceFiles->read( + $deviceForFiles->read( $path, ($i * MAX_OUTPUT_CHUNK_SIZE), min(MAX_OUTPUT_CHUNK_SIZE, $size - ($i * MAX_OUTPUT_CHUNK_SIZE)) @@ -1303,7 +1303,7 @@ App::get('/v1/storage/buckets/:bucketId/files/:fileId/view') ); } } else { - $response->send($deviceFiles->read($path)); + $response->send($deviceForFiles->read($path)); } }); @@ -1438,9 +1438,9 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') ->inject('dbForProject') ->inject('queueForEvents') ->inject('mode') - ->inject('deviceFiles') + ->inject('deviceForFiles') ->inject('queueForDeletes') - ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceFiles, Delete $queueForDeletes) { + ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Event $queueForEvents, string $mode, Device $deviceForFiles, Delete $queueForDeletes) { $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); $isAPIKey = Auth::isAppUser(Authorization::getRoles()); @@ -1471,12 +1471,12 @@ App::delete('/v1/storage/buckets/:bucketId/files/:fileId') $deviceDeleted = false; if ($file->getAttribute('chunksTotal') !== $file->getAttribute('chunksUploaded')) { - $deviceDeleted = $deviceFiles->abort( + $deviceDeleted = $deviceForFiles->abort( $file->getAttribute('path'), ($file->getAttribute('metadata', [])['uploadId'] ?? '') ); } else { - $deviceDeleted = $deviceFiles->delete($file->getAttribute('path')); + $deviceDeleted = $deviceForFiles->delete($file->getAttribute('path')); } if ($deviceDeleted) { diff --git a/app/init.php b/app/init.php index 8935fc7265..17b4a4321d 100644 --- a/app/init.php +++ b/app/init.php @@ -1383,19 +1383,19 @@ App::setResource('cache', function (Group $pools) { return new Cache(new Sharding($adapters)); }, ['pools']); -App::setResource('deviceLocal', function () { +App::setResource('deviceForLocal', function () { return new Local(); }); -App::setResource('deviceFiles', function ($project) { +App::setResource('deviceForFiles', function ($project) { return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); }, ['project']); -App::setResource('deviceFunctions', function ($project) { +App::setResource('deviceForFunctions', function ($project) { return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()); }, ['project']); -App::setResource('deviceBuilds', function ($project) { +App::setResource('deviceForBuilds', function ($project) { return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); }, ['project']); diff --git a/app/worker.php b/app/worker.php index 701f7b7036..cd0601afad 100644 --- a/app/worker.php +++ b/app/worker.php @@ -34,6 +34,7 @@ use Utopia\Logger\Log; use Utopia\Logger\Logger; use Utopia\Pools\Group; use Utopia\Queue\Connection; +use Utopia\Storage\Device\Local; Authorization::disable(); Runtime::enableCoroutine(SWOOLE_HOOK_ALL); @@ -201,22 +202,26 @@ Server::setResource('pools', function (Registry $register) { return $register->get('pools'); }, ['register']); -Server::setResource('functionsDevice', function (Document $project) { +Server::setResource('deviceForFunctions', function (Document $project) { return getDevice(APP_STORAGE_FUNCTIONS . '/app-' . $project->getId()); }, ['project']); -Server::setResource('filesDevice', function (Document $project) { +Server::setResource('deviceForFiles', function (Document $project) { return getDevice(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); }, ['project']); -Server::setResource('buildsDevice', function (Document $project) { +Server::setResource('deviceForBuilds', function (Document $project) { return getDevice(APP_STORAGE_BUILDS . '/app-' . $project->getId()); }, ['project']); -Server::setResource('cacheDevice', function (Document $project) { +Server::setResource('deviceForCache', function (Document $project) { return getDevice(APP_STORAGE_CACHE . '/app-' . $project->getId()); }, ['project']); +Server::setResource('deviceForLocalFiles', function (Document $project) { + return new Local(APP_STORAGE_UPLOADS . '/app-' . $project->getId()); +}, ['project']); + $pools = $register->get('pools'); $platform = new Appwrite(); $args = $_SERVER['argv']; diff --git a/composer.json b/composer.json index a31599ce32..3b4c8677cc 100644 --- a/composer.json +++ b/composer.json @@ -56,7 +56,7 @@ "utopia-php/image": "0.6.*", "utopia-php/locale": "0.4.*", "utopia-php/logger": "0.3.*", - "utopia-php/messaging": "0.9.*", + "utopia-php/messaging": "0.10.*", "utopia-php/migration": "0.3.*", "utopia-php/orchestration": "0.9.*", "utopia-php/platform": "0.5.*", @@ -70,7 +70,7 @@ "utopia-php/websocket": "0.1.*", "matomo/device-detector": "6.1.*", "dragonmantank/cron-expression": "3.3.2", - "phpmailer/phpmailer": "6.8.0", + "phpmailer/phpmailer": "6.9.1", "chillerlan/php-qrcode": "4.3.4", "adhocore/jwt": "1.1.2", "spomky-labs/otphp": "^10.0", diff --git a/composer.lock b/composer.lock index 3956a87327..c9e1d72b93 100644 --- a/composer.lock +++ b/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "609062319cc652e2760367f39604ac77", + "content-hash": "a65e4309e3fd851aec97be2cf5b83cb4", "packages": [ { "name": "adhocore/jwt", @@ -885,16 +885,16 @@ }, { "name": "phpmailer/phpmailer", - "version": "v6.8.0", + "version": "v6.9.1", "source": { "type": "git", "url": "https://github.com/PHPMailer/PHPMailer.git", - "reference": "df16b615e371d81fb79e506277faea67a1be18f1" + "reference": "039de174cd9c17a8389754d3b877a2ed22743e18" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/df16b615e371d81fb79e506277faea67a1be18f1", - "reference": "df16b615e371d81fb79e506277faea67a1be18f1", + "url": "https://api.github.com/repos/PHPMailer/PHPMailer/zipball/039de174cd9c17a8389754d3b877a2ed22743e18", + "reference": "039de174cd9c17a8389754d3b877a2ed22743e18", "shasum": "" }, "require": { @@ -904,16 +904,17 @@ "php": ">=5.5.0" }, "require-dev": { - "dealerdirect/phpcodesniffer-composer-installer": "^0.7.2", + "dealerdirect/phpcodesniffer-composer-installer": "^1.0", "doctrine/annotations": "^1.2.6 || ^1.13.3", "php-parallel-lint/php-console-highlighter": "^1.0.0", "php-parallel-lint/php-parallel-lint": "^1.3.2", "phpcompatibility/php-compatibility": "^9.3.5", "roave/security-advisories": "dev-latest", - "squizlabs/php_codesniffer": "^3.7.1", + "squizlabs/php_codesniffer": "^3.7.2", "yoast/phpunit-polyfills": "^1.0.4" }, "suggest": { + "decomplexity/SendOauth2": "Adapter for using XOAUTH2 authentication", "ext-mbstring": "Needed to send email in multibyte encoding charset or decode encoded addresses", "ext-openssl": "Needed for secure SMTP sending and DKIM signing", "greew/oauth2-azure-provider": "Needed for Microsoft Azure XOAUTH2 authentication", @@ -953,7 +954,7 @@ "description": "PHPMailer is a full-featured email creation and transfer class for PHP", "support": { "issues": "https://github.com/PHPMailer/PHPMailer/issues", - "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.8.0" + "source": "https://github.com/PHPMailer/PHPMailer/tree/v6.9.1" }, "funding": [ { @@ -961,7 +962,7 @@ "type": "github" } ], - "time": "2023-03-06T14:43:22+00:00" + "time": "2023-11-25T22:23:28+00:00" }, { "name": "spomky-labs/otphp", @@ -1911,28 +1912,28 @@ }, { "name": "utopia-php/messaging", - "version": "0.9.1", + "version": "0.10.0", "source": { "type": "git", "url": "https://github.com/utopia-php/messaging.git", - "reference": "7beec07684e9e1dfcf4ab5b1ba731fa396dccbdf" + "reference": "71dce00ad43eb278a877cb2c329f7b8d677adfeb" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/utopia-php/messaging/zipball/7beec07684e9e1dfcf4ab5b1ba731fa396dccbdf", - "reference": "7beec07684e9e1dfcf4ab5b1ba731fa396dccbdf", + "url": "https://api.github.com/repos/utopia-php/messaging/zipball/71dce00ad43eb278a877cb2c329f7b8d677adfeb", + "reference": "71dce00ad43eb278a877cb2c329f7b8d677adfeb", "shasum": "" }, "require": { "ext-curl": "*", "ext-openssl": "*", - "php": ">=8.0.0" + "php": ">=8.0.0", + "phpmailer/phpmailer": "6.9.1" }, "require-dev": { - "laravel/pint": "1.13.*", - "phpmailer/phpmailer": "6.8.*", - "phpstan/phpstan": "1.10.*", - "phpunit/phpunit": "9.6.10" + "laravel/pint": "1.13.11", + "phpstan/phpstan": "1.10.58", + "phpunit/phpunit": "10.5.10" }, "type": "library", "autoload": { @@ -1955,9 +1956,9 @@ ], "support": { "issues": "https://github.com/utopia-php/messaging/issues", - "source": "https://github.com/utopia-php/messaging/tree/0.9.1" + "source": "https://github.com/utopia-php/messaging/tree/0.10.0" }, - "time": "2024-02-15T03:44:44+00:00" + "time": "2024-02-20T07:30:15+00:00" }, { "name": "utopia-php/migration", @@ -3409,16 +3410,16 @@ }, { "name": "phpdocumentor/type-resolver", - "version": "1.8.0", + "version": "1.8.1", "source": { "type": "git", "url": "https://github.com/phpDocumentor/TypeResolver.git", - "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc" + "reference": "bc3dc91a5e9b14aa06d1d9e90647c5c5a2cc5353" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/fad452781b3d774e3337b0c0b245dd8e5a4455fc", - "reference": "fad452781b3d774e3337b0c0b245dd8e5a4455fc", + "url": "https://api.github.com/repos/phpDocumentor/TypeResolver/zipball/bc3dc91a5e9b14aa06d1d9e90647c5c5a2cc5353", + "reference": "bc3dc91a5e9b14aa06d1d9e90647c5c5a2cc5353", "shasum": "" }, "require": { @@ -3461,9 +3462,9 @@ "description": "A PSR-5 based resolver of Class names, Types and Structural Element Names", "support": { "issues": "https://github.com/phpDocumentor/TypeResolver/issues", - "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.0" + "source": "https://github.com/phpDocumentor/TypeResolver/tree/1.8.1" }, - "time": "2024-01-11T11:49:22+00:00" + "time": "2024-01-18T19:15:27+00:00" }, { "name": "phpspec/prophecy", @@ -5019,16 +5020,16 @@ }, { "name": "squizlabs/php_codesniffer", - "version": "3.8.1", + "version": "3.9.0", "source": { "type": "git", "url": "https://github.com/PHPCSStandards/PHP_CodeSniffer.git", - "reference": "14f5fff1e64118595db5408e946f3a22c75807f7" + "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b" }, "dist": { "type": "zip", - "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/14f5fff1e64118595db5408e946f3a22c75807f7", - "reference": "14f5fff1e64118595db5408e946f3a22c75807f7", + "url": "https://api.github.com/repos/PHPCSStandards/PHP_CodeSniffer/zipball/d63cee4890a8afaf86a22e51ad4d97c91dd4579b", + "reference": "d63cee4890a8afaf86a22e51ad4d97c91dd4579b", "shasum": "" }, "require": { @@ -5095,7 +5096,7 @@ "type": "open_collective" } ], - "time": "2024-01-11T20:47:48+00:00" + "time": "2024-02-16T15:06:51+00:00" }, { "name": "swoole/ide-helper", diff --git a/src/Appwrite/Platform/Workers/Builds.php b/src/Appwrite/Platform/Workers/Builds.php index 3d2ef365be..5b2e5be761 100644 --- a/src/Appwrite/Platform/Workers/Builds.php +++ b/src/Appwrite/Platform/Workers/Builds.php @@ -50,9 +50,9 @@ class Builds extends Action ->inject('queueForUsage') ->inject('cache') ->inject('dbForProject') - ->inject('functionsDevice') + ->inject('deviceForFunctions') ->inject('log') - ->callback(fn($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $functionsDevice, Log $log) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $functionsDevice, $log)); + ->callback(fn($message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $usage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log) => $this->action($message, $dbForConsole, $queueForEvents, $queueForFunctions, $usage, $cache, $dbForProject, $deviceForFunctions, $log)); } /** @@ -63,12 +63,12 @@ class Builds extends Action * @param Usage $queueForUsage * @param Cache $cache * @param Database $dbForProject - * @param Device $functionsDevice + * @param Device $deviceForFunctions * @param Log $log * @return void * @throws \Utopia\Database\Exception */ - public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $functionsDevice, Log $log): void + public function action(Message $message, Database $dbForConsole, Event $queueForEvents, Func $queueForFunctions, Usage $queueForUsage, Cache $cache, Database $dbForProject, Device $deviceForFunctions, Log $log): void { $payload = $message->getPayload() ?? []; @@ -90,7 +90,7 @@ class Builds extends Action case BUILD_TYPE_RETRY: Console::info('Creating build for deployment: ' . $deployment->getId()); $github = new GitHub($cache); - $this->buildDeployment($functionsDevice, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log); + $this->buildDeployment($deviceForFunctions, $queueForFunctions, $queueForEvents, $queueForUsage, $dbForConsole, $dbForProject, $github, $project, $resource, $deployment, $template, $log); break; default: @@ -99,7 +99,7 @@ class Builds extends Action } /** - * @param Device $functionsDevice + * @param Device $deviceForFunctions * @param Func $queueForFunctions * @param Event $queueForEvents * @param Usage $queueForUsage @@ -115,7 +115,7 @@ class Builds extends Action * @throws \Utopia\Database\Exception * @throws Exception */ - protected function buildDeployment(Device $functionsDevice, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void + protected function buildDeployment(Device $deviceForFunctions, Func $queueForFunctions, Event $queueForEvents, Usage $queueForUsage, Database $dbForConsole, Database $dbForProject, GitHub $github, Document $project, Document $function, Document $deployment, Document $template, Log $log): void { $executor = new Executor(App::getEnv('_APP_EXECUTOR_HOST')); @@ -170,7 +170,7 @@ class Builds extends Action 'path' => '', 'runtime' => $function->getAttribute('runtime'), 'source' => $deployment->getAttribute('path', ''), - 'sourceType' => strtolower($functionsDevice->getType()), + 'sourceType' => strtolower($deviceForFunctions->getType()), 'logs' => '', 'endTime' => null, 'duration' => 0, @@ -311,8 +311,8 @@ class Builds extends Action Console::execute('tar --exclude code.tar.gz -czf ' . $tmpPathFile . ' -C /tmp/builds/' . \escapeshellcmd($buildId) . '/code' . (empty($rootDirectory) ? '' : '/' . $rootDirectory) . ' .', '', $stdout, $stderr); - $path = $functionsDevice->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); - $result = $localDevice->transfer($tmpPathFile, $path, $functionsDevice); + $path = $deviceForFunctions->getPath($deployment->getId() . '.' . \pathinfo('code.tar.gz', PATHINFO_EXTENSION)); + $result = $localDevice->transfer($tmpPathFile, $path, $deviceForFunctions); if (!$result) { throw new \Exception("Unable to move file"); diff --git a/src/Appwrite/Platform/Workers/Deletes.php b/src/Appwrite/Platform/Workers/Deletes.php index 473bdc6de9..e5a8445858 100644 --- a/src/Appwrite/Platform/Workers/Deletes.php +++ b/src/Appwrite/Platform/Workers/Deletes.php @@ -44,22 +44,22 @@ class Deletes extends Action ->inject('message') ->inject('dbForConsole') ->inject('getProjectDB') - ->inject('filesDevice') - ->inject('functionsDevice') - ->inject('buildsDevice') - ->inject('cacheDevice') + ->inject('deviceForFiles') + ->inject('deviceForFunctions') + ->inject('deviceForBuilds') + ->inject('deviceForCache') ->inject('abuseRetention') ->inject('executionRetention') ->inject('auditRetention') ->inject('log') - ->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $filesDevice, Device $functionsDevice, Device $buildsDevice, Device $cacheDevice, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $filesDevice, $functionsDevice, $buildsDevice, $cacheDevice, $abuseRetention, $executionRetention, $auditRetention, $log)); + ->callback(fn ($message, $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log) => $this->action($message, $dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $abuseRetention, $executionRetention, $auditRetention, $log)); } /** * @throws Exception * @throws Throwable */ - public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $filesDevice, Device $functionsDevice, Device $buildsDevice, Device $cacheDevice, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log): void + public function action(Message $message, Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, string $abuseRetention, string $executionRetention, string $auditRetention, Log $log): void { $payload = $message->getPayload() ?? []; @@ -87,13 +87,13 @@ class Deletes extends Action $this->deleteCollection($getProjectDB, $document, $project); break; case DELETE_TYPE_PROJECTS: - $this->deleteProject($dbForConsole, $getProjectDB, $filesDevice, $functionsDevice, $buildsDevice, $cacheDevice, $document); + $this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $document); break; case DELETE_TYPE_FUNCTIONS: - $this->deleteFunction($dbForConsole, $getProjectDB, $functionsDevice, $buildsDevice, $document, $project); + $this->deleteFunction($dbForConsole, $getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $project); break; case DELETE_TYPE_DEPLOYMENTS: - $this->deleteDeployment($getProjectDB, $functionsDevice, $buildsDevice, $document, $project); + $this->deleteDeployment($getProjectDB, $deviceForFunctions, $deviceForBuilds, $document, $project); break; case DELETE_TYPE_USERS: $this->deleteUser($getProjectDB, $document, $project); @@ -101,11 +101,11 @@ class Deletes extends Action case DELETE_TYPE_TEAMS: $this->deleteMemberships($getProjectDB, $document, $project); if ($project->getId() === 'console') { - $this->deleteProjectsByTeam($dbForConsole, $getProjectDB, $filesDevice, $functionsDevice, $buildsDevice, $cacheDevice, $document); + $this->deleteProjectsByTeam($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $document); } break; case DELETE_TYPE_BUCKETS: - $this->deleteBucket($getProjectDB, $filesDevice, $document, $project); + $this->deleteBucket($getProjectDB, $deviceForFiles, $document, $project); break; case DELETE_TYPE_INSTALLATIONS: $this->deleteInstallation($dbForConsole, $getProjectDB, $document, $project); @@ -511,14 +511,14 @@ class Deletes extends Action * @throws Restricted * @throws Structure */ - private function deleteProjectsByTeam(Database $dbForConsole, callable $getProjectDB, Device $filesDevice, Device $functionsDevice, Device $buildsDevice, Device $cacheDevice, Document $document): void + private function deleteProjectsByTeam(Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, Document $document): void { $projects = $dbForConsole->find('projects', [ Query::equal('teamInternalId', [$document->getInternalId()]) ]); foreach ($projects as $project) { - $this->deleteProject($dbForConsole, $getProjectDB, $filesDevice, $functionsDevice, $buildsDevice, $cacheDevice, $project); + $this->deleteProject($dbForConsole, $getProjectDB, $deviceForFiles, $deviceForFunctions, $deviceForBuilds, $deviceForCache, $project); $dbForConsole->deleteDocument('projects', $project->getId()); } } @@ -526,17 +526,17 @@ class Deletes extends Action /** * @param Database $dbForConsole * @param callable $getProjectDB - * @param Device $filesDevice - * @param Device $functionsDevice - * @param Device $buildsDevice - * @param Device $cacheDevice + * @param Device $deviceForFiles + * @param Device $deviceForFunctions + * @param Device $deviceForBuilds + * @param Device $deviceForCache * @param Document $document * @return void * @throws Exception * @throws Authorization * @throws \Utopia\Database\Exception */ - private function deleteProject(Database $dbForConsole, callable $getProjectDB, Device $filesDevice, Device $functionsDevice, Device $buildsDevice, Device $cacheDevice, Document $document): void + private function deleteProject(Database $dbForConsole, callable $getProjectDB, Device $deviceForFiles, Device $deviceForFunctions, Device $deviceForBuilds, Device $deviceForCache, Document $document): void { $projectId = $document->getId(); $projectInternalId = $document->getInternalId(); @@ -602,10 +602,10 @@ class Deletes extends Action } // Delete all storage directories - $filesDevice->delete($filesDevice->getRoot(), true); - $functionsDevice->delete($functionsDevice->getRoot(), true); - $buildsDevice->delete($buildsDevice->getRoot(), true); - $cacheDevice->delete($cacheDevice->getRoot(), true); + $deviceForFiles->delete($deviceForFiles->getRoot(), true); + $deviceForFunctions->delete($deviceForFunctions->getRoot(), true); + $deviceForBuilds->delete($deviceForBuilds->getRoot(), true); + $deviceForCache->delete($deviceForCache->getRoot(), true); } /** @@ -767,14 +767,14 @@ class Deletes extends Action /** * @param callable $getProjectDB - * @param Device $functionsDevice - * @param Device $buildsDevice + * @param Device $deviceForFunctions + * @param Device $deviceForBuilds * @param Document $document function document * @param Document $project * @return void * @throws Exception */ - private function deleteFunction(Database $dbForConsole, callable $getProjectDB, Device $functionsDevice, Device $buildsDevice, Document $document, Document $project): void + private function deleteFunction(Database $dbForConsole, callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, Document $project): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); @@ -810,9 +810,9 @@ class Deletes extends Action $deploymentInternalIds = []; $this->deleteByGroup('deployments', [ Query::equal('resourceInternalId', [$functionInternalId]) - ], $dbForProject, function (Document $document) use ($functionsDevice, &$deploymentInternalIds) { + ], $dbForProject, function (Document $document) use ($deviceForFunctions, &$deploymentInternalIds) { $deploymentInternalIds[] = $document->getInternalId(); - $this->deleteDeploymentFiles($functionsDevice, $document); + $this->deleteDeploymentFiles($deviceForFunctions, $document); }); /** @@ -823,8 +823,8 @@ class Deletes extends Action foreach ($deploymentInternalIds as $deploymentInternalId) { $this->deleteByGroup('builds', [ Query::equal('deploymentInternalId', [$deploymentInternalId]) - ], $dbForProject, function (Document $document) use ($buildsDevice) { - $this->deleteBuildFiles($buildsDevice, $document); + ], $dbForProject, function (Document $document) use ($deviceForBuilds) { + $this->deleteBuildFiles($deviceForBuilds, $document); }); } @@ -924,14 +924,14 @@ class Deletes extends Action /** * @param callable $getProjectDB - * @param Device $functionsDevice - * @param Device $buildsDevice + * @param Device $deviceForFunctions + * @param Device $deviceForBuilds * @param Document $document * @param Document $project * @return void * @throws Exception */ - private function deleteDeployment(callable $getProjectDB, Device $functionsDevice, Device $buildsDevice, Document $document, Document $project): void + private function deleteDeployment(callable $getProjectDB, Device $deviceForFunctions, Device $deviceForBuilds, Document $document, Document $project): void { $projectId = $project->getId(); $dbForProject = $getProjectDB($project); @@ -941,7 +941,7 @@ class Deletes extends Action /** * Delete deployment files */ - $this->deleteDeploymentFiles($functionsDevice, $document); + $this->deleteDeploymentFiles($deviceForFunctions, $document); /** * Delete builds @@ -950,8 +950,8 @@ class Deletes extends Action $this->deleteByGroup('builds', [ Query::equal('deploymentInternalId', [$deploymentInternalId]) - ], $dbForProject, function (Document $document) use ($buildsDevice) { - $this->deleteBuildFiles($buildsDevice, $document); + ], $dbForProject, function (Document $document) use ($deviceForBuilds) { + $this->deleteBuildFiles($deviceForBuilds, $document); }); /** @@ -1095,18 +1095,18 @@ class Deletes extends Action /** * @param callable $getProjectDB - * @param Device $filesDevice + * @param Device $deviceForFiles * @param Document $document * @param Document $project * @return void */ - private function deleteBucket(callable $getProjectDB, Device $filesDevice, Document $document, Document $project): void + private function deleteBucket(callable $getProjectDB, Device $deviceForFiles, Document $document, Document $project): void { $dbForProject = $getProjectDB($project); $dbForProject->deleteCollection('bucket_' . $document->getInternalId()); - $filesDevice->deletePath($document->getId()); + $deviceForFiles->deletePath($document->getId()); } /** diff --git a/src/Appwrite/Platform/Workers/Messaging.php b/src/Appwrite/Platform/Workers/Messaging.php index 2b954e4a9e..28f5081dee 100644 --- a/src/Appwrite/Platform/Workers/Messaging.php +++ b/src/Appwrite/Platform/Workers/Messaging.php @@ -2,11 +2,14 @@ namespace Appwrite\Platform\Workers; +use Appwrite\Auth\Auth; use Appwrite\Event\Usage; use Appwrite\Extend\Exception; use Appwrite\Messaging\Status as MessageStatus; use Utopia\App; use Utopia\CLI\Console; +use Utopia\Config\Config; +use Utopia\Database\Validator\Authorization; use Utopia\DSN\DSN; use Utopia\Database\Database; use Utopia\Database\DateTime; @@ -29,10 +32,13 @@ use Utopia\Messaging\Adapter\SMS\Textmagic; use Utopia\Messaging\Adapter\SMS\Twilio; use Utopia\Messaging\Adapter\SMS\Vonage; use Utopia\Messaging\Messages\Email; +use Utopia\Messaging\Messages\Email\Attachment; use Utopia\Messaging\Messages\Push; use Utopia\Messaging\Messages\SMS; use Utopia\Platform\Action; use Utopia\Queue\Message; +use Utopia\Storage\Device; +use Utopia\Storage\Storage; use function Swoole\Coroutine\batch; @@ -53,20 +59,29 @@ class Messaging extends Action ->inject('message') ->inject('log') ->inject('dbForProject') + ->inject('deviceForFiles') + ->inject('deviceForLocalFiles') ->inject('queueForUsage') - ->callback(fn(Message $message, Log $log, Database $dbForProject, Usage $queueForUsage) => $this->action($message, $log, $dbForProject, $queueForUsage)); + ->callback(fn(Message $message, Log $log, Database $dbForProject, Device $deviceForFiles, Device $deviceForLocalFiles, Usage $queueForUsage) => $this->action($message, $log, $dbForProject, $deviceForFiles, $deviceForLocalFiles, $queueForUsage)); } /** * @param Message $message * @param Log $log * @param Database $dbForProject + * @param callable $getLocalCache * @param Usage $queueForUsage * @return void * @throws \Exception */ - public function action(Message $message, Log $log, Database $dbForProject, Usage $queueForUsage): void - { + public function action( + Message $message, + Log $log, + Database $dbForProject, + Device $deviceForFiles, + Device $deviceForLocalFiles, + Usage $queueForUsage + ): void { $payload = $message->getPayload() ?? []; if (empty($payload)) { @@ -86,15 +101,19 @@ class Messaging extends Action case MESSAGE_SEND_TYPE_EXTERNAL: $message = $dbForProject->getDocument('messages', $payload['messageId']); - $this->sendExternalMessage($dbForProject, $message); + $this->sendExternalMessage($dbForProject, $message, $deviceForFiles, $deviceForLocalFiles,); break; default: throw new Exception('Unknown message type: ' . $type); } } - private function sendExternalMessage(Database $dbForProject, Document $message): void - { + private function sendExternalMessage( + Database $dbForProject, + Document $message, + Device $deviceForFiles, + Device $deviceForLocalFiles, + ): void { $topicIds = $message->getAttribute('topics', []); $targetIds = $message->getAttribute('targets', []); $userIds = $message->getAttribute('users', []); @@ -198,8 +217,8 @@ class Messaging extends Action /** * @var array $results */ - $results = batch(\array_map(function ($providerId) use ($identifiers, $providers, $fallback, $message, $dbForProject) { - return function () use ($providerId, $identifiers, $providers, $fallback, $message, $dbForProject) { + $results = batch(\array_map(function ($providerId) use ($identifiers, $providers, $fallback, $message, $dbForProject, $deviceForFiles, $deviceForLocalFiles) { + return function () use ($providerId, $identifiers, $providers, $fallback, $message, $dbForProject, $deviceForFiles, $deviceForLocalFiles) { if (\array_key_exists($providerId, $providers)) { $provider = $providers[$providerId]; } else { @@ -225,8 +244,8 @@ class Messaging extends Action $batches = \array_chunk($identifiers, $maxBatchSize); $batchIndex = 0; - return batch(\array_map(function ($batch) use ($message, $provider, $adapter, &$batchIndex, $dbForProject) { - return function () use ($batch, $message, $provider, $adapter, &$batchIndex, $dbForProject) { + return batch(\array_map(function ($batch) use ($message, $provider, $adapter, &$batchIndex, $dbForProject, $deviceForFiles, $deviceForLocalFiles) { + return function () use ($batch, $message, $provider, $adapter, &$batchIndex, $dbForProject, $deviceForFiles, $deviceForLocalFiles) { $deliveredTotal = 0; $deliveryErrors = []; $messageData = clone $message; @@ -235,7 +254,7 @@ class Messaging extends Action $data = match ($provider->getAttribute('type')) { MESSAGE_TYPE_SMS => $this->buildSmsMessage($messageData, $provider), MESSAGE_TYPE_PUSH => $this->buildPushMessage($messageData), - MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($dbForProject, $messageData, $provider), + MESSAGE_TYPE_EMAIL => $this->buildEmailMessage($dbForProject, $messageData, $provider, $deviceForFiles, $deviceForLocalFiles), default => throw new Exception(Exception::PROVIDER_INCORRECT_TYPE) }; @@ -309,6 +328,37 @@ class Messaging extends Action $message->setAttribute('deliveredAt', DateTime::now()); $dbForProject->updateDocument('messages', $message->getId(), $message); + + // Delete any attachments that were downloaded to the local cache + if ($provider->getAttribute('type') === MESSAGE_TYPE_EMAIL) { + if ($deviceForFiles->getType() === Storage::DEVICE_LOCAL) { + return; + } + + $data = $message->getAttribute('data'); + $attachments = $data['attachments'] ?? []; + + foreach ($attachments as $attachment) { + $bucketId = $attachment['bucketId']; + $fileId = $attachment['fileId']; + + $bucket = $dbForProject->getDocument('buckets', $bucketId); + if ($bucket->isEmpty()) { + throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); + } + + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + if ($file->isEmpty()) { + throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); + } + + $path = $file->getAttribute('path', ''); + + if ($deviceForLocalFiles->exists($path)) { + $deviceForLocalFiles->delete($path); + } + } + } } private function sendInternalSMSMessage(Document $message, Document $project, array $recipients, Usage $queueForUsage, Log $log): void @@ -458,8 +508,13 @@ class Messaging extends Action }; } - private function buildEmailMessage(Database $dbForProject, Document $message, Document $provider): Email - { + private function buildEmailMessage( + Database $dbForProject, + Document $message, + Document $provider, + Device $deviceForFiles, + Device $deviceForLocalFiles, + ): Email { $fromName = $provider['options']['fromName'] ?? null; $fromEmail = $provider['options']['fromEmail'] ?? null; $replyToEmail = $provider['options']['replyToEmail'] ?? null; @@ -469,8 +524,9 @@ class Messaging extends Action $bccTargets = $data['bcc'] ?? []; $cc = []; $bcc = []; + $attachments = $data['attachments'] ?? []; - if (\count($ccTargets) > 0) { + if (!empty($ccTargets)) { $ccTargets = $dbForProject->find('targets', [ Query::equal('$id', $ccTargets), Query::limit(\count($ccTargets)), @@ -480,7 +536,7 @@ class Messaging extends Action } } - if (\count($bccTargets) > 0) { + if (!empty($bccTargets)) { $bccTargets = $dbForProject->find('targets', [ Query::equal('$id', $bccTargets), Query::limit(\count($bccTargets)), @@ -490,12 +546,64 @@ class Messaging extends Action } } + if (!empty($attachments)) { + foreach ($attachments as &$attachment) { + $bucketId = $attachment['bucketId']; + $fileId = $attachment['fileId']; + + $bucket = $dbForProject->getDocument('buckets', $bucketId); + if ($bucket->isEmpty()) { + throw new Exception(Exception::STORAGE_BUCKET_NOT_FOUND); + } + + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + if ($file->isEmpty()) { + throw new Exception(Exception::STORAGE_FILE_NOT_FOUND); + } + + $mimes = Config::getParam('storage-mimes'); + $path = $file->getAttribute('path', ''); + + if (!$deviceForFiles->exists($path)) { + throw new Exception(Exception::STORAGE_FILE_NOT_FOUND, 'File not found in ' . $path); + } + + $contentType = 'text/plain'; + + if (\in_array($file->getAttribute('mimeType'), $mimes)) { + $contentType = $file->getAttribute('mimeType'); + } + + if ($deviceForFiles->getType() !== Storage::DEVICE_LOCAL) { + $deviceForFiles->transfer($path, $path, $deviceForLocalFiles); + } + + $attachment = new Attachment( + $file->getAttribute('name'), + $path, + $contentType + ); + } + } + $to = $message['to']; $subject = $data['subject']; $content = $data['content']; $html = $data['html'] ?? false; - return new Email($to, $subject, $content, $fromName, $fromEmail, $replyToName, $replyToEmail, $cc, $bcc, null, $html); + return new Email( + $to, + $subject, + $content, + $fromName, + $fromEmail, + $replyToName, + $replyToEmail, + $cc, + $bcc, + $attachments, + $html + ); } private function buildSmsMessage(Document $message, Document $provider): SMS @@ -504,7 +612,11 @@ class Messaging extends Action $content = $message['data']['content']; $from = $provider['options']['from']; - return new SMS($to, $content, $from); + return new SMS( + $to, + $content, + $from + ); } private function buildPushMessage(Document $message): Push @@ -520,6 +632,17 @@ class Messaging extends Action $tag = $message['data']['tag'] ?? null; $badge = $message['data']['badge'] ?? null; - return new Push($to, $title, $body, $data, $action, $sound, $icon, $color, $tag, $badge); + return new Push( + $to, + $title, + $body, + $data, + $action, + $sound, + $icon, + $color, + $tag, + $badge + ); } } diff --git a/src/Appwrite/Specification/Format/OpenAPI3.php b/src/Appwrite/Specification/Format/OpenAPI3.php index 6672631ff3..b405ec6d17 100644 --- a/src/Appwrite/Specification/Format/OpenAPI3.php +++ b/src/Appwrite/Specification/Format/OpenAPI3.php @@ -451,6 +451,10 @@ class OpenAPI3 extends Format $node['format'] = 'int32'; } break; + case 'Appwrite\Utopia\Database\Validator\CompoundUID': + $node['schema']['type'] = $validator->getType(); + $node['schema']['x-example'] = '[ID1:ID2]'; + break; default: $node['schema']['type'] = 'string'; break; diff --git a/src/Appwrite/Specification/Format/Swagger2.php b/src/Appwrite/Specification/Format/Swagger2.php index 20aeb96222..b2ccef6c07 100644 --- a/src/Appwrite/Specification/Format/Swagger2.php +++ b/src/Appwrite/Specification/Format/Swagger2.php @@ -446,6 +446,10 @@ class Swagger2 extends Format $node['format'] = 'int32'; } break; + case 'Appwrite\Utopia\Database\Validator\CompoundUID': + $node['type'] = $validator->getType(); + $node['x-example'] = '[ID1:ID2]'; + break; default: $node['type'] = 'string'; break; diff --git a/src/Appwrite/Utopia/Database/Validator/CompoundUID.php b/src/Appwrite/Utopia/Database/Validator/CompoundUID.php new file mode 100644 index 0000000000..3f23500952 --- /dev/null +++ b/src/Appwrite/Utopia/Database/Validator/CompoundUID.php @@ -0,0 +1,58 @@ +isValid($id)) { + return false; + } + } + + return true; + } + + public function getType(): string + { + return self::TYPE_STRING; + } + + public static function parse(string $key): array + { + $parts = \explode(':', $key); + $result = []; + + foreach ($parts as $part) { + $result[] = $part; + } + + return $result; + } +} diff --git a/tests/unit/Utopia/Database/Validator/CompoundUIDTest.php b/tests/unit/Utopia/Database/Validator/CompoundUIDTest.php new file mode 100644 index 0000000000..b443cf590b --- /dev/null +++ b/tests/unit/Utopia/Database/Validator/CompoundUIDTest.php @@ -0,0 +1,37 @@ +object = new CompoundUID(); + } + + public function tearDown(): void + { + } + + public function testValues(): void + { + $this->assertEquals($this->object->isValid('123:456'), true); + $this->assertEquals($this->object->isValid('123'), false); + $this->assertEquals($this->object->isValid('123:_456'), false); + $this->assertEquals($this->object->isValid('dasda asdasd'), false); + $this->assertEquals($this->object->isValid('dasda:asdasd'), true); + $this->assertEquals($this->object->isValid('_asdas:dasdas'), false); + $this->assertEquals($this->object->isValid('as$$5da:sdasdas'), false); + $this->assertEquals($this->object->isValid(false), false); + $this->assertEquals($this->object->isValid(null), false); + $this->assertEquals($this->object->isValid('socialAccountForYoutubeAndRestSubscribers:12345'), false); + $this->assertEquals($this->object->isValid('socialAccountForYoutubeAndRSubscriber:12345'), false); + $this->assertEquals($this->object->isValid('socialAccount:ForYoutubeSubscribe'), true); + $this->assertEquals($this->object->isValid('socialAccountForYoutubeSubscribe:socialAccountForYoutubeSubscribe'), true); + } +}