1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313
|
"""
Utils for handle files.
"""
import logging
from django.core.exceptions import ImproperlyConfigured
from . import settings, utils
def get_storage(path=None, options=None):
"""
Get the specified storage configured with options.
:param path: Path in Python dot style to module containing the storage
class. If empty settings.DBBACKUP_STORAGE will be used.
:type path: ``str``
:param options: Parameters for configure the storage, if empty
settings.DBBACKUP_STORAGE_OPTIONS will be used.
:type options: ``dict``
:return: Storage configured
:rtype: :class:`.Storage`
"""
path = path or settings.STORAGE
options = options or settings.STORAGE_OPTIONS
if not path:
raise ImproperlyConfigured(
"You must specify a storage class using " "DBBACKUP_STORAGE settings."
)
return Storage(path, **options)
class StorageError(Exception):
pass
class FileNotFound(StorageError):
pass
class Storage:
"""
This object make high-level storage operations for upload/download or
list and filter files. It uses a Django storage object for low-level
operations.
"""
@property
def logger(self):
if not hasattr(self, "_logger"):
self._logger = logging.getLogger("dbbackup.storage")
return self._logger
def __init__(self, storage_path=None, **options):
"""
Initialize a Django Storage instance with given options.
:param storage_path: Path to a Django Storage class with dot style
If ``None``, ``settings.DBBACKUP_STORAGE`` will
be used.
:type storage_path: str
"""
self._storage_path = storage_path or settings.STORAGE
options = options.copy()
options.update(settings.STORAGE_OPTIONS)
options = {key.lower(): value for key, value in options.items()}
self.storageCls = get_storage_class(self._storage_path)
self.storage = self.storageCls(**options)
self.name = self.storageCls.__name__
def __str__(self):
return f"dbbackup-{self.storage.__str__()}"
def delete_file(self, filepath):
self.logger.debug("Deleting file %s", filepath)
self.storage.delete(name=filepath)
def list_directory(self, path=""):
return self.storage.listdir(path)[1]
def write_file(self, filehandle, filename):
self.logger.debug("Writing file %s", filename)
self.storage.save(name=filename, content=filehandle)
def read_file(self, filepath):
self.logger.debug("Reading file %s", filepath)
file_ = self.storage.open(name=filepath, mode="rb")
if not getattr(file_, "name", None):
file_.name = filepath
return file_
def list_backups(
self,
encrypted=None,
compressed=None,
content_type=None,
database=None,
servername=None,
):
"""
List stored files except given filter. If filter is None, it won't be
used. ``content_type`` must be ``'db'`` for database backups or
``'media'`` for media backups.
:param encrypted: Filter by encrypted or not
:type encrypted: ``bool`` or ``None``
:param compressed: Filter by compressed or not
:type compressed: ``bool`` or ``None``
:param content_type: Filter by media or database backup, must be
``'db'`` or ``'media'``
:type content_type: ``str`` or ``None``
:param database: Filter by source database's name
:type: ``str`` or ``None``
:param servername: Filter by source server's name
:type: ``str`` or ``None``
:returns: List of files
:rtype: ``list`` of ``str``
"""
if content_type not in ("db", "media", None):
msg = "Bad content_type %s, must be 'db', 'media', or None" % (content_type)
raise TypeError(msg)
# TODO: Make better filter for include only backups
files = [f for f in self.list_directory() if utils.filename_to_datestring(f)]
if encrypted is not None:
files = [f for f in files if (".gpg" in f) == encrypted]
if compressed is not None:
files = [f for f in files if (".gz" in f) == compressed]
if content_type == "media":
files = [f for f in files if ".tar" in f]
elif content_type == "db":
files = [f for f in files if ".tar" not in f]
if database:
files = [f for f in files if database in f]
if servername:
files = [f for f in files if servername in f]
return files
def get_latest_backup(
self,
encrypted=None,
compressed=None,
content_type=None,
database=None,
servername=None,
):
"""
Return the latest backup file name.
:param encrypted: Filter by encrypted or not
:type encrypted: ``bool`` or ``None``
:param compressed: Filter by compressed or not
:type compressed: ``bool`` or ``None``
:param content_type: Filter by media or database backup, must be
``'db'`` or ``'media'``
:type content_type: ``str`` or ``None``
:param database: Filter by source database's name
:type: ``str`` or ``None``
:param servername: Filter by source server's name
:type: ``str`` or ``None``
:returns: Most recent file
:rtype: ``str``
:raises: FileNotFound: If no backup file is found
"""
files = self.list_backups(
encrypted=encrypted,
compressed=compressed,
content_type=content_type,
database=database,
servername=servername,
)
if not files:
raise FileNotFound("There's no backup file available.")
return max(files, key=utils.filename_to_date)
def get_older_backup(
self,
encrypted=None,
compressed=None,
content_type=None,
database=None,
servername=None,
):
"""
Return the older backup's file name.
:param encrypted: Filter by encrypted or not
:type encrypted: ``bool`` or ``None``
:param compressed: Filter by compressed or not
:type compressed: ``bool`` or ``None``
:param content_type: Filter by media or database backup, must be
``'db'`` or ``'media'``
:type content_type: ``str`` or ``None``
:param database: Filter by source database's name
:type: ``str`` or ``None``
:param servername: Filter by source server's name
:type: ``str`` or ``None``
:returns: Older file
:rtype: ``str``
:raises: FileNotFound: If no backup file is found
"""
files = self.list_backups(
encrypted=encrypted,
compressed=compressed,
content_type=content_type,
database=database,
servername=servername,
)
if not files:
raise FileNotFound("There's no backup file available.")
return min(files, key=utils.filename_to_date)
def clean_old_backups(
self,
encrypted=None,
compressed=None,
content_type=None,
database=None,
servername=None,
keep_number=None,
):
"""
Delete olders backups and hold the number defined.
:param encrypted: Filter by encrypted or not
:type encrypted: ``bool`` or ``None``
:param compressed: Filter by compressed or not
:type compressed: ``bool`` or ``None``
:param content_type: Filter by media or database backup, must be
``'db'`` or ``'media'``
:type content_type: ``str`` or ``None``
:param database: Filter by source database's name
:type: ``str`` or ``None``
:param servername: Filter by source server's name
:type: ``str`` or ``None``
:param keep_number: Number of files to keep, other will be deleted
:type keep_number: ``int`` or ``None``
"""
if keep_number is None:
keep_number = (
settings.CLEANUP_KEEP
if content_type == "db"
else settings.CLEANUP_KEEP_MEDIA
)
keep_filter = settings.CLEANUP_KEEP_FILTER
files = self.list_backups(
encrypted=encrypted,
compressed=compressed,
content_type=content_type,
database=database,
servername=servername,
)
files = sorted(files, key=utils.filename_to_date, reverse=True)
files_to_delete = [fi for i, fi in enumerate(files) if i >= keep_number]
for filename in files_to_delete:
if keep_filter(filename):
continue
self.delete_file(filename)
def get_storage_class(path=None):
"""
Return the configured storage class.
:param path: Path in Python dot style to module containing the storage
class. If empty, the default storage class will be used.
:type path: str or None
:returns: Storage class
:rtype: :class:`django.core.files.storage.Storage`
"""
from django.utils.module_loading import import_string
if path:
# this is a workaround to keep compatibility with Django >= 5.1 (django.core.files.storage.get_storage_class is removed)
return import_string(path)
try:
from django.core.files.storage import DefaultStorage
return DefaultStorage
except Exception:
from django.core.files.storage import get_storage_class
return get_storage_class()
|