- Вопрос или проблема
- Ответ или решение
- Загрузка фотографии через Expo в Firebase Storage с использованием Firebase JS SDK
- Шаг 1: Подготовка к загрузке
- Шаг 2: Получение фотографии
- Шаг 3: Загрузка фотографии в Firebase Storage
- Шаг 4: Конвертация URI в Blob
- Шаг 5: Загрузка Blob в Firebase
- Шаг 6: Обработка ошибок
- Заключение
Вопрос или проблема
Я не уверен, что еще попробовать, и мне нужна помощь в загрузке фотографии, сделанной с помощью expo-camera на физическом устройстве Android, работающем под управлением Expo Go, чтобы загрузить ее в Firebase storage (в данный момент эмулируется) с использованием Firebase JS SDK.
Я видел много тем по этому вопросу, но большинство из них устарели или используют React Native Firebase вместо JS SDK. Поскольку мне нужно, чтобы это работало и в вебе, я бы хотел придерживаться JS.
Это работает совершенно нормально с многими подходами, которые вы увидите ниже в вебе.
Без лишних слов, вот мой код. Пожалуйста! Сообщите мне, как правильно конвертировать blob и загрузить его (мне не важно, с помощью uploadBytes, uploadBytesResumable или uploadString…)
Camera.tsx
const takePhoto = async () => {
const photo = await cameraRef.current?.takePictureAsync({
quality: 0.5,
base64: true,
})
if (!photo) {
console.error('Фотография не сделана')
return
}
console.log(photo)
const uploadResult = await uploadPhoto(photo)
console.log(uploadResult)
}
api/uploadPhoto.ts
import { SaveFormat, manipulateAsync } from 'expo-image-manipulator'
import * as FileSystem from 'expo-file-system'
import uuid from 'react-native-uuid'
import {
ref,
uploadBytesResumable,
getDownloadURL,
uploadBytes,
StorageReference,
FirebaseStorage,
uploadString,
} from 'firebase/storage'
import { FIREBASE_DB, FIREBASE_STORAGE } from '@/utils/firebaseConfig'
import { addDoc, collection, serverTimestamp } from 'firebase/firestore'
const MAX_FILE_SIZE_MB = 1
export default async function uploadPhoto(photo) {
console.log('Полученная фотография', photo)
// Создаем ссылку для хранения
const storage = FIREBASE_STORAGE
const storageRef = ref(storage, `photos/${uuid.v4()}`)
console.log('storageRef', storageRef)
// Получаем uri из фотографии
const uri = await getUriFromPhoto(photo)
console.log('Uri фотографии', uri)
try {
// Получаем файл
const file = await fetch(uri.replace('file:///', 'file:/'))
console.log('файл', file)
// Сжимаем файл
const compressedFile = await compressFile(uri)
console.log('сжатый файл', compressedFile)
// Проверяем, меньше ли файл 1MB
const smallerThanMaxSize = await checkSizeIsLessThan(
compressedFile.uri,
MAX_FILE_SIZE_MB
)
if (!smallerThanMaxSize) {
throw new Error('Изображение слишком большое')
} else {
console.log('Файл меньше 1MB')
}
// Создаем blob из файла
const fetchedCompressedFile = await fetch(
compressedFile.uri.replace('file:///', 'file:/')
)
console.log('полученный сжатый файл', fetchedCompressedFile)
// const blob1 = await uriToBlob(fetchedCompressedFile.uri)
// console.log('blob1', blob1)
// const blob2 = await createBlobFromUriXhr(compressedFile)
// console.log('blob2', blob2)
// const blob3 = await createBlobFromUriWorkaround(compressedFile)
// console.log('blob3', blob3)
// Загружаем файл и получаем URL для скачивания
// const downloadUrl = await uploadBlob(storageRef, blob1, {
// contentType: 'image/jpeg',
// })
// console.log('downloadUrl', downloadUrl)
return
// Добавляем URL в Firestore
// const id = await addDownloadUrlToFirestore(photo.filename, downloadUrl)
} catch (uploadError) {
console.error('Ошибка загрузки байтов:', uploadError)
}
}
async function uploadImageAsync(uri) {
// Почему мы используем XMLHttpRequest? См.:
// https://github.com/expo/expo/issues/2402#issuecomment-443726662
const storage = FIREBASE_STORAGE
const blob = await new Promise<Blob>((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.onload = function () {
resolve(xhr.response as Blob)
}
xhr.onerror = function (e) {
console.log(e)
reject(new TypeError('Ошибка сетевого запроса'))
}
xhr.responseType="blob"
xhr.open('GET', uri, true)
xhr.send(null)
})
const storageRef = ref(storage, `photos/${uuid.v4()}`)
const snapshot = await uploadBytes(storageRef, blob)
return await getDownloadURL(snapshot.ref)
}
async function getUriFromPhoto(photo) {
const uri = photo.uri
return uri
}
async function fetchFile(uri: string) {
const response = await fetch(uri)
if (!response.ok) {
throw new Error(
`Не удалось получить файл по uri: ${uri}: response.statusText`
)
}
return response
}
async function compressFile(uri: string) {
try {
const result = await manipulateAsync(
uri,
[
{
resize: {
width: 800,
},
},
],
{
format: SaveFormat.JPEG,
base64: true,
compress: 0.1,
}
)
console.log('Результат сжатия файла:', result)
return result
} catch (error) {
console.error('Ошибка сжатия файла:', error)
throw error
}
}
async function checkSizeIsLessThan(
uri: string,
maxSizeMb: number
): Promise<boolean> {
const fileInfo = await FileSystem.getInfoAsync(uri)
if (!fileInfo.exists) {
throw new Error(`Файл не существует по uri: ${uri}`)
}
return fileInfo.size! < maxSizeMb * 1024 * 1024
}
async function createBlobFromUri(uri: string): Promise<Blob> {
try {
const response = await fetch(uri)
const blob = await response.blob()
console.log('createBlobFromUri blob', blob)
return blob
} catch (error) {
console.error('Не удалось создать blob из URI', error)
throw error
}
}
/**
* Функция для конвертации URI в объект Blob
* @param {string} uri - URI файла
* @returns {Promise} - Возвращает промис, который разрешается с объектом Blob
*/
export function uriToBlob(uri: string): Promise<Blob> {
return new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest()
// Если успешно -> вернуть с blob
xhr.onload = function () {
resolve(xhr.response)
}
// отклонить в случае ошибки
xhr.onerror = function () {
reject(new Error('uriToBlob не удался'))
}
// Установите тип ответа на 'blob' - это означает, что ответ сервера
// будет доступен как двоичный объект
xhr.responseType="blob"
// Инициализируйте запрос. Третий аргумент, установленный в 'true', обозначает
// что запрос является асинхронным
xhr.open('GET', uri, true)
// Отправьте запрос. Аргумент 'null' означает, что для запроса не задано содержимое тела
xhr.send(null)
})
}
async function createBlobFromUriXhr(uri: string): Promise<Blob> {
console.log('createBlobViaXhrAsync uri', uri)
const blob = await new Promise<Blob>((resolve, reject) => {
const xhr = new XMLHttpRequest()
xhr.onload = function () {
resolve(xhr.response as Blob)
}
xhr.onerror = function (e) {
console.log(e)
reject(new TypeError('Ошибка сетевого запроса'))
}
xhr.responseType="blob"
xhr.open('GET', uri, true)
xhr.send(null)
})
console.log('createBlobViaXhrAsync blob', blob)
return blob
}
async function createBlobFromUriWorkaround(uri: string): Promise<Blob> {
const originalUri = uri
const fileName = uri.substring(uri.lastIndexOf("https://stackoverflow.com/") + 1)
// Обходное решение, см. https://github.com/facebook/react-native/issues/27099
const newUri = `${FileSystem.documentDirectory}resumableUploadManager-${fileName}.toupload`
await FileSystem.copyAsync({ from: originalUri, to: newUri })
const response = await fetch(newUri)
const blobData = await response.blob()
const blob = new Blob([blobData], { type: 'image/jpeg' })
console.log('createBlobFromUriWorkaround blob', blob)
return blob
}
async function uploadBlob(
storageRef: StorageReference,
blob: Blob,
metadata?: any
): Promise<string> {
const uploadBytesResponse = await uploadBytes(storageRef, blob, metadata)
console.log('uploadBytesResponse', uploadBytesResponse)
try {
const uploadTask = uploadBytesResumable(storageRef, blob, metadata)
return new Promise((resolve, reject) => {
uploadTask.on(
'state_changed',
(snapshot) => {
const progress =
(snapshot.bytesTransferred / snapshot.totalBytes) * 100
console.log('Загрузка ' + progress + '% завершена')
switch (snapshot.state) {
case 'paused':
console.log('Загрузка приостановлена')
break
case 'running':
console.log('Загрузка идет')
break
}
},
(error) => {
console.error('Ошибка загрузки байтов:', error)
reject(error)
},
() => {
console.log('Загрузка завершена')
getDownloadURL(uploadTask.snapshot.ref)
.then((downloadURL) => {
console.log('Файл доступен по', downloadURL)
resolve(uploadTask.snapshot.ref.fullPath)
})
.catch((error) => {
console.error('Ошибка получения URL для скачивания:', error)
reject(error)
})
}
)
})
} catch (error) {
console.error('Ошибка загрузки байтов:', error)
throw error
}
}
async function addDownloadUrlToFirestore(
fileName: string,
downloadURL: string
) {
try {
const docRef = await addDoc(collection(FIREBASE_DB, 'photos'), {
fileName,
downloadURL,
createdAt: serverTimestamp(),
})
console.log('Документ записан с ID: ', docRef.id)
return docRef.id
} catch (error) {
console.error('Ошибка добавления документа: ', error)
throw error
}
}
А теперь список ресурсов, которые я использовал или посты, которые я попробовал (как вы увидите в моем коде)
- https://firebase.google.com/docs/storage/web/upload-files (конечно)
- https://www.youtube.com/watch?v=KkZckepfm2Q (слишком старо/использует RNF)
- https://github.com/choru-k/React-Native-Tips/tree/master/How_to_upload_photo%2Cfile_in%20react-native (слишком старо/использует RNF)
- Конвертация пути изображения в blob в react native (в основном сосредоточено на RNF)
- Ошибка fetch blob в React Native: Не удалось создать ‘Response’: указанный статус (0) находится вне диапазона [200, 599] (это помогло для загрузки в эмуляторе Android, но также не удается на физическом устройстве)
- Конвертация пути изображения в blob в react native
- Загрузка фотографии с Expo Camera в Firebase Storage неопределена – React Native (уже включено решение здесь – возможно, устарело?)
Я также пробовал это на физическом устройстве iOS, результат такой же (нет).
Ответ или решение
Загрузка фотографии через Expo в Firebase Storage с использованием Firebase JS SDK
Загрузка изображений с использованием Expo и Firebase может быть затруднительной задачей, особенно когда речь идет о конвертации блобов и различных платформах. Рассмотрим, как правильно настроить ваше приложение для загрузки фотографии, сделанной с помощью expo-camera
, на физическом Android-устройстве с использованием Firebase Storage и Firebase JS SDK.
Шаг 1: Подготовка к загрузке
Первым делом убедитесь, что все обязательные библиотеки и плагины установлены. Вы должны иметь установленные следующие зависимости:
firebase
(Firebase JS SDK)expo-image-manipulator
expo-file-system
react-native-uuid
expo-camera
Шаг 2: Получение фотографии
Ваш код для получения фотографии уже в порядке, но стоит убедиться, что вы получаете правильный URI:
const takePhoto = async () => {
const photo = await cameraRef.current?.takePictureAsync({
quality: 0.5,
base64: true,
});
if (!photo) {
console.error('No photo taken');
return;
}
const uploadResult = await uploadPhoto(photo);
console.log(uploadResult);
}
Шаг 3: Загрузка фотографии в Firebase Storage
Теперь разберем функцию uploadPhoto
. Прежде всего, нужно убедиться, что URI корректный и что мы можем получить файл. Проверьте вашу функцию fetchFile
:
async function fetchFile(uri) {
const response = await fetch(uri);
if (!response.ok) {
throw new Error(`Failed to fetch file from uri: ${uri}: response.statusText`);
}
return response;
}
Шаг 4: Конвертация URI в Blob
Для загрузки файла в Firebase необходимо преобразовать URI в Blob. Вам следует использовать XMLHttpRequest
, как вы уже делали в createBlobFromUriXhr
.
async function createBlobFromUriXhr(uri) {
const blob = await new Promise((resolve, reject) => {
const xhr = new XMLHttpRequest();
xhr.onload = function () {
resolve(xhr.response);
};
xhr.onerror = function (e) {
reject(new TypeError('Network request failed'));
};
xhr.responseType = "blob";
xhr.open('GET', uri, true);
xhr.send(null);
});
return blob;
}
Вы можете использовать эту функцию в вашей функции uploadPhoto
, чтобы создать Blob:
const blob = await createBlobFromUriXhr(compressedFile.uri);
Шаг 5: Загрузка Blob в Firebase
Теперь, когда у вас есть Blob, вы можете использовать его для загрузки в Firebase.
const snapshot = await uploadBytes(storageRef, blob, {
contentType: 'image/jpeg',
});
Не забудьте также получить URL загруженного файла:
const downloadURL = await getDownloadURL(snapshot.ref);
console.log('File available at', downloadURL);
Шаг 6: Обработка ошибок
Обратите внимание на то, чтобы корректно обрабатывать ошибки. Вероятно, вам стоит расширить блок catch:
} catch (uploadError) {
if (uploadError instanceof TypeError) {
console.error('Network error:', uploadError.message);
} else {
console.error('Error uploading bytes:', uploadError);
}
}
Заключение
Если вы выполните все шаги выше, ваше приложение должно корректно загружать фотографии в Firebase Storage. Убедитесь, что у вас есть настроенные правила доступа в Firebase Storage, которые позволяют загружать файлы, и проверьте соответствие всех URI и Blob.
Также важно протестировать приложение как на физических Android-устройствах, так и на iOS, чтобы убедиться в универсальности вашего решения.