001/* 002 * (C) Copyright 2015-2018 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 <[email protected]> 018 * Luís Duarte 019 * Florent Guillaume 020 */ 021package org.nuxeo.ecm.restapi.server.jaxrs; 022 023import java.io.File; 024import java.io.IOException; 025import java.io.InputStream; 026import java.io.UnsupportedEncodingException; 027import java.net.URLDecoder; 028import java.util.ArrayList; 029import java.util.Collections; 030import java.util.HashMap; 031import java.util.List; 032import java.util.Map; 033import java.util.Set; 034import java.util.stream.Collectors; 035 036import javax.mail.MessagingException; 037import javax.servlet.http.HttpServletRequest; 038import javax.servlet.http.HttpServletResponse; 039import javax.ws.rs.DELETE; 040import javax.ws.rs.GET; 041import javax.ws.rs.POST; 042import javax.ws.rs.Path; 043import javax.ws.rs.PathParam; 044import javax.ws.rs.Produces; 045import javax.ws.rs.core.Context; 046import javax.ws.rs.core.MediaType; 047import javax.ws.rs.core.Response; 048import javax.ws.rs.core.Response.Status; 049import javax.ws.rs.core.Response.Status.Family; 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.webengine.forms.FormData; 073import org.nuxeo.ecm.webengine.jaxrs.context.RequestContext; 074import org.nuxeo.ecm.webengine.model.WebObject; 075import org.nuxeo.ecm.webengine.model.exceptions.IllegalParameterException; 076import org.nuxeo.ecm.webengine.model.impl.AbstractResource; 077import org.nuxeo.ecm.webengine.model.impl.ResourceTypeImpl; 078import org.nuxeo.runtime.api.Framework; 079import org.nuxeo.runtime.transaction.TransactionHelper; 080 081import com.fasterxml.jackson.databind.JsonNode; 082import com.fasterxml.jackson.databind.ObjectMapper; 083 084/** 085 * Batch upload endpoint. 086 * <p> 087 * Provides the APIs listed below: 088 * <ul> 089 * <li>POST /upload, see {@link #initBatch()}</li> 090 * <li>POST /upload/{batchId}/{fileIdx}, see {@link #upload(HttpServletRequest, String, String)}</li> 091 * <li>GET /upload/{batchId}, see {@link #getBatchInfo(String)}</li> 092 * <li>GET /upload/{batchId}/{fileIdx}, see {@link #getFileInfo(String, String)}</li> 093 * <li>POST /upload/{batchId}/execute/{operationId}, see {@link #execute(String, String, ExecutionRequest)}</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 contentType = request.getHeader("Content-Type"); 203 String uploadType = request.getHeader("X-Upload-Type"); 204 // Use non chunked mode by default if X-Upload-Type header is not provided 205 if (!UPLOAD_TYPE_CHUNKED.equals(uploadType)) { 206 uploadType = UPLOAD_TYPE_NORMAL; 207 } 208 String uploadChunkIndexHeader = request.getHeader("X-Upload-Chunk-Index"); 209 String chunkCountHeader = request.getHeader("X-Upload-Chunk-Count"); 210 String fileName = request.getHeader("X-File-Name"); 211 String fileSizeHeader = request.getHeader("X-File-Size"); 212 String mimeType = request.getHeader("X-File-Type"); 213 String requestBodyFile = request.getHeader(NginxConstants.X_REQUEST_BODY_FILE_HEADER); 214 String contentMd5 = request.getHeader(NginxConstants.X_CONTENT_MD5_HEADER); 215 216 int chunkCount = -1; 217 int uploadChunkIndex = -1; 218 long fileSize = -1; 219 if (UPLOAD_TYPE_CHUNKED.equals(uploadType)) { 220 try { 221 chunkCount = Integer.parseInt(chunkCountHeader); 222 uploadChunkIndex = Integer.parseInt(uploadChunkIndexHeader); 223 fileSize = Long.parseLong(fileSizeHeader); 224 } catch (NumberFormatException e) { 225 throw new IllegalParameterException( 226 "X-Upload-Chunk-Index, X-Upload-Chunk-Count and X-File-Size headers must be numbers"); 227 } 228 } 229 230 // TODO NXP-18247: should be set to the actual number of bytes uploaded instead of relying on the Content-Length 231 // header which is not necessarily set 232 long uploadedSize = getUploadedSize(request); 233 boolean isMultipart = contentType != null && contentType.contains("multipart"); 234 235 // Handle multipart case: mainly MSIE with jQueryFileupload 236 if (isMultipart) { 237 FormData formData = new FormData(request); 238 Blob blob = formData.getFirstBlob(); 239 if (blob == null) { 240 throw new NuxeoException("Cannot upload in multipart with no blobs"); 241 } 242 if (!UPLOAD_TYPE_CHUNKED.equals(uploadType)) { 243 fileName = blob.getFilename(); 244 } 245 // Don't change the mime-type if it was forced via the X-File-Type header 246 if (StringUtils.isBlank(mimeType)) { 247 mimeType = blob.getMimeType(); 248 } 249 uploadedSize = blob.getLength(); 250 addBlob(uploadType, batchId, fileIdx, blob, fileName, mimeType, uploadedSize, chunkCount, uploadChunkIndex, 251 fileSize); 252 } else if (Framework.isBooleanPropertyTrue(NginxConstants.X_ACCEL_ENABLED) 253 && StringUtils.isNotEmpty(requestBodyFile)) { 254 if (StringUtils.isNotEmpty(fileName)) { 255 fileName = URLDecoder.decode(fileName, "UTF-8"); 256 } 257 File file = new File(requestBodyFile); 258 Blob blob = new FileBlob(file, true); 259 260 if (StringUtils.isNotEmpty(contentMd5)) { 261 blob.setDigest(contentMd5); 262 } 263 264 uploadedSize = file.length(); 265 addBlob(uploadType, batchId, fileIdx, blob, fileName, mimeType, uploadedSize, chunkCount, uploadChunkIndex, 266 fileSize); 267 } else { 268 if (StringUtils.isNotEmpty(fileName)) { 269 fileName = URLDecoder.decode(fileName, "UTF-8"); 270 } 271 try (InputStream is = request.getInputStream()) { 272 Blob blob = Blobs.createBlob(is); 273 addBlob(uploadType, batchId, fileIdx, blob, fileName, mimeType, uploadedSize, chunkCount, 274 uploadChunkIndex, fileSize); 275 } 276 } 277 278 StatusType status = Status.CREATED; 279 Map<String, Object> result = new HashMap<>(); 280 result.put("uploaded", "true"); 281 result.put("batchId", batchId); 282 result.put("fileIdx", fileIdx); 283 result.put("uploadType", uploadType); 284 result.put("uploadedSize", String.valueOf(uploadedSize)); 285 if (UPLOAD_TYPE_CHUNKED.equals(uploadType)) { 286 BatchFileEntry fileEntry = bm.getFileEntry(batchId, fileIdx); 287 if (fileEntry != null) { 288 result.put("uploadedChunkIds", fileEntry.getOrderedChunkIndexes()); 289 result.put("chunkCount", fileEntry.getChunkCount()); 290 if (!fileEntry.isChunksCompleted()) { 291 status = new ResumeIncompleteStatusType(); 292 } 293 } 294 } 295 return buildResponse(status, result, isMultipart); 296 } 297 298 protected long getUploadedSize(HttpServletRequest request) { 299 String contentLength = request.getHeader("Content-Length"); 300 if (contentLength == null) { 301 return -1; 302 } 303 return Long.parseLong(contentLength); 304 } 305 306 protected void addBlob(String uploadType, String batchId, String fileIdx, Blob blob, String fileName, 307 String mimeType, long uploadedSize, int chunkCount, int uploadChunkIndex, long fileSize) { 308 BatchManager bm = Framework.getService(BatchManager.class); 309 String uploadedSizeDisplay = uploadedSize > -1 ? uploadedSize + "b" : "unknown size"; 310 Batch batch = bm.getBatch(batchId); 311 if (UPLOAD_TYPE_CHUNKED.equals(uploadType)) { 312 if (log.isDebugEnabled()) { 313 log.debug(String.format("Uploading chunk [index=%d / total=%d] (%s) for file %s", uploadChunkIndex, 314 chunkCount, uploadedSizeDisplay, fileName)); 315 } 316 batch.addChunk(fileIdx, blob, chunkCount, uploadChunkIndex, fileName, mimeType, fileSize); 317 } else { 318 if (log.isDebugEnabled()) { 319 log.debug(String.format("Uploading file %s (%s)", fileName, uploadedSizeDisplay)); 320 } 321 batch.addFile(fileIdx, blob, fileName, mimeType); 322 } 323 } 324 325 @GET 326 @Path("{batchId}") 327 public Response getBatchInfo(@PathParam(REQUEST_BATCH_ID) String batchId) throws IOException { 328 BatchManager bm = Framework.getService(BatchManager.class); 329 if (!bm.hasBatch(batchId)) { 330 return buildEmptyResponse(Status.NOT_FOUND); 331 } 332 List<BatchFileEntry> fileEntries = bm.getFileEntries(batchId); 333 if (CollectionUtils.isEmpty(fileEntries)) { 334 return buildEmptyResponse(Status.NO_CONTENT); 335 } 336 List<Map<String, Object>> result = new ArrayList<>(); 337 for (BatchFileEntry fileEntry : fileEntries) { 338 result.add(getFileInfo(fileEntry)); 339 } 340 return buildResponse(Status.OK, result); 341 } 342 343 @GET 344 @Path("{batchId}/{fileIdx}") 345 public Response getFileInfo(@PathParam(REQUEST_BATCH_ID) String batchId, 346 @PathParam(REQUEST_FILE_IDX) String fileIdx) throws IOException { 347 BatchManager bm = Framework.getService(BatchManager.class); 348 if (!bm.hasBatch(batchId)) { 349 return buildEmptyResponse(Status.NOT_FOUND); 350 } 351 BatchFileEntry fileEntry = bm.getFileEntry(batchId, fileIdx); 352 if (fileEntry == null) { 353 return buildEmptyResponse(Status.NOT_FOUND); 354 } 355 StatusType status = Status.OK; 356 if (fileEntry.isChunked() && !fileEntry.isChunksCompleted()) { 357 status = new ResumeIncompleteStatusType(); 358 } 359 Map<String, Object> result = getFileInfo(fileEntry); 360 return buildResponse(status, result); 361 } 362 363 @DELETE 364 @Path("{batchId}") 365 public Response cancel(@PathParam(REQUEST_BATCH_ID) String batchId) { 366 BatchManager bm = Framework.getService(BatchManager.class); 367 if (!bm.hasBatch(batchId)) { 368 return buildEmptyResponse(Status.NOT_FOUND); 369 } 370 bm.clean(batchId); 371 return buildEmptyResponse(Status.NO_CONTENT); 372 } 373 374 /** 375 * @since 8.4 376 */ 377 @DELETE 378 @Path("{batchId}/{fileIdx}") 379 public Response removeFile(@PathParam(REQUEST_BATCH_ID) String batchId, 380 @PathParam(REQUEST_FILE_IDX) String fileIdx) { 381 BatchManager bm = Framework.getService(BatchManager.class); 382 if (!bm.removeFileEntry(batchId, fileIdx)) { 383 return buildEmptyResponse(Status.NOT_FOUND); 384 } 385 return buildEmptyResponse(Status.NO_CONTENT); 386 } 387 388 @Context 389 protected HttpServletRequest request; 390 391 @Context 392 protected HttpServletResponse response; 393 394 @POST 395 @Produces(MediaType.APPLICATION_JSON) 396 @Path("{batchId}/execute/{operationId}") 397 public Object execute(@PathParam(REQUEST_BATCH_ID) String batchId, @PathParam(OPERATION_ID) String operationId, 398 ExecutionRequest xreq) { 399 return executeBatch(batchId, null, operationId, request, xreq); 400 } 401 402 @POST 403 @Produces(MediaType.APPLICATION_JSON) 404 @Path("{batchId}/{fileIdx}/execute/{operationId}") 405 public Object execute(@PathParam(REQUEST_BATCH_ID) String batchId, @PathParam(REQUEST_FILE_IDX) String fileIdx, 406 @PathParam(OPERATION_ID) String operationId, ExecutionRequest xreq) { 407 return executeBatch(batchId, fileIdx, operationId, request, xreq); 408 } 409 410 @GET 411 @Path("{batchId}/info") 412 public Response getBatchExtraInfo(@PathParam(REQUEST_BATCH_ID) String batchId) throws IOException { 413 BatchManager bm = Framework.getService(BatchManager.class); 414 if (!bm.hasBatch(batchId)) { 415 return buildEmptyResponse(Status.NOT_FOUND); 416 } 417 Batch batch = bm.getBatch(batchId); 418 Map<String, Object> properties = batch.getProperties(); 419 List<BatchFileEntry> fileEntries = batch.getFileEntries(); 420 421 List<Map<String, Object>> fileInfos = new ArrayList<>(); 422 if (!CollectionUtils.isEmpty(fileEntries)) { 423 fileEntries.stream().map(this::getFileInfo).forEach(fileInfos::add); 424 } 425 426 Map<String, Object> result = new HashMap<>(); 427 result.put("provider", batch.getHandlerName()); 428 if (properties != null && !properties.isEmpty()) { 429 result.put("extraInfo", properties); 430 } 431 432 result.put("fileEntries", fileInfos); 433 result.put("batchId", batch.getKey()); 434 return buildResponse(Status.OK, result); 435 } 436 437 @POST 438 @Path("{batchId}/{fileIdx}/complete") 439 public Response uploadCompleted(@PathParam(REQUEST_BATCH_ID) String batchId, 440 @PathParam(REQUEST_FILE_IDX) String fileIdx, String body) throws IOException { 441 BatchManager bm = Framework.getService(BatchManager.class); 442 JsonNode jsonNode = new ObjectMapper().readTree(body); 443 444 Batch batch = bm.getBatch(batchId); 445 if (batch == null) { 446 return buildEmptyResponse(Status.NOT_FOUND); 447 } 448 449 String key = jsonNode.hasNonNull(KEY) ? jsonNode.get(KEY).asText(null) : null; 450 String filename = jsonNode.hasNonNull(NAME) ? jsonNode.get(NAME).asText() : null; 451 String mimeType = jsonNode.hasNonNull(MIMETYPE) ? jsonNode.get(MIMETYPE).asText(null) : null; 452 Long length = jsonNode.hasNonNull(FILE_SIZE) ? jsonNode.get(FILE_SIZE).asLong() : -1L; 453 String md5 = jsonNode.hasNonNull(MD5) ? jsonNode.get(MD5).asText() : null; 454 455 BatchFileInfo batchFileInfo = new BatchFileInfo(key, filename, mimeType, length, md5); 456 457 BatchHandler handler = bm.getHandler(batch.getHandlerName()); 458 if (!handler.completeUpload(batchId, fileIdx, batchFileInfo)) { 459 return Response.status(Status.CONFLICT).build(); 460 } 461 462 Map<String, Object> result = new HashMap<>(); 463 result.put("uploaded", "true"); 464 result.put("batchId", batchId); 465 result.put("fileIdx", fileIdx); 466 return buildResponse(Status.OK, result); 467 } 468 469 protected Object executeBatch(String batchId, String fileIdx, String operationId, HttpServletRequest request, 470 ExecutionRequest xreq) { 471 BatchManager bm = Framework.getService(BatchManager.class); 472 473 if (!bm.hasBatch(batchId)) { 474 return buildEmptyResponse(Status.NOT_FOUND); 475 } 476 477 if (!Boolean.parseBoolean( 478 RequestContext.getActiveContext(request).getRequest().getHeader(BatchManagerConstants.NO_DROP_FLAG))) { 479 RequestContext.getActiveContext(request).addRequestCleanupHandler(req -> bm.clean(batchId)); 480 } 481 482 try { 483 CoreSession session = ctx.getCoreSession(); 484 Object result; 485 try (OperationContext ctx = xreq.createContext(request, response, session)) { 486 Map<String, Object> params = xreq.getParams(); 487 if (StringUtils.isBlank(fileIdx)) { 488 result = bm.execute(batchId, operationId, session, ctx, params); 489 } else { 490 result = bm.execute(batchId, fileIdx, operationId, session, ctx, params); 491 } 492 } 493 return ResponseHelper.getResponse(result, request); 494 } catch (MessagingException | IOException e) { 495 log.error("Error while executing automation batch ", e); 496 throw new NuxeoException(e); 497 } 498 } 499 500 protected Response buildResponse(StatusType status, Object object) throws IOException { 501 return buildResponse(status, object, false); 502 } 503 504 protected Response buildResponse(StatusType status, Object object, boolean html) throws IOException { 505 ObjectMapper mapper = new ObjectMapper(); 506 String result = mapper.writeValueAsString(object); 507 if (html) { 508 // For MSIE with iframe transport: we need to return HTML! 509 return buildHTMLResponse(status, result); 510 } else { 511 return buildJSONResponse(status, result); 512 } 513 } 514 515 protected Response buildJSONResponse(StatusType status, String message) throws UnsupportedEncodingException { 516 return buildResponse(status, MediaType.APPLICATION_JSON, message); 517 } 518 519 protected Response buildHTMLResponse(StatusType status, String message) throws UnsupportedEncodingException { 520 message = "<html>" + message + "</html>"; 521 return buildResponse(status, MediaType.TEXT_HTML, message); 522 } 523 524 protected Response buildTextResponse(StatusType status, String message) throws UnsupportedEncodingException { 525 return buildResponse(status, MediaType.TEXT_PLAIN, message); 526 } 527 528 protected Response buildEmptyResponse(StatusType status) { 529 return Response.status(status).build(); 530 } 531 532 protected Response buildResponse(StatusType status, String type, String message) 533 throws UnsupportedEncodingException { 534 return Response.status(status) 535 .header("Content-Length", message.getBytes("UTF-8").length) 536 .type(type + "; charset=UTF-8") 537 .entity(message) 538 .build(); 539 } 540 541 protected Map<String, Object> getFileInfo(BatchFileEntry fileEntry) { 542 Map<String, Object> info = new HashMap<>(); 543 boolean chunked = fileEntry.isChunked(); 544 String uploadType; 545 if (chunked) { 546 uploadType = UPLOAD_TYPE_CHUNKED; 547 } else { 548 uploadType = UPLOAD_TYPE_NORMAL; 549 } 550 info.put("name", fileEntry.getFileName()); 551 info.put("size", fileEntry.getFileSize()); 552 info.put("uploadType", uploadType); 553 if (chunked) { 554 info.put("uploadedChunkIds", fileEntry.getOrderedChunkIndexes()); 555 info.put("chunkCount", fileEntry.getChunkCount()); 556 } 557 return info; 558 } 559 560 public final class ResumeIncompleteStatusType implements StatusType { 561 562 @Override 563 public int getStatusCode() { 564 return 308; 565 } 566 567 @Override 568 public String getReasonPhrase() { 569 return "Resume Incomplete"; 570 } 571 572 @Override 573 public Family getFamily() { 574 // Technically we don't use 308 Resume Incomplete as a redirection but it is the default family for 3xx 575 // status codes defined by Response$Status 576 return Family.REDIRECTION; 577 } 578 } 579 580}