# -*- coding: UTF-8 -*-
import json
import oss2
import base64
import requests
from oss2 import compat
import time

from aliyunsdkcore import client
from aliyunsdkvod.request.v20170321 import CreateUploadVideoRequest
from aliyunsdkvod.request.v20170321 import RefreshUploadVideoRequest
from aliyunsdkvod.request.v20170321 import CreateUploadImageRequest
from aliyunsdkvod.request.v20170321 import CreateUploadAttachedMediaRequest
from voduploadsdk.AliyunVodUtils import *
from voduploadsdk.UploadVideoRequest import UploadVideoRequest

VOD_MAX_TITLE_LENGTH = 128
VOD_MAX_DESCRIPTION_LENGTH = 1024

class AliyunVodUploader:
    
    def __init__(self, accessKeyId, accessKeySecret, ecsRegionId=None):
        """
        constructor for VodUpload
        :param accessKeyId: string, access key id
        :param accessKeySecret: string, access key secret
        :param ecsRegion: string, 部署迁移脚本的ECS所在的Region，详细参考：https://help.aliyun.com/document_detail/40654.html，如：cn-beijing
        :return
        """
        self.__accessKeyId = accessKeyId
        self.__accessKeySecret = accessKeySecret
        self.__ecsRegion = ecsRegionId
        self.__vodApiRegion = None
        self.__connTimeout = 3
        self.__bucketClient = None
        self.__maxRetryTimes = 3
        self.__vodClient = None
        self.__EnableCrc = True
        
        # 分片上传参数
        self.__multipartThreshold = 10 * 1024 * 1024    # 分片上传的阈值，超过此值开启分片上传
        self.__multipartPartSize = 10 * 1024 * 1024     # 分片大小，单位byte
        self.__multipartThreadsNum = 3                  # 分片上传时并行上传的线程数，暂时为串行上传，不支持并行，后续会支持。

        self.setApiRegion('cn-shanghai')


    def setApiRegion(self, apiRegion):
        """
        设置VoD的接入地址，中国大陆为cn-shanghai，海外支持ap-southeast-1(新加坡)等区域，详情参考：https://help.aliyun.com/document_detail/98194.html
        :param apiRegion: 接入地址的Region英文表示
        :return:
        """
        self.__vodApiRegion = apiRegion
        self.__vodClient = self.__initVodClient()


    def setMultipartUpload(self, multipartThreshold=10*1024*1024, multipartPartSize=10*1024*1024, multipartThreadsNum=1):
        if multipartThreshold > 0:
            self.__multipartThreshold = multipartThreshold
        if multipartPartSize > 0:
            self.__multipartPartSize = multipartPartSize
        if multipartThreadsNum > 0:
            self.__multipartThreadsNum = multipartThreadsNum

    def setEnableCrc(self, isEnable=False):
        self.__EnableCrc = True if isEnable else False
    
    @catch_error
    def uploadLocalVideo(self, uploadVideoRequest, startUploadCallback=None):
        """
        上传本地视频或音频文件到点播，最大支持48.8TB的单个文件，暂不支持断点续传
        :param uploadVideoRequest: UploadVideoRequest类的实例，注意filePath为本地文件的绝对路径
        :param startUploadCallback为获取到上传地址和凭证(uploadInfo)后开始进行文件上传时的回调，可用于记录上传日志等；uploadId为设置的上传ID，可用于关联导入视频。
        :return
        """
        uploadInfo = self.__createUploadVideo(uploadVideoRequest)
        if startUploadCallback:
            startUploadCallback(uploadVideoRequest.uploadId, uploadInfo)
        headers = self.__getUploadHeaders(uploadVideoRequest)
        self.__uploadOssObjectWithRetry(uploadVideoRequest.filePath, uploadInfo['UploadAddress']['FileName'], uploadInfo, headers)
        return uploadInfo
    
    @catch_error    
    def uploadWebVideo(self, uploadVideoRequest, startUploadCallback=None):
        """
        上传网络视频或音频文件到点播，最大支持48.8TB的单个文件(需本地磁盘空间足够)；会先下载到本地临时目录，再上传到点播存储
        :param uploadVideoRequest: UploadVideoRequest类的实例，注意filePath为网络文件的URL地址
        :return
        """
        # 下载文件
        uploadVideoRequest = self.__downloadWebMedia(uploadVideoRequest)

        # 上传到点播
        uploadInfo = self.__createUploadVideo(uploadVideoRequest)
        if startUploadCallback:
            startUploadCallback(uploadVideoRequest.uploadId, uploadInfo)
        headers = self.__getUploadHeaders(uploadVideoRequest)
        self.__uploadOssObjectWithRetry(uploadVideoRequest.filePath, uploadInfo['UploadAddress']['FileName'], uploadInfo, headers)

        # 删除本地临时文件
        os.remove(uploadVideoRequest.filePath)

        return uploadInfo['VideoId']
    
    @catch_error   
    def uploadLocalM3u8(self, uploadVideoRequest, sliceFilePaths=None):
        """
        上传本地m3u8视频或音频文件到点播，m3u8文件和分片文件默认在同一目录
        :param uploadVideoRequest: UploadVideoRequest类的实例，注意filePath为本地m3u8索引文件的绝对路径，
              且m3u8文件的分片信息必须是相对地址，不能含有URL或本地绝对路径
        :param sliceFilePaths: list, 分片文件的本地路径列表，例如：['/opt/m3u8_video/sample_001.ts', '/opt/m3u8_video/sample_002.ts']
              sliceFilePaths为None时，会按照同一目录去解析分片地址；如不在同一目录等原因导致解析有误，可自行组装分片地址
        :return
        """

        if sliceFilePaths is None:
            sliceFilePaths = self.parseLocalM3u8(uploadVideoRequest.filePath)

        if (not isinstance(sliceFilePaths, list)) or len(sliceFilePaths) <= 0:
            raise AliyunVodException('InvalidM3u8SliceFile', 'M3u8 slice files invalid', 'sliceFilePaths invalid or m3u8 index file error')

        # 上传到点播的m3u8索引文件会重写，以此确保分片地址都为相对地址
        downloader = AliyunVodDownloader()
        m3u8LocalDir = downloader.getSaveLocalDir() + '/' + AliyunVodUtils.getStringMd5(uploadVideoRequest.fileName)
        downloader.setSaveLocalDir(m3u8LocalDir)
        m3u8LocalPath = m3u8LocalDir + '/' + os.path.basename(uploadVideoRequest.fileName)
        self.__rewriteM3u8File(uploadVideoRequest.filePath, m3u8LocalPath, True)
        
        # 获取上传凭证
        uploadVideoRequest.setFilePath(m3u8LocalPath)
        uploadInfo = self.__createUploadVideo(uploadVideoRequest)
        uploadAddress = uploadInfo['UploadAddress']
        headers = self.__getUploadHeaders(uploadVideoRequest)

        # 依次上传分片文件
        for sliceFilePath in sliceFilePaths:
            tempFilePath, sliceFileName = AliyunVodUtils.getFileBriefPath(sliceFilePath)
            self.__uploadOssObjectWithRetry(sliceFilePath, uploadAddress['ObjectPrefix'] + sliceFileName, uploadInfo, headers)

        # 上传m3u8文件
        self.__uploadOssObjectWithRetry(m3u8LocalPath, uploadAddress['FileName'], uploadInfo, headers)

        # 删除重写到本地的m3u8文件
        if os.path.exists(m3u8LocalPath):
           os.remove(m3u8LocalPath)
        if not os.listdir(m3u8LocalDir):
           os.rmdir(m3u8LocalDir)
            
        return uploadInfo['VideoId']
            
    @catch_error   
    def uploadWebM3u8(self, uploadVideoRequest, sliceFileUrls=None):
        """
        上传网络m3u8视频或音频文件到点播，需本地磁盘空间足够，会先下载到本地临时目录，再上传到点播存储
        :param uploadVideoRequest: UploadVideoRequest类的实例，注意filePath为m3u8网络文件的URL地址
        :param sliceFileUrls: list, 分片文件的url，例如：['http://host/sample_001.ts', 'http://host/sample_002.ts']
            sliceFileUrls为None时，会按照同一前缀解析分片地址；如分片路径和m3u8索引文件前缀不同等原因导致解析有误，可自行组装分片地址
        :return
        """
        if sliceFileUrls is None:
            sliceFileUrls = self.parseWebM3u8(uploadVideoRequest.filePath)

        if (not isinstance(sliceFileUrls, list)) or len(sliceFileUrls) <= 0:
            raise AliyunVodException('InvalidM3u8SliceFile', 'M3u8 slice urls invalid',
                                     'sliceFileUrls invalid or m3u8 index file error')

        # 下载m3u8文件和所有ts分片文件到本地；上传到点播的m3u8索引文件会重写，以此确保分片地址都为相对地址
        downloader = AliyunVodDownloader()
        m3u8LocalDir = downloader.getSaveLocalDir() + '/' + AliyunVodUtils.getStringMd5(uploadVideoRequest.fileName)
        downloader.setSaveLocalDir(m3u8LocalDir)
        m3u8LocalPath = m3u8LocalDir + '/' + os.path.basename(uploadVideoRequest.fileName)
        self.__rewriteM3u8File(uploadVideoRequest.filePath, m3u8LocalPath, False)

        sliceList = []
        for sliceFileUrl in sliceFileUrls:
            tempFilePath, sliceFileName = AliyunVodUtils.getFileBriefPath(sliceFileUrl)
            err, sliceLocalPath = downloader.downloadFile(sliceFileUrl, sliceFileName)
            if sliceLocalPath is None:
                raise AliyunVodException('FileDownloadError', 'Download M3u8 File Error', '')
            sliceList.append((sliceLocalPath, sliceFileName))

        # 获取上传凭证
        uploadVideoRequest.setFilePath(m3u8LocalPath)
        uploadInfo = self.__createUploadVideo(uploadVideoRequest)
        uploadAddress = uploadInfo['UploadAddress']
        headers = self.__getUploadHeaders(uploadVideoRequest)

        # 依次上传分片文件
        for sliceFile in sliceList:
            self.__uploadOssObjectWithRetry(sliceFile[0], uploadAddress['ObjectPrefix'] + sliceFile[1], uploadInfo, headers)

        # 上传m3u8文件
        self.__uploadOssObjectWithRetry(m3u8LocalPath, uploadAddress['FileName'], uploadInfo, headers)

        # 删除下载到本地的m3u8文件和分片文件
        if os.path.exists(m3u8LocalPath):
            os.remove(m3u8LocalPath)
        for sliceFile in sliceList:
            if os.path.exists(sliceFile[0]):
                os.remove(sliceFile[0])
        if not os.listdir(m3u8LocalDir):
            os.rmdir(m3u8LocalDir)

        return uploadInfo['VideoId']

    
    @catch_error
    def uploadImage(self, uploadImageRequest, isLocalFile=True): 
        """
        上传图片文件到点播，不支持断点续传；该接口可支持上传本地图片或网络图片
        :param uploadImageRequest: UploadImageRequest，注意filePath为本地文件的绝对路径或网络文件的URL地址
        :param isLocalFile: bool, 是否为本地文件。True：本地文件，False：网络文件
        :return
        """
        # 网络图片需要先下载到本地
        if not isLocalFile:
            uploadImageRequest = self.__downloadWebMedia(uploadImageRequest)

        # 上传到点播
        uploadInfo = self.__createUploadImage(uploadImageRequest)
        self.__uploadOssObject(uploadImageRequest.filePath, uploadInfo['UploadAddress']['FileName'], uploadInfo, None)

        # 删除本地临时文件
        if not isLocalFile:
            os.remove(uploadImageRequest.filePath)

        return uploadInfo['ImageId'], uploadInfo['ImageURL']

    @catch_error
    def uploadAttachedMedia(self, uploadAttachedRequest, isLocalFile=True):
        """
        上传辅助媒资文件(如水印、字幕文件)到点播，不支持断点续传；该接口可支持上传本地或网络文件
        :param uploadAttachedRequest: UploadAttachedMediaRequest，注意filePath为本地文件的绝对路径或网络文件的URL地址
        :param isLocalFile: bool, 是否为本地文件。True：本地文件，False：网络文件
        :return
        """
        # 网络文件需要先下载到本地
        if not isLocalFile:
            uploadAttachedRequest = self.__downloadWebMedia(uploadAttachedRequest)

        # 上传到点播
        uploadInfo = self.__createUploadAttachedMedia(uploadAttachedRequest)
        self.__uploadOssObject(uploadAttachedRequest.filePath, uploadInfo['UploadAddress']['FileName'], uploadInfo, None)

        # 删除本地临时文件
        if not isLocalFile:
            os.remove(uploadAttachedRequest.filePath)

        result = {'MediaId': uploadInfo['MediaId'], 'MediaURL': uploadInfo['MediaURL'], 'FileURL': uploadInfo['FileURL']}
        return result

    @catch_error
    def parseWebM3u8(self, m3u8FileUrl):
        """
        解析网络m3u8文件得到所有分片文件地址，原理是将m3u8地址前缀拼接ts分片名称作为后者的下载url，适用于url不带签名或分片与m3u8文件签名相同的情况
        本函数解析时会默认分片文件和m3u8文件位于同一目录，如不是则请自行拼接分片文件的地址列表
        :param m3u8FileUrl: string, m3u8网络文件url，例如：http://host/sample.m3u8
        :return sliceFileUrls
        """
        sliceFileUrls = []
        res = requests.get(m3u8FileUrl)
        res.raise_for_status()
        for line in res.iter_lines():
            if line.startswith('#'):
                continue
            sliceFileUrl = AliyunVodUtils.replaceFileName(m3u8FileUrl, line.strip())
            sliceFileUrls.append(sliceFileUrl)

        return sliceFileUrls

    @catch_error
    def parseLocalM3u8(self, m3u8FilePath):
        """
        解析本地m3u8文件得到所有分片文件地址，原理是将m3u8地址前缀拼接ts分片名称作为后者的本地路径
        本函数解析时会默认分片文件和m3u8文件位于同一目录，如不是则请自行拼接分片文件的地址列表
        :param m3u8FilePath: string, m3u8本地文件路径，例如：/opt/videos/sample.m3u8
        :return sliceFilePaths
        """
        sliceFilePaths = []
        m3u8FilePath = AliyunVodUtils.toUnicode(m3u8FilePath)
        for line in open(m3u8FilePath):
            if line.startswith('#'):
                continue
            sliceFileName = line.strip()
            sliceFilePath = AliyunVodUtils.replaceFileName(m3u8FilePath, sliceFileName)
            sliceFilePaths.append(sliceFilePath)

        return sliceFilePaths


    # 定义进度条回调函数；consumedBytes: 已经上传的数据量，totalBytes：总数据量
    def uploadProgressCallback(self, consumedBytes, totalBytes):
    
        if totalBytes:
            rate = int(100 * (float(consumedBytes) / float(totalBytes)))
        else:
            rate = 0
              
        print ("[%s]uploaded %s bytes, percent %s%s" % (AliyunVodUtils.getCurrentTimeStr(), consumedBytes, format(rate), '%'))
        sys.stdout.flush()


    def __initVodClient(self):
         return client.AcsClient(self.__accessKeyId, self.__accessKeySecret, self.__vodApiRegion,
                                 auto_retry=True, max_retry_time=self.__maxRetryTimes, timeout=self.__connTimeout)

    def __downloadWebMedia(self, request):

        # 下载媒体文件到本地临时目录
        downloader = AliyunVodDownloader()
        localFileName = "%s.%s" % (AliyunVodUtils.getStringMd5(request.fileName), request.mediaExt)
        fileUrl = request.filePath
        err, localFilePath = downloader.downloadFile(fileUrl, localFileName)
        if err < 0:
            raise AliyunVodException('FileDownloadError', 'Download File Error', '')

        # 重新设置上传请求对象
        request.setFilePath(localFilePath)
        return request

    def __rewriteM3u8File(self, srcM3u8File, dstM3u8File, isSrcLocal=True):
        newM3u8Text = ''
        if isSrcLocal:
            for line in open(AliyunVodUtils.toUnicode(srcM3u8File)):
                item = self.__processM3u8Line(line)
                if item is not None:
                    newM3u8Text += item + "\n"
        else:
            res = requests.get(srcM3u8File)
            res.raise_for_status()
            for line in res.iter_lines():
                item = self.__processM3u8Line(line)
                if item is not None:
                    newM3u8Text += item + "\n"

        AliyunVodUtils.mkDir(dstM3u8File)
        with open(dstM3u8File, 'w') as f:
            f.write(newM3u8Text)


    def __processM3u8Line(self, line):
        item = line.strip()
        if len(item) <= 0:
            return None

        if item.startswith('#'):
            return item

        tempFilePath, fileName = AliyunVodUtils.getFileBriefPath(item)
        return fileName


    def __requestUploadInfo(self, request, mediaType):
        request.set_accept_format('JSON')
        result = json.loads(self.__vodClient.do_action_with_exception(request).decode('utf-8'))
        result['OriUploadAddress'] = result['UploadAddress']
        result['OriUploadAuth'] = result['UploadAuth']

        result['UploadAddress'] = json.loads(base64.b64decode(result['OriUploadAddress']).decode('utf-8'))
        result['UploadAuth'] = json.loads(base64.b64decode(result['OriUploadAuth']).decode('utf-8'))

        result['MediaType'] = mediaType
        if mediaType == 'video':
            result['MediaId'] = result['VideoId']
        elif mediaType == 'image':
            result['MediaId'] = result['ImageId']
            result['MediaURL'] = result['ImageURL']

        return result


    # 获取视频上传地址和凭证
    def __createUploadVideo(self, uploadVideoRequest):
        request = CreateUploadVideoRequest.CreateUploadVideoRequest()
        
        title = AliyunVodUtils.subString(uploadVideoRequest.title, VOD_MAX_TITLE_LENGTH)
        request.set_Title(title)
        request.set_FileName(uploadVideoRequest.fileName)
    
        if uploadVideoRequest.description:
            description = AliyunVodUtils.subString(uploadVideoRequest.description, VOD_MAX_DESCRIPTION_LENGTH)
            request.set_Description(description)
        if uploadVideoRequest.coverURL:
            request.set_CoverURL(uploadVideoRequest.coverURL)  
        if uploadVideoRequest.tags:
            request.set_Tags(uploadVideoRequest.tags)
        if uploadVideoRequest.cateId:
            request.set_CateId(uploadVideoRequest.cateId)
        if uploadVideoRequest.templateGroupId:
            request.set_TemplateGroupId(uploadVideoRequest.templateGroupId)
        if uploadVideoRequest.storageLocation:
            request.set_StorageLocation(uploadVideoRequest.storageLocation)
        if uploadVideoRequest.userData:
            request.set_UserData(uploadVideoRequest.userData)
        if uploadVideoRequest.appId:
            request.set_AppId(uploadVideoRequest.appId)
        if uploadVideoRequest.workflowId:
            request.set_WorkflowId(uploadVideoRequest.workflowId)

        result = self.__requestUploadInfo(request, 'video')
        logger.info("CreateUploadVideo, FilePath: %s, VideoId: %s" % (uploadVideoRequest.filePath, result['VideoId']))
        return result

    # 刷新上传凭证
    def __refresh_upload_video(self, videoId):
        request = RefreshUploadVideoRequest.RefreshUploadVideoRequest();
        request.set_VideoId(videoId)

        result = self.__requestUploadInfo(request, 'video')
        logger.info("RefreshUploadVideo, VideoId %s" % (result['VideoId']))
        return result
    
    # 获取图片上传地址和凭证
    def __createUploadImage(self, uploadImageRequest):
        request = CreateUploadImageRequest.CreateUploadImageRequest()
        
        request.set_ImageType(uploadImageRequest.imageType)
        request.set_ImageExt(uploadImageRequest.imageExt)
        if uploadImageRequest.title:
            title = AliyunVodUtils.subString(uploadImageRequest.title, VOD_MAX_TITLE_LENGTH)
            request.set_Title(title)
        if uploadImageRequest.description:
            description = AliyunVodUtils.subString(uploadImageRequest.description, VOD_MAX_DESCRIPTION_LENGTH)
            request.set_Description(description)
        if uploadImageRequest.tags:
            request.set_Tags(uploadImageRequest.tags)
        if uploadImageRequest.cateId:
            request.set_CateId(uploadImageRequest.cateId)
        if uploadImageRequest.storageLocation:
            request.set_StorageLocation(uploadImageRequest.storageLocation)
        if uploadImageRequest.userData:
            request.set_UserData(uploadImageRequest.userData)
        if uploadImageRequest.appId:
            request.set_AppId(uploadImageRequest.appId)
        if uploadImageRequest.workflowId:
            request.set_WorkflowId(uploadImageRequest.workflowId)

        result = self.__requestUploadInfo(request, 'image')
        logger.info("CreateUploadImage, FilePath: %s, ImageId: %s, ImageUrl: %s" % (
            uploadImageRequest.filePath, result['ImageId'], result['ImageURL']))
        return result

    def __createUploadAttachedMedia(self, uploadAttachedRequest):
        request = CreateUploadAttachedMediaRequest.CreateUploadAttachedMediaRequest()
        request.set_BusinessType(uploadAttachedRequest.businessType)
        request.set_MediaExt(uploadAttachedRequest.mediaExt)

        if uploadAttachedRequest.title:
            title = AliyunVodUtils.subString(uploadAttachedRequest.title, VOD_MAX_TITLE_LENGTH)
            request.set_Title(title)
        if uploadAttachedRequest.description:
            description = AliyunVodUtils.subString(uploadAttachedRequest.description, VOD_MAX_DESCRIPTION_LENGTH)
            request.set_Description(description)
        if uploadAttachedRequest.tags:
            request.set_Tags(uploadAttachedRequest.tags)
        if uploadAttachedRequest.cateId:
            request.set_CateId(uploadAttachedRequest.cateId)
        if uploadAttachedRequest.storageLocation:
            request.set_StorageLocation(uploadAttachedRequest.storageLocation)
        if uploadAttachedRequest.userData:
            request.set_UserData(uploadAttachedRequest.userData)
        if uploadAttachedRequest.appId:
            request.set_AppId(uploadAttachedRequest.appId)
        if uploadAttachedRequest.workflowId:
            request.set_WorkflowId(uploadAttachedRequest.workflowId)

        result = self.__requestUploadInfo(request, 'attached')
        logger.info("CreateUploadImage, FilePath: %s, MediaId: %s, MediaURL: %s" % (
            uploadAttachedRequest.filePath, result['MediaId'], result['MediaURL']))
        return result

    
    def __getUploadHeaders(self, uploadVideoRequest):
        if uploadVideoRequest.isShowWatermark is None:
            return None
        else:
            userData = "{\"Vod\":{\"UserData\":{\"IsShowWaterMark\": \"%s\"}}}" % (uploadVideoRequest.isShowWatermark)
            return {'x-oss-notification': base64.b64encode(userData, 'utf-8')}

    # uploadType，可选：multipart, put, web
    def __uploadOssObjectWithRetry(self, filePath, object, uploadInfo, headers=None):
        retryTimes = 0
        while retryTimes < self.__maxRetryTimes:
            try:
                return self.__uploadOssObject(filePath, object, uploadInfo, headers)
            except OssError as e:
                # 上传凭证过期需要重新获取凭证
                if e.code == 'SecurityTokenExpired' or e.code == 'InvalidAccessKeyId':
                    uploadInfo = self.__refresh_upload_video(uploadInfo['MediaId'])
            except Exception as e:
                raise e
            except:
                raise AliyunVodException('UnkownError', repr(e), traceback.format_exc())
            finally:
                retryTimes += 1
            
        
    def __uploadOssObject(self, filePath, object, uploadInfo, headers=None):
        self.__createOssClient(uploadInfo['UploadAuth'], uploadInfo['UploadAddress'])
        """
        p = os.path.dirname(os.path.realpath(__file__))
        store = os.path.dirname(p) + '/osstmp'
        return oss2.resumable_upload(self.__bucketClient, object, filePath,
                              store=oss2.ResumableStore(root=store), headers=headers,
                              multipart_threshold=self.__multipartThreshold, part_size=self.__multipartPartSize,
                              num_threads=self.__multipartThreadsNum, progress_callback=self.uploadProgressCallback)
        """
        uploader = _VodResumableUploader(self.__bucketClient, filePath, object, uploadInfo, headers,
                                         self.uploadProgressCallback, self.__refreshUploadAuth)
        uploader.setMultipartInfo(self.__multipartThreshold, self.__multipartPartSize, self.__multipartThreadsNum)
        uploader.setClientId(self.__accessKeyId)
        res = uploader.upload()

        uploadAddress = uploadInfo['UploadAddress']
        bucketHost = uploadAddress['Endpoint'].replace('://', '://' + uploadAddress['Bucket'] + ".")
        logger.info("UploadFile %s Finish, MediaId: %s, FilePath: %s, Destination: %s/%s" % (
            uploadInfo['MediaType'], uploadInfo['MediaId'], filePath, bucketHost, object))
        return res
        
    # 使用上传凭证和地址信息初始化OSS客户端（注意需要先Base64解码并Json Decode再传入）
    # 如果上传的ECS位于点播相同的存储区域（如上海），则可以指定internal为True，通过内网上传更快且免费
    def __createOssClient(self, uploadAuth, uploadAddress):
        auth = oss2.StsAuth(uploadAuth['AccessKeyId'], uploadAuth['AccessKeySecret'], uploadAuth['SecurityToken'])
        endpoint = AliyunVodUtils.convertOssInternal(uploadAddress['Endpoint'], self.__ecsRegion)
        self.__bucketClient = oss2.Bucket(auth, endpoint, uploadAddress['Bucket'],
                                          connect_timeout=self.__connTimeout, enable_crc=self.__EnableCrc)
        return self.__bucketClient

    def __refreshUploadAuth(self, videoId):
        uploadInfo = self.__refresh_upload_video(videoId)
        uploadAuth = uploadInfo['UploadAuth']
        uploadAddress = uploadInfo['UploadAddress']
        return self.__createOssClient(uploadAuth, uploadAddress)


