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

411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
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 has ivle:published property in
64
#   Subversion.
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 stat
103
import time
104
import mimetypes
578 by dcoles
fileservice: Added code to allow browing of past revisons in the File Browser
105
import urlparse
106
from cgi import parse_qs
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
107
108
import cjson
109
import pysvn
110
111
from common import (util, studpath)
112
import conf.mimetypes
113
114
# Make a Subversion client object
115
svnclient = pysvn.Client()
116
117
# For time calculations
118
seconds_per_day = 86400 # 60 * 60 * 24
119
if time.daylight:
120
    timezone_offset = time.altzone
121
else:
122
    timezone_offset = time.timezone
123
124
# Mime types
125
# application/json is the "best" content type but is not good for
126
# debugging because Firefox just tries to download it
127
mime_dirlisting = "text/html"
128
#mime_dirlisting = "application/json"
129
601 by mattgiuca
fileservice_lib: Added to fileservice a new query argument "return".
130
def handle_return(req, return_contents):
131
    """
132
    Perform the "return" part of the response.
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
133
    This function returns the file or directory listing contained in
134
    req.path. Sets the HTTP response code in req, writes additional headers,
601 by mattgiuca
fileservice_lib: Added to fileservice a new query argument "return".
135
    and writes the HTTP response, if any.
136
137
    If return_contents is True, and the path is a non-directory, returns the
138
    contents of the file verbatim. If False, returns a directory listing
139
    with a single file, ".", and info about the file.
140
141
    If the path is a directory, return_contents is ignored.
142
    """
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
143
436 by drtomc
fileservice: Make things less broken than they were. No claims of perfection yet! Unpacking form data with cgi.py is AWFUL.
144
    (user, jail, path) = studpath.url_to_jailpaths(req.path)
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
145
146
    # FIXME: What to do about req.path == ""?
147
    # Currently goes to 403 Forbidden.
578 by dcoles
fileservice: Added code to allow browing of past revisons in the File Browser
148
    urlpath = urlparse.urlparse(path)
149
    path = urlpath[2]
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
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'
162
        req.write(cjson.encode(get_dirlisting(req, svnclient, path)))
601 by mattgiuca
fileservice_lib: Added to fileservice a new query argument "return".
163
    elif return_contents:
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
164
        # It's a file. Return the file contents.
165
        # First get the mime type of this file
166
        # (Note that importing common.util has already initialised mime types)
167
        (type, _) = mimetypes.guess_type(path)
168
        if type is None:
169
            type = conf.mimetypes.default_mimetype
170
        req.content_type = type
171
        req.headers_out['X-IVLE-Return'] = 'File'
172
173
        req.sendfile(path)
601 by mattgiuca
fileservice_lib: Added to fileservice a new query argument "return".
174
    else:
175
        # It's a file. Return a "fake directory listing" with just this file.
176
        req.content_type = mime_dirlisting
177
        req.headers_out['X-IVLE-Return'] = 'File'
178
        req.write(cjson.encode(get_dirlisting(req, svnclient, path)))
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
179
180
def get_dirlisting(req, svnclient, path):
181
    """Given a local absolute path, creates a directory listing object
182
    ready to be JSONized and sent to the client.
183
184
    req: Request object. Will not be mutated; just reads the session.
185
    svnclient: Svn client object.
186
    path: String. Absolute path on the local file system. Not checked,
601 by mattgiuca
fileservice_lib: Added to fileservice a new query argument "return".
187
        must already be guaranteed safe. May be a file or a directory.
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
188
    """
578 by dcoles
fileservice: Added code to allow browing of past revisons in the File Browser
189
    # Are we in 'revision mode' - has someone sent the 'r' query
190
    # Work out the revisions from query
191
    revision = None
192
600 by mattgiuca
listing.py: Changed code looking for the "r" field (revisions) so it uses
193
    r_str = req.get_fieldstorage().getfirst("r")
578 by dcoles
fileservice: Added code to allow browing of past revisons in the File Browser
194
600 by mattgiuca
listing.py: Changed code looking for the "r" field (revisions) so it uses
195
    if r_str is None:
196
        pass
197
    elif r_str == "HEAD":
578 by dcoles
fileservice: Added code to allow browing of past revisons in the File Browser
198
        revision = pysvn.Revision( pysvn.opt_revision_kind.head )
199
    elif r_str == "WORKING":
200
        revision = pysvn.Revision( pysvn.opt_revision_kind.working )
201
    elif r_str == "BASE":
202
        revision = pysvn.Revision( pysvn.opt_revision_kind.base )
203
    else:
