├── .gitignore ├── LICENSE ├── README.md ├── passthrough.py └── requirements.txt /.gitignore: -------------------------------------------------------------------------------- 1 | # Byte-compiled / optimized / DLL files 2 | __pycache__/ 3 | *.py[cod] 4 | *$py.class 5 | 6 | # C extensions 7 | *.so 8 | 9 | # Distribution / packaging 10 | .Python 11 | env/ 12 | build/ 13 | develop-eggs/ 14 | dist/ 15 | downloads/ 16 | eggs/ 17 | .eggs/ 18 | lib/ 19 | lib64/ 20 | parts/ 21 | sdist/ 22 | var/ 23 | *.egg-info/ 24 | .installed.cfg 25 | *.egg 26 | 27 | # PyInstaller 28 | # Usually these files are written by a python script from a template 29 | # before PyInstaller builds the exe, so as to inject date/other infos into it. 30 | *.manifest 31 | *.spec 32 | 33 | # Installer logs 34 | pip-log.txt 35 | pip-delete-this-directory.txt 36 | 37 | # Unit test / coverage reports 38 | htmlcov/ 39 | .tox/ 40 | .coverage 41 | .coverage.* 42 | .cache 43 | nosetests.xml 44 | coverage.xml 45 | *,cover 46 | .hypothesis/ 47 | 48 | # Translations 49 | *.mo 50 | *.pot 51 | 52 | # Django stuff: 53 | *.log 54 | 55 | # Sphinx documentation 56 | docs/_build/ 57 | 58 | # PyBuilder 59 | target/ 60 | 61 | #Ipython Notebook 62 | .ipynb_checkpoints 63 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2016, Stavros Korokithakis 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 15 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 16 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 17 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 18 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 19 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 20 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 21 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 22 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 23 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 24 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # python-fuse-sample 2 | 3 | This repo contains a very simple FUSE filesystem example in Python. It's the 4 | code from a post I wrote a while back: 5 | 6 | https://www.stavros.io/posts/python-fuse-filesystem/ 7 | 8 | If you see anything needing improvement or have any feedback, please open an 9 | issue. 10 | -------------------------------------------------------------------------------- /passthrough.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | 3 | from __future__ import with_statement 4 | 5 | import os 6 | import sys 7 | import errno 8 | 9 | from fuse import FUSE, FuseOSError, Operations, fuse_get_context 10 | 11 | 12 | class Passthrough(Operations): 13 | def __init__(self, root): 14 | self.root = root 15 | 16 | # Helpers 17 | # ======= 18 | 19 | def _full_path(self, partial): 20 | if partial.startswith("/"): 21 | partial = partial[1:] 22 | path = os.path.join(self.root, partial) 23 | return path 24 | 25 | # Filesystem methods 26 | # ================== 27 | 28 | def access(self, path, mode): 29 | full_path = self._full_path(path) 30 | if not os.access(full_path, mode): 31 | raise FuseOSError(errno.EACCES) 32 | 33 | def chmod(self, path, mode): 34 | full_path = self._full_path(path) 35 | return os.chmod(full_path, mode) 36 | 37 | def chown(self, path, uid, gid): 38 | full_path = self._full_path(path) 39 | return os.chown(full_path, uid, gid) 40 | 41 | def getattr(self, path, fh=None): 42 | full_path = self._full_path(path) 43 | st = os.lstat(full_path) 44 | return dict((key, getattr(st, key)) for key in ('st_atime', 'st_ctime', 45 | 'st_gid', 'st_mode', 'st_mtime', 'st_nlink', 'st_size', 'st_uid')) 46 | 47 | def readdir(self, path, fh): 48 | full_path = self._full_path(path) 49 | 50 | dirents = ['.', '..'] 51 | if os.path.isdir(full_path): 52 | dirents.extend(os.listdir(full_path)) 53 | for r in dirents: 54 | yield r 55 | 56 | def readlink(self, path): 57 | pathname = os.readlink(self._full_path(path)) 58 | if pathname.startswith("/"): 59 | # Path name is absolute, sanitize it. 60 | return os.path.relpath(pathname, self.root) 61 | else: 62 | return pathname 63 | 64 | def mknod(self, path, mode, dev): 65 | return os.mknod(self._full_path(path), mode, dev) 66 | 67 | def rmdir(self, path): 68 | full_path = self._full_path(path) 69 | return os.rmdir(full_path) 70 | 71 | def mkdir(self, path, mode): 72 | return os.mkdir(self._full_path(path), mode) 73 | 74 | def statfs(self, path): 75 | full_path = self._full_path(path) 76 | stv = os.statvfs(full_path) 77 | return dict((key, getattr(stv, key)) for key in ('f_bavail', 'f_bfree', 78 | 'f_blocks', 'f_bsize', 'f_favail', 'f_ffree', 'f_files', 'f_flag', 79 | 'f_frsize', 'f_namemax')) 80 | 81 | def unlink(self, path): 82 | return os.unlink(self._full_path(path)) 83 | 84 | def symlink(self, name, target): 85 | return os.symlink(target, self._full_path(name)) 86 | 87 | def rename(self, old, new): 88 | return os.rename(self._full_path(old), self._full_path(new)) 89 | 90 | def link(self, target, name): 91 | return os.link(self._full_path(name), self._full_path(target)) 92 | 93 | def utimens(self, path, times=None): 94 | return os.utime(self._full_path(path), times) 95 | 96 | # File methods 97 | # ============ 98 | 99 | def open(self, path, flags): 100 | full_path = self._full_path(path) 101 | return os.open(full_path, flags) 102 | 103 | def create(self, path, mode, fi=None): 104 | uid, gid, pid = fuse_get_context() 105 | full_path = self._full_path(path) 106 | fd = os.open(full_path, os.O_WRONLY | os.O_CREAT, mode) 107 | os.chown(full_path,uid,gid) #chown to context uid & gid 108 | return fd 109 | 110 | def read(self, path, length, offset, fh): 111 | os.lseek(fh, offset, os.SEEK_SET) 112 | return os.read(fh, length) 113 | 114 | def write(self, path, buf, offset, fh): 115 | os.lseek(fh, offset, os.SEEK_SET) 116 | return os.write(fh, buf) 117 | 118 | def truncate(self, path, length, fh=None): 119 | full_path = self._full_path(path) 120 | with open(full_path, 'r+') as f: 121 | f.truncate(length) 122 | 123 | def flush(self, path, fh): 124 | return os.fsync(fh) 125 | 126 | def release(self, path, fh): 127 | return os.close(fh) 128 | 129 | def fsync(self, path, fdatasync, fh): 130 | return self.flush(path, fh) 131 | 132 | 133 | def main(mountpoint, root): 134 | FUSE(Passthrough(root), mountpoint, nothreads=True, foreground=True, allow_other=True) 135 | 136 | 137 | if __name__ == '__main__': 138 | main(sys.argv[2], sys.argv[1]) 139 | -------------------------------------------------------------------------------- /requirements.txt: -------------------------------------------------------------------------------- 1 | fusepy 2 | --------------------------------------------------------------------------------