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

« back to all changes in this revision

Viewing changes to lib/fileservice_lib/action.py

  • Committer: mattgiuca
  • Date: 2008-02-29 01:18:22 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:621
Added 2 new apps: home and subjects. Both fairly incomplete, just a basic
    skeleton.
    "home" is the new default app, replacing "files".
    (It has a link to files).

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
#
65
67
#               does. The dir will be made with this name.
66
68
#
67
69
# The differences between putfile and putfiles are:
68
 
# * putfile can only accept a single file, and can't unpack zipfiles.
 
70
# * putfile can only accept a single file.
69
71
# * putfile can accept string data, doesn't have to be a file upload.
70
72
# * putfile ignores the upload filename, the entire filename is specified on
71
73
#       path. putfiles calls files after the name on the user's machine.
72
74
#
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
 
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
76
96
#               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
97
#
81
98
# Subversion actions.
82
99
# action=svnadd: Add an existing file(s) to version control.
111
128
#               checked out.
112
129
113
130
# TODO: Implement the following actions:
114
 
#   svnupdate (done?)
115
 
# TODO: Implement ZIP unpacking in putfiles (done?).
 
131
#   putfiles, svnrevert, svnupdate, svncommit
 
132
# TODO: Implement ZIP unpacking in putfile and putfiles.
116
133
# TODO: svnupdate needs a digest to tell the user the files that were updated.
117
134
#   This can be implemented by some message passing between action and
118
135
#   listing, and having the digest included in the listing. (Problem if
128
145
from common import (util, studpath, zip)
129
146
import conf
130
147
 
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)
 
148
def get_login(_realm, _username, _may_save):
 
149
    """Return the subversion credentials for the user."""
 
150
    return (True, conf.login, conf.passwd, True)
144
151
 
145
152
# Make a Subversion client object
146
153
svnclient = pysvn.Client()
147
154
svnclient.callback_get_login = get_login
148
 
svnclient.exception_style = 0               # Simple (string) exceptions
149
155
 
150
156
DEFAULT_LOGMESSAGE = "No log message supplied."
151
157
 
239
245
    if os.path.exists(topath):
240
246
        if frompath == topath:
241
247
            raise ActionError("Source and destination are the same")
242
 
        raise ActionError("A file already exists with that name")
 
248
        raise ActionError("Another file already exists with that name")
243
249
 
244
250
    try:
245
251
        if copy:
256
262
 
257
263
### ACTIONS ###
258
264
 
259
 
def action_delete(req, fields):
 
265
def action_remove(req, fields):
 
266
    # TODO: Do an SVN rm if the file is versioned.
260
267
    # TODO: Disallow removal of student's home directory
261
268
    """Removes a list of files or directories.
262
269
 
302
309
        raise ActionError("Required field missing")
303
310
    path = actionpath_to_local(req, path)
304
311
 
305
 
    if os.path.exists(path):
306
 
        raise ActionError("A file already exists with that name")
307
 
 
308
312
    # Create the directory
309
313
    try:
310
314
        os.mkdir(path)
315
319
    """Writes data to a file, overwriting it if it exists and creating it if
316
320
    it doesn't.
317
321
 
318
 
    Reads fields: 'path', 'data' (file upload), 'overwrite'
 
322
    Reads fields: 'path', 'data' (file upload)
319
323
    """
320
324
    # TODO: Read field "unpack".
321
325
    # Important: Data is "None" if the file submitted is empty.
332
336
    if data is not None:
333
337
        data = cStringIO.StringIO(data)
334
338
 
335
 
    overwrite = fields.getfirst('overwrite')
336
 
    if overwrite is None:
337
 
        overwrite = False
338
 
    else:
339
 
        overwrite = True
340
 
 
341
 
    if overwrite:
342
 
        # Overwrite files; but can't if it's a directory
343
 
        if os.path.isdir(path):
344
 
            raise ActionError("A directory already exists "
345
 
                    + "with that name")
346
 
    else:
347
 
        if os.path.exists(path):
348
 
            raise ActionError("A file already exists with that name")
349
 
 
350
339
    # Copy the contents of file object 'data' to the path 'path'
351
340
    try:
352
341
        dest = open(path, 'wb')
353
342
        if data is not None:
354
343
            shutil.copyfileobj(data, dest)
355
 
    except (IOError, OSError), e:
356
 
        raise ActionError("Could not write to target file: %s" % e.strerror)
 
344
    except OSError:
 
345
        raise ActionError("Could not write to target file")
357
346
 
358
347
def action_putfiles(req, fields):
359
348
    """Writes data to one or more files in a directory, overwriting them if
