All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.volcengine.tos.internal.TosObjectRequestHandler Maven / Gradle / Ivy

There is a newer version: 2.8.3
Show newest version
package com.volcengine.tos.internal;

import com.fasterxml.jackson.core.type.TypeReference;
import com.volcengine.tos.TosClientException;
import com.volcengine.tos.TosException;
import com.volcengine.tos.TosServerException;
import com.volcengine.tos.comm.HttpMethod;
import com.volcengine.tos.comm.HttpStatus;
import com.volcengine.tos.comm.MimeType;
import com.volcengine.tos.comm.TosHeader;
import com.volcengine.tos.comm.common.BucketType;
import com.volcengine.tos.comm.event.DataTransferListener;
import com.volcengine.tos.comm.ratelimit.RateLimiter;
import com.volcengine.tos.internal.model.*;
import com.volcengine.tos.internal.util.*;
import com.volcengine.tos.internal.util.aborthook.DefaultAbortTosObjectInputStreamHook;
import com.volcengine.tos.internal.util.ratelimit.RateLimitedInputStream;
import com.volcengine.tos.model.GenericInput;
import com.volcengine.tos.model.bucket.HeadBucketV2Input;
import com.volcengine.tos.model.bucket.HeadBucketV2Output;
import com.volcengine.tos.model.object.*;

import java.io.*;
import java.lang.reflect.Field;
import java.net.URLDecoder;
import java.net.URLEncoder;
import java.nio.channels.FileChannel;
import java.time.ZoneOffset;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;

public class TosObjectRequestHandler {
    private TosBucketRequestHandler bucketRequestHandler;
    private RequestHandler objectHandler;
    private TosRequestFactory factory;
    private boolean clientAutoRecognizeContentType;
    private boolean enableCrcCheck;
    private boolean useTrailerHeader;
    private boolean disableEncodingMeta;
    private final BucketCacheLock[] bucketCacheLocks;

    private static class BucketCache {
        BucketType bucketType;
        long lastUpdateTimeNanos;
        double timeout;
    }

    private static class BucketCacheLock {
        Map bucketTypes;
        ReadWriteLock lock;
    }

    public TosObjectRequestHandler(Transport transport, TosRequestFactory factory) {
        this(transport, factory, null);
    }

    public TosObjectRequestHandler(Transport transport, TosRequestFactory factory, TosBucketRequestHandler bucketRequestHandler) {
        this.objectHandler = new RequestHandler(transport);
        this.factory = factory;
        this.bucketRequestHandler = bucketRequestHandler;
        this.bucketCacheLocks = new BucketCacheLock[16];
        for (int i = 0; i < this.bucketCacheLocks.length; i++) {
            BucketCacheLock bucketCacheLock = new BucketCacheLock();
            bucketCacheLock.bucketTypes = new HashMap<>();
            bucketCacheLock.lock = new ReentrantReadWriteLock();
            this.bucketCacheLocks[i] = bucketCacheLock;
        }
    }

    public TosObjectRequestHandler setTransport(Transport transport) {
        if (this.objectHandler == null) {
            this.objectHandler = new RequestHandler(transport);
        } else {
            this.objectHandler.setTransport(transport);
        }
        return this;
    }

    public Transport getTransport() {
        if (this.objectHandler != null) {
            return this.objectHandler.getTransport();
        }
        return null;
    }

    public TosRequestFactory getFactory() {
        return factory;
    }

    public TosObjectRequestHandler setFactory(TosRequestFactory factory) {
        this.factory = factory;
        return this;
    }

    public boolean isClientAutoRecognizeContentType() {
        return clientAutoRecognizeContentType;
    }

    public boolean isEnableCrcCheck() {
        return enableCrcCheck;
    }

    public TosObjectRequestHandler setClientAutoRecognizeContentType(boolean clientAutoRecognizeContentType) {
        this.clientAutoRecognizeContentType = clientAutoRecognizeContentType;
        return this;
    }

    public TosObjectRequestHandler setEnableCrcCheck(boolean enableCrcCheck) {
        this.enableCrcCheck = enableCrcCheck;
        return this;
    }

    public TosObjectRequestHandler setUseTrailerHeader(boolean useTrailerHeader) {
        this.useTrailerHeader = useTrailerHeader;
        return this;
    }

    public TosObjectRequestHandler setDisableEncodingMeta(boolean disableEncodingMeta) {
        this.disableEncodingMeta = disableEncodingMeta;
        return this;
    }

    private RequestBuilder handleGenericInput(RequestBuilder builder, GenericInput input) {
        if (StringUtils.isNotEmpty(input.getRequestHost())) {
            builder = builder.withHeader(TosHeader.HEADER_HOST, input.getRequestHost());
        }
        if (input.getRequestDate() != null) {
            builder = builder.withHeader(SigningUtils.v4Date, SigningUtils.iso8601Layout.format(input.getRequestDate().toInstant().atOffset(ZoneOffset.UTC)));
        }
        return builder;
    }

    private BucketType getBucketType(String bucket) {
        if (this.bucketRequestHandler == null) {
            return null;
        }
        BucketCacheLock bcl = this.bucketCacheLocks[Math.abs(bucket.hashCode()) % this.bucketCacheLocks.length];

        bcl.lock.readLock().lock();
        BucketCache bc = bcl.bucketTypes.get(bucket);
        bcl.lock.readLock().unlock();

        if (bc != null && (System.nanoTime() - bc.lastUpdateTimeNanos < bc.timeout)) {
            return bc.bucketType;
        }

        bcl.lock.writeLock().lock();
        try {
            bc = bcl.bucketTypes.get(bucket);
            if (bc != null && (System.nanoTime() - bc.lastUpdateTimeNanos < bc.timeout)) {
                return bc.bucketType;
            }
            HeadBucketV2Output output = this.bucketRequestHandler.headBucket(new HeadBucketV2Input().setBucket(bucket));
            bc = new BucketCache();
            bc.bucketType = output.getBucketType();
            bc.lastUpdateTimeNanos = System.nanoTime();
            bc.timeout = 15 * 60 * 1e9;
            bcl.bucketTypes.put(bucket, bc);
            return bc.bucketType;
        } catch (TosServerException ex) {
            if (bc != null) {
                bcl.bucketTypes.remove(bucket);
            }
            TosUtils.getLogger().warn("try to get bucket type failed", ex);
            throw ex;
        } finally {
            bcl.lock.writeLock().unlock();
        }
    }

