HriBB / graphql-server-express-upload

Graphql Server Express file upload middleware
MIT License
41 stars 5 forks source link

Example for handling file on server #4

Open sandervanhooft opened 7 years ago

sandervanhooft commented 7 years ago

@HriBB Do you perhaps have an example how to handle the file on the graphql server?

If I forward it to my REST endpoint, it doesn't seem to be recognised as a file.

My resolver:

...
uploadUserAvatar(root: Function, args: Object) {
      console.log(args.files) // see output below
      Viewer.uploadAvatar(args.token, args.files[0])
      return Viewer.fromToken(args.token)
}
...

Viewer connection:

uploadAvatar(token: string, avatar: Object){
    const options = {
      method: 'POST',
      uri: this.buildRestURL('settings/profile/uploadavatar'),
      headers: {
        'Authorization': `Bearer ${token}`,
      },
      body: {
        avatar,
      },
      json: true,
    }
    return rp(options)
      .then((res) => {
        console.log("\nuploadAvatar SUCCESS:", res)
        return {
          ...res.data,
          token
        }
      })
      .catch((err) => console.log("\nuploadAvatar ERROR:", err))

Console.log(args.files) from resolver:

[ { fieldname: 'files',
       originalname: 'elephant.jpeg',
       encoding: '7bit',
       mimetype: 'image/jpeg',
       destination: '../tmp/',
       filename: '5b4af2fa02e1cfde3995873a2d7ea959',
       path: '../tmp/5b4af2fa02e1cfde3995873a2d7ea959',
       size: 3536 } ] }
HriBB commented 7 years ago

Hmm ... I don't exactly understand your problem. In your case, files[0].path points to the uploaded file, so you can move it to your uploads folder or do whatever with it.

When I find some time, I will make an example app with client & server demonstrating file uploads. Until then ... this is how I handle files on the server:

schema

addSalonProfilePicture(id: Int!, files: [UploadedFile!]!): SalonPicture

mutation

async addSalonProfilePicture(root, { id, files }, context) {
  // must be logged in
  if (!context.user) throw new Error('Must be logged in!')
  // load salon
  const salon = await Salon.find({ where: { id } })
  if (!salon) throw new Error('Salon not found!')
  // upload picture
  const path = await uploadPicture(`salon/${salon.slug}/profile`, files[0])
  // parse uploaded path
  const info = parse(path)
  // build picture data
  const data = {
    salon_id: salon.id,
    type: 'profile',
    filename: `${info.base}`,
    active: true,
    created_on: new Date(),
    created_by: context.user.id,
  }
  // insert into database
  const picture = await SalonPicture.create(data)
  // return picture
  return picture
}

upload

export async function uploadPicture(folder, file, name) {
  if (!file.path || !file.originalname || !file.mimetype) {
    throw new Error('Invalid file parameters!')
  }
  if (config.picture.allowedMimeTypes.indexOf(file.mimetype) === -1) {
    throw new Error('Invalid picture type!')
  }

  const base = `${config.documents.path}/${folder}`
  const finfo = parse(file.originalname)
  const filename = createSlug(name || finfo.name).toLowerCase()
  const path = await generateUniquePath(`${base}/${filename}${finfo.ext}`)

  await move(file.path, path)

  const info = parse(path)
  for (let size in config.picture.sizes) {
    let { width, height, crop, quality } = config.picture.sizes[size]
    let destination = `${base}/${info.name}-${size}${info.ext}`
    if (crop) {
      await cropImage(path, destination, width, height, quality)
    } else {
      await resizeImage(path, destination, width, height, quality)
    }
  }

  return path
}
sandervanhooft commented 7 years ago

@HriBB

Thanks for the quick response, seems like I have to handle file.path as your code suggests.

I'll give this a try tomorrow and let you know! :)

sandervanhooft commented 7 years ago

By the way, what is that parse(...) function, where is it coming from? Are you using a helper library?

HriBB commented 7 years ago
import { parse } from 'path'

https://nodejs.org/api/path.html#path_path_parse_path

HriBB commented 7 years ago

We should create an example app to demonstrate the entire upload flow. Maybe we can use https://github.com/apollostack/react-apollo/tree/master/examples/create-react-app

thebigredgeek commented 7 years ago

You can use MemoryStore for Multer to avoid having to read the file after the fact, as each file object will then contain a Buffer object with the uploaded file contents

sandervanhooft commented 7 years ago

@thebigredgeek Do you have example code for that?

You can use MemoryStore for Multer to avoid having to read the file after the fact, as each file object will then contain a Buffer object with the uploaded file contents

thebigredgeek commented 7 years ago

Just look at multer docs. It's in the readme. This package doesn't care about how the file is shaped

sandervanhooft commented 7 years ago

@thebigredgeek I just did, learned a lot from this issue (parse, multer, fs-extra)....

After a lot of trial and error, I decided to go without the memoryStore/buffer.

In order to pass the file on to the REST endpoint, I had to:

  1. save the file to disk with multer (settings as provided in readme)
  2. move the tmp file to a hashed directory name and rename the file to its original name (I found no way to do this using the memoryStore), as the REST endpoint only accepts image MIMEs. (used fs-extra)
  3. build the REST request (request-promise in this case)
  4. clean up the tmp directory/file (used fs-extra)

Resolver method:

uploadUserAvatar(root: Function, args: Object) {
      const tmpFile = args.files[0]
      const tmpFileParsed = parse(tmpFile.path)
      const tmpAvatarDirPath = `${tmpFileParsed.dir}/avatar-${tmpFileParsed.base}`
      const newTmpFilePath = `${tmpAvatarDirPath}/${tmpFile.originalname}`

      fs.move(tmpFile.path, newTmpFilePath, function (err) {
        if (err) return console.error(err)

        return Viewer.uploadAvatar(args.token, newTmpFilePath).then(
          // cleanup tmpAvatarDir
          fs.remove(tmpAvatarDirPath, function (err) {
            if (err) return console.error(err)
          })
        )
      })
    }

Connector method:

uploadAvatar(token: string, avatarPath: string){
    const options = {
      method: 'POST',
      uri: this.buildRestURL('settings/profile/uploadavatar'),
      headers: {
        'Authorization': `Bearer ${token}`,
        'Accept': 'application/json',
      },
      formData: {
        avatar: fs.createReadStream(avatarPath),
      },
      json: true,
    }
    return rp(options)
      .then((res) => {
        return {
          ...res.data,
          token
        }
      })
      .catch((err) => console.error("\nViewer.uploadAvatar ERROR:", err.message))
  }
}