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

« back to all changes in this revision

Viewing changes to ivle/fileservice_lib/action.py

  • Committer: chadnickbok
  • Date: 2009-01-19 22:56:46 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:1170
This commit fixes issue #10 and part of issue #9

There are now two options for moving files with their
svn history intact; svn move and svn copy. These
use the svn commands to move the files, allowing students
to move and rename files without their histories being
lost.

This commit also shows the svn status of a dir, if it is
the 'head' of an svn repository.

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.
 
45
#       path:   The path to the file to be written. Error if the target
 
46
#               file is a directory.
 
47
#       data:   Bytes to be written to the file verbatim. May either be
 
48
#               a string variable or a file upload.
 
49
#       overwrite: Optional. If supplied, the file will be overwritten.
 
50
#               Otherwise, error if path already exists.
 
51
#
 
52
# action=putfiles: Upload multiple files to the student workspace, and
 
53
#                 optionally accept zip files which will be unpacked.
 
54
#       path:   The path to the DIRECTORY to place files in. Must not be a
 
55
#               file.
 
56
#       data:   A file upload (may not be a simple string). The filename
 
57
#               will be used to determine the target filename within
 
58
#               the given path.
 
59
#       unpack: Optional. If supplied, if any data is a valid ZIP file,
 
60
#               will create a directory instead and unpack the ZIP file
 
61
#               into it.
 
62
#
 
63
# action=mkdir: Create a directory. The parent dir must exist.
 
64
#       path:   The path to a file which does not exist, but whose parent
 
65
#               does. The dir will be made with this name.
 
66
#
 
67
# The differences between putfile and putfiles are:
 
68
# * putfile can only accept a single file, and can't unpack zipfiles.
 
69
# * putfile can accept string data, doesn't have to be a file upload.
 
70
# * putfile ignores the upload filename, the entire filename is specified on
 
71
#       path. putfiles calls files after the name on the user's machine.
 
72
#
 
73
# action=paste: Copy or move the files to a specified dir.
 
74
#       src:    The path to the DIRECTORY to get the files from (relative).
 
75
#       dst:    The path to the DIRECTORY to paste the files to. Must not
 
76
#               be a file.
 
77
#       mode:   'copy' or 'move'
 
78
#       file:   File to be copied or moved, relative to src, to a destination
 
79
#               relative to dst. Can be specified multiple times.
 
80
#
 
81
# Subversion actions.
 
82
# action=svnadd: Add an existing file(s) to version control.
 
83
#       path:   The path to the file to be added. Can be specified multiple
 
84
#               times.
 
85
#
 
86
# action=svnrevert: Revert a file(s) to its state as of the current revision
 
87
#               / undo local edits.
 
88
#       path:   The path to the file to be reverted. Can be specified multiple
 
89
#               times.
 
90
#
 
91
# action=svnupdate: Bring a file up to date with the head revision.
 
92
#       path:   The path to the file to be updated. Only one file may be
 
93
#               specified.
 
94
#
 
95
# action=svnpublish: Set the "published" flag on a file to True.
 
96
#       path:   The path to the file to be published. Can be specified
 
97
#               multiple times.
 
98
#
 
99
# action=svnunpublish: Set the "published" flag on a file to False.
 
100
#       path:   The path to the file to be unpublished. Can be specified
 
101
#               multiple times.
 
102
#
 
103
# action=svncommit: Commit a file(s) or directory(s) to the repository.
 
104
#       path:   The path to the file or directory to be committed. Can be
 
105
#               specified multiple times. Directories are committed
 
106
#               recursively.
 
107
#       logmsg: Text of the log message. Optional. There is a default log
 
108
#               message if unspecified.
 
109
# action=svncheckout: Checkout a file/directory from the repository.
 
110
#       path:   The [repository] path to the file or directory to be
 
111
#               checked out.
 
112
 
113
# action=svnrepomkdir: Create a directory in a repository (not WC).
 
