#!/usr/bin/env python
# -*- coding: utf-8 -*-
"""
sessions2trash.py

Run this script in a web2py environment shell e.g. python web2py.py -S app
If models are loaded (-M option) auth.settings.expiration is assumed
for sessions without an expiration. If models are not loaded, sessions older
than 60 minutes are removed. Use the --expiration option to override these
values.

Typical usage:

    # Delete expired sessions every 5 minutes
    nohup python web2py.py -S app -M -R scripts/sessions2trash.py &

    # Delete sessions older than 60 minutes regardless of expiration,
    # with verbose output, then exit.
    python web2py.py -S app -M -R scripts/sessions2trash.py -A -o -x 3600 -f -v

    # Delete all sessions regardless of expiry and exit.
    python web2py.py -S app -M -R scripts/sessions2trash.py -A -o -x 0

    # Delete session in a module (move to the modules folder)
    from sessions2trash import single_loop
    def delete_sessions():
        single_loop(auth.settings.expiration)

Command lines options specific to session2trash.py:
NOTE: They should be preceeded by web2py command line option "-A" to be passed to script.

-f, --force : Ignore session expiration. Force expiry based on -x option or auth.settings.expiration.
-o, --once : Delete sessions, then exit. Essential when trigger trash sessions from system CRON JOB
-s, --sleep : Number of seconds to sleep between executions. Default 300.
-v, --verbose : print verbose output, a second -v increases verbosity
-x, --expiration : Expiration value for sessions without expiration (in seconds)
"""

from __future__ import with_statement

from gluon import current
from gluon.storage import Storage
from gluon._compat import pickle
from optparse import OptionParser
import datetime
import stat
import time
import os

EXPIRATION_MINUTES = 60
SLEEP_MINUTES = 5
VERSION = 0.3


class SessionSet(object):
    """Class representing a set of sessions"""

    def __init__(self, expiration, force, verbose):
        self.expiration = expiration
        self.force = force
        self.verbose = verbose

    def get(self):
        """Get session files/records."""
        raise NotImplementedError

    def trash(self):
        """Trash expired sessions."""
        now = datetime.datetime.now()
        for item in self.get():
            status = 'OK'
            last_visit = item.last_visit_default()

            try:
                session = item.get()
                if session.auth:
                    if session.auth.expiration and not self.force:
                        self.expiration = session.auth.expiration
                    if session.auth.last_visit:
                        last_visit = session.auth.last_visit
            except:
                pass

            age = 0
            if last_visit:
                age = total_seconds(now - last_visit)

            if age > self.expiration or not self.expiration:
                item.delete()
                status = 'trashed'

            if self.verbose > 1:
                print('key: %s' % str(item))
                print('expiration: %s seconds' % self.expiration)
                print('last visit: %s' % str(last_visit))
                print('age: %s seconds' % age)
                print('status: %s' % status)
                print('')
            elif self.verbose > 0:
                print('%s %s' % (str(item), status))


class SessionSetDb(SessionSet):
    """Class representing a set of sessions stored in database"""

    def __init__(self, expiration, force, verbose):
        SessionSet.__init__(self, expiration, force, verbose)

    def get(self):
        """Return list of SessionDb instances for existing sessions."""
        table = current.response.session_db_table
        if table:
            for row in table._db(table.id > 0).select():
                yield SessionDb(row)


class SessionSetFiles(SessionSet):
    """Class representing a set of sessions stored in flat files"""

    def __init__(self, expiration, force, verbose):
        SessionSet.__init__(self, expiration, force, verbose)

    def cleanup_empty_folders(self, root_path):
        for path, dirs, files in os.walk(root_path, topdown=False):
            for d in dirs:
                dd = os.path.join(path, d)
                if not os.listdir(dd):
                    os.rmdir(dd)

    def get(self):
        """Return list of SessionFile instances for existing sessions."""
        root_path = os.path.join(current.request.folder, 'sessions')
        for path, dirs, files in os.walk(root_path, topdown=False):
            for x in files:
                yield SessionFile(os.path.join(path, x))
        self.cleanup_empty_folders(root_path)


class SessionDb(object):
    """Class representing a single session stored in database"""

    def __init__(self, row):
        self.row = row

    def delete(self):
        table = current.response.session_db_table
        self.row.delete_record()
        table._db.commit()

    def get(self):
        session = Storage()
        session.update(pickle.loads(self.row.session_data))
        return session

    def last_visit_default(self):
        if isinstance(self.row.modified_datetime, datetime.datetime):
            return self.row.modified_datetime
        else:
            try:
                return datetime.datetime.strptime(self.row.modified_datetime, '%Y-%m-%d %H:%M:%S.%f')
            except:
                print('failed to retrieve last modified time (value: %s)' % self.row.modified_datetime)

    def __str__(self):
        return self.row.unique_key


class SessionFile(object):
    """Class representing a single session stored as a flat file"""

    def __init__(self, filename):
        self.filename = filename

    def delete(self):
        try:
            os.unlink(self.filename)
        except:
            pass

    def get(self):
        session = Storage()
        with open(self.filename, 'rb+') as f:
            session.update(cPickle.load(f))
        return session

    def last_visit_default(self):
        return datetime.datetime.fromtimestamp(
            os.stat(self.filename)[stat.ST_MTIME])

    def __str__(self):
        return self.filename


def total_seconds(delta):
    """
    Adapted from Python 2.7's timedelta.total_seconds() method.

    Args:
        delta: datetime.timedelta instance.
    """
    return (delta.microseconds + (delta.seconds + (delta.days * 24 * 3600)) *
            10 ** 6) / 10 ** 6

def single_loop(expiration=None, force=False, verbose=False):
    if expiration is None:
        try:
            expiration = auth.settings.expiration
        except:
            expiration = EXPIRATION_MINUTES * 60

    set_files = SessionSetFiles(expiration, force, verbose)
    set_files.trash()
    set_db = SessionSetDb(expiration, force, verbose)
    set_db.trash()


def main():
    """Main processing."""

    usage = '%prog [options]' + '\nVersion: %s' % VERSION
    parser = OptionParser(usage=usage)

    parser.add_option('-f', '--force',
                      action='store_true', dest='force', default=False,
                      help=('Ignore session expiration. '
                            'Force expiry based on -x option or auth.settings.expiration.')
                      )
    parser.add_option('-o', '--once',
                      action='store_true', dest='once', default=False,
                      help='Delete sessions, then exit.',
                      )
    parser.add_option('-s', '--sleep',
                      dest='sleep', default=SLEEP_MINUTES * 60, type="int",
                      help='Number of seconds to sleep between executions. Default 300.',
                      )
    parser.add_option('-v', '--verbose',
                      default=0, action='count',
                      help="print verbose output, a second -v increases verbosity")
    parser.add_option('-x', '--expiration',
                      dest='expiration', default=None, type="int",
                      help='Expiration value for sessions without expiration (in seconds)',
                      )

    (options, unused_args) = parser.parse_args()

    expiration = options.expiration

    while True:
        single_loop(expiration, options.force, options.verbose)

        if options.once:
            break
        else:
            if options.verbose:
                print('Sleeping %s seconds' % (options.sleep))
            time.sleep(options.sleep)

if __name__ == '__main__':
    main()
