Completed
Push — feature-datacookie ( 457ec2 )
by Felipe A.
42s
created

sort()   F

Complexity

Conditions 9

Size

Total Lines 32

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
c 0
b 0
f 0
dl 0
loc 32
rs 3
cc 9
1
#!/usr/bin/env python
2
# -*- coding: UTF-8 -*-
3
4
import logging
5
import os
6
import os.path
7
8
from flask import Response, request, render_template, redirect, \
9
                  url_for, send_from_directory, stream_with_context, \
10
                  make_response
11
from werkzeug.exceptions import NotFound
12
13
from .http import DataCookie
14
from .appconfig import Flask
15
from .manager import PluginManager
16
from .file import Node, secure_filename
17
from .exceptions import OutsideRemovableBase, OutsideDirectoryBase, \
18
                        InvalidFilenameError, InvalidPathError, \
19
                        InvalidCookieSizeError
20
from . import compat
21
from . import __meta__ as meta
22
23
__app__ = meta.app  # noqa
24
__version__ = meta.version  # noqa
25
__license__ = meta.license  # noqa
26
__author__ = meta.author  # noqa
27
__basedir__ = os.path.abspath(os.path.dirname(compat.fsdecode(__file__)))
28
29
logger = logging.getLogger(__name__)
30
31
app = Flask(
32
    __name__,
33
    static_url_path='/static',
34
    static_folder=os.path.join(__basedir__, "static"),
35
    template_folder=os.path.join(__basedir__, "templates")
36
    )
37
app.config.update(
38
    directory_base=compat.getcwd(),
39
    directory_start=None,
40
    directory_remove=None,
41
    directory_upload=None,
42
    directory_tar_buffsize=262144,
43
    directory_downloadable=True,
44
    use_binary_multiples=True,
45
    plugin_modules=[],
46
    plugin_namespaces=(
47
        'browsepy.plugin',
48
        'browsepy_',
49
        '',
50
        ),
51
    exclude_fnc=None,
52
    )
53
app.jinja_env.add_extension('browsepy.transform.htmlcompress.HTMLCompress')
54
55
if 'BROWSEPY_SETTINGS' in os.environ:
56
    app.config.from_envvar('BROWSEPY_SETTINGS')
57
58
plugin_manager = PluginManager(app)
59
sorting_cookie = DataCookie('browse-sorting')
60
61
62
def get_cookie_browse_sorting(path, default):
63
    '''
64
    Get sorting-cookie data for path of current request.
65
66
    :returns: sorting property
67
    :rtype: string
68
    '''
69
    if request:
70
        try:
71
            for cpath, cprop in sorting_cookie.load_headers(request.headers):
72
                if path == cpath:
73
                    return cprop
74
        except BaseException:
75
            pass
76
    return default
77
78
79
def browse_sortkey_reverse(prop):
80
    '''
81
    Get sorting function for directory listing based on given attribute
82
    name, with some caveats:
83
    * Directories will be first.
84
    * If *name* is given, link widget lowercase text will be used instead.
85
    * If *size* is given, bytesize will be used.
86
87
    :param prop: file attribute name
88
    :type prop: str
89
    :returns: tuple with sorting function and reverse bool
90
    :rtype: tuple of a dict and a bool
91
    '''
92
    if prop.startswith('-'):
93
        prop = prop[1:]
94
        reverse = True
95
    else:
96
        reverse = False
97
98
    if prop == 'text':
99
        return (
100
            lambda x: (
101
                x.is_directory == reverse,
102
                x.link.text.lower() if x.link and x.link.text else x.name
103
                ),
104
            reverse
105
            )
106
    if prop == 'size':
107
        return (
108
            lambda x: (
109
                x.is_directory == reverse,
110
                x.stats.st_size
111
                ),
112
            reverse
113
            )
114
    return (
115
        lambda x: (
116
            x.is_directory == reverse,
117
            getattr(x, prop, None)
118
            ),
119
        reverse
120
        )
121
122
123
def stream_template(template_name, **context):
124
    '''
125
    Some templates can be huge, this function returns an streaming response,
126
    sending the content in chunks and preventing from timeout.
127
128
    :param template_name: template
129
    :param **context: parameters for templates.
130
    :yields: HTML strings
131
    '''
132
    app.update_template_context(context)
133
    template = app.jinja_env.get_template(template_name)
134
    stream = template.generate(context)
135
    return Response(stream_with_context(stream))
136
137
138
@app.context_processor
139
def template_globals():
140
    return {
141
        'manager': app.extensions['plugin_manager'],
142
        'len': len,
143
        }
144
145
146
@app.route('/sort/<string:property>', defaults={"path": ""})
147
@app.route('/sort/<string:property>/<path:path>')
148
def sort(property, path):
149
    try:
150
        directory = Node.from_urlpath(path)
151
    except OutsideDirectoryBase:
152
        return NotFound()
153
154
    if not directory.is_directory or directory.is_excluded:
155
        return NotFound()
156
157
    data = [(path, property)]
