Wednesday, 9 January 2019

node upload to cloud, download from cloud

project sitehttps://chuanshuoge1-node-upload.herokuapp.com/

upload by button

upload by drag & drop

files uploaded to cloud, stored in binary

download from cloud

files are zipped and downloaded

extract zip folder

delete files on cloud

Concept

upload

files -> front end axios -> node server multer read -> req.files -> convert file to binary buffer -> save on cloud

download

cloud download -> node server jszip -> front end axios -> file-save -> computer

package.json

{
  "name": "upload",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "scripts": {
    "start": "node server.js",
    "dev": "nodemon server.js",
    "heroku-postbuild": "cd client && npm install && npm run build"
  },
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.18.3",
    "express": "^4.16.4",
    "jszip": "^3.1.5",
    "mongoose": "^5.4.2",
    "mongoose-timestamp": "^0.6.0",
    "multer": "^1.4.1"
  }
}

-------------------------------------

client/package.json

{
  "name": "client",
  "version": "0.1.0",
  "private": true,
  "dependencies": {
    "antd": "^3.12.1",
    "axios": "^0.18.0",
    "file-saver": "^2.0.0",
    "react": "^16.7.0",
    "react-dom": "^16.7.0",
    "react-scripts": "2.1.3"
  },
  "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject"
  },
  "eslintConfig": {
    "extends": "react-app"
  },
  "browserslist": [
    ">0.2%",
    "not dead",
    "not ie <= 11",
    "not op_mini all"
  ],
  "proxy": "http://localhost:5000"
}

--------------------------------------------

server.js

const express = require('express')
const bodyParser = require('body-parser')
const multer = require('multer');
const path = require('path');
const mongoose = require('mongoose');
const fileSchema = require('./schema');
const jszip = require('jszip');

const app = express();

//Body parser Middleware
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));

//public folder
//app.use(express.static('./public'));
app.use(express.static(path.join(__dirname, 'client/build')));

const cloudUpload = multer();

//upload to cloud
app.post('/uploadToCloud', cloudUpload.any(), (req, res, next) => {

    if (!req.files) { next('file not found') }

    req.files.map(async (item, index) => {

        const newFile = new fileSchema({
            buffer: item.buffer,
            fileName: item.originalname
        })

        //save file in mongodb
        try {
            await newFile.save();

            if (index === req.files.length - 1) { res.send('ok'); }
        } catch (err) {
            return next(err)
        }
    })
})

app.post('/downloadfile', async (req, res, next) => {
    try {
        const lists = req.body;
        //find files with ids match that in the lists which contains array of ids.
        const files = await fileSchema.find({ '_id': { $in: lists } });
        console.log(files)

        if (files.length === 0) {
            return next('files not found')
        }

        const zip = new jszip();

        files.map(async (item, index) => {

            await zip.file(item.fileName, item.buffer, { base64: true })

            if (index === files.length - 1) {
                zip.generateAsync({ type: "nodebuffer" })
                    .then(function (content) {

                        res.send(content);
                    })
                    .catch(err => {
                        next(err)
                    });
            }

        })


    } catch (err) {
        return next(err)
    }
})

app.get('/searchfile', async (req, res, next) => {
    try {
        //only download id and name, ignore actual file 
        const files = await fileSchema.find({}, 'fileName');

        res.send(files)
    } catch (err) {
        return next(err)
    }
})

//delete all
app.delete('/delete', async (req, res, next) => {

    try {
        const lists = req.body;
        await fileSchema.find({ '_id': { $in: lists } }).remove();

        res.send('deleted')
    } catch (err) {
        return next(err);
    }
});


app.get('*', (req, res) => {
    res.sendFile(path.join(__dirname + '/client/build/index.html'));
});

const port = process.env.PORT || 5000;
app.listen(port, () => {
    const url = "mongodb://xxxxxx@ds149984.mlab.com:49984/upload"
    mongoose.connect(url, {
        useNewUrlParser: true
    });

});

const db = mongoose.connection;

db.on('error', (err) => console.log(err));

db.once('open', () => {
    // require('./routes/userAccount')(app);
    console.log('server started');
});

--------------------------------------------

schema.js

const mongoose = require('mongoose');
const timestamp = require('mongoose-timestamp');

const fileSchema = new mongoose.Schema({
    buffer: {
        type: Buffer,
    },
    fileName: {
        type: String,
    }
})

fileSchema.plugin(timestamp);

const schema = mongoose.model('fileSchema', fileSchema);

module.exports = schema;

--------------------------------------------------

client/app.js

import React, { Component } from 'react';
import './App.css';
import ButtonUpload from './buttonUpload';
import Delete from './delete';
import Download from './download';
import { Row, Col, Checkbox, Icon } from 'antd';
import 'antd/dist/antd.css';
import axios from 'axios';
import DragUpload from './dragUpload';

class App extends Component {
  constructor(props) {
    super(props);

    this.state = {
      files: [],
      downloadCheck: [],
      deleteCheck: [],
    };
  }

