Anonymous
Angular + Spring Boot: загрузка файлов блокируется до тех пор, пока они не будут полностью получены (сначала необходимо
Сообщение
Anonymous » 09 окт 2025, 14:15
У меня есть интерфейс Angular, вызывающий конечную точку Spring Boot, которая возвращает CSV (с аутентификацией JWT). В настоящее время загрузка не начинается сразу: браузер завершает загрузку всего файла в фоновом режиме, пользовательский интерфейс кажется заблокированным до тех пор, пока файл не будет загружен (без появления диалогового окна «Сохранить как…»). Мне нужен обратный процесс: сначала предложить пользователю выбрать местоположение/имя, а затем передать байты на диск в фоновом режиме, пока пользователь может продолжать использовать приложение.
Бэкенд (Spring)
Код: Выделить всё
public ResponseEntity downloadFeedbackCsv(String search, HttpServletRequest request) {
List rows = feedbackService.findAllForExport(search, request);
String csvContent = generateFeedbackCsv(rows);
byte[] bytes = csvContent.getBytes(StandardCharsets.UTF_8);
ByteArrayResource resource = new ByteArrayResource(bytes);
String fileName = "feedback" + nowStamp() + ".csv";
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\"");
headers.add("File-Name", fileName);
headers.add("Access-Control-Expose-Headers", "File-Name");
return ResponseEntity.ok()
.headers(headers)
.contentType(MediaType.parseMediaType("text/csv"))
.contentLength(resource.contentLength())
.body(resource);
}
private String generateFeedbackCsv(List rows) {
StringWriter writer = new StringWriter();
try (CSVPrinter csv = new CSVPrinter(writer, CSVFormat.DEFAULT
.withHeader("Id", "Date", "Creation Date", "Sensor Code", "Reading Value",
"Latitude", "Longitude", "Altitude", "Status value",
"Battery level", "Sensor state"))) {
for (Feedback f : rows) {
csv.printRecord(
f.getFeedbackId(),
f.getFeedDate(),
f.getFeedCreationDate(),
f.getSensCode(),
f.getFeedValue(),
f.getLatitude(),
f.getLongitude(),
f.getAltitude(),
f.getFeedAlphanumericValue(),
f.getFeedBattery(),
f.getFeedState()
);
}
} catch (Exception e) {
log.error("Error while generating feedback list CSV", e);
}
return writer.toString();
}
@GetMapping("/download")
@Operation(summary = "Download feedback list", description = "Returns a CSV file with feedback data")
public ResponseEntity downloadFeedbackList(
@RequestParam(defaultValue = "", name = "search") String search,
HttpServletRequest request) {
return utilitiesService.downloadFeedbackCsv(search, request);
}
Фронтенд (Angular)
JWT добавляется перехватчиком HTTP (токен-носитель)
Код: Выделить всё
public onDownloadFeedbacks(): void {
const filterString = this.dataSource.filterToString(this.filters);
this.feedbackService.downloadFeedbacks(filterString)
.pipe(takeUntil(this.destroy$))
.subscribe(event => {
if (event.type == HttpEventType.Response) {
saveAs(
new File(
[event.body!],
event.headers.get('File-Name')!,
{ type: `${event.headers.get('Content-Type')};charset=utf-8` }
)
);
} else {
console.log(event);
}
});
}
downloadFeedbacks(filterString: string): Observable {
const url = `${config.apiUrl}/api/feedback/download`;
return this.http.get(url, {
reportProgress: true,
observe: 'events',
responseType: 'blob',
params: new HttpParams().set('search', filterString)
})
.pipe(tap(() => console.log(`fetched feedbacks file`)));
}
Что я пробовал:
Сохраняя текущий подход HttpClient + FileSaver. Но перед сохранением всегда требуется полный BLOB-объект.
Использую ссылку, но у меня проблема с аутентификацией; также по-прежнему имеет тенденцию загружаться первым при использовании XHR/Blob.
Подробнее здесь:
https://stackoverflow.com/questions/797 ... ds-save-as
1760008534
Anonymous
У меня есть интерфейс Angular, вызывающий конечную точку Spring Boot, которая возвращает CSV (с аутентификацией JWT). В настоящее время загрузка не начинается сразу: браузер завершает загрузку всего файла в фоновом режиме, пользовательский интерфейс кажется заблокированным до тех пор, пока файл не будет загружен (без появления диалогового окна «Сохранить как…»). Мне нужен обратный процесс: сначала предложить пользователю выбрать местоположение/имя, а затем передать байты на диск в фоновом режиме, пока пользователь может продолжать использовать приложение. [b]Бэкенд (Spring)[/b] [code]public ResponseEntity downloadFeedbackCsv(String search, HttpServletRequest request) { List rows = feedbackService.findAllForExport(search, request); String csvContent = generateFeedbackCsv(rows); byte[] bytes = csvContent.getBytes(StandardCharsets.UTF_8); ByteArrayResource resource = new ByteArrayResource(bytes); String fileName = "feedback" + nowStamp() + ".csv"; HttpHeaders headers = new HttpHeaders(); headers.add(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + fileName + "\""); headers.add("File-Name", fileName); headers.add("Access-Control-Expose-Headers", "File-Name"); return ResponseEntity.ok() .headers(headers) .contentType(MediaType.parseMediaType("text/csv")) .contentLength(resource.contentLength()) .body(resource); } private String generateFeedbackCsv(List rows) { StringWriter writer = new StringWriter(); try (CSVPrinter csv = new CSVPrinter(writer, CSVFormat.DEFAULT .withHeader("Id", "Date", "Creation Date", "Sensor Code", "Reading Value", "Latitude", "Longitude", "Altitude", "Status value", "Battery level", "Sensor state"))) { for (Feedback f : rows) { csv.printRecord( f.getFeedbackId(), f.getFeedDate(), f.getFeedCreationDate(), f.getSensCode(), f.getFeedValue(), f.getLatitude(), f.getLongitude(), f.getAltitude(), f.getFeedAlphanumericValue(), f.getFeedBattery(), f.getFeedState() ); } } catch (Exception e) { log.error("Error while generating feedback list CSV", e); } return writer.toString(); } @GetMapping("/download") @Operation(summary = "Download feedback list", description = "Returns a CSV file with feedback data") public ResponseEntity downloadFeedbackList( @RequestParam(defaultValue = "", name = "search") String search, HttpServletRequest request) { return utilitiesService.downloadFeedbackCsv(search, request); } [/code] [b]Фронтенд (Angular)[/b] JWT добавляется перехватчиком HTTP (токен-носитель) [code]public onDownloadFeedbacks(): void { const filterString = this.dataSource.filterToString(this.filters); this.feedbackService.downloadFeedbacks(filterString) .pipe(takeUntil(this.destroy$)) .subscribe(event => { if (event.type == HttpEventType.Response) { saveAs( new File( [event.body!], event.headers.get('File-Name')!, { type: `${event.headers.get('Content-Type')};charset=utf-8` } ) ); } else { console.log(event); } }); } downloadFeedbacks(filterString: string): Observable { const url = `${config.apiUrl}/api/feedback/download`; return this.http.get(url, { reportProgress: true, observe: 'events', responseType: 'blob', params: new HttpParams().set('search', filterString) }) .pipe(tap(() => console.log(`fetched feedbacks file`))); } [/code] Что я пробовал: [list] [*]Сохраняя текущий подход HttpClient + FileSaver. Но перед сохранением всегда требуется полный BLOB-объект. [*]Использую ссылку, но у меня проблема с аутентификацией; также по-прежнему имеет тенденцию загружаться первым при использовании XHR/Blob. [/list] Подробнее здесь: [url]https://stackoverflow.com/questions/79786308/angular-spring-boot-file-download-blocks-until-fully-received-needs-save-as[/url]