version_updater.py 27.1 KB
Newer Older
1
# Author: echel0n <[email protected]>
echel0n's avatar
echel0n committed
2
# URL: https://sickrage.ca
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#
# This file is part of SickRage.
#
# SickRage is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# SickRage is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with SickRage.  If not, see <http://www.gnu.org/licenses/>.

19
20
from __future__ import unicode_literals

21
import io
22
23
24
import os
import platform
import re
25
import shutil
echel0n's avatar
echel0n committed
26
import stat
27
import subprocess
28
import sys
echel0n's avatar
echel0n committed
29
import tarfile
30
import threading
31
32
import traceback

33
34
import scandir

35
import sickrage
36
from sickrage.core.helpers import backupSR
37
from sickrage.notifiers import srNotifiers
38

39

echel0n's avatar
echel0n committed
40
class srVersionUpdater(object):
41
42
43
44
    """
    Version check class meant to run as a thread object with the sr scheduler.
    """

45
    def __init__(self):
echel0n's avatar
echel0n committed
46
        self.name = "VERSIONUPDATER"
Fernando's avatar
Fernando committed
47
        self.amActive = False
48
49
50
51

    @property
    def updater(self):
        return self.find_install_type()
52
53

    def run(self, force=False):
echel0n's avatar
echel0n committed
54
        if self.amActive:
55
            return
56

Fernando's avatar
Fernando committed
57
        self.amActive = True
58

59
60
61
        # set thread name
        threading.currentThread().setName(self.name)

62
        try:
echel0n's avatar
echel0n committed
63
            if self.check_for_new_version(force) and sickrage.srCore.srConfig.AUTO_UPDATE:
64
65
66
67
                if sickrage.srCore.SHOWUPDATER.amActive:
                    sickrage.srCore.srLogger.debug("We can't proceed with auto-updating. Shows are being updated")
                    return

echel0n's avatar
echel0n committed
68
                sickrage.srCore.srLogger.info("New update found for SiCKRAGE, starting auto-updater ...")
echel0n's avatar
echel0n committed
69
                sickrage.srCore.srNotifications.message(_('New update found for SiCKRAGE, starting auto-updater'))
echel0n's avatar
echel0n committed
70
71
                if self.update():
                    sickrage.srCore.srLogger.info("Update was successful!")
echel0n's avatar
echel0n committed
72
                    sickrage.srCore.srNotifications.message(_('Update was successful'))
73
                    sickrage.srCore.shutdown(restart=True)
echel0n's avatar
echel0n committed
74
75
                else:
                    sickrage.srCore.srLogger.info("Update failed!")
echel0n's avatar
echel0n committed
76
                    sickrage.srCore.srNotifications.message(_('Update failed!'))
echel0n's avatar
echel0n committed
77
78
        finally:
            self.amActive = False
79

80
81
82
83
    def backup(self):
        if self.safe_to_update():
            # Do a system backup before update
            sickrage.srCore.srLogger.info("Config backup in progress...")
echel0n's avatar
echel0n committed
84
            sickrage.srCore.srNotifications.message(_('Backup'), _('Config backup in progress...'))
85
86
87
88
            try:
                backupDir = os.path.join(sickrage.DATA_DIR, 'backup')
                if not os.path.isdir(backupDir):
                    os.mkdir(backupDir)
Dustyn Gibson's avatar
Dustyn Gibson committed
89

echel0n's avatar
echel0n committed
90
                if backupSR(backupDir, keep_latest=True):
91
                    sickrage.srCore.srLogger.info("Config backup successful, updating...")
echel0n's avatar
echel0n committed
92
                    sickrage.srCore.srNotifications.message(_('Backup'), _('Config backup successful, updating...'))
93
94
95
                    return True
                else:
                    sickrage.srCore.srLogger.error("Config backup failed, aborting update")
echel0n's avatar
echel0n committed
96
                    sickrage.srCore.srNotifications.message(_('Backup'), _('Config backup failed, aborting update'))
