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

159 by mattgiuca
fileservice: Split growing module into three modules: top-level,
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:
162 by mattgiuca
fileservice: Modified the listing output. Now there is an extra object
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
159 by mattgiuca
fileservice: Split growing module into three modules: top-level,
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
#   * isdir: Boolean. True if the file is a directory. Always present unless
64
#   svnstatus is "missing".
65
#   * size: Number. Size of the file in bytes. Present for non-directory
66
#   files.
67
#   * type: String. Guessed mime type of the file. Present for non-directory
68
#   files.
179 by mattgiuca
fileservice: Added an extra field to the directory listing: mtime_nice - a
69
#   * mtime: Number. Number of seconds elapsed since the epoch.
70
#   The epoch is not defined (this is an arbitrary number used for sorting
71
#   dates).
72
#   * mtime_nice: String. Modification time of the file or directory. Always
159 by mattgiuca
fileservice: Split growing module into three modules: top-level,
73
#   present unless svnstatus is "Missing". Human-friendly.
74
#
75
# Members are not guaranteed to be present - client code should always check
76
# for each member that it is present, and handle gracefully if a member is not
77
# present.
78
#
79
# The listing object is guaranteed to have a "." key. Use this key to
80
# determine whether the directory is under version control or not. If this
81
# member does NOT have a "svnstatus" key, or "svnstatus" is "unversioned",
82
# then the directory is not under revision control (and no other files will
83
# have "svnstatus" either).
162 by mattgiuca
fileservice: Modified the listing output. Now there is an extra object
84
#
85
# The top-level object MAY contain a "clipboard" key, which specifies the
86
# files copied to the clipboard. This can be used by the client to show the
87
# user what files will be pasted. At the very least, the client should take
88
# the presence or absence of a "clipboard" key as whether to grey out the
89
# "paste" button.
90
#
91
# The "clipboard" object has three members:
92
#   * mode: String. Either "copy" or "cut".
93
#   * base: String. Path relative to the user's root. The common path between
94
#   the files.
95
#   * files: Array of Strings. Each element is a filename relative to base.
96
#   Base and files exactly correspond to the listing path and argument paths
97
#   which were supplied during the last copy or cut request.
159 by mattgiuca
fileservice: Split growing module into three modules: top-level,
98
99
import os
100
import stat
101
import time
102
import mimetypes
103
104
import cjson
105
import pysvn
106
107
from common import (util, studpath)
108
import conf.mimetypes
109
166 by mattgiuca
fileservice:
110
# Make a Subversion client object
111
svnclient = pysvn.Client()
112
200 by mattgiuca
fileservice.listing: Now returns a nicer date format for mtime_nice.
113
# For time calculations
114
seconds_per_day = 86400 # 60 * 60 * 24
115
if time.daylight:
116
    timezone_offset = time.altzone
117
else:
118
    timezone_offset = time.timezone
119
159 by mattgiuca
fileservice: Split growing module into three modules: top-level,
120
# Mime types
121
# application/json is the "best" content type but is not good for
122
# debugging because Firefox just tries to download it
123
mime_dirlisting = "text/html"
124
#mime_dirlisting = "application/json"
125
166 by mattgiuca
fileservice:
126
def handle_return(req):
159 by mattgiuca
fileservice: Split growing module into three modules: top-level,
127
    """Perform the "return" part of the response.
128
    This function returns the file or directory listing contained in
129
    req.path. Sets the HTTP response code in req, writes additional headers,
130
    and writes the HTTP response, if any."""
131
132
    (user, path) = studpath.url_to_local(req.path)
133
134
    # FIXME: What to do about req.path == ""?
135
    # Currently goes to 403 Forbidden.
136
    if path is None:
137
        req.status = req.HTTP_FORBIDDEN
138
        req.headers_out['X-IVLE-Return-Error'] = 'Forbidden'
139
        req.write("Forbidden")
140
    elif not os.access(path, os.R_OK):
141
        req.status = req.HTTP_NOT_FOUND
142
        req.headers_out['X-IVLE-Return-Error'] = 'File not found'
143
        req.write("File not found")
144
    elif os.path.isdir(path):
