1""" 2fs.mountfs 3========== 4 5Contains MountFS class which is a virtual filesystem which can have other filesystems linked as branched directories. 6 7For example, lets say we have two filesystems containing config files and resources respectively:: 8 9 [config_fs] 10 |-- config.cfg 11 `-- defaults.cfg 12 13 [resources_fs] 14 |-- images 15 | |-- logo.jpg 16 | `-- photo.jpg 17 `-- data.dat 18 19We can combine these filesystems in to a single filesystem with the following code:: 20 21 from fs.mountfs import MountFS 22 combined_fs = MountFS() 23 combined_fs.mountdir('config', config_fs) 24 combined_fs.mountdir('resources', resources_fs) 25 26This will create a single filesystem where paths under `config` map to `config_fs`, and paths under `resources` map to `resources_fs`:: 27 28 [combined_fs] 29 |-- config 30 | |-- config.cfg 31 | `-- defaults.cfg 32 `-- resources 33 |-- images 34 | |-- logo.jpg 35 | `-- photo.jpg 36 `-- data.dat 37 38Now both filesystems can be accessed with the same path structure:: 39 40 print combined_fs.getcontents('/config/defaults.cfg') 41 read_jpg(combined_fs.open('/resources/images/logo.jpg') 42 43""" 44 45from fs.base import * 46from fs.errors import * 47from fs.path import * 48from fs import _thread_synchronize_default 49from fs import iotools 50 51 52class DirMount(object): 53 def __init__(self, path, fs): 54 self.path = path 55 self.fs = fs 56 57 def __str__(self): 58 return "<DirMount %s, %s>" % (self.path, self.fs) 59 60 def __repr__(self): 61 return "<DirMount %s, %s>" % (self.path, self.fs) 62 63 def __unicode__(self): 64 return u"<DirMount %s, %s>" % (self.path, self.fs) 65 66 67class FileMount(object): 68 def __init__(self, path, open_callable, info_callable=None): 69 self.open_callable = open_callable 70 def no_info_callable(path): 71 return {} 72 self.info_callable = info_callable or no_info_callable 73 74 75class MountFS(FS): 76 """A filesystem that delegates to other filesystems.""" 77 78 _meta = { 'virtual': True, 79 'read_only' : False, 80 'unicode_paths' : True, 81 'case_insensitive_paths' : False, 82 } 83 84 DirMount = DirMount 85 FileMount = FileMount 86 87 def __init__(self, auto_close=True, thread_synchronize=_thread_synchronize_default): 88 self.auto_close = auto_close 89 super(MountFS, self).__init__(thread_synchronize=thread_synchronize) 90 self.mount_tree = PathMap() 91 92 def __str__(self): 93 return "<%s [%s]>" % (self.__class__.__name__,self.mount_tree.items(),) 94 95 __repr__ = __str__ 96 97 def __unicode__(self): 98 return u"<%s [%s]>" % (self.__class__.__name__,self.mount_tree.items(),) 99 100 def _delegate(self, path): 101 path = abspath(normpath(path)) 102 object = None 103 head_path = "/" 104 tail_path = path 105 106 for prefix in recursepath(path): 107 try: 108 object = self.mount_tree[prefix] 109 except KeyError: 110 pass 111 else: 112 head_path = prefix 113 tail_path = path[len(head_path):] 114 115 if type(object) is MountFS.DirMount: 116 return object.fs, head_path, tail_path 117 118 if type(object) is MountFS.FileMount: 119 return self, "/", path 120 121 try: 122 self.mount_tree.iternames(path).next() 123 except StopIteration: 124 return None, None, None 125 else: 126 return self, "/", path 127 128 @synchronize 129 def close(self): 130 # Explicitly closes children if requested 131 if self.auto_close: 132 for mount in self.mount_tree.itervalues(): 133 mount.fs.close() 134 # Free references (which may incidently call the close method of the child filesystems) 135 self.mount_tree.clear() 136 super(MountFS, self).close() 137 138 def getsyspath(self, path, allow_none=False): 139 fs, _mount_path, delegate_path = self._delegate(path) 140 if fs is self or fs is None: 141 if allow_none: 142 return None 143 else: 144 raise NoSysPathError(path=path) 145 return fs.getsyspath(delegate_path, allow_none=allow_none) 146 147 def getpathurl(self, path, allow_none=False): 148 fs, _mount_path, delegate_path = self._delegate(path) 149 if fs is self or fs is None: 150 if allow_none: 151 return None 152 else: 153 raise NoPathURLError(path=path) 154 return fs.getpathurl(delegate_path, allow_none=allow_none) 155 156 @synchronize 157 def desc(self, path): 158 fs, _mount_path, delegate_path = self._delegate(path) 159 if fs is self: 160 if fs.isdir(path): 161 return "Mount dir" 162 else: 163 return "Mounted file" 164 return "Mounted dir, maps to path %s on %s" % (abspath(delegate_path) or '/', str(fs)) 165 166 @synchronize 167 def isdir(self, path): 168 fs, _mount_path, delegate_path = self._delegate(path) 169 if fs is None: 170 path = normpath(path) 171 if path in ("/", ""): 172 return True 173 return False 174 if fs is self: 175 obj = self.mount_tree.get(path, None) 176 return not isinstance(obj, MountFS.FileMount) 177 return fs.isdir(delegate_path) 178 179 @synchronize 180 def isfile(self, path): 181 fs, _mount_path, delegate_path = self._delegate(path) 182 if fs is None: 183 return False 184 if fs is self: 185 obj = self.mount_tree.get(path, None) 186 return isinstance(obj, MountFS.FileMount) 187 return fs.isfile(delegate_path) 188 189 @synchronize 190 def exists(self, path): 191 if path in ("/", ""): 192 return True 193 fs, _mount_path, delegate_path = self._delegate(path) 194 if fs is None: 195 return False 196 if fs is self: 197 return True 198 return fs.exists(delegate_path) 199 200 @synchronize 201 def listdir(self, path="/", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False): 202 fs, _mount_path, delegate_path = self._delegate(path) 203 204 if fs is None: 205 if path in ("/", ""): 206 return [] 207 raise ResourceNotFoundError("path") 208 209 elif fs is self: 210 paths = self.mount_tree.names(path) 211 return self._listdir_helper(path, 212 paths, 213 wildcard, 214 full, 215 absolute, 216 dirs_only, 217 files_only) 218 else: 219 paths = fs.listdir(delegate_path, 220 wildcard=wildcard, 221 full=False, 222 absolute=False, 223 dirs_only=dirs_only, 224 files_only=files_only) 225 for nm in self.mount_tree.names(path): 226 if nm not in paths: 227 if dirs_only: 228 if self.isdir(pathjoin(path,nm)): 229 paths.append(nm) 230 elif files_only: 231 if self.isfile(pathjoin(path,nm)): 232 paths.append(nm) 233 else: 234 paths.append(nm) 235 if full or absolute: 236 if full: 237 path = relpath(normpath(path)) 238 else: 239 path = abspath(normpath(path)) 240 paths = [pathjoin(path, p) for p in paths] 241 242 return paths 243 244 @synchronize 245 def ilistdir(self, path="/", wildcard=None, full=False, absolute=False, dirs_only=False, files_only=False): 246 fs, _mount_path, delegate_path = self._delegate(path) 247 248 if fs is None: 249 if path in ("/", ""): 250 return 251 raise ResourceNotFoundError(path) 252 253 if fs is self: 254 paths = self.mount_tree.names(path) 255 for path in self._listdir_helper(path,paths,wildcard,full,absolute,dirs_only,files_only): 256 yield path 257 else: 258 paths = fs.ilistdir(delegate_path, 259 wildcard=wildcard, 260 full=False, 261 absolute=False, 262 dirs_only=dirs_only) 263 extra_paths = set(self.mount_tree.names(path)) 264 if full: 265 pathhead = relpath(normpath(path)) 266 def mkpath(p): 267 return pathjoin(pathhead,p) 268 elif absolute: 269 pathhead = abspath(normpath(path)) 270 def mkpath(p): 271 return pathjoin(pathhead,p) 272 else: 273 def mkpath(p): 274 return p 275 for p in paths: 276 if p not in extra_paths: 277 yield mkpath(p) 278 for p in extra_paths: 279 if dirs_only: 280 if self.isdir(pathjoin(path,p)): 281 yield mkpath(p) 282 elif files_only: 283 if self.isfile(pathjoin(path,p)): 284 yield mkpath(p) 285 else: 286 yield mkpath(p) 287 288 @synchronize 289 def makedir(self, path, recursive=False, allow_recreate=False): 290 fs, _mount_path, delegate_path = self._delegate(path) 291 if fs is self or fs is None: 292 raise UnsupportedError("make directory", msg="Can only makedir for mounted paths") 293 if not delegate_path: 294 if allow_recreate: 295 return 296 else: 297 raise DestinationExistsError(path, msg="Can not create a directory that already exists (try allow_recreate=True): %(path)s") 298 return fs.makedir(delegate_path, recursive=recursive, allow_recreate=allow_recreate) 299 300 @synchronize 301 def open(self, path, mode='r', buffering=-1, encoding=None, errors=None, newline=None, line_buffering=False, **kwargs): 302 obj = self.mount_tree.get(path, None) 303 if type(obj) is MountFS.FileMount: 304 callable = obj.open_callable 305 return callable(path, mode, **kwargs) 306 307 fs, _mount_path, delegate_path = self._delegate(path) 308 309 if fs is self or fs is None: 310 raise ResourceNotFoundError(path) 311 312 return fs.open(delegate_path, mode, **kwargs) 313 314 @synchronize 315 def setcontents(self, path, data=b'', encoding=None, errors=None, chunk_size=64*1024): 316 obj = self.mount_tree.get(path, None) 317 if type(obj) is MountFS.FileMount: 318 return super(MountFS, self).setcontents(path, 319 data, 320 encoding=encoding, 321 errors=errors, 322 chunk_size=chunk_size) 323 fs, _mount_path, delegate_path = self._delegate(path) 324 if fs is self or fs is None: 325 raise ParentDirectoryMissingError(path) 326 return fs.setcontents(delegate_path, data, encoding=encoding, errors=errors, chunk_size=chunk_size) 327 328 @synchronize 329 def createfile(self, path, wipe=False): 330 obj = self.mount_tree.get(path, None) 331 if type(obj) is MountFS.FileMount: 332 return super(MountFS, self).createfile(path, wipe=wipe) 333 fs, _mount_path, delegate_path = self._delegate(path) 334 if fs is self or fs is None: 335 raise ParentDirectoryMissingError(path) 336 return fs.createfile(delegate_path, wipe=wipe) 337 338 @synchronize 339 def remove(self, path): 340 fs, _mount_path, delegate_path = self._delegate(path) 341 if fs is self or fs is None: 342 raise UnsupportedError("remove file", msg="Can only remove paths within a mounted dir") 343 return fs.remove(delegate_path) 344 345 @synchronize 346 def removedir(self, path, recursive=False, force=False): 347 path = normpath(path) 348 if path in ('', '/'): 349 raise RemoveRootError(path) 350 fs, _mount_path, delegate_path = self._delegate(path) 351 if fs is self or fs is None: 352 raise ResourceInvalidError(path, msg="Can not removedir for an un-mounted path") 353 return fs.removedir(delegate_path, recursive, force) 354 355 @synchronize 356 def rename(self, src, dst): 357 fs1, _mount_path1, delegate_path1 = self._delegate(src) 358 fs2, _mount_path2, delegate_path2 = self._delegate(dst) 359 360 if fs1 is not fs2: 361 raise OperationFailedError("rename resource", path=src) 362 363 if fs1 is not self: 364 return fs1.rename(delegate_path1, delegate_path2) 365 366 object = self.mount_tree.get(src, None) 367 _object2 = self.mount_tree.get(dst, None) 368 369 if object is None: 370 raise ResourceNotFoundError(src) 371 372 raise UnsupportedError("rename resource", path=src) 373 374 @synchronize 375 def move(self,src,dst,**kwds): 376 fs1, _mount_path1, delegate_path1 = self._delegate(src) 377 fs2, _mount_path2, delegate_path2 = self._delegate(dst) 378 if fs1 is fs2 and fs1 is not self: 379 fs1.move(delegate_path1,delegate_path2,**kwds) 380 else: 381 super(MountFS,self).move(src,dst,**kwds) 382 383 @synchronize 384 def movedir(self,src,dst,**kwds): 385 fs1, _mount_path1, delegate_path1 = self._delegate(src) 386 fs2, _mount_path2, delegate_path2 = self._delegate(dst) 387 if fs1 is fs2 and fs1 is not self: 388 fs1.movedir(delegate_path1,delegate_path2,**kwds) 389 else: 390 super(MountFS,self).movedir(src,dst,**kwds) 391 392 @synchronize 393 def copy(self,src,dst,**kwds): 394 fs1, _mount_path1, delegate_path1 = self._delegate(src) 395 fs2, _mount_path2, delegate_path2 = self._delegate(dst) 396 if fs1 is fs2 and fs1 is not self: 397 fs1.copy(delegate_path1,delegate_path2,**kwds) 398 else: 399 super(MountFS,self).copy(src,dst,**kwds) 400 401 @synchronize 402 def copydir(self,src,dst,**kwds): 403 fs1, _mount_path1, delegate_path1 = self._delegate(src) 404 fs2, _mount_path2, delegate_path2 = self._delegate(dst) 405 if fs1 is fs2 and fs1 is not self: 406 fs1.copydir(delegate_path1,delegate_path2,**kwds) 407 else: 408 super(MountFS,self).copydir(src,dst,**kwds) 409 410 @synchronize 411 def mountdir(self, path, fs): 412 """Mounts a host FS object on a given path. 413 414 :param path: A path within the MountFS 415 :param fs: A filesystem object to mount 416 417 """ 418 path = abspath(normpath(path)) 419 self.mount_tree[path] = MountFS.DirMount(path, fs) 420 mount = mountdir 421 422 @synchronize 423 def mountfile(self, path, open_callable=None, info_callable=None): 424 """Mounts a single file path. 425 426 :param path: A path within the MountFS 427 :param open_callable: A callable that returns a file-like object, 428 `open_callable` should have the same signature as :py:meth:`~fs.base.FS.open` 429 :param info_callable: A callable that returns a dictionary with information regarding the file-like object, 430 `info_callable` should have the same signagture as :py:meth:`~fs.base.FS.getinfo` 431 432 """ 433 self.mount_tree[path] = MountFS.FileMount(path, open_callable, info_callable) 434 435 @synchronize 436 def unmount(self, path): 437 """Unmounts a path. 438 439 :param path: Path to unmount 440 :return: True if a path was unmounted, False if the path was already unmounted 441 :rtype: bool 442 443 """ 444 try: 445 del self.mount_tree[path] 446 except KeyError: 447 return False 448 else: 449 return True 450 451 @synchronize 452 def settimes(self, path, accessed_time=None, modified_time=None): 453 path = normpath(path) 454 fs, _mount_path, delegate_path = self._delegate(path) 455 456 if fs is None: 457 raise ResourceNotFoundError(path) 458 459 if fs is self: 460 raise UnsupportedError("settimes") 461 fs.settimes(delegate_path, accessed_time, modified_time) 462 463 @synchronize 464 def getinfo(self, path): 465 path = normpath(path) 466 467 fs, _mount_path, delegate_path = self._delegate(path) 468 469 if fs is None: 470 if path in ("/", ""): 471 return {} 472 raise ResourceNotFoundError(path) 473 474 if fs is self: 475 if self.isfile(path): 476 return self.mount_tree[path].info_callable(path) 477 return {} 478 return fs.getinfo(delegate_path) 479 480 @synchronize 481 def getsize(self, path): 482 path = normpath(path) 483 fs, _mount_path, delegate_path = self._delegate(path) 484 485 if fs is None: 486 raise ResourceNotFoundError(path) 487 488 if fs is self: 489 object = self.mount_tree.get(path, None) 490 491 if object is None: 492 raise ResourceNotFoundError(path) 493 if not isinstance(object,MountFS.FileMount): 494 raise ResourceInvalidError(path) 495 496 size = object.info_callable(path).get("size", None) 497 return size 498 499 return fs.getinfo(delegate_path).get("size", None) 500 501 @synchronize 502 def getxattr(self,path,name,default=None): 503 path = normpath(path) 504 fs, _mount_path, delegate_path = self._delegate(path) 505 if fs is None: 506 if path in ("/", ""): 507 return default 508 raise ResourceNotFoundError(path) 509 if fs is self: 510 return default 511 return fs.getxattr(delegate_path,name,default) 512 513 @synchronize 514 def setxattr(self,path,name,value): 515 path = normpath(path) 516 fs, _mount_path, delegate_path = self._delegate(path) 517 if fs is None: 518 raise ResourceNotFoundError(path) 519 if fs is self: 520 raise UnsupportedError("setxattr") 521 return fs.setxattr(delegate_path,name,value) 522 523 @synchronize 524 def delxattr(self,path,name): 525 path = normpath(path) 526 fs, _mount_path, delegate_path = self._delegate(path) 527 if fs is None: 528 raise ResourceNotFoundError(path) 529 if fs is self: 530 return True 531 return fs.delxattr(delegate_path, name) 532 533 @synchronize 534 def listxattrs(self,path): 535 path = normpath(path) 536 fs, _mount_path, delegate_path = self._delegate(path) 537 if fs is None: 538 raise ResourceNotFoundError(path) 539 if fs is self: 540 return [] 541 return fs.listxattrs(delegate_path) 542 543 544