97
98
99
                    return False
            except Exception as e:
                sickrage.srCore.srLogger.error('Update: Config backup failed. Error: %s' % e)
echel0n's avatar
echel0n committed
100
                sickrage.srCore.srNotifications.message(_('Backup'), _('Config backup failed, aborting update'))
101
102
                return False

103
104
    @staticmethod
    def safe_to_update():
echel0n's avatar
echel0n committed
105
106
107
        if sickrage.srCore.srConfig.DEVELOPER:
            return False

108
109
        if not sickrage.srCore.started:
            return True
echel0n's avatar
echel0n committed
110
        if not sickrage.srCore.AUTOPOSTPROCESSOR.amActive:
111
            return True
112

113
        sickrage.srCore.srLogger.debug("We can't proceed with the update. Post-Processor is running")
114

115
116
    @staticmethod
    def find_install_type():
117
118
119
120
        """
        Determines how this copy of sr was installed.

        returns: type of installation. Possible values are:
121

122
            'git': running from source using git
123
            'pip': running from source using pip
124
125
126
            'source': running from source without git
        """

127
128
129
        # default to source install type
        install_type = SourceUpdateManager()

130
        if os.path.isdir(os.path.join(os.path.dirname(sickrage.PROG_DIR), '.git')):
131
132
            # GIT install type
            install_type = GitUpdateManager()
133
        elif PipUpdateManager().version:
134
135
136
137
            # PIP install type
            install_type = PipUpdateManager()

        return install_type
138
139
140
141
142
143

    def check_for_new_version(self, force=False):
        """
        Checks the internet for a newer version.

        returns: bool, True for new version or False for no new version.
144
        :param force: if true the VERSION_NOTIFY setting will be ignored and a check will be forced
145
146
        """

147
        if self.updater and self.updater.need_update():
148
            if force: self.updater.set_newest_text()
echel0n's avatar
echel0n committed
149
            return True
150

151
    def update(self):
echel0n's avatar
echel0n committed
152
        if self.updater and self.backup():
153
154
            # check for updates
            if self.updater.need_update():
155
                if self.updater.update():
156
                    # Clean up after update
157
                    to_clean = os.path.join(sickrage.CACHE_DIR, 'mako')
158

159
                    for root, dirs, files in scandir.walk(to_clean, topdown=False):
160
                        [os.remove(os.path.join(root, name)) for name in files]
161
                        [shutil.rmtree(os.path.join(root, name)) for name in dirs]
162

163
                    return True
164

165
    @property
166
    def version(self):
167
        if self.updater:
echel0n's avatar
echel0n committed
168
            return self.updater.version
169

echel0n's avatar
echel0n committed
170

171
class UpdateManager(object):
172
    @property
173
    def _git_path(self):
174
        test_cmd = '--version'
175

176
        main_git = sickrage.srCore.srConfig.GIT_PATH or 'git'
177

178
        sickrage.srCore.srLogger.debug("Checking if we can use git commands: " + main_git + ' ' + test_cmd)
179
        __, __, exit_status = self._git_cmd(main_git, test_cmd)
180
181

        if exit_status == 0:
182
            sickrage.srCore.srLogger.debug("Using: " + main_git)
183
184
            return main_git
        else:
185
            sickrage.srCore.srLogger.debug("Not using: " + main_git)
186
187
188
189
190
191
192
193
194
195
196
197
198

        # trying alternatives
        alternative_git = []

        # osx people who start sr from launchd have a broken path, so try a hail-mary attempt for them
        if platform.system().lower() == 'darwin':
            alternative_git.append('/usr/local/git/bin/git')

        if platform.system().lower() == 'windows':
            if main_git != main_git.lower():
                alternative_git.append(main_git.lower())

        if alternative_git:
199
            sickrage.srCore.srLogger.debug("Trying known alternative git locations")