145
        # It's a directory. Return the directory listing.
146
        req.content_type = mime_dirlisting
147
        req.headers_out['X-IVLE-Return'] = 'Dir'
162 by mattgiuca
fileservice: Modified the listing output. Now there is an extra object
148
        req.write(cjson.encode(get_dirlisting(req, svnclient, path)))
159 by mattgiuca
fileservice: Split growing module into three modules: top-level,
149
    else:
150
        # It's a file. Return the file contents.
151
        # First get the mime type of this file
152
        # (Note that importing common.util has already initialised mime types)
153
        (type, _) = mimetypes.guess_type(path)
154
        if type is None:
155
            type = conf.mimetypes.default_mimetype
156
        req.content_type = type
157
        req.headers_out['X-IVLE-Return'] = 'File'
158
159
        req.sendfile(path)
160
162 by mattgiuca
fileservice: Modified the listing output. Now there is an extra object
161
def get_dirlisting(req, svnclient, path):
160 by mattgiuca
fileservice: Abstracted get_dirlisting into a separate function.
162
    """Given a local absolute path, creates a directory listing object
162 by mattgiuca
fileservice: Modified the listing output. Now there is an extra object
163
    ready to be JSONized and sent to the client.
164
165
    req: Request object. Will not be mutated; just reads the session.
166
    svnclient: Svn client object.
167
    path: String. Absolute path on the local file system. Not checked,
168
        must already be guaranteed safe.
169
    """
160 by mattgiuca
fileservice: Abstracted get_dirlisting into a separate function.
170
    # Start by trying to do an SVN status, so we can report file version
171
    # status
172
    listing = {}
173
    try:
174
        status_list = svnclient.status(path, recurse=False, get_all=True,
175
                        update=False)
176
        for status in status_list:
177
            filename, attrs = PysvnStatus_to_fileinfo(path, status)
178
            listing[filename] = attrs
179
    except pysvn.ClientError:
180
        # Presumably the directory is not under version control.
181
        # Fallback to just an OS file listing.
182
        for filename in os.listdir(path):
183
            listing[filename] = file_to_fileinfo(path, filename)
184
        # The subversion one includes "." while the OS one does not.
185
        # Add "." to the output, so the caller can see we are
186
        # unversioned.
179 by mattgiuca
fileservice: Added an extra field to the directory listing: mtime_nice - a
187
        mtime = os.path.getmtime(path)
160 by mattgiuca
fileservice: Abstracted get_dirlisting into a separate function.
188
        listing["."] = {"isdir" : True,
200 by mattgiuca
fileservice.listing: Now returns a nicer date format for mtime_nice.
189
            "mtime" : mtime, "mtime_nice" : make_date_nice(mtime),
190
            "mtime_short" : make_date_nice_short(mtime)}
162 by mattgiuca
fileservice: Modified the listing output. Now there is an extra object
191
192
    # Listing is a nested object inside the top-level JSON.
193
    listing = {"listing" : listing}
194
195
    # The other object is the clipboard, if present in the browser session.
196
    # This can go straight from the session to JSON.
197
    session = req.get_session()
198
    try:
199
        listing['clipboard'] = session['clipboard']
200
    except KeyError:
201
        pass
202
    
160 by mattgiuca
fileservice: Abstracted get_dirlisting into a separate function.
203
    return listing
204
159 by mattgiuca
fileservice: Split growing module into three modules: top-level,
205
def file_to_fileinfo(path, filename):
206
    """Given a filename (relative to a given path), gets all the info "ls"
207
    needs to display about the filename. Returns a dict containing a number
208
    of fields related to the file (excluding the filename itself)."""
209
    fullpath = os.path.join(path, filename)
210
    d = {}
211
    file_stat = os.stat(fullpath)
212
    if stat.S_ISDIR(file_stat.st_mode):
213
        d["isdir"] = True
214
    else:
215
        d["isdir"] = False
216
        d["size"] = file_stat.st_size
217
        (type, _) = mimetypes.guess_type(filename)
218
        if type is None:
219
            type = conf.mimetypes.default_mimetype
220
        d["type"] = type
