Python Library FS-Tools
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

__init__.py 11KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. #!/usr/bin/env python
  2. # -*- coding: utf-8 -*-
  3. #
  4. """
  5. fstools (Filesystem Tools)
  6. ==========================
  7. **Author:**
  8. * Dirk Alders <sudo-dirk@mount-mockery.de>
  9. **Description:**
  10. This module supports functions and classes to handle files and paths
  11. **Submodules:**
  12. * :func:`fstools.dirlist`
  13. * :func:`fstools.filelist`
  14. * :func:`fstools.is_writeable`
  15. * :func:`fstools.mkdir`
  16. * :func:`fstools.open_locked_blocking`
  17. * :func:`fstools.open_locked_non_blocking`
  18. * :func:`fstools.uid`
  19. **Unittest:**
  20. See also the :download:`unittest <fstools/_testresults_/unittest.pdf>` documentation.
  21. **Module Documentation:**
  22. """
  23. __DEPENDENCIES__ = []
  24. import glob
  25. import hashlib
  26. import hmac
  27. import logging
  28. import os
  29. from functools import partial
  30. import sys
  31. import time
  32. try:
  33. from config import APP_NAME as ROOT_LOGGER_NAME
  34. except ImportError:
  35. ROOT_LOGGER_NAME = 'root'
  36. logger = logging.getLogger(ROOT_LOGGER_NAME).getChild(__name__)
  37. try:
  38. import fcntl
  39. except ImportError:
  40. logger.warning('Importing "fcntl" was not possible. Only i limited functionality of fstools is available.')
  41. __DESCRIPTION__ = """The Module {\\tt %s} is designed to help on all issues with files and folders.
  42. For more Information read the documentation.""" % __name__.replace('_', '\_')
  43. """The Module Description"""
  44. __INTERPRETER__ = (3, )
  45. """The Tested Interpreter-Versions"""
  46. def dirlist(path='.', rekursive=True):
  47. """
  48. Function returning a list of directories below a given path.
  49. :param str path: folder which is the basepath for searching files.
  50. :param bool rekursive: search all subfolders if True.
  51. :returns: list of filenames including the pathe
  52. :rtype: list
  53. .. note:: The returned filenames could be relative pathes depending on argument path.
  54. **Example:**
  55. .. literalinclude:: fstools/_examples_/dirlist.py
  56. .. literalinclude:: fstools/_examples_/dirlist.log
  57. """
  58. li = list()
  59. if os.path.exists(path):
  60. logger.debug('DIRLIST: path (%s) exists - looking for directories to append', path)
  61. for dirname in os.listdir(path):
  62. fulldir = os.path.join(path, dirname)
  63. if os.path.isdir(fulldir):
  64. li.append(fulldir)
  65. if rekursive:
  66. li.extend(dirlist(fulldir))
  67. else:
  68. logger.warning('DIRLIST: path (%s) does not exist - empty filelist will be returned', path)
  69. return li
  70. def filelist(path='.', expression='*', rekursive=True):
  71. """
  72. Function returning a list of files below a given path.
  73. :param str path: folder which is the basepath for searching files.
  74. :param str expression: expression to fit including shell-style wildcards.
  75. :param bool rekursive: search all subfolders if True.
  76. :returns: list of filenames including the pathe
  77. :rtype: list
  78. .. note:: The returned filenames could be relative pathes depending on argument path.
  79. **Example:**
  80. .. literalinclude:: fstools/_examples_/filelist.py
  81. .. literalinclude:: fstools/_examples_/filelist.log
  82. """
  83. li = list()
  84. if os.path.exists(path):
  85. logger.debug('FILELIST: path (%s) exists - looking for files to append', path)
  86. for filename in glob.glob(os.path.join(path, expression)):
  87. if os.path.isfile(filename):
  88. li.append(filename)
  89. for directory in os.listdir(path):
  90. directory = os.path.join(path, directory)
  91. if os.path.isdir(directory) and rekursive and not os.path.islink(directory):
  92. li.extend(filelist(directory, expression))
  93. else:
  94. logger.warning('FILELIST: path (%s) does not exist - empty filelist will be returned', path)
  95. return li
  96. def is_writeable(path):
  97. """
  98. Method to get the Information, if a file or folder is writable.
  99. :param str path: file or folder to check.
  100. :returns: Whether path is writable or not.
  101. :rtype: bool
  102. .. note:: If path does not exist, the return Value is :const:`False`.
  103. **Example:**
  104. .. literalinclude:: fstools/_examples_/is_writeable.py
  105. .. literalinclude:: fstools/_examples_/is_writeable.log
  106. """
  107. if os.access(path, os.W_OK):
  108. # path is writable whatever it is, file or directory
  109. return True
  110. else:
  111. # path is not writable whatever it is, file or directory
  112. return False
  113. def mkdir(path):
  114. """
  115. Method to create a folder.
  116. .. note:: All needed subfoilders will also be created (rekursive mkdir).
  117. :param str path: folder to be created.
  118. :returns: True, if folder exists after creation commands, otherwise False.
  119. :rtype: bool
  120. """
  121. path = os.path.abspath(path)
  122. if not os.path.exists(os.path.dirname(path)):
  123. mkdir(os.path.dirname(path))
  124. if not os.path.exists(path):
  125. os.mkdir(path)
  126. return os.path.isdir(path)
  127. def open_locked_blocking(*args, **kwargs):
  128. """
  129. Method to get exclusive access to a file.
  130. :param args: Arguments for a standard file open call.
  131. :param kwargs: Keyword arguments for a standard file open call.
  132. :returns: A file descriptor.
  133. :rtype: file handle
  134. .. note:: The call blocks until file is able to be used. This can cause a deadlock, if the file release es done after trying to open the file!
  135. """
  136. locked_file_descriptor = open(*args, **kwargs)
  137. fcntl.lockf(locked_file_descriptor, fcntl.LOCK_EX)
  138. return locked_file_descriptor
  139. def open_locked_non_blocking(*args, **kwargs):
  140. """
  141. Method to get exclusive access to a file.
  142. :param args: Arguments for a standard file open call.
  143. :param kwargs: Keyword arguments for a standard file open call.
  144. :raises: OSError, if the file is already blocked.
  145. :returns: A file descriptor.
  146. :rtype: file handle
  147. .. note:: The call blocks until file is able to be used. This can cause a deadlock, if the file release es done after trying to open the file!
  148. """
  149. locked_file_descriptor = open(*args, **kwargs)
  150. fcntl.lockf(locked_file_descriptor, fcntl.LOCK_EX | fcntl.LOCK_NB)
  151. return locked_file_descriptor
  152. def uid(path, max_staleness=3600):
  153. """
  154. Function returning a "unique" id for a given file or path.
  155. :param str path: File or folder to generate a uid for.
  156. :param int max_staleness: If a file or folder is older than that, we may consider
  157. it stale and return a different uid - this is a
  158. dirty trick to work around changes never being
  159. detected. Default is 3600 seconds, use None to
  160. disable this trickery. See below for more details.
  161. :returns: An object that changes value if the file changed,
  162. None is returned if there were problems accessing the file
  163. or folder.
  164. :rtype: str
  165. .. warning:: Depending on the operating system capabilities and the way the
  166. file update is done, this function might return the same value
  167. even if the file has changed. It should be better than just
  168. using file's mtime though.
  169. max_staleness tries to avoid the worst for these cases.
  170. .. note:: If this function is used for a path, it will stat all pathes and files rekursively.
  171. **Example:**
  172. .. literalinclude:: fstools/_examples_/uid.py
  173. .. literalinclude:: fstools/_examples_/uid.log
  174. Using just the file's mtime to determine if the file has changed is
  175. not reliable - if file updates happen faster than the file system's
  176. mtime granularity, then the modification is not detectable because
  177. the mtime is still the same.
  178. This function tries to improve by using not only the mtime, but also
  179. other metadata values like file size and inode to improve reliability.
  180. For the calculation of this value, we of course only want to use data
  181. that we can get rather fast, thus we use file metadata, not file data
  182. (file content).
  183. """
  184. if os.path.isdir(path):
  185. pathlist = dirlist(path) + filelist(path)
  186. pathlist.sort()
  187. else:
  188. pathlist = [path]
  189. uid = []
  190. for element in pathlist:
  191. try:
  192. st = os.stat(element)
  193. except (IOError, OSError):
  194. uid.append(None) # for permanent errors on stat() this does not change, but
  195. # having a changing value would be pointless because if we
  196. # can't even stat the file, it is unlikely we can read it.
  197. else:
  198. fake_mtime = int(st.st_mtime)
  199. if not st.st_ino and max_staleness:
  200. # st_ino being 0 likely means that we run on a platform not
  201. # supporting it (e.g. win32) - thus we likely need this dirty
  202. # trick
  203. now = int(time.time())
  204. if now >= st.st_mtime + max_staleness:
  205. # keep same fake_mtime for each max_staleness interval
  206. fake_mtime = int(now / max_staleness) * max_staleness
  207. uid.append((
  208. st.st_mtime, # might have a rather rough granularity, e.g. 2s
  209. # on FAT, 1s on ext3 and might not change on fast
  210. # updates
  211. st.st_ino, # inode number (will change if the update is done
  212. # by e.g. renaming a temp file to the real file).
  213. # not supported on win32 (0 ever)
  214. st.st_size, # likely to change on many updates, but not
  215. # sufficient alone
  216. fake_mtime) # trick to workaround file system / platform
  217. # limitations causing permanent trouble
  218. )
  219. if sys.version_info < (3, 0):
  220. secret = ''
  221. return hmac.new(secret, repr(uid), hashlib.sha1).hexdigest()
  222. else:
  223. secret = b''
  224. return hmac.new(secret, bytes(repr(uid), 'latin-1'), hashlib.sha1).hexdigest()
  225. def uid_filelist(path='.', expression='*', rekursive=True):
  226. """
  227. Function returning a unique id for a given file or path.
  228. :param str path: folder which is the basepath for searching files.
  229. :param str expression: expression to fit including shell-style wildcards.
  230. :param bool rekursive: search all subfolders if True.
  231. :returns: An object that changes value if one of the files change.
  232. :rtype: str
  233. .. note:: This UID is created out of the file content. Therefore it is more
  234. reliable then :func:`fstools.uid`, but also much slower.
  235. **Example:**
  236. .. literalinclude:: fstools/_examples_/uid_filelist.py
  237. .. literalinclude:: fstools/_examples_/uid_filelist.log
  238. """
  239. SHAhash = hashlib.md5()
  240. #
  241. fl = filelist(path, expression, rekursive)
  242. fl.sort()
  243. for f in fl:
  244. if sys.version_info < (3, 0):
  245. with open(f, 'rb') as fh:
  246. SHAhash.update(hashlib.md5(fh.read()).hexdigest())
  247. else:
  248. with open(f, mode='rb') as fh:
  249. d = hashlib.md5()
  250. for buf in iter(partial(fh.read, 128), b''):
  251. d.update(buf)
  252. SHAhash.update(d.hexdigest().encode())
  253. #
  254. return SHAhash.hexdigest()