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

1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
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]
1089 by chadnickbok
Fixes Issue #14
149
    json = None
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
150
    if path is None:
151
        req.status = req.HTTP_FORBIDDEN
152
        req.headers_out['X-IVLE-Return-Error'] = 'Forbidden'
153
        req.write("Forbidden")
154
    elif not os.access(path, os.R_OK):
155
        req.status = req.HTTP_NOT_FOUND
156
        req.headers_out['X-IVLE-Return-Error'] = 'File not found'
157
        req.write("File not found")
158
    elif os.path.isdir(path):
159
        # It's a directory. Return the directory listing.
160
        req.content_type = mime_dirlisting
161
        req.headers_out['X-IVLE-Return'] = 'Dir'
1089 by chadnickbok
Fixes Issue #14
162
        # TODO: Fix this dirty, dirty hack
163
        newjson = get_dirlisting(req, svnclient, path)
164
        if ("X-IVLE-Action-Error" in req.headers_out):
165
            newjson["Error"] = req.headers_out["X-IVLE-Action-Error"]
166
        req.write(cjson.encode(newjson))
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
167
    elif return_contents:
168
        # It's a file. Return the file contents.
169
        # First get the mime type of this file
170
        # (Note that importing ivle.util has already initialised mime types)
171
        (type, _) = mimetypes.guess_type(path)
172
        if type is None:
173
            type = ivle.conf.mimetypes.default_mimetype
174
        req.content_type = type
175
        req.headers_out['X-IVLE-Return'] = 'File'
176
177
        send_file(req, svnclient, path)
178
    else:
179
        # It's a file. Return a "fake directory listing" with just this file.
180
        req.content_type = mime_dirlisting
181
        req.headers_out['X-IVLE-Return'] = 'File'
182
        req.write(cjson.encode(get_dirlisting(req, svnclient, path)))
183
184
def _get_revision_or_die(req, svnclient, path):
185
    '''Looks for a revision specification in req's URL, returning the revision
186
       specified. Returns None if there was no revision specified. Errors and
187
       terminates the request if the specification was bad, or it doesn't exist
188
       for the given path.
189
    '''
190
    # Work out the revisions from query
191
    r_str = req.get_fieldstorage().getfirst("r")
192
    revision = ivle.svn.revision_from_string(r_str)
193
194
    # Was some revision specified AND (it didn't resolve OR it was nonexistent)
195
    if r_str and not (revision and
196
                      ivle.svn.revision_exists(svnclient, path, revision)):
197
        req.status = req.HTTP_NOT_FOUND
198
        req.headers_out['X-IVLE-Return-Error'] = 'Revision not found'
199
        req.ensure_headers_written()
200
        req.write('Revision not found')
201
        req.flush()
202
        sys.exit()
203
    return revision
204
205
def send_file(req, svnclient, path):
206
    revision = _get_revision_or_die(req, svnclient, path)
207
    if revision:
208
        req.write(svnclient.cat(path, revision=revision))
209
    else:
210
        req.sendfile(path)
211
212
def get_dirlisting(req, svnclient, path):
213
    """Given a local absolute path, creates a directory listing object
214
    ready to be JSONized and sent to the client.
215
216
    req: Request object. Will not be mutated; just reads the session.
217
    svnclient: Svn client object.
218
    path: String. Absolute path on the local file system. Not checked,
219
        must already be guaranteed safe. May be a file or a directory.
220
    """
221
222
    revision = _get_revision_or_die(req, svnclient, path)
223
224
    # Start by trying to do an SVN status, so we can report file version
225
    # status
226
    listing = {}
227
    try:
228
        if revision:
229
            ls_list = svnclient.list(path, revision=revision, recurse=False)
230
            for ls in ls_list:
231
                filename, attrs = PysvnList_to_fileinfo(path, ls)
232
                listing[filename.decode('utf-8')] = attrs
233
        else:
234
            status_list = svnclient.status(path, recurse=False, get_all=True,
235
                        update=False)
236
            for status in status_list:
237
                filename, attrs = PysvnStatus_to_fileinfo(path, status)
238
                listing[filename.decode('utf-8')] = attrs
239
    except pysvn.ClientError:
240
        # Presumably the directory is not under version control.
241
        # Fallback to just an OS file listing.
242
        try:
243
            for filename in os.listdir(path):
244
                listing[filename.decode('utf-8')] = file_to_fileinfo(path, filename)[1]
1086 by chadnickbok
This commit fixes issue #10 and part of issue #9
245
                try:
246
                    svnclient.status(os.path.join(path, filename), recurse = False)
247
                    listing[filename.decode('utf-8')]['svnstatus'] = 'normal'
248
                except:
249
                    pass
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
250
        except OSError:
