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
# action=svnpublish: Set the "published" flag on a file to True.
96
# path: The path to the file to be published. Can be specified
99
# action=svnunpublish: Set the "published" flag on a file to False.
100
# path: The path to the file to be unpublished. Can be specified
97
103
# action=svncommit: Commit a file(s) or directory(s) to the repository.
98
104
# path: The path to the file or directory to be committed. Can be
113
119
# path: The path to the directory to be checked (under the IVLE
114
120
# repository base).
116
# action=svncleanup: Recursively clean up the working copy, removing locks,
117
# resuming unfinished operations, etc.
118
# path: The path to the directory to be cleaned
120
122
# TODO: Implement the following actions:
121
123
# svnupdate (done?)
122
124
# TODO: Implement ZIP unpacking in putfiles (done?).
136
137
from ivle import (util, studpath, zip)
137
from ivle.fileservice_lib.exceptions import WillNotOverwrite
141
# Make a Subversion client object (which will log in with this user's
142
# credentials, upon request)
143
svnclient = ivle.svn.create_auth_svn_client(username=ivle.conf.login,
144
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 != "", 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
145
157
svnclient.exception_style = 0 # Simple (string) exceptions
147
159
DEFAULT_LOGMESSAGE = "No log message supplied."
251
263
except shutil.Error:
252
264
raise ActionError("Could not move the file specified")
254
def svn_movefile(req, frompath, topath, copy=False):
255
"""Performs an svn move, resolving filenames, checking for any errors,
256
and throwing ActionErrors if necessary. Can also be used to do a copy
259
frompath and topath are straight paths from the client. Will be checked.
261
if frompath is None or topath is None:
262
raise ActionError("Required field missing")
263
frompath = actionpath_to_local(req, frompath)
264
topath = actionpath_to_local(req, topath)
265
if not os.path.exists(frompath):
266
raise ActionError("The source file does not exist")
267
if os.path.exists(topath):
268
if frompath == topath:
269
raise ActionError("Source and destination are the same")
270
raise ActionError("A file already exists with that name")
274
svnclient.copy(frompath, topath)
276
svnclient.move(frompath, topath)
278
raise ActionError("Could not move the file specified")
279
except pysvn.ClientError:
280
raise ActionError("Could not move the file specified")
285
268
def action_delete(req, fields):
405
389
for datum in data:
406
390
# Each of the uploaded files
407
391
filepath = os.path.join(path, datum.filename)
408
filepath_local = studpath.to_home_path(filepath)
409
if os.path.isdir(filepath_local):
410
raise ActionError("A directory already exists "
413
if os.path.exists(filepath_local):
414
raise ActionError("A file already exists with that name")
415
392
filedata = datum.file
417
394
if unpack and datum.filename.lower().endswith(".zip"):
423
400
# First get the entire path (within jail)
424
abspath = studpath.to_home_path(path)
401
_, _, abspath = studpath.url_to_jailpaths(path)
425
402
abspath = os.path.join(os.sep, abspath)
426
403
zip.unzip(abspath, filedata)
427
404
except (OSError, IOError):
429
except WillNotOverwrite, e:
430
raise ActionError("File '" + e.filename + "' already exists.")
433
filepath_local = studpath.to_home_path(filepath)
408
(_, _, filepath_local) = studpath.url_to_jailpaths(filepath)
434
409
if filepath_local is None:
435
410
raise ActionError("Invalid path")
469
444
files = fields.getlist('file')
470
445
if dst is None or src is None or mode is None:
471
446
raise ActionError("Required field missing")
452
raise ActionError("Invalid mode (must be 'copy' or 'move')")
473
453
dst_local = actionpath_to_local(req, dst)
474
454
if not os.path.isdir(dst_local):
475
455
raise ActionError("dst is not a directory")
484
464
# The destination is found by taking just the basename of the file
485
465
topath = os.path.join(dst, os.path.basename(file))
488
movefile(req, frompath, topath, True)
490
movefile(req, frompath, topath, False)
491
elif mode == "svncopy":
492
svn_movefile(req, frompath, topath, True)
493
elif mode == "svnmove":
494
svn_movefile(req, frompath, topath, False)
496
raise ActionError("Invalid mode (must be '(svn)copy' or '(svn)move')")
467
movefile(req, frompath, topath, copy)
497
468
except ActionError, message:
498
469
# Store the error for later; we want to copy as many as possible
499
470
if errormsg is None:
517
488
Reads fields: 'path'
519
490
paths = fields.getlist('path')
520
user = util.split_path(req.path)[0]
491
user = studpath.url_to_local(req.path)[0]
521
492
homedir = "/home/%s" % user
523
494
paths = map(lambda path: actionpath_to_local(req, path), paths)
525
paths = [studpath.to_home_path(req.path)]
496
paths = [studpath.url_to_jailpaths(req.path)[2]]
527
498
# Set all the dirs in home dir world browsable (o+r,o+x)
528
499
#FIXME: Should really only do those in the direct path not all of the
572
543
Reads fields: 'path' (multiple)
574
545
paths = fields.getlist('path')
575
paths = map(lambda path: actionpath_to_local(req, path).decode('utf-8'),
546
paths = map(lambda path: actionpath_to_local(req, path), paths)
579
549
svnclient.add(paths, recurse=True, force=True)
586
556
Reads fields: 'path' (multiple)
588
558
paths = fields.getlist('path')
589
paths = map(lambda path: actionpath_to_local(req, path).decode('utf-8'),
559
paths = map(lambda path: actionpath_to_local(req, path), paths)
593
562
svnclient.remove(paths, force=True)
597
566
def action_svnupdate(req, fields):
598
567
"""Performs a "svn update" to each file specified.
600
Reads fields: 'path' and 'revision'
602
571
path = fields.getfirst('path')
603
revision = fields.getfirst('revision')
605
573
raise ActionError("Required field missing")
607
revision = pysvn.Revision( pysvn.opt_revision_kind.head )
610
revision = pysvn.Revision(pysvn.opt_revision_kind.number,
612
except ValueError, e:
613
raise ActionError("Bad revision number: '%s'"%revision,)
614
path = actionpath_to_local(req, path).decode('utf-8')
574
path = actionpath_to_local(req, path)
617
svnclient.update(path, recurse=True, revision=revision)
577
svnclient.update(path, recurse=True)
618
578
except pysvn.ClientError, e:
619
579
raise ActionError(str(e))
639
599
Reads fields: 'path' (multiple)
641
601
paths = fields.getlist('path')
642
paths = map(lambda path: actionpath_to_local(req, path).decode('utf-8'),
602
paths = map(lambda path: actionpath_to_local(req, path), paths)
646
605
svnclient.revert(paths, recurse=True)
647
606
except pysvn.ClientError, e:
648
607
raise ActionError(str(e))
609
def action_svnpublish(req, fields):
610
"""Sets svn property "ivle:published" on each file specified.
611
Should only be called on directories (only effective on directories
616
XXX Currently unused by the client (calls action_publish instead, which
617
has a completely different publishing model).
619
paths = fields.getlist('path')
621
paths = map(lambda path: actionpath_to_local(req, path), paths)
623
paths = [studpath.url_to_jailpaths(req.path)[2]]
627
# Note: Property value doesn't matter
628
svnclient.propset("ivle:published", "", path, recurse=False)
629
except pysvn.ClientError, e:
630
raise ActionError("Directory could not be published")
632
def action_svnunpublish(req, fields):
633
"""Deletes svn property "ivle:published" on each file specified.
637
XXX Currently unused by the client (calls action_unpublish instead, which
638
has a completely different publishing model).
640
paths = fields.getlist('path')
641
paths = map(lambda path: actionpath_to_local(req, path), paths)
645
svnclient.propdel("ivle:published", path, recurse=False)
646
except pysvn.ClientError, e:
647
raise ActionError("Directory could not be unpublished")
650
649
def action_svncommit(req, fields):
651
650
"""Performs a "svn commit" to each file specified.
653
652
Reads fields: 'path' (multiple), 'logmsg' (optional)
655
654
paths = fields.getlist('path')
657
paths = map(lambda path:actionpath_to_local(req,path).decode('utf-8'),
660
paths = [studpath.to_home_path(req.path).decode('utf-8')]
661
logmsg = str(fields.getfirst('logmsg',
662
DEFAULT_LOGMESSAGE)).decode('utf-8')
655
paths = map(lambda path: actionpath_to_local(req, str(path)), paths)
656
logmsg = str(fields.getfirst('logmsg', DEFAULT_LOGMESSAGE))
663
657
if logmsg == '': logmsg = DEFAULT_LOGMESSAGE
675
669
paths = fields.getlist('path')
676
670
if len(paths) != 2:
677
671
raise ActionError("usage: svncheckout url local-path")
678
url = ivle.conf.svn_addr + "/" + urllib.quote(paths[0])
672
url = ivle.conf.svn_addr + "/" + paths[0]
679
673
local_path = actionpath_to_local(req, str(paths[1]))
680
url = url.decode('utf-8')
681
local_path = local_path.decode('utf-8')
675
svnclient.callback_get_login = get_login
683
676
svnclient.checkout(url, local_path, recurse=True)
684
677
except pysvn.ClientError, e:
685
678
raise ActionError(str(e))
692
685
path = fields.getfirst('path')
693
686
logmsg = fields.getfirst('logmsg')
694
url = (ivle.conf.svn_addr + "/" + urllib.quote(path)).decode('utf-8')
687
url = ivle.conf.svn_addr + "/" + path
689
svnclient.callback_get_login = get_login
696
690
svnclient.mkdir(url, log_message=logmsg)
697
691
except pysvn.ClientError, e:
698
692
raise ActionError(str(e))
700
694
def action_svnrepostat(req, fields):
701
695
"""Discovers whether a path exists in a repo under the IVLE SVN root.
703
If it does exist, returns a dict containing its metadata.
705
697
Reads fields: 'path'
707
699
path = fields.getfirst('path')
708
url = (ivle.conf.svn_addr + "/" + urllib.quote(path)).decode('utf-8')
709
svnclient.exception_style = 1
700
url = ivle.conf.svn_addr + "/" + path
701
svnclient.exception_style = 1
712
info = svnclient.info2(url,
713
revision=pysvn.Revision(pysvn.opt_revision_kind.head))[0][1]
714
return {'svnrevision': info['rev'].number
716
info['rev'].kind == pysvn.opt_revision_kind.number
704
svnclient.callback_get_login = get_login
718
706
except pysvn.ClientError, e:
719
707
# Error code 170000 means ENOENT in this revision.
720
708
if e[1][0][1] == 170000:
723
711
raise ActionError(str(e[0]))
726
def action_svncleanup(req, fields):
727
"""Recursively clean up the working copy, removing locks, resuming
728
unfinished operations, etc.
729
path: The path to be cleaned"""
731
path = fields.getfirst('path')
733
raise ActionError("Required field missing")
734
path = actionpath_to_local(req, path).decode('utf-8')
737
svnclient.cleanup(path)
738
except pysvn.ClientError, e:
739
raise ActionError(str(e))
742
713
# Table of all action functions #
743
714
# Each function has the interface f(req, fields).
757
728
"svnupdate" : action_svnupdate,
758
729
"svnresolved" : action_svnresolved,
759
730
"svnrevert" : action_svnrevert,
731
"svnpublish" : action_svnpublish,
732
"svnunpublish" : action_svnunpublish,
760
733
"svncommit" : action_svncommit,
761
734
"svncheckout" : action_svncheckout,
762
735
"svnrepomkdir" : action_svnrepomkdir,
763
736
"svnrepostat" : action_svnrepostat,
764
"svncleanup" : action_svncleanup,