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
echel0n's avatar
echel0n committed
28
import tarfile
29
import threading
30
31
import traceback

32
33
import scandir

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

38

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

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

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

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

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

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

61
        try:
echel0n's avatar
echel0n committed
62
            if self.check_for_new_version(force) and sickrage.srCore.srConfig.AUTO_UPDATE:
63
64
65
66
                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
67
                sickrage.srCore.srLogger.info("New update found for SiCKRAGE, starting auto-updater ...")
echel0n's avatar
echel0n committed
68
                sickrage.srCore.srNotifications.message(_('New update found for SiCKRAGE, starting auto-updater'))
echel0n's avatar
echel0n committed
69
70
                if self.update():
                    sickrage.srCore.srLogger.info("Update was successful!")
echel0n's avatar
echel0n committed
71
                    sickrage.srCore.srNotifications.message(_('Update was successful'))
72
                    sickrage.srCore.shutdown(restart=True)
echel0n's avatar
echel0n committed
73
74
                else:
                    sickrage.srCore.srLogger.info("Update failed!")
echel0n's avatar
echel0n committed
75
                    sickrage.srCore.srNotifications.message(_('Update failed!'))
echel0n's avatar
echel0n committed
76
77
        finally:
            self.amActive = False
78

79
80
81
82
    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
83
            sickrage.srCore.srNotifications.message(_('Backup'), _('Config backup in progress...'))
84
85
86
87
            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
88

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

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

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

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

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

        returns: type of installation. Possible values are:
120

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

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

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

        return install_type
137
138
139
140
141
142

    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.
143
        :param force: if true the VERSION_NOTIFY setting will be ignored and a check will be forced
144
145
        """

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

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

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

162
                    return True
163

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

echel0n's avatar
echel0n committed
169

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

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

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

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

        # 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:
198
            sickrage.srCore.srLogger.debug("Trying known alternative git locations")
199
200

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

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

        # Still haven't found a working git
echel0n's avatar
echel0n committed
211
212
        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
213

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

        return None

218
219
220
221
222
223
224
    @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)
225
        __, __, exit_status = self._pip_cmd(main_pip, test_cmd)
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242

        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())
243

244
245
246
247
248
        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)
249
                __, __, exit_status = self._pip_cmd(cur_pip, test_cmd)
250
251
252
253
254
255
256
257

                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
258
        error_message = _('Unable to find your pip executable - Set your pip path from Settings->General->Advanced')
259
260
261
262
263
264
        sickrage.srCore.NEWEST_VERSION_STRING = error_message

        return None

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

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

272
        cmd = [git_path] + args.split()
273
274

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

            if output:
                output = output.strip()

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

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

305
        return output, err, exit_status
306

307
308
309
310
311
312
313
314
315
    @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

316
        cmd = [pip_path] + args.split()
317
318

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

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

        if exit_status == 0:
332
            sickrage.srCore.srLogger.debug(' '.join(cmd) + " : returned successful")
333
334
            exit_status = 0
        else:
335
            sickrage.srCore.srLogger.debug(' '.join(cmd) + " returned : " + str(output) + ", treat as error for now")
336
337
338
339
340
341
342
343
344
            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):
345
        __, __, exit_status = self._pip_cmd(self._pip_path,
346
347
                                            'install --no-cache-dir --user -r {}'.format(sickrage.REQS_FILE))
        return (False, True)[exit_status == 0]
348
349
350
351
352
353
354
355
356
357
358
359
360
361


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
362
    def _find_installed_version(self):
363
        """
364
        Attempts to find the currently installed version of SiCKRAGE.
365
366
367
368
369
370

        Uses git show to get commit version.

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

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

379
    def _check_for_new_version(self):
380
381
382
383
384
        """
        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.
        """

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

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

    def set_newest_text(self):

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

echel0n's avatar
echel0n committed
401
        if self.version != self.get_newest_version:
echel0n's avatar
echel0n committed
402
403
404
405
            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
406
            sickrage.srCore.NEWEST_VERSION_STRING = newest_text
407
408

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

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

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

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

434
        return False
435
436
437
438
439
440

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

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

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

463
464
465
466
467
468
469
470
    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()

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

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

        return False

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

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

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

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

503
504
        return []

echel0n's avatar
echel0n committed
505

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

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

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

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

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

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

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

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

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

echel0n's avatar
echel0n committed
545
546
547
            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
548
        else:
echel0n's avatar
echel0n committed
549
550
            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
551
552
553

        sickrage.srCore.NEWEST_VERSION_STRING = newest_text

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

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

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

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

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

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

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

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

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

            # delete .tar.gz
594
            sickrage.srCore.srLogger.info("Deleting file " + tar_download_path)
echel0n's avatar
echel0n committed
595
596
597
598
599
600
            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:
601
                sickrage.srCore.srLogger.error("Invalid update data, update failed: " + str(update_dir_contents))
echel0n's avatar
echel0n committed
602
603
604
605
                return False
            content_dir = os.path.join(sr_update_dir, update_dir_contents[0])

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

                    # 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:
622
                            sickrage.srCore.srLogger.debug("Unable to update " + new_path + ': ' + e.message)
echel0n's avatar
echel0n committed
623
624
625
626
627
628
629
630
                            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:
631
632
            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
633
634
635
            return False

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

638
639
640
        # install requirements
        self.install_requirements()

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


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

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

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

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

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

    def _check_for_new_version(self):
675
676
677
678
679
        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)
680

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

686
687
688
    def set_newest_text(self):

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

echel0n's avatar
echel0n committed
691
        if not self.version:
692
            sickrage.srCore.srLogger.debug("Unknown current version number, don't know if we should update or not")
693
694
            return
        else:
echel0n's avatar
echel0n committed
695
696
            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())
697

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

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

710
        return False