204
        # Is it a number?
205
        try:
206
            r = int(r_str)
207
            revision = pysvn.Revision( pysvn.opt_revision_kind.number, r)
208
        except:
209
            pass
210
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
211
    # Start by trying to do an SVN status, so we can report file version
212
    # status
213
    listing = {}
214
    try:
578 by dcoles
fileservice: Added code to allow browing of past revisons in the File Browser
215
        if revision:
216
            ls_list = svnclient.list(path, revision=revision, recurse=False)
217
            for ls in ls_list:
218
                filename, attrs = PysvnList_tofileinfo(path, ls)
219
                listing[filename] = attrs
220
        else:
221
            status_list = svnclient.status(path, recurse=False, get_all=True,
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
222
                        update=False)
578 by dcoles
fileservice: Added code to allow browing of past revisons in the File Browser
223
            for status in status_list:
224
                filename, attrs = PysvnStatus_to_fileinfo(path, status)
225
                listing[filename] = attrs
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
226
    except pysvn.ClientError:
227
        # Presumably the directory is not under version control.
228
        # Fallback to just an OS file listing.
601 by mattgiuca
fileservice_lib: Added to fileservice a new query argument "return".
229
        try:
230
            for filename in os.listdir(path):
231
                listing[filename] = file_to_fileinfo(path, filename)
232
        except OSError:
233
            # Non-directories will error - that's OK, we just want the "."
234
            pass
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
235
        # The subversion one includes "." while the OS one does not.
236
        # Add "." to the output, so the caller can see we are
237
        # unversioned.
238
        mtime = os.path.getmtime(path)
601 by mattgiuca
fileservice_lib: Added to fileservice a new query argument "return".
239
        listing["."] = file_to_fileinfo(path, "")
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
240
241
    # Listing is a nested object inside the top-level JSON.
242
    listing = {"listing" : listing}
243
244
    # The other object is the clipboard, if present in the browser session.
245
    # This can go straight from the session to JSON.
246
    session = req.get_session()
436 by drtomc
fileservice: Make things less broken than they were. No claims of perfection yet! Unpacking form data with cgi.py is AWFUL.
247
    if session and 'clipboard' in session:
248
        # In CGI mode, we can't get our hands on the
249
        # session (for the moment), so just leave it out.
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
250
        listing['clipboard'] = session['clipboard']
251
    
252
    return listing
253
254
def file_to_fileinfo(path, filename):
255
    """Given a filename (relative to a given path), gets all the info "ls"
256
    needs to display about the filename. Returns a dict containing a number
257
    of fields related to the file (excluding the filename itself)."""
601 by mattgiuca
fileservice_lib: Added to fileservice a new query argument "return".
258
    fullpath = path if len(filename) == 0 else os.path.join(path, filename)
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
259
    d = {}
260
    file_stat = os.stat(fullpath)
261
    if stat.S_ISDIR(file_stat.st_mode):
262
        d["isdir"] = True
263
        d["type_nice"] = util.nice_filetype("/")
264
    else:
265
        d["isdir"] = False
266
        d["size"] = file_stat.st_size
601 by mattgiuca
fileservice_lib: Added to fileservice a new query argument "return".
267
        (type, _) = mimetypes.guess_type(fullpath)
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
268
        if type is None:
269
            type = conf.mimetypes.default_mimetype
270
        d["type"] = type
601 by mattgiuca
fileservice_lib: Added to fileservice a new query argument "return".
271
        d["type_nice"] = util.nice_filetype(fullpath)
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
272
    d["mtime"] = file_stat.st_mtime
273
    d["mtime_nice"] = make_date_nice(file_stat.st_mtime)
274
    d["mtime_short"] = make_date_nice_short(file_stat.st_mtime)
275
    return d
276
277
def PysvnStatus_to_fileinfo(path, status):
278
    """Given a PysvnStatus object, gets all the info "ls"
279
    needs to display about the filename. Returns a pair mapping filename to
280
    a dict containing a number of other fields."""
281
    path = os.path.normcase(path)
282
    fullpath = status.path
283
    # If this is "." (the directory itself)
284
    if path == os.path.normcase(fullpath):
285
        # If this directory is unversioned, then we aren't
286
        # looking at any interesting files, so throw
287
        # an exception and default to normal OS-based listing. 
288
        if status.text_status == pysvn.wc_status_kind.unversioned:
289
            raise pysvn.ClientError
290
        # We actually want to return "." because we want its
291
        # subversion status.
292
        filename = "."
293
    else:
294
        filename = os.path.basename(fullpath)
295
    d = {}
296
    text_status = status.text_status
