~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-19 21:17:21 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:512
Renaming of problems to exercises (initial commit).
Fix up module naming (exercises sometimes called tutorials).

Show diffs side-by-side

added added

removed removed

Lines of Context:
41
41
#       to:     The path of the target filename. Error if the file already
42
42
#               exists.
43
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.
 
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.
47
48
#       data:   Bytes to be written to the file verbatim. May either be
48
49
#               a string variable or a file upload.
49
 
#       overwrite: Optional. If supplied, the file will be overwritten.
50
 
#               Otherwise, error if path already exists.
 
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.
51
53
#
52
54
# action=putfiles: Upload multiple files to the student workspace, and
53
55
#                 optionally accept zip files which will be unpacked.
56
58
#       data:   A file upload (may not be a simple string). The filename
57
59
#               will be used to determine the target filename within
58
60
#               the given path.
59
 
#       unpack: Optional. If supplied, if any data is a valid ZIP file,
 
61
#       unpack: Optional. If "true", if any data is a valid ZIP file,
60
62
#               will create a directory instead and unpack the ZIP file
61
63
#               into it.
62
64
#
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
65
# The differences between putfile and putfiles are:
68
 
# * putfile can only accept a single file, and can't unpack zipfiles.
 
66
# * putfile can only accept a single file.
69
67
# * putfile can accept string data, doesn't have to be a file upload.
70
68
# * putfile ignores the upload filename, the entire filename is specified on
71
69
#       path. putfiles calls files after the name on the user's machine.
72
70
#
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
 
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
76
92
#               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
93
#
81
94
# Subversion actions.
82
95
# action=svnadd: Add an existing file(s) to version control.
106
119
#               recursively.
107
120
#       logmsg: Text of the log message. Optional. There is a default log
108
121
#               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
122
113
123
# TODO: Implement the following actions:
114
 
#   svnupdate (done?)
115
 
# TODO: Implement ZIP unpacking in putfiles (done?).
 
124
#   putfiles, svnrevert, svnupdate, svncommit
 
125
# TODO: Implement ZIP unpacking in putfile and putfiles.
116
126
# TODO: svnupdate needs a digest to tell the user the files that were updated.
117
127
#   This can be implemented by some message passing between action and
118
128
#   listing, and having the digest included in the listing. (Problem if
126
136
import pysvn
127
137
 
128
138
from common import (util, studpath, zip)
129
 
import conf
130
 
 
131
 
def get_login(_realm, existing_login, _may_save):
132
 
    """Callback function used by pysvn for authentication.
133
 
    realm, existing_login, _may_save: The 3 arguments passed by pysvn to
134
 
        callback_get_login.
135
 
        The following has been determined empirically, not from docs:
136
 
        existing_login will be the name of the user who owns the process on
137
 
        the first attempt, "" on subsequent attempts. We use this fact.
138
 
    """
139
 
    # Only provide credentials on the _first_ attempt.
140
 
    # If we're being asked again, then it means the credentials failed for
141
 
    # some reason and we should just fail. (This is not desirable, but it's
142
 
    # better than being asked an infinite number of times).
143
 
    return (existing_login != "", conf.login, conf.svn_pass, True)
144
139
 
145
140
# Make a Subversion client object
146
141
svnclient = pysvn.Client()
147
 
svnclient.callback_get_login = get_login
148
 
svnclient.exception_style = 0               # Simple (string) exceptions
149
142
 
150
143
DEFAULT_LOGMESSAGE = "No log message supplied."
151
144
 
239
232
    if os.path.exists(topath):
240
233
        if frompath == topath:
241
234
            raise ActionError("Source and destination are the same")
242
 
        raise ActionError("A file already exists with that name")
 
235
        raise ActionError("Another file already exists with that name")
243
236
 
244
237
    try:
245
238
        if copy:
294
287
    topath = fields.getfirst('to')
295
288
    movefile(req, frompath, topath)
296
289
 
297
 
def action_mkdir(req, fields):
298
 
    """Creates a directory with the given path.
299
 
    Reads fields: 'path'
300
 
    """
301
 
    path = fields.getfirst('path')
302
 
    if path is None:
303
 
        raise ActionError("Required field missing")
304
 
    path = actionpath_to_local(req, path)
305
 
 
306
 
    if os.path.exists(path):
307
 
        raise ActionError("A file already exists with that name")
308
 
 
309
 
    # Create the directory
310
 
    try:
