1

I'm trying to get the permanent (unsigned) download URL after uploading a file to Google Cloud Storage. I can get the signed download URL using file.createWriteStream() but file.createWriteStream() doesn't return the UploadResponse that includes the unsigned download URL. bucket.upload() includes the UploadResponse, and Get Download URL from file uploaded with Cloud Functions for Firebase has several answers explaining how to get the unsigned download URL from the UploadResponse. How do I change file.createWriteStream() in my code to bucket.upload()? Here's my code:

const {Storage} = require('@google-cloud/storage');
const storage = new Storage({ projectId: 'my-app' });
const bucket = storage.bucket('my-app.appspot.com');
var file = bucket.file('Audio/' + longLanguage + '/' + pronunciation + '/' + wordFileType);

const config = {
  action: 'read',
  expires: '03-17-2025',
  content_type: 'audio/mp3'
};

function oedPromise() {
  return new Promise(function(resolve, reject) {
    http.get(oedAudioURL, function(response) {
        response.pipe(file.createWriteStream(options))
        .on('error', function(error) {
          console.error(error);
          reject(error);
        })
        .on('finish', function() {
          file.getSignedUrl(config, function(err, url) {
            if (err) {
              console.error(err);
              return;
            } else {
              resolve(url);
            }
          });
        });
      });
    });
  }

I tried this, it didn't work:

  function oedPromise() {
    return new Promise(function(resolve, reject) {
      http.get(oedAudioURL, function(response) {
        bucket.upload(response, options)
        .then(function(uploadResponse) {
          console.log('Then do something with UploadResponse.');
        })
        .catch(error => console.error(error));
      });
    });
  }

The error message was Path must be a string. In other words, response is a variable but needs to be a string.

alextru
  • 441
  • 2
  • 12
Thomas David Kehoe
  • 10,040
  • 14
  • 61
  • 100
  • with your existing get.writestream could you just use FS as input to the cloud storage put(file)? http to localfilesys -> cloudStorageRef.put(getFile(file)) its extra io but seems to conform to what various functions expect/ result in . – Robert Rowntree Apr 28 '19 at 18:22
  • What exactly is `config` that you're passing to `getSignedUrl`? Is it whatever getSignedUrl accepts as a first argument? – Doug Stevenson Apr 29 '19 at 01:59
  • Doug, I've edited my question to include `config`. It's copied from the `getSignedUrl` documentation, including the strange `expires` date that implies that signed URLs work for more than seven days. – Thomas David Kehoe Apr 29 '19 at 16:14
  • Robert, that's a good question. Google Cloud Storage uses `file` instead of `fs` because it doesn't have a local file system. The documentation is here: https://cloud.google.com/nodejs/docs/reference/storage/2.5.x/. Looking at other documentation, I see: "You can use the Google Cloud Storage FUSE tool to mount a Cloud Storage bucket to your Compute Engine instance. The mounted bucket behaves similarly to a persistent disk even though Cloud Storage buckets are object storage." https://cloud.google.com/compute/docs/disks/gcs-buckets – Thomas David Kehoe Apr 29 '19 at 16:21
  • Could I use the FUSE tool with `file.createWriteStream()` to save the file to a location that `bucket.upload()` can recognize as a string? That seems like overkill. If `bucket.upload` is a wrapper around `file.createWriteStream`, how does `bucket.upload()` have an `uploadResponse` but `file.createWriteStream` doesn't? – Thomas David Kehoe Apr 30 '19 at 01:47

3 Answers3

2

I used the Google Cloud text-to-speech API to simulate what you are doing. Getting the text to create the audio file from a text file. Once the file was created, I used the upload method to add it to my bucket and the makePublic method to got its public URL. Also I used the async/await feature offered by node.js instead of function chaining (using then) to avoid the 'No such object: ..." error produced because the makePublic method is executed before the file finishes uploading to the bucket.

// Imports the Google Cloud client library
const {Storage} = require('@google-cloud/storage');
// Creates a client using Application Default Credentials
const storage = new Storage();
// Imports the Google Cloud client library
const textToSpeech = require('@google-cloud/text-to-speech');
// Get the bucket
const myBucket = storage.bucket('my_bucket');

// Import other required libraries
const fs = require('fs');
const util = require('util');

// Create a client
const client = new textToSpeech.TextToSpeechClient();
    
// Create the variable to save the text to create the audio file
var text = "";

