~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to www/apps/fileservice/action.py

  • Committer: mattgiuca
  • Date: 2008-01-10 07:25:38 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:166
fileservice:
    * Moved the svnclient object out to the sub-modules (each has their own).
        Makes it easier for them to be global.
    * Added new discussion about how putfile action should change (zip uploads
        and multiple file uploads).
    * Added action svnadd.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# IVLE
 
2
# Copyright (C) 2007-2008 The University of Melbourne
 
3
#
 
4
# This program is free software; you can redistribute it and/or modify
 
5
# it under the terms of the GNU General Public License as published by
 
6
# the Free Software Foundation; either version 2 of the License, or
 
7
# (at your option) any later version.
 
8
#
 
9
# This program is distributed in the hope that it will be useful,
 
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
 
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 
12
# GNU General Public License for more details.
 
13
#
 
14
# You should have received a copy of the GNU General Public License
 
15
# along with this program; if not, write to the Free Software
 
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301  USA
 
17
 
 
18
# Module: File Service / Action
 
19
# Author: Matt Giuca
 
20
# Date: 10/1/2008
 
21
 
 
22
# Handles actions requested by the client as part of the 2-stage process of
 
23
# fileservice (the second part being the return listing).
 
24
 
 
25
### Actions ###
 
26
 
 
27
# The most important argument is "action". This determines which action is
 
28
# taken. Note that action, and all other arguments, are ignored unless the
 
29
# request is a POST request. The other arguments depend upon the action.
 
30
# Note that paths are often specified as arguments. Paths that begin with a
 
31
# slash are taken relative to the user's home directory (the top-level
 
32
# directory seen when fileservice has no arguments or path). Paths without a
 
33
# slash are taken relative to the specified path.
 
34
 
 
35
# action=remove: Delete a file(s) or directory(s) (recursively).
 
36
#       path:   The path to the file or directory to delete. Can be specified
 
37
#               multiple times.
 
38
#
 
39
# action=move: Move or rename a file or directory.
 
40
#       from:   The path to the file or directory to be renamed.
 
41
#       to:     The path of the target filename. Error if the file already
 
42
#               exists.
 
43
#
 
44
# action=putfile: Upload a file to the student workspace, and optionally
 
45
#               accept zip files which will be unpacked.
 
46
#       path:   The path to the file to be written. If it exists, will
 
47
#               overwrite. Error if the target file is a directory.
 
48
#       data:   Bytes to be written to the file verbatim. May either be
 
49
#               a string variable or a file upload.
 
50
#       unpack: Optional. If "true", and the data is a valid ZIP file,
 
51
#               will create a directory instead and unpack the ZIP file
 
52
#               into it.
 
53
#
 
54
# action=putfiles: Upload multiple files to the student workspace, and
 
55
#                 optionally accept zip files which will be unpacked.
 
56
#       path:   The path to the DIRECTORY to place files in. Must not be a
 
57
#               file.
 
58
#       data:   A file upload (may not be a simple string). The filename
 
59
#               will be used to determine the target filename within
 
60
#               the given path.
 
61
#       unpack: Optional. If "true", if any data is a valid ZIP file,
 
62
#               will create a directory instead and unpack the ZIP file
 
63
#               into it.
 
64
#
 
65
# The differences between putfile and putfiles are:
 
66
# * putfile can only accept a single file.
 
67
# * putfile can accept string data, doesn't have to be a file upload.
 
68
# * putfile ignores the upload filename, the entire filename is specified on
 
69
#       path. putfiles calls files after the name on the user's machine.
 
70
#
 
71
# Clipboard-based actions. Cut/copy/paste work in the same way as modern
 
72
# file browsers, by keeping a server-side clipboard of files that have been
 
73
# cut and copied. The clipboard is stored in the session data, so it persists
 
74
# across navigation, tabs and browser windows, but not across browser
 
75
# sessions.
 
76
 
77
# action=copy: Write file(s) to the session-based clipboard. Overrides any
 
78
#               existing clipboard data. Does not actually copy the file.
 
79
#               The files are physically copied when the clipboard is pasted.
 
80
#       path:   The path to the file or directory to copy. Can be specified
 
81
#               multiple times.
 
82
 
83
# action=cut: Write file(s) to the session-based clipboard. Overrides any
 
84
#               existing clipboard data. Does not actually move the file.
 
85
#               The files are physically moved when the clipboard is pasted.
 
