Strip out _urlfetch function, will fix write()
[fuse-mediawiki.git] / fuse_mediawiki / __init__.py
blob1de58af9f06c459e3f0dfe93c0dd6437693cd4b2
1 ###
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.
18 ###
20 """fuse-mediawiki provides a FUSE filesystem for MediaWiki websites."""
22 import fuse
23 from fuse import Fuse
24 import os
25 import sys
26 import stat
27 import errno
28 import time
29 import urllib
30 import urllib2
31 import cookielib
32 import simplejson
33 import re
34 from StringIO import StringIO
35 from getpass import getpass
36 import base64
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().
44 attrs = {}
45 # For files, the value of a key is the contents of that file.
46 files = {}
47 # Here are some edit tokens that might help (wikititle: token)
48 edittoken = {}
49 timestamp = {}
51 def __init__(self, version, usage):
52 # init fuse
53 fuse.fuse_python_api = (0, 2)
54 Fuse.__init__(self, version=version, usage=usage)
55 # init default file attributes
56 for path in ['/', '/image', '/cat', '/content']:
57 self._setdirattr(path)
58 # init file system and default directories
59 self.files['/image'] = 0
60 self.files['/cat'] = 0
61 self.files['/content'] = 0
62 # init cookie jar
63 self.cookiejar = cookielib.LWPCookieJar()
64 urllib2.install_opener(urllib2.build_opener(
65 urllib2.HTTPCookieProcessor(self.cookiejar)))
67 def _file2fd(self, path):
68 """Create a file descriptor out of a file within this filesystem"""
69 try:
70 return StringIO(self.files[path])
71 except KeyError:
72 return StringIO()
74 def _unescape(self, text):
75 """Unescape text in the textarea on retrieval"""
76 text = text.replace('&lt;', '<')
77 text = text.replace('&gt;', '>')
78 text = text.replace('&amp;', '&')
79 return text
81 def _setdirattr(self, path):
82 """Set default attributes for a directory at the specified path"""
83 self.attrs[path] = fuse.Stat()
84 self.attrs[path].st_mode = stat.S_IFDIR | 0755 # drwxr-xr-x
85 self.attrs[path].st_uid = int(os.getuid())
86 self.attrs[path].st_gid = int(os.getgid())
87 self.attrs[path].st_size = 4096 # 4.0 K
88 self.attrs[path].st_atime = time.time()
89 self.attrs[path].st_mtime = time.time()
90 self.attrs[path].st_ctime = time.time()
91 self.attrs[path].st_nlink = 2
93 def _setregattr(self, path):
94 """Set default attributes for a regular file at the specified path"""
95 self.attrs[path] = fuse.Stat()
96 self.attrs[path].st_mode = stat.S_IFREG | 0644 # -rw-r--r--
97 self.attrs[path].st_uid = int(os.getuid())
98 self.attrs[path].st_gid = int(os.getgid())
99 self.attrs[path].st_size = 0
100 self.attrs[path].st_atime = time.time()
101 self.attrs[path].st_mtime = time.time()
102 self.attrs[path].st_ctime = time.time()
103 self.attrs[path].st_nlink = 1
105 def _log(self, message):
106 """Log a message. Currently it just uses print, and therefore logs only
107 if the -f or -d options are sent from the command line."""
108 # it's simple now, but in case we want to change it later... here we go
109 print message
110 return
112 def _setrooturl(self, url):
113 self.rooturl = url
114 return
116 def _apifetch(self, apivars=None, headers={}, format="json"):
117 apivars["format"] = format
118 post = urllib.urlencode(apivars)
119 request = urllib2.Request(self.rooturl, post)
120 for header in headers:
121 request.add_header(header, headers[header])
122 response = urllib2.urlopen(request)
123 return (response, request)
125 def _setupauth(self, username):
126 # if there is no username, anonymous auth
127 if username == None:
128 self._log("Anonymously accessing wiki")
129 else:
130 self.username = username
131 self._asklogin()
132 return
134 def _asklogin(self):
135 self.password = getpass(self.username + "'s password: ")
136 return self._login()
138 def _login(self):
139 """Attempts to login to the wiki."""
140 print "Logging in..."
141 apivars = {'lgname': self.username, 'lgpassword': self.password,
142 'action': 'login'}
143 response = self._apifetch(apivars)[0]
144 data = simplejson.loads(response.read())
145 if data['login']['result'] == "Success":
146 # login OK
147 self._log("Logged in successfully as %s" % self.username)
148 return True
149 elif data['login']['result'] == "Throttled":
150 # oh god we're throttling!
151 self._log("The API thinks we're throttling, please wait %i seconds"
152 % data['login']['wait'])
153 sys.exit()
154 else:
155 # bad login
156 print "Login failed"
157 sys.exit()
159 def getattr(self, path):
160 if path != '/':
161 self._log('*** getattr '+path)
162 if path in self.attrs:
163 # file found
164 return self.attrs[path]
165 elif path[-5:] == ".wiki" and path[:8] == "/content":
166 # file technically found... we need to go make it.
167 self.mknod(path, 0100644, 0)
168 return self.attrs[path]
169 else:
170 # file not found
171 return -errno.ENOENT
173 def getdir(self, path):
174 self._log('*** getdir '+path)
175 keys = self.files.keys()
176 flist = []
177 for key in keys:
178 l = len(path)
179 print (key, key[0:l], key[l:], key[l+1:])
180 if key[0:l] == path:
181 if key[l:] != '':
182 if '/' == key[l]:
183 if '/' not in key[l+1:]:
184 flist.append((key[l+1:], 0))
185 else:
186 if '/' not in key[l:]:
187 flist.append((key[l:], 0))
188 flist.append(('.', 0))
189 flist.append(('..', 0))
190 return flist
192 def readdir(self, path, offset):
193 self._log('*** readdir '+str([path, offset]))
194 return self.readdir_compat_0_1(path, offset)
196 def mythread(self):
197 self._log('*** mythread')
198 return -errno.ENOSYS
200 def chmod(self, path, mode):
201 self._log('*** chmod '+str([path, oct(mode)]))
202 self.attrs[path].st_mode = mode
203 return
205 def chown(self, path, uid, gid):
206 self._log('*** chown '+str([path, uid, gid]))
207 return -errno.ENOSYS
209 def fsync(self, path, isFsyncFile, fd=None):
210 self._log('*** fsync '+str([path, isFsyncFile, fd]))
211 return
213 def link(self, targetPath, linkPath):
214 self._log('*** link '+str([targetPath, linkPath]))
215 return -errno.ENOSYS
217 def mkdir(self, path, mode):
218 """Create a directory."""
219 self._log('*** mkdir '+str([path, oct(mode)]))
220 if re.match('^/image/.*$', path):
221 return -errno.EPERM
222 if re.match('^/cat/.*$', path):
223 return -errno.ENOSYS
224 self.files[path] = 0
225 self._setdirattr(path)
226 self.attrs[path].st_mode = stat.S_IFDIR | mode
227 return
229 def mknod(self, path, mode, dev):
230 """Create a file. Not sure what the dev argument is, but it doesn't
231 seem useful."""
232 self._log('*** mknod '+str([path, oct(mode), dev]))
233 self.files[path] = ""
234 self._setregattr(path)
235 self.attrs[path].st_mode = mode
236 if path[-5:] == '.wiki' and path[:8] == "/content":
237 # this is a wiki page, get page contents
238 wikititle = path[9:-5]
239 apivars = {'action': 'query', 'titles': wikititle, 'prop':
240 'info|revisions', 'rvprop': 'content|timestamp',
241 'intoken': 'edit'}
242 (response, request) = self._apifetch(apivars)
243 data = simplejson.loads(response.read())
244 page = data['query']['pages'][data['query']['pages'].keys()[0]]\
245 ['revisions'][0]['*'].encode('utf8')
246 print type(page)
247 self.files[path] = page
248 self.attrs[path].st_size = len(page)
249 self.edittoken[path] = data['query']['pages'][data['query']\
250 ['pages'].keys()[0]]['edittoken']
251 self.timestamp[path] = data['query']['pages'][data['query']\
252 ['pages'].keys()[0]]['revisions'][0]['timestamp']
253 return
255 def open(self, path, flags):
256 self._log('*** open '+str([path, flags]))
257 return self._file2fd(path)
259 def read(self, path, length, offset, fd=None):
260 self._log('*** read'+str([path, length, offset, fd]))
261 if fd != None:
262 # we'll just read from the provided StringIO
263 fd.seek(offset)
264 return fd.read(length)
265 else:
266 # this should never really happen
267 try:
268 cwd = self.files[path]
269 except KeyError:
270 return -errno.ENOENT
271 return cwd[offset:length]
273 def readlink(self, path):
274 self._log('*** readlink '+path)
275 return -errno.ENOSYS
277 def release(self, path, flags, fd=None):
278 self._log('*** release '+str([path, flags, fd]))
279 if fd != None:
280 fd.close()
281 return
282 else:
283 # i don't think we really care
284 return
286 def rename(self, oldPath, newPath):
287 self._log('*** rename '+str([oldPath, newPath]))
288 self.files[newPath] = self.files[oldPath]
289 del self.files[oldPath]
290 self.attrs[newPath] = self.attrs[oldPath]
291 del self.attrs[oldPath]
292 return
294 def rmdir(self, path):
295 self._log('*** rmdir '+path)
296 # -errno.ENOTEMPTY will be useful
297 return -errno.ENOSYS
299 def statfs(self):
300 #self._log('*** statfs')
301 vfs = fuse.StatVfs()
302 return vfs
304 def symlink(self, targetPath, linkPath):
305 self._log('*** symlink '+str([targetPath, linkPath]))
306 return -errno.ENOSYS
308 def truncate(self, path, size):
309 self._log('*** truncate '+str([path, size]))
310 self.files[path] = self.files[path][0:size]
311 self.attrs[path].st_size = len(self.files[path])
312 return None
314 def unlink(self, path):
315 self._log('*** unlink '+path)
316 if path not in self.files:
317 return -errno.ENOENT
318 del self.attrs[path]
319 del self.files[path]
320 return
322 def utime(self, path, times):
323 self._log('*** utime '+str([path, times]))
324 self.attrs[path].st_atime = times[0]
325 self.attrs[path].st_mtime = times[1]
326 return
328 def write(self, path, buf, offset, fd=None):
329 self._log('*** write'+str([path, len(buf), offset, fd]))
330 if fd != None:
331 fd.seek(offset)
332 fd.write(buf)
333 x = self.files[path]
334 self.files[path] = x[:offset] + buf + x[offset:]
335 self.attrs[path].st_size = len(self.files[path])
336 if path[-5:] == '.wiki' and path[:8] == "/content":
337 # this is a wiki page, save page contents
338 editsumm = self._re_editsumm.search(self.files[path]).group(1)
339 editsumm = editsumm.strip()
340 data = self._re_fmwcomm.sub('', self.files[path]).strip()
341 wikititle = path[9:-5]
342 getvars = {'title': wikititle, 'action': 'submit'}
343 postvars = {'wpSection': '', 'wpStarttime':
344 self.wikitokens[wikititle]['start'], 'wpEdittime':
345 self.wikitokens[wikititle]['edit'], 'wpScrolltop': '0',
346 'wpTextbox1': data, 'wpSummary': editsumm,
347 'wpSave': 'Save page', 'wpEditToken':
348 self.wikitokens[wikititle]['token'], 'wpAutoSummary':
349 self.wikitokens[wikititle]['auto']}
350 response = self._urlfetch(getvars, postvars)[0]
351 respdata = response.read()
352 return len(buf)
355 def main():
356 usage = """Usage: python %prog [OPTIONS] ROOT_URL MOUNTPOINT
357 fuse-mediawiki is a FUSE filesystem for editing MediaWiki websites.
359 # setup FUSE
360 fs = FuseMediaWiki(version="fuse-mediawiki", usage=usage)
361 # setup option parser
362 # -u, --username: set the username. password is asked for on mount
363 fs.parser.add_option("-u", "--username", dest="username",
364 help="username for login into wiki")
365 # allow for FUSE mount options
366 fs.parser.mountopt = True
367 # get arguments
368 (options, args) = fs.parser.parse_args()
369 # if we have zero or two or more arguments before the MOUNTPOINT, fail.
370 if len(args) != 1:
371 print "Must specify exactly 1 api.php URL and 1 mount point, " \
372 "in that order"
373 print "api.php URL example: http://en.wikipedia.org/w/api.php"
374 sys.exit(1)
375 # set the root URL
376 fs._setrooturl(args[0])
377 # tell the object what are authentication method is
378 fs._setupauth(options.username)
379 # this does something, I'm not really sure what at the moment
380 fs.parse(values=fs, errex=1)
381 # go into main loop
382 fs.main()
385 # hi!
386 if __name__ == '__main__':
387 main()