297
    d["svnstatus"] = str(text_status)
298
    try:
299
        file_stat = os.stat(fullpath)
300
        if stat.S_ISDIR(file_stat.st_mode):
301
            d["isdir"] = True
302
            d["type_nice"] = util.nice_filetype("/")
303
            # Only directories can be published
304
            d["published"] = studpath.published(fullpath)
305
        else:
306
            d["isdir"] = False
307
            d["size"] = file_stat.st_size
308
            (type, _) = mimetypes.guess_type(fullpath)
309
            if type is None:
310
                type = conf.mimetypes.default_mimetype
311
            d["type"] = type
312
            d["type_nice"] = util.nice_filetype(filename)
313
        d["mtime"] = file_stat.st_mtime
314
        d["mtime_nice"] = make_date_nice(file_stat.st_mtime)
315
        d["mtime_short"] = make_date_nice_short(file_stat.st_mtime)
316
    except OSError:
317
        # Here if, eg, the file is missing.
318
        # Can't get any more information so just return d
319
        pass
320
    return filename, d
321
578 by dcoles
fileservice: Added code to allow browing of past revisons in the File Browser
322
def PysvnList_tofileinfo(path, list):
323
    """Given a List object from pysvn.Client.list, gets all the info "ls"
324
    needs to display about the filename. Returns a pair mapping filename to
325
    a dict containing a number of other fields."""
326
    path = os.path.normcase(path)
327
    pysvnlist = list[0]
328
    fullpath = pysvnlist.path
329
    # If this is "." (the directory itself)
330
    if path == os.path.normcase(fullpath):
331
        # If this directory is unversioned, then we aren't
332
        # looking at any interesting files, so throw
333
        # an exception and default to normal OS-based listing. 
334
        #if status.text_status == pysvn.wc_status_kind.unversioned:
335
        #    raise pysvn.ClientError
336
        # We actually want to return "." because we want its
337
        # subversion status.
338
        filename = "."
339
    else:
340
        filename = os.path.basename(fullpath)
341
    d = {}
342
    d["svnstatus"] = "revision" # A special status
343
344
    if pysvnlist.kind == pysvn.node_kind.dir:
345
        d["isdir"] = True
346
        d["type_nice"] = util.nice_filetype("/")
347
        # Only directories can be published
348
        #d["published"] = studpath.published(fullpath)
349
    else:
350
        d["isdir"] = False
351
        d["size"] = pysvnlist.size
352
        (type, _) = mimetypes.guess_type(fullpath)
353
        if type is None:
354
            type = conf.mimetypes.default_mimetype
355
        d["type"] = type
356
        d["type_nice"] = util.nice_filetype(filename)
357
        d["mtime"] = pysvnlist.time
358
        d["mtime_nice"] = make_date_nice(pysvnlist.time)
359
        d["mtime_short"] = make_date_nice_short(pysvnlist.time)
360
361
    return filename, d
362
411 by mattgiuca
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
363
def make_date_nice(seconds_since_epoch):
364
    """Given a number of seconds elapsed since the epoch,
365
    generates a string representing the date/time in human-readable form.
366
    "ddd mmm dd, yyyy h:m a"
367
    """
368
    #return time.ctime(seconds_since_epoch)
369
    return time.strftime("%a %b %d %Y, %I:%M %p",
370
        time.localtime(seconds_since_epoch))
371
372
def make_date_nice_short(seconds_since_epoch):
373
    """Given a number of seconds elapsed since the epoch,
374
    generates a string representing the date in human-readable form.
375
    Does not include the time.
376
    This function generates a very compact representation."""
377
    # Use a "naturalisation" algorithm.
378
    days_ago = (int(time.time() - timezone_offset) / seconds_per_day
379
        - int(seconds_since_epoch - timezone_offset) / seconds_per_day)
380
    if days_ago <= 5:
381
        # Dates today or yesterday, return "today" or "yesterday".
382
        if days_ago == 0:
383
            return "Today"
384
        elif days_ago == 1:
385
            return "Yesterday"
386
        else:
387
            return str(days_ago) + " days ago"
388
        # Dates in the last 5 days, return "n days ago".
389
    # Other dates, return a short date format.
390
    # If within the same year, omit the year (mmm dd)
391
    if time.localtime(seconds_since_epoch).tm_year==time.localtime().tm_year:
392
        return time.strftime("%b %d", time.localtime(seconds_since_epoch))
393
    # Else, include the year (mmm dd, yyyy)
394
    else:
395
        return time.strftime("%b %d, %Y", time.localtime(seconds_since_epoch))