├── .python-version
├── browsepy
├── tests
│ ├── __init__.py
│ ├── deprecated
│ │ ├── __init__.py
│ │ └── plugin
│ │ │ ├── __init__.py
│ │ │ └── player.py
│ ├── utils.py
│ ├── test_app.py
│ ├── runner.py
│ ├── test_extensions.py
│ ├── test_transform.py
│ ├── test_plugins.py
│ ├── test_main.py
│ └── test_compat.py
├── plugin
│ ├── __init__.py
│ └── player
│ │ ├── static
│ │ ├── js
│ │ │ ├── jquery.jplayer.swf
│ │ │ ├── base.js
│ │ │ └── jplayer.playlist.min.js
│ │ ├── css
│ │ │ ├── browse.css
│ │ │ ├── base.css
│ │ │ └── jplayer.blue.monday.min.css
│ │ └── image
│ │ │ ├── jplayer.blue.monday.jpg
│ │ │ ├── jplayer.blue.monday.seeking.gif
│ │ │ └── jplayer.blue.monday.video.play.png
│ │ ├── templates
│ │ └── audio.player.html
│ │ ├── __init__.py
│ │ └── playable.py
├── static
│ ├── giorgio.jpg
│ ├── fonts
│ │ ├── icomoon.eot
│ │ ├── icomoon.ttf
│ │ └── icomoon.woff
│ ├── browse.directory.head.js
│ ├── browse.directory.body.js
│ └── base.css
├── templates
│ ├── 404.html
│ ├── remove.html
│ ├── base.html
│ ├── 400.html
│ └── browse.html
├── __meta__.py
├── mimetype.py
├── appconfig.py
├── widget.py
├── exceptions.py
├── transform
│ ├── htmlcompress.py
│ ├── __init__.py
│ └── glob.py
├── stream.py
├── __main__.py
├── __init__.py
└── compat.py
├── .eslintignore
├── doc
├── screenshot.0.3.1-0.png
├── .templates
│ ├── layout.html
│ └── sidebar.html
├── exceptions.rst
├── tests_utils.rst
├── stream.rst
├── .static
│ └── logo.css
├── manager.rst
├── file.rst
├── builtin_plugins.rst
├── index.rst
├── compat.rst
├── quickstart.rst
├── integrations.rst
├── Makefile
├── plugins.rst
└── exclude.rst
├── setup.cfg
├── MANIFEST.in
├── .scrutinizer.yml
├── requirements.txt
├── .gitignore
├── .editorconfig
├── .eslintrc.json
├── .travis.yml
├── LICENSE
├── Makefile
├── .appveyor.yml
├── setup.py
└── README.rst
/.python-version:
--------------------------------------------------------------------------------
1 | 3.5.2
2 |
--------------------------------------------------------------------------------
/browsepy/tests/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/.eslintignore:
--------------------------------------------------------------------------------
1 | **/*.min.js
2 |
--------------------------------------------------------------------------------
/browsepy/tests/deprecated/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/browsepy/tests/deprecated/plugin/__init__.py:
--------------------------------------------------------------------------------
1 |
--------------------------------------------------------------------------------
/browsepy/plugin/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 |
--------------------------------------------------------------------------------
/browsepy/static/giorgio.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ergoithz/browsepy/HEAD/browsepy/static/giorgio.jpg
--------------------------------------------------------------------------------
/doc/screenshot.0.3.1-0.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ergoithz/browsepy/HEAD/doc/screenshot.0.3.1-0.png
--------------------------------------------------------------------------------
/browsepy/static/fonts/icomoon.eot:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ergoithz/browsepy/HEAD/browsepy/static/fonts/icomoon.eot
--------------------------------------------------------------------------------
/browsepy/static/fonts/icomoon.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ergoithz/browsepy/HEAD/browsepy/static/fonts/icomoon.ttf
--------------------------------------------------------------------------------
/browsepy/static/fonts/icomoon.woff:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ergoithz/browsepy/HEAD/browsepy/static/fonts/icomoon.woff
--------------------------------------------------------------------------------
/setup.cfg:
--------------------------------------------------------------------------------
1 | [metadata]
2 | description-file = README.rst
3 |
4 | [wheel]
5 | universal = 1
6 |
7 | [egg_info]
8 | tag_build =
--------------------------------------------------------------------------------
/browsepy/plugin/player/static/js/jquery.jplayer.swf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ergoithz/browsepy/HEAD/browsepy/plugin/player/static/js/jquery.jplayer.swf
--------------------------------------------------------------------------------
/browsepy/plugin/player/static/css/browse.css:
--------------------------------------------------------------------------------
1 | a.button.play:after {
2 | content: "\e903";
3 | }
4 |
5 | .playlist.icon:after{
6 | content: "\e907";
7 | }
8 |
--------------------------------------------------------------------------------
/MANIFEST.in:
--------------------------------------------------------------------------------
1 | include README.rst
2 | graft browsepy/templates
3 | graft browsepy/static
4 | graft browsepy/plugin/player/templates
5 | graft browsepy/plugin/player/static
6 |
--------------------------------------------------------------------------------
/browsepy/plugin/player/static/image/jplayer.blue.monday.jpg:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ergoithz/browsepy/HEAD/browsepy/plugin/player/static/image/jplayer.blue.monday.jpg
--------------------------------------------------------------------------------
/.scrutinizer.yml:
--------------------------------------------------------------------------------
1 | checks:
2 | python:
3 | code_rating: true
4 | duplicate_code: true
5 | filter:
6 | excluded_paths:
7 | - "*/tests.py"
8 | - "*/tests/*"
9 |
--------------------------------------------------------------------------------
/doc/.templates/layout.html:
--------------------------------------------------------------------------------
1 | {# layout.html #}
2 | {# Import the theme's layout. #}
3 | {% extends "!layout.html" %}
4 |
5 | {% set css_files = css_files + ['_static/logo.css'] %}
6 |
--------------------------------------------------------------------------------
/browsepy/plugin/player/static/image/jplayer.blue.monday.seeking.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ergoithz/browsepy/HEAD/browsepy/plugin/player/static/image/jplayer.blue.monday.seeking.gif
--------------------------------------------------------------------------------
/browsepy/plugin/player/static/image/jplayer.blue.monday.video.play.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/ergoithz/browsepy/HEAD/browsepy/plugin/player/static/image/jplayer.blue.monday.video.play.png
--------------------------------------------------------------------------------
/browsepy/static/browse.directory.head.js:
--------------------------------------------------------------------------------
1 | (function(){
2 | if(document.documentElement && document.querySelectorAll && document.addEventListener){
3 | document.documentElement.className += ' autosubmit-support';
4 | }
5 | }());
6 |
--------------------------------------------------------------------------------
/doc/exceptions.rst:
--------------------------------------------------------------------------------
1 | .. _exceptions:
2 |
3 | Exceptions Module
4 | =================
5 |
6 | Exceptions raised by browsepy classes.
7 |
8 | .. automodule:: browsepy.exceptions
9 | :show-inheritance:
10 | :members:
11 | :inherited-members:
12 |
--------------------------------------------------------------------------------
/browsepy/templates/404.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block title %}Upload failed{% endblock %}
4 | {% block content %}
5 |
Not Found
6 |
7 |
8 | {% endblock %}
9 |
--------------------------------------------------------------------------------
/doc/tests_utils.rst:
--------------------------------------------------------------------------------
1 | .. _test-utils:
2 |
3 | Test Utility Module
4 | ===================
5 |
6 | Convenience functions for unit testing.
7 |
8 | .. automodule:: browsepy.tests.utils
9 | :show-inheritance:
10 | :members:
11 | :inherited-members:
12 |
--------------------------------------------------------------------------------
/requirements.txt:
--------------------------------------------------------------------------------
1 | flask
2 | unicategories
3 |
4 | # for python < 3.6
5 | scandir
6 |
7 | # for python < 3.3
8 | backports.shutil_get_terminal_size
9 |
10 | # for building
11 | setuptools
12 | wheel
13 |
14 | # for Makefile tasks (testing, coverage, docs)
15 | pep8
16 | flake8
17 | coverage
18 | pyaml
19 | sphinx
20 |
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | .git/*
2 | .c9/*
3 | .idea/*
4 | .coverage
5 | htmlcov/*
6 | dist/*
7 | build/*
8 | doc/.build/*
9 | env/*
10 | env2/*
11 | env3/*
12 | wenv2/*
13 | wenv3/*
14 | node_modules/*
15 | .eggs/*
16 | browsepy.build/*
17 | browsepy.egg-info/*
18 | **/__pycache__/*
19 | **.egg/*
20 | **.eggs/*
21 | **.egg-info/*
22 | **.pyc
23 | **/node_modules/*
24 |
--------------------------------------------------------------------------------
/browsepy/__meta__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 |
4 | app = 'browsepy'
5 | description = 'Simple web file browser'
6 | version = '0.5.6'
7 | license = 'MIT'
8 | author_name = 'Felipe A. Hernandez'
9 | author_mail = 'ergoithz@gmail.com'
10 | author = '%s <%s>' % (author_name, author_mail)
11 | url = 'https://github.com/ergoithz/browsepy'
12 | tarball = '%s/archive/%s.tar.gz' % (url, version)
13 |
--------------------------------------------------------------------------------
/doc/stream.rst:
--------------------------------------------------------------------------------
1 | .. _file:
2 |
3 | Stream Module
4 | =============
5 |
6 | .. currentmodule:: browsepy.stream
7 |
8 | This module provides a class for streaming directory tarballs. This is used
9 | by :meth:`browsepy.file.Directory.download` method.
10 |
11 | .. _tarfilestream-node:
12 |
13 | TarFileStream
14 | ----
15 |
16 | .. currentmodule:: browsepy.stream
17 |
18 | .. autoclass:: TarFileStream
19 | :members:
20 | :inherited-members:
21 | :undoc-members:
22 |
--------------------------------------------------------------------------------
/.editorconfig:
--------------------------------------------------------------------------------
1 | root = true
2 |
3 | [*]
4 | charset = utf-8
5 | indent_style = space
6 | indent_size = 4
7 | end_of_line = lf
8 | max_line_length = 79
9 | charset = utf-8
10 | trim_trailing_whitespace = true
11 | insert_final_newline = true
12 |
13 | [Makefile]
14 | indent_style = tab
15 |
16 | [*.{yml,json,css,js,html,rst}]
17 | indent_style = space
18 | indent_size = 2
19 |
20 | [*.{py,js}]
21 | quote_type = single
22 |
23 | [*.md]
24 | trim_trailing_whitespace = false
25 |
--------------------------------------------------------------------------------
/doc/.templates/sidebar.html:
--------------------------------------------------------------------------------
1 |
2 | The simple web file browser.
3 | Useful Links
4 |
15 |
--------------------------------------------------------------------------------
/browsepy/static/browse.directory.body.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | if (document.querySelectorAll) {
3 | var
4 | forms = document.querySelectorAll('html.autosubmit-support form.autosubmit'),
5 | i = forms.length;
6 | while (i--) {
7 | var files = forms[i].querySelectorAll('input[type=file]');
8 | files[0].addEventListener('change', (function(form) {
9 | return function() {
10 | form.submit();
11 | };
12 | }(forms[i])));
13 | }
14 | }
15 | }());
16 |
--------------------------------------------------------------------------------
/.eslintrc.json:
--------------------------------------------------------------------------------
1 | {
2 | "env": {
3 | "browser": true,
4 | "jquery": true
5 | },
6 | "extends": "eslint:recommended",
7 | "parserOptions": {
8 | "sourceType": "module"
9 | },
10 | "rules": {
11 | "indent": [
12 | "error",
13 | 2, {
14 | "SwitchCase": 1
15 | }
16 | ],
17 | "linebreak-style": [
18 | "error",
19 | "unix"
20 | ],
21 | "quotes": [
22 | "error",
23 | "single"
24 | ],
25 | "semi": [
26 | "error",
27 | "always"
28 | ]
29 | }
30 | }
31 |
--------------------------------------------------------------------------------
/browsepy/templates/remove.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% block content %}
4 | Remove
5 | Do you really want to remove {{ file.name }}?
8 |
11 |
14 | {% endblock %}
15 |
--------------------------------------------------------------------------------
/doc/.static/logo.css:
--------------------------------------------------------------------------------
1 | div.sphinxsidebar h3.logo{
2 | line-height: 1.15em;
3 | font-family: sans;
4 | font-size: 2em;
5 | margin: 0;
6 | padding: 0 0 0.3em;
7 | display: block;
8 | color: black;
9 | text-shadow: 1px 1px 0 white, -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 0 0 white, -1px 0 0 white, 0 -1px 0 white, 0 1px 0 white, 2px 2px 0 black, -2px -2px 0 black, 2px -2px 0 black, -2px 2px 0 black, 2px 0 0 black, -2px 0 0 black, 0 2px 0 black, 0 -2px 0 black, 2px 1px 0 black, -2px 1px 0 black, 1px 2px 0 black, 1px -2px 0 black, 2px -1px 0 black, -2px -1px 0 black, -1px 2px 0 black, -1px -2px 0 black;
10 | }
11 | div.sphinxsidebar h3.logo a{
12 | color: black;
13 | }
14 |
--------------------------------------------------------------------------------
/browsepy/plugin/player/static/css/base.css:
--------------------------------------------------------------------------------
1 | .jp-audio {
2 | width: auto;
3 | }
4 |
5 | .jp-audio .jp-controls {
6 | width: auto;
7 | }
8 |
9 | .jp-current-time, .jp-duration {
10 | width: 4.5em;
11 | }
12 |
13 | .jp-audio .jp-type-playlist .jp-toggles {
14 | left: auto;
15 | right: -90px;
16 | top: 0;
17 | }
18 |
19 | .jp-audio .jp-type-single .jp-progress, .jp-audio .jp-type-playlist .jp-progress {
20 | width: auto;
21 | right: 130px;
22 | }
23 |
24 | .jp-volume-controls {
25 | left: auto;
26 | right: 15px;
27 | width: 100px;
28 | }
29 |
30 | .jp-audio .jp-type-single .jp-time-holder, .jp-audio .jp-type-playlist .jp-time-holder {
31 | width: auto;
32 | right: 130px;
33 | }
34 |
--------------------------------------------------------------------------------
/browsepy/templates/base.html:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 |
5 |
6 | {% block title %}{{ config.get("title", "BrowsePy") }}{% endblock %}
7 | {% block styles %}
8 |
9 | {% endblock %}
10 | {% block head %}{% endblock %}
11 |
12 |
13 | {% block header %}{% endblock %}
14 | {% block content %}{% endblock %}
15 | {% block footer %}{% endblock %}
16 | {% block scripts %}{% endblock %}
17 |
18 |
19 |
--------------------------------------------------------------------------------
/browsepy/tests/utils.py:
--------------------------------------------------------------------------------
1 |
2 | import flask
3 |
4 |
5 | def clear_localstack(stack):
6 | '''
7 | Clear given werkzeug LocalStack instance.
8 |
9 | :param ctx: local stack instance
10 | :type ctx: werkzeug.local.LocalStack
11 | '''
12 | while stack.pop():
13 | pass
14 |
15 |
16 | def clear_flask_context():
17 | '''
18 | Clear flask current_app and request globals.
19 |
20 | When using :meth:`flask.Flask.test_client`, even as context manager,
21 | the flask's globals :attr:`flask.current_app` and :attr:`flask.request`
22 | are left dirty, so testing code relying on them will probably fail.
23 |
24 | This function clean said globals, and should be called after testing
25 | with :meth:`flask.Flask.test_client`.
26 | '''
27 | clear_localstack(flask._app_ctx_stack)
28 | clear_localstack(flask._request_ctx_stack)
29 |
--------------------------------------------------------------------------------
/.travis.yml:
--------------------------------------------------------------------------------
1 | language: python
2 |
3 | addon:
4 | apt:
5 | nodejs
6 |
7 | matrix:
8 | include:
9 | - python: "2.7"
10 | - python: "pypy"
11 | # pypy3 disabled until fixed
12 | #- python: "pypy3"
13 | - python: "3.3"
14 | - python: "3.4"
15 | - python: "3.5"
16 | - python: "3.6"
17 | env:
18 | - eslint=yes
19 | - sphinx=yes
20 |
21 | install:
22 | - pip install --upgrade pip
23 | - pip install travis-sphinx flake8 pep8 codecov coveralls .
24 | - |
25 | if [ "$eslint" = "yes" ]; then
26 | nvm install stable
27 | nvm use stable
28 | npm install eslint
29 | export PATH="$PWD/node_modules/.bin:$PATH"
30 | fi
31 |
32 | script:
33 | - make travis-script
34 | - |
35 | if [ "$eslint" = "yes" ]; then
36 | make eslint
37 | fi
38 | - |
39 | if [ "$sphinx" = "yes" ]; then
40 | make travis-script-sphinx
41 | fi
42 |
43 | after_success:
44 | - make travis-success
45 | - |
46 | if [ "$sphinx" = "yes" ]; then
47 | make travis-success-sphinx
48 | fi
49 |
50 | notifications:
51 | email: false
52 |
53 | cache:
54 | directories:
55 | - $HOME/.cache/pip
56 |
--------------------------------------------------------------------------------
/LICENSE:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 |
3 | Copyright (c) 2013-2016 Felipe A. Hernandez
4 |
5 | Permission is hereby granted, free of charge, to any person obtaining a copy of
6 | this software and associated documentation files (the "Software"), to deal in
7 | the Software without restriction, including without limitation the rights to
8 | use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
9 | the Software, and to permit persons to whom the Software is furnished to do so,
10 | subject to the following conditions:
11 |
12 | The above copyright notice and this permission notice shall be included in all
13 | copies or substantial portions of the Software.
14 |
15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
17 | FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
18 | COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
19 | IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
20 | CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
21 |
--------------------------------------------------------------------------------
/browsepy/mimetype.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 |
4 | import re
5 | import subprocess
6 | import mimetypes
7 |
8 | from .compat import FileNotFoundError, which # noqa
9 |
10 | generic_mimetypes = frozenset(('application/octet-stream', None))
11 | re_mime_validate = re.compile('\w+/\w+(; \w+=[^;]+)*')
12 |
13 |
14 | def by_python(path):
15 | mime, encoding = mimetypes.guess_type(path)
16 | if mime in generic_mimetypes:
17 | return None
18 | return "%s%s%s" % (
19 | mime or "application/octet-stream", "; "
20 | if encoding else
21 | "", encoding or ""
22 | )
23 |
24 |
25 | if which('file'):
26 | def by_file(path):
27 | try:
28 | output = subprocess.check_output(
29 | ("file", "-ib", path),
30 | universal_newlines=True
31 | ).strip()
32 | if (
33 | re_mime_validate.match(output) and
34 | output not in generic_mimetypes
35 | ):
36 | # 'file' command can return status zero with invalid output
37 | return output
38 | except (subprocess.CalledProcessError, FileNotFoundError):
39 | pass
40 | return None
41 | else:
42 | def by_file(path):
43 | return None
44 |
45 |
46 | def by_default(path):
47 | return "application/octet-stream"
48 |
--------------------------------------------------------------------------------
/doc/manager.rst:
--------------------------------------------------------------------------------
1 | .. _manager:
2 |
3 | Manager Module
4 | ==============
5 |
6 | .. currentmodule:: browsepy.manager
7 |
8 | The browsepy's :doc:`manager` module centralizes all plugin-related
9 | logic, hook calls and component registration methods.
10 |
11 | For an extended explanation head over :doc:`plugins`.
12 |
13 | .. _manager-argument:
14 |
15 | Argument Plugin Manager
16 | -----------------------
17 |
18 | This class represents a subset of :class:`PluginManager` functionality, and
19 | an instance of this class will be passed to :func:`register_arguments` plugin
20 | module-level functions.
21 |
22 | .. autoclass:: ArgumentPluginManager
23 | :members:
24 | :inherited-members:
25 | :undoc-members:
26 |
27 | .. _manager-plugin:
28 |
29 | Plugin Manager
30 | --------------
31 |
32 | This class includes are the plugin registration functionality, and itself
33 | will be passed to :func:`register_plugin` plugin module-level functions.
34 |
35 | .. autoclass:: PluginManager
36 | :members:
37 | :inherited-members:
38 | :undoc-members:
39 | :exclude-members: register_action, get_actions, action_class, style_class,
40 | button_class, javascript_class, link_class, widget_types
41 |
42 | .. autoattribute:: widget_types
43 |
44 | Dictionary with widget type names and their corresponding class (based on
45 | namedtuple, see :func:`defaultsnamedtuple`) so it could be instanced and
46 | reused (see :meth:`register_widget`).
47 |
48 | .. _manager-util:
49 |
50 | Utility functions
51 | -----------------
52 |
53 | .. autofunction:: defaultsnamedtuple
54 |
--------------------------------------------------------------------------------
/browsepy/tests/test_app.py:
--------------------------------------------------------------------------------
1 | import os
2 | import os.path
3 | import unittest
4 | import tempfile
5 |
6 | import browsepy
7 | import browsepy.appconfig
8 |
9 |
10 | class TestApp(unittest.TestCase):
11 | module = browsepy
12 | app = browsepy.app
13 |
14 | def test_config(self):
15 | try:
16 | with tempfile.NamedTemporaryFile(delete=False) as f:
17 | f.write(b'DIRECTORY_DOWNLOADABLE = False\n')
18 | name = f.name
19 | os.environ['BROWSEPY_TEST_SETTINGS'] = name
20 | self.app.config['directory_downloadable'] = True
21 | self.app.config.from_envvar('BROWSEPY_TEST_SETTINGS')
22 | self.assertFalse(self.app.config['directory_downloadable'])
23 | finally:
24 | os.remove(name)
25 |
26 |
27 | class TestConfig(unittest.TestCase):
28 | pwd = os.path.dirname(os.path.abspath(__file__))
29 | module = browsepy.appconfig
30 |
31 | def test_case_insensitivity(self):
32 | cfg = self.module.Config(self.pwd, defaults={'prop': 2})
33 | self.assertEqual(cfg['prop'], cfg['PROP'])
34 | self.assertEqual(cfg['pRoP'], cfg.pop('prop'))
35 | cfg.update(prop=1)
36 | self.assertEqual(cfg['PROP'], 1)
37 | self.assertEqual(cfg.get('pRop'), 1)
38 | self.assertEqual(cfg.popitem(), ('PROP', 1))
39 | self.assertRaises(KeyError, cfg.pop, 'prop')
40 | cfg.update(prop=1)
41 | del cfg['PrOp']
42 | self.assertRaises(KeyError, cfg.__delitem__, 'prop')
43 | self.assertIsNone(cfg.pop('prop', None))
44 | self.assertIsNone(cfg.get('prop'))
45 |
--------------------------------------------------------------------------------
/doc/file.rst:
--------------------------------------------------------------------------------
1 | .. _file:
2 |
3 | File Module
4 | ===========
5 |
6 | .. currentmodule:: browsepy.file
7 |
8 | For more advanced use-cases dealing with the filesystem, the browsepy's own
9 | classes (:class:`Node`, :class:`File` and :class:`Directory`) can be
10 | instantiated and inherited.
11 |
12 | :class:`Node` class is meant for implementing your own special filesystem
13 | nodes, via inheritance (it's abstract so shouldn't be instantiated directly).
14 | Just remember to overload its :attr:`Node.generic` attribute value to False.
15 |
16 | Both :class:`File` and :class:`Directory` classes can be instantiated or
17 | extended, via inheritance, with logic like different default widgets, virtual data (see player plugin code).
18 |
19 | .. _file-node:
20 |
21 | Node
22 | ----
23 |
24 | .. currentmodule:: browsepy.file
25 |
26 | .. autoclass:: Node
27 | :members:
28 | :inherited-members:
29 | :undoc-members:
30 |
31 | .. _file-directory:
32 |
33 | Directory
34 | ---------
35 |
36 | .. autoclass:: Directory
37 | :show-inheritance:
38 | :members:
39 | :inherited-members:
40 | :undoc-members:
41 |
42 | .. _file-file:
43 |
44 | File
45 | ----
46 |
47 | .. autoclass:: File
48 | :show-inheritance:
49 | :members:
50 | :inherited-members:
51 | :undoc-members:
52 |
53 | .. _file-util:
54 |
55 | Utility functions
56 | -----------------
57 |
58 | .. autofunction:: fmt_size
59 | .. autofunction:: abspath_to_urlpath
60 | .. autofunction:: urlpath_to_abspath
61 | .. autofunction:: check_under_base
62 | .. autofunction:: check_base
63 | .. autofunction:: check_path
64 | .. autofunction:: secure_filename
65 | .. autofunction:: alternative_filename
66 | .. autofunction:: scandir
67 |
--------------------------------------------------------------------------------
/doc/builtin_plugins.rst:
--------------------------------------------------------------------------------
1 | .. _builtin-plugins:
2 |
3 | Bultin Plugins
4 | ==============
5 |
6 | A player plugin is provided by default, and more are planned.
7 |
8 | Builtin-plugins serve as developers reference, while also being useful for
9 | unit-testing.
10 |
11 | .. _builtin-plugins-player:
12 |
13 | Player
14 | ------
15 |
16 | Player plugin provides their own endpoints, widgets and extends both
17 | :class:`browsepy.file.File` and :class:`browsepy.file.Directory` so playlists
18 | and directories could be handled.
19 |
20 | At the client-side, a slighty tweaked `jPlayer `_
21 | implementation is used.
22 |
23 | Sources are available at browsepy's `plugin.player`_ submodule.
24 |
25 | .. _plugin.player: https://github.com/ergoithz/browsepy/tree/master/browsepy/plugin/player
26 |
27 | .. _builtin-plugins-contributing:
28 |
29 | Contributing Builtin Plugins
30 | ----------------------------
31 |
32 | Browsepy's team is open to contributions of any kind, even about adding
33 | built-in plugins, as long as they comply with the following requirements:
34 |
35 | * Plugins must be sufficiently covered by tests to avoid lowering browsepy's
36 | overall test coverage.
37 | * Plugins must not add external requirements to browsepy, optional
38 | requirements are allowed if plugin can work without them, even with
39 | limited functionality.
40 | * Plugins should avoid adding specific logic on browsepy itself, but extending
41 | browsepy's itself (specially via plugin interface) in a generic and useful
42 | way is definitely welcome.
43 |
44 | Said that, feel free to fork, code great stuff and fill pull requests at
45 | `GitHub `_.
46 |
--------------------------------------------------------------------------------
/browsepy/plugin/player/static/js/base.js:
--------------------------------------------------------------------------------
1 | (function() {
2 | var
3 | jPlayerPlaylist = window.jPlayerPlaylist,
4 | $player = $('.jp-jplayer'),
5 | playlists = (window._player = window._player || {playlist: []}).playlist,
6 | options = {
7 | swfPath: $player.attr('data-player-swf'),
8 | wmode: 'window',
9 | useStateClassSkin: true,
10 | autoBlur: false,
11 | smoothPlayBar: true,
12 | keyEnabled: true,
13 | remainingDuration: true,
14 | toggleDuration: true,
15 | cssSelectorAncestor: '.jp-audio',
16 | playlistOptions: {
17 | autoPlay: true
18 | }
19 | };
20 | if ($player.is('[data-player-urls]')) {
21 | var
22 | list = [],
23 | formats = [],
24 | urls = $player.attr('data-player-urls').split('|'),
25 | sel = {
26 | jPlayer: $player,
27 | cssSelectorAncestor: '.jp-audio'
28 | };
29 | for (var i = 0, stack = [], d, o; (o = urls[i++]);) {
30 | stack.push(o);
31 | if (stack.length == 3) {
32 | d = {
33 | title: stack[1]
34 | };
35 | d[stack[0]] = stack[2];
36 | list.push(d);
37 | formats.push(stack[0]);
38 | stack.splice(0, stack.length);
39 | }
40 | }
41 | options.supplied = formats.join(', ');
42 | playlists.push(new jPlayerPlaylist(sel, list, options));
43 | } else {
44 | var
45 | media = {},
46 | format = $player.attr('data-player-format');
47 | media.title = $player.attr('data-player-title');
48 | media[format] = $player.attr('data-player-url');
49 | options.supplied = format;
50 | options.ready = function() {
51 | $(this).jPlayer('setMedia', media).jPlayer('play');
52 | };
53 | $player.jPlayer(options);
54 | }
55 | }());
56 |
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY: doc clean pep8 coverage travis
2 |
3 | test: pep8 flake8 eslint
4 | python -c 'import yaml;yaml.load(open(".travis.yml").read())'
5 | ifdef debug
6 | python setup.py test --debug=$(debug)
7 | else
8 | python setup.py test
9 | endif
10 |
11 | clean:
12 | rm -rf build dist browsepy.egg-info htmlcov MANIFEST \
13 | .eggs *.egg .coverage
14 | find browsepy -type f -name "*.py[co]" -delete
15 | find browsepy -type d -name "__pycache__" -delete
16 | $(MAKE) -C doc clean
17 |
18 | build-env:
19 | mkdir -p build
20 | python3 -m venv build/env3
21 | build/env3/bin/pip install pip --upgrade
22 | build/env3/bin/pip install wheel
23 |
24 | build: clean build-env
25 | build/env3/bin/python setup.py bdist_wheel
26 | build/env3/bin/python setup.py sdist
27 |
28 | upload: clean build-env
29 | build/env3/bin/python setup.py bdist_wheel upload
30 | build/env3/bin/python setup.py sdist upload
31 |
32 | doc:
33 | $(MAKE) -C doc html 2>&1 | grep -v \
34 | 'WARNING: more than one target found for cross-reference'
35 |
36 | showdoc: doc
37 | xdg-open file://${CURDIR}/doc/.build/html/index.html >> /dev/null
38 |
39 | pep8:
40 | find browsepy -type f -name "*.py" -exec pep8 --ignore=E123,E126,E121 {} +
41 |
42 | eslint:
43 | eslint \
44 | --ignore-path .gitignore \
45 | --ignore-pattern *.min.js \
46 | ${CURDIR}/browsepy
47 |
48 | flake8:
49 | flake8 browsepy/
50 |
51 | coverage:
52 | coverage run --source=browsepy setup.py test
53 |
54 | showcoverage: coverage
55 | coverage html
56 | xdg-open file://${CURDIR}/htmlcov/index.html >> /dev/null
57 |
58 | travis-script: pep8 flake8 coverage
59 |
60 | travis-script-sphinx:
61 | travis-sphinx --nowarn --source=doc build
62 |
63 | travis-success:
64 | codecov
65 | coveralls
66 |
67 | travis-success-sphinx:
68 | travis-sphinx deploy
69 |
--------------------------------------------------------------------------------
/browsepy/tests/runner.py:
--------------------------------------------------------------------------------
1 |
2 | import os
3 | import unittest
4 |
5 |
6 | class DebuggerTextTestResult(unittest._TextTestResult): # pragma: no cover
7 | def __init__(self, stream, descriptions, verbosity, debugger):
8 | self.debugger = debugger
9 | self.shouldStop = True
10 | supa = super(DebuggerTextTestResult, self)
11 | supa.__init__(stream, descriptions, verbosity)
12 |
13 | def addError(self, test, exc_info):
14 | self.debugger(exc_info)
15 | super(DebuggerTextTestResult, self).addError(test, exc_info)
16 |
17 | def addFailure(self, test, exc_info):
18 | self.debugger(exc_info)
19 | super(DebuggerTextTestResult, self).addFailure(test, exc_info)
20 |
21 |
22 | class DebuggerTextTestRunner(unittest.TextTestRunner): # pragma: no cover
23 | debugger = os.environ.get('UNITTEST_DEBUG', 'none')
24 | test_result_class = DebuggerTextTestResult
25 |
26 | def __init__(self, *args, **kwargs):
27 | kwargs.setdefault('verbosity', 2)
28 | super(DebuggerTextTestRunner, self).__init__(*args, **kwargs)
29 |
30 | @staticmethod
31 | def debug_none(exc_info):
32 | pass
33 |
34 | @staticmethod
35 | def debug_pdb(exc_info):
36 | import pdb
37 | pdb.post_mortem(exc_info[2])
38 |
39 | @staticmethod
40 | def debug_ipdb(exc_info):
41 | import ipdb
42 | ipdb.post_mortem(exc_info[2])
43 |
44 | @staticmethod
45 | def debug_pudb(exc_info):
46 | import pudb
47 | pudb.post_mortem(exc_info[2], exc_info[1], exc_info[0])
48 |
49 | def _makeResult(self):
50 | return self.test_result_class(
51 | self.stream, self.descriptions, self.verbosity,
52 | getattr(self, 'debug_%s' % self.debugger, self.debug_none)
53 | )
54 |
--------------------------------------------------------------------------------
/doc/index.rst:
--------------------------------------------------------------------------------
1 | .. browsepy documentation master file, created by
2 | sphinx-quickstart on Thu Nov 17 11:54:15 2016.
3 | You can adapt this file completely to your liking, but it should at least
4 | contain the root `toctree` directive.
5 |
6 | Welcome to browsepy's documentation!
7 | ====================================
8 |
9 | Welcome to browsepy's documentation. It's recommended to start reading both
10 | :ref:`quickstart` and, specifically :ref:`quickstart-installation`, while more
11 | detailed tutorials about integrating :mod:`browsepy` as module or plugin
12 | development are also available.
13 |
14 | Browsepy has few dependencies: `Flask`_ and `Scandir`_. `Flask`_ is an awesome
15 | web microframework while `Scandir`_ is a directory listing library which `was
16 | included `_ in Python 3.5's
17 | standard library.
18 |
19 | If you want to dive into their documentation, check out the following links:
20 |
21 | * `Flask Documentation
22 | `_
23 | * `Scandir Readme
24 | `_
25 | * `Scandir Python Documentation
26 | `_
27 |
28 | .. _Flask: http://jinja.pocoo.org/
29 | .. _Scandir: http://werkzeug.pocoo.org/
30 |
31 | User's Guide
32 | ============
33 | Instructions for users, implementers and developers.
34 |
35 | .. toctree::
36 | :maxdepth: 2
37 |
38 | quickstart
39 | exclude
40 | builtin_plugins
41 | plugins
42 | integrations
43 |
44 | API Reference
45 | =============
46 | Specific information about functions, class or methods.
47 |
48 | .. toctree::
49 | :maxdepth: 2
50 |
51 | manager
52 | file
53 | stream
54 | compat
55 | exceptions
56 | tests_utils
57 |
58 | Indices and tables
59 | ==================
60 | Random documentation content references.
61 |
62 | * :ref:`genindex`
63 | * :ref:`modindex`
64 | * :ref:`search`
65 |
--------------------------------------------------------------------------------
/browsepy/tests/test_extensions.py:
--------------------------------------------------------------------------------
1 |
2 | import unittest
3 | import jinja2
4 |
5 | import browsepy.transform.htmlcompress
6 |
7 |
8 | class TestHTMLCompress(unittest.TestCase):
9 | extension = browsepy.transform.htmlcompress.HTMLCompress
10 |
11 | def setUp(self):
12 | self.env = jinja2.Environment(
13 | autoescape=True,
14 | extensions=[self.extension]
15 | )
16 |
17 | def render(self, html, **kwargs):
18 | return self.env.from_string(html).render(**kwargs)
19 |
20 | def test_compress(self):
21 | html = self.render('''
22 |
23 |
24 | {{ title }}
25 |
26 |
29 | a b
30 | {% if a %}b{% endif %}
31 |
32 |
33 | ''', title=42, href='index.html', css='t', a=True)
34 | self.assertEqual(
35 | html,
36 | '42'
37 | 'a bb'
38 | ''
39 | )
40 |
41 | def test_ignored_content(self):
42 | html = self.render(
43 | '\n
'
44 | )
45 | self.assertEqual(
46 | html,
47 | '
'
48 | )
49 |
50 | def test_cdata(self):
51 | html = self.render(
52 | '
\n]]>\n
\n'
53 | )
54 | self.assertEqual(
55 | html,
56 | '
\n]]>
'
57 | )
58 |
59 | def test_broken(self):
60 | html = self.render('',
67 | 'style': '',
68 | }
69 |
70 |
71 | class HTMLCompress(jinja2.ext.Extension):
72 | context_class = HTMLCompressContext
73 | token_class = jinja2.lexer.Token
74 | block_tokens = {
75 | 'variable_begin': 'variable_end',
76 | 'block_begin': 'block_end'
77 | }
78 |
79 | def filter_stream(self, stream):
80 | transform = self.context_class()
81 | lineno = 0
82 | skip_until_token = None
83 | for token in stream:
84 | if skip_until_token:
85 | yield token
86 | if token.type == skip_until_token:
87 | skip_until_token = None
88 | continue
89 |
90 | if token.type != 'data':
91 | for data in transform.finish():
92 | yield self.token_class(lineno, 'data', data)
93 | yield token
94 | skip_until_token = self.block_tokens.get(token.type)
95 | continue
96 |
97 | if not transform.pending:
98 | lineno = token.lineno
99 |
100 | for data in transform.feed(token.value):
101 | yield self.token_class(lineno, 'data', data)
102 | lineno = token.lineno
103 |
104 | for data in transform.finish():
105 | yield self.token_class(lineno, 'data', data)
106 |
--------------------------------------------------------------------------------
/browsepy/plugin/player/templates/audio.player.html:
--------------------------------------------------------------------------------
1 | {% extends "browse.html" %}
2 |
3 | {% block styles %}
4 | {{ super() }}
5 |
6 |
7 | {% endblock %}
8 |
9 | {% block content %}
10 |
28 |
29 |
30 |
31 |
32 | {% if playlist %}
33 |
34 | {% endif %}
35 |
36 | {% if playlist %}
37 |
38 | {% endif %}
39 |
40 |
41 |
46 |
47 |
48 |
49 |
52 |
53 |
54 |
55 |
56 |
57 |
58 | {% if playlist %}
59 |
60 | {% endif %}
61 |
62 |
63 |
64 |
67 | {% if playlist %}
68 |
73 | {% endif %}
74 |
75 |
Update Required
76 | In order to get this player working either update your browse or your
Flash plugin.
77 |
78 |
79 |
80 | {% endblock %}
81 |
82 | {% block scripts %}
83 | {{ super() }}
84 |
85 |
86 | {% if playlist %}
87 |
88 | {% endif %}
89 |
90 | {% endblock %}
91 |
--------------------------------------------------------------------------------
/browsepy/tests/test_transform.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import re
5 | import unittest
6 | import warnings
7 |
8 | from browsepy.compat import unicode
9 |
10 | import browsepy.transform
11 | import browsepy.transform.glob
12 |
13 |
14 | def fu(c):
15 | if isinstance(c, unicode):
16 | return c
17 | return c.decode('utf-8')
18 |
19 |
20 | class TestStateMachine(unittest.TestCase):
21 | module = browsepy.transform
22 |
23 | def test_nearest_error(self):
24 | m = self.module.StateMachine()
25 | self.assertRaises(KeyError, lambda: m.nearest)
26 |
27 |
28 | class TestGlob(unittest.TestCase):
29 | module = browsepy.transform.glob
30 | translate = staticmethod(module.translate)
31 |
32 | def assertSubclass(self, cls, base):
33 | self.assertIn(base, cls.mro())
34 |
35 | def test_glob(self):
36 | translations = [
37 | ('/a', r'^/a(/|$)'),
38 | ('a', r'/a(/|$)'),
39 | ('/a*', r'^/a[^/]*(/|$)'),
40 | ('/a**', r'^/a.*(/|$)'),
41 | ('a?', r'/a[^/](/|$)'),
42 | ('/a{b,c}', r'^/a(b|c)(/|$)'),
43 | ('/a[a,b]', r'^/a[a,b](/|$)'),
44 | ('/a[!b]', r'^/a[^b](/|$)'),
45 | ('/a[!/]', r'^/a[^/](/|$)'),
46 | ('/a[]]', r'^/a[\]](/|$)'),
47 | ('/a\\*', r'^/a\*(/|$)'),
48 | ('a{,.{txt,py[!od]}}', r'/a(|\.(txt|py[^od]))(/|$)'),
49 | ('a,a', r'/a,a(/|$)'),
50 | ]
51 | self.assertListEqual(
52 | [self.translate(g, sep='/') for g, r in translations],
53 | [r for g, r in translations]
54 | )
55 |
56 | translations = [
57 | ('/a', r'^\\a(\\|$)'),
58 | ('a', r'\\a(\\|$)'),
59 | ('/a*', r'^\\a[^\\]*(\\|$)'),
60 | ('/a**', r'^\\a.*(\\|$)'),
61 | ('a?', r'\\a[^\\](\\|$)'),
62 | ('/a{b,c}', r'^\\a(b|c)(\\|$)'),
63 | ('/a[a,b]', r'^\\a[a,b](\\|$)'),
64 | ('/a[!b]', r'^\\a[^b](\\|$)'),
65 | ('/a[!/]', r'^\\a[^\\](\\|$)'),
66 | ('/a[]]', r'^\\a[\]](\\|$)'),
67 | ('/a\\*', r'^\\a\*(\\|$)'),
68 | ]
69 | self.assertListEqual(
70 | [self.translate(g, sep='\\') for g, r in translations],
71 | [r for g, r in translations]
72 | )
73 |
74 | def test_unicode(self):
75 | tests = [
76 | ('/[[:alpha:][:digit:]]', (
77 | '/a',
78 | '/ñ',
79 | '/1',
80 | ), (
81 | '/_',
82 | )),
83 | ('/[[:alpha:]0-5]', (
84 | '/a',
85 | '/á',
86 | ), (
87 | '/6',
88 | '/_',
89 | )),
90 | ]
91 | for pattern, matching, nonmatching in tests:
92 | pattern = re.compile(self.translate(pattern, sep='/'))
93 | for test in matching:
94 | self.assertTrue(pattern.match(fu(test)))
95 | for test in nonmatching:
96 | self.assertFalse(pattern.match(fu(test)))
97 |
98 | def test_unsupported(self):
99 | translations = [
100 | ('[[.a-acute.]]a', '/.a(/|$)'),
101 | ('/[[=a=]]a', '^/.a(/|$)'),
102 | ('/[[=a=]\d]a', '^/.a(/|$)'),
103 | ('[[:non-existent-class:]]a', '/.a(/|$)'),
104 | ]
105 | for source, result in translations:
106 | with warnings.catch_warnings(record=True) as w:
107 | warnings.simplefilter("always")
108 | self.assertEqual(self.translate(source, sep='/'), result)
109 | self.assertSubclass(w[-1].category, Warning)
110 |
--------------------------------------------------------------------------------
/doc/quickstart.rst:
--------------------------------------------------------------------------------
1 | .. _quickstart:
2 |
3 | Quickstart
4 | ==========
5 |
6 | .. _quickstart-installation:
7 |
8 | Installation
9 | ------------
10 |
11 | browsepy is available at the `Python Package Index `_
12 | so you can use pip to install. Please remember `virtualenv`_ or :mod:`venv`
13 | usage, for Python 2 and Python 3 respectively, is highly recommended when
14 | working with pip.
15 |
16 | .. code-block:: bash
17 |
18 | pip install browsepy
19 |
20 | Alternatively, you can get the development version from our
21 | `github repository`_ using `git`_. Brench **master** will be
22 | pointing to current release while new versions will reside on
23 | their own branches.
24 |
25 | .. code-block:: bash
26 |
27 | pip install git+https://github.com/ergoithz/browsepy.git
28 |
29 | .. _virtualenv: https://virtualenv.pypa.io/
30 | .. _github repository: https://github.com/ergoithz/browsepy
31 | .. _git: https://git-scm.com/
32 |
33 | .. _quickstart-usage:
34 |
35 | Usage
36 | -----
37 |
38 | These examples assume python's `bin` directory is in `PATH`, if not,
39 | replace `browsepy` with `python -m browsepy`.
40 |
41 | Serving ``$HOME/shared`` to all addresses:
42 |
43 | .. code-block:: bash
44 |
45 | browsepy 0.0.0.0 8080 --directory $HOME/shared
46 |
47 | Showing help:
48 |
49 | .. code-block:: bash
50 |
51 | browsepy --help
52 |
53 | And this is what is printed when you run `browsepy --help`, keep in
54 | mind that plugins (loaded with `plugin` argument) could add extra arguments to
55 | this list.
56 |
57 | ::
58 |
59 | usage: browsepy [-h] [--directory PATH] [--initial PATH]
60 | [--removable PATH] [--upload PATH]
61 | [--exclude PATTERN] [--exclude-from PATH]
62 | [--plugin MODULE]
63 | [host] [port]
64 |
65 | description: starts a browsepy web file browser
66 |
67 | positional arguments:
68 | host address to listen (default: 127.0.0.1)
69 | port port to listen (default: 8080)
70 |
71 | optional arguments:
72 | -h, --help show this help message and exit
73 | --directory PATH serving directory (default: current path)
74 | --initial PATH default directory (default: same as --directory)
75 | --removable PATH base directory allowing remove (default: none)
76 | --upload PATH base directory allowing upload (default: none)
77 | --exclude PATTERN exclude paths by pattern (multiple)
78 | --exclude-from PATH exclude paths by pattern file (multiple)
79 | --plugin MODULE load plugin module (multiple)
80 |
81 | Showing help including player plugin arguments:
82 |
83 | .. code-block:: bash
84 |
85 | browsepy --plugin player --help
86 |
87 | And this is what is printed when you run `browsepy --plugin player --help`.
88 | Please note the extra parameters below `player arguments`.
89 |
90 | ::
91 |
92 | usage: browsepy [-h] [--directory PATH] [--initial PATH]
93 | [--removable PATH] [--upload PATH]
94 | [--exclude PATTERN] [--exclude-from PATH]
95 | [--plugin MODULE] [--player-directory-play]
96 | [host] [port]
97 |
98 | description: starts a browsepy web file browser
99 |
100 | positional arguments:
101 | host address to listen (default: 127.0.0.1)
102 | port port to listen (default: 8080)
103 |
104 | optional arguments:
105 | -h, --help show this help message and exit
106 | --directory PATH serving directory (default: current path)
107 | --initial PATH default directory (default: same as --directory)
108 | --removable PATH base directory allowing remove (default: none)
109 | --upload PATH base directory allowing upload (default: none)
110 | --exclude PATTERN exclude paths by pattern (multiple)
111 | --exclude-from PATH exclude paths by pattern file (multiple)
112 | --plugin MODULE load plugin module (multiple)
113 |
114 | player arguments:
115 | --player-directory-play
116 | enable directories as playlist
117 |
--------------------------------------------------------------------------------
/doc/integrations.rst:
--------------------------------------------------------------------------------
1 | .. _integrations:
2 |
3 | Integrations
4 | ============
5 |
6 | Browsepy is a Flask application and python module, so it could be integrated
7 | anywhere python's `WSGI `_ protocol
8 | is supported. Also, browsepy's public API could be easily reused.
9 |
10 | Browsepy app config (available at :attr:`browsepy.app.config`) exposes the
11 | following configuration options.
12 |
13 | * **directory_base**: anything under this directory will be served,
14 | defaults to current path.
15 | * **directory_start**: directory will be served when accessing root URL
16 | * **directory_remove**: file removing will be available under this path,
17 | defaults to **None**.
18 | * **directory_upload**: file upload will be available under this path,
19 | defaults to **None**.
20 | * **directory_tar_buffsize**, directory tar streaming buffer size,
21 | defaults to **262144** and must be multiple of 512.
22 | * **directory_downloadable** whether enable directory download or not,
23 | defaults to **True**.
24 | * **use_binary_multiples** whether use binary units (bi-bytes, like KiB)
25 | instead of common ones (bytes, like KB), defaults to **True**.
26 | * **plugin_modules** list of module names (absolute or relative to
27 | plugin_namespaces) will be loaded.
28 | * **plugin_namespaces** prefixes for module names listed at plugin_modules
29 | where relative plugin_modules are searched.
30 |
31 | Please note: After editing `plugin_modules` value, plugin manager (available
32 | at module :data:`browsepy.plugin_manager` and
33 | :data:`browsepy.app.extensions['plugin_manager']`) should be reloaded using
34 | the :meth:`browsepy.plugin_manager.reload` instance method of :meth:`browsepy.manager.PluginManager.reload` for browsepy's plugin
35 | manager.
36 |
37 | The other way of loading a plugin programmatically is calling
38 | :meth:`browsepy.plugin_manager.load_plugin` instance method of
39 | :meth:`browsepy.manager.PluginManager.load_plugin` for browsepy's plugin
40 | manager.
41 |
42 | .. _integrations-cherrymusic:
43 |
44 | Cherrypy and Cherrymusic
45 | -------------------------
46 |
47 | Startup script running browsepy inside the `cherrypy `_
48 | server provided by `cherrymusic `_.
49 |
50 | .. code-block:: python
51 |
52 | #!/env/bin/python
53 | # -*- coding: UTF-8 -*-
54 |
55 | import os
56 | import sys
57 | import cherrymusicserver
58 | import cherrypy
59 |
60 | from os.path import expandvars, dirname, abspath, join as joinpath
61 | from browsepy import app as browsepy, plugin_manager
62 |
63 |
64 | class HTTPHandler(cherrymusicserver.httphandler.HTTPHandler):
65 | def autoLoginActive(self):
66 | return True
67 |
68 | class Root(object):
69 | pass
70 |
71 | cherrymusicserver.httphandler.HTTPHandler = HTTPHandler
72 |
73 | base_path = abspath(dirname(__file__))
74 | static_path = joinpath(base_path, 'static')
75 | media_path = expandvars('$HOME/media')
76 | download_path = joinpath(media_path, 'downloads')
77 | root_config = {
78 | '/': {
79 | 'tools.staticdir.on': True,
80 | 'tools.staticdir.dir': static_path,
81 | 'tools.staticdir.index': 'index.html',
82 | }
83 | }
84 | cherrymusic_config = {
85 | 'server.rootpath': '/player',
86 | }
87 | browsepy.config.update(
88 | APPLICATION_ROOT = '/browse',
89 | directory_base = media_path,
90 | directory_start = media_path,
91 | directory_remove = media_path,
92 | directory_upload = media_path,
93 | plugin_modules = ['player'],
94 | )
95 | plugin_manager.reload()
96 |
97 | if __name__ == '__main__':
98 | sys.stderr = open(joinpath(base_path, 'stderr.log'), 'w')
99 | sys.stdout = open(joinpath(base_path, 'stdout.log'), 'w')
100 |
101 | with open(joinpath(base_path, 'pidfile.pid'), 'w') as f:
102 | f.write('%d' % os.getpid())
103 |
104 | cherrymusicserver.setup_config(cherrymusic_config)
105 | cherrymusicserver.setup_services()
106 | cherrymusicserver.migrate_databases()
107 | cherrypy.tree.graft(browsepy, '/browse')
108 | cherrypy.tree.mount(Root(), '/', config=root_config)
109 |
110 | try:
111 | cherrymusicserver.start_server(cherrymusic_config)
112 | finally:
113 | print('Exiting...')
114 |
--------------------------------------------------------------------------------
/browsepy/tests/test_plugins.py:
--------------------------------------------------------------------------------
1 |
2 | import unittest
3 | import flask
4 |
5 | import browsepy
6 | import browsepy.manager
7 | import browsepy.tests.utils as test_utils
8 |
9 | from browsepy.plugin.player.tests import * # noqa
10 |
11 |
12 | class FileMock(object):
13 | def __init__(self, **kwargs):
14 | self.__dict__.update(kwargs)
15 |
16 |
17 | class TestMimetypePluginManager(unittest.TestCase):
18 | module = browsepy.manager
19 |
20 | def test_mimetype(self):
21 | manager = self.module.MimetypePluginManager()
22 | self.assertEqual(
23 | manager.get_mimetype('potato'),
24 | 'application/octet-stream'
25 | )
26 | self.assertEqual(
27 | manager.get_mimetype('potato.txt'),
28 | 'text/plain'
29 | )
30 | manager.register_mimetype_function(
31 | lambda x: 'application/xml' if x == 'potato' else None
32 | )
33 | self.assertEqual(
34 | manager.get_mimetype('potato.txt'),
35 | 'text/plain'
36 | )
37 | self.assertEqual(
38 | manager.get_mimetype('potato'),
39 | 'application/xml'
40 | )
41 |
42 |
43 | class TestPlugins(unittest.TestCase):
44 | app_module = browsepy
45 | manager_module = browsepy.manager
46 |
47 | def setUp(self):
48 | self.app = self.app_module.app
49 | self.original_namespaces = self.app.config['plugin_namespaces']
50 | self.plugin_namespace, self.plugin_name = __name__.rsplit('.', 1)
51 | self.app.config['plugin_namespaces'] = (self.plugin_namespace,)
52 | self.manager = self.manager_module.PluginManager(self.app)
53 |
54 | def tearDown(self):
55 | self.app.config['plugin_namespaces'] = self.original_namespaces
56 | self.manager.clear()
57 | test_utils.clear_flask_context()
58 |
59 | def test_manager(self):
60 | self.manager.load_plugin(self.plugin_name)
61 | self.assertTrue(self.manager._plugin_loaded)
62 |
63 | endpoints = sorted(
64 | action.endpoint
65 | for action in self.manager.get_widgets(FileMock(mimetype='a/a'))
66 | )
67 |
68 | self.assertEqual(
69 | endpoints,
70 | sorted(('test_x_x', 'test_a_x', 'test_x_a', 'test_a_a'))
71 | )
72 | self.assertEqual(
73 | self.app.view_functions['test_plugin.root'](),
74 | 'test_plugin_root'
75 | )
76 | self.assertIn('test_plugin', self.app.blueprints)
77 |
78 | self.assertRaises(
79 | self.manager_module.PluginNotFoundError,
80 | self.manager.load_plugin,
81 | 'non_existent_plugin_module'
82 | )
83 |
84 | self.assertRaises(
85 | self.manager_module.InvalidArgumentError,
86 | self.manager.register_widget
87 | )
88 |
89 | def test_namespace_prefix(self):
90 | self.assertTrue(self.manager.import_plugin(self.plugin_name))
91 | self.app.config['plugin_namespaces'] = (
92 | self.plugin_namespace + '.test_',
93 | )
94 | self.assertTrue(self.manager.import_plugin('module'))
95 |
96 |
97 | def register_plugin(manager):
98 | manager._plugin_loaded = True
99 | manager.register_widget(
100 | type='button',
101 | place='entry-actions',
102 | endpoint='test_x_x',
103 | filter=lambda f: True
104 | )
105 | manager.register_widget(
106 | type='button',
107 | place='entry-actions',
108 | endpoint='test_a_x',
109 | filter=lambda f: f.mimetype.startswith('a/')
110 | )
111 | manager.register_widget(
112 | type='button',
113 | place='entry-actions',
114 | endpoint='test_x_a',
115 | filter=lambda f: f.mimetype.endswith('/a')
116 | )
117 | manager.register_widget(
118 | type='button',
119 | place='entry-actions',
120 | endpoint='test_a_a',
121 | filter=lambda f: f.mimetype == 'a/a'
122 | )
123 | manager.register_widget(
124 | type='button',
125 | place='entry-actions',
126 | endpoint='test_b_x',
127 | filter=lambda f: f.mimetype.startswith('b/')
128 | )
129 |
130 | test_plugin_blueprint = flask.Blueprint(
131 | 'test_plugin',
132 | __name__,
133 | url_prefix='/test_plugin_blueprint')
134 | test_plugin_blueprint.add_url_rule(
135 | '/',
136 | endpoint='root',
137 | view_func=lambda: 'test_plugin_root')
138 |
139 | manager.register_blueprint(test_plugin_blueprint)
140 |
--------------------------------------------------------------------------------
/browsepy/templates/browse.html:
--------------------------------------------------------------------------------
1 | {% extends "base.html" %}
2 |
3 | {% macro draw_widget(f, widget) -%}
4 | {%- if widget.type == 'button' -%}
5 | {{ widget.text or '' }}
12 | {%- elif widget.type == 'link' -%}
13 | {{ widget.text or '' }}
16 | {%- elif widget.type == 'script' -%}
17 |
20 | {%- elif widget.type == 'stylesheet' -%}
21 |
25 | {%- elif widget.type == 'upload' -%}
26 |
36 | {%- elif widget.type == 'html' -%}
37 | {{ widget.html|safe }}
38 | {%- endif -%}
39 | {%- endmacro %}
40 |
41 | {% macro draw_widgets(f, place) -%}
42 | {%- for widget in f.widgets -%}
43 | {%- if widget.place == place -%}
44 | {{ draw_widget(f, widget) }}
45 | {%- endif -%}
46 | {%- endfor -%}
47 | {%- endmacro %}
48 |
49 | {% macro th(text, property, type='text', colspan=1) -%}
50 | 1 %} colspan="{{ colspan }}"{% endif %}>
51 | {% set urlpath = file.urlpath or None %}
52 | {% set property_desc = '-{}'.format(property) %}
53 | {% set prop = property_desc if sort_property == property else property %}
54 | {% set active = ' active' if sort_property in (property, property_desc) else '' %}
55 | {% set desc = ' desc' if sort_property == property_desc else '' %}
56 | {{ text }}
59 | |
60 | {%- endmacro %}
61 |
62 | {% block styles %}
63 | {{ super() }}
64 | {{ draw_widgets(file, 'styles') }}
65 | {% endblock %}
66 |
67 | {% block head %}
68 | {{ super() }}
69 | {{ draw_widgets(file, 'head') }}
70 | {% endblock %}
71 |
72 | {% block scripts %}
73 | {{ super() }}
74 | {{ draw_widgets(file, 'scripts') }}
75 | {% endblock %}
76 |
77 | {% block header %}
78 |
79 |
80 | {% for parent in file.ancestors[::-1] %}
81 | -
82 | {{ parent.name }}
85 |
86 | {% endfor %}
87 | {% if file.name %}
88 | - {{ file.name }}
89 | {% endif %}
90 |
91 |
92 | {% endblock %}
93 |
94 | {% block content %}
95 | {% block content_header %}
96 | {{ draw_widgets(file, 'header') }}
97 | {% endblock %}
98 |
99 | {% block content_table %}
100 | {% if file.is_empty %}
101 | No files in directory
102 | {% else %}
103 |
104 |
105 |
106 | {{ th('Name', 'text', 'text', 3) }}
107 | {{ th('Mimetype', 'type') }}
108 | {{ th('Modified', 'modified', 'numeric') }}
109 | {{ th('Size', 'size', 'numeric') }}
110 |
111 |
112 |
113 | {% for f in file.listdir(sortkey=sort_fnc, reverse=sort_reverse) %}
114 |
115 | {% if f.link %}
116 | |
117 | {{ draw_widget(f, f.link) }} |
118 | {% else %}
119 | |
120 | |
121 | {% endif %}
122 | {{ draw_widgets(f, 'entry-actions') }} |
123 | {{ f.type or '' }} |
124 | {{ f.modified or '' }} |
125 | {{ f.size or '' }} |
126 |
127 | {% endfor %}
128 |
129 |
130 | {% endif %}
131 | {% endblock %}
132 |
133 | {% block content_footer %}
134 | {{ draw_widgets(file, 'footer') }}
135 | {% endblock %}
136 | {% endblock %}
137 |
--------------------------------------------------------------------------------
/browsepy/plugin/player/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 |
4 | import os.path
5 |
6 | from flask import Blueprint, render_template
7 | from werkzeug.exceptions import NotFound
8 |
9 | from browsepy import stream_template, get_cookie_browse_sorting, \
10 | browse_sortkey_reverse
11 | from browsepy.file import OutsideDirectoryBase
12 |
13 | from .playable import PlayableFile, PlayableDirectory, \
14 | PlayListFile, detect_playable_mimetype
15 |
16 |
17 | __basedir__ = os.path.dirname(os.path.abspath(__file__))
18 |
19 | player = Blueprint(
20 | 'player',
21 | __name__,
22 | url_prefix='/play',
23 | template_folder=os.path.join(__basedir__, 'templates'),
24 | static_folder=os.path.join(__basedir__, 'static'),
25 | )
26 |
27 |
28 | @player.route('/audio/')
29 | def audio(path):
30 | try:
31 | file = PlayableFile.from_urlpath(path)
32 | if file.is_file:
33 | return render_template('audio.player.html', file=file)
34 | except OutsideDirectoryBase:
35 | pass
36 | return NotFound()
37 |
38 |
39 | @player.route('/list/')
40 | def playlist(path):
41 | try:
42 | file = PlayListFile.from_urlpath(path)
43 | if file.is_file:
44 | return stream_template(
45 | 'audio.player.html',
46 | file=file,
47 | playlist=True
48 | )
49 | except OutsideDirectoryBase:
50 | pass
51 | return NotFound()
52 |
53 |
54 | @player.route("/directory", defaults={"path": ""})
55 | @player.route('/directory/')
56 | def directory(path):
57 | sort_property = get_cookie_browse_sorting(path, 'text')
58 | sort_fnc, sort_reverse = browse_sortkey_reverse(sort_property)
59 | try:
60 | file = PlayableDirectory.from_urlpath(path)
61 | if file.is_directory:
62 | return stream_template(
63 | 'audio.player.html',
64 | file=file,
65 | sort_property=sort_property,
66 | sort_fnc=sort_fnc,
67 | sort_reverse=sort_reverse,
68 | playlist=True
69 | )
70 | except OutsideDirectoryBase:
71 | pass
72 | return NotFound()
73 |
74 |
75 | def register_arguments(manager):
76 | '''
77 | Register arguments using given plugin manager.
78 |
79 | This method is called before `register_plugin`.
80 |
81 | :param manager: plugin manager
82 | :type manager: browsepy.manager.PluginManager
83 | '''
84 |
85 | # Arguments are forwarded to argparse:ArgumentParser.add_argument,
86 | # https://docs.python.org/3.7/library/argparse.html#the-add-argument-method
87 | manager.register_argument(
88 | '--player-directory-play', action='store_true',
89 | help='enable directories as playlist'
90 | )
91 |
92 |
93 | def register_plugin(manager):
94 | '''
95 | Register blueprints and actions using given plugin manager.
96 |
97 | :param manager: plugin manager
98 | :type manager: browsepy.manager.PluginManager
99 | '''
100 | manager.register_blueprint(player)
101 | manager.register_mimetype_function(detect_playable_mimetype)
102 |
103 | # add style tag
104 | manager.register_widget(
105 | place='styles',
106 | type='stylesheet',
107 | endpoint='player.static',
108 | filename='css/browse.css'
109 | )
110 |
111 | # register link actions
112 | manager.register_widget(
113 | place='entry-link',
114 | type='link',
115 | endpoint='player.audio',
116 | filter=PlayableFile.detect
117 | )
118 | manager.register_widget(
119 | place='entry-link',
120 | icon='playlist',
121 | type='link',
122 | endpoint='player.playlist',
123 | filter=PlayListFile.detect
124 | )
125 |
126 | # register action buttons
127 | manager.register_widget(
128 | place='entry-actions',
129 | css='play',
130 | type='button',
131 | endpoint='player.audio',
132 | filter=PlayableFile.detect
133 | )
134 | manager.register_widget(
135 | place='entry-actions',
136 | css='play',
137 | type='button',
138 | endpoint='player.playlist',
139 | filter=PlayListFile.detect
140 | )
141 |
142 | # check argument (see `register_arguments`) before registering
143 | if manager.get_argument('player_directory_play'):
144 | # register header button
145 | manager.register_widget(
146 | place='header',
147 | type='button',
148 | endpoint='player.directory',
149 | text='Play directory',
150 | filter=PlayableDirectory.detect
151 | )
152 |
--------------------------------------------------------------------------------
/browsepy/transform/__init__.py:
--------------------------------------------------------------------------------
1 |
2 |
3 | class StateMachine(object):
4 | '''
5 | Abstract character-driven finite state machine implementation, used to
6 | chop down and transform strings.
7 |
8 | Useful for implementig simple transpilators, compressors and so on.
9 |
10 | Important: when implementing this class, you must set the :attr:`current`
11 | attribute to a key defined in :attr:`jumps` dict.
12 | '''
13 | jumps = {} # finite state machine jumps
14 | start = '' # character which started current state
15 | current = '' # current state (an initial value must be set)
16 | pending = '' # unprocessed remaining data
17 | streaming = False # stream mode toggle
18 |
19 | @property
20 | def nearest(self):
21 | '''
22 | Get the next state jump.
23 |
24 | The next jump is calculated looking at :attr:`current` state
25 | and its possible :attr:`jumps` to find the nearest and bigger
26 | option in :attr:`pending` data.
27 |
28 | If none is found, the returned next state label will be None.
29 |
30 | :returns: tuple with index, substring and next state label
31 | :rtype: tuple
32 | '''
33 | try:
34 | options = self.jumps[self.current]
35 | except KeyError:
36 | raise KeyError(
37 | 'Current state %r not defined in %s.jumps.'
38 | % (self.current, self.__class__)
39 | )
40 | offset = len(self.start)
41 | index = len(self.pending)
42 | if self.streaming:
43 | index -= max(map(len, options))
44 | key = (index, 1)
45 | result = (index, '', None)
46 | for amark, anext in options.items():
47 | asize = len(amark)
48 | aindex = self.pending.find(amark, offset, index + asize)
49 | if aindex > -1:
50 | index = aindex
51 | akey = (aindex, -asize)
52 | if akey < key:
53 | key = akey
54 | result = (aindex, amark, anext)
55 | return result
56 |
57 | def __init__(self, data=''):
58 | '''
59 | :param data: content will be added to pending data
60 | :type data: str
61 | '''
62 | self.pending += data
63 |
64 | def __iter__(self):
65 | '''
66 | Yield over result chunks, consuming :attr:`pending` data.
67 |
68 | On :attr:`streaming` mode, yield only finished states.
69 |
70 | On non :attr:`streaming` mode, yield last state's result chunk
71 | even if unfinished, consuming all pending data.
72 |
73 | :yields: transformation result chunka
74 | :ytype: str
75 | '''
76 | index, mark, next = self.nearest
77 | while next is not None:
78 | data = self.transform(self.pending[:index], mark, next)
79 | self.start = mark
80 | self.current = next
81 | self.pending = self.pending[index:]
82 | if data:
83 | yield data
84 | index, mark, next = self.nearest
85 | if not self.streaming:
86 | data = self.transform(self.pending, mark, next)
87 | self.start = ''
88 | self.pending = ''
89 | if data:
90 | yield data
91 |
92 | def transform(self, data, mark, next):
93 | '''
94 | Apply the appropriate transformation function on current state data,
95 | which is supposed to end at this point.
96 |
97 | It is expected transformation logic makes use of :attr:`start`,
98 | :attr:`current` and :attr:`streaming` instance attributes to
99 | bettee know the state is being left.
100 |
101 | :param data: string to transform (includes start)
102 | :type data: str
103 | :param mark: string producing the new state jump
104 | :type mark: str
105 | :param next: state is about to star, None on finish
106 | :type next: str or None
107 |
108 | :returns: transformed data
109 | :rtype: str
110 | '''
111 | method = getattr(self, 'transform_%s' % self.current, None)
112 | return method(data, mark, next) if method else data
113 |
114 | def feed(self, data=''):
115 | '''
116 | Optionally add pending data, switch into streaming mode, and yield
117 | result chunks.
118 |
119 | :yields: result chunks
120 | :ytype: str
121 | '''
122 | self.streaming = True
123 | self.pending += data
124 | for i in self:
125 | yield i
126 |
127 | def finish(self, data=''):
128 | '''
129 | Optionally add pending data, turn off streaming mode, and yield
130 | result chunks, which implies all pending data will be consumed.
131 |
132 | :yields: result chunks
133 | :ytype: str
134 | '''
135 | self.pending += data
136 | self.streaming = False
137 | for i in self:
138 | yield i
139 |
--------------------------------------------------------------------------------
/browsepy/stream.py:
--------------------------------------------------------------------------------
1 |
2 | import os
3 | import os.path
4 | import tarfile
5 | import functools
6 | import threading
7 |
8 |
9 | class TarFileStream(object):
10 | '''
11 | Tarfile which compresses while reading for streaming.
12 |
13 | Buffsize can be provided, it must be 512 multiple (the tar block size) for
14 | compression.
15 |
16 | Note on corroutines: this class uses threading by default, but
17 | corroutine-based applications can change this behavior overriding the
18 | :attr:`event_class` and :attr:`thread_class` values.
19 | '''
20 | event_class = threading.Event
21 | thread_class = threading.Thread
22 | tarfile_class = tarfile.open
23 |
24 | def __init__(self, path, buffsize=10240, exclude=None):
25 | '''
26 | Internal tarfile object will be created, and compression will start
27 | on a thread until buffer became full with writes becoming locked until
28 | a read occurs.
29 |
30 | :param path: local path of directory whose content will be compressed.
31 | :type path: str
32 | :param buffsize: size of internal buffer on bytes, defaults to 10KiB
33 | :type buffsize: int
34 | :param exclude: path filter function, defaults to None
35 | :type exclude: callable
36 | '''
37 | self.path = path
38 | self.name = os.path.basename(path) + ".tgz"
39 | self.exclude = exclude
40 |
41 | self._finished = 0
42 | self._want = 0
43 | self._data = bytes()
44 | self._add = self.event_class()
45 | self._result = self.event_class()
46 | self._tarfile = self.tarfile_class( # stream write
47 | fileobj=self,
48 | mode="w|gz",
49 | bufsize=buffsize
50 | )
51 | self._th = self.thread_class(target=self.fill)
52 | self._th.start()
53 |
54 | def fill(self):
55 | '''
56 | Writes data on internal tarfile instance, which writes to current
57 | object, using :meth:`write`.
58 |
59 | As this method is blocking, it is used inside a thread.
60 |
61 | This method is called automatically, on a thread, on initialization,
62 | so there is little need to call it manually.
63 | '''
64 | if self.exclude:
65 | exclude = self.exclude
66 | ap = functools.partial(os.path.join, self.path)
67 | self._tarfile.add(
68 | self.path, "",
69 | filter=lambda info: None if exclude(ap(info.name)) else info
70 | )
71 | else:
72 | self._tarfile.add(self.path, "")
73 | self._tarfile.close() # force stream flush
74 | self._finished += 1
75 | if not self._result.is_set():
76 | self._result.set()
77 |
78 | def write(self, data):
79 | '''
80 | Write method used by internal tarfile instance to output data.
81 | This method blocks tarfile execution once internal buffer is full.
82 |
83 | As this method is blocking, it is used inside the same thread of
84 | :meth:`fill`.
85 |
86 | :param data: bytes to write to internal buffer
87 | :type data: bytes
88 | :returns: number of bytes written
89 | :rtype: int
90 | '''
91 | self._add.wait()
92 | self._data += data
93 | if len(self._data) > self._want:
94 | self._add.clear()
95 | self._result.set()
96 | return len(data)
97 |
98 | def read(self, want=0):
99 | '''
100 | Read method, gets data from internal buffer while releasing
101 | :meth:`write` locks when needed.
102 |
103 | The lock usage means it must ran on a different thread than
104 | :meth:`fill`, ie. the main thread, otherwise will deadlock.
105 |
106 | The combination of both write and this method running on different
107 | threads makes tarfile being streamed on-the-fly, with data chunks being
108 | processed and retrieved on demand.
109 |
110 | :param want: number bytes to read, defaults to 0 (all available)
111 | :type want: int
112 | :returns: tarfile data as bytes
113 | :rtype: bytes
114 | '''
115 | if self._finished:
116 | if self._finished == 1:
117 | self._finished += 1
118 | return ""
119 | return EOFError("EOF reached")
120 |
121 | # Thread communication
122 | self._want = want
123 | self._add.set()
124 | self._result.wait()
125 | self._result.clear()
126 |
127 | if want:
128 | data = self._data[:want]
129 | self._data = self._data[want:]
130 | else:
131 | data = self._data
132 | self._data = bytes()
133 | return data
134 |
135 | def __iter__(self):
136 | '''
137 | Iterate through tarfile result chunks.
138 |
139 | Similarly to :meth:`read`, this methos must ran on a different thread
140 | than :meth:`write` calls.
141 |
142 | :yields: data chunks as taken from :meth:`read`.
143 | :ytype: bytes
144 | '''
145 | data = self.read()
146 | while data:
147 | yield data
148 | data = self.read()
149 |
--------------------------------------------------------------------------------
/browsepy/tests/test_main.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import os
3 | import os.path
4 | import tempfile
5 | import shutil
6 | import browsepy.__main__
7 |
8 |
9 | class TestMain(unittest.TestCase):
10 | module = browsepy.__main__
11 |
12 | def setUp(self):
13 | self.app = browsepy.app
14 | self.parser = self.module.ArgParse(sep=os.sep)
15 | self.base = tempfile.mkdtemp()
16 | self.exclude_file = os.path.join(self.base, '.ignore')
17 | with open(self.exclude_file, 'w') as f:
18 | f.write('.ignore\n')
19 |
20 | def tearDown(self):
21 | shutil.rmtree(self.base)
22 |
23 | def test_defaults(self):
24 | result = self.parser.parse_args([])
25 | self.assertEqual(result.host, '127.0.0.1')
26 | self.assertEqual(result.port, 8080)
27 | self.assertEqual(result.directory, os.getcwd())
28 | self.assertEqual(result.initial, None)
29 | self.assertEqual(result.removable, None)
30 | self.assertEqual(result.upload, None)
31 | self.assertListEqual(result.exclude, [])
32 | self.assertListEqual(result.exclude_from, [])
33 | self.assertEqual(result.plugin, [])
34 |
35 | def test_params(self):
36 | plugins = ['plugin_1', 'plugin_2', 'namespace.plugin_3']
37 | result = self.parser.parse_args([
38 | '127.1.1.1',
39 | '5000',
40 | '--directory=%s' % self.base,
41 | '--initial=%s' % self.base,
42 | '--removable=%s' % self.base,
43 | '--upload=%s' % self.base,
44 | '--exclude=a',
45 | '--exclude-from=%s' % self.exclude_file,
46 | ] + [
47 | '--plugin=%s' % plugin
48 | for plugin in plugins
49 | ])
50 | self.assertEqual(result.host, '127.1.1.1')
51 | self.assertEqual(result.port, 5000)
52 | self.assertEqual(result.directory, self.base)
53 | self.assertEqual(result.initial, self.base)
54 | self.assertEqual(result.removable, self.base)
55 | self.assertEqual(result.upload, self.base)
56 | self.assertListEqual(result.exclude, ['a'])
57 | self.assertListEqual(result.exclude_from, [self.exclude_file])
58 | self.assertEqual(result.plugin, plugins)
59 |
60 | result = self.parser.parse_args([
61 | '--directory', self.base,
62 | '--plugin', ','.join(plugins),
63 | '--exclude', '/.*'
64 | ])
65 | self.assertEqual(result.directory, self.base)
66 | self.assertEqual(result.plugin, plugins)
67 | self.assertListEqual(result.exclude, ['/.*'])
68 |
69 | result = self.parser.parse_args([
70 | '--directory=%s' % self.base,
71 | '--initial='
72 | ])
73 | self.assertEqual(result.host, '127.0.0.1')
74 | self.assertEqual(result.port, 8080)
75 | self.assertEqual(result.directory, self.base)
76 | self.assertIsNone(result.initial)
77 | self.assertIsNone(result.removable)
78 | self.assertIsNone(result.upload)
79 | self.assertListEqual(result.exclude, [])
80 | self.assertListEqual(result.exclude_from, [])
81 | self.assertListEqual(result.plugin, [])
82 |
83 | self.assertRaises(
84 | SystemExit,
85 | self.parser.parse_args,
86 | ['--directory=%s' % __file__]
87 | )
88 |
89 | self.assertRaises(
90 | SystemExit,
91 | self.parser.parse_args,
92 | ['--exclude-from=non-existing']
93 | )
94 |
95 | def test_exclude(self):
96 | result = self.parser.parse_args([
97 | '--exclude', '/.*',
98 | '--exclude-from', self.exclude_file,
99 | ])
100 | extra = self.module.collect_exclude_patterns(result.exclude_from)
101 | self.assertListEqual(extra, ['.ignore'])
102 | match = self.module.create_exclude_fnc(
103 | result.exclude + extra, '/b', sep='/')
104 | self.assertTrue(match('/b/.a'))
105 | self.assertTrue(match('/b/.a/b'))
106 | self.assertFalse(match('/b/a/.a'))
107 | self.assertTrue(match('/b/a/.ignore'))
108 |
109 | match = self.module.create_exclude_fnc(
110 | result.exclude + extra, 'C:\\b', sep='\\')
111 | self.assertTrue(match('C:\\b\\.a'))
112 | self.assertTrue(match('C:\\b\\.a\\b'))
113 | self.assertFalse(match('C:\\b\\a\\.a'))
114 | self.assertTrue(match('C:\\b\\a\\.ignore'))
115 |
116 | def test_main(self):
117 | params = {}
118 | self.module.main(
119 | argv=[],
120 | run_fnc=lambda app, **kwargs: params.update(kwargs)
121 | )
122 |
123 | defaults = {
124 | 'host': '127.0.0.1',
125 | 'port': 8080,
126 | 'debug': False,
127 | 'threaded': True
128 | }
129 | params_subset = {k: v for k, v in params.items() if k in defaults}
130 | self.assertEqual(defaults, params_subset)
131 |
132 | def test_filter_union(self):
133 | fu = self.module.filter_union
134 | self.assertIsNone(fu())
135 | self.assertIsNone(fu(None))
136 | self.assertIsNone(fu(None, None))
137 |
138 | def fnc1(path):
139 | return False
140 |
141 | self.assertEqual(fu(fnc1), fnc1)
142 |
143 | def fnc2(path):
144 | return True
145 |
146 | self.assertTrue(fu(fnc1, fnc2)('a'))
147 |
--------------------------------------------------------------------------------
/browsepy/tests/test_compat.py:
--------------------------------------------------------------------------------
1 | import unittest
2 | import re
3 |
4 | from werkzeug.utils import cached_property
5 |
6 | import browsepy.compat
7 |
8 |
9 | class TestCompat(unittest.TestCase):
10 | module = browsepy.compat
11 |
12 | def _warn(self, message, category=None, stacklevel=None):
13 | if not hasattr(self, '_warnings'):
14 | self._warnings = []
15 | self._warnings.append({
16 | 'message': message,
17 | 'category': category,
18 | 'stacklevel': stacklevel
19 | })
20 |
21 | @cached_property
22 | def assertWarnsRegex(self):
23 | supa = super(TestCompat, self)
24 | if hasattr(supa, 'assertWarnsRegex'):
25 | return supa.assertWarnsRegex
26 | return self.customAssertWarnsRegex
27 |
28 | def customAssertWarnsRegex(self, expected_warning, expected_regex, fnc,
29 | *args, **kwargs):
30 |
31 | import warnings
32 | old_warn = warnings.warn
33 | warnings.warn = self._warn
34 | try:
35 | fnc(*args, **kwargs)
36 | finally:
37 | warnings.warn = old_warn
38 | warnings = ()
39 | if hasattr(self, '_warnings'):
40 | warnings = self._warnings
41 | del self._warnings
42 | regex = re.compile(expected_regex)
43 | self.assertTrue(any(
44 | warn['category'] == expected_warning and
45 | regex.match(warn['message'])
46 | for warn in warnings
47 | ))
48 |
49 | def test_which(self):
50 | self.assertTrue(self.module.which('python'))
51 | self.assertIsNone(self.module.which('lets-put-a-wrong-executable'))
52 |
53 | def test_fsdecode(self):
54 | path = b'/a/\xc3\xb1'
55 | self.assertEqual(
56 | self.module.fsdecode(path, os_name='posix', fs_encoding='utf-8'),
57 | path.decode('utf-8')
58 | )
59 | path = b'/a/\xf1'
60 | self.assertEqual(
61 | self.module.fsdecode(path, os_name='nt', fs_encoding='latin-1'),
62 | path.decode('latin-1')
63 | )
64 | path = b'/a/\xf1'
65 | self.assertRaises(
66 | UnicodeDecodeError,
67 | self.module.fsdecode,
68 | path,
69 | fs_encoding='utf-8',
70 | errors='strict'
71 | )
72 |
73 | def test_fsencode(self):
74 | path = b'/a/\xc3\xb1'
75 | self.assertEqual(
76 | self.module.fsencode(
77 | path.decode('utf-8'),
78 | fs_encoding='utf-8'
79 | ),
80 | path
81 | )
82 | path = b'/a/\xf1'
83 | self.assertEqual(
84 | self.module.fsencode(
85 | path.decode('latin-1'),
86 | fs_encoding='latin-1'
87 | ),
88 | path
89 | )
90 | path = b'/a/\xf1'
91 | self.assertEqual(
92 | self.module.fsencode(path, fs_encoding='utf-8'),
93 | path
94 | )
95 |
96 | def test_pathconf(self):
97 | kwargs = {
98 | 'os_name': 'posix',
99 | 'pathconf_fnc': lambda x, k: 500,
100 | 'pathconf_names': ('PC_PATH_MAX', 'PC_NAME_MAX')
101 | }
102 | pcfg = self.module.pathconf('/', **kwargs)
103 | self.assertEqual(pcfg['PC_PATH_MAX'], 500)
104 | self.assertEqual(pcfg['PC_NAME_MAX'], 500)
105 | kwargs.update(
106 | pathconf_fnc=None,
107 | )
108 | pcfg = self.module.pathconf('/', **kwargs)
109 | self.assertEqual(pcfg['PC_PATH_MAX'], 255)
110 | self.assertEqual(pcfg['PC_NAME_MAX'], 254)
111 | kwargs.update(
112 | os_name='nt',
113 | isdir_fnc=lambda x: False
114 | )
115 | pcfg = self.module.pathconf('c:\\a', **kwargs)
116 | self.assertEqual(pcfg['PC_PATH_MAX'], 259)
117 | self.assertEqual(pcfg['PC_NAME_MAX'], 255)
118 | kwargs.update(
119 | isdir_fnc=lambda x: True
120 | )
121 | pcfg = self.module.pathconf('c:\\a', **kwargs)
122 | self.assertEqual(pcfg['PC_PATH_MAX'], 246)
123 | self.assertEqual(pcfg['PC_NAME_MAX'], 242)
124 |
125 | def test_getcwd(self):
126 | self.assertIsInstance(self.module.getcwd(), self.module.unicode)
127 | self.assertIsInstance(
128 | self.module.getcwd(
129 | fs_encoding='latin-1',
130 | cwd_fnc=lambda: b'\xf1'
131 | ),
132 | self.module.unicode
133 | )
134 | self.assertIsInstance(
135 | self.module.getcwd(
136 | fs_encoding='utf-8',
137 | cwd_fnc=lambda: b'\xc3\xb1'
138 | ),
139 | self.module.unicode
140 | )
141 |
142 | def test_path(self):
143 | parse = self.module.pathparse
144 | self.assertListEqual(
145 | list(parse('"/":/escaped\\:path:asdf/', sep=':', os_sep='/')),
146 | ['/', '/escaped:path', 'asdf']
147 | )
148 |
149 | def test_getdebug(self):
150 | enabled = ('TRUE', 'true', 'True', '1', 'yes', 'enabled')
151 | for case in enabled:
152 | self.assertTrue(self.module.getdebug({'DEBUG': case}))
153 | disabled = ('FALSE', 'false', 'False', '', '0', 'no', 'disabled')
154 | for case in disabled:
155 | self.assertFalse(self.module.getdebug({'DEBUG': case}))
156 |
157 | def test_deprecated(self):
158 | environ = {'DEBUG': 'true'}
159 | self.assertWarnsRegex(
160 | DeprecationWarning,
161 | 'DEPRECATED',
162 | self.module.deprecated('DEPRECATED', environ)(lambda: None)
163 | )
164 |
--------------------------------------------------------------------------------
/browsepy/__main__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: utf-8 -*-
3 |
4 | import re
5 | import sys
6 | import os
7 | import os.path
8 | import argparse
9 | import warnings
10 |
11 | import flask
12 |
13 | from . import app
14 | from . import __meta__ as meta
15 | from .compat import PY_LEGACY, getdebug, get_terminal_size
16 | from .transform.glob import translate
17 |
18 |
19 | class HelpFormatter(argparse.RawTextHelpFormatter):
20 | def __init__(self, prog, indent_increment=2, max_help_position=24,
21 | width=None):
22 | if width is None:
23 | try:
24 | width = get_terminal_size().columns - 2
25 | except ValueError: # https://bugs.python.org/issue24966
26 | pass
27 | super(HelpFormatter, self).__init__(
28 | prog, indent_increment, max_help_position, width)
29 |
30 |
31 | class PluginAction(argparse.Action):
32 | def __call__(self, parser, namespace, value, option_string=None):
33 | warned = '%s_warning' % self.dest
34 | if ',' in value and not getattr(namespace, warned, False):
35 | setattr(namespace, warned, True)
36 | warnings.warn(
37 | 'Comma-separated --plugin value is deprecated, '
38 | 'use multiple --plugin options instead.'
39 | )
40 | values = value.split(',')
41 | prev = getattr(namespace, self.dest, None)
42 | if isinstance(prev, list):
43 | values = prev + [p for p in values if p not in prev]
44 | setattr(namespace, self.dest, values)
45 |
46 |
47 | class ArgParse(argparse.ArgumentParser):
48 | default_directory = app.config['directory_base']
49 | default_initial = (
50 | None
51 | if app.config['directory_start'] == app.config['directory_base'] else
52 | app.config['directory_start']
53 | )
54 | default_removable = app.config['directory_remove']
55 | default_upload = app.config['directory_upload']
56 |
57 | default_host = os.getenv('BROWSEPY_HOST', '127.0.0.1')
58 | default_port = os.getenv('BROWSEPY_PORT', '8080')
59 | plugin_action_class = PluginAction
60 |
61 | defaults = {
62 | 'prog': meta.app,
63 | 'formatter_class': HelpFormatter,
64 | 'description': 'description: starts a %s web file browser' % meta.app
65 | }
66 |
67 | def __init__(self, sep=os.sep):
68 | super(ArgParse, self).__init__(**self.defaults)
69 | self.add_argument(
70 | 'host', nargs='?',
71 | default=self.default_host,
72 | help='address to listen (default: %(default)s)')
73 | self.add_argument(
74 | 'port', nargs='?', type=int,
75 | default=self.default_port,
76 | help='port to listen (default: %(default)s)')
77 | self.add_argument(
78 | '--directory', metavar='PATH', type=self._directory,
79 | default=self.default_directory,
80 | help='serving directory (default: %(default)s)')
81 | self.add_argument(
82 | '--initial', metavar='PATH',
83 | type=lambda x: self._directory(x) if x else None,
84 | default=self.default_initial,
85 | help='default directory (default: same as --directory)')
86 | self.add_argument(
87 | '--removable', metavar='PATH', type=self._directory,
88 | default=self.default_removable,
89 | help='base directory allowing remove (default: %(default)s)')
90 | self.add_argument(
91 | '--upload', metavar='PATH', type=self._directory,
92 | default=self.default_upload,
93 | help='base directory allowing upload (default: %(default)s)')
94 | self.add_argument(
95 | '--exclude', metavar='PATTERN',
96 | action='append',
97 | default=[],
98 | help='exclude paths by pattern (multiple)')
99 | self.add_argument(
100 | '--exclude-from', metavar='PATH', type=self._file,
101 | action='append',
102 | default=[],
103 | help='exclude paths by pattern file (multiple)')
104 | self.add_argument(
105 | '--plugin', metavar='MODULE',
106 | action=self.plugin_action_class,
107 | default=[],
108 | help='load plugin module (multiple)')
109 | self.add_argument(
110 | '--debug', action='store_true',
111 | help=argparse.SUPPRESS)
112 |
113 | def _path(self, arg):
114 | if PY_LEGACY and hasattr(sys.stdin, 'encoding'):
115 | encoding = sys.stdin.encoding or sys.getdefaultencoding()
116 | arg = arg.decode(encoding)
117 | return os.path.abspath(arg)
118 |
119 | def _file(self, arg):
120 | path = self._path(arg)
121 | if os.path.isfile(path):
122 | return path
123 | self.error('%s is not a valid file' % arg)
124 |
125 | def _directory(self, arg):
126 | path = self._path(arg)
127 | if os.path.isdir(path):
128 | return path
129 | self.error('%s is not a valid directory' % arg)
130 |
131 |
132 | def create_exclude_fnc(patterns, base, sep=os.sep):
133 | if patterns:
134 | regex = '|'.join(translate(pattern, sep, base) for pattern in patterns)
135 | return re.compile(regex).search
136 | return None
137 |
138 |
139 | def collect_exclude_patterns(paths):
140 | patterns = []
141 | for path in paths:
142 | with open(path, 'r') as f:
143 | for line in f:
144 | line = line.split('#')[0].strip()
145 | if line:
146 | patterns.append(line)
147 | return patterns
148 |
149 |
150 | def list_union(*lists):
151 | lst = [i for l in lists for i in l]
152 | return sorted(frozenset(lst), key=lst.index)
153 |
154 |
155 | def filter_union(*functions):
156 | filtered = [fnc for fnc in functions if fnc]
157 | if filtered:
158 | if len(filtered) == 1:
159 | return filtered[0]
160 | return lambda data: any(fnc(data) for fnc in filtered)
161 | return None
162 |
163 |
164 | def main(argv=sys.argv[1:], app=app, parser=ArgParse, run_fnc=flask.Flask.run):
165 | plugin_manager = app.extensions['plugin_manager']
166 | args = plugin_manager.load_arguments(argv, parser())
167 | patterns = args.exclude + collect_exclude_patterns(args.exclude_from)
168 | if args.debug:
169 | os.environ['DEBUG'] = 'true'
170 | app.config.update(
171 | directory_base=args.directory,
172 | directory_start=args.initial or args.directory,
173 | directory_remove=args.removable,
174 | directory_upload=args.upload,
175 | plugin_modules=list_union(
176 | app.config['plugin_modules'],
177 | args.plugin,
178 | ),
179 | exclude_fnc=filter_union(
180 | app.config['exclude_fnc'],
181 | create_exclude_fnc(patterns, args.directory),
182 | ),
183 | )
184 | plugin_manager.reload()
185 | run_fnc(
186 | app,
187 | host=args.host,
188 | port=args.port,
189 | debug=getdebug(),
190 | use_reloader=False,
191 | threaded=True
192 | )
193 |
194 |
195 | if __name__ == '__main__': # pragma: no cover
196 | main()
197 |
--------------------------------------------------------------------------------
/browsepy/transform/glob.py:
--------------------------------------------------------------------------------
1 |
2 | import os
3 | import warnings
4 |
5 | from unicategories import categories as unicat, RangeGroup as ranges
6 |
7 | from ..compat import re_escape, chr
8 | from . import StateMachine
9 |
10 |
11 | class GlobTransform(StateMachine):
12 | jumps = {
13 | 'start': {
14 | '': 'text',
15 | '/': 'sep',
16 | },
17 | 'text': {
18 | '*': 'wildcard',
19 | '**': 'wildcard',
20 | '?': 'wildcard',
21 | '[': 'range',
22 | '[!': 'range',
23 | '[]': 'range',
24 | '{': 'group',
25 | ',': 'group',
26 | '}': 'group',
27 | '\\': 'literal',
28 | '/': 'sep',
29 | },
30 | 'sep': {
31 | '': 'text',
32 | },
33 | 'literal': {
34 | c: 'text' for c in ('\\', '*', '?', '[', '{', '}', ',', '/', '')
35 | },
36 | 'wildcard': {
37 | '': 'text',
38 | },
39 | 'range': {
40 | '/': 'range_sep',
41 | ']': 'range_close',
42 | '[.': 'posix_collating_symbol',
43 | '[:': 'posix_character_class',
44 | '[=': 'posix_equivalence_class',
45 | },
46 | 'range_sep': {
47 | '': 'range',
48 | },
49 | 'range_ignore': {
50 | '': 'range',
51 | },
52 | 'range_close': {
53 | '': 'text',
54 | },
55 | 'posix_collating_symbol': {
56 | '.]': 'range_ignore',
57 | },
58 | 'posix_character_class': {
59 | ':]': 'range_ignore',
60 | },
61 | 'posix_equivalence_class': {
62 | '=]': 'range_ignore',
63 | },
64 | 'group': {
65 | '': 'text',
66 | },
67 | }
68 | character_classes = {
69 | 'alnum': (
70 | # [\p{L}\p{Nl}\p{Nd}]
71 | unicat['L'] + unicat['Nl'] + unicat['Nd']
72 | ),
73 | 'alpha': (
74 | # \p{L}\p{Nl}
75 | unicat['L'] + unicat['Nl']
76 | ),
77 | 'ascii': (
78 | # [\x00-\x7F]
79 | ranges(((0, 0x80),))
80 | ),
81 | 'blank': (
82 | # [\p{Zs}\t]
83 | unicat['Zs'] + ranges(((9, 10),))
84 | ),
85 | 'cntrl': (
86 | # \p{Cc}
87 | unicat['Cc']
88 | ),
89 | 'digit': (
90 | # \p{Nd}
91 | unicat['Nd']
92 | ),
93 | 'graph': (
94 | # [^\p{Z}\p{C}]
95 | unicat['M'] + unicat['L'] + unicat['N'] + unicat['P'] + unicat['S']
96 | ),
97 | 'lower': (
98 | # \p{Ll}
99 | unicat['Ll']
100 | ),
101 | 'print': (
102 | # \P{C}
103 | unicat['C']
104 | ),
105 | 'punct': (
106 | # \p{P}
107 | unicat['P']
108 | ),
109 | 'space': (
110 | # [\p{Z}\t\n\v\f\r]
111 | unicat['Z'] + ranges(((9, 14),))
112 | ),
113 | 'upper': (
114 | # \p{Lu}
115 | unicat['Lu']
116 | ),
117 | 'word': (
118 | # [\p{L}\p{Nl}\p{Nd}\p{Pc}]
119 | unicat['L'] + unicat['Nl'] + unicat['Nd'] + unicat['Pc']
120 | ),
121 | 'xdigit': (
122 | # [0-9A-Fa-f]
123 | ranges(((48, 58), (65, 71), (97, 103)))
124 | ),
125 | }
126 | current = 'start'
127 | deferred = False
128 |
129 | def __init__(self, data, sep=os.sep, base=None):
130 | self.sep = sep
131 | self.base = base or ''
132 | self.deferred_data = []
133 | self.deep = 0
134 | super(GlobTransform, self).__init__(data)
135 |
136 | def transform(self, data, mark, next):
137 | data = super(GlobTransform, self).transform(data, mark, next)
138 | if self.deferred:
139 | self.deferred_data.append(data)
140 | data = ''
141 | elif self.deferred_data:
142 | data = ''.join(self.deferred_data) + data
143 | self.deferred_data[:] = ()
144 | return data
145 |
146 | def transform_posix_collating_symbol(self, data, mark, next):
147 | warnings.warn(
148 | 'Posix collating symbols (like %s%s) are not supported.'
149 | % (data, mark))
150 | return None
151 |
152 | def transform_posix_character_class(self, data, mark, next):
153 | name = data[len(self.start):]
154 | if name not in self.character_classes:
155 | warnings.warn(
156 | 'Posix character class %s is not supported.'
157 | % name)
158 | return None
159 | return ''.join(
160 | chr(start)
161 | if 1 == end - start else
162 | '%s-%s' % (chr(start), chr(end - 1))
163 | for start, end in self.character_classes[name]
164 | )
165 |
166 | def transform_posix_equivalence_class(self, data, mark, next):
167 | warnings.warn(
168 | 'Posix equivalence class expresions (like %s%s) are not supported.'
169 | % (data, mark))
170 | return None
171 |
172 | def transform_wildcard(self, data, mark, next):
173 | if self.start == '**':
174 | return '.*'
175 | if self.start == '*':
176 | return '[^%s]*' % re_escape(self.sep)
177 | return '[^%s]' % re_escape(self.sep)
178 |
179 | def transform_text(self, data, mark, next):
180 | if next is None:
181 | return '%s(%s|$)' % (re_escape(data), re_escape(self.sep))
182 | return re_escape(data)
183 |
184 | def transform_sep(self, data, mark, next):
185 | return re_escape(self.sep)
186 |
187 | def transform_literal(self, data, mark, next):
188 | return data[len(self.start):]
189 |
190 | def transform_range(self, data, mark, next):
191 | self.deferred = True
192 | if self.start == '[!':
193 | return '[^%s' % data[2:]
194 | if self.start == '[]':
195 | return '[\\]%s' % data[2:]
196 | return data
197 |
198 | def transform_range_sep(self, data, mark, next):
199 | return re_escape(self.sep)
200 |
201 | def transform_range_close(self, data, mark, next):
202 | self.deferred = False
203 | if None in self.deferred_data:
204 | self.deferred_data[:] = ()
205 | return '.'
206 | return data
207 |
208 | def transform_range_ignore(self, data, mark, next):
209 | return ''
210 |
211 | def transform_group(self, data, mark, next):
212 | if self.start == '{':
213 | self.deep += 1
214 | return '('
215 | if self.start == ',' and self.deep:
216 | return '|'
217 | if self.start == '}' and self.deep:
218 | self.deep -= 1
219 | return ')'
220 | return data
221 |
222 | def transform_start(self, data, mark, next):
223 | if mark == '/':
224 | return '^%s' % re_escape(self.base)
225 | return re_escape(self.sep)
226 |
227 |
228 | def translate(data, sep=os.sep, base=None):
229 | self = GlobTransform(data, sep, base)
230 | return ''.join(self)
231 |
--------------------------------------------------------------------------------
/browsepy/static/base.css:
--------------------------------------------------------------------------------
1 | @font-face {
2 | font-family: 'icomoon';
3 | src: url('fonts/icomoon.eot?c0qaf0');
4 | src: url('fonts/icomoon.eot?c0qaf0#iefix') format('embedded-opentype'), url('fonts/icomoon.ttf?c0qaf0') format('truetype'), url('fonts/icomoon.woff?c0qaf0') format('woff'), url('fonts/icomoon.svg?c0qaf0#icomoon') format('svg');
5 | font-weight: normal;
6 | font-style: normal;
7 | }
8 |
9 | .button:after,
10 | .icon:after,
11 | .sorting:after,
12 | .sorting:before {
13 | font-family: 'icomoon';
14 | speak: none;
15 | font-style: normal;
16 | font-weight: normal;
17 | font-variant: normal;
18 | text-transform: none;
19 | -webkit-font-smoothing: antialiased;
20 | -moz-osx-font-smoothing: grayscale;
21 | }
22 |
23 | html {
24 | min-height: 100%;
25 | position: relative;
26 | }
27 |
28 | body {
29 | font-family: sans;
30 | font-size: 0.75em;
31 | padding: 3.5em 1em 3.5em;
32 | margin: 0;
33 | min-height: 100%;
34 | }
35 |
36 | a,
37 | label,
38 | input[type=submit],
39 | input[type=file] {
40 | cursor: pointer;
41 | }
42 |
43 | a {
44 | color: #666;
45 | text-decoration: none;
46 | }
47 |
48 | a:hover {
49 | color: black;
50 | }
51 |
52 | a:active {
53 | color: #666;
54 | }
55 |
56 | ul.main-menu {
57 | padding: 0;
58 | margin: 0;
59 | list-style: none;
60 | display: block;
61 | }
62 |
63 | ul.main-menu > li {
64 | font-size: 1.25em;
65 | }
66 |
67 | form.upload {
68 | border: 1px solid #333;
69 | display: inline-block;
70 | margin: 0 0 1em;
71 | padding: 0;
72 | }
73 |
74 | form.upload:after {
75 | content: '';
76 | clear: both;
77 | }
78 |
79 | form.upload h2 {
80 | display: inline-block;
81 | padding: 1em 0;
82 | margin: 0;
83 | }
84 |
85 | form.upload label {
86 | display: inline-block;
87 | background: #333;
88 | color: white;
89 | padding: 0 1.5em;
90 | }
91 |
92 | form.upload label input {
93 | margin-right: 0;
94 | }
95 |
96 | form.upload input {
97 | margin: 0.5em 1em;
98 | }
99 |
100 | form.remove {
101 | display: inline-block;
102 | }
103 |
104 | form.remove input[type=submit] {
105 | min-width: 10em;
106 | }
107 |
108 | html.autosubmit-support form.autosubmit{
109 | border: 0;
110 | background: transparent;
111 | }
112 |
113 | html.autosubmit-support form.autosubmit input{
114 | position: fixed;
115 | top: -500px;
116 | }
117 |
118 | html.autosubmit-support form.autosubmit h2{
119 | font-size: 1em;
120 | font-weight: normal;
121 | padding: 0;
122 | }
123 |
124 | table.browser {
125 | display: block;
126 | table-layout: fixed;
127 | border-collapse: collapse;
128 | overflow-x: auto;
129 | }
130 |
131 | table.browser tr:nth-child(2n) {
132 | background-color: #efefef;
133 | }
134 |
135 | table.browser th,
136 | table.browser td {
137 | margin: 0;
138 | text-align: center;
139 | vertical-align: middle;
140 | white-space: nowrap;
141 | padding: 5px;
142 | }
143 |
144 | table.browser th {
145 | padding-bottom: 10px;
146 | border-bottom: 1px solid black;
147 | }
148 |
149 | table.browser td:first-child {
150 | min-width: 1em;
151 | }
152 |
153 | table.browser td:nth-child(2) {
154 | text-align: left;
155 | width: 100%;
156 | }
157 |
158 | table.browser td:nth-child(3) {
159 | text-align: left;
160 | }
161 |
162 | h1 {
163 | line-height: 1.15em;
164 | font-size: 3em;
165 | white-space: normal;
166 | word-wrap: normal;
167 | margin: 0;
168 | padding: 0 0 0.3em;
169 | display: block;
170 | color: black;
171 | text-shadow: 1px 1px 0 white, -1px -1px 0 white, 1px -1px 0 white, -1px 1px 0 white, 1px 0 0 white, -1px 0 0 white, 0 -1px 0 white, 0 1px 0 white, 2px 2px 0 black, -2px -2px 0 black, 2px -2px 0 black, -2px 2px 0 black, 2px 0 0 black, -2px 0 0 black, 0 2px 0 black, 0 -2px 0 black, 2px 1px 0 black, -2px 1px 0 black, 1px 2px 0 black, 1px -2px 0 black, 2px -1px 0 black, -2px -1px 0 black, -1px 2px 0 black, -1px -2px 0 black;
172 | }
173 |
174 | ol.path,
175 | ol.path li {
176 | display: inline;
177 | margin: 0;
178 | padding: 0;
179 | }
180 |
181 | ol.path li *:before {
182 | content: ' ';
183 | }
184 |
185 | ol.path li *:after{
186 | content: ' /';
187 | }
188 |
189 | ol.path li:first-child *:before,
190 | ol.path li:last-child *:after,
191 | ol.path li *.root:after {
192 | content: '';
193 | }
194 |
195 | ul.navbar,
196 | ul.footer {
197 | position: absolute;
198 | left: 0;
199 | right: 0;
200 | width: auto;
201 | margin: 0;
202 | padding: 0 1em;
203 | display: block;
204 | line-height: 2.5em;
205 | background: #333;
206 | color: white;
207 | }
208 |
209 | ul.navbar a,
210 | ul.footer a {
211 | color: white;
212 | font-weight: bold;
213 | }
214 |
215 | ul.navbar {
216 | top: 0;
217 | }
218 |
219 | ul.footer {
220 | bottom: 0;
221 | }
222 |
223 | ul.navbar a:hover {
224 | color: gray;
225 | }
226 |
227 | ul.navbar > li,
228 | ul.footer > li {
229 | list-style: none;
230 | display: inline;
231 | margin-right: 10px;
232 | width: 100%;
233 | }
234 |
235 | .inode.icon:after,
236 | .dir.icon:after{
237 | content: "\e90a";
238 | }
239 |
240 | .text.icon:after{
241 | content: "\e901";
242 | }
243 |
244 | .audio.icon:after{
245 | content: "\e906";
246 | }
247 |
248 | .video.icon:after{
249 | content: "\e908";
250 | }
251 |
252 | .image.icon:after{
253 | content: "\e905";
254 | }
255 |
256 | .multipart.icon:after,
257 | .model.icon:after,
258 | .message.icon:after,
259 | .example.icon:after,
260 | .application.icon:after{
261 | content: "\e900";
262 | }
263 |
264 | a.sorting {
265 | display: block;
266 | padding: 0 1.4em 0 0;
267 | }
268 |
269 | a.sorting:after,
270 | a.sorting:before {
271 | float: right;
272 | margin: 0.2em -1.3em -0.2em;
273 | }
274 |
275 | a.sorting:hover:after,
276 | a.sorting.active:after {
277 | content: "\ea4c";
278 | }
279 |
280 | a.sorting.desc:hover:after,
281 | a.sorting.desc.active:after {
282 | content: "\ea4d";
283 | }
284 |
285 | a.sorting.text:hover:after,
286 | a.sorting.text.active:after {
287 | content: "\ea48";
288 | }
289 |
290 | a.sorting.text.desc:hover:after,
291 | a.sorting.text.desc.active:after {
292 | content: "\ea49";
293 | }
294 |
295 | a.sorting.numeric:hover:after,
296 | a.sorting.numeric.active:after {
297 | content: "\ea4a";
298 | }
299 |
300 | a.sorting.numeric.desc:hover:after,
301 | a.sorting.numeric.desc.active:after {
302 | content: "\ea4b";
303 | }
304 |
305 | a.button,
306 | html.autosubmit-support form.autosubmit label {
307 | color: white;
308 | background: #333;
309 | display: inline-block;
310 | vertical-align: middle;
311 | line-height: 1.5em;
312 | text-align: center;
313 | border-radius: 0.25em;
314 | border: 1px solid gray;
315 | box-shadow: 1px 1px 0 black, -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black;
316 | text-shadow: 1px 1px 0 black, -1px -1px 0 black, 1px -1px 0 black, -1px 1px 0 black, 1px 0 0 black, -1px 0 0 black, 0 -1px 0 black, 0 1px 0 black;
317 | }
318 |
319 | a.button:active,
320 | html.autosubmit-support form.autosubmit label:active{
321 | border: 1px solid black;
322 | }
323 |
324 | a.button:hover,
325 | html.autosubmit-support form.autosubmit label:hover {
326 | color: white;
327 | background: black;
328 | }
329 |
330 | a.button,
331 | html.autosubmit-support form.autosubmit{
332 | margin-left: 3px;
333 | }
334 |
335 | a.button {
336 | width: 1.5em;
337 | height: 1.5em;
338 | }
339 |
340 | a.button.text{
341 | width: auto;
342 | height: auto;
343 | }
344 |
345 | a.button.text,
346 | html.autosubmit-support form.autosubmit label{
347 | padding: 0.25em 0.5em;
348 | }
349 |
350 | a.button.download:after {
351 | content: "\e904";
352 | }
353 |
354 | a.button.remove:after {
355 | content: "\e902";
356 | }
357 |
358 | strong[data-prefix] {
359 | display: inline-block;
360 | cursor: help;
361 | }
362 |
363 | strong[data-prefix]:after {
364 | display: inline-block;
365 | position: relative;
366 | top: -0.5em;
367 | margin: 0 0.25em;
368 | width: 1.2em;
369 | height: 1.2em;
370 | font-size: 0.75em;
371 | text-align: center;
372 | line-height: 1.2em;
373 | content: 'i';
374 | border-radius: 0.5em;
375 | color: white;
376 | background-color: darkgray;
377 | }
378 |
379 | strong[data-prefix]:hover:after {
380 | display: none;
381 | }
382 |
383 | strong[data-prefix]:hover:before {
384 | content: attr(data-prefix);
385 | }
386 |
--------------------------------------------------------------------------------
/README.rst:
--------------------------------------------------------------------------------
1 | browsepy
2 | ========
3 |
4 | .. image:: http://img.shields.io/travis/ergoithz/browsepy/master.svg?style=flat-square
5 | :target: https://travis-ci.org/ergoithz/browsepy
6 | :alt: Travis-CI badge
7 |
8 | .. image:: https://img.shields.io/appveyor/ci/ergoithz/browsepy/master.svg?style=flat-square
9 | :target: https://ci.appveyor.com/project/ergoithz/browsepy/branch/master
10 | :alt: AppVeyor badge
11 |
12 | .. image:: http://img.shields.io/coveralls/ergoithz/browsepy/master.svg?style=flat-square
13 | :target: https://coveralls.io/r/ergoithz/browsepy?branch=master
14 | :alt: Coveralls badge
15 |
16 | .. image:: https://img.shields.io/codacy/grade/e27821fb6289410b8f58338c7e0bc686/master.svg?style=flat-square
17 | :target: https://www.codacy.com/app/ergoithz/browsepy/dashboard?bid=4246124
18 | :alt: Codacy badge
19 |
20 | .. image:: http://img.shields.io/pypi/l/browsepy.svg?style=flat-square
21 | :target: https://pypi.python.org/pypi/browsepy/
22 | :alt: License: MIT
23 |
24 | .. image:: http://img.shields.io/pypi/v/browsepy.svg?style=flat-square
25 | :target: https://pypi.python.org/pypi/browsepy/
26 | :alt: Version: 0.5.6
27 |
28 | .. image:: https://img.shields.io/badge/python-2.7%2B%2C%203.3%2B-FFC100.svg?style=flat-square
29 | :target: https://pypi.python.org/pypi/browsepy/
30 | :alt: Python 2.7+, 3.3+
31 |
32 | The simple web file browser.
33 |
34 | Documentation
35 | -------------
36 |
37 | Head to http://ergoithz.github.io/browsepy/ for an online version of current
38 | *master* documentation,
39 |
40 | You can also build yourself from sphinx sources using the documentation
41 | `Makefile` located at `docs` directory.
42 |
43 | Screenshots
44 | -----------
45 |
46 | .. image:: https://raw.githubusercontent.com/ergoithz/browsepy/master/doc/screenshot.0.3.1-0.png
47 | :target: https://raw.githubusercontent.com/ergoithz/browsepy/master/doc/screenshot.0.3.1-0.png
48 | :alt: Screenshot of directory with enabled remove
49 |
50 | Features
51 | --------
52 |
53 | * **Simple**, like Python's SimpleHTTPServer or Apache's Directory Listing.
54 | * **Downloadable directories**, streaming directory tarballs on the fly.
55 | * **Optional remove** for files under given path.
56 | * **Optional upload** for directories under given path.
57 | * **Player** audio player plugin is provided (without transcoding).
58 |
59 | New in 0.5
60 | ----------
61 |
62 | * File and plugin APIs have been fully reworked making them more complete and
63 | extensible, so they can be considered stable now. As a side-effect backward
64 | compatibility on some edge cases could be broken (please fill an issue if
65 | your code is affected).
66 |
67 | * Old widget API have been deprecated and warnings will be shown if used.
68 | * Widget registration in a single call (passing a widget instances is still
69 | available though), no more action-widget duality.
70 | * Callable-based widget filtering (no longer limited to mimetypes).
71 | * A raw HTML widget for maximum flexibility.
72 |
73 | * Plugins can register command-line arguments now.
74 | * Player plugin is now able to load `m3u` and `pls` playlists, and optionally
75 | play everything on a directory (adding a command-line argument).
76 | * Browsing now takes full advantage of `scandir` (already in Python 3.5 and an
77 | external dependency for older versions) providing faster directory listing.
78 | * Custom file ordering while browsing directories.
79 | * Easy multi-file uploads.
80 | * Jinja2 template output minification, saving those precious bytes.
81 | * Setup script now registers a proper `browsepy` command.
82 |
83 | Install
84 | -------
85 |
86 | It's on `pypi` so...
87 |
88 | .. _pypi: https://pypi.python.org/pypi/browsepy/
89 |
90 | .. code-block:: bash
91 |
92 | pip install browsepy
93 |
94 |
95 | You can get the development version from our `github repository`.
96 |
97 | .. _github repository: https://github.com/ergoithz/browsepy
98 |
99 | .. code-block:: bash
100 |
101 | pip install git+https://github.com/ergoithz/browsepy.git
102 |
103 |
104 | Usage
105 | -----
106 |
107 | Serving $HOME/shared to all addresses
108 |
109 | .. code-block:: bash
110 |
111 | browsepy 0.0.0.0 8080 --directory $HOME/shared
112 |
113 | Showing help
114 |
115 | .. code-block:: bash
116 |
117 | browsepy --help
118 |
119 | Showing help including player plugin arguments
120 |
121 | .. code-block:: bash
122 |
123 | browsepy --plugin=player --help
124 |
125 | This examples assume python's `bin` directory is in `PATH`, otherwise try
126 | replacing `browsepy` with `python -m browsepy`.
127 |
128 | Command-line arguments
129 | ----------------------
130 |
131 | This is what is printed when you run `browsepy --help`, keep in mind that
132 | plugins (loaded with `plugin` argument) could add extra arguments to this list.
133 |
134 | ::
135 |
136 | usage: browsepy [-h] [--directory PATH] [--initial PATH] [--removable PATH]
137 | [--upload PATH] [--exclude PATTERN] [--exclude-from PATH]
138 | [--plugin MODULE]
139 | [host] [port]
140 |
141 | positional arguments:
142 | host address to listen (default: 127.0.0.1)
143 | port port to listen (default: 8080)
144 |
145 | optional arguments:
146 | -h, --help show this help message and exit
147 | --directory PATH serving directory (default: current path)
148 | --initial PATH default directory (default: same as --directory)
149 | --removable PATH base directory allowing remove (default: none)
150 | --upload PATH base directory allowing upload (default: none)
151 | --exclude PATTERN exclude paths by pattern (multiple)
152 | --exclude-from PATH exclude paths by pattern file (multiple)
153 | --plugin MODULE load plugin module (multiple)
154 |
155 |
156 | Using as library
157 | ----------------
158 |
159 | It's a python module, so you can import **browsepy**, mount **app**, and serve
160 | it (it's `WSGI`_ compliant) using
161 | your preferred server.
162 |
163 | Browsepy is a Flask application, so it can be served along with any `WSGI`_ app
164 | just setting **APPLICATION_ROOT** in **browsepy.app** config to browsepy prefix
165 | url, and mounting **browsepy.app** on the appropriate parent
166 | *url-resolver*/*router*.
167 |
168 | .. _WSGI: https://www.python.org/dev/peps/pep-0333/
169 |
170 | Browsepy app config (available at :attr:`browsepy.app.config`) uses the
171 | following configuration options.
172 |
173 | * **directory_base**: anything under this directory will be served,
174 | defaults to current path.
175 | * **directory_start**: directory will be served when accessing root URL
176 | * **directory_remove**: file removing will be available under this path,
177 | defaults to **None**.
178 | * **directory_upload**: file upload will be available under this path,
179 | defaults to **None**.
180 | * **directory_tar_buffsize**, directory tar streaming buffer size,
181 | defaults to **262144** and must be multiple of 512.
182 | * **directory_downloadable** whether enable directory download or not,
183 | defaults to **True**.
184 | * **use_binary_multiples** whether use binary units (bi-bytes, like KiB)
185 | instead of common ones (bytes, like KB), defaults to **True**.
186 | * **plugin_modules** list of module names (absolute or relative to
187 | plugin_namespaces) will be loaded.
188 | * **plugin_namespaces** prefixes for module names listed at plugin_modules
189 | where relative plugin_modules are searched.
190 | * **exclude_fnc** function will be used to exclude files from listing and directory tarballs. Can be either None or function receiving an absolute path and returning a boolean.
191 |
192 | After editing `plugin_modules` value, plugin manager (available at module
193 | plugin_manager and app.extensions['plugin_manager']) should be reloaded using
194 | the `reload` method.
195 |
196 | The other way of loading a plugin programmatically is calling plugin manager's
197 | `load_plugin` method.
198 |
199 | Extend via plugin API
200 | ---------------------
201 |
202 | Starting from version 0.4.0, browsepy is extensible via plugins. A functional
203 | 'player' plugin is provided as example, and some more are planned.
204 |
205 | Plugins can add HTML content to browsepy's browsing view, using some
206 | convenience abstraction for already used elements like external stylesheet and
207 | javascript tags, links, buttons and file upload.
208 |
209 | More information at http://ergoithz.github.io/browsepy/plugins.html
210 |
--------------------------------------------------------------------------------
/browsepy/plugin/player/static/css/jplayer.blue.monday.min.css:
--------------------------------------------------------------------------------
1 | /*! Blue Monday Skin for jPlayer 2.9.2 ~ (c) 2009-2014 Happyworm Ltd ~ MIT License */.jp-audio :focus,.jp-audio-stream :focus,.jp-video :focus{outline:0}.jp-audio button::-moz-focus-inner,.jp-audio-stream button::-moz-focus-inner,.jp-video button::-moz-focus-inner{border:0}.jp-audio,.jp-audio-stream,.jp-video{font-size:16px;font-family:Verdana,Arial,sans-serif;line-height:1.6;color:#666;border:1px solid #009be3;background-color:#eee}.jp-audio{width:420px}.jp-audio-stream{width:182px}.jp-video-270p{width:480px}.jp-video-360p{width:640px}.jp-video-full{width:480px;height:270px;position:static!important;position:relative}.jp-video-full div div{z-index:1000}.jp-video-full .jp-jplayer{top:0;left:0;position:fixed!important;position:relative;overflow:hidden}.jp-video-full .jp-gui{position:fixed!important;position:static;top:0;left:0;width:100%;height:100%;z-index:1001}.jp-video-full .jp-interface{position:absolute!important;position:relative;bottom:0;left:0}.jp-interface{position:relative;background-color:#eee;width:100%}.jp-audio .jp-interface,.jp-audio-stream .jp-interface{height:80px}.jp-video .jp-interface{border-top:1px solid #009be3}.jp-controls-holder{clear:both;width:440px;margin:0 auto;position:relative;overflow:hidden;top:-8px}.jp-interface .jp-controls{margin:0;padding:0;overflow:hidden}.jp-audio .jp-controls{width:380px;padding:20px 20px 0}.jp-audio-stream .jp-controls{position:absolute;top:20px;left:20px;width:142px}.jp-video .jp-type-single .jp-controls{width:78px;margin-left:200px}.jp-video .jp-type-playlist .jp-controls{width:134px;margin-left:172px}.jp-video .jp-controls{float:left}.jp-controls button{display:block;float:left;overflow:hidden;text-indent:-9999px;border:none;cursor:pointer}.jp-play{width:40px;height:40px;background:url(../image/jplayer.blue.monday.jpg) no-repeat}.jp-play:focus{background:url(../image/jplayer.blue.monday.jpg) -41px 0 no-repeat}.jp-state-playing .jp-play{background:url(../image/jplayer.blue.monday.jpg) 0 -42px no-repeat}.jp-state-playing .jp-play:focus{background:url(../image/jplayer.blue.monday.jpg) -41px -42px no-repeat}.jp-next,.jp-previous,.jp-stop{width:28px;height:28px;margin-top:6px}.jp-stop{background:url(../image/jplayer.blue.monday.jpg) 0 -83px no-repeat;margin-left:10px}.jp-stop:focus{background:url(../image/jplayer.blue.monday.jpg) -29px -83px no-repeat}.jp-previous{background:url(../image/jplayer.blue.monday.jpg) 0 -112px no-repeat}.jp-previous:focus{background:url(../image/jplayer.blue.monday.jpg) -29px -112px no-repeat}.jp-next{background:url(../image/jplayer.blue.monday.jpg) 0 -141px no-repeat}.jp-next:focus{background:url(../image/jplayer.blue.monday.jpg) -29px -141px no-repeat}.jp-progress{overflow:hidden;background-color:#ddd}.jp-audio .jp-progress{position:absolute;top:32px;height:15px}.jp-audio .jp-type-single .jp-progress{left:110px;width:186px}.jp-audio .jp-type-playlist .jp-progress{left:166px;width:130px}.jp-video .jp-progress{top:0;left:0;width:100%;height:10px}.jp-seek-bar{background:url(../image/jplayer.blue.monday.jpg) 0 -202px repeat-x;width:0;height:100%;cursor:pointer}.jp-play-bar{background:url(../image/jplayer.blue.monday.jpg) 0 -218px repeat-x;width:0;height:100%}.jp-seeking-bg{background:url(../image/jplayer.blue.monday.seeking.gif)}.jp-state-no-volume .jp-volume-controls{display:none}.jp-volume-controls{position:absolute;top:32px;left:308px;width:200px}.jp-audio-stream .jp-volume-controls{left:70px}.jp-video .jp-volume-controls{top:12px;left:50px}.jp-volume-controls button{display:block;position:absolute;overflow:hidden;text-indent:-9999px;border:none;cursor:pointer}.jp-mute,.jp-volume-max{width:18px;height:15px}.jp-volume-max{left:74px}.jp-mute{background:url(../image/jplayer.blue.monday.jpg) 0 -170px no-repeat}.jp-mute:focus{background:url(../image/jplayer.blue.monday.jpg) -19px -170px no-repeat}.jp-state-muted .jp-mute{background:url(../image/jplayer.blue.monday.jpg) -60px -170px no-repeat}.jp-state-muted .jp-mute:focus{background:url(../image/jplayer.blue.monday.jpg) -79px -170px no-repeat}.jp-volume-max{background:url(../image/jplayer.blue.monday.jpg) 0 -186px no-repeat}.jp-volume-max:focus{background:url(../image/jplayer.blue.monday.jpg) -19px -186px no-repeat}.jp-volume-bar{position:absolute;overflow:hidden;background:url(../image/jplayer.blue.monday.jpg) 0 -250px repeat-x;top:5px;left:22px;width:46px;height:5px;cursor:pointer}.jp-volume-bar-value{background:url(../image/jplayer.blue.monday.jpg) 0 -256px repeat-x;width:0;height:5px}.jp-audio .jp-time-holder{position:absolute;top:50px}.jp-audio .jp-type-single .jp-time-holder{left:110px;width:186px}.jp-audio .jp-type-playlist .jp-time-holder{left:166px;width:130px}.jp-current-time,.jp-duration{width:60px;font-size:.64em;font-style:oblique}.jp-current-time{float:left;display:inline;cursor:default}.jp-duration{float:right;display:inline;text-align:right;cursor:pointer}.jp-video .jp-current-time{margin-left:20px}.jp-video .jp-duration{margin-right:20px}.jp-details{font-weight:700;text-align:center;cursor:default}.jp-details,.jp-playlist{width:100%;background-color:#ccc;border-top:1px solid #009be3}.jp-type-playlist .jp-details,.jp-type-single .jp-details{border-top:none}.jp-details .jp-title{margin:0;padding:5px 20px;font-size:.72em;font-weight:700}.jp-playlist ul{list-style-type:none;margin:0;padding:0 20px;font-size:.72em}.jp-playlist li{padding:5px 0 4px 20px;border-bottom:1px solid #eee}.jp-playlist li div{display:inline}div.jp-type-playlist div.jp-playlist li:last-child{padding:5px 0 5px 20px;border-bottom:none}div.jp-type-playlist div.jp-playlist li.jp-playlist-current{list-style-type:square;list-style-position:inside;padding-left:7px}div.jp-type-playlist div.jp-playlist a{color:#333;text-decoration:none}div.jp-type-playlist div.jp-playlist a.jp-playlist-current,div.jp-type-playlist div.jp-playlist a:hover{color:#0d88c1}div.jp-type-playlist div.jp-playlist a.jp-playlist-item-remove{float:right;display:inline;text-align:right;margin-right:10px;font-weight:700;color:#666}div.jp-type-playlist div.jp-playlist a.jp-playlist-item-remove:hover{color:#0d88c1}div.jp-type-playlist div.jp-playlist span.jp-free-media{float:right;display:inline;text-align:right;margin-right:10px}div.jp-type-playlist div.jp-playlist span.jp-free-media a{color:#666}div.jp-type-playlist div.jp-playlist span.jp-free-media a:hover{color:#0d88c1}span.jp-artist{font-size:.8em;color:#666}.jp-video-play{width:100%;overflow:hidden;cursor:pointer;background-color:transparent}.jp-video-270p .jp-video-play{margin-top:-270px;height:270px}.jp-video-360p .jp-video-play{margin-top:-360px;height:360px}.jp-video-full .jp-video-play{height:100%}.jp-video-play-icon{position:relative;display:block;width:112px;height:100px;margin-left:-56px;margin-top:-50px;left:50%;top:50%;background:url(../image/jplayer.blue.monday.video.play.png) no-repeat;text-indent:-9999px;border:none;cursor:pointer}.jp-video-play-icon:focus{background:url(../image/jplayer.blue.monday.video.play.png) 0 -100px no-repeat}.jp-jplayer,.jp-jplayer audio{width:0;height:0}.jp-jplayer{background-color:#000}.jp-toggles{padding:0;margin:0 auto;overflow:hidden}.jp-audio .jp-type-single .jp-toggles{width:25px}.jp-audio .jp-type-playlist .jp-toggles{width:55px;margin:0;position:absolute;left:325px;top:50px}.jp-video .jp-toggles{position:absolute;right:16px;margin:10px 0 0;width:100px}.jp-toggles button{display:block;float:left;width:25px;height:18px;text-indent:-9999px;line-height:100%;border:none;cursor:pointer}.jp-full-screen{background:url(../image/jplayer.blue.monday.jpg) 0 -310px no-repeat;margin-left:20px}.jp-full-screen:focus{background:url(../image/jplayer.blue.monday.jpg) -30px -310px no-repeat}.jp-state-full-screen .jp-full-screen{background:url(../image/jplayer.blue.monday.jpg) -60px -310px no-repeat}.jp-state-full-screen .jp-full-screen:focus{background:url(../image/jplayer.blue.monday.jpg) -90px -310px no-repeat}.jp-repeat{background:url(../image/jplayer.blue.monday.jpg) 0 -290px no-repeat}.jp-repeat:focus{background:url(../image/jplayer.blue.monday.jpg) -30px -290px no-repeat}.jp-state-looped .jp-repeat{background:url(../image/jplayer.blue.monday.jpg) -60px -290px no-repeat}.jp-state-looped .jp-repeat:focus{background:url(../image/jplayer.blue.monday.jpg) -90px -290px no-repeat}.jp-shuffle{background:url(../image/jplayer.blue.monday.jpg) 0 -270px no-repeat;margin-left:5px}.jp-shuffle:focus{background:url(../image/jplayer.blue.monday.jpg) -30px -270px no-repeat}.jp-state-shuffled .jp-shuffle{background:url(../image/jplayer.blue.monday.jpg) -60px -270px no-repeat}.jp-state-shuffled .jp-shuffle:focus{background:url(../image/jplayer.blue.monday.jpg) -90px -270px no-repeat}.jp-no-solution{padding:5px;font-size:.8em;background-color:#eee;border:2px solid #009be3;color:#000;display:none}.jp-no-solution a{color:#000}.jp-no-solution span{font-size:1em;display:block;text-align:center;font-weight:700}
--------------------------------------------------------------------------------
/doc/Makefile:
--------------------------------------------------------------------------------
1 | # Makefile for Sphinx documentation
2 | #
3 |
4 | # You can set these variables from the command line.
5 | SPHINXOPTS =
6 | SPHINXBUILD = sphinx-build
7 | PAPER =
8 | BUILDDIR = .build
9 |
10 | # Internal variables.
11 | PAPEROPT_a4 = -D latex_paper_size=a4
12 | PAPEROPT_letter = -D latex_paper_size=letter
13 | ALLSPHINXOPTS = -d $(BUILDDIR)/doctrees $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
14 | # the i18n builder cannot share the environment and doctrees with the others
15 | I18NSPHINXOPTS = $(PAPEROPT_$(PAPER)) $(SPHINXOPTS) .
16 |
17 | .PHONY: help
18 | help:
19 | @echo "Please use \`make ' where is one of"
20 | @echo " html to make standalone HTML files"
21 | @echo " dirhtml to make HTML files named index.html in directories"
22 | @echo " singlehtml to make a single large HTML file"
23 | @echo " pickle to make pickle files"
24 | @echo " json to make JSON files"
25 | @echo " htmlhelp to make HTML files and a HTML help project"
26 | @echo " qthelp to make HTML files and a qthelp project"
27 | @echo " applehelp to make an Apple Help Book"
28 | @echo " devhelp to make HTML files and a Devhelp project"
29 | @echo " epub to make an epub"
30 | @echo " epub3 to make an epub3"
31 | @echo " latex to make LaTeX files, you can set PAPER=a4 or PAPER=letter"
32 | @echo " latexpdf to make LaTeX files and run them through pdflatex"
33 | @echo " latexpdfja to make LaTeX files and run them through platex/dvipdfmx"
34 | @echo " text to make text files"
35 | @echo " man to make manual pages"
36 | @echo " texinfo to make Texinfo files"
37 | @echo " info to make Texinfo files and run them through makeinfo"
38 | @echo " gettext to make PO message catalogs"
39 | @echo " changes to make an overview of all changed/added/deprecated items"
40 | @echo " xml to make Docutils-native XML files"
41 | @echo " pseudoxml to make pseudoxml-XML files for display purposes"
42 | @echo " linkcheck to check all external links for integrity"
43 | @echo " doctest to run all doctests embedded in the documentation (if enabled)"
44 | @echo " coverage to run coverage check of the documentation (if enabled)"
45 | @echo " dummy to check syntax errors of document sources"
46 |
47 | .PHONY: clean
48 | clean:
49 | rm -rf $(BUILDDIR)/*
50 |
51 | .PHONY: html
52 | html:
53 | $(SPHINXBUILD) -b html $(ALLSPHINXOPTS) $(BUILDDIR)/html
54 | @echo
55 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/html."
56 |
57 | .PHONY: dirhtml
58 | dirhtml:
59 | $(SPHINXBUILD) -b dirhtml $(ALLSPHINXOPTS) $(BUILDDIR)/dirhtml
60 | @echo
61 | @echo "Build finished. The HTML pages are in $(BUILDDIR)/dirhtml."
62 |
63 | .PHONY: singlehtml
64 | singlehtml:
65 | $(SPHINXBUILD) -b singlehtml $(ALLSPHINXOPTS) $(BUILDDIR)/singlehtml
66 | @echo
67 | @echo "Build finished. The HTML page is in $(BUILDDIR)/singlehtml."
68 |
69 | .PHONY: pickle
70 | pickle:
71 | $(SPHINXBUILD) -b pickle $(ALLSPHINXOPTS) $(BUILDDIR)/pickle
72 | @echo
73 | @echo "Build finished; now you can process the pickle files."
74 |
75 | .PHONY: json
76 | json:
77 | $(SPHINXBUILD) -b json $(ALLSPHINXOPTS) $(BUILDDIR)/json
78 | @echo
79 | @echo "Build finished; now you can process the JSON files."
80 |
81 | .PHONY: htmlhelp
82 | htmlhelp:
83 | $(SPHINXBUILD) -b htmlhelp $(ALLSPHINXOPTS) $(BUILDDIR)/htmlhelp
84 | @echo
85 | @echo "Build finished; now you can run HTML Help Workshop with the" \
86 | ".hhp project file in $(BUILDDIR)/htmlhelp."
87 |
88 | .PHONY: qthelp
89 | qthelp:
90 | $(SPHINXBUILD) -b qthelp $(ALLSPHINXOPTS) $(BUILDDIR)/qthelp
91 | @echo
92 | @echo "Build finished; now you can run "qcollectiongenerator" with the" \
93 | ".qhcp project file in $(BUILDDIR)/qthelp, like this:"
94 | @echo "# qcollectiongenerator $(BUILDDIR)/qthelp/browsepy.qhcp"
95 | @echo "To view the help file:"
96 | @echo "# assistant -collectionFile $(BUILDDIR)/qthelp/browsepy.qhc"
97 |
98 | .PHONY: applehelp
99 | applehelp:
100 | $(SPHINXBUILD) -b applehelp $(ALLSPHINXOPTS) $(BUILDDIR)/applehelp
101 | @echo
102 | @echo "Build finished. The help book is in $(BUILDDIR)/applehelp."
103 | @echo "N.B. You won't be able to view it unless you put it in" \
104 | "~/Library/Documentation/Help or install it in your application" \
105 | "bundle."
106 |
107 | .PHONY: devhelp
108 | devhelp:
109 | $(SPHINXBUILD) -b devhelp $(ALLSPHINXOPTS) $(BUILDDIR)/devhelp
110 | @echo
111 | @echo "Build finished."
112 | @echo "To view the help file:"
113 | @echo "# mkdir -p $$HOME/.local/share/devhelp/browsepy"
114 | @echo "# ln -s $(BUILDDIR)/devhelp $$HOME/.local/share/devhelp/browsepy"
115 | @echo "# devhelp"
116 |
117 | .PHONY: epub
118 | epub:
119 | $(SPHINXBUILD) -b epub $(ALLSPHINXOPTS) $(BUILDDIR)/epub
120 | @echo
121 | @echo "Build finished. The epub file is in $(BUILDDIR)/epub."
122 |
123 | .PHONY: epub3
124 | epub3:
125 | $(SPHINXBUILD) -b epub3 $(ALLSPHINXOPTS) $(BUILDDIR)/epub3
126 | @echo
127 | @echo "Build finished. The epub3 file is in $(BUILDDIR)/epub3."
128 |
129 | .PHONY: latex
130 | latex:
131 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
132 | @echo
133 | @echo "Build finished; the LaTeX files are in $(BUILDDIR)/latex."
134 | @echo "Run \`make' in that directory to run these through (pdf)latex" \
135 | "(use \`make latexpdf' here to do that automatically)."
136 |
137 | .PHONY: latexpdf
138 | latexpdf:
139 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
140 | @echo "Running LaTeX files through pdflatex..."
141 | $(MAKE) -C $(BUILDDIR)/latex all-pdf
142 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
143 |
144 | .PHONY: latexpdfja
145 | latexpdfja:
146 | $(SPHINXBUILD) -b latex $(ALLSPHINXOPTS) $(BUILDDIR)/latex
147 | @echo "Running LaTeX files through platex and dvipdfmx..."
148 | $(MAKE) -C $(BUILDDIR)/latex all-pdf-ja
149 | @echo "pdflatex finished; the PDF files are in $(BUILDDIR)/latex."
150 |
151 | .PHONY: text
152 | text:
153 | $(SPHINXBUILD) -b text $(ALLSPHINXOPTS) $(BUILDDIR)/text
154 | @echo
155 | @echo "Build finished. The text files are in $(BUILDDIR)/text."
156 |
157 | .PHONY: man
158 | man:
159 | $(SPHINXBUILD) -b man $(ALLSPHINXOPTS) $(BUILDDIR)/man
160 | @echo
161 | @echo "Build finished. The manual pages are in $(BUILDDIR)/man."
162 |
163 | .PHONY: texinfo
164 | texinfo:
165 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
166 | @echo
167 | @echo "Build finished. The Texinfo files are in $(BUILDDIR)/texinfo."
168 | @echo "Run \`make' in that directory to run these through makeinfo" \
169 | "(use \`make info' here to do that automatically)."
170 |
171 | .PHONY: info
172 | info:
173 | $(SPHINXBUILD) -b texinfo $(ALLSPHINXOPTS) $(BUILDDIR)/texinfo
174 | @echo "Running Texinfo files through makeinfo..."
175 | make -C $(BUILDDIR)/texinfo info
176 | @echo "makeinfo finished; the Info files are in $(BUILDDIR)/texinfo."
177 |
178 | .PHONY: gettext
179 | gettext:
180 | $(SPHINXBUILD) -b gettext $(I18NSPHINXOPTS) $(BUILDDIR)/locale
181 | @echo
182 | @echo "Build finished. The message catalogs are in $(BUILDDIR)/locale."
183 |
184 | .PHONY: changes
185 | changes:
186 | $(SPHINXBUILD) -b changes $(ALLSPHINXOPTS) $(BUILDDIR)/changes
187 | @echo
188 | @echo "The overview file is in $(BUILDDIR)/changes."
189 |
190 | .PHONY: linkcheck
191 | linkcheck:
192 | $(SPHINXBUILD) -b linkcheck $(ALLSPHINXOPTS) $(BUILDDIR)/linkcheck
193 | @echo
194 | @echo "Link check complete; look for any errors in the above output " \
195 | "or in $(BUILDDIR)/linkcheck/output.txt."
196 |
197 | .PHONY: doctest
198 | doctest:
199 | $(SPHINXBUILD) -b doctest $(ALLSPHINXOPTS) $(BUILDDIR)/doctest
200 | @echo "Testing of doctests in the sources finished, look at the " \
201 | "results in $(BUILDDIR)/doctest/output.txt."
202 |
203 | .PHONY: coverage
204 | coverage:
205 | $(SPHINXBUILD) -b coverage $(ALLSPHINXOPTS) $(BUILDDIR)/coverage
206 | @echo "Testing of coverage in the sources finished, look at the " \
207 | "results in $(BUILDDIR)/coverage/python.txt."
208 |
209 | .PHONY: xml
210 | xml:
211 | $(SPHINXBUILD) -b xml $(ALLSPHINXOPTS) $(BUILDDIR)/xml
212 | @echo
213 | @echo "Build finished. The XML files are in $(BUILDDIR)/xml."
214 |
215 | .PHONY: pseudoxml
216 | pseudoxml:
217 | $(SPHINXBUILD) -b pseudoxml $(ALLSPHINXOPTS) $(BUILDDIR)/pseudoxml
218 | @echo
219 | @echo "Build finished. The pseudo-XML files are in $(BUILDDIR)/pseudoxml."
220 |
221 | .PHONY: dummy
222 | dummy:
223 | $(SPHINXBUILD) -b dummy $(ALLSPHINXOPTS) $(BUILDDIR)/dummy
224 | @echo
225 | @echo "Build finished. Dummy builder generates no files."
226 |
--------------------------------------------------------------------------------
/browsepy/plugin/player/static/js/jplayer.playlist.min.js:
--------------------------------------------------------------------------------
1 | /*! jPlayerPlaylist for jPlayer 2.9.2 ~ (c) 2009-2014 Happyworm Ltd ~ MIT License */
2 | !function(a,b){jPlayerPlaylist=function(b,c,d){var e=this;this.current=0,this.loop=!1,this.shuffled=!1,this.removing=!1,this.cssSelector=a.extend({},this._cssSelector,b),this.options=a.extend(!0,{keyBindings:{next:{key:221,fn:function(){e.next()}},previous:{key:219,fn:function(){e.previous()}},shuffle:{key:83,fn:function(){e.shuffle()}}},stateClass:{shuffled:"jp-state-shuffled"}},this._options,d),this.playlist=[],this.original=[],this._initPlaylist(c),this.cssSelector.details=this.cssSelector.cssSelectorAncestor+" .jp-details",this.cssSelector.playlist=this.cssSelector.cssSelectorAncestor+" .jp-playlist",this.cssSelector.next=this.cssSelector.cssSelectorAncestor+" .jp-next",this.cssSelector.previous=this.cssSelector.cssSelectorAncestor+" .jp-previous",this.cssSelector.shuffle=this.cssSelector.cssSelectorAncestor+" .jp-shuffle",this.cssSelector.shuffleOff=this.cssSelector.cssSelectorAncestor+" .jp-shuffle-off",this.options.cssSelectorAncestor=this.cssSelector.cssSelectorAncestor,this.options.repeat=function(a){e.loop=a.jPlayer.options.loop},a(this.cssSelector.jPlayer).bind(a.jPlayer.event.ready,function(){e._init()}),a(this.cssSelector.jPlayer).bind(a.jPlayer.event.ended,function(){e.next()}),a(this.cssSelector.jPlayer).bind(a.jPlayer.event.play,function(){a(this).jPlayer("pauseOthers")}),a(this.cssSelector.jPlayer).bind(a.jPlayer.event.resize,function(b){b.jPlayer.options.fullScreen?a(e.cssSelector.details).show():a(e.cssSelector.details).hide()}),a(this.cssSelector.previous).click(function(a){a.preventDefault(),e.previous(),e.blur(this)}),a(this.cssSelector.next).click(function(a){a.preventDefault(),e.next(),e.blur(this)}),a(this.cssSelector.shuffle).click(function(b){b.preventDefault(),e.shuffle(e.shuffled&&a(e.cssSelector.jPlayer).jPlayer("option","useStateClassSkin")?!1:!0),e.blur(this)}),a(this.cssSelector.shuffleOff).click(function(a){a.preventDefault(),e.shuffle(!1),e.blur(this)}).hide(),this.options.fullScreen||a(this.cssSelector.details).hide(),a(this.cssSelector.playlist+" ul").empty(),this._createItemHandlers(),a(this.cssSelector.jPlayer).jPlayer(this.options)},jPlayerPlaylist.prototype={_cssSelector:{jPlayer:"#jquery_jplayer_1",cssSelectorAncestor:"#jp_container_1"},_options:{playlistOptions:{autoPlay:!1,loopOnPrevious:!1,shuffleOnLoop:!0,enableRemoveControls:!1,displayTime:"slow",addTime:"fast",removeTime:"fast",shuffleTime:"slow",itemClass:"jp-playlist-item",freeGroupClass:"jp-free-media",freeItemClass:"jp-playlist-item-free",removeItemClass:"jp-playlist-item-remove"}},option:function(a,c){if(c===b)return this.options.playlistOptions[a];switch(this.options.playlistOptions[a]=c,a){case"enableRemoveControls":this._updateControls();break;case"itemClass":case"freeGroupClass":case"freeItemClass":case"removeItemClass":this._refresh(!0),this._createItemHandlers()}return this},_init:function(){var a=this;this._refresh(function(){a.options.playlistOptions.autoPlay?a.play(a.current):a.select(a.current)})},_initPlaylist:function(b){this.current=0,this.shuffled=!1,this.removing=!1,this.original=a.extend(!0,[],b),this._originalPlaylist()},_originalPlaylist:function(){var b=this;this.playlist=[],a.each(this.original,function(a){b.playlist[a]=b.original[a]})},_refresh:function(b){var c=this;if(b&&!a.isFunction(b))a(this.cssSelector.playlist+" ul").empty(),a.each(this.playlist,function(b){a(c.cssSelector.playlist+" ul").append(c._createListItem(c.playlist[b]))}),this._updateControls();else{var d=a(this.cssSelector.playlist+" ul").children().length?this.options.playlistOptions.displayTime:0;a(this.cssSelector.playlist+" ul").slideUp(d,function(){var d=a(this);a(this).empty(),a.each(c.playlist,function(a){d.append(c._createListItem(c.playlist[a]))}),c._updateControls(),a.isFunction(b)&&b(),c.playlist.length?a(this).slideDown(c.options.playlistOptions.displayTime):a(this).show()})}},_createListItem:function(b){var c=this,d=""},_createItemHandlers:function(){var b=this;a(this.cssSelector.playlist).off("click","a."+this.options.playlistOptions.itemClass).on("click","a."+this.options.playlistOptions.itemClass,function(c){c.preventDefault();var d=a(this).parent().parent().index();b.current!==d?b.play(d):a(b.cssSelector.jPlayer).jPlayer("play"),b.blur(this)}),a(this.cssSelector.playlist).off("click","a."+this.options.playlistOptions.freeItemClass).on("click","a."+this.options.playlistOptions.freeItemClass,function(c){c.preventDefault(),a(this).parent().parent().find("."+b.options.playlistOptions.itemClass).click(),b.blur(this)}),a(this.cssSelector.playlist).off("click","a."+this.options.playlistOptions.removeItemClass).on("click","a."+this.options.playlistOptions.removeItemClass,function(c){c.preventDefault();var d=a(this).parent().parent().index();b.remove(d),b.blur(this)})},_updateControls:function(){this.options.playlistOptions.enableRemoveControls?a(this.cssSelector.playlist+" ."+this.options.playlistOptions.removeItemClass).show():a(this.cssSelector.playlist+" ."+this.options.playlistOptions.removeItemClass).hide(),this.shuffled?a(this.cssSelector.jPlayer).jPlayer("addStateClass","shuffled"):a(this.cssSelector.jPlayer).jPlayer("removeStateClass","shuffled"),a(this.cssSelector.shuffle).length&&a(this.cssSelector.shuffleOff).length&&(this.shuffled?(a(this.cssSelector.shuffleOff).show(),a(this.cssSelector.shuffle).hide()):(a(this.cssSelector.shuffleOff).hide(),a(this.cssSelector.shuffle).show()))},_highlight:function(c){this.playlist.length&&c!==b&&(a(this.cssSelector.playlist+" .jp-playlist-current").removeClass("jp-playlist-current"),a(this.cssSelector.playlist+" li:nth-child("+(c+1)+")").addClass("jp-playlist-current").find(".jp-playlist-item").addClass("jp-playlist-current"))},setPlaylist:function(a){this._initPlaylist(a),this._init()},add:function(b,c){a(this.cssSelector.playlist+" ul").append(this._createListItem(b)).find("li:last-child").hide().slideDown(this.options.playlistOptions.addTime),this._updateControls(),this.original.push(b),this.playlist.push(b),c?this.play(this.playlist.length-1):1===this.original.length&&this.select(0)},remove:function(c){var d=this;return c===b?(this._initPlaylist([]),this._refresh(function(){a(d.cssSelector.jPlayer).jPlayer("clearMedia")}),!0):this.removing?!1:(c=0>c?d.original.length+c:c,c>=0&&cb?this.original.length+b:b,b>=0&&bc?this.original.length+c:c,c>=0&&c1?this.shuffle(!0,!0):this.play(a):a>0&&this.play(a)},previous:function(){var a=this.current-1>=0?this.current-1:this.playlist.length-1;(this.loop&&this.options.playlistOptions.loopOnPrevious||a`.
22 |
23 | Aiming to make plugin names shorter, browsepy try to load plugins
24 | using namespaces
25 | and prefixes defined on configuration's ``plugin_namespaces`` entry on
26 | :attr:`browsepy.app.config`. Its default value is browsepy's built-in module namespace
27 | `browsepy.plugins`, `browsepy_` prefix and an empty namespace (so any plugin could
28 | be used with its full module name).
29 |
30 | Summarizing, with default configuration:
31 |
32 | * Any python module inside `browsepy.plugin` can be loaded as plugin by its
33 | relative module name, ie. ``player`` instead of ``browsepy.plugin.player``.
34 | * Any python module prefixed by ``browsepy_`` can be loaded as plugin by its
35 | unprefixed name, ie. ``myplugin`` instead of ``browsepy_myplugin``.
36 | * Any python module can be loaded as plugin by its full module name.
37 |
38 | Said that, you can name your own plugin so it could be loaded easily.
39 |
40 | .. _plugins-namespace-examples:
41 |
42 | Examples
43 | ++++++++
44 |
45 | Your built-in plugin, placed under `browsepy/plugins/` in your own
46 | browsepy fork:
47 |
48 | .. code-block:: bash
49 |
50 | browsepy --plugin=my_builtin_module
51 |
52 | Your prefixed plugin, a regular python module in python's library path,
53 | named `browsepy_prefixed_plugin`:
54 |
55 | .. code-block:: bash
56 |
57 | browsepy --plugin=prefixed_plugin
58 |
59 | Your plugin, a regular python module in python's library path, named `my_plugin`.
60 |
61 | .. code-block:: bash
62 |
63 | browsepy --plugin=my_plugin
64 |
65 | .. _plugins-protocol:
66 |
67 | Protocol
68 | --------
69 |
70 | The plugin manager subsystem expects a `register_plugin` callable at module
71 | level (in your **__init__** module globals) which will be called with the
72 | manager itself (type :class:`PluginManager`) as first parameter.
73 |
74 | Plugin manager exposes several methods to register widgets and mimetype
75 | detection functions.
76 |
77 | A *sregister_plugin*s function looks like this (taken from player plugin):
78 |
79 | .. code-block:: python
80 |
81 | def register_plugin(manager):
82 | '''
83 | Register blueprints and actions using given plugin manager.
84 |
85 | :param manager: plugin manager
86 | :type manager: browsepy.manager.PluginManager
87 | '''
88 | manager.register_blueprint(player)
89 | manager.register_mimetype_function(detect_playable_mimetype)
90 |
91 | # add style tag
92 | manager.register_widget(
93 | place='styles',
94 | type='stylesheet',
95 | endpoint='player.static',
96 | filename='css/browse.css'
97 | )
98 |
99 | # register link actions
100 | manager.register_widget(
101 | place='entry-link',
102 | type='link',
103 | endpoint='player.audio',
104 | filter=PlayableFile.detect
105 | )
106 | manager.register_widget(
107 | place='entry-link',
108 | icon='playlist',
109 | type='link',
110 | endpoint='player.playlist',
111 | filter=PlayListFile.detect
112 | )
113 |
114 | # register action buttons
115 | manager.register_widget(
116 | place='entry-actions',
117 | css='play',
118 | type='button',
119 | endpoint='player.audio',
120 | filter=PlayableFile.detect
121 | )
122 | manager.register_widget(
123 | place='entry-actions',
124 | css='play',
125 | type='button',
126 | endpoint='player.playlist',
127 | filter=PlayListFile.detect
128 | )
129 |
130 | # check argument (see `register_arguments`) before registering
131 | if manager.get_argument('player_directory_play'):
132 | # register header button
133 | manager.register_widget(
134 | place='header',
135 | type='button',
136 | endpoint='player.directory',
137 | text='Play directory',
138 | filter=PlayableDirectory.detect
139 | )
140 |
141 | In case you need to add extra command-line-arguments to browsepy command,
142 | :func:`register_arguments` method can be also declared (like register_plugin,
143 | at your plugin's module level). It will receive a
144 | :class:`ArgumentPluginManager` instance, providing an argument-related subset of whole plugin manager's functionality.
145 |
146 | A simple `register_arguments` example (from player plugin):
147 |
148 | .. code-block:: python
149 |
150 | def register_arguments(manager):
151 | '''
152 | Register arguments using given plugin manager.
153 |
154 | This method is called before `register_plugin`.
155 |
156 | :param manager: plugin manager
157 | :type manager: browsepy.manager.PluginManager
158 | '''
159 |
160 | # Arguments are forwarded to argparse:ArgumentParser.add_argument,
161 | # https://docs.python.org/3.7/library/argparse.html#the-add-argument-method
162 | manager.register_argument(
163 | '--player-directory-play', action='store_true',
164 | help='enable directories as playlist'
165 | )
166 |
167 | .. _widgets:
168 |
169 | Widgets
170 | -------
171 |
172 | Widget registration is provided by :meth:`PluginManager.register_widget`.
173 |
174 | You can alternatively pass a widget object, via `widget` keyword
175 | argument, or use a pure functional approach by passing `place`,
176 | `type` and the widget-specific properties as keyword arguments.
177 |
178 | In addition to that, you can also define in which cases widget will be shown passing
179 | a callable to `filter` argument keyword, which will receive a
180 | :class:`browsepy.file.Node` (commonly a :class:`browsepy.file.File` or a
181 | :class:`browsepy.file.Directory`) instance.
182 |
183 | For those wanting the object-oriented approach, and for reference for those
184 | wanting to know widget properties for using the functional way,
185 | :attr:`WidgetPluginManager.widget_types` dictionary is
186 | available, containing widget namedtuples (see :func:`collections.namedtuple`) definitions.
187 |
188 |
189 | Here is the "widget_types" for reference.
190 |
191 | .. code-block:: python
192 |
193 | class WidgetPluginManager(RegistrablePluginManager):
194 | ''' ... '''
195 | widget_types = {
196 | 'base': defaultsnamedtuple(
197 | 'Widget',
198 | ('place', 'type')),
199 | 'link': defaultsnamedtuple(
200 | 'Link',
201 | ('place', 'type', 'css', 'icon', 'text', 'endpoint', 'href'),
202 | {
203 | 'text': lambda f: f.name,
204 | 'icon': lambda f: f.category
205 | }),
206 | 'button': defaultsnamedtuple(
207 | 'Button',
208 | ('place', 'type', 'css', 'text', 'endpoint', 'href')),
209 | 'upload': defaultsnamedtuple(
210 | 'Upload',
211 | ('place', 'type', 'css', 'text', 'endpoint', 'action')),
212 | 'stylesheet': defaultsnamedtuple(
213 | 'Stylesheet',
214 | ('place', 'type', 'endpoint', 'filename', 'href')),
215 | 'script': defaultsnamedtuple(
216 | 'Script',
217 | ('place', 'type', 'endpoint', 'filename', 'src')),
218 | 'html': defaultsnamedtuple(
219 | 'Html',
220 | ('place', 'type', 'html')),
221 | }
222 |
223 | Function :func:`browsepy.file.defaultsnamedtuple` is a
224 | :func:`collections.namedtuple` which uses a third argument dictionary
225 | as default attribute values. None is assumed as implicit default.
226 |
227 | All attribute values can be either :class:`str` or a callable accepting
228 | a :class:`browsepy.file.Node` instance as argument and returning said :class:`str`,
229 | allowing dynamic widget content and behavior.
230 |
231 | Please note place and type are always required (otherwise widget won't be
232 | drawn), and this properties are mutually exclusive:
233 |
234 | * **link**: attribute href supersedes endpoint.
235 | * **button**: attribute href supersedes endpoint.
236 | * **upload**: attribute action supersedes endpoint.
237 | * **stylesheet**: attribute href supersedes endpoint and filename
238 | * **script**: attribute src supersedes endpoint and filename.
239 |
240 | Endpoints are Flask endpoint names, and endpoint handler functions must receive
241 | a "filename" parameter for stylesheet and script widgets (allowing it to point
242 | using with Flask's statics view) and a "path" argument for other use-cases. In the
243 | former case it is recommended to use
244 | :meth:`browsepy.file.Node.from_urlpath` static method to create the
245 | appropriate file/directory object (see :mod:`browsepy.file`).
246 |
247 | .. _plugins-considerations:
248 |
249 | Considerations
250 | --------------
251 |
252 | Name your plugin wisely, look at `pypi `_ for
253 | conflicting module names.
254 |
255 | Always choose the less intrusive approach on plugin development, so new
256 | browsepy versions will not likely break it. That's why stuff like
257 | :meth:`PluginManager.register_blueprint` is provided and its usage is
258 | preferred over directly registering blueprints via plugin manager's app
259 | reference (or even module-level app reference).
260 |
261 | A good way to keep your plugin working on future browsepy releases is
262 | :ref:`upstreaming it ` onto browsepy itself.
263 |
264 | Feel free to hack everything you want. Pull requests are definitely welcome.
265 |
--------------------------------------------------------------------------------
/doc/exclude.rst:
--------------------------------------------------------------------------------
1 | .. _excluding-paths:
2 |
3 | Excluding paths
4 | ===============
5 |
6 | Starting from version **0.5.3**, browsepy accepts **--exclude** command line
7 | arguments expecting linux filename expansion strings, also known as globs.
8 |
9 | **Note (windows):** on nt platforms, the accepted glob syntax will be the same
10 | (``/`` for filepath separator and ``\`` used for character escapes),
11 | browsepy will transform them appropriately.
12 |
13 | They allow matching filemames using wildcards, being the most common `*`
14 | (matching any string, even empty) and `?` (matching a single character). See
15 | :ref:`glob-manpage` for further info.
16 |
17 | Please note that both collating symbols (like ``[.a-acute.]``) and
18 | equivalence class expressions (like ``[=a=]``) are currently unsupported.
19 |
20 | Excluded paths will be omitted from both directory listing and directory
21 | tarball downloads.
22 |
23 | As seen at :ref:`quickstart-usage`, the exclude parameter can be provided
24 | as follows:
25 |
26 | .. code-block:: bash
27 |
28 | browsepy --exclude=.*
29 |
30 | The above example will exclude all files prefixed with ``.``, which are
31 | considered hidden on POSIX systems. In other words, it will match ``.myfile``
32 | and not ``my.file``.
33 |
34 | You can, alternatively, restrict the above exclusion to only top-level filenames:
35 |
36 | .. code-block:: bash
37 |
38 | browsepy --exclude=/.*
39 |
40 |
41 | The following example will hide all files ending with ``.ini``, but only on the
42 | base directory.
43 |
44 | .. code-block:: bash
45 |
46 | browsepy --exclude=/*.ini
47 |
48 | You will find this syntax very similar to definitions found in **.gitignore**,
49 | **.dockerignore** and others ignore definition files. As browsepy uses
50 | same format, you can pass them to browsepy using **--exclude-from**
51 | options.
52 |
53 | .. code-block:: bash
54 |
55 | browsepy --exclude-from=.gitignore
56 |
57 | .. _glob-manpage:
58 |
59 | Glob manpage
60 | ------------
61 |
62 | As glob reference, this is returned by ``man glob.7``.
63 |
64 | ::
65 |
66 | GLOB(7) Linux Programmer's Manual GLOB(7)
67 |
68 | NAME
69 | glob - globbing pathnames
70 |
71 | DESCRIPTION
72 | Long ago, in UNIX V6, there was a program /etc/glob that
73 | would expand wildcard patterns. Soon afterward this became
74 | a shell built-in.
75 |
76 | These days there is also a library routine glob(3) that
77 | will perform this function for a user program.
78 |
79 | The rules are as follows (POSIX.2, 3.13).
80 |
81 | Wildcard matching
82 | A string is a wildcard pattern if it contains one of the
83 | characters '?', '*' or '['. Globbing is the operation that
84 | expands a wildcard pattern into the list of pathnames
85 | matching the pattern. Matching is defined by:
86 |
87 | A '?' (not between brackets) matches any single character.
88 |
89 | A '*' (not between brackets) matches any string, including
90 | the empty string.
91 |
92 | Character classes
93 |
94 | An expression "[...]" where the first character after the
95 | leading '[' is not an '!' matches a single character,
96 | namely any of the characters enclosed by the brackets. The
97 | string enclosed by the brackets cannot be empty; therefore
98 | ']' can be allowed between the brackets, provided that it
99 | is the first character. (Thus, "[][!]" matches the three
100 | characters '[', ']' and '!'.)
101 |
102 | Ranges
103 |
104 | There is one special convention: two characters separated
105 | by '-' denote a range. (Thus, "[A-Fa-f0-9]" is equivalent
106 | to "[ABCDEFabcdef0123456789]".) One may include '-' in its
107 | literal meaning by making it the first or last character
108 | between the brackets. (Thus, "[]-]" matches just the two
109 | characters ']' and '-', and "[--0]" matches the three char‐
110 | acters '-', '.', '0', since '/' cannot be matched.)
111 |
112 | Complementation
113 |
114 | An expression "[!...]" matches a single character, namely
115 | any character that is not matched by the expression
116 | obtained by removing the first '!' from it. (Thus,
117 | "[!]a-]" matches any single character except ']', 'a' and
118 | '-'.)
119 |
120 | One can remove the special meaning of '?', '*' and '[' by
121 | preceding them by a backslash, or, in case this is part of
122 | a shell command line, enclosing them in quotes. Between
123 | brackets these characters stand for themselves. Thus,
124 | "[[?*\]" matches the four characters '[', '?', '*' and '\'.
125 |
126 | Pathnames
127 | Globbing is applied on each of the components of a pathname
128 | separately. A '/' in a pathname cannot be matched by a '?'
129 | or '*' wildcard, or by a range like "[.-0]". A range con‐
130 | taining an explicit '/' character is syntactically incor‐
131 | rect. (POSIX requires that syntactically incorrect pat‐
132 | terns are left unchanged.)
133 |
134 | If a filename starts with a '.', this character must be
135 | matched explicitly. (Thus, rm * will not remove .profile,
136 | and tar c * will not archive all your files; tar c . is
137 | better.)
138 |
139 | Empty lists
140 | The nice and simple rule given above: "expand a wildcard
141 | pattern into the list of matching pathnames" was the origi‐
142 | nal UNIX definition. It allowed one to have patterns that
143 | expand into an empty list, as in
144 |
145 | xv -wait 0 *.gif *.jpg
146 |
147 | where perhaps no *.gif files are present (and this is not
148 | an error). However, POSIX requires that a wildcard pattern
149 | is left unchanged when it is syntactically incorrect, or
150 | the list of matching pathnames is empty. With bash one can
151 | force the classical behavior using this command:
152 |
153 | shopt -s nullglob
154 |
155 | (Similar problems occur elsewhere. For example, where old
156 | scripts have
157 |
158 | rm `find . -name "*~"`
159 |
160 | new scripts require
161 |
162 | rm -f nosuchfile `find . -name "*~"`
163 |
164 | to avoid error messages from rm called with an empty argu‐
165 | ment list.)
166 |
167 | NOTES
168 | Regular expressions
169 | Note that wildcard patterns are not regular expressions,
170 | although they are a bit similar. First of all, they match
171 | filenames, rather than text, and secondly, the conventions
172 | are not the same: for example, in a regular expression '*'
173 | means zero or more copies of the preceding thing.
174 |
175 | Now that regular expressions have bracket expressions where
176 | the negation is indicated by a '^', POSIX has declared the
177 | effect of a wildcard pattern "[^...]" to be undefined.
178 |
179 | Character classes and internationalization
180 | Of course ranges were originally meant to be ASCII ranges,
181 | so that "[ -%]" stands for "[ !"#$%]" and "[a-z]" stands
182 | for "any lowercase letter". Some UNIX implementations gen‐
183 | eralized this so that a range X-Y stands for the set of
184 | characters with code between the codes for X and for Y.
185 | However, this requires the user to know the character cod‐
186 | ing in use on the local system, and moreover, is not conve‐
187 | nient if the collating sequence for the local alphabet dif‐
188 | fers from the ordering of the character codes. Therefore,
189 | POSIX extended the bracket notation greatly, both for wild‐
190 | card patterns and for regular expressions. In the above we
191 | saw three types of items that can occur in a bracket
192 | expression: namely (i) the negation, (ii) explicit single
193 | characters, and (iii) ranges. POSIX specifies ranges in an
194 | internationally more useful way and adds three more types:
195 |
196 | (iii) Ranges X-Y comprise all characters that fall between
197 | X and Y (inclusive) in the current collating sequence as
198 | defined by the LC_COLLATE category in the current locale.
199 |
200 | (iv) Named character classes, like
201 |
202 | [:alnum:] [:alpha:] [:blank:] [:cntrl:]
203 | [:digit:] [:graph:] [:lower:] [:print:]
204 | [:punct:] [:space:] [:upper:] [:xdigit:]
205 |
206 | so that one can say "[[:lower:]]" instead of "[a-z]", and
207 | have things work in Denmark, too, where there are three
208 | letters past 'z' in the alphabet. These character classes
209 | are defined by the LC_CTYPE category in the current locale.
210 |
211 | (v) Collating symbols, like "[.ch.]" or "[.a-acute.]",
212 | where the string between "[." and ".]" is a collating ele‐
213 | ment defined for the current locale. Note that this may be
214 | a multicharacter element.
215 |
216 | (vi) Equivalence class expressions, like "[=a=]", where the
217 | string between "[=" and "=]" is any collating element from
218 | its equivalence class, as defined for the current locale.
219 | For example, "[[=a=]]" might be equivalent to "[aáàäâ]",
220 | that is, to "[a[.a-acute.][.a-grave.][.a-umlaut.][.a-cir‐
221 | cumflex.]]".
222 |
223 | SEE ALSO
224 | sh(1), fnmatch(3), glob(3), locale(7), regex(7)
225 |
226 | COLOPHON
227 | This page is part of release 4.10 of the Linux man-pages
228 | project. A description of the project, information about
229 | reporting bugs, and the latest version of this page, can be
230 | found at https://www.kernel.org/doc/man-pages/.
231 |
232 | Linux 2016-10-08 GLOB(7)
233 |
--------------------------------------------------------------------------------
/browsepy/__init__.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 |
4 | import logging
5 | import os
6 | import os.path
7 | import json
8 | import base64
9 |
10 | from flask import Response, request, render_template, redirect, \
11 | url_for, send_from_directory, stream_with_context, \
12 | make_response
13 | from werkzeug.exceptions import NotFound
14 |
15 | from .appconfig import Flask
16 | from .manager import PluginManager
17 | from .file import Node, secure_filename
18 | from .exceptions import OutsideRemovableBase, OutsideDirectoryBase, \
19 | InvalidFilenameError, InvalidPathError
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 |
60 |
61 | def iter_cookie_browse_sorting(cookies):
62 | '''
63 | Get sorting-cookie from cookies dictionary.
64 |
65 | :yields: tuple of path and sorting property
66 | :ytype: 2-tuple of strings
67 | '''
68 | try:
69 | data = cookies.get('browse-sorting', 'e30=').encode('ascii')
70 | for path, prop in json.loads(base64.b64decode(data).decode('utf-8')):
71 | yield path, prop
72 | except (ValueError, TypeError, KeyError) as e:
73 | logger.exception(e)
74 |
75 |
76 | def get_cookie_browse_sorting(path, default):
77 | '''
78 | Get sorting-cookie data for path of current request.
79 |
80 | :returns: sorting property
81 | :rtype: string
82 | '''
83 | if request:
84 | for cpath, cprop in iter_cookie_browse_sorting(request.cookies):
85 | if path == cpath:
86 | return cprop
87 | return default
88 |
89 |
90 | def browse_sortkey_reverse(prop):
91 | '''
92 | Get sorting function for directory listing based on given attribute
93 | name, with some caveats:
94 | * Directories will be first.
95 | * If *name* is given, link widget lowercase text will be used istead.
96 | * If *size* is given, bytesize will be used.
97 |
98 | :param prop: file attribute name
99 | :returns: tuple with sorting gunction and reverse bool
100 | :rtype: tuple of a dict and a bool
101 | '''
102 | if prop.startswith('-'):
103 | prop = prop[1:]
104 | reverse = True
105 | else:
106 | reverse = False
107 |
108 | if prop == 'text':
109 | return (
110 | lambda x: (
111 | x.is_directory == reverse,
112 | x.link.text.lower() if x.link and x.link.text else x.name
113 | ),
114 | reverse
115 | )
116 | if prop == 'size':
117 | return (
118 | lambda x: (
119 | x.is_directory == reverse,
120 | x.stats.st_size
121 | ),
122 | reverse
123 | )
124 | return (
125 | lambda x: (
126 | x.is_directory == reverse,
127 | getattr(x, prop, None)
128 | ),
129 | reverse
130 | )
131 |
132 |
133 | def stream_template(template_name, **context):
134 | '''
135 | Some templates can be huge, this function returns an streaming response,
136 | sending the content in chunks and preventing from timeout.
137 |
138 | :param template_name: template
139 | :param **context: parameters for templates.
140 | :yields: HTML strings
141 | '''
142 | app.update_template_context(context)
143 | template = app.jinja_env.get_template(template_name)
144 | stream = template.generate(context)
145 | return Response(stream_with_context(stream))
146 |
147 |
148 | @app.context_processor
149 | def template_globals():
150 | return {
151 | 'manager': app.extensions['plugin_manager'],
152 | 'len': len,
153 | }
154 |
155 |
156 | @app.route('/sort/', defaults={"path": ""})
157 | @app.route('/sort//')
158 | def sort(property, path):
159 | try:
160 | directory = Node.from_urlpath(path)
161 | except OutsideDirectoryBase:
162 | return NotFound()
163 |
164 | if not directory.is_directory or directory.is_excluded:
165 | return NotFound()
166 |
167 | data = [
168 | (cpath, cprop)
169 | for cpath, cprop in iter_cookie_browse_sorting(request.cookies)
170 | if cpath != path
171 | ]
172 | data.append((path, property))
173 | raw_data = base64.b64encode(json.dumps(data).encode('utf-8'))
174 |
175 | # prevent cookie becoming too large
176 | while len(raw_data) > 3975: # 4000 - len('browse-sorting=""; Path=/')
177 | data.pop(0)
178 | raw_data = base64.b64encode(json.dumps(data).encode('utf-8'))
179 |
180 | response = redirect(url_for(".browse", path=directory.urlpath))
181 | response.set_cookie('browse-sorting', raw_data)
182 | return response
183 |
184 |
185 | @app.route("/browse", defaults={"path": ""})
186 | @app.route('/browse/')
187 | def browse(path):
188 | sort_property = get_cookie_browse_sorting(path, 'text')
189 | sort_fnc, sort_reverse = browse_sortkey_reverse(sort_property)
190 |
191 | try:
192 | directory = Node.from_urlpath(path)
193 | if directory.is_directory and not directory.is_excluded:
194 | return stream_template(
195 | 'browse.html',
196 | file=directory,
197 | sort_property=sort_property,
198 | sort_fnc=sort_fnc,
199 | sort_reverse=sort_reverse
200 | )
201 | except OutsideDirectoryBase:
202 | pass
203 | return NotFound()
204 |
205 |
206 | @app.route('/open/', endpoint="open")
207 | def open_file(path):
208 | try:
209 | file = Node.from_urlpath(path)
210 | if file.is_file and not file.is_excluded:
211 | return send_from_directory(file.parent.path, file.name)
212 | except OutsideDirectoryBase:
213 | pass
214 | return NotFound()
215 |
216 |
217 | @app.route("/download/file/")
218 | def download_file(path):
219 | try:
220 | file = Node.from_urlpath(path)
221 | if file.is_file and not file.is_excluded:
222 | return file.download()
223 | except OutsideDirectoryBase:
224 | pass
225 | return NotFound()
226 |
227 |
228 | @app.route("/download/directory/.tgz")
229 | def download_directory(path):
230 | try:
231 | directory = Node.from_urlpath(path)
232 | if directory.is_directory and not directory.is_excluded:
233 | return directory.download()
234 | except OutsideDirectoryBase:
235 | pass
236 | return NotFound()
237 |
238 |
239 | @app.route("/remove/", methods=("GET", "POST"))
240 | def remove(path):
241 | try:
242 | file = Node.from_urlpath(path)
243 | except OutsideDirectoryBase:
244 | return NotFound()
245 |
246 | if not file.can_remove or file.is_excluded or not file.parent:
247 | return NotFound()
248 |
249 | if request.method == 'GET':
250 | return render_template('remove.html', file=file)
251 |
252 | file.remove()
253 | return redirect(url_for(".browse", path=file.parent.urlpath))
254 |
255 |
256 | @app.route("/upload", defaults={'path': ''}, methods=("POST",))
257 | @app.route("/upload/", methods=("POST",))
258 | def upload(path):
259 | try:
260 | directory = Node.from_urlpath(path)
261 | except OutsideDirectoryBase:
262 | return NotFound()
263 |
264 | if (
265 | not directory.is_directory or
266 | not directory.can_upload or
267 | directory.is_excluded
268 | ):
269 | return NotFound()
270 |
271 | for v in request.files.listvalues():
272 | for f in v:
273 | filename = secure_filename(f.filename)
274 | if filename:
275 | filename = directory.choose_filename(filename)
276 | filepath = os.path.join(directory.path, filename)
277 | f.save(filepath)
278 | else:
279 | raise InvalidFilenameError(
280 | path=directory.path,
281 | filename=f.filename
282 | )
283 | return redirect(url_for(".browse", path=directory.urlpath))
284 |
285 |
286 | @app.route("/")
287 | def index():
288 | path = app.config["directory_start"] or app.config["directory_base"]
289 | try:
290 | urlpath = Node(path).urlpath
291 | except OutsideDirectoryBase:
292 | return NotFound()
293 | return browse(urlpath)
294 |
295 |
296 | @app.after_request
297 | def page_not_found(response):
298 | if response.status_code == 404:
299 | return make_response((render_template('404.html'), 404))
300 | return response
301 |
302 |
303 | @app.errorhandler(InvalidPathError)
304 | def bad_request_error(e):
305 | file = None
306 | if hasattr(e, 'path'):
307 | if isinstance(e, InvalidFilenameError):
308 | file = Node(e.path)
309 | else:
310 | file = Node(e.path).parent
311 | return render_template('400.html', file=file, error=e), 400
312 |
313 |
314 | @app.errorhandler(OutsideRemovableBase)
315 | @app.errorhandler(404)
316 | def page_not_found_error(e):
317 | return render_template('404.html'), 404
318 |
319 |
320 | @app.errorhandler(500)
321 | def internal_server_error(e): # pragma: no cover
322 | logger.exception(e)
323 | return getattr(e, 'message', 'Internal server error'), 500
324 |
--------------------------------------------------------------------------------
/browsepy/compat.py:
--------------------------------------------------------------------------------
1 | #!/usr/bin/env python
2 | # -*- coding: UTF-8 -*-
3 |
4 | import os
5 | import os.path
6 | import sys
7 | import itertools
8 |
9 | import warnings
10 | import functools
11 |
12 | import posixpath
13 | import ntpath
14 | import errno
15 |
16 | FS_ENCODING = sys.getfilesystemencoding()
17 | PY_LEGACY = sys.version_info < (3, )
18 | TRUE_VALUES = frozenset(('true', 'yes', '1', 'enable', 'enabled', True, 1))
19 |
20 | try:
21 | from os import scandir, walk
22 | except ImportError:
23 | from scandir import scandir, walk # noqa
24 |
25 | try:
26 | from shutil import get_terminal_size
27 | except ImportError:
28 | from backports.shutil_get_terminal_size import get_terminal_size # noqa
29 |
30 |
31 | def isexec(path):
32 | '''
33 | Check if given path points to an executable file.
34 |
35 | :param path: file path
36 | :type path: str
37 | :return: True if executable, False otherwise
38 | :rtype: bool
39 | '''
40 | return os.path.isfile(path) and os.access(path, os.X_OK)
41 |
42 |
43 | def fsdecode(path, os_name=os.name, fs_encoding=FS_ENCODING, errors=None):
44 | '''
45 | Decode given path.
46 |
47 | :param path: path will be decoded if using bytes
48 | :type path: bytes or str
49 | :param os_name: operative system name, defaults to os.name
50 | :type os_name: str
51 | :param fs_encoding: current filesystem encoding, defaults to autodetected
52 | :type fs_encoding: str
53 | :return: decoded path
54 | :rtype: str
55 | '''
56 | if not isinstance(path, bytes):
57 | return path
58 | if not errors:
59 | use_strict = PY_LEGACY or os_name == 'nt'
60 | errors = 'strict' if use_strict else 'surrogateescape'
61 | return path.decode(fs_encoding, errors=errors)
62 |
63 |
64 | def fsencode(path, os_name=os.name, fs_encoding=FS_ENCODING, errors=None):
65 | '''
66 | Encode given path.
67 |
68 | :param path: path will be encoded if not using bytes
69 | :type path: bytes or str
70 | :param os_name: operative system name, defaults to os.name
71 | :type os_name: str
72 | :param fs_encoding: current filesystem encoding, defaults to autodetected
73 | :type fs_encoding: str
74 | :return: encoded path
75 | :rtype: bytes
76 | '''
77 | if isinstance(path, bytes):
78 | return path
79 | if not errors:
80 | use_strict = PY_LEGACY or os_name == 'nt'
81 | errors = 'strict' if use_strict else 'surrogateescape'
82 | return path.encode(fs_encoding, errors=errors)
83 |
84 |
85 | def getcwd(fs_encoding=FS_ENCODING, cwd_fnc=os.getcwd):
86 | '''
87 | Get current work directory's absolute path.
88 | Like os.getcwd but garanteed to return an unicode-str object.
89 |
90 | :param fs_encoding: filesystem encoding, defaults to autodetected
91 | :type fs_encoding: str
92 | :param cwd_fnc: callable used to get the path, defaults to os.getcwd
93 | :type cwd_fnc: Callable
94 | :return: path
95 | :rtype: str
96 | '''
97 | path = fsdecode(cwd_fnc(), fs_encoding=fs_encoding)
98 | return os.path.abspath(path)
99 |
100 |
101 | def getdebug(environ=os.environ, true_values=TRUE_VALUES):
102 | '''
103 | Get if app is expected to be ran in debug mode looking at environment
104 | variables.
105 |
106 | :param environ: environment dict-like object
107 | :type environ: collections.abc.Mapping
108 | :returns: True if debug contains a true-like string, False otherwise
109 | :rtype: bool
110 | '''
111 | return environ.get('DEBUG', '').lower() in true_values
112 |
113 |
114 | def deprecated(func_or_text, environ=os.environ):
115 | '''
116 | Decorator used to mark functions as deprecated. It will result in a
117 | warning being emmitted hen the function is called.
118 |
119 | Usage:
120 |
121 | >>> @deprecated
122 | ... def fnc():
123 | ... pass
124 |
125 | Usage (custom message):
126 |
127 | >>> @deprecated('This is deprecated')
128 | ... def fnc():
129 | ... pass
130 |
131 | :param func_or_text: message or callable to decorate
132 | :type func_or_text: callable
133 | :param environ: optional environment mapping
134 | :type environ: collections.abc.Mapping
135 | :returns: nested decorator or new decorated function (depending on params)
136 | :rtype: callable
137 | '''
138 | def inner(func):
139 | message = (
140 | 'Deprecated function {}.'.format(func.__name__)
141 | if callable(func_or_text) else
142 | func_or_text
143 | )
144 |
145 | @functools.wraps(func)
146 | def new_func(*args, **kwargs):
147 | with warnings.catch_warnings():
148 | if getdebug(environ):
149 | warnings.simplefilter('always', DeprecationWarning)
150 | warnings.warn(message, category=DeprecationWarning,
151 | stacklevel=3)
152 | return func(*args, **kwargs)
153 | return new_func
154 | return inner(func_or_text) if callable(func_or_text) else inner
155 |
156 |
157 | def usedoc(other):
158 | '''
159 | Decorator which copies __doc__ of given object into decorated one.
160 |
161 | Usage:
162 |
163 | >>> def fnc1():
164 | ... """docstring"""
165 | ... pass
166 | >>> @usedoc(fnc1)
167 | ... def fnc2():
168 | ... pass
169 | >>> fnc2.__doc__
170 | 'docstring'collections.abc.D
171 |
172 | :param other: anything with a __doc__ attribute
173 | :type other: any
174 | :returns: decorator function
175 | :rtype: callable
176 | '''
177 | def inner(fnc):
178 | fnc.__doc__ = fnc.__doc__ or getattr(other, '__doc__')
179 | return fnc
180 | return inner
181 |
182 |
183 | def pathsplit(value, sep=os.pathsep):
184 | '''
185 | Get enviroment PATH elements as list.
186 |
187 | This function only cares about spliting across OSes.
188 |
189 | :param value: path string, as given by os.environ['PATH']
190 | :type value: str
191 | :param sep: PATH separator, defaults to os.pathsep
192 | :type sep: str
193 | :yields: every path
194 | :ytype: str
195 | '''
196 | for part in value.split(sep):
197 | if part[:1] == part[-1:] == '"' or part[:1] == part[-1:] == '\'':
198 | part = part[1:-1]
199 | yield part
200 |
201 |
202 | def pathparse(value, sep=os.pathsep, os_sep=os.sep):
203 | '''
204 | Get enviroment PATH directories as list.
205 |
206 | This function cares about spliting, escapes and normalization of paths
207 | across OSes.
208 |
209 | :param value: path string, as given by os.environ['PATH']
210 | :type value: str
211 | :param sep: PATH separator, defaults to os.pathsep
212 | :type sep: str
213 | :param os_sep: OS filesystem path separator, defaults to os.sep
214 | :type os_sep: str
215 | :yields: every path
216 | :ytype: str
217 | '''
218 | escapes = []
219 | normpath = ntpath.normpath if os_sep == '\\' else posixpath.normpath
220 | if '\\' not in (os_sep, sep):
221 | escapes.extend((
222 | ('\\\\', '', '\\'),
223 | ('\\"', '', '"'),
224 | ('\\\'', '', '\''),
225 | ('\\%s' % sep, '', sep),
226 | ))
227 | for original, escape, unescape in escapes:
228 | value = value.replace(original, escape)
229 | for part in pathsplit(value, sep=sep):
230 | if part[-1:] == os_sep and part != os_sep:
231 | part = part[:-1]
232 | for original, escape, unescape in escapes:
233 | part = part.replace(escape, unescape)
234 | yield normpath(fsdecode(part))
235 |
236 |
237 | def pathconf(path,
238 | os_name=os.name,
239 | isdir_fnc=os.path.isdir,
240 | pathconf_fnc=getattr(os, 'pathconf', None),
241 | pathconf_names=getattr(os, 'pathconf_names', ())):
242 | '''
243 | Get all pathconf variables for given path.
244 |
245 | :param path: absolute fs path
246 | :type path: str
247 | :returns: dictionary containing pathconf keys and their values (both str)
248 | :rtype: dict
249 | '''
250 |
251 | if pathconf_fnc and pathconf_names:
252 | pathconf_output = {}
253 | for key in pathconf_names:
254 | try:
255 | pathconf_output[key] = pathconf_fnc(path, key)
256 | except OSError as exc:
257 | if exc.errno != errno.EINVAL:
258 | raise
259 | return pathconf_output
260 | if os_name == 'nt':
261 | maxpath = 246 if isdir_fnc(path) else 259 # 260 minus
262 | else:
263 | maxpath = 255 # conservative sane default
264 | return {
265 | 'PC_PATH_MAX': maxpath,
266 | 'PC_NAME_MAX': maxpath - len(path),
267 | }
268 |
269 |
270 | ENV_PATH = tuple(pathparse(os.getenv('PATH', '')))
271 | ENV_PATHEXT = tuple(pathsplit(os.getenv('PATHEXT', '')))
272 |
273 |
274 | def which(name,
275 | env_path=ENV_PATH,
276 | env_path_ext=ENV_PATHEXT,
277 | is_executable_fnc=isexec,
278 | path_join_fnc=os.path.join,
279 | os_name=os.name):
280 | '''
281 | Get command absolute path.
282 |
283 | :param name: name of executable command
284 | :type name: str
285 | :param env_path: OS environment executable paths, defaults to autodetected
286 | :type env_path: list of str
287 | :param is_executable_fnc: callable will be used to detect if path is
288 | executable, defaults to `isexec`
289 | :type is_executable_fnc: Callable
290 | :param path_join_fnc: callable will be used to join path components
291 | :type path_join_fnc: Callable
292 | :param os_name: os name, defaults to os.name
293 | :type os_name: str
294 | :return: absolute path
295 | :rtype: str or None
296 | '''
297 | for path in env_path:
298 | for suffix in env_path_ext:
299 | exe_file = path_join_fnc(path, name) + suffix
300 | if is_executable_fnc(exe_file):
301 | return exe_file
302 | return None
303 |
304 |
305 | def re_escape(pattern, chars=frozenset("()[]{}?*+|^$\\.-#")):
306 | '''
307 | Escape all special regex characters in pattern.
308 | Logic taken from regex module.
309 |
310 | :param pattern: regex pattern to escape
311 | :type patterm: str
312 | :returns: escaped pattern
313 | :rtype: str
314 | '''
315 | escape = '\\{}'.format
316 | return ''.join(
317 | escape(c) if c in chars or c.isspace() else
318 | '\\000' if c == '\x00' else c
319 | for c in pattern
320 | )
321 |
322 |
323 | if PY_LEGACY:
324 | FileNotFoundError = OSError # noqa
325 | range = xrange # noqa
326 | filter = itertools.ifilter
327 | basestring = basestring # noqa
328 | unicode = unicode # noqa
329 | chr = unichr # noqa
330 | bytes = str # noqa
331 | else:
332 | FileNotFoundError = FileNotFoundError
333 | range = range
334 | filter = filter
335 | basestring = str
336 | unicode = str
337 | chr = chr
338 | bytes = bytes
339 |
--------------------------------------------------------------------------------