diff --git a/Dockerfile b/Dockerfile index 70d135746d..121858ef05 100755 --- a/Dockerfile +++ b/Dockerfile @@ -266,14 +266,14 @@ COPY ./src /usr/src/code/src # Set Volumes RUN mkdir -p /storage/uploads && \ - mkdir -p /storage/video && \ + mkdir -p /storage/videos && \ mkdir -p /storage/cache && \ mkdir -p /storage/config && \ mkdir -p /storage/certificates && \ mkdir -p /storage/functions && \ mkdir -p /storage/debug && \ chown -Rf www-data.www-data /storage/uploads && chmod -Rf 0755 /storage/uploads && \ - chown -Rf www-data.www-data /storage/video && chmod -Rf 0755 /storage/video && \ + chown -Rf www-data.www-data /storage/videos && chmod -Rf 0755 /storage/videos && \ chown -Rf www-data.www-data /storage/cache && chmod -Rf 0755 /storage/cache && \ chown -Rf www-data.www-data /storage/config && chmod -Rf 0755 /storage/config && \ chown -Rf www-data.www-data /storage/certificates && chmod -Rf 0755 /storage/certificates && \ diff --git a/app/config/collections.php b/app/config/collections.php index 50b6360689..a71eb542c1 100644 --- a/app/config/collections.php +++ b/app/config/collections.php @@ -16,10 +16,10 @@ $auth = Config::getParam('auth', []); */ $collections = [ - 'collections' => [ + 'databases' => [ '$collection' => Database::METADATA, - '$id' => 'collections', - 'name' => 'Collections', + '$id' => 'databases', + 'name' => 'Databases', 'attributes' => [ [ '$id' => 'name', @@ -31,27 +31,63 @@ $collections = [ 'filters' => [], ], [ - '$id' => 'dateCreated', - 'type' => Database::VAR_INTEGER, + '$id' => 'search', + 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 0, + 'size' => 16384, 'signed' => true, 'required' => false, 'default' => null, 'array' => false, 'filters' => [], ], + ], + 'indexes' => [ [ - '$id' => 'dateUpdated', - 'type' => Database::VAR_INTEGER, + '$id' => '_fulltext_search', + 'type' => Database::INDEX_FULLTEXT, + 'attributes' => ['search'], + 'lengths' => [], + 'orders' => [], + ], + ], + ], + 'collections' => [ + '$collection' => 'databases', + '$id' => 'collections', + 'name' => 'Collections', + 'attributes' => [ + [ + '$id' => 'databaseInternalId', + 'type' => Database::VAR_STRING, 'format' => '', - 'size' => 0, + 'size' => Database::LENGTH_KEY, 'signed' => true, - 'required' => false, + 'required' => true, 'default' => null, 'array' => false, 'filters' => [], ], + [ + '$id' => 'databaseId', + 'type' => Database::VAR_STRING, + 'signed' => true, + 'size' => Database::LENGTH_KEY, + 'format' => '', + 'filters' => [], + 'required' => true, + 'default' => null, + 'array' => false, + ], + [ + '$id' => 'name', + 'type' => Database::VAR_STRING, + 'size' => 256, + 'required' => true, + 'signed' => true, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'enabled', 'type' => Database::VAR_BOOLEAN, @@ -120,7 +156,7 @@ $collections = [ 'name' => 'Attributes', 'attributes' => [ [ - '$id' => 'collectionId', + '$id' => 'databaseInternalId', 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, @@ -130,6 +166,39 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'databaseId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => false, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'collectionInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'collectionId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'key', 'type' => Database::VAR_STRING, @@ -249,11 +318,11 @@ $collections = [ ], 'indexes' => [ [ - '$id' => '_key_collection', + '$id' => '_key_db_collection', 'type' => Database::INDEX_KEY, - 'attributes' => ['collectionId'], - 'lengths' => [Database::LENGTH_KEY], - 'orders' => [Database::ORDER_ASC], + 'attributes' => ['databaseInternalId', 'collectionInternalId'], + 'lengths' => [Database::LENGTH_KEY, Database::LENGTH_KEY], + 'orders' => [Database::ORDER_ASC, Database::ORDER_ASC], ], ], ], @@ -264,7 +333,7 @@ $collections = [ 'name' => 'Indexes', 'attributes' => [ [ - '$id' => 'collectionId', + '$id' => 'databaseInternalId', 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, @@ -274,6 +343,39 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'databaseId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => false, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'collectionInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'collectionId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => true, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'key', 'type' => Database::VAR_STRING, @@ -343,11 +445,11 @@ $collections = [ ], 'indexes' => [ [ - '$id' => '_key_collection', + '$id' => '_key_db_collection', 'type' => Database::INDEX_KEY, - 'attributes' => ['collectionId'], - 'lengths' => [Database::LENGTH_KEY], - 'orders' => [Database::ORDER_ASC], + 'attributes' => ['databaseInternalId', 'collectionInternalId'], + 'lengths' => [Database::LENGTH_KEY, Database::LENGTH_KEY], + 'orders' => [Database::ORDER_ASC, Database::ORDER_ASC], ], ], ], @@ -357,6 +459,17 @@ $collections = [ '$id' => 'projects', 'name' => 'Projects', 'attributes' => [ + [ + '$id' => 'teamInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'teamId', 'type' => Database::VAR_STRING, @@ -594,6 +707,17 @@ $collections = [ '$id' => 'platforms', 'name' => 'platforms', 'attributes' => [ + [ + '$id' => 'projectInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'projectId', 'type' => Database::VAR_STRING, @@ -659,35 +783,13 @@ $collections = [ 'default' => null, 'array' => false, 'filters' => [], - ], - [ - '$id' => 'dateCreated', - 'type' => Database::VAR_INTEGER, - 'format' => '', - 'size' => 0, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'dateUpdated', - 'type' => Database::VAR_INTEGER, - 'format' => '', - 'size' => 0, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], + ] ], 'indexes' => [ [ '$id' => '_key_project', 'type' => Database::INDEX_KEY, - 'attributes' => ['projectId'], + 'attributes' => ['projectInternalId'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], ], @@ -699,6 +801,17 @@ $collections = [ '$id' => 'domains', 'name' => 'domains', 'attributes' => [ + [ + '$id' => 'projectInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'projectId', 'type' => Database::VAR_STRING, @@ -781,7 +894,7 @@ $collections = [ [ '$id' => '_key_project', 'type' => Database::INDEX_KEY, - 'attributes' => ['projectId'], + 'attributes' => ['projectInternalId'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], ], @@ -794,7 +907,7 @@ $collections = [ 'name' => 'keys', 'attributes' => [ [ - '$id' => 'projectId', + '$id' => 'projectInternalId', 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, @@ -804,6 +917,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'projectId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => 0, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'name', 'type' => Database::VAR_STRING, @@ -837,12 +961,23 @@ $collections = [ 'array' => false, 'filters' => ['encrypt'], ], + [ + '$id' => 'expire', + 'type' => Database::VAR_INTEGER, + 'format' => '', + 'size' => 0, + 'signed' => false, + 'required' => false, + 'default' => 0, + 'array' => false, + 'filters' => [], + ], ], 'indexes' => [ [ '$id' => '_key_project', 'type' => Database::INDEX_KEY, - 'attributes' => ['projectId'], + 'attributes' => ['projectInternalId'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], ], @@ -854,6 +989,17 @@ $collections = [ '$id' => 'webhooks', 'name' => 'webhooks', 'attributes' => [ + [ + '$id' => 'projectInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'projectId', 'type' => Database::VAR_STRING, @@ -947,10 +1093,10 @@ $collections = [ [ '$id' => '_key_project', 'type' => Database::INDEX_KEY, - 'attributes' => ['projectId'], + 'attributes' => ['projectInternalId'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], - ], + ] ], ], @@ -981,6 +1127,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'phone', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => 16, // leading '+' and 15 digitts maximum by E.164 format + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'status', 'type' => Database::VAR_BOOLEAN, @@ -1047,6 +1204,17 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'phoneVerification', + 'type' => Database::VAR_BOOLEAN, + 'format' => '', + 'size' => 0, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'reset', 'type' => Database::VAR_BOOLEAN, @@ -1111,6 +1279,13 @@ $collections = [ 'lengths' => [320], 'orders' => [Database::ORDER_ASC], ], + [ + '$id' => '_key_phone', + 'type' => Database::INDEX_UNIQUE, + 'attributes' => ['phone'], + 'lengths' => [16], + 'orders' => [Database::ORDER_ASC], + ], [ '$id' => '_key_search', 'type' => Database::INDEX_FULLTEXT, @@ -1126,6 +1301,17 @@ $collections = [ '$id' => 'tokens', 'name' => 'Tokens', 'attributes' => [ + [ + '$id' => 'userInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'userId', 'type' => Database::VAR_STRING, @@ -1197,7 +1383,7 @@ $collections = [ [ '$id' => '_key_user', 'type' => Database::INDEX_KEY, - 'attributes' => ['userId'], + 'attributes' => ['userInternalId'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], ], @@ -1209,6 +1395,17 @@ $collections = [ '$id' => 'sessions', 'name' => 'Sessions', 'attributes' => [ + [ + '$id' => 'userInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'userId', 'type' => Database::VAR_STRING, @@ -1474,7 +1671,7 @@ $collections = [ [ '$id' => '_key_user', 'type' => Database::INDEX_KEY, - 'attributes' => ['userId'], + 'attributes' => ['userInternalId'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], ], @@ -1497,17 +1694,6 @@ $collections = [ 'array' => false, 'filters' => [], ], - [ - '$id' => 'dateCreated', - 'type' => Database::VAR_INTEGER, - 'format' => '', - 'size' => 0, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], [ '$id' => 'total', 'type' => Database::VAR_INTEGER, @@ -1548,7 +1734,7 @@ $collections = [ 'name' => 'Memberships', 'attributes' => [ [ - '$id' => 'teamId', + '$id' => 'userInternalId', 'type' => Database::VAR_STRING, 'format' => '', 'size' => Database::LENGTH_KEY, @@ -1569,6 +1755,28 @@ $collections = [ 'array' => false, 'filters' => [], ], + [ + '$id' => 'teamInternalId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], + [ + '$id' => 'teamId', + 'type' => Database::VAR_STRING, + 'format' => '', + 'size' => Database::LENGTH_KEY, + 'signed' => true, + 'required' => false, + 'default' => null, + 'array' => false, + 'filters' => [], + ], [ '$id' => 'roles', 'type' => Database::VAR_STRING, @@ -1640,21 +1848,21 @@ $collections = [ [ '$id' => '_key_unique', 'type' => Database::INDEX_UNIQUE, - 'attributes' => ['teamId', 'userId'], + 'attributes' => ['teamInternalId', 'userInternalId'], 'lengths' => [Database::LENGTH_KEY, Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC, Database::ORDER_ASC], ], [ - '$id' => '_key_team', + '$id' => '_key_user', 'type' => Database::INDEX_KEY, - 'attributes' => ['teamId'], + 'attributes' => ['userInternalId'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], ], [ - '$id' => '_key_user', + '$id' => '_key_team', 'type' => Database::INDEX_KEY, - 'attributes' => ['userId'], + 'attributes' => ['teamInternalId'], 'lengths' => [Database::LENGTH_KEY], 'orders' => [Database::ORDER_ASC], ], @@ -1695,28 +1903,6 @@ $collections = [ 'array' => false, 'filters' => [], ], - [ - '$id' => 'dateCreated', - 'type' => Database::VAR_INTEGER, - 'format' => '', - 'size' => 0, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'dateUpdated', - 'type' => Database::VAR_INTEGER, - 'format' => '', - 'size' => 0, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], [ 'array' => false, '$id' => 'status', @@ -1726,7 +1912,6 @@ $collections = [ 'signed' => true, 'required' => false, 'default' => null, - 'array' => false, 'filters' => [], ], [ @@ -1845,17 +2030,6 @@ $collections = [ '$id' => 'deployments', 'name' => 'Deployments', 'attributes' => [ - [ - '$id' => 'dateCreated', - 'type' => Database::VAR_INTEGER, - 'format' => '', - 'size' => 0, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], [ '$id' => 'resourceId', 'type' => Database::VAR_STRING, @@ -1898,7 +2072,6 @@ $collections = [ 'signed' => true, 'required' => false, 'default' => null, - 'array' => false, 'filters' => [], ], [ @@ -2147,17 +2320,6 @@ $collections = [ '$id' => 'executions', 'name' => 'Executions', 'attributes' => [ - [ - '$id' => 'dateCreated', - 'type' => Database::VAR_INTEGER, - 'format' => '', - 'size' => 0, - 'signed' => true, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], [ '$id' => 'functionId', 'type' => Database::VAR_STRING, @@ -2189,7 +2351,6 @@ $collections = [ 'signed' => true, 'required' => false, 'default' => null, - 'array' => false, 'filters' => [], ], [ @@ -2368,26 +2529,6 @@ $collections = [ '$id' => 'buckets', 'name' => 'Buckets', 'attributes' => [ - [ - '$id' => 'dateCreated', - 'type' => Database::VAR_INTEGER, - 'format' => '', - 'signed' => false, - 'size' => 0, - 'required' => false, - 'array' => false, - 'filters' => [], - ], - [ - '$id' => 'dateUpdated', - 'type' => Database::VAR_INTEGER, - 'size' => 0, - 'format' => '', - 'signed' => false, - 'required' => false, - 'array' => false, - 'filters' => [], - ], [ '$id' => 'enabled', 'type' => Database::VAR_BOOLEAN, @@ -2628,17 +2769,6 @@ $collections = [ '$id' => 'files', '$name' => 'Files', 'attributes' => [ - [ - '$id' => 'dateCreated', - 'type' => Database::VAR_INTEGER, - 'format' => '', - 'size' => 0, - 'signed' => false, - 'required' => false, - 'default' => null, - 'array' => false, - 'filters' => [], - ], [ 'array' => false, '$id' => 'bucketId', @@ -2648,7 +2778,6 @@ $collections = [ 'signed' => true, 'required' => false, 'default' => null, - 'array' => false, 'filters' => [], ], [ @@ -3077,10 +3206,10 @@ $collections = [ ], [ '$id' => 'width', - 'type' => Database::VAR_STRING, + 'type' => Database::VAR_INTEGER, 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, + 'size' => 0, + 'signed' => false, 'required' => false, 'default' => null, 'array' => false, @@ -3088,10 +3217,10 @@ $collections = [ ], [ '$id' => 'height', - 'type' => Database::VAR_STRING, + 'type' => Database::VAR_INTEGER, 'format' => '', - 'size' => Database::LENGTH_KEY, - 'signed' => true, + 'size' => 0, + 'signed' => false, 'required' => false, 'default' => null, 'array' => false, @@ -3300,7 +3429,6 @@ $collections = [ ], ] ], - ]; return $collections; diff --git a/app/config/renditions.php b/app/config/renditions.php deleted file mode 100644 index de6d09c930..0000000000 --- a/app/config/renditions.php +++ /dev/null @@ -1,29 +0,0 @@ - 1, - 'name' => '360p', - 'videoBitrate' => 890, //video bitrate in Kbps - 'audioBitrate' => 64, //audio bitrate in Kbps - 'width' => 640, //width resolution in px - 'height' => 360, //height resolution in px - ], - [ - 'id' => 2, - 'name' => '576p', - 'videoBitrate' => 2538, - 'audioBitrate' => 128, - 'width' => 1024, - 'height' => 576, - ], - [ - 'id' => 3, - 'name' => '720p', - 'videoBitrate' => 3551, - 'audioBitrate' => 128, - 'width' => 1280, - 'height' => 720, - ], - -]; diff --git a/app/config/profiles.php b/app/config/video-profiles.php similarity index 90% rename from app/config/profiles.php rename to app/config/video-profiles.php index de6d09c930..a32788dbd7 100644 --- a/app/config/profiles.php +++ b/app/config/video-profiles.php @@ -2,7 +2,7 @@ return [ [ - 'id' => 1, + 'name' => '360p', 'videoBitrate' => 890, //video bitrate in Kbps 'audioBitrate' => 64, //audio bitrate in Kbps @@ -10,7 +10,6 @@ return [ 'height' => 360, //height resolution in px ], [ - 'id' => 2, 'name' => '576p', 'videoBitrate' => 2538, 'audioBitrate' => 128, @@ -18,7 +17,6 @@ return [ 'height' => 576, ], [ - 'id' => 3, 'name' => '720p', 'videoBitrate' => 3551, 'audioBitrate' => 128, diff --git a/app/controllers/api/projects.php b/app/controllers/api/projects.php index 98c08f6e94..793c2d19dd 100644 --- a/app/controllers/api/projects.php +++ b/app/controllers/api/projects.php @@ -153,7 +153,7 @@ App::post('/v1/projects') } if($dbForProject->exists($dbForProject->getDefaultDatabase(), 'video_profiles')) { - foreach (Config::getParam('profiles', []) as $profile) { + foreach (Config::getParam('video-profiles', []) as $profile) { Authorization::skip(function () use ($project, $profile, $dbForProject) { return $dbForProject->createDocument('video_profiles', new Document([ 'name' => $profile['name'], diff --git a/app/controllers/api/video.php b/app/controllers/api/video.php index ef3d816ea3..2ba4407148 100644 --- a/app/controllers/api/video.php +++ b/app/controllers/api/video.php @@ -55,7 +55,7 @@ App::get('/v1/video/profiles') $profiles = Authorization::skip(fn () => $dbForProject->find('video_profiles', [], 12, 0, [], ['ASC'])); - if(empty($profiles)) { + if (empty($profiles)) { throw new Exception('Video profiles where not found', 404, Exception::PROFILES_NOT_FOUND); } @@ -63,7 +63,7 @@ App::get('/v1/video/profiles') 'total' => $dbForProject->count('video_profiles', [], APP_LIMIT_COUNT), 'profiles' => $profiles, ]), Response::MODEL_VIDEO_PROFILE_LIST); - }); + }); App::post('/v1/video/buckets/:bucketId/files/:fileId') @@ -135,7 +135,7 @@ App::post('/v1/video/buckets/:bucketId/files/:fileId') $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); } try { - $video = Authorization::skip(function () use ($dbForProject, $bucket, $file) { + $video = Authorization::skip(function () use ($dbForProject, $bucket, $file) { return $dbForProject->createDocument('videos', new Document([ 'bucketId' => $bucket->getId(), 'fileId' => $file->getId(), @@ -156,13 +156,13 @@ App::post('/v1/video/buckets/:bucketId/files/:fileId') App::post('/v1/video/:videoId/rendition/:profileId') - ->alias('/v1/video/files', ['bucketId' => 'default']) + ->alias('/v1/video/:videoId/rendition/:profileId', []) ->desc('Start transcoding video rendition') ->groups(['api', 'storage']) ->label('scope', 'files.write') // ->label('event', 'buckets.[bucketId].files.[fileId].create') - ->param('videoId', 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('profileId', '', new CustomId(), 'File ID. Choose your own unique ID or pass the string "unique()" to auto generate it. Valid chars are a-z, A-Z, 0-9, period, hyphen, and underscore. Can\'t start with a special char. Max length is 36 chars.') + ->param('videoId', null, new UID(), 'Video unique ID.') + ->param('profileId', '', new CustomId(), 'Profile unique ID.') ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) ->inject('request') @@ -182,7 +182,7 @@ App::post('/v1/video/:videoId/rendition/:profileId') $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])])); - if($video->isEmpty()){ + if ($video->isEmpty()) { throw new Exception('Video not found', 400, Exception::VIDEO_NOT_FOUND); } @@ -232,7 +232,7 @@ App::post('/v1/video/:videoId/rendition/:profileId') $profile = Authorization::skip(fn() => $dbForProject->findOne('video_profiles', [new Query('_uid', Query::TYPE_EQUAL, [$profileId])])); - if(!$profile){ + if (!$profile) { throw new Exception('Video profile not found', 400, Exception::PROFILES_NOT_FOUND); } @@ -248,27 +248,31 @@ App::post('/v1/video/:videoId/rendition/:profileId') }); -App::get('/v1/video/buckets/:bucketId/files/:fileId/renditions') - ->alias('/v1/storage/files/:fileId/renditions', ['bucketId' => 'default']) +App::get('/v1/video/:videoId/:stream/renditions') + ->alias('/v1/video/:videoId/renditions', []) ->desc('Get File renditions') ->groups(['api', 'storage']) ->label('scope', 'files.read') - ->label('sdk.auth', [APP_AUTH_TYPE_SESSION, APP_AUTH_TYPE_KEY, APP_AUTH_TYPE_JWT]) - ->label('sdk.namespace', 'storage') - ->label('sdk.method', 'getFile') - ->label('sdk.description', '/docs/references/storage/get-file.md') - ->label('sdk.response.code', Response::STATUS_CODE_OK) - ->label('sdk.response.type', Response::CONTENT_TYPE_JSON) - ->label('sdk.response.model', Response::MODEL_FILE) - ->param('bucketId', null, new UID(), 'Storage bucket unique ID. You can create a new storage bucket using the Storage service [server integration](/docs/server/storage#createBucket).') - ->param('fileId', '', new UID(), 'File ID.') + ->param('videoId', null, new UID(), 'Video unique ID.') + ->param('stream', '', new WhiteList(['hls', 'mpeg-dash']), 'stream protocol name') + ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) + ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) ->inject('response') ->inject('dbForProject') ->inject('usage') ->inject('mode') - ->action(function (string $bucketId, string $fileId, Response $response, Database $dbForProject, Stats $usage, string $mode) { + ->action(function (string $videoId, string $stream, ?array $read, ?array $write, Response $response, Database $dbForProject, Stats $usage, string $mode) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + /** @var Utopia\Database\Document $project */ + /** @var Utopia\Database\Document $user */ + + $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])])); + + if ($video->isEmpty()) { + throw new Exception('Video not found', 400, Exception::VIDEO_NOT_FOUND); + } + + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $video['bucketId'])); if ( $bucket->isEmpty() @@ -278,52 +282,81 @@ App::get('/v1/video/buckets/:bucketId/files/:fileId/renditions') } // Check bucket permissions when enforced - if ($bucket->getAttribute('permission') === 'bucket') { - $validator = new Authorization('read'); - if (!$validator->isValid($bucket->getRead())) { + $permissionBucket = $bucket->getAttribute('permission') === 'bucket'; + if ($permissionBucket) { + $validator = new Authorization('write'); + if (!$validator->isValid($bucket->getWrite())) { throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); } } - if ($bucket->getAttribute('permission') === 'bucket') { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); - } else { - $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + $read = (is_null($read) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $read ?? []; // By default set read permissions for user + $write = (is_null($write) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $write ?? []; + + // Users can only add their roles to files, API keys and Admin users can add any + $roles = Authorization::getRoles(); + + if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { + foreach ($read as $role) { + if (!Authorization::isRole($role)) { + throw new Exception('Read permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); + } + } + foreach ($write as $role) { + if (!Authorization::isRole($role)) { + throw new Exception('Write permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); + } + } } - if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) { - throw new Exception('File not found', 404, Exception::STORAGE_FILE_NOT_FOUND); + if ($bucket->getAttribute('permission') === 'bucket') { + // skip authorization + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $video['fileId'])); + } else { + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $video['fileId']); } $queries = [ - new Query('bucketId', Query::TYPE_EQUAL, [$bucketId]), - new Query('fileId', Query::TYPE_EQUAL, [$fileId]), - new Query('stream', Query::TYPE_EQUAL, ['dash']), + new Query('videoId', Query::TYPE_EQUAL, [$video->getId()]), + new Query('endedAt', Query::TYPE_GREATER, [0]), + new Query('status', Query::TYPE_EQUAL, ['ready']), + new Query('stream', Query::TYPE_EQUAL, [$stream]), ]; - $renditions = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId() . '_video_renditions', $queries, 12, 0, [], ['ASC'])); + $renditions = Authorization::skip(fn () => $dbForProject->find('video_renditions', $queries, 12, 0, [], ['ASC'])); $response->dynamic(new Document([ - 'total' => $dbForProject->count('bucket_' . $bucket->getInternalId() . '_video_renditions', $queries, APP_LIMIT_COUNT), + 'total' => $dbForProject->count('video_renditions', $queries, APP_LIMIT_COUNT), 'renditions' => $renditions, - ]), Response::MODEL_FILE_RENDITIONS_LIST); + ]), Response::MODEL_VIDEO_RENDITIONS_LIST); }); -App::get('/v1/video/buckets/:bucketId/files/:stream/:fileId') +App::get('/v1/video/:videoId/:stream/:namespace/:fileName') ->alias('/v1/video/buckets/:bucketId/files/:stream/:fileId', []) ->desc('Get video playlist manifest') ->groups(['api', 'storage']) ->label('scope', 'files.read') - ->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('stream', '', new WhiteList(['hls', 'dash']), 'stream protocol name') - ->param('fileId', '', new UID(), 'File ID.') + ->param('videoId', null, new UID(), 'Video unique ID.') + ->param('stream', '', new WhiteList(['hls', 'mpeg-dash']), 'stream protocol name') + ->param('namespace', '', new WhiteList(['master']), 'stream protocol name') + ->param('read', null, new Permissions(), 'An array of strings with read permissions. By default only the current user is granted with read permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) + ->param('write', null, new Permissions(), 'An array of strings with write permissions. By default only the current user is granted with write permissions. [learn more about permissions](https://appwrite.io/docs/permissions) and get a full list of available permissions.', true) ->inject('response') ->inject('dbForProject') ->inject('usage') ->inject('mode') - ->action(function (string $bucketId, string $stream, string $fileId, Response $response, Database $dbForProject, Stats $usage, string $mode) { + ->action(function (string $videoId, string $stream, string $namespace, ?array $read, ?array $write, Response $response, Database $dbForProject, Stats $usage, string $mode) { - $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $bucketId)); + /** @var Utopia\Database\Document $project */ + /** @var Utopia\Database\Document $user */ + + $video = Authorization::skip(fn() => $dbForProject->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$videoId])])); + + if ($video->isEmpty()) { + throw new Exception('Video not found', 400, Exception::VIDEO_NOT_FOUND); + } + + $bucket = Authorization::skip(fn () => $dbForProject->getDocument('buckets', $video['bucketId'])); if ( $bucket->isEmpty() @@ -333,32 +366,48 @@ App::get('/v1/video/buckets/:bucketId/files/:stream/:fileId') } // Check bucket permissions when enforced - if ($bucket->getAttribute('permission') === 'bucket') { - $validator = new Authorization('read'); - if (!$validator->isValid($bucket->getRead())) { + $permissionBucket = $bucket->getAttribute('permission') === 'bucket'; + if ($permissionBucket) { + $validator = new Authorization('write'); + if (!$validator->isValid($bucket->getWrite())) { throw new Exception('Unauthorized permissions', 401, Exception::USER_UNAUTHORIZED); } } - if ($bucket->getAttribute('permission') === 'bucket') { - $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId)); - } else { - $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $fileId); + $read = (is_null($read) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $read ?? []; // By default set read permissions for user + $write = (is_null($write) && !$user->isEmpty()) ? ['user:' . $user->getId()] : $write ?? []; + + // Users can only add their roles to files, API keys and Admin users can add any + $roles = Authorization::getRoles(); + + if (!Auth::isAppUser($roles) && !Auth::isPrivilegedUser($roles)) { + foreach ($read as $role) { + if (!Authorization::isRole($role)) { + throw new Exception('Read permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); + } + } + foreach ($write as $role) { + if (!Authorization::isRole($role)) { + throw new Exception('Write permissions must be one of: (' . \implode(', ', $roles) . ')', 400, Exception::USER_UNAUTHORIZED); + } + } } - if ($file->isEmpty() || $file->getAttribute('bucketId') !== $bucketId) { - throw new Exception('File not found', 404, Exception::STORAGE_FILE_NOT_FOUND); + if ($bucket->getAttribute('permission') === 'bucket') { + // skip authorization + $file = Authorization::skip(fn () => $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $video['fileId'])); + } else { + $file = $dbForProject->getDocument('bucket_' . $bucket->getInternalId(), $video['fileId']); } $queries = [ - new Query('bucketId', Query::TYPE_EQUAL, [$bucketId]), - new Query('fileId', Query::TYPE_EQUAL, [$fileId]), - new Query('stream', Query::TYPE_EQUAL, [$stream]), + new Query('videoId', Query::TYPE_EQUAL, [$video->getId()]), new Query('endedAt', Query::TYPE_GREATER, [0]), - new Query('status', Query::TYPE_EQUAL, ['ready']) + new Query('status', Query::TYPE_EQUAL, ['ready']), + new Query('stream', Query::TYPE_EQUAL, ['stream']), ]; - $renditions = Authorization::skip(fn () => $dbForProject->find('bucket_' . $bucket->getInternalId() . '_video_renditions', $queries, 12, 0, [], ['ASC'])); + $renditions = Authorization::skip(fn () => $dbForProject->find('video_renditions', $queries, 12, 0, [], ['ASC'])); if (empty($renditions)) { throw new Exception('Renditions not found'); @@ -379,7 +428,9 @@ App::get('/v1/video/buckets/:bucketId/files/:stream/:fileId') $template->setParam('params', $params); $response->setContentType('application/x-mpegurl'); $response->send($template->render()); + } else { + $adaptations = []; foreach ($renditions as $rendition) { $metadata = $rendition->getAttribute('metadata'); diff --git a/app/controllers/general.php b/app/controllers/general.php index 747799e369..620f16f1c1 100644 --- a/app/controllers/general.php +++ b/app/controllers/general.php @@ -19,6 +19,7 @@ use Appwrite\Network\Validator\Origin; use Appwrite\Utopia\Response\Filters\V11 as ResponseV11; use Appwrite\Utopia\Response\Filters\V12 as ResponseV12; use Appwrite\Utopia\Response\Filters\V13 as ResponseV13; +use Appwrite\Utopia\Response\Filters\V14 as ResponseV14; use Utopia\CLI\Console; use Utopia\Database\Database; use Utopia\Database\Document; @@ -177,6 +178,9 @@ App::init(function (App $utopia, Request $request, Response $response, Document case version_compare($responseFormat, '0.13.4', '<='): Response::setFilter(new ResponseV13()); break; + case version_compare($responseFormat, '0.14.0', '<='): + Response::setFilter(new ResponseV14()); + break; default: Response::setFilter(null); } @@ -279,6 +283,12 @@ App::init(function (App $utopia, Request $request, Response $response, Document $role = Auth::USER_ROLE_APP; $scopes = \array_merge($roles[$role]['scopes'], $key->getAttribute('scopes', [])); + $expire = $key->getAttribute('expire', 0); + + if (!empty($expire) && $expire < \time()) { + throw new AppwriteException('Project key expired', 401, AppwriteException:: PROJECT_KEY_EXPIRED); + } + Authorization::setRole('role:' . Auth::USER_ROLE_APP); Authorization::setDefaultStatus(false); // Cancel security segmentation for API keys. } @@ -292,11 +302,10 @@ App::init(function (App $utopia, Request $request, Response $response, Document $service = $route->getLabel('sdk.namespace', ''); if (!empty($service)) { - $roles = Authorization::getRoles(); if ( array_key_exists($service, $project->getAttribute('services', [])) && !$project->getAttribute('services', [])[$service] - && !(Auth::isPrivilegedUser($roles) || Auth::isAppUser($roles)) + && !(Auth::isPrivilegedUser(Authorization::getRoles()) || Auth::isAppUser(Authorization::getRoles())) ) { throw new AppwriteException('Service is disabled', 503, AppwriteException::GENERAL_SERVICE_DISABLED); } diff --git a/app/init.php b/app/init.php index f46224a7ac..26f8b2a04c 100644 --- a/app/init.php +++ b/app/init.php @@ -23,11 +23,17 @@ use Ahc\Jwt\JWT; use Ahc\Jwt\JWTException; use Appwrite\Extend\Exception; use Appwrite\Auth\Auth; +use Appwrite\Auth\Phone\Mock; +use Appwrite\Auth\Phone\Telesign; +use Appwrite\Auth\Phone\TextMagic; +use Appwrite\Auth\Phone\Twilio; +use Appwrite\DSN\DSN; use Appwrite\Event\Audit; use Appwrite\Event\Database as EventDatabase; use Appwrite\Event\Delete; use Appwrite\Event\Event; use Appwrite\Event\Mail; +use Appwrite\Event\Phone; use Appwrite\Network\Validator\Email; use Appwrite\Network\Validator\IP; use Appwrite\Network\Validator\URL; @@ -78,9 +84,10 @@ const APP_LIMIT_ANTIVIRUS = 20000000; //20MB const APP_LIMIT_ENCRYPTION = 20000000; //20MB const APP_LIMIT_COMPRESSION = 20000000; //20MB const APP_LIMIT_ARRAY_PARAMS_SIZE = 100; // Default maximum of how many elements can there be in API parameter that expects array value +const APP_LIMIT_ARRAY_ELEMENT_SIZE = 4096; // Default maximum length of element in array parameter represented by maximum URL length. const APP_LIMIT_SUBQUERY = 1000; -const APP_CACHE_BUSTER = 305; -const APP_VERSION_STABLE = '0.14.2'; +const APP_CACHE_BUSTER = 401; +const APP_VERSION_STABLE = '0.15.1'; const APP_DATABASE_ATTRIBUTE_EMAIL = 'email'; const APP_DATABASE_ATTRIBUTE_ENUM = 'enum'; const APP_DATABASE_ATTRIBUTE_IP = 'ip'; @@ -89,9 +96,9 @@ const APP_DATABASE_ATTRIBUTE_INT_RANGE = 'intRange'; const APP_DATABASE_ATTRIBUTE_FLOAT_RANGE = 'floatRange'; const APP_DATABASE_ATTRIBUTE_STRING_MAX_LENGTH = 1073741824; // 2^32 bits / 4 bits per char const APP_STORAGE_UPLOADS = '/storage/uploads'; -const APP_STORAGE_VIDEO = '/storage/video'; const APP_STORAGE_FUNCTIONS = '/storage/functions'; const APP_STORAGE_BUILDS = '/storage/builds'; +const APP_STORAGE_VIDEO = '/storage/videos'; const APP_STORAGE_CACHE = '/storage/cache'; const APP_STORAGE_CERTIFICATES = '/storage/certificates'; const APP_STORAGE_CONFIG = '/storage/config'; @@ -119,6 +126,7 @@ const DATABASE_TYPE_DELETE_INDEX = 'deleteIndex'; const BUILD_TYPE_DEPLOYMENT = 'deployment'; const BUILD_TYPE_RETRY = 'retry'; // Deletion Types +const DELETE_TYPE_DATABASES = 'databases'; const DELETE_TYPE_DOCUMENT = 'document'; const DELETE_TYPE_COLLECTIONS = 'collections'; const DELETE_TYPE_PROJECTS = 'projects'; @@ -133,6 +141,7 @@ const DELETE_TYPE_CERTIFICATES = 'certificates'; const DELETE_TYPE_USAGE = 'usage'; const DELETE_TYPE_REALTIME = 'realtime'; const DELETE_TYPE_BUCKETS = 'buckets'; +const DELETE_TYPE_SESSIONS = 'sessions'; // Mail Types const MAIL_TYPE_VERIFICATION = 'verification'; const MAIL_TYPE_MAGIC_SESSION = 'magicSession'; @@ -154,32 +163,6 @@ App::setMode(App::getEnv('_APP_ENV', App::MODE_TYPE_PRODUCTION)); /* * ENV vars */ -Config::load('profiles', __DIR__ . '/config/profiles.php'); -Config::load('events', __DIR__ . '/config/events.php'); -Config::load('auth', __DIR__ . '/config/auth.php'); -Config::load('errors', __DIR__ . '/config/errors.php'); -Config::load('providers', __DIR__ . '/config/providers.php'); -Config::load('platforms', __DIR__ . '/config/platforms.php'); -Config::load('collections', __DIR__ . '/config/collections.php'); -Config::load('runtimes', __DIR__ . '/config/runtimes.php'); -Config::load('roles', __DIR__ . '/config/roles.php'); // User roles and scopes -Config::load('scopes', __DIR__ . '/config/scopes.php'); // User roles and scopes -Config::load('services', __DIR__ . '/config/services.php'); // List of services -Config::load('variables', __DIR__ . '/config/variables.php'); // List of env variables -Config::load('avatar-browsers', __DIR__ . '/config/avatars/browsers.php'); -Config::load('avatar-credit-cards', __DIR__ . '/config/avatars/credit-cards.php'); -Config::load('avatar-flags', __DIR__ . '/config/avatars/flags.php'); -Config::load('locale-codes', __DIR__ . '/config/locale/codes.php'); -Config::load('locale-currencies', __DIR__ . '/config/locale/currencies.php'); -Config::load('locale-eu', __DIR__ . '/config/locale/eu.php'); -Config::load('locale-languages', __DIR__ . '/config/locale/languages.php'); -Config::load('locale-phones', __DIR__ . '/config/locale/phones.php'); -Config::load('locale-countries', __DIR__ . '/config/locale/countries.php'); -Config::load('locale-continents', __DIR__ . '/config/locale/continents.php'); -Config::load('storage-logos', __DIR__ . '/config/storage/logos.php'); -Config::load('storage-mimes', __DIR__ . '/config/storage/mimes.php'); -Config::load('storage-inputs', __DIR__ . '/config/storage/inputs.php'); -Config::load('storage-outputs', __DIR__ . '/config/storage/outputs.php'); Config::load('events', __DIR__ . '/config/events.php'); Config::load('auth', __DIR__ . '/config/auth.php'); Config::load('errors', __DIR__ . '/config/errors.php'); @@ -191,6 +174,7 @@ Config::load('roles', __DIR__ . '/config/roles.php'); // User roles and scopes Config::load('scopes', __DIR__ . '/config/scopes.php'); // User roles and scopes Config::load('services', __DIR__ . '/config/services.php'); // List of services Config::load('variables', __DIR__ . '/config/variables.php'); // List of env variables +Config::load('video-profiles', __DIR__ . '/config/video-profiles.php'); Config::load('avatar-browsers', __DIR__ . '/config/avatars/browsers.php'); Config::load('avatar-credit-cards', __DIR__ . '/config/avatars/credit-cards.php'); Config::load('avatar-flags', __DIR__ . '/config/avatars/flags.php'); @@ -206,6 +190,7 @@ Config::load('storage-mimes', __DIR__ . '/config/storage/mimes.php'); Config::load('storage-inputs', __DIR__ . '/config/storage/inputs.php'); Config::load('storage-outputs', __DIR__ . '/config/storage/outputs.php'); + $user = App::getEnv('_APP_REDIS_USER', ''); $pass = App::getEnv('_APP_REDIS_PASS', ''); if (!empty($user) || !empty($pass)) { @@ -283,8 +268,9 @@ Database::addFilter( function (mixed $value, Document $document, Database $database) { return $database ->find('attributes', [ - new Query('collectionId', Query::TYPE_EQUAL, [$document->getId()]) - ], $database->getAttributeLimit()); + new Query('collectionInternalId', Query::TYPE_EQUAL, [$document->getInternalId()]), + new Query('databaseInternalId', Query::TYPE_EQUAL, [$document->getAttribute('databaseInternalId')]) + ], $database->getAttributeLimit(), 0, []); } ); @@ -296,7 +282,8 @@ Database::addFilter( function (mixed $value, Document $document, Database $database) { return $database ->find('indexes', [ - new Query('collectionId', Query::TYPE_EQUAL, [$document->getId()]) + new Query('collectionInternalId', Query::TYPE_EQUAL, [$document->getInternalId()]), + new Query('databaseInternalId', Query::TYPE_EQUAL, [$document->getAttribute('databaseInternalId')]) ], 64); } ); @@ -309,7 +296,7 @@ Database::addFilter( function (mixed $value, Document $document, Database $database) { return $database ->find('platforms', [ - new Query('projectId', Query::TYPE_EQUAL, [$document->getId()]) + new Query('projectInternalId', Query::TYPE_EQUAL, [$document->getInternalId()]) ], APP_LIMIT_SUBQUERY); } ); @@ -322,7 +309,7 @@ Database::addFilter( function (mixed $value, Document $document, Database $database) { return $database ->find('domains', [ - new Query('projectId', Query::TYPE_EQUAL, [$document->getId()]) + new Query('projectInternalId', Query::TYPE_EQUAL, [$document->getInternalId()]) ], APP_LIMIT_SUBQUERY); } ); @@ -335,7 +322,7 @@ Database::addFilter( function (mixed $value, Document $document, Database $database) { return $database ->find('keys', [ - new Query('projectId', Query::TYPE_EQUAL, [$document->getId()]) + new Query('projectInternalId', Query::TYPE_EQUAL, [$document->getInternalId()]) ], APP_LIMIT_SUBQUERY); } ); @@ -348,7 +335,7 @@ Database::addFilter( function (mixed $value, Document $document, Database $database) { return $database ->find('webhooks', [ - new Query('projectId', Query::TYPE_EQUAL, [$document->getId()]) + new Query('projectInternalId', Query::TYPE_EQUAL, [$document->getInternalId()]) ], APP_LIMIT_SUBQUERY); } ); @@ -360,7 +347,7 @@ Database::addFilter( }, function (mixed $value, Document $document, Database $database) { return Authorization::skip(fn () => $database->find('sessions', [ - new Query('userId', Query::TYPE_EQUAL, [$document->getId()]) + new Query('userInternalId', Query::TYPE_EQUAL, [$document->getInternalId()]) ], APP_LIMIT_SUBQUERY)); } ); @@ -373,7 +360,7 @@ Database::addFilter( function (mixed $value, Document $document, Database $database) { return Authorization::skip(fn() => $database ->find('tokens', [ - new Query('userId', Query::TYPE_EQUAL, [$document->getId()]) + new Query('userInternalId', Query::TYPE_EQUAL, [$document->getInternalId()]) ], APP_LIMIT_SUBQUERY)); } ); @@ -386,7 +373,7 @@ Database::addFilter( function (mixed $value, Document $document, Database $database) { return Authorization::skip(fn() => $database ->find('memberships', [ - new Query('userId', Query::TYPE_EQUAL, [$document->getId()]) + new Query('userInternalId', Query::TYPE_EQUAL, [$document->getInternalId()]) ], APP_LIMIT_SUBQUERY)); } ); @@ -453,7 +440,7 @@ Structure::addFormat(APP_DATABASE_ATTRIBUTE_FLOAT_RANGE, function ($attribute) { * Registry */ $register->set('logger', function () { - // Register error logger + // Register error logger $providerName = App::getEnv('_APP_LOGGING_PROVIDER', ''); $providerConfig = App::getEnv('_APP_LOGGING_CONFIG', ''); @@ -487,6 +474,11 @@ $register->set('dbPool', function () { ->withPassword($dbPass) ->withOptions([ PDO::ATTR_ERRMODE => App::isDevelopment() ? PDO::ERRMODE_WARNING : PDO::ERRMODE_SILENT, // If in production mode, warnings are not displayed + PDO::ATTR_TIMEOUT => 3, // Seconds + PDO::ATTR_PERSISTENT => true, + PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, + PDO::ATTR_EMULATE_PREPARES => true, + PDO::ATTR_STRINGIFY_FETCHES => true, ]), 64 ); @@ -568,7 +560,7 @@ $register->set('smtp', function () { return $mail; }); $register->set('geodb', function () { - return new Reader(__DIR__ . '/db/DBIP/dbip-country-lite-2022-03.mmdb'); + return new Reader(__DIR__ . '/db/DBIP/dbip-country-lite-2022-06.mmdb'); }); $register->set('db', function () { // This is usually for our workers or CLI commands scope @@ -579,11 +571,12 @@ $register->set('db', function () { $dbScheme = App::getEnv('_APP_DB_SCHEMA', ''); $pdo = new PDO("mysql:host={$dbHost};port={$dbPort};dbname={$dbScheme};charset=utf8mb4", $dbUser, $dbPass, array( - PDO::MYSQL_ATTR_INIT_COMMAND => 'SET NAMES utf8mb4', PDO::ATTR_TIMEOUT => 3, // Seconds PDO::ATTR_PERSISTENT => true, PDO::ATTR_DEFAULT_FETCH_MODE => PDO::FETCH_ASSOC, PDO::ATTR_ERRMODE => PDO::ERRMODE_EXCEPTION, + PDO::ATTR_EMULATE_PREPARES => true, + PDO::ATTR_STRINGIFY_FETCHES => true, )); return $pdo; @@ -712,6 +705,7 @@ App::setResource('audits', fn() => new Audit()); App::setResource('mails', fn() => new Mail()); App::setResource('deletes', fn() => new Delete()); App::setResource('database', fn() => new EventDatabase()); +App::setResource('messaging', fn() => new Phone()); App::setResource('usage', function ($register) { return new Stats($register->get('statsd')); }, ['register']); @@ -862,6 +856,7 @@ App::setResource('project', function ($dbForConsole, $request, $console) { App::setResource('console', function () { return new Document([ '$id' => 'console', + '$internalId' => 'console', 'name' => 'Appwrite', '$collection' => 'projects', 'description' => 'Appwrite core engine', @@ -891,12 +886,12 @@ App::setResource('console', function () { ]); }, []); -App::setResource('dbForProject', function ($db, $cache, $project) { +App::setResource('dbForProject', function ($db, $cache, Document $project) { $cache = new Cache(new RedisCache($cache)); $database = new Database(new MariaDB($db), $cache); $database->setDefaultDatabase(App::getEnv('_APP_DB_SCHEMA', 'appwrite')); - $database->setNamespace("_{$project->getId()}"); + $database->setNamespace("_{$project->getInternalId()}"); return $database; }, ['db', 'cache', 'project']); @@ -987,3 +982,17 @@ App::setResource('geodb', function ($register) { /** @var Utopia\Registry\Registry $register */ return $register->get('geodb'); }, ['register']); + +App::setResource('phone', function () { + $dsn = new DSN(App::getEnv('_APP_PHONE_PROVIDER')); + $user = $dsn->getUser(); + $secret = $dsn->getPassword(); + + return match ($dsn->getHost()) { + 'mock' => new Mock('', ''), // used for tests + 'twilio' => new Twilio($user, $secret), + 'text-magic' => new TextMagic($user, $secret), + 'telesign' => new Telesign($user, $secret), + default => null + }; +}); diff --git a/app/workers/transcoding.php b/app/workers/transcoding.php index 9927fe4b3c..a53a9d9db0 100644 --- a/app/workers/transcoding.php +++ b/app/workers/transcoding.php @@ -5,6 +5,7 @@ use Appwrite\OpenSSL\OpenSSL; use Appwrite\Resque\Worker; use Streaming\Format\StreamFormat; use Streaming\Media; +use Streaming\Metadata; use Streaming\Representation; use Utopia\App; use Utopia\CLI\Console; @@ -34,7 +35,8 @@ class TranscodingV1 extends Worker const HLS_BASE_URL = ''; - protected string $basePath = '/tmp/'; + //protected string $basePath = '/tmp/'; + protected string $basePath = '/usr/src/code/tests/tmp/'; protected string $inDir; @@ -68,20 +70,20 @@ class TranscodingV1 extends Worker $this->database = $this->getProjectDB($project->getId()); $sourceVideo = Authorization::skip(fn() => $this->database->findOne('videos', [new Query('_uid', Query::TYPE_EQUAL, [$this->args['videoId']])])); - if(empty($sourceVideo)){ + if (empty($sourceVideo)) { throw new Exception('Video not found'); } $profile = Authorization::skip(fn() => $this->database->findOne('video_profiles', [new Query('_uid', Query::TYPE_EQUAL, [$this->args['profileId']])])); - if(empty($profile)){ + if (empty($profile)) { throw new Exception('profile not found'); - } + } $user = new Document($this->args['user'] ?? []); - $bucket = Authorization::skip(fn() => $this->database->getDocument('buckets', $sourceVideo['bucketId'])); + $bucket = Authorization::skip(fn() => $this->database->getDocument('buckets', $sourceVideo['bucketId'])); if ($bucket->getAttribute('permission') === 'bucket') { - $file = Authorization::skip(fn() => $this->database->getDocument('bucket_' . $bucket->getInternalId(), $video['fileId'])); + $file = Authorization::skip(fn() => $this->database->getDocument('bucket_' . $bucket->getInternalId(), $video['fileId'])); } else { $file = $this->database->getDocument('bucket_' . $bucket->getInternalId(), $sourceVideo['fileId']); } @@ -127,25 +129,24 @@ class TranscodingV1 extends Worker if (!empty($rendition)) { Authorization::skip(fn() => $this->database->deleteDocument($collection, $rendition->getId())); $deviceFiles = $this->getVideoDevice($project->getId()); - if(!empty($rendition['path'])) { + if (!empty($rendition['path'])) { $deviceFiles->deletePath($rendition['path']); } } $stream = !empty($rendition['stream']) ? $rendition['stream'] : 'hls'; - //$general = $this->getMetadataExport($ffprobe->streams($inPath)); - //var_dump($general); -// if(!empty($general)) { -// foreach ($general as $key => $value) { -// $sourceVideo->setAttribute($key, $value); -// } -// -// Authorization::skip(fn() => $this->database->updateDocument( -// 'videos', -// $sourceVideo->getId(), -// $sourceVideo -// )); -// } + $general = $this->getVideoInfo($ffprobe->streams($inPath)); + if(!empty($general)) { + foreach ($general as $key => $value) { + $sourceVideo->setAttribute($key, $value); + } + + Authorization::skip(fn() => $this->database->updateDocument( + 'videos', + $sourceVideo->getId(), + $sourceVideo + )); + } $video = $ffmpeg->open($inPath); @@ -159,142 +160,108 @@ class TranscodingV1 extends Worker 'status' => self::STATUS_TRANSCODE_START, 'stream' => $stream, ])); - }); + }); - try { - $representation = (new Representation())-> - setKiloBitrate($profile->getAttribute('videoBitrate'))-> - setAudioKiloBitrate($profile->getAttribute('audioBitrate'))-> - setResize( $profile->getAttribute('width'), $profile->getAttribute('height')); + try { + $representation = (new Representation())-> + setKiloBitrate($profile->getAttribute('videoBitrate'))-> + setAudioKiloBitrate($profile->getAttribute('audioBitrate'))-> + setResize($profile->getAttribute('width'), $profile->getAttribute('height')); - $format = new Streaming\Format\X264(); - $format->on('progress', function ($video, $format, $percentage) use ($query, $collection) { - if ($percentage % 3 === 0) { - $query->setAttribute('progress', (string)$percentage); - Authorization::skip(fn() => $this->database->updateDocument( - $collection, - $query->getId(), - $query - )); - } - }); - - - list($general, $metadata) = $this->transcode($stream, $video, $format, $representation); - - if (!empty($metadata)) { - $query->setAttribute('metadata', json_encode($metadata)); - } - - if(!empty($general)) { - foreach ($general as $key => $value) { - $query->setAttribute($key, $value); - } - } - - $query->setAttribute('status', self::STATUS_TRANSCODE_END); - $query->setAttribute('endedAt', time()); - Authorization::skip(fn() => $this->database->updateDocument( - $collection, - $query->getId(), - $query - )); - - /** Upload & remove files **/ - $start = 0; - $fileNames = scandir($this->outDir); - - foreach ($fileNames as $fileName) { - if ( - $fileName === '.' || - $fileName === '..' || - str_contains($fileName, '.json') - ) { - continue; - } - - $deviceFiles = $this->getVideoDevice($project->getId()); - $devicePath = $deviceFiles->getPath($this->args['videoId']); - $devicePath = str_ireplace($deviceFiles->getRoot(), $deviceFiles->getRoot() . DIRECTORY_SEPARATOR . $bucket->getId(), $devicePath); - $data = $this->getFilesDevice($project->getId())->read($this->outDir . $fileName); - $renditionDir = $profile->getAttribute('width') . 'X' . $profile->getAttribute('height') . '@' .$profile->getAttribute('videoBitrate'); - $renditionPath = $devicePath . DIRECTORY_SEPARATOR . $renditionDir; - $this->getVideoDevice($project->getId())->write($renditionPath . DIRECTORY_SEPARATOR . $fileName, $data, \mime_content_type($this->outDir . $fileName)); - - if ($start === 0) { - $query->setAttribute('status', self::STATUS_UPLOADING); - $query->setAttribute('path', $renditionPath); - Authorization::skip(fn() => $this->database->updateDocument( - $collection, - $query->getId(), - $query - )); - $start = 1; - } - - //$metadata=[]; - //$chunksUploaded = $deviceFiles->upload($file, $path, -1, 1, $metadata); - //var_dump($chunksUploaded); - // if (empty($chunksUploaded)) { - // throw new Exception('Failed uploading file', 500, Exception::GENERAL_SERVER_ERROR); - //} - // } - - @unlink($this->outDir . $fileName); - } - - $query->setAttribute('status', self::STATUS_PACKAGE_END); - Authorization::skip(fn() => $this->database->updateDocument( - $collection, - $query->getId(), - $query - )); - } catch (\Throwable $th) { - $query->setAttribute('metadata', json_encode([ - 'code' => $th->getCode(), - 'message' => $th->getMessage(), - ])); - - $query->setAttribute('status', self::STATUS_ERROR); + $format = new Streaming\Format\X264(); + $format->on('progress', function ($video, $format, $percentage) use ($query, $collection) { + if ($percentage % 3 === 0) { + $query->setAttribute('progress', (string)$percentage); Authorization::skip(fn() => $this->database->updateDocument( $collection, $query->getId(), $query )); } - } + }); - /** - * @param $metadata array - * @return array - */ - private function getMetadataExport(array $metadata): array - { - $info = []; - if (!empty($metadata['stream']['resolutions'][0])) { - $general = $metadata['stream']['resolutions'][0]; - $parts = explode("X", $general['dimension']); - $info['width'] = $parts['0']; - $info['height'] = $parts['1']; - } + list($general, $metadata) = $this->transcode($stream, $video, $format, $representation); - if (!empty($metadata['video']['streams'])) { - foreach ($metadata['video']['streams'] as $streams) { - if ($streams['codec_type'] === 'video') { - $info['duration'] = $streams['duration']; - $info['videoCodec'] = $streams['codec_name'] . ',' . $streams['codec_tag_string']; - $info['videoBitrate'] = $streams['bit_rate']; - $info['videoFramerate'] = $streams['avg_frame_rate']; - } elseif ($streams['codec_type'] === 'audio') { - $info['audioCodec'] = $streams['codec_name'] . ',' . $streams['codec_tag_string']; - $info['audioBitrate'] = $streams['sample_rate']; - $info['audioSamplerate'] = $streams['bit_rate']; + if (!empty($metadata)) { + $query->setAttribute('metadata', json_encode($metadata)); + } + + if (!empty($general)) { + foreach ($general as $key => $value) { + $query->setAttribute($key, $value); } } - } - return $info; + $query->setAttribute('status', self::STATUS_TRANSCODE_END); + $query->setAttribute('endedAt', time()); + Authorization::skip(fn() => $this->database->updateDocument( + $collection, + $query->getId(), + $query + )); + + /** Upload & remove files **/ + $start = 0; + $fileNames = scandir($this->outDir); + + foreach ($fileNames as $fileName) { + if ( + $fileName === '.' || + $fileName === '..' || + str_contains($fileName, '.json') + ) { + continue; + } + + $deviceFiles = $this->getVideoDevice($project->getId()); + $devicePath = $deviceFiles->getPath($this->args['videoId']); + $data = $this->getFilesDevice($project->getId())->read($this->outDir . $fileName); + $renditionDir = $profile->getAttribute('width') . 'X' . $profile->getAttribute('height') . '@' . $profile->getAttribute('videoBitrate'); + $renditionPath = $devicePath . DIRECTORY_SEPARATOR . $renditionDir; + $this->getVideoDevice($project->getId())->write($renditionPath . DIRECTORY_SEPARATOR . $fileName, $data, \mime_content_type($this->outDir . $fileName)); + + if ($start === 0) { + $query->setAttribute('status', self::STATUS_UPLOADING); + $query->setAttribute('path', $renditionPath); + Authorization::skip(fn() => $this->database->updateDocument( + $collection, + $query->getId(), + $query + )); + $start = 1; + } + + //$metadata=[]; + //$chunksUploaded = $deviceFiles->upload($file, $path, -1, 1, $metadata); + //var_dump($chunksUploaded); + // if (empty($chunksUploaded)) { + // throw new Exception('Failed uploading file', 500, Exception::GENERAL_SERVER_ERROR); + //} + // } + + //@unlink($this->outDir . $fileName); + } + + $query->setAttribute('status', self::STATUS_PACKAGE_END); + Authorization::skip(fn() => $this->database->updateDocument( + $collection, + $query->getId(), + $query + )); + } catch (\Throwable $th) { + $query->setAttribute('metadata', json_encode([ + 'code' => $th->getCode(), + 'message' => $th->getMessage(), + ])); + + $query->setAttribute('status', self::STATUS_ERROR); + Authorization::skip(fn() => $this->database->updateDocument( + $collection, + $query->getId(), + $query + )); + } } /** @@ -321,13 +288,16 @@ class TranscodingV1 extends Worker ->addRepresentation($representation) ->setAdditionalParams($additionalParams) ->save($this->outPath); - var_dump($this->outPath); + $xml = simplexml_load_string( file_get_contents($this->outDir . $this->args['fileId'] . '.mpd') ); + $metadata = $this->getVideoInfo($dash->metadata()); + $metadata['width'] = $representation->getWidth(); + $metadata['height'] = $representation->getHeight(); return [ - $this->getMetadataExport($dash->metadata()->export()), + $metadata, ['mpeg-dash' => !empty($xml) ? json_decode(json_encode((array)$xml), true) : []], ]; } @@ -340,12 +310,33 @@ class TranscodingV1 extends Worker ->setAdditionalParams($additionalParams) ->setHlsBaseUrl(self::HLS_BASE_URL) ->save($this->outPath); - var_dump($this->outPath); + + $metadata = $this->getVideoInfo($hls->metadata()->getVideoStreams()); + $metadata['width'] = $representation->getWidth(); + $metadata['height'] = $representation->getHeight(); + + $this->rewriteHlsRefs($this->outPath . '_' . $representation->getHeight() . 'P.m3u8'); + return [ - $this->getMetadataExport($hls->metadata()->export()), [] + $metadata, [] ]; } + private function rewriteHlsRefs($path) + { + $handle = fopen($path, "r"); + $destination = fopen($path . '_tmp', "w"); + if ($handle) { + while (($line = fgets($handle)) !== false) { + if (str_contains($line, ".ts")) { + //$return['data'][$i]['url'] = str_replace(array("\r","\n"),"",$line); + } + fwrite($destination, str_replace(array("\r","\n"), "", $line) . PHP_EOL); + } + fclose($handle); + fclose($destination); + } + } /** * @param $streams StreamCollection @@ -353,13 +344,27 @@ class TranscodingV1 extends Worker */ private function getVideoInfo(StreamCollection $streams): array { + var_dump([ + 'duration' => $streams->videos()->first()->get('duration'), + 'height' => $streams->videos()->first()->get('height'), + 'width' => $streams->videos()->first()->get('width'), + 'videoCodec' => $streams->videos()->first()->get('codec_name') . ',' . $streams->videos()->first()->get('codec_tag_string'), + 'videoFramerate' => $streams->videos()->first()->get('avg_frame_rate'), + 'videoBitrate' => (int)$streams->videos()->first()->get('bit_rate'), + 'audioCodec' => $streams->audios()->first()->get('codec_name') . ',' . $streams->audios()->first()->get('codec_tag_string'), + 'audioSamplerate' => (int)$streams->audios()->first()->get('sample_rate'), + 'audioBitrate' => (int)$streams->audios()->first()->get('bit_rate'), + ]); return [ 'duration' => $streams->videos()->first()->get('duration'), 'height' => $streams->videos()->first()->get('height'), 'width' => $streams->videos()->first()->get('width'), - 'frameRate' => $streams->videos()->first()->get('r_frame_rate'), - 'bitrateKb' => $streams->videos()->first()->get('bit_rate') / 1000, - 'bitrateMb' => $streams->videos()->first()->get('bit_rate') / 1000 / 1000, + 'videoCodec' => $streams->videos()->first()->get('codec_name') . ',' . $streams->videos()->first()->get('codec_tag_string'), + 'videoFramerate' => $streams->videos()->first()->get('avg_frame_rate'), + 'videoBitrate' => (int)$streams->videos()->first()->get('bit_rate'), + 'audioCodec' => $streams->audios()->first()->get('codec_name') . ',' . $streams->audios()->first()->get('codec_tag_string'), + 'audioSamplerate' => (int)$streams->audios()->first()->get('sample_rate'), + 'audioBitrate' => (int)$streams->audios()->first()->get('bit_rate'), ]; } diff --git a/docker-compose.yml b/docker-compose.yml index f4001f53d8..6f532235e1 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -7,14 +7,38 @@ x-logging: &x-logging logging: driver: 'json-file' options: - max-file: "5" - max-size: 10m + max-file: '5' + max-size: '10m' + +x-env-storage: &x-env-storage |- + _APP_STORAGE_DEVICE + _APP_STORAGE_S3_ACCESS_KEY + _APP_STORAGE_S3_SECRET + _APP_STORAGE_S3_REGION + _APP_STORAGE_S3_BUCKET + _APP_STORAGE_DO_SPACES_ACCESS_KEY + _APP_STORAGE_DO_SPACES_SECRET + _APP_STORAGE_DO_SPACES_REGION + _APP_STORAGE_DO_SPACES_BUCKET + _APP_STORAGE_BACKBLAZE_ACCESS_KEY + _APP_STORAGE_BACKBLAZE_SECRET + _APP_STORAGE_BACKBLAZE_REGION + _APP_STORAGE_BACKBLAZE_BUCKET + _APP_STORAGE_DO_SPACES_BUCKET + _APP_STORAGE_LINODE_ACCESS_KEY + _APP_STORAGE_LINODE_SECRET + _APP_STORAGE_LINODE_REGION + _APP_STORAGE_LINODE_BUCKET + _APP_STORAGE_WASABI_ACCESS_KEY + _APP_STORAGE_WASABI_SECRET + _APP_STORAGE_WASABI_REGION + _APP_STORAGE_WASABI_BUCKET version: '3' services: traefik: - image: traefik:2.5 + image: traefik:2.7 <<: *x-logging container_name: appwrite-traefik command: @@ -72,7 +96,7 @@ services: - traefik.http.routers.appwrite_api_https.tls=true volumes: - appwrite-uploads:/storage/uploads:rw - - appwrite-video:/storage/video:rw + - appwrite-videos:/storage/videos:rw - appwrite-cache:/storage/cache:rw - appwrite-config:/storage/config:rw - appwrite-certificates:/storage/certificates:rw @@ -81,6 +105,7 @@ services: - ./tests:/usr/src/code/tests - ./app:/usr/src/code/app - ./vendor:/usr/src/code/vendor #TODO remove when done!! + # - ./vendor/utopia/database:/usr/src/code/vendor/utopia/database - ./docs:/usr/src/code/docs - ./public:/usr/src/code/public - ./src:/usr/src/code/src @@ -129,18 +154,11 @@ services: - _APP_INFLUXDB_HOST - _APP_INFLUXDB_PORT - _APP_STORAGE_LIMIT + - _APP_STORAGE_PREVIEW_LIMIT - _APP_STORAGE_ANTIVIRUS - _APP_STORAGE_ANTIVIRUS_HOST - _APP_STORAGE_ANTIVIRUS_PORT - - _APP_STORAGE_DEVICE - - _APP_STORAGE_S3_ACCESS_KEY - - _APP_STORAGE_S3_SECRET - - _APP_STORAGE_S3_REGION - - _APP_STORAGE_S3_BUCKET - - _APP_STORAGE_DO_SPACES_ACCESS_KEY - - _APP_STORAGE_DO_SPACES_SECRET - - _APP_STORAGE_DO_SPACES_REGION - - _APP_STORAGE_DO_SPACES_BUCKET + - *x-env-storage - _APP_FUNCTIONS_SIZE_LIMIT - _APP_FUNCTIONS_TIMEOUT - _APP_FUNCTIONS_BUILD_TIMEOUT @@ -150,6 +168,7 @@ services: - _APP_FUNCTIONS_MEMORY_SWAP - _APP_FUNCTIONS_RUNTIMES - _APP_EXECUTOR_SECRET + - _APP_EXECUTOR_HOST - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG - _APP_STATSD_HOST @@ -158,6 +177,8 @@ services: - _APP_MAINTENANCE_RETENTION_EXECUTION - _APP_MAINTENANCE_RETENTION_ABUSE - _APP_MAINTENANCE_RETENTION_AUDIT + - _APP_PHONE_PROVIDER + - _APP_PHONE_SECRET appwrite-realtime: entrypoint: realtime @@ -220,7 +241,7 @@ services: - ./app:/usr/src/code/app - ./src:/usr/src/code/src - appwrite-uploads:/storage/uploads:rw - - appwrite-video:/storage/video:rw + - appwrite-videos:/storage/videos:rw - ./tests:/usr/src/code/tests #TODO remove when done!! - ./vendor:/usr/src/code/vendor #TODO remove when done!! @@ -310,7 +331,7 @@ services: - mariadb volumes: - appwrite-uploads:/storage/uploads:rw - - appwrite-video:/storage/video:rw + - appwrite-videos:/storage/videos:rw - appwrite-cache:/storage/cache:rw - appwrite-functions:/storage/functions:rw - appwrite-builds:/storage/builds:rw @@ -329,23 +350,16 @@ services: - _APP_DB_SCHEMA - _APP_DB_USER - _APP_DB_PASS - - _APP_STORAGE_DEVICE - - _APP_STORAGE_S3_ACCESS_KEY - - _APP_STORAGE_S3_SECRET - - _APP_STORAGE_S3_REGION - - _APP_STORAGE_S3_BUCKET - - _APP_STORAGE_DO_SPACES_ACCESS_KEY - - _APP_STORAGE_DO_SPACES_SECRET - - _APP_STORAGE_DO_SPACES_REGION - - _APP_STORAGE_DO_SPACES_BUCKET + - *x-env-storage - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG - _APP_EXECUTOR_SECRET + - _APP_EXECUTOR_HOST - appwrite-worker-database: - entrypoint: worker-database + appwrite-worker-databases: + entrypoint: worker-databases <<: *x-logging - container_name: appwrite-worker-database + container_name: appwrite-worker-databases build: context: . networks: @@ -390,6 +404,7 @@ services: - _APP_ENV - _APP_OPENSSL_KEY_V1 - _APP_EXECUTOR_SECRET + - _APP_EXECUTOR_HOST - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER @@ -421,6 +436,7 @@ services: environment: - _APP_ENV - _APP_OPENSSL_KEY_V1 + - _APP_DOMAIN - _APP_DOMAIN_TARGET - _APP_SYSTEM_SECURITY_EMAIL_ADDRESS - _APP_REDIS_HOST @@ -464,6 +480,7 @@ services: - _APP_DB_PASS - _APP_FUNCTIONS_TIMEOUT - _APP_EXECUTOR_SECRET + - _APP_EXECUTOR_HOST - _APP_USAGE_STATS - DOCKERHUB_PULL_USERNAME - DOCKERHUB_PULL_PASSWORD @@ -510,15 +527,7 @@ services: - OPEN_RUNTIMES_NETWORK - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG - - _APP_STORAGE_DEVICE - - _APP_STORAGE_S3_ACCESS_KEY - - _APP_STORAGE_S3_SECRET - - _APP_STORAGE_S3_REGION - - _APP_STORAGE_S3_BUCKET - - _APP_STORAGE_DO_SPACES_ACCESS_KEY - - _APP_STORAGE_DO_SPACES_SECRET - - _APP_STORAGE_DO_SPACES_REGION - - _APP_STORAGE_DO_SPACES_BUCKET + - *x-env-storage - DOCKERHUB_PULL_USERNAME - DOCKERHUB_PULL_PASSWORD @@ -554,6 +563,30 @@ services: - _APP_LOGGING_PROVIDER - _APP_LOGGING_CONFIG + appwrite-worker-messaging: + entrypoint: worker-messaging + <<: *x-logging + container_name: appwrite-worker-messaging + build: + context: . + networks: + - appwrite + volumes: + - ./app:/usr/src/code/app + - ./src:/usr/src/code/src + depends_on: + - redis + environment: + - _APP_ENV + - _APP_REDIS_HOST + - _APP_REDIS_PORT + - _APP_REDIS_USER + - _APP_REDIS_PASS + - _APP_PHONE_PROVIDER + - _APP_PHONE_FROM + - _APP_LOGGING_PROVIDER + - _APP_LOGGING_CONFIG + appwrite-maintenance: entrypoint: maintenance <<: *x-logging @@ -569,11 +602,18 @@ services: - redis environment: - _APP_ENV + - _APP_DOMAIN + - _APP_DOMAIN_TARGET - _APP_OPENSSL_KEY_V1 - _APP_REDIS_HOST - _APP_REDIS_PORT - _APP_REDIS_USER - _APP_REDIS_PASS + - _APP_DB_HOST + - _APP_DB_PORT + - _APP_DB_SCHEMA + - _APP_DB_USER + - _APP_DB_PASS - _APP_MAINTENANCE_INTERVAL - _APP_MAINTENANCE_RETENTION_EXECUTION - _APP_MAINTENANCE_RETENTION_ABUSE @@ -611,6 +651,8 @@ services: - _APP_REDIS_PORT - _APP_REDIS_USER - _APP_REDIS_PASS + - _APP_LOGGING_PROVIDER + - _APP_LOGGING_CONFIG appwrite-schedule: entrypoint: schedule @@ -666,6 +708,11 @@ services: image: redis:6.2-alpine <<: *x-logging container_name: appwrite-redis + command: > + redis-server + --maxmemory 512mb + --maxmemory-policy allkeys-lru + --maxmemory-samples 5 ports: - "6379:6379" networks: @@ -802,7 +849,7 @@ volumes: appwrite-redis: appwrite-cache: appwrite-uploads: - appwrite-video: + appwrite-videos: appwrite-certificates: appwrite-functions: appwrite-builds: diff --git a/src/Appwrite/Event/Event.php b/src/Appwrite/Event/Event.php index 4009dfe103..222cf59444 100644 --- a/src/Appwrite/Event/Event.php +++ b/src/Appwrite/Event/Event.php @@ -32,17 +32,17 @@ class Event public const BUILDS_QUEUE_NAME = 'v1-builds'; public const BUILDS_CLASS_NAME = 'BuildsV1'; - const TRANSCODING_QUEUE_NAME = 'v1-transcoding'; - const TRANSCODING_CLASS_NAME = 'TranscodingV1'; + public const MESSAGING_QUEUE_NAME = 'v1-messaging'; + public const MESSAGING_CLASS_NAME = 'MessagingV1'; protected string $queue = ''; protected string $class = ''; protected string $event = ''; protected array $params = []; protected array $payload = []; + protected array $context = []; protected ?Document $project = null; protected ?Document $user = null; - protected ?Document $context = null; /** * @param string $queue @@ -172,12 +172,13 @@ class Event /** * Set context for this event. * + * @param string $key * @param Document $context * @return self */ - public function setContext(Document $context): self + public function setContext(string $key, Document $context): self { - $this->context = $context; + $this->context[$key] = $context; return $this; } @@ -185,11 +186,13 @@ class Event /** * Get context for this event. * + * @param string $key + * * @return null|Document */ - public function getContext(): ?Document + public function getContext(string $key): ?Document { - return $this->context; + return $this->context[$key] ?? null; } /** @@ -295,14 +298,28 @@ class Event $type = $parts[0] ?? false; $resource = $parts[1] ?? false; $hasSubResource = $count > 3 && \str_starts_with($parts[3], '['); + $hasSubSubResource = $count > 5 && \str_starts_with($parts[5], '[') && $hasSubResource; if ($hasSubResource) { $subType = $parts[2]; $subResource = $parts[3]; + } + + if ($hasSubSubResource) { + $subSubType = $parts[4]; + $subSubResource = $parts[5]; + if ($count == 8) { + $attribute = $parts[7]; + } + } + + if ($hasSubResource && !$hasSubSubResource) { if ($count === 6) { $attribute = $parts[5]; } - } else { + } + + if (!$hasSubResource) { if ($count === 4) { $attribute = $parts[3]; } @@ -310,18 +327,25 @@ class Event $subType ??= false; $subResource ??= false; + $subSubType ??= false; + $subSubResource ??= false; $attribute ??= false; $action = match (true) { !$hasSubResource && $count > 2 => $parts[2], + $hasSubSubResource => $parts[6] ?? false, $hasSubResource && $count > 4 => $parts[4], default => false }; + + return [ 'type' => $type, 'resource' => $resource, 'subType' => $subType, 'subResource' => $subResource, + 'subSubType' => $subSubType, + 'subSubResource' => $subSubResource, 'action' => $action, 'attribute' => $attribute, ]; @@ -348,6 +372,8 @@ class Event $resource = $parsed['resource']; $subType = $parsed['subType']; $subResource = $parsed['subResource']; + $subSubType = $parsed['subSubType']; + $subSubResource = $parsed['subSubResource']; $action = $parsed['action']; $attribute = $parsed['attribute']; @@ -359,11 +385,21 @@ class Event throw new InvalidArgumentException("{$subResource} is missing from the params."); } + if ($subSubResource && !\in_array(\trim($subSubResource, "\[\]"), $paramKeys)) { + throw new InvalidArgumentException("{$subSubResource} is missing from the params."); + } + /** * Create all possible patterns including placeholders. */ if ($action) { - if ($subResource) { + if ($subSubResource) { + if ($attribute) { + $patterns[] = \implode('.', [$type, $resource, $subType, $subResource, $subSubType, $subSubResource, $action, $attribute]); + } + $patterns[] = \implode('.', [$type, $resource, $subType, $subResource, $subSubType, $subSubResource, $action]); + $patterns[] = \implode('.', [$type, $resource, $subType, $subResource, $subSubType, $subSubResource]); + } elseif ($subResource) { if ($attribute) { $patterns[] = \implode('.', [$type, $resource, $subType, $subResource, $action, $attribute]); } @@ -376,6 +412,9 @@ class Event $patterns[] = \implode('.', [$type, $resource, $action, $attribute]); } } + if ($subSubResource) { + $patterns[] = \implode('.', [$type, $resource, $subType, $subResource, $subSubType, $subSubResource]); + } if ($subResource) { $patterns[] = \implode('.', [$type, $resource, $subType, $subResource]); } @@ -395,12 +434,24 @@ class Event $events[] = \str_replace($paramKeys, '*', $eventPattern); foreach ($paramKeys as $key) { foreach ($paramKeys as $current) { - if ($current === $key) { - continue; + if ($subSubResource) { + foreach ($paramKeys as $subCurrent) { + if ($subCurrent === $current || $subCurrent === $key) { + continue; + } + $filtered1 = \array_filter($paramKeys, fn(string $k) => $k === $subCurrent); + $events[] = \str_replace($paramKeys, $paramValues, \str_replace($filtered1, '*', $eventPattern)); + $filtered2 = \array_filter($paramKeys, fn(string $k) => $k === $current); + $events[] = \str_replace($paramKeys, $paramValues, \str_replace($filtered2, '*', \str_replace($filtered1, '*', $eventPattern))); + $events[] = \str_replace($paramKeys, $paramValues, \str_replace($filtered2, '*', $eventPattern)); + } + } else { + if ($current === $key) { + continue; + } + $filtered = \array_filter($paramKeys, fn(string $k) => $k === $current); + $events[] = \str_replace($paramKeys, $paramValues, \str_replace($filtered, '*', $eventPattern)); } - - $filtered = \array_filter($paramKeys, fn(string $k) => $k === $current); - $events[] = \str_replace($paramKeys, $paramValues, \str_replace($filtered, '*', $eventPattern)); } } } @@ -411,6 +462,9 @@ class Event $events = \array_map(fn (string $event) => \str_replace(['[', ']'], '', $event), $events); $events = \array_unique($events); - return $events; + /** + * Force a non-assoc array. + */ + return \array_values($events); } } diff --git a/src/Appwrite/Utopia/Response.php b/src/Appwrite/Utopia/Response.php index 09cdd704a7..69f6d3de8b 100644 --- a/src/Appwrite/Utopia/Response.php +++ b/src/Appwrite/Utopia/Response.php @@ -22,11 +22,11 @@ use Appwrite\Utopia\Response\Model\AttributeIP; use Appwrite\Utopia\Response\Model\AttributeURL; use Appwrite\Utopia\Response\Model\BaseList; use Appwrite\Utopia\Response\Model\Collection; +use Appwrite\Utopia\Response\Model\Database; use Appwrite\Utopia\Response\Model\Continent; use Appwrite\Utopia\Response\Model\Country; use Appwrite\Utopia\Response\Model\Currency; use Appwrite\Utopia\Response\Model\Document as ModelDocument; -use Appwrite\Utopia\Response\Model\DocumentList; use Appwrite\Utopia\Response\Model\Domain; use Appwrite\Utopia\Response\Model\Error; use Appwrite\Utopia\Response\Model\ErrorDev; @@ -65,11 +65,12 @@ use Appwrite\Utopia\Response\Model\Runtime; use Appwrite\Utopia\Response\Model\UsageBuckets; use Appwrite\Utopia\Response\Model\UsageCollection; use Appwrite\Utopia\Response\Model\UsageDatabase; +use Appwrite\Utopia\Response\Model\UsageDatabases; use Appwrite\Utopia\Response\Model\UsageFunctions; use Appwrite\Utopia\Response\Model\UsageProject; use Appwrite\Utopia\Response\Model\UsageStorage; use Appwrite\Utopia\Response\Model\UsageUsers; -use Appwrite\Utopia\Response\Model\FileRendition; +use Appwrite\Utopia\Response\Model\VideoRendition; use Appwrite\Utopia\Response\Model\Video; use Appwrite\Utopia\Response\Model\VideoProfile; @@ -88,6 +89,7 @@ class Response extends SwooleResponse public const MODEL_METRIC_LIST = 'metricList'; public const MODEL_ERROR_DEV = 'errorDev'; public const MODEL_BASE_LIST = 'baseList'; + public const MODEL_USAGE_DATABASES = 'usageDatabases'; public const MODEL_USAGE_DATABASE = 'usageDatabase'; public const MODEL_USAGE_COLLECTION = 'usageCollection'; public const MODEL_USAGE_USERS = 'usageUsers'; @@ -97,6 +99,8 @@ class Response extends SwooleResponse public const MODEL_USAGE_PROJECT = 'usageProject'; // Database + public const MODEL_DATABASE = 'database'; + public const MODEL_DATABASE_LIST = 'databaseList'; public const MODEL_COLLECTION = 'collection'; public const MODEL_COLLECTION_LIST = 'collectionList'; public const MODEL_INDEX = 'index'; @@ -135,8 +139,8 @@ class Response extends SwooleResponse public const MODEL_VIDEO = 'video'; public const MODEL_VIDEO_PROFILE = 'videoProfile'; public const MODEL_VIDEO_PROFILE_LIST = 'videoProfileList'; - public const MODEL_FILE_RENDITION = 'fileRendition'; - public const MODEL_FILE_RENDITIONS_LIST = 'fileRenditionsList'; + public const MODEL_VIDEO_RENDITION = 'videoRendition'; + public const MODEL_VIDEO_RENDITIONS_LIST = 'videoRenditionsList'; // Locale public const MODEL_LOCALE = 'locale'; @@ -223,6 +227,7 @@ class Response extends SwooleResponse // Lists ->setModel(new BaseList('Documents List', self::MODEL_DOCUMENT_LIST, 'documents', self::MODEL_DOCUMENT)) ->setModel(new BaseList('Collections List', self::MODEL_COLLECTION_LIST, 'collections', self::MODEL_COLLECTION)) + ->setModel(new BaseList('Databases List', self::MODEL_DATABASE_LIST, 'databases', self::MODEL_DATABASE)) ->setModel(new BaseList('Indexes List', self::MODEL_INDEX_LIST, 'indexes', self::MODEL_INDEX)) ->setModel(new BaseList('Users List', self::MODEL_USER_LIST, 'users', self::MODEL_USER)) ->setModel(new BaseList('Sessions List', self::MODEL_SESSION_LIST, 'sessions', self::MODEL_SESSION)) @@ -247,10 +252,11 @@ class Response extends SwooleResponse ->setModel(new BaseList('Currencies List', self::MODEL_CURRENCY_LIST, 'currencies', self::MODEL_CURRENCY)) ->setModel(new BaseList('Phones List', self::MODEL_PHONE_LIST, 'phones', self::MODEL_PHONE)) ->setModel(new BaseList('Metric List', self::MODEL_METRIC_LIST, 'metrics', self::MODEL_METRIC, true, false)) - ->setModel(new BaseList('File Renditions List', self::MODEL_FILE_RENDITIONS_LIST, 'renditions', self::MODEL_FILE_RENDITION)) + ->setModel(new BaseList('Video Renditions List', self::MODEL_VIDEO_RENDITIONS_LIST, 'renditions', self::MODEL_VIDEO_RENDITION)) ->setModel(new BaseList('video profile List', self::MODEL_VIDEO_PROFILE_LIST, 'profiles', self::MODEL_VIDEO_PROFILE)) // Entities + ->setModel(new Database()) ->setModel(new Collection()) ->setModel(new Attribute()) ->setModel(new AttributeList()) @@ -296,6 +302,7 @@ class Response extends SwooleResponse ->setModel(new HealthTime()) ->setModel(new HealthVersion()) ->setModel(new Metric()) + ->setModel(new UsageDatabases()) ->setModel(new UsageDatabase()) ->setModel(new UsageCollection()) ->setModel(new UsageUsers()) @@ -303,7 +310,7 @@ class Response extends SwooleResponse ->setModel(new UsageBuckets()) ->setModel(new UsageFunctions()) ->setModel(new UsageProject()) - ->setModel(new FileRendition()) + ->setModel(new VideoRendition()) ->setModel(new Video()) ->setModel(new VideoProfile()) diff --git a/src/Appwrite/Utopia/Response/Model/Database.php b/src/Appwrite/Utopia/Response/Model/Database.php new file mode 100644 index 0000000000..96d925c58f --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/Database.php @@ -0,0 +1,47 @@ +addRule('$id', [ + 'type' => self::TYPE_STRING, + 'description' => 'Database ID.', + 'default' => '', + 'example' => '5e5ea5c16897e', + ]) + ->addRule('name', [ + 'type' => self::TYPE_STRING, + 'description' => 'Database name.', + 'default' => '', + 'example' => 'My Database', + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'Database'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_DATABASE; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/UsageDatabases.php b/src/Appwrite/Utopia/Response/Model/UsageDatabases.php new file mode 100644 index 0000000000..9fb6f55f99 --- /dev/null +++ b/src/Appwrite/Utopia/Response/Model/UsageDatabases.php @@ -0,0 +1,146 @@ +addRule('range', [ + 'type' => self::TYPE_STRING, + 'description' => 'The time range of the usage stats.', + 'default' => '', + 'example' => '30d', + ]) + ->addRule('databasesCount', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for total number of documents.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ->addRule('documentsCount', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for total number of documents.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ->addRule('collectionsCount', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for total number of collections.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ->addRule('databasesCreate', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for documents created.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ->addRule('databasesRead', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for documents read.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ->addRule('databasesUpdate', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for documents updated.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ->addRule('databasesDelete', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for total number of collections.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ->addRule('documentsCreate', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for documents created.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ->addRule('documentsRead', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for documents read.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ->addRule('documentsUpdate', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for documents updated.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ->addRule('documentsDelete', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for documents deleted.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ->addRule('collectionsCreate', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for collections created.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ->addRule('collectionsRead', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for collections read.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ->addRule('collectionsUpdate', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for collections updated.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ->addRule('collectionsDelete', [ + 'type' => Response::MODEL_METRIC_LIST, + 'description' => 'Aggregated stats for collections delete.', + 'default' => [], + 'example' => new \stdClass(), + 'array' => true + ]) + ; + } + + /** + * Get Name + * + * @return string + */ + public function getName(): string + { + return 'UsageDatabases'; + } + + /** + * Get Type + * + * @return string + */ + public function getType(): string + { + return Response::MODEL_USAGE_DATABASES; + } +} diff --git a/src/Appwrite/Utopia/Response/Model/FileRendition.php b/src/Appwrite/Utopia/Response/Model/VideoRendition.php similarity index 82% rename from src/Appwrite/Utopia/Response/Model/FileRendition.php rename to src/Appwrite/Utopia/Response/Model/VideoRendition.php index 2376f34453..96fdf43163 100644 --- a/src/Appwrite/Utopia/Response/Model/FileRendition.php +++ b/src/Appwrite/Utopia/Response/Model/VideoRendition.php @@ -5,7 +5,7 @@ namespace Appwrite\Utopia\Response\Model; use Appwrite\Utopia\Response; use Appwrite\Utopia\Response\Model; -class FileRendition extends Model +class VideoRendition extends Model { public function __construct() { @@ -16,25 +16,19 @@ class FileRendition extends Model 'default' => '', 'example' => '5e5ea5c16897e', ]) - ->addRule('bucketId', [ + ->addRule('videoId', [ 'type' => self::TYPE_STRING, - 'description' => 'Bucket ID.', + 'description' => 'Video ID.', 'default' => '', 'example' => '5e5ea5c16897e', ]) - ->addRule('fileId', [ + ->addRule('profileId', [ 'type' => self::TYPE_STRING, - 'description' => 'File ID.', + 'description' => 'profile ID.', 'default' => '', 'example' => 'd5fg5ehg1c168g7c', ]) - ->addRule('renditionId', [ - 'type' => self::TYPE_INTEGER, - 'description' => 'Rendition ID.', - 'default' => '', - 'example' => 3, - ]) - ->addRule('renditionName', [ + ->addRule('name', [ 'type' => self::TYPE_STRING, 'description' => 'Rendition name.', 'default' => '', @@ -64,6 +58,12 @@ class FileRendition extends Model 'default' => 0, 'example' => 88, ]) + ->addRule('stream', [ + 'type' => self::TYPE_STRING, + 'description' => 'Rendition trascoding stream protocol type', + 'default' => 0, + 'example' => 88, + ]) ; } diff --git a/tests/e2e/Services/Video/VideoCustomServerTest.php b/tests/e2e/Services/Video/VideoCustomServerTest.php index 6cb065d337..fd6f239dee 100644 --- a/tests/e2e/Services/Video/VideoCustomServerTest.php +++ b/tests/e2e/Services/Video/VideoCustomServerTest.php @@ -82,7 +82,7 @@ class VideoCustomServerTest extends Scope ], [ 'read' => ['role:all'], 'write' => ['role:all'] - ]); + ]); $videoId = $response['body']['$id']; @@ -104,29 +104,8 @@ class VideoCustomServerTest extends Scope 'write' => ['role:all'] ]); - var_dump($response['body']); - -// var_dump($pid); -// var_dump($key); -// var_dump($fid); -// var_dump($bid); -// -//// -//// $pid = '62b1956c92e0e39ef577'; -//// $key = 'f9b8dd3800b93a3dd513cf7cbcbd436cd61850b2c101662210e8ee2f052f796b3d4c7a08149634ce90da6037f6164d7faa36b32b91b568524e6720014e149b83f7a970c28de1a14a97a69010be325d142d51ca51f0f1b29783a7c1f4689d1b90a42cf19a7b55ec9ea6dc51974a1740b67e71de9f80009c2d91c6f3c686aa616c'; -//// $fid = '62b1956e4f03f57a0f74'; -//// $bid = '62b1956d0c600d70c8f7'; -//// -// $video = $this->client->call(Client::METHOD_POST, '/video/buckets/' . $bid . '/files/' . $fid, [ -// 'content-type' => 'application/json', -// 'x-appwrite-project' => $pid, -// 'x-appwrite-key' => $key, -// ], [ -// 'read' => ['role:all'], -// 'write' => ['role:all'] -// ]); -// var_dump($video); + $this->assertNotEmpty($profileId); return [ 'videoId' => $videoId, @@ -135,40 +114,39 @@ class VideoCustomServerTest extends Scope } /** - * @depends testCreateBucketFile + * @depends testTranscodingRendition */ - public function testRenditions(): void + public function testGetRenditions($data): void { - $pid = '62b1956c92e0e39ef577'; - $key = 'f9b8dd3800b93a3dd513cf7cbcbd436cd61850b2c101662210e8ee2f052f796b3d4c7a08149634ce90da6037f6164d7faa36b32b91b568524e6720014e149b83f7a970c28de1a14a97a69010be325d142d51ca51f0f1b29783a7c1f4689d1b90a42cf19a7b55ec9ea6dc51974a1740b67e71de9f80009c2d91c6f3c686aa616c'; - $fid = '62b1956e4f03f57a0f74'; - $bid = '62b1956d0c600d70c8f7'; + sleep(30); - - $renditions = $this->client->call(Client::METHOD_GET, '/video/buckets/' . $bid . '/files/' . $fid . '/renditions', [ + $response = $this->client->call(Client::METHOD_GET, '/video/' . $data['videoId'] . '/hls/renditions', [ 'content-type' => 'application/json', - 'x-appwrite-project' => $pid, - 'x-appwrite-key' => $key, + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ],[ + 'read' => ['role:all'], + 'write' => ['role:all'] ]); - var_dump($renditions['body']); + + var_dump($response['body']); } - - public function testPlaylist(): void + /** + * @depends testTranscodingRendition + */ + public function testPlaylist($data): void { + sleep(60); - $pid = '62b1956c92e0e39ef577'; - $key = 'f9b8dd3800b93a3dd513cf7cbcbd436cd61850b2c101662210e8ee2f052f796b3d4c7a08149634ce90da6037f6164d7faa36b32b91b568524e6720014e149b83f7a970c28de1a14a97a69010be325d142d51ca51f0f1b29783a7c1f4689d1b90a42cf19a7b55ec9ea6dc51974a1740b67e71de9f80009c2d91c6f3c686aa616c'; - $fid = '62b1956e4f03f57a0f74'; - $bid = '62b1956d0c600d70c8f7'; - $stream = 'hls'; - - $renditions = $this->client->call(Client::METHOD_GET, '/video/buckets/' . $bid . '/files/' . $stream . '/' . $fid, [ + $response = $this->client->call(Client::METHOD_GET, '/video/' . $data['videoId'] . '/master/hls/' . $data['videoId'].'.m3u8', [ 'content-type' => 'application/json', - 'x-appwrite-project' => $pid, - 'x-appwrite-key' => $key, + 'x-appwrite-project' => $this->getProject()['$id'], + 'x-appwrite-key' => $this->getProject()['apiKey'], + ],[ + 'read' => ['role:all'], + 'write' => ['role:all'] ]); - var_dump($renditions['body']); } }