memory.py 10 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311
  1. from __future__ import annotations
  2. import logging
  3. from datetime import datetime, timezone
  4. from errno import ENOTEMPTY
  5. from io import BytesIO
  6. from pathlib import PurePath, PureWindowsPath
  7. from typing import Any, ClassVar
  8. from fsspec import AbstractFileSystem
  9. from fsspec.implementations.local import LocalFileSystem
  10. from fsspec.utils import stringify_path
  11. logger = logging.getLogger("fsspec.memoryfs")
  12. class MemoryFileSystem(AbstractFileSystem):
  13. """A filesystem based on a dict of BytesIO objects
  14. This is a global filesystem so instances of this class all point to the same
  15. in memory filesystem.
  16. """
  17. store: ClassVar[dict[str, Any]] = {} # global, do not overwrite!
  18. pseudo_dirs = [""] # global, do not overwrite!
  19. protocol = "memory"
  20. root_marker = "/"
  21. @classmethod
  22. def _strip_protocol(cls, path):
  23. if isinstance(path, PurePath):
  24. if isinstance(path, PureWindowsPath):
  25. return LocalFileSystem._strip_protocol(path)
  26. else:
  27. path = stringify_path(path)
  28. path = path.removeprefix("memory://")
  29. if "::" in path or "://" in path:
  30. return path.rstrip("/")
  31. path = path.lstrip("/").rstrip("/")
  32. return "/" + path if path else ""
  33. def ls(self, path, detail=True, **kwargs):
  34. path = self._strip_protocol(path)
  35. if path in self.store:
  36. # there is a key with this exact name
  37. if not detail:
  38. return [path]
  39. return [
  40. {
  41. "name": path,
  42. "size": self.store[path].size,
  43. "type": "file",
  44. "created": self.store[path].created.timestamp(),
  45. }
  46. ]
  47. paths = set()
  48. starter = path + "/"
  49. out = []
  50. for p2 in tuple(self.store):
  51. if p2.startswith(starter):
  52. if "/" not in p2[len(starter) :]:
  53. # exact child
  54. out.append(
  55. {
  56. "name": p2,
  57. "size": self.store[p2].size,
  58. "type": "file",
  59. "created": self.store[p2].created.timestamp(),
  60. }
  61. )
  62. elif len(p2) > len(starter):
  63. # implied child directory
  64. ppath = starter + p2[len(starter) :].split("/", 1)[0]
  65. if ppath not in paths:
  66. out = out or []
  67. out.append(
  68. {
  69. "name": ppath,
  70. "size": 0,
  71. "type": "directory",
  72. }
  73. )
  74. paths.add(ppath)
  75. for p2 in self.pseudo_dirs:
  76. if p2.startswith(starter):
  77. if "/" not in p2[len(starter) :]:
  78. # exact child pdir
  79. if p2 not in paths:
  80. out.append({"name": p2, "size": 0, "type": "directory"})
  81. paths.add(p2)
  82. else:
  83. # directory implied by deeper pdir
  84. ppath = starter + p2[len(starter) :].split("/", 1)[0]
  85. if ppath not in paths:
  86. out.append({"name": ppath, "size": 0, "type": "directory"})
  87. paths.add(ppath)
  88. if not out:
  89. if path in self.pseudo_dirs:
  90. # empty dir
  91. return []
  92. raise FileNotFoundError(path)
  93. if detail:
  94. return out
  95. return sorted([f["name"] for f in out])
  96. def mkdir(self, path, create_parents=True, **kwargs):
  97. path = self._strip_protocol(path)
  98. if path in self.store or path in self.pseudo_dirs:
  99. raise FileExistsError(path)
  100. if self._parent(path).strip("/") and self.isfile(self._parent(path)):
  101. raise NotADirectoryError(self._parent(path))
  102. if create_parents and self._parent(path).strip("/"):
  103. try:
  104. self.mkdir(self._parent(path), create_parents, **kwargs)
  105. except FileExistsError:
  106. pass
  107. if path and path not in self.pseudo_dirs:
  108. self.pseudo_dirs.append(path)
  109. def makedirs(self, path, exist_ok=False):
  110. try:
  111. self.mkdir(path, create_parents=True)
  112. except FileExistsError:
  113. if not exist_ok:
  114. raise
  115. def pipe_file(self, path, value, mode="overwrite", **kwargs):
  116. """Set the bytes of given file
  117. Avoids copies of the data if possible
  118. """
  119. mode = "xb" if mode == "create" else "wb"
  120. self.open(path, mode=mode, data=value)
  121. def rmdir(self, path):
  122. path = self._strip_protocol(path)
  123. if path == "":
  124. # silently avoid deleting FS root
  125. return
  126. if path in self.pseudo_dirs:
  127. if not self.ls(path):
  128. self.pseudo_dirs.remove(path)
  129. else:
  130. raise OSError(ENOTEMPTY, "Directory not empty", path)
  131. else:
  132. raise FileNotFoundError(path)
  133. def info(self, path, **kwargs):
  134. logger.debug("info: %s", path)
  135. path = self._strip_protocol(path)
  136. if path in self.pseudo_dirs or any(
  137. p.startswith(path + "/") for p in list(self.store) + self.pseudo_dirs
  138. ):
  139. return {
  140. "name": path,
  141. "size": 0,
  142. "type": "directory",
  143. }
  144. elif path in self.store:
  145. filelike = self.store[path]
  146. return {
  147. "name": path,
  148. "size": filelike.size,
  149. "type": "file",
  150. "created": getattr(filelike, "created", None),
  151. }
  152. else:
  153. raise FileNotFoundError(path)
  154. def _open(
  155. self,
  156. path,
  157. mode="rb",
  158. block_size=None,
  159. autocommit=True,
  160. cache_options=None,
  161. **kwargs,
  162. ):
  163. path = self._strip_protocol(path)
  164. if "x" in mode and self.exists(path):
  165. raise FileExistsError
  166. if path in self.pseudo_dirs:
  167. raise IsADirectoryError(path)
  168. parent = path
  169. while len(parent) > 1:
  170. parent = self._parent(parent)
  171. if self.isfile(parent):
  172. raise FileExistsError(parent)
  173. if mode in ["rb", "ab", "r+b", "a+b"]:
  174. if path in self.store:
  175. f = self.store[path]
  176. if "a" in mode:
  177. # position at the end of file
  178. f.seek(0, 2)
  179. else:
  180. # position at the beginning of file
  181. f.seek(0)
  182. return f
  183. else:
  184. raise FileNotFoundError(path)
  185. elif mode in {"wb", "w+b", "xb", "x+b"}:
  186. if "x" in mode and self.exists(path):
  187. raise FileExistsError
  188. m = MemoryFile(self, path, kwargs.get("data"))
  189. if not self._intrans:
  190. m.commit()
  191. return m
  192. else:
  193. name = self.__class__.__name__
  194. raise ValueError(f"unsupported file mode for {name}: {mode!r}")
  195. def cp_file(self, path1, path2, **kwargs):
  196. path1 = self._strip_protocol(path1)
  197. path2 = self._strip_protocol(path2)
  198. if self.isfile(path1):
  199. self.store[path2] = MemoryFile(
  200. self, path2, self.store[path1].getvalue()
  201. ) # implicit copy
  202. elif self.isdir(path1):
  203. if path2 not in self.pseudo_dirs:
  204. self.pseudo_dirs.append(path2)
  205. else:
  206. raise FileNotFoundError(path1)
  207. def cat_file(self, path, start=None, end=None, **kwargs):
  208. logger.debug("cat: %s", path)
  209. path = self._strip_protocol(path)
  210. try:
  211. return bytes(self.store[path].getbuffer()[start:end])
  212. except KeyError as e:
  213. raise FileNotFoundError(path) from e
  214. def _rm(self, path):
  215. path = self._strip_protocol(path)
  216. try:
  217. del self.store[path]
  218. except KeyError as e:
  219. raise FileNotFoundError(path) from e
  220. def modified(self, path):
  221. path = self._strip_protocol(path)
  222. try:
  223. return self.store[path].modified
  224. except KeyError as e:
  225. raise FileNotFoundError(path) from e
  226. def created(self, path):
  227. path = self._strip_protocol(path)
  228. try:
  229. return self.store[path].created
  230. except KeyError as e:
  231. raise FileNotFoundError(path) from e
  232. def isfile(self, path):
  233. path = self._strip_protocol(path)
  234. return path in self.store
  235. def rm(self, path, recursive=False, maxdepth=None):
  236. if isinstance(path, str):
  237. path = self._strip_protocol(path)
  238. else:
  239. path = [self._strip_protocol(p) for p in path]
  240. paths = self.expand_path(path, recursive=recursive, maxdepth=maxdepth)
  241. for p in reversed(paths):
  242. if self.isfile(p):
  243. self.rm_file(p)
  244. # If the expanded path doesn't exist, it is only because the expanded
  245. # path was a directory that does not exist in self.pseudo_dirs. This
  246. # is possible if you directly create files without making the
  247. # directories first.
  248. elif not self.exists(p):
  249. continue
  250. else:
  251. self.rmdir(p)
  252. class MemoryFile(BytesIO):
  253. """A BytesIO which can't close and works as a context manager
  254. Can initialise with data. Each path should only be active once at any moment.
  255. No need to provide fs, path if auto-committing (default)
  256. """
  257. def __init__(self, fs=None, path=None, data=None):
  258. logger.debug("open file %s", path)
  259. self.fs = fs
  260. self.path = path
  261. self.created = datetime.now(tz=timezone.utc)
  262. self.modified = datetime.now(tz=timezone.utc)
  263. if data:
  264. super().__init__(data)
  265. self.seek(0)
  266. @property
  267. def size(self):
  268. return self.getbuffer().nbytes
  269. def __enter__(self):
  270. return self
  271. def close(self):
  272. pass
  273. def discard(self):
  274. pass
  275. def commit(self):
  276. self.fs.store[self.path] = self
  277. self.modified = datetime.now(tz=timezone.utc)