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

« back to all changes in this revision

Viewing changes to lib/fileservice_lib/action.py

  • Committer: stevenbird
  • Date: 2008-02-04 04:22:44 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:400
first commit of homepage at ivle.sourceforge.net, including screenshots; just web-presence at this stage

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. Only one file may be
106
 
#               specified.
107
 
#
108
 
# action=svnpublish: Set the "published" flag on a file to True.
109
 
#       path:   The path to the file to be published. Can be specified
110
 
#               multiple times.
111
 
#
112
 
# action=svnunpublish: Set the "published" flag on a file to False.
113
 
#       path:   The path to the file to be unpublished. Can be specified
114
 
#               multiple times.
115
 
#
116
 
# action=svncommit: Commit a file(s) or directory(s) to the repository.
117
 
#       path:   The path to the file or directory to be committed. Can be
118
 
#               specified multiple times. Directories are committed
119
 
#               recursively.
120
 
#       logmsg: Text of the log message. Optional. There is a default log
121
 
#               message if unspecified.
122
 
123
 
# TODO: Implement the following actions:
124
 
#   putfiles, svnrevert, svnupdate, svncommit
125
 
# TODO: Implement ZIP unpacking in putfile and putfiles.
126
 
# TODO: svnupdate needs a digest to tell the user the files that were updated.
127
 
#   This can be implemented by some message passing between action and
128
 
#   listing, and having the digest included in the listing. (Problem if
129
 
#   the listing is not a directory, but we could make it an error to do an
130
 
#   update if the path is not a directory).
131
 
 
132
 
import os
133
 
import cStringIO
134
 
import shutil
135
 
 
136
 
import pysvn
137
 
 
138
 
from common import (util, studpath, zip)
139
 
 
140
 
# Make a Subversion client object
141
 
svnclient = pysvn.Client()
142
 
 
143
 
DEFAULT_LOGMESSAGE = "No log message supplied."
144
 
 
145
 
# Mime types
146
 
# application/json is the "best" content type but is not good for
147
 
# debugging because Firefox just tries to download it
148
 
mime_dirlisting = "text/html"
149
 
#mime_dirlisting = "application/json"
150
 
 
151
 
class ActionError(Exception):
152
 
    """Represents an error processing an action. This can be
153
 
    raised by any of the action functions, and will be caught
154
 
    by the top-level handler, put into the HTTP response field,
155
 
    and continue.
156
 
 
157
 
    Important Security Consideration: The message passed to this
158
 
    exception will be relayed to the client.
159
 
    """
160
 
    pass
161
 
 
162
 
def handle_action(req, action, fields):
163
 
    """Perform the "action" part of the response.
164
 
    This function should only be called if the response is a POST.
165
 
    This performs the action's side-effect on the server. If unsuccessful,
166
 
    writes the X-IVLE-Action-Error header to the request object. Otherwise,
167
 
    does not touch the request object. Does NOT write any bytes in response.
168
 
 
169
 
    May throw an ActionError. The caller should put this string into the
170
 
    X-IVLE-Action-Error header, and then continue normally.
171
 
 
172
 
    action: String, the action requested. Not sanitised.
173
 
    fields: FieldStorage object containing all arguments passed.
174
 
    """
175
 
    global actions_table        # Table of function objects
176
 
    try:
177
 
        action = actions_table[action]
178
 
    except KeyError:
179
 
        # Default, just send an error but then continue
180
 
        raise ActionError("Unknown action")
181
 
    action(req, fields)
182
 
 
183
 
def actionpath_to_urlpath(req, path):
184
 
    """Determines the URL path (relative to the student home) upon which the
185
 
    action is intended to act. See actionpath_to_local.
186
 
    """
187
 
    if path is None:
188
 
        return req.path
189
 
    elif len(path) > 0 and path[0] == os.sep:
190
 
        # Relative to student home
191
 
        return path[1:]
192
 
    else:
193
 
        # Relative to req.path
194
 
        return os.path.join(req.path, path)
195
 
 
196
 
def actionpath_to_local(req, path):
197
 
    """Determines the local path upon which an action is intended to act.
198
 
    Note that fileservice actions accept two paths: the request path,
199
 
    and the "path" argument given to the action.
200
 
    According to the rules, if the "path" argument begins with a '/' it is
201
 
    relative to the user's home; if it does not, it is relative to the
202
 
    supplied path.
203
 
 
204
 
    This resolves the path, given the request and path argument.
205
 
 
206
 
    May raise an ActionError("Invalid path"). The caller is expected to
207
 
    let this fall through to the top-level handler, where it will be
208
 
    put into the HTTP response field. Never returns None.
209
 
 
210
 
    Does not mutate req.
211
 
    """
212
 
    (_, _, r) = studpath.url_to_jailpaths(actionpath_to_urlpath(req, path))
