base.py 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. # Use of this source code is governed by a BSD-style
  2. # license that can be found in the LICENSE file.
  3. # Copyright 2019 The OSArchiver Authors. All rights reserved.
  4. """
  5. Base class file of file backend implementation.
  6. """
  7. import logging
  8. import os
  9. import shutil
  10. import re
  11. from importlib import import_module
  12. from abc import ABCMeta, abstractmethod
  13. import arrow
  14. from osarchiver.destination.base import Destination
  15. from osarchiver.destination.file.remote_store import factory as remote_store_factory
  16. class File(Destination):
  17. """
  18. The base File class is a Destination like class which implement file
  19. backend.
  20. """
  21. def __init__(self,
  22. directory=None,
  23. archive_format='tar',
  24. formats='csv',
  25. dry_run=False,
  26. source=None,
  27. remote_store=None,
  28. **kwargs):
  29. """
  30. Initiator
  31. :param str directory: the directory where store the files
  32. :param str archive_format: which format to use to compress file
  33. default is tar, format available are formats available with
  34. shutil.make_archive
  35. :param list formats: list of formats in which data will be written.
  36. The format should be implemented as a subclass of the current class, it
  37. is called a formatter
  38. :param bool dry_run: if enable will not write for real
  39. :param source: the Source instance
  40. """
  41. # Archive formats: zip, tar, gztar, bztar, xztar
  42. Destination.__init__(self, backend='file',
  43. conf=kwargs.get('conf', None))
  44. self.date = arrow.now().strftime('%F_%T')
  45. self.directory = str(directory).format(date=self.date)
  46. self.archive_format = archive_format
  47. self.formats = re.split(r'\n|,|;', formats)
  48. self.formatters = {}
  49. self.source = source
  50. self.dry_run = dry_run
  51. self.remote_store = None
  52. if remote_store is not None:
  53. self.remote_store = re.split(r'\n|,|;', remote_store)
  54. self.init()
  55. def close(self):
  56. """
  57. This method close will call close() method of each formatter
  58. """
  59. for formatter in self.formatters:
  60. getattr(self.formatters[formatter], 'close')()
  61. def clean_exit(self):
  62. """
  63. clean_exit method that should be implemented. Close all formatter and
  64. compress file
  65. """
  66. self.close()
  67. compressed_files = self.compress()
  68. # Send log files remotely if needed
  69. if self.remote_store:
  70. logging.info("Sending osarchiver files remotely")
  71. for store in self.remote_store:
  72. logging.info("Sending remotely on '%s'", store)
  73. # Retrieve store config options
  74. store_options = self.conf.section(
  75. 'remote_store:%s' % store, default=False)
  76. remote_store = remote_store_factory(
  77. name=store, date=self.date, store_options=store_options)
  78. if self.dry_run:
  79. logging.info(
  80. "As we are in dry-run mode we do not send on %s store", store)
  81. continue
  82. remote_store.send(files=compressed_files)
  83. if self.dry_run:
  84. try:
  85. logging.info(
  86. "Removing target directory %s because dry-run "
  87. "mode enabled", self.directory)
  88. os.rmdir(self.directory)
  89. except OSError as oserror_exception:
  90. logging.error(
  91. "Unable to remove dest directory (certainly not "
  92. "empty dir): %s", oserror_exception)
  93. def files(self):
  94. """
  95. Return a list of files open by all formatters
  96. """
  97. files = []
  98. for formatter in self.formatters:
  99. files.extend(getattr(self.formatters[formatter], 'files')())
  100. return files
  101. def compress(self):
  102. """
  103. Compress all the files open by formatters
  104. """
  105. compressed_files = []
  106. for file_to_compress in self.files():
  107. logging.info("Archiving %s using %s format", file_to_compress,
  108. self.archive_format)
  109. compressed_file = shutil.make_archive(
  110. file_to_compress,
  111. self.archive_format,
  112. root_dir=os.path.dirname(file_to_compress),
  113. base_dir=os.path.basename(file_to_compress),
  114. dry_run=self.dry_run)
  115. if compressed_file:
  116. logging.info("Compressed file available at %s",
  117. compressed_file)
  118. compressed_files.append(compressed_file)
  119. os.remove(file_to_compress)
  120. return compressed_files
  121. def init(self):
  122. """
  123. init stuff
  124. """
  125. # in case of multiple destinations using file backend
  126. # the class will be instantiated multiple times
  127. # so we need to accept that destination directory
  128. # already exist
  129. # https://github.com/ovh/osarchiver/issues/11
  130. os.makedirs(self.directory, exist_ok=True)
  131. def write(self, database=None, table=None, data=None):
  132. """
  133. Write method that should be implemented
  134. For each format instanciate a formatter and writes the data set
  135. """
  136. logging.info("Writing on backend %s %s data length", self.backend,
  137. len(data))
  138. for write_format in self.formats:
  139. # initiate formatter
  140. if write_format not in self.formatters:
  141. try:
  142. class_name = write_format.capitalize()
  143. module = import_module(
  144. 'osarchiver.destination.file.{write_format}'.format(
  145. write_format=write_format))
  146. formatter_class = getattr(module, class_name)
  147. formatter_instance = formatter_class(
  148. directory=self.directory,
  149. dry_run=self.dry_run,
  150. source=self.source)
  151. self.formatters[write_format] = formatter_instance
  152. except (AttributeError, ImportError) as my_exception:
  153. logging.error(my_exception)
  154. raise ImportError(
  155. "{} is not part of our file formatter".format(
  156. write_format))
  157. else:
  158. if not issubclass(formatter_class, Formatter):
  159. raise ImportError(
  160. "Unsupported '{}' file format ".format(
  161. write_format))
  162. writer = self.formatters[write_format]
  163. writer.write(database=database, table=table, data=data)
  164. class Formatter(metaclass=ABCMeta):
  165. """
  166. Formatter base class which implements a backend, each backend have to
  167. inherit from that class
  168. """
  169. def __init__(self, name=None, directory=None, dry_run=None, source=None):
  170. """
  171. Initiator:
  172. """
  173. self.directory = directory
  174. self.source = source
  175. self.handlers = {}
  176. self.now = arrow.now().strftime('%F_%T')
  177. self.dry_run = dry_run
  178. self.name = name or type(self).__name__.upper()
  179. def files(self):
  180. """
  181. Return the list of file handlers
  182. """
  183. return [self.handlers[h]['file'] for h in self.handlers]
  184. @abstractmethod
  185. def write(self, data=None):
  186. """
  187. Write method that should be implemented by the classes that inherit
  188. from the import formatter class
  189. """
  190. def close(self):
  191. """
  192. The method close all the file handler which are not closed
  193. """
  194. for handler in self.handlers:
  195. if self.handlers[handler]['fh'].closed:
  196. continue
  197. logging.info("Closing handler of %s",
  198. self.handlers[handler]['file'])
  199. self.handlers[handler]['fh'].close()
  200. self.handlers[handler]['fh'].close()