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

« back to all changes in this revision

Viewing changes to lib/fileservice_lib/listing.py

  • Committer: mattgiuca
  • Date: 2008-02-05 01:51:26 UTC
  • Revision ID: svn-v3-trunk0:2b9c9e99-6f39-0410-b283-7f802c844ae2:trunk:411
Renamed lib/fileservice to lib/fileservice_lib (naming conflict).
Added new app: fileservice. This app replaces the old one that was deleted -
it simply calls fileservice_lib.handle, and that's it (so it functions exactly
the same).

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 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
 
105
 
 
106
import cjson
 
107
import pysvn
 
108
 
 
109
from common import (util, studpath)
 
110
import conf.mimetypes
 
111
 
 
112
# Make a Subversion client object
 
113
svnclient = pysvn.Client()
 
114
 
 
115
# For time calculations
 
116
seconds_per_day = 86400 # 60 * 60 * 24
 
117
if time.daylight:
 
118
    timezone_offset = time.altzone
 
119
else:
 
120
    timezone_offset = time.timezone
 
121
 
 
122
# Mime types
 
123
# application/json is the "best" content type but is not good for
 
124
# debugging because Firefox just tries to download it
 
125
mime_dirlisting = "text/html"
 
126
#mime_dirlisting = "application/json"
 
127
 
 
128
def handle_return(req):
 
129
    """Perform the "return" part of the response.
 
130
    This function returns the file or directory listing contained in
 
131
    req.path. Sets the HTTP response code in req, writes additional headers,
 
132
    and writes the HTTP response, if any."""
 
133
 
 
134
    (user, path) = studpath.url_to_local(req.path)
 
135
 
 
136
    # FIXME: What to do about req.path == ""?
 
137
    # Currently goes to 403 Forbidden.
 
138
    if path is None:
 
139
        req.status = req.HTTP_FORBIDDEN
 
140
        req.headers_out['X-IVLE-Return-Error'] = 'Forbidden'
 
141
        req.write("Forbidden")
 
142
    elif not os.access(path, os.R_OK):
 
143
        req.status = req.HTTP_NOT_FOUND
 
144
        req.headers_out['X-IVLE-Return-Error'] = 'File not found'
 
145
        req.write("File not found")
 
146
    elif os.path.isdir(path):
 
147
        # It's a directory. Return the directory listing.
 
148
        req.content_type = mime_dirlisting
 
149
        req.headers_out['X-IVLE-Return'] = 'Dir'
 
150
        req.write(cjson.encode(get_dirlisting(req, svnclient, path)))
 
151
    else:
 
152
        # It's a file. Return the file contents.
 
153
        # First get the mime type of this file
 
154
        # (Note that importing common.util has already initialised mime types)
 
155
        (type, _) = mimetypes.guess_type(path)
 
156
        if type is None:
 
157
            type = conf.mimetypes.default_mimetype
 
158
        req.content_type = type
 
159
        req.headers_out['X-IVLE-Return'] = 'File'
 
160
 
 
161
        req.sendfile(path)
 
162
 
 
163
def get_dirlisting(req, svnclient, path):
 
164
    """Given a local absolute path, creates a directory listing object
 
165
    ready to be JSONized and sent to the client.
 
166
 
 
167
    req: Request object. Will not be mutated; just reads the session.
 
168
    svnclient: Svn client object.
 
169
    path: String. Absolute path on the local file system. Not checked,
 
170
        must already be guaranteed safe.
 
171
    """
 
172
    # Start by trying to do an SVN status, so we can report file version
 
173
    # status
 
174
    listing = {}
 
175
    try:
 
176
        status_list = svnclient.status(path, recurse=False, get_all=True,
 
177
                        update=False)
 
178
        for status in status_list:
 
179
            filename, attrs = PysvnStatus_to_fileinfo(path, status)
 
180
            listing[filename] = attrs
 
181
    except pysvn.ClientError:
 
182
        # Presumably the directory is not under version control.
 
183
        # Fallback to just an OS file listing.
 
184
        for filename in os.listdir(path):
 
185
            listing[filename] = file_to_fileinfo(path, filename)
 
186
        # The subversion one includes "." while the OS one does not.
 
187
        # Add "." to the output, so the caller can see we are
 
188
        # unversioned.
 
189
        mtime = os.path.getmtime(path)
 
190
        listing["."] = {"isdir" : True,
 
191
            "mtime" : mtime, "mtime_nice" : make_date_nice(mtime),
 
192
            "mtime_short" : make_date_nice_short(mtime),
 
193
            "type_nice" : util.nice_filetype("/")}
 
194
 
 
195
    # Listing is a nested object inside the top-level JSON.
 
196
    listing = {"listing" : listing}
 
197
 
 
198
    # The other object is the clipboard, if present in the browser session.
 
199
    # This can go straight from the session to JSON.
 
200
    session = req.get_session()
 
201
    try:
 
202
        listing['clipboard'] = session['clipboard']
 
203
    except KeyError:
 
204
        pass
 
205
    
 
206
    return listing
 
207
 
 
208
def file_to_fileinfo(path, filename):
 
