~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
1207 by William Grant
Move ivle.conf.mimetypes to ivle.mimetypes, and rename things in it.
113
import ivle.mimetypes
114
from ivle import studpath
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
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
1632 by Matt Giuca
listing.py: Fixed regression crash when viewing unversioned directories or files, due to silly code raising pysvn.ClientError (not an instance of it). Now raise a special exception UnversionedDir, which is checked properly.
129
# Indicates that a directory is unversioned
130
class UnversionedDir(Exception):
131
    pass
132
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
133
def handle_return(req, return_contents):
134
    """
135
    Perform the "return" part of the response.
136
    This function returns the file or directory listing contained in
137
    req.path. Sets the HTTP response code in req, writes additional headers,
138
    and writes the HTTP response, if any.
139
140
    If return_contents is True, and the path is a non-directory, returns the
141
    contents of the file verbatim. If False, returns a directory listing
142
    with a single file, ".", and info about the file.
143
144
    If the path is a directory, return_contents is ignored.
145
    """
146
1272 by William Grant
.. and one more
147
    path = studpath.to_home_path(req.path)
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
148
149
    # FIXME: What to do about req.path == ""?
150
    # Currently goes to 403 Forbidden.
151
    urlpath = urlparse.urlparse(path)
152
    path = urlpath[2]
1089 by chadnickbok
Fixes Issue #14
153
    json = None
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
154
    if path is None:
155
        req.status = req.HTTP_FORBIDDEN
156
        req.headers_out['X-IVLE-Return-Error'] = 'Forbidden'
157
        req.write("Forbidden")
1193 by Matt Giuca
ivle.svn: Added revision_is_dir (like os.path.isdir for revision history).
158
        return
159
160
    # If this is a repository-revision request, it needs to be treated
161
    # differently than if it were a regular file request.
162
    # Note: If there IS a revision requested but the file doesn't exist in
163
    # that revision, this will terminate.
164
    revision = _get_revision_or_die(req, svnclient, path)
165
1194 by Matt Giuca
fileservice: Fixed a bug when browsing previous revisions, that the
166
    if revision is None:
167
        if not os.access(path, os.R_OK):
168
            req.status = req.HTTP_NOT_FOUND
169
            req.headers_out['X-IVLE-Return-Error'] = 'File not found'
170
            req.write("File not found")
171
            return
172
        is_dir = os.path.isdir(path)
173
    else:
174
        is_dir = ivle.svn.revision_is_dir(svnclient, path, revision)
175
176
    if is_dir:
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
177
        # It's a directory. Return the directory listing.
178
        req.content_type = mime_dirlisting
179
        req.headers_out['X-IVLE-Return'] = 'Dir'
1089 by chadnickbok
Fixes Issue #14
180
        # TODO: Fix this dirty, dirty hack
1193 by Matt Giuca
ivle.svn: Added revision_is_dir (like os.path.isdir for revision history).
181
        newjson = get_dirlisting(req, svnclient, path, revision)
1089 by chadnickbok
Fixes Issue #14
182
        if ("X-IVLE-Action-Error" in req.headers_out):
183
            newjson["Error"] = req.headers_out["X-IVLE-Action-Error"]
184
        req.write(cjson.encode(newjson))
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
185
    elif return_contents:
186
        # It's a file. Return the file contents.
187
        # First get the mime type of this file
188
        (type, _) = mimetypes.guess_type(path)
189
        if type is None:
1207 by William Grant
Move ivle.conf.mimetypes to ivle.mimetypes, and rename things in it.
190
            type = ivle.mimetypes.DEFAULT_MIMETYPE
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
191
        req.content_type = type
192
        req.headers_out['X-IVLE-Return'] = 'File'
193
1193 by Matt Giuca
ivle.svn: Added revision_is_dir (like os.path.isdir for revision history).
194
        send_file(req, svnclient, path, revision)
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
195
    else:
196
        # It's a file. Return a "fake directory listing" with just this file.
197
        req.content_type = mime_dirlisting
198
        req.headers_out['X-IVLE-Return'] = 'File'
