6

Aim: to upload a file into a folder within Firebase Storage

E.g.

default_bucket/folder1/file1
default_bucket/folder1/file2
default_bucket/folder2/file3

Using Firebase client-side I am able to upload a file to a folder within Firebase Storage like this:

    const storageRef = firebase.storage().ref();
    const fileRef = storageRef.child(`${folder}/${filename}`);
    const metadata = {
      contentType: file.type,
      customMetadata: { }
    };
    return fileRef.put(file, metadata);

If the folder does not exist, it get's created.

However I have not managed to do the same server-side using the Admin SDK.

The code below, uploads the file into the default bucket.

But, I want to upload the file into a named folder within the default bucket.

The client side makes a POST request to the GCF, sending the file and a folder name.

Busboy is used to extra the folder name and file and pass them to the upload function; which uploads the file, then returns a donwnload link for it.

index.js

const task = require('./tasks/upload-file-to-storage');

app.post('/upload', (req, res, next) => {
  try {
    let uploadedFilename;
    let folder;

    if (req.method === 'OPTIONS') {
      optionsHelper.doOptions(res);
    } else if (req.method === 'POST') {
      res.set('Access-Control-Allow-Origin', '*');

      const busboy = new Busboy({ headers: req.headers });
      const uploads = [];

      busboy.on('file', (fieldname, file, filename, encoding, mimetype) => {
        uploadedFilename = `${folder}^${filename}`;

        const filepath = path.join(os.tmpdir(), uploadedFilename);
        uploads.push({ file: filepath, filename: filename, folder: folder });
        file.pipe(fs.createWriteStream(filepath));
      });

      busboy.on('field', (fieldname, val) => {
        if (fieldname === 'folder') {
          folder = val;
        } 
      });

      busboy.on('finish', () => {
        if (uploads.length === 0) {
          res.end('no files found');
        }
        for (let i = 0; i < uploads.length; i++) {
          const upload = uploads[i];
          const file = upload.file;

          task.uploadFile(helpers.fbAdmin, upload.folder, upload.file, uploadedFilename).then(downloadLink => {
            res.write(`${downloadLink}\n`);
            fs.unlinkSync(file);
            res.end();
          });
        }
      });
      busboy.end(req.rawBody);
    } else {
      // Client error - only support POST
      res.status(405).end();
    }
  } catch (e) {
    console.error(e);
    res.sendStatus(500);
  }
});

const api = functions.https.onRequest(app);

module.exports = {
  api
;

upload-file-to-storage.js

exports.uploadFile = (fbAdmin, folder, filepath, filename) => {
  // get the bucket to upload to
  const bucket = fbAdmin.storage().bucket(); //`venture-spec-sheet.appspot.com/${folder}`

const uuid = uuid();
  // Uploads a local file to the bucket
  return bucket
    .upload(filepath, {
      gzip: true,
      metadata: {
        //destination: `/${folder}/${filename}`,
        cacheControl: 'public, max-age=31536000',
        firebaseStorageDownloadTokens: uuid
      }
    })
    .then(() => {
      const d = new Date();
      const expires = d.setFullYear(d.getFullYear() + 50);

      // get file from the bucket
      const myFile = fbAdmin
        .storage()
        .bucket()
        .file(filename);

      // generate a download link and return it
      return myFile.getSignedUrl({ action: 'read', expires: expires }).then(urls => {
        const signedUrl = urls[0];
        return signedUrl;
      });
    });
};

I've tried a few things

Setting the bucket name to default and a folder. This resulted in a server error.

const bucket = fbAdmin.storage().bucket(`${defaultName}/${folder}`); 

Setting the bucket name to the folder. This resulted in a server error.

const bucket = fbAdmin.storage().bucket(folder); 

And, I've also tried using the destination property of uploadOptions. But this still puts the file in the default bucket.

    .upload(filepath, {
      gzip: true,
      metadata: {
        destination: `${folder}/${filename}`, // and /${folder}/${filename}
      }
    })

Is it possible to upload to a folder using the Admin SDK?

E.g. I want to upload a file so that is is placed in a named "folder".

I.e. so I can reference the file at the path: bucket/folder/file.jpg

In the example below, each "folder" is named with a firebase key.

enter image description here

Kildareflare
  • 4,590
  • 5
  • 51
  • 65
  • 1
    I'd expect the last sample to work. What's wrong with it? Maybe you should show the entire function and not just little bits of code, as you could be doing other things wrong. – Doug Stevenson Feb 28 '19 at 19:41
  • There really is much more code than that, but I'll add it just in case. – Kildareflare Mar 01 '19 at 08:23
  • That full code still doesn't look like it defines a Cloud Functions trigger. You could be doing something wrong from the perspective of the trigger itself. MCVE, please. Please also indicate what the code is actually doing, that's different than what you expect. – Doug Stevenson Mar 01 '19 at 19:20
  • @DougStevenson Hi Doug, I've added the full code now and some more context. Thanks for your help on this. – Kildareflare Mar 03 '19 at 12:48
  • You seem to be confused about the difference between a "bucket" and "folder". Buckets have unique names across all of GCS. You can't just make up a new bucket name at any time - you have to create at the console or using gsutil. Also, there is not really such a thing as a "folder" on GCS. There are just files that have path components that look like folders. – Doug Stevenson Mar 03 '19 at 18:38
  • @DougStevenson OK, perhaps I should not have used the word folders, but this is what they are called in the console. I'm aware I cannot change the bucket. But I can, using the client side method, specify a path and have the file end up at that path (See screenshot). And then navigate to it in the console. But I've not been able to replicate this server-side. – Kildareflare Mar 03 '19 at 21:28
  • The `firebaseStorageDownloadTokens` property is not used. Moreover, it should be located at `{metadata:{metadata:{firebaseStorageDownloadTokens:uuid}}}`. – Mason Nov 18 '19 at 12:54

2 Answers2

7

Found the problem. I stupidly declared the destination option in the wrong place.

Instead of in the metadata object:

 return bucket
    .upload(filepath, {
      gzip: true,
      metadata: {
        destination: `${folder}/${filename}`,
        cacheControl: 'public, max-age=31536000',
        firebaseStorageDownloadTokens: uuid
      }
    })

It should have been on the options object:

 return bucket
    .upload(filepath, {
      gzip: true,
      destination: `${folder}/${filename}`,
      metadata: {   
        cacheControl: 'public, max-age=31536000',
        firebaseStorageDownloadTokens: uuid
      }
    })

With this change made the file now gets uploaded into a named "folder".

Kildareflare
  • 4,590
  • 5
  • 51
  • 65
1

There is a create folder option besides Upload File button for a bucket in Storage console.One can create folders in bucket and upload files to it on console. To create such folders in bucket using admin APIs, add folders before file reference. e.g.

const blob = bucket.file('folder1/folder2/' + req.file.originalname);
HatLess
  • 10,622
  • 5
  • 14
  • 32
Chirag Dave
  • 786
  • 6
  • 5