version_updater.py 26 KB
Newer Older
echel0n's avatar
echel0n committed
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# ##############################################################################
#  Author: echel0n <[email protected]>
#  URL: https://sickrage.ca/
#  Git: https://git.sickrage.ca/SiCKRAGE/sickrage.git
#  -
#  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/>.
# ##############################################################################
21
import configparser
22
23
24
import os
import platform
import re
25
import shutil
26
import subprocess
27
import sys
echel0n's avatar
echel0n committed
28
import tarfile
29
import tempfile
echel0n's avatar
echel0n committed
30
from distutils.version import LooseVersion
31
from time import sleep
32

echel0n's avatar
echel0n committed
33
34
import dirsync as dirsync

35
import sickrage
echel0n's avatar
echel0n committed
36
from sickrage.core.helpers import backup_app_data
echel0n's avatar
echel0n committed
37
from sickrage.core.websession import WebSession
echel0n's avatar
echel0n committed
38
from sickrage.core.websocket import WebSocketMessage
39
from sickrage.notification_providers import NotificationProvider
40

41

42
class VersionUpdater(object):
43
    def __init__(self):
echel0n's avatar
echel0n committed
44
        self.name = "VERSIONUPDATER"
45
        self.running = False
46
47
48

    @property
    def updater(self):
echel0n's avatar
echel0n committed
49
50
51
        # default to source install type
        install_type = SourceUpdateManager()

echel0n's avatar
echel0n committed
52
        if sickrage.install_type() == 'git':
echel0n's avatar
echel0n committed
53
            install_type = GitUpdateManager()
echel0n's avatar
echel0n committed
54
        elif sickrage.install_type() == 'windows':
echel0n's avatar
echel0n committed
55
            install_type = WindowsUpdateManager()
echel0n's avatar
echel0n committed
56
        elif sickrage.install_type() == 'synology':
echel0n's avatar
echel0n committed
57
            install_type = SynologyUpdateManager()
echel0n's avatar
echel0n committed
58
        elif sickrage.install_type() == 'docker':
echel0n's avatar
echel0n committed
59
            install_type = DockerUpdateManager()
echel0n's avatar
echel0n committed
60
        elif sickrage.install_type() == 'qnap':
echel0n's avatar
echel0n committed
61
            install_type = QnapUpdateManager()
echel0n's avatar
echel0n committed
62
        elif sickrage.install_type() == 'readynas':
echel0n's avatar
echel0n committed
63
            install_type = ReadynasUpdateManager()
echel0n's avatar
echel0n committed
64
        elif sickrage.install_type() == 'pip':
echel0n's avatar
echel0n committed
65
66
67
68
69
70
71
72
73
74
75
            install_type = PipUpdateManager()

        return install_type

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

    @property
    def branch(self):
        return self.updater.current_branch
76

77
    def task(self, force=False):
78
        if self.running:
79
            return
80

81
82
        try:
            self.running = True
83

echel0n's avatar
echel0n committed
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
            if not self.check_for_update():
                return

            if not sickrage.app.config.general.auto_update and not force:
                return

            if self.updater.manual_update:
                sickrage.app.log.debug("We can't proceed with auto-updating, install type only allows manual updating")
                return

            if sickrage.app.show_updater.running:
                sickrage.app.log.debug("We can't proceed with auto-updating, shows are being updated")
                return

            sickrage.app.log.info("New update found for SiCKRAGE, starting auto-updater ...")
            sickrage.app.alerts.message(_('Updater'), _('New update found for SiCKRAGE, starting auto-updater'))

            if self.update():
                sickrage.app.log.info("Update was successful!")
                sickrage.app.alerts.message(_('Updater'), _('Update was successful'))
104
                sickrage.app.restart()
echel0n's avatar
echel0n committed
105
106
107
            else:
                sickrage.app.log.info("Update failed!")
                sickrage.app.alerts.error(_('Updater'), _('Update failed!'))
108
109
        finally:
            self.running = False
110

111
    def backup(self):
112
113
        # Do a system backup before update
        sickrage.app.log.info("Config backup in progress...")
