{"id":13442318,"url":"https://github.com/libfuse/python-fuse","last_synced_at":"2025-04-14T22:12:09.587Z","repository":{"id":3049351,"uuid":"48296121","full_name":"libfuse/python-fuse","owner":"libfuse","description":"Python 2.x/3.x bindings for libfuse 2.x","archived":false,"fork":false,"pushed_at":"2025-04-03T06:49:55.000Z","size":250,"stargazers_count":304,"open_issues_count":18,"forks_count":76,"subscribers_count":17,"default_branch":"master","last_synced_at":"2025-04-07T19:06:52.958Z","etag":null,"topics":["filesystem","fuse","python"],"latest_commit_sha":null,"homepage":"","language":"Python","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"lgpl-2.1","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/libfuse.png","metadata":{"files":{"readme":"README.cups.md","changelog":null,"contributing":null,"funding":null,"license":"COPYING","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":"AUTHORS","dei":null,"publiccode":null,"codemeta":null}},"created_at":"2015-12-19T20:25:15.000Z","updated_at":"2025-04-06T22:04:39.000Z","dependencies_parsed_at":"2024-01-16T09:47:37.175Z","dependency_job_id":"a1aa692c-79cc-4457-b518-e1fd071be5ec","html_url":"https://github.com/libfuse/python-fuse","commit_stats":{"total_commits":170,"total_committers":16,"mean_commits":10.625,"dds":"0.41764705882352937","last_synced_commit":"8dcf5a7de962553543abe68066ce8de774081cb8"},"previous_names":[],"tags_count":18,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/libfuse%2Fpython-fuse","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/libfuse%2Fpython-fuse/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/libfuse%2Fpython-fuse/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/libfuse%2Fpython-fuse/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/libfuse","download_url":"https://codeload.github.com/libfuse/python-fuse/tar.gz/refs/heads/master","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":248968916,"owners_count":21191162,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["filesystem","fuse","python"],"created_at":"2024-07-31T03:01:44.280Z","updated_at":"2025-04-14T22:12:09.560Z","avatar_url":"https://github.com/libfuse.png","language":"Python","funding_links":[],"categories":["HarmonyOS","Python"],"sub_categories":["Windows Manager"],"readme":"## Simple FUSE Filesystem HOWTO in Python\n\nI've written a few filesystems (k8055fs, ltspfs, etc) using FUSE, and\nsomething I keep seeing on the lists is a request for a tutorial from\npeople new to FUSE. Here's my stab at one.\n\n### The Problem\n\nWhere I work, we access some government functions through a web based\nJava 3270 emulator. Recently, the government updated the Java client,\nand broke printing, so my users couldn't print out screenshots of\ninformation that they needed. Rather than trying to argue with the\ngovernment that they should support my GNU/Linux clients, which would\nhave gone nowhere, I started trying to come up with an alternative\nsolution. It seemed that \"printing to a text file\" still worked fine\nwithin the client. So, all we need to do is just find a way to have\ntext files automatically spit out on a printer as soon as they're\nwritten. We've got a thin-client environment, so multiple people are\nlogged onto a server, and there are several printers defined on a\nserver.\n\n### The Design\n\nWe needed the following:\n\n  * To print to any of the printers defined on the server.\n  * To have multiple people print to the same printer on the same server\n  * Screen prints are small, typically less than 2k of text\n  * I needed to get it going quickly\n\nSo, it seemed the following would make sense:\n\n  * The filesystem, when mounted, would have a directory for each of the printers on the system\n  * Under each of the \"printer directories\", any file could be created.\n  * This file, when closed, would be sent to the printer whose directory it was in.\n\nSeems simple enough. I've done this in Python, since it's fast to get\nsomething going in, and has good FUSE bindings.\n\nSo, how will we manage this within Python? Well, having three\ndictionaries seemed to make the most sense:\n\n  1. A dictionary with an entry for each of the printers, that\n     referenced a list of all the \"files\" that printer owned.\n  2. Two dictionaries of all the files, one that is currently being\n     written to, and a shadow one that contains the last file printed\n     (in case you want to see the last thing printed for debugging, or\n     to re-print it).\n\nSo, by way of example, lets say we have 3 printers, babypuss, dino, and\nhoparoo. Barney prints to dino, Wilma and Betty print to babypuss, and\nFred prints to hoparoo. So:\n\n     printers = {\"dino\": [ \"barney.txt\" ], \"babypuss\": [ \"wilma.txt\", \"betty.txt\" ], \"hoparoo\": [ \"fred.txt\" ]}\n     files = { \"barney.txt\": \"blahblah...\", \"wilma.txt\": \"etcetc...\", ...}\n     lastfiles = { \"barney.txt\": \"lastblahblah...\", \"wilma.txt\": \"lastetcetc...\", ...}\n\nFairly straightforward, and shouldn't take too much to get going.\n\n### Implementation\n\nUsually, when I'm implementing a filesystem using FUSE, the first\nthree functions\n\nI'll implement will be the *init* to kick things off, *getattr*, so\nthat the attributes of a file can be passed back, and *readdir*, so I\ncan \"see\" the files using `ls`. Once you implement those three\nfunctions, you've at least got a filesystem you can `cd` around\nin. We'll flesh it out from there.\n\n#### __init__\n\nSo, first thing we're going to need is a list of printers. There's\nprobably some cups bindings for Python, but `lpstat` will give us what\nwe need:\n\n       sbalneav@bedrock:~$ lpstat -p\n       printer babypuss is idle.  enabled since Fri 27 Jun 2008 10:53:43 AM CDT\n       printer dino is idle.  enabled since Mon 14 Jul 2008 11:15:56 AM CDT\n       printer hoparoo is idle.  enabled since Tue 08 Jul 2008 05:30:44 PM CDT\n       sbalneav@bedrock:~$\n\nSo, we'll want to get this list going, and build our printer list in\nthe *init* function using this output.\n\nFirst, we'll subclass the Fuse object in the usual manner, and define the *init* function:\n\n       class CupsFS(fuse.Fuse):\n       def __init__(self, *args, **kw):\n           fuse.Fuse.__init__(self, *args, **kw)\n\nWe'll need to get our list of printers. Let's split this out using the subprocess module:\n\n        lpstat = Popen(['lpstat -p'], shell=True, stdout=PIPE)\n        output = lpstat.communicate()[0]\n        lines = output.split(b'\\n');\n        lpstat.wait()\n\nAnd, we'll build our dictionary of *printers*, and the (currently empty) dictionary of *files*, and *lastfiles*:\n\n       self.printers = {}\n       self.files = {}\n       self.lastfiles = {}\n           for line in lines:\n               words = line.split(b' ')\n               if len(words) \u003e 2:\n                   self.printers[words[1]] = []  # the second word on the line is the printer name\n\n#### getattr\n\nNext, we'll need to make up some attributes. Since this is a \"fake\"\nfilesystem (i.e. it isn't really storing real file objects), we can be\na little \"loosey goosey\" with the file attributes. Let's have all files\nowned by root, with the directories' mode 0755, and files 0666, so we\nwon't have to worry about access problems.\n\nSo, first off, we'll need a separate class to return the status\nobject. Fortunately, the Python fuse bindings give us one we can\nsubclass:\n\n       class MyStat(fuse.Stat):\n       def __init__(self):\n           self.st_mode = stat.S_IFDIR | 0o755\n           self.st_ino = 0\n           self.st_dev = 0\n           self.st_nlink = 2\n           self.st_uid = 0\n           self.st_gid = 0\n           self.st_size = 4096\n           self.st_atime = 0\n           self.st_mtime = 0\n           self.st_ctime = 0\n\n\nThe inode and dev numbers we can ignore, as FUSE will handle those for\nus. We'll make the default status object be a directory, so we set the\nnumber of links to two (all directories have at least 2 links, itself,\nand the link back to ...) and a size of 4096, which is usually the\n\"default\" directory size. The access, modify, and change times are set\nto zero for now.\n\nAs well, we'll need to provide the *getattr* function in the CupsFS object itself:\n\n       def getattr(self, path):\n           st = MyStat()\n           pe = path.split('/')[1:]\n\n           st.st_atime = int(time())\n           st.st_mtime = st.st_atime\n           st.st_ctime = st.st_atime\n\n\nSo, we'll create a stat object, and split out our path we're handed on\nthe '/' character, and set the access, modification, and change time\nto be \"now\". But why split up the path elements?\n\nWell, for our little filesystem, we're either going to be handed paths\nlike `/` for the root, `/printer` to look at the printer directory, or\n`/printer/file` to get attributes for one of the files, so, by\nbreaking them into path elements:\n\n       \u003e\u003e\u003e path = \"/dino/barney.txt\"\n       \u003e\u003e\u003e path.split('/')[1:]\n       ['dino', 'barney.txt']\n       \u003e\u003e\u003e\n\nWe'll be able to look at the last element (`pe[-1]`), and see if it's\neither a printer, or a file. And that's what we'll do next:\n\n        if path == '/':                         # root\n            pass\n        elif pe[-1] in self.printers:           # a printer\n            pass\n        elif pe[-1] in self.lastfiles:          # a file\n            st.st_mode = stat.S_IFREG | o0666\n            st.st_nlink = 1\n            st.st_size = len(self.lastfiles[pe[-1]]\n        else:\n            return -errno.ENOENT\n        return st\n\n\nSo, if the path is '/' (the root), or, we can find the last path\nelement in the *printers* dictionary (i.e. '/dino'), then we don't\nhave to do anything, since we defaulted the status object above to be\na directory.\n\nIf, however, it's in the *lastfiles* dictionary, then we set the mode\nof the file with the all important `stat.S_IFREG`, which will mark\nthis entry as a \"regular\" file, and give it mode 0666 (-rw-rw-rw). As\nwell, we drop the link count to 1 (regular files that don't have hard\nlinks to them only have a link count of one), and set the size to the\nsize of the string stored in *lastfiles*. If the file was simply\ncreated, via, say, the \"touch\" command, the *lastfiles* string will be\nzero length, and we'll get the expected return value. However, if\nwe've written to a file, we'll get the size, as we'd expect if we do\nan `ls` in the printer directory.\n\nIf it didn't satisfy one of the three conditions ('/', '/printer',\n'/printer/file') we return `-errno.ENOENT` so we'll get the expected\n\"no such file or directory\" error if we try to access something that\nisn't there.\n\nSo much for our simple getattr! On to readdir!\n\n#### readdir\n\nIf we're in the root directory of our filesystem, we'd like to see the list of printers:\n\n       sbalneav@bedrock:/printer$ ls -la\n       total 40K\n       drwxr-xr-x  2 root root 4.0K 2008-07-16 13:21 .\n       drwxr-xr-x 23 root root 4.0K 2008-07-15 15:44 ..\n       drwxr-xr-x  2 root root 4.0K 2008-07-16 13:21 babypuss\n       drwxr-xr-x  2 root root 4.0K 2008-07-16 13:21 dino\n       drwxr-xr-x  2 root root 4.0K 2008-07-16 13:21 hoparoo\n       sbalneav@bedrock:/printer$\n\nAnd, if we're in a printer directory, we'd like to see the list of files:\n\n       sbalneav@bedrock:/printer/babypuss$ ls -la\n       total 8.0K\n       drwxr-xr-x 2 root root 4.0K 2008-07-16 13:24 .\n       drwxr-xr-x 2 root root 4.0K 2008-07-16 13:24 ..\n       -rw-rw-rw- 1 root root    0 2008-07-16 13:24 betty.txt\n       -rw-rw-rw- 1 root root    0 2008-07-16 13:24 wilma.txt\n       sbalneav@bedrock:/printer/babypuss$\n\nFUSE requires that you return the standard '.' and '..' files, plus\nthe list of files you want to display. If we're in the root, we want\nto return the list of keys that's in our *printers* dictionary, and if\nwe're in a printer directory, we want to return the list of files\nthat's associated with that printer key.\n\nSince each key in the *printers* dictionary has a list of the files\nassociated with it, this is fairly simple:\n\n       def readdir(self, path, offset):\n           dirents = [ '.', '..' ]\n           if path == '/':\n               dirents.extend(list(self.printers.keys()))\n           else:\n               # Note use of path[1:] to strip the leading '/'\n               # from the path, so we just get the printer name\n               dirents.extend(self.printers[path[1:]])\n           for r in dirents:\n               yield fuse.Direntry(r)\n\nSo, we start off with the list containing the '.' and '..' directory\nentries, and then check to see if we're in the root. If we are, we\njust add the list of keys by calling the *.keys()* method of the\n*printers* dictionary, and if not, we add the list associated with the\npath we've been given.\n\nThat's it. We should have a filesystem that we're able to cd around\nin. The full program's listed at the end of the page.\n\n#### mknod\n\nWell, we've got a boring filesystem that just has a few directories\ncorresponding to printers on the system. Now we want to be able to\ncreate files, so we can print them! For that, we'll have to implement\nthe mknod call.\n\nSince we're not doing anything fancy with our filesystem, like making\npipes, or /dev nodes, etc., we don't have to worry about examining the\n*mode* and *dev* parameters we're passed. If you were trying to\nimplement something more complicated, you would, but since this is a\nsimple example, and a quick tutorial, we'll gloss over that. All *we*\nneed to do, to add a file for printing, is to add the filename to the\nprinter entry in the *printers* dictionary, and create a new empty\nstring entry in the *files* and *lastfiles* dictionaries for that\nfile.\n\n        def mknod(self, path, mode, dev):\n            pe = path.split('/')[1:]        # Path elements 0 = printer 1 = file\n            self.printers[pe[0]].append(pe[1])\n            self.files[pe[1]] = \"\"\n            self.lastfiles[pe[1]] = \"\"\n            return 0\n\nSo, once again, we split up the path elements to get the printer name\nand filename easily. We then append the filename to the list of files\nassociated with that printer, and add the empty string dictionary\nentries for the file.\n\nSo, when we start up the filesystem, there won't be any files under\nthe printers, but if we do a:\n\n    sbalneav@bedrock$ touch /printers/babypuss/wilma.txt\n\nthe *mknod* function will be called, and we'll add the entry to the\n*printers*, *files*, and *lastfiles* dictionaries, so that doing an\n\n    ls\n\nwill now list\n\n    wilma.txt\n\nin the right place.\n\n#### unlink\n\nFor debugging purposes when trying to solve printer problems, it might\nbe handy to be able to \"remove\" the file, so we can see if the\napplication that's printing to the file is recreating the \"print job\"\nwhen it's supposed to. So,\n\nlet's implement the unlink function, so we can do a\n\n    rm wilma.txt\n\nThis is almost the exact opposite of the mknod. Now, we just want to\nremove the filename from the list associated with the printer, and\ndelete the dictionary entries in the *files* and *lastfiles*\ndictionaries:\n\n        def unlink(self, path):\n            pe = path.split('/')[1:]        # Path elements 0 = printer 1 = file\n            self.printers[pe[0]].remove(pe[1])\n            del(self.files[pe[1]])\n            del(self.lastfiles[pe[1]])\n            return 0\n\nSimple.\n\n#### write\n\nOK, meat and potatoes time. Although, for our little example here, a\nsurprisingly small slice of meat! When FUSE calls the *write*\nfunction, you're passed the path, data buffer, and offset within the\nfile to write the data to.\n\nSince we're just handling files that are 1) small and 2) linear, we\ndon't have to worry about the offset! In a real filesystem you would,\nfor say, doing random access reads and writes to a database file. But\nfor our little \"text print\" filesystem, we can just concatenate any\ndata we receive to the dictionary entry associated with the filename\nin the *files* dictionary.\n\n        def write(self, path, buf, offset):\n            pe = path.split('/')[1:]        # Path elements 0 = printer 1 = file\n            self.files[pe[1]] += buf\n            return len(buf)\n\nthe length of the buffer. The *write* system call expects you to\nreturn the *actual* amount of data read or written, so if you have a\nshort write, the kernel can handle it appropriately. We won't worry\nabout that for our little application.\n\n#### read\n\nReading's even simpler. We're just going to read from the *lastfiles*\ndictionary we're maintaining. We're passed a size and offset of the\nchunk to read, so all we need to do is take a string slice from the\n*lastfiles* dictionary.\n\n        def read(self, path, size, offset):\n            pe = path.split('/')[1:]        # Path elements 0 = printer 1 = file\n            return self.lastfiles[pe[1]][offset:offset+size]\n\nSo, all that remains is: how do we actually print the data?\n\n#### release\n\nNothing causes beginning FUSE filesystems authors more problems than\nhow to handle the close() of a file. For reasons that are outlined in\nthe FUSE FAQ, there *IS* no *close* function, but rather the *flush*\nand *release*functions, the semantics of which are tricky for some\nfiles (devices, pipes, mmap()'d files, etc.). However, for *OUR*\nlittle example, we'll get one call to *release* when the text file's\nclosed for writing, so we'll use that to be our signal to print the\nfile.\n\nAll we want to do is check and see if the string length in the *files*\ndictionary is greater than zero. If it is, then we've written to the\nfile, so we want to print out the text. However, if it isn't, we've\ngotten the release call from an open for reading. In that case, we'll\njust ignore it.\n\nOnce we get a release of the filename, all we want to do is pipe the\ntext associated with the file to the *lpr* command. We know what\nprinter to print to by the directory we're in. Once the file's\nprinted, we just want to save the current text in the *lastfiles*\ndictionary (so it can be read later, by the \"read\" call) and zero out\nthe text that's been built up in the *files* dictionary, so that every\nprint job does what you'd expect. If we didn't do that, every time\nyou'd print onto that file, you'd get all the previous print jobs out\nas well.\n\nOnce again, we'll use the subprocess module to pipe our data to lpr:\n\n        def release(self, path, flags):\n            pe = path.split('/')[1:]        # Path elements 0 = printer 1 = file\n            if len(self.files[pe[1]]) \u003e 0:\n                lpr = Popen(['lpr -P ' + pe[0]], shell=True, stdin=PIPE)\n                lpr.communicate(input=self.files[pe[1]])\n                lpr.wait()\n                self.lastfiles[pe[1]] = self.files[pe[1]]\n                self.files[pe[1]] = \"\"          # Clear out string\n            return 0\n\nThe only fancy footwork is passing the string we've built up to the\ninput of *lpr* via the *communicate* call.\n\n#### The rest\n\nAs you'll see in the full listing below, we stub out the rest of the\nfilesystem calls, just so they don't return \"function not implemented\"\nerrors.\n\n### Conclusion\n\nAs you can see, it's pretty easy to throw together a FUSE filesystem\nthat accomplishes a useful, non-trivial task in a relatively few lines\nof code. FUSE is a lot of fun to write filesystems with, and it's\nreally nice tool to be able to solve problems that would otherwise be\nmessy or impossible.\n\nHave fun, and enjoy\n\n### Listing of cups.py\n\n- [cups.py](example/cups.py)\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flibfuse%2Fpython-fuse","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flibfuse%2Fpython-fuse","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flibfuse%2Fpython-fuse/lists"}