i'm using next js 13 with firebase v9. and i'm using a drop zone to upload images. the dropzone returns an array with blob as it's src.
[
{
id: 1
name: "image_processing20220628-4591-yzir35.png"
src: "blob:http://localhost:3000/6e2f33e5-a749-4e9a-b502-d20b8e3f38ca"
}
...
]
the above array is returned from the drop zone. and when i tried to upload to firebase storage it throws an error .
FirebaseError: Firebase Storage: Object 'livingImages/blob:http:/localhost:3000/ca0e3eaf-dbe9-4d77-8053-f4b6d1bd8600' does not exist. (storage/object-not-found)
so how can i upload blob of images to firebase storage?
const imgURL = [];
//this is the images stored inside Redux
const images = useSelector(selectImages);
const storage = getStorage();
images.map(async (file) => {
const storageRef = ref(storage, `livingImages/${file.src}`);
await getDownloadURL(storageRef).then((url) => {
imgURL.push(url);
});
});
const createDocument = () => {
const docRef = doc(db, "livingPosts", session?.user?.email);
const colRef = collection(docRef, "posts");
addDoc(colRef, {
name: "test upload",
images: imgURL,
});
};
the dropzone code
const dispatch = useDispatch();
const images = useSelector(selectImages);
const [files, setFiles] = useState(images == [] ? [] : images);
const {getRootProps, getInputProps} = useDropzone({
onDrop: (acceptedFiles) => {
acceptedFiles.map((file, index) => {
const reader = new FileReader();
reader.onload = async function (e) {
const options = {
maxSizeMB: 5,
maxWidthOrHeight: 1920,
useWebWorker: true,
};
const compressedFile = await imageCompression(file, options);
const tot = parseInt(acceptedFiles.length) + parseInt(files.length);
if (tot > 9) {
alert("select maximum of 9 images");
} else if (parseInt(acceptedFiles.length) > 9) {
alert("maximum images to be selected is 9");
} else if (parseInt(files.length) < 9) {
setFiles((prevState) => [
...prevState,
{
id: index,
src: URL.createObjectURL(compressedFile),
name: file.name,
},
]);
files.map((filename) => {
acceptedFiles.forEach((newFile) => {
if (newFile.name == filename.name) {
alert("a duplicate image is detected");
setFiles(
files,
files.filter((val) => val !== newFile)
);
}
});
});
} else {
alert("something went wrong");
}
};
reader.readAsDataURL(file);
return file;
});
},
})
and the output of the dropzone is
As mentioned in the comments, you'll need the actual File or Blob object to upload the file and not the object URL. You can set the blob in state as shown below:
setFiles((prevState) => [
...prevState,
{
id: index,
src: URL.createObjectURL(compressedFile),
blob: compressedFile, // <-- add blob
name: file.name,
},
]);
Then to upload the files and storing download URLs in Firestore document, try the following function:
import { ref, uploadBytes, getDownloadURL } from "firebase/storage";
import { addDoc } from "firebase/firestore";
const uploadFiles = async () => {
console.log(files);
const promises = files.map((file) => {
const storageRef = ref(storage, `images/${file.name}`);
return uploadBytes(storageRef, file.blob);
});
// upload all files
const res = await Promise.all(promises);
// get download URLs
const links = await Promise.all(res.map((r) => getDownloadURL(r.ref)));
console.log({ links })
// Add Firestore document
const colRef = collection(db, "livingPosts", session?.user?.email, "posts")
const docRef = await addDoc(colRef, {
name: "test",
images: links,
});
console.log("Document written with ID: ", docRef.id);
};
You can call this function on a submit button click or any event when you want to start the upload.
Related
I use lodash clonedeep for uploading files.
I wrote a function that forbids uploading identical files. But if I delete some file after uploading, it still stays in state and I can't upload file with the same name.
What can I do to get the file removed from the state too?
const [files, setFiles] = useState([]);
//state to store uploaded file's name
const [fileNames, setFileNames] = useState([]);
const onSelectFile = (e) => {
try {
let fileArr = cloneDeep(files);
let promises = [];
for (let file of e.target.files) {
promises.push(
new Promise((resolve, reject) => {
const fileName = file.name
//if the file has not been already uploaded
if (!fileNames.includes(fileName)) {
//add the current fileName in state
setFileNames([fileName, ...fileNames]);
const type = file.type;
let reader = new FileReader();
reader.readAsArrayBuffer(file);
reader.onload = function (evt) {
const fileData = evt.target.result;
fileArr.push({
name: fileName,
type: type,
data: fileData,
comment: "",
id: `${new Date().getTime()}_${fileName}`,
canDelete: true
});
if (typeof props.onFileSelected == "function")
props.onFileSelected(fileArr);
resolve(true);
}
reader.onerror = function (evt) {
console.log("error reading file");
reject(false);
}
} else {
alert("File has already been uploaded");
reject(false);
}
})
);
}
Promise.all(promises).then(r => {
setFiles(fileArr);
})
}
catch(e) {
console.log(e);
}
}
I don’t know what to do, it took me 40 hours to think, but I still didn’t understand anything.
From what I can understand from your question, you aren't saving the images on disk. You also haven't included the logic you are using for deleting files. Either way, the implementation is similar.
So when a user deletes a file, assuming they are deleting by filename, we use the filter() method to only keep those that aren't the file we want to delete.
const fileNameToRemove = 'example.txt';
setFiles(files.filter(file=> file.name !== fileNameToRemove));
setFileNames(fileNames.filter(name => name !== fileNameToRemove));
So you will want to do something like this, I haven't used clonedeep like you're in this example, but it's a quick add. I have also moved the read file section into its own function, and am I returning a promise, so I can use async/await within the core upload function.
const [files, setFiles] = useState([]);
const [fileNames, setFileNames] = useState([]);
const readFileAsync = async (file) => {
return new Promise((resolve, reject) => {
const fileReader = new FileReader();
fileReader.readAsArrayBuffer(file);
fileReader.onload = () => {
resolve(fileReader.result);
}
fileReader.onerror = () => {
reject(null);
}
})
}
const onSelectFile = async (e) => {
const uploadingFiles = e.target.files;
for (const file of uploadingFiles) {
const fileName = file.name;
if (fileNames.includes(fileName)) {
console.error("File duplicate");
continue;
}
try {
const fileContentsBuffer = await readFileAsync(file);
setFiles([...files, {
name: fileName,
type: file.type,
data: fileContentsBuffer,
comment: "",
id: `${new Date().getTime()}_${fileName}`,
canDelete: true
}])
setFileNames([...fileNames, fileName]);
} catch (e) {
console.error("Error reading file");
continue;
}
}
}
const deleteFile = (fileName) => {
const fileDataToDelete = files.find((file) => {
return file.name === fileName;
})
if (!fileDataToDelete.canDelete) {
console.error('Can\'t delete file!');
return;
}
setFileNames(fileNames.filter(name => name != fileName));
setFiles(files.filter(file => file.name != fileName && file.canDelete));
}
I made function that upload multiple images to storage and save links to document but i can't figure out how to monitor progress.
const getURLS = async () => {
const promises = [];
images &&
images.map((image) => {
const storageRef = ref(storage, `images/${image?.file?.name + v4()}`);
promises.push(
uploadBytesResumable(storageRef, dataURLtoBlob(image.data_url)).then((uploadResult) => {
return getDownloadURL(uploadResult.ref);
})
);
});
const urls = await Promise.all(promises);
try {
await addDoc(collection(db, 'posts'), {
message: data,
createdAt: serverTimestamp(),
createdBy: user,
likes: [],
comments: [],
images: urls,
}).then(() => {
setData('');
setImages([]);
});
} catch (err) {
console.log(err);
}
};
How to add firebase monitor upload progress to this function?
I tried different ways but it dosn't work
thx for help
The uploadBytesResumable() does not return a promise but an UploadTask. You can iterate over all the images selected and track their progress individually as shown below:
function App() {
const [images, setImages] = useState([])
const [progress, setProgress] = useState([])
const handleFileChange = (e) => {
const files = e.target.files
const newImages = []
for (let i = 0; i < files.length; i++) {
newImages.push(files[i])
}
setImages(newImages)
}
const handleUpload = async () => {
for (let i = 0; i < images.length; i++) {
const image = images[i]
const storageRef = ref(storage, `images/${image.name}`)
const uploadTask = uploadBytesResumable(storageRef, image)
uploadTask.on('state_changed', (snapshot) => {
const progress = (snapshot.bytesTransferred / snapshot.totalBytes) * 100
setProgress((prevProgress) => {
const newProgress = [...prevProgress]
newProgress[i] = progress.toFixed(2)
return newProgress
})
}, (error) => {
console.log(error)
}, async () => {
const imageUrl = await getDownloadURL(uploadTask.snapshot.ref)
// Add to Firestore
})
}
}
return (
<div className="App">
<input type="file" multiple onChange={handleFileChange} />
<button onClick={handleUpload}>Upload</button>
{progress.map((progress, i) => (
<div key={i}>{images[i].name} {progress}%</div>
))}
</div>
)
}
Each file is uploaded separately so you would have to implement some logic to get all URLs together and add in Firestore document like you can call a function when progress of all uploads i.e. all items in progress array become 100.
I'm trying to adapt a React Native project to the new Firebase methods. In it I upload images to Storage and they are added to the App interface. I can also remove these images from the interface as shown in the following code:
const removeImage = (img) => { // delete an image selected by the user
Alert.alert(
"Eliminar imagen",
"¿Estás seguro de eliminar esta imagen?",
[
{
text: "Cancelar",
style: "cancel",
},
{
text: "Eliminar",
onPress: () => {
const result = filter(
formik.values.images,
(image) => image !== img
)
formik.setFieldValue("images", result)
},
},
],
{ cancelable: false }
)
}
The problem is that in this way, they are only removed from my App, while the images are still stored in Firebase. My idea is that when I remove the images from the frontend, they will also be removed from the Firebase Storage.
I have read Firebase documentation, and this would be possible with the deleteObject function
const storage = getStorage();
// Create a reference to the file to delete
const desertRef = ref(storage, 'images/desert.jpg');
// Delete the file
deleteObject(desertRef).then(() => {
// File deleted successfully
}).catch((error) => {
// Uh-oh, an error occurred!
})
I did some test, and I can't get it to work.
I don't know exactly how I should add the Firebase instructions shown here.
How should I implement this function in my code to remove images from Storage?
Thank you
import { getStorage, ref, deleteObject, uploadBytes, getDownloadURL } from "firebase/storage"
export function UploadImagesForm(props) {
const { formik } = props
const [isLoading, setIsLoading] = useState(false) // status for loading
// Function in charge of opening the image gallery
const openGallery = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
})
if (!result.cancelled) {
// console.log('buscando imagenes')
setIsLoading(true) // uploading the image
uploadImage(result.uri)
}
}
// function to upload the images to Firebase
const uploadImage = async (uri) => {
const response = await fetch(uri)
const blob = await response.blob()
const storage = getStorage()
const storageRef = ref(storage, `restaurants/${uuid()}`)
// we go to storage where we want to save the images
uploadBytes(storageRef, blob).then((snapshot) => {
// console.log(snapshot)
updatePhotosRestaurant(snapshot.metadata.fullPath)
})
}
// we take the URL in the previous function and set it in the state of the form
const updatePhotosRestaurant = async (imagePath) => {
const storage = getStorage()
const imageRef = ref(storage, imagePath)
const imageUrl = await getDownloadURL(imageRef) // get the url
// code to upload all images without replacing them
// get the current images and add the new ones with the array
formik.setFieldValue("images", [...formik.values.images, imageUrl])
setIsLoading(false)
}
const removeImage = (img) => { // delete an image selected by the user
Alert.alert(
"Eliminar imagen",
"¿Estás seguro de eliminar esta imagen?",
[
{
text: "Cancelar",
style: "cancel",
},
{
text: "Eliminar",
onPress: () => {
const result = filter(
formik.values.images,
(image) => image !== img
)
formik.setFieldValue("images", result)
},
},
],
{ cancelable: false }
)
}
return (
<>
<ScrollView
style={Styles.viewImage}
horizontal
showsHorizontalScrollIndicator={false}
>
<Icon
type="material-community"
name="camera"
color="#a7a7a7"
containerStyle={Styles.containerIcon}
onPress={openGallery}
/>
{map(formik.values.images, (image) => ( // display the images on the screen
<Avatar
key={image}
source={{ uri: image }}
containerStyle={Styles.imageStyle}
onPress={() => removeImage(image)}
/>
))}
</ScrollView>
<Text style={Styles.error}>{formik.errors.images}</Text>
<LoadingModal show={isLoading} text="Subiendo la imagen" />
</>
)
}
I finally figured out where to implement the deleteObject function in my file to make it all work.
You can delete the images from the Application and Firebase Storage at the same time.
I found a React expert who helped me with this.
As the Firebase documentation says:
To delete a file, first create a reference to that file.
( const imageRef = ref(storage, img ))
Firebase explains it like this:
import { getStorage, ref, deleteObject } from "firebase/storage";
const storage = getStorage();
// Create a reference to the file to delete
const desertRef = ref(storage, 'images/desert.jpg');
Then call the delete() method, (in my case: deleteObject(imageRef) ), for that reference, which will return either a Promise that resolves, or an error if the Promise is rejected.
import { getStorage, ref, deleteObject } from "firebase/storage";
const storage = getStorage();
// Create a reference to the file to delete
const desertRef = ref(storage, 'images/desert.jpg');
// Delete the file
deleteObject(desertRef).then(() => {
// File deleted successfully
}).catch((error) => {
// Uh-oh, an error occurred!
});
I just hope this can help other users who are in my situation learning Firebase
I show. the complete file so that they do not have the doubts that I had, which was the correct place where I should place the Firebase methods
const storage = getStorage()
const imageRef = ref(storage, img)
deleteObject(imageRef).then(() => { // also remove the image from Firebase
console.log("la imagen se elimino");
}).catch((error) => {
console.log("ocurrio un error: ", error)
})
Thanks to #FrankvanPuffelen and #BhavyaKoshiya who tried to help.
import { getStorage, ref, uploadBytes, getDownloadURL, deleteObject } from 'firebase/storage'
import { v4 as uuid } from 'uuid'
import { map, filter } from 'lodash'
import { LoadingModal } from '../../Shared/LoadingModal/LoadingModal'
import Styles from './Styles'
export function UploadImagesForm(props) {
const { formik } = props
const [isLoading, setIsLoading] = useState(false) // status for loading
// Function in charge of opening the image gallery
const openGallery = async () => {
const result = await ImagePicker.launchImageLibraryAsync({
mediaTypes: ImagePicker.MediaTypeOptions.All,
allowsEditing: true,
aspect: [4, 3],
quality: 1,
})
if (!result.cancelled) {
// console.log('buscando imagenes')
setIsLoading(true) // uploading the image
uploadImage(result.uri)
}
}
// function to upload the images to Firebase
const uploadImage = async (uri) => {
const response = await fetch(uri)
const blob = await response.blob()
const storage = getStorage()
const storageRef = ref(storage, `restaurants/${uuid()}`)
// we go to storage where we want to save the images
uploadBytes(storageRef, blob).then((snapshot) => {
// console.log(snapshot)
updatePhotosRestaurant(snapshot.metadata.fullPath)
})
}
// we take the URL in the previous function and set it in the state of the form
const updatePhotosRestaurant = async (imagePath) => {
const storage = getStorage()
const imageRef = ref(storage, imagePath)
const imageUrl = await getDownloadURL(imageRef) // get the url
// code to upload all images without replacing them
// get the current images and add the new ones with the array
formik.setFieldValue("images", [...formik.values.images, imageUrl])
setIsLoading(false)
}
const removeImage = (img) => { // delete an image selected by the user
Alert.alert(
"Eliminar imagen",
"¿Estás seguro de eliminar esta imagen?",
[
{
text: "Cancelar",
style: "cancel",
},
{
text: "Eliminar",
onPress: async () => {
const result = filter(
formik.values.images,
(image) => image !== img
)
formik.setFieldValue("images", result)
**// THIS IS THE CODE I ADDED FROM FIREBASE**
const storage = getStorage()
const imageRef = ref(storage, img)
deleteObject(imageRef).then(() => { // also remove the image from Firebase
console.log("la imagen se elimino");
}).catch((error) => {
console.log("ocurrio un error: ", error)
})
**// END OF THE CODE I ADDED FROM FIREBASE**
},
},
],
{ cancelable: false }
)
}
Great explanation! If anyone has the ref URLs stored in state already, I added the below to my useImages hook and it worked great.
const storage = getStorage();
const deleteImg = (refUrl) => {
const imageRef = ref(storage, refUrl)
deleteObject(imageRef)
.catch((error) => {
console.log("Failed to delete image: ", error)
})
}
You can try this
let imageRef = storage.refFromURL(URL);
imageRef.delete()
I am trying to updload a picture to firebase storage, no problem with that.
However when I try to set the state with the image URL and some other things that I am getting from a form, my variables (section and price) are empty inside the promise. Below my code:
handleForm = e =>{
e.preventDefault();
const {section, price, image} = e.target
const file = image.files[0]
const pictureName = userInformation.name.replace(/ /g, "_");
if(file.size <= 1000000){
const myRoute = '/img/userPictures/' + pictureName;
const storageRef = firebase.storage().ref(myRoute);
const task = storageRef.put(file);
task.on('state_changed', snapshot =>{
console.log('Uploaded');
}, error =>{
console.log(error.message)
}, () =>{
task.snapshot.ref.getDownloadURL().then(downloadURL =>{
this.setState({
information: this.state.data.concat([{
section: section.value,
price: price.value,
image:downloadURL}])
})
});
})
e.currentTarget.reset();
this.notifySuccess('Information uploaded');
}else{
this.notifyError('Image should be less than 1 MB')
}
}
Where do I have the error? thanks!
It's because you are using e.currentTarget.reset() outside the callback.
Try to put it inside on the success of your callback, it should work as expected
(As shown below)
handleForm = e => {
e.preventDefault()
const {section, price, image} = e.target
const file = image.files[0]
const pictureName = userInformation.name.replace(/ /g, '_')
if (file.size <= 1000000) {
const myRoute = '/img/userPictures/' + pictureName
const storageRef = firebase.storage().ref(myRoute)
const task = storageRef.put(file)
task.on(
'state_changed',
snapshot => {
console.log('Uploaded')
},
error => {
console.log(error.message)
},
() => {
task.snapshot.ref.getDownloadURL().then(downloadURL => {
this.setState({
information: this.state.data.concat([
{
section: section.value,
price: price.value,
image: downloadURL
}
])
})
})
e.currentTarget.reset()
this.notifySuccess('Information uploaded')
}
)
} else {
this.notifyError('Image should be less than 1 MB')
}
}
i am working on a cooking android app where firebase is the backend, i need to upload multiple images of a recipe in firebase stoarge and then store the downloadurl into firebase database.
i managed to upload the files into firebase but i am having some trouble to get the downloadUrl of these files.
i have created an array of promises to upload the files and i had created another array to store the url of each file which i get when it finishes the uploading task.
here is my code
var promises = [];
for (var i=0 ;i< prepaImages.length;i++)
{
//alert(prepaImages[i].name);
var storageRef = firebase.storage().ref("receipes"+"/"+category+"/"+title+"/"+uploadTime+prepaImages[i].name );
var uploadTask = storageRef.put(prepaImages[i]);
promises.push(uploadTask);
uploadTask.on('state_changed', snapshot => {
var percentage = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
$("#prepaImageUploadprogress").html(Math.round(percentage)+"%");
$("#prepaImageUploadprogress").attr("style", "width:"+percentage+"%");
}, error => { alert(error) }, () => {
uploadTask.snapshot.ref.getDownloadURL().then(downloadURL => {
//prepaImagesUrl+="+"+downloadURL;
prepaImagesUrl.push(downloadURL);
});
});
the problem is i am getting an array of the length of the number of uploaded files minus one (the legnth it should be equal to the number of uploaded files) and it has the same value (the same downloadurl)
. any help will be appreciated
Thank you.
I think the problem is with the promisies. I suggest you to use Promise.all and await. Therefore your code will be more reliable. Here is my solution to multiple file upload (adapt to your variable names):
const array = Array.from({ length: prepaImages.length }, (value, index) => index);
const uploadedImages = await Promise.all(array.map(async index => {
const image = prepaImages[index];
const metadata = { contentType: image.type };
const storageRef = firebase.storage().ref(`receipes/${category}/${title}/${uploadTime}${prepaImages[i].name}`);
const uploadTask = storageRef.put(image, metadata);
const url = await new Promise((resolve, reject) => {
uploadTask.on('state_changed', snapshot => {
const percentage = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
$('#prepaImageUploadprogress').html(`${Math.round(percentage)}%`);
$('#prepaImageUploadprogress').attr('style', `width: ${percentage}%`);
}, error => reject(error),
async () => {
const downloadUrl = await uploadTask.snapshot.ref.getDownloadURL();
resolve(downloadUrl);
});
});
return { name: image.name, url };
}));
The uploadedImages will contains an array with the image names and download urls. You can make this without await of course, but I prefer this way.
UPDATE:
Here is my own code (without error handling) to achieve this, also, I need to mention that I'm using this with react, redux and using the firebase, firestore wrapper for redux redux-firestore and react-redux-firebase but these are just wrappers:
export const addNewWork = work => async (dispatch, getState, { getFirebase, getFirestore }) => {
const { files, ...restWork } = work;
const firebase = getFirebase();
const firestore = getFirestore();
const storageRef = firebase.storage().ref();
const array = Array.from({ length: files.length }, (value, index) => index);
const uploadedFiles = await Promise.all(array.map(async index => {
const file = files[index];
const metadata = { contentType: file.type };
const uploadTask = storageRef.child(`works/${file.name}`).put(file, metadata);
const url = await new Promise((resolve, reject) => {
uploadTask.on('state_changed', () => {}, error => reject(error), async () => {
const downloadUrl = await uploadTask.snapshot.ref.getDownloadURL();
resolve(downloadUrl);
});
});
return { name: file.name, url };
}));
await firestore.collection('works').add({
...restWork,
image: uploadedFiles[0], // Use only one image for the clean example
createdAt: new Date()
});
});