311
 
        os.mkdir(path)
312
 
    except OSError:
313
 
        raise ActionError("Could not create directory")
314
 
 
315
290
def action_putfile(req, fields):
316
291
    """Writes data to a file, overwriting it if it exists and creating it if
317
292
    it doesn't.
318
293
 
319
 
    Reads fields: 'path', 'data' (file upload), 'overwrite'
 
294
    Reads fields: 'path', 'data' (file upload)
320
295
    """
321
296
    # TODO: Read field "unpack".
322
297
    # Important: Data is "None" if the file submitted is empty.
323
298
    path = fields.getfirst('path')
324
299
    data = fields.getfirst('data')
325
 
    if path is None:
 
300
    if path is None or data is None:
326
301
        raise ActionError("Required field missing")
327
 
    if data is None:
328
 
        # Workaround - field reader treats "" as None, so this is the only
329
 
        # way to allow blank file uploads
330
 
        data = ""
331
302
    path = actionpath_to_local(req, path)
332
303
 
333
304
    if data is not None:
334
305
        data = cStringIO.StringIO(data)
335
306
 
336
 
    overwrite = fields.getfirst('overwrite')
337
 
    if overwrite is None:
338
 
        overwrite = False
339
 
    else:
340
 
        overwrite = True
341
 
 
342
 
    if overwrite:
343
 
        # Overwrite files; but can't if it's a directory
344
 
        if os.path.isdir(path):
345
 
            raise ActionError("A directory already exists "
346
 
                    + "with that name")
347
 
    else:
348
 
        if os.path.exists(path):
349
 
            raise ActionError("A file already exists with that name")
350
 
 
351
307
    # Copy the contents of file object 'data' to the path 'path'
352
308
    try:
353
309
        dest = open(path, 'wb')
354
310
        if data is not None:
355
311
            shutil.copyfileobj(data, dest)
356
 
    except (IOError, OSError), e:
357
 
        raise ActionError("Could not write to target file: %s" % e.strerror)
 
312
    except OSError:
 
313
        raise ActionError("Could not write to target file")
358
314
 
359
315
def action_putfiles(req, fields):
360
316
    """Writes data to one or more files in a directory, overwriting them if
378
334
    path = actionpath_to_urlpath(req, path)
379
335
    goterror = False
380
336
 
 
337
 
381
338
    for datum in data:
382
339
        # Each of the uploaded files
383
340
        filepath = os.path.join(path, datum.filename)
384
 
        filedata = datum.file
 
341
        filedata = datum.value
385
342
 
386
343
        if unpack and datum.filename.lower().endswith(".zip"):
387
344
            # A zip file - unpack it instead of just copying
389
346
            # Note: Just unzip into the current directory (ignore the
390
347
            # filename)
391
348
            try:
392
 
                # First get the entire path (within jail)
393
 
                _, _, abspath = studpath.url_to_jailpaths(path)
394
 
                abspath = os.path.join(os.sep, abspath)
395
 
                zip.unzip(abspath, filedata)
 
349
                zip.unzip(path, filedata)
396
350
            except (OSError, IOError):
397
351
                goterror = True
398
352
        else:
405
359
            try:
406
360
                dest = open(filepath_local, 'wb')
407
361
                if data is not None:
408
 
                    shutil.copyfileobj(filedata, dest)
 
362
                    shutil.copyfileobj(cStringIO.StringIO(filedata), dest)
409
363
            except OSError:
410
364
                goterror = True
411
365
 
416
370
            raise ActionError(
417
371
                "Could not write to one or more of the target files")
418
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
 
419
406
def action_paste(req, fields):
420
 
    """Performs the copy or move action with the files specified.
421
 
    Copies/moves the files to the specified directory.
 
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.
422
410
 
423
 
    Reads fields: 'src', 'dst', 'mode', 'file' (multiple).
424
 
    src: Base path that all the files are relative to (source).
425
 
    dst: Destination path to paste into.
426
 
    mode: 'copy' or 'move'.
427
 
    file: (Multiple) Files relative to base, which will be copied
428
 
        or moved to new locations relative to path.
 
411
    Reads fields: 'path'
