fix: file upload fixed when using Base64 type in FilePicker and multipart/form-data in REST API (#39332)
## Description > This PR fixes the file upload issues when we use the Base64 type in the File Picker Widget and the REST API uses a multipart/form-data type. From the table [here](https://github.com/appsmithorg/appsmith/issues/39109#issuecomment-2655659006), this issue points to the 3rd one in the list. Fixes #39227 ## Automation /ok-to-test tags="@tag.Sanity" ### 🔍 Cypress test results <!-- This is an auto-generated comment: Cypress test results --> > [!TIP] > 🟢 🟢 🟢 All cypress tests have passed! 🎉 🎉 🎉 > Workflow run: <https://github.com/appsmithorg/appsmith/actions/runs/13392351852> > Commit: c731c6515d4bba00ca3232b5309401d76471f184 > <a href="https://internal.appsmith.com/app/cypress-dashboard/rundetails-65890b3c81d7400d08fa9ee5?branch=master&workflowId=13392351852&attempt=1" target="_blank">Cypress dashboard</a>. > Tags: `@tag.Sanity` > Spec: > <hr>Tue, 18 Feb 2025 14:36:12 UTC <!-- end of auto-generated comment: Cypress test results --> ## Communication Should the DevRel and Marketing teams inform users about this change? - [ ] Yes - [ ] No <!-- This is an auto-generated comment: release notes by coderabbit.ai --> ## Summary by CodeRabbit ## Summary by CodeRabbit - **New Features** - Enhanced multipart form data handling for file uploads, now supporting robust processing of base64-encoded content with options for custom filenames and MIME types. - Introduced specific error messages for invalid multipart data and base64 format issues. - **Refactor** - Streamlined the data processing workflow with clearer error messages and simplified logic for improved reliability during file upload operations. <!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
95e8e3703c
commit
f05e3be955
|
|
@ -21,4 +21,8 @@ public abstract class BasePluginErrorMessages {
|
|||
public static final String INVALID_SSH_KEY_FORMAT_ERROR_MSG =
|
||||
"Invalid SSH key format. Supported formats: OpenSSH, PKCS#8, or RSA PEM.";
|
||||
public static final String SSH_KEY_PARSING_ERROR_MSG = "The provided SSH key could not be parsed.";
|
||||
public static final String ERROR_INVALID_MULTIPART_DATA =
|
||||
"Unable to parse content. Expected an array or object of multipart data";
|
||||
public static final String ERROR_INVALID_BASE64_FORMAT =
|
||||
"Invalid BASE64 format. Expected format: data:mimetype;base64,content";
|
||||
}
|
||||
|
|
|
|||
|
|
@ -39,15 +39,18 @@ import java.io.IOException;
|
|||
import java.io.UnsupportedEncodingException;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Arrays;
|
||||
import java.util.Base64;
|
||||
import java.util.Collections;
|
||||
import java.util.LinkedHashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
|
||||
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.ERROR_INVALID_BASE64_FORMAT;
|
||||
import static com.appsmith.external.exceptions.pluginExceptions.BasePluginErrorMessages.ERROR_INVALID_MULTIPART_DATA;
|
||||
|
||||
public class DataUtils {
|
||||
|
||||
public static String FIELD_API_CONTENT_TYPE = "apiContentType";
|
||||
|
|
@ -232,8 +235,7 @@ public class DataUtils {
|
|||
} catch (IOException e) {
|
||||
e.printStackTrace();
|
||||
throw new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
"Unable to parse content. Expected to receive an array or object of multipart data");
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, ERROR_INVALID_MULTIPART_DATA);
|
||||
}
|
||||
break;
|
||||
case ARRAY:
|
||||
|
|
@ -300,37 +302,108 @@ public class DataUtils {
|
|||
|
||||
private void populateFileTypeBodyBuilder(
|
||||
MultipartBodyBuilder bodyBuilder, Property property, ClientHttpRequest outputMessage) throws IOException {
|
||||
|
||||
final String fileValue = (String) property.getValue();
|
||||
final String key = property.getKey();
|
||||
List<MultipartFormDataDTO> multipartFormDataDTOs = new ArrayList<>();
|
||||
|
||||
if (fileValue.startsWith("{")) {
|
||||
// Check whether the JSON string is an object
|
||||
final MultipartFormDataDTO multipartFormDataDTO =
|
||||
objectMapper.readValue(fileValue, MultipartFormDataDTO.class);
|
||||
multipartFormDataDTOs.add(multipartFormDataDTO);
|
||||
} else if (fileValue.startsWith("[")) {
|
||||
// Check whether the JSON string is an array
|
||||
multipartFormDataDTOs = Arrays.asList(objectMapper.readValue(fileValue, MultipartFormDataDTO[].class));
|
||||
if (fileValue.contains(BASE64_DELIMITER)) {
|
||||
processBase64Data(fileValue, key, bodyBuilder, outputMessage);
|
||||
} else {
|
||||
List<MultipartFormDataDTO> multipartFormDataDTOs = parseMultipartData(fileValue);
|
||||
|
||||
for (MultipartFormDataDTO dto : multipartFormDataDTOs) {
|
||||
String dataString = String.valueOf(dto.getData());
|
||||
if (dataString.contains(BASE64_DELIMITER)) {
|
||||
processBase64Data(dataString, key, bodyBuilder, outputMessage, dto.getName(), dto.getType());
|
||||
} else {
|
||||
processRegularData(dataString, key, bodyBuilder, outputMessage, dto.getName(), dto.getType());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Process BASE64-encoded content
|
||||
private void processBase64Data(
|
||||
String fileValue, String key, MultipartBodyBuilder bodyBuilder, ClientHttpRequest outputMessage) {
|
||||
processBase64Data(fileValue, key, bodyBuilder, outputMessage, "file", "application/octet-stream");
|
||||
}
|
||||
|
||||
// Overloaded method with custom filename & MIME type
|
||||
private void processBase64Data(
|
||||
String fileValue,
|
||||
String key,
|
||||
MultipartBodyBuilder bodyBuilder,
|
||||
ClientHttpRequest outputMessage,
|
||||
String filename,
|
||||
String defaultMimeType) {
|
||||
String[] parts = fileValue.split(BASE64_DELIMITER, 2);
|
||||
if (parts.length != 2) {
|
||||
throw new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR,
|
||||
"Unable to parse content. Expected to receive an array or object of multipart data");
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, ERROR_INVALID_BASE64_FORMAT);
|
||||
}
|
||||
|
||||
multipartFormDataDTOs.forEach(multipartFormDataDTO -> {
|
||||
final MultipartFormDataDTO finalMultipartFormDataDTO = multipartFormDataDTO;
|
||||
Flux<DataBuffer> data = DataBufferUtils.readInputStream(
|
||||
() -> new ByteArrayInputStream(
|
||||
String.valueOf(finalMultipartFormDataDTO.getData()).getBytes(StandardCharsets.ISO_8859_1)),
|
||||
outputMessage.bufferFactory(),
|
||||
4096);
|
||||
String metadataPart = parts[0];
|
||||
String base64Content = parts[1];
|
||||
String mimeType = extractMimeType(metadataPart, defaultMimeType);
|
||||
byte[] decodedBytes = Base64.getMimeDecoder().decode(base64Content);
|
||||
|
||||
bodyBuilder
|
||||
.asyncPart(key, data, DataBuffer.class)
|
||||
.filename(multipartFormDataDTO.getName())
|
||||
.contentType(MediaType.valueOf(multipartFormDataDTO.getType()));
|
||||
});
|
||||
addPartToBody(bodyBuilder, key, decodedBytes, outputMessage, filename, mimeType);
|
||||
}
|
||||
|
||||
// Process regular string data
|
||||
private void processRegularData(
|
||||
String data,
|
||||
String key,
|
||||
MultipartBodyBuilder bodyBuilder,
|
||||
ClientHttpRequest outputMessage,
|
||||
String filename,
|
||||
String mimeType) {
|
||||
byte[] bytes = data.getBytes(StandardCharsets.ISO_8859_1);
|
||||
addPartToBody(bodyBuilder, key, bytes, outputMessage, filename, mimeType);
|
||||
}
|
||||
|
||||
// Extract MIME type from metadata string
|
||||
// The metadataPart typically follows this format: "data:mimetype;base64"
|
||||
// Example values:
|
||||
// - "data:image/png;base64" -> Extracted MIME type: "image/png"
|
||||
// - "data:application/pdf;base64" -> Extracted MIME type: "application/pdf"
|
||||
// - "data:text/plain;" (without base64) -> Extracted MIME type: "text/plain"
|
||||
// If the format is incorrect or missing, it falls back to the default MIME type.
|
||||
private String extractMimeType(String metadataPart, String defaultMimeType) {
|
||||
if (metadataPart.startsWith("data:")) {
|
||||
int endIndex = metadataPart.indexOf(';');
|
||||
if (endIndex > 5) {
|
||||
return metadataPart.substring(5, endIndex);
|
||||
} else {
|
||||
return metadataPart.substring(5);
|
||||
}
|
||||
}
|
||||
return defaultMimeType;
|
||||
}
|
||||
|
||||
// Add a part to the MultipartBodyBuilder
|
||||
private void addPartToBody(
|
||||
MultipartBodyBuilder bodyBuilder,
|
||||
String key,
|
||||
byte[] bytes,
|
||||
ClientHttpRequest outputMessage,
|
||||
String filename,
|
||||
String mimeType) {
|
||||
Flux<DataBuffer> data = DataBufferUtils.readInputStream(
|
||||
() -> new ByteArrayInputStream(bytes), outputMessage.bufferFactory(), 4096);
|
||||
bodyBuilder.asyncPart(key, data, DataBuffer.class).filename(filename).contentType(MediaType.valueOf(mimeType));
|
||||
}
|
||||
|
||||
// Parse JSON multipart data
|
||||
private List<MultipartFormDataDTO> parseMultipartData(String fileValue) throws IOException {
|
||||
if (fileValue.startsWith("{")) {
|
||||
return Collections.singletonList(objectMapper.readValue(fileValue, MultipartFormDataDTO.class));
|
||||
} else if (fileValue.startsWith("[")) {
|
||||
return Arrays.asList(objectMapper.readValue(fileValue, MultipartFormDataDTO[].class));
|
||||
} else {
|
||||
throw new AppsmithPluginException(
|
||||
AppsmithPluginError.PLUGIN_DATASOURCE_ARGUMENT_ERROR, ERROR_INVALID_MULTIPART_DATA);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user