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
95
97
# action=svnpublish: Set the "published" flag on a file to True.
96
98
# path: The path to the file to be published. Can be specified
119
121
# path: The path to the directory to be checked (under the IVLE
120
122
# 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
122
128
# TODO: Implement the following actions:
123
129
# svnupdate (done?)
124
130
# TODO: Implement ZIP unpacking in putfiles (done?).
137
144
from ivle import (util, studpath, zip)
145
from ivle.fileservice_lib.exceptions import WillNotOverwrite
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 != "", ivle.conf.login, ivle.conf.svn_pass, True)
154
# Make a Subversion client object
155
svnclient = pysvn.Client()
156
svnclient.callback_get_login = get_login
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)
157
153
svnclient.exception_style = 0 # Simple (string) exceptions
159
155
DEFAULT_LOGMESSAGE = "No log message supplied."
195
191
# Default, just send an error but then continue
196
192
raise ActionError("Unknown action")
193
return action(req, fields)
199
195
def actionpath_to_urlpath(req, path):
200
196
"""Determines the URL path (relative to the student home) upon which the
226
222
Does not mutate req.
228
(_, _, r) = studpath.url_to_jailpaths(actionpath_to_urlpath(req, path))
224
r = studpath.to_home_path(actionpath_to_urlpath(req, path))
230
226
raise ActionError("Invalid path")
263
259
except shutil.Error:
264
260
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")
268
293
def action_delete(req, fields):
371
396
Reads fields: 'path', 'data' (file upload, multiple), 'unpack'
374
398
# Important: Data is "None" if the file submitted is empty.
375
399
path = fields.getfirst('path')
376
400
data = fields['data']
389
413
for datum in data:
390
414
# Each of the uploaded files
391
415
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")
392
423
filedata = datum.file
394
425
if unpack and datum.filename.lower().endswith(".zip"):
400
431
# First get the entire path (within jail)
401
_, _, abspath = studpath.url_to_jailpaths(path)
432
abspath = studpath.to_home_path(path)
402
433
abspath = os.path.join(os.sep, abspath)
403
434
zip.unzip(abspath, filedata)
404
435
except (OSError, IOError):
437
except WillNotOverwrite, e:
438
raise ActionError("File '" + e.filename + "' already exists.")
408
(_, _, filepath_local) = studpath.url_to_jailpaths(filepath)
441
filepath_local = studpath.to_home_path(filepath)
409
442
if filepath_local is None:
410
443
raise ActionError("Invalid path")
444
477
files = fields.getlist('file')
445
478
if dst is None or src is None or mode is None:
446
479
raise ActionError("Required field missing")
452
raise ActionError("Invalid mode (must be 'copy' or 'move')")
453
481
dst_local = actionpath_to_local(req, dst)
454
482
if not os.path.isdir(dst_local):
455
483
raise ActionError("dst is not a directory")
464
492
# The destination is found by taking just the basename of the file
465
493
topath = os.path.join(dst, os.path.basename(file))
467
movefile(req, frompath, topath, copy)
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
505
except ActionError, message:
469
506
# Store the error for later; we want to copy as many as possible
470
507
if errormsg is None:
488
525
Reads fields: 'path'
490
527
paths = fields.getlist('path')
491
user = studpath.url_to_local(req.path)[0]
528
user = util.split_path(req.path)[0]
492
529
homedir = "/home/%s" % user
494
531
paths = map(lambda path: actionpath_to_local(req, path), paths)
496
paths = [studpath.url_to_jailpaths(req.path)[2]]
533
paths = [studpath.to_home_path(req.path)]
498
535
# Set all the dirs in home dir world browsable (o+r,o+x)
499
536
#FIXME: Should really only do those in the direct path not all of the
566
603
def action_svnupdate(req, fields):
567
604
"""Performs a "svn update" to each file specified.
606
Reads fields: 'path' and 'revision'
571
608
path = fields.getfirst('path')
609
revision = fields.getfirst('revision')
573
611
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,)
574
620
path = actionpath_to_local(req, path)
577
svnclient.update(path, recurse=True)
623
svnclient.update(path, recurse=True, revision=revision)
578
624
except pysvn.ClientError, e:
579
625
raise ActionError(str(e))
669
715
paths = fields.getlist('path')
670
716
if len(paths) != 2:
671
717
raise ActionError("usage: svncheckout url local-path")
672
url = ivle.conf.svn_addr + "/" + paths[0]
718
url = ivle.conf.svn_addr + "/" + urllib.quote(paths[0])
673
719
local_path = actionpath_to_local(req, str(paths[1]))
675
svnclient.callback_get_login = get_login
676
721
svnclient.checkout(url, local_path, recurse=True)
677
722
except pysvn.ClientError, e:
678
723
raise ActionError(str(e))
685
730
path = fields.getfirst('path')
686
731
logmsg = fields.getfirst('logmsg')
687
url = ivle.conf.svn_addr + "/" + path
732
url = ivle.conf.svn_addr + "/" + urllib.quote(path)
689
svnclient.callback_get_login = get_login
690
734
svnclient.mkdir(url, log_message=logmsg)
691
735
except pysvn.ClientError, e:
692
736
raise ActionError(str(e))
694
738
def action_svnrepostat(req, fields):
695
739
"""Discovers whether a path exists in a repo under the IVLE SVN root.
741
If it does exist, returns a dict containing its metadata.
697
743
Reads fields: 'path'
699
745
path = fields.getfirst('path')
700
url = ivle.conf.svn_addr + "/" + path
701
svnclient.exception_style = 1
746
url = ivle.conf.svn_addr + "/" + urllib.quote(path)
747
svnclient.exception_style = 1
704
svnclient.callback_get_login = get_login
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
706
756
except pysvn.ClientError, e:
707
757
# Error code 170000 means ENOENT in this revision.
708
758
if e[1][0][1] == 170000:
711
761
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))
713
780
# Table of all action functions #
714
781
# Each function has the interface f(req, fields).
734
801
"svncheckout" : action_svncheckout,
735
802
"svnrepomkdir" : action_svnrepomkdir,
736
803
"svnrepostat" : action_svnrepostat,
804
"svncleanup" : action_svncleanup,