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

« back to all changes in this revision

Viewing changes to lib/fileservice_lib/action.py

  • Committer: agdimech
  • Date: 2008-02-29 00:14:16 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:616
/console/console.js: Added dynamic scrolling for the console.

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