114
        sickrage.app.alerts.message(_('Updater'), _('Config backup in progress...'))
115

116
117
118
119
120
        try:
            backupDir = os.path.join(sickrage.app.data_dir, 'backup')
            if not os.path.isdir(backupDir):
                os.mkdir(backupDir)

echel0n's avatar
echel0n committed
121
            if backup_app_data(backupDir, keep_latest=True):
122
                sickrage.app.log.info("Config backup successful, updating...")
123
                sickrage.app.alerts.message(_('Updater'), _('Config backup successful, updating...'))
124
125
126
                return True
            else:
                sickrage.app.log.warning("Config backup failed, aborting update")
127
                sickrage.app.alerts.error(_('Updater'), _('Config backup failed, aborting update'))
128
                return False
129
        except Exception as e:
echel0n's avatar
echel0n committed
130
            sickrage.app.log.warning(f'Update: Config backup failed. Error: {e!r}')
131
            sickrage.app.alerts.error(_('Updater'), _('Config backup failed, aborting update'))
132
            return False
133

echel0n's avatar
echel0n committed
134
    def safe_to_update(self):
135
        sickrage.app.postprocessor_queue.shutdown()
136
        sickrage.app.log.debug("Waiting for jobs in post-processor queue to finish before updating")
137
        sickrage.app.alerts.message(_('Updater'), _("Waiting for jobs in post-processor queue to finish before updating"))
138

139
        while sickrage.app.postprocessor_queue.is_busy:
140
141
            sleep(1)

142
        sickrage.app.show_queue.shutdown()
143
        sickrage.app.log.debug("Waiting for jobs in show queue to finish before updating")
144
        sickrage.app.alerts.message(_('Updater'), _("Waiting for jobs in show queue to finish before updating"))
145

echel0n's avatar
echel0n committed
146
147
        while sickrage.app.show_queue.is_busy:
            sleep(1)
148
149
150

        return True

echel0n's avatar
echel0n committed
151
    def check_for_update(self):
152
153
154
155
        """
        Checks the internet for a newer version.

        returns: bool, True for new version or False for no new version.
echel0n's avatar
echel0n committed
156
        :param force: forces return value of True
157
158
        """

159
        if sickrage.app.disable_updates:
160
161
            return False

162
163
        sickrage.app.log.info('Checking for SiCKRAGE server updates')

echel0n's avatar
echel0n committed
164
        if not self.updater.need_update():
165
            sickrage.app.log.info('SiCKRAGE server is up to date')
166
167
            return False

168
169
        sickrage.app.log.info('New SiCKRAGE server update is available!')

echel0n's avatar
echel0n committed
170
        self.updater.set_latest_version()
171

echel0n's avatar
echel0n committed
172
        return True
173

echel0n's avatar
echel0n committed
174
175
176
177
    def update(self, webui=False):
        # check if updater only allows manual updates
        if self.updater.manual_update:
            return False
178

echel0n's avatar
echel0n committed
179
180
181
        # check for updates
        if not self.updater.need_update():
            return False
182

echel0n's avatar
echel0n committed
183
184
185
        # check if its safe to update
        if not self.safe_to_update():
            return False
186

echel0n's avatar
echel0n committed
187
188
189
        # backup
        if sickrage.app.config.general.backup_on_update and not self.backup():
            return False
190

echel0n's avatar
echel0n committed
191
192
193
194
        # attempt update
        if self.updater.update():
            # Clean up after update
            to_clean = os.path.join(sickrage.app.cache_dir, 'mako')
195

echel0n's avatar
echel0n committed
196
197
198
            for root, dirs, files in os.walk(to_clean, topdown=False):
                [os.remove(os.path.join(root, name)) for name in files]
                [shutil.rmtree(os.path.join(root, name)) for name in dirs]
echel0n's avatar
echel0n committed
199

echel0n's avatar
echel0n committed
200
            sickrage.app.config.general.view_changelog = True
201

echel0n's avatar
echel0n committed
202
            if webui:
echel0n's avatar
echel0n committed
203
                WebSocketMessage('redirect', {'url': f'{sickrage.app.config.general.web_root}/home/restart/?pid={sickrage.app.pid}'}).push()
