Getting octet-stream instead of image in node js
Asked Answered
J

1

8

In my front-end I use angular6 and I have this form where you can choose an image either by dropping a file in a div or clicking the div to open a file picker.

The form is

<form [formGroup]="imageUpload" (ngSubmit)="imageUploadSubmitted($event.target)"  > 
  <div id="imageDrop" (click)='imageInput.click()' (drop)="drop($event)" (dragover)="allowDrop($event)" #imageDrop></div> 
  <input type="file"  (change)='imageChange($event)' #imageInput id="imageInput" name = 'imageInput'  accept=".png, .jpg, .jpeg, .gif" formControlName="imageInput"  required  > 
  <button type="submit" >Submit</button>  
</form>

This is the typescript

  selectedFile:File=null;

  allowDrop(e) {
    e.preventDefault();
  }

  drop(e) {    
    e.preventDefault();  
    this.imageUpload.controls.imageInput.reset();  
    this.selectedFile = e.dataTransfer.files[0];
    let input = this.imageUpload.controls.imageInput as any;        
    input.value = e.dataTransfer.files[0];     
  }

  imageChange(e){            
    this.selectedFile = e.target.files[0];
  }

So, when dropping an image, get it from the event and put it in the file input. When the form is submitted, send the form data to a service for posting. The console.log shows a File object (__proto__ : File) whether you picked an image from the file picker, or dropped one in the div.

  imageUploadSubmitted(form){
    console.log('imageInput value - ', this.imageUpload.controls.imageInput.value);           
    this.mapcmsService.uploadImage(form).subscribe((data) =>{
      if(data.success){   alert('all good'); }
      else{ alert('error');  }
    })
  }

The service grabs the form controls and builds a FormData object to send in node.

  uploadImage(data){
    let formData = new FormData(data);
    let headers = new Headers();
    headers.append('Authorization',this.userToken);
    return this.http.post('http://localhost:3000/user/upload/image', formData  ,{headers:headers}  ).pipe(map(res => res.json()));  
  }

In node I use formidable to get the file and save it. This is for testing.

  var form = new formidable.IncomingForm();

  form.parse(req);
  form.on('file', function (name, file){
    console.log('file' , file);
  });

The problem is that if I have chose an image via the file picker, I get a file of type image/jpeg , a name a path and a size.

If I chose an image by drag and drop, I get a file of type application/octet-stream. This has no name and no size.

I would like to get image/jpeg in both cases. I am confused, is this a node or angular issue? How can I fix this ?

Thanks

angular 6 , node 8.11.1, formidable 1.2.1

Jettiejettison answered 24/9, 2018 at 18:36 Comment(2)
Could you isolate this (the client side part only) on some code playground like Codepen? codepen.io/anon/pen/zJgVea (this is an angular template)Muscovite
What is powering that (drop)="drop($event)" function? what code is implementing drop directive?Muscovite
S
3

The issue is that the assignment in the drop event does not actually set the value of the input because file inputs are not supported by Angular reactive forms. I am talking about this lines:

let input = this.imageUpload.controls.imageInput as any;        
input.value = e.dataTransfer.files[0];

So when you drop in your case you are not actually sending the file to the server at all. That is why the data you get is wrong. Here are also links to two other stack overflow questions about reactive forms and file upload where there is more information regarding this issue.

There are two possible solutions to workaround this issue. The first is that you get the ElementRef of the file input by using the ViewChild query. And then assign the files to the native html element directly. The good thing with this approach is that you will see the dropped file name also in the file input. The downside is that this might not work in all browsers. The official documentation on MDN says that it works in modern browsers but for me it did work in Chrome and not in Edge. Here is a sample of the code:

@ViewChild('imageInput') private imageInput: ElementRef;

public drop(e: DragEvent) {    
    e.preventDefault();
    this.imageUpload.controls.imageInput.reset();  
    this.selectedFile = e.dataTransfer.files[0];
    this.imageInput.nativeElement.files = e.dataTransfer.files;
  }

The other approach is that you build the FormData object yourself by adding the selected file yourself in code before sending it to the server. This should work anywhere without issues. Here is a sample code:

let formData = new FormData();
formData.append('imageInput', this.selectedFile);

I have created also a StackBlitz example where you can see all the code.

Secretin answered 29/9, 2018 at 23:35 Comment(2)
Great answer. I knew I can somehow append the file object, but I wanted to avoid it, it felt more right to have one form without appending extra stuff. Anyway, I will append it, since I cannot find any other solution. Problem now is that formidable in node says undefined for both files and fields. I cannot name the append field imageInput, since there is already another one. Deleting a field from the formData is not an option since deleting fields is not supported in all browsers. I put form.multiples = true; in formidable , but still get undefined. Any ideas? Thanks JS stack hates me....Jettiejettison
I would suggest you build a new FormData object like I do in the second sample. And not reuse the original one. If you send also other data with the form you of course need to append all the other values manually too. It is a bit of work but his way you don't get two file entries. Another option is to add the drag drop file with a different name and then on the server check both and use only the one which has correct data.Gambetta

© 2022 - 2024 — McMap. All rights reserved.