// Function that reads my_text.txt file (which contains the text that will be
// used to create my_audio.mp3) and saves its content in a variable.
function readFile() {
    // This line opens the file as a readable stream
    var readStream = fs.createReadStream('/home/usr/my_text.txt');

    // Read and display the file data on console
    readStream.on('data', function (data) {
        text = data.toString();
    });

    // Execute the createAndUploadFile() fuction until the whole file is read
    readStream.on('end', function (data) {
        createAndUploadFile();
    });
}

// Function that uploads the file to the bucket and generates it public URL.
async function createAndUploadFile() {
    // Construct the request
    const request = {
        input: {text: text},
        // Select the language and SSML voice gender (optional)
        voice: {languageCode: 'en-US', ssmlGender: 'NEUTRAL'},
        // select the type of audio encoding
        audioConfig: {audioEncoding: 'MP3'},
    };

    // Performs the text-to-speech request
    const [response] = await client.synthesizeSpeech(request);
    // Write the binary audio content to a local file
    const writeFile = util.promisify(fs.writeFile);
    await writeFile('my_audio.mp3', response.audioContent, 'binary');
    console.log('Audio content written to file: my_audio.mp3');

    // Wait for the myBucket.upload() function to complete before moving on to the
    // next line to execute it
    let res = await myBucket.upload('/home/usr/my_audio.mp3');
    // If there is an error, it is printed
    if (res.err) { 
        console.log('error'); 
    }
    // If not, the makePublic() fuction is executed
    else { 
        // Get the file in the bucket
        let file = myBucket.file('my_audio.mp3');
        file.makePublic(); 
    }
}


readFile();

SSoulMiles
  • 88
  • 9
0

bucket.upload() is a convenience wrapper around file.createWriteStream() that takes a local filesystem path and upload the file into the bucket as an object:

bucket.upload("path/to/local/file.ext", options)
  .then(() => {
    // upload has completed
  });

To generate a signed URL, you'll need to get a file object from the bucket:

const theFile = bucket.file('file_name');

The file name will either be that of your local file, or if you specified an alternate remote name options.destination for the file on GCS.

Then, use File.getSignedUrl() to get a signed URL:

bucket.upload("path/to/local/file.ext", options)
  .then(() => {
    const theFile = bucket.file('file.ext');
    return theFile.getSignedURL(signedUrlOptions); // getSignedURL returns a Promise
  })
  .then((signedUrl) => {
    // do something with the signedURL
  });

See:

Bucket.upload() documentation

File.getSignedUrl() documentation

  • Thanks for responding. I don't want the signed download URL because signed download URLs expire in seven days. I want the permanent, unsigned download URL. – Thomas David Kehoe Apr 30 '19 at 01:43
0

You can make a specific file in a bucket publicly readable with the method makePublic.

From the docs:

const {Storage} = require('@google-cloud/storage');
const storage = new Storage();

// 'my-bucket' is your bucket's name
const myBucket = storage.bucket('my-bucket');

// 'my-file' is the path to your file inside your bucket
const file = myBucket.file('my-file');

file.makePublic(function(err, apiResponse) {});

//-
// If the callback is omitted, we'll return a Promise.
//-
file.makePublic().then(function(data) {
  const apiResponse = data[0];
});

Now the URI http://storage.googleapis.com/[BUCKET_NAME]/[OBJECT_NAME] is a public link to the file, as explained here.

The point is that you only need this minimal code to make an object public, for instance with a Cloud Function. Then you already know how the public link is and can use it directly in your app.

alextru
  • 441
  • 2
  • 12
  • `file.makePublic()` produces an error, "No such object:` and my bucket_name/object_name. This is one promise after `file.createWriteStream()`, so `file` hasn't changed. I'm not sure how it can't find the object it just made. I also tried `predefinedAcl: 'authenticatedRead'` and `predefinedAcl: 'publicRead'`, these produce `403 (Forbidden)` errors. I'm not getting `404 (Not Found)` errors so the URI appears to be good. – Thomas David Kehoe May 02 '19 at 03:05
  • Can you check if your file and bucket object references are ok? It looks like you pass an url to storage.bucket(), is it the bucket name? I suggest you try to see if the minimal example code, with minimal changes, works for you. – alextru May 02 '19 at 09:53
  • I just opened a new question with these problems: https://stackoverflow.com/questions/55957920/google-cloud-storage-predefinedacl-and-file-makepublic-not-working – Thomas David Kehoe May 02 '19 at 18:04