2 # fuse-mediawiki - FUSE filesystem for editing MediaWiki websites
3 # Copyright (C) 2008 Ian Weller <ianweller@gmail.com>
5 # This program is free software; you can redistribute it and/or modify
6 # it under the terms of the GNU General Public License as published by
7 # the Free Software Foundation; either version 2 of the License, or
8 # (at your option) any later version.
10 # This program is distributed in the hope that it will be useful,
11 # but WITHOUT ANY WARRANTY; without even the implied warranty of
12 # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 # GNU General Public License for more details.
15 # You should have received a copy of the GNU General Public License along
16 # with this program; if not, write to the Free Software Foundation, Inc.,
17 # 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
20 """fuse-mediawiki provides a FUSE filesystem for MediaWiki websites."""
34 from StringIO
import StringIO
35 from getpass
import getpass
39 class FuseMediaWiki(Fuse
):
40 """Class to provide FUSE filesystem."""
42 # The filesystem used is simply a dict of keys (filenames) and values.
43 # For attrs, the value of a key is a specific instance of fuse.Stat().
45 # For files, the value of a key is the contents of that file.
47 # Here are some edit tokens that might help (wikititle: token)
50 def __init__(self
, version
, usage
):
52 fuse
.fuse_python_api
= (0, 2)
53 Fuse
.__init
__(self
, version
=version
, usage
=usage
)
54 # init default file attributes
55 for path
in ['/', '/image', '/cat', '/content']:
56 self
._setdirattr
(path
)
57 # init file system and default directories
58 self
.files
['/image'] = 0
59 self
.files
['/cat'] = 0
60 self
.files
['/content'] = 0
62 self
.cookiejar
= cookielib
.LWPCookieJar()
63 urllib2
.install_opener(urllib2
.build_opener(
64 urllib2
.HTTPCookieProcessor(self
.cookiejar
)))
66 def _file2fd(self
, path
):
67 """Create a file descriptor out of a file within this filesystem"""
69 return StringIO(self
.files
[path
])
73 def _unescape(self
, text
):
74 """Unescape text in the textarea on retrieval"""
75 text
= text
.replace('<', '<')
76 text
= text
.replace('>', '>')
77 text
= text
.replace('&', '&')
80 def _setdirattr(self
, path
):
81 """Set default attributes for a directory at the specified path"""
82 self
.attrs
[path
] = fuse
.Stat()
83 self
.attrs
[path
].st_mode
= stat
.S_IFDIR |
0755 # drwxr-xr-x
84 self
.attrs
[path
].st_uid
= int(os
.getuid())
85 self
.attrs
[path
].st_gid
= int(os
.getgid())
86 self
.attrs
[path
].st_size
= 4096 # 4.0 K
87 self
.attrs
[path
].st_atime
= time
.time()
88 self
.attrs
[path
].st_mtime
= time
.time()
89 self
.attrs
[path
].st_ctime
= time
.time()
90 self
.attrs
[path
].st_nlink
= 2
92 def _setregattr(self
, path
):
93 """Set default attributes for a regular file at the specified path"""
94 self
.attrs
[path
] = fuse
.Stat()
95 self
.attrs
[path
].st_mode
= stat
.S_IFREG |
0644 # -rw-r--r--
96 self
.attrs
[path
].st_uid
= int(os
.getuid())
97 self
.attrs
[path
].st_gid
= int(os
.getgid())
98 self
.attrs
[path
].st_size
= 0
99 self
.attrs
[path
].st_atime
= time
.time()
100 self
.attrs
[path
].st_mtime
= time
.time()
101 self
.attrs
[path
].st_ctime
= time
.time()
102 self
.attrs
[path
].st_nlink
= 1
104 def _log(self
, message
):
105 """Log a message. Currently it just uses print, and therefore logs only
106 if the -f or -d options are sent from the command line."""
107 # it's simple now, but in case we want to change it later... here we go
111 def _setrooturl(self
, url
):
115 def _urlfetch(self
, getvars
, postvars
=None, headers
={}):
116 get
= urllib
.urlencode(getvars
)
117 url
= self
.rooturl
+ '?' + get
119 post
= urllib
.urlencode(postvars
)
122 request
= urllib2
.Request(url
, post
)
123 for header
in headers
:
124 request
.add_header(header
, headers
[header
])
126 response
= urllib2
.urlopen(request
)
127 except urllib2
.HTTPError
, args
:
128 # we assume this works. if not, let me know.
130 auth
= base64
.encodestring('%s:%s' % (self
.username
,
132 request
.add_header('Authorization', 'Basic %s' % auth
)
134 response
= urllib2
.urlopen(request
)
135 except urllib2
.HTTPError
, args
:
138 return (response
, request
)
140 def _apifetch(self
, apivars
=None, headers
={}, format
="json"):
141 apivars
["format"] = format
142 post
= urllib
.urlencode(apivars
)
143 request
= urllib2
.Request(self
.rooturl
, post
)
144 for header
in headers
:
145 request
.add_header(header
, headers
[header
])
146 response
= urllib2
.urlopen(request
)
147 return (response
, request
)
149 def _setupauth(self
, username
):
150 # if there is no username, anonymous auth
152 self
._log
("Anonymously accessing wiki")
154 self
.username
= username
159 self
.password
= getpass(self
.username
+ "'s password: ")
163 """Attempts to login to the wiki."""
164 print "Logging in..."
165 apivars
= {'lgname': self
.username
, 'lgpassword': self
.password
,
167 response
= self
._apifetch
(apivars
)[0]
168 data
= simplejson
.loads(response
.read())
169 if data
['login']['result'] == "Success":
171 self
._log
("Logged in successfully as %s" % self
.username
)
173 elif data
['login']['result'] == "Throttled":
174 # oh god we're throttling!
175 self
._log
("The API thinks we're throttling, please wait %i seconds"
176 % data
['login']['wait'])
183 def getattr(self
, path
):
185 self
._log
('*** getattr '+path
)
186 if path
in self
.attrs
:
188 return self
.attrs
[path
]
189 elif path
[-5:] == ".wiki" and path
[:8] == "/content":
190 # file technically found... we need to go make it.
191 self
.mknod(path
, 0100644, 0)
192 return self
.attrs
[path
]
197 def getdir(self
, path
):
198 self
._log
('*** getdir '+path
)
199 keys
= self
.files
.keys()
203 print (key
, key
[0:l
], key
[l
:], key
[l
+1:])
207 if '/' not in key
[l
+1:]:
208 flist
.append((key
[l
+1:], 0))
210 if '/' not in key
[l
:]:
211 flist
.append((key
[l
:], 0))
212 flist
.append(('.', 0))
213 flist
.append(('..', 0))
216 def readdir(self
, path
, offset
):
217 self
._log
('*** readdir '+str([path
, offset
]))
218 return self
.readdir_compat_0_1(path
, offset
)
221 self
._log
('*** mythread')
224 def chmod(self
, path
, mode
):
225 self
._log
('*** chmod '+str([path
, oct(mode
)]))
226 self
.attrs
[path
].st_mode
= mode
229 def chown(self
, path
, uid
, gid
):
230 self
._log
('*** chown '+str([path
, uid
, gid
]))
233 def fsync(self
, path
, isFsyncFile
, fd
=None):
234 self
._log
('*** fsync '+str([path
, isFsyncFile
, fd
]))
237 def link(self
, targetPath
, linkPath
):
238 self
._log
('*** link '+str([targetPath
, linkPath
]))
241 def mkdir(self
, path
, mode
):
242 """Create a directory."""
243 self
._log
('*** mkdir '+str([path
, oct(mode
)]))
244 if re
.match('^/image/.*$', path
):
246 if re
.match('^/cat/.*$', path
):
249 self
._setdirattr
(path
)
250 self
.attrs
[path
].st_mode
= stat
.S_IFDIR | mode
253 def mknod(self
, path
, mode
, dev
):
254 """Create a file. Not sure what the dev argument is, but it doesn't
256 self
._log
('*** mknod '+str([path
, oct(mode
), dev
]))
257 self
.files
[path
] = ""
258 self
._setregattr
(path
)
259 self
.attrs
[path
].st_mode
= mode
260 if path
[-5:] == '.wiki' and path
[:8] == "/content":
261 # this is a wiki page, get page contents
262 wikititle
= path
[9:-5]
263 apivars
= {'action': 'query', 'titles': wikititle
, 'prop':
264 'info|revisions', 'rvprop': 'content|timestamp',
266 (response
, request
) = self
._apifetch
(apivars
)
267 data
= simplejson
.loads(response
.read())
268 page
= data
['query']['pages'][data
['query']['pages'].keys()[0]]\
269 ['revisions'][0]['*'].encode('utf8')
271 self
.files
[path
] = page
272 self
.attrs
[path
].st_size
= len(page
)
273 self
.edittoken
[path
] = data
['query']['pages'][data
['query']\
274 ['pages'].keys()[0]]['edittoken']
275 self
.timestamp
[path
] = data
['query']['pages'][data
['query']\
276 ['pages'].keys()[0]]['revisions'][0]['timestamp']
279 def open(self
, path
, flags
):
280 self
._log
('*** open '+str([path
, flags
]))
281 return self
._file
2fd
(path
)
283 def read(self
, path
, length
, offset
, fd
=None):
284 self
._log
('*** read'+str([path
, length
, offset
, fd
]))
286 # we'll just read from the provided StringIO
288 return fd
.read(length
)
290 # this should never really happen
292 cwd
= self
.files
[path
]
295 return cwd
[offset
:length
]
297 def readlink(self
, path
):
298 self
._log
('*** readlink '+path
)
301 def release(self
, path
, flags
, fd
=None):
302 self
._log
('*** release '+str([path
, flags
, fd
]))
307 # i don't think we really care
310 def rename(self
, oldPath
, newPath
):
311 self
._log
('*** rename '+str([oldPath
, newPath
]))
312 self
.files
[newPath
] = self
.files
[oldPath
]
313 del self
.files
[oldPath
]
314 self
.attrs
[newPath
] = self
.attrs
[oldPath
]
315 del self
.attrs
[oldPath
]
318 def rmdir(self
, path
):
319 self
._log
('*** rmdir '+path
)
320 # -errno.ENOTEMPTY will be useful
324 #self._log('*** statfs')
328 def symlink(self
, targetPath
, linkPath
):
329 self
._log
('*** symlink '+str([targetPath
, linkPath
]))
332 def truncate(self
, path
, size
):
333 self
._log
('*** truncate '+str([path
, size
]))
334 self
.files
[path
] = self
.files
[path
][0:size
]
335 self
.attrs
[path
].st_size
= len(self
.files
[path
])
338 def unlink(self
, path
):
339 self
._log
('*** unlink '+path
)
340 if path
not in self
.files
:
346 def utime(self
, path
, times
):
347 self
._log
('*** utime '+str([path
, times
]))
348 self
.attrs
[path
].st_atime
= times
[0]
349 self
.attrs
[path
].st_mtime
= times
[1]
352 def write(self
, path
, buf
, offset
, fd
=None):
353 self
._log
('*** write'+str([path
, len(buf
), offset
, fd
]))
358 self
.files
[path
] = x
[:offset
] + buf
+ x
[offset
:]
359 self
.attrs
[path
].st_size
= len(self
.files
[path
])
360 if path
[-5:] == '.wiki' and path
[:8] == "/content":
361 # this is a wiki page, save page contents
362 editsumm
= self
._re
_editsumm
.search(self
.files
[path
]).group(1)
363 editsumm
= editsumm
.strip()
364 data
= self
._re
_fmwcomm
.sub('', self
.files
[path
]).strip()
365 wikititle
= path
[9:-5]
366 getvars
= {'title': wikititle
, 'action': 'submit'}
367 postvars
= {'wpSection': '', 'wpStarttime':
368 self
.wikitokens
[wikititle
]['start'], 'wpEdittime':
369 self
.wikitokens
[wikititle
]['edit'], 'wpScrolltop': '0',
370 'wpTextbox1': data
, 'wpSummary': editsumm
,
371 'wpSave': 'Save page', 'wpEditToken':
372 self
.wikitokens
[wikititle
]['token'], 'wpAutoSummary':
373 self
.wikitokens
[wikititle
]['auto']}
374 response
= self
._urlfetch
(getvars
, postvars
)[0]
375 respdata
= response
.read()
380 usage
= """Usage: python %prog [OPTIONS] ROOT_URL MOUNTPOINT
381 fuse-mediawiki is a FUSE filesystem for editing MediaWiki websites.
384 fs
= FuseMediaWiki(version
="fuse-mediawiki", usage
=usage
)
385 # setup option parser
386 # -u, --username: set the username. password is asked for on mount
387 fs
.parser
.add_option("-u", "--username", dest
="username",
388 help="username for login into wiki")
389 # allow for FUSE mount options
390 fs
.parser
.mountopt
= True
392 (options
, args
) = fs
.parser
.parse_args()
393 # if we have zero or two or more arguments before the MOUNTPOINT, fail.
395 print "Must specify exactly 1 root URL and 1 mount point, " + \
399 fs
._setrooturl
(args
[0])
400 # tell the object what are authentication method is
401 fs
._setupauth
(options
.username
)
402 # this does something, I'm not really sure what at the moment
403 fs
.parse(values
=fs
, errex
=1)
409 if __name__
== '__main__':