200
201

            for cur_git in alternative_git:
202
                sickrage.srCore.srLogger.debug("Checking if we can use git commands: " + cur_git + ' ' + test_cmd)
203
                __, __, exit_status = self._git_cmd(cur_git, test_cmd)
204
205

                if exit_status == 0:
206
                    sickrage.srCore.srLogger.debug("Using: " + cur_git)
207
208
                    return cur_git
                else:
209
                    sickrage.srCore.srLogger.debug("Not using: " + cur_git)
210
211

        # Still haven't found a working git
echel0n's avatar
echel0n committed
212
213
        error_message = _('Unable to find your git executable - Set your git path from Settings->General->Advanced OR '
                          'delete your .git folder and run from source to enable updates.')
echel0n's avatar
echel0n committed
214

echel0n's avatar
echel0n committed
215
        sickrage.srCore.NEWEST_VERSION_STRING = error_message
216
217
218

        return None

219
220
221
222
223
224
225
    @property
    def _pip_path(self):
        test_cmd = '-V'

        main_pip = sickrage.srCore.srConfig.PIP_PATH or 'pip'

        sickrage.srCore.srLogger.debug("Checking if we can use pip commands: " + main_pip + ' ' + test_cmd)
226
        __, __, exit_status = self._pip_cmd(main_pip, test_cmd)
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243

        if exit_status == 0:
            sickrage.srCore.srLogger.debug("Using: " + main_pip)
            return main_pip
        else:
            sickrage.srCore.srLogger.debug("Not using: " + main_pip)

        # trying alternatives
        alternative_pip = []

        # osx people who start sr from launchd have a broken path, so try a hail-mary attempt for them
        if platform.system().lower() == 'darwin':
            alternative_pip.append('/usr/local/python2.7/bin/pip')

        if platform.system().lower() == 'windows':
            if main_pip != main_pip.lower():
                alternative_pip.append(main_pip.lower())
244

245
246
247
248
249
        if alternative_pip:
            sickrage.srCore.srLogger.debug("Trying known alternative pip locations")

            for cur_pip in alternative_pip:
                sickrage.srCore.srLogger.debug("Checking if we can use pip commands: " + cur_pip + ' ' + test_cmd)
250
                __, __, exit_status = self._pip_cmd(cur_pip, test_cmd)
251
252
253
254
255
256
257
258

                if exit_status == 0:
                    sickrage.srCore.srLogger.debug("Using: " + cur_pip)
                    return cur_pip
                else:
                    sickrage.srCore.srLogger.debug("Not using: " + cur_pip)

        # Still haven't found a working git
echel0n's avatar
echel0n committed
259
        error_message = _('Unable to find your pip executable - Set your pip path from Settings->General->Advanced')
260
261
262
263
264
265
        sickrage.srCore.NEWEST_VERSION_STRING = error_message

        return None

    @staticmethod
    def _git_cmd(git_path, args):
266
        output = err = None
267
268

        if not git_path:
269
            sickrage.srCore.srLogger.warning("No path to git specified, can't use git commands")
270
            exit_status = 1
271
            return output, err, exit_status
272

273
        cmd = [git_path, args]
274
275

        try:
276
277
278
            sickrage.srCore.srLogger.debug("Executing " + ' '.join(cmd) + " with your shell in " + sickrage.PROG_DIR)
            p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                                 shell=(sys.platform == 'win32'), cwd=sickrage.PROG_DIR)
279
280
281
282
283
284
285
            output, err = p.communicate()
            exit_status = p.returncode

            if output:
                output = output.strip()

        except OSError:
286
            sickrage.srCore.srLogger.info("Command " + ' '.join(cmd) + " didn't work")
287
288
289
            exit_status = 1

        if exit_status == 0:
290
            sickrage.srCore.srLogger.debug(' '.join(cmd) + " : returned successful")
291
292
293
            exit_status = 0
        elif exit_status == 1:
            if 'stash' in output:
