如何将文件从后端 (Heroku) 上传到 github 上托管的 (Netlify) 的前端

How to upload files from a backend (Heroku) to frontend in (Netlify) hosted on github

我开发了一个上传到 Github 的应用程序,我使用 Heroku 从 Github 使用(自动部署)托管(后端文件夹),还使用 ​​Netlify 托管(前端文件夹)它在我的本地计算机上运行良好,但是当我尝试从前端的表单上传文件时,它会向后端发送请求,后端会自行将文件保存到位于前端目录中的 /uploads 文件夹中。

我的文件结构是这样的:

[Server]
- controllers
- - food.js
[Client]
- public
-- uploads

- src
-- pages
--- dashboard
---- food
----- AddFood.js

它在本地主机上运行良好,这是我的代码: (客户) AddFood.js:

import { useState, useEffect } from 'react'
import { Link } from 'react-router-dom'
import Axios from 'axios'

import useDocumentTitle from '../../../hooks/useDocumentTitle'

import Modal from '../../../components/Modal/Modal'
import { Success, Error, Loading } from '../../../components/Icons/Status'

import { createSlug } from '../../../functions/slug'
import goTo from '../../../functions/goTo'

const AddFood = () => {
  useDocumentTitle('Add Food')

  //Form States
  const [foodName, setFoodName] = useState('')
  const [foodPrice, setFoodPrice] = useState('')
  const [foodDesc, setFoodDesc] = useState('')

  const [foodFile, setFoodFile] = useState('')
  const [preview, setPreview] = useState()

  const [addFoodStatus, setAddFoodStatus] = useState()
  const [addFoodMessage, setAddFoodMessage] = useState()

  //Form errors messages
  const ImgErr = document.querySelector('[data-form-img-msg]')
  const foodNameErr = document.querySelector('[data-form-name-msg]')
  const priceErr = document.querySelector('[data-form-price-msg]')
  const descErr = document.querySelector('[data-form-desc-msg]')
  const formMsg = document.querySelector('[data-form-msg]')

  const modalLoading = document.querySelector('#modal')
  const BASE_URL =
    process.env.NODE_ENV === 'development'
      ? process.env.REACT_APP_API_LOCAL_URL
      : process.env.REACT_APP_API_URL

  const updateFoodImg = e => {
    const file = e.target.files[0]

    if (file) {
      const fileType = file.type.split('/')[0]
      if (fileType === 'image') setFoodFile(file)

      const fileSizeToMB = file.size / 1000000
      const MAX_FILE_SIZE = 1 //mb

      if (fileSizeToMB > MAX_FILE_SIZE) {
        if (ImgErr)
          ImgErr.textContent = `file size can't be more than ${MAX_FILE_SIZE} MB`
      } else {
        ImgErr.textContent = ''
      }
    }
  }

  useEffect(() => {
    // if there's an image
    if (foodFile) {
      const reader = new FileReader()

      reader.onloadend = () => setPreview(reader.result)

      reader.readAsDataURL(foodFile)
    } else {
      setPreview(null)
    }
  }, [foodFile])

  const handleAddFood = async e => {
    e.preventDefault()

    //using FormData to send constructed data
    const formData = new FormData()
    formData.append('foodName', foodName)
    formData.append('foodPrice', foodPrice)
    formData.append('foodDesc', foodDesc)
    formData.append('foodImg', foodFile)

    if (
      ImgErr.textContent === '' &&
      foodNameErr.textContent === '' &&
      priceErr.textContent === '' &&
      descErr.textContent === ''
    ) {
      //show waiting modal
      modalLoading.classList.remove('hidden')

      try {
        const response = await Axios.post(`${BASE_URL}/foods`, formData)

        const { foodAdded, message } = response.data
        setAddFoodStatus(foodAdded)
        setAddFoodMessage(message)
        //Remove waiting modal
        setTimeout(() => {
          modalLoading.classList.add('hidden')
        }, 300)
      } catch (err) {
        formMsg.textContent = `Sorry something went wrong ${err}`
      }
    } else {
      formMsg.textContent = 'please add all details'
    }
  }

  return (
    <>
      {addFoodStatus === 1 ? (
        <Modal
          status={Success}
          msg='Added food'
          redirectLink='menu'
          redirectTime='3000'
        />
      ) : addFoodStatus === 0 ? (
        <Modal
          status={Error}
          msg={addFoodMessage}
          msg=''
        />
      ) : null}

      <section className='py-12 my-8 dashboard'>
        <div className='container mx-auto'>
          <h3 className='mx-0 mt-4 mb-12 text-2xl text-center'>Add food</h3>
          <div>
            <div className='food'>
              {/* Show Modal Loading when submitting form */}
              <Modal
                status={Loading}
                modalHidden='hidden'
                classes='text-blue-500 text-center'
                msg='Please wait'
              />

              <form
                method='POST'
                className='form'
                encType='multipart/form-data'
                onSubmit={handleAddFood}
              >
                <label className='flex flex-wrap items-center justify-center gap-4 mb-8 sm:justify-between'>
                  <img
                    src={
                      preview === null
                        ? 'https://source.unsplash.com/random?food'
                        : preview
                    }
                    alt='food' //change with food image name
                    className='object-cover p-1 border border-gray-400 w-28 h-28 dark:border-gray-300 rounded-xl'
                  />
                  <input
                    type='file'
                    name='foodImg'
                    id='foodImg'
                    accept='image/*'
                    onChange={updateFoodImg}
                    className='grow-[.7] cursor-pointer text-lg text-white p-3 rounded-xl bg-orange-800 hover:bg-orange-700 transition-colors'
                    required
                  />
                  <span
                    className='inline-block my-2 text-red-400 font-[600]'
                    data-form-img-msg
                  ></span>
                </label>

                <label htmlFor='foodName' className='form-group'>
                  <input
                    type='text'
                    id='foodName'
                    className='form-input'
                    autoFocus
                    required
                    onChange={e => setFoodName(createSlug(e.target.value.trim()))}
                  />
                  <span className='form-label'>Food Name</span>
                  <span
                    className='inline-block my-2 text-red-400 font-[600]'
                    data-form-name-msg
                  ></span>
                </label>

                <label htmlFor='foodPrice' className='form-group'>
                  <input
                    type='number'
                    id='foodPrice'
                    className='form-input'
                    min='5'
                    max='500'
                    required
                    onChange={e => setFoodPrice(e.target.value.trim())}
                  />
                  <span className='form-label'>Price</span>
                  <span
                    className='inline-block my-2 text-red-400 font-[600]'
                    data-form-price-msg
                  ></span>
                </label>

                <label htmlFor='foodDescription' className='form-group'>
                  <textarea
                    name='foodDescription'
                    id='foodDescription'
                    minLength='10'
                    maxLength='300'
                    className='form-input'
                    required
                    onChange={e => setFoodDesc(e.target.value.trim())}
                  ></textarea>
                  <span className='form-label'>Description</span>
                  <span
                    className='inline-block my-2 text-red-400 font-[600]'
                    data-form-desc-msg
                  ></span>
                </label>

                <div
                  className='my-14 text-red-400 font-[600] text-center text-xl'
                  data-form-msg
                ></div>

                <div className='flex items-center justify-evenly'>
                  <button
                    type='submit'
                    className='min-w-[7rem] bg-green-600 hover:bg-green-700 text-white py-1.5 px-6 rounded-md'
                  >
                    Add
                  </button>
                  <Link
                    to={goTo('menu')}
                    className='text-gray-800 underline-hover text-bold dark:text-white'
                  >
                    Food Menu
                  </Link>
                </div>
              </form>
            </div>
          </div>
        </div>
      </section>
    </>
  )
}