echel0n's avatar
echel0n committed
204

echel0n's avatar
echel0n committed
205
            return True
206

echel0n's avatar
echel0n committed
207
208
        if webui:
            sickrage.app.alerts.error(_("Updater"), _("Update wasn't successful, not restarting. Check your log for more information."))
echel0n's avatar
echel0n committed
209

210

211
class UpdateManager(object):
echel0n's avatar
echel0n committed
212
213
    def __init__(self):
        self.manual_update = False
214

215
    @property
echel0n's avatar
echel0n committed
216
217
    def version(self):
        return sickrage.version()
218

echel0n's avatar
echel0n committed
219
220
221
222
    @property
    def latest_version(self):
        releases = []
        latest_version = None
echel0n's avatar
echel0n committed
223

echel0n's avatar
echel0n committed
224
225
226
        try:
            version_url = "https://git.sickrage.ca/SiCKRAGE/sickrage/-/releases.json"
            resp = WebSession().get(version_url).json()
227

echel0n's avatar
echel0n committed
228
229
230
231
            if self.current_branch == 'develop':
                releases = [x['tag'] for x in resp if 'dev' in x['tag']]
            elif self.current_branch == 'master':
                releases = [x['tag'] for x in resp if 'dev' not in x['tag']]
232

echel0n's avatar
echel0n committed
233
234
235
236
            if releases:
                latest_version = sorted(releases, key=LooseVersion, reverse=True)[0]
        finally:
            return latest_version or self.version
237

echel0n's avatar
echel0n committed
238
239
240
    @property
    def current_branch(self):
        return ("master", "develop")["dev" in sickrage.version()]
241

echel0n's avatar
echel0n committed
242
    def need_update(self):
243
        try:
echel0n's avatar
echel0n committed
244
            latest_version = self.latest_version
echel0n's avatar
echel0n committed
245
            if LooseVersion(self.version) < LooseVersion(latest_version):
echel0n's avatar
echel0n committed
246
247
248
249
250
                sickrage.app.log.debug(f"SiCKRAGE version upgrade: {self.version} -> {latest_version}")
                return True
        except Exception as e:
            sickrage.app.log.warning(f"Unable to check for updates: {e!r}")
            return False
echel0n's avatar
echel0n committed
251

echel0n's avatar
echel0n committed
252
253
    def update(self):
        pass
254

echel0n's avatar
echel0n committed
255
256
    def set_latest_version(self):
        latest_version = self.latest_version
257

echel0n's avatar
echel0n committed
258
259
        if not self.manual_update:
            update_url = f"{sickrage.app.config.general.web_root}/home/update/?pid={sickrage.app.pid}"
echel0n's avatar
echel0n committed
260
            message = f'New SiCKRAGE {self.current_branch} {sickrage.install_type()} update available, version {latest_version} &mdash; <a href=\"{update_url}\">Update Now</a>'
echel0n's avatar
echel0n committed
261
        else:
echel0n's avatar
echel0n committed
262
            message = f"New SiCKRAGE {self.current_branch} {sickrage.install_type()} update available, version {latest_version}, please manually update!"
263

echel0n's avatar
echel0n committed
264
        sickrage.app.latest_version_string = message
265

266
    @staticmethod
267
    def _pip_cmd(args, silent=False):
268
269
        output = err = None

270
        cmd = [sys.executable, "-m", "pip"] + args.split()
271
272

        try:
echel0n's avatar
echel0n committed
273
274
275
            if not silent:
                sickrage.app.log.debug("Executing " + ' '.join(cmd) + " with your shell in " + sickrage.MAIN_DIR)

276
277
278
            p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT, shell=(sys.platform == 'win32'),
                                 cwd=sickrage.MAIN_DIR)

279
280
            output, err = p.communicate()
            exit_status = p.returncode
281
        except (RuntimeError, OSError):
echel0n's avatar
echel0n committed
282
            sickrage.app.log.info(f"Command {' '.join(cmd)} didn't work")
283
284
285
286
287
288
289
            exit_status = 1

        if exit_status == 0:
            exit_status = 0
        else:
            exit_status = 1

