Commit eb7ad2f7 authored by Yassine Doghri's avatar Yassine Doghri
Browse files

fix(import): rewrite download_file helper to output curl response directly to file

This prevents memory exhaustion when downloading large files
parent 281eefc6
Loading
Loading
Loading
Loading
Loading
+3 −1
Original line number Diff line number Diff line
@@ -136,7 +136,9 @@ if (! function_exists('publication_pill')) {
            $customClass .
            '">' .
            $label .
            ($publicationStatus === 'with_podcast' ? '<Icon glyph="error-warning" class="flex-shrink-0 ml-1 text-lg" />' : '') .
            ($publicationStatus === 'with_podcast' ? icon('error-warning-fill', [
                'class' => 'flex-shrink-0 ml-1 text-lg',
            ]) : '') .
            '</span>';
    }
}
+21 −39
Original line number Diff line number Diff line
@@ -9,47 +9,12 @@ declare(strict_types=1);
 */

use CodeIgniter\Files\File;
use CodeIgniter\HTTP\ResponseInterface;
use Config\Mimes;
use Config\Services;

if (! function_exists('download_file')) {
    function download_file(string $fileUrl, string $mimetype = ''): File
    {
        $client = Services::curlrequest();

        $response = $client->get($fileUrl, [
            'headers' => [
                'User-Agent' => 'Castopod/' . CP_VERSION,
            ],
        ]);

        // redirect to new file location
        $newFileUrl = $fileUrl;
        while (
            in_array(
                $response->getStatusCode(),
                [
                    ResponseInterface::HTTP_MOVED_PERMANENTLY,
                    ResponseInterface::HTTP_FOUND,
                    ResponseInterface::HTTP_SEE_OTHER,
                    ResponseInterface::HTTP_NOT_MODIFIED,
                    ResponseInterface::HTTP_TEMPORARY_REDIRECT,
                    ResponseInterface::HTTP_PERMANENT_REDIRECT,
                ],
                true,
            )
        ) {
            $newFileUrl = trim($response->header('location')->getValue());
            $response = $client->get($newFileUrl, [
                'headers' => [
                    'User-Agent' => 'Castopod/' . CP_VERSION,
                ],
                'http_errors' => false,
            ]);
        }

        $fileExtension = pathinfo(parse_url($newFileUrl, PHP_URL_PATH), PATHINFO_EXTENSION);
        $fileExtension = pathinfo(parse_url($fileUrl, PHP_URL_PATH), PATHINFO_EXTENSION);
        $extension = $fileExtension === '' ? Mimes::guessExtensionFromType($mimetype) : $fileExtension;
        $tmpFilename =
            time() .
@@ -57,9 +22,26 @@ if (! function_exists('download_file')) {
            bin2hex(random_bytes(10)) .
            '.' .
            $extension;
        $tmpfileKey = WRITEPATH . 'uploads/' . $tmpFilename;
        file_put_contents($tmpfileKey, $response->getBody());
        $tmpfilePath = WRITEPATH . 'uploads/' . $tmpFilename;

        $file = fopen($tmpfilePath, 'w');
        $ch = curl_init();

        curl_setopt($ch, CURLOPT_URL, $fileUrl);

        // output directly to file
        curl_setopt($ch, CURLOPT_FILE, $file);
        curl_setopt($ch, CURLOPT_HTTPHEADER, ['User-Agent: Castopod/' . CP_VERSION]);

        // follow redirects up to 20, like Apple Podcasts
        curl_setopt($ch, CURLOPT_FOLLOWLOCATION, 1);
        curl_setopt($ch, CURLOPT_MAXREDIRS, 20);

        curl_exec($ch);
        curl_close($ch);

        fclose($file);

        return new File($tmpfileKey);
        return new File($tmpfilePath);
    }
}
+1 −0
Original line number Diff line number Diff line
@@ -39,6 +39,7 @@ $isEpisodeArea = isset($podcast) && isset($episode);
                    <div class="flex flex-wrap items-center truncate">
                    <?php if (($isEpisodeArea && $episode->is_premium) || ($isPodcastArea && $podcast->is_premium)): ?>
                        <div class="inline-flex items-center">
                            <?php // @icon('exchange-dollar-fill')?>
                            <IconButton uri="<?= route_to('subscription-list', $podcast->id) ?>" glyph="exchange-dollar-fill" variant="secondary" size="large" class="p-0 mr-2 border-0"><?= ($isEpisodeArea && $episode->is_premium) ? lang('PremiumPodcasts.episode_is_premium') : lang('PremiumPodcasts.podcast_is_premium') ?></IconButton>
                            <Heading tagName="h1" size="large" class="truncate"><?= $this->renderSection('pageTitle') ?></Heading>
                        </div>