2 # -*- coding: utf-8 -*-
4 # Copyright 2006, 2008, 2009 Nedko Arnaudov <nedko@arnaudov.name>
5 # You can use this software for any purpose you want
6 # Use it at your own risk
7 # You are allowed to modify the code
8 # You are disallowed to remove author credits from header
10 # FUSE filesystem for wiki
12 # Uses XML-RPC for accessing wiki
13 # Documentation here: http://www.jspwiki.org/Wiki.jsp?page=WikiRPCInterface2
16 # FUSE python bindings (0.2 or later), available from FUSE project http://fuse.sourceforge.net/
17 # xmlrpclib, available since python 2.2
21 # FUSE lkm: one comming with 2.6.15, 2.6.23.1
22 # FUSE usermode: 2.5.2, 2.7.3
23 # Python: 2.4.2, 2.5.1
24 # Wiki engines: MoinMoinWiki 1.5.0, 1.6.3
26 # Expected to work with:
30 # MoinMoinWiki > 1.5.0 (90%)
32 # If you improve this code, send a modified version to author please
34 ########################################################################################
37 # example configuration file for wikifuse
39 # wikis is array of dictionaries
40 # each dictionary represents one wiki server
43 # * name - REQUIRED name of the wiki server, will be used as directory name
44 # * xmlrpc_url - REQUIRED xml-rpc url of the wiki server
45 # * auth - OPTIONAL authentication method.
47 # authentication methods and related keys:
48 # * 'moin_cookie' required keys:
49 # * user_id - moin internal moin user id. Look at your data/users directory in wiki instance to get exact value
50 # * 'moin_auth' required keys:
51 # * username - your moin username
52 # * password - your moin password
56 'name': 'example.com',
57 'xmlrpc_url': 'http://wiki.example.com/moin.cgi?action=xmlrpc2',
58 'auth': 'moin_cookie',
59 'user_id': '1111111111.22.3333'
63 'xmlrpc_url': 'http://wiki.example.com/moin.cgi/?action=xmlrpc2',
66 'name': 'trac_http_basic',
67 'xmlrpc_url': 'https://user:pass@wiki.example.com/login/xmlrpc',
70 'name': 'example.org',
71 'xmlrpc_url': 'http://wiki.example.org/wiki/moin.cgi/?action=xmlrpc2',
97 fuse
.fuse_python_api
= (0, 2)
99 out_charset
= locale
.getpreferredencoding()
100 if out_charset
== None:
101 out_charset
= "utf-8"
102 page_coding
= "utf-8"
104 #print("Using charset %s for stdout" % out_charset)
107 if type(str) != types
.StringType
and type(str) != types
.UnicodeType
:
108 raise "trying to print non-string type %s" % type(str)
111 print str.encode(out_charset
, "replace")
112 except UnicodeDecodeError, e
:
113 print "---------------- cannot print: %s" % e
115 print "---------------- cannot print: %s" % sys
.exc_info()[0]
132 class WikiServerProxy
:
133 def __init__(self
, url
, transport
=None, prefix
=""):
134 # if available, strip username and password from url, because the self.url string is used in prints
135 m
= re
.match(r
"(https?://)([^:]+):([^@]+)@(.*)", url
)
137 self
.url
= m
.group(1) + m
.group(4)
146 self
.xmlrpc
= xmlrpclib
.ServerProxy(url
, verbose
=xmlrpc_verbose
, transport
=transport
)
148 print self
.url
+ ": introspection is supported"# + repr(self.xmlrpc.system.listMethods())
149 except xmlrpclib
.Fault
, f
:
150 print self
.url
+ ": introspection is not supported. code=%d (%s)" % (f
.faultCode
, f
.faultString
)
152 self
.fnprefix
= prefix
157 # xml rpc proxy without authentication
158 class WikiServerProxySimple(WikiServerProxy
):
159 def __init__(self
, url
, prefix
=""):
160 WikiServerProxy
.__init
__(self
, url
, prefix
=prefix
)
162 def __getattr__(self
, name
):
163 #log("__getattr__ " + name)
164 return self
.function(self
.xmlrpc
, self
.fnprefix
+ name
)
167 def __init__(self
, xmlrpc
, name
):
171 def __call__(self
, *args
):
172 #log("proxy call %s%s" % (self.name, repr(args)))
173 func
= self
.xmlrpc
.__getattr
__(self
.name
)
176 # moin_cookie authentication (MoinMoinWiki)
178 # For this to work change server check for self.request.user.trusted to check for self.request.user.valid
179 # The check is made in wikirpc.py, near line 362
181 # user_id is simply internal moin user id
182 # Look at your data/users directory in wiki instance to get exact value
184 class WikiServerProxyMoinCookie(WikiServerProxy
):
185 def __init__(self
, url
, user_id
, prefix
=""):
186 if url
.startswith("https"):
187 transport
= self
.safeTransport(user_id
)
189 transport
= self
.transport(user_id
)
190 WikiServerProxy
.__init
__(self
, url
, transport
=transport
, prefix
=prefix
)
192 def __getattr__(self
, name
):
193 #log("__getattr__ " + name)
194 return self
.function(self
.xmlrpc
, self
.fnprefix
+ name
)
197 def __init__(self
, xmlrpc
, name
):
201 def __call__(self
, *args
):
202 #log("proxy call %s%s" % (self.name, repr(args)))
203 func
= self
.xmlrpc
.__getattr
__(self
.name
)
206 class transport(xmlrpclib
.Transport
):
207 def __init__(self
, user_id
):
208 xmlrpclib
.Transport
.__init
__(self
)
209 self
.user_id
= user_id
211 def get_host_info(self
, host
):
212 host
, extra_headers
, x509
= xmlrpclib
.Transport
.get_host_info(self
, host
)
214 if extra_headers
== None:
216 extra_headers
.append(("Cookie", "MOIN_ID=" + self
.user_id
))
218 return host
, extra_headers
, x509
220 class safeTransport(xmlrpclib
.SafeTransport
):
221 def __init__(self
, user_id
):
222 xmlrpclib
.SafeTransport
.__init
__(self
)
223 self
.user_id
= user_id
225 def get_host_info(self
, host
):
226 host
, extra_headers
, x509
= xmlrpclib
.SafeTransport
.get_host_info(self
, host
)
228 if extra_headers
== None:
230 extra_headers
.append(("Cookie", "MOIN_ID=" + self
.user_id
))
232 return host
, extra_headers
, x509
234 # MoinMoinWiki xmlrpc authentication
236 class WikiServerProxyMoinAuth(WikiServerProxy
):
237 def __init__(self
, url
, username
, password
, prefix
=""):
238 WikiServerProxy
.__init
__(self
, url
, prefix
=prefix
)
240 self
.username
= username
241 self
.password
= password
245 def __getattr__(self
, name
):
246 #log("__getattr__ " + name)
248 return self
.function(self
.xmlrpc
, self
.fnprefix
+ name
, self
.token
)
252 log_basic("Deleting moin auth token for '%s'" % self
.url
)
253 assert self
.xmlrpc
.deleteAuthToken(self
.token
) == 'SUCCESS'
255 def check_token(self
):
256 # Verify that the token is valid by using it
257 # and checking that the result is 'SUCCESS'.
258 # The token should be valid for 15 minutes.
262 self
.xmlrpc
.applyAuthToken(self
.token
)
268 log_basic("Getting new moin auth token for '%s'" % self
.url
)
269 self
.token
= self
.xmlrpc
.getAuthToken(self
.username
, self
.password
)
271 if self
.xmlrpc
.applyAuthToken(self
.token
) != 'SUCCESS':
272 log_error("Invalid username/password when authenticating to '%s' (%s/%s)" % (url
, username
, password
))
275 def __init__(self
, xmlrpc
, name
, token
):
280 def __call__(self
, *args
):
281 #log("proxy call %s%s" % (self.name, repr(args)))
283 # build a multicall object that
284 mcall
= xmlrpclib
.MultiCall(self
.xmlrpc
)
286 # first applies the token and
287 mcall
.applyAuthToken(self
.token
)
289 # then call the real function
290 mcall
.__getattr
__(self
.name
)(*args
)
292 # now execute the multicall
295 #log_basic(results[0])
299 except xmlrpclib
.Fault
, f
:
300 ret
= {'faultCode': f
.faultCode
, 'faultString': f
.faultString
}
304 class MyStat(fuse
.Stat
):
318 def __init__(self
, name
, proxy
, put2
=False):
321 self
.wikiproxy
= proxy
325 info
= "name: %s\n" % name
326 info
+= "url: %s\n" % proxy
.url
327 info
+= "proxy: %s\n" % proxy
.__class
__.__name
__
330 #log("%10s: %s (%s)" % (name, self.wikiproxy.WhoAmI(), proxy.url))
333 whoami
= self
.wikiproxy
.WhoAmI()
334 except xmlrpclib
.Fault
, e
:
337 whoami
= sys
.exc_info()[0]
339 info
+= "whoami: %s\n" % whoami
342 version
= self
.wikiproxy
.getRPCVersionSupported()
343 except xmlrpclib
.Fault
, e
:
346 version
= sys
.exc_info()[0]
348 info
+= "wiki xml rpc version: %s\n" % version
352 self
.all
= [unicode(x
).encode(page_coding
)
353 for x
in self
.wikiproxy
.getAllPages()]
354 info
+= "pages: %s\n%s" % (len(self
.all
),"\n".join(self
.all
))
356 self
.virt_pages
['_'] = info
357 #self.virt_pages['cyr'] = u"кириÐ��»Ð¸Ñ Ð��°"
360 self
.wikiproxy
.cleanup()
362 def readdir(self
, offset
):
363 yield fuse
.Direntry('.')
364 yield fuse
.Direntry('..')
366 for page
in self
.virt_pages
.iterkeys():
367 yield fuse
.Direntry(page
)
371 page
= page
.replace("/","%%")
372 if page
.find('/') == -1:
373 yield fuse
.Direntry(page
)
375 def getattr(self
, path
):
376 log_basic("getattr \"%s\"" % path
)
381 st
.st_mode
= S_IFREG |
0666
383 if self
.virt_pages
.has_key(path
):
384 st
.st_size
= len(self
.virt_pages
[path
])
387 if self
.pages
.has_key(path
):
388 st
.st_size
= self
.pages
[path
]['size']
391 if path
.replace("%%","/") not in self
.all
:
392 log_basic("not in all")
395 info
= self
.wikiproxy
.getPageInfo(path
.replace("%%","/"))
396 if info
.has_key('faultCode'):
397 log_error("getPageInfo(%s) failed" % path
+ repr(info
))
400 data
= self
.wikiproxy
.getPage(path
.replace("%%","/"))
401 data
= unicode(data
).encode(out_charset
)
402 st
.st_size
= len(data
)
406 def open(self
, path
, flags
):
407 log_basic("open \"%s\" flags %u" % (path
, flags
))
409 if not self
.pages
.has_key(path
):
410 self
.pages
[path
] = {}
412 if self
.virt_pages
.has_key(path
):
413 data
= self
.virt_pages
[path
]
414 self
.pages
[path
]['data'] = data
415 self
.pages
[path
]['size'] = len(data
)
416 self
.pages
[path
]['modified'] = False
419 data
= self
.wikiproxy
.getPage(path
.replace("%%","/"))
420 data
= unicode(data
).encode(out_charset
)
421 if self
.pages
[path
].has_key('size'):
422 size
= self
.pages
[path
]['size']
423 if size
!= len(data
):
424 log_basic("Truncating to %u bytes" % size
)
429 self
.pages
[path
]['modified'] = True
430 self
.pages
[path
]['data'] = data
431 log_debug("\"%s\"" % data
)
434 log_basic("Updating size of '%s' to %u bytes" % (path
, size
))
435 self
.pages
[path
]['modified'] = False
436 self
.pages
[path
]['data'] = data
437 self
.pages
[path
]['size'] = size
440 def read(self
, path
, length
, offset
):
441 log_basic("read \"%s\" %u bytes, from offset %u" % (path
, length
, offset
))
443 if offset
+ length
> self
.pages
[path
]['size']:
444 length
= self
.pages
[path
]['size'] - offset
449 data
= self
.pages
[path
]['data'][offset
:offset
+length
]
450 log_debug("\"%s\"" % data
)
451 log_basic("data length: %u bytes" % len(data
))
452 log_basic("type: %s" % type(data
))
455 def write(self
, path
, buf
, offset
):
456 log_basic("write \"%s\" %u bytes, to offset %u" % (path
, len(buf
), offset
))
458 pre
= self
.pages
[path
]['data'][:offset
]
459 post
= self
.pages
[path
]['data'][offset
+size
:]
460 data
= pre
+ buf
+ post
461 self
.pages
[path
]['size'] = len(data
)
462 log_debug("\"%s\"" % data
)
463 self
.pages
[path
]['data'] = data
464 self
.pages
[path
]['modified'] = True
467 def truncate(self
, path
, size
):
468 log_basic("truncate \"%s\" %u bytes" % (path
, size
))
470 if not self
.pages
.has_key(path
):
471 self
.pages
[path
] = {}
473 if size
> self
.pages
[path
]['size']:
475 self
.pages
[path
]['modified'] = True
476 self
.pages
[path
]['size'] = size
477 if self
.pages
[path
].has_key('data'):
478 self
.pages
[path
]['data'] = self
.pages
[path
]['data'][size
:]
481 def release(self
, path
, flags
):
482 log_basic("release \"%s\"" % path
)
484 ret
= self
.fsync_do(path
)
486 del self
.pages
[path
]['data']
490 def fsync(self
, path
, isfsyncfile
):
491 log_basic("fsync: path=%s, isfsyncfile=%s" % (path
, isfsyncfile
))
492 return self
.fsync_do(path
)
494 def fsync_do(self
, path
):
495 if self
.pages
.has_key(path
):
496 if self
.pages
[path
]['modified']:
497 log_basic("PUT PAGE")
498 log_debug("\"%s\"" % self
.pages
[path
]['data'])
499 data
= unicode(self
.pages
[path
]['data'], out_charset
)
500 safepath
= path
.replace("%%","/")
502 ret
= self
.wikiproxy
.putPage(safepath
, data
)
504 ret
= self
.wikiproxy
.putPage(safepath
, data
, {})
505 if type(ret
) == types
.BooleanType
and ret
== True:
506 self
.pages
[path
]['modified'] = False
509 log_error("putPage(%s) failed" % path
+ repr(ret
))
512 class WikiFS(fuse
.Fuse
):
514 def __init__(self
, servers
, *args
, **kw
):
515 fuse
.Fuse
.__init
__(self
, *args
, **kw
)
517 #log_basic("mountpoint: %s" % repr(self.mountpoint))
518 #log_basic("unnamed mount options: %s" % self.optlist)
519 #log_basic("named mount options: %s" % self.optdict)
521 # do stuff to set up your filesystem here, if you want
522 #thread.start_new_thread(self.mythread, ())
524 self
.servers
= servers
529 The beauty of the FUSE python implementation is that with the python interp
530 running in foreground, you can have threads
532 log_basic("mythread: started")
535 # log_basic("mythread: ticking")
539 def getattr(self
, path
):
540 log_basic("getattr \"%s\"" % path
)
547 path
.count("/") == 1 and
548 path
[1:] in [server
.name
for server
in self
.servers
] ):
549 st
.st_mode
= S_IFDIR |
0700
552 server
, subname
= self
.get_subname(path
)
554 return server
.getattr(subname
);
558 def readlink(self
, path
):
559 log_basic("readlink")
562 def readdir_top(self
, path
, offset
):
563 yield fuse
.Direntry('.')
564 yield fuse
.Direntry('..')
566 for server
in self
.servers
:
567 yield fuse
.Direntry(server
.name
)
569 def readdir(self
, path
, offset
):
570 log_basic("readdir(path='%s', offset=%u)" % (path
, offset
))
573 return self
.readdir_top(path
, offset
)
575 server_name
= path
[1:]
576 for server
in self
.servers
:
577 if server
.name
== server_name
:
578 return server
.readdir(offset
)
582 def unlink(self
, path
):
585 def rmdir(self
, path
):
588 def symlink(self
, path
, path1
):
591 def rename(self
, path
, path1
):
594 def link(self
, path
, path1
):
597 def chmod(self
, path
, mode
):
600 def chown(self
, path
, user
, group
):
604 def truncate(self
, path
, size
):
605 log_basic("truncate \"%s\" %u bytes" % (path
, size
))
607 server
, subname
= self
.get_subname(path
)
609 return server
.truncate(subname
, size
);
613 def mknod(self
, path
, mode
, dev
):
616 def mkdir(self
, path
, mode
):
619 def utime(self
, path
, times
):
623 def get_subname(self
, path
):
624 for server
in self
.servers
:
625 prefix
= "/" + server
.name
+ "/"
626 if path
.startswith(prefix
):
627 return (server
, path
[len(prefix
):])
631 def open(self
, path
, flags
):
632 log_basic("open \"%s\" flags %u" % (path
, flags
))
634 server
, subname
= self
.get_subname(path
)
636 return server
.open(subname
, flags
);
640 #def opendir(self, path):
641 # log_basic("opendir \"%s\"" % path)
644 def read(self
, path
, length
, offset
):
645 log_basic("read \"%s\" %u bytes, from offset %u" % (path
, length
, offset
))
647 server
, subname
= self
.get_subname(path
)
649 return server
.read(subname
, length
, offset
);
653 def write(self
, path
, buf
, offset
):
654 log_basic("write \"%s\" %u bytes, to offset %u" % (path
, len(buf
), offset
))
656 server
, subname
= self
.get_subname(path
)
658 return server
.write(subname
, buf
, offset
);
662 def release(self
, path
, flags
):
663 log_basic("release \"%s\"" % path
)
665 server
, subname
= self
.get_subname(path
)
667 return server
.release(subname
, flags
);
671 def fsync(self
, path
, isfsyncfile
):
672 log_basic("fsync: path=%s, isfsyncfile=%s" % (path
, isfsyncfile
))
674 server
, subname
= self
.get_subname(path
)
676 return server
.fsync(subname
, isfsyncfile
);
685 sys
.path
= [".",os
.environ
['HOME'] + "/.config/wikifuse"]
689 log_error("No configuration file found in '%s'" % sys
.path
[0])
690 #log_error(repr(sys.exc_info()[0]))
691 log_error("Example configuration file (save it as %s/config.py):" % sys
.path
[0])
692 log_error(example_config
)
699 for wiki
in config
.wikis
:
702 if wiki
.has_key('use_prefix'):
703 use_prefix
= wiki
['use_prefix']
705 use_prefix
= True # default is to follow the spec
712 if wiki
.has_key('put2'):
715 put2
= False # default is to follow the spec
717 if wiki
.has_key('auth'):
718 if wiki
['auth'] == 'moin_cookie':
719 proxy
= WikiServerProxyMoinCookie(wiki
['xmlrpc_url'], wiki
['user_id'], prefix
=prefix
)
720 elif wiki
['auth'] == 'moin_auth':
721 proxy
= WikiServerProxyMoinAuth(wiki
['xmlrpc_url'], wiki
['username'], wiki
['password'], prefix
=prefix
)
723 log_error("Unknown authentication method '%s'" % wiki
['auth'])
726 proxy
= WikiServerProxySimple(wiki
['xmlrpc_url'], prefix
=prefix
)
727 servers
.append(WikiServer(wiki
['name'], proxy
, put2
=put2
))
729 fs
= WikiFS(servers
,version
="%prog " + fuse
.__version
__,dash_s_do
='setsingle')
730 #fs.multithreaded = 1
734 for server
in servers
:
737 if __name__
== '__main__':