Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
92 changes: 92 additions & 0 deletions app/Console/Commands/ProcessPendingMediaUploadsCommand.php
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;
}
}
}
108 changes: 108 additions & 0 deletions app/Console/Commands/ReconcileMediaUploadsCommand.php
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
);
Comment thread
smarcet marked this conversation as resolved.

$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;
}
}
}
109 changes: 109 additions & 0 deletions app/Console/Commands/TestDropboxTokenCommand.php
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
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

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

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
$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) . '...');
$accessToken = $tokenService->getToken();
if (empty($accessToken)) {
$this->error(' Token service returned an empty access token.');
return 1;
}
$this->info(' Access token obtained.');
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Console/Commands/TestDropboxTokenCommand.php` around lines 55 - 63, The
code prints a portion of the Dropbox access token (accessed via
$tokenService->getToken() into $accessToken) which can leak secrets; remove any
logging of the token or its prefix and instead log a non-sensitive success
message (e.g., "Access token obtained successfully") in the
TestDropboxTokenCommand handling code, eliminating the substr($accessToken, 0,
12) usage and any direct or partial token output while preserving the
empty-token check and exit behavior.

$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);
}
}
4 changes: 4 additions & 0 deletions app/Console/Kernel.php
Original file line number Diff line number Diff line change
Expand Up @@ -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
Copy link
Copy Markdown

@coderabbitai coderabbitai Bot Apr 21, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

Scheduling looks correct; one consideration on overlap timing.

withoutOverlapping()->onOneServer() at a 5-minute cadence is appropriate. Note that with the current RetryAfterDropboxClient::DEFAULT_RETRY_AFTER_SECONDS = 300 combined with maxUploadChunkRetries = 5, a single stuck chunk could hold the cron lock for ~25 minutes per invocation. Consider passing ->withoutOverlapping($expiresAt) with an explicit expiry (e.g., 30 minutes) to avoid a stale lock if a run is killed mid-sleep. Related: see the comment on DEFAULT_RETRY_AFTER_SECONDS in RetryAfterDropboxClient.php.

Also applies to: 110-110

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@app/Console/Kernel.php` around lines 58 - 59, The schedule for
ProcessPendingMediaUploadsCommand uses ->withoutOverlapping()->onOneServer() at
5-minute cadence but a stuck chunk (given
RetryAfterDropboxClient::DEFAULT_RETRY_AFTER_SECONDS = 300 and
maxUploadChunkRetries = 5) can hold the lock much longer; update the scheduler
invocation for ProcessPendingMediaUploadsCommand (and the other affected
scheduled entry) to pass an explicit expiry to withoutOverlapping (e.g.,
->withoutOverlapping(1800)) so the lock will expire after a safe upper bound (30
minutes) if a run is killed mid-sleep.

Copy link
Copy Markdown
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this run on one single k8 pod

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@smarcet, understood — with a single pod onOneServer() is effectively a no-op for lock contention between instances. The only residual edge case would be if the pod is evicted or OOM-killed mid-run: Laravel's default withoutOverlapping() lock TTL is 24 hours, so subsequent cron ticks would be skipped until it naturally expires. If that scenario is acceptable for this workload, the current setup is fine. Happy to close this comment if you're satisfied with the behaviour.

ʕ •ᴥ•ʔ

];

/**
Expand Down Expand Up @@ -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();
Expand Down
4 changes: 4 additions & 0 deletions app/Jobs/ProcessMediaUpload.php
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,10 @@
/**
* Class ProcessMediaUpload
* @package App\Jobs
*
* @deprecated Replaced by PendingMediaUpload table + cron-based processing.
* This job may still be referenced by in-flight queue jobs during deployment.
* Can be removed in a future cleanup after all queued jobs are processed.
*/
class ProcessMediaUpload implements ShouldQueue
{
Expand Down
Loading
Loading