65
65
# response status. (404 File Not Found, 403 Forbidden, etc), and a header
66
66
# "X-IVLE-Return-Error: <errormessage>".
70
# The most important argument is "action". This determines which action is
71
# taken. Note that action, and all other arguments, are ignored unless the
72
# request is a POST request. The other arguments depend upon the action.
73
# Note that paths are often specified as arguments. Paths that begin with a
74
# slash are taken relative to the user's home directory (the top-level
75
# directory seen when fileservice has no arguments or path). Paths without a
76
# slash are taken relative to the specified path.
78
# action=remove: Delete a file(s) or directory(s) (recursively).
79
# path: The path to the file or directory to delete. Can be specified
82
# action=move: Move or rename a file or directory.
83
# from: The path to the file or directory to be renamed.
84
# to: The path of the target filename. Error if the file already
87
# action=putfile: Upload a file to the student workspace.
88
# path: The path to the file to be written. If it exists, will
89
# overwrite. Error if the target file is a directory.
90
# data: Bytes to be written to the file verbatim. May either be
91
# a string variable or a file upload.
93
# Clipboard-based actions. Cut/copy/paste work in the same way as modern
94
# file browsers, by keeping a server-side clipboard of files that have been
95
# cut and copied. The clipboard is stored in the session data, so it persists
96
# across navigation, tabs and browser windows, but not across browser
99
# action=copy: Write file(s) to the session-based clipboard. Overrides any
100
# existing clipboard data. Does not actually copy the file.
101
# The files are physically copied when the clipboard is pasted.
102
# path: The path to the file or directory to copy. Can be specified
105
# action=cut: Write file(s) to the session-based clipboard. Overrides any
106
# existing clipboard data. Does not actually move the file.
107
# The files are physically moved when the clipboard is pasted.
108
# path: The path to the file or directory to cut. Can be specified
111
# action=paste: Copy or move the files stored in the clipboard. Clears the
112
# clipboard. The files are copied or moved to a specified dir.
113
# path: The path to the DIRECTORY to paste the files to. Must not
116
# Subversion actions.
117
# action=svnadd: Add an existing file(s) to version control.
118
# path: The path to the file to be added. Can be specified multiple
121
# action=svnrevert: Revert a file(s) to its state as of the current revision
122
# / undo local edits.
123
# path: The path to the file to be reverted. Can be specified multiple
126
# action=svnupdate: Bring a file up to date with the head revision.
127
# path: The path to the file to be updated. Can be specified multiple
130
# action=svncommit: Commit a file(s) or directory(s) to the repository.
131
# path: The path to the file or directory to be committed. Can be
132
# specified multiple times. Directories are committed
134
# logmsg: Text of the log message. Optional. There is a default log
135
# message if unspecified.
137
# TODO: Implement the following actions:
138
# move, copy, cut, paste, svnadd, svnrevert, svnupdate, svncommit
68
# See action.py for a full description of the actions.
69
# See listing.py for a full description of the output format of the directory
180
101
# Get all the arguments, if POST.
181
102
# Ignore arguments if not POST, since we aren't allowed to cause
182
103
# side-effects on the server.
185
106
if req.method == 'POST':
186
107
fields = req.get_fieldstorage()
187
action = fields.getfirst('action')
108
act = fields.getfirst('action')
189
if action is not None:
191
handle_action(req, action, fields)
192
except ActionError, message:
112
action.handle_action(req, svnclient, act, fields)
113
except action.ActionError, message:
193
114
req.headers_out['X-IVLE-Action-Error'] = str(message)
197
def handle_action(req, action, fields):
198
"""Perform the "action" part of the response.
199
This function should only be called if the response is a POST.
200
This performs the action's side-effect on the server. If unsuccessful,
201
writes the X-IVLE-Action-Error header to the request object. Otherwise,
202
does not touch the request object. Does NOT write any bytes in response.
204
May throw an ActionError. The caller should put this string into the
205
X-IVLE-Action-Error header, and then continue normally.
207
action: String, the action requested. Not sanitised.
208
fields: FieldStorage object containing all arguments passed.
210
global actions_table # Table of function objects
212
action = actions_table[action]
214
# Default, just send an error but then continue
215
raise ActionError("Unknown action")
218
def handle_return(req):
219
"""Perform the "return" part of the response.
220
This function returns the file or directory listing contained in
221
req.path. Sets the HTTP response code in req, writes additional headers,
222
and writes the HTTP response, if any."""
224
(user, path) = studpath.url_to_local(req.path)
226
# FIXME: What to do about req.path == ""?
227
# Currently goes to 403 Forbidden.
229
req.status = req.HTTP_FORBIDDEN
230
req.headers_out['X-IVLE-Return-Error'] = 'Forbidden'
231
req.write("Forbidden")
232
elif not os.access(path, os.R_OK):
233
req.status = req.HTTP_NOT_FOUND
234
req.headers_out['X-IVLE-Return-Error'] = 'File not found'
235
req.write("File not found")
236
elif os.path.isdir(path):
237
# It's a directory. Return the directory listing.
238
req.content_type = mime_dirlisting
239
req.headers_out['X-IVLE-Return'] = 'Dir'
240
# Start by trying to do an SVN status, so we can report file version
244
status_list = svnclient.status(path, recurse=False, get_all=True,
246
for status in status_list:
247
filename, attrs = PysvnStatus_to_fileinfo(path, status)
248
listing[filename] = attrs
249
except pysvn.ClientError:
250
# Presumably the directory is not under version control.
251
# Fallback to just an OS file listing.
252
for filename in os.listdir(path):
253
listing[filename] = file_to_fileinfo(path, filename)
254
# The subversion one includes "." while the OS one does not.
255
# Add "." to the output, so the caller can see we are
257
listing["."] = {"isdir" : True,
258
"mtime" : time.ctime(os.path.getmtime(path))}
260
req.write(cjson.encode(listing))
262
# It's a file. Return the file contents.
263
# First get the mime type of this file
264
# (Note that importing common.util has already initialised mime types)
265
(type, _) = mimetypes.guess_type(path)
267
type = conf.mimetypes.default_mimetype
268
req.content_type = type
269
req.headers_out['X-IVLE-Return'] = 'File'
273
def file_to_fileinfo(path, filename):
274
"""Given a filename (relative to a given path), gets all the info "ls"
275
needs to display about the filename. Returns a dict containing a number
276
of fields related to the file (excluding the filename itself)."""
277
fullpath = os.path.join(path, filename)
279
file_stat = os.stat(fullpath)
280
if stat.S_ISDIR(file_stat.st_mode):
284
d["size"] = file_stat.st_size
285
(type, _) = mimetypes.guess_type(filename)
287
type = conf.mimetypes.default_mimetype
289
d["mtime"] = time.ctime(file_stat.st_mtime)
292
def PysvnStatus_to_fileinfo(path, status):
293
"""Given a PysvnStatus object, gets all the info "ls"
294
needs to display about the filename. Returns a pair mapping filename to
295
a dict containing a number of other fields."""
296
path = os.path.normcase(path)
297
fullpath = status.path
298
# If this is "." (the directory itself)
299
if path == os.path.normcase(fullpath):
300
# If this directory is unversioned, then we aren't
301
# looking at any interesting files, so throw
302
# an exception and default to normal OS-based listing.
303
if status.text_status == pysvn.wc_status_kind.unversioned:
304
raise pysvn.ClientError
305
# We actually want to return "." because we want its
309
filename = os.path.basename(fullpath)
311
text_status = status.text_status
312
d["svnstatus"] = str(text_status)
314
file_stat = os.stat(fullpath)
315
if stat.S_ISDIR(file_stat.st_mode):
319
d["size"] = file_stat.st_size
320
(type, _) = mimetypes.guess_type(fullpath)
322
type = conf.mimetypes.default_mimetype
324
d["mtime"] = time.ctime(file_stat.st_mtime)
326
# Here if, eg, the file is missing.
327
# Can't get any more information so just return d
333
def actionpath_to_local(req, path):
334
"""Determines the local path upon which an action is intended to act.
335
Note that fileservice actions accept two paths: the request path,
336
and the "path" argument given to the action.
337
According to the rules, if the "path" argument begins with a '/' it is
338
relative to the user's home; if it does not, it is relative to the
341
This resolves the path, given the request and path argument.
343
May raise an ActionError("Invalid path"). The caller is expected to
344
let this fall through to the top-level handler, where it will be
345
put into the HTTP response field. Never returns None.
351
elif len(path) > 0 and path[0] == os.sep:
352
# Relative to student home
355
# Relative to req.path
356
path = os.path.join(req.path, path)
358
_, r = studpath.url_to_local(path)
360
raise ActionError("Invalid path")
363
def action_remove(req, fields):
364
# TODO: Do an SVN rm if the file is versioned.
365
# TODO: Disallow removal of student's home directory
366
"""Removes a list of files or directories.
368
Reads fields: 'path' (multiple)
370
paths = fields.getlist('path')
373
path = actionpath_to_local(req, path)
375
if os.path.isdir(path):
385
raise ActionError("Could not delete the file specified")
388
"Could not delete one or more of the files specified")
390
def action_putfile(req, fields):
391
"""Writes data to a file, overwriting it if it exists and creating it if
394
Reads fields: 'path', 'data' (file upload)
396
path = fields.getfirst('path')
397
data = fields.getfirst('data')
398
if path is None: raise ActionError("No path specified")
399
if data is None: raise ActionError("No data specified")
400
path = actionpath_to_local(req, path)
403
# Copy the contents of file object 'data' to the path 'path'
405
dest = open(path, 'wb')
406
shutil.copyfileobj(data, dest)
408
raise ActionError("Could not write to target file")
410
# Table of all action functions #
413
"remove" : action_remove,
414
"putfile" : action_putfile,
116
listing.handle_return(req, svnclient)