429
412
    """
430
413
    errormsg = None
431
414
 
432
 
    dst = fields.getfirst('dst')
433
 
    src = fields.getfirst('src')
434
 
    mode = fields.getfirst('mode')
435
 
    files = fields.getlist('file')
436
 
    if dst is None or src is None or mode is None:
 
415
    todir = fields.getfirst('path')
 
416
    if todir is None:
437
417
        raise ActionError("Required field missing")
438
 
    if mode == "copy":
439
 
        copy = True
440
 
    elif mode == "move":
441
 
        copy = False
442
 
    else:
443
 
        raise ActionError("Invalid mode (must be 'copy' or 'move')")
444
 
    dst_local = actionpath_to_local(req, dst)
445
 
    if not os.path.isdir(dst_local):
446
 
        raise ActionError("dst is not a directory")
 
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")
447
433
 
448
434
    errorfiles = []
449
435
    for file in files:
450
436
        # The source must not be interpreted as relative to req.path
451
437
        # Add a slash (relative to top-level)
452
 
        if src[:1] != '/':
453
 
            src = '/' + src
454
 
        frompath = os.path.join(src, file)
 
438
        frompath = os.sep + os.path.join(base, file)
455
439
        # The destination is found by taking just the basename of the file
456
 
        topath = os.path.join(dst, os.path.basename(file))
 
440
        topath = os.path.join(todir, os.path.basename(file))
457
441
        try:
458
442
            movefile(req, frompath, topath, copy)
459
443
        except ActionError, message:
466
450
            # Add this file to errorfiles; it will be put back on the
467
451
            # clipboard for possible future pasting.
468
452
            errorfiles.append(file)
469
 
    if errormsg is not None:
 
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()
470
458
        raise ActionError(errormsg)
471
459
 
472
 
    # XXX errorfiles contains a list of files that couldn't be pasted.
473
 
    # we currently do nothing with this.
474
 
 
475
 
def action_publish(req,fields):
476
 
    """Marks the folder as published by adding a '.published' file to the 
477
 
    directory and ensuring that the parent directory permissions are correct
478
 
 
479
 
    Reads fields: 'path'
480
 
    """
481
 
    paths = fields.getlist('path')
482
 
    user = studpath.url_to_local(req.path)[0]
483
 
    homedir = "/home/%s" % user
484
 
    if len(paths):
485
 
        paths = map(lambda path: actionpath_to_local(req, path), paths)
486
 
    else:
487
 
        paths = [studpath.url_to_jailpaths(req.path)[2]]
488
 
 
489
 
    # Set all the dirs in home dir world browsable (o+r,o+x)
490
 
    #FIXME: Should really only do those in the direct path not all of the 
491
 
    # folders in a students home directory
492
 
    for root,dirs,files in os.walk(homedir):
493
 
        os.chmod(root, os.stat(root).st_mode|0005)
494
 
 
495
 
    try:
496
 
        for path in paths:
497
 
            if os.path.isdir(path):
498
 
                pubfile = open(os.path.join(path,'.published'),'w')
499
 
                pubfile.write("This directory is published\n")
500
 
                pubfile.close()
501
 
            else:
502
 
                raise ActionError("Can only publish directories")
503
 
    except OSError, e:
504
 
        raise ActionError("Directory could not be published")
505
 
 
506
 
def action_unpublish(req,fields):
507
 
    """Marks the folder as unpublished by removing a '.published' file in the 
508
 
    directory (if it exits). It does not change the permissions of the parent 
509
 
    directories.
510
 
 
511
 
    Reads fields: 'path'
512
 
    """
513
 
    paths = fields.getlist('path')
514
 
    if len(paths):
515
 
        paths = map(lambda path: actionpath_to_local(req, path), paths)
516
 
    else:
517
 
        paths = [studpath.url_to_jailpaths(req.path)[2]]
518
 
 
519
 
    try:
520
 
        for path in paths:
521
 
            if os.path.isdir(path):
522
 
                pubfile = os.path.join(path,'.published')
523
 
                if os.path.isfile(pubfile):
524
 
                    os.remove(pubfile)
525
 
            else:
526
 
                raise ActionError("Can only unpublish directories")
527
 
    except OSError, e:
528
 
        raise ActionError("Directory could not be unpublished")
529
 
 
 
460
    # Success: Clear the clipboard
 
461
    del session['clipboard']
 
462
    session.save()
530
463
 
531
464
def action_svnadd(req, fields):
532
465
    """Performs a "svn add" to each file specified.
538
471
 
539
472
    try:
540
473
        svnclient.add(paths, recurse=True, force=True)
541
 
    except pysvn.ClientError, e:
