Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
mikf
GitHub Repository: mikf/gallery-dl
Path: blob/master/gallery_dl/cache.py
5457 views
1
# -*- coding: utf-8 -*-
2
3
# Copyright 2016-2021 Mike Fährmann
4
#
5
# This program is free software; you can redistribute it and/or modify
6
# it under the terms of the GNU General Public License version 2 as
7
# published by the Free Software Foundation.
8
9
"""Decorators to keep function results in an in-memory and database cache"""
10
11
import sqlite3
12
import pickle
13
import time
14
import os
15
import functools
16
from . import config, util
17
18
19
class CacheDecorator():
20
"""Simplified in-memory cache"""
21
def __init__(self, func, keyarg):
22
self.func = func
23
self.cache = {}
24
self.keyarg = keyarg
25
26
def __get__(self, instance, cls):
27
return functools.partial(self.__call__, instance)
28
29
def __call__(self, *args, **kwargs):
30
key = "" if self.keyarg is None else args[self.keyarg]
31
try:
32
value = self.cache[key]
33
except KeyError:
34
value = self.cache[key] = self.func(*args, **kwargs)
35
return value
36
37
def update(self, key, value):
38
self.cache[key] = value
39
40
def invalidate(self, key=""):
41
try:
42
del self.cache[key]
43
except KeyError:
44
pass
45
46
47
class MemoryCacheDecorator(CacheDecorator):
48
"""In-memory cache"""
49
def __init__(self, func, keyarg, maxage):
50
CacheDecorator.__init__(self, func, keyarg)
51
self.maxage = maxage
52
53
def __call__(self, *args, **kwargs):
54
key = "" if self.keyarg is None else args[self.keyarg]
55
timestamp = int(time.time())
56
try:
57
value, expires = self.cache[key]
58
except KeyError:
59
expires = 0
60
if expires <= timestamp:
61
value = self.func(*args, **kwargs)
62
expires = timestamp + self.maxage
63
self.cache[key] = value, expires
64
return value
65
66
def update(self, key, value):
67
self.cache[key] = value, int(time.time()) + self.maxage
68
69
70
class DatabaseCacheDecorator():
71
"""Database cache"""
72
db = None
73
_init = True
74
75
def __init__(self, func, keyarg, maxage):
76
self.key = f"{func.__module__}.{func.__name__}"
77
self.func = func
78
self.cache = {}
79
self.keyarg = keyarg
80
self.maxage = maxage
81
82
def __get__(self, obj, objtype):
83
return functools.partial(self.__call__, obj)
84
85
def __call__(self, *args, **kwargs):
86
key = "" if self.keyarg is None else args[self.keyarg]
87
timestamp = int(time.time())
88
89
# in-memory cache lookup
90
try:
91
value, expires = self.cache[key]
92
if expires > timestamp:
93
return value
94
except KeyError:
95
pass
96
97
# database lookup
98
fullkey = f"{self.key}-{key}"
99
with self.database() as db:
100
cursor = db.cursor()
101
try:
102
cursor.execute("BEGIN EXCLUSIVE")
103
except sqlite3.OperationalError:
104
pass # Silently swallow exception - workaround for Python 3.6
105
cursor.execute(
106
"SELECT value, expires FROM data WHERE key=? LIMIT 1",
107
(fullkey,),
108
)
109
result = cursor.fetchone()
110
111
if result and result[1] > timestamp:
112
value, expires = result
113
value = pickle.loads(value)
114
else:
115
value = self.func(*args, **kwargs)
116
expires = timestamp + self.maxage
117
cursor.execute(
118
"INSERT OR REPLACE INTO data VALUES (?,?,?)",
119
(fullkey, pickle.dumps(value), expires),
120
)
121
122
self.cache[key] = value, expires
123
return value
124
125
def update(self, key, value):
126
expires = int(time.time()) + self.maxage
127
self.cache[key] = value, expires
128
with self.database() as db:
129
db.execute(
130
"INSERT OR REPLACE INTO data VALUES (?,?,?)",
131
(f"{self.key}-{key}", pickle.dumps(value), expires),
132
)
133
134
def invalidate(self, key):
135
try:
136
del self.cache[key]
137
except KeyError:
138
pass
139
with self.database() as db:
140
db.execute(
141
"DELETE FROM data WHERE key=?",
142
(f"{self.key}-{key}",),
143
)
144
145
def database(self):
146
if self._init:
147
self.db.execute(
148
"CREATE TABLE IF NOT EXISTS data "
149
"(key TEXT PRIMARY KEY, value TEXT, expires INTEGER)"
150
)
151
DatabaseCacheDecorator._init = False
152
return self.db
153
154
155
def memcache(maxage=None, keyarg=None):
156
if maxage:
157
def wrap(func):
158
return MemoryCacheDecorator(func, keyarg, maxage)
159
else:
160
def wrap(func):
161
return CacheDecorator(func, keyarg)
162
return wrap
163
164
165
def cache(maxage=3600, keyarg=None):
166
def wrap(func):
167
return DatabaseCacheDecorator(func, keyarg, maxage)
168
return wrap
169
170
171
def clear(module):
172
"""Delete database entries for 'module'"""
173
db = DatabaseCacheDecorator.db
174
if not db:
175
return None
176
177
rowcount = 0
178
cursor = db.cursor()
179
180
try:
181
if module == "ALL":
182
cursor.execute("DELETE FROM data")
183
else:
184
cursor.execute(
185
"DELETE FROM data "
186
"WHERE key LIKE 'gallery_dl.extractor.' || ? || '.%'",
187
(module.lower(),)
188
)
189
except sqlite3.OperationalError:
190
pass # database not initialized, cannot be modified, etc.
191
else:
192
rowcount = cursor.rowcount
193
db.commit()
194
if rowcount:
195
cursor.execute("VACUUM")
196
return rowcount
197
198
199
def _path():
200
path = config.get(("cache",), "file", util.SENTINEL)
201
if path is not util.SENTINEL:
202
return util.expand_path(path)
203
204
if util.WINDOWS:
205
cachedir = os.environ.get("APPDATA", "~")
206
else:
207
cachedir = os.environ.get("XDG_CACHE_HOME", "~/.cache")
208
209
cachedir = util.expand_path(os.path.join(cachedir, "gallery-dl"))
210
os.makedirs(cachedir, exist_ok=True)
211
return os.path.join(cachedir, "cache.sqlite3")
212
213
214
def _init():
215
try:
216
dbfile = _path()
217
218
# restrict access permissions for new db files
219
os.close(os.open(dbfile, os.O_CREAT | os.O_RDONLY, 0o600))
220
221
DatabaseCacheDecorator.db = sqlite3.connect(
222
dbfile, timeout=60, check_same_thread=False)
223
except (OSError, TypeError, sqlite3.OperationalError):
224
global cache
225
cache = memcache
226
227
228
_init()
229
230