version_updater.py 28.6 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
import time
31
32
import traceback

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

37

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

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

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

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

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

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

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

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

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

101
102
103
    @staticmethod
    def _keeplatestbackup(backupDir=None):
        if not backupDir:
104
            return False
Dustyn Gibson's avatar
Dustyn Gibson committed
105

106
        import glob
107
        files = glob.glob(os.path.join(backupDir, '*.zip'))
108
109
110
111
        if not files:
            return True

        now = time.time()
112
        newest = files[0], now - os.path.getctime(files[0])
113
        for f in files[1:]:
114
            age = now - os.path.getctime(f)
115
116
117
118
119
            if age < newest[1]:
                newest = f, age
        files.remove(newest[0])

        for f in files:
120
            os.remove(f)
121
122
123

        return True

124
125
    @staticmethod
    def safe_to_update():
echel0n's avatar
echel0n committed
126
127
128
        if sickrage.srCore.srConfig.DEVELOPER:
            return False

129
130
        if not sickrage.srCore.started:
            return True
echel0n's avatar
echel0n committed
131
        if not sickrage.srCore.AUTOPOSTPROCESSOR.amActive:
132
            return True
133

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

136
137
    @staticmethod
    def find_install_type():
138
139
140
141
        """
        Determines how this copy of sr was installed.

        returns: type of installation. Possible values are:
142

143
            'git': running from source using git
144
            'pip': running from source using pip
145
146
147
            'source': running from source without git
        """

148
149
150
        # default to source install type
        install_type = SourceUpdateManager()

151
        if os.path.isdir(os.path.join(os.path.dirname(sickrage.PROG_DIR), '.git')):
152
153
            # GIT install type
            install_type = GitUpdateManager()
154
        elif PipUpdateManager().version:
155
156
157
158
            # PIP install type
            install_type = PipUpdateManager()

        return install_type
159
160
161
162
163
164

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

168
        if self.updater and self.updater.need_update():
169
            if force: self.updater.set_newest_text()
echel0n's avatar
echel0n committed
170
            return True
171

172
    def update(self):
echel0n's avatar
echel0n committed
173
        if self.updater and self.backup():
174
175
            # check for updates
            if self.updater.need_update():
176
                if self.updater.update():
177
                    # Clean up after update
178
                    to_clean = os.path.join(sickrage.CACHE_DIR, 'mako')
179

180
                    for root, dirs, files in os.walk(to_clean, topdown=False):
181
                        [os.remove(os.path.join(root, name)) for name in files]
182
                        [shutil.rmtree(os.path.join(root, name)) for name in dirs]
183

184
                    return True
185

186
    @property
187
    def version(self):
188
        if self.updater:
echel0n's avatar
echel0n committed
189
            return self.updater.version
190

echel0n's avatar
echel0n committed
191

192
class UpdateManager(object):
193
    @property
194
    def _git_path(self):
195
196
        test_cmd = 'version'

197
        main_git = sickrage.srCore.srConfig.GIT_PATH or 'git'
198

199
        sickrage.srCore.srLogger.debug("Checking if we can use git commands: " + main_git + ' ' + test_cmd)
200
        _, _, exit_status = self._git_cmd(main_git, test_cmd)
201
202

        if exit_status == 0:
203
            sickrage.srCore.srLogger.debug("Using: " + main_git)
204
205
            return main_git
        else:
206
            sickrage.srCore.srLogger.debug("Not using: " + main_git)
207
208
209
210
211
212
213
214
215
216
217
218
219

        # 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:
220
            sickrage.srCore.srLogger.debug("Trying known alternative git locations")
221
222

            for cur_git in alternative_git:
223
                sickrage.srCore.srLogger.debug("Checking if we can use git commands: " + cur_git + ' ' + test_cmd)
224
                _, _, exit_status = self._git_cmd(cur_git, test_cmd)
225
226

                if exit_status == 0:
227
                    sickrage.srCore.srLogger.debug("Using: " + cur_git)
228
229
                    return cur_git
                else:
230
                    sickrage.srCore.srLogger.debug("Not using: " + cur_git)
231
232

        # Still haven't found a working git
