Search code examples
reactjsfile-uploadgraphqlapolloapollo-client

Files bigger than 16 kb gets corrupted when uploaded, using apollo and graphql


Think i tried everything, but nothing seems to work. I have a React app, and i want to upload pictures to a folder. I am using Apollo server and Graphql.

The weird thing is that if the file is less than 17 kb, it works!. But anything bigger than that, the file gets corrupted. I can see the image file in the folder but it is 0kb, and when i try to open it it says that the image contains errors". I don't have a limit to size of items.

The resolver: I tried using a different method of doing this but i got the same issue, so i don't believe this is the issue.

const bcrypt = require('bcryptjs');
const jwt = require('jsonwebtoken');
const { UserInputError } = require('apollo-server');
const path = require('path');
const fs = require('fs');

async uploadFile(_, { file }) {
      const { createReadStream, filename, mimetype, encoding } = await file;
       
      const stream = createReadStream();
      const pathName = path.join(__dirname, "../../public/images/", filename);
      await stream.pipe(fs.createWriteStream(pathName));

      return {
        url: `http://localhost:5000/images/${filename}`,
      }
    }  

Type definitions:

    type File {
    url: String!
    filename: String!
    mimetype: String!
    encoding: String!
  }
    type Mutation {
    uploadFile(file: Upload!): File!
  }

The Apollo provider:

import React from 'react';
import App from './App';
import ApolloClient from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
/* import { createHttpLink } from 'apollo-link-http'; */
import { ApolloProvider } from '@apollo/react-hooks';
import { setContext } from 'apollo-link-context';
import { createUploadLink } from 'apollo-upload-client';


const uploadLink = createUploadLink({
  uri: 'http://localhost:5000/graphql'
});

const authLink = setContext(() => {
  const token = localStorage.getItem('jwtToken');
  return {
    headers: {
      Authorization: token ? `Bearer ${token}` : ''
    }
  };
});

const client = new ApolloClient({
  link: authLink.concat(uploadLink),
  cache: new InMemoryCache(),
  onError: ({ networkError, graphQLErrors }) => {
    console.log( 'graphQLErrors', graphQLErrors)
    console.log( 'networkError', networkError)
  }
});

export default (
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

The Index.js on the server:

const { ApolloServer, PubSub } = require('apollo-server-express');
const express = require('express');
const cors = require('cors');
const mongoose = require('mongoose');

const typeDefs = require('./graphql/typeDefs');
const resolvers = require('./graphql/resolvers');
const { MONGODB } = require('./config.js');

const pubsub = new PubSub();

const PORT = process.env.PORT || 5000;

const server = new ApolloServer({
  typeDefs,
  resolvers,
  context: ({ req }) => ({ req, pubsub })
});

const app = express();
server.applyMiddleware({ app });

app.use(express.static('public'));
app.use(cors());

mongoose
  .connect(MONGODB, { useNewUrlParser: true })
  .then(() => {
    console.log('MongoDB Connected');
    return app.listen({ port: PORT }, () => {
      console.log(`Server running at http://localhost:5000/ Graphql Playground at http://localhost:5000/graphql `);
    });
  })
  .catch(err => {
    console.error(err)
  })

I tried doing the input form differently but i got the same issue so i don't think the issue is here, but i don't know.

import React, { useCallback } from 'react';
import { useDropzone } from 'react-dropzone';
import { useMutation, gql } from '@apollo/client';

function UploadForm() {

    const [uploadFile] = useMutation(UPLOAD_FILE_MUTATION, {
        onCompleted: data => console.log(data),
    });

    const onDrop = useCallback(
        ([file]) => {
            uploadFile({ variables: { file } });
        },
        [uploadFile]
    );
    const { getRootProps, getInputProps, isDragActive } = useDropzone({ onDrop });

    return (
        <div {...getRootProps()}>
            <input {...getInputProps()}/>
            {isDragActive ? (
                <p>Drop the files here!</p>
            ) : (
                <p>Drag and drop files here, or click to select files</p>
            )}
        </div>
    )
}

const UPLOAD_FILE_MUTATION = gql`
    mutation uploadFile($file: Upload!){
        uploadFile(file: $file){
            url
        }
    }
`

export default UploadForm;

I suspect the issue is in the index.js or the Apollo provider, but i really don't know. Thanks.


Solution

  • "async/await only works with promises, not with streams" - xadm

    Solution:

    Changed the resolver to this:

    const path = require('path');
    const fs = require('fs');
    
    const Image = require('../../models/Image');
    
    const files = [];
      
      module.exports = {
    
        Mutation: {
        async uploadProfilePic(_, { file }, context) {
          const { createReadStream, filename, mimetype, encoding } = await file;
    
          const { ext } = path.parse(filename);
          const randomName = generateRandomString(12) + ext;
    
            files.push(randomName);
    
            console.log(file);
    
            const storeUpload = async ({ stream, filename, mimetype, encoding }) => {
            
                const path = `public/images/${randomName}`
            
                return new Promise((resolve, reject) =>
                stream
                    .pipe(fs.createWriteStream(path))
                    .on("finish", () => resolve({ 
                    category: "profilePic",
                    url: `http://localhost:5000/images/${randomName}`, 
                    path, 
                    filename: randomName, 
                    mimetype,
                    encoding,
                    createdAt: new Date().toISOString(),
                    commentsCount: 0,
                    likesCount: 0,
                    sharesCount: 0
                    }))
                    .on("error", reject)
                );
            };
    
            const processUpload = async (upload) => {
              const { createReadStream, filename, mimetype, encoding } = await upload;
              const stream = createReadStream();
              const file = await storeUpload({ stream, filename, mimetype, encoding });
              return file;
            };
    
            const upload = await processUpload(file);
    
            
            return {
              url: `http://localhost:5000/images/${randomName}`,
            };
          
        }
      }
    };