Home > Enterprise >  How to get a list of image URL from firebase storage and upload it to cloud firestore?
How to get a list of image URL from firebase storage and upload it to cloud firestore?

Time:12-19

I am trying to upload multiple image to firebase cloud storage at once and after upload completes it gets the URL for all the images and push it to a list variable call urlsList then upload it to cloud fireStore. Every thing works up until the point where you should store the list of image urls to cloud fireStore.

In firestore it does not show the list of urls instead it shows an empty list, But when I console.log it shows the urlsList with all the urls in it,

Image of my database

Click here to see image of database

Image of console.log

Click here to see image of console.log

Here is an example of my code

  const uplaodImage = async () => {
    const promises = []
    let urlsList = []
    if (files.length > 0) {
      setLoading(true)
      files.map((file) => {
        console.log('loop');
        const storageRef = ref(storage, `juxtaposition/${currentUser?.uid}/${currentJob?.jobID}/${file.name}`);
        const uploadTask = uploadBytesResumable(storageRef, file);
        promises.push(uploadTask)
        uploadTask.on(
          "state_changed",
          (snapshot) => {
            const prog = Math.round(
              (snapshot.bytesTransferred / snapshot.totalBytes) * 100
            );
            setProgressPercent(prog);
          },
          (err) => dispatch(openSnackBar(err.message)),
          async () => {
            await getDownloadURL(uploadTask.snapshot.ref).then((downloadURLs) => {
              urlsList.push(downloadURLs)
              console.log("File available at", downloadURLs);
            });
          }
        );
      })
      Promise.all(promises)
        .then(async () => {
          try {
            console.log(urlsList)
            const collectionRef = doc(db, "prevJobs", currentUser?.uid, "pendingJobs", currentJob?.jobID)
            const deleteRef = doc(db, 'currentJobs', currentUser?.uid, 'yourJobs', currentJob?.jobID)
            await setDoc(collectionRef, {
              ...currentJob,
              juxtapositionImage: urlsList,
              manufacturerID: currentUser?.uid,
              status: 'pending'
            })
            deleteDoc(deleteRef)
          } catch (err) {
            setLoading(false)
            dispatch(openSnackBar(err.message))
          }
        })
        .then(() => {
          setLoading(false)
          closeDialog(false)
          // navigate('/currentjobs', { replace: true })
        })
    } else {
      setLoading(false)
      setError(true)
    }
  }

CodePudding user response:

Overview

Because you are mixing the .on("state_changed", next, error, complete) listener with the the Promise chaining syntax, your code is executing in a different order to what you expect. This is mainly because the chain hasn't been properly linked together.

When an upload completes, the getDownloadURL() method is called - but because of the other uploads, it probably isn't being worked on yet and instead is added to the queue.

Once all three uploads complete, then the Promise.all(promises) chain is executed. At this point, urlsList is still empty which causes the the setDoc() call to upload an empty array.

At this point, each of the getDownloadURL() calls resolve, adding their respective URLs to urlsList.