290
        if output:
291
            output = output.decode("utf-8", "ignore").strip() if isinstance(output, bytes) else output.strip()
292

293
294
        return output, err, exit_status

295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
    def upgrade_pip(self):
        output, __, exit_status = self._pip_cmd('install --no-cache-dir -U pip')

        if exit_status != 0:
            __, __, exit_status = self._pip_cmd('install --no-cache-dir --user -U PIP')

        if exit_status == 0:
            return True

        sickrage.app.alerts.error(_('Updater'), _('Failed to update PIP'))

        sickrage.app.log.warning('Unable to update PIP')

        if output:
            output = output.decode("utf-8", "ignore").strip() if isinstance(output, bytes) else output.strip()
echel0n's avatar
echel0n committed
310
            sickrage.app.log.debug(f"PIP CMD OUTPUT: {output}")
311

312
    def install_requirements(self, branch):
echel0n's avatar
echel0n committed
313
        requirements_url = f"https://git.sickrage.ca/SiCKRAGE/sickrage/raw/{branch}/requirements.txt"
echel0n's avatar
echel0n committed
314
        requirements_file = tempfile.NamedTemporaryFile(delete=False)
echel0n's avatar
echel0n committed
315

316
        try:
echel0n's avatar
echel0n committed
317
318
            requirements_file.write(WebSession().get(requirements_url).content)
            requirements_file.close()
319
        except Exception:
echel0n's avatar
echel0n committed
320
321
            requirements_file.close()
            os.unlink(requirements_file.name)
322
            return False
echel0n's avatar
echel0n committed
323

echel0n's avatar
echel0n committed
324
        output, __, exit_status = self._pip_cmd(f'install --no-deps --no-cache-dir -r {requirements_file.name}')
echel0n's avatar
echel0n committed
325
        if exit_status != 0:
echel0n's avatar
echel0n committed
326
            __, __, exit_status = self._pip_cmd(f'install --no-deps --no-cache-dir --user -r {requirements_file.name}')
327

echel0n's avatar
echel0n committed
328
329
330
        if exit_status == 0:
            requirements_file.close()
            os.unlink(requirements_file.name)
331
            return True
echel0n's avatar
echel0n committed
332

333
        sickrage.app.alerts.error(_('Updater'), _('Failed to update requirements'))
334

335
        sickrage.app.log.warning('Unable to update requirements')
echel0n's avatar
echel0n committed
336

echel0n's avatar
echel0n committed
337
338
339
340
341
342
343
        if output:
            output = output.decode("utf-8", "ignore").strip() if isinstance(output, bytes) else output.strip()
            sickrage.app.log.debug("PIP CMD OUTPUT: {}".format(output))

        requirements_file.close()
        os.unlink(requirements_file.name)

echel0n's avatar
echel0n committed
344
        return False
345
346
347
348


class GitUpdateManager(UpdateManager):
    def __init__(self):
echel0n's avatar
echel0n committed
349
        super(GitUpdateManager, self).__init__()
350
        self.type = "git"
351
352
        self._num_commits_behind = 0
        self._num_commits_ahead = 0
353
354
355

    @property
    def version(self):
