显示上传百分比的替代方法
Alternative way to show upload percentage
由于错误 https://github.com/codenameone/CodenameOne/issues/3043,我不知道如何在使用 MultipartRequest
时显示上传百分比。你有什么建议,比如显示百分比的替代方法吗?谢谢
目前还没有,因为没有对问题所在进行评估。我认为进度侦听器跟踪写入上传代码的输出流而不是实际连接时间,这在 Java 中通常很难跟踪。
例如在 Java SE 中,您将打开一个 URL,然后写入 POST 连接的输出流。然后当您尝试获取输入流响应时,写入实际上会发生。但在这一点上,我没有关于上传状态的指示,因为它是完全抽象的并且在幕后发生。
所以我不确定这在技术上是否可行。
我解决了这个问题,解决了它。在花了几天时间尝试仅客户端解决方案之后,最终我得到了一个涉及客户端代码(代号一)和服务器代码(Spring 引导)的解决方案。
基本上,在客户端我把要上传的文件分成100kb的小块,一个一个上传,所以我可以准确计算上传百分比。在服务器上,我放置了一个控制器来接收小文件,另一个控制器来合并它们。我知道我的代码特定于我的用例(将图像和视频发送到 Cloudinary),但是我复制了一些相关部分,这些部分可能会启发其他与代号一有类似问题的人。
截图("Caricamento"表示"Uploading","Annulla"表示"Cancel"):
客户代码
服务器class
/**
* 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 class
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);
服务器代码
CloudinaryControllerclass
/**
* 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);
}
CloudinaryServiceclass
/**
* 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 class
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:
*/
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);
}
}
}
由于错误 https://github.com/codenameone/CodenameOne/issues/3043,我不知道如何在使用 MultipartRequest
时显示上传百分比。你有什么建议,比如显示百分比的替代方法吗?谢谢
目前还没有,因为没有对问题所在进行评估。我认为进度侦听器跟踪写入上传代码的输出流而不是实际连接时间,这在 Java 中通常很难跟踪。
例如在 Java SE 中,您将打开一个 URL,然后写入 POST 连接的输出流。然后当您尝试获取输入流响应时,写入实际上会发生。但在这一点上,我没有关于上传状态的指示,因为它是完全抽象的并且在幕后发生。
所以我不确定这在技术上是否可行。
我解决了这个问题,解决了它。在花了几天时间尝试仅客户端解决方案之后,最终我得到了一个涉及客户端代码(代号一)和服务器代码(Spring 引导)的解决方案。
基本上,在客户端我把要上传的文件分成100kb的小块,一个一个上传,所以我可以准确计算上传百分比。在服务器上,我放置了一个控制器来接收小文件,另一个控制器来合并它们。我知道我的代码特定于我的用例(将图像和视频发送到 Cloudinary),但是我复制了一些相关部分,这些部分可能会启发其他与代号一有类似问题的人。
截图("Caricamento"表示"Uploading","Annulla"表示"Cancel"):
客户代码
服务器class
/**
* 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 class
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);
服务器代码
CloudinaryControllerclass
/**
* 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);
}
CloudinaryServiceclass
/**
* 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 class
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:
*/
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);
}
}
}