  componentWillMount() {
    this.cloudSearch();
  }

  cloudSearch() {
    axios({
      method: 'get',
      url: '/searchfile'
    })
      .then(res => {
        this.setState({ files: res.data })
      })
      .catch(err => {
        console.log(err)
      })
  }

  downloadCheck(e) {
    this.setState({ downloadCheck: e })
  }

  deleteCheck(e) {
    this.setState({ deleteCheck: e })
  }

  render() {



    return (
      <div style={{ padding: '16px' }}>
        <h1 style={{ textAlign: 'center' }}><Icon type="laptop" />  <Icon type="swap" /> <Icon type="global" /> <Icon type="swap" /> <Icon type="database" /> <Icon type="swap" /> <Icon type="global" /> <Icon type="swap" /> <Icon type="cloud" /></h1>
        <Row>
          <Col xs={12} sm={8}>
            <ButtonUpload refresh={() => this.cloudSearch()}></ButtonUpload><br />

            <DragUpload refresh={() => this.cloudSearch()}></DragUpload>

          </Col>
          <Col xs={12} sm={8}>
            <h3>Download Files from Cloud</h3>

            <Checkbox.Group onChange={(e) => this.downloadCheck(e)} style={{ width: '100%' }} >
              <Row>
                {this.state.files.map((item, index) =>
                  <Col key={index}>
                    <Checkbox value={item._id} >{item.fileName}</Checkbox>
                  </Col>
                )}
              </Row>
            </Checkbox.Group><br /><br />

            <Download check={this.state.downloadCheck}></Download><br />

          </Col>
          <Col xs={12} sm={8}>
            <h3>Delete Files on Cloud</h3>

            <Checkbox.Group onChange={(e) => this.deleteCheck(e)} style={{ width: '100%' }} >
              <Row>
                {this.state.files.map((item, index) =>
                  <Col key={index}>
                    <Checkbox value={item._id} >{item.fileName}</Checkbox>
                  </Col>
                )}
              </Row>
            </Checkbox.Group><br /><br />

            <Delete refresh={() => this.cloudSearch()} check={this.state.deleteCheck}></Delete>

          </Col>
        </Row>

      </div>
    );
  }
}

export default App;

---------------------------------------------------

client/buttonUpload.js

import React, { Component } from 'react';
import './App.css';
import 'antd/dist/antd.css';
import { Button, Upload, Icon, message, Progress } from 'antd';
import axios from 'axios';

class ButtonUpload extends Component {
    constructor(props) {
        super(props);

        this.state = {
            fileList: [],
            uploading: false,
            progress: 0,
        };
    }

    handleUpload = () => {
        const { fileList } = this.state;
        const formData = new FormData();
        fileList.forEach((file) => {
            formData.append('files[]', file);
        });

        this.setState({
            uploading: true,
        });

        axios({
            method: 'post',
            url: '/uploadToCloud',
            data: formData,
            headers: { 'Content-Type': 'multipart/form-data', "Access-Control-Allow-Origin": "*" },
            onUploadProgress: progressEvent => {
                const p = parseInt(progressEvent.loaded / progressEvent.total * 99);
                this.setState({ progress: p });
            },
        })
            .then(res => {
                console.log(res.data);
                if (res.data === 'ok') {

                    this.setState({
                        fileList: [],
                        uploading: false,
                        progress: 100,
                    });

                    message.success('upload successful');
                    this.props.refresh();
                }
            })
            .catch((err) => {
                message.error(err.toString());
            })
    }

    render() {

        const { uploading, fileList } = this.state;
        const configs = {
            multiple: true,

            onRemove: (file) => {
                this.setState((state) => {
                    const index = state.fileList.indexOf(file);
                    const newFileList = state.fileList.slice();
                    newFileList.splice(index, 1);
                    return {
                        fileList: newFileList,
                    };
                });
            },

            beforeUpload: (file) => {
                this.setState(state => ({
                    fileList: [...state.fileList, file],
                    progress: 0,
                }));
                return false;
            },

            fileList,

        };

        return (
            <div >
                <Upload {...configs}>
                    <Button>
                        <Icon type="upload" /> Select File
          </Button>
                </Upload>
                <Button
                    type="primary"
                    onClick={this.handleUpload}
                    disabled={fileList.length === 0}
                    loading={uploading}
                    style={{ marginTop: 16 }}
                >
                    {uploading ? 'Uploading' : 'Start Upload'}
                </Button>{' '}
                <Progress type="circle" percent={this.state.progress} width={38} />
            </div>
        );
    }
}

export default ButtonUpload;

----------------------------------------------------------

client/dragUpload.js

import React, { Component } from 'react';
import './App.css';
import 'antd/dist/antd.css';
import { Upload, Icon, message } from 'antd';

const Dragger = Upload.Dragger;

class DragUpload extends Component {
    constructor(props) {
        super(props);

        this.state = {

        };
    }

