def copy(self, src, dst, overwrite=False, chunk_size=1024*64): """An optimized copy() that will skip mounting an archive if one is involved as either the src or dst. This allows the file containing the archive to be copied.""" src_is_archive = libarchive.is_archive_name(src) # If src path is a mounted archive, unmount it. if src_is_archive and self.ismount(src): self.unmount(src) # Now delegate the path, if the path is an archive, don't remount it. srcfs, _ignored, src = self._delegate(src, auto_mount=(not src_is_archive)) # Follow the same steps for dst. dst_is_archive = libarchive.is_archive_name(dst) if dst_is_archive and self.ismount(dst): self.unmount(dst) dstfs, _ignored, dst = self._delegate(dst, auto_mount=(not dst_is_archive)) # srcfs, src and dstfs, dst are now the file system and path for our src and dst. if srcfs is dstfs and srcfs is not self: # Both src and dst are on the same fs, let it do the copy. srcfs.copy(src, dst, overwrite=overwrite, chunk_size=chunk_size) else: # Src and dst are on different file systems. Just do the copy... srcfd = None try: srcfd = srcfs.open(src, 'rb') dstfs.setcontents(dst, srcfd, chunk_size=chunk_size) except ResourceNotFoundError: if srcfs.exists(src) and not dstfs.exists(dirname(dst)): raise ParentDirectoryMissingError(dst) finally: if srcfd: srcfd.close()
def test_formats(self): self.assertEqual(is_archive_name('foo'), None) self.assertEqual(is_archive_name('foo.txt'), None) self.assertEqual(is_archive_name('foo.txt.gz'), None) self.assertEqual(is_archive_name('foo.tar.gz'), 'tar') self.assertEqual(is_archive_name('foo.tar.bz2'), 'tar') self.assertEqual(is_archive_name('foo.zip'), 'zip') self.assertEqual(is_archive_name('foo.rar'), 'rar') self.assertEqual(is_archive_name('foo.iso'), 'iso') self.assertEqual(is_archive_name('foo.rpm'), 'cpio')
def remove(self, path, **kwargs): """A remove() override that deletes an archive directly. It is not fooled by a mounted archive. If the path is not an archive, the call is delegated.""" if libarchive.is_archive_name(path) and self.ismount(path): self.unmount(path) fs, _mount_path, delegate_path = self._delegate(path, auto_mount=False) return fs.remove(delegate_path)
def open(self, path, *args, **kwargs): """An open() override that opens an archive. It is not fooled by mounted archives. If the path is a mounted archive, it is unmounted and the archive file is opened and returned.""" if libarchive.is_archive_name(path) and self.ismount(path): self.unmount(path) fs, _mount_path, delegate_path = self._delegate(path, auto_mount=False) return fs.open(delegate_path, *args, **kwargs)
def rename(self, src, dst): """An rename() implementation that ensures the rename does not span file systems. It also ensures that an archive can be renamed (without trying to mount either the src or destination paths).""" src_is_archive = libarchive.is_archive_name(src) # If src path is a mounted archive, unmount it. if src_is_archive and self.ismount(src): self.unmount(src) # Now delegate the path, if the path is an archive, don't remount it. srcfs, _ignored, src = self._delegate(src, auto_mount=(not src_is_archive)) # Follow the same steps for dst. dst_is_archive = libarchive.is_archive_name(dst) if dst_is_archive and self.ismount(dst): self.unmount(dst) dstfs, _ignored, dst = self._delegate(dst, auto_mount=(not dst_is_archive)) # srcfs, src and dstfs, dst are now the file system and path for our src and dst. if srcfs is dstfs and srcfs is not self: # Both src and dst are on the same fs, let it do the copy. return srcfs.rename(src, dst) raise OperationFailedError("rename resource", path=src)
def remove(self, path): # In case one of our mounted file systems backing archive is being # deleted, unmout it before continuing. Once unmounted, the archive # can be deleted by root fs, otherwise, the ArchiveFS will be asked # to remove itself, which it cannot do. if self.ismount(path) and libarchive.is_archive_name(path): self.unmount(path) # Send the delete directoy to the root filesystem. This avoids # being delegated, and the fs we just unmounted being remounted. return self.rootfs.remove(path) # Otherwise, just delegate to the responsible fs. return super(ArchiveMountFS, self).remove(path)
def _delegate(self, path): for ppath in recursepath(path)[1:]: # Don't mount again... if self.ismount(ppath): break if libarchive.is_archive_name(ppath): # It looks like an archive, try mounting it. full_path = self.mount_tree['/'].fs.getsyspath(ppath) try: self.mountdir(ppath, ArchiveFS(full_path, 'r')) except: pass # Must NOT have been an archive after all # Stop recursing path, we support just one archive per path! # No nested archives yet! break return super(ArchiveMountFS, self)._delegate(path)
def _delegate(self, path, auto_mount=True): """A _delegate() override that will automatically mount archives that are encountered in the path. For example, the path /foo/bar.zip/baz.txt contains the archive path /foo/bar.zip. If this archive can be mounted by ArchiveFS, it will be. Then the file system call will be delegated to that mounted file system, which will act upon /baz.txt within the archive. This is lazy initialization which means users of this class need not crawl the file system for archives and mount them all up-front. This behavior can be overridden by self.auto_mount=False or by passing the auto_mount=False keyword argument. """ if self.auto_mount and auto_mount: for ppath in recursepath(path)[1:]: if self.ismount(ppath): # If something is already mounted here, no need to continue. break if libarchive.is_archive_name(ppath): # It looks like an archive, we might mount it. # First check that the size is acceptable. if self.max_size: if self.rootfs.exists(ppath) and \ self.rootfs.getsize(ppath) > self.max_size: break # Looks good, the proof is in the pudding, so let's try to # mount this *supposed* archive... full_path = self.rootfs.getsyspath(ppath) try: # TODO: it would be really nice if we could open the path using # self.rootfs.open(), that way we could support archives on a file # system other than osfs (even nested archives). However, the libarchive # wrapper is not sophisticated enough to handle a Python file-like object, # it uses an actual fd. self.mountdir(ppath, ArchiveFS(full_path, 'r')) # That worked!! Stop recursing path, we support just one archive per path! break except: # Must NOT have been an archive after all, but maybe # there is one deeper in the directory... continue return super(ArchiveMountFS, self)._delegate(path)
def isarchive(path): try: return libarchive.is_archive(path) except: return libarchive.is_archive_name(path)