from oss2 import SizedFileAdapter, determine_part_size
from oss2.models import PartInfo
from aliyunsdkcore.utils import parameter_helper as helper
class _VodResumableUploader:
    def __init__(self, bucket, filePath, object, uploadInfo, headers, progressCallback, refreshAuthCallback):
        self.__bucket = bucket
        self.__filePath = filePath
        self.__object = object
        self.__uploadInfo = uploadInfo
        self.__totalSize = None
        self.__headers = headers
        self.__mtime = os.path.getmtime(filePath)
        self.__progressCallback = progressCallback
        self.__refreshAuthCallback = refreshAuthCallback

        self.__threshold = None
        self.__partSize = None
        self.__threadsNum = None
        self.__uploadId = 0

        self.__record = {}
        self.__finishedSize = 0
        self.__finishedParts = []
        self.__filePartHash = None
        self.__clientId = None

    def setMultipartInfo(self, threshold, partSize, threadsNum):
        self.__threshold = threshold
        self.__partSize = partSize
        self.__threadsNum = threadsNum


    def setClientId(self, clientId):
        self.__clientId = clientId


    def upload(self):
        self.__totalSize = os.path.getsize(self.__filePath)
        if self.__threshold and self.__totalSize <= self.__threshold:
            return self.simpleUpload()
        else:
            return self.multipartUpload()


    def simpleUpload(self):
        with open(AliyunVodUtils.toUnicode(self.__filePath), 'rb') as f:
            result = self.__bucket.put_object(self.__object, f, headers=self.__headers, progress_callback=None)
            if self.__uploadInfo['MediaType'] == 'video':
                self.__reportUploadProgress('put', 1, self.__totalSize)

            return result

    def multipartUpload(self):
        psize = oss2.determine_part_size(self.__totalSize, preferred_size=self.__partSize)
        
        # 初始化分片
        self.__uploadId = self.__bucket.init_multipart_upload(self.__object).upload_id

        startTime = time.time()
        expireSeconds = 2500    # 上传凭证有效期3000秒，提前刷新
        # 逐个上传分片
        with open(AliyunVodUtils.toUnicode(self.__filePath), 'rb') as fileObj:
            partNumber = 1
            offset = 0

            while offset < self.__totalSize:
                uploadSize = min(psize, self.__totalSize - offset)
                #logger.info("UploadPart, FilePath: %s, VideoId: %s, UploadId: %s, PartNumber: %s, PartSize: %s" % (self.__fileName, self.__videoId, self.__uploadId, partNumber, uploadSize))
                result = self.__bucket.upload_part(self.__object, self.__uploadId, partNumber, SizedFileAdapter(fileObj,uploadSize))
                #print(result.request_id)
                self.__finishedParts.append(PartInfo(partNumber, result.etag))
                offset += uploadSize
                partNumber += 1

                # 上传进度回调
                self.__progressCallback(offset, self.__totalSize)

                if self.__uploadInfo['MediaType'] == 'video':
                    # 上报上传进度
                    self.__reportUploadProgress('multipart', partNumber - 1, offset)

                    # 检测上传凭证是否过期
                    nowTime = time.time()
                    if nowTime - startTime >= expireSeconds:
                        self.__bucket = self.__refreshAuthCallback(self.__uploadInfo['MediaId'])
                        startTime = nowTime


        # 完成分片上传
        self.__bucket.complete_multipart_upload(self.__object, self.__uploadId, self.__finishedParts, headers=self.__headers)
        
        return result


    def __reportUploadProgress(self, uploadMethod, donePartsCount, doneBytes):
        reportHost = 'vod.cn-shanghai.aliyuncs.com'
        sdkVersion = '1.3.1'
        reportKey = 'HBL9nnSwhtU2$STX'

        uploadPoint = {'upMethod': uploadMethod, 'partSize': self.__partSize, 'doneBytes': doneBytes}
        timestamp = int(time.time())
        authInfo = AliyunVodUtils.getStringMd5("%s|%s|%s" % (self.__clientId, reportKey, timestamp))

        fields = {'Action': 'ReportUploadProgress', 'Format': 'JSON', 'Version': '2017-03-21',
                'Timestamp': helper.get_iso_8061_date(), 'SignatureNonce': helper.get_uuid(),
                'VideoId': self.__uploadInfo['MediaId'], 'Source': 'PythonSDK', 'ClientId': self.__clientId,
                'BusinessType': 'UploadVideo', 'TerminalType': 'PC', 'DeviceModel': 'Server',
                'AppVersion': sdkVersion, 'AuthTimestamp': timestamp, 'AuthInfo': authInfo,

                'FileName': self.__filePath, 'FileHash': self.__getFilePartHash(self.__clientId, self.__filePath, self.__totalSize),
                'FileSize': self.__totalSize, 'FileCreateTime': timestamp, 'UploadRatio': 0, 'UploadId': self.__uploadId,
                'DonePartsCount': donePartsCount, 'PartSize': self.__partSize, 'UploadPoint': json.dumps(uploadPoint),
                'UploadAddress': self.__uploadInfo['OriUploadAddress']
        }
        requests.post('http://' + reportHost, fields, timeout=1)


    def __getFilePartHash(self, clientId, filePath, fileSize):
        if self.__filePartHash:
            return self.__filePartHash

        length = 1 * 1024 * 1024
        if fileSize < length:
            length = fileSize

        try:
            fp = open(AliyunVodUtils.toUnicode(filePath), 'rb')
            strVal = fp.read(length)
            self.__filePartHash = AliyunVodUtils.getStringMd5(strVal, False)
            fp.close()
        except:
            self.__filePartHash = "%s|%s|%s" % (clientId, filePath, self.__mtime)

        return self.__filePartHash