86
#       path:   The path to the file or directory to cut. Can be specified
 
87
#               multiple times.
 
88
 
89
# action=paste: Copy or move the files stored in the clipboard. Clears the
 
90
#               clipboard. The files are copied or moved to a specified dir.
 
91
#       path:   The path to the DIRECTORY to paste the files to. Must not
 
92
#               be a file.
 
93
#
 
94
# Subversion actions.
 
95
# action=svnadd: Add an existing file(s) to version control.
 
96
#       path:   The path to the file to be added. Can be specified multiple
 
97
#               times.
 
98
#
 
99
# action=svnrevert: Revert a file(s) to its state as of the current revision
 
100
#               / undo local edits.
 
101
#       path:   The path to the file to be reverted. Can be specified multiple
 
102
#               times.
 
103
#
 
104
# action=svnupdate: Bring a file up to date with the head revision.
 
105
#       path:   The path to the file to be updated. Can be specified multiple
 
106
#               times.
 
107
#
 
108
# action=svncommit: Commit a file(s) or directory(s) to the repository.
 
109
#       path:   The path to the file or directory to be committed. Can be
 
110
#               specified multiple times. Directories are committed
 
111
#               recursively.
 
112
#       logmsg: Text of the log message. Optional. There is a default log
 
113
#               message if unspecified.
 
114
 
115
# TODO: Implement the following actions:
 
116
#   putfiles, svnrevert, svnupdate, svncommit
 
117
# TODO: Implement ZIP unpacking in putfile and putfiles.
 
118
 
 
119
import os
 
120
import shutil
 
121
 
 
122
import pysvn
 
123
 
 
124
from common import (util, studpath)
 
125
 
 
126
# Make a Subversion client object
 
127
svnclient = pysvn.Client()
 
128
 
 
129
DEFAULT_LOGMESSAGE = "No log message supplied."
 
130
 
 
131
# Mime types
 
132
# application/json is the "best" content type but is not good for
 
133
# debugging because Firefox just tries to download it
 
134
mime_dirlisting = "text/html"
 
135
#mime_dirlisting = "application/json"
 
136
 
 
137
class ActionError(Exception):
 
138
    """Represents an error processing an action. This can be
 
139
    raised by any of the action functions, and will be caught
 
140
    by the top-level handler, put into the HTTP response field,
 
141
    and continue.
 
142
 
 
143
    Important Security Consideration: The message passed to this
 
144
    exception will be relayed to the client.
 
145
    """
 
146
    pass
 
147
 
 
148
def handle_action(req, action, fields):
 
149
    """Perform the "action" part of the response.
 
150
    This function should only be called if the response is a POST.
 
151
    This performs the action's side-effect on the server. If unsuccessful,
 
152
    writes the X-IVLE-Action-Error header to the request object. Otherwise,
 
153
    does not touch the request object. Does NOT write any bytes in response.
 
154
 
 
155
    May throw an ActionError. The caller should put this string into the
 
156
    X-IVLE-Action-Error header, and then continue normally.
 
157
 
 
158
    action: String, the action requested. Not sanitised.
 
159
    fields: FieldStorage object containing all arguments passed.
 
160
    """
 
161
    global actions_table        # Table of function objects
 
162
    try:
 
163
        action = actions_table[action]
 
164
    except KeyError:
 
165
        # Default, just send an error but then continue
 
166
        raise ActionError("Unknown action")
 
167
    action(req, fields)
 
168
 
 
169
def actionpath_to_local(req, path):
 
170
    """Determines the local path upon which an action is intended to act.
 
171
    Note that fileservice actions accept two paths: the request path,
 
172
    and the "path" argument given to the action.
 
173
    According to the rules, if the "path" argument begins with a '/' it is
 
174
    relative to the user's home; if it does not, it is relative to the
 
175
    supplied path.
 
176
 
 
177
    This resolves the path, given the request and path argument.
 
178
 
 
179
    May raise an ActionError("Invalid path"). The caller is expected to
 
180
    let this fall through to the top-level handler, where it will be
 
181
    put into the HTTP response field. Never returns None.
 
182
 
 
183
    Does not mutate req.
 
184
    """
 
185
    if path is None:
 
186
        path = req.path
 
187
    elif len(path) > 0 and path[0] == os.sep:
 
188
        # Relative to student home
 
189
        path = path[1:]
 
190
    else:
 