    public GetFileStatusOutput getFileStatus(GetFileStatusInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "GetFileStatusInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        BucketType bucketType = this.getBucketType(input.getBucket());
        if (bucketType != null && bucketType.getType().equals(BucketType.BUCKET_TYPE_HNS.getType())) {
            HeadObjectV2Input hinput = new HeadObjectV2Input().setBucket(input.getBucket())
                    .setKey(input.getKey());
            hinput.setRequestDate(input.getRequestDate());
            hinput.setRequestHost(input.getRequestHost());
            HeadObjectV2Output output = this.headObject(hinput);
            GetFileStatusOutput goutput = new GetFileStatusOutput();
            goutput.setRequestInfo(output.getRequestInfo());
            goutput.setKey(input.getKey());
            goutput.setLastModified(output.getLastModified());
            goutput.setCrc64(output.getHashCrc64ecma());
            if (output.getRequestInfo().getHeader() != null) {
                goutput.setCrc32(output.getRequestInfo().getHeader().get(TosHeader.HEADER_CRC32.toLowerCase()));
            }
            goutput.setSize(output.getContentLength());
            return goutput;
        }

        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withQuery("stat", "");
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.GET, null);
        return objectHandler.doRequest(req, HttpStatus.OK, this::buildGetFileStatusOutput);
    }

    private GetFileStatusOutput buildGetFileStatusOutput(TosResponse response) {
        return PayloadConverter.parsePayload(response.getInputStream(), new TypeReference() {
                })
                .setRequestInfo(response.RequestInfo());
    }

    public GetObjectV2Output getObject(GetObjectV2Input input) throws TosException {
        ParamsChecker.ensureNotNull(input, "GetObjectV2Input");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(),
                        input.getAllSettedHeaders()).withQuery("versionId", input.getVersionID())
                .withQuery(TosHeader.QUERY_RESPONSE_CACHE_CONTROL, input.getResponseCacheControl())
                .withQuery(TosHeader.QUERY_RESPONSE_CONTENT_DISPOSITION, input.getResponseContentDisposition())
                .withQuery(TosHeader.QUERY_RESPONSE_CONTENT_ENCODING, input.getResponseContentEncoding())
                .withQuery(TosHeader.QUERY_RESPONSE_CONTENT_TYPE, input.getResponseContentType())
                .withQuery(TosHeader.QUERY_RESPONSE_CONTENT_LANGUAGE, input.getResponseContentLanguage())
                .withQuery(TosHeader.QUERY_RESPONSE_EXPIRES, DateConverter.dateToRFC1123String(input.getResponseExpires()))
                .withQuery(TosHeader.QUERY_DATA_PROCESS, input.getProcess())
                .withQuery(TosHeader.QUERY_SAVE_BUCKET, input.getSaveBucket())
                .withQuery(TosHeader.QUERY_SAVE_OBJECT, input.getSaveObject());

        if (input.getDocPage() > 0) {
            builder = builder.withQuery(TosHeader.QUERY_DOC_PAGE, Integer.toString(input.getDocPage()));
        }
        if (input.getSrcType() != null) {
            builder = builder.withQuery(TosHeader.QUERY_DOC_SRC_TYPE, input.getSrcType().toString());
        }
        if (input.getDstType() != null) {
            builder = builder.withQuery(TosHeader.QUERY_DOC_DST_TYPE, input.getDstType().toString());
        }
        builder = this.handleGenericInput(builder, input);
        boolean useTrailerHeader = this.useTrailerHeader;
        // 如果是 Range 下载,则启用 Trailer Header 机制,否则不启用 Trailer Header 机制;
        if (StringUtils.isEmpty(input.getRange()) && (input.getOptions() == null || StringUtils.isEmpty(input.getOptions().getRange()))) {
            useTrailerHeader = false;
        }

        if (useTrailerHeader) {
            builder.withHeader(TosHeader.HEADER_ACCEPT_ENCODING, Consts.TOS_RAW_TRAILER);
            builder.withHeader(TosHeader.HEADER_TRAILER, TosHeader.HEADER_HASH_RANGE_CRC64ECMA);
        }

        TosRequest req = this.factory.build(builder, HttpMethod.GET, null);
        try {
            TosResponse response = objectHandler.doRequest(req, getExpectedCodes(input.getAllSettedHeaders()));
            return buildGetObjectV2Output(response, input.getRateLimiter(), input.getDataTransferListener(), useTrailerHeader);
        } catch (TosException ex) {
            throw ex.setRequestUrl(req.toURL().toString());
        }
    }

    private static List getExpectedCodes(Map headers) {
        List codes = new ArrayList<>(1);
        if (headers == null) {
            // default
            codes.add(HttpStatus.OK);
            return codes;
        }
        codes.add(HttpStatus.OK);
        if (headers.get(TosHeader.HEADER_RANGE) != null) {
            codes.add(HttpStatus.PARTIAL_CONTENT);
        }
        return codes;
    }

    private GetObjectV2Output buildGetObjectV2Output(TosResponse response, RateLimiter rateLimiter,
                                                     DataTransferListener dataTransferListener, boolean useTrailerHeader) {
        GetObjectBasicOutput basicOutput = new GetObjectBasicOutput()
                .setRequestInfo(response.RequestInfo()).parseFromTosResponse(response);
        InputStream content = response.getInputStream();
        if (rateLimiter != null) {
            content = new RateLimitedInputStream(content, rateLimiter);
        }
        if (dataTransferListener != null) {
            content = new SimpleDataTransferListenInputStream(content, dataTransferListener, response.getContentLength());
        }
        if (useTrailerHeader && this.checkTrailerHeaderFromServer(response)) {
            content = new TosRawTrailerInputStream(content, basicOutput.getContentLength(),
                    response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_TRAILER));
        } else if (this.enableCrcCheck && response.getStatusCode() != HttpStatus.PARTIAL_CONTENT) {
            // 开启 crc 校验且非 Range 下载
            String serverCrc64ecma = response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_CRC64);
            if (StringUtils.isNotEmpty(serverCrc64ecma)) {
                content = new CheckCrc64AutoInputStream(content, new CRC64Checksum(), serverCrc64ecma);
            }
        }

        return new GetObjectV2Output(basicOutput, new TosObjectInputStream(content))
                .setHook(new DefaultAbortTosObjectInputStreamHook(content, response.getSource()));
    }

    private boolean checkTrailerHeaderFromServer(TosResponse response) {
        String contentEncoding = response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_CONTENT_ENCODING);
        return StringUtils.isNotEmpty(contentEncoding) && contentEncoding.startsWith(Consts.TOS_RAW_TRAILER);
    }

    public HeadObjectV2Output headObject(HeadObjectV2Input input) throws TosException {
        ParamsChecker.ensureNotNull(input, "HeadObjectV2Input");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), input.getAllSettedHeaders())
                .withQuery("versionId", input.getVersionID());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.HEAD, null);
        return objectHandler.doRequest(req, getExpectedCodes(input.getAllSettedHeaders()),
                response -> {
                    HeadObjectV2Output output = new HeadObjectV2Output(new GetObjectBasicOutput()
                            .setRequestInfo(response.RequestInfo()).parseFromTosResponse(response));
                    String symlinkTargetSize = response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SYMLINK_TARGET_SIZE);
                    return StringUtils.isNotEmpty(symlinkTargetSize) ? output.setSymlinkTargetSize(Long.parseLong(symlinkTargetSize)) : output;
                });
    }

    public DeleteObjectOutput deleteObject(DeleteObjectInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "DeleteObjectInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        if (!input.isRecursive()) {
            RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withQuery("versionId", input.getVersionID());
            builder = this.handleGenericInput(builder, input);
            TosRequest req = this.factory.build(builder, HttpMethod.DELETE, null);
            return objectHandler.doRequest(req, HttpStatus.NO_CONTENT,
                    response -> new DeleteObjectOutput().setRequestInfo(response.RequestInfo())
                            .setDeleteMarker(Boolean.parseBoolean(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_DELETE_MARKER)))
                            .setVersionID(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_VERSIONID))
            );
        }

        BucketType bucketType = this.getBucketType(input.getBucket());
        boolean hns = bucketType != null && bucketType.getType().equals(BucketType.BUCKET_TYPE_HNS.getType());
        if (hns && this.isRecursiveByServer(input)) {
            RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                    .withQuery("versionId", input.getVersionID())
                    .withQuery("recursive", "true");
            builder = this.handleGenericInput(builder, input);
            TosRequest req = this.factory.build(builder, HttpMethod.DELETE, null);
            return objectHandler.doRequest(req, HttpStatus.NO_CONTENT,
                    response -> new DeleteObjectOutput().setRequestInfo(response.RequestInfo())
            );
        }

        return new RecursiveDeleter(input, hns, this).deleteRecursive();
    }

    private boolean isRecursiveByServer(DeleteObjectInput input) {
        try {
            Field f = input.getClass().getDeclaredField("recursiveByServer");
            f.setAccessible(true);
            return f.getBoolean(input);
        } catch (Exception e) {
            return false;
        }
    }

    public DeleteMultiObjectsV2Output deleteMultiObjects(DeleteMultiObjectsV2Input input) throws TosException {
        ParamsChecker.ensureNotNull(input, "DeleteMultiObjectsV2Input");
        ParamsChecker.ensureNotNull(input.getObjects(), "objects to be deleted");
        ensureValidBucketName(input.getBucket());
        for (ObjectTobeDeleted objectTobeDeleted : input.getObjects()) {
            ensureValidKey(objectTobeDeleted.getKey());
        }
        TosMarshalResult marshalResult = PayloadConverter.serializePayloadAndComputeMD5(input);
        RequestBuilder builder = this.factory.init(input.getBucket(), "", null)
                .withHeader(TosHeader.HEADER_CONTENT_MD5, marshalResult.getContentMD5()).withQuery("delete", "");
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.POST, new ByteArrayInputStream(marshalResult.getData()))
                .setContentLength(marshalResult.getData().length);
        return objectHandler.doRequest(req, HttpStatus.OK, response -> PayloadConverter.parsePayload(response.getInputStream(),
                new TypeReference() {
                }).requestInfo(response.RequestInfo())
        );
    }

    private PutObjectOutput putObject(PutObjectBasicInput input, InputStream content) {
        ParamsChecker.ensureNotNull(input, "PutObjectBasicInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        content = ensureNotNullContent(content);
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), input.getAllSettedHeaders())
                .withHeader(TosHeader.HEADER_CALLBACK, input.getCallback())
                .withHeader(TosHeader.HEADER_CALLBACK_VAR, input.getCallbackVar())
                .withHeader(TosHeader.HEADER_X_IF_MATCH, input.getIfMatch())
                .withHeader(TosHeader.HEADER_TAGGING, input.getTagging());
        if (input.isForbidOverwrite()) {
            builder = builder.withHeader(TosHeader.HEADER_FORBID_OVERWRITE, "true");
        }

        if (input.getObjectExpires() >= 0) {
            builder = builder.withHeader(TosHeader.HEADER_OBJECT_EXPIRES, Long.toString(input.getObjectExpires()));
        }

        addContentType(builder, input.getKey());
        builder = this.handleGenericInput(builder, input);
        boolean useTrailerHeader = this.prepareTrailerHeader(builder, input.getOptions(), input.getContentLength(), content);
        TosRequest req = this.factory.build(builder, HttpMethod.PUT, content)
                .setEnableCrcCheck(this.enableCrcCheck)
                .setRateLimiter(input.getRateLimiter())
                .setDataTransferListener(input.getDataTransferListener())
                .setReadLimit(input.getReadLimit())
                .setUseTrailerHeader(useTrailerHeader);
        setRetryStrategy(req, content);
        return objectHandler.doRequest(req, HttpStatus.OK, this::buildPutObjectOutput);
    }

    private boolean prepareTrailerHeader(RequestBuilder builder, ObjectMetaRequestOptions options, long contentLength, InputStream content) {
        if (contentLength >= 0) {
            builder.withContentLength(contentLength);
        } else if (StringUtils.isNotEmpty(builder.getHeaders().get(TosHeader.HEADER_CONTENT_LENGTH))) {
            try {
                long cl = Long.parseLong(builder.getHeaders().get(TosHeader.HEADER_CONTENT_LENGTH));
                builder.withContentLength(cl >= 0 ? cl : -1L);
            } catch (NumberFormatException e) {
                TosUtils.getLogger().debug("tos: try to get content length from header failed, ", e);
            }
        }

        if (content instanceof FileInputStream && contentLength < 0) {
            // 文件流,尝试获取文件长度
            try {
                FileChannel channel = ((FileInputStream) content).getChannel();
                builder.withContentLength(channel.size());
            } catch (IOException e) {
                TosUtils.getLogger().debug("tos: try to get content length from file failed, ", e);
            }
        }
        builder.setSkipTryResolveContentLength(true);
        if (!this.checkUseTrailerHeader(options, builder.getContentLength())) {
            return false;
        }
        builder.withHeader(TosHeader.HEADER_CONTENT_SHA256, Consts.STREAMING_UNSIGNED_PAYLOAD_TRAILER);
        builder.withHeader(TosHeader.HEADER_TRAILER, TosHeader.HEADER_CRC64);
        if (options == null || StringUtils.isEmpty(options.getContentEncoding())) {
            builder.withHeader(TosHeader.HEADER_CONTENT_ENCODING, Consts.TOS_CHUNKED);
        } else {
            builder.withHeader(TosHeader.HEADER_CONTENT_ENCODING, Consts.TOS_CHUNKED + "," + options.getContentEncoding());
        }

        // has content length, calc real content length
        if (builder.getContentLength() > 0) {
            builder.withHeader(TosHeader.HEADER_DECODED_CONTENT_LENGTH, String.valueOf(builder.getContentLength()));
            builder.getHeaders().remove(TosHeader.HEADER_CONTENT_LENGTH);
            // data content length
            // length of hex data content length
            // length of hex 0
            // length of crc64 header
            // length of :
            // length of base64
            // total length of /r/n
            builder.withContentLength(builder.getContentLength() + Long.toHexString(builder.getContentLength()).length() + 1 + TosHeader.HEADER_CRC64.length() + 1 + 12 + 10);
        }
        return true;
    }

    private boolean checkUseTrailerHeader(ObjectMetaRequestOptions options, long contentLength) {
        // 上传 0 字节数据时,不启用 Trailer Header 机制
        if (contentLength == 0) {
            return false;
        }

        boolean useTrailerHeader = this.useTrailerHeader;
        // 请求参数携带了 ContentMD5 或 ContentSHA256,则不启用 Trailer Header 机制
        if (options != null && (StringUtils.isNotEmpty(options.getContentMD5()) || StringUtils.isNotEmpty(options.getContentSHA256()))) {
            useTrailerHeader = false;
        }
        return useTrailerHeader;
    }

    private InputStream ensureNotNullContent(InputStream content) {
        if (content == null) {
            content = new ByteArrayInputStream("".getBytes());
        }
        return content;
    }

    private static void setRetryStrategy(TosRequest request, InputStream stream) {
        boolean canRetry = stream.markSupported() || stream instanceof FileInputStream;
        request.setRetryableOnServerException(canRetry);
        request.setRetryableOnClientException(canRetry);
    }

    private PutObjectOutput buildPutObjectOutput(TosResponse res) {
        String callbackResult = StringUtils.toString(res.getInputStream(), "callbackResult");
        return new PutObjectOutput().setRequestInfo(res.RequestInfo())
                .setEtag(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_ETAG))
                .setVersionID(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_VERSIONID))
                .setHashCrc64ecma(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_CRC64))
                .setSseCustomerAlgorithm(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_CUSTOMER_ALGORITHM))
                .setSseCustomerKeyMD5(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_CUSTOMER_KEY_MD5))
                .setSseCustomerKey(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_CUSTOMER_KEY))
                .setServerSideEncryption(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE))
                .setServerSideEncryptionKeyID(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_KEY_ID))
                .setCallbackResult(callbackResult);
    }

    public PutObjectOutput putObject(PutObjectInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "PutObjectInput");
        if (input.getPutObjectBasicInput() != null) {
            input.getPutObjectBasicInput().setRequestHost(input.getRequestHost());
            input.getPutObjectBasicInput().setRequestDate(input.getRequestDate());
        }
        return putObject(input.getPutObjectBasicInput(), input.getContent());
    }

    public AppendObjectOutput appendObject(AppendObjectInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "AppendObjectInput");
        if (this.enableCrcCheck && input.getOffset() > 0 && StringUtils.isEmpty(input.getPreHashCrc64ecma())) {
            throw new TosClientException("tos: client enable crc64 check but preHashCrc64ecma is not set", null);
        }
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());

        BucketType bucketType = this.getBucketType(input.getBucket());
        if (bucketType != null && bucketType.getType().equals(BucketType.BUCKET_TYPE_HNS.getType())) {
            if (input.getOffset() == 0 && input.getContentLength() >= 0) {
                PutObjectInput pinput = new PutObjectInput().setBucket(input.getBucket())
                        .setKey(input.getKey()).setContent(input.getContent())
                        .setContentLength(input.getContentLength()).setDataTransferListener(input.getDataTransferListener())
                        .setRateLimiter(input.getRateLimiter()).setIfMatch(input.getIfMatch())
                        .setOptions(input.getOptions()).setForbidOverwrite(true);

                pinput.setRequestDate(input.getRequestDate());
                pinput.setRequestHost(input.getRequestHost());
                PutObjectOutput poutput = this.putObject(pinput);
                AppendObjectOutput aoutput = new AppendObjectOutput().setRequestInfo(poutput.getRequestInfo()).setHashCrc64ecma(poutput.getHashCrc64ecma());
                aoutput.setNextAppendOffset(input.getContentLength());
                return aoutput;
            }

            ModifyObjectInput minput = new ModifyObjectInput().setBucket(input.getBucket())
                    .setKey(input.getKey()).setOffset(input.getOffset()).setContent(input.getContent())
                    .setContentLength(input.getContentLength()).setDataTransferListener(input.getDataTransferListener())
                    .setRateLimiter(input.getRateLimiter());
            minput.setRequestDate(input.getRequestDate());
            minput.setRequestHost(input.getRequestHost());
            if (input.getOptions() != null) {
                long trafficLimit = input.getOptions().getTrafficLimit();
                if (trafficLimit > 0) {
                    minput.setTrafficLimit(trafficLimit);
                }
            }
            ModifyObjectOutput output = this.modifyObject(minput, input.getPreHashCrc64ecma(), this.enableCrcCheck);
            return new AppendObjectOutput().setRequestInfo(output.getRequestInfo())
                    .setNextAppendOffset(output.getNextModifyOffset()).setHashCrc64ecma(output.getHashCrc64ecma());
        }

        // append not support chunked, need to set contentLength