echel0n's avatar
echel0n committed
233
234
235
        error_message = _('Unable to find your git executable - Shutdown SiCKRAGE and EITHER set git_path in your '
                          'config.ini OR delete your .git folder and run from source to enable updates.')

echel0n's avatar
echel0n committed
236
        sickrage.srCore.NEWEST_VERSION_STRING = error_message
237
238
239

        return None

240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
    @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)
        _, _, exit_status = self._pip_cmd(main_pip, test_cmd)

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

266
267
268
269
270
271
272
273
274
275
276
277
278
279
        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)
                _, _, exit_status = self._pip_cmd(cur_pip, test_cmd)

                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
280
        error_message = _('Unable to find your pip executable - Shutdown SiCKRAGE and set pip_path in your config.ini')
281
282
283
284
285
286
        sickrage.srCore.NEWEST_VERSION_STRING = error_message

        return None

    @staticmethod
    def _git_cmd(git_path, args):
287
        output = err = None
288
289

        if not git_path:
290
            sickrage.srCore.srLogger.warning("No path to git specified, can't use git commands")
291
            exit_status = 1
292
            return output, err, exit_status
293
294
295
296

        cmd = git_path + ' ' + args

        try:
297
            sickrage.srCore.srLogger.debug("Executing " + cmd + " with your shell in " + sickrage.PROG_DIR)
298
            p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
299
                                 shell=True, cwd=sickrage.PROG_DIR)
300
301
302
303
304
305
306
            output, err = p.communicate()
            exit_status = p.returncode

            if output:
                output = output.strip()

        except OSError:
307
            sickrage.srCore.srLogger.info("Command " + cmd + " didn't work")
308
309
310
            exit_status = 1

        if exit_status == 0:
311
            sickrage.srCore.srLogger.debug(cmd + " : returned successful")
312
313
314
315
            exit_status = 0

        elif exit_status == 1:
            if 'stash' in output:
316
                sickrage.srCore.srLogger.warning(
echel0n's avatar
echel0n committed
317
                    "Please enable 'git reset' in settings or stash your changes in local files")
318
            else:
319
                sickrage.srCore.srLogger.debug(cmd + " returned : " + str(output))
320
321
322
            exit_status = 1

        elif exit_status == 128 or 'fatal:' in output or err:
323
            sickrage.srCore.srLogger.debug(cmd + " returned : " + str(output))
324
325
326
            exit_status = 128

        else:
327
            sickrage.srCore.srLogger.debug(cmd + " returned : " + str(output) + ", treat as error for now")
328
329
            exit_status = 1

330
        return output, err, exit_status
331

332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
    @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

        cmd = pip_path + ' ' + args

        try:
            sickrage.srCore.srLogger.debug("Executing " + cmd + " with your shell in " + sickrage.PROG_DIR)
            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:
            sickrage.srCore.srLogger.info("Command " + cmd + " didn't work")
            exit_status = 1

        if exit_status == 0:
            sickrage.srCore.srLogger.debug(cmd + " : returned successful")
            exit_status = 0
        else:
            sickrage.srCore.srLogger.debug(cmd + " returned : " + str(output) + ", treat as error for now")
            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)

    @staticmethod
    def github():
        import github

        try:
            return github.Github(
                login_or_token=sickrage.srCore.srConfig.GIT_USERNAME,
                password=sickrage.srCore.srConfig.GIT_PASSWORD,
                user_agent="SiCKRAGE")
echel0n's avatar
echel0n committed
378
        except Exception:
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
            return github.Github(user_agent="SiCKRAGE")

    def install_requirements(self):
        _, _, exit_status = self._pip_cmd(self._pip_path,
                                          'install --no-cache-dir --user -r {}'.format(sickrage.REQS_FILE))
        if not exit_status == 0:
            sickrage.srCore.srLogger.warning(
                "Failed to install requirements, please manually run 'pip install --no-cache-dir --user -r {}".format(
                    sickrage.REQS_FILE))


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

    @staticmethod
    def _git_error():
echel0n's avatar
echel0n committed
404
405
406
        error_message = _('Unable to find your git executable - Shutdown SiCKRAGE and EITHER set git_path in your '
                          'config.ini OR delete your .git folder and run from source to enable updates. ')

407
408
        sickrage.srCore.NEWEST_VERSION_STRING = error_message

