2016-03-05 17:49:18 +01:00
|
|
|
# -*- coding: utf-8 -*-
|
|
|
|
|
2021-05-03 22:24:15 +02:00
|
|
|
# Copyright 2016-2021 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
|
2019-07-31 15:27:06 +02:00
|
|
|
import os
|
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
|
|
|
|
2020-01-04 23:46:29 +01:00
|
|
|
def invalidate(self, key=""):
|
2019-03-14 15:55:48 +01:00
|
|
|
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
|
2020-05-09 23:55:14 +02:00
|
|
|
if expires <= timestamp:
|
2019-03-14 15:55:48 +01:00
|
|
|
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)
|
2020-02-25 23:08:47 +01:00
|
|
|
with self.database() as db:
|
|
|
|
cursor = db.cursor()
|
|
|
|
try:
|
|
|
|
cursor.execute("BEGIN EXCLUSIVE")
|
|
|
|
except sqlite3.OperationalError:
|
|
|
|
pass # Silently swallow exception - workaround for Python 3.6
|
2019-03-14 15:55:48 +01:00
|
|
|
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),
|
|
|
|
)
|
2020-02-25 23:08:47 +01:00
|
|
|
|
2019-03-14 15:55:48 +01:00
|
|
|
self.cache[key] = value, expires
|
|
|
|
return value
|
|
|
|
|
|
|
|
def update(self, key, value):
|
|
|
|
expires = int(time.time()) + self.maxage
|
|
|
|
self.cache[key] = value, expires
|
2020-02-25 23:08:47 +01:00
|
|
|
with self.database() as db:
|
|
|
|
db.execute(
|
2020-02-23 20:59:16 +01:00
|
|
|
"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
|
2020-02-25 23:08:47 +01:00
|
|
|
with self.database() as db:
|
|
|
|
db.execute(
|
2020-02-23 20:59:16 +01:00
|
|
|
"DELETE FROM data WHERE key=?",
|
|
|
|
("%s-%s" % (self.key, key),),
|
|
|
|
)
|
2016-04-20 08:40:41 +02:00
|
|
|
|
2020-02-25 23:08:47 +01:00
|
|
|
def database(self):
|
2019-03-14 15:55:48 +01:00
|
|
|
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
|
2020-02-25 23:08:47 +01:00
|
|
|
return self.db
|
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
|
|
|
|
2021-06-11 23:18:49 +02:00
|
|
|
def clear(module):
|
2021-05-03 22:24:15 +02:00
|
|
|
"""Delete database entries for 'module'"""
|
2019-04-25 21:30:16 +02:00
|
|
|
db = DatabaseCacheDecorator.db
|
2021-05-03 22:24:15 +02:00
|
|
|
if not db:
|
|
|
|
return None
|
2019-04-25 21:30:16 +02:00
|
|
|
|
2021-05-03 22:24:15 +02:00
|
|
|
rowcount = 0
|
|
|
|
cursor = db.cursor()
|
|
|
|
|
|
|
|
try:
|
2021-06-11 23:18:49 +02:00
|
|
|
if module == "ALL":
|
2019-04-25 21:30:16 +02:00
|
|
|
cursor.execute("DELETE FROM data")
|
|
|
|
else:
|
2021-05-03 22:24:15 +02:00
|
|
|
cursor.execute(
|
|
|
|
"DELETE FROM data "
|
|
|
|
"WHERE key LIKE 'gallery_dl.extractor.' || ? || '.%'",
|
2021-06-11 23:18:49 +02:00
|
|
|
(module.lower(),)
|
2021-05-03 22:24:15 +02:00
|
|
|
)
|
|
|
|
except sqlite3.OperationalError:
|
2021-06-11 23:18:49 +02:00
|
|
|
pass # database not initialized, cannot be modified, etc.
|
2021-05-03 22:24:15 +02:00
|
|
|
else:
|
|
|
|
rowcount = cursor.rowcount
|
|
|
|
db.commit()
|
|
|
|
if rowcount:
|
2019-04-25 21:30:16 +02:00
|
|
|
cursor.execute("VACUUM")
|
2021-05-03 22:24:15 +02:00
|
|
|
return rowcount
|
2019-04-25 21:30:16 +02:00
|
|
|
|
|
|
|
|
|
|
|
def _path():
|
2020-05-19 21:47:18 +02:00
|
|
|
path = config.get(("cache",), "file", util.SENTINEL)
|
|
|
|
if path is not util.SENTINEL:
|
2019-07-31 22:45:02 +02:00
|
|
|
return util.expand_path(path)
|
2019-04-25 21:30:16 +02:00
|
|
|
|
2020-05-19 21:42:11 +02:00
|
|
|
if util.WINDOWS:
|
2020-05-28 02:56:38 +02:00
|
|
|
cachedir = os.environ.get("APPDATA", "~")
|
|
|
|
else:
|
|
|
|
cachedir = os.environ.get("XDG_CACHE_HOME", "~/.cache")
|
2016-03-05 17:49:18 +01:00
|
|
|
|
2020-05-28 02:56:38 +02:00
|
|
|
cachedir = util.expand_path(os.path.join(cachedir, "gallery-dl"))
|
2019-08-01 21:47:13 +02:00
|
|
|
os.makedirs(cachedir, exist_ok=True)
|
|
|
|
return os.path.join(cachedir, "cache.sqlite3")
|
2016-03-05 17:49:18 +01:00
|
|
|
|
2019-04-25 21:30:16 +02:00
|
|
|
|
2021-10-13 03:10:55 +02:00
|
|
|
def _init():
|
|
|
|
try:
|
|
|
|
dbfile = _path()
|
|
|
|
|
|
|
|
# restrict access permissions for new db files
|
|
|
|
os.close(os.open(dbfile, os.O_CREAT | os.O_RDONLY, 0o600))
|
|
|
|
|
|
|
|
DatabaseCacheDecorator.db = sqlite3.connect(
|
|
|
|
dbfile, timeout=60, check_same_thread=False)
|
|
|
|
except (OSError, TypeError, sqlite3.OperationalError):
|
|
|
|
global cache
|
|
|
|
cache = memcache
|
2020-05-28 02:56:38 +02:00
|
|
|
|
|
|
|
|
2021-10-13 03:10:55 +02:00
|
|
|
_init()
|