294
                sickrage.srCore.srLogger.warning(
echel0n's avatar
echel0n committed
295
                    "Please enable 'git reset' in settings or stash your changes in local files")
296
            else:
297
                sickrage.srCore.srLogger.debug(' '.join(cmd) + " returned : " + str(output))
298
299
            exit_status = 1
        elif exit_status == 128 or 'fatal:' in output or err:
300
            sickrage.srCore.srLogger.debug(' '.join(cmd) + " returned : " + str(output))
301
302
            exit_status = 128
        else:
303
            sickrage.srCore.srLogger.debug(' '.join(cmd) + " returned : " + str(output) + ", treat as error for now")
304
305
            exit_status = 1

306
        return output, err, exit_status
307

308
309
310
311
312
313
314
315
316
    @staticmethod
    def _pip_cmd(pip_path, args):
        output = err = None

        if not pip_path:
            sickrage.srCore.srLogger.warning("No path to pip specified, can't use pip commands")
            exit_status = 1
            return output, err, exit_status

317
        cmd = [pip_path, args]
318
319

        try:
320
321
322
            sickrage.srCore.srLogger.debug("Executing " + ' '.join(cmd) + " with your shell in " + sickrage.PROG_DIR)
            p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                                 shell=(sys.platform == 'win32'), cwd=sickrage.PROG_DIR)
323
324
325
326
327
328
            output, err = p.communicate()
            exit_status = p.returncode

            if output:
                output = output.strip()
        except OSError:
329
            sickrage.srCore.srLogger.info("Command " + ' '.join(cmd) + " didn't work")
330
331
332
            exit_status = 1

        if exit_status == 0:
333
            sickrage.srCore.srLogger.debug(' '.join(cmd) + " : returned successful")
334
335
            exit_status = 0
        else:
336
            sickrage.srCore.srLogger.debug(' '.join(cmd) + " returned : " + str(output) + ", treat as error for now")
337
338
339
340
341
342
343
344
345
            exit_status = 1

        return output, err, exit_status

    @staticmethod
    def get_update_url():
        return "{}/home/update/?pid={}".format(sickrage.srCore.srConfig.WEB_ROOT, sickrage.srCore.PID)

    def install_requirements(self):
346
        __, __, exit_status = self._pip_cmd(self._pip_path,
347
348
                                            'install --no-cache-dir --user -r {}'.format(sickrage.REQS_FILE))
        return (False, True)[exit_status == 0]
349
350
351
352
353
354
355
356
357
358
359
360
361
362


class GitUpdateManager(UpdateManager):
    def __init__(self):
        self.type = "git"

    @property
    def version(self):
        return self._find_installed_version()

    @property
    def get_newest_version(self):
        return self._check_for_new_version() or self.version

echel0n's avatar
echel0n committed
363
    def _find_installed_version(self):
364
        """
365
        Attempts to find the currently installed version of SiCKRAGE.
366
367
368
369
370
371

        Uses git show to get commit version.

        Returns: True for success or False for failure
        """

372
        output, __, exit_status = self._git_cmd(self._git_path, 'rev-parse HEAD')
373
374
375
        if exit_status == 0 and output:
            cur_commit_hash = output.strip()
            if not re.match('^[a-z0-9]+$', cur_commit_hash):
376
                sickrage.srCore.srLogger.error("Output doesn't look like a hash, not using it")
377
                return False
echel0n's avatar
echel0n committed
378
            return cur_commit_hash
379

380
    def _check_for_new_version(self):
381
382
383
384
385
        """
        Uses git commands to check if there is a newer version that the provided
        commit hash. If there is a newer version it sets _num_commits_behind.
        """

386
        # get all new info from server
387
        output, __, exit_status = self._git_cmd(self._git_path, 'remote update')
388
        if not exit_status == 0:
389
            sickrage.srCore.srLogger.warning("Unable to contact server, can't check for update")