191
        # Relative to req.path
 
192
        path = os.path.join(req.path, path)
 
193
 
 
194
    _, r = studpath.url_to_local(path)
 
195
    if r is None:
 
196
        raise ActionError("Invalid path")
 
197
    return r
 
198
 
 
199
def movefile(req, frompath, topath, copy=False):
 
200
    """Performs a file move, resolving filenames, checking for any errors,
 
201
    and throwing ActionErrors if necessary. Can also be used to do a copy
 
202
    operation instead.
 
203
 
 
204
    frompath and topath are straight paths from the client. Will be checked.
 
205
    """
 
206
    # TODO: Do an SVN mv if the file is versioned.
 
207
    # TODO: Disallow tampering with student's home directory
 
208
    if frompath is None or topath is None:
 
209
        raise ActionError("Required field missing")
 
210
    frompath = actionpath_to_local(req, frompath)
 
211
    topath = actionpath_to_local(req, topath)
 
212
    if not os.path.exists(frompath):
 
213
        raise ActionError("The source file does not exist")
 
214
    if os.path.exists(topath):
 
215
        if frompath == topath:
 
216
            raise ActionError("Source and destination are the same")
 
217
        raise ActionError("Another file already exists with that name")
 
218
 
 
219
    try:
 
220
        if copy:
 
221
            if os.path.isdir(frompath):
 
222
                shutil.copytree(frompath, topath)
 
223
            else:
 
224
                shutil.copy2(frompath, topath)
 
225
        else:
 
226
            shutil.move(frompath, topath)
 
227
    except OSError:
 
228
        raise ActionError("Could not move the file specified")
 
229
    except shutil.Error:
 
230
        raise ActionError("Could not move the file specified")
 
231
 
 
232
### ACTIONS ###
 
233
 
 
234
def action_remove(req, fields):
 
235
    # TODO: Do an SVN rm if the file is versioned.
 
236
    # TODO: Disallow removal of student's home directory
 
237
    """Removes a list of files or directories.
 
238
 
 
239
    Reads fields: 'path' (multiple)
 
240
    """
 
241
    paths = fields.getlist('path')
 
242
    goterror = False
 
243
    for path in paths:
 
244
        path = actionpath_to_local(req, path)
 
245
        try:
 
246
            if os.path.isdir(path):
 
247
                shutil.rmtree(path)
 
248
            else:
 
249
                os.remove(path)
 
250
        except OSError:
 
251
            goterror = True
 
252
        except shutil.Error:
 
253
            goterror = True
 
254
    if goterror:
 
255
        if len(paths) == 1:
 
256
            raise ActionError("Could not delete the file specified")
 
257
        else:
 
258
            raise ActionError(
 
259
                "Could not delete one or more of the files specified")
 
260
 
 
261
def action_move(req, fields):
 
262
    # TODO: Do an SVN mv if the file is versioned.
 
263
    # TODO: Disallow tampering with student's home directory
 
264
    """Removes a list of files or directories.
 
265
 
 
266
    Reads fields: 'from', 'to'
 
267
    """
 
268
    frompath = fields.getfirst('from')
 
269
    topath = fields.getfirst('to')
 
270
    movefile(req, frompath, topath)
 
271
 
 
272
def action_putfile(req, fields):
 
273
    """Writes data to a file, overwriting it if it exists and creating it if
 
274
    it doesn't.
 
275
 
 
276
    Reads fields: 'path', 'data' (file upload)
 
277
    """
 
278
    path = fields.getfirst('path')
 
279
    data = fields.getfirst('data')
 
280
    if path is None or data is None:
 
281
        raise ActionError("Required field missing")
 
282
    path = actionpath_to_local(req, path)
 
283
    data = data.file
 
284
 
 
285
    # Copy the contents of file object 'data' to the path 'path'
 
286
    try:
 
287
        dest = open(path, 'wb')
 
288
        shutil.copyfileobj(data, dest)
 
289
    except OSError:
 
290
        raise ActionError("Could not write to target file")
 
291
 
 
292
def action_copy_or_cut(req, fields, mode):
 
293
    """Marks specified files on the clipboard, stored in the
 
294
    browser session. Sets clipboard for either a cut or copy operation
 
295
    as specified.
 
296
 
 
297
    Reads fields: 'path'
 
298
    """
 
299
    # The clipboard object created conforms to the JSON clipboard
 