114
#       path:   The path to the directory to be created (under the IVLE
 
115
#               repository base).
 
116
#       logmsg: Text of the log message.
 
117
 
118
# action=svnrepostat: Check if a path exists in a repository (not WC).
 
119
#       path:   The path to the directory to be checked (under the IVLE
 
120
#               repository base).
 
121
#
 
122
# TODO: Implement the following actions:
 
123
#   svnupdate (done?)
 
124
# TODO: Implement ZIP unpacking in putfiles (done?).
 
125
# TODO: svnupdate needs a digest to tell the user the files that were updated.
 
126
#   This can be implemented by some message passing between action and
 
127
#   listing, and having the digest included in the listing. (Problem if
 
128
#   the listing is not a directory, but we could make it an error to do an
 
129
#   update if the path is not a directory).
 
130
 
 
131
import os
 
132
import cStringIO
 
133
import shutil
 
134
 
 
135
import pysvn
 
136
 
 
137
from ivle import (util, studpath, zip)
 
138
import ivle.conf
 
139
 
 
140
def get_login(_realm, existing_login, _may_save):
 
141
    """Callback function used by pysvn for authentication.
 
142
    realm, existing_login, _may_save: The 3 arguments passed by pysvn to
 
143
        callback_get_login.
 
144
        The following has been determined empirically, not from docs:
 
145
        existing_login will be the name of the user who owns the process on
 
146
        the first attempt, "" on subsequent attempts. We use this fact.
 
147
    """
 
148
    # Only provide credentials on the _first_ attempt.
 
149
    # If we're being asked again, then it means the credentials failed for
 
150
    # some reason and we should just fail. (This is not desirable, but it's
 
151
    # better than being asked an infinite number of times).
 
152
    return (existing_login != "", ivle.conf.login, ivle.conf.svn_pass, True)
 
153
 
 
154
# Make a Subversion client object
 
155
svnclient = pysvn.Client()
 
156
svnclient.callback_get_login = get_login
 
157
svnclient.exception_style = 0               # Simple (string) exceptions
 
158
 
 
159
DEFAULT_LOGMESSAGE = "No log message supplied."
 
160
 
 
161
# Mime types
 
162
# application/json is the "best" content type but is not good for
 
163
# debugging because Firefox just tries to download it
 
164
mime_dirlisting = "text/html"
 
165
#mime_dirlisting = "application/json"
 
166
 
 
167
class ActionError(Exception):
 
168
    """Represents an error processing an action. This can be
 
169
    raised by any of the action functions, and will be caught
 
170
    by the top-level handler, put into the HTTP response field,
 
171
    and continue.
 
172
 
 
173
    Important Security Consideration: The message passed to this
 
174
    exception will be relayed to the client.
 
175
    """
 
176
    pass
 
177
 
 
178
def handle_action(req, action, fields):
 
179
    """Perform the "action" part of the response.
 
180
    This function should only be called if the response is a POST.
 
181
    This performs the action's side-effect on the server. If unsuccessful,
 
182
    writes the X-IVLE-Action-Error header to the request object. Otherwise,
 
183
    does not touch the request object. Does NOT write any bytes in response.
 
184
 
 
185
    May throw an ActionError. The caller should put this string into the
 
186
    X-IVLE-Action-Error header, and then continue normally.
 
187
 
 
188
    action: String, the action requested. Not sanitised.
 
189
    fields: FieldStorage object containing all arguments passed.
 
190
    """
 
191
    global actions_table        # Table of function objects
 
192
    try:
 
193
        action = actions_table[action]
 
194
    except KeyError:
 
195
        # Default, just send an error but then continue
 
196
        raise ActionError("Unknown action")
 
197
    action(req, fields)
 
198
 
 
199
def actionpath_to_urlpath(req, path):
 
200
    """Determines the URL path (relative to the student home) upon which the
 
201
    action is intended to act. See actionpath_to_local.
 
202
    """
 
203
    if path is None:
 
204
        return req.path
 
