private void handleUploadPart(HttpServletRequest request,
            HttpServletResponse response, InputStream is, BlobStore blobStore,
            String containerName, String blobName, String uploadId)
            throws IOException, S3Exception {
        // TODO: duplicated from handlePutBlob
        String contentLengthString = null;
        String decodedContentLengthString = null;
        String contentMD5String = null;
        for (String headerName : Collections.list(request.getHeaderNames())) {
            String headerValue = Strings.nullToEmpty(request.getHeader(
                    headerName));
            if (headerName.equalsIgnoreCase(HttpHeaders.CONTENT_LENGTH)) {
                contentLengthString = headerValue;
            } else if (headerName.equalsIgnoreCase(
                    AwsHttpHeaders.DECODED_CONTENT_LENGTH)) {
                decodedContentLengthString = headerValue;
            } else if (headerName.equalsIgnoreCase(HttpHeaders.CONTENT_MD5)) {
                contentMD5String = headerValue;
            }
        }
        if (decodedContentLengthString != null) {
            contentLengthString = decodedContentLengthString;
        }

        HashCode contentMD5 = null;
        if (contentMD5String != null) {
            try {
                contentMD5 = HashCode.fromBytes(
                        Base64.getDecoder().decode(contentMD5String));
            } catch (IllegalArgumentException iae) {
                throw new S3Exception(S3ErrorCode.INVALID_DIGEST, iae);
            }
            if (contentMD5.bits() != MD5.bits()) {
                throw new S3Exception(S3ErrorCode.INVALID_DIGEST);
            }
        }

        if (contentLengthString == null) {
            throw new S3Exception(S3ErrorCode.MISSING_CONTENT_LENGTH);
        }
        long contentLength;
        try {
            contentLength = Long.parseLong(contentLengthString);
        } catch (NumberFormatException nfe) {
            throw new S3Exception(S3ErrorCode.INVALID_ARGUMENT, nfe);
        }
        if (contentLength < 0) {
            throw new S3Exception(S3ErrorCode.INVALID_ARGUMENT);
        }

        String partNumberString = request.getParameter("partNumber");
        if (partNumberString == null) {
            throw new S3Exception(S3ErrorCode.INVALID_ARGUMENT);
        }
        int partNumber;
        try {
            partNumber = Integer.parseInt(partNumberString);
        } catch (NumberFormatException nfe) {
            throw new S3Exception(S3ErrorCode.INVALID_ARGUMENT,
                    "Part number must be an integer between 1 and 10000" +
                    ", inclusive", nfe, Map.of(
                            "ArgumentName", "partNumber",
                            "ArgumentValue", partNumberString));
        }
        if (partNumber < 1 || partNumber > 10_000) {
            throw new S3Exception(S3ErrorCode.INVALID_ARGUMENT,
                    "Part number must be an integer between 1 and 10000" +
                    ", inclusive", (Throwable) null, Map.of(
                            "ArgumentName", "partNumber",
                            "ArgumentValue", partNumberString));
        }

        // GCS only supports 32 parts so partition MPU into 32-part chunks.
        String blobStoreType = getBlobStoreType(blobStore);
        if (blobStoreType.equals("google-cloud-storage")) {
            // fix up 1-based part numbers
            uploadId = String.format(
                    "%s_%08d", uploadId, ((partNumber - 1) / 32) + 1);
            partNumber = ((partNumber - 1) % 32) + 1;
        }

        // TODO: how to reconstruct original mpu?
        BlobMetadata blobMetadata;
        if (Quirks.MULTIPART_REQUIRES_STUB.contains(getBlobStoreType(
                blobStore))) {
            blobMetadata = blobStore.blobMetadata(containerName, uploadId);
        } else {
            blobMetadata = createFakeBlobMetadata(blobStore);
        }
        MultipartUpload mpu = MultipartUpload.create(containerName,
                blobName, uploadId, blobMetadata, new PutOptions());

        if (getBlobStoreType(blobStore).equals("azureblob")) {
            // Azure has a smaller maximum part size than S3.  Split a single
            // S3 part multiple Azure parts.
            long azureMaximumMultipartPartSize =
                        blobStore.getMaximumMultipartPartSize();
            var his = new HashingInputStream(MD5, is);
            int subPartNumber = 0;
            for (long offset = 0; offset < contentLength;
                    offset += azureMaximumMultipartPartSize,
                    ++subPartNumber) {
                Payload payload = Payloads.newInputStreamPayload(
                        ByteStreams.limit(his,
                                azureMaximumMultipartPartSize));
                payload.getContentMetadata().setContentLength(
                        Math.min(azureMaximumMultipartPartSize,
                                contentLength - offset));
                blobStore.uploadMultipartPart(mpu,
                        10_000 * partNumber + subPartNumber, payload);
            }
            response.addHeader(HttpHeaders.ETAG, maybeQuoteETag(
                    BaseEncoding.base16().lowerCase().encode(
                            his.hash().asBytes())));
        } else {
            MultipartPart part;
            Payload payload = Payloads.newInputStreamPayload(is);
            payload.getContentMetadata().setContentLength(contentLength);
            if (contentMD5 != null) {
                payload.getContentMetadata().setContentMD5(contentMD5);
            }

            part = blobStore.uploadMultipartPart(mpu, partNumber, payload);

            if (part.partETag() != null) {
                response.addHeader(HttpHeaders.ETAG,
                        maybeQuoteETag(part.partETag()));
            }
        }

        addCorsResponseHeader(request, response);
    }
Map XML ACLs to a canned policy if an exact tranformation exists.
src/main/java/org/gaul/s3proxy/S3ProxyHandler.java