377
366
    path = actionpath_to_urlpath(req, path)
378
367
    goterror = False
379
368
 
 
369
 
380
370
    for datum in data:
381
371
        # Each of the uploaded files
382
372
        filepath = os.path.join(path, datum.filename)
383
 
        filedata = datum.file
 
373
        filedata = datum.value
384
374
 
385
375
        if unpack and datum.filename.lower().endswith(".zip"):
386
376
            # A zip file - unpack it instead of just copying
388
378
            # Note: Just unzip into the current directory (ignore the
389
379
            # filename)
390
380
            try:
391
 
                # First get the entire path (within jail)
392
 
                _, _, abspath = studpath.url_to_jailpaths(path)
393
 
                abspath = os.path.join(os.sep, abspath)
394
 
                zip.unzip(abspath, filedata)
 
381
                zip.unzip(path, filedata)
395
382
            except (OSError, IOError):
396
383
                goterror = True
397
384
        else:
404
391
            try:
405
392
                dest = open(filepath_local, 'wb')
406
393
                if data is not None:
407
 
                    shutil.copyfileobj(filedata, dest)
 
394
                    shutil.copyfileobj(cStringIO.StringIO(filedata), dest)
408
395
            except OSError:
409
396
                goterror = True
410
397
 
415
402
            raise ActionError(
416
403
                "Could not write to one or more of the target files")
417
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
 
418
438
def action_paste(req, fields):
419
 
    """Performs the copy or move action with the files specified.
420
 
    Copies/moves the files to the specified directory.
 
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.
421
442
 
422
 
    Reads fields: 'src', 'dst', 'mode', 'file' (multiple).
423
 
    src: Base path that all the files are relative to (source).
424
 
    dst: Destination path to paste into.
425
 
    mode: 'copy' or 'move'.
426
 
    file: (Multiple) Files relative to base, which will be copied
427
 
        or moved to new locations relative to path.
 
443
    Reads fields: 'path'
428
444
    """
429
445
    errormsg = None
430
446
 
431
 
    dst = fields.getfirst('dst')
432
 
    src = fields.getfirst('src')
433
 
    mode = fields.getfirst('mode')
434
 
    files = fields.getlist('file')
435
 
    if dst is None or src is None or mode is None:
 
447
    todir = fields.getfirst('path')
 
448
    if todir is None:
436
449
        raise ActionError("Required field missing")
437
 
    if mode == "copy":
438
 
        copy = True
439
 
    elif mode == "move":
440
 
        copy = False
441
 
    else:
442
 
        raise ActionError("Invalid mode (must be 'copy' or 'move')")
443
 
    dst_local = actionpath_to_local(req, dst)
444
 
    if not os.path.isdir(dst_local):
445
 
        raise ActionError("dst is not a directory")
 
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")
446
465
 
447
466
    errorfiles = []
448
467
    for file in files:
449
468
        # The source must not be interpreted as relative to req.path
450
469
        # Add a slash (relative to top-level)
451
 
        if src[:1] != '/':
452
 
            src = '/' + src
453
 
        frompath = os.path.join(src, file)
 
470
        frompath = os.sep + os.path.join(base, file)
454
471
        # The destination is found by taking just the basename of the file
455
 
        topath = os.path.join(dst, os.path.basename(file))
 
472
        topath = os.path.join(todir, os.path.basename(file))
456
473
        try:
457
474
            movefile(req, frompath, topath, copy)
458
475
        except ActionError, message:
465
482
            # Add this file to errorfiles; it will be put back on the
466
483
            # clipboard for possible future pasting.
467
484
            errorfiles.append(file)