echel0n's avatar
echel0n committed
409
    def _find_installed_version(self):
410
        """
411
        Attempts to find the currently installed version of SiCKRAGE.
412
413
414
415
416
417

        Uses git show to get commit version.

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

418
        output, _, exit_status = self._git_cmd(self._git_path, 'rev-parse HEAD')
419
420
421
        if exit_status == 0 and output:
            cur_commit_hash = output.strip()
            if not re.match('^[a-z0-9]+$', cur_commit_hash):
422
                sickrage.srCore.srLogger.error("Output doesn't look like a hash, not using it")
423
                return False
echel0n's avatar
echel0n committed
424
            return cur_commit_hash
425

426
    def _check_for_new_version(self):
427
428
429
430
431
        """
        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.
        """

432
        # get all new info from server
433
        output, _, exit_status = self._git_cmd(self._git_path, 'remote update')
434
        if not exit_status == 0:
435
            sickrage.srCore.srLogger.warning("Unable to contact server, can't check for update")
436
437
438
            return

        # get latest commit_hash from remote
439
        output, _, exit_status = self._git_cmd(self._git_path, 'rev-parse --verify --quiet "@{upstream}"')
440
        if exit_status == 0 and output:
echel0n's avatar
echel0n committed
441
            return output.strip()
442
443
444
445

    def set_newest_text(self):

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

echel0n's avatar
echel0n committed
448
        if self.version != self.get_newest_version:
echel0n's avatar
echel0n committed
449
450
451
452
            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
453
            sickrage.srCore.NEWEST_VERSION_STRING = newest_text
454
455

    def need_update(self):
echel0n's avatar
echel0n committed
456
        try:
echel0n's avatar
echel0n committed
457
            return (False, True)[self.version != self.get_newest_version]
echel0n's avatar
echel0n committed
458
        except Exception as e:
459
            sickrage.srCore.srLogger.warning("Unable to contact server, can't check for update: " + repr(e))
echel0n's avatar
echel0n committed
460
            return False
461
462
463

    def update(self):
        """
464
        Calls git pull origin <branch> in order to update SiCKRAGE. Returns a bool depending
465
466
467
468
        on the call's success.
        """

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

473
        _, _, exit_status = self._git_cmd(self._git_path, 'pull -f {} {}'.format(sickrage.srCore.srConfig.GIT_REMOTE,
echel0n's avatar
echel0n committed
474
                                                                                 self.current_branch))
475
        if exit_status == 0:
476
            sickrage.srCore.srLogger.info("Updating SiCKRAGE from GIT servers")
477
            srNotifiers.notify_version_update(self.get_newest_version)
478
            self.install_requirements()
479
            return True
480

481
        return False
482
483
484
485
486
487

    def clean(self):
        """
        Calls git clean to remove all untracked files. Returns a bool depending
        on the call's success.
        """
488
        _, _, exit_status = self._git_cmd(self._git_path, 'clean -df ""')
489
        return (False, True)[exit_status == 0]
490
491
492
493
494
495

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

499
500
    def fetch(self):
        """
501
        Calls git fetch to fetch all remote branches