    onChange(info) {
        const status = info.file.status;
        if (status !== 'uploading') {
            console.log(info.file, info.fileList);
        }
        if (status === 'done') {
            message.success(`${info.file.name} file uploaded successfully.`);

            this.props.refresh();

        } else if (status === 'error') {
            message.error(`${info.file.name} file upload failed.`);
        }
    }

    render() {
        const baseUrl = 'https://chuanshuoge1-node-upload.herokuapp.com' || 'http://localhost:3000';
        const url = baseUrl + '/uploadToCloud'


        return (
            <div style={{ paddingRight: '16px' }}>
                <Dragger multiple={true} action={url} onChange={(e) => this.onChange(e)}>
                    <p className="ant-upload-drag-icon">
                        <Icon type="inbox" />
                    </p>
                    <p className="ant-upload-text">Click or drag file to this area to upload</p>
                    <p className="ant-upload-hint">Support for a single or bulk upload. Strictly prohibit from uploading company data or other band files</p>
                </Dragger>
            </div>
        );
    }
}

export default DragUpload;

---------------------------------------

client/download.js

import React, { Component } from 'react';
import './App.css';
import 'antd/dist/antd.css';
import { Button, message, Progress } from 'antd';
import axios from 'axios';
import saveAs from 'file-saver';

class Download extends Component {
    constructor(props) {
        super(props);

        this.state = {
            progress: 0,
        };
    }


    downloadAxios = () => {
        this.setState({ progress: 0 })
        axios({
            url: '/downloadfile',
            method: 'post',
            data: this.props.check, //array of ids selected by checkbox
            responseType: 'blob', // important
            onDownloadProgress: progressEvent => {
                const p = parseInt(progressEvent.loaded / progressEvent.total * 100);
                this.setState({ progress: p });
            },
        })
            .then(res => {
                const name = Date.now() + '.zip'
                saveAs(res.data, name);

            })
            .catch(err => {
                message.error(err)
            })
    }

    render() {

        return (
            <div  >
                <Button type='dashed' onClick={() => { this.downloadAxios() }} disabled={this.props.check.length === 0 ? true : false}>Download zip</Button>{' '}
                <Progress type="circle" percent={this.state.progress} width={38} />
            </div>
        );
    }
}

export default Download;

--------------------------------------------

client/delete.js

import React, { Component } from 'react';
import './App.css';
import 'antd/dist/antd.css';
import { Button, message } from 'antd';
import axios from 'axios';


class Delete extends Component {
    constructor(props) {
        super(props);

        this.state = {

        };
    }

    delete = () => {
        axios({
            method: 'delete',
            url: '/delete',
            data: this.props.check,
        })
            .then(res => {
                message.success('deleted all')
                this.props.refresh();
            })
            .catch(err => {
                console.log(err)
            })
    }

    render() {



        return (
            <div  >
                <Button type='danger' onClick={() => { this.delete() }} disabled={this.props.check.length === 0 ? true : false}>Delete</Button>
            </div>
        );
    }
}

export default Delete;

--------------------------------------------------

multer interpreted sample

[ { fieldname: 'files[]',
    originalname: 'IMG_20170411_183204.jpg',
    encoding: '7bit',
    mimetype: 'image/jpeg',
    buffer:
     <Buffer ff d8 ff e1 49 b8 45 78 69 66 00 00 49 49 2a 00 08 00 00 00 11 00 0e 01 02 00 20 00 00 00 da 00 00 00 0f 01 02 00 20 00 00 00 fa 00 00 00 10 01 02 00 ... >,
    size: 4731016 },
  { fieldname: 'files[]',
    originalname: 'IMG_20170411_183241.png',
    encoding: '7bit',
    mimetype: 'image/png',
    buffer:
     <Buffer 89 50 4e 47 0d 0a 1a 0a 00 00 00 0d 49 48 44 52 00 00 04 31 00 00 03 5e 08 02 00 00 00 84 6d 02 db 00 00 00 01 73 52 47 42 00 ae ce 1c e9 00 00 00 04 ... >,
    size: 1534256 },
  { fieldname: 'files[]',
    originalname: 'IMG_20170411_183247.jpg',
    encoding: '7bit',
    mimetype: 'image/jpeg',
    buffer:
     <Buffer ff d8 ff e1 49 77 45 78 69 66 00 00 49 49 2a 00 08 00 00 00 11 00 0e 01 02 00 20 00 00 00 da 00 00 00 0f 01 02 00 20 00 00 00 fa 00 00 00 10 01 02 00 ... >,
    size: 4464859 } ]

---------------------------------------

jsziped sample

<Buffer 50 4b 03 04 0a 00 00 00 00 00 8e 9c 29 4e 48 9a d4 db 30 69 17 00 30 69 17 00 17 00 00 00 49 4d 47 5f 32 30 31 37 30 34 31 31 5f 31 38 33 32 34 31 2e ... >

---------------------------------
reference:
http://chuanshuoge2.blogspot.com/2019/01/node-upload-image.html

No comments:

Post a Comment