468
 
    if errormsg is not None:
 
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()
469
490
        raise ActionError(errormsg)
470
491
 
471
 
    # XXX errorfiles contains a list of files that couldn't be pasted.
472
 
    # we currently do nothing with this.
473
 
 
474
 
def action_publish(req,fields):
475
 
    """Marks the folder as published by adding a '.published' file to the 
476
 
    directory and ensuring that the parent directory permissions are correct
477
 
 
478
 
    Reads fields: 'path'
479
 
    """
480
 
    paths = fields.getlist('path')
481
 
    user = studpath.url_to_local(req.path)[0]
482
 
    homedir = "/home/%s" % user
483
 
    if len(paths):
484
 
        paths = map(lambda path: actionpath_to_local(req, path), paths)
485
 
    else:
486
 
        paths = [studpath.url_to_jailpaths(req.path)[2]]
487
 
 
488
 
    # Set all the dirs in home dir world browsable (o+r,o+x)
489
 
    #FIXME: Should really only do those in the direct path not all of the 
490
 
    # folders in a students home directory
491
 
    for root,dirs,files in os.walk(homedir):
492
 
        os.chmod(root, os.stat(root).st_mode|0005)
493
 
 
494
 
    try:
495
 
        for path in paths:
496
 
            if os.path.isdir(path):
497
 
                pubfile = open(os.path.join(path,'.published'),'w')
498
 
                pubfile.write("This directory is published\n")
499
 
                pubfile.close()
500
 
            else:
501
 
                raise ActionError("Can only publish directories")
502
 
    except OSError, e:
503
 
        raise ActionError("Directory could not be published")
504
 
 
505
 
def action_unpublish(req,fields):
506
 
    """Marks the folder as unpublished by removing a '.published' file in the 
507
 
    directory (if it exits). It does not change the permissions of the parent 
508
 
    directories.
509
 
 
510
 
    Reads fields: 'path'
511
 
    """
512
 
    paths = fields.getlist('path')
513
 
    if len(paths):
514
 
        paths = map(lambda path: actionpath_to_local(req, path), paths)
515
 
    else:
516
 
        paths = [studpath.url_to_jailpaths(req.path)[2]]
517
 
 
518
 
    try:
519
 
        for path in paths:
520
 
            if os.path.isdir(path):
521
 
                pubfile = os.path.join(path,'.published')
522
 
                if os.path.isfile(pubfile):
523
 
                    os.remove(pubfile)
524
 
            else:
525
 
                raise ActionError("Can only unpublish directories")
526
 
    except OSError, e:
527
 
        raise ActionError("Directory could not be unpublished")
528
 
 
 
492
    # Success: Clear the clipboard
 
493
    del session['clipboard']
 
494
    session.save()
529
495
 
530
496
def action_svnadd(req, fields):
531
497
    """Performs a "svn add" to each file specified.
537
503
 
538
504
    try:
539
505
        svnclient.add(paths, recurse=True, force=True)
540
 
    except pysvn.ClientError, e:
541
 
        raise ActionError(str(e))
542
 
 
543
 
def action_svnremove(req, fields):
544
 
    """Performs a "svn remove" on each file specified.
545
 
 
546
 
    Reads fields: 'path' (multiple)
547
 
    """
548
 
    paths = fields.getlist('path')
549
 
    paths = map(lambda path: actionpath_to_local(req, path), paths)
550
 
 
551
 
    try:
552
 
        svnclient.remove(paths, force=True)
553
 
    except pysvn.ClientError, e:
554
 
        raise ActionError(str(e))
 
506
    except pysvn.ClientError:
 
507
        raise ActionError("One or more files could not be added")
555
508
 
556
509
def action_svnupdate(req, fields):
557
510
    """Performs a "svn update" to each file specified.
565
518
 
566
519
    try:
567
520
        svnclient.update(path, recurse=True)
568
 
    except pysvn.ClientError, e:
569
 
        raise ActionError(str(e))
570
 
 
571
 
def action_svnresolved(req, fields):
572
 
    """Performs a "svn resolved" to each file specified.
573
 
 
574
 
    Reads fields: 'path'
575
 
    """