356
        """
357
        Attempts to find the currently installed version of SiCKRAGE.
358
359
360
361
362
363

        Uses git show to get commit version.

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

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

echel0n's avatar
echel0n committed
372
373
    @property
    def latest_version(self):
374
375
376
377
378
        """
        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.
        """

379
380
        # check if branch exists on remote
        if self.current_branch not in self.remote_branches:
echel0n's avatar
echel0n committed
381
            return self.version
382

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

388
            if output:
echel0n's avatar
echel0n committed
389
390
391
                sickrage.app.log.debug(f'GIT CMD OUTPUT: {output.strip()}')

            return self.version
392

393
        # get number of commits behind and ahead (option --count not supported git < 1.7.2)
echel0n's avatar
echel0n committed
394
        output, __, exit_status = self._git_cmd(self._git_path, f'rev-list --left-right origin/{self.current_branch}...HEAD')
395
396
397
398
399
400
        if exit_status == 0 and output:
            try:
                self._num_commits_behind = int(output.count("<"))
                self._num_commits_ahead = int(output.count(">"))
            except Exception:
                sickrage.app.log.debug("Unable to determine number of commits ahead or behind for git install, failed new version check.")
echel0n's avatar
echel0n committed
401
                return self.version
402

403
        # get latest commit_hash from remote
echel0n's avatar
echel0n committed
404
        output, __, exit_status = self._git_cmd(self._git_path, f'rev-parse --verify --quiet origin/{self.current_branch}')
405
        if exit_status == 0 and output:
echel0n's avatar
echel0n committed
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
            return output.strip() or self.version

    @property
    def current_branch(self):
        branch_ref, __, exit_status = self._git_cmd(self._git_path, 'symbolic-ref -q HEAD')
        if exit_status == 0 and branch_ref is not None:
            return branch_ref.strip().replace('refs/heads/', '', 1)
        return ""

    @property
    def remote_branches(self):
        branches, __, exit_status = self._git_cmd(self._git_path, f'ls-remote --heads {sickrage.app.git_remote_url}')
        if exit_status == 0 and branches:
            return re.findall(r'refs/heads/(.*)', branches)

        return []

    @property
    def _git_path(self):
        test_cmd = '--version'

        alternative_git = {
            'windows': 'git',
            'darwin': '/usr/local/git/bin/git'
        }
431

echel0n's avatar
echel0n committed
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
        main_git = sickrage.app.config.general.git_path or 'git'

        # sickrage.app.log.debug("Checking if we can use git commands: " + main_git + ' ' + test_cmd)
        __, __, exit_status = self._git_cmd(main_git, test_cmd, silent=True)
        if exit_status == 0:
            # sickrage.app.log.debug("Using: " + main_git)
            return main_git

        if platform.system().lower() in alternative_git:
            sickrage.app.log.debug("Trying known alternative GIT application locations")

            # sickrage.app.log.debug("Checking if we can use git commands: " + cur_git + ' ' + test_cmd)
            __, __, exit_status = self._git_cmd(alternative_git[platform.system().lower()], test_cmd)
            if exit_status == 0:
                # sickrage.app.log.debug("Using: " + cur_git)
                return alternative_git[platform.system().lower()]

        # Still haven't found a working git
        error_message = _('Unable to find your git executable - Set your git path from Settings->General->Advanced OR '
                          'delete your {git_folder} folder and run from source to enable '
                          'updates.'.format(**{'git_folder': os.path.join(sickrage.MAIN_DIR, '.git')}))

        sickrage.app.alerts.error(_('Updater'), error_message)

    @staticmethod
    def _git_cmd(git_path, args, silent=False):
        output = err = None

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

        cmd = [git_path] + args.split()

        try:
            if not silent:
                sickrage.app.log.debug("Executing " + ' '.join(cmd) + " with your shell in " + sickrage.MAIN_DIR)

            p = subprocess.Popen(cmd, stdin=subprocess.PIPE, stdout=subprocess.PIPE, stderr=subprocess.STDOUT,
                                 shell=(sys.platform == 'win32'), cwd=sickrage.MAIN_DIR)
            output, err = p.communicate()
            exit_status = p.returncode

            if output is not None:
                output = output.decode("utf-8", "ignore").strip() if isinstance(output, bytes) else output.strip()
        except (RuntimeError, OSError):
            sickrage.app.log.info(f"Command {' '.join(cmd)} didn\'t work")
            exit_status = 1

        if exit_status == 0:
            if not silent:
                sickrage.app.log.debug(f"{' '.join(cmd)} : returned successful")
            exit_status = 0
        elif exit_status == 1:
            if output:
                if 'stash' in output:
                    sickrage.app.log.warning("Please enable 'git reset' in settings or stash your changes in local files")
                else:
                    sickrage.app.log.debug(f"{' '.join(cmd)} returned : {str(output)}")
            else:
                sickrage.app.log.warning(f'{cmd} returned no data')
            exit_status = 1
        elif exit_status == 128 or 'fatal:' in output or err:
            sickrage.app.log.debug(f"{' '.join(cmd)} returned : {str(output)}")
            exit_status = 128
        else:
            sickrage.app.log.debug(f"{' '.join(cmd)} returned : {str(output)}, treat as error for now")
            exit_status = 1

        return output, err, exit_status
503
504

    def need_update(self):
echel0n's avatar
echel0n committed
505
        try:
echel0n's avatar
echel0n committed
506
            return self.version != self.latest_version and self._num_commits_behind > 0
echel0n's avatar
echel0n committed
507
        except Exception as e:
echel0n's avatar
echel0n committed
508
            sickrage.app.log.error(f"Unable to contact server, can't check for update: {e!r}")
echel0n's avatar
echel0n committed
509
            return False
510
511
512

    def update(self):
        """
