update.py 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199
  1. #!/usr/bin/env python
  2. # -*- encoding: utf-8 -*-
  3. '''
  4. @Contact : liuyuqi.gov@msn.cn
  5. @Time : 2022/05/24 13:13:52
  6. @License : Copyright © 2017-2022 liuyuqi. All Rights Reserved.
  7. @Desc : update
  8. '''
  9. # todo
  10. from __future__ import unicode_literals
  11. import io
  12. import json
  13. import traceback
  14. import hashlib
  15. import os
  16. import subprocess
  17. import sys
  18. from zipimport import zipimporter
  19. from .version import __version__
  20. def rsa_verify(message, signature, key):
  21. from hashlib import sha256
  22. assert isinstance(message, bytes)
  23. byte_size = (len(bin(key[0])) - 2 + 8 - 1) // 8
  24. signature = ('%x' % pow(int(signature, 16), key[1], key[0])).encode()
  25. signature = (byte_size * 2 - len(signature)) * b'0' + signature
  26. asn1 = b'3031300d060960864801650304020105000420'
  27. asn1 += sha256(message).hexdigest().encode()
  28. if byte_size < len(asn1) // 2 + 11:
  29. return False
  30. expected = b'0001' + (byte_size - len(asn1) //
  31. 2 - 3) * b'ff' + b'00' + asn1
  32. return expected == signature
  33. def update_self(to_screen, verbose, opener):
  34. """Update the program file with the latest version from the repository"""
  35. UPDATE_URL = 'https://yt-dl.org/update/'
  36. VERSION_URL = UPDATE_URL + 'LATEST_VERSION'
  37. JSON_URL = UPDATE_URL + 'versions.json'
  38. UPDATES_RSA_KEY = (0x9d60ee4d8f805312fdb15a62f87b95bd66177b91df176765d13514a0f1754bcd2057295c5b6f1d35daa6742c3ffc9a82d3e118861c207995a8031e151d863c9927e304576bc80692bc8e094896fcf11b66f3e29e04e3a71e9a11558558acea1840aec37fc396fb6b65dc81a1c4144e03bd1c011de62e3f1357b327d08426fe93, 65537)
  39. if not isinstance(globals().get('__loader__'), zipimporter) and not hasattr(sys, 'frozen'):
  40. to_screen('It looks like you installed youtube-dl with a package manager, pip, setup.py or a tarball. Please use that to update.')
  41. return
  42. # Check if there is a new version
  43. try:
  44. newversion = opener.open(VERSION_URL).read().decode('utf-8').strip()
  45. except Exception:
  46. if verbose:
  47. to_screen(encode_compat_str(traceback.format_exc()))
  48. to_screen('ERROR: can\'t find the current version. Please try again later.')
  49. return
  50. if newversion == __version__:
  51. to_screen('youtube-dl is up-to-date (' + __version__ + ')')
  52. return
  53. # Download and check versions info
  54. try:
  55. versions_info = opener.open(JSON_URL).read().decode('utf-8')
  56. versions_info = json.loads(versions_info)
  57. except Exception:
  58. if verbose:
  59. to_screen(encode_compat_str(traceback.format_exc()))
  60. to_screen('ERROR: can\'t obtain versions info. Please try again later.')
  61. return
  62. if 'signature' not in versions_info:
  63. to_screen('ERROR: the versions file is not signed or corrupted. Aborting.')
  64. return
  65. signature = versions_info['signature']
  66. del versions_info['signature']
  67. if not rsa_verify(json.dumps(versions_info, sort_keys=True).encode('utf-8'), signature, UPDATES_RSA_KEY):
  68. to_screen('ERROR: the versions file signature is invalid. Aborting.')
  69. return
  70. version_id = versions_info['latest']
  71. def version_tuple(version_str):
  72. return tuple(map(int, version_str.split('.')))
  73. if version_tuple(__version__) >= version_tuple(version_id):
  74. to_screen('youtube-dl is up to date (%s)' % __version__)
  75. return
  76. to_screen('Updating to version ' + version_id + ' ...')
  77. version = versions_info['versions'][version_id]
  78. print_notes(to_screen, versions_info['versions'])
  79. # sys.executable is set to the full pathname of the exe-file for py2exe
  80. # though symlinks are not followed so that we need to do this manually
  81. # with help of realpath
  82. filename = compat_realpath(
  83. sys.executable if hasattr(sys, 'frozen') else sys.argv[0])
  84. if not os.access(filename, os.W_OK):
  85. to_screen('ERROR: no write permissions on %s' % filename)
  86. return
  87. # Py2EXE
  88. if hasattr(sys, 'frozen'):
  89. exe = filename
  90. directory = os.path.dirname(exe)
  91. if not os.access(directory, os.W_OK):
  92. to_screen('ERROR: no write permissions on %s' % directory)
  93. return
  94. try:
  95. urlh = opener.open(version['exe'][0])
  96. newcontent = urlh.read()
  97. urlh.close()
  98. except (IOError, OSError):
  99. if verbose:
  100. to_screen(encode_compat_str(traceback.format_exc()))
  101. to_screen('ERROR: unable to download latest version')
  102. return
  103. newcontent_hash = hashlib.sha256(newcontent).hexdigest()
  104. if newcontent_hash != version['exe'][1]:
  105. to_screen('ERROR: the downloaded file hash does not match. Aborting.')
  106. return
  107. try:
  108. with open(exe + '.new', 'wb') as outf:
  109. outf.write(newcontent)
  110. except (IOError, OSError):
  111. if verbose:
  112. to_screen(encode_compat_str(traceback.format_exc()))
  113. to_screen('ERROR: unable to write the new version')
  114. return
  115. try:
  116. bat = os.path.join(directory, 'youtube-dl-updater.bat')
  117. with io.open(bat, 'w') as batfile:
  118. batfile.write('''
  119. @echo off
  120. echo Waiting for file handle to be closed ...
  121. ping 127.0.0.1 -n 5 -w 1000 > NUL
  122. move /Y "%s.new" "%s" > NUL
  123. echo Updated youtube-dl to version %s.
  124. start /b "" cmd /c del "%%~f0"&exit /b"
  125. \n''' % (exe, exe, version_id))
  126. subprocess.Popen([bat]) # Continues to run in the background
  127. return # Do not show premature success messages
  128. except (IOError, OSError):
  129. if verbose:
  130. to_screen(encode_compat_str(traceback.format_exc()))
  131. to_screen('ERROR: unable to overwrite current version')
  132. return
  133. # Zip unix package
  134. elif isinstance(globals().get('__loader__'), zipimporter):
  135. try:
  136. urlh = opener.open(version['bin'][0])
  137. newcontent = urlh.read()
  138. urlh.close()
  139. except (IOError, OSError):
  140. if verbose:
  141. to_screen(encode_compat_str(traceback.format_exc()))
  142. to_screen('ERROR: unable to download latest version')
  143. return
  144. newcontent_hash = hashlib.sha256(newcontent).hexdigest()
  145. if newcontent_hash != version['bin'][1]:
  146. to_screen('ERROR: the downloaded file hash does not match. Aborting.')
  147. return
  148. try:
  149. with open(filename, 'wb') as outf:
  150. outf.write(newcontent)
  151. except (IOError, OSError):
  152. if verbose:
  153. to_screen(encode_compat_str(traceback.format_exc()))
  154. to_screen('ERROR: unable to overwrite current version')
  155. return
  156. to_screen('Updated youtube-dl. Restart youtube-dl to use the new version.')
  157. def get_notes(versions, fromVersion):
  158. notes = []
  159. for v, vdata in sorted(versions.items()):
  160. if v > fromVersion:
  161. notes.extend(vdata.get('notes', []))
  162. return notes
  163. def print_notes(to_screen, versions, fromVersion=__version__):
  164. notes = get_notes(versions, fromVersion)
  165. if notes:
  166. to_screen('PLEASE NOTE:')
  167. for note in notes:
  168. to_screen(note)