251
            # Non-directories will error - that's OK, we just want the "."
252
            pass
253
        # The subversion one includes "." while the OS one does not.
254
        # Add "." to the output, so the caller can see we are
255
        # unversioned.
256
        listing["."] = file_to_fileinfo(path, "")[1]
257
258
    if ignore_dot_files:
259
        for fn in listing.keys():
260
            if fn != "." and fn.startswith("."):
261
                del listing[fn]
262
263
    # Listing is a nested object inside the top-level JSON.
264
    listing = {"listing" : listing}
265
266
    if revision:
267
        listing['revision'] = revision.number
268
269
    # The other object is the clipboard, if present in the browser session.
270
    # This can go straight from the session to JSON.
271
    session = req.get_session()
272
    if session and 'clipboard' in session:
273
        # In CGI mode, we can't get our hands on the
274
        # session (for the moment), so just leave it out.
275
        listing['clipboard'] = session['clipboard']
276
    
277
    return listing
278
279
def _fullpath_stat_fileinfo(fullpath):
280
    file_stat = os.stat(fullpath)
281
    return _stat_fileinfo(fullpath, file_stat)
282
283
def _stat_fileinfo(fullpath, file_stat):
284
    d = {}
285
    if stat.S_ISDIR(file_stat.st_mode):
286
        d["isdir"] = True
287
        d["type_nice"] = util.nice_filetype("/")
288
        # Only directories can be published
289
        d["published"] = studpath.published(fullpath)
290
    else:
291
        d["isdir"] = False
292
        d["size"] = file_stat.st_size
293
        (type, _) = mimetypes.guess_type(fullpath)
294
        if type is None:
295
            type = ivle.conf.mimetypes.default_mimetype
296
        d["type"] = type
297
        d["type_nice"] = util.nice_filetype(fullpath)
298
    d["mtime"] = file_stat.st_mtime
299
    d["mtime_nice"] = ivle.date.make_date_nice(file_stat.st_mtime)
300
    d["mtime_short"] = ivle.date.make_date_nice_short(file_stat.st_mtime)
301
    return d
302
303
def file_to_fileinfo(path, filename):
304
    """Given a filename (relative to a given path), gets all the info "ls"
305
    needs to display about the filename. Returns pair mapping filename to
306
    a dict containing a number of other fields."""
307
    fullpath = path if filename in ('', '.') else os.path.join(path, filename)
308
    return filename, _fullpath_stat_fileinfo(fullpath)
309
310
def PysvnStatus_to_fileinfo(path, status):
311
    """Given a PysvnStatus object, gets all the info "ls"
312
    needs to display about the filename. Returns a pair mapping filename to
313
    a dict containing a number of other fields."""
314
    path = os.path.normcase(path)
315
    fullpath = status.path
316
    # If this is "." (the directory itself)
317
    if path == os.path.normcase(fullpath):
318
        # If this directory is unversioned, then we aren't
319
        # looking at any interesting files, so throw
320
        # an exception and default to normal OS-based listing. 
321
        if status.text_status == pysvn.wc_status_kind.unversioned:
322
            raise pysvn.ClientError
323
        # We actually want to return "." because we want its
324
        # subversion status.
325
        filename = "."
326
    else:
327
        filename = os.path.basename(fullpath)
328
    text_status = status.text_status
329
    d = {'svnstatus': str(text_status)}
330
    try:
331
        d.update(_fullpath_stat_fileinfo(fullpath))
332
    except OSError:
333
        # Here if, eg, the file is missing.
334
        # Can't get any more information so just return d
335
        pass
336
    return filename, d
337
338
def PysvnList_to_fileinfo(path, list):
339
    """Given a List object from pysvn.Client.list, gets all the info "ls"
340
    needs to display about the filename. Returns a pair mapping filename to
341
    a dict containing a number of other fields."""
342
    path = os.path.normcase(path)
343
    pysvnlist = list[0]
344
    fullpath = pysvnlist.path
345
    # If this is "." (the directory itself)
346
    if path == os.path.normcase(fullpath):
347
        # If this directory is unversioned, then we aren't
348
        # looking at any interesting files, so throw
349
        # an exception and default to normal OS-based listing. 
350
        #if status.text_status == pysvn.wc_status_kind.unversioned:
351
        #    raise pysvn.ClientError
352
        # We actually want to return "." because we want its
353
        # subversion status.
354
        filename = "."
355
    else:
356
        filename = os.path.basename(fullpath)
357
    d = {'svnstatus': 'revision'} # A special status.
358
359
    wrapped = ivle.svn.PysvnListStatWrapper(pysvnlist)
360
    d.update(_stat_fileinfo(fullpath, wrapped))
361
362
    return filename, d