Deep
Deep's Blog

Deep's Blog

Drag/Drop, Resumable, Multiple File Upload with Progress Bar in Angular and Node

Deep's photo
Deep
ยทJan 23, 2021ยท

10 min read

Subscribe to my newsletter and never miss my upcoming articles

Let's start after seeing this what we gonna make

Alt Text


Features :

  • Drag/drop upload
  • Resumable
  • Multiple uploads
  • progress bar

Let's start now :

Step 1 :

Create project first..

ng new frontend

To generate frontend

then...

For Backend(Node)

mkdir backend
cd backend 
npm init -y

to create backend of our project

Structure :

Root:
  1. Frontend
    • Angular folder structure
  2. Backend
    • package.json
    • index.js
    • name(folder/directory where we will store all the uploaded files)

Note: that name folder in our backend is important don't forget to create it or you will get an error in the filestream.


Step 2 :

###Working on our backend first :

Add this dependecies in your Backend/package.json :

"dependencies": {
    "body-parser": "^1.19.0",
    "cors": "^2.8.5",
    "express": "^4.17.1",
    "fs": "0.0.1-security"
  }

After adding this dependency to your file use npm i to install them :

In Backend/index.js :

const express = require("express");
const bodyParser = require("body-parser");
const cors = require("cors");
const fs = require("fs");

const app = express();
app.use(cors({ origin: "*" }));
app.use(bodyParser.urlencoded({ extended: true }));

const server = app.listen(3000, () => {
  console.log("Started in 3000");
});


let uploads = {};

app.post("/upload", (req, res, next) => {
  let fileId = req.headers["x-file-id"];
  let startByte = parseInt(req.headers["x-start-byte"], 10);
  let name = req.headers["name"];
  let fileSize = parseInt(req.headers["size"], 10);
  console.log("file Size", fileSize, fileId, startByte);
  if (uploads[fileId] && fileSize == uploads[fileId].bytesReceived) {
    res.end();
    return;
  }

  console.log(fileSize);

  if (!fileId) {
    res.writeHead(400, "No file id");
    res.end(400);
  }
  console.log(uploads[fileId]);
  if (!uploads[fileId]) uploads[fileId] = {};

  let upload = uploads[fileId];

  let fileStream;

  if (!startByte) {
    upload.bytesReceived = 0;
    let name = req.headers["name"];
    fileStream = fs.createWriteStream(`./name/${name}`, {
      flags: "w",
    });
  } else {
    if (upload.bytesReceived != startByte) {
      res.writeHead(400, "Wrong start byte");
      res.end(upload.bytesReceived);
      return;
    }
    // append to existing file
    fileStream = fs.createWriteStream(`./name/${name}`, {
      flags: "a",
    });
  }

  req.on("data", function (data) {
    //console.log("bytes received", upload.bytesReceived);
    upload.bytesReceived += data.length;
  });

  console.log("-------------------------------------------");


  //req is a readable stream so read from it 
  //and whatever data we got from reading provide it to 
  // writable stream which is fileStream, so read data from req Stream and then write in fileStream 
  req.pipe(fileStream);


  // when the request is finished, and all its data is written
  fileStream.on("close", function () {
    console.log(upload.bytesReceived, fileSize);
    if (upload.bytesReceived == fileSize) {
      console.log("Upload finished");
      delete uploads[fileId];

      // can do something else with the uploaded file here
      res.send({ status: "uploaded" });
      res.end();
    } else {
      // connection lost, we leave the unfinished file around
      console.log("File unfinished, stopped at " + upload.bytesReceived);
      res.writeHead(500, "Server Error");
      res.end();
    }
  });

  // in case of I/O error - finish the request
  fileStream.on("error", function (err) {
    console.log("fileStream error", err);
    res.writeHead(500, "File error");
    res.end();
  });
});

app.get("/", (req, res) => {
  res.send(
    `<h1 style='text-align: center'>
            <br>Code By <a href="https://github.com/deep1144">Deep<br>
            <b style="font-size: 182px;">๐Ÿ˜ƒ๐Ÿ‘ป</b>
        </h1>`
  );
});

