예제 #1
0
def run_init(storage_path):
    config = ConfigHelper(storage_path)

    if config.is_present():
        do_override_input = input('Do you want to override the existing' +
                                  ' config [y/n]?   ').lower()
        while do_override_input not in ['y', 'n']:
            do_override_input = input('Unrecognized input.' +
                                      ' Try again:   ').lower()

        if do_override_input == 'n':
            sys.exit(0)

    MailService(config).interactively_configure()

    raw_do_sentry = ''
    while raw_do_sentry not in ['y', 'n']:
        raw_do_sentry = input('Do you want to configure Error Reporting via' +
                              ' Sentry? [y/n]   ').lower()
    if raw_do_sentry == 'y':
        sentry_dsn = input('Please enter your Sentry DSN:   ')
        config.set_property('sentry_dsn', sentry_dsn)

    moodle = MoodleService(config, storage_path)
    moodle.interactively_acquire_token()

    print('Configuration finished and saved!')

    if (storage_path == '.'):
        print('  To set a cron-job for this program on your Unix-System:\n' +
              '    1. `crontab -e`\n' +
              '    2. Add `*/15 * * * * cd %s && python3 %smain.py`\n' %
              (os.getcwd(),
               os.path.join(os.path.dirname(os.path.realpath(__file__)), '')) +
              '    3. Save and you\'re done!')
    else:
        print('  To set a cron-job for this program on your Unix-System:\n' +
              '    1. `crontab -e`\n' + '    2. Add `*/15 * * * *' +
              ' cd %s && python3 %smain.py --path %s`\n' %
              (os.getcwd(),
               os.path.join(os.path.dirname(os.path.realpath(__file__)), ''),
               storage_path) + '    3. Save and you\'re done!')

    print('')
    raw_do_config = ''
    while raw_do_config not in ['y', 'n']:
        raw_do_config = input(
            'Do you want to make additional configurations now?' +
            ' You can always do the additional configuration later' +
            ' with the --config option. [y/n]   ').lower()
    if raw_do_config == 'y':
        run_configure(storage_path)

    print('')
    print('All set and ready to go!')
예제 #2
0
def run_new_token(storage_path):
    config = ConfigHelper(storage_path)
    config.load()  # because we do not want to override the other settings

    MoodleService(config, storage_path).interactively_acquire_token()

    print('New Token successfully saved!')
예제 #3
0
def run_init(storage_path, use_sso=False, skip_cert_verify=False):
    config = ConfigHelper(storage_path)

    if config.is_present():
        do_override_input = cutie.prompt_yes_or_no(
            'Do you want to override the existing config?')

        if not do_override_input:
            sys.exit(0)

    MailService(config).interactively_configure()

    do_sentry = cutie.prompt_yes_or_no(
        'Do you want to configure Error Reporting via Sentry?')
    if do_sentry:
        sentry_dsn = input('Please enter your Sentry DSN:   ')
        config.set_property('sentry_dsn', sentry_dsn)

    moodle = MoodleService(config, storage_path, skip_cert_verify)

    if (use_sso):
        moodle.interactively_acquire_sso_token()
    else:
        moodle.interactively_acquire_token()

    print('Configuration finished and saved!')

    if (storage_path == '.'):
        print(
            '  To set a cron-job for this program on your Unix-System:\n' +
            '    1. `crontab -e`\n' +
            '    2. Add `*/15 * * * * cd %s && python3 %smain.py`\n' % (
                os.getcwd(), os.path.join(os.path.dirname(
                    os.path.realpath(__file__)), '')) +
            '    3. Save and you\'re done!'
        )
    else:
        print(
            '  To set a cron-job for this program on your Unix-System:\n' +
            '    1. `crontab -e`\n' +
            '    2. Add `*/15 * * * *' +
            ' cd %s && python3 %smain.py --path %s`\n' % (
                os.getcwd(), os.path.join(os.path.dirname(
                    os.path.realpath(__file__)), ''), storage_path) +
            '    3. Save and you\'re done!'
        )

    print('')

    print('You can always do the additional configuration later' +
          ' with the --config option.')

    do_config = cutie.prompt_yes_or_no(
        'Do you want to make additional configurations now?')

    if do_config:
        run_configure(storage_path, skip_cert_verify)

    print('')
    print('All set and ready to go!')