213
 
    if r is None:
214
 
        raise ActionError("Invalid path")
215
 
    return r
216
 
 
217
 
def movefile(req, frompath, topath, copy=False):
218
 
    """Performs a file move, resolving filenames, checking for any errors,
219
 
    and throwing ActionErrors if necessary. Can also be used to do a copy
220
 
    operation instead.
221
 
 
222
 
    frompath and topath are straight paths from the client. Will be checked.
223
 
    """
224
 
    # TODO: Do an SVN mv if the file is versioned.
225
 
    # TODO: Disallow tampering with student's home directory
226
 
    if frompath is None or topath is None:
227
 
        raise ActionError("Required field missing")
228
 
    frompath = actionpath_to_local(req, frompath)
229
 
    topath = actionpath_to_local(req, topath)
230
 
    if not os.path.exists(frompath):
231
 
        raise ActionError("The source file does not exist")
232
 
    if os.path.exists(topath):
233
 
        if frompath == topath:
234
 
            raise ActionError("Source and destination are the same")
235
 
        raise ActionError("Another file already exists with that name")
236
 
 
237
 
    try:
238
 
        if copy:
239
 
            if os.path.isdir(frompath):
240
 
                shutil.copytree(frompath, topath)
241
 
            else:
242
 
                shutil.copy2(frompath, topath)
243
 
        else:
244
 
            shutil.move(frompath, topath)
245
 
    except OSError:
246
 
        raise ActionError("Could not move the file specified")
247
 
    except shutil.Error:
248
 
        raise ActionError("Could not move the file specified")
249
 
 
250
 
### ACTIONS ###
251
 
 
252
 
def action_remove(req, fields):
253
 
    # TODO: Do an SVN rm if the file is versioned.
254
 
    # TODO: Disallow removal of student's home directory
255
 
    """Removes a list of files or directories.
256
 
 
257
 
    Reads fields: 'path' (multiple)
258
 
    """
259
 
    paths = fields.getlist('path')
260
 
    goterror = False
261
 
    for path in paths:
262
 
        path = actionpath_to_local(req, path)
263
 
        try:
264
 
            if os.path.isdir(path):
265
 
                shutil.rmtree(path)
266
 
            else:
267
 
                os.remove(path)
268
 
        except OSError:
269
 
            goterror = True
270
 
        except shutil.Error:
271
 
            goterror = True
272
 
    if goterror:
273
 
        if len(paths) == 1:
274
 
            raise ActionError("Could not delete the file specified")
275
 
        else:
276
 
            raise ActionError(
277
 
                "Could not delete one or more of the files specified")
278
 
 
279
 
def action_move(req, fields):
280
 
    # TODO: Do an SVN mv if the file is versioned.
281
 
    # TODO: Disallow tampering with student's home directory
282
 
    """Removes a list of files or directories.
283
 
 
284
 
    Reads fields: 'from', 'to'
285
 
    """
286
 
    frompath = fields.getfirst('from')
287
 
    topath = fields.getfirst('to')
288
 
    movefile(req, frompath, topath)
289
 
 
290
 
def action_putfile(req, fields):
291
 
    """Writes data to a file, overwriting it if it exists and creating it if
292
 
    it doesn't.
293
 
 
294
 
    Reads fields: 'path', 'data' (file upload)
295
 
    """
296
 
    # TODO: Read field "unpack".
297
 
    # Important: Data is "None" if the file submitted is empty.
298
 
    path = fields.getfirst('path')
299
 
    data = fields.getfirst('data')
300
 
    if path is None or data is None:
301
 
        raise ActionError("Required field missing")
302
 
    path = actionpath_to_local(req, path)
303
 
 
304
 
    if data is not None:
305
 
        data = cStringIO.StringIO(data)
306
 
 
307
 
    # Copy the contents of file object 'data' to the path 'path'
308
 
    try:
309
 
        dest = open(path, 'wb')
310
 
        if data is not None:
311
 
            shutil.copyfileobj(data, dest)
312
 
    except OSError:
313
 
        raise ActionError("Could not write to target file")
314
 
 
315
 
def action_putfiles(req, fields):
316
 
    """Writes data to one or more files in a directory, overwriting them if
317
 
    it they exist.
318
 
 
319
 
    Reads fields: 'path', 'data' (file upload, multiple), 'unpack'
320
 
    """
321
 
 
322
 
    # Important: Data is "None" if the file submitted is empty.
323
 
    path = fields.getfirst('path')
324
 
    data = fields['data']
325
 
    if type(data) != type([]):
326
 
        data = [data]
327
 
    unpack = fields.getfirst('unpack')
328
 
    if unpack is None:
329
 
        unpack = False
330
 
    else:
331
 
        unpack = True
332
 
    if path is None:
333
 
        raise ActionError("Required field missing")