export default AddFood

(服务器) foods.js:

const FoodsModel = require(`${__dirname}/../models/food-model.js`)
const { v4: uuidv4 } = require('uuid')
const sharp = require('sharp')
const deleteFile = require('../functions/deleteFile')

const addFood = async (req, res) => {
  const { foodName, foodPrice, foodDesc } = req.body
  const { foodImg } = req.files
  const foodImgName = uuidv4() + foodImg.name
  const foodImgMovePath = `${__dirname}/../../client/public/uploads/${foodImgName}.webp`
  const foodImgDisplayPath = `/uploads/${foodImgName}`

  const foods = new FoodsModel({
    foodImgDisplayPath,
    foodName,
    foodPrice,
    foodDesc
  })

  sharp(foodImg.data)
    .rotate()
    .resize(200)
    .jpeg({ mozjpeg: true, quality: 50 })
    .toBuffer()
    .then(newBuffer => {
      //changing the old jpg image buffer to new webp buffer
      foodImg.data = newBuffer

      foodImg.mv(foodImgMovePath, err => {
        if (err) {
          res.json({
            message: `Sorry something wrong with server! : ${err}`
          })
          return
        }

        foods.save()

        res.json({
          message: 'Food Added Successfully',
          foodAdded: 1
        })
      })
    })
    .catch(err => {
      res.json({
        //https://mhmdhidr-restaurant.netlify.app/uploads/20cc09a0-1811-48b0-bffa-49e7a1981537chicken-legs.webp
        message: `Sorry! Something went wrong, check the error => : \n ${err}`,
        foodAdded: 0
      })
    })
}

