2016-03-05 17:49:18 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
# Copyright 2016-2019 Mike Fährmann
|
2016-03-05 17:49:18 +01:00
|
|
|
#
|
|
|
|
# This program is free software; you can redistribute it and/or modify
|
|
|
|
# it under the terms of the GNU General Public License version 2 as
|
|
|
|
# published by the Free Software Foundation.
|
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
"""Decorators to keep function results in an in-memory and database cache"""
|
2016-04-20 08:40:41 +02:00
|
|
|
|
2016-03-05 17:49:18 +01:00
|
|
|
import sqlite3
|
|
|
|
import pickle
|
|
|
|
import time
|
2016-04-20 08:40:41 +02:00
|
|
|
import functools
|
2017-10-26 00:04:28 +02:00
|
|
|
from . import config, util
|
2016-03-05 17:49:18 +01:00
|
|
|
|
2016-04-20 08:40:41 +02:00
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
class CacheDecorator():
|
|
|
|
"""Simplified in-memory cache"""
|
|
|
|
def __init__(self, func, keyarg):
|
|
|
|
self.func = func
|
|
|
|
self.cache = {}
|
|
|
|
self.keyarg = keyarg
|
2016-04-20 08:40:41 +02:00
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
def __get__(self, instance, cls):
|
|
|
|
return functools.partial(self.__call__, instance)
|
2016-11-22 17:57:41 +01:00
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
def __call__(self, *args, **kwargs):
|
|
|
|
key = "" if self.keyarg is None else args[self.keyarg]
|
|
|
|
try:
|
|
|
|
value = self.cache[key]
|
|
|
|
except KeyError:
|
|
|
|
value = self.cache[key] = self.func(*args, **kwargs)
|
|
|
|
return value
|
2016-04-20 08:40:41 +02:00
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
def update(self, key, value):
|
|
|
|
self.cache[key] = value
|
2016-04-20 08:40:41 +02:00
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
def invalidate(self, key):
|
|
|
|
try:
|
|
|
|
del self.cache[key]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
2016-04-20 08:40:41 +02:00
|
|
|
|
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
class MemoryCacheDecorator(CacheDecorator):
|
|
|
|
"""In-memory cache"""
|
|
|
|
def __init__(self, func, keyarg, maxage):
|
|
|
|
CacheDecorator.__init__(self, func, keyarg)
|
|
|
|
self.maxage = maxage
|
2016-04-20 08:40:41 +02:00
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
def __call__(self, *args, **kwargs):
|
|
|
|
key = "" if self.keyarg is None else args[self.keyarg]
|
|
|
|
timestamp = int(time.time())
|
|
|
|
try:
|
|
|
|
value, expires = self.cache[key]
|
|
|
|
except KeyError:
|
|
|
|
expires = 0
|
|
|
|
if expires < timestamp:
|
|
|
|
value = self.func(*args, **kwargs)
|
|
|
|
expires = timestamp + self.maxage
|
|
|
|
self.cache[key] = value, expires
|
2016-04-20 08:40:41 +02:00
|
|
|
return value
|
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
def update(self, key, value):
|
|
|
|
self.cache[key] = value, int(time.time()) + self.maxage
|
2016-04-20 08:40:41 +02:00
|
|
|
|
2016-11-22 17:57:41 +01:00
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
class DatabaseCacheDecorator():
|
|
|
|
"""Database cache"""
|
|
|
|
db = None
|
|
|
|
_init = True
|
2016-04-20 08:40:41 +02:00
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
def __init__(self, func, keyarg, maxage):
|
|
|
|
self.key = "%s.%s" % (func.__module__, func.__name__)
|
|
|
|
self.func = func
|
2016-04-20 08:40:41 +02:00
|
|
|
self.cache = {}
|
2019-03-14 15:55:48 +01:00
|
|
|
self.keyarg = keyarg
|
|
|
|
self.maxage = maxage
|
2016-04-20 08:40:41 +02:00
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
def __get__(self, obj, objtype):
|
|
|
|
return functools.partial(self.__call__, obj)
|
|
|
|
|
|
|
|
def __call__(self, *args, **kwargs):
|
|
|
|
key = "" if self.keyarg is None else args[self.keyarg]
|
|
|
|
timestamp = int(time.time())
|
|
|
|
|
|
|
|
# in-memory cache lookup
|
2016-04-20 08:40:41 +02:00
|
|
|
try:
|
|
|
|
value, expires = self.cache[key]
|
2019-03-14 15:55:48 +01:00
|
|
|
if expires > timestamp:
|
|
|
|
return value
|
2016-04-20 08:40:41 +02:00
|
|
|
except KeyError:
|
|
|
|
pass
|
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
# database lookup
|
|
|
|
fullkey = "%s-%s" % (self.key, key)
|
|
|
|
cursor = self.cursor()
|
|
|
|
try:
|
|
|
|
cursor.execute("BEGIN EXCLUSIVE")
|
|
|
|
except sqlite3.OperationalError:
|
|
|
|
pass # Silently swallow exception - workaround for Python 3.6
|
|
|
|
try:
|
|
|
|
cursor.execute(
|
|
|
|
"SELECT value, expires FROM data WHERE key=? LIMIT 1",
|
|
|
|
(fullkey,),
|
|
|
|
)
|
|
|
|
result = cursor.fetchone()
|
|
|
|
|
|
|
|
if result and result[1] > timestamp:
|
|
|
|
value, expires = result
|
|
|
|
value = pickle.loads(value)
|
|
|
|
else:
|
|
|
|
value = self.func(*args, **kwargs)
|
|
|
|
expires = timestamp + self.maxage
|
|
|
|
cursor.execute(
|
|
|
|
"INSERT OR REPLACE INTO data VALUES (?,?,?)",
|
|
|
|
(fullkey, pickle.dumps(value), expires),
|
|
|
|
)
|
|
|
|
finally:
|
|
|
|
self.db.commit()
|
|
|
|
self.cache[key] = value, expires
|
|
|
|
return value
|
|
|
|
|
|
|
|
def update(self, key, value):
|
|
|
|
expires = int(time.time()) + self.maxage
|
|
|
|
self.cache[key] = value, expires
|
|
|
|
self.cursor().execute(
|
|
|
|
"INSERT OR REPLACE INTO data VALUES (?,?,?)",
|
|
|
|
("%s-%s" % (self.key, key), pickle.dumps(value), expires),
|
|
|
|
)
|
2016-04-20 08:40:41 +02:00
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
def invalidate(self, key):
|
2016-11-22 17:57:41 +01:00
|
|
|
try:
|
|
|
|
del self.cache[key]
|
|
|
|
except KeyError:
|
|
|
|
pass
|
2019-03-14 15:55:48 +01:00
|
|
|
self.cursor().execute(
|
|
|
|
"DELETE FROM data WHERE key=? LIMIT 1",
|
|
|
|
("%s-%s" % (self.key, key),),
|
2017-01-30 19:40:15 +01:00
|
|
|
)
|
2016-04-20 08:40:41 +02:00
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
def cursor(self):
|
|
|
|
if self._init:
|
|
|
|
self.db.execute(
|
|
|
|
"CREATE TABLE IF NOT EXISTS data "
|
|
|
|
"(key TEXT PRIMARY KEY, value TEXT, expires INTEGER)"
|
2017-01-30 19:40:15 +01:00
|
|
|
)
|
2019-03-14 15:55:48 +01:00
|
|
|
DatabaseCacheDecorator._init = False
|
|
|
|
return self.db.cursor()
|
2016-04-20 08:40:41 +02:00
|
|
|
|
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
def memcache(maxage=None, keyarg=None):
|
|
|
|
if maxage:
|
|
|
|
def wrap(func):
|
|
|
|
return MemoryCacheDecorator(func, keyarg, maxage)
|
|
|
|
else:
|
|
|
|
def wrap(func):
|
|
|
|
return CacheDecorator(func, keyarg)
|
|
|
|
return wrap
|
2016-04-20 08:40:41 +02:00
|
|
|
|
2016-11-22 17:57:41 +01:00
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
def cache(maxage=3600, keyarg=None):
|
|
|
|
def wrap(func):
|
|
|
|
return DatabaseCacheDecorator(func, keyarg, maxage)
|
|
|
|
return wrap
|
2018-10-12 22:18:29 +02:00
|
|
|
|
2016-04-20 08:40:41 +02:00
|
|
|
|
2019-04-25 21:30:16 +02:00
|
|
|
def clear():
|
|
|
|
"""Delete all database entries"""
|
|
|
|
db = DatabaseCacheDecorator.db
|
|
|
|
|
|
|
|
if db:
|
|
|
|
rowcount = 0
|
|
|
|
cursor = db.cursor()
|
|
|
|
try:
|
|
|
|
cursor.execute("DELETE FROM data")
|
|
|
|
except sqlite3.OperationalError:
|
|
|
|
pass # database is not initialized, can't be modified, etc.
|
|
|
|
else:
|
|
|
|
rowcount = cursor.rowcount
|
|
|
|
db.commit()
|
|
|
|
cursor.execute("VACUUM")
|
|
|
|
return rowcount
|
|
|
|
|
|
|
|
return None
|
|
|
|
|
|
|
|
|
|
|
|
def _path():
|
|
|
|
path = config.get(("cache", "file"), -1)
|
|
|
|
|
|
|
|
if path == -1:
|
2019-03-14 15:55:48 +01:00
|
|
|
import tempfile
|
|
|
|
import os.path
|
2019-04-25 21:30:16 +02:00
|
|
|
return os.path.join(tempfile.gettempdir(), ".gallery-dl.cache")
|
2016-03-05 17:49:18 +01:00
|
|
|
|
2019-04-25 21:30:16 +02:00
|
|
|
return util.expand_path(path)
|
2016-03-05 17:49:18 +01:00
|
|
|
|
2019-04-25 21:30:16 +02:00
|
|
|
|
|
|
|
try:
|
|
|
|
DatabaseCacheDecorator.db = sqlite3.connect(
|
|
|
|
_path(), timeout=30, check_same_thread=False)
|
|
|
|
except (TypeError, sqlite3.OperationalError):
|
2019-03-14 15:55:48 +01:00
|
|
|
cache = memcache # noqa: F811
|