~azzar1/unity/add-show-desktop-key

« back to all changes in this revision

Viewing changes to ivle/fileservice_lib/listing.py

  • Committer: William Grant
  • Date: 2009-01-13 01:36:15 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:1123
Merge setup-refactor branch. This completely breaks existing installations;
every path (both filesystem and Python) has changed. Do not upgrade without
knowing what you are doing.

Show diffs side-by-side

added added

removed removed

Lines of Context:
 
1
# IVLE
 
2
# Copyright (C) 2007-2008 The University of Melbourne
 
3
#
 
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.
 
8
#
 
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.
 
13
#
 
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
 
17
 
 
18
# Module: File Service / Listing
 
19
# Author: Matt Giuca
 
20
# Date: 10/1/2008
 
21
 
 
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.
 
24
 
 
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".
 
30
 
 
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".
 
35
#
 
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
 
61
#   (From pysvn)
 
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
 
68
#   files.
 
69
#   * type: String. Guessed mime type of the file. Present for non-directory
 
70
#   files.
 
71
#   * mtime: Number. Number of seconds elapsed since the epoch.
 
72
#   The epoch is not defined (this is an arbitrary number used for sorting
 
73
#   dates).
 
74
#   * mtime_nice: String. Modification time of the file or directory. Always
 
75
#   present unless svnstatus is "Missing". Human-friendly.
 
76
#
 
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
 
79
# present.
 
80
#
 
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).
 
86
#
 
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
 
91
# "paste" button.
 
92
#
 
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
 
96
#   the files.
 
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.
 
100
 
 
101
import os
 
102
import sys
 
103
import stat
 
104
import mimetypes
 
105
import urlparse
 
106
from cgi import parse_qs
 
107
 
 
108
import cjson
 
109
import pysvn
 
110
 
 
111
import ivle.svn
 
112
import ivle.date
 
113
from ivle import (util, studpath)
 
114
import ivle.conf.mimetypes
 
115
 
 
116
# Make a Subversion client object
 
117
svnclient = pysvn.Client()
 
118
 
 
119
# Whether or not to ignore dot files.
 
120
# TODO check settings!
 
121
ignore_dot_files = True
 
122
 
 
123
# Mime types
 
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"
 
128
 
 
129
def handle_return(req, return_contents):
 
130
    """
 
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.
 
135
 
 
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.
 
139
 
 
140
    If the path is a directory, return_contents is ignored.
 
141
    """
 
142
 
 
143
    (user, jail, path) = studpath.url_to_jailpaths(req.path)
 
144
 
 
145
    # FIXME: What to do about req.path == ""?
 
146
    # Currently goes to 403 Forbidden.
 
147
    urlpath = urlparse.urlparse(path)
 
148
    path = urlpath[2]
 
149
    if path is None:
 
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)
 
167
        if type is None:
 
168
            type = ivle.conf.mimetypes.default_mimetype
 
169
        req.content_type = type
 
170
        req.headers_out['X-IVLE-Return'] = 'File'
 
171
 
 
172
        send_file(req, svnclient, path)
 
173
    else:
 
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)))
 
178
 
 
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
 
183
       for the given path.
 
184
    '''
 
185
    # Work out the revisions from query
 
186
    r_str = req.get_fieldstorage().getfirst("r")
 
187
    revision = ivle.svn.revision_from_string(r_str)
 
188
 
 
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')
 
196
        req.flush()
 
197
        sys.exit()
 
198
    return revision
 
199
 
 
200
def send_file(req, svnclient, path):
 
201
    revision = _get_revision_or_die(req, svnclient, path)
 
202
    if revision:
 
203
        req.write(svnclient.cat(path, revision=revision))
 
204
    else:
 
205
        req.sendfile(path)
 
206
 
 
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.
 
210
 
 
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.
 
215
    """
 
216
 
 
217
    revision = _get_revision_or_die(req, svnclient, path)
 
218
 
 
219
    # Start by trying to do an SVN status, so we can report file version
 
220
    # status
 
221
    listing = {}
 
222
    try:
 
223
        if revision:
 
224
            ls_list = svnclient.list(path, revision=revision, recurse=False)
 
225
            for ls in ls_list:
 
226
                filename, attrs = PysvnList_to_fileinfo(path, ls)
 
227
                listing[filename.decode('utf-8')] = attrs
 
228
        else:
 
229
            status_list = svnclient.status(path, recurse=False, get_all=True,
 
230
                        update=False)
 
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.
 
237
        try:
 
238
            for filename in os.listdir(path):
 
239
                listing[filename.decode('utf-8')] = file_to_fileinfo(path, filename)[1]
 
240
        except OSError:
 
241
            # Non-directories will error - that's OK, we just want the "."
 
242
            pass
 
243
        # The subversion one includes "." while the OS one does not.
 
244
        # Add "." to the output, so the caller can see we are
 
245
        # unversioned.
 
246
        listing["."] = file_to_fileinfo(path, "")[1]
 
247
 
 
248
    if ignore_dot_files:
 
249
        for fn in listing.keys():
 
250
            if fn != "." and fn.startswith("."):
 
251
                del listing[fn]
 
252
 
 
253
    # Listing is a nested object inside the top-level JSON.
 
254
    listing = {"listing" : listing}
 
255
 
 
256
    if revision:
 
257
        listing['revision'] = revision.number
 
258
 
 
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']
 
266
    
 
267
    return listing
 
268
 
 
269
def _fullpath_stat_fileinfo(fullpath):
 
270
    file_stat = os.stat(fullpath)
 
271
    return _stat_fileinfo(fullpath, file_stat)
 
272
 
 
273
def _stat_fileinfo(fullpath, file_stat):
 
274
    d = {}
 
275
    if stat.S_ISDIR(file_stat.st_mode):
 
276
        d["isdir"] = True
 
277
        d["type_nice"] = util.nice_filetype("/")
 
278
        # Only directories can be published
 
279
        d["published"] = studpath.published(fullpath)
 
280
    else:
 
281
        d["isdir"] = False
 
282
        d["size"] = file_stat.st_size
 
283
        (type, _) = mimetypes.guess_type(fullpath)
 
284
        if type is None:
 
285
            type = ivle.conf.mimetypes.default_mimetype
 
286
        d["type"] = type
 
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)
 
291
    return d
 
292
 
 
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)
 
299
 
 
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
 
314
        # subversion status.
 
315
        filename = "."
 
316
    else:
 
317
        filename = os.path.basename(fullpath)
 
318
    text_status = status.text_status
 
319
    d = {'svnstatus': str(text_status)}
 
320
    try:
 
321
        d.update(_fullpath_stat_fileinfo(fullpath))
 
322
    except OSError:
 
323
        # Here if, eg, the file is missing.
 
324
        # Can't get any more information so just return d
 
325
        pass
 
326
    return filename, d
 
327
 
 
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)
 
333
    pysvnlist = list[0]
 
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
 
343
        # subversion status.
 
344
        filename = "."
 
345
    else:
 
346
        filename = os.path.basename(fullpath)
 
347
    d = {'svnstatus': 'revision'} # A special status.
 
348
 
 
349
    wrapped = ivle.svn.PysvnListStatWrapper(pysvnlist)
 
350
    d.update(_stat_fileinfo(fullpath, wrapped))
 
351
 
 
352
    return filename, d