version_updater.py 26.5 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
import sickrage
34
from sickrage.core.helpers import backupSR
35
from sickrage.notifiers import Notifiers
36

37

38
class VersionUpdater(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:
61
            if self.check_for_new_version(force) and sickrage.app.config.auto_update:
62
                if sickrage.app.SHOWUPDATER.amActive:
63
                    sickrage.app.log.debug("We can't proceed with auto-updating. Shows are being updated")
64
65
                    return

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

78
79
80
    def backup(self):
        if self.safe_to_update():
            # Do a system backup before update
81
            sickrage.app.log.info("Config backup in progress...")
82
            sickrage.app.alerts.message(_('Backup'), _('Config backup in progress...'))
83
            try:
84
                backupDir = os.path.join(sickrage.app.data_dir, 'backup')
85
86
                if not os.path.isdir(backupDir):
                    os.mkdir(backupDir)
Dustyn Gibson's avatar
Dustyn Gibson committed
87

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

101
102
    @staticmethod
    def safe_to_update():
103
        if sickrage.app.config.developer:
echel0n's avatar
echel0n committed
104
105
            return False

106
        if not sickrage.app.started:
107
            return True
108
        if not sickrage.app.auto_postprocessor.amActive:
109
            return True
110

111
        sickrage.app.log.debug("We can't proceed with the update. Post-Processor is running")
112

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

        returns: type of installation. Possible values are:
119

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

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

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

        return install_type
136
137
138
139
140
141

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

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

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

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

161
                    return True
162

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

echel0n's avatar
echel0n committed
168

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

174
        main_git = sickrage.app.config.git_path or 'git'
175

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

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

        # 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:
197
            sickrage.app.log.debug("Trying known alternative git locations")
198
199

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

                if exit_status == 0:
204
                    sickrage.app.log.debug("Using: " + cur_git)
205
206
                    return cur_git
                else:
207
                    sickrage.app.log.debug("Not using: " + cur_git)
208
209

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

213
        sickrage.app.newest_version_string = error_message
214
215
216

        return None

217
218
219
220
    @property
    def _pip_path(self):
        test_cmd = '-V'

221
        main_pip = sickrage.app.config.pip_path or 'pip'
222

223
        sickrage.app.log.debug("Checking if we can use pip commands: " + main_pip + ' ' + test_cmd)
224
        __, __, exit_status = self._pip_cmd(main_pip, test_cmd)
225
226

        if exit_status == 0:
227
            sickrage.app.log.debug("Using: " + main_pip)
228
229
            return main_pip
        else:
230
            sickrage.app.log.debug("Not using: " + main_pip)
231
232
233
234
235
236
237
238
239
240
241

        # 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())
242

243
        if alternative_pip:
244
            sickrage.app.log.debug("Trying known alternative pip locations")
245
246

            for cur_pip in alternative_pip:
247
                sickrage.app.log.debug("Checking if we can use pip commands: " + cur_pip + ' ' + test_cmd)
248
                __, __, exit_status = self._pip_cmd(cur_pip, test_cmd)
249
250

                if exit_status == 0:
251
                    sickrage.app.log.debug("Using: " + cur_pip)
252
253
                    return cur_pip
                else:
254
                    sickrage.app.log.debug("Not using: " + cur_pip)
255
256

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

        return None

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

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

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

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

            if output:
                output = output.strip()

        except OSError:
284
            sickrage.app.log.info("Command " + ' '.join(cmd) + " didn't work")
285
286
287
            exit_status = 1

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

304
        return output, err, exit_status
305

306
307
308
309
310
    @staticmethod
    def _pip_cmd(pip_path, args):
        output = err = None

        if not pip_path:
311
            sickrage.app.log.warning("No path to pip specified, can't use pip commands")
312
313
314
            exit_status = 1
            return output, err, exit_status

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

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

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

        if exit_status == 0:
331
            sickrage.app.log.debug(' '.join(cmd) + " : returned successful")
332
333
            exit_status = 0
        else:
334
            sickrage.app.log.debug(' '.join(cmd) + " returned : " + str(output) + ", treat as error for now")
335
336
337
338
339
340
            exit_status = 1

        return output, err, exit_status

    @staticmethod
    def get_update_url():
341
        return "{}/home/update/?pid={}".format(sickrage.app.config.web_root, sickrage.app.pid)
342
343

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


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

        Uses git show to get commit version.

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

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

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

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

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

    def set_newest_text(self):
        # if we're up to date then don't set this
398
        sickrage.app.newest_version_string = None
399

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

405
            sickrage.app.newest_version_string = newest_text
406
407

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

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

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

425
        __, __, exit_status = self._git_cmd(self._git_path, 'pull -f {} {}'.format(sickrage.app.config.git_remote,
426
                                                                                   self.current_branch))
427
        if exit_status == 0:
428
            sickrage.app.log.info("Updating SiCKRAGE from GIT servers")
429
            Notifiers.notify_version_update(self.get_newest_version)
430
            self.install_requirements()
431
            return True
432

433
        return False
434
435
436
437
438
439

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

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

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

462
463
    def checkout_branch(self, branch):
        if branch in self.remote_branches:
464
            sickrage.app.log.debug("Branch checkout: " + self._find_installed_version() + "->" + branch)
465
466

            # remove untracked files and performs a hard reset on git branch to avoid update issues
467
            if sickrage.app.config.git_reset:
468
469
                self.reset()

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

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

        return False

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

    def set_remote_url(self):
486
487
488
        if not sickrage.app.config.developer:
            self._git_cmd(self._git_path, 'remote set-url {} {}'.format(sickrage.app.config.git_remote,
                                                                        sickrage.app.config.git_remote_url))
489

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

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

502
503
        return []

echel0n's avatar
echel0n committed
504

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

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

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

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

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

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

532
        try:
533
            return sickrage.app.wsession.get(git_version_url).text
534
535
        except Exception:
            return self._find_installed_version()
echel0n's avatar
echel0n committed
536

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

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

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

551
        sickrage.app.newest_version_string = newest_text
echel0n's avatar
echel0n committed
552

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

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

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

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

568
            sickrage.app.log.info("Creating update folder " + sr_update_dir + " before extracting")
echel0n's avatar
echel0n committed
569
570
571
            os.makedirs(sr_update_dir)

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

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

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

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

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

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

                    # 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:
621
                            sickrage.app.log.debug("Unable to update " + new_path + ': ' + e.message)
echel0n's avatar
echel0n committed
622
623
624
625
626
627
628
629
                            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:
630
631
            sickrage.app.log.error("Error while trying to update: {}".format(e.message))
            sickrage.app.log.debug("Traceback: " + traceback.format_exc())
echel0n's avatar
echel0n committed
632
633
634
            return False

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

637
638
639
        # install requirements
        self.install_requirements()

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


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

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

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

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

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

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

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

685
686
687
    def set_newest_text(self):

        # if we're up to date then don't set this
688
        sickrage.app.newest_version_string = None
689

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

697
        sickrage.app.newest_version_string = newest_text
698
699
700

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

709
        return False