334
 
    path = actionpath_to_urlpath(req, path)
335
 
    goterror = False
336
 
 
337
 
 
338
 
    for datum in data:
339
 
        # Each of the uploaded files
340
 
        filepath = os.path.join(path, datum.filename)
341
 
        filedata = datum.value
342
 
 
343
 
        if unpack and datum.filename.lower().endswith(".zip"):
344
 
            # A zip file - unpack it instead of just copying
345
 
            # TODO: Use the magic number instead of file extension
346
 
            # Note: Just unzip into the current directory (ignore the
347
 
            # filename)
348
 
            try:
349
 
                zip.unzip(path, filedata)
350
 
            except (OSError, IOError):
351
 
                goterror = True
352
 
        else:
353
 
            # Not a zip file
354
 
            (_, _, filepath_local) = studpath.url_to_jailpaths(filepath)
355
 
            if filepath_local is None:
356
 
                raise ActionError("Invalid path")
357
 
 
358
 
            # Copy the contents of file object 'data' to the path 'path'
359
 
            try:
360
 
                dest = open(filepath_local, 'wb')
361
 
                if data is not None:
362
 
                    shutil.copyfileobj(cStringIO.StringIO(filedata), dest)
363
 
            except OSError:
364
 
                goterror = True
365
 
 
366
 
    if goterror:
367
 
        if len(data) == 1:
368
 
            raise ActionError("Could not write to target file")
369
 
        else:
370
 
            raise ActionError(
371
 
                "Could not write to one or more of the target files")
372
 
 
373
 
def action_copy_or_cut(req, fields, mode):
374
 
    """Marks specified files on the clipboard, stored in the
375
 
    browser session. Sets clipboard for either a cut or copy operation
376
 
    as specified.
377
 
 
378
 
    Reads fields: 'path'
379
 
    """
380
 
    # The clipboard object created conforms to the JSON clipboard
381
 
    # specification given at the top of listing.py.
382
 
    # Note that we do not check for the existence of files here. That is done
383
 
    # in the paste operation.
384
 
    files = fields.getlist('path')
385
 
    clipboard = { "mode" : mode, "base" : req.path, "files" : files }
386
 
    session = req.get_session()
387
 
    session['clipboard'] = clipboard
388
 
    session.save()
389
 
 
390
 
def action_copy(req, fields):
391
 
    """Marks specified files on the clipboard, stored in the
392
 
    browser session. Sets clipboard for a "copy" action.
393
 
 
394
 
    Reads fields: 'path'
395
 
    """
396
 
    action_copy_or_cut(req, fields, "copy")
397
 
 
398
 
def action_cut(req, fields):
399
 
    """Marks specified files on the clipboard, stored in the
400
 
    browser session. Sets clipboard for a "cut" action.
401
 
 
402
 
    Reads fields: 'path'
403
 
    """
404
 
    action_copy_or_cut(req, fields, "cut")
405
 
 
406
 
def action_paste(req, fields):
407
 
    """Performs the copy or move action with the files stored on
408
 
    the clipboard in the browser session. Copies/moves the files
409
 
    to the specified directory. Clears the clipboard.
410
 
 
411
 
    Reads fields: 'path'
412
 
    """
413
 
    errormsg = None
414
 
 
415
 
    todir = fields.getfirst('path')
416
 
    if todir is None:
417
 
        raise ActionError("Required field missing")
418
 
    todir_local = actionpath_to_local(req, todir)
419
 
    if not os.path.isdir(todir_local):
420
 
        raise ActionError("Target is not a directory")
421
 
 
422
 
    session = req.get_session()
423
 
    try:
424
 
        clipboard = session['clipboard']
425
 
        files = clipboard['files']
426
 
        base = clipboard['base']
427
 
        if clipboard['mode'] == "copy":
428
 
            copy = True
429
 
        else:
430
 
            copy = False
431
 
    except KeyError:
432
 
        raise ActionError("Clipboard was empty")
433
 
 
434
 
    errorfiles = []
435
 
    for file in files:
436
 
        # The source must not be interpreted as relative to req.path
437
 
        # Add a slash (relative to top-level)
438
 
        frompath = os.sep + os.path.join(base, file)
439
 
        # The destination is found by taking just the basename of the file
440
 
        topath = os.path.join(todir, os.path.basename(file))
441
 
        try:
442
 
            movefile(req, frompath, topath, copy)
443
 
        except ActionError, message:
444
 
            # Store the error for later; we want to copy as many as possible
445
 
            if errormsg is None:
446
 
                errormsg = message
447
 
            else:
448
 
                # Multiple errors; generic message
449
 
                errormsg = "One or more files could not be pasted"
450
 
            # Add this file to errorfiles; it will be put back on the
451
 
            # clipboard for possible future pasting.
452
 
            errorfiles.append(file)
453
 
    # If errors occured, augment the clipboard and raise ActionError