502
503
        on the call's success.
        """
504
        _, _, exit_status = self._git_cmd(self._git_path,
505
                                          'config remote.origin.fetch %s' % '+refs/heads/*:refs/remotes/origin/*')
506
        if exit_status == 0:
507
            _, _, exit_status = self._git_cmd(self._git_path, 'fetch --all')
508
509
        return (False, True)[exit_status == 0]

510
511
512
513
514
515
516
517
    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()

518
519
520
            # fetch all branches
            self.fetch()

521
            _, _, exit_status = self._git_cmd(self._git_path, 'checkout -f ' + branch)
522
            if exit_status == 0:
523
                self.install_requirements()
524
525
526
527
                return True

        return False

528
    def get_remote_url(self):
529
        url, _, exit_status = self._git_cmd(self._git_path,
echel0n's avatar
echel0n committed
530
                                            'remote get-url {}'.format(sickrage.srCore.srConfig.GIT_REMOTE))
531
532
533
        return ("", url)[exit_status == 0 and url is not None]

    def set_remote_url(self):
echel0n's avatar
echel0n committed
534
        if not sickrage.srCore.srConfig.DEVELOPER:
535
            self._git_cmd(self._git_path, 'remote set-url {} {}'.format(sickrage.srCore.srConfig.GIT_REMOTE,
echel0n's avatar
echel0n committed
536
                                                                        sickrage.srCore.srConfig.GIT_REMOTE_URL))
537

538
539
    @property
    def current_branch(self):
540
        branch, _, exit_status = self._git_cmd(self._git_path, 'rev-parse --abbrev-ref HEAD')
541
        return ("", branch)[exit_status == 0 and branch is not None]
542

543
    @property
echel0n's avatar
echel0n committed
544
    def remote_branches(self):
545
        branches, _, exit_status = self._git_cmd(self._git_path,
echel0n's avatar
echel0n committed
546
                                                 'ls-remote --heads {}'.format(sickrage.srCore.srConfig.GIT_REMOTE))
547
        if exit_status == 0 and branches:
echel0n's avatar
echel0n committed
548
            return re.findall(r'refs/heads/(.*)', branches)
549

550
551
        return []

echel0n's avatar
echel0n committed
552

553
554
class SourceUpdateManager(UpdateManager):
    def __init__(self):
555
        self.type = "source"
556

echel0n's avatar
echel0n committed
557
558
559
    @property
    def version(self):
        return self._find_installed_version()
560

echel0n's avatar
echel0n committed
561
562
    @property
    def get_newest_version(self):
563
        return self._check_for_new_version() or self.version
564

565
566
    @staticmethod
    def _find_installed_version():
567
        with io.open(os.path.join(sickrage.PROG_DIR, 'version.txt')) as f:
echel0n's avatar
echel0n committed
568
            return f.read().strip() or ""
569

echel0n's avatar
echel0n committed
570
571
    def need_update(self):
        try:
echel0n's avatar
echel0n committed
572
            return (False, True)[self.version != self.get_newest_version]
echel0n's avatar
echel0n committed
573
        except Exception as e:
574
            sickrage.srCore.srLogger.warning("Unable to contact server, can't check for update: " + repr(e))
echel0n's avatar
echel0n committed
575
            return False
576

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

580
581
582
583
        try:
            return sickrage.srCore.srWebSession.get(git_version_url).text
        except Exception:
            return self._find_installed_version()
echel0n's avatar
echel0n committed
584

585
    def set_newest_text(self):
echel0n's avatar
echel0n committed
586
587
588
589
        # if we're up to date then don't set this
        sickrage.srCore.NEWEST_VERSION_STRING = None

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

echel0n's avatar
echel0n committed
592
593
594
            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
595
        else:
echel0n's avatar
echel0n committed
596
597
            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
598
599
600

        sickrage.srCore.NEWEST_VERSION_STRING = newest_text

601
    def update(self):
echel0n's avatar
echel0n committed
602
        """
603
        Downloads the latest source tarball from server and installs it over the existing version.