209
    """Given a filename (relative to a given path), gets all the info "ls"
 
210
    needs to display about the filename. Returns a dict containing a number
 
211
    of fields related to the file (excluding the filename itself)."""
 
212
    fullpath = os.path.join(path, filename)
 
213
    d = {}
 
214
    file_stat = os.stat(fullpath)
 
215
    if stat.S_ISDIR(file_stat.st_mode):
 
216
        d["isdir"] = True
 
217
        d["type_nice"] = util.nice_filetype("/")
 
218
    else:
 
219
        d["isdir"] = False
 
220
        d["size"] = file_stat.st_size
 
221
        (type, _) = mimetypes.guess_type(filename)
 
222
        if type is None:
 
223
            type = conf.mimetypes.default_mimetype
 
224
        d["type"] = type
 
225
        d["type_nice"] = util.nice_filetype(filename)
 
226
    d["mtime"] = file_stat.st_mtime
 
227
    d["mtime_nice"] = make_date_nice(file_stat.st_mtime)
 
228
    d["mtime_short"] = make_date_nice_short(file_stat.st_mtime)
 
229
    return d
 
230
 
 
231
def PysvnStatus_to_fileinfo(path, status):
 
232
    """Given a PysvnStatus object, gets all the info "ls"
 
233
    needs to display about the filename. Returns a pair mapping filename to
 
234
    a dict containing a number of other fields."""
 
235
    path = os.path.normcase(path)
 
236
    fullpath = status.path
 
237
    # If this is "." (the directory itself)
 
238
    if path == os.path.normcase(fullpath):
 
239
        # If this directory is unversioned, then we aren't
 
240
        # looking at any interesting files, so throw
 
241
        # an exception and default to normal OS-based listing. 
 
242
        if status.text_status == pysvn.wc_status_kind.unversioned:
 
243
            raise pysvn.ClientError
 
244
        # We actually want to return "." because we want its
 
245
        # subversion status.
 
246
        filename = "."
 
247
    else:
 
248
        filename = os.path.basename(fullpath)
 
249
    d = {}
 
250
    text_status = status.text_status
 
251
    d["svnstatus"] = str(text_status)
 
252
    try:
 
253
        file_stat = os.stat(fullpath)
 
254
        if stat.S_ISDIR(file_stat.st_mode):
 
255
            d["isdir"] = True
 
256
            d["type_nice"] = util.nice_filetype("/")
 
257
            # Only directories can be published
 
258
            d["published"] = studpath.published(fullpath)
 
259
        else:
 
260
            d["isdir"] = False
 
261
            d["size"] = file_stat.st_size
 
262
            (type, _) = mimetypes.guess_type(fullpath)
 
263
            if type is None:
 
264
                type = conf.mimetypes.default_mimetype
 
265
            d["type"] = type
 
266
            d["type_nice"] = util.nice_filetype(filename)
 
267
        d["mtime"] = file_stat.st_mtime
 
268
        d["mtime_nice"] = make_date_nice(file_stat.st_mtime)
 
269
        d["mtime_short"] = make_date_nice_short(file_stat.st_mtime)
 
270
    except OSError:
 
271
        # Here if, eg, the file is missing.
 
272
        # Can't get any more information so just return d
 
273
        pass
 
274
    return filename, d
 
275
 
 
276
def make_date_nice(seconds_since_epoch):
 
277
    """Given a number of seconds elapsed since the epoch,
 
278
    generates a string representing the date/time in human-readable form.
 
279
    "ddd mmm dd, yyyy h:m a"
 
280
    """
 
281
    #return time.ctime(seconds_since_epoch)
 
282
    return time.strftime("%a %b %d %Y, %I:%M %p",
 
283
        time.localtime(seconds_since_epoch))
 
284
 
 
285
def make_date_nice_short(seconds_since_epoch):
 
286
    """Given a number of seconds elapsed since the epoch,
 
287
    generates a string representing the date in human-readable form.
 
288
    Does not include the time.
 
289
    This function generates a very compact representation."""
 
290
    # Use a "naturalisation" algorithm.
 
291
    days_ago = (int(time.time() - timezone_offset) / seconds_per_day
 
292
        - int(seconds_since_epoch - timezone_offset) / seconds_per_day)
 
293
    if days_ago <= 5:
 
294
        # Dates today or yesterday, return "today" or "yesterday".
 
295
        if days_ago == 0:
 
296
            return "Today"
 
297
        elif days_ago == 1:
 
298
            return "Yesterday"
 
299
        else:
 
300
            return str(days_ago) + " days ago"
 
301
        # Dates in the last 5 days, return "n days ago".
 
302
    # Other dates, return a short date format.
 
303
    # If within the same year, omit the year (mmm dd)
 
304
    if time.localtime(seconds_since_epoch).tm_year==time.localtime().tm_year:
 
305
        return time.strftime("%b %d", time.localtime(seconds_since_epoch))
 
306
    # Else, include the year (mmm dd, yyyy)
 
307
    else:
 
308
        return time.strftime("%b %d, %Y", time.localtime(seconds_since_epoch))