542
 
        raise ActionError(str(e))
 
474
    except pysvn.ClientError:
 
475
        raise ActionError("One or more files could not be added")
543
476
 
544
477
def action_svnupdate(req, fields):
545
478
    """Performs a "svn update" to each file specified.
553
486
 
554
487
    try:
555
488
        svnclient.update(path, recurse=True)
556
 
    except pysvn.ClientError, e:
557
 
        raise ActionError(str(e))
 
489
    except pysvn.ClientError:
 
490
        raise ActionError("One or more files could not be updated")
558
491
 
559
492
def action_svnrevert(req, fields):
560
493
    """Performs a "svn revert" to each file specified.
566
499
 
567
500
    try:
568
501
        svnclient.revert(paths, recurse=True)
569
 
    except pysvn.ClientError, e:
570
 
        raise ActionError(str(e))
 
502
    except pysvn.ClientError:
 
503
        raise ActionError("One or more files could not be reverted")
571
504
 
572
505
def action_svnpublish(req, fields):
573
506
    """Sets svn property "ivle:published" on each file specified.
575
508
    anyway).
576
509
 
577
510
    Reads fields: 'path'
578
 
 
579
 
    XXX Currently unused by the client (calls action_publish instead, which
580
 
    has a completely different publishing model).
581
511
    """
582
512
    paths = fields.getlist('path')
583
 
    if len(paths):
584
 
        paths = map(lambda path: actionpath_to_local(req, path), paths)
585
 
    else:
586
 
        paths = [studpath.url_to_jailpaths(req.path)[2]]
 
513
    paths = map(lambda path: actionpath_to_local(req, path), paths)
587
514
 
588
515
    try:
589
516
        for path in paths:
596
523
    """Deletes svn property "ivle:published" on each file specified.
597
524
 
598
525
    Reads fields: 'path'
599
 
 
600
 
    XXX Currently unused by the client (calls action_unpublish instead, which
601
 
    has a completely different publishing model).
602
526
    """
603
527
    paths = fields.getlist('path')
604
528
    paths = map(lambda path: actionpath_to_local(req, path), paths)
606
530
    try:
607
531
        for path in paths:
608
532
            svnclient.propdel("ivle:published", path, recurse=False)
609
 
    except pysvn.ClientError, e:
 
533
    except pysvn.ClientError:
610
534
        raise ActionError("Directory could not be unpublished")
611
535
 
612
536
def action_svncommit(req, fields):
621
545
 
622
546
    try:
623
547
        svnclient.checkin(paths, logmsg, recurse=True)
624
 
    except pysvn.ClientError, e:
625
 
        raise ActionError(str(e))
626
 
 
627
 
def action_svncheckout(req, fields):
628
 
    """Performs a "svn checkout" of each path specified.
629
 
 
630
 
    Reads fields: 'path'    (multiple)
631
 
    """
632
 
    paths = fields.getlist('path')
633
 
    if len(paths) != 2:
634
 
        raise ActionError("usage: svncheckout url local-path")
635
 
    url = conf.svn_addr + "/" + login + "/" + paths[0]
636
 
    local_path = actionpath_to_local(req, str(paths[1]))
637
 
    try:
638
 
        svnclient.callback_get_login = get_login
639
 
        svnclient.checkout(url, local_path, recurse=True)
640
 
    except pysvn.ClientError, e:
641
 
        raise ActionError(str(e))
 
548
    except pysvn.ClientError:
 
549
        raise ActionError("One or more files could not be committed")
642
550
 
643
551
# Table of all action functions #
644
552
# Each function has the interface f(req, fields).
646
554
actions_table = {
647
555
    "remove" : action_remove,
648
556
    "move" : action_move,
649
 
    "mkdir" : action_mkdir,
650
557
    "putfile" : action_putfile,
651
558
    "putfiles" : action_putfiles,
 
559
 
 
560
    "copy" : action_copy,
 
561
    "cut" : action_cut,
652
562
    "paste" : action_paste,
653
 
    "publish" : action_publish,
654
 
    "unpublish" : action_unpublish,
655
563
 
656
564
    "svnadd" : action_svnadd,
657
565
    "svnupdate" : action_svnupdate,
659
567
    "svnpublish" : action_svnpublish,
660
568
    "svnunpublish" : action_svnunpublish,
661
569
    "svncommit" : action_svncommit,
662
 
    "svncheckout" : action_svncheckout,
663
570
}