echel0n's avatar
echel0n committed
604
605
        """

606
        tar_download_url = 'https://git.sickrage.ca/SiCKRAGE/sickrage/repository/archive.tar?ref=master'
echel0n's avatar
echel0n committed
607
608
609

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

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

616
            sickrage.srCore.srLogger.info("Creating update folder " + sr_update_dir + " before extracting")
echel0n's avatar
echel0n committed
617
618
619
            os.makedirs(sr_update_dir)

            # retrieve file
620
            sickrage.srCore.srLogger.info("Downloading update from " + repr(tar_download_url))
echel0n's avatar
echel0n committed
621
            tar_download_path = os.path.join(sr_update_dir, 'sr-update.tar')
622
            sickrage.srCore.srWebSession.download(tar_download_url, tar_download_path)
echel0n's avatar
echel0n committed
623
624

            if not os.path.isfile(tar_download_path):
625
                sickrage.srCore.srLogger.warning(
echel0n's avatar
echel0n committed
626
627
628
629
                    "Unable to retrieve new version from " + tar_download_url + ", can't update")
                return False

            if not tarfile.is_tarfile(tar_download_path):
630
631
                sickrage.srCore.srLogger.error(
                    "Retrieved version from " + tar_download_url + " is corrupt, can't update")
echel0n's avatar
echel0n committed
632
633
634
                return False

            # extract to sr-update dir
635
            sickrage.srCore.srLogger.info("Extracting file " + tar_download_path)
echel0n's avatar
echel0n committed
636
637
638
639
640
            tar = tarfile.open(tar_download_path)
            tar.extractall(sr_update_dir)
            tar.close()

            # delete .tar.gz
641
            sickrage.srCore.srLogger.info("Deleting file " + tar_download_path)
echel0n's avatar
echel0n committed
642
643
644
645
646
647
            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:
648
                sickrage.srCore.srLogger.error("Invalid update data, update failed: " + str(update_dir_contents))
echel0n's avatar
echel0n committed
649
650
651
652
                return False
            content_dir = os.path.join(sr_update_dir, update_dir_contents[0])

            # walk temp folder and move files to main folder
653
            sickrage.srCore.srLogger.info("Moving files from " + content_dir + " to " + sickrage.PROG_DIR)
654
            for dirname, _, filenames in os.walk(content_dir):
echel0n's avatar
echel0n committed
655
656
657
                dirname = dirname[len(content_dir) + 1:]
                for curfile in filenames:
                    old_path = os.path.join(content_dir, dirname, curfile)
658
                    new_path = os.path.join(sickrage.PROG_DIR, dirname, curfile)
echel0n's avatar
echel0n committed
659
660
661
662
663
664
665
666
667
668

                    # 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:
669
                            sickrage.srCore.srLogger.debug("Unable to update " + new_path + ': ' + e.message)
echel0n's avatar
echel0n committed
670
671
672
673
674
675
676
677
                            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:
678
679
            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
680
681
682
            return False

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

685
686
687
        # install requirements
        self.install_requirements()

echel0n's avatar
echel0n committed
688
689
690
691
692
693
        return True


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

695
    @property
echel0n's avatar
echel0n committed
696
    def version(self):
697
        return self._find_installed_version()
698

699
700
    @property
    def get_newest_version(self):
701
        return self._check_for_new_version() or self.version
echel0n's avatar
echel0n committed
702

703
704
    @staticmethod
    def _pip_error():
echel0n's avatar
echel0n committed
705
        error_message = _('Unable to find your pip executable - Shutdown SiCKRAGE and set pip_path in your config.ini.')
706
707
        sickrage.srCore.NEWEST_VERSION_STRING = error_message

708
    def _find_installed_version(self):
709
        out, _, exit_status = self._pip_cmd(self._pip_path, 'show sickrage')
710
711
712
        if exit_status == 0:
            return out.split('\n')[1].split()[1]
        return ""
echel0n's avatar
echel0n committed
713
714
715
716
717

    def need_update(self):
        # need this to run first to set self._newest_commit_hash
        try:
            pypi_version = self.get_newest_version
718
            if self.version != pypi_version:
719
                sickrage.srCore.srLogger.debug(
720
                    "Version upgrade: " + self._find_installed_version() + " -> " + pypi_version)
echel0n's avatar
echel0n committed
721
722
                return True
        except Exception as e:
723
            sickrage.srCore.srLogger.warning("Unable to contact PyPi, can't check for update: " + repr(e))
echel0n's avatar
echel0n committed
724
725
726
            return False

    def _check_for_new_version(self):
727
728
729
730
731
        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)
732

733
734
        try:
            return versions[0]
echel0n's avatar
echel0n committed
735
        except Exception:
736
            return self._find_installed_version()
737

738
739
740
    def set_newest_text(self):

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

echel0n's avatar
echel0n committed
743
        if not self.version:
744
            sickrage.srCore.srLogger.debug("Unknown current version number, don't know if we should update or not")
745
746
            return
        else:
echel0n's avatar
echel0n committed
747
748
            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())
749

echel0n's avatar
echel0n committed
750
        sickrage.srCore.NEWEST_VERSION_STRING = newest_text
751
752
753

    def update(self):
        """
754
        Performs pip upgrade
755
        """
756
        _, _, exit_status = self._pip_cmd(self._pip_path, 'install -U --no-cache-dir sickrage')
757
758
        if exit_status == 0:
            sickrage.srCore.srLogger.info("Updating SiCKRAGE from PyPi servers")
759
            srNotifiers.notify_version_update(self.get_newest_version)
760
            return True
761

762
        return False