205
    elif len(path) > 0 and path[0] == os.sep:
 
206
        # Relative to student home
 
207
        return path[1:]
 
208
    else:
 
209
        # Relative to req.path
 
210
        return os.path.join(req.path, path)
 
211
 
 
212
def actionpath_to_local(req, path):
 
213
    """Determines the local path upon which an action is intended to act.
 
214
    Note that fileservice actions accept two paths: the request path,
 
215
    and the "path" argument given to the action.
 
216
    According to the rules, if the "path" argument begins with a '/' it is
 
217
    relative to the user's home; if it does not, it is relative to the
 
218
    supplied path.
 
219
 
 
220
    This resolves the path, given the request and path argument.
 
221
 
 
222
    May raise an ActionError("Invalid path"). The caller is expected to
 
223
    let this fall through to the top-level handler, where it will be
 
224
    put into the HTTP response field. Never returns None.
 
225
 
 
226
    Does not mutate req.
 
227
    """
 
228
    (_, _, r) = studpath.url_to_jailpaths(actionpath_to_urlpath(req, path))
 
229
    if r is None:
 
230
        raise ActionError("Invalid path")
 
231
    return r
 
232
 
 
233
def movefile(req, frompath, topath, copy=False):
 
234
    """Performs a file move, resolving filenames, checking for any errors,
 
235
    and throwing ActionErrors if necessary. Can also be used to do a copy
 
236
    operation instead.
 
237
 
 
238
    frompath and topath are straight paths from the client. Will be checked.
 
239
    """
 
240
    # TODO: Do an SVN mv if the file is versioned.
 
241
    # TODO: Disallow tampering with student's home directory
 
242
    if frompath is None or topath is None:
 
243
        raise ActionError("Required field missing")
 
244
    frompath = actionpath_to_local(req, frompath)
 
245
    topath = actionpath_to_local(req, topath)
 
246
    if not os.path.exists(frompath):
 
247
        raise ActionError("The source file does not exist")
 
248
    if os.path.exists(topath):
 
249
        if frompath == topath:
 
250
            raise ActionError("Source and destination are the same")
 
251
        raise ActionError("A file already exists with that name")
 
252
 
 
253
    try:
 
254
        if copy:
 
255
            if os.path.isdir(frompath):
 
256
                shutil.copytree(frompath, topath)
 
257
            else:
 
258
                shutil.copy2(frompath, topath)
 
259
        else:
 
260
            shutil.move(frompath, topath)
 
261
    except OSError:
 
262
        raise ActionError("Could not move the file specified")
 
263
    except shutil.Error:
 
264
        raise ActionError("Could not move the file specified")
 
265
 
 
266
def svn_movefile(req, frompath, topath, copy=False):
 
267
    """Performs an svn move, resolving filenames, checking for any errors,
 
268
    and throwing ActionErrors if necessary. Can also be used to do a copy
 
269
    operation instead.
 
270
 
 
271
    frompath and topath are straight paths from the client. Will be checked.
 
272
    """
 
273
    if frompath is None or topath is None:
 
274
        raise ActionError("Required field missing")
 
275
    frompath = actionpath_to_local(req, frompath)
 
276
    topath = actionpath_to_local(req, topath)
 
277
    if not os.path.exists(frompath):
 
278
        raise ActionError("The source file does not exist")
 
279
    if os.path.exists(topath):
 
280
        if frompath == topath:
 
281
            raise ActionError("Source and destination are the same")
 
282
        raise ActionError("A file already exists with that name")
 
283
 
 
284
    try:
 
285
        if copy:
 
286
            svnclient.copy(frompath, topath)
 
287
        else:
 
288
            svnclient.move(frompath, topath)
 
289
    except OSError:
 
290
        raise ActionError("Could not move the file specified")
 
291
    except pysvn.ClientError:
 
292
        raise ActionError("Could not move the file specified")  
 
293
 
 
294
 
 
295
### ACTIONS ###
 
