Я решил эту проблему, обойти ее. Потратив несколько дней на пробные решения для клиентов, я наконец-то нашел решение, включающее клиентский код (Codename One) и серверный код (Spring Boot).
По сути, на клиенте я разбил файл так, чтобы загружать небольшими кусочками по 100 КБ, и я загружаю их по одному, чтобы я мог точно рассчитать загруженный процент. На сервере я установил контроллер для приема небольших файлов и другой контроллер для их объединения. Я знаю, что мой код указан c для моего варианта использования (отправка изображений и видео в Cloudinary), однако я копирую некоторые важные части, которые могут вдохновить других людей, имеющих аналогичную проблему с Codename One.
Снимок экрана ("Caricamento" означает "Загрузка", а "Annulla" означает "Отмена"):
Код клиента
Класс сервера
/**
* SYNC - Upload a MultipartFile as partial file
*
* @param data
* @param partNumber
* @param uniqueId containing the totalBytes before the first "-"
* @return true if success, false otherwise
*/
public static boolean uploadPartialFile(byte[] data, int partNumber, String uniqueId) {
String api = "/cloud/partialUpload";
MultipartRequest request = new MultipartRequest();
request.setUrl(Server.getServerURL() + api);
request.addData("file", data, "application/octet-stream");
request.addRequestHeader("authToken", DB.userDB.authToken.get());
request.addRequestHeader("email", DB.userDB.email.get());
request.addRequestHeader("partNumber", partNumber + "");
request.addRequestHeader("uniqueId", uniqueId);
NetworkManager.getInstance().addToQueueAndWait(request);
try {
String response = Util.readToString(new ByteArrayInputStream(request.getResponseData()), "UTF-8");
if ("OK".equals(response)) {
return true;
}
} catch (IOException ex) {
Log.p("Server.uploadPartialFile ERROR -> Util.readToString failed");
Log.e(ex);
SendLog.sendLogAsync();
}
return false;
}
/**
* ASYNC - Merges the previously upload partial files
*
* @param uniqueId containing the totalBytes before the first "-"
* @param callback to do something with the publicId of the uploaded file
*/
public static void mergeUpload(String uniqueId, OnComplete<Response<String>> callback) {
String api = "/cloud/mergeUpload";
Map<String, String> headers = Server.getUserHeaders();
headers.put("uniqueId", uniqueId);
Server.asyncGET(api, headers, callback);
}
public static void uploadFile(String filePath, OnComplete<String> callback) {
String api = "/cloud/upload";
// to show the progress, we send a piece of the file at a time
String url = Server.getServerURL() + api;
Map<String, String> headers = new HashMap<>();
headers.put("authToken", DB.userDB.authToken.get());
headers.put("email", DB.userDB.email.get());
DialogUtilities.genericUploadProgress(url, filePath, headers, callback);
}
}
Класс DialogUtilities
public static void genericUploadProgress(String url, String filePath, Map<String, String> headers, OnComplete<String> callback) {
Command[] cmds = {Command.create("Cancel", null, ev -> {
((Dialog) Display.getInstance().getCurrent()).dispose();
uploadThread.kill();
})};
Container bodyCmp = new Container(new BorderLayout());
Label infoText = new Label("DialogUtilities-Upload-Starting");
bodyCmp.add(BorderLayout.CENTER, infoText);
// Dialog blocks the current thread (that is the EDT), so the following code needs to be run in another thread
uploadThread.run(() -> {
// waits some time to give the Dialog the time to be open
// it's not necessary, but useful to use the SelectorUtilities below in the case that the uploaded file is very small
Util.sleep(500);
try {
long size = FileSystemStorage.getInstance().getLength(filePath);
String uniqueId = size + "-" + DB.userDB.email + "_" + System.currentTimeMillis();
// splits the file in blocks of 100kb
InputStream inputStream = FileSystemStorage.getInstance().openInputStream(filePath);
byte[] buffer = new byte[100 * 1024];
int readByte = inputStream.read(buffer);
int totalReadByte = 0;
int partNumber = 0;
while (readByte != -1) {
boolean result = Server.uploadPartialFile(Arrays.copyOfRange(buffer, 0, readByte), partNumber, uniqueId);
if (!result) {
CN.callSerially(() -> {
DialogUtilities.genericServerError();
});
break;
}
partNumber++;
totalReadByte += readByte;
int percentage = (int) (totalReadByte * 100 / size);
CN.callSerially(() -> {
infoText.setText(percentage + "%");
});
readByte = inputStream.read(buffer);
}
CN.callSerially(() -> {
if (CN.getCurrentForm() instanceof Dialog) {
// upload finished, before merging the files on the server we disable the "Cancel" button
Button cancelBtn = SelectorUtilities.$(Button.class, CN.getCurrentForm()).iterator().next();
cancelBtn.setEnabled(false);
cancelBtn.setText("DialogUtilities-Wait");
cancelBtn.repaint();
}
});
Server.mergeUpload(uniqueId, new OnComplete<Response<String>>() {
@Override
public void completed(Response<String> response) {
String fileId = response.getResponseData();
CN.callSerially(() -> {
if (Display.getInstance().getCurrent() instanceof Dialog) {
((Dialog) Display.getInstance().getCurrent()).dispose();
}
});
callback.completed(fileId);
}
});
} catch (IOException ex) {
Log.p("DialogUtilities.genericUploadProgress ERROR", Log.ERROR);
CN.callSerially(() -> {
DialogUtilities.genericDialogError("DialogUtilities-UploadError-Title", "DialogUtilities-UploadError-Text");
});
Log.e(ex);
SendLog.sendLogAsync();
}
});
showDialog("Server-Uploading", null, cmds[0], cmds, DialogUtilities.TYPE_UPLOAD, null, 0l, CommonTransitions.createDialogPulsate().copy(false), null, null, bodyCmp);
Код сервера
Класс CloudinaryController
/**
* Upload a MultipartFile as partial file.
*
* @param authToken
* @param email
* @param partNumber
* @param uniqueId containing the totalBytes before the first "-"
* @param file
* @return "OK" if success
*/
@PostMapping("/partialUpload")
public @ResponseBody
String partialUpload(@RequestHeader(value = "authToken") String authToken, @RequestHeader(value = "email") String email, @RequestHeader(value = "partNumber") String partNumber, @RequestHeader(value = "uniqueId") String uniqueId, @RequestParam("file") MultipartFile file) throws IOException {
return cloudinaryService.partialUpload(authToken, email, partNumber, uniqueId, file);
}
/**
* Merges the files previuosly uploaded by "/partialUpload", upload that
* file to Cloudinary and returns the id assigned by Cloudinary
*
* @param authToken
* @param email
* @param uniqueId containing the totalBytes before the first "-"
* @return the id assigned by Cloudinary
*/
@GetMapping("/mergeUpload")
public @ResponseBody
String mergeUpload(@RequestHeader(value = "authToken") String authToken, @RequestHeader(value = "email") String email, @RequestHeader(value = "uniqueId") String uniqueId) throws IOException {
return cloudinaryService.mergeUpload(authToken, email, uniqueId);
}
Класс CloudinaryService
/**
* Upload a MultipartFile as partial file.
*
* @param authToken
* @param email
* @param partNumber
* @param uniqueId containing the totalBytes before the first "-"
* @param file
* @return "OK" if success
*/
public String partialUpload(String authToken, String email, String partNumber, String uniqueId, MultipartFile file) throws IOException {
User user = userService.getUser(authToken, email);
if (user != null) {
String output = AppApplication.uploadTempDir + "/" + uniqueId + "-" + partNumber;
Path destination = Paths.get(output);
Files.copy(file.getInputStream(), destination, StandardCopyOption.REPLACE_EXISTING);
return "OK";
} else {
logger.error("Error: a not authenticated user tried to upload a file (email: " + email + ", authToken: " + authToken + ")");
return null;
}
}
/**
* Merges the files previuosly uploaded by "/partialUpload", upload that
* file to Cloudinary and returns the id assigned by Cloudinary
*
* @param authToken
* @param email
* @param uniqueId containing the totalBytes before the first "-"
* @return the id assigned by Cloudinary
*/
public String mergeUpload(String authToken, String email, String uniqueId) throws IOException {
User user = userService.getUser(authToken, email);
if (user != null) {
long totalBytes = Long.valueOf(uniqueId.split("-", 2)[0]);
List<File> files = new ArrayList<>();
int partNumber = 0;
File testFile = new File(AppApplication.uploadTempDir + "/" + uniqueId + "-" + partNumber);
while (testFile.exists()) {
files.add(testFile);
partNumber++;
testFile = new File(AppApplication.uploadTempDir + "/" + uniqueId + "-" + partNumber);
}
// the list of files is ready, we can now merge them
File merged = new File(AppApplication.uploadTempDir + "/" + uniqueId);
IOCopier.joinFiles(merged, files);
// uploads the file to Cloudinary
Map uploadResult = cloudinary.uploader().upload(merged, ObjectUtils.emptyMap());
String publicId = uploadResult.get("public_id").toString();
// removes the files
for (File file : files) {
file.delete();
}
merged.delete();
return publicId;
} else {
logger.error("Error: a not authenticated user tried to upload a file (email: " + email + ", authToken: " + authToken + ")");
return null;
}
}
Класс IOCopier
import java.io.BufferedInputStream;
import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.List;
import org.apache.commons.io.IOUtils;
/**
* Useful to merge files. See: https://stackoverflow.com/a/14673198
*/
public class IOCopier {
public static void joinFiles(File destination, List<File> sources)
throws IOException {
OutputStream output = null;
try {
output = createAppendableStream(destination);
for (File source : sources) {
appendFile(output, source);
}
} finally {
IOUtils.closeQuietly(output);
}
}
private static BufferedOutputStream createAppendableStream(File destination)
throws FileNotFoundException {
return new BufferedOutputStream(new FileOutputStream(destination, true));
}
private static void appendFile(OutputStream output, File source)
throws IOException {
InputStream input = null;
try {
input = new BufferedInputStream(new FileInputStream(source));
IOUtils.copy(input, output);
} finally {
IOUtils.closeQuietly(input);
}
}
}