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