576
 
    path = fields.getfirst('path')
577
 
    if path is None:
578
 
        raise ActionError("Required field missing")
579
 
    path = actionpath_to_local(req, path)
580
 
 
581
 
    try:
582
 
        svnclient.resolved(path, recurse=True)
583
 
    except pysvn.ClientError, e:
584
 
        raise ActionError(str(e))
 
521
    except pysvn.ClientError:
 
522
        raise ActionError("One or more files could not be updated")
585
523
 
586
524
def action_svnrevert(req, fields):
587
525
    """Performs a "svn revert" to each file specified.
593
531
 
594
532
    try:
595
533
        svnclient.revert(paths, recurse=True)
596
 
    except pysvn.ClientError, e:
597
 
        raise ActionError(str(e))
 
534
    except pysvn.ClientError:
 
535
        raise ActionError("One or more files could not be reverted")
598
536
 
599
537
def action_svnpublish(req, fields):
600
538
    """Sets svn property "ivle:published" on each file specified.
602
540
    anyway).
603
541
 
604
542
    Reads fields: 'path'
605
 
 
606
 
    XXX Currently unused by the client (calls action_publish instead, which
607
 
    has a completely different publishing model).
608
543
    """
609
544
    paths = fields.getlist('path')
610
 
    if len(paths):
611
 
        paths = map(lambda path: actionpath_to_local(req, path), paths)
612
 
    else:
613
 
        paths = [studpath.url_to_jailpaths(req.path)[2]]
 
545
    paths = map(lambda path: actionpath_to_local(req, path), paths)
614
546
 
615
547
    try:
616
548
        for path in paths:
623
555
    """Deletes svn property "ivle:published" on each file specified.
624
556
 
625
557
    Reads fields: 'path'
626
 
 
627
 
    XXX Currently unused by the client (calls action_unpublish instead, which
628
 
    has a completely different publishing model).
629
558
    """
630
559
    paths = fields.getlist('path')
631
560
    paths = map(lambda path: actionpath_to_local(req, path), paths)
633
562
    try:
634
563
        for path in paths:
635
564
            svnclient.propdel("ivle:published", path, recurse=False)
636
 
    except pysvn.ClientError, e:
 
565
    except pysvn.ClientError:
637
566
        raise ActionError("Directory could not be unpublished")
638
567
 
639
568
def action_svncommit(req, fields):
648
577
 
649
578
    try:
650
579
        svnclient.checkin(paths, logmsg, recurse=True)
651
 
    except pysvn.ClientError, e:
652
 
        raise ActionError(str(e))
 
580
    except pysvn.ClientError:
 
581
        raise ActionError("One or more files could not be committed")
653
582
 
654
583
def action_svncheckout(req, fields):
655
584
    """Performs a "svn checkout" of each path specified.
664
593
    try:
665
594
        svnclient.callback_get_login = get_login
666
595
        svnclient.checkout(url, local_path, recurse=True)
667
 
    except pysvn.ClientError, e:
668
 
        raise ActionError(str(e))
 
596
    except pysvn.ClientError:
 
597
        raise ActionError("One or more files could not be checked out")
669
598
 
670
599
# Table of all action functions #
671
600
# Each function has the interface f(req, fields).
672
601
 
673
602
actions_table = {
674
 
    "delete" : action_delete,
 
603
    "remove" : action_remove,
675
604
    "move" : action_move,
676
605
    "mkdir" : action_mkdir,
677
606
    "putfile" : action_putfile,
678
607
    "putfiles" : action_putfiles,
 
608
 
 
609
    "copy" : action_copy,
 
610
    "cut" : action_cut,
679
611
    "paste" : action_paste,
680
 
    "publish" : action_publish,
681
 
    "unpublish" : action_unpublish,
682
612
 
683
613
    "svnadd" : action_svnadd,
684
 
    "svnremove" : action_svnremove,
685
614
    "svnupdate" : action_svnupdate,
686
 
    "svnresolved" : action_svnresolved,
687
615
    "svnrevert" : action_svnrevert,
688
616
    "svnpublish" : action_svnpublish,
689
617
    "svnunpublish" : action_svnunpublish,