CB-6089 write file stream directly to http response (#3301)

* CB-6089 write file stream directly to http response

* CB-6089 write file stream directly to http response

* CB-6089 write file stream directly to http response

* CB-6089 write file stream directly to http response

* CB-6089 write file stream directly to http response

* CB-6089 remove deprecated method

* CB-6089 change ParquetExporter buffer size

* CB-6089 remove extra async

* CB-6089 add info text

---------

Co-authored-by: naumov <iamemptyhuh@gmail.com>
Co-authored-by: Evgenia <139753579+EvgeniaBzzz@users.noreply.github.com>
Co-authored-by: Ainur <59531286+yagudin10@users.noreply.github.com>
This commit is contained in:
Ruslan Musaev
2025-03-11 17:45:24 +01:00
committed by GitHub
co-authored by naumov Evgenia Ainur
parent e87e3397e2
commit 655613ccf1
16 changed files with 152 additions and 200 deletions
@@ -73,6 +73,6 @@ extend type Query {
parameters: DataTransferParameters!
): AsyncTaskInfo!
dataTransferRemoveDataFile(dataFileId: String!): Boolean
dataTransferRemoveDataFile(dataFileId: String!): Boolean @deprecated(reason: "25.0.1")
}
@@ -24,11 +24,15 @@ import io.cloudbeaver.service.DBWService;
import io.cloudbeaver.service.data.transfer.impl.WebDataTransferDefaultExportSettings;
import io.cloudbeaver.service.data.transfer.impl.WebDataTransferParameters;
import io.cloudbeaver.service.data.transfer.impl.WebDataTransferStreamProcessor;
import io.cloudbeaver.service.data.transfer.impl.WebDataTransferTaskConfig;
import io.cloudbeaver.service.sql.WebSQLContextInfo;
import io.cloudbeaver.service.sql.WebSQLProcessor;
import io.cloudbeaver.service.sql.WebSQLResultsInfo;
import org.jkiss.code.NotNull;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import java.io.OutputStream;
import java.nio.file.Path;
import java.util.List;
@@ -62,8 +66,22 @@ public interface DBWServiceDataTransfer extends DBWService {
String resultsId,
WebDataTransferParameters parameters) throws DBWebException;
/**
* It's deprecated because now we use streaming file to response directly, and we don't need to clean up any files
* after data transfer.
*/
@WebAction
@Deprecated
Boolean dataTransferRemoveDataFile(WebSession session, String dataFileId) throws DBWebException;
WebDataTransferDefaultExportSettings defaultExportSettings();
/**
* Usefully for exporting directly to http response and avoid to create temp files.
*/
void exportDataTransferToStream(
@NotNull DBRProgressMonitor monitor,
@NotNull WebDataTransferTaskConfig taskConfig,
@NotNull OutputStream outputStream
) throws DBException;
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -21,10 +21,10 @@ import io.cloudbeaver.service.DBWBindingContext;
import io.cloudbeaver.service.DBWServiceBindingServlet;
import io.cloudbeaver.service.DBWServletContext;
import io.cloudbeaver.service.WebServiceBindingBase;
import io.cloudbeaver.service.data.transfer.impl.WebDataTransferImportServlet;
import io.cloudbeaver.service.data.transfer.impl.WebDataTransferParameters;
import io.cloudbeaver.service.data.transfer.impl.WebDataTransferServlet;
import io.cloudbeaver.service.data.transfer.impl.WebServiceDataTransfer;
import io.cloudbeaver.service.data.transfer.impl.WebDataTransferImportServlet;
import io.cloudbeaver.service.sql.WebServiceBindingSQL;
import org.jkiss.dbeaver.DBException;
@@ -28,13 +28,8 @@ import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.tools.transfer.registry.DataTransferProcessorDescriptor;
import org.jkiss.dbeaver.tools.transfer.registry.DataTransferRegistry;
import org.jkiss.utils.CommonUtils;
import org.jkiss.utils.IOUtils;
import java.io.IOException;
import java.io.InputStream;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Map;
public class WebDataTransferServlet extends WebServiceServletBase {
@@ -73,20 +68,10 @@ public class WebDataTransferServlet extends WebServiceServletBase {
fileName = taskInfo.getDataFileId();
}
fileName = WebDataTransferUtils.normalizeFileName(fileName, taskInfo.getParameters().getOutputSettings());
Path dataFile = taskInfo.getDataFile();
session.addInfoMessage("Download data ...");
response.setHeader("Content-Type", processor.getContentType());
response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
response.setHeader("Content-Length", String.valueOf(Files.size(dataFile)));
try (InputStream is = Files.newInputStream(dataFile)) {
IOUtils.copyStream(is, response.getOutputStream());
}
// TODO: cleanup export files ASAP?
if (false) {
dtConfig.removeTask(taskInfo);
}
dtManager.exportDataTransferToStream(session.getProgressMonitor(), taskInfo, response.getOutputStream());
}
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -36,16 +36,8 @@ public class WebDataTransferSessionConfig {
}
}
public void removeTask(WebDataTransferTaskConfig taskConfig) {
synchronized (tasks) {
tasks.remove(taskConfig.getDataFileId());
taskConfig.deleteFile();
}
}
public WebDataTransferSessionConfig deleteExportFiles() {
synchronized (tasks) {
tasks.values().forEach(WebDataTransferTaskConfig::deleteFile);
tasks.clear();
}
return this;
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -16,31 +16,36 @@
*/
package io.cloudbeaver.service.data.transfer.impl;
import io.cloudbeaver.service.sql.WebSQLResultsInfo;
import org.jkiss.dbeaver.Log;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import org.jkiss.dbeaver.model.struct.DBSDataContainer;
public class WebDataTransferTaskConfig {
private static final Log log = Log.getLog(WebDataTransferTaskConfig.class);
private Path dataFile;
private String fileNameKey;
private WebDataTransferParameters parameters;
private String exportFileName;
private DBSDataContainer dataContainer;
private WebSQLResultsInfo resultsInfo;
public WebDataTransferTaskConfig(Path dataFile, WebDataTransferParameters parameters) {
this.dataFile = dataFile;
public WebDataTransferTaskConfig(
String fileNameKey,
WebDataTransferParameters parameters,
String exportFileName,
DBSDataContainer dataContainer,
WebSQLResultsInfo webSQLResultsInfo
) {
this.fileNameKey = fileNameKey;
this.parameters = parameters;
}
public Path getDataFile() {
return dataFile;
this.exportFileName = exportFileName;
this.dataContainer = dataContainer;
this.resultsInfo = webSQLResultsInfo;
}
public String getDataFileId() {
return dataFile.getFileName().toString();
return fileNameKey;
}
public WebDataTransferParameters getParameters() {
@@ -51,15 +56,11 @@ public class WebDataTransferTaskConfig {
return exportFileName;
}
public void setExportFileName(String exportFileName) {
this.exportFileName = exportFileName;
public DBSDataContainer getDataContainer() {
return dataContainer;
}
public void deleteFile() {
try {
Files.delete(dataFile);
} catch (IOException e) {
log.error("Error deleting export file " + dataFile.toAbsolutePath(), e);
}
public WebSQLResultsInfo getResultsInfo() {
return resultsInfo;
}
}
@@ -1,6 +1,6 @@
/*
* DBeaver - Universal Database Manager
* Copyright (C) 2010-2024 DBeaver Corp and others
* Copyright (C) 2010-2025 DBeaver Corp and others
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -20,7 +20,6 @@ import io.cloudbeaver.DBWebException;
import io.cloudbeaver.model.WebAsyncTaskInfo;
import io.cloudbeaver.model.session.WebAsyncTaskProcessor;
import io.cloudbeaver.model.session.WebSession;
import io.cloudbeaver.server.CBApplication;
import io.cloudbeaver.server.CBPlatform;
import io.cloudbeaver.service.data.transfer.DBWServiceDataTransfer;
import io.cloudbeaver.service.sql.WebSQLContextInfo;
@@ -30,14 +29,9 @@ import org.jkiss.code.NotNull;
import org.jkiss.code.Nullable;
import org.jkiss.dbeaver.DBException;
import org.jkiss.dbeaver.Log;
import org.jkiss.dbeaver.model.exec.DBCException;
import org.jkiss.dbeaver.model.exec.DBCResultSet;
import org.jkiss.dbeaver.model.exec.DBCSession;
import org.jkiss.dbeaver.model.preferences.DBPPropertyDescriptor;
import org.jkiss.dbeaver.model.runtime.DBRProgressMonitor;
import org.jkiss.dbeaver.model.runtime.VoidProgressMonitor;
import org.jkiss.dbeaver.model.sql.DBQuotaException;
import org.jkiss.dbeaver.model.sql.DBSQLException;
import org.jkiss.dbeaver.model.struct.DBSDataContainer;
import org.jkiss.dbeaver.model.struct.DBSDataManipulator;
import org.jkiss.dbeaver.model.struct.DBSEntity;
@@ -51,13 +45,11 @@ import org.jkiss.dbeaver.tools.transfer.stream.*;
import org.jkiss.dbeaver.utils.ContentUtils;
import org.jkiss.utils.CommonUtils;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.reflect.InvocationTargetException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.sql.BatchUpdateException;
import java.util.*;
import java.util.stream.Collectors;
@@ -66,8 +58,6 @@ import java.util.stream.Collectors;
*/
public class WebServiceDataTransfer implements DBWServiceDataTransfer {
public static final String QUOTA_PROP_FILE_LIMIT = "dataExportFileSizeLimit";
private static final Log log = Log.getLog(WebServiceDataTransfer.class);
private final Path dataExportFolder;
@@ -145,79 +135,66 @@ public class WebServiceDataTransfer implements DBWServiceDataTransfer {
return asyncExportFromDataContainer(sqlContext.getProcessor(), parameters, results.getDataContainer(), results);
}
@Override
public Boolean dataTransferRemoveDataFile(WebSession webSession, String dataFileId) throws DBWebException {
WebDataTransferSessionConfig dtConfig = WebDataTransferUtils.getSessionDataTransferConfig(webSession);
WebDataTransferTaskConfig taskInfo = dtConfig.getTask(dataFileId);
if (taskInfo == null) {
throw new DBWebException("Session task '" + dataFileId + "' not found");
}
Path dataFile = taskInfo.getDataFile();
if (dataFile != null) {
try {
Files.delete(dataFile);
} catch (IOException e) {
log.warn("Error deleting data file '" + dataFile.toAbsolutePath() + "'", e);
}
}
dtConfig.removeTask(taskInfo);
return true;
}
@Override
public WebDataTransferDefaultExportSettings defaultExportSettings() {
return new WebDataTransferDefaultExportSettings();
}
private WebAsyncTaskInfo asyncExportFromDataContainer(WebSQLProcessor sqlProcessor, WebDataTransferParameters parameters, DBSDataContainer dataContainer,
@Nullable WebSQLResultsInfo resultsInfo) {
@Override
@Deprecated
public Boolean dataTransferRemoveDataFile(WebSession webSession, String dataFileId) throws DBWebException {
//deprecated
return true;
}
@Override
public void exportDataTransferToStream(
@NotNull DBRProgressMonitor monitor,
@NotNull WebDataTransferTaskConfig taskConfig,
@NotNull OutputStream outputStream
) throws DBException {
WebDataTransferParameters parameters = taskConfig.getParameters();
DBSDataContainer dataContainer = taskConfig.getDataContainer();
WebSQLResultsInfo resultsInfo = taskConfig.getResultsInfo();
DataTransferProcessorDescriptor processor = DataTransferRegistry.getInstance().getProcessor(parameters.getProcessorId());
try {
exportData(monitor, processor, dataContainer, parameters, resultsInfo, outputStream);
} catch (Exception e) {
throw new DBException("Error exporting data", e);
}
}
private WebAsyncTaskInfo asyncExportFromDataContainer(
@NotNull WebSQLProcessor sqlProcessor,
@NotNull WebDataTransferParameters parameters,
@NotNull DBSDataContainer dataContainer,
@Nullable WebSQLResultsInfo resultsInfo
) {
sqlProcessor.getWebSession().addInfoMessage("Export data");
DataTransferProcessorDescriptor processor = DataTransferRegistry.getInstance().getProcessor(parameters.getProcessorId());
WebAsyncTaskProcessor<String> runnable = new WebAsyncTaskProcessor<String>() {
@Override
public void run(DBRProgressMonitor monitor) throws InvocationTargetException {
monitor.beginTask("Export data", 1);
try {
monitor.subTask("Export data using " + processor.getName());
Path exportFile = dataExportFolder.resolve(
makeUniqueFileName(sqlProcessor, processor, parameters.getProcessorProperties()));
try {
exportData(monitor, processor, dataContainer, parameters, resultsInfo, exportFile);
} catch (Exception e) {
if (Files.exists(exportFile)) {
try {
Files.delete(exportFile);
} catch (IOException ex) {
log.error("Error deleting export file " + exportFile.toAbsolutePath(), e);
}
}
if (e instanceof DBException) {
throw e;
}
throw new DBException("Error exporting data", e);
}
var outputSettings = parameters.getOutputSettings();
Path finallyExportFile = outputSettings.isCompress()
? exportFile.resolveSibling(WebDataTransferUtils.normalizeFileName(
exportFile.getFileName().toString(), outputSettings))
: exportFile;
WebDataTransferTaskConfig taskConfig = new WebDataTransferTaskConfig(finallyExportFile, parameters);
String exportFileName = CommonUtils.isEmpty(outputSettings.getFileName()) ?
CommonUtils.escapeFileName(CommonUtils.truncateString(dataContainer.getName(), 32)) :
outputSettings.getFileName();
taskConfig.setExportFileName(exportFileName);
WebDataTransferUtils.getSessionDataTransferConfig(sqlProcessor.getWebSession()).addTask(taskConfig);
String uniqueFileName = makeUniqueFileName(sqlProcessor, processor, parameters.getProcessorProperties());
var outputSettings = parameters.getOutputSettings();
String fileNameKey = WebDataTransferUtils.normalizeFileName(uniqueFileName, outputSettings);
String exportFileName = CommonUtils.isEmpty(outputSettings.getFileName())
? CommonUtils.escapeFileName(CommonUtils.truncateString(dataContainer.getName(), 32))
: outputSettings.getFileName();
WebDataTransferTaskConfig taskConfig = new WebDataTransferTaskConfig(
fileNameKey, parameters, exportFileName, dataContainer, resultsInfo);
result = finallyExportFile.getFileName().toString();
} catch (Throwable e) {
throw new InvocationTargetException(e);
} finally {
monitor.done();
WebDataTransferUtils.getSessionDataTransferConfig(sqlProcessor.getWebSession())
.addTask(taskConfig);
//fixme fake task for keeping api
return sqlProcessor.getWebSession().createAndRunAsyncTask(
"Data export", new WebAsyncTaskProcessor<>() {
@Override
public void run(DBRProgressMonitor monitor) throws InvocationTargetException {
result = fileNameKey;
}
}
};
return sqlProcessor.getWebSession().createAndRunAsyncTask("Data export", runnable);
);
}
public WebAsyncTaskInfo asyncImportDataContainer(@NotNull String processorId,
@@ -263,34 +240,50 @@ public class WebServiceDataTransfer implements DBWServiceDataTransfer {
DBSDataContainer dataContainer,
WebDataTransferParameters parameters,
WebSQLResultsInfo resultsInfo,
Path exportFile) throws DBException, IOException
{
OutputStream outputStream
) throws DBException, IOException {
IDataTransferProcessor processorInstance = processor.getInstance();
if (!(processorInstance instanceof IStreamDataExporter)) {
if (!(processorInstance instanceof IStreamDataExporter exporter)) {
throw new DBException("Invalid processor. " + IStreamDataExporter.class.getSimpleName() + " expected");
}
IStreamDataExporter exporter = (IStreamDataExporter) processorInstance;
Number fileSizeLimit = CBApplication.getInstance().getAppConfiguration().getResourceQuota(QUOTA_PROP_FILE_LIMIT);
Map<String, Object> processorProperties = parameters.getProcessorProperties();
if (processorProperties == null) processorProperties = Collections.emptyMap();
Map<String, Object> properties = new HashMap<>();
for (DBPPropertyDescriptor prop : processor.getProperties()) {
Object propValue = processorProperties.get(CommonUtils.toString(prop.getId()));
properties.put(prop.getId(), propValue != null ? propValue : prop.getDefaultValue());
}
// Remove extension property (we specify file name directly)
properties.remove(StreamConsumerSettings.PROP_FILE_EXTENSION);
StreamTransferConsumer consumer = new StreamTransferConsumer() {
@Override
public void fetchRow(@NotNull DBCSession session, @NotNull DBCResultSet resultSet) throws DBCException {
super.fetchRow(session, resultSet);
if (fileSizeLimit != null && getBytesWritten() > fileSizeLimit.longValue()) {
throw new DBQuotaException(
"Data export quota exceeded \n Please increase the resourceQuotas parameter in configuration",
QUOTA_PROP_FILE_LIMIT, fileSizeLimit.longValue(), getBytesWritten()
);
}
}
};
DatabaseProducerSettings producerSettings = new DatabaseProducerSettings();
producerSettings.setExtractType(DatabaseProducerSettings.ExtractType.SINGLE_QUERY);
producerSettings.setQueryRowCount(false);
producerSettings.setOpenNewConnections(CommonUtils.getOption(parameters.getDbProducerSettings(), "openNewConnection"));
StreamTransferConsumer consumer = new StreamTransferConsumer();
StreamConsumerSettings settings = makeStreamConsumerSettings(parameters);
DatabaseTransferProducer producer = new DatabaseTransferProducer(
dataContainer,
parameters.getFilter() == null ? null : parameters.getFilter().makeDataFilter(resultsInfo));
consumer.initTransfer(
dataContainer,
settings,
new IDataTransferConsumer.TransferParameters(processor.isBinaryFormat(), processor.isHTMLFormat(), outputStream),
exporter,
properties,
producer.getProject());
producer.transferData(monitor, consumer, null, producerSettings, null);
consumer.finishTransfer(monitor, false);
}
@NotNull
private StreamConsumerSettings makeStreamConsumerSettings(@NotNull WebDataTransferParameters parameters) {
StreamConsumerSettings settings = new StreamConsumerSettings();
settings.setOutputFolder(exportFile.getParent().toAbsolutePath().toString());
settings.setOutputFilePattern(exportFile.getFileName().toString());
WebDataTransferOutputSettings outputSettings = parameters.getOutputSettings();
settings.setOutputEncodingBOM(outputSettings.isInsertBom());
settings.setCompressResults(outputSettings.isCompress());
@@ -300,38 +293,7 @@ public class WebServiceDataTransfer implements DBWServiceDataTransfer {
if (!CommonUtils.isEmpty(outputSettings.getTimestampPattern())) {
settings.setOutputTimestampPattern(outputSettings.getTimestampPattern());
}
Map<String, Object> properties = new HashMap<>();
Map<String, Object> processorProperties = parameters.getProcessorProperties();
if (processorProperties == null) processorProperties = Collections.emptyMap();
for (DBPPropertyDescriptor prop : processor.getProperties()) {
Object propValue = processorProperties.get(CommonUtils.toString(prop.getId()));
properties.put(prop.getId(), propValue != null ? propValue : prop.getDefaultValue());
}
// Remove extension property (we specify file name directly)
properties.remove(StreamConsumerSettings.PROP_FILE_EXTENSION);
DatabaseTransferProducer producer = new DatabaseTransferProducer(
dataContainer,
parameters.getFilter() == null ? null : parameters.getFilter().makeDataFilter(resultsInfo));
DatabaseProducerSettings producerSettings = new DatabaseProducerSettings();
producerSettings.setExtractType(DatabaseProducerSettings.ExtractType.SINGLE_QUERY);
producerSettings.setQueryRowCount(false);
producerSettings.setOpenNewConnections(CommonUtils.getOption(parameters.getDbProducerSettings(), "openNewConnection"));
consumer.initTransfer(
dataContainer,
settings,
new IDataTransferConsumer.TransferParameters(processor.isBinaryFormat(), processor.isHTMLFormat()),
exporter,
properties,
producer.getProject());
producer.transferData(monitor, consumer, null, producerSettings, null);
consumer.finishTransfer(monitor, false);
return settings;
}
private void importData(
@@ -1,5 +0,0 @@
query removeDataTransferFile(
$dataFileId: String!
) {
result: dataTransferRemoveDataFile(dataFileId: $dataFileId)
}
@@ -36,7 +36,7 @@ export class DataExportProcessService {
private readonly notificationService: NotificationService,
) {}
async cancel(exportId: string): Promise<void> {
cancel(exportId: string): void {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
@@ -44,21 +44,14 @@ export class DataExportProcessService {
process.process.cancel();
}
async delete(exportId: string): Promise<void> {
delete(exportId: string): void {
const process = this.exportProcesses.get(exportId);
if (!process) {
return;
}
try {
const dataFileId = process.process.getPayload();
if (dataFileId) {
await this.graphQLService.sdk.removeDataTransferFile({ dataFileId });
}
} catch (exception: any) {
this.notificationService.logException(exception, 'Error occurred while deleting file');
} finally {
this.exportProcesses.remove(exportId);
}
this.exportProcesses.remove(exportId);
}
download(exportId: string): void {
@@ -24,10 +24,6 @@ export class DataExportService {
await this.dataExportProcessService.cancel(exportId);
}
async delete(exportId: string): Promise<void> {
await this.dataExportProcessService.delete(exportId);
}
download(exportId: string): void {
this.dataExportProcessService.download(exportId);
}
@@ -14,8 +14,11 @@
white-space: pre-line;
}
.sourceName {
.subText {
composes: theme-typography--body2 from global;
}
.sourceName {
padding-top: 16px;
max-height: 50px;
overflow: hidden;
@@ -15,6 +15,7 @@ import {
SnackbarFooter,
SnackbarStatus,
SnackbarWrapper,
Text,
useErrorDetails,
useS,
useTranslate,
@@ -37,14 +38,16 @@ export const ExportNotification = observer<Props>(function ExportNotification({
const errorDetails = useErrorDetails(state.task?.process.getRejectionReason() ?? null);
const { title, status, message } = state.status;
const isReadyToDownload = status === ENotificationType.Info && !!state.downloadUrl;
return (
<SnackbarWrapper persistent={status === ENotificationType.Loading} onClose={state.delete}>
<SnackbarStatus status={status} />
<SnackbarContent>
<SnackbarBody title={translate(title)}>
{isReadyToDownload && <Text className={s(style, { subText: true })}>{translate('plugin_data_export_download_process_info')}</Text>}
{message && <div className={s(style, { message: true })}>{message}</div>}
<div className={s(style, { sourceName: true })}>
<div className={s(style, { subText: true, sourceName: true })}>
{state.sourceName}
{state.task?.context.query && (
<pre className={s(style, { pre: true })} title={state.task.context.query}>
@@ -54,7 +57,7 @@ export const ExportNotification = observer<Props>(function ExportNotification({
</div>
</SnackbarBody>
<SnackbarFooter timestamp={notification.timestamp}>
{status === ENotificationType.Info && state.downloadUrl && (
{isReadyToDownload && (
<>
<Button type="button" mod={['outlined']} onClick={state.delete}>
{translate('ui_processing_cancel')}
@@ -18,4 +18,5 @@ export default [
['data_transfer_output_settings', 'Output'],
['data_transfer_output_settings_compress', 'Compression'],
['plugin_data_export_download_process_info', 'The download process may take some time to start'],
];
@@ -24,4 +24,5 @@ export default [
['data_transfer_dialog_select_processor_fail', 'Échec de la sélection du processeur'],
['data_transfer_output_settings', 'Sortie'],
['data_transfer_output_settings_compress', 'Compression'],
['plugin_data_export_download_process_info', 'The download process may take some time to start'],
];
@@ -15,4 +15,5 @@ export default [
['data_transfer_dialog_select_processor_fail', 'Failed to select processor'],
['data_transfer_output_settings_compress', 'Compression'],
['plugin_data_export_download_process_info', 'The download process may take some time to start'],
];
@@ -16,4 +16,5 @@ export default [
['data_transfer_dialog_select_processor_fail', 'Не удалось выбрать обработчик данных'],
['data_transfer_output_settings_compress', 'Сжатие'],
['plugin_data_export_download_process_info', 'Начало загрузки может занять некоторое время'],
];