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)
51 def __init__(self
, version
, usage
):
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
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"""
70 return StringIO(self
.files
[path
])
74 def _unescape(self
, text
):
75 """Unescape text in the textarea on retrieval"""
76 text
= text
.replace('<', '<')
77 text
= text
.replace('>', '>')
78 text
= text
.replace('&', '&')
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
112 def _setrooturl(self
, url
):
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
128 self
._log
("Anonymously accessing wiki")
130 self
.username
= username
135 self
.password
= getpass(self
.username
+ "'s password: ")
139 """Attempts to login to the wiki."""
140 print "Logging in..."
141 apivars
= {'lgname': self
.username
, 'lgpassword': self
.password
,
143 response
= self
._apifetch
(apivars
)[0]
144 data
= simplejson
.loads(response
.read())
145 if data
['login']['result'] == "Success":
147 self
._log
("Logged in successfully as %s" % self
.username
)
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'])
159 def getattr(self
, path
):
161 self
._log
('*** getattr '+path
)
162 if path
in self
.attrs
:
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
]
173 def getdir(self
, path
):
174 self
._log
('*** getdir '+path
)
175 keys
= self
.files
.keys()
179 print (key
, key
[0:l
], key
[l
:], key
[l
+1:])
183 if '/' not in key
[l
+1:]:
184 flist
.append((key
[l
+1:], 0))
186 if '/' not in key
[l
:]:
187 flist
.append((key
[l
:], 0))
188 flist
.append(('.', 0))
189 flist
.append(('..', 0))
192 def readdir(self
, path
, offset
):
193 self
._log
('*** readdir '+str([path
, offset
]))
194 return self
.readdir_compat_0_1(path
, offset
)
197 self
._log
('*** mythread')
200 def chmod(self
, path
, mode
):
201 self
._log
('*** chmod '+str([path
, oct(mode
)]))
202 self
.attrs
[path
].st_mode
= mode
205 def chown(self
, path
, uid
, gid
):
206 self
._log
('*** chown '+str([path
, uid
, gid
]))
209 def fsync(self
, path
, isFsyncFile
, fd
=None):
210 self
._log
('*** fsync '+str([path
, isFsyncFile
, fd
]))
213 def link(self
, targetPath
, linkPath
):
214 self
._log
('*** link '+str([targetPath
, linkPath
]))
217 def mkdir(self
, path
, mode
):
218 """Create a directory."""
219 self
._log
('*** mkdir '+str([path
, oct(mode
)]))
220 if re
.match('^/image/.*$', path
):
222 if re
.match('^/cat/.*$', path
):
225 self
._setdirattr
(path
)
226 self
.attrs
[path
].st_mode
= stat
.S_IFDIR | mode
229 def mknod(self
, path
, mode
, dev
):
230 """Create a file. Not sure what the dev argument is, but it doesn't
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',
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')
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']
255 def open(self
, path
, flags
):
256 self
._log
('*** open '+str([path
, flags
]))
257 return self
._file
2fd
(path
)
259 def read(self
, path
, length
, offset
, fd
=None):
260 self
._log
('*** read'+str([path
, length
, offset
, fd
]))
262 # we'll just read from the provided StringIO
264 return fd
.read(length
)
266 # this should never really happen
268 cwd
= self
.files
[path
]
271 return cwd
[offset
:length
]
273 def readlink(self
, path
):
274 self
._log
('*** readlink '+path
)
277 def release(self
, path
, flags
, fd
=None):
278 self
._log
('*** release '+str([path
, flags
, fd
]))
283 # i don't think we really care
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
]
294 def rmdir(self
, path
):
295 self
._log
('*** rmdir '+path
)
296 # -errno.ENOTEMPTY will be useful
300 #self._log('*** statfs')
304 def symlink(self
, targetPath
, linkPath
):
305 self
._log
('*** symlink '+str([targetPath
, linkPath
]))
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
])
314 def unlink(self
, path
):
315 self
._log
('*** unlink '+path
)
316 if path
not in self
.files
:
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]
328 def write(self
, path
, buf
, offset
, fd
=None):
329 self
._log
('*** write'+str([path
, len(buf
), offset
, fd
]))
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()
356 usage
= """Usage: python %prog [OPTIONS] ROOT_URL MOUNTPOINT
357 fuse-mediawiki is a FUSE filesystem for editing MediaWiki websites.
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
368 (options
, args
) = fs
.parser
.parse_args()
369 # if we have zero or two or more arguments before the MOUNTPOINT, fail.
371 print "Must specify exactly 1 api.php URL and 1 mount point, " \
373 print "api.php URL example: http://en.wikipedia.org/w/api.php"
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)
386 if __name__
== '__main__':