app.get("/status", (req, res) => {
  //console.log('came');
  let fileId = req.headers["x-file-id"];
  let name = req.headers["name"];
  let fileSize = parseInt(req.headers["size"], 10);
  console.log(name);
  if (name) {
    try {
      let stats = fs.statSync("name/" + name);
      if (stats.isFile()) {
        console.log(
          `fileSize is ${fileSize} and already uploaded file size ${stats.size}`
        );
        if (fileSize == stats.size) {
          res.send({ status: "file is present", uploaded: stats.size });
          return;
        }
        if (!uploads[fileId]) uploads[fileId] = {};
        console.log(uploads[fileId]);
        uploads[fileId]["bytesReceived"] = stats.size;
        console.log(uploads[fileId], stats.size);
      }
    } catch (er) {}
  }
  let upload = uploads[fileId];
  if (upload) res.send({ uploaded: upload.bytesReceived });
  else res.send({ uploaded: 0 });
});

with this, we are pretty much done with our backend.


Step 3 :

Now let's start working on our Frontend:

In frontend/ run the following command:

ng add @angular/material

To add angular material theme to our project

I have used pink&blue-gray in my project

In frontend/src/app/app.module.ts paste the following code :

We are using @angular/material for this project

import { BrowserModule } from '@angular/platform-browser';
import { NgModule } from '@angular/core';

import { AppComponent } from './app.component';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';

import {MatProgressSpinnerModule} from '@angular/material/progress-spinner';
import {MatButtonModule} from '@angular/material/button';
import {MatIconModule} from '@angular/material/icon';
import { HttpClientModule } from '@angular/common/http';
import {MatDividerModule} from '@angular/material/divider';
import {MatListModule} from '@angular/material/list';
import {MatProgressBarModule} from '@angular/material/progress-bar';
import {MatCardModule} from '@angular/material/card';

@NgModule({
  declarations: [
    AppComponent
  ],
  imports: [
    BrowserModule,
    BrowserAnimationsModule,
    HttpClientModule,
    MatProgressSpinnerModule,
    MatButtonModule,
    MatIconModule,
    MatDividerModule,
    MatListModule,
    MatProgressBarModule,
    MatCardModule
  ],
  providers: [],
  bootstrap: [AppComponent]
})
export class AppModule { }


Step 4 :

In frontend/src/app/app.component.html paste the following code :

<div>
  <mat-card>

    <mat-card-header class="mb-3">
      <mat-card-title>File Uploads</mat-card-title>
    </mat-card-header>

    <div id="drag_zone" class="file-upload-wrapper" (drop)="dropFiles($event)" (dragover)='dragOverHandler($event)'>Drag
      your thing here</div>

    <mat-list>
      <mat-list-item *ngFor="let file of this.selectedFiles" [class.upload-sucess]="file.uploadCompleted">
        <mat-icon mat-list-icon>note</mat-icon>
        <div mat-line>{{file.fileName}}</div>

        <mat-progress-bar class="mr-4" mode="determinate" value="{{file.uploadedPercent}}"></mat-progress-bar>

        <div>
          <mat-icon style="cursor: grab;margin-top: 2px;" (click)="deleteFile(file)">delete</mat-icon>
        </div>
        <mat-divider></mat-divider>
      </mat-list-item>
    </mat-list>

    <button (click)="uploadFiles()" class="btn btn-primary mt-3" [disabled]="this.selectedFiles.length <=0"> upload
      Files
    </button>


  </mat-card>
</div>

In frontend/src/app/app.component.css paste followings:

#drag_zone{
  width:100%;
  height: 200px;
  margin: auto;
  padding: 8px;
  text-align: center;
  background-color: #f0f0f0;
  border: 2px dashed gray;
  border-radius: 8px;
  color: black;
}

This is our code for our UI :


Step 5 :

This is our last step :

in frontend/src/app/app.component.ts paste the following code :

import { Component } from '@angular/core';
import { HttpClient, HttpHeaders, HttpRequest, HttpEventType } from "@angular/common/http";

