/// <summary>
        /// 创建块(携带首片数据),同时检查CRC32
        /// </summary>
        /// <param name="chunkBuffer">数据片,此操作都会携带第一个数据片</param>
        /// <param name="blockSize">块大小,除了最后一块可能不足4MB,前面的所有数据块恒定位4MB</param>
        /// <param name="chunkSize">分片大小,一个块可以被分为若干片依次上传然后拼接或者不分片直接上传整块</param>
        /// <param name="upToken">上传凭证</param>
        /// <returns>此操作执行后的返回结果</returns>
        private HttpResult MakeBlock(byte[] chunkBuffer, long blockSize, long chunkSize, string upToken)
        {
            HttpResult result = new HttpResult();

            try
            {
                //get upload host
                string ak     = UpToken.GetAccessKeyFromUpToken(upToken);
                string bucket = UpToken.GetBucketFromUpToken(upToken);
                if (ak == null || bucket == null)
                {
                    return(HttpResult.InvalidToken);
                }

                string uploadHost = this.config.UpHost(ak, bucket);

                string url        = string.Format("{0}/mkblk/{1}", uploadHost, blockSize);
                string upTokenStr = string.Format("UpToken {0}", upToken);
                using (MemoryStream ms = new MemoryStream(chunkBuffer, 0, (int)chunkSize))
                {
                    byte[] data = ms.ToArray();

                    result = httpManager.PostData(url, data, upTokenStr);

                    if (result.Code == (int)HttpCode.OK)
                    {
                        ResumeContext rc = JsonConvert.DeserializeObject <ResumeContext>(result.Text);

                        if (rc.Crc32 > 0)
                        {
                            uint crc_1 = rc.Crc32;
                            uint crc_2 = CRC32.CheckSumSlice(chunkBuffer, 0, (int)chunkSize);
                            if (crc_1 != crc_2)
                            {
                                result.RefCode  = (int)HttpCode.USER_NEED_RETRY;
                                result.RefText += string.Format(" CRC32: remote={0}, local={1}\n", crc_1, crc_2);
                            }
                        }
                        else
                        {
                            result.RefText += string.Format("[{0}] JSON Decode Error: text = {1}",
                                                            DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), result.Text);
                            result.RefCode = (int)HttpCode.USER_NEED_RETRY;
                        }
                    }
                    else
                    {
                        result.RefCode = (int)HttpCode.USER_NEED_RETRY;
                    }
                }
            }
            catch (Exception ex)
            {
                StringBuilder sb = new StringBuilder();
                sb.AppendFormat("[{0}] mkblk Error: ", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"));
                Exception e = ex;
                while (e != null)
                {
                    sb.Append(e.Message + " ");
                    e = e.InnerException;
                }
                sb.AppendLine();

                if (ex is QiniuException)
                {
                    QiniuException qex = (QiniuException)ex;
                    result.Code     = qex.HttpResult.Code;
                    result.RefCode  = qex.HttpResult.Code;
                    result.Text     = qex.HttpResult.Text;
                    result.RefText += sb.ToString();
                }
                else
                {
                    result.RefCode  = (int)HttpCode.USER_UNDEF;
                    result.RefText += sb.ToString();
                }
            }

            return(result);
        }
        /// <summary>
        /// 分片上传/断点续上传,带有自定义进度处理和上传控制,检查CRC32,可自动重试
        /// </summary>
        /// <param name="stream">待上传文件流</param>
        /// <param name="key">要保存的文件名称</param>
        /// <param name="upToken">上传凭证</param>
        /// <param name="putExtra">可选配置参数</param>
        /// <returns>上传文件后返回结果</returns>
        public HttpResult UploadStream(Stream stream, string key, string upToken, PutExtra putExtra)
        {
            HttpResult result = new HttpResult();

            //check put extra
            if (putExtra == null)
            {
                putExtra = new PutExtra();
            }
            if (putExtra.ProgressHandler == null)
            {
                putExtra.ProgressHandler = DefaultUploadProgressHandler;
            }
            if (putExtra.UploadController == null)
            {
                putExtra.UploadController = DefaultUploadController;
            }
            if (putExtra.MaxRetryTimes == 0)
            {
                putExtra.MaxRetryTimes = DEFAULT_MAX_RETRY_TIMES;
            }

            //start to upload
            try
            {
                long   fileSize    = stream.Length;
                long   chunkSize   = CHUNK_SIZE;
                long   blockSize   = BLOCK_SIZE;
                byte[] chunkBuffer = new byte[chunkSize];
                int    blockCount  = (int)((fileSize + blockSize - 1) / blockSize);
                int    index       = 0; // zero block

                //check resume record file
                ResumeInfo resumeInfo = null;
                if (File.Exists(putExtra.ResumeRecordFile))
                {
                    bool useLastRecord = false;
                    resumeInfo = ResumeHelper.Load(putExtra.ResumeRecordFile);
                    if (resumeInfo != null && fileSize == resumeInfo.FileSize)
                    {
                        //check whether ctx expired
                        if (!UnixTimestamp.IsContextExpired(resumeInfo.ExpiredAt))
                        {
                            useLastRecord = true;
                        }
                    }

                    if (useLastRecord)
                    {
                        index = resumeInfo.BlockIndex;
                    }
                }
                if (resumeInfo == null)
                {
                    resumeInfo = new ResumeInfo()
                    {
                        FileSize   = fileSize,
                        BlockIndex = 0,
                        BlockCount = blockCount,
                        Contexts   = new string[blockCount],
                        ExpiredAt  = 0,
                    };
                }

                //read from offset
                long          offset      = index * blockSize;
                string        context     = null;
                long          expiredAt   = 0;
                long          leftBytes   = fileSize - offset;
                long          blockLeft   = 0;
                long          blockOffset = 0;
                HttpResult    hr          = null;
                ResumeContext rc          = null;

                stream.Seek(offset, SeekOrigin.Begin);

                var  upts             = UploadControllerAction.Activated;
                bool bres             = true;
                var  manualResetEvent = new ManualResetEvent(true);
                int  iTry             = 0;

                while (leftBytes > 0)
                {
                    // 每上传一个BLOCK之前,都要检查一下UPTS
                    upts = putExtra.UploadController();

                    if (upts == UploadControllerAction.Aborted)
                    {
                        result.Code     = (int)HttpCode.USER_CANCELED;
                        result.RefCode  = (int)HttpCode.USER_CANCELED;
                        result.RefText += string.Format("[{0}] [ResumableUpload] Info: upload task is aborted\n",
                                                        DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"));

                        return(result);
                    }
                    else if (upts == UploadControllerAction.Suspended)
                    {
                        if (bres)
                        {
                            bres = false;
                            manualResetEvent.Reset();

                            result.RefCode  = (int)HttpCode.USER_PAUSED;
                            result.RefText += string.Format("[{0}] [ResumableUpload] Info: upload task is paused\n",
                                                            DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"));
                        }
                        manualResetEvent.WaitOne(1000);
                    }
                    else
                    {
                        if (!bres)
                        {
                            bres = true;
                            manualResetEvent.Set();

                            result.RefCode  = (int)HttpCode.USER_RESUMED;
                            result.RefText += string.Format("[{0}] [ResumableUpload] Info: upload task is resumed\n",
                                                            DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"));
                        }

                        #region one-block

                        #region mkblk
                        if (leftBytes < BLOCK_SIZE)
                        {
                            blockSize = leftBytes;
                        }
                        else
                        {
                            blockSize = BLOCK_SIZE;
                        }

                        if (leftBytes < CHUNK_SIZE)
                        {
                            chunkSize = leftBytes;
                        }
                        else
                        {
                            chunkSize = CHUNK_SIZE;
                        }

                        //read data buffer
                        stream.Read(chunkBuffer, 0, (int)chunkSize);

                        iTry = 0;
                        while (++iTry <= putExtra.MaxRetryTimes)
                        {
                            result.RefText += string.Format("[{0}] [ResumableUpload] try mkblk#{1}\n", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), iTry);

                            hr = MakeBlock(chunkBuffer, blockSize, chunkSize, upToken);
                            if (hr.Code == (int)HttpCode.OK && hr.RefCode != (int)HttpCode.USER_NEED_RETRY)
                            {
                                break;
                            }
                        }

                        if (hr.Code != (int)HttpCode.OK || hr.RefCode == (int)HttpCode.USER_NEED_RETRY)
                        {
                            result.Shadow(hr);
                            result.RefText += string.Format("[{0}] [ResumableUpload] Error: mkblk: code = {1}, text = {2}, offset = {3}, blockSize = {4}, chunkSize = {5}\n",
                                                            DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), hr.Code, hr.Text, offset, blockSize, chunkSize);

                            return(result);
                        }

                        if ((rc = JsonConvert.DeserializeObject <ResumeContext>(hr.Text)) == null)
                        {
                            result.Shadow(hr);
                            result.RefCode  = (int)HttpCode.USER_UNDEF;
                            result.RefText += string.Format("[{0}] [ResumableUpload] mkblk Error: JSON Decode Error: text = {1}\n",
                                                            DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), hr.Text);

                            return(result);
                        }

                        context    = rc.Ctx;
                        offset    += chunkSize;
                        leftBytes -= chunkSize;

                        #endregion mkblk
                        putExtra.ProgressHandler(offset, fileSize);

                        if (leftBytes > 0)
                        {
                            blockLeft   = blockSize - chunkSize;
                            blockOffset = chunkSize;
                            while (blockLeft > 0)
                            {
                                #region bput-loop

                                if (blockLeft < CHUNK_SIZE)
                                {
                                    chunkSize = blockLeft;
                                }
                                else
                                {
                                    chunkSize = CHUNK_SIZE;
                                }

                                stream.Read(chunkBuffer, 0, (int)chunkSize);

                                iTry = 0;
                                while (++iTry <= putExtra.MaxRetryTimes)
                                {
                                    result.RefText += string.Format("[{0}] [ResumableUpload] try bput#{1}\n", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), iTry);

                                    hr = BputChunk(chunkBuffer, blockOffset, chunkSize, context, upToken);

                                    if (hr.Code == (int)HttpCode.OK && hr.RefCode != (int)HttpCode.USER_NEED_RETRY)
                                    {
                                        break;
                                    }
                                }
                                if (hr.Code != (int)HttpCode.OK || hr.RefCode == (int)HttpCode.USER_NEED_RETRY)
                                {
                                    result.Shadow(hr);
                                    result.RefText += string.Format("[{0}] [ResumableUpload] Error: bput: code = {1}, text = {2}, offset = {3}, blockOffset = {4}, chunkSize = {5}\n",
                                                                    DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), hr.Code, hr.Text, offset, blockOffset, chunkSize);

                                    return(result);
                                }

                                if ((rc = JsonConvert.DeserializeObject <ResumeContext>(hr.Text)) == null)
                                {
                                    result.Shadow(hr);
                                    result.RefCode  = (int)HttpCode.USER_UNDEF;
                                    result.RefText += string.Format("[{0}] [ResumableUpload] bput Error: JSON Decode Error: text = {1}\n",
                                                                    DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), hr.Text);

                                    return(result);
                                }

                                context = rc.Ctx;
                                if (expiredAt == 0)
                                {
                                    expiredAt = rc.Expired_At;
                                }

                                offset      += chunkSize;
                                leftBytes   -= chunkSize;
                                blockOffset += chunkSize;
                                blockLeft   -= chunkSize;
                                #endregion bput-loop

                                putExtra.ProgressHandler(offset, fileSize);
                            }
                        }

                        #endregion one-block

                        resumeInfo.BlockIndex      = index;
                        resumeInfo.Contexts[index] = context;
                        resumeInfo.ExpiredAt       = expiredAt;
                        if (!string.IsNullOrEmpty(putExtra.ResumeRecordFile))
                        {
                            ResumeHelper.Save(resumeInfo, putExtra.ResumeRecordFile);
                        }
                        ++index;
                    }
                }

                hr = MakeFile(key, fileSize, key, upToken, putExtra, resumeInfo.Contexts);
                if (hr.Code != (int)HttpCode.OK)
                {
                    result.Shadow(hr);
                    result.RefText += string.Format("[{0}] [ResumableUpload] Error: mkfile: code = {1}, text = {2}\n",
                                                    DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), hr.Code, hr.Text);

                    return(result);
                }

                if (File.Exists(putExtra.ResumeRecordFile))
                {
                    File.Delete(putExtra.ResumeRecordFile);
                }
                result.Shadow(hr);
                result.RefText += string.Format("[{0}] [ResumableUpload] Uploaded: \"{1}\" ==> \"{2}\"\n",
                                                DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), putExtra.ResumeRecordFile, key);
            }
            catch (Exception ex)
            {
                Console.WriteLine(ex.StackTrace);
                StringBuilder sb = new StringBuilder();
                sb.AppendFormat("[{0}] [ResumableUpload] Error: ", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"));
                Exception e = ex;
                while (e != null)
                {
                    sb.Append(e.Message + " ");
                    e = e.InnerException;
                }
                sb.AppendLine();

                result.RefCode  = (int)HttpCode.USER_UNDEF;
                result.RefText += sb.ToString();
            }
            finally
            {
                if (stream != null)
                {
                    stream.Close();
                    stream.Dispose();
                }
            }

            return(result);
        }
        /// <summary>
        /// 创建块(携带首片数据),同时检查CRC32
        /// </summary>
        /// <param name="resumeBlockerObj">创建分片上次的块请求</param>
        private void MakeBlock(object resumeBlockerObj)
        {
            ResumeBlocker    resumeBlocker = (ResumeBlocker)resumeBlockerObj;
            ManualResetEvent doneEvent     = resumeBlocker.DoneEvent;
            Dictionary <long, HttpResult> blockMakeResults = resumeBlocker.BlockMakeResults;
            PutExtra   putExtra   = resumeBlocker.PutExtra;
            long       blockIndex = resumeBlocker.BlockIndex;
            HttpResult result     = new HttpResult();

            //check whether to cancel
            while (true)
            {
                UploadControllerAction upCtl = resumeBlocker.PutExtra.UploadController();
                if (upCtl == UploadControllerAction.Suspended)
                {
                    doneEvent.WaitOne(1000);
                    continue;
                }
                else if (upCtl == UploadControllerAction.Aborted)
                {
                    doneEvent.Set();

                    result.Code     = (int)HttpCode.USER_CANCELED;
                    result.RefCode  = (int)HttpCode.USER_CANCELED;
                    result.RefText += string.Format("[{0}] [ResumableUpload] Info: upload task is aborted, mkblk {1}\n",
                                                    DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), blockIndex);
                    blockMakeResults.Add(blockIndex, result);
                    return;
                }
                else
                {
                    break;
                }
            }

            byte[] blockBuffer = resumeBlocker.BlockBuffer;
            int    blockSize   = blockBuffer.Length;

            string upToken = resumeBlocker.UploadToken;
            Dictionary <string, long> uploadedBytesDict = resumeBlocker.UploadedBytesDict;
            long       fileSize     = resumeBlocker.FileSize;
            object     progressLock = resumeBlocker.ProgressLock;
            ResumeInfo resumeInfo   = resumeBlocker.ResumeInfo;

            try
            {
                //get upload host
                string ak     = UpToken.GetAccessKeyFromUpToken(upToken);
                string bucket = UpToken.GetBucketFromUpToken(upToken);
                if (ak == null || bucket == null)
                {
                    result = HttpResult.InvalidToken;
                    doneEvent.Set();
                    return;
                }

                string uploadHost = this.config.UpHost(ak, bucket);

                string url        = string.Format("{0}/mkblk/{1}", uploadHost, blockSize);
                string upTokenStr = string.Format("UpToken {0}", upToken);
                using (MemoryStream ms = new MemoryStream(blockBuffer, 0, blockSize))
                {
                    byte[] data = ms.ToArray();

                    result = httpManager.PostData(url, data, upTokenStr);

                    if (result.Code == (int)HttpCode.OK)
                    {
                        ResumeContext rc = JsonConvert.DeserializeObject <ResumeContext>(result.Text);

                        if (rc.Crc32 > 0)
                        {
                            uint crc_1 = rc.Crc32;
                            uint crc_2 = CRC32.CheckSumSlice(blockBuffer, 0, blockSize);
                            if (crc_1 != crc_2)
                            {
                                result.RefCode  = (int)HttpCode.USER_NEED_RETRY;
                                result.RefText += string.Format(" CRC32: remote={0}, local={1}\n", crc_1, crc_2);
                            }
                            else
                            {
                                //write the mkblk context
                                resumeInfo.Contexts[blockIndex] = rc.Ctx;
                                resumeInfo.ExpiredAt            = rc.ExpiredAt;
                                lock (progressLock)
                                {
                                    uploadedBytesDict["UploadProgress"] += blockSize;
                                }
                                putExtra.ProgressHandler(uploadedBytesDict["UploadProgress"], fileSize);
                            }
                        }
                        else
                        {
                            result.RefText += string.Format("[{0}] JSON Decode Error: text = {1}",
                                                            DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"), result.Text);
                            result.RefCode = (int)HttpCode.USER_NEED_RETRY;
                        }
                    }
                    else
                    {
                        result.RefCode = (int)HttpCode.USER_NEED_RETRY;
                    }
                }
            }
            catch (Exception ex)
            {
                StringBuilder sb = new StringBuilder();
                sb.AppendFormat("[{0}] mkblk Error: ", DateTime.Now.ToString("yyyy-MM-dd HH:mm:ss.ffff"));
                Exception e = ex;
                while (e != null)
                {
                    sb.Append(e.Message + " ");
                    e = e.InnerException;
                }
                sb.AppendLine();

                if (ex is QiniuException)
                {
                    QiniuException qex = (QiniuException)ex;
                    result.Code     = qex.HttpResult.Code;
                    result.RefCode  = qex.HttpResult.Code;
                    result.Text     = qex.HttpResult.Text;
                    result.RefText += sb.ToString();
                }
                else
                {
                    result.RefCode  = (int)HttpCode.USER_UNDEF;
                    result.RefText += sb.ToString();
                }
            }

            //return the http result
            blockMakeResults.Add(blockIndex, result);
            doneEvent.Set();
        }