예제 #4
0
def run_new_token(storage_path, use_sso=False, skip_cert_verify=False):
    config = ConfigHelper(storage_path)
    config.load()  # because we do not want to override the other settings

    moodle = MoodleService(config, storage_path, skip_cert_verify)

    if (use_sso):
        moodle.interactively_acquire_sso_token()
    else:
        moodle.interactively_acquire_token()

    print('New Token successfully saved!')
예제 #5
0
def run_main(storage_path,
             skip_cert_verify=False,
             without_downloading_files=False):
    logging.basicConfig(
        filename=os.path.join(storage_path, 'MoodleDownloader.log'),
        level=logging.DEBUG,
        format='%(asctime)s  %(levelname)s  {%(module)s}  %(message)s',
        datefmt='%Y-%m-%d %H:%M:%S')

    logging.info('--- main started ---------------------')
    Log.info('Moodle Downloader starting...')
    if IS_DEBUG:
        logging.info(
            'Debug-Mode detected. Errors will not be logged but instead' +
            ' re-risen.')
        debug_logger = logging.getLogger()
        debug_logger.setLevel(logging.ERROR)
        debug_logger.addHandler(ReRaiseOnError())

    try:
        logging.debug('Loading config...')
        Log.debug('Loading config...')
        config = ConfigHelper(storage_path)
        config.load()
    except BaseException as e:
        logging.error('Error while trying to load the Configuration! ' +
                      'Exiting...',
                      extra={'exception': e})
        Log.error('Error while trying to load the Configuration!')
        sys.exit(-1)

    r_client = False
    try:
        sentry_dsn = config.get_property('sentry_dsn')
        if sentry_dsn:
            sentry_sdk.init(sentry_dsn)
    except BaseException:
        pass

    mail_service = MailService(config)
    console_service = ConsoleService(config)

    try:
        moodle = MoodleService(config, storage_path, skip_cert_verify)

        logging.debug(
            'Checking for changes for the configured Moodle-Account....')
        Log.debug('Checking for changes for the configured Moodle-Account...')
        changed_courses = moodle.fetch_state()

        diff_count = 0

        logging.debug('Start downloading changed files...')
        Log.debug('Start downloading changed files...')

        if (without_downloading_files):
            downloader = FakeDownloadService(changed_courses, moodle,
                                             storage_path)
        else:
            downloader = DownloadService(changed_courses, moodle, storage_path)
        downloader.run()

        changed_courses_to_notify = moodle.recorder.changes_to_notify()

        for course in changed_courses:
            diff_count += len(course.files)

        if diff_count > 0:
            logging.info(
                '%s changes found for the configured Moodle-Account.' %
                (diff_count))

            Log.success('%s changes found for the configured Moodle-Account.' %
                        (diff_count))

            console_service.notify_about_changes_in_moodle(changed_courses)
        else:
            logging.info('No changes found for the configured Moodle-Account.')
            Log.warning('No changes found for the configured Moodle-Account.')

        if (len(changed_courses_to_notify) > 0):
            mail_service.notify_about_changes_in_moodle(
                changed_courses_to_notify)
            moodle.recorder.notified(changed_courses_to_notify)

        logging.debug('All done. Exiting...')
        Log.success('All done. Exiting..')
    except BaseException as e:
        error_formatted = traceback.format_exc()
        logging.error(error_formatted, extra={'exception': e})

        if r_client:
            sentry_sdk.capture_exception(e)

        mail_service.notify_about_error(str(e))

        logging.debug('Exception-Handling completed. Exiting...',
                      extra={'exception': e})
        Log.critical('Exception:\n%s' % (error_formatted))
        Log.error('The following error occurred during execution: %s' %
                  (str(e)))

        sys.exit(-1)
