├── .gitignore ├── README.rst └── mysqlfuse.py /.gitignore: -------------------------------------------------------------------------------- 1 | DBG 2 | README.html 3 | -------------------------------------------------------------------------------- /README.rst: -------------------------------------------------------------------------------- 1 | ****************************************************** 2 | mysqlfuse - Access a MySQL database like a file system 3 | ****************************************************** 4 | 5 | This is an attempt to use FUSE to provide a view of a MySQL database as a 6 | filesystem. See the __doc__ string in the mysqlfuse.py file for an 7 | explanation. 8 | 9 | Currently it MOSTLY works. But not everything really works quite right. 10 | This is v.0.00001alpha, mind you. 11 | 12 | 13 | Concept 14 | ======= 15 | The root directory contains subdirs named for each table in the db. 16 | 17 | Within each table, we look up the PRIMARY indexes (those have to be unique, 18 | right?). Ummm... for the first cut, we'll take them in order given by 19 | Sequence (ideally we should be able to take them in all orders, see below). 20 | This will be better with an example:: 21 | 22 | CREATE TABLE `testtab` ( 23 | `key1` varchar(10) default NULL, 24 | `key2` varchar(7) default NULL, 25 | `data1` text default NULL, 26 | `data2` varchar(18) default NULL, 27 | PRIMARY KEY (`key1`, `key2`) 28 | ) 29 | 30 | This branch of mysqlfuse is going to have the directories alternating 31 | between keyname and keyvalue. This should allow browsing by any ordering 32 | of keys. 33 | 34 | (N.B. Handling NULL keys and different types of keys (int, varchar) is 35 | going to get tricky--actually it hasn't.) The above should lead to this:: 36 | 37 | 38 | . testtab 39 | / \ 40 | / \ 41 | key1 key2 (all the key fields) 42 | _____/ | \ / | \ 43 | / | \ / | \ 44 | foo ... bar baz ... quux (all the values for each field) 45 | | | | | 46 | | | | | 47 | key2 key2 key1 key1 (the key fields not above each) 48 | / | \ / | \ / | \ / | \ 49 | / | \ | | | | | | | | \ 50 | baz ... quux baz .. foo..bar foo... bar (values for those) 51 | / \ 52 | data1 data2 ... 53 | 54 | 55 | At the bottom level, the filenames are ``data1`` and ``data2``, i.e. all the 56 | *names* of the non-key fields, and each such file contains the *value* for 57 | that field in that row (note that each directory just above the bottom 58 | level contains a single row, assuming the keys have to be unique, 59 | i.e. primary). Higher-level entries are all directories, not files. They 60 | alternate between name-of-a-key-field and values-of-a-key-field. For 61 | example, ``/books/book/My_Friend_Flicka/page/71/pagecontents``. 62 | 63 | This way, the data can be accessed by any possible permutation of keys, 64 | since both ``/books/book/My_Friend_Flicka/page/71/`` and also 65 | ``/books/page/71/book/My_Friend_Flicka/`` are there. This gets more fun when 66 | there are more elements in the key. 67 | 68 | 69 | 70 | Usage 71 | ===== 72 | :: 73 | 74 | $ mysqlfuse.py -o host=localhost,user=sqluser,passwd=password,db=databasename 75 | 76 | Debugging information can be found in the ``DBG`` file. 77 | 78 | 79 | Dependencies 80 | ============ 81 | * fuse-python__ 82 | * MySQL-Python__ 83 | 84 | __ https://pypi.python.org/pypi/fuse-python 85 | __ http://mysql-python.sourceforge.net/ 86 | 87 | On Debian, simply install ``python-fuse`` and ``python-mysql``:: 88 | 89 | $ sudo apt-get install python-fuse python-mysql 90 | -------------------------------------------------------------------------------- /mysqlfuse.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python 2 | # coding=utf-8 3 | 4 | from fuse import Fuse, Stat 5 | import fuse 6 | import stat 7 | from time import time 8 | from subprocess import * 9 | import os 10 | import errno 11 | import MySQLdb 12 | from copy import copy 13 | 14 | import sys 15 | import pdb 16 | 17 | 18 | """ 19 | So here's the general concept: 20 | 21 | The root directory contains subdirs named for each table in the db. 22 | 23 | Within each table, we look up the PRIMARY indexes (those have to be unique, 24 | right?). Ummm... for the first cut, we'll take them in order given by 25 | Sequence (ideally we should be able to take them in all orders, see below). 26 | This will be better with an example. 27 | 28 | CREATE TABLE `testtab` ( 29 | `key1` varchar(10) default NULL, 30 | `key2` varchar(7) default NULL, 31 | `data1` text default NULL, 32 | `data2` varchar(18) default NULL, 33 | PRIMARY KEY (`key1`, `key2`) 34 | ) 35 | 36 | This branch of mysqlfuse is going to have the directories alternating 37 | between keyname and keyvalue. This should allow browsing by any ordering 38 | of keys. 39 | 40 | (N.B. Handling NULL keys and different types of keys (int, varchar) is 41 | going to get tricky--actually it hasn't.) The above should lead to this: 42 | 43 | 44 | testtab 45 | / \ 46 | / \ 47 | key1 key2 (all the key fields) 48 | _____/ | \ / | \ 49 | / | \ / | \ 50 | foo ... bar baz ... quux (all the values for each field) 51 | | | | | 52 | | | | | 53 | key2 key2 key1 key1 (the key fields not above each) 54 | / | \ / | \ / | \ / | \ 55 | / | \ | | | | | | | | \ 56 | baz ... quux baz .. foo..bar foo... bar (values for those) 57 | / \ 58 | data1 data2 ... 59 | 60 | 61 | At the bottom level, the filenames are "data1" and "data2", i.e. all the 62 | *names* of the non-key fields, and each such file contains the *value* for 63 | that field in that row (note that each directory just above the bottom 64 | level contains a single row, assuming the keys have to be unique, 65 | i.e. primary). Higher-level entries are all directories, not files. They 66 | alternate between name-of-a-key-field and values-of-a-key-field. For 67 | example, /books/book/My_Friend_Flicka/page/71/pagecontents. 68 | 69 | This way, the data can be accessed by any possible permutation of keys, 70 | since both /books/book/My_Friend_Flicka/page/71/ and also 71 | /books/page/71/book/My_Friend_Flicka/ are there. This gets more fun when 72 | there are more elements in the key. 73 | 74 | """ 75 | 76 | 77 | def getDepth(path): 78 | """ 79 | Return the depth of a given path, zero-based from root ('/') 80 | """ 81 | if path == '/': 82 | return 0 83 | else: 84 | return path.count('/') 85 | 86 | def getParts(path): 87 | """ 88 | Return the slash-separated parts of a given path as a list 89 | """ 90 | if path == '/': 91 | return [['/']] 92 | else: 93 | return path.split('/') 94 | 95 | def escape_for_fs(string): 96 | x=string 97 | x=x.replace("%","%%") 98 | x=x.replace("/","%/") 99 | return x 100 | 101 | def unescape_from_fs(string): 102 | x=string 103 | x=x.replace("%/","/") 104 | x=x.replace("%%","%") 105 | return x 106 | 107 | def escape_for_sql(string): 108 | x=string 109 | x=x.replace("'","''") 110 | return x 111 | 112 | def unescape_from_sql(string): 113 | return string.replace("''","'") 114 | 115 | 116 | def make_criteria(elts): 117 | # Elements are going to be alternating key/value pairs. 118 | criteria="" 119 | for i in range(0,len(elts),2): # Count by twos! 120 | (key, name)=elts[i:i+2] 121 | key=escape_for_sql(unescape_from_fs(key)) 122 | name=escape_for_sql(unescape_from_fs(name)) 123 | criteria+="`%s`='%s' AND "%(key,name) 124 | if len(criteria)>5: 125 | criteria=criteria[:-5] # Chop off the final AND. 126 | return criteria 127 | 128 | class MyStat(Stat): 129 | def __init__(self): 130 | self.st_mode = stat.S_IFDIR | 0755 131 | self.st_ino = 0 132 | self.st_dev = 0 133 | self.st_nlink = 2 134 | self.st_uid = 0 135 | self.st_gid = 0 136 | self.st_size = 4096 137 | self.st_atime = 0 138 | self.st_mtime = 0 139 | self.st_ctime = 0 140 | 141 | 142 | # decorator for debugging! 143 | def debugfunc(f): 144 | def newf(*args, **kwargs): 145 | MySQLFUSE.dbg.write(">>entering function %s\n"%f.__name__) 146 | MySQLFUSE.dbg.flush() 147 | x=f(*args, **kwargs) 148 | MySQLFUSE.dbg.write("<