-
Notifications
You must be signed in to change notification settings - Fork 2
feat(dropbox): move media upload processing from LV jobs to cron task #534
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
27d6008
8d8ecee
e8e1727
62d4c72
4f07f71
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,92 @@ | ||
| <?php namespace App\Console\Commands; | ||
| /** | ||
| * Copyright 2026 OpenStack Foundation | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| **/ | ||
|
|
||
| use Illuminate\Console\Command; | ||
| use Illuminate\Support\Facades\Log; | ||
| use Exception; | ||
| use services\model\ISummitService; | ||
|
|
||
| /** | ||
| * Class ProcessPendingMediaUploadsCommand | ||
| * @package App\Console\Commands | ||
| */ | ||
| class ProcessPendingMediaUploadsCommand extends Command | ||
| { | ||
| /** | ||
| * @var ISummitService | ||
| */ | ||
| private $summit_service; | ||
|
|
||
| /** | ||
| * @param ISummitService $summit_service | ||
| */ | ||
| public function __construct(ISummitService $summit_service) | ||
| { | ||
| parent::__construct(); | ||
| $this->summit_service = $summit_service; | ||
| } | ||
|
|
||
| /** | ||
| * The console command name. | ||
| * | ||
| * @var string | ||
| */ | ||
| protected $name = 'summit:process-pending-media-uploads'; | ||
|
|
||
| /** | ||
| * The name and signature of the console command. | ||
| * | ||
| * @var string | ||
| */ | ||
| protected $signature = 'summit:process-pending-media-uploads'; | ||
|
|
||
| /** | ||
| * The console command description. | ||
| * | ||
| * @var string | ||
| */ | ||
| protected $description = 'Process pending media uploads from the PendingMediaUpload table'; | ||
|
|
||
| /** | ||
| * Execute the console command. | ||
| * | ||
| * @return int | ||
| */ | ||
| public function handle(): int | ||
| { | ||
| try { | ||
| $this->info("ProcessPendingMediaUploadsCommand::handle starting"); | ||
| $start = time(); | ||
|
|
||
| $stats = $this->summit_service->processPendingMediaUploads(); | ||
|
|
||
| $end = time(); | ||
| $delta = $end - $start; | ||
|
|
||
| $this->info(sprintf( | ||
| "ProcessPendingMediaUploadsCommand::handle completed in %s seconds - processed: %s, errors: %s", | ||
| $delta, | ||
| $stats['processed'], | ||
| $stats['errors'] | ||
| )); | ||
|
|
||
| return self::SUCCESS; | ||
|
|
||
| } catch (Exception $ex) { | ||
| Log::warning($ex); | ||
| $this->error($ex->getMessage()); | ||
| return self::FAILURE; | ||
| } | ||
| } | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,108 @@ | ||
| <?php namespace App\Console\Commands; | ||
| /** | ||
| * Copyright 2024 OpenStack Foundation | ||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||
| * you may not use this file except in compliance with the License. | ||
| * You may obtain a copy of the License at | ||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||
| * Unless required by applicable law or agreed to in writing, software | ||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||
| * See the License for the specific language governing permissions and | ||
| * limitations under the License. | ||
| **/ | ||
| use Illuminate\Console\Command; | ||
| use Illuminate\Support\Facades\Log; | ||
| use Exception; | ||
| use services\model\ISummitService; | ||
|
|
||
| /** | ||
| * Class ReconcileMediaUploadsCommand | ||
| * @package App\Console\Commands | ||
| */ | ||
| class ReconcileMediaUploadsCommand extends Command | ||
| { | ||
| /** | ||
| * @var ISummitService | ||
| */ | ||
| private $summit_service; | ||
|
|
||
| /** | ||
| * @param ISummitService $summit_service | ||
| */ | ||
| public function __construct(ISummitService $summit_service) | ||
| { | ||
| parent::__construct(); | ||
| $this->summit_service = $summit_service; | ||
| } | ||
|
|
||
| /** | ||
| * The console command name. | ||
| * | ||
| * @var string | ||
| */ | ||
| protected $name = 'summit:reconcile-media-uploads'; | ||
|
|
||
| /** | ||
| * The name and signature of the console command. | ||
| * | ||
| * @var string | ||
| */ | ||
| protected $signature = 'summit:reconcile-media-uploads {summit_id} {media_upload_type_id?}'; | ||
|
|
||
| /** | ||
| * The console command description. | ||
| * | ||
| * @var string | ||
| */ | ||
| protected $description = 'Reconcile media uploads missing from private storage (Dropbox) by re-uploading from public storage'; | ||
|
|
||
| /** | ||
| * Execute the console command. | ||
| * | ||
| * @return int | ||
| */ | ||
| public function handle(): int | ||
| { | ||
| try { | ||
|
|
||
| $summit_id = $this->argument('summit_id'); | ||
| if (empty($summit_id)) | ||
| throw new \InvalidArgumentException("summit_id is required"); | ||
|
|
||
| $media_upload_type_id = $this->argument('media_upload_type_id'); | ||
|
|
||
| $this->info(sprintf( | ||
| "ReconcileMediaUploadsCommand::handle processing summit %s media upload type %s", | ||
| $summit_id, | ||
| $media_upload_type_id ?? 'all' | ||
| )); | ||
|
|
||
| $start = time(); | ||
|
|
||
| $result = $this->summit_service->reconcileMediaUploadsToPrivateStorage( | ||
| intval($summit_id), | ||
| !empty($media_upload_type_id) ? intval($media_upload_type_id) : null | ||
| ); | ||
|
|
||
| $end = time(); | ||
| $delta = $end - $start; | ||
|
|
||
| $this->info(sprintf( | ||
| "ReconcileMediaUploadsCommand::handle completed in %s seconds - checked: %s, reconciled: %s, missing: %s, errors: %s", | ||
| $delta, | ||
| $result['checked'], | ||
| $result['reconciled'], | ||
| $result['missing'], | ||
| $result['errors'] | ||
| )); | ||
|
|
||
| return self::SUCCESS; | ||
|
|
||
| } catch (Exception $ex) { | ||
| Log::warning($ex); | ||
| $this->error($ex->getMessage()); | ||
| return self::FAILURE; | ||
| } | ||
| } | ||
| } | ||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
| @@ -0,0 +1,109 @@ | ||||||||||||||||||||||||||||||||||
| <?php namespace App\Console\Commands; | ||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Copyright 2026 OpenStack Foundation | ||||||||||||||||||||||||||||||||||
| * Licensed under the Apache License, Version 2.0 (the "License"); | ||||||||||||||||||||||||||||||||||
| * you may not use this file except in compliance with the License. | ||||||||||||||||||||||||||||||||||
| * You may obtain a copy of the License at | ||||||||||||||||||||||||||||||||||
| * http://www.apache.org/licenses/LICENSE-2.0 | ||||||||||||||||||||||||||||||||||
| * Unless required by applicable law or agreed to in writing, software | ||||||||||||||||||||||||||||||||||
| * distributed under the License is distributed on an "AS IS" BASIS, | ||||||||||||||||||||||||||||||||||
| * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | ||||||||||||||||||||||||||||||||||
| * See the License for the specific language governing permissions and | ||||||||||||||||||||||||||||||||||
| * limitations under the License. | ||||||||||||||||||||||||||||||||||
| **/ | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| use App\Services\FileSystem\Dropbox\AutoRefreshingDropBoxTokenService; | ||||||||||||||||||||||||||||||||||
| use App\Services\FileSystem\Dropbox\RetryAfterDropboxClient as DropboxClient; | ||||||||||||||||||||||||||||||||||
| use Illuminate\Console\Command; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| /** | ||||||||||||||||||||||||||||||||||
| * Class TestDropboxTokenCommand | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * Quick diagnostic command to verify the Dropbox OAuth2 refresh token | ||||||||||||||||||||||||||||||||||
| * flow is working. Instantiates AutoRefreshingDropBoxTokenService, | ||||||||||||||||||||||||||||||||||
| * obtains an access token, then calls the Dropbox API to list the | ||||||||||||||||||||||||||||||||||
| * root folder as proof. | ||||||||||||||||||||||||||||||||||
| * | ||||||||||||||||||||||||||||||||||
| * @package App\Console\Commands | ||||||||||||||||||||||||||||||||||
| */ | ||||||||||||||||||||||||||||||||||
| class TestDropboxTokenCommand extends Command | ||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||
| protected $signature = 'dropbox:test-token'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| protected $description = 'Test Dropbox OAuth2 token refresh and verify API access'; | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| public function handle(): int | ||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||
| $appKey = config('filesystems.disks.dropbox.app_key', ''); | ||||||||||||||||||||||||||||||||||
| $appSecret = config('filesystems.disks.dropbox.app_secret', ''); | ||||||||||||||||||||||||||||||||||
| $refreshToken = config('filesystems.disks.dropbox.refresh_token', ''); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (empty($appKey) || empty($appSecret) || empty($refreshToken)) { | ||||||||||||||||||||||||||||||||||
| $this->error('Missing config. Ensure DROPBOX_APP_KEY, DROPBOX_APP_SECRET, and DROPBOX_REFRESH_TOKEN are set in .env'); | ||||||||||||||||||||||||||||||||||
| return 1; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| $this->info('1. Creating AutoRefreshingDropBoxTokenService...'); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| $tokenService = new AutoRefreshingDropBoxTokenService($appKey, $appSecret, $refreshToken); | ||||||||||||||||||||||||||||||||||
| } catch (\Exception $e) { | ||||||||||||||||||||||||||||||||||
| $this->error(" Failed to obtain access token: {$e->getMessage()}"); | ||||||||||||||||||||||||||||||||||
| return 1; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| $accessToken = $tokenService->getToken(); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (empty($accessToken)) { | ||||||||||||||||||||||||||||||||||
| $this->error(' Token service returned an empty access token.'); | ||||||||||||||||||||||||||||||||||
| return 1; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| $this->info(' Access token obtained: ' . substr($accessToken, 0, 12) . '...'); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
|
Comment on lines
+55
to
+63
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do not print access-token material. Even a prefix can leak sensitive token metadata into shell history or CI logs. 🛡️ Proposed fix- $this->info(' Access token obtained: ' . substr($accessToken, 0, 12) . '...');
+ $this->info(' Access token obtained.');📝 Committable suggestion
Suggested change
🤖 Prompt for AI Agents |
||||||||||||||||||||||||||||||||||
| $this->info('2. Creating DropboxClient and listing root folder...'); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| try { | ||||||||||||||||||||||||||||||||||
| $client = new DropboxClient($tokenService); | ||||||||||||||||||||||||||||||||||
| $result = $client->listFolder(''); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| $entries = $result['entries'] ?? []; | ||||||||||||||||||||||||||||||||||
| $this->info(" Success! Found {$this->countEntries($entries)} entries in root folder."); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| foreach (array_slice($entries, 0, 5) as $entry) { | ||||||||||||||||||||||||||||||||||
| $tag = $entry['.tag'] ?? 'unknown'; | ||||||||||||||||||||||||||||||||||
| $name = $entry['name'] ?? '?'; | ||||||||||||||||||||||||||||||||||
| $this->line(" [{$tag}] {$name}"); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| if (count($entries) > 5) { | ||||||||||||||||||||||||||||||||||
| $this->line(' ... and ' . (count($entries) - 5) . ' more'); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } catch (\Spatie\Dropbox\Exceptions\BadRequest $e) { | ||||||||||||||||||||||||||||||||||
| $this->error(" API call failed: BadRequest"); | ||||||||||||||||||||||||||||||||||
| $this->error(" Dropbox error code: " . ($e->dropboxCode ?? 'none')); | ||||||||||||||||||||||||||||||||||
| $this->error(" Message: " . ($e->getMessage() ?: '(empty)')); | ||||||||||||||||||||||||||||||||||
| // Rewind and re-read the response body | ||||||||||||||||||||||||||||||||||
| $e->response->getBody()->rewind(); | ||||||||||||||||||||||||||||||||||
| $this->error(" Response body: " . $e->response->getBody()->getContents()); | ||||||||||||||||||||||||||||||||||
| $this->error(" HTTP status: " . $e->response->getStatusCode()); | ||||||||||||||||||||||||||||||||||
| return 1; | ||||||||||||||||||||||||||||||||||
| } catch (\GuzzleHttp\Exception\ClientException $e) { | ||||||||||||||||||||||||||||||||||
| $this->error(" API call failed: " . get_class($e)); | ||||||||||||||||||||||||||||||||||
| $this->error(" Response body: " . $e->getResponse()->getBody()->getContents()); | ||||||||||||||||||||||||||||||||||
| return 1; | ||||||||||||||||||||||||||||||||||
| } catch (\Exception $e) { | ||||||||||||||||||||||||||||||||||
| $this->error(" API call failed: " . get_class($e) . " — {$e->getMessage()}"); | ||||||||||||||||||||||||||||||||||
| return 1; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| $this->info('3. Token refresh flow is working correctly.'); | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| return 0; | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
|
|
||||||||||||||||||||||||||||||||||
| private function countEntries(array $entries): int | ||||||||||||||||||||||||||||||||||
| { | ||||||||||||||||||||||||||||||||||
| return count($entries); | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| } | ||||||||||||||||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -55,6 +55,8 @@ class Kernel extends ConsoleKernel | |
| \App\Console\Commands\SetupSponsorServiceMessageBrokerCommand::class, | ||
| \App\Console\Commands\SetupPaymentServiceMessageBrokerCommand::class, | ||
| \App\Console\Commands\SetupSponsorUsersServiceMessageBrokerCommand::class, | ||
| \App\Console\Commands\ProcessPendingMediaUploadsCommand::class, | ||
| \App\Console\Commands\TestDropboxTokenCommand::class, | ||
|
Comment on lines
+58
to
+59
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Scheduling looks correct; one consideration on overlap timing.
Also applies to: 110-110 🤖 Prompt for AI Agents
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. this run on one single k8 pod There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
ʕ •ᴥ•ʔ |
||
| ]; | ||
|
|
||
| /** | ||
|
|
@@ -105,6 +107,8 @@ protected function schedule(Schedule $schedule) | |
|
|
||
| $schedule->command('summit:presentations-regenerate-media-uploads-temporal-public-urls')->everyMinute()->withoutOverlapping()->onOneServer(); | ||
|
|
||
| $schedule->command('summit:process-pending-media-uploads')->everyMinute()->withoutOverlapping()->onOneServer(); | ||
|
|
||
| //$schedule->command('summit:publish-stream-updates')->everyMinute()->withoutOverlapping()->onOneServer(); | ||
|
|
||
| $schedule->command('summit:purge-mark-as-deleted')->everyTwoHours()->withoutOverlapping()->onOneServer(); | ||
|
|
||
Uh oh!
There was an error while loading. Please reload this page.