41
41
# to: The path of the target filename. Error if the file already
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
54
# action=putfiles: Upload multiple files to the student workspace, and
53
55
# optionally accept zip files which will be unpacked.
65
67
# does. The dir will be made with this name.
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.
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
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
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
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
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.
81
98
# Subversion actions.
82
99
# action=svnadd: Add an existing file(s) to version control.
128
145
from common import (util, studpath, zip)
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
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.
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)
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
150
156
DEFAULT_LOGMESSAGE = "No log message supplied."
332
336
if data is not None:
333
337
data = cStringIO.StringIO(data)
335
overwrite = fields.getfirst('overwrite')
336
if overwrite is None:
342
# Overwrite files; but can't if it's a directory
343
if os.path.isdir(path):
344
raise ActionError("A directory already exists "
347
if os.path.exists(path):
348
raise ActionError("A file already exists with that name")
350
339
# Copy the contents of file object 'data' to the path 'path'
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)
345
raise ActionError("Could not write to target file")
358
347
def action_putfiles(req, fields):
359
348
"""Writes data to one or more files in a directory, overwriting them if
415
402
raise ActionError(
416
403
"Could not write to one or more of the target files")
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
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
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.
428
action_copy_or_cut(req, fields, "copy")
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.
436
action_copy_or_cut(req, fields, "cut")
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.
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.
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')
436
449
raise ActionError("Required field missing")
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")
454
session = req.get_session()
456
clipboard = session['clipboard']
457
files = clipboard['files']
458
base = clipboard['base']
459
if clipboard['mode'] == "copy":
464
raise ActionError("Clipboard was empty")
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)
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))
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
469
490
raise ActionError(errormsg)
471
# XXX errorfiles contains a list of files that couldn't be pasted.
472
# we currently do nothing with this.
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
480
paths = fields.getlist('path')
481
user = studpath.url_to_local(req.path)[0]
482
homedir = "/home/%s" % user
484
paths = map(lambda path: actionpath_to_local(req, path), paths)
486
paths = [studpath.url_to_jailpaths(req.path)[2]]
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)
496
if os.path.isdir(path):
497
pubfile = open(os.path.join(path,'.published'),'w')
498
pubfile.write("This directory is published\n")
501
raise ActionError("Can only publish directories")
503
raise ActionError("Directory could not be published")
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
512
paths = fields.getlist('path')
514
paths = map(lambda path: actionpath_to_local(req, path), paths)
516
paths = [studpath.url_to_jailpaths(req.path)[2]]
520
if os.path.isdir(path):
521
pubfile = os.path.join(path,'.published')
522
if os.path.isfile(pubfile):
525
raise ActionError("Can only unpublish directories")
527
raise ActionError("Directory could not be unpublished")
492
# Success: Clear the clipboard
493
del session['clipboard']
530
496
def action_svnadd(req, fields):
531
497
"""Performs a "svn add" to each file specified.
539
505
svnclient.add(paths, recurse=True, force=True)
540
except pysvn.ClientError, e:
541
raise ActionError(str(e))
543
def action_svnremove(req, fields):
544
"""Performs a "svn remove" on each file specified.
546
Reads fields: 'path' (multiple)
548
paths = fields.getlist('path')
549
paths = map(lambda path: actionpath_to_local(req, path), paths)
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")
556
509
def action_svnupdate(req, fields):
557
510
"""Performs a "svn update" to each file specified.
567
520
svnclient.update(path, recurse=True)
568
except pysvn.ClientError, e:
569
raise ActionError(str(e))
571
def action_svnresolved(req, fields):
572
"""Performs a "svn resolved" to each file specified.
576
path = fields.getfirst('path')
578
raise ActionError("Required field missing")
579
path = actionpath_to_local(req, path)
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")
586
524
def action_svnrevert(req, fields):
587
525
"""Performs a "svn revert" to each file specified.
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")
670
599
# Table of all action functions #
671
600
# Each function has the interface f(req, fields).
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,
609
"copy" : action_copy,
679
611
"paste" : action_paste,
680
"publish" : action_publish,
681
"unpublish" : action_unpublish,
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,