513
        Calls git pull origin <branch> in order to update SiCKRAGE. Returns a bool depending
514
515
516
        on the call's success.
        """

517
        if sickrage.app.config.general.git_reset:
518
519
            self.reset()

520
521
522
        if not self.upgrade_pip():
            return False

523
524
525
        if not self.install_requirements(self.current_branch):
            return False

echel0n's avatar
echel0n committed
526
        __, __, exit_status = self._git_cmd(self._git_path, f'pull -f {sickrage.app.git_remote_url} {self.current_branch}')
527
        if exit_status == 0:
528
            sickrage.app.log.info("Updating SiCKRAGE from GIT servers")
529
            sickrage.app.alerts.message(_('Updater'), _('Updating SiCKRAGE from GIT servers'))
echel0n's avatar
echel0n committed
530
            NotificationProvider.mass_notify_version_update(self.latest_version)
531
            return True
532

533
        return False
534
535
536
537
538
539

    def clean(self):
        """
        Calls git clean to remove all untracked files. Returns a bool depending
        on the call's success.
        """
540
        __, __, exit_status = self._git_cmd(self._git_path, 'clean -df ""')
541
        return (False, True)[exit_status == 0]
542
543
544
545
546
547

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

551
552
    def fetch(self):
        """
553
        Calls git fetch to fetch all remote branches
554
555
        on the call's success.
        """
echel0n's avatar
echel0n committed
556
        __, __, exit_status = self._git_cmd(self._git_path, f'config remote.origin.fetch {"+refs/heads/*:refs/remotes/origin/*"}')
557
        if exit_status == 0:
558
            __, __, exit_status = self._git_cmd(self._git_path, 'fetch --all')
559
560
        return (False, True)[exit_status == 0]

561
562
    def checkout_branch(self, branch):
        if branch in self.remote_branches:
echel0n's avatar
echel0n committed
563
            sickrage.app.log.debug(f"Branch checkout: {self.version} -> {branch}")
564

565
566
567
            if not self.upgrade_pip():
                return False

568
569
570
            if not self.install_requirements(self.current_branch):
                return False

571
            # remove untracked files and performs a hard reset on git branch to avoid update issues
572
            if sickrage.app.config.general.git_reset:
573
574
                self.reset()

575
576
577
            # fetch all branches
            self.fetch()

echel0n's avatar
echel0n committed
578
            __, __, exit_status = self._git_cmd(self._git_path, f'checkout -f {branch}')
579
            if exit_status == 0:
580
                return True
581
582
583

        return False

584
    def get_remote_url(self):
echel0n's avatar
echel0n committed
585
        url, __, exit_status = self._git_cmd(self._git_path, f'remote get-url {sickrage.app.git_remote_url}')
586
587
588
        return ("", url)[exit_status == 0 and url is not None]

    def set_remote_url(self):
echel0n's avatar
echel0n committed
589
        if not sickrage.app.developer:
echel0n's avatar
echel0n committed
590
            self._git_cmd(self._git_path, f'remote set-url {sickrage.app.git_remote_url} {sickrage.app.app.git_remote_url}')
591

echel0n's avatar
echel0n committed
592

echel0n's avatar
echel0n committed
593
class WindowsUpdateManager(UpdateManager):
594
    def __init__(self):
echel0n's avatar
echel0n committed
595
596
597
        super(WindowsUpdateManager, self).__init__()
        self.type = "windows"
        self.manual_update = True
598

599
600
601
602
603
604
605
606
607
608
609
610
    @property
    def latest_version(self):
        latest_version = None

        try:
            version_url = "https://www.sickrage.ca/downloads/windows/updates.txt"
            version_config = configparser.ConfigParser()
            version_config.read_string(WebSession().get(version_url).text)
            latest_version = version_config['SiCKRAGE']['Version'].rsplit('.', 1)[0]
        finally:
            return latest_version or self.version

611

echel0n's avatar
echel0n committed
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
class SynologyUpdateManager(UpdateManager):
    def __init__(self):
        super(SynologyUpdateManager, self).__init__()
        self.type = "synology"
        self.manual_update = True


class DockerUpdateManager(UpdateManager):
    def __init__(self):
        super(DockerUpdateManager, self).__init__()
        self.type = "docker"
        self.manual_update = True


class ReadynasUpdateManager(UpdateManager):
    def __init__(self):
        super(ReadynasUpdateManager, self).__init__()
        self.type = "readynas"
        self.manual_update = True


class QnapUpdateManager(UpdateManager):
    def __init__(self):
        super(QnapUpdateManager, self).__init__()
        self.type = "qnap"
        self.manual_update = True


class PipUpdateManager(UpdateManager):
    def __init__(self):
        super(PipUpdateManager, self).__init__()
        self.type = "pip"
        self.manual_update = True
645

echel0n's avatar
echel0n committed
646
647
648
649
650
651

class SourceUpdateManager(UpdateManager):
    def __init__(self):
        super(SourceUpdateManager, self).__init__()
        self.type = "source"

652
    def update(self):
echel0n's avatar
echel0n committed
653
        """