Now, when you open the console to inspect it, you see the urlsList object, but in its current state, not in the state when it was actually logged. To do that, you need to clone the object as you log it using something like console.log(JSON.parse(JSON.stringify(urlsList))) (it's a poor clone, but serves our purposes fine).

Correcting the Code

To start with, let's refactor these lines to a "fail fast" guard pattern as it will show the consequence right next to the problem:

if (files.length > 0) {
  // lots of lines
} else {
  setLoading(false)
  setError(true)
}

becomes:

if (files.length == 0) {
  setLoading(false)
  setError(true)
  return
}

// lots of lines

Additionally because your code is going to run into other problems when either currentUser?.uid or currentJob?.jobID return undefined, you should add them to the guard too.

if (!currentUser || !currentJob || files.length == 0) {
  setLoading(false);
  setError(true); // <- consider passing a reason here rather than just true
  return;
}
  
// pull out uid & jobID to eliminate currentUser?.uid and currentJob?.jobID everywhere
const { uid } = currentUser;
const { jobID } = currentJob;

Next, let's extract the logic used to upload the file, update progress and gets the download URL when done, in that order, returning a Promise once ALL of that is done.

const uploadTask = uploadBytesResumable(storageRef, file);
promises.push(uploadTask)
uploadTask.on(
  "state_changed",
  (snapshot) => {
    const prog = Math.round(
      (snapshot.bytesTransferred / snapshot.totalBytes) * 100
    );
    setProgressPercent(prog);
  },
  (err) => dispatch(openSnackBar(err.message)),
  async () => { // <- onComplete must be synchronous - async/Promises will be floating
    await getDownloadURL(uploadTask.snapshot.ref).then((downloadURLs) => {
      urlsList.push(downloadURLs)
      console.log("File available at", downloadURLs);
    });
  }
);

So that it uploads the file, updates the progress and gets the download URL, in order, and returns a Promise once ALL of that is done.

// place this outside of your component - saves rebuilding it multiple times
const doFileUpload = async (storageRef, file, setProgressPercent) {
  console.log('Starting '   file.name   ' upload...'); // informational
  const uploadTask = uploadBytesResumable(storageRef, file);
  uploadTask.on(
    "state_changed",
    (snapshot) => {
      const prog = Math.round(
        (snapshot.bytesTransferred / snapshot.totalBytes) * 100
      );
      setProgressPercent(prog);
    },
    (err) => {
      console.error('Upload of '   file.name   ' failed.', err); // informational
      // note: use uploadTask.catch() / doFileUpload.catch() for error handling
    }
  );
  const uploadResult = await uploadTask; // wait for upload to resolve
  const downloadURL = await getDownloadURL(uploadResult.ref);
  setProgressPercent(100); // mark as complete
  console.log('Uploaded '   file.name   ' successfully...'); // informational
  console.log('File available at ', downloadURL);
  return downloadURL;
}

Next, you should update your "move job" operation so that it writes both changes at the same time (this helps the user resubmit the changes if something goes wrong):

const collectionRef = doc(db, "prevJobs", currentUser.uid, "pendingJobs", currentJob.jobID)
const deleteRef = doc(db, 'currentJobs', currentUser.uid, 'yourJobs', currentJob.jobID)
await setDoc(collectionRef, {
  ...currentJob,
  juxtapositionImage: urlsList,
  manufacturerID: currentUser?.uid,
  status: 'pending'
})
deleteDoc(deleteRef)

becomes:

const batch = writeBatch(firestore);

// queue new document
const pendingJobsDocRef = doc(db, 'prevJobs', currentUser.uid, 'pendingJobs', currentJob.jobID)
batch.set(pendingJobsDocRef, {
  ...currentJob,
  juxtapositionImage: urlsList,
  manufacturerID: currentUser.uid,
  status: 'pending'
})

// queue delete document
const yourJobsDocRef = doc(db, 'currentJobs', currentUser.uid, 'yourJobs', currentJob.jobID)
batch.delete(yourJobsDocRef);

// submit the changes
await batch.commit();

Reassembling the pieces, gives:

// Don't forget doFileUpload from above should be placed outside of your component

const uploadImage = () => { // <- note spelling correction
  // fail fast first - handle all your problems here
  if (!currentUser || !currentJob || files.length == 0) {
    setLoading(false);
    setError(true);
    return;
  }
  
  // pull out uid & jobID to eliminate currentUser?.uid and currentJob?.jobID everywhere
  const { uid } = currentUser;
  const { jobID } = currentJob;
  
  setLoading(true);
  
  // upload all files
  const promises = files.map((file, idx) => {
    console.log('Starting upload of files['   idx   ']'); // informational
    const storageRef = ref(storage, `juxtaposition/${uid}/${jobID}/${file.name}`);
    return doFileUpload(storageRef, file, setProgressPercent);
  });
  
  return Promise.all(promises)
    .then((urlsArray) => {
      // move job from currentJobs to prevJobs
      const pendingJobsDocRef = doc(db, 'prevJobs', uid, 'pendingJobs', jobID),
        yourJobsDocRef = doc(db, 'currentJobs', uid, 'yourJobs', jobID);
    
      return writeBatch(firestore)
        .set(pendingJobsDocRef, {
          ...currentJob,
          juxtapositionImage: urlsList,
          manufacturerID: uid,
          status: 'pending'
        })
        .delete(yourJobsDocRef);
        .commit();
    })
    .then(() => {
      // all uploads completed, and database has been updated
      setLoading(false);
      closeDialog(false);
    })
    .catch((err) => {
      // one or more uploads failed
      console.error('One or more uploads failed: ', err); // log for diagnosis rather than just ignoring it
      setLoading(false);
      setError(true);
    })
}
  • Related