//        if (input.getContentLength() <= 0) {
//            throw new TosClientException("content length should be set in appendObject method.", null);
//        }

        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), input.getAllSettedHeaders())
                .withQuery("append", "")
                .withQuery("offset", String.valueOf(input.getOffset()))
                .withContentLength(input.getContentLength())
                .withHeader(TosHeader.HEADER_X_IF_MATCH, input.getIfMatch());

        if (input.getObjectExpires() >= 0) {
            builder = builder.withHeader(TosHeader.HEADER_OBJECT_EXPIRES, Long.toString(input.getObjectExpires()));
        }

        addContentType(builder, input.getKey());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.POST, input.getContent())
                // appendObject should not retry
                .setRetryableOnServerException(false).setRetryableOnClientException(false)
                .setEnableCrcCheck(this.enableCrcCheck)
                .setCrc64InitValue(CRC64Utils.unsignedLongStringToLong(input.getPreHashCrc64ecma()))
                .setRateLimiter(input.getRateLimiter())
                .setDataTransferListener(input.getDataTransferListener());
        return objectHandler.doRequest(req, HttpStatus.OK, this::buildAppendObjectOutput);
    }

    private AppendObjectOutput buildAppendObjectOutput(TosResponse response) {
        String nextOffset = response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_NEXT_APPEND_OFFSET);
        long appendOffset;
        try {
            appendOffset = Long.parseLong(nextOffset);
        } catch (NumberFormatException nfe) {
            throw new TosClientException("tos: server return unexpected Next-Append-Offset header: " + nextOffset, nfe);
        }
        return new AppendObjectOutput().setRequestInfo(response.RequestInfo())
                .setNextAppendOffset(appendOffset)
                .setHashCrc64ecma(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_CRC64));
    }

    public SetObjectMetaOutput setObjectMeta(SetObjectMetaInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "SetObjectMetaInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(),
                input.getAllSettedHeaders()).withQuery("metadata", "").withQuery("versionId", input.getVersionID());
        if (input.getObjectExpires() >= 0) {
            builder = builder.withHeader(TosHeader.HEADER_OBJECT_EXPIRES, Long.toString(input.getObjectExpires()));
        }
        addContentType(builder, input.getKey());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.POST, null);
        return objectHandler.doRequest(req, HttpStatus.OK,
                response -> new SetObjectMetaOutput().setRequestInfo(response.RequestInfo())
        );
    }

    public ListObjectsV2Output listObjects(ListObjectsV2Input input) throws TosException {
        ParamsChecker.ensureNotNull(input, "ListObjectsV2Input");
        ensureValidBucketName(input.getBucket());
        RequestBuilder builder = this.factory.init(input.getBucket(), "", null)
                .withQuery("prefix", input.getPrefix())
                .withQuery("delimiter", input.getDelimiter())
                .withQuery("marker", input.getMarker())
                .withQuery("max-keys", TosUtils.convertInteger(input.getMaxKeys()))
                .withQuery("reverse", String.valueOf(input.isReverse()))
                .withQuery("encoding-type", input.getEncodingType());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.GET, null);
        return objectHandler.doRequest(req, HttpStatus.OK, response -> PayloadConverter.parsePayload(response.getInputStream(),
                new TypeReference() {
                }).setRequestInfo(response.RequestInfo()));
    }

    public ListObjectsType2Output listObjectsType2(ListObjectsType2Input input) throws TosException {
        ParamsChecker.ensureNotNull(input, "ListObjectsType2Input");
        ensureValidBucketName(input.getBucket());
        RequestBuilder builder = this.factory.init(input.getBucket(), "", null)
                .withQuery("list-type", "2")
                .withQuery("prefix", input.getPrefix())
                .withQuery("delimiter", input.getDelimiter())
                .withQuery("start-after", input.getStartAfter())
                .withQuery("continuation-token", input.getContinuationToken())
                .withQuery("max-keys", TosUtils.convertInteger(input.getMaxKeys()))
                .withQuery("encoding-type", input.getEncodingType())
                .withQuery("fetch-owner", "true");
        if (input.isFetchMeta()) {
            builder = builder.withQuery("fetch-meta", "true");
        }
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.GET, null);
        ListObjectsType2Output output = objectHandler.doRequest(req, HttpStatus.OK, response -> PayloadConverter.parsePayload(response.getInputStream(),
                new TypeReference() {
                }).setRequestInfo(response.RequestInfo()));
        if (output.getContents() != null && output.getContents().size() > 0 && this.disableEncodingMeta) {
            for (ListedObjectV2 obj : output.getContents()) {
                try {
                    Field f = obj.getClass().getDeclaredField("disableEncodingMeta");
                    f.setAccessible(true);
                    f.set(obj, true);
                } catch (Exception e) {
                }
            }
        }

        return output;
    }

    public ListObjectsType2Output listObjectsType2UntilFinished(ListObjectsType2Input input) {
        ParamsChecker.ensureNotNull(input, "ListObjectsType2Input");
        if (input.isListOnlyOnce()) {
            return listObjectsType2(input);
        }

        int mk = input.getMaxKeys() > 0 ? input.getMaxKeys() : 1000;

        int totalRecords = 0;
        List commonPrefixes = null;
        List contents = null;

        ListObjectsType2Output tmp = null;
        String continuationToken = input.getContinuationToken();
        boolean listFinished = false;
        while (!listFinished) {
            tmp = listObjectsType2(input.setContinuationToken(continuationToken));

            if (tmp.getCommonPrefixes() != null) {
                if (commonPrefixes == null) {
                    commonPrefixes = tmp.getCommonPrefixes();
                } else {
                    commonPrefixes.addAll(tmp.getCommonPrefixes());
                }
            }
            if (tmp.getContents() != null) {
                if (contents == null) {
                    contents = tmp.getContents();
                } else {
                    contents.addAll(tmp.getContents());
                }
            }

            totalRecords += tmp.getKeyCount();
            continuationToken = tmp.getNextContinuationToken();
            listFinished = isListFinished(tmp.isTruncated(), mk, totalRecords);
        }
        return tmp.setCommonPrefixes(commonPrefixes).setContents(contents).setKeyCount(totalRecords);
    }

    private boolean isListFinished(boolean isTruncated, int mk, int totalRecords) {
        boolean returnMaxKeysRecord = mk == totalRecords;
        // if returnMaxKeysRecord, or if not returnMaxKeysRecord but not truncated
        // it means that the list request is finished.
        return returnMaxKeysRecord || !isTruncated;
    }

    public ListObjectVersionsV2Output listObjectVersions(ListObjectVersionsV2Input input) throws TosException {
        ParamsChecker.ensureNotNull(input, "ListObjectVersionsV2Input");
        ensureValidBucketName(input.getBucket());
        RequestBuilder builder = this.factory.init(input.getBucket(), "", null)
                .withQuery("prefix", input.getPrefix())
                .withQuery("delimiter", input.getDelimiter())
                .withQuery("key-marker", input.getKeyMarker())
                .withQuery("max-keys", TosUtils.convertInteger(input.getMaxKeys()))
                .withQuery("encoding-type", input.getEncodingType())
                .withQuery("version-id-marker", input.getVersionIDMarker())
                .withQuery("versions", "");
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.GET, null);
        return objectHandler.doRequest(req, HttpStatus.OK,
                response -> PayloadConverter.parsePayload(response.getInputStream(),
                        new TypeReference() {
                        }).setRequestInfo(response.RequestInfo())
        );
    }

    public CopyObjectV2Output copyObject(CopyObjectV2Input input) throws TosException {
        ParamsChecker.ensureNotNull(input, "CopyObjectV2Input");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        ensureValidBucketName(input.getSrcBucket());
        ensureValidKey(input.getSrcKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), input.getAllSettedHeaders())
                .withQuery("versionId", input.getSrcVersionID());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.buildWithCopy(builder, HttpMethod.PUT, input.getSrcBucket(), input.getSrcKey());
        return objectHandler.doRequest(req, HttpStatus.OK, this::buildCopyObjectV2Output);
    }

    private CopyObjectV2Output buildCopyObjectV2Output(TosResponse response) {
        // 一把解 CopyObjectV2Output 和 ServerExceptionJson
        String rspMsg = StringUtils.toString(response.getInputStream(), "copy result");
        try {
            CopyObjectV2Output output = PayloadConverter.parsePayload(rspMsg,
                    new TypeReference() {
                    });
            return output.setRequestInfo(response.RequestInfo())
                    .setVersionID(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_VERSIONID))
                    .setSourceVersionID(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_COPY_SOURCE_VERSION_ID))
                    .setHashCrc64ecma(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_CRC64))
                    .setSsecAlgorithm(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_CUSTOMER_ALGORITHM))
                    .setSsecKeyMD5(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_CUSTOMER_KEY_MD5))
                    .setServerSideEncryption(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE))
                    .setServerSideEncryptionKeyID(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_KEY_ID));
        } catch (TosClientException e) {
            ServerExceptionJson errMsg = PayloadConverter.parsePayload(rspMsg,
                    new TypeReference() {
                    });
            throw new TosServerException(response.getStatusCode(), errMsg.getCode(), errMsg.getMessage(),
                    errMsg.getRequestID(), errMsg.getHostID()).setEc(errMsg.getEc()).setKey(errMsg.getKey());
        }
    }

    public UploadPartCopyV2Output uploadPartCopy(UploadPartCopyV2Input input) throws TosException {
        ParamsChecker.ensureNotNull(input, "UploadPartCopyV2Input");
        ParamsChecker.ensureNotNull(input.getUploadID(), "UploadID");
        ParamsChecker.isValidPartNumber(input.getPartNumber());
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        ensureValidBucketName(input.getSourceBucket());
        ensureValidKey(input.getSourceKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), input.getAllSettedHeaders())
                .withQuery("partNumber", TosUtils.convertInteger(input.getPartNumber()))
                .withQuery("uploadId", input.getUploadID())
                .withQuery("versionId", input.getSourceVersionID());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.buildWithCopy(builder, HttpMethod.PUT, input.getSourceBucket(), input.getSourceKey());
        return objectHandler.doRequest(req, HttpStatus.OK, response -> buildUploadPartCopyV2Output(input, response));
    }

    private UploadPartCopyV2Output buildUploadPartCopyV2Output(UploadPartCopyV2Input input, TosResponse response) {
        String rspMsg = StringUtils.toString(response.getInputStream(), "copy result");
        try {
            UploadPartCopyOutputJson out = PayloadConverter.parsePayload(rspMsg,
                    new TypeReference() {
                    });
            return new UploadPartCopyV2Output().requestInfo(response.RequestInfo())
                    .copySourceVersionID(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_COPY_SOURCE_VERSION_ID))
                    .setServerSideEncryptionKeyID(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE))
                    .setServerSideEncryptionKeyID(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_KEY_ID))
                    .setSsecAlgorithm(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_CUSTOMER_ALGORITHM))
                    .setSsecKeyMD5(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_CUSTOMER_KEY_MD5))
                    .partNumber(input.getPartNumber()).etag(out.getEtag()).lastModified(out.getLastModified());
        } catch (TosClientException e) {
            ServerExceptionJson errMsg = PayloadConverter.parsePayload(rspMsg,
                    new TypeReference() {
                    });
            throw new TosServerException(response.getStatusCode(), errMsg.getCode(), errMsg.getMessage(),
                    errMsg.getRequestID(), errMsg.getHostID()).setEc(errMsg.getEc()).setKey(errMsg.getKey());
        }
    }

    public PutObjectACLOutput putObjectAcl(PutObjectACLInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "PutObjectACLInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withQuery("acl", "").withQuery("versionId", input.getVersionID())
                .withHeader(TosHeader.HEADER_ACL, input.getAcl() == null ? null : input.getAcl().toString())
                .withHeader(TosHeader.HEADER_GRANT_FULL_CONTROL, input.getGrantFullControl())
                .withHeader(TosHeader.HEADER_GRANT_READ, input.getGrantRead())
                .withHeader(TosHeader.HEADER_GRANT_READ_ACP, input.getGrantReadAcp())
                .withHeader(TosHeader.HEADER_GRANT_WRITE_ACP, input.getGrantWriteAcp());
        byte[] content = new byte[0];
        if (input.getObjectAclRules() != null) {
            TosMarshalResult res = PayloadConverter.serializePayloadAndComputeMD5(input.getObjectAclRules());
            content = res.getData();
            builder.withHeader(TosHeader.HEADER_CONTENT_MD5, res.getContentMD5());
        }
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.PUT, new ByteArrayInputStream(content)).setContentLength(content.length);
        return objectHandler.doRequest(req, HttpStatus.OK, response -> new PutObjectACLOutput().requestInfo(response.RequestInfo()));
    }

    public GetObjectACLV2Output getObjectAcl(GetObjectACLV2Input input) throws TosException {
        ParamsChecker.ensureNotNull(input, "GetObjectACLV2Input");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withQuery("acl", "").withQuery("versionId", input.getVersionID());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.GET, null);
        return objectHandler.doRequest(req, HttpStatus.OK, this::buildGetObjectACLV2Output);
    }

    private GetObjectACLV2Output buildGetObjectACLV2Output(TosResponse response) {
        return PayloadConverter.parsePayload(response.getInputStream(), new TypeReference() {
                })
                .setRequestInfo(response.RequestInfo())
                .setVersionID(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_VERSIONID));
    }

    public PutObjectTaggingOutput putObjectTagging(PutObjectTaggingInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "PutObjectTaggingInput");
        ParamsChecker.ensureNotNull(input.getTagSet(), "TagSet");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        TosMarshalResult marshalResult = PayloadConverter.serializePayloadAndComputeMD5(input);
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withQuery("tagging", "").withQuery("versionId", input.getVersionID())
                .withHeader(TosHeader.HEADER_CONTENT_MD5, marshalResult.getContentMD5());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.PUT, new ByteArrayInputStream(marshalResult.getData()))
                .setContentLength(marshalResult.getData().length);
        return objectHandler.doRequest(req, HttpStatus.OK, response -> new PutObjectTaggingOutput()
                .setRequestInfo(response.RequestInfo()).setVersionID(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_VERSIONID)));
    }

    public GetObjectTaggingOutput getObjectTagging(GetObjectTaggingInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "GetObjectTaggingInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withQuery("tagging", "").withQuery("versionId", input.getVersionID());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.GET, null);
        return objectHandler.doRequest(req, HttpStatus.OK, res -> PayloadConverter.parsePayload(res.getInputStream(),
                        new TypeReference() {
                        }).setRequestInfo(res.RequestInfo())
                .setVersionID(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_VERSIONID)));
    }

    public DeleteObjectTaggingOutput deleteObjectTagging(DeleteObjectTaggingInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "DeleteObjectTaggingInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withQuery("tagging", "").withQuery("versionId", input.getVersionID());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.DELETE, null);
        return objectHandler.doRequest(req, HttpStatus.NO_CONTENT, res -> new DeleteObjectTaggingOutput()
                .setRequestInfo(res.RequestInfo()).setVersionID(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_VERSIONID)));
    }

    public FetchObjectOutput fetchObject(FetchObjectInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "FetchObjectInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        ParamsChecker.ensureNotNull(input.getUrl(), "URL");
        TosMarshalResult marshalResult = PayloadConverter.serializePayloadAndComputeMD5(input);
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), input.getAllSettedHeaders())
                .withQuery("fetch", "").withHeader(TosHeader.HEADER_CONTENT_MD5, marshalResult.getContentMD5());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.POST, new ByteArrayInputStream(marshalResult.getData()))
                .setContentLength(marshalResult.getData().length);
        return objectHandler.doRequest(req, HttpStatus.OK, response -> PayloadConverter.parsePayload(response.getInputStream(),
                        new TypeReference() {
                        }).setRequestInfo(response.RequestInfo())
                .setVersionID(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_VERSIONID))
                .setSsecAlgorithm(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_CUSTOMER_ALGORITHM))
                .setSsecKeyMD5(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_CUSTOMER_KEY_MD5))
        );
    }

    public PutFetchTaskOutput putFetchTask(PutFetchTaskInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "PutFetchTaskInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        ParamsChecker.ensureNotNull(input.getUrl(), "URL");
        TosMarshalResult marshalResult = PayloadConverter.serializePayloadAndComputeMD5(input);
        RequestBuilder builder = this.factory.init(input.getBucket(), "", input.getAllSettedHeaders())
                .withQuery("fetchTask", "").withHeader(TosHeader.HEADER_CONTENT_MD5, marshalResult.getContentMD5());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.POST, new ByteArrayInputStream(marshalResult.getData()))
                .setContentLength(marshalResult.getData().length);
        return objectHandler.doRequest(req, HttpStatus.OK, response -> PayloadConverter.parsePayload(response.getInputStream(),
                new TypeReference() {
                }).setRequestInfo(response.RequestInfo()));
    }

    public GetFetchTaskOutput getFetchTask(GetFetchTaskInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "GetFetchTaskInput");
        ensureValidBucketName(input.getBucket());
        if (StringUtils.isEmpty(input.getTaskId())) {
            throw new TosClientException("empty task id", null);
        }
        RequestBuilder builder = this.factory.init(input.getBucket(), "", null).withQuery("fetchTask", "").withQuery("taskId", input.getTaskId());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.GET, null);
        GetFetchTaskOutput output = objectHandler.doRequest(req, HttpStatus.OK, response -> PayloadConverter.parsePayload(response.getInputStream(),
                new TypeReference() {
                }).setRequestInfo(response.RequestInfo()));
        if (output.getTask() != null && output.getTask().getMeta() != null && output.getTask().getMeta().size() > 0 && disableEncodingMeta) {
            try {
                Field f = output.getTask().getClass().getDeclaredField("disableEncodingMeta");
                f.setAccessible(true);
                f.set(output.getTask(), true);
            } catch (Exception e) {
            }
        }
        return output;
    }

    public CreateMultipartUploadOutput createMultipartUpload(CreateMultipartUploadInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "CreateMultipartUploadInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), input.getAllSettedHeaders())
                .withQuery("uploads", "").withHeader(TosHeader.HEADER_TAGGING, input.getTagging());
        if (input.getObjectExpires() >= 0) {
            builder = builder.withHeader(TosHeader.HEADER_OBJECT_EXPIRES, String.valueOf(input.getObjectExpires()));
        }
        addContentType(builder, input.getKey());
        if (input.isForbidOverwrite()) {
            builder = builder.withHeader(TosHeader.HEADER_FORBID_OVERWRITE, "true");
        }
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.POST, null).setRetryableOnClientException(false);
        return objectHandler.doRequest(req, HttpStatus.OK, this::buildCreateMultipartUploadOutput);
    }

    private CreateMultipartUploadOutput buildCreateMultipartUploadOutput(TosResponse response) {
        CreateMultipartUploadOutputJson upload = PayloadConverter.parsePayload(response.getInputStream(),
                new TypeReference() {
                });
        return new CreateMultipartUploadOutput().setRequestInfo(response.RequestInfo())
                .setBucket(upload.getBucket()).setKey(upload.getKey()).setUploadID(upload.getUploadID())
                .setEncodingType(upload.getEncodingType())
                .setSseCustomerAlgorithm(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_CUSTOMER_ALGORITHM))
                .setSseCustomerMD5(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_CUSTOMER_KEY_MD5))
                .setSseCustomerKey(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_CUSTOMER_KEY))
                .setServerSideEncryption(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE))
                .setServerSideEncryptionKeyID(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_KEY_ID));
    }

    private UploadPartV2Output buildUploadPartV2Output(TosResponse res, int partNumber) {
        return new UploadPartV2Output().setRequestInfo(res.RequestInfo())
                .setPartNumber(partNumber).setEtag(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_ETAG))
                .setSsecAlgorithm(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_CUSTOMER_ALGORITHM))
                .setSsecKeyMD5(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_CUSTOMER_KEY_MD5))
                .setServerSideEncryption(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE))
                .setServerSideEncryptionKeyID(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SSE_KEY_ID))
                .setHashCrc64ecma(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_CRC64));
    }

    private UploadPartV2Output uploadPart(UploadPartBasicInput input, long contentLength, InputStream content) {
        ParamsChecker.ensureNotNull(input, "UploadPartBasicInput");
        ParamsChecker.ensureNotNull(input.getUploadID(), "uploadID");
        ParamsChecker.ensureNotNull(content, "InputStream");
        ParamsChecker.isValidPartNumber(input.getPartNumber());
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), input.getAllSettedHeaders())
                .withQuery("uploadId", input.getUploadID())
                .withQuery("partNumber", TosUtils.convertInteger(input.getPartNumber()));
        builder = this.handleGenericInput(builder, input);

        boolean useTrailerHeader = this.prepareTrailerHeader(builder, input.getOptions(), contentLength, content);
        TosRequest req = this.factory.build(builder, HttpMethod.PUT, content)
                .setEnableCrcCheck(this.enableCrcCheck).setRateLimiter(input.getRateLimiter())
                .setDataTransferListener(input.getDataTransferListener())
                .setReadLimit(input.getReadLimit())
                .setUseTrailerHeader(useTrailerHeader);

        setRetryStrategy(req, content);
        return objectHandler.doRequest(req, HttpStatus.OK, response -> buildUploadPartV2Output(response, input.getPartNumber()));
    }

    public UploadPartV2Output uploadPart(UploadPartV2Input input) throws TosException {
        ParamsChecker.ensureNotNull(input, "UploadPartV2Input");
        if (input.getUploadPartBasicInput() != null) {
            input.getUploadPartBasicInput().setRequestDate(input.getRequestDate());
            input.getUploadPartBasicInput().setRequestHost(input.getRequestHost());
        }
        return uploadPart(input.getUploadPartBasicInput(), input.getContentLength(), input.getContent());
    }

    public CompleteMultipartUploadV2Output completeMultipartUpload(CompleteMultipartUploadV2Input input) throws TosException {
        ParamsChecker.ensureNotNull(input, "CompleteMultipartUploadV2Input");
        ParamsChecker.ensureNotNull(input.getUploadID(), "uploadID");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withQuery("uploadId", input.getUploadID())
                .withHeader(TosHeader.HEADER_CALLBACK, input.getCallback())
                .withHeader(TosHeader.HEADER_CALLBACK_VAR, input.getCallbackVar());
        if (input.isForbidOverwrite()) {
            builder = builder.withHeader(TosHeader.HEADER_FORBID_OVERWRITE, "true");
        }
        String contentMd5 = null;
        byte[] data = new byte[0];
        if (!input.isCompleteAll()) {
            ensureUploadedPartsNotNull(input);
            TosMarshalResult marshalResult = PayloadConverter.serializePayloadAndComputeMD5(input);
            contentMd5 = marshalResult.getContentMD5();
            data = marshalResult.getData();
        } else {
            ensureUploadedPartsNull(input);
            builder.withHeader(TosHeader.HEADER_COMPLETE_ALL, Consts.USE_COMPLETE_ALL);
        }
        builder.withHeader(TosHeader.HEADER_CONTENT_MD5, contentMd5);
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.POST, new ByteArrayInputStream(data))
                .setContentLength(data.length).setRetryableOnClientException(false);
        List unexpectedCodes = new ArrayList<>();
        unexpectedCodes.add(HttpStatus.NON_AUTHORITATIVE_INFO);
        return objectHandler.doRequest(req, HttpStatus.OK, unexpectedCodes, response -> {
            boolean hasCallbackResult = StringUtils.isNotEmpty(input.getCallback());
            return buildCompleteMultipartUploadOutput(response, hasCallbackResult);
        });
    }

    private void ensureUploadedPartsNull(CompleteMultipartUploadV2Input input) {
        if (input != null && input.getUploadedParts() != null && input.getUploadedParts().size() != 0) {
            throw new TosClientException("tos: you should not set uploadedParts while using completeAll.", null);
        }
    }

    private void ensureUploadedPartsNotNull(CompleteMultipartUploadV2Input input) {
        if (input == null || input.getUploadedParts() == null || input.getUploadedParts().size() == 0) {
            throw new TosClientException("tos: you must specify at least one part.", null);
        }
    }

    private CompleteMultipartUploadV2Output buildCompleteMultipartUploadOutput(TosResponse response, boolean hasCallbackResult) {
        String respBody = StringUtils.toString(response.getInputStream(), "response body");
        CompleteMultipartUploadV2Output output = new CompleteMultipartUploadV2Output();
        if (hasCallbackResult) {
            // if response body return callback result, then set etag and location from header.
            output.setCallbackResult(respBody);
            output.setEtag(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_ETAG));
            output.setLocation(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_LOCATION));
        } else {
            output = PayloadConverter.parsePayload(respBody, new TypeReference() {
            });
        }
        return output.setRequestInfo(response.RequestInfo())
                .setVersionID(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_VERSIONID))
                .setServerSideEncryption(TosHeader.HEADER_SSE)
                .setServerSideEncryptionKeyID(TosHeader.HEADER_SSE_KEY_ID)
                .setHashCrc64ecma(response.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_CRC64));
    }

    public AbortMultipartUploadOutput abortMultipartUpload(AbortMultipartUploadInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "AbortMultipartUploadInput");
        ParamsChecker.ensureNotNull(input.getUploadID(), "uploadID");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withQuery("uploadId", input.getUploadID());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.DELETE, null).setRetryableOnClientException(false);
        return objectHandler.doRequest(req, HttpStatus.NO_CONTENT,
                response -> new AbortMultipartUploadOutput().setRequestInfo(response.RequestInfo())
        );
    }

    public ListPartsOutput listParts(ListPartsInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "ListPartsInput");
        ParamsChecker.ensureNotNull(input.getUploadID(), "uploadID");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        if (input.getMaxParts() < 0 || input.getPartNumberMarker() < 0) {
            throw new TosClientException("ListPartsInput maxParts or partNumberMarker is small than 0", null);
        }
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withQuery("uploadId", input.getUploadID())
                .withQuery("max-parts", TosUtils.convertInteger(input.getMaxParts()))
                .withQuery("part-number-marker", TosUtils.convertInteger(input.getPartNumberMarker()))
                .withQuery("encoding-type", input.getEncodingType());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.GET, null);
        return objectHandler.doRequest(req, HttpStatus.OK,
                response -> PayloadConverter.parsePayload(response.getInputStream(),
                        new TypeReference() {
                        }).setRequestInfo(response.RequestInfo())
        );
    }

    public ListMultipartUploadsV2Output listMultipartUploads(ListMultipartUploadsV2Input input) throws TosException {
        ParamsChecker.ensureNotNull(input, "ListMultipartUploadsV2Input");
        ensureValidBucketName(input.getBucket());
        if (input.getMaxUploads() < 0) {
            throw new TosClientException("ListMultipartUploadsV2Input maxUploads is small than 0", null);
        }
        RequestBuilder builder = this.factory.init(input.getBucket(), "", null)
                .withQuery("uploads", "")
                .withQuery("prefix", input.getPrefix())
                .withQuery("delimiter", input.getDelimiter())
                .withQuery("key-marker", input.getKeyMarker())
                .withQuery("upload-id-marker", input.getUploadIDMarker())
                .withQuery("max-uploads", TosUtils.convertInteger(input.getMaxUploads()))
                .withQuery("encoding-type", input.getEncodingType());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.GET, null);
        return objectHandler.doRequest(req, HttpStatus.OK,
                response -> PayloadConverter.parsePayload(response.getInputStream(),
                        new TypeReference() {
                        }).setRequestInfo(response.RequestInfo())
        );
    }

    public RenameObjectOutput renameObject(RenameObjectInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "RenameObjectInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        ensureValidKey(input.getNewKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withQuery("name", input.getNewKey()).withQuery("rename", "");

        if (input.isForbidOverwrite()) {
            builder = builder.withHeader(TosHeader.HEADER_FORBID_OVERWRITE, "true");
        }

        if (input.isRecursiveMkdir()) {
            builder = builder.withHeader(TosHeader.HEADER_RECURSIVE_MKDIR, "true");
        }

        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.PUT, null);
        return objectHandler.doRequest(req, HttpStatus.NO_CONTENT, response -> new RenameObjectOutput()
                .setRequestInfo(response.RequestInfo()));
    }

    public RestoreObjectOutput restoreObject(RestoreObjectInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "RestoreObjectInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        TosMarshalResult marshalResult = PayloadConverter.serializePayloadAndComputeMD5(input);
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withHeader(TosHeader.HEADER_CONTENT_MD5, marshalResult.getContentMD5())
                .withQuery("restore", "")
                .withQuery("versionId", input.getVersionID());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.POST, new ByteArrayInputStream(marshalResult.getData()))
                .setContentLength(marshalResult.getData().length);
        return objectHandler.doRequest(req, restoreObjectExceptedCodes(), response -> new RestoreObjectOutput()
                .setRequestInfo(response.RequestInfo()));
    }

    public PutSymlinkOutput putSymlink(PutSymlinkInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "PutSymlinkInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        if (StringUtils.isEmpty(input.getSymlinkTargetKey())) {
            throw new TosClientException("empty symlink target key", null);
        }

        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withQuery("symlink", "")
                .withHeader(TosHeader.HEADER_SYMLINK_BUCKET, input.getSymlinkTargetBucket())
                .withHeader(TosHeader.HEADER_ACL, input.getAcl() == null ? null : input.getAcl().toString())
                .withHeader(TosHeader.HEADER_STORAGE_CLASS, input.getStorageClass() == null ? null : input.getStorageClass().toString());

        try {
            builder = builder.withHeader(TosHeader.HEADER_SYMLINK_TARGET, URLEncoder.encode(input.getSymlinkTargetKey(), "UTF-8"));
        } catch (UnsupportedEncodingException e) {
            throw new TosClientException("encoding symlink target key failed", e);
        }

        if (input.isForbidOverwrite()) {
            builder = builder.withHeader(TosHeader.HEADER_FORBID_OVERWRITE, "true");
        }
        if (input.getMeta() != null) {
            for (Map.Entry entry : input.getMeta().entrySet()) {
                String key = entry.getKey();
                String value = entry.getValue();
                if (StringUtils.isNotEmpty(value)) {
                    builder = builder.withHeader(TosHeader.HEADER_META_PREFIX + key, value);
                }
            }
        }

        builder = builder.withHeader(TosHeader.HEADER_CACHE_CONTROL, input.getCacheControl())
                .withHeader(TosHeader.HEADER_CONTENT_DISPOSITION, input.getContentDisposition())
                .withHeader(TosHeader.HEADER_CONTENT_ENCODING, input.getContentEncoding())
                .withHeader(TosHeader.HEADER_CONTENT_LANGUAGE, input.getContentLanguage())
                .withHeader(TosHeader.HEADER_CONTENT_TYPE, input.getContentType())
                .withHeader(TosHeader.HEADER_EXPIRES, DateConverter.dateToRFC1123String(input.getExpires()))
                .withHeader(TosHeader.HEADER_TAGGING, input.getTagging());

        builder = this.handleGenericInput(builder, input);
        addContentType(builder, input.getKey());
        TosRequest req = this.factory.build(builder, HttpMethod.PUT, null).setContentLength(0);
        return objectHandler.doRequest(req, HttpStatus.OK, res -> new PutSymlinkOutput()
                .setRequestInfo(res.RequestInfo()).setVersionID(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_VERSIONID)));
    }

    public GetSymlinkOutput getSymlink(GetSymlinkInput input) throws TosException {
        ParamsChecker.ensureNotNull(input, "GetSymlinkInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withQuery("symlink", "").withQuery("versionId", input.getVersionID());
        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.GET, null);

        return objectHandler.doRequest(req, HttpStatus.OK, res -> {
            String symlinkTargetKey = null;
            try {
                symlinkTargetKey = URLDecoder.decode(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SYMLINK_TARGET), "UTF-8");
            } catch (UnsupportedEncodingException e) {
                throw new TosClientException("decode symlink target key failed", e);
            }
            return new GetSymlinkOutput()
                    .setRequestInfo(res.RequestInfo())
                    .setVersionID(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_VERSIONID))
                    .setSymlinkTargetKey(symlinkTargetKey)
                    .setSymlinkTargetBucket(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_SYMLINK_BUCKET))
                    .setLastModified(DateConverter.rfc1123StringToDate(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_LAST_MODIFIED)));
        });
    }

    public ModifyObjectOutput modifyObject(ModifyObjectInput input, String preHashCrc64ecma, boolean enableCrcCheck) {
        ParamsChecker.ensureNotNull(input, "ModifyObjectInput");
        ensureValidBucketName(input.getBucket());
        ensureValidKey(input.getKey());
        InputStream content = ensureNotNullContent(input.getContent());
        RequestBuilder builder = this.factory.init(input.getBucket(), input.getKey(), null)
                .withQuery("modify", "")
                .withQuery("offset", String.valueOf(input.getOffset()))
                .withContentLength(input.getContentLength());

        if (input.getTrafficLimit() > 0) {
            builder = builder.withHeader(TosHeader.HEADER_TRAFFIC_LIMIT, String.valueOf(input.getTrafficLimit()));
        }

        builder = this.handleGenericInput(builder, input);
        TosRequest req = this.factory.build(builder, HttpMethod.POST, content)
                // modifyobject should not retry
                .setRetryableOnServerException(false).setRetryableOnClientException(false)
                .setRateLimiter(input.getRateLimiter())
                .setEnableCrcCheck(enableCrcCheck)
                .setCrc64InitValue(CRC64Utils.unsignedLongStringToLong(preHashCrc64ecma))
                .setDataTransferListener(input.getDataTransferListener());


        return objectHandler.doRequest(req, HttpStatus.OK, res -> {
            String nextModifyOffset = res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_NEXT_MODIFY_OFFSET);
            try {
                return new ModifyObjectOutput().setRequestInfo(res.RequestInfo())
                        .setNextModifyOffset(Long.parseLong(nextModifyOffset))
                        .setHashCrc64ecma(res.getHeaderWithKeyIgnoreCase(TosHeader.HEADER_CRC64));
            } catch (NumberFormatException nfe) {
                throw new TosClientException("tos: server return unexpected Next-Modify-Offset header: " + nextModifyOffset, nfe);
            }
        });
    }

    private List restoreObjectExceptedCodes() {
        List expectedCodes = new ArrayList<>();
        expectedCodes.add(HttpStatus.OK);
        expectedCodes.add(HttpStatus.ACCEPTED);
        return expectedCodes;
    }

    private void addContentType(RequestBuilder rb, String objectKey) throws TosClientException {
        String contentType = rb.getHeaders().get(TosHeader.HEADER_CONTENT_TYPE);
        if (StringUtils.isEmpty(contentType)) {
            // request does not attach content-type and will auto recognize it default.
            // disable it by withAutoRecognizeContentType(false) in a request
            // or by clientAutoRecognizeContentType(false) while setting TosClientConfiguration
            if (this.clientAutoRecognizeContentType && rb.isAutoRecognizeContentType()) {
                // set content type before upload
                contentType = MimeType.getInstance().getMimetype(objectKey);
                rb.withHeader(TosHeader.HEADER_CONTENT_TYPE, contentType);
            }
        }
    }

    private void ensureValidBucketName(String bucket) {
        if (this.factory.isCustomDomain()) {
            // 使用自定义域名时不校验桶名
            return;
        }
        ParamsChecker.isValidBucketName(bucket);
    }

    private void ensureValidKey(String key) {
        ParamsChecker.isValidKey(key);
    }
}




© 2015 - 2024 Weber Informatics LLC | Privacy Policy