1193 by Matt Giuca
ivle.svn: Added revision_is_dir (like os.path.isdir for revision history).
199
        req.write(cjson.encode(get_dirlisting(req, svnclient, path,
200
                                              revision)))
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
201
202
def _get_revision_or_die(req, svnclient, path):
1192 by Matt Giuca
ivle.fileservice_lib.listing: Proper docstring for the rather confusing
203
    """Looks for a revision specification in req's URL.
204
    Errors and terminates the request if the specification was bad, or it
205
    doesn't exist for the given path.
206
    @param req: Request object.
207
    @param svnclient: pysvn Client object.
208
    @param path: Path to the file whose revision is to be retrieved.
209
    @returns: pysvn Revision object, for the file+revision specified, or None
210
        if there was no revision specified.
211
    """
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
212
    # Work out the revisions from query
213
    r_str = req.get_fieldstorage().getfirst("r")
214
    revision = ivle.svn.revision_from_string(r_str)
215
216
    # Was some revision specified AND (it didn't resolve OR it was nonexistent)
217
    if r_str and not (revision and
218
                      ivle.svn.revision_exists(svnclient, path, revision)):
219
        req.status = req.HTTP_NOT_FOUND
1195 by Matt Giuca
Fileservice: Improved the error message "Revision not found" to
220
        message = ('Revision not found or file not found in revision %d' %
221
                   revision.number)
222
        req.headers_out['X-IVLE-Return-Error'] = message
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
223
        req.ensure_headers_written()
1195 by Matt Giuca
Fileservice: Improved the error message "Revision not found" to
224
        req.write(message)
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
225
        req.flush()
226
        sys.exit()
227
    return revision
228
1193 by Matt Giuca
ivle.svn: Added revision_is_dir (like os.path.isdir for revision history).
229
def send_file(req, svnclient, path, revision):
230
    """Given a local absolute path to a file, sends the contents of the file
231
    to the client.
232
233
    @param req: Request object. Will not be mutated; just reads the session.
234
    @param svnclient: Svn client object.
235
    @param path: String. Absolute path on the local file system. Not checked,
236
        must already be guaranteed safe. May be a file or a directory.
237
    @param revision: pysvn Revision object for the given path, or None.
238
    """
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
239
    if revision:
240
        req.write(svnclient.cat(path, revision=revision))
241
    else:
242
        req.sendfile(path)
243
1193 by Matt Giuca
ivle.svn: Added revision_is_dir (like os.path.isdir for revision history).
244
def get_dirlisting(req, svnclient, path, revision):
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
245
    """Given a local absolute path, creates a directory listing object
246
    ready to be JSONized and sent to the client.
247
1193 by Matt Giuca
ivle.svn: Added revision_is_dir (like os.path.isdir for revision history).
248
    @param req: Request object. Will not be mutated; just reads the session.
249
    @param svnclient: Svn client object.
250
    @param path: String. Absolute path on the local file system. Not checked,
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
251
        must already be guaranteed safe. May be a file or a directory.
1193 by Matt Giuca
ivle.svn: Added revision_is_dir (like os.path.isdir for revision history).
252
    @param revision: pysvn Revision object for the given path, or None.
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
253
    """
254
255
    # Start by trying to do an SVN status, so we can report file version
256
    # status
257
    listing = {}
1629 by Matt Giuca
ivle.fileservice_lib.listing: No longer detect not-a-working-copy errors by string matching; instead check error code.
258
    svnclient.exception_style = 1       # Get rich exceptions
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
259
    try:
260
        if revision:
261
            ls_list = svnclient.list(path, revision=revision, recurse=False)
262
            for ls in ls_list:
263
                filename, attrs = PysvnList_to_fileinfo(path, ls)
1634 by Matt Giuca
fileservice_lib.listing: Fix some encode/decode errors. Now possible to get a file listing of a SVN directory with Unicode characters in filenames, although a lot of other things still don't work.
264
                if isinstance(filename, str):   # Expect a unicode from pysvn
265
                    filename = filename.decode('utf-8')
266
                listing[filename] = attrs
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
267
        else:
268
            status_list = svnclient.status(path, recurse=False, get_all=True,
269
                        update=False)
270
            for status in status_list:
271
                filename, attrs = PysvnStatus_to_fileinfo(path, status)
1634 by Matt Giuca
fileservice_lib.listing: Fix some encode/decode errors. Now possible to get a file listing of a SVN directory with Unicode characters in filenames, although a lot of other things still don't work.
272
                if isinstance(filename, str):   # Expect a unicode from pysvn
