You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

328 lines
11KB

  1. # -*- coding: UTF-8 -*-
  2. import os, sys
  3. import hashlib
  4. import datetime
  5. import functools
  6. import logging
  7. from loguru import logger
  8. from oss2.exceptions import OssError
  9. from aliyunsdkcore.acs_exception.exceptions import ServerException
  10. from aliyunsdkcore.acs_exception.exceptions import ClientException
  11. import traceback
  12. import requests
  13. if sys.version_info[0] == 3:
  14. import urllib.parse
  15. else:
  16. from urllib import unquote
  17. VOD_PRINT_INFO_LOG_SWITCH = 1
  18. class AliyunVodLog:
  19. """
  20. VOD日志类,基于logging实现
  21. """
  22. @staticmethod
  23. def printLogStr(msg, *args, **kwargs):
  24. if VOD_PRINT_INFO_LOG_SWITCH:
  25. print("[%s]%s" % (AliyunVodUtils.getCurrentTimeStr(), msg))
  26. @staticmethod
  27. def info(msg, *args, **kwargs):
  28. logging.info(msg, *args, **kwargs)
  29. AliyunVodLog.printLogStr(msg, *args, **kwargs)
  30. @staticmethod
  31. def error(msg, *args, **kwargs):
  32. logging.error(msg, *args, **kwargs)
  33. AliyunVodLog.printLogStr(msg, *args, **kwargs)
  34. @staticmethod
  35. def warning(msg, *args, **kwargs):
  36. logging.warning(msg, *args, **kwargs)
  37. AliyunVodLog.printLogStr(msg, *args, **kwargs)
  38. class AliyunVodUtils:
  39. """
  40. VOD上传SDK的工具类,提供截取字符串、获取扩展名、获取文件名等静态函数
  41. """
  42. # 截取字符串,在不超过最大字节数前提下确保中文字符不被截断出现乱码(先转换成unicode,再取子串,然后转换成utf-8)
  43. @staticmethod
  44. def subString(strVal, maxBytes, charSet='utf-8'):
  45. i = maxBytes
  46. if sys.version_info[0] == 3:
  47. while len(strVal.encode(charSet)) > maxBytes:
  48. if i < 0:
  49. return ''
  50. strVal = strVal[:i]
  51. i -= 1
  52. else:
  53. while len(strVal) > maxBytes:
  54. if i < 0:
  55. return ''
  56. strVal = strVal.decode(charSet)[:i].encode(charSet)
  57. i -= 1
  58. return strVal
  59. @staticmethod
  60. def getFileExtension(fileName):
  61. end = fileName.rfind('?')
  62. if end <= 0:
  63. end = len(fileName)
  64. i = fileName.rfind('.')
  65. if i >= 0:
  66. return fileName[i + 1:end].lower()
  67. else:
  68. return None
  69. # urldecode
  70. @staticmethod
  71. def urlDecode(fileUrl):
  72. if sys.version_info[0] == 3:
  73. return urllib.parse.unquote(fileUrl)
  74. else:
  75. return unquote(fileUrl)
  76. # urlencode
  77. @staticmethod
  78. def urlEncode(fileUrl):
  79. if sys.version_info[0] == 3:
  80. return urllib.parse.urlencode(fileUrl)
  81. else:
  82. return urllib.urlencode(fileUrl)
  83. # 获取Url的摘要地址(去除?后的参数,如果有)以及文件名
  84. @staticmethod
  85. def getFileBriefPath(fileUrl):
  86. # fileUrl = AliyunVodUtils.urlDecode(fileUrl)
  87. i = fileUrl.rfind('?')
  88. if i > 0:
  89. briefPath = fileUrl[:i]
  90. else:
  91. briefPath = fileUrl
  92. briefName = os.path.basename(briefPath)
  93. return briefPath, AliyunVodUtils.urlDecode(briefName)
  94. @staticmethod
  95. def getStringMd5(strVal, isEncode=True):
  96. m = hashlib.md5()
  97. m.update(strVal.encode('utf-8') if isEncode else strVal)
  98. return m.hexdigest()
  99. @staticmethod
  100. def getCurrentTimeStr():
  101. now = datetime.datetime.now()
  102. return now.strftime("%Y-%m-%d %H:%M:%S")
  103. # 将oss地址转换为内网地址(如果脚本部署的ecs与oss bucket在同一区域)
  104. @staticmethod
  105. def convertOssInternal(ossUrl, ecsRegion=None, isVpc=False):
  106. if (not ossUrl) or (not ecsRegion):
  107. return ossUrl
  108. availableRegions = ['cn-qingdao', 'cn-beijing', 'cn-zhangjiakou', 'cn-huhehaote', 'cn-hangzhou', 'cn-shanghai',
  109. 'cn-shenzhen',
  110. 'cn-hongkong', 'ap-southeast-1', 'ap-southeast-2', 'ap-southeast-3',
  111. 'ap-northeast-1', 'us-west-1', 'us-east-1', 'eu-central-1', 'me-east-1']
  112. if ecsRegion not in availableRegions:
  113. return ossUrl
  114. ossUrl = ossUrl.replace("https:", "http:")
  115. if isVpc:
  116. return ossUrl.replace("oss-%s.aliyuncs.com" % (ecsRegion), "vpc100-oss-%s.aliyuncs.com" % (ecsRegion))
  117. else:
  118. return ossUrl.replace("oss-%s.aliyuncs.com" % (ecsRegion), "oss-%s-internal.aliyuncs.com" % (ecsRegion))
  119. # 把输入转换为unicode
  120. @staticmethod
  121. def toUnicode(data):
  122. if isinstance(data, bytes):
  123. return data.decode('utf-8')
  124. else:
  125. return data
  126. # 替换路径中的文件名;考虑分隔符为"/" 或 "\"(windows)
  127. @staticmethod
  128. def replaceFileName(filePath, replace):
  129. if len(filePath) <= 0 or len(replace) <= 0:
  130. return filePath
  131. filePath = AliyunVodUtils.urlDecode(filePath)
  132. separator = '/'
  133. start = filePath.rfind(separator)
  134. if start < 0:
  135. separator = '\\'
  136. start = filePath.rfind(separator)
  137. if start < 0:
  138. return None
  139. result = "%s%s%s" % (filePath[0:start], separator, replace)
  140. return result
  141. # 创建文件中的目录
  142. @staticmethod
  143. def mkDir(filePath):
  144. if len(filePath) <= 0:
  145. return -1
  146. separator = '/'
  147. i = filePath.rfind(separator)
  148. if i < 0:
  149. separator = '\\'
  150. i = filePath.rfind(separator)
  151. if i < 0:
  152. return -2
  153. dirs = filePath[:i]
  154. if os.path.exists(dirs) and os.path.isdir(dirs):
  155. return 0
  156. os.makedirs(dirs)
  157. return 1
  158. class AliyunVodException(Exception):
  159. """
  160. VOD上传SDK的异常类,做统一的异常处理,外部捕获此异常即可
  161. """
  162. def __init__(self, type, code, msg, http_status=None, request_id=None):
  163. Exception.__init__(self)
  164. self.type = type or 'UnkownError'
  165. self.code = code
  166. self.message = msg
  167. self.http_status = http_status or 'NULL'
  168. self.request_id = request_id or 'NULL'
  169. def __str__(self):
  170. return "Type: %s, Code: %s, Message: %s, HTTPStatus: %s, RequestId: %s" % (
  171. self.type, self.code, self.message, str(self.http_status), self.request_id)
  172. def catch_error(method):
  173. """
  174. 装饰器,将内部异常转换成统一的异常类AliyunVodException
  175. """
  176. @functools.wraps(method)
  177. def wrapper(self, *args, **kwargs):
  178. try:
  179. return method(self, *args, **kwargs)
  180. except ServerException as e:
  181. logger.error("阿里云ServerException异常, error_code: {}, error_msg:{}, status:{}",
  182. e.get_error_code(), e.get_error_msg(), e.get_http_status())
  183. # 可能原因:AK错误、账号无权限、参数错误等
  184. raise AliyunVodException('ServerException', e.get_error_code(), e.get_error_msg(), e.get_http_status(),
  185. e.get_request_id())
  186. except ClientException as e:
  187. logger.error("阿里云ClientException异常, error_code: {}, error_msg:{}", e.get_error_code(),
  188. e.get_error_msg())
  189. # 可能原因:本地网络故障(如不能连接外网)等
  190. raise AliyunVodException('ClientException', e.get_error_code(), e.get_error_msg())
  191. except OssError as e:
  192. logger.error("阿里云OssError异常, error_code: {}, error_msg:{}, status:{}", e.code, e.message,
  193. e.status)
  194. # 可能原因:上传凭证过期等
  195. raise AliyunVodException('OssError', e.code, e.message, e.status, e.request_id)
  196. except IOError as e:
  197. logger.error("阿里云IOError异常: {}", traceback.format_exc())
  198. # 可能原因:文件URL不能访问、本地文件无法读取等
  199. raise AliyunVodException('IOError', repr(e), traceback.format_exc())
  200. except OSError as e:
  201. logger.error("阿里云OSError异常: {}", traceback.format_exc())
  202. # 可能原因:本地文件不存在等
  203. raise AliyunVodException('OSError', repr(e), traceback.format_exc())
  204. except AliyunVodException as e:
  205. logger.error("阿里云VodException异常: {}", e)
  206. # 可能原因:参数错误
  207. raise e
  208. except Exception as e:
  209. logger.error("阿里云UnkownException异常: {}", traceback.format_exc())
  210. raise AliyunVodException('UnkownException', repr(e), traceback.format_exc())
  211. except:
  212. logger.error("阿里云UnkownError异常: {}", traceback.format_exc())
  213. raise AliyunVodException('UnkownError', 'UnkownError', traceback.format_exc())
  214. return wrapper
  215. class AliyunVodDownloader:
  216. """
  217. VOD网络文件的下载类,上传网络文件时会先下载到本地临时目录,再上传到点播
  218. """
  219. def __init__(self, localDir=None):
  220. if localDir:
  221. self.__localDir = localDir
  222. else:
  223. p = os.path.dirname(os.path.realpath(__file__))
  224. self.__localDir = os.path.dirname(p) + '/dlfiles'
  225. def setSaveLocalDir(self, localDir):
  226. self.__localDir = localDir
  227. def getSaveLocalDir(self):
  228. return self.__localDir
  229. def downloadFile(self, fileUrl, localFileName, fileSize=None):
  230. localPath = self.__localDir + '/' + localFileName
  231. logger.info("Download %s To %s" % (fileUrl, localPath))
  232. try:
  233. lsize = self.getFileSize(localPath)
  234. if fileSize and lsize == fileSize:
  235. logger.info('Download OK, File Exists')
  236. return 0, localPath
  237. AliyunVodUtils.mkDir(self.__localDir)
  238. err, webPage = self.__openWebFile(fileUrl, lsize)
  239. if err == 0:
  240. logger.info('Download OK, File Exists')
  241. webPage.close()
  242. return 0, localPath
  243. fileObj = open(localPath, 'ab+')
  244. for chunk in webPage.iter_content(chunk_size=8 * 1024):
  245. if chunk:
  246. fileObj.write(chunk)
  247. except Exception as e:
  248. logger.error("Download fail: %s" % (e))
  249. return -1, None
  250. fileObj.close()
  251. webPage.close()
  252. logger.info('Download OK')
  253. return 1, localPath
  254. def getFileSize(self, filePath):
  255. try:
  256. lsize = os.stat(filePath).st_size
  257. except:
  258. lsize = 0
  259. return lsize
  260. def __openWebFile(self, fileUrl, offset):
  261. webPage = None
  262. try:
  263. headers = {'Range': 'bytes=%d-' % offset}
  264. webPage = requests.get(fileUrl, stream=True, headers=headers, timeout=120, verify=False)
  265. status_code = webPage.status_code
  266. err = -1
  267. if status_code in [200, 206]:
  268. err = 1
  269. elif status_code == 416:
  270. err = 0
  271. else:
  272. logger.error("Download offset %s fail, invalid url, status: %s" % (offset, status_code))
  273. except Exception as e:
  274. logger.error("Download offset %s fail: %s" % (offset, e))
  275. err = -2
  276. finally:
  277. return err, webPage