001/* 002 * (C) Copyright 2015-2020 Nuxeo (http://nuxeo.com/) and others. 003 * 004 * Licensed under the Apache License, Version 2.0 (the "License"); 005 * you may not use this file except in compliance with the License. 006 * You may obtain a copy of the License at 007 * 008 * http://www.apache.org/licenses/LICENSE-2.0 009 * 010 * Unless required by applicable law or agreed to in writing, software 011 * distributed under the License is distributed on an "AS IS" BASIS, 012 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 013 * See the License for the specific language governing permissions and 014 * limitations under the License. 015 * 016 * Contributors: 017 * Antoine Taillefer <ataillefer@nuxeo.com> 018 * Luís Duarte 019 * Florent Guillaume 020 * Mickaël Schoentgen 021 */ 022package org.nuxeo.ecm.restapi.server.jaxrs; 023 024import java.io.File; 025import java.io.IOException; 026import java.io.InputStream; 027import java.io.UnsupportedEncodingException; 028import java.net.URLDecoder; 029import java.util.ArrayList; 030import java.util.Collections; 031import java.util.HashMap; 032import java.util.List; 033import java.util.Map; 034import java.util.Set; 035import java.util.stream.Collectors; 036 037import javax.mail.MessagingException; 038import javax.servlet.http.HttpServletRequest; 039import javax.servlet.http.HttpServletResponse; 040import javax.ws.rs.DELETE; 041import javax.ws.rs.GET; 042import javax.ws.rs.POST; 043import javax.ws.rs.Path; 044import javax.ws.rs.PathParam; 045import javax.ws.rs.Produces; 046import javax.ws.rs.core.Context; 047import javax.ws.rs.core.MediaType; 048import javax.ws.rs.core.Response; 049import javax.ws.rs.core.Response.Status; 050import javax.ws.rs.core.Response.StatusType; 051 052import org.apache.commons.collections.CollectionUtils; 053import org.apache.commons.lang3.StringUtils; 054import org.apache.commons.lang3.math.NumberUtils; 055import org.apache.commons.logging.Log; 056import org.apache.commons.logging.LogFactory; 057import org.nuxeo.ecm.automation.OperationContext; 058import org.nuxeo.ecm.automation.jaxrs.io.operations.ExecutionRequest; 059import org.nuxeo.ecm.automation.server.jaxrs.ResponseHelper; 060import org.nuxeo.ecm.automation.server.jaxrs.batch.Batch; 061import org.nuxeo.ecm.automation.server.jaxrs.batch.BatchFileEntry; 062import org.nuxeo.ecm.automation.server.jaxrs.batch.BatchHandler; 063import org.nuxeo.ecm.automation.server.jaxrs.batch.BatchManager; 064import org.nuxeo.ecm.automation.server.jaxrs.batch.BatchManagerConstants; 065import org.nuxeo.ecm.automation.server.jaxrs.batch.handler.BatchFileInfo; 066import org.nuxeo.ecm.core.api.Blob; 067import org.nuxeo.ecm.core.api.Blobs; 068import org.nuxeo.ecm.core.api.CoreSession; 069import org.nuxeo.ecm.core.api.NuxeoException; 070import org.nuxeo.ecm.core.api.impl.blob.FileBlob; 071import org.nuxeo.ecm.core.io.NginxConstants; 072import org.nuxeo.ecm.platform.web.common.RequestContext; 073import org.nuxeo.ecm.webengine.model.WebObject; 074import org.nuxeo.ecm.webengine.model.exceptions.IllegalParameterException; 075import org.nuxeo.ecm.webengine.model.impl.AbstractResource; 076import org.nuxeo.ecm.webengine.model.impl.ResourceTypeImpl; 077import org.nuxeo.runtime.api.Framework; 078import org.nuxeo.runtime.transaction.TransactionHelper; 079 080import com.fasterxml.jackson.databind.JsonNode; 081import com.fasterxml.jackson.databind.ObjectMapper; 082 083/** 084 * Batch upload endpoint. 085 * <p> 086 * Provides the APIs listed below: 087 * <ul> 088 * <li>POST /upload, see {@link #initBatch()}</li> 089 * <li>POST /upload/{batchId}/{fileIdx}, see {@link #upload(HttpServletRequest, String, String)}</li> 090 * <li>GET /upload/{batchId}, see {@link #getBatchInfo(String)}</li> 091 * <li>GET /upload/{batchId}/{fileIdx}, see {@link #getFileInfo(String, String)}</li> 092 * <li>POST /upload/{batchId}/execute/{operationId}, see {@link #execute(String, String, ExecutionRequest)}</li> 093 * <li>POST /upload/{batchId}/refreshToken, see {@link #refreshToken(String)}</li> 094 * <li>POST /upload/{batchId}/{fileIdx}/execute/{operationId}, see 095 * {@link #execute(String, String, String, ExecutionRequest)}</li> 096 * <li>DELETE /upload/{batchId}, see {@link #cancel(String)}</li> 097 * <li>DELETE /upload/{batchId}/{fileIdx}, see {@link #removeFile(String, String)}</li> 098 * </ul> 099 * Largely inspired by the excellent Google Drive REST API documentation about 100 * <a href="https://developers.google.com/drive/web/manage-uploads#resumable">resumable upload</a>. 101 * 102 * @since 7.4 103 */ 104@WebObject(type = "upload") 105public class BatchUploadObject extends AbstractResource<ResourceTypeImpl> { 106 107 protected static final Log log = LogFactory.getLog(BatchUploadObject.class); 108 109 protected static final String REQUEST_BATCH_ID = "batchId"; 110 111 protected static final String REQUEST_FILE_IDX = "fileIdx"; 112 113 protected static final String OPERATION_ID = "operationId"; 114 115 protected static final String REQUEST_HANDLER_NAME = "handlerName"; 116 117 public static final String UPLOAD_TYPE_NORMAL = "normal"; 118 119 public static final String UPLOAD_TYPE_CHUNKED = "chunked"; 120 121 public static final String KEY = "key"; 122 123 public static final String NAME = "name"; 124 125 public static final String MIMETYPE = "mimeType"; 126 127 public static final String FILE_SIZE = "fileSize"; 128 129 public static final String MD5 = "md5"; 130 131 protected Map<String, String> mapWithName(String name) { 132 return Collections.singletonMap("name", name); 133 } 134 135 @GET 136 @Path("handlers") 137 public Response handlers() throws IOException { 138 BatchManager bm = Framework.getService(BatchManager.class); 139 Set<String> supportedHandlers = bm.getSupportedHandlers(); 140 List<Map<String, String>> handlers = supportedHandlers.stream().map(this::mapWithName).collect( 141 Collectors.toList()); 142 Map<String, Object> result = Collections.singletonMap("handlers", handlers); 143 return buildResponse(Status.OK, result); 144 } 145 146 @GET 147 @Path("handlers/{handlerName}") 148 public Response getHandlerInfo(@PathParam(REQUEST_HANDLER_NAME) String handlerName) throws IOException { 149 BatchManager bm = Framework.getService(BatchManager.class); 150 BatchHandler handler = bm.getHandler(handlerName); 151 if (handler == null) { 152 return Response.status(Status.NOT_FOUND).build(); 153 } 154 Map<String, String> result = mapWithName(handler.getName()); 155 return buildResponse(Status.OK, result); 156 } 157 158 @POST 159 @Path("new/{handlerName}") 160 public Response createNewBatch(@PathParam(REQUEST_HANDLER_NAME) String handlerName) throws IOException { 161 BatchManager bm = Framework.getService(BatchManager.class); 162 Batch batch = bm.initBatch(handlerName); 163 return getBatchExtraInfo(batch.getKey()); 164 } 165 166 @POST 167 public Response initBatch() throws IOException { 168 BatchManager bm = Framework.getService(BatchManager.class); 169 String batchId = bm.initBatch(); 170 Map<String, String> result = new HashMap<>(); 171 result.put("batchId", batchId); 172 return buildResponse(Status.CREATED, result); 173 } 174 175 @POST 176 @Path("{batchId}/{fileIdx}") 177 public Response upload(@Context HttpServletRequest request, @PathParam(REQUEST_BATCH_ID) String batchId, 178 @PathParam(REQUEST_FILE_IDX) String fileIdx) throws IOException { 179 TransactionHelper.commitOrRollbackTransaction(); 180 try { 181 return uploadNoTransaction(request, batchId, fileIdx); 182 } finally { 183 TransactionHelper.startTransaction(); 184 } 185 } 186 187 protected Response uploadNoTransaction(@Context HttpServletRequest request, 188 @PathParam(REQUEST_BATCH_ID) String batchId, @PathParam(REQUEST_FILE_IDX) String fileIdx) 189 throws IOException { 190 BatchManager bm = Framework.getService(BatchManager.class); 191 192 if (!bm.hasBatch(batchId)) { 193 return buildEmptyResponse(Status.NOT_FOUND); 194 } 195 196 // Check file index parameter 197 if (!NumberUtils.isDigits(fileIdx)) { 198 return buildTextResponse(Status.BAD_REQUEST, "fileIdx request path parameter must be a number"); 199 } 200 201 // Parameters are passed as request header, the request body is the stream 202 String uploadType = request.getHeader("X-Upload-Type"); 203 // Use non chunked mode by default if X-Upload-Type header is not provided 204 if (!UPLOAD_TYPE_CHUNKED.equals(uploadType)) { 205 uploadType = UPLOAD_TYPE_NORMAL; 206 } 207 String uploadChunkIndexHeader = request.getHeader("X-Upload-Chunk-Index"); 208 String chunkCountHeader = request.getHeader("X-Upload-Chunk-Count"); 209 String fileName = request.getHeader("X-File-Name"); 210 String fileSizeHeader = request.getHeader("X-File-Size"); 211 String mimeType = request.getHeader("X-File-Type"); 212 String requestBodyFile = request.getHeader(NginxConstants.X_REQUEST_BODY_FILE_HEADER); 213 String contentMd5 = request.getHeader(NginxConstants.X_CONTENT_MD5_HEADER); 214 215 int chunkCount = -1; 216 int uploadChunkIndex = -1; 217 long fileSize = -1; 218 if (UPLOAD_TYPE_CHUNKED.equals(uploadType)) { 219 try { 220 chunkCount = Integer.parseInt(chunkCountHeader); 221 uploadChunkIndex = Integer.parseInt(uploadChunkIndexHeader); 222 fileSize = Long.parseLong(fileSizeHeader); 223 } catch (NumberFormatException e) { 224 throw new IllegalParameterException( 225 "X-Upload-Chunk-Index, X-Upload-Chunk-Count and X-File-Size headers must be numbers"); 226 } 227 } 228 229 // TODO NXP-18247: should be set to the actual number of bytes uploaded instead of relying on the Content-Length 230 // header which is not necessarily set 231 long uploadedSize = getUploadedSize(request); 232 if (Framework.isBooleanPropertyTrue(NginxConstants.X_ACCEL_ENABLED) 233 && StringUtils.isNotEmpty(requestBodyFile)) { 234 if (StringUtils.isNotEmpty(fileName)) { 235 fileName = URLDecoder.decode(fileName, "UTF-8"); 236 } 237 File file = new File(requestBodyFile); 238 Blob blob = new FileBlob(file, true); 239 240 if (StringUtils.isNotEmpty(contentMd5)) { 241 blob.setDigest(contentMd5); 242 } 243 244 uploadedSize = file.length(); 245 addBlob(uploadType, batchId, fileIdx, blob, fileName, mimeType, uploadedSize, chunkCount, uploadChunkIndex, 246 fileSize); 247 } else { 248 if (StringUtils.isNotEmpty(fileName)) { 249 fileName = URLDecoder.decode(fileName, "UTF-8"); 250 } 251 try (InputStream is = request.getInputStream()) { 252 Blob blob = Blobs.createBlob(is); 253 addBlob(uploadType, batchId, fileIdx, blob, fileName, mimeType, uploadedSize, chunkCount, 254 uploadChunkIndex, fileSize); 255 } 256 } 257 258 StatusType status = Status.CREATED; 259 Map<String, Object> result = new HashMap<>(); 260 result.put("uploaded", "true"); 261 result.put("batchId", batchId); 262 result.put("fileIdx", fileIdx); 263 result.put("uploadType", uploadType); 264 result.put("uploadedSize", String.valueOf(uploadedSize)); 265 if (UPLOAD_TYPE_CHUNKED.equals(uploadType)) { 266 BatchFileEntry fileEntry = bm.getFileEntry(batchId, fileIdx); 267 if (fileEntry != null) { 268 result.put("uploadedChunkIds", fileEntry.getOrderedChunkIndexes()); 269 result.put("chunkCount", fileEntry.getChunkCount()); 270 if (!fileEntry.isChunksCompleted()) { 271 status = Status.ACCEPTED; 272 } 273 } 274 } 275 return buildResponse(status, result); 276 } 277 278 protected long getUploadedSize(HttpServletRequest request) { 279 String contentLength = request.getHeader("Content-Length"); 280 if (contentLength == null) { 281 return -1; 282 } 283 return Long.parseLong(contentLength); 284 } 285 286 protected void addBlob(String uploadType, String batchId, String fileIdx, Blob blob, String fileName, 287 String mimeType, long uploadedSize, int chunkCount, int uploadChunkIndex, long fileSize) { 288 BatchManager bm = Framework.getService(BatchManager.class); 289 String uploadedSizeDisplay = uploadedSize > -1 ? uploadedSize + "b" : "unknown size"; 290 Batch batch = bm.getBatch(batchId); 291 if (UPLOAD_TYPE_CHUNKED.equals(uploadType)) { 292 if (log.isDebugEnabled()) { 293 log.debug(String.format("Uploading chunk [index=%d / total=%d] (%s) for file %s", uploadChunkIndex, 294 chunkCount, uploadedSizeDisplay, fileName)); 295 } 296 batch.addChunk(fileIdx, blob, chunkCount, uploadChunkIndex, fileName, mimeType, fileSize); 297 } else { 298 if (log.isDebugEnabled()) { 299 log.debug(String.format("Uploading file %s (%s)", fileName, uploadedSizeDisplay)); 300 } 301 batch.addFile(fileIdx, blob, fileName, mimeType); 302 } 303 } 304 305 @GET 306 @Path("{batchId}") 307 public Response getBatchInfo(@PathParam(REQUEST_BATCH_ID) String batchId) throws IOException { 308 BatchManager bm = Framework.getService(BatchManager.class); 309 if (!bm.hasBatch(batchId)) { 310 return buildEmptyResponse(Status.NOT_FOUND); 311 } 312 List<BatchFileEntry> fileEntries = bm.getFileEntries(batchId); 313 if (CollectionUtils.isEmpty(fileEntries)) { 314 return buildEmptyResponse(Status.NO_CONTENT); 315 } 316 List<Map<String, Object>> result = new ArrayList<>(); 317 for (BatchFileEntry fileEntry : fileEntries) { 318 result.add(getFileInfo(fileEntry)); 319 } 320 return buildResponse(Status.OK, result); 321 } 322 323 @GET 324 @Path("{batchId}/{fileIdx}") 325 public Response getFileInfo(@PathParam(REQUEST_BATCH_ID) String batchId, 326 @PathParam(REQUEST_FILE_IDX) String fileIdx) throws IOException { 327 BatchManager bm = Framework.getService(BatchManager.class); 328 if (!bm.hasBatch(batchId)) { 329 return buildEmptyResponse(Status.NOT_FOUND); 330 } 331 BatchFileEntry fileEntry = bm.getFileEntry(batchId, fileIdx); 332 if (fileEntry == null) { 333 return buildEmptyResponse(Status.NOT_FOUND); 334 } 335 StatusType status = Status.OK; 336 if (fileEntry.isChunked() && !fileEntry.isChunksCompleted()) { 337 status = Status.ACCEPTED; 338 } 339 Map<String, Object> result = getFileInfo(fileEntry); 340 return buildResponse(status, result); 341 } 342 343 @DELETE 344 @Path("{batchId}") 345 public Response cancel(@PathParam(REQUEST_BATCH_ID) String batchId) { 346 BatchManager bm = Framework.getService(BatchManager.class); 347 if (!bm.hasBatch(batchId)) { 348 return buildEmptyResponse(Status.NOT_FOUND); 349 } 350 bm.clean(batchId); 351 return buildEmptyResponse(Status.NO_CONTENT); 352 } 353 354 /** 355 * @since 8.4 356 */ 357 @DELETE 358 @Path("{batchId}/{fileIdx}") 359 public Response removeFile(@PathParam(REQUEST_BATCH_ID) String batchId, 360 @PathParam(REQUEST_FILE_IDX) String fileIdx) { 361 BatchManager bm = Framework.getService(BatchManager.class); 362 if (!bm.removeFileEntry(batchId, fileIdx)) { 363 return buildEmptyResponse(Status.NOT_FOUND); 364 } 365 return buildEmptyResponse(Status.NO_CONTENT); 366 } 367 368 @Context 369 protected HttpServletRequest request; 370 371 @Context 372 protected HttpServletResponse response; 373 374 @POST 375 @Produces(MediaType.APPLICATION_JSON) 376 @Path("{batchId}/execute/{operationId}") 377 public Object execute(@PathParam(REQUEST_BATCH_ID) String batchId, @PathParam(OPERATION_ID) String operationId, 378 ExecutionRequest xreq) { 379 return executeBatch(batchId, null, operationId, request, xreq); 380 } 381 382 @POST 383 @Produces(MediaType.APPLICATION_JSON) 384 @Path("{batchId}/{fileIdx}/execute/{operationId}") 385 public Object execute(@PathParam(REQUEST_BATCH_ID) String batchId, @PathParam(REQUEST_FILE_IDX) String fileIdx, 386 @PathParam(OPERATION_ID) String operationId, ExecutionRequest xreq) { 387 return executeBatch(batchId, fileIdx, operationId, request, xreq); 388 } 389 390 @GET 391 @Path("{batchId}/info") 392 public Response getBatchExtraInfo(@PathParam(REQUEST_BATCH_ID) String batchId) throws IOException { 393 BatchManager bm = Framework.getService(BatchManager.class); 394 if (!bm.hasBatch(batchId)) { 395 return buildEmptyResponse(Status.NOT_FOUND); 396 } 397 Batch batch = bm.getBatch(batchId); 398 Map<String, Object> properties = batch.getProperties(); 399 List<BatchFileEntry> fileEntries = batch.getFileEntries(); 400 401 List<Map<String, Object>> fileInfos = new ArrayList<>(); 402 if (!CollectionUtils.isEmpty(fileEntries)) { 403 fileEntries.stream().map(this::getFileInfo).forEach(fileInfos::add); 404 } 405 406 Map<String, Object> result = new HashMap<>(); 407 result.put("provider", batch.getHandlerName()); 408 if (properties != null && !properties.isEmpty()) { 409 result.put("extraInfo", properties); 410 } 411 412 result.put("fileEntries", fileInfos); 413 result.put("batchId", batch.getKey()); 414 return buildResponse(Status.OK, result); 415 } 416 417 /** @since 11.1 */ 418 @POST 419 @Path("{batchId}/refreshToken") 420 public Response refreshToken(@PathParam(REQUEST_BATCH_ID) String batchId) throws IOException { 421 BatchManager bm = Framework.getService(BatchManager.class); 422 423 Batch batch = bm.getBatch(batchId); 424 if (batch == null) { 425 return buildEmptyResponse(Status.NOT_FOUND); 426 } 427 428 BatchHandler handler = bm.getHandler(batch.getHandlerName()); 429 try { 430 Map<String, Object> result = handler.refreshToken(batchId); 431 result.put("batchId", batchId); 432 return buildResponse(Status.OK, result); 433 } catch (UnsupportedOperationException e) { 434 return Response.status(HttpServletResponse.SC_NOT_IMPLEMENTED).build(); 435 } 436 } 437 438 @POST 439 @Path("{batchId}/{fileIdx}/complete") 440 public Response uploadCompleted(@PathParam(REQUEST_BATCH_ID) String batchId, 441 @PathParam(REQUEST_FILE_IDX) String fileIdx, String body) throws IOException { 442 BatchManager bm = Framework.getService(BatchManager.class); 443 JsonNode jsonNode = new ObjectMapper().readTree(body); 444 445 Batch batch = bm.getBatch(batchId); 446 if (batch == null) { 447 return buildEmptyResponse(Status.NOT_FOUND); 448 } 449 450 String key = jsonNode.hasNonNull(KEY) ? jsonNode.get(KEY).asText(null) : null; 451 String filename = jsonNode.hasNonNull(NAME) ? jsonNode.get(NAME).asText() : null; 452 String mimeType = jsonNode.hasNonNull(MIMETYPE) ? jsonNode.get(MIMETYPE).asText(null) : null; 453 Long length = jsonNode.hasNonNull(FILE_SIZE) ? jsonNode.get(FILE_SIZE).asLong() : -1L; 454 String md5 = jsonNode.hasNonNull(MD5) ? jsonNode.get(MD5).asText() : null; 455 456 BatchFileInfo batchFileInfo = new BatchFileInfo(key, filename, mimeType, length, md5); 457 458 BatchHandler handler = bm.getHandler(batch.getHandlerName()); 459 if (!handler.completeUpload(batchId, fileIdx, batchFileInfo)) { 460 return Response.status(Status.CONFLICT).build(); 461 } 462 463 Map<String, Object> result = new HashMap<>(); 464 result.put("uploaded", "true"); 465 result.put("batchId", batchId); 466 result.put("fileIdx", fileIdx); 467 return buildResponse(Status.OK, result); 468 } 469 470 @SuppressWarnings("resource") // ExecutionRequest's OperationContext not owned by us, don't close it 471 protected Object executeBatch(String batchId, String fileIdx, String operationId, HttpServletRequest request, 472 ExecutionRequest xreq) { 473 BatchManager bm = Framework.getService(BatchManager.class); 474 475 if (!bm.hasBatch(batchId)) { 476 return buildEmptyResponse(Status.NOT_FOUND); 477 } 478 479 if (!Boolean.parseBoolean( 480 RequestContext.getActiveContext(request).getRequest().getHeader(BatchManagerConstants.NO_DROP_FLAG))) { 481 RequestContext.getActiveContext(request).addRequestCleanupHandler(req -> bm.clean(batchId)); 482 } 483 484 try { 485 CoreSession session = ctx.getCoreSession(); 486 // ExecutionRequest's OperationContext not owned by us, don't close it 487 OperationContext ctx = xreq.createContext(request, response, session); // NOSONAR 488 Map<String, Object> params = xreq.getParams(); 489 Object result; 490 if (StringUtils.isBlank(fileIdx)) { 491 result = bm.execute(batchId, operationId, session, ctx, params); 492 } else { 493 result = bm.execute(batchId, fileIdx, operationId, session, ctx, params); 494 } 495 return ResponseHelper.getResponse(result, request); 496 } catch (MessagingException | IOException e) { 497 log.error("Error while executing automation batch ", e); 498 throw new NuxeoException(e); 499 } 500 } 501 502 protected Response buildResponse(StatusType status, Object object) throws IOException { 503 ObjectMapper mapper = new ObjectMapper(); 504 String result = mapper.writeValueAsString(object); 505 return buildResponse(status, MediaType.APPLICATION_JSON, result); 506 } 507 508 /** 509 * @deprecated since 11.2, not used anymore 510 */ 511 @Deprecated(since = "11.2") 512 protected Response buildResponse(StatusType status, Object object, boolean html) throws IOException { 513 ObjectMapper mapper = new ObjectMapper(); 514 String result = mapper.writeValueAsString(object); 515 if (html) { 516 // For MSIE with iframe transport: we need to return HTML! 517 return buildHTMLResponse(status, result); 518 } else { 519 return buildJSONResponse(status, result); 520 } 521 } 522 523 /** 524 * @deprecated since 11.2, not used anymore 525 */ 526 @Deprecated(since = "11.2") 527 protected Response buildJSONResponse(StatusType status, String message) throws UnsupportedEncodingException { 528 return buildResponse(status, MediaType.APPLICATION_JSON, message); 529 } 530 531 /** 532 * @deprecated since 11.2, not used anymore 533 */ 534 @Deprecated(since = "11.2") 535 protected Response buildHTMLResponse(StatusType status, String message) throws UnsupportedEncodingException { 536 message = "<html>" + message + "</html>"; 537 return buildResponse(status, MediaType.TEXT_HTML, message); 538 } 539 540 protected Response buildTextResponse(StatusType status, String message) throws UnsupportedEncodingException { 541 return buildResponse(status, MediaType.TEXT_PLAIN, message); 542 } 543 544 protected Response buildEmptyResponse(StatusType status) { 545 return Response.status(status).build(); 546 } 547 548 protected Response buildResponse(StatusType status, String type, String message) 549 throws UnsupportedEncodingException { 550 return Response.status(status) 551 .header("Content-Length", message.getBytes("UTF-8").length) 552 .type(type + "; charset=UTF-8") 553 .entity(message) 554 .build(); 555 } 556 557 protected Map<String, Object> getFileInfo(BatchFileEntry fileEntry) { 558 Map<String, Object> info = new HashMap<>(); 559 boolean chunked = fileEntry.isChunked(); 560 String uploadType; 561 if (chunked) { 562 uploadType = UPLOAD_TYPE_CHUNKED; 563 } else { 564 uploadType = UPLOAD_TYPE_NORMAL; 565 } 566 info.put("name", fileEntry.getFileName()); 567 info.put("size", fileEntry.getFileSize()); 568 info.put("uploadType", uploadType); 569 if (chunked) { 570 info.put("uploadedChunkIds", fileEntry.getOrderedChunkIndexes()); 571 info.put("chunkCount", fileEntry.getChunkCount()); 572 } 573 return info; 574 } 575 576}