273
                    filename = filename.decode('utf-8')
274
                listing[filename] = attrs
1632 by Matt Giuca
listing.py: Fixed regression crash when viewing unversioned directories or files, due to silly code raising pysvn.ClientError (not an instance of it). Now raise a special exception UnversionedDir, which is checked properly.
275
    except (pysvn.ClientError, UnversionedDir), e:
1627 by Matt Giuca
ivle.fileservice_lib.listing: Previously assumed any SVN client error meant the directory was not versioned, and silently dropped SVN metadata. Now checks the error message, and for any unexpected errors, raises the exception rather than assuming unversioned. Fixes Launchpad Bug #523592.
276
        # Could indicate a serious SVN error, or just that the directory is
277
        # not under version control (which is perfectly normal).
1629 by Matt Giuca
ivle.fileservice_lib.listing: No longer detect not-a-working-copy errors by string matching; instead check error code.
278
        # Error code 155007 is "<dir> is not a working copy"
1632 by Matt Giuca
listing.py: Fixed regression crash when viewing unversioned directories or files, due to silly code raising pysvn.ClientError (not an instance of it). Now raise a special exception UnversionedDir, which is checked properly.
279
        if isinstance(e, pysvn.ClientError) and e[1][0][1] != 155007:
1627 by Matt Giuca
ivle.fileservice_lib.listing: Previously assumed any SVN client error meant the directory was not versioned, and silently dropped SVN metadata. Now checks the error message, and for any unexpected errors, raises the exception rather than assuming unversioned. Fixes Launchpad Bug #523592.
280
            # This is a serious error -- let it propagate upwards
281
            raise
282
        # The directory is not under version control.
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
283
        # Fallback to just an OS file listing.
284
        try:
285
            for filename in os.listdir(path):
286
                listing[filename.decode('utf-8')] = file_to_fileinfo(path, filename)[1]
1086 by chadnickbok
This commit fixes issue #10 and part of issue #9
287
                try:
288
                    svnclient.status(os.path.join(path, filename), recurse = False)
289
                    listing[filename.decode('utf-8')]['svnstatus'] = 'normal'
290
                except:
291
                    pass
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
292
        except OSError:
293
            # Non-directories will error - that's OK, we just want the "."
294
            pass
295
        # The subversion one includes "." while the OS one does not.
296
        # Add "." to the output, so the caller can see we are
297
        # unversioned.
298
        listing["."] = file_to_fileinfo(path, "")[1]
299
300
    if ignore_dot_files:
301
        for fn in listing.keys():
302
            if fn != "." and fn.startswith("."):
303
                del listing[fn]
304
305
    # Listing is a nested object inside the top-level JSON.
306
    listing = {"listing" : listing}
307
308
    if revision:
309
        listing['revision'] = revision.number
310
311
    # The other object is the clipboard, if present in the browser session.
312
    # This can go straight from the session to JSON.
313
    session = req.get_session()
314
    if session and 'clipboard' in session:
315
        # In CGI mode, we can't get our hands on the
316
        # session (for the moment), so just leave it out.
317
        listing['clipboard'] = session['clipboard']
318
    
319
    return listing
320
321
def _fullpath_stat_fileinfo(fullpath):
1634 by Matt Giuca
fileservice_lib.listing: Fix some encode/decode errors. Now possible to get a file listing of a SVN directory with Unicode characters in filenames, although a lot of other things still don't work.
322
    if isinstance(fullpath, unicode):
323
        fullpath = fullpath.encode('utf-8')     # os.stat can't handle unicode
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
324
    file_stat = os.stat(fullpath)
325
    return _stat_fileinfo(fullpath, file_stat)
326
327
def _stat_fileinfo(fullpath, file_stat):
328
    d = {}
329
    if stat.S_ISDIR(file_stat.st_mode):
330
        d["isdir"] = True
1207 by William Grant
Move ivle.conf.mimetypes to ivle.mimetypes, and rename things in it.
331
        d["type_nice"] = ivle.mimetypes.nice_filetype("/")
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
332
        # Only directories can be published
333
        d["published"] = studpath.published(fullpath)
334
    else:
335
        d["isdir"] = False
336
        d["size"] = file_stat.st_size