@Component({
  selector: 'app-root',
  templateUrl: './app.component.html',
  styleUrls: ['./app.component.css']
})
export class AppComponent {
  title = 'frontend';


  selectedFiles = [];

  constructor(private http: HttpClient){}

  dropFiles(ev) {
    // Prevent default behavior(file from being opened)
    ev.preventDefault();

    if (ev.dataTransfer.items) {
      // Use DataTransferItemList interface to access the file(s)
      for (var i = 0; i < ev.dataTransfer.items.length; i++) {
        // If dropped items aren't files, reject them
        if (ev.dataTransfer.items[i].kind === 'file') {
          let file = ev.dataTransfer.items[i].getAsFile();
          let obj = {
            fileName: file.name,
            selectedFile: file,
            fileId: `${file.name}-${file.lastModified}`,
            uploadCompleted: false
          }
          this.selectedFiles.push(obj);
          console.log('... file[' + i + '].name = ' + file.name);
        }
      }
      this.selectedFiles.forEach(file => this.getFileUploadStatus(file));
    } else {

      for (var i = 0; i < ev.dataTransfer.files.length; i++) {
        console.log('... file[' + i + '].name = ' + ev.dataTransfer.files[i].name);
      }
    }
  }

  dragOverHandler(ev) {
    console.log('File(s) in drop zone');

    // Prevent default behavior (Prevent file from being opened)
    ev.preventDefault();
    ev.stopPropagation();
  }



  getFileUploadStatus(file){
    // fetch the file status on upload
    let headers = new HttpHeaders({
      "size": file.selectedFile.size.toString(),
      "x-file-id": file.fileId,
      'name': file.fileName
    });

    this.http
      .get("http://localhost:3000/status", { headers: headers }).subscribe(
        (res: any) => {
          file.uploadedBytes = res.uploaded;
          file.uploadedPercent = Math.round(100* file.uploadedBytes/file.selectedFile.size);
          if(file.uploadedPercent >= 100){
            file.uploadCompleted = true;
          }
        },err => {
          console.log(err);
        }
      )
  }

  uploadFiles(){
    this.selectedFiles.forEach(file => {
      if(file.uploadedPercent < 100)
        this.resumeUpload(file);
    })
  }

  resumeUpload(file){
    //make upload call and update the file percentage
    const headers2 = new HttpHeaders({
      "size": file.selectedFile.size.toString(),
      "x-file-id": file.fileId,
      "x-start-byte": file.uploadedBytes.toString(),
      'name': file.fileName
    });
    console.log(file.uploadedBytes, file.selectedFile.size, file.selectedFile.slice(file.uploadedBytes).size);

    const req = new HttpRequest('POST', "http://localhost:3000/upload", file.selectedFile.slice(file.uploadedBytes, file.selectedFile.size + 1),{
           headers: headers2,
          reportProgress: true //this will give us percentage of file uploaded
        });

    this.http.request(req).subscribe(
      (res: any) => {

        if(res.type === HttpEventType.UploadProgress){
          console.log("-----------------------------------------------");
          console.log(res);
          file.uploadedPercent = Math.round(100* (file.uploadedBytes+res.loaded)/res.total);
          // Remember, reportProgress: true  (res.loaded and res.total) are returned by it while upload is in progress


          console.log(file.uploadedPercent);
          if(file.uploadedPercent >= 100){
            file.uploadCompleted = true;
          }
        }else{
          if(file.uploadedPercent >= 100){
            file.uploadCompleted = true;
            this.selectedFiles.splice(this.selectedFiles.indexOf(file), 1);
          }
        }
      },
      err => {
        console.log(err)
      }
    )
  }

  deleteFile(file){
    this.selectedFiles.splice(this.selectedFiles.indexOf(file), 1);
  }
}


##Step 6 :

It's time to run our project :

1. In frontend/ run:

ng serve -o

2. In backend/ run:

npm start

to see result : http://localhost:4200/

Done !!!

Notes :

- Read comments for better understanding of code

Github Link : here

Did you find this article valuable?

Support Deep by becoming a sponsor. Any amount is appreciated!

Learn more about Hashnode Sponsors
ย 
Share this