296
 
 
297
def action_delete(req, fields):
 
298
    # TODO: Disallow removal of student's home directory
 
299
    """Removes a list of files or directories.
 
300
 
 
301
    Reads fields: 'path' (multiple)
 
302
    """
 
303
    paths = fields.getlist('path')
 
304
    goterror = False
 
305
    for path in paths:
 
306
        path = actionpath_to_local(req, path)
 
307
        try:
 
308
            if os.path.isdir(path):
 
309
                shutil.rmtree(path)
 
310
            else:
 
311
                os.remove(path)
 
312
        except OSError:
 
313
            goterror = True
 
314
        except shutil.Error:
 
315
            goterror = True
 
316
    if goterror:
 
317
        if len(paths) == 1:
 
318
            raise ActionError("Could not delete the file specified")
 
319
        else:
 
320
            raise ActionError(
 
321
                "Could not delete one or more of the files specified")
 
322
 
 
323
def action_move(req, fields):
 
324
    # TODO: Do an SVN mv if the file is versioned.
 
325
    # TODO: Disallow tampering with student's home directory
 
326
    """Removes a list of files or directories.
 
327
 
 
328
    Reads fields: 'from', 'to'
 
329
    """
 
330
    frompath = fields.getfirst('from')
 
331
    topath = fields.getfirst('to')
 
332
    movefile(req, frompath, topath)
 
333
 
 
334
def action_mkdir(req, fields):
 
335
    """Creates a directory with the given path.
 
336
    Reads fields: 'path'
 
337
    """
 
338
    path = fields.getfirst('path')
 
339
    if path is None:
 
340
        raise ActionError("Required field missing")
 
341
    path = actionpath_to_local(req, path)
 
342
 
 
343
    if os.path.exists(path):
 
344
        raise ActionError("A file already exists with that name")
 
345
 
 
346
    # Create the directory
 
347
    try:
 
348
        os.mkdir(path)
 
349
    except OSError:
 
350
        raise ActionError("Could not create directory")
 
351
 
 
352
def action_putfile(req, fields):
 
353
    """Writes data to a file, overwriting it if it exists and creating it if
 
354
    it doesn't.
 
355
 
 
356
    Reads fields: 'path', 'data' (file upload), 'overwrite'
 
357
    """
 
358
    # TODO: Read field "unpack".
 
359
    # Important: Data is "None" if the file submitted is empty.
 
360
    path = fields.getfirst('path')
 
361
    data = fields.getfirst('data')
 
362
    if path is None:
 
363
        raise ActionError("Required field missing")
 
364
    if data is None:
 
365
        # Workaround - field reader treats "" as None, so this is the only
 
366
        # way to allow blank file uploads
 
367
        data = ""
 
368
    path = actionpath_to_local(req, path)
 
369
 
 
370
    if data is not None:
 
371
        data = cStringIO.StringIO(data)
 
372
 
 
373
    overwrite = fields.getfirst('overwrite')
 
374
    if overwrite is None:
 
375
        overwrite = False
 
376
    else:
 
377
        overwrite = True
 
378
 
 
379
    if overwrite:
 
380
        # Overwrite files; but can't if it's a directory
 
381
        if os.path.isdir(path):
 
382
            raise ActionError("A directory already exists "
 
383
                    + "with that name")
 
384
    else:
 
385
        if os.path.exists(path):
 
386
            raise ActionError("A file already exists with that name")
 
387
 
 
388
    # Copy the contents of file object 'data' to the path 'path'
 
389
    try:
 
390
        dest = open(path, 'wb')
 
391
        if data is not None:
 
392
            shutil.copyfileobj(data, dest)
 
393
    except (IOError, OSError), e:
 
394
        raise ActionError("Could not write to target file: %s" % e.strerror)
 
395
 
 
396
def action_putfiles(req, fields):
 
397
    """Writes data to one or more files in a directory, overwriting them if
 
398
    it they exist.
 
399
 
 
400
    Reads fields: 'path', 'data' (file upload, multiple), 'unpack'
 
401
    """
 