158
    try:
159
        data[:-1] = [
160
            (cpath, cprop)
161
            for cpath, cprop in sorting_cookie.load_headers(request.headers)
162
            if cpath != path
163
            ]
164
    except BaseException:
165
        pass
166
167
    # handle cookie becoming too large
168
    while True:
169
        try:
170
            headers = sorting_cookie.dump_headers(data, request.headers)
171
            break
172
        except InvalidCookieSizeError:
173
            data.pop(0)
174
175
    response = redirect(url_for(".browse", path=directory.urlpath))
176
    response.headers.extend(headers)
177
    return response
178
179
180
@app.route("/browse", defaults={"path": ""})
181
@app.route('/browse/<path:path>')
182
def browse(path):
183
    sort_property = get_cookie_browse_sorting(path, 'text')
184
    sort_fnc, sort_reverse = browse_sortkey_reverse(sort_property)
185
186
    try:
187 View Code Duplication
        directory = Node.from_urlpath(path)
0 ignored issues
show
Duplication introduced by
This code seems to be duplicated in your project.
Loading history...
188
        if directory.is_directory and not directory.is_excluded:
189
            return stream_template(
190
                'browse.html',
191
                file=directory,
192
                sort_property=sort_property,
193
                sort_fnc=sort_fnc,
194
                sort_reverse=sort_reverse
195
                )
196
    except OutsideDirectoryBase:
197
        pass
198
    return NotFound()
199
200
201
@app.route('/open/<path:path>', endpoint="open")
202
def open_file(path):
203
    try:
204
        file = Node.from_urlpath(path)
205
        if file.is_file and not file.is_excluded:
206
            return send_from_directory(file.parent.path, file.name)
207
    except OutsideDirectoryBase:
208
        pass
209
    return NotFound()
210
211
212
@app.route("/download/file/<path:path>")
213
def download_file(path):
214
    try:
215
        file = Node.from_urlpath(path)
216
        if file.is_file and not file.is_excluded:
217
            return file.download()
218
    except OutsideDirectoryBase:
219
        pass
220
    return NotFound()
221
222
223
@app.route("/download/directory/<path:path>.tgz")
224
def download_directory(path):
225
    try:
226
        directory = Node.from_urlpath(path)
227
        if directory.is_directory and not directory.is_excluded:
228
            return directory.download()
229
    except OutsideDirectoryBase:
230
        pass
231
    return NotFound()
232
233
234
@app.route("/remove/<path:path>", methods=("GET", "POST"))
235
def remove(path):
236
    try:
237
        file = Node.from_urlpath(path)
238
    except OutsideDirectoryBase:
239
        return NotFound()
240
241
    if not file.can_remove or file.is_excluded:
242
        return NotFound()
243
244
    if request.method == 'GET':
245
        return render_template('remove.html', file=file)
246
247
    file.remove()
248
    return redirect(url_for(".browse", path=file.parent.urlpath))
249
250
251
@app.route("/upload", defaults={'path': ''}, methods=("POST",))
252
@app.route("/upload/<path:path>", methods=("POST",))
253
def upload(path):
254
    try:
255
        directory = Node.from_urlpath(path)
256
    except OutsideDirectoryBase:
257
        return NotFound()
258
259
    if (
260
      not directory.is_directory or
261
      not directory.can_upload or
262
      directory.is_excluded
263
      ):
264
        return NotFound()
265
266
    for v in request.files.listvalues():
267
        for f in v:
268
            filename = secure_filename(f.filename)
269
            if filename:
270
                filename = directory.choose_filename(filename)
271
                filepath = os.path.join(directory.path, filename)
272
                f.save(filepath)
273
            else:
274
                raise InvalidFilenameError(
275
                    path=directory.path,
276
                    filename=f.filename
277
                    )
278
    return redirect(url_for(".browse", path=directory.urlpath))
279
280
281
@app.route("/")
282
def index():
283
    path = app.config["directory_start"] or app.config["directory_base"]
284
    try:
285
        urlpath = Node(path).urlpath
286
    except OutsideDirectoryBase:
287
        return NotFound()
288
    return browse(urlpath)
289
290
291
@app.after_request
292
def page_not_found(response):
293
    if response.status_code == 404:
294
        return make_response((render_template('404.html'), 404))
295
    return response
296
297
298
@app.errorhandler(InvalidPathError)
299
def bad_request_error(e):
300
    file = None
301
    if hasattr(e, 'path'):
302
        if isinstance(e, InvalidFilenameError):
303
            file = Node(e.path)
304
        else:
305
            file = Node(e.path).parent
306
    return render_template('400.html', file=file, error=e), 400
307
308
309
@app.errorhandler(OutsideRemovableBase)
310
@app.errorhandler(404)
311
def page_not_found_error(e):
312
    return render_template('404.html'), 404
313
314
315
@app.errorhandler(500)
316
def internal_server_error(e):  # pragma: no cover
317
    logger.exception(e)
318
    return getattr(e, 'message', 'Internal server error'), 500
319