Issue
I have a working upload backend with Postman. But I can't make it work from angular.
Backend code:
package com.demo.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 class="ui-g" 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
class="ui-lg-12 ui-md-12 ui-g-12"
style="padding: 1.25%; padding-bottom: 0; height: 99%"
>
<mat-card
class="ui-lg-12 ui-md-12 ui-g-12 app-card"
style="height: 100%; overflow: auto"
>
<!-- Card-header -->
<div class="app-card-header flex-align-center ui-lg-12 ui-md-12 ui-g-12">
<!-- Breadcrumbs -->
<div class="ui-lg-12 ui-md-12 ui-g-12 p-0 d-flex align-center">
<i
[matTooltip]="''"
matTooltipClass="mat-tool-cust"
class="fas fa-table-cells-large app-icon-head ocular-app-card-header-icon"
></i>
<span style="padding: 0 8px"> Data Processing </span>
</div>
</div>
<!-- Card Content -->
<div class="upload-container" (fileDropped)="onFileDropped($event)">
<form method="post" enctype="multipart/form-data">
<input
type="file"
#fileDropRef
id="fileDropRef"
multiple
(change)="fileBrowseHandler($event.target.files)"
/>
<i class="fas fa-regular fa-upload"></i>
<h3>Drag and drop files here</h3>
<h3>or</h3>
<label
for="fileDropRef"
class="app-btn1 mat-raised-button mat-button-base"
>Click here to browse files</label
>
</form>
</div>
<div class="files-list">
<div class="single-file" *ngFor="let file of files; let i = index">
<div class="file-icon">
<i class="fa {{ file.className }}" style="font-size: 20px"></i>
</div>
<div class="info">
<h4
class="name"
[ngClass]="file.invalidFileExtension ? 'strike-through' : ''"
>
{{ file?.name }}
</h4>
<p class="size">
{{ formatBytes(file?.size) }}
</p>
<app-progress [progress]="file?.progress"></app-progress>
</div>
<div class="delete-ctn" (click)="deleteFile(i)">
<i class="fas fa-trash action-icons" matTooltip="Delete"></i>
</div>
</div>
</div>
<div
class="upload-btn-container"
*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
class="upload-btn-container"
*ngIf="
files.length > 0 && !hasInvalidFile && totalSize <= maxUploadSize
"
>
<button
type="submit"
(click)="upload()"
mat-raised-button
class="app-btn2 save-btn"
>
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.
Solution
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.
Answered By - ericute
0 comments:
Post a Comment
Note: Only a member of this blog may post a comment.