2
# Copyright (C) 2007-2008 The University of Melbourne
4
# This program is free software; you can redistribute it and/or modify
5
# it under the terms of the GNU General Public License as published by
6
# the Free Software Foundation; either version 2 of the License, or
7
# (at your option) any later version.
9
# This program is distributed in the hope that it will be useful,
10
# but WITHOUT ANY WARRANTY; without even the implied warranty of
11
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
12
# GNU General Public License for more details.
14
# You should have received a copy of the GNU General Public License
15
# along with this program; if not, write to the Free Software
16
# Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA 02110-1301 USA
18
# Module: File Service / Listing
22
# Handles the return part of the 2-stage process of fileservice. This
23
# is both the directory listing, and the raw serving of non-directory files.
25
# File Service Format.
26
# If a non-directory file is requested, then the HTTP response body will be
27
# the verbatim bytes of that file (if the file is valid). The HTTP response
28
# headers will include the guessed content type of the file, and the header
29
# "X-IVLE-Return: File".
31
# Directory Listing Format.
32
# If the path requested is a directory, then the HTTP response body will be
33
# a valid JSON string describing the directory. The HTTP response headers
34
# will include the header "X-IVLE-Return: Dir".
36
# The JSON structure is as follows:
37
# * The top-level value is an object. It always contains the key "listing",
38
# whose value is the primary listing object. It may also contain a key
39
# "clipboard" which contains the clipboard object.
40
# * The value for "listing" is an object, with one member for each file in the
41
# directory, plus an additional member (key ".") for the directory itself.
42
# * Each member's key is the filename. Its value is an object, which has
43
# various members describing the file.
44
# The members of this object are as follows:
45
# * svnstatus: String. The svn status of the file. Either all files in a
46
# directory or no files have an svnstatus. String may take the values:
47
# - none - does not exist
48
# - unversioned - is not a versioned thing in this wc
49
# - normal - exists, but uninteresting.
50
# - added - is scheduled for addition
51
# - missing - under v.c., but is missing
52
# - deleted - scheduled for deletion
53
# - replaced - was deleted and then re-added
54
# - modified - text or props have been modified
55
# - merged - local mods received repos mods
56
# - conflicted - local mods received conflicting repos mods
57
# - ignored - a resource marked as ignored
58
# - obstructed - an unversioned resource is in the way of the versioned resource
59
# - external - an unversioned path populated by an svn:external property
60
# - incomplete - a directory doesn't contain a complete entries list
62
# If svnstatus is "Missing" then the file has no other attributes.
63
# * published: Boolean. True if the file is published. (Marked by a
64
# .published file in the folder)
65
# * isdir: Boolean. True if the file is a directory. Always present unless
66
# svnstatus is "missing".
67
# * size: Number. Size of the file in bytes. Present for non-directory
69
# * type: String. Guessed mime type of the file. Present for non-directory
71
# * mtime: Number. Number of seconds elapsed since the epoch.
72
# The epoch is not defined (this is an arbitrary number used for sorting
74
# * mtime_nice: String. Modification time of the file or directory. Always
75
# present unless svnstatus is "Missing". Human-friendly.
77
# Members are not guaranteed to be present - client code should always check
78
# for each member that it is present, and handle gracefully if a member is not
81
# The listing object is guaranteed to have a "." key. Use this key to
82
# determine whether the directory is under version control or not. If this
83
# member does NOT have a "svnstatus" key, or "svnstatus" is "unversioned",
84
# then the directory is not under revision control (and no other files will
85
# have "svnstatus" either).
87
# The top-level object MAY contain a "clipboard" key, which specifies the
88
# files copied to the clipboard. This can be used by the client to show the
89
# user what files will be pasted. At the very least, the client should take
90
# the presence or absence of a "clipboard" key as whether to grey out the
93
# The "clipboard" object has three members:
94
# * mode: String. Either "copy" or "cut".
95
# * base: String. Path relative to the user's root. The common path between
97
# * files: Array of Strings. Each element is a filename relative to base.
98
# Base and files exactly correspond to the listing path and argument paths
99
# which were supplied during the last copy or cut request.
106
from cgi import parse_qs
113
from ivle import (util, studpath)
114
import ivle.conf.mimetypes
116
# Make a Subversion client object
117
svnclient = pysvn.Client()
119
# Whether or not to ignore dot files.
120
# TODO check settings!
121
ignore_dot_files = True
124
# application/json is the "best" content type but is not good for
125
# debugging because Firefox just tries to download it
126
mime_dirlisting = "text/plain"
127
#mime_dirlisting = "application/json"
129
def handle_return(req, return_contents):
131
Perform the "return" part of the response.
132
This function returns the file or directory listing contained in
133
req.path. Sets the HTTP response code in req, writes additional headers,
134
and writes the HTTP response, if any.
136
If return_contents is True, and the path is a non-directory, returns the
137
contents of the file verbatim. If False, returns a directory listing
138
with a single file, ".", and info about the file.
140
If the path is a directory, return_contents is ignored.
143
(user, jail, path) = studpath.url_to_jailpaths(req.path)
145
# FIXME: What to do about req.path == ""?
146
# Currently goes to 403 Forbidden.
147
urlpath = urlparse.urlparse(path)
150
req.status = req.HTTP_FORBIDDEN
151
req.headers_out['X-IVLE-Return-Error'] = 'Forbidden'
152
req.write("Forbidden")
153
elif not os.access(path, os.R_OK):
154
req.status = req.HTTP_NOT_FOUND
155
req.headers_out['X-IVLE-Return-Error'] = 'File not found'
156
req.write("File not found")
157
elif os.path.isdir(path):
158
# It's a directory. Return the directory listing.
159
req.content_type = mime_dirlisting
160
req.headers_out['X-IVLE-Return'] = 'Dir'
161
req.write(cjson.encode(get_dirlisting(req, svnclient, path)))
162
elif return_contents:
163
# It's a file. Return the file contents.
164
# First get the mime type of this file
165
# (Note that importing ivle.util has already initialised mime types)
166
(type, _) = mimetypes.guess_type(path)
168
type = ivle.conf.mimetypes.default_mimetype
169
req.content_type = type
170
req.headers_out['X-IVLE-Return'] = 'File'
172
send_file(req, svnclient, path)
174
# It's a file. Return a "fake directory listing" with just this file.
175
req.content_type = mime_dirlisting
176
req.headers_out['X-IVLE-Return'] = 'File'
177
req.write(cjson.encode(get_dirlisting(req, svnclient, path)))
179
def _get_revision_or_die(req, svnclient, path):
180
'''Looks for a revision specification in req's URL, returning the revision
181
specified. Returns None if there was no revision specified. Errors and
182
terminates the request if the specification was bad, or it doesn't exist
185
# Work out the revisions from query
186
r_str = req.get_fieldstorage().getfirst("r")
187
revision = ivle.svn.revision_from_string(r_str)
189
# Was some revision specified AND (it didn't resolve OR it was nonexistent)
190
if r_str and not (revision and
191
ivle.svn.revision_exists(svnclient, path, revision)):
192
req.status = req.HTTP_NOT_FOUND
193
req.headers_out['X-IVLE-Return-Error'] = 'Revision not found'
194
req.ensure_headers_written()
195
req.write('Revision not found')
200
def send_file(req, svnclient, path):
201
revision = _get_revision_or_die(req, svnclient, path)
203
req.write(svnclient.cat(path, revision=revision))
207
def get_dirlisting(req, svnclient, path):
208
"""Given a local absolute path, creates a directory listing object
209
ready to be JSONized and sent to the client.
211
req: Request object. Will not be mutated; just reads the session.
212
svnclient: Svn client object.
213
path: String. Absolute path on the local file system. Not checked,
214
must already be guaranteed safe. May be a file or a directory.
217
revision = _get_revision_or_die(req, svnclient, path)
219
# Start by trying to do an SVN status, so we can report file version
224
ls_list = svnclient.list(path, revision=revision, recurse=False)
226
filename, attrs = PysvnList_to_fileinfo(path, ls)
227
listing[filename.decode('utf-8')] = attrs
229
status_list = svnclient.status(path, recurse=False, get_all=True,
231
for status in status_list:
232
filename, attrs = PysvnStatus_to_fileinfo(path, status)
233
listing[filename.decode('utf-8')] = attrs
234
except pysvn.ClientError:
235
# Presumably the directory is not under version control.
236
# Fallback to just an OS file listing.
238
for filename in os.listdir(path):
239
listing[filename.decode('utf-8')] = file_to_fileinfo(path, filename)[1]
241
# Non-directories will error - that's OK, we just want the "."
243
# The subversion one includes "." while the OS one does not.
244
# Add "." to the output, so the caller can see we are
246
listing["."] = file_to_fileinfo(path, "")[1]
249
for fn in listing.keys():
250
if fn != "." and fn.startswith("."):
253
# Listing is a nested object inside the top-level JSON.
254
listing = {"listing" : listing}
257
listing['revision'] = revision.number
259
# The other object is the clipboard, if present in the browser session.
260
# This can go straight from the session to JSON.
261
session = req.get_session()
262
if session and 'clipboard' in session:
263
# In CGI mode, we can't get our hands on the
264
# session (for the moment), so just leave it out.
265
listing['clipboard'] = session['clipboard']
269
def _fullpath_stat_fileinfo(fullpath):
270
file_stat = os.stat(fullpath)
271
return _stat_fileinfo(fullpath, file_stat)
273
def _stat_fileinfo(fullpath, file_stat):
275
if stat.S_ISDIR(file_stat.st_mode):
277
d["type_nice"] = util.nice_filetype("/")
278
# Only directories can be published
279
d["published"] = studpath.published(fullpath)
282
d["size"] = file_stat.st_size
283
(type, _) = mimetypes.guess_type(fullpath)
285
type = ivle.conf.mimetypes.default_mimetype
287
d["type_nice"] = util.nice_filetype(fullpath)
288
d["mtime"] = file_stat.st_mtime
289
d["mtime_nice"] = ivle.date.make_date_nice(file_stat.st_mtime)
290
d["mtime_short"] = ivle.date.make_date_nice_short(file_stat.st_mtime)
293
def file_to_fileinfo(path, filename):
294
"""Given a filename (relative to a given path), gets all the info "ls"
295
needs to display about the filename. Returns pair mapping filename to
296
a dict containing a number of other fields."""
297
fullpath = path if filename in ('', '.') else os.path.join(path, filename)
298
return filename, _fullpath_stat_fileinfo(fullpath)
300
def PysvnStatus_to_fileinfo(path, status):
301
"""Given a PysvnStatus object, gets all the info "ls"
302
needs to display about the filename. Returns a pair mapping filename to
303
a dict containing a number of other fields."""
304
path = os.path.normcase(path)
305
fullpath = status.path
306
# If this is "." (the directory itself)
307
if path == os.path.normcase(fullpath):
308
# If this directory is unversioned, then we aren't
309
# looking at any interesting files, so throw
310
# an exception and default to normal OS-based listing.
311
if status.text_status == pysvn.wc_status_kind.unversioned:
312
raise pysvn.ClientError
313
# We actually want to return "." because we want its
317
filename = os.path.basename(fullpath)
318
text_status = status.text_status
319
d = {'svnstatus': str(text_status)}
321
d.update(_fullpath_stat_fileinfo(fullpath))
323
# Here if, eg, the file is missing.
324
# Can't get any more information so just return d
328
def PysvnList_to_fileinfo(path, list):
329
"""Given a List object from pysvn.Client.list, gets all the info "ls"
330
needs to display about the filename. Returns a pair mapping filename to
331
a dict containing a number of other fields."""
332
path = os.path.normcase(path)
334
fullpath = pysvnlist.path
335
# If this is "." (the directory itself)
336
if path == os.path.normcase(fullpath):
337
# If this directory is unversioned, then we aren't
338
# looking at any interesting files, so throw
339
# an exception and default to normal OS-based listing.
340
#if status.text_status == pysvn.wc_status_kind.unversioned:
341
# raise pysvn.ClientError
342
# We actually want to return "." because we want its
346
filename = os.path.basename(fullpath)
347
d = {'svnstatus': 'revision'} # A special status.
349
wrapped = ivle.svn.PysvnListStatWrapper(pysvnlist)
350
d.update(_stat_fileinfo(fullpath, wrapped))