Get the timestamp with the revision fetch
[fuse-mediawiki.git] / fuse_mediawiki / __init__.py
blob931badfa2f6286751bbd4adc3019e33a7e60f38d
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 = {}
50 def __init__(self, version, usage):
51 # init fuse
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
61 # init cookie jar
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"""
68 try:
69 return StringIO(self.files[path])
70 except KeyError:
71 return StringIO()
73 def _unescape(self, text):
74 """Unescape text in the textarea on retrieval"""
75 text = text.replace('&lt;', '<')
76 text = text.replace('&gt;', '>')
77 text = text.replace('&amp;', '&')
78 return text
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
108 print message
109 return
111 def _setrooturl(self, url):
112 self.rooturl = url
113 return
115 def _urlfetch(self, getvars, postvars=None, headers={}):
116 get = urllib.urlencode(getvars)
117 url = self.rooturl + '?' + get
118 if postvars != None:
119 post = urllib.urlencode(postvars)
120 else:
121 post = None
122 request = urllib2.Request(url, post)
123 for header in headers:
124 request.add_header(header, headers[header])
125 try:
126 response = urllib2.urlopen(request)
127 except urllib2.HTTPError, args:
128 # we assume this works. if not, let me know.
129 if args.code == 401:
130 auth = base64.encodestring('%s:%s' % (self.username,
131 self.password))[:-1]
132 request.add_header('Authorization', 'Basic %s' % auth)
133 try:
134 response = urllib2.urlopen(request)
135 except urllib2.HTTPError, args:
136 # bad login
137 print "Login failed"
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
151 if username == None:
152 self._log("Anonymously accessing wiki")
153 else:
154 self.username = username
155 self._asklogin()
156 return
158 def _asklogin(self):
159 self.password = getpass(self.username + "'s password: ")
160 return self._login()
162 def _login(self):
163 """Attempts to login to the wiki."""
164 print "Logging in..."
165 apivars = {'lgname': self.username, 'lgpassword': self.password,
166 'action': 'login'}
167 response = self._apifetch(apivars)[0]
168 data = simplejson.loads(response.read())
169 if data['login']['result'] == "Success":
170 # login OK
171 self._log("Logged in successfully as %s" % self.username)
172 return True
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'])
177 sys.exit()
178 else:
179 # bad login
180 print "Login failed"
181 sys.exit()
183 def getattr(self, path):
184 if path != '/':
185 self._log('*** getattr '+path)
186 if path in self.attrs:
187 # file found
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]
193 else:
194 # file not found
195 return -errno.ENOENT
197 def getdir(self, path):
198 self._log('*** getdir '+path)
199 keys = self.files.keys()
200 flist = []
201 for key in keys:
202 l = len(path)
203 print (key, key[0:l], key[l:], key[l+1:])
204 if key[0:l] == path:
205 if key[l:] != '':
206 if '/' == key[l]:
207 if '/' not in key[l+1:]:
208 flist.append((key[l+1:], 0))
209 else:
210 if '/' not in key[l:]:
211 flist.append((key[l:], 0))
212 flist.append(('.', 0))
213 flist.append(('..', 0))
214 return flist
216 def readdir(self, path, offset):
217 self._log('*** readdir '+str([path, offset]))
218 return self.readdir_compat_0_1(path, offset)
220 def mythread(self):
221 self._log('*** mythread')
222 return -errno.ENOSYS
224 def chmod(self, path, mode):
225 self._log('*** chmod '+str([path, oct(mode)]))
226 self.attrs[path].st_mode = mode
227 return
229 def chown(self, path, uid, gid):
230 self._log('*** chown '+str([path, uid, gid]))
231 return -errno.ENOSYS
233 def fsync(self, path, isFsyncFile, fd=None):
234 self._log('*** fsync '+str([path, isFsyncFile, fd]))
235 return
237 def link(self, targetPath, linkPath):
238 self._log('*** link '+str([targetPath, linkPath]))
239 return -errno.ENOSYS
241 def mkdir(self, path, mode):
242 """Create a directory."""
243 self._log('*** mkdir '+str([path, oct(mode)]))
244 if re.match('^/image/.*$', path):
245 return -errno.EPERM
246 if re.match('^/cat/.*$', path):
247 return -errno.ENOSYS
248 self.files[path] = 0
249 self._setdirattr(path)
250 self.attrs[path].st_mode = stat.S_IFDIR | mode
251 return
253 def mknod(self, path, mode, dev):
254 """Create a file. Not sure what the dev argument is, but it doesn't
255 seem useful."""
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',
265 'intoken': 'edit'}
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')
270 print type(page)
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']
277 return
279 def open(self, path, flags):
280 self._log('*** open '+str([path, flags]))
281 return self._file2fd(path)
283 def read(self, path, length, offset, fd=None):
284 self._log('*** read'+str([path, length, offset, fd]))
285 if fd != None:
286 # we'll just read from the provided StringIO
287 fd.seek(offset)
288 return fd.read(length)
289 else:
290 # this should never really happen
291 try:
292 cwd = self.files[path]
293 except KeyError:
294 return -errno.ENOENT
295 return cwd[offset:length]
297 def readlink(self, path):
298 self._log('*** readlink '+path)
299 return -errno.ENOSYS
301 def release(self, path, flags, fd=None):
302 self._log('*** release '+str([path, flags, fd]))
303 if fd != None:
304 fd.close()
305 return
306 else:
307 # i don't think we really care
308 return
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]
316 return
318 def rmdir(self, path):
319 self._log('*** rmdir '+path)
320 # -errno.ENOTEMPTY will be useful
321 return -errno.ENOSYS
323 def statfs(self):
324 #self._log('*** statfs')
325 vfs = fuse.StatVfs()
326 return vfs
328 def symlink(self, targetPath, linkPath):
329 self._log('*** symlink '+str([targetPath, linkPath]))
330 return -errno.ENOSYS
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])
336 return None
338 def unlink(self, path):
339 self._log('*** unlink '+path)
340 if path not in self.files:
341 return -errno.ENOENT
342 del self.attrs[path]
343 del self.files[path]
344 return
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]
350 return
352 def write(self, path, buf, offset, fd=None):
353 self._log('*** write'+str([path, len(buf), offset, fd]))
354 if fd != None:
355 fd.seek(offset)
356 fd.write(buf)
357 x = self.files[path]
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()
376 return len(buf)
379 def main():
380 usage = """Usage: python %prog [OPTIONS] ROOT_URL MOUNTPOINT
381 fuse-mediawiki is a FUSE filesystem for editing MediaWiki websites.
383 # setup FUSE
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
391 # get arguments
392 (options, args) = fs.parser.parse_args()
393 # if we have zero or two or more arguments before the MOUNTPOINT, fail.
394 if len(args) != 1:
395 print "Must specify exactly 1 root URL and 1 mount point, " + \
396 "in that order"
397 sys.exit(1)
398 # set the root URL
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)
404 # go into main loop
405 fs.main()
408 # hi!
409 if __name__ == '__main__':
410 main()