402
 
 
403
    # Important: Data is "None" if the file submitted is empty.
 
404
    path = fields.getfirst('path')
 
405
    data = fields['data']
 
406
    if type(data) != type([]):
 
407
        data = [data]
 
408
    unpack = fields.getfirst('unpack')
 
409
    if unpack is None:
 
410
        unpack = False
 
411
    else:
 
412
        unpack = True
 
413
    if path is None:
 
414
        raise ActionError("Required field missing")
 
415
    path = actionpath_to_urlpath(req, path)
 
416
    goterror = False
 
417
 
 
418
    for datum in data:
 
419
        # Each of the uploaded files
 
420
        filepath = os.path.join(path, datum.filename)
 
421
        filedata = datum.file
 
422
 
 
423
        if unpack and datum.filename.lower().endswith(".zip"):
 
424
            # A zip file - unpack it instead of just copying
 
425
            # TODO: Use the magic number instead of file extension
 
426
            # Note: Just unzip into the current directory (ignore the
 
427
            # filename)
 
428
            try:
 
429
                # First get the entire path (within jail)
 
430
                _, _, abspath = studpath.url_to_jailpaths(path)
 
431
                abspath = os.path.join(os.sep, abspath)
 
432
                zip.unzip(abspath, filedata)
 
433
            except (OSError, IOError):
 
434
                goterror = True
 
435
        else:
 
436
            # Not a zip file
 
437
            (_, _, filepath_local) = studpath.url_to_jailpaths(filepath)
 
438
            if filepath_local is None:
 
439
                raise ActionError("Invalid path")
 
440
 
 
441
            # Copy the contents of file object 'data' to the path 'path'
 
442
            try:
 
443
                dest = open(filepath_local, 'wb')
 
444
                if data is not None:
 
445
                    shutil.copyfileobj(filedata, dest)
 
446
            except (OSError, IOError):
 
447
                # TODO: Be more descriptive.
 
448
                goterror = True
 
449
 
 
450
    if goterror:
 
451
        if len(data) == 1:
 
452
            raise ActionError("Could not write to target file")
 
453
        else:
 
454
            raise ActionError(
 
455
                "Could not write to one or more of the target files")
 
456
 
 
457
def action_paste(req, fields):
 
458
    """Performs the copy or move action with the files specified.
 
459
    Copies/moves the files to the specified directory.
 
460
 
 
461
    Reads fields: 'src', 'dst', 'mode', 'file' (multiple).
 
462
    src: Base path that all the files are relative to (source).
 
463
    dst: Destination path to paste into.
 
464
    mode: 'copy' or 'move'.
 
465
    file: (Multiple) Files relative to base, which will be copied
 
466
        or moved to new locations relative to path.
 
467
    """
 
468
    errormsg = None
 
469
 
 
470
    dst = fields.getfirst('dst')
 
471
    src = fields.getfirst('src')
 
472
    mode = fields.getfirst('mode')
 
473
    files = fields.getlist('file')
 
474
    if dst is None or src is None or mode is None:
 
475
        raise ActionError("Required field missing")
 
476
 
 
477
    dst_local = actionpath_to_local(req, dst)
 
478
    if not os.path.isdir(dst_local):
 
479
        raise ActionError("dst is not a directory")
 
480
 
 
481
    errorfiles = []
 
482
    for file in files:
 
483
        # The source must not be interpreted as relative to req.path
 
484
        # Add a slash (relative to top-level)
 
485
        if src[:1] != '/':
 
486
            src = '/' + src
 
487
        frompath = os.path.join(src, file)
 
488
        # The destination is found by taking just the basename of the file
 
489
        topath = os.path.join(dst, os.path.basename(file))
 
490
        try:
 
491
            if mode == "copy":
 
492
                movefile(req, frompath, topath, True)
 
493
            elif mode == "move":
 