390
391
392
            return

        # get latest commit_hash from remote
393
        output, __, exit_status = self._git_cmd(self._git_path, 'rev-parse --verify --quiet "@{upstream}"')
394
        if exit_status == 0 and output:
echel0n's avatar
echel0n committed
395
            return output.strip()
396
397
398
399

    def set_newest_text(self):

        # if we're up to date then don't set this
echel0n's avatar
echel0n committed
400
        sickrage.srCore.NEWEST_VERSION_STRING = None
401

echel0n's avatar
echel0n committed
402
        if self.version != self.get_newest_version:
echel0n's avatar
echel0n committed
403
404
405
406
            newest_text = _(
                'There is a newer version available, version {} &mdash; <a href=\"{}\">Update Now</a>').format(
                self.get_newest_version, self.get_update_url())

echel0n's avatar
echel0n committed
407
            sickrage.srCore.NEWEST_VERSION_STRING = newest_text
408
409

    def need_update(self):
echel0n's avatar
echel0n committed
410
        try:
echel0n's avatar
echel0n committed
411
            return (False, True)[self.version != self.get_newest_version]
echel0n's avatar
echel0n committed
412
        except Exception as e:
413
            sickrage.srCore.srLogger.warning("Unable to contact server, can't check for update: " + repr(e))
echel0n's avatar
echel0n committed
414
            return False
415
416
417

    def update(self):
        """
418
        Calls git pull origin <branch> in order to update SiCKRAGE. Returns a bool depending
419
420
421
422
        on the call's success.
        """

        # remove untracked files and performs a hard reset on git branch to avoid update issues
423
        if sickrage.srCore.srConfig.GIT_RESET:
labrys's avatar
labrys committed
424
            # self.clean() # This is removing user data and backups
425
426
            self.reset()

427
        __, __, exit_status = self._git_cmd(self._git_path, 'pull -f {} {}'.format(sickrage.srCore.srConfig.GIT_REMOTE,
428
                                                                                   self.current_branch))
429
        if exit_status == 0:
430
            sickrage.srCore.srLogger.info("Updating SiCKRAGE from GIT servers")
431
            srNotifiers.notify_version_update(self.get_newest_version)
432
            self.install_requirements()
433
            return True
434

435
        return False
436
437
438
439
440
441

    def clean(self):
        """
        Calls git clean to remove all untracked files. Returns a bool depending
        on the call's success.
        """
442
        __, __, exit_status = self._git_cmd(self._git_path, 'clean -df ""')
443
        return (False, True)[exit_status == 0]
444
445
446
447
448
449

    def reset(self):
        """
        Calls git reset --hard to perform a hard reset. Returns a bool depending
        on the call's success.
        """
450
        __, __, exit_status = self._git_cmd(self._git_path, 'reset --hard')
451
452
        return (False, True)[exit_status == 0]

453
454
    def fetch(self):
        """
455
        Calls git fetch to fetch all remote branches
456
457
        on the call's success.
        """
458
        __, __, exit_status = self._git_cmd(self._git_path,
459
                                            'config remote.origin.fetch %s' % '+refs/heads/*:refs/remotes/origin/*')
460
        if exit_status == 0:
461
            __, __, exit_status = self._git_cmd(self._git_path, 'fetch --all')
462
463
        return (False, True)[exit_status == 0]

464
465
466
467
468
469
470
471
    def checkout_branch(self, branch):
        if branch in self.remote_branches:
            sickrage.srCore.srLogger.debug("Branch checkout: " + self._find_installed_version() + "->" + branch)

            # remove untracked files and performs a hard reset on git branch to avoid update issues
            if sickrage.srCore.srConfig.GIT_RESET:
                self.reset()

472
473
474
            # fetch all branches
            self.fetch()

475
            __, __, exit_status = self._git_cmd(self._git_path, 'checkout -f ' + branch)
476
            if exit_status == 0:
477
                self.install_requirements()
