]> jfr.im git - yt-dlp.git/blame - yt_dlp/update.py
[build,update] Add GNU-style SHA512 and prepare updater for simlar SHA256 (#383)
[yt-dlp.git] / yt_dlp / update.py
CommitLineData
15938ab6
PH
1from __future__ import unicode_literals
2
c19bc311 3import hashlib
d5ed35b6 4import json
ce02ed60 5import os
e5813e53 6import platform
d2790370 7import subprocess
46353f67 8import sys
c19bc311 9import traceback
d5ed35b6
FV
10from zipimport import zipimporter
11
bfe2b8cf 12from .compat import compat_realpath
c0384f22 13from .utils import encode_compat_str
d3d3e2e3 14
d5ed35b6
FV
15from .version import __version__
16
5f6a1245 17
3dd264bf 18''' # Not signed
d5ed35b6 19def rsa_verify(message, signature, key):
d5ed35b6 20 from hashlib import sha256
15938ab6 21 assert isinstance(message, bytes)
4d318be1
FV
22 byte_size = (len(bin(key[0])) - 2 + 8 - 1) // 8
23 signature = ('%x' % pow(int(signature, 16), key[1], key[0])).encode()
24 signature = (byte_size * 2 - len(signature)) * b'0' + signature
25 asn1 = b'3031300d060960864801650304020105000420'
26 asn1 += sha256(message).hexdigest().encode()
27 if byte_size < len(asn1) // 2 + 11:
5f6a1245 28 return False
4d318be1
FV
29 expected = b'0001' + (byte_size - len(asn1) // 2 - 3) * b'ff' + b'00' + asn1
30 return expected == signature
3dd264bf 31'''
d5ed35b6 32
d7386f62 33
0c3e5f49 34def update_self(to_screen, verbose, opener):
c19bc311 35 ''' Exists for backward compatibility. Use run_update(ydl) instead '''
36
37 printfn = to_screen
38
39 class FakeYDL():
40 _opener = opener
41 to_screen = printfn
42
43 @staticmethod
44 def report_warning(msg, *args, **kwargs):
45 return printfn('WARNING: %s' % msg, *args, **kwargs)
46
47 @staticmethod
48 def report_error(msg, tb=None):
49 printfn('ERROR: %s' % msg)
50 if not verbose:
51 return
52 if tb is None:
53 # Copied from YoutubeDl.trouble
54 if sys.exc_info()[0]:
55 tb = ''
56 if hasattr(sys.exc_info()[1], 'exc_info') and sys.exc_info()[1].exc_info[0]:
57 tb += ''.join(traceback.format_exception(*sys.exc_info()[1].exc_info))
58 tb += encode_compat_str(traceback.format_exc())
59 else:
60 tb_data = traceback.format_list(traceback.extract_stack())
61 tb = ''.join(tb_data)
62 if tb:
63 printfn(tb)
64
65 return run_update(FakeYDL())
66
67
68def run_update(ydl):
e5813e53 69 """
70 Update the program file with the latest version from the repository
71 Returns whether the program should terminate
72 """
d5ed35b6 73
7a5c1cfe 74 JSON_URL = 'https://api.github.com/repos/yt-dlp/yt-dlp/releases/latest'
d5ed35b6 75
c19bc311 76 def report_error(msg, network=False, expected=False, delim=';'):
77 if network:
78 msg += '%s Visit https://github.com/yt-dlp/yt-dlp/releases/latest' % delim
79 ydl.report_error(msg, tb='' if network or expected else None)
80
44f705d0 81 def calc_sha256sum(path):
fa57af1e
U
82 h = hashlib.sha256()
83 b = bytearray(128 * 1024)
84 mv = memoryview(b)
44f705d0 85 with open(os.path.realpath(path), 'rb', buffering=0) as f:
fa57af1e
U
86 for n in iter(lambda: f.readinto(mv), 0):
87 h.update(mv[:n])
88 return h.hexdigest()
89
4040428e 90 err = None
91 if isinstance(globals().get('__loader__'), zipimporter):
92 # We only support python 3.6 or above
93 if sys.version_info < (3, 6):
94 err = 'This is the last release of yt-dlp for Python version %d.%d! Please update to Python 3.6 or above' % sys.version_info[:2]
95 elif hasattr(sys, 'frozen'):
96 # Python 3.6 supports only vista and above
97 if sys.getwindowsversion()[0] < 6:
98 err = 'This is the last release of yt-dlp for your version of Windows. Please update to Windows Vista or above'
99 else:
100 err = 'It looks like you installed yt-dlp with a package manager, pip, setup.py or a tarball. Please use that to update'
101 if err:
102 return report_error(err, expected=True)
d5ed35b6 103
7815e555 104 # sys.executable is set to the full pathname of the exe-file for py2exe
105 # though symlinks are not followed so that we need to do this manually
106 # with help of realpath
107 filename = compat_realpath(sys.executable if hasattr(sys, 'frozen') else sys.argv[0])
c19bc311 108 ydl.to_screen('Current Build Hash %s' % calc_sha256sum(filename))
7815e555 109
d5ed35b6
FV
110 # Download and check versions info
111 try:
c19bc311 112 version_info = ydl._opener.open(JSON_URL).read().decode('utf-8')
3dd264bf 113 version_info = json.loads(version_info)
70a1165b 114 except Exception:
c19bc311 115 return report_error('can\'t obtain versions info. Please try again later ', True, delim='or')
d5ed35b6 116
d7386f62
PH
117 def version_tuple(version_str):
118 return tuple(map(int, version_str.split('.')))
3dd264bf 119
e5813e53 120 version_id = version_info['tag_name']
2e767313 121 if version_tuple(__version__) >= version_tuple(version_id):
c19bc311 122 ydl.to_screen('yt-dlp is up to date (%s)' % __version__)
d7386f62
PH
123 return
124
c19bc311 125 ydl.to_screen('Updating to version ' + version_id + ' ...')
3bf79c75 126
44f705d0 127 version_labels = {
128 'zip_3': '',
44f705d0 129 'exe_64': '.exe',
130 'exe_32': '_x86.exe',
131 }
132
e5813e53 133 def get_bin_info(bin_or_exe, version):
44f705d0 134 label = version_labels['%s_%s' % (bin_or_exe, version)]
c19bc311 135 return next((i for i in version_info['assets'] if i['name'] == 'yt-dlp%s' % label), {})
44f705d0 136
137 def get_sha256sum(bin_or_exe, version):
beb982be 138 filename = 'yt-dlp%s' % version_labels['%s_%s' % (bin_or_exe, version)]
44f705d0 139 urlh = next(
c19bc311 140 (i for i in version_info['assets'] if i['name'] in ('SHA2-256SUMS')),
141 {}).get('browser_download_url')
44f705d0 142 if not urlh:
143 return None
c19bc311 144 hash_data = ydl._opener.open(urlh).read().decode('utf-8')
beb982be
NA
145 if hash_data.startswith('version:'):
146 # Old colon-separated hash file
147 return dict(ln.split(':') for ln in hash_data.splitlines()).get(filename)
148 else:
149 # GNU-style hash file
150 return dict(ln.split()[::-1] for ln in hash_data.splitlines()).get(filename)
d5ed35b6
FV
151
152 if not os.access(filename, os.W_OK):
c19bc311 153 return report_error('no write permissions on %s' % filename, expected=True)
d5ed35b6 154
3dd264bf 155 # PyInstaller
611c1dd9 156 if hasattr(sys, 'frozen'):
e9297256 157 exe = filename
d5ed35b6
FV
158 directory = os.path.dirname(exe)
159 if not os.access(directory, os.W_OK):
c19bc311 160 return report_error('no write permissions on %s' % directory, expected=True)
b25522ba 161 try:
162 if os.path.exists(filename + '.old'):
163 os.remove(filename + '.old')
164 except (IOError, OSError):
165 return report_error('unable to remove the old version')
d5ed35b6
FV
166
167 try:
e5813e53 168 arch = platform.architecture()[0][:2]
44f705d0 169 url = get_bin_info('exe', arch).get('browser_download_url')
170 if not url:
c19bc311 171 return report_error('unable to fetch updates', True)
172 urlh = ydl._opener.open(url)
d5ed35b6
FV
173 newcontent = urlh.read()
174 urlh.close()
e5813e53 175 except (IOError, OSError, StopIteration):
c19bc311 176 return report_error('unable to download latest version', True)
d5ed35b6
FV
177
178 try:
179 with open(exe + '.new', 'wb') as outf:
180 outf.write(newcontent)
0b63aed8 181 except (IOError, OSError):
c19bc311 182 return report_error('unable to write the new version')
d5ed35b6 183
44f705d0 184 expected_sum = get_sha256sum('exe', arch)
185 if not expected_sum:
c19bc311 186 ydl.report_warning('no hash information found for the release')
44f705d0 187 elif calc_sha256sum(exe + '.new') != expected_sum:
c19bc311 188 report_error('unable to verify the new executable', True)
44f705d0 189 try:
190 os.remove(exe + '.new')
191 except OSError:
c19bc311 192 return report_error('unable to remove corrupt download')
44f705d0 193
d5ed35b6 194 try:
b25522ba 195 os.rename(exe, exe + '.old')
196 except (IOError, OSError):
197 return report_error('unable to move current version')
198 try:
199 os.rename(exe + '.new', exe)
0b63aed8 200 except (IOError, OSError):
c19bc311 201 report_error('unable to overwrite current version')
b25522ba 202 os.rename(exe + '.old', exe)
203 return
204 try:
205 # Continues to run in the background
206 subprocess.Popen(
207 'ping 127.0.0.1 -n 5 -w 1000 & del /F "%s.old"' % exe,
208 shell=True, stdout=subprocess.DEVNULL, stderr=subprocess.DEVNULL)
209 ydl.to_screen('Updated yt-dlp to version %s' % version_id)
210 return True # Exit app
211 except OSError:
212 report_error('unable to delete old version')
d5ed35b6
FV
213
214 # Zip unix package
215 elif isinstance(globals().get('__loader__'), zipimporter):
216 try:
4040428e 217 url = get_bin_info('zip', '3').get('browser_download_url')
44f705d0 218 if not url:
c19bc311 219 return report_error('unable to fetch updates', True)
220 urlh = ydl._opener.open(url)
d5ed35b6
FV
221 newcontent = urlh.read()
222 urlh.close()
e5813e53 223 except (IOError, OSError, StopIteration):
c19bc311 224 return report_error('unable to download latest version', True)
d5ed35b6 225
4040428e 226 expected_sum = get_sha256sum('zip', '3')
beb982be
NA
227 if not expected_sum:
228 ydl.report_warning('no hash information found for the release')
229 elif hashlib.sha256(newcontent).hexdigest() != expected_sum:
c19bc311 230 return report_error('unable to verify the new zip', True)
44f705d0 231
232 try:
c4a508ab 233 with open(filename, 'wb') as outf:
234 outf.write(newcontent)
235 except (IOError, OSError):
c19bc311 236 return report_error('unable to overwrite current version')
d5ed35b6 237
c19bc311 238 ydl.to_screen('Updated yt-dlp to version %s; Restart yt-dlp to use the new version' % version_id)
3bf79c75 239
5f6a1245 240
3dd264bf 241''' # UNUSED
46a127ee 242def get_notes(versions, fromVersion):
3bf79c75 243 notes = []
5f6a1245 244 for v, vdata in sorted(versions.items()):
3bf79c75
PH
245 if v > fromVersion:
246 notes.extend(vdata.get('notes', []))
46a127ee
PH
247 return notes
248
5f6a1245 249
46a127ee
PH
250def print_notes(to_screen, versions, fromVersion=__version__):
251 notes = get_notes(versions, fromVersion)
3bf79c75 252 if notes:
15938ab6 253 to_screen('PLEASE NOTE:')
3bf79c75
PH
254 for note in notes:
255 to_screen(note)
3dd264bf 256'''