예제 #6
0
    def interactively_manage_database(self):
        RESET_SEQ = '\033[0m'
        COLOR_SEQ = '\033[1;%dm'

        BLACK, RED, GREEN, YELLOW, BLUE, MAGENTA, CYAN, WHITE = range(30, 38)

        stored_files = self.state_recorder.get_stored_files()

        download_course_ids = self.config_helper.get_download_course_ids()
        dont_download_course_ids = self.config_helper.get_dont_download_course_ids(
        )
        download_submissions = self.config_helper.get_download_submissions()
        download_descriptions = self.config_helper.get_download_descriptions()
        download_databases = self.config_helper.get_download_databases()

        stored_files = MoodleService._filter_courses(
            stored_files, download_course_ids, dont_download_course_ids,
            download_submissions, download_descriptions, download_databases)

        if len(stored_files) <= 0:
            return

        print('This management tool will navigate you through a menu to' +
              ' selectively remove file entries from the database so' +
              ' that these files can be downloaded again.')

        course_options = []
        courses = []
        for course in stored_files:
            for course_file in course.files:
                if not os.path.exists(course_file.saved_to):
                    course_options.append(COLOR_SEQ % BLUE + course.fullname +
                                          RESET_SEQ)
                    courses.append(course)
                    break

        print('Choose one of the courses:')
        print('[Confirm your selection with the Enter key]')
        print('')
        selected_course_id = cutie.select(options=course_options)

        selected_course = courses[selected_course_id]

        section_options = []
        sections = []
        for course_file in selected_course.files:
            if not os.path.exists(course_file.saved_to) and (
                    course_file.section_name not in sections):
                section_options.append(COLOR_SEQ % MAGENTA +
                                       course_file.section_name + RESET_SEQ)
                sections.append(course_file.section_name)

        print('From which sections you want to select files.')
        print(
            '[You can select with the space bar and confirm your selection with the enter key]'
        )
        print('')

        selected_sections_ids = cutie.select_multiple(options=section_options,
                                                      minimal_count=1)
        selected_sections = []
        for selected_sections_id in selected_sections_ids:
            if selected_sections_id < len(sections):
                selected_sections.append(sections[selected_sections_id])

        file_options = []
        files = []
        for course_file in selected_course.files:
            if not os.path.exists(course_file.saved_to) and (
                    course_file.section_name in selected_sections):
                file_options.append(COLOR_SEQ % CYAN +
                                    course_file.content_filename + RESET_SEQ)
                files.append(course_file)

        print(
            'Which of the files should be removed form the database, so that they will be redownloaded?'
        )
        print(
            '[You can select with the space bar and confirm your selection with the enter key]'
        )
        print('')
        selected_files = cutie.select_multiple(options=file_options)

        files_to_delete = []
        for file_index in selected_files:
            if file_index < len(files) and isinstance(files[file_index], File):
                files_to_delete.append(files[file_index])

        self.state_recorder.batch_delete_files_from_db(files_to_delete)
    def __init__(self,
                 courses: [Course],
                 moodle_service: MoodleService,
                 storage_path: str,
                 skip_cert_verify: bool = False):
        """
        Initiates the DownloadService with all files that
        need to be downloaded. A URLTarget is created for each file.
        @param courses: A list of courses that contains all modified files.
        @param moodle_service: A reference to the moodle_service, currently
                               only to get to the state_recorder and the token.
        @param storage_path: The location where the files will be saved.
        """

        # How much threads should be created
        DownloadService.thread_count = 5
        # How often should the downloader try to download
        # a file again if an error occurs.
        DownloadService.url_tries = 3

        self.courses = courses
        self.state_recorder = moodle_service.recorder
        self.token = moodle_service.get_token()
        self.storage_path = storage_path

        # The wait queue for all URL targets to be downloaded.
        self.queue = Queue(0)
        # A list of the created threads
        self.threads = []
        # A lock to stabilize thread insecure resources.
        # writing in DB
        self.lock = threading.Lock()
        # reading file system
        self.lock2 = threading.Lock()

        # report is used to collect successful and failed downloads
        self.report = {'success': [], 'failure': []}
        # thread_report is used to get live reports from the threads
        self.thread_report = [{
            'total': 0,
            'percentage': 0
        } for i in range(self.thread_count)]
        # Collects the total size of the files that needs to be downloaded.
        self.total_to_download = 0

        # delete files, that should be deleted
        self.state_recorder.batch_delete_files(self.courses)

        if skip_cert_verify:
            self.ssl_context = ssl._create_unverified_context()
        else:
            self.ssl_context = ssl._create_default_https_context()

        # Prepopulate queue with any files that were given
        for course in self.courses:
            for file in course.files:
                if (file.deleted is False):
                    self.total_to_download += file.content_filesize

                    save_destination = self.genPath(self.storage_path, course,
                                                    file)

                    self.queue.put(
                        URLTarget(file, course, save_destination, self.token,
                                  self.thread_report, self.lock2,
                                  self.ssl_context))