494
                movefile(req, frompath, topath, False)
 
495
            elif mode == "svncopy":
 
496
                svn_movefile(req, frompath, topath, True)
 
497
            elif mode == "svnmove":
 
498
                svn_movefile(req, frompath, topath, False)
 
499
            else:
 
500
                raise ActionError("Invalid mode (must be '(svn)copy' or '(svn)move')")
 
501
        except ActionError, message:
 
502
            # Store the error for later; we want to copy as many as possible
 
503
            if errormsg is None:
 
504
                errormsg = message
 
505
            else:
 
506
                # Multiple errors; generic message
 
507
                errormsg = "One or more files could not be pasted"
 
508
            # Add this file to errorfiles; it will be put back on the
 
509
            # clipboard for possible future pasting.
 
510
            errorfiles.append(file)
 
511
    if errormsg is not None:
 
512
        raise ActionError(errormsg)
 
513
 
 
514
    # XXX errorfiles contains a list of files that couldn't be pasted.
 
515
    # we currently do nothing with this.
 
516
 
 
517
def action_publish(req,fields):
 
518
    """Marks the folder as published by adding a '.published' file to the 
 
519
    directory and ensuring that the parent directory permissions are correct
 
520
 
 
521
    Reads fields: 'path'
 
522
    """
 
523
    paths = fields.getlist('path')
 
524
    user = studpath.url_to_local(req.path)[0]
 
525
    homedir = "/home/%s" % user
 
526
    if len(paths):
 
527
        paths = map(lambda path: actionpath_to_local(req, path), paths)
 
528
    else:
 
529
        paths = [studpath.url_to_jailpaths(req.path)[2]]
 
530
 
 
531
    # Set all the dirs in home dir world browsable (o+r,o+x)
 
532
    #FIXME: Should really only do those in the direct path not all of the 
 
533
    # folders in a students home directory
 
534
    for root,dirs,files in os.walk(homedir):
 
535
        os.chmod(root, os.stat(root).st_mode|0005)
 
536
 
 
537
    try:
 
538
        for path in paths:
 
539
            if os.path.isdir(path):
 
540
                pubfile = open(os.path.join(path,'.published'),'w')
 
541
                pubfile.write("This directory is published\n")
 
542
                pubfile.close()
 
543
            else:
 
544
                raise ActionError("Can only publish directories")
 
545
    except OSError, e:
 
546
        raise ActionError("Directory could not be published")
 
547
 
 
548
def action_unpublish(req,fields):
 
549
    """Marks the folder as unpublished by removing a '.published' file in the 
 
550
    directory (if it exits). It does not change the permissions of the parent 
 
551
    directories.
 
552
 
 
553
    Reads fields: 'path'
 
554
    """
 
555
    paths = fields.getlist('path')
 
556
    if len(paths):
 
557
        paths = map(lambda path: actionpath_to_local(req, path), paths)
 
558
    else:
 
559
        paths = [studpath.url_to_jailpaths(req.path)[2]]
 
560
 
 
561
    try:
 
562
        for path in paths:
 
563
            if os.path.isdir(path):
 
564
                pubfile = os.path.join(path,'.published')
 
565
                if os.path.isfile(pubfile):
 
566
                    os.remove(pubfile)
 
567
            else:
 
568
                raise ActionError("Can only unpublish directories")
 
569
    except OSError, e:
 
570
        raise ActionError("Directory could not be unpublished")
 
571
 
 
572
 
 
573
def action_svnadd(req, fields):
 
574
    """Performs a "svn add" to each file specified.
 
575
 
 
576
    Reads fields: 'path' (multiple)
 
577
    """
 
578
    paths = fields.getlist('path')
 
579
    paths = map(lambda path: actionpath_to_local(req, path), paths)
 
580
 
 
581
    try:
 
582
        svnclient.add(paths, recurse=True, force=True)
 
583
    except pysvn.ClientError, e:
 
584
        raise ActionError(str(e))
 