478
479
480
481
                return True

        return False

482
    def get_remote_url(self):
483
        url, __, exit_status = self._git_cmd(self._git_path,
484
                                             'remote get-url {}'.format(sickrage.srCore.srConfig.GIT_REMOTE))
485
486
487
        return ("", url)[exit_status == 0 and url is not None]

    def set_remote_url(self):
echel0n's avatar
echel0n committed
488
        if not sickrage.srCore.srConfig.DEVELOPER:
489
            self._git_cmd(self._git_path, 'remote set-url {} {}'.format(sickrage.srCore.srConfig.GIT_REMOTE,
echel0n's avatar
echel0n committed
490
                                                                        sickrage.srCore.srConfig.GIT_REMOTE_URL))
491

492
493
    @property
    def current_branch(self):
494
        branch, __, exit_status = self._git_cmd(self._git_path, 'rev-parse --abbrev-ref HEAD')
495
        return ("", branch)[exit_status == 0 and branch is not None]
496

497
    @property
echel0n's avatar
echel0n committed
498
    def remote_branches(self):
499
        branches, __, exit_status = self._git_cmd(self._git_path,
500
                                                  'ls-remote --heads {}'.format(sickrage.srCore.srConfig.GIT_REMOTE))
501
        if exit_status == 0 and branches:
echel0n's avatar
echel0n committed
502
            return re.findall(r'refs/heads/(.*)', branches)
503

504
505
        return []

echel0n's avatar
echel0n committed
506

507
508
class SourceUpdateManager(UpdateManager):
    def __init__(self):
509
        self.type = "source"
510

echel0n's avatar
echel0n committed
511
512
513
    @property
    def version(self):
        return self._find_installed_version()
514

echel0n's avatar
echel0n committed
515
516
    @property
    def get_newest_version(self):
517
        return self._check_for_new_version() or self.version
518

519
520
    @staticmethod
    def _find_installed_version():
521
        with io.open(os.path.join(sickrage.PROG_DIR, 'version.txt')) as f:
echel0n's avatar
echel0n committed
522
            return f.read().strip() or ""
523

echel0n's avatar
echel0n committed
524
525
    def need_update(self):
        try:
echel0n's avatar
echel0n committed
526
            return (False, True)[self.version != self.get_newest_version]
echel0n's avatar
echel0n committed
527
        except Exception as e:
528
            sickrage.srCore.srLogger.warning("Unable to contact server, can't check for update: " + repr(e))
echel0n's avatar
echel0n committed
529
            return False
530

echel0n's avatar
echel0n committed
531
    def _check_for_new_version(self):
532
        git_version_url = "https://git.sickrage.ca/SiCKRAGE/sickrage/raw/master/sickrage/version.txt"
echel0n's avatar
echel0n committed
533

534
535
536
537
        try:
            return sickrage.srCore.srWebSession.get(git_version_url).text
        except Exception:
            return self._find_installed_version()
echel0n's avatar
echel0n committed
538

539
    def set_newest_text(self):
echel0n's avatar
echel0n committed
540
541
542
543
        # if we're up to date then don't set this
        sickrage.srCore.NEWEST_VERSION_STRING = None

        if not self.version:
544
            sickrage.srCore.srLogger.debug("Unknown current version number, don't know if we should update or not")
echel0n's avatar
echel0n committed
545

echel0n's avatar
echel0n committed
546
547
548
            newest_text = _("Unknown current version number: If yo've never used the SiCKRAGE upgrade system before "
                            "then current version is not set. &mdash; "
                            "<a href=\"{}\">Update Now</a>").format(self.get_update_url())
echel0n's avatar
echel0n committed
549
        else:
echel0n's avatar
echel0n committed
550
551
            newest_text = _("There is a newer version available, version {} &mdash; "
                            "<a href=\"{}\">Update Now</a>").format(self.get_newest_version, self.get_update_url())
