Загрузка фотографии через Expo в хранилище Firebase (Firebase JS SDK) не работает (преобразование blob)

Вопрос или проблема

Я не уверен, что еще попробовать, и мне нужна помощь в загрузке фотографии, сделанной с помощью 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
  }
}

А теперь список ресурсов, которые я использовал или посты, которые я попробовал (как вы увидите в моем коде)

Я также пробовал это на физическом устройстве 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, чтобы убедиться в универсальности вашего решения.

Оцените материал
Добавить комментарий

Капча загружается...