const getFood = async (req, res) => {
  res.json(res.paginatedResults)
}

const deleteFood = async (req, res) => {
  const { prevFoodImg } = req.body
  const { foodId } = req.params

  deleteFile(prevFoodImg)

  try {
    await FoodsModel.findByIdAndRemove(foodId)
    res.json({
      message: 'Food Deleted Successfully',
      foodDeleted: 1
    })
  } catch (error) {
    res.json({
      message: `Sorry! Something went wrong, check the error => : \n ${error}`,
      foodDeleted: 0
    })
  }
}

const updateFood = async (req, res) => {
  const { foodName, foodPrice, foodDesc, prevFoodImg } = req.body
  const { foodId } = req.params

  const { foodImg } = req.files || ''
  const foodImgName = uuidv4() + foodImg?.name || ''
  const foodImgMovePath = `${__dirname}/../../client/public/uploads/${foodImgName || ''}`
  const foodImgDisplayPath =
    foodImg !== '' && foodImg !== undefined ? `/uploads/${foodImgName}` : prevFoodImg

  try {
    await FoodsModel.findByIdAndUpdate(foodId, {
      foodImgDisplayPath,
      foodName,
      foodPrice,
      foodDesc
    })

    if (foodImg !== '' && foodImg !== undefined) {
      deleteFile(prevFoodImg)

      foodImg.mv(foodImgMovePath, err => {
        if (err) {
          res.json({ message: `Sorry something wrong with server! : ${err}` })
        }
      })
    }

    res.json({
      message: 'Food Updated Successfully',
      foodUpdated: 1
    })
  } catch (error) {
    res.json({
      message: `Sorry! Something went wrong, check the error => : \n ${error}`,
      foodUpdated: 0
    })
  }
}

module.exports = { addFood, getFood, deleteFood, updateFood }

但是当我尝试在 Netlify 应用程序中上传文件时出现此错误:

Error: ENOENT: no such file or directory, open '/app/controllers/../../client/public/uploads/9631bb96-e41d-4c9a-aa35-d22b551ab662MASHAWI-IN-DUBAI.jpeg.webp'

我尝试了很多Google,但很遗憾没有找到解决方案。

感谢您的帮助。

两部分答案:

  • 您的 back-end 没有必要将文件放入您的 front-end 的目录结构中。

    更好的选择可能是在 back-end 项目中使用 uploads/ 文件夹,通过 HTTPS 公开这些文件夹,然后 linking从你的 front-end.

    给他们
  • 但这在 Heroku 上不起作用,因为它 ephemeral filesystem

    甚至 更好 的选择是将它们保存到 cloud-based 对象存储,如 Amazon S3 或 Azure Blob Storage,或者更专业的服务,如 Cloudinary,如果它们'是图像。 Heroku 倾向于 recommend S3.

    您的 back-end 现在只需要将 URL 存储到每个文件,并根据要求将 link 提供给您的 front-end。

    即使在允许您将文件保存到 back-end 文件系统的其他主机上,使用 third-party 服务也有很多好处。您可以简单地水平扩展(添加新节点),您的应用程序变得不那么有状态,等等。

用户上传的内容永远不属于您的代码存储库,无论您选择如何以及在何处托管它们。它们是 内容,不是代码,不应与您的代码一起进行跟踪和版本控制。