300
    # specification given at the top of listing.py.
 
301
    # Note that we do not check for the existence of files here. That is done
 
302
    # in the paste operation.
 
303
    files = fields.getlist('path')
 
304
    files = map(lambda field: field.value, files)
 
305
    clipboard = { "mode" : mode, "base" : req.path, "files" : files }
 
306
    session = req.get_session()
 
307
    session['clipboard'] = clipboard
 
308
    session.save()
 
309
 
 
310
def action_copy(req, fields):
 
311
    """Marks specified files on the clipboard, stored in the
 
312
    browser session. Sets clipboard for a "copy" action.
 
313
 
 
314
    Reads fields: 'path'
 
315
    """
 
316
    action_copy_or_cut(req, fields, "copy")
 
317
 
 
318
def action_cut(req, fields):
 
319
    """Marks specified files on the clipboard, stored in the
 
320
    browser session. Sets clipboard for a "cut" action.
 
321
 
 
322
    Reads fields: 'path'
 
323
    """
 
324
    action_copy_or_cut(req, fields, "cut")
 
325
 
 
326
def action_paste(req, fields):
 
327
    """Performs the copy or move action with the files stored on
 
328
    the clipboard in the browser session. Copies/moves the files
 
329
    to the specified directory. Clears the clipboard.
 
330
 
 
331
    Reads fields: 'path'
 
332
    """
 
333
    errormsg = None
 
334
 
 
335
    todir = fields.getfirst('path')
 
336
    if todir is None:
 
337
        raise ActionError("Required field missing")
 
338
    todir_local = actionpath_to_local(req, todir)
 
339
    if not os.path.isdir(todir_local):
 
340
        raise ActionError("Target is not a directory")
 
341
 
 
342
    session = req.get_session()
 
343
    try:
 
344
        clipboard = session['clipboard']
 
345
        files = clipboard['files']
 
346
        base = clipboard['base']
 
347
        if clipboard['mode'] == "copy":
 
348
            copy = True
 
349
        else:
 
350
            copy = False
 
351
    except KeyError:
 
352
        raise ActionError("Clipboard was empty")
 
353
 
 
354
    errorfiles = []
 
355
    for file in files:
 
356
        # The source must not be interpreted as relative to req.path
 
357
        # Add a slash (relative to top-level)
 
358
        frompath = os.sep + os.path.join(base, file)
 
359
        # The destination is found by taking just the basename of the file
 
360
        topath = os.path.join(todir, os.path.basename(file))
 
361
        try:
 
362
            movefile(req, frompath, topath, copy)
 
363
        except ActionError, message:
 
364
            # Store the error for later; we want to copy as many as possible
 
365
            if errormsg is None:
 
366
                errormsg = message
 
367
            else:
 
368
                # Multiple errors; generic message
 
369
                errormsg = "One or more files could not be pasted"
 
370
            # Add this file to errorfiles; it will be put back on the
 
371
            # clipboard for possible future pasting.
 
372
            errorfiles.append(file)
 
373
    # If errors occured, augment the clipboard and raise ActionError
 
374
    if len(errorfiles) > 0:
 
375
        clipboard['files'] = errorfiles
 
376
        session['clipboard'] = clipboard
 
377
        session.save()
 
378
        raise ActionError(errormsg)
 
379
 
 
380
    # Success: Clear the clipboard
 
381
    del session['clipboard']
 
382
    session.save()
 
383
 
 
384
def action_svnadd(req, fields):
 
385
    """Performs a "svn add" to each file specified.
 
386
 
 
387
    Reads fields: 'path'
 
388
    """
 
389
    paths = fields.getlist('path')
 
390
    paths = map(lambda path: actionpath_to_local(req, path), paths)
 
391
 
 
392
    try:
 
393
        svnclient.add(paths, recurse=True, force=True)
 
394
    except pysvn.ClientError:
 
395
        raise ActionError("One or more files could not be added")
 
396
 
 
397
# Table of all action functions #
 
398
# Each function has the interface f(req, fields).
 
399
 
 
400
actions_table = {
 
401
    "remove" : action_remove,
 
402
    "move" : action_move,
 
403
    "putfile" : action_putfile,
 
404
 
 
405
    "copy" : action_copy,
 
406
    "cut" : action_cut,
 
407
    "paste" : action_paste,
 
408
 
 
409
    "svnadd" : action_svnadd,
 
410
}