654
        Downloads the latest source tarball from server and installs it over the existing version.
echel0n's avatar
echel0n committed
655
656
        """

echel0n's avatar
echel0n committed
657
658
659
        latest_version = self.latest_version

        tar_download_url = f'https://git.sickrage.ca/SiCKRAGE/sickrage/-/archive/{latest_version}/sickrage-{latest_version}.tar.gz'
echel0n's avatar
echel0n committed
660
661

        try:
662
663
664
            if not self.upgrade_pip():
                return False

665
666
667
            if not self.install_requirements(self.current_branch):
                return False

668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
            retry_count = 0
            while retry_count < 3:
                with tempfile.TemporaryFile() as update_tarfile:
                    sickrage.app.log.info(f"Downloading update from {tar_download_url!r}")
                    resp = WebSession().get(tar_download_url)
                    if not resp or not resp.content:
                        sickrage.app.log.warning('Failed to download SiCKRAGE update')
                        retry_count += 1
                        continue

                    update_tarfile.write(resp.content)
                    update_tarfile.seek(0)

                    with tempfile.TemporaryDirectory(prefix='sr_update_', dir=sickrage.app.data_dir) as unpack_dir:
                        sickrage.app.log.info("Extracting SiCKRAGE update file")
                        try:
                            tar = tarfile.open(fileobj=update_tarfile, mode='r:gz')
                            tar.extractall(unpack_dir)
                            tar.close()
                        except tarfile.TarError:
                            sickrage.app.log.warning("Invalid update data, update failed: not a gzip file")
                            retry_count += 1
                            continue

                        if len(os.listdir(unpack_dir)) != 1:
                            sickrage.app.log.warning("Invalid update data, update failed")
                            retry_count += 1
                            continue

                        update_dir = os.path.join(*[unpack_dir, os.listdir(unpack_dir)[0], 'sickrage'])
                        sickrage.app.log.info(f"Sync folder {update_dir} to {sickrage.PROG_DIR}")
                        dirsync.sync(update_dir, sickrage.PROG_DIR, 'sync', purge=True)

                        # Notify update successful
                        NotificationProvider.mass_notify_version_update(latest_version)

                        return True
echel0n's avatar
echel0n committed
705
        except Exception as e:
echel0n's avatar
echel0n committed
706
            sickrage.app.log.error(f"Error while trying to update: {e!r}")
echel0n's avatar
echel0n committed
707
            return False