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.
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
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
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.
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.
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
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
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
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
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
94
# Subversion actions.
82
95
# action=svnadd: Add an existing file(s) to version control.
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
113
123
# TODO: Implement the following actions:
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
128
138
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)
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
150
143
DEFAULT_LOGMESSAGE = "No log message supplied."
294
287
topath = fields.getfirst('to')
295
288
movefile(req, frompath, topath)
297
def action_mkdir(req, fields):
298
"""Creates a directory with the given path.
301
path = fields.getfirst('path')
303
raise ActionError("Required field missing")
304
path = actionpath_to_local(req, path)
306
if os.path.exists(path):
307
raise ActionError("A file already exists with that name")
309
# Create the directory
313
raise ActionError("Could not create directory")
315
290
def action_putfile(req, fields):
316
291
"""Writes data to a file, overwriting it if it exists and creating it if
319
Reads fields: 'path', 'data' (file upload), 'overwrite'
294
Reads fields: 'path', 'data' (file upload)
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')
300
if path is None or data is None:
326
301
raise ActionError("Required field missing")
328
# Workaround - field reader treats "" as None, so this is the only
329
# way to allow blank file uploads
331
302
path = actionpath_to_local(req, path)
333
304
if data is not None:
334
305
data = cStringIO.StringIO(data)
336
overwrite = fields.getfirst('overwrite')
337
if overwrite is None:
343
# Overwrite files; but can't if it's a directory
344
if os.path.isdir(path):
345
raise ActionError("A directory already exists "
348
if os.path.exists(path):
349
raise ActionError("A file already exists with that name")
351
307
# Copy the contents of file object 'data' to the path 'path'
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)
313
raise ActionError("Could not write to target file")
359
315
def action_putfiles(req, fields):
360
316
"""Writes data to one or more files in a directory, overwriting them if
416
370
raise ActionError(
417
371
"Could not write to one or more of the target files")
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
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
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.
396
action_copy_or_cut(req, fields, "copy")
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.
404
action_copy_or_cut(req, fields, "cut")
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.
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.
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')
437
417
raise ActionError("Required field missing")
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")
422
session = req.get_session()
424
clipboard = session['clipboard']
425
files = clipboard['files']
426
base = clipboard['base']
427
if clipboard['mode'] == "copy":
432
raise ActionError("Clipboard was empty")
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)
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))
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
470
458
raise ActionError(errormsg)
472
# XXX errorfiles contains a list of files that couldn't be pasted.
473
# we currently do nothing with this.
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
481
paths = fields.getlist('path')
482
user = studpath.url_to_local(req.path)[0]
483
homedir = "/home/%s" % user
485
paths = map(lambda path: actionpath_to_local(req, path), paths)
487
paths = [studpath.url_to_jailpaths(req.path)[2]]
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)
497
if os.path.isdir(path):
498
pubfile = open(os.path.join(path,'.published'),'w')
499
pubfile.write("This directory is published\n")
502
raise ActionError("Can only publish directories")
504
raise ActionError("Directory could not be published")
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
513
paths = fields.getlist('path')
515
paths = map(lambda path: actionpath_to_local(req, path), paths)
517
paths = [studpath.url_to_jailpaths(req.path)[2]]
521
if os.path.isdir(path):
522
pubfile = os.path.join(path,'.published')
523
if os.path.isfile(pubfile):
526
raise ActionError("Can only unpublish directories")
528
raise ActionError("Directory could not be unpublished")
460
# Success: Clear the clipboard
461
del session['clipboard']
531
464
def action_svnadd(req, fields):
532
465
"""Performs a "svn add" to each file specified.
623
547
svnclient.checkin(paths, logmsg, recurse=True)
624
except pysvn.ClientError, e:
625
raise ActionError(str(e))
627
def action_svncheckout(req, fields):
628
"""Performs a "svn checkout" of each path specified.
630
Reads fields: 'path' (multiple)
632
paths = fields.getlist('path')
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]))
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")
643
551
# Table of all action functions #
644
552
# Each function has the interface f(req, fields).