I have a working upload backend with Postman. But I can't make it work from angular.
Backend code:
package com.riaadvisory.dih.web.api.file;
import static java.nio.file.Files.copy;
import static java.nio.file.Paths.get;
import static java.nio.file.StandardCopyOption.REPLACE_EXISTING;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;
import com.esotericsoftware.minlog.Log;
@RestController
@RequestMapping(value = "/files")
public class FileUploadService {
@PostMapping(value = "/upload", consumes = MediaType.ALL_VALUE)
public ResponseEntity<List<String>> uploadFiles(@RequestParam("files") MultipartFile[] files) {
Log.info("Processing file upload...");
List<String> exceptions = new ArrayList<>();
// Upload directory
final String DIRECTORY = System.getProperty("user.home") "/Documents/Uploads";
List<String> fileNames = new ArrayList<>();
for (MultipartFile file : files) {
String fileName = file.getOriginalFilename();
try {
Path fileStorage = get(DIRECTORY, fileName).toAbsolutePath().normalize();
copy(file.getInputStream(), fileStorage, REPLACE_EXISTING);
} catch (Exception e) {
exceptions.add(e.getMessage());
e.printStackTrace();
}
fileNames.add(fileName);
}
if (!exceptions.isEmpty()) {
return ResponseEntity.badRequest().body(exceptions);
}
return ResponseEntity.ok().body(fileNames);
}
}
Here's a sample execution in Postman that returns the list of uploaded files:
As seen in the image above, the backend code is working as expected.
upload.component.ts
import { Component } from '@angular/core';
import { AppConfigService } from 'src/shared/services/app-config.service';
import getClassNameForExtension from 'font-awesome-filetypes';
import { HttpClient } from '@angular/common/http';
import { NgxSpinnerService } from 'ngx-spinner';
@Component({
selector: 'my-app',
templateUrl: './upload.component.html',
styleUrls: ['./upload.component.scss'],
})
export class UploadComponent {
files = [];
totalSize: number = 0;
maxUploadSize: number;
fileExtensions: Array<string> = [];
hasInvalidFile: boolean = false;
constructor(
private appConfigService: AppConfigService,
private spinner: NgxSpinnerService,
private http: HttpClient
) {}
ngOnInit() {
this.maxUploadSize = this.appConfigService.configData.maxUploadSize;
if (this.appConfigService.configData.fileExtensions) {
const extensions =
this.appConfigService.configData.fileExtensions.split(',');
extensions.forEach((ext) => {
this.fileExtensions.push(ext.trim());
});
}
}
onFileDropped($event) {
this.prepareFilesList($event);
}
fileBrowseHandler(files) {
this.prepareFilesList(files);
}
deleteFile(index: number) {
let newSize = 0;
this.files.splice(index, 1);
let allValid = true;
this.files.forEach((file) => {
if (file.invalidFileExtension) {
allValid = false;
}
newSize = file.size;
});
this.hasInvalidFile = !allValid;
this.totalSize = newSize;
}
uploadFilesSimulator(index: number) {
setTimeout(() => {
if (index === this.files.length) {
return;
} else {
const progressInterval = setInterval(() => {
if (this.files[index]) {
if (this.files[index].progress === 100) {
clearInterval(progressInterval);
this.uploadFilesSimulator(index 1);
} else {
this.files[index].progress = 5;
}
}
}, 200);
}
}, 1000);
}
prepareFilesList(files: Array<any>) {
for (const file of files) {
// const ext = file.name.substr(file.name.lastIndexOf('.') 1);
file.progress = 0;
const extension = file.name.split('.').pop();
file.extension = extension;
const className = getClassNameForExtension(extension);
file.className = className;
if (
this.fileExtensions.length > 0 &&
!this.fileExtensions.includes(extension)
) {
file.invalidFileExtension = true;
this.hasInvalidFile = true;
}
this.files.push(file);
this.totalSize = file.size;
}
this.uploadFilesSimulator(0);
}
/**
* Format size in bytes
* @param bytes (File size in bytes)
* @param decimals (Decimals point)
*/
formatBytes(bytes) {
if (bytes === 0) {
return '0 Bytes';
}
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB', 'TB', 'PB', 'EB', 'ZB', 'YB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) ' ' sizes[i];
}
upload() {
const formData = new FormData();
for (const file of this.files) {
formData.append('files', file);
}
this.spinner.show();
this.http.post(`http://localhost:16080/files/upload`, formData);
}
}
upload.component.html
<div style="height: 87vh">
<!-- Spinner -->
<ngx-spinner
bdColor="rgba(189,188,188,0.6)"
size="medium"
color="#4a4848"
type="ball-beat"
[fullScreen]="true"
>
</ngx-spinner>
<div
style="padding: 1.25%; padding-bottom: 0; height: 99%"
>
<mat-card
style="height: 100%; overflow: auto"
>
<!-- Card-header -->
<div >
<!-- Breadcrumbs -->
<div >
<i
[matTooltip]="''"
matTooltipClass="mat-tool-cust"
></i>
<span style="padding: 0 8px"> Data Processing </span>
</div>
</div>
<!-- Card Content -->
<div (fileDropped)="onFileDropped($event)">
<form method="post" enctype="multipart/form-data">
<input
type="file"
#fileDropRef
id="fileDropRef"
multiple
(change)="fileBrowseHandler($event.target.files)"
/>
<i ></i>
<h3>Drag and drop files here</h3>
<h3>or</h3>
<label
for="fileDropRef"
>Click here to browse files</label
>
</form>
</div>
<div >
<div *ngFor="let file of files; let i = index">
<div >
<i style="font-size: 20px"></i>
</div>
<div >
<h4
[ngClass]="file.invalidFileExtension ? 'strike-through' : ''"
>
{{ file?.name }}
</h4>
<p >
{{ formatBytes(file?.size) }}
</p>
<app-progress [progress]="file?.progress"></app-progress>
</div>
<div (click)="deleteFile(i)">
<i matTooltip="Delete"></i>
</div>
</div>
</div>
<div
*ngIf="
files.length > 0 && (hasInvalidFile || totalSize > maxUploadSize)
"
>
<div style="justify-content: center; color: red">
Files cannot be uploaded. Some files may not be in the supported
format ({{ fileExtensions }}) or the total allowable upload size may
have exceeded {{ formatBytes(maxUploadSize) }}.
</div>
</div>
<div
*ngIf="
files.length > 0 && !hasInvalidFile && totalSize <= maxUploadSize
"
>
<button
type="submit"
(click)="upload()"
mat-raised-button
>
Upload Files
</button>
</div>
</mat-card>
</div>
</div>
Error I'm getting
nested exception is org.springframework.web.multipart.MultipartException: Current request is not a multipart request] with root cause
org.springframework.web.multipart.MultipartException: Current request is not a multipart request
I can see a slight difference in the payload from Postman as it gives the full path. But that is not possible to achieve in javascript for security purpose.
CodePudding user response:
Provide the header Content-Type: file, It should resolve the issue.
this.http.post(`http://localhost:16080/files/upload`, formData,
headers: {
'Content-Type': 'file'
}
);
Or
const _headers = new HttpHeaders({
'Content-Type': 'file'
});
this.http.post(`http://localhost:16080/files/upload`, formData,
headers: _headers
);
CodePudding user response:
My original code is working as expected. The problem is in our project, there's an HttpInterceptor. that automatically appends a header of 'Content-Type', 'application/json'
if it wasn't passed.
I added this in my http call.
const headers = new HttpHeaders().set('isUpload', 'true');
this.http.post(`http://localhost:16080/files/upload`, formData, {headers});
And in the interceptor, I added a condition to preserve the previous logic.
import { Observable } from 'rxjs';
import { Injectable } from '@angular/core';
import {
HttpInterceptor,
HttpRequest,
HttpHandler,
HttpEvent,
HttpParams,
} from '@angular/common/http';
import { MatDialogRef } from '@angular/material/dialog';
import { MessageComponent } from '../message/message.component';
@Injectable({
providedIn: 'root',
})
export class RequestInterceptorService implements HttpInterceptor {
constructor() {}
etagValue = undefined;
// error dialog ref var
dialogRef!: MatDialogRef<MessageComponent>;
intercept(
request: HttpRequest<any>,
next: HttpHandler
): Observable<HttpEvent<any>> {
request = request.clone({
withCredentials: true,
headers: request.headers.set('calling-entity', 'UI'),
});
if (!request.headers.has('Content-Type')) {
if (!request.headers.has('isUpload')) {
request = request.clone({
headers: request.headers.set('Content-Type', 'application/json'),
});
}
}
if (
request.params instanceof CustomHttpParams &&
request.params.etagValue
) {
request = request.clone({
setHeaders: {
'If-Match': request.params.etagValue,
},
});
}
return next.handle(request).pipe();
}
}
export class CustomHttpParams extends HttpParams {
constructor(public etagValue: any) {
super();
}
}
After commenting out the part where Content-Type was being set, it worked as expected.