echel0n's avatar
echel0n committed
552
553
554

        sickrage.srCore.NEWEST_VERSION_STRING = newest_text

555
    def update(self):
echel0n's avatar
echel0n committed
556
        """
557
        Downloads the latest source tarball from server and installs it over the existing version.
echel0n's avatar
echel0n committed
558
559
        """

560
        tar_download_url = 'https://git.sickrage.ca/SiCKRAGE/sickrage/repository/archive.tar?ref=master'
echel0n's avatar
echel0n committed
561
562
563

        try:
            # prepare the update dir
564
            sr_update_dir = os.path.join(sickrage.PROG_DIR, 'sr-update')
echel0n's avatar
echel0n committed
565
566

            if os.path.isdir(sr_update_dir):
567
                sickrage.srCore.srLogger.info("Clearing out update folder " + sr_update_dir + " before extracting")
568
                shutil.rmtree(sr_update_dir)
echel0n's avatar
echel0n committed
569

570
            sickrage.srCore.srLogger.info("Creating update folder " + sr_update_dir + " before extracting")
echel0n's avatar
echel0n committed
571
572
573
            os.makedirs(sr_update_dir)

            # retrieve file
574
            sickrage.srCore.srLogger.info("Downloading update from " + repr(tar_download_url))
echel0n's avatar
echel0n committed
575
            tar_download_path = os.path.join(sr_update_dir, 'sr-update.tar')
576
            sickrage.srCore.srWebSession.download(tar_download_url, tar_download_path)
echel0n's avatar
echel0n committed
577
578

            if not os.path.isfile(tar_download_path):
579
                sickrage.srCore.srLogger.warning(
echel0n's avatar
echel0n committed
580
581
582
583
                    "Unable to retrieve new version from " + tar_download_url + ", can't update")
                return False

            if not tarfile.is_tarfile(tar_download_path):
584
585
                sickrage.srCore.srLogger.error(
                    "Retrieved version from " + tar_download_url + " is corrupt, can't update")
echel0n's avatar
echel0n committed
586
587
588
                return False

            # extract to sr-update dir
589
            sickrage.srCore.srLogger.info("Extracting file " + tar_download_path)
echel0n's avatar
echel0n committed
590
591
592
593
594
            tar = tarfile.open(tar_download_path)
            tar.extractall(sr_update_dir)
            tar.close()

            # delete .tar.gz
595
            sickrage.srCore.srLogger.info("Deleting file " + tar_download_path)
echel0n's avatar
echel0n committed
596
597
598
599
600
601
            os.remove(tar_download_path)

            # find update dir name
            update_dir_contents = [x for x in os.listdir(sr_update_dir) if
                                   os.path.isdir(os.path.join(sr_update_dir, x))]
            if len(update_dir_contents) != 1:
602
                sickrage.srCore.srLogger.error("Invalid update data, update failed: " + str(update_dir_contents))
echel0n's avatar
echel0n committed
603
604
605
606
                return False
            content_dir = os.path.join(sr_update_dir, update_dir_contents[0])

            # walk temp folder and move files to main folder
607
            sickrage.srCore.srLogger.info("Moving files from " + content_dir + " to " + sickrage.PROG_DIR)
608
            for dirname, __, filenames in scandir.walk(content_dir):
echel0n's avatar
echel0n committed
609
610
611
                dirname = dirname[len(content_dir) + 1:]
                for curfile in filenames:
                    old_path = os.path.join(content_dir, dirname, curfile)
612
                    new_path = os.path.join(sickrage.PROG_DIR, dirname, curfile)
echel0n's avatar
echel0n committed
613
614
615
616
617
618
619
620
621
622

                    # Avoid DLL access problem on WIN32/64
                    # These files needing to be updated manually
                    # or find a way to kill the access from memory
                    if curfile in ('unrar.dll', 'unrar64.dll'):
                        try:
                            os.chmod(new_path, stat.S_IWRITE)
                            os.remove(new_path)
                            os.renames(old_path, new_path)
                        except Exception as e:
