41
41
# to: The path of the target filename. Error if the file already
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.
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.
48
47
# data: Bytes to be written to the file verbatim. May either be
49
48
# a string variable or a file upload.
50
# unpack: Optional. If "true", and the data is a valid ZIP file,
51
# will create a directory instead and unpack the ZIP file
49
# overwrite: Optional. If supplied, the file will be overwritten.
50
# Otherwise, error if path already exists.
54
52
# action=putfiles: Upload multiple files to the student workspace, and
55
53
# optionally accept zip files which will be unpacked.
58
56
# data: A file upload (may not be a simple string). The filename
59
57
# will be used to determine the target filename within
61
# unpack: Optional. If "true", if any data is a valid ZIP file,
59
# unpack: Optional. If supplied, if any data is a valid ZIP file,
62
60
# 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.
65
67
# The differences between putfile and putfiles are:
66
# * putfile can only accept a single file.
68
# * putfile can only accept a single file, and can't unpack zipfiles.
67
69
# * putfile can accept string data, doesn't have to be a file upload.
68
70
# * putfile ignores the upload filename, the entire filename is specified on
69
71
# path. putfiles calls files after the name on the user's machine.
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
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
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.
94
81
# Subversion actions.
95
82
# action=svnadd: Add an existing file(s) to version control.
120
107
# logmsg: Text of the log message. Optional. There is a default log
121
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
113
# action=svnrepomkdir: Create a directory in a repository (not WC).
114
# path: The path to the directory to be created (under the IVLE
116
# logmsg: Text of the log message.
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
123
122
# TODO: Implement the following actions:
124
# putfiles, svnrevert, svnupdate, svncommit
125
# TODO: Implement ZIP unpacking in putfile and putfiles.
124
# TODO: Implement ZIP unpacking in putfiles (done?).
126
125
# TODO: svnupdate needs a digest to tell the user the files that were updated.
127
126
# This can be implemented by some message passing between action and
128
127
# listing, and having the digest included in the listing. (Problem if
138
137
from common import (util, studpath, zip)
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
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.
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 != "", conf.login, conf.svn_pass, True)
140
154
# Make a Subversion client object
141
155
svnclient = pysvn.Client()
156
svnclient.callback_get_login = get_login
157
svnclient.exception_style = 0 # Simple (string) exceptions
143
159
DEFAULT_LOGMESSAGE = "No log message supplied."
287
302
topath = fields.getfirst('to')
288
303
movefile(req, frompath, topath)
305
def action_mkdir(req, fields):
306
"""Creates a directory with the given path.
309
path = fields.getfirst('path')
311
raise ActionError("Required field missing")
312
path = actionpath_to_local(req, path)
314
if os.path.exists(path):
315
raise ActionError("A file already exists with that name")
317
# Create the directory
321
raise ActionError("Could not create directory")
290
323
def action_putfile(req, fields):
291
324
"""Writes data to a file, overwriting it if it exists and creating it if
294
Reads fields: 'path', 'data' (file upload)
327
Reads fields: 'path', 'data' (file upload), 'overwrite'
296
329
# TODO: Read field "unpack".
297
330
# Important: Data is "None" if the file submitted is empty.
298
331
path = fields.getfirst('path')
299
332
data = fields.getfirst('data')
300
if path is None or data is None:
301
334
raise ActionError("Required field missing")
336
# Workaround - field reader treats "" as None, so this is the only
337
# way to allow blank file uploads
302
339
path = actionpath_to_local(req, path)
304
341
if data is not None:
305
342
data = cStringIO.StringIO(data)
344
overwrite = fields.getfirst('overwrite')
345
if overwrite is None:
351
# Overwrite files; but can't if it's a directory
352
if os.path.isdir(path):
353
raise ActionError("A directory already exists "
356
if os.path.exists(path):
357
raise ActionError("A file already exists with that name")
307
359
# Copy the contents of file object 'data' to the path 'path'
309
361
dest = open(path, 'wb')
310
362
if data is not None:
311
363
shutil.copyfileobj(data, dest)
313
raise ActionError("Could not write to target file")
364
except (IOError, OSError), e:
365
raise ActionError("Could not write to target file: %s" % e.strerror)
315
367
def action_putfiles(req, fields):
316
368
"""Writes data to one or more files in a directory, overwriting them if
370
424
raise ActionError(
371
425
"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
files = map(lambda field: field.value, files)
386
clipboard = { "mode" : mode, "base" : req.path, "files" : files }
387
session = req.get_session()
388
session['clipboard'] = clipboard
391
def action_copy(req, fields):
392
"""Marks specified files on the clipboard, stored in the
393
browser session. Sets clipboard for a "copy" action.
397
action_copy_or_cut(req, fields, "copy")
399
def action_cut(req, fields):
400
"""Marks specified files on the clipboard, stored in the
401
browser session. Sets clipboard for a "cut" action.
405
action_copy_or_cut(req, fields, "cut")
407
427
def action_paste(req, fields):
408
"""Performs the copy or move action with the files stored on
409
the clipboard in the browser session. Copies/moves the files
410
to the specified directory. Clears the clipboard.
428
"""Performs the copy or move action with the files specified.
429
Copies/moves the files to the specified directory.
431
Reads fields: 'src', 'dst', 'mode', 'file' (multiple).
432
src: Base path that all the files are relative to (source).
433
dst: Destination path to paste into.
434
mode: 'copy' or 'move'.
435
file: (Multiple) Files relative to base, which will be copied
436
or moved to new locations relative to path.
416
todir = fields.getfirst('path')
440
dst = fields.getfirst('dst')
441
src = fields.getfirst('src')
442
mode = fields.getfirst('mode')
443
files = fields.getlist('file')
444
if dst is None or src is None or mode is None:
418
445
raise ActionError("Required field missing")
419
todir_local = actionpath_to_local(req, todir)
420
if not os.path.isdir(todir_local):
421
raise ActionError("Target is not a directory")
423
session = req.get_session()
425
clipboard = session['clipboard']
426
files = clipboard['files']
427
base = clipboard['base']
428
if clipboard['mode'] == "copy":
433
raise ActionError("Clipboard was empty")
451
raise ActionError("Invalid mode (must be 'copy' or 'move')")
452
dst_local = actionpath_to_local(req, dst)
453
if not os.path.isdir(dst_local):
454
raise ActionError("dst is not a directory")
436
457
for file in files:
437
458
# The source must not be interpreted as relative to req.path
438
459
# Add a slash (relative to top-level)
439
frompath = os.sep + os.path.join(base, file)
462
frompath = os.path.join(src, file)
440
463
# The destination is found by taking just the basename of the file
441
topath = os.path.join(todir, os.path.basename(file))
464
topath = os.path.join(dst, os.path.basename(file))
443
466
movefile(req, frompath, topath, copy)
444
467
except ActionError, message:
451
474
# Add this file to errorfiles; it will be put back on the
452
475
# clipboard for possible future pasting.
453
476
errorfiles.append(file)
454
# If errors occured, augment the clipboard and raise ActionError
455
if len(errorfiles) > 0:
456
clipboard['files'] = errorfiles
457
session['clipboard'] = clipboard
477
if errormsg is not None:
459
478
raise ActionError(errormsg)
461
# Success: Clear the clipboard
462
del session['clipboard']
480
# XXX errorfiles contains a list of files that couldn't be pasted.
481
# we currently do nothing with this.
483
def action_publish(req,fields):
484
"""Marks the folder as published by adding a '.published' file to the
485
directory and ensuring that the parent directory permissions are correct
489
paths = fields.getlist('path')
490
user = studpath.url_to_local(req.path)[0]
491
homedir = "/home/%s" % user
493
paths = map(lambda path: actionpath_to_local(req, path), paths)
495
paths = [studpath.url_to_jailpaths(req.path)[2]]
497
# Set all the dirs in home dir world browsable (o+r,o+x)
498
#FIXME: Should really only do those in the direct path not all of the
499
# folders in a students home directory
500
for root,dirs,files in os.walk(homedir):
501
os.chmod(root, os.stat(root).st_mode|0005)
505
if os.path.isdir(path):
506
pubfile = open(os.path.join(path,'.published'),'w')
507
pubfile.write("This directory is published\n")
510
raise ActionError("Can only publish directories")
512
raise ActionError("Directory could not be published")
514
def action_unpublish(req,fields):
515
"""Marks the folder as unpublished by removing a '.published' file in the
516
directory (if it exits). It does not change the permissions of the parent
521
paths = fields.getlist('path')
523
paths = map(lambda path: actionpath_to_local(req, path), paths)
525
paths = [studpath.url_to_jailpaths(req.path)[2]]
529
if os.path.isdir(path):
530
pubfile = os.path.join(path,'.published')
531
if os.path.isfile(pubfile):
534
raise ActionError("Can only unpublish directories")
536
raise ActionError("Directory could not be unpublished")
465
539
def action_svnadd(req, fields):
466
540
"""Performs a "svn add" to each file specified.
474
548
svnclient.add(paths, recurse=True, force=True)
475
except pysvn.ClientError:
476
raise ActionError("One or more files could not be added")
549
except pysvn.ClientError, e:
550
raise ActionError(str(e))
552
def action_svnremove(req, fields):
553
"""Performs a "svn remove" on each file specified.
555
Reads fields: 'path' (multiple)
557
paths = fields.getlist('path')
558
paths = map(lambda path: actionpath_to_local(req, path), paths)
561
svnclient.remove(paths, force=True)
562
except pysvn.ClientError, e:
563
raise ActionError(str(e))
478
565
def action_svnupdate(req, fields):
479
566
"""Performs a "svn update" to each file specified.
489
576
svnclient.update(path, recurse=True)
490
except pysvn.ClientError:
491
raise ActionError("One or more files could not be updated")
577
except pysvn.ClientError, e:
578
raise ActionError(str(e))
580
def action_svnresolved(req, fields):
581
"""Performs a "svn resolved" to each file specified.
585
path = fields.getfirst('path')
587
raise ActionError("Required field missing")
588
path = actionpath_to_local(req, path)
591
svnclient.resolved(path, recurse=True)
592
except pysvn.ClientError, e:
593
raise ActionError(str(e))
493
595
def action_svnrevert(req, fields):
494
596
"""Performs a "svn revert" to each file specified.
511
613
Reads fields: 'path'
615
XXX Currently unused by the client (calls action_publish instead, which
616
has a completely different publishing model).
513
618
paths = fields.getlist('path')
514
paths = map(lambda path: actionpath_to_local(req, path), paths)
620
paths = map(lambda path: actionpath_to_local(req, path), paths)
622
paths = [studpath.url_to_jailpaths(req.path)[2]]
517
625
for path in paths:
518
626
# Note: Property value doesn't matter
519
627
svnclient.propset("ivle:published", "", path, recurse=False)
520
except pysvn.ClientError:
521
raise ActionError("One or more files could not be updated")
628
except pysvn.ClientError, e:
629
raise ActionError("Directory could not be published")
523
631
def action_svnunpublish(req, fields):
524
632
"""Deletes svn property "ivle:published" on each file specified.
526
634
Reads fields: 'path'
636
XXX Currently unused by the client (calls action_unpublish instead, which
637
has a completely different publishing model).
528
639
paths = fields.getlist('path')
529
640
paths = map(lambda path: actionpath_to_local(req, path), paths)
548
659
svnclient.checkin(paths, logmsg, recurse=True)
549
except pysvn.ClientError:
550
raise ActionError("One or more files could not be committed")
660
except pysvn.ClientError, e:
661
raise ActionError(str(e))
663
def action_svncheckout(req, fields):
664
"""Performs a "svn checkout" of the first path into the second path.
666
Reads fields: 'path' (multiple)
668
paths = fields.getlist('path')
670
raise ActionError("usage: svncheckout url local-path")
671
url = conf.svn_addr + "/" + paths[0]
672
local_path = actionpath_to_local(req, str(paths[1]))
674
svnclient.callback_get_login = get_login
675
svnclient.checkout(url, local_path, recurse=True)
676
except pysvn.ClientError, e:
677
raise ActionError(str(e))
679
def action_svnrepomkdir(req, fields):
680
"""Performs a "svn mkdir" on a path under the IVLE SVN root.
684
path = fields.getfirst('path')
685
logmsg = fields.getfirst('logmsg')
686
url = conf.svn_addr + "/" + path
688
svnclient.callback_get_login = get_login
689
svnclient.mkdir(url, log_message=logmsg)
690
except pysvn.ClientError, e:
691
raise ActionError(str(e))
693
def action_svnrepostat(req, fields):
694
"""Discovers whether a path exists in a repo under the IVLE SVN root.
698
path = fields.getfirst('path')
699
url = conf.svn_addr + "/" + path
700
svnclient.exception_style = 1
703
svnclient.callback_get_login = get_login
705
except pysvn.ClientError, e:
706
# Error code 170000 means ENOENT in this revision.
707
if e[1][0][1] == 170000:
708
raise util.IVLEError(404, 'The specified repository path does not exist')
710
raise ActionError(str(e[0]))
552
712
# Table of all action functions #
553
713
# Each function has the interface f(req, fields).
555
715
actions_table = {
556
"remove" : action_remove,
716
"delete" : action_delete,
557
717
"move" : action_move,
718
"mkdir" : action_mkdir,
558
719
"putfile" : action_putfile,
559
720
"putfiles" : action_putfiles,
561
"copy" : action_copy,
563
721
"paste" : action_paste,
722
"publish" : action_publish,
723
"unpublish" : action_unpublish,
565
725
"svnadd" : action_svnadd,
726
"svnremove" : action_svnremove,
566
727
"svnupdate" : action_svnupdate,
728
"svnresolved" : action_svnresolved,
567
729
"svnrevert" : action_svnrevert,
568
730
"svnpublish" : action_svnpublish,
569
731
"svnunpublish" : action_svnunpublish,
570
732
"svncommit" : action_svncommit,
733
"svncheckout" : action_svncheckout,
734
"svnrepomkdir" : action_svnrepomkdir,
735
"svnrepostat" : action_svnrepostat,