585
 
 
586
def action_svnremove(req, fields):
 
587
    """Performs a "svn remove" on each file specified.
 
588
 
 
589
    Reads fields: 'path' (multiple)
 
590
    """
 
591
    paths = fields.getlist('path')
 
592
    paths = map(lambda path: actionpath_to_local(req, path), paths)
 
593
 
 
594
    try:
 
595
        svnclient.remove(paths, force=True)
 
596
    except pysvn.ClientError, e:
 
597
        raise ActionError(str(e))
 
598
 
 
599
def action_svnupdate(req, fields):
 
600
    """Performs a "svn update" to each file specified.
 
601
 
 
602
    Reads fields: 'path'
 
603
    """
 
604
    path = fields.getfirst('path')
 
605
    if path is None:
 
606
        raise ActionError("Required field missing")
 
607
    path = actionpath_to_local(req, path)
 
608
 
 
609
    try:
 
610
        svnclient.update(path, recurse=True)
 
611
    except pysvn.ClientError, e:
 
612
        raise ActionError(str(e))
 
613
 
 
614
def action_svnresolved(req, fields):
 
615
    """Performs a "svn resolved" to each file specified.
 
616
 
 
617
    Reads fields: 'path'
 
618
    """
 
619
    path = fields.getfirst('path')
 
620
    if path is None:
 
621
        raise ActionError("Required field missing")
 
622
    path = actionpath_to_local(req, path)
 
623
 
 
624
    try:
 
625
        svnclient.resolved(path, recurse=True)
 
626
    except pysvn.ClientError, e:
 
627
        raise ActionError(str(e))
 
628
 
 
629
def action_svnrevert(req, fields):
 
630
    """Performs a "svn revert" to each file specified.
 
631
 
 
632
    Reads fields: 'path' (multiple)
 
633
    """
 
634
    paths = fields.getlist('path')
 
635
    paths = map(lambda path: actionpath_to_local(req, path), paths)
 
636
 
 
637
    try:
 
638
        svnclient.revert(paths, recurse=True)
 
639
    except pysvn.ClientError, e:
 
640
        raise ActionError(str(e))
 
641
 
 
642
def action_svnpublish(req, fields):
 
643
    """Sets svn property "ivle:published" on each file specified.
 
644
    Should only be called on directories (only effective on directories
 
645
    anyway).
 
646
 
 
647
    Reads fields: 'path'
 
648
 
 
649
    XXX Currently unused by the client (calls action_publish instead, which
 
650
    has a completely different publishing model).
 
651
    """
 
652
    paths = fields.getlist('path')
 
653
    if len(paths):
 
654
        paths = map(lambda path: actionpath_to_local(req, path), paths)
 
655
    else:
 
656
        paths = [studpath.url_to_jailpaths(req.path)[2]]
 
657
 
 
658
    try:
 
659
        for path in paths:
 
660
            # Note: Property value doesn't matter
 
661
            svnclient.propset("ivle:published", "", path, recurse=False)
 
662
    except pysvn.ClientError, e:
 
663
        raise ActionError("Directory could not be published")
 
664
 
 
665
def action_svnunpublish(req, fields):
 
666
    """Deletes svn property "ivle:published" on each file specified.
 
667
 
 
668
    Reads fields: 'path'
 
669
 
 
670
    XXX Currently unused by the client (calls action_unpublish instead, which
 
671
    has a completely different publishing model).
 
672
    """
 
673
    paths = fields.getlist('path')
 
674
    paths = map(lambda path: actionpath_to_local(req, path), paths)
 
675
 
 
676
    try:
 
677
        for path in paths:
 
678
            svnclient.propdel("ivle:published", path, recurse=False)
 
679
    except pysvn.ClientError, e:
 
680
        raise ActionError("Directory could not be unpublished")
 
681
 
 
682
def action_svncommit(req, fields):
 
683
    """Performs a "svn commit" to each file specified.
 
684
 
 
685
    Reads fields: 'path' (multiple), 'logmsg' (optional)
 
686
    """
 