예제 #8
0
def run_init(storage_path, use_sso=False, skip_cert_verify=False):
    config = ConfigHelper(storage_path)

    if config.is_present():
        do_override_input = cutie.prompt_yes_or_no(
            Log.error_str('Do you want to override the existing config?'))

        if not do_override_input:
            sys.exit(0)

    MailService(config).interactively_configure()
    TelegramService(config).interactively_configure()

    do_sentry = cutie.prompt_yes_or_no(
        'Do you want to configure Error Reporting via Sentry?')
    if do_sentry:
        sentry_dsn = input('Please enter your Sentry DSN:   ')
        config.set_property('sentry_dsn', sentry_dsn)

    moodle = MoodleService(config, storage_path, skip_cert_verify)

    if use_sso:
        moodle.interactively_acquire_sso_token()
    else:
        moodle.interactively_acquire_token()

    if os.name != 'nt':
        Log.info(
            'On Windows many characters are forbidden in filenames and paths, if you want, these characters can be'
            + ' automatically removed from filenames.')

        Log.warning(
            'If you want to view the downloaded files on Windows this is important!'
        )

        default_windows_map = cutie.prompt_yes_or_no(
            'Do you want to load the default filename character map for windows?'
        )
        if default_windows_map:
            config.set_default_filename_character_map(True)
        else:
            config.set_default_filename_character_map(False)
    else:
        config.set_default_filename_character_map(True)

    Log.success('Configuration finished and saved!')

    if os.name != 'nt':
        if storage_path == '.':
            Log.info(
                '  To set a cron-job for this program on your Unix-System:\n' +
                '    1. `crontab -e`\n' +
                '    2. Add `*/15 * * * * cd %s && python3 %smain.py`\n' %
                (os.getcwd(),
                 os.path.join(os.path.dirname(os.path.realpath(__file__)), ''))
                + '    3. Save and you\'re done!')
        else:
            Log.info(
                '  To set a cron-job for this program on your Unix-System:\n' +
                '    1. `crontab -e`\n' + '    2. Add `*/15 * * * *' +
                ' cd %s && python3 %smain.py --path %s`\n' %
                (os.getcwd(),
                 os.path.join(os.path.dirname(os.path.realpath(__file__)), ''),
                 storage_path) + '    3. Save and you\'re done!')

    print('')

    Log.info(
        'You can always do the additional configuration later with the --config option.'
    )

    do_config = cutie.prompt_yes_or_no(
        'Do you want to make additional configurations now?')

    if do_config:
        run_configure(storage_path, skip_cert_verify)

    print('')
    Log.success('All set and ready to go!')
