260 lines
8.3 KiB
Python
260 lines
8.3 KiB
Python
|
#!/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()
|