687
    paths = fields.getlist('path')
 
688
    paths = map(lambda path: actionpath_to_local(req, str(path)), paths)
 
689
    logmsg = str(fields.getfirst('logmsg', DEFAULT_LOGMESSAGE))
 
690
    if logmsg == '': logmsg = DEFAULT_LOGMESSAGE
 
691
 
 
692
    try:
 
693
        svnclient.checkin(paths, logmsg, recurse=True)
 
694
    except pysvn.ClientError, e:
 
695
        raise ActionError(str(e))
 
696
 
 
697
def action_svncheckout(req, fields):
 
698
    """Performs a "svn checkout" of the first path into the second path.
 
699
 
 
700
    Reads fields: 'path'    (multiple)
 
701
    """
 
702
    paths = fields.getlist('path')
 
703
    if len(paths) != 2:
 
704
        raise ActionError("usage: svncheckout url local-path")
 
705
    url = ivle.conf.svn_addr + "/" + paths[0]
 
706
    local_path = actionpath_to_local(req, str(paths[1]))
 
707
    try:
 
708
        svnclient.callback_get_login = get_login
 
709
        svnclient.checkout(url, local_path, recurse=True)
 
710
    except pysvn.ClientError, e:
 
711
        raise ActionError(str(e))
 
712
 
 
713
def action_svnrepomkdir(req, fields):
 
714
    """Performs a "svn mkdir" on a path under the IVLE SVN root.
 
715
 
 
716
    Reads fields: 'path'
 
717
    """
 
718
    path = fields.getfirst('path')
 
719
    logmsg = fields.getfirst('logmsg')
 
720
    url = ivle.conf.svn_addr + "/" + path
 
721
    try:
 
722
        svnclient.callback_get_login = get_login
 
723
        svnclient.mkdir(url, log_message=logmsg)
 
724
    except pysvn.ClientError, e:
 
725
        raise ActionError(str(e))
 
726
 
 
727
def action_svnrepostat(req, fields):
 
728
    """Discovers whether a path exists in a repo under the IVLE SVN root.
 
729
 
 
730
    Reads fields: 'path'
 
731
    """
 
732
    path = fields.getfirst('path')
 
733
    url = ivle.conf.svn_addr + "/" + path
 
734
    svnclient.exception_style = 1 
 
735
 
 
736
    try:
 
737
        svnclient.callback_get_login = get_login
 
738
        svnclient.info2(url)
 
739
    except pysvn.ClientError, e:
 
740
        # Error code 170000 means ENOENT in this revision.
 
741
        if e[1][0][1] == 170000:
 
742
            raise util.IVLEError(404, 'The specified repository path does not exist')
 
743
        else:
 
744
            raise ActionError(str(e[0]))
 
745
            
 
746
 
 
747
# Table of all action functions #
 
748
# Each function has the interface f(req, fields).
 
749
 
 
750
actions_table = {
 
751
    "delete" : action_delete,
 
752
    "move" : action_move,
 
753
    "mkdir" : action_mkdir,
 
754
    "putfile" : action_putfile,
 
755
    "putfiles" : action_putfiles,
 
756
    "paste" : action_paste,
 
757
    "publish" : action_publish,
 
758
    "unpublish" : action_unpublish,
 
759
 
 
760
    "svnadd" : action_svnadd,
 
761
    "svnremove" : action_svnremove,
 
762
    "svnupdate" : action_svnupdate,
 
763
    "svnresolved" : action_svnresolved,
 
764
    "svnrevert" : action_svnrevert,
 
765
    "svnpublish" : action_svnpublish,
 
766
    "svnunpublish" : action_svnunpublish,
 
767
    "svncommit" : action_svncommit,
 
768
    "svncheckout" : action_svncheckout,
 
769
    "svnrepomkdir" : action_svnrepomkdir,
 
770
    "svnrepostat" : action_svnrepostat,
 
771
}