mountstorage.py 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165
  1. # GNU MediaGoblin -- federated, autonomous media hosting
  2. # Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
  3. #
  4. # This program is free software: you can redistribute it and/or modify
  5. # it under the terms of the GNU Affero General Public License as published by
  6. # the Free Software Foundation, either version 3 of the License, or
  7. # (at your option) any later version.
  8. #
  9. # This program is distributed in the hope that it will be useful,
  10. # but WITHOUT ANY WARRANTY; without even the implied warranty of
  11. # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  12. # GNU Affero General Public License for more details.
  13. #
  14. # You should have received a copy of the GNU Affero General Public License
  15. # along with this program. If not, see <http://www.gnu.org/licenses/>.
  16. from __future__ import print_function
  17. import six
  18. from mediagoblin.storage import StorageInterface, clean_listy_filepath
  19. class MountError(Exception):
  20. pass
  21. class MountStorage(StorageInterface):
  22. """
  23. Experimental "Mount" virtual Storage Interface
  24. This isn't an interface to some real storage, instead it's a
  25. redirecting interface, that redirects requests to other
  26. "StorageInterface"s.
  27. For example, say you have the paths:
  28. 1. ['user_data', 'cwebber', 'avatar.jpg']
  29. 2. ['user_data', 'elrond', 'avatar.jpg']
  30. 3. ['media_entries', '34352f304c3f4d0ad8ad0f043522b6f2', 'thumb.jpg']
  31. You could mount media_entries under CloudFileStorage and user_data
  32. under BasicFileStorage. Then 1 would be passed to
  33. BasicFileStorage under the path ['cwebber', 'avatar.jpg'] and 3
  34. would be passed to CloudFileStorage under
  35. ['34352f304c3f4d0ad8ad0f043522b6f2', 'thumb.jpg'].
  36. In other words, this is kind of like mounting /home/ and /etc/
  37. under different filesystems on your operating system... but with
  38. mediagoblin filestorages :)
  39. To set this up, you currently need to call the mount() method with
  40. the target path and a backend, that shall be available under that
  41. target path. You have to mount things in a sensible order,
  42. especially you can't mount ["a", "b"] before ["a"].
  43. """
  44. def __init__(self, **kwargs):
  45. self.mounttab = {}
  46. def mount(self, dirpath, backend):
  47. """
  48. Mount a new backend under dirpath
  49. """
  50. new_ent = clean_listy_filepath(dirpath)
  51. print("Mounting:", repr(new_ent))
  52. already, rem_1, table, rem_2 = self._resolve_to_backend(new_ent, True)
  53. print("===", repr(already), repr(rem_1), repr(rem_2), len(table))
  54. assert (len(rem_2) > 0) or (None not in table), \
  55. "That path is already mounted"
  56. assert (len(rem_2) > 0) or (len(table) == 0), \
  57. "A longer path is already mounted here"
  58. for part in rem_2:
  59. table[part] = {}
  60. table = table[part]
  61. table[None] = backend
  62. def _resolve_to_backend(self, filepath, extra_info=False):
  63. """
  64. extra_info = True is for internal use!
  65. Normally, returns the backend and the filepath inside that backend.
  66. With extra_info = True it returns the last directory node and the
  67. remaining filepath from there in addition.
  68. """
  69. table = self.mounttab
  70. filepath = filepath[:]
  71. res_fp = None
  72. while True:
  73. new_be = table.get(None)
  74. if (new_be is not None) or res_fp is None:
  75. res_be = new_be
  76. res_fp = filepath[:]
  77. res_extra = (table, filepath[:])
  78. # print "... New res: %r, %r, %r" % (res_be, res_fp, res_extra)
  79. if len(filepath) == 0:
  80. break
  81. query = filepath.pop(0)
  82. entry = table.get(query)
  83. if entry is not None:
  84. table = entry
  85. res_extra = (table, filepath[:])
  86. else:
  87. break
  88. if extra_info:
  89. return (res_be, res_fp) + res_extra
  90. else:
  91. return (res_be, res_fp)
  92. def resolve_to_backend(self, filepath):
  93. backend, filepath = self._resolve_to_backend(filepath)
  94. if backend is None:
  95. raise MountError("Path not mounted")
  96. return backend, filepath
  97. def __repr__(self, table=None, indent=[]):
  98. res = []
  99. if table is None:
  100. res.append("MountStorage<")
  101. table = self.mounttab
  102. v = table.get(None)
  103. if v:
  104. res.append(" " * len(indent) + repr(indent) + ": " + repr(v))
  105. for k, v in six.iteritems(table):
  106. if k == None:
  107. continue
  108. res.append(" " * len(indent) + repr(k) + ":")
  109. res += self.__repr__(v, indent + [k])
  110. if table is self.mounttab:
  111. res.append(">")
  112. return "\n".join(res)
  113. else:
  114. return res
  115. def file_exists(self, filepath):
  116. backend, filepath = self.resolve_to_backend(filepath)
  117. return backend.file_exists(filepath)
  118. def get_file(self, filepath, mode='r'):
  119. backend, filepath = self.resolve_to_backend(filepath)
  120. return backend.get_file(filepath, mode)
  121. def delete_file(self, filepath):
  122. backend, filepath = self.resolve_to_backend(filepath)
  123. return backend.delete_file(filepath)
  124. def file_url(self, filepath):
  125. backend, filepath = self.resolve_to_backend(filepath)
  126. return backend.file_url(filepath)
  127. def get_local_path(self, filepath):
  128. backend, filepath = self.resolve_to_backend(filepath)
  129. return backend.get_local_path(filepath)
  130. def copy_locally(self, filepath, dest_path):
  131. """
  132. Need to override copy_locally, because the local_storage
  133. attribute is not correct.
  134. """
  135. backend, filepath = self.resolve_to_backend(filepath)
  136. backend.copy_locally(filepath, dest_path)