179 by mattgiuca
fileservice: Added an extra field to the directory listing: mtime_nice - a
221
    d["mtime"] = file_stat.st_mtime
200 by mattgiuca
fileservice.listing: Now returns a nicer date format for mtime_nice.
222
    d["mtime_nice"] = make_date_nice(file_stat.st_mtime)
223
    d["mtime_short"] = make_date_nice_short(file_stat.st_mtime)
159 by mattgiuca
fileservice: Split growing module into three modules: top-level,
224
    return d
225
226
def PysvnStatus_to_fileinfo(path, status):
227
    """Given a PysvnStatus object, gets all the info "ls"
228
    needs to display about the filename. Returns a pair mapping filename to
229
    a dict containing a number of other fields."""
230
    path = os.path.normcase(path)
231
    fullpath = status.path
232
    # If this is "." (the directory itself)
233
    if path == os.path.normcase(fullpath):
234
        # If this directory is unversioned, then we aren't
235
        # looking at any interesting files, so throw
236
        # an exception and default to normal OS-based listing. 
237
        if status.text_status == pysvn.wc_status_kind.unversioned:
238
            raise pysvn.ClientError
239
        # We actually want to return "." because we want its
240
        # subversion status.
241
        filename = "."
242
    else:
243
        filename = os.path.basename(fullpath)
244
    d = {}
245
    text_status = status.text_status
246
    d["svnstatus"] = str(text_status)
247
    try:
248
        file_stat = os.stat(fullpath)
249
        if stat.S_ISDIR(file_stat.st_mode):
250
            d["isdir"] = True
251
        else:
252
            d["isdir"] = False
253
            d["size"] = file_stat.st_size
254
            (type, _) = mimetypes.guess_type(fullpath)
255
            if type is None:
256
                type = conf.mimetypes.default_mimetype
257
            d["type"] = type
179 by mattgiuca
fileservice: Added an extra field to the directory listing: mtime_nice - a
258
        d["mtime"] = file_stat.st_mtime
200 by mattgiuca
fileservice.listing: Now returns a nicer date format for mtime_nice.
259
        d["mtime_nice"] = make_date_nice(file_stat.st_mtime)
260
        d["mtime_short"] = make_date_nice_short(file_stat.st_mtime)
159 by mattgiuca
fileservice: Split growing module into three modules: top-level,
261
    except OSError:
262
        # Here if, eg, the file is missing.
263
        # Can't get any more information so just return d
264
        pass
265
    return filename, d
200 by mattgiuca
fileservice.listing: Now returns a nicer date format for mtime_nice.
266
267
def make_date_nice(seconds_since_epoch):
268
    """Given a number of seconds elapsed since the epoch,
269
    generates a string representing the date/time in human-readable form.
270
    "ddd mmm dd, yyyy h:m a"
271
    """
272
    #return time.ctime(seconds_since_epoch)
273
    return time.strftime("%a %b %d, %Y %I:%M %p",
274
        time.localtime(seconds_since_epoch))
275
276
def make_date_nice_short(seconds_since_epoch):
277
    """Given a number of seconds elapsed since the epoch,
278
    generates a string representing the date in human-readable form.
279
    Does not include the time.
280
    This function generates a very compact representation."""
281
    # Use a "naturalisation" algorithm.
282
    days_ago = (int(time.time() - timezone_offset) / seconds_per_day
283
        - int(seconds_since_epoch - timezone_offset) / seconds_per_day)
284
    if days_ago <= 5:
285
        # Dates today or yesterday, return "today" or "yesterday".
286
        if days_ago == 0:
287
            return "Today"
288
        elif days_ago == 1:
289
            return "Yesterday"
290
        else:
291
            return str(days_ago) + " days ago"
292
        # Dates in the last 5 days, return "n days ago".
293
    # Other dates, return a short date format.
294
    # If within the same year, omit the year (mmm dd)
295
    if time.localtime(seconds_since_epoch).tm_year==time.localtime().tm_year:
296
        return time.strftime("%b %d", time.localtime(seconds_since_epoch))
297
    # Else, include the year (mmm dd, yyyy)
298
    else:
299
        return time.strftime("%b %d, %Y", time.localtime(seconds_since_epoch))