예제 #9
0
def run_main(storage_path,
             skip_cert_verify=False,
             without_downloading_files=False):

    log_formatter = logging.Formatter(
        '%(asctime)s  %(levelname)s  {%(module)s}  %(message)s',
        '%Y-%m-%d %H:%M:%S')
    log_file = os.path.join(storage_path, 'MoodleDownloader.log')
    log_handler = RotatingFileHandler(log_file,
                                      mode='a',
                                      maxBytes=1 * 1024 * 1024,
                                      backupCount=2,
                                      encoding=None,
                                      delay=0)

    log_handler.setFormatter(log_formatter)
    log_handler.setLevel(logging.DEBUG)

    app_log = logging.getLogger()
    app_log.setLevel(logging.DEBUG)
    app_log.addHandler(log_handler)

    logging.info('--- main started ---------------------')
    Log.info('Moodle Downloader starting...')
    if IS_DEBUG:
        logging.info(
            'Debug-Mode detected. Errors will not be logged but instead re-risen.'
        )
        debug_logger = logging.getLogger()
        debug_logger.setLevel(logging.ERROR)
        debug_logger.addHandler(ReRaiseOnError())

    try:
        logging.debug('Loading config...')
        Log.debug('Loading config...')
        config = ConfigHelper(storage_path)
        config.load()
    except BaseException as e:
        logging.error(
            'Error while trying to load the Configuration! Exiting...',
            extra={'exception': e})
        Log.error('Error while trying to load the Configuration!')
        sys.exit(-1)

    r_client = False
    try:
        sentry_dsn = config.get_property('sentry_dsn')
        if sentry_dsn:
            sentry_sdk.init(sentry_dsn)
    except BaseException:
        pass

    mail_service = MailService(config)
    tg_service = TelegramService(config)
    console_service = ConsoleService(config)

    PathTools.filename_character_map = config.get_filename_character_map()

    try:
        if not IS_DEBUG:
            process_lock.lock(storage_path)

        moodle = MoodleService(config, storage_path, skip_cert_verify)

        logging.debug(
            'Checking for changes for the configured Moodle-Account....')
        Log.debug('Checking for changes for the configured Moodle-Account...')
        changed_courses = moodle.fetch_state()

        logging.debug('Start downloading changed files...')
        Log.debug('Start downloading changed files...')

        if without_downloading_files:
            downloader = FakeDownloadService(changed_courses, moodle,
                                             storage_path)
        else:
            downloader = DownloadService(changed_courses, moodle, storage_path,
                                         skip_cert_verify)
        downloader.run()

        changed_courses_to_notify = moodle.recorder.changes_to_notify()

        if len(changed_courses_to_notify) > 0:
            console_service.notify_about_changes_in_moodle(
                changed_courses_to_notify)
            mail_service.notify_about_changes_in_moodle(
                changed_courses_to_notify)
            tg_service.notify_about_changes_in_moodle(
                changed_courses_to_notify)

            moodle.recorder.notified(changed_courses_to_notify)

        else:
            logging.info('No changes found for the configured Moodle-Account.')
            Log.warning('No changes found for the configured Moodle-Account.')

        process_lock.unlock(storage_path)

        logging.debug('All done. Exiting...')
        Log.success('All done. Exiting..')
    except BaseException as e:
        if not isinstance(e, process_lock.LockError):
            process_lock.unlock(storage_path)

        error_formatted = traceback.format_exc()
        logging.error(error_formatted, extra={'exception': e})

        if r_client:
            sentry_sdk.capture_exception(e)

        short_error = '%s\r\n%s' % (str(e), traceback.format_exc(limit=1))
        mail_service.notify_about_error(short_error)
        tg_service.notify_about_error(short_error)

        logging.debug('Exception-Handling completed. Exiting...',
                      extra={'exception': e})
        Log.critical('Exception:\n%s' % (error_formatted))
        Log.error('The following error occurred during execution: %s' %
                  (str(e)))

        sys.exit(-1)
예제 #10
0
    def __init__(self, courses: [Course], moodle_service: MoodleService,
                 storage_path: str):
        """
        Initiates the DownloadService with all files that
        need to be downloaded. A URLTarget is created for each file.
        @param courses: A list of courses that contains all modified files.
        @param moodle_service: A reference to the moodle_service, currently
                               only to get to the state_recorder and the token.
        @param storage_path: The location where the files will be saved.
        """

        # How much threads should be created
        DownloadService.thread_count = 5
        # How often should the downloader try to download
        # a file again if an error occurs.
        DownloadService.url_tries = 3

        self.courses = courses
        self.state_recorder = moodle_service.recorder
        self.token = moodle_service.get_token()
        self.storage_path = storage_path

        # The wait queue for all URL targets to be downloaded.
        self.queue = Queue(0)
        # A list of the created threads
        self.threads = []
        # A lock to stabilize thread insecure resources.
        # writing in DB
        self.lock = threading.Lock()
        # reading file system
        self.lock2 = threading.Lock()

        # report is used to collect successful and failed downloads
        self.report = {'success': [], 'failure': []}
        # thread_report is used to get live reports from the threads
        self.thread_report = [{'total': 0, 'percentage': 0}
                              for i in range(self.thread_count)]
        # Collects the total size of the files that needs to be downloaded.
        self.total_to_download = 0

        # delete files, that should be deleted
        self.state_recorder.batch_delete_files(self.courses)

        # Prepopulate queue with any files that were given
        for course in self.courses:
            for file in course.files:
                if(file.deleted is False):
                    self.total_to_download += file.content_filesize

                    save_destination = StringTools.path_of_file(
                        self.storage_path, course.fullname,
                        file.section_name,
                        file.content_filepath
                    )

                    # If the file is located in a folder or in an assignment,
                    # it should be saved in a subfolder
                    # (with the name of the module).
                    if (file.module_modname == "assign" or
                            file.module_modname == "folder"):
                        file_path = file.content_filepath
                        if (file.content_type == "submission_file"):
                            file_path = os.path.join('/submissions/',
                                                     file_path.strip('/'))

                        save_destination = StringTools.path_of_file_in_module(
                            self.storage_path, course.fullname,
                            file.section_name, file.module_name,
                            file_path
                        )

                    self.queue.put(URLTarget(
                        file, course, save_destination, self.token,
                        self.thread_report, self.lock2))