454
 
    if len(errorfiles) > 0:
455
 
        clipboard['files'] = errorfiles
456
 
        session['clipboard'] = clipboard
457
 
        session.save()
458
 
        raise ActionError(errormsg)
459
 
 
460
 
    # Success: Clear the clipboard
461
 
    del session['clipboard']
462
 
    session.save()
463
 
 
464
 
def action_svnadd(req, fields):
465
 
    """Performs a "svn add" to each file specified.
466
 
 
467
 
    Reads fields: 'path' (multiple)
468
 
    """
469
 
    paths = fields.getlist('path')
470
 
    paths = map(lambda path: actionpath_to_local(req, path), paths)
471
 
 
472
 
    try:
473
 
        svnclient.add(paths, recurse=True, force=True)
474
 
    except pysvn.ClientError:
475
 
        raise ActionError("One or more files could not be added")
476
 
 
477
 
def action_svnupdate(req, fields):
478
 
    """Performs a "svn update" to each file specified.
479
 
 
480
 
    Reads fields: 'path'
481
 
    """
482
 
    path = fields.getfirst('path')
483
 
    if path is None:
484
 
        raise ActionError("Required field missing")
485
 
    path = actionpath_to_local(req, path)
486
 
 
487
 
    try:
488
 
        svnclient.update(path, recurse=True)
489
 
    except pysvn.ClientError:
490
 
        raise ActionError("One or more files could not be updated")
491
 
 
492
 
def action_svnrevert(req, fields):
493
 
    """Performs a "svn revert" to each file specified.
494
 
 
495
 
    Reads fields: 'path' (multiple)
496
 
    """
497
 
    paths = fields.getlist('path')
498
 
    paths = map(lambda path: actionpath_to_local(req, path), paths)
499
 
 
500
 
    try:
501
 
        svnclient.revert(paths, recurse=True)
502
 
    except pysvn.ClientError:
503
 
        raise ActionError("One or more files could not be reverted")
504
 
 
505
 
def action_svnpublish(req, fields):
506
 
    """Sets svn property "ivle:published" on each file specified.
507
 
    Should only be called on directories (only effective on directories
508
 
    anyway).
509
 
 
510
 
    Reads fields: 'path'
511
 
    """
512
 
    paths = fields.getlist('path')
513
 
    paths = map(lambda path: actionpath_to_local(req, path), paths)
514
 
 
515
 
    try:
516
 
        for path in paths:
517
 
            # Note: Property value doesn't matter
518
 
            svnclient.propset("ivle:published", "", path, recurse=False)
519
 
    except pysvn.ClientError, e:
520
 
        raise ActionError("Directory could not be published")
521
 
 
522
 
def action_svnunpublish(req, fields):
523
 
    """Deletes svn property "ivle:published" on each file specified.
524
 
 
525
 
    Reads fields: 'path'
526
 
    """
527
 
    paths = fields.getlist('path')
528
 
    paths = map(lambda path: actionpath_to_local(req, path), paths)
529
 
 
530
 
    try:
531
 
        for path in paths:
532
 
            svnclient.propdel("ivle:published", path, recurse=False)
533
 
    except pysvn.ClientError:
534
 
        raise ActionError("Directory could not be unpublished")
535
 
 
536
 
def action_svncommit(req, fields):
537
 
    """Performs a "svn commit" to each file specified.
538
 
 
539
 
    Reads fields: 'path' (multiple), 'logmsg' (optional)
540
 
    """
541
 
    paths = fields.getlist('path')
542
 
    paths = map(lambda path: actionpath_to_local(req, str(path)), paths)
543
 
    logmsg = str(fields.getfirst('logmsg', DEFAULT_LOGMESSAGE))
544
 
    if logmsg == '': logmsg = DEFAULT_LOGMESSAGE
545
 
 
546
 
    try:
547
 
        svnclient.checkin(paths, logmsg, recurse=True)
548
 
    except pysvn.ClientError:
549
 
        raise ActionError("One or more files could not be committed")
550
 
 
551
 
# Table of all action functions #
552
 
# Each function has the interface f(req, fields).
553
 
 
554
 
actions_table = {
555
 
    "remove" : action_remove,
556
 
    "move" : action_move,
557
 
    "putfile" : action_putfile,
558
 
    "putfiles" : action_putfiles,
559
 
 
560
 
    "copy" : action_copy,
561
 
    "cut" : action_cut,
562
 
    "paste" : action_paste,
563
 
 
564
 
    "svnadd" : action_svnadd,
565
 
    "svnupdate" : action_svnupdate,
566
 
    "svnrevert" : action_svnrevert,
567
 
    "svnpublish" : action_svnpublish,
568
 
    "svnunpublish" : action_svnunpublish,
569
 
    "svncommit" : action_svncommit,
570
 
}