mozi
mozi

Reputation: 147

How to upload images to an auto-generated doc through add() in firestore

I'm creating a dynamic page board.

And the url on this page was created through firestore add().

I want to allow users to upload images when they create posts on this bulletin board.

Is there a way to put an image in the firestore field?

If not, should I make them upload images to storage? If so, can I load the image from the page created through add()?

firestore

Upvotes: 0

Views: 732

Answers (1)

samthecodingman
samthecodingman

Reputation: 26171

Because you have failed to specify a target language, I'm going to assume you are using the JavaScript Web SDK. In every SDK, add(data) is syntactic sugar for doc().set(data). So to generate a form ID we can use in our file uploads, we can use:

const formRef = firebase.firestore().collection("forms").doc();

Ultimately, when storing images in your database, you have two options.

Not recommended: Embed the resources in a Cloud Firestore Document

One of the simplest ways to store a binary file in a document is to make use of a Data URL. These URLs take the binary data, encode it into Base64, add some metadata about the stored data and then produce a string that looks like:

// Data URL for a PNG image of a single #FFFFFF pixel
"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAIAAACQd1PeAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA7DAcdvqGQAAAAMSURBVBhXY/j//z8ABf4C/qc1gYQAAAAASUVORK5CYII="

These data URLs can then be stored in a document (e.g. /forms/{formId}) or in one of its subcollections (e.g. /forms/{formId}/attachments/{attachmentId}). By storing images this way, you inflate the file size by about 25% or more due to Base64 encoding.

Using this method, you gain the ability to control access to the image using Cloud Firestore Security Rules but it also requires that you implement the logic to take the image from the database and insert it into your webpage.

To convert an image that is added to a form to a Data URL, refer to the docs for FileReader#readAsDataURL(). You can also convert a web canvas to a Data URL as shown in this question thread.

function readFileAsDataURL(file) {
  return new Promise((resolve, reject) => {
    const reader = new FileReader();

    reader.addEventListener("load", function () { resolve(this.result) }, false);
    reader.addEventListener("error", function () { resolve(this.error) }, false);

    reader.readAsDataURL(file);
  });
}

async function onSubmitForm(e) {
  const user = firebase.auth().currentUser;

  if (!user) {
    setErrorMessage("You must be signed in!");
    return false;
  }

  const uid = user.uid;
  const files = document.querySelector('input[type=file]').files;
  const dataURLs = files
    ? await Promise.all([].map.call(files, readFileAsDataURL))
    : [];

  // prepare database stuff
  const db = firebase.firestore();
  const batch = db.batch();

  // create form reference
  const formRef = db.collection("forms").doc();

  // assemble & queue upload of each attachment
  const attachmentsColRef = formRef.collection("attachments");
  const attachmentIds = dataURLs.map((url, i) => {
    const file = files[i];
    const attachmentRef = attachmentsColRef.doc();
    const attachmentData = {
      id: attachmentRef.id, // optional
      lastModified: firebase.firestore.Timestamp.fromMillis(file.lastModified),
      name: file.name,
      src: url,
      type: file.type
    }

    batch.set(attachmentRef, attachmentData);
    return attachmentRef.id;
  }

  // assemble form data
  const formData = {
    attachments: attachmentIds,
    content: "this is my first post",
    createdAt: firebase.firestore.FieldValue.serverTimestamp(),
    id: formRef.id, // optional
    name: "john",
    title: "Hello world! I'm John",
    uid,
    updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
    views: 0
  }
  
  // queue upload of form data
  batch.set(formRef, formData);

  // do the upload
  return batch.commit()
    .then(() => {
      console.log(`Successfully created Form #${formRef.id}!`);
      return true;
    })
    .catch((error) => {
      setErrorMessage("Failed to submit form data!");
      console.error(`Failed to create Form #${formRef.id}!`, error);
      return false;
    });
}

Recommended: Store the file in Cloud Storage (or other suitable binary data store)

This part will focus on Google Cloud Storage, but can also be applied to other app-based file storage services (like Amazon Elastic File System, Amazon S3 Buckets, DigitalOcean Spaces, etc.) and consumer-focused file storage services (like Google Drive, OneDrive, Dropbox, WeTransfer, etc.).

Similar to Cloud Firestore, you can store images in this "database of binary objects" and secure it using Cloud Storage Security Rules. However, in contrast to Cloud Firestore, this storage method is optimized for storing binary data in many different forms and offers many focused features that are specific to file handling like restricting uploaded files, resumed uploads, caching information for browsers, retention policies (e.g. delete all files older than 1 year) and file versioning if you wanted to make use of it.

A file stored in Cloud Storage can be accessed through the Cloud Storage and Firebase SDKs, or via a URL (if a file is private, it will require an access token be provided when accessing the URL).

These URLs are usually of the form:

// public file, in Google Cloud Storage
"https://storage.googleapis.com/BUCKET_NAME/OBJECT_NAME"

// private file, with an access token, in Firebase Cloud Storage
"https://firebasestorage.googleapis.com/v0/b/PROJECT_ID.appspot.com/o/OBJECT_NAME.png?alt=media&token=ACCESS_TOKEN"
async function onSubmitForm(e) {
  const user = firebase.auth().currentUser;

  if (!user) {
    setErrorMessage("You must be signed in!");
    return false;
  }

  const uid = user.uid;
  const files = document.querySelector('input[type=file]').files;

  // prepare database stuff
  const db = firebase.firestore();
  const batch = db.batch();

  // create form reference
  const formRef = db.collection("forms").doc();

  // upload and assemble the document for each attachment
  const attachmentsColRef = formRef.collection("attachments");
  const attachmentIds = !files ? [] : await Promise.all(
    [].map.call(files, async (file) => {
      const attachmentRef = attachmentsColRef.doc();

      const storageRef = firebase.storage()
        .ref(`formUploads/${formRef.id}`)
        .child(`attachments/${attachmentRef.id}`)
        .child(uid)
        .child(file.name);

      const uploadResult = await storageRef.put(file);
      const url = await storageRef.getDownloadURL();
      
      const attachmentData = {
        id: attachmentRef.id, // optional
        lastModified: firebase.firestore.Timestamp.fromMillis(file.lastModified),
        name: file.name,
        src: url,
        type: file.type
      }

      batch.set(attachmentRef, attachmentData);
      return attachmentRef.id;
    })
  );

  // assemble form data
  const formData = {
    attachments: attachmentIds,
    content: "this is my first post",
    createdAt: firebase.firestore.FieldValue.serverTimestamp(),
    id: formRef.id, // optional
    name: "john",
    title: "Hello world! I'm John",
    uid,
    updatedAt: firebase.firestore.FieldValue.serverTimestamp(),
    views: 0
  }
  
  // queue upload of form data
  batch.set(formRef, formData);

  // do the upload
  return batch.commit()
    .then(() => {
      console.log(`Successfully created Form #${formRef.id}!`);
      return true;
    })
    .catch((error) => {
      setErrorMessage("Failed to submit form data!");
      console.error(`Failed to create Form #${formRef.id}!`, error);
      return false;
    });
}

Upvotes: 1

Related Questions