337
        (type, _) = mimetypes.guess_type(fullpath)
338
        if type is None:
1207 by William Grant
Move ivle.conf.mimetypes to ivle.mimetypes, and rename things in it.
339
            type = ivle.mimetypes.DEFAULT_MIMETYPE
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
340
        d["type"] = type
1207 by William Grant
Move ivle.conf.mimetypes to ivle.mimetypes, and rename things in it.
341
        d["type_nice"] = ivle.mimetypes.nice_filetype(fullpath)
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
342
    d["mtime"] = file_stat.st_mtime
343
    d["mtime_nice"] = ivle.date.make_date_nice(file_stat.st_mtime)
344
    d["mtime_short"] = ivle.date.make_date_nice_short(file_stat.st_mtime)
345
    return d
346
347
def file_to_fileinfo(path, filename):
348
    """Given a filename (relative to a given path), gets all the info "ls"
349
    needs to display about the filename. Returns pair mapping filename to
350
    a dict containing a number of other fields."""
351
    fullpath = path if filename in ('', '.') else os.path.join(path, filename)
352
    return filename, _fullpath_stat_fileinfo(fullpath)
353
354
def PysvnStatus_to_fileinfo(path, status):
355
    """Given a PysvnStatus object, gets all the info "ls"
356
    needs to display about the filename. Returns a pair mapping filename to
357
    a dict containing a number of other fields."""
358
    path = os.path.normcase(path)
359
    fullpath = status.path
360
    # If this is "." (the directory itself)
361
    if path == os.path.normcase(fullpath):
362
        # If this directory is unversioned, then we aren't
363
        # looking at any interesting files, so throw
364
        # an exception and default to normal OS-based listing. 
365
        if status.text_status == pysvn.wc_status_kind.unversioned:
1632 by Matt Giuca
listing.py: Fixed regression crash when viewing unversioned directories or files, due to silly code raising pysvn.ClientError (not an instance of it). Now raise a special exception UnversionedDir, which is checked properly.
366
            raise UnversionedDir()
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
367
        # We actually want to return "." because we want its
368
        # subversion status.
369
        filename = "."
370
    else:
371
        filename = os.path.basename(fullpath)
372
    text_status = status.text_status
373
    d = {'svnstatus': str(text_status)}
1165.1.31 by William Grant
Expose SVN revision and URL information through fileservice.
374
375
    if status.entry is not None:
376
        d.update({
377
           'svnurl': status.entry.url,
378
           'svnrevision': status.entry.revision.number
379
             if status.entry.revision.kind == pysvn.opt_revision_kind.number
380
             else None,
381
        })
382
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
383
    try:
384
        d.update(_fullpath_stat_fileinfo(fullpath))
385
    except OSError:
386
        # Here if, eg, the file is missing.
387
        # Can't get any more information so just return d
388
        pass
389
    return filename, d
390
391
def PysvnList_to_fileinfo(path, list):
392
    """Given a List object from pysvn.Client.list, gets all the info "ls"
393
    needs to display about the filename. Returns a pair mapping filename to
394
    a dict containing a number of other fields."""
395
    path = os.path.normcase(path)
396
    pysvnlist = list[0]
397
    fullpath = pysvnlist.path
398
    # If this is "." (the directory itself)
399
    if path == os.path.normcase(fullpath):
400
        # If this directory is unversioned, then we aren't
401
        # looking at any interesting files, so throw
402
        # an exception and default to normal OS-based listing. 
403
        #if status.text_status == pysvn.wc_status_kind.unversioned:
1632 by Matt Giuca
listing.py: Fixed regression crash when viewing unversioned directories or files, due to silly code raising pysvn.ClientError (not an instance of it). Now raise a special exception UnversionedDir, which is checked properly.
404
        #    raise UnversionedDir()
1079 by William Grant
Merge setup-refactor branch. This completely breaks existing installations;
405
        # We actually want to return "." because we want its
406
        # subversion status.
407
        filename = "."
408
    else:
409
        filename = os.path.basename(fullpath)
410
    d = {'svnstatus': 'revision'} # A special status.
411
412
    wrapped = ivle.svn.PysvnListStatWrapper(pysvnlist)
413
    d.update(_stat_fileinfo(fullpath, wrapped))
414
415
    return filename, d