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:
Nilansh Bansal 2025-02-18 20:13:13 +05:30 committed by GitHub
parent 95e8e3703c
commit f05e3be955
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
2 changed files with 103 additions and 26 deletions

View File

@ -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";
}

View File

@ -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);
}
}
/**