91
91
# action=svnupdate: Bring a file up to date with the head revision.
92
92
# path: The path to the file to be updated. Only one file may be
94
# revision: The revision number to update to. If not provided this
97
95
# action=svnpublish: Set the "published" flag on a file to True.
98
96
# path: The path to the file to be published. Can be specified
121
119
# path: The path to the directory to be checked (under the IVLE
122
120
# repository base).
124
# action=svncleanup: Recursively clean up the working copy, removing locks,
125
# resuming unfinished operations, etc.
126
# path: The path to the directory to be cleaned
128
122
# TODO: Implement the following actions:
129
123
# svnupdate (done?)
130
124
# TODO: Implement ZIP unpacking in putfiles (done?).
144
137
from ivle import (util, studpath, zip)
145
from ivle.fileservice_lib.exceptions import WillNotOverwrite
149
# Make a Subversion client object (which will log in with this user's
150
# credentials, upon request)
151
svnclient = ivle.svn.create_auth_svn_client(username=ivle.conf.login,
152
password=ivle.conf.svn_pass)
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 != "", str(ivle.conf.login),
153
str(ivle.conf.svn_pass), True)
155
# Make a Subversion client object
156
svnclient = pysvn.Client()
157
svnclient.callback_get_login = get_login
153
158
svnclient.exception_style = 0 # Simple (string) exceptions
155
160
DEFAULT_LOGMESSAGE = "No log message supplied."
191
196
# Default, just send an error but then continue
192
197
raise ActionError("Unknown action")
193
return action(req, fields)
195
200
def actionpath_to_urlpath(req, path):
196
201
"""Determines the URL path (relative to the student home) upon which the
222
227
Does not mutate req.
224
r = studpath.to_home_path(actionpath_to_urlpath(req, path))
229
(_, _, r) = studpath.url_to_jailpaths(actionpath_to_urlpath(req, path))
226
231
raise ActionError("Invalid path")
259
264
except shutil.Error:
260
265
raise ActionError("Could not move the file specified")
262
def svn_movefile(req, frompath, topath, copy=False):
263
"""Performs an svn move, resolving filenames, checking for any errors,
264
and throwing ActionErrors if necessary. Can also be used to do a copy
267
frompath and topath are straight paths from the client. Will be checked.
269
if frompath is None or topath is None:
270
raise ActionError("Required field missing")
271
frompath = actionpath_to_local(req, frompath)
272
topath = actionpath_to_local(req, topath)
273
if not os.path.exists(frompath):
274
raise ActionError("The source file does not exist")
275
if os.path.exists(topath):
276
if frompath == topath:
277
raise ActionError("Source and destination are the same")
278
raise ActionError("A file already exists with that name")
282
svnclient.copy(frompath, topath)
284
svnclient.move(frompath, topath)
286
raise ActionError("Could not move the file specified")
287
except pysvn.ClientError:
288
raise ActionError("Could not move the file specified")
293
269
def action_delete(req, fields):
396
372
Reads fields: 'path', 'data' (file upload, multiple), 'unpack'
398
375
# Important: Data is "None" if the file submitted is empty.
399
376
path = fields.getfirst('path')
400
377
data = fields['data']
413
390
for datum in data:
414
391
# Each of the uploaded files
415
392
filepath = os.path.join(path, datum.filename)
416
filepath_local = studpath.to_home_path(filepath)
417
if os.path.isdir(filepath_local):
418
raise ActionError("A directory already exists "
421
if os.path.exists(filepath_local):
422
raise ActionError("A file already exists with that name")
423
393
filedata = datum.file
425
395
if unpack and datum.filename.lower().endswith(".zip"):
431
401
# First get the entire path (within jail)
432
abspath = studpath.to_home_path(path)
402
_, _, abspath = studpath.url_to_jailpaths(path)
433
403
abspath = os.path.join(os.sep, abspath)
434
404
zip.unzip(abspath, filedata)
435
405
except (OSError, IOError):
437
except WillNotOverwrite, e:
438
raise ActionError("File '" + e.filename + "' already exists.")
441
filepath_local = studpath.to_home_path(filepath)
409
(_, _, filepath_local) = studpath.url_to_jailpaths(filepath)
442
410
if filepath_local is None:
443
411
raise ActionError("Invalid path")
477
445
files = fields.getlist('file')
478
446
if dst is None or src is None or mode is None:
479
447
raise ActionError("Required field missing")
453
raise ActionError("Invalid mode (must be 'copy' or 'move')")
481
454
dst_local = actionpath_to_local(req, dst)
482
455
if not os.path.isdir(dst_local):
483
456
raise ActionError("dst is not a directory")
492
465
# The destination is found by taking just the basename of the file
493
466
topath = os.path.join(dst, os.path.basename(file))
496
movefile(req, frompath, topath, True)
498
movefile(req, frompath, topath, False)
499
elif mode == "svncopy":
500
svn_movefile(req, frompath, topath, True)
501
elif mode == "svnmove":
502
svn_movefile(req, frompath, topath, False)
504
raise ActionError("Invalid mode (must be '(svn)copy' or '(svn)move')")
468
movefile(req, frompath, topath, copy)
505
469
except ActionError, message:
506
470
# Store the error for later; we want to copy as many as possible
507
471
if errormsg is None:
525
489
Reads fields: 'path'
527
491
paths = fields.getlist('path')
528
user = util.split_path(req.path)[0]
492
user = studpath.url_to_local(req.path)[0]
529
493
homedir = "/home/%s" % user
531
495
paths = map(lambda path: actionpath_to_local(req, path), paths)
533
paths = [studpath.to_home_path(req.path)]
497
paths = [studpath.url_to_jailpaths(req.path)[2]]
535
499
# Set all the dirs in home dir world browsable (o+r,o+x)
536
500
#FIXME: Should really only do those in the direct path not all of the
603
567
def action_svnupdate(req, fields):
604
568
"""Performs a "svn update" to each file specified.
606
Reads fields: 'path' and 'revision'
608
572
path = fields.getfirst('path')
609
revision = fields.getfirst('revision')
611
574
raise ActionError("Required field missing")
613
revision = pysvn.Revision( pysvn.opt_revision_kind.head )
616
revision = pysvn.Revision(pysvn.opt_revision_kind.number,
618
except ValueError, e:
619
raise ActionError("Bad revision number: '%s'"%revision,)
620
575
path = actionpath_to_local(req, path)
623
svnclient.update(path, recurse=True, revision=revision)
578
svnclient.update(path, recurse=True)
624
579
except pysvn.ClientError, e:
625
580
raise ActionError(str(e))
715
670
paths = fields.getlist('path')
716
671
if len(paths) != 2:
717
672
raise ActionError("usage: svncheckout url local-path")
718
url = ivle.conf.svn_addr + "/" + urllib.quote(paths[0])
673
url = ivle.conf.svn_addr + "/" + paths[0]
719
674
local_path = actionpath_to_local(req, str(paths[1]))
676
svnclient.callback_get_login = get_login
721
677
svnclient.checkout(url, local_path, recurse=True)
722
678
except pysvn.ClientError, e:
723
679
raise ActionError(str(e))
730
686
path = fields.getfirst('path')
731
687
logmsg = fields.getfirst('logmsg')
732
url = ivle.conf.svn_addr + "/" + urllib.quote(path)
688
url = ivle.conf.svn_addr + "/" + path
690
svnclient.callback_get_login = get_login
734
691
svnclient.mkdir(url, log_message=logmsg)
735
692
except pysvn.ClientError, e:
736
693
raise ActionError(str(e))
738
695
def action_svnrepostat(req, fields):
739
696
"""Discovers whether a path exists in a repo under the IVLE SVN root.
741
If it does exist, returns a dict containing its metadata.
743
698
Reads fields: 'path'
745
700
path = fields.getfirst('path')
746
url = ivle.conf.svn_addr + "/" + urllib.quote(path)
747
svnclient.exception_style = 1
701
url = ivle.conf.svn_addr + "/" + path
702
svnclient.exception_style = 1
750
info = svnclient.info2(url,
751
revision=pysvn.Revision(pysvn.opt_revision_kind.head))[0][1]
752
return {'svnrevision': info['rev'].number
754
info['rev'].kind == pysvn.opt_revision_kind.number
705
svnclient.callback_get_login = get_login
756
707
except pysvn.ClientError, e:
757
708
# Error code 170000 means ENOENT in this revision.
758
709
if e[1][0][1] == 170000:
761
712
raise ActionError(str(e[0]))
764
def action_svncleanup(req, fields):
765
"""Recursively clean up the working copy, removing locks, resuming
766
unfinished operations, etc.
767
path: The path to be cleaned"""
769
path = fields.getfirst('path')
771
raise ActionError("Required field missing")
772
path = actionpath_to_local(req, path)
775
svnclient.cleanup(path)
776
except pysvn.ClientError, e:
777
raise ActionError(str(e))
780
714
# Table of all action functions #
781
715
# Each function has the interface f(req, fields).
801
735
"svncheckout" : action_svncheckout,
802
736
"svnrepomkdir" : action_svnrepomkdir,
803
737
"svnrepostat" : action_svnrepostat,
804
"svncleanup" : action_svncleanup,