Force download on GCS via App Engine using Signed URL

I get my file via:

require_once 'google/appengine/api/cloud_storage/CloudStorageTools.php';    
use google\appengine\api\cloud_storage\CloudStorageTools;

$public_link = CloudStorageTools::getPublicUrl("gs://bucket/file.pdf", false);

If I go to $public_link in the browser, it shows the PDF inside the browser. I am trying to figure out how I can force the download of this file.

Google App Engine only has a 60 second timeout so I'm afraid the serve function wont work via GAE. Does anyone have any suggestions?

--

EDIT

Andrei Volga's previous answer in this post suggests I use a Signed URL with a response-content-distribution header.

So far, I am able to create a signed URL that successfully shows the file but I am not able to generate a signed url that has any sort of header at all aka create a signed URL that will force the download instead of just showing it.

This is what I have so far, most of which is courtesy of mloureiro.

function googleBuildConfigurationString($method, $expiration, $file, array $options = [])
{
    $allowedMethods = ['GET', 'HEAD', 'PUT', 'DELETE'];
    // initialize
    $method = strtoupper($method);
    $contentType = $options['Content_Type'];
    $contentMd5 = $options['Content_MD5'] ? base64_encode($options['Content_MD5']) : '';
     $headers = $options['Canonicalized_Extension_Headers'] ? $options['Canonicalized_Extension_Headers'] . PHP_EOL : '';
     $file = $file ? $file : $options['Canonicalized_Resource'];

     // validate
    if(array_search($method, $allowedMethods) === false)
    {
        throw new RuntimeException("Method '{$method}' is not allowed");
    }

    if(!$expiration)
    {
        throw new RuntimeException("An expiration date should be provided.");
    }

    return <<<TXT
{$method}
{$contentMd5}
{$contentType}
{$expiration}
{$headers}{$file}
TXT;
    }

function googleSignString($p12FilePath, $string)
{
    $certs = [];

    if (!openssl_pkcs12_read(file_get_contents($p12FilePath), $certs, 'notasecret'))
    {
        echo "Unable to parse the p12 file. OpenSSL error: " . openssl_error_string(); exit();
    }

    $RSAPrivateKey = openssl_pkey_get_private($certs["pkey"]);
    $signed = '';

    if(!openssl_sign( $string, $signed, $RSAPrivateKey, 'sha256' ))
    {
        error_log( 'openssl_sign failed!' );
        $signed = 'failed';
    }
    else $signed = base64_encode($signed);

    return $signed;
}

function googleBuildSignedUrl($serviceEmail, $file, $expiration, $signature)
{
    return "http://storage.googleapis.com{$file}" . "?GoogleAccessId={$serviceEmail}" . "&Expires={$expiration}" . "&Signature=" . urlencode($signature);
}

$serviceEmail = '<EMAIL>';
$p12FilePath = '../../path/to/cert.p12';
$expiration = (new DateTime())->modify('+3hours')->getTimestamp();
$bucket = 'bucket';
$fileToGet = 'picture.jpg';

$file = "/{$bucket}/{$fileToGet}";
$string = googleBuildConfigurationString('GET', $expiration, $file, array("Canonicalized_Extension_Headers" => ''));
$signedString = googleSignString($p12FilePath, $string);
$signedUrl = googleBuildSignedUrl($serviceEmail, $file, $expiration, $signedString);

echo $signedUrl;

Solution 1:

For small files you can use serve option instead of public URL with save-as option set to true. See documentation.

For large files you can use a Signed URL with response-content-disposition parameter.