623
                            sickrage.srCore.srLogger.debug("Unable to update " + new_path + ': ' + e.message)
echel0n's avatar
echel0n committed
624
625
626
627
628
629
630
631
                            os.remove(old_path)  # Trash the updated file without moving in new path
                        continue

                    if os.path.isfile(new_path):
                        os.remove(new_path)
                    os.renames(old_path, new_path)

        except Exception as e:
632
633
            sickrage.srCore.srLogger.error("Error while trying to update: {}".format(e.message))
            sickrage.srCore.srLogger.debug("Traceback: " + traceback.format_exc())
echel0n's avatar
echel0n committed
634
635
636
            return False

        # Notify update successful
637
        srNotifiers.notify_version_update(self.get_newest_version)
echel0n's avatar
echel0n committed
638

639
640
641
        # install requirements
        self.install_requirements()

echel0n's avatar
echel0n committed
642
643
644
645
646
647
        return True


class PipUpdateManager(UpdateManager):
    def __init__(self):
        self.type = "pip"
648

649
    @property
echel0n's avatar
echel0n committed
650
    def version(self):
651
        return self._find_installed_version()
652

653
654
    @property
    def get_newest_version(self):
655
        return self._check_for_new_version() or self.version
echel0n's avatar
echel0n committed
656

657
    def _find_installed_version(self):
658
        out, __, exit_status = self._pip_cmd(self._pip_path, 'show sickrage')
659
660
661
        if exit_status == 0:
            return out.split('\n')[1].split()[1]
        return ""
echel0n's avatar
echel0n committed
662
663
664
665
666

    def need_update(self):
        # need this to run first to set self._newest_commit_hash
        try:
            pypi_version = self.get_newest_version
667
            if self.version != pypi_version:
668
                sickrage.srCore.srLogger.debug(
669
                    "Version upgrade: " + self._find_installed_version() + " -> " + pypi_version)
echel0n's avatar
echel0n committed
670
671
                return True
        except Exception as e:
672
            sickrage.srCore.srLogger.warning("Unable to contact PyPi, can't check for update: " + repr(e))
echel0n's avatar
echel0n committed
673
674
675
            return False

    def _check_for_new_version(self):
676
677
678
679
680
        from distutils.version import StrictVersion
        url = "https://pypi.python.org/pypi/{}/json".format('sickrage')
        resp = sickrage.srCore.srWebSession.get(url)
        versions = resp.json()["releases"].keys()
        versions.sort(key=StrictVersion, reverse=True)
681

682
683
        try:
            return versions[0]
echel0n's avatar
echel0n committed
684
        except Exception:
685
            return self._find_installed_version()
686

687
688
689
    def set_newest_text(self):

        # if we're up to date then don't set this
echel0n's avatar
echel0n committed
690
        sickrage.srCore.NEWEST_VERSION_STRING = None
691

echel0n's avatar
echel0n committed
692
        if not self.version:
693
            sickrage.srCore.srLogger.debug("Unknown current version number, don't know if we should update or not")
694
695
            return
        else:
echel0n's avatar
echel0n committed
696
697
            newest_text = _("New SiCKRAGE update found on PyPy servers, version {} &mdash; "
                            "<a href=\"{}\">Update Now</a>").format(self.get_newest_version, self.get_update_url())
698

echel0n's avatar
echel0n committed
699
        sickrage.srCore.NEWEST_VERSION_STRING = newest_text
700
701
702

    def update(self):
        """
703
        Performs pip upgrade
704
        """
705
        __, __, exit_status = self._pip_cmd(self._pip_path, 'install -U --no-cache-dir sickrage')
706
707
        if exit_status == 0:
            sickrage.srCore.srLogger.info("Updating SiCKRAGE from PyPi servers")
708
            srNotifiers.notify_version_update(self.get_newest_version)
709
            return True
710

711
        return False