Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

FastCGI-support #5

Merged
merged 20 commits into from
Sep 18, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion .github/workflows/pylint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
python-version: ["3.8", "3.9", "3.10"]
python-version: ["3.9", "3.10", "3.11", "3.12", "3.13.0-rc.2"]
steps:
- uses: actions/checkout@v3
- name: Set up Python ${{ matrix.python-version }}
Expand All @@ -18,6 +18,7 @@ jobs:
run: |
python -m pip install --upgrade pip
pip install pylint
pip install -r requirements.txt
- name: Analysing the code with pylint
run: |
pylint $(git ls-files '*.py')
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -5,3 +5,4 @@ __pycache__/
*~
# C extensions
*.so
.venv
78 changes: 53 additions & 25 deletions __init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -8,23 +8,28 @@
"""

import os
from flask import Flask
import flask
import urllib.parse
from io import StringIO
import re

import flask
from flask import Flask

from . import templatehelper
import urllib.parse
import yaml

def create_app():

def create_application():
"""
Factory for creating a Flask application.
"""
app = Flask(__name__, instance_relative_config=True, static_folder='templates/{}/static'.format(templatehelper.config['template']))
# paths sent by flask are relative to the "public" directory. This prefix should be added to get paths relative to the pages root directory.
application = Flask(
__name__,
instance_relative_config=True,
static_folder=f"templates/{templatehelper.CONFIG['template']}/static")
# paths sent by flask are relative to the "public" directory. This prefix should be added to
# get paths relative to the pages root directory.
pathprefix = ''
@app.route('/<path:path>')

@application.route('/<path:path>')
def serve_directory_or_file(path):
"""
Main handler for all paths
Expand All @@ -35,11 +40,13 @@ def serve_directory_or_file(path):
return serve_error(404, "File not found...")
if os.path.isdir(fullpath):
return serve_directory(path)
else:
# If a specified path exists, dreamhost seems to serve the file directly without invoking this Flask application.
# As nonexistent paths are taken care of above, this else-branch is not expected to ever be called. It is left in as a
# stub though, in case this ever changes.
return serve_file(path)

# If a specified path exists, dreamhost seems to serve the file directly without
# invoking this Flask application.
# As nonexistent paths are taken care of above, this else-branch is not expected to
# ever be called. It is left in as a
# stub though, in case this ever changes.
return serve_file(path)

def serve_directory(path):
"""
Expand All @@ -48,27 +55,48 @@ def serve_directory(path):
if not path.endswith('/') and len(path) > 1:
# Ensure paths always end with a "/"
return flask.redirect('/' + path + '/')
else:
return flask.render_template(os.path.join(templatehelper.config['template'], 'directory.html'), pathprefix = pathprefix, path = path, templatehelper = templatehelper)

@app.route('/')
return flask.render_template(
os.path.join(templatehelper.CONFIG['template'], 'directory.html'),
pathprefix = pathprefix,
path = path,
templatehelper = templatehelper)

@application.route('/')
def serve_root():
"""
`@app.route('/<path:path>')` doesn't match '/' and thus this convenience function is needed.
`@application.route('/<path:path>')` doesn't match '/' and thus this convenience function
is needed.
"""
return serve_directory_or_file(os.path.curdir)

def serve_error(code, message=None):
if os.path.exists(os.path.join(__name__, 'templates', templatehelper.config['template'], '%d.html' % (code))):
template = '%d.html' % (code)
if os.path.exists(
os.path.join(
__name__,
'templates',
templatehelper.CONFIG['template'],
f'{code}.html')):
template = f'{code}.html'
else:
template = 'error.html'
return flask.make_response((flask.render_template(os.path.join(templatehelper.config['template'], template), code=code, message=message, templatehelper=templatehelper, pathprefix=pathprefix, path=''), code, None))
return flask.make_response((
flask.render_template(
os.path.join(templatehelper.CONFIG['template'], template),
code=code,
message=message,
templatehelper=templatehelper,
pathprefix=pathprefix,
path=''),
code,
None))

def serve_file(path):
"""
Left as just a stub as it is unlikely to be called anytime soon.
"""
return flask.send_file(os.path.join('..', 'public', path))

return app
return application

# Create an instance of the scms application.
app = create_app()
app = create_application()
3 changes: 3 additions & 0 deletions requirements.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
flask
pyyaml
regex
15 changes: 9 additions & 6 deletions scms.fcgi.example
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
#!/home/user/venv/bin/python
#!/home/<dreamhostuser>/venv/bin/python

# scms Copyright (C) 2020 - nomike <[email protected]>
# This program comes with ABSOLUTELY NO WARRANTY; for details see LICENSE.
# This is free software, and you are welcome to redistribute it
# under certain conditions; type `show c' for details.

import sys

sys.path.append('..')
sys.path.append('.')
sys.path.append('../scms')

from flup.server.fcgi import WSGIServer
from scms import app

if __name__ == '__main__':
WSGIServer(app).run()
from scms import app as application
123 changes: 69 additions & 54 deletions templatehelper.py
Original file line number Diff line number Diff line change
Expand Up @@ -5,33 +5,33 @@
Example:

{% for parentpath, parentname in templatehelper.getparents(path) %}
<a class="powerline__component" href="{{ parentpath }}"><i class="fas fa-folder"></i> {{ parentname }}</a>
<a class="powerline__component" href="{{ parentpath }}">
<i class="fas fa-folder"></i> {{ parentname }}
</a>
{% endfor %}
"""

import fnmatch
import json
import mimetypes
import os
import re

import regex
import mimetypes
import fnmatch
from datetime import datetime, tzinfo, timezone
import yaml
import markdown
import urllib
import json

config = None
with open("../config.yaml") as file:
config = yaml.load(file, Loader=yaml.SafeLoader)
CONFIG = None
with open("../config.yaml", encoding='utf-8') as file:
CONFIG = yaml.load(file, Loader=yaml.SafeLoader)


# paths sent by flask are relative to the "public" directory. This prefix should be added to get paths relative to the pages root directory.
#TODO: This is a redundant specification and should be avoided.
pathprefix = ''
# paths sent by flask are relative to the "public" directory. This prefix should be added to get
# paths relative to the pages root directory.
PATHPREFIX = ''

# List of official MIME Types: http://www.iana.org/assignments/media-types/media-types.xhtml
# If you want additional mimetypes to be covered, add them to this list.
# The types map to FontAwesome identifiers. Check out https://fontawesome.com/icons?d=gallery for a list of available images.
# The types map to FontAwesome identifiers. Check out https://fontawesome.com/icons?d=gallery
# for a list of available images.
mimetype_fas_mapping = {
# Media
'image': 'fa-file-image',
Expand Down Expand Up @@ -62,22 +62,27 @@ def listdir(path):
List all child-elements of the specified path.
Hidden files and, files ending in ".scmsfasicon" and files ending with a "~" are ignored.

You can also ignore additional files by creating a file called ".scmsignore" in the current folder.
You can also ignore additional files by creating a file called ".scmsignore" in the current
folder.
All files listed in there will not be listed.

If a file named "index" is present, it is supposed to be rendered as the main content of the page
and thus it will be ommited from the list as well.
If a file named "index" is present, it is supposed to be rendered as the main content of the
page and thus it will be ommited from the list as well.
"""
ignorelist = ['index', 'index.md', '*.scmsfasicon', '*.scmstarget']
if os.path.exists(os.path.join(pathprefix, path, '.scmsignore')):
with open(os.path.join(pathprefix, path, '.scmsignore')) as file:
ignorelist.extend([line.strip('\n') for line in file.readlines()])
dirlist = [os.path.basename(f) for f in os.listdir(os.path.join(pathprefix, path)) if regex.match('^(?!\\.).*(?<!~)$', f) and not f in ignorelist]
if os.path.exists(os.path.join(PATHPREFIX, path, '.scmsignore')):
with open(os.path.join(PATHPREFIX, path, '.scmsignore'), encoding='utf-8') as scsmignore:
ignorelist.extend([line.strip('\n') for line in scsmignore.readlines()])
dirlist = [
os.path.basename(f)
for f in os.listdir(os.path.join(PATHPREFIX, path))
if regex.match('^(?!\\.).*(?<!~)$', f) and not f in ignorelist
]
removeitems = []
for dir in dirlist:
for directory in dirlist:
for ignore in ignorelist:
if fnmatch.fnmatch(dir, ignore):
removeitems.append(dir)
if fnmatch.fnmatch(directory, ignore):
removeitems.append(directory)
for removeitem in removeitems:
dirlist.remove(removeitem)
dirlist.sort()
Expand All @@ -88,26 +93,31 @@ def listchildren(path):
List all child-elements of the specified path.
Hidden files and, files ending in ".scmsfasicon" and files ending with a "~" are ignored.

You can also ignore additional files by creating a file called ".scmsignore" in the current folder.
You can also ignore additional files by creating a file called ".scmsignore" in the current
folder.
All files listed in there will not be listed.

If a file named "index" is present, it is supposed to be rendered as the main content of the page
and thus it will be ommited from the list as well.
If a file named "index" is present, it is supposed to be rendered as the main content of the
page and thus it will be ommited from the list as well.
"""
ignorelist = ['index', 'index.md', '*.scmsfasicon', '*.scmstarget']
if os.path.exists(os.path.join(pathprefix, path, '.scmsignore')):
with open(os.path.join(pathprefix, path, '.scmsignore')) as file:
ignorelist.extend([line.strip('\n') for line in file.readlines()])
dirlist = [[os.path.basename(f), os.path.basename(f)] for f in os.listdir(os.path.join(pathprefix, path)) if regex.match('^(?!\\.).*(?<!~)$', f) and not f in ignorelist]
if os.path.exists(os.path.join(pathprefix, path, '.scmslinks')):
with open(os.path.join(pathprefix, path, '.scmslinks')) as file:
additional_links = json.load(file)
if os.path.exists(os.path.join(PATHPREFIX, path, '.scmsignore')):
with open(os.path.join(PATHPREFIX, path, '.scmsignore'), encoding='utf-8') as scmsignore:
ignorelist.extend([line.strip('\n') for line in scmsignore.readlines()])
dirlist = [
[os.path.basename(f), os.path.basename(f)]
for f in os.listdir(os.path.join(PATHPREFIX, path))
if regex.match('^(?!\\.).*(?<!~)$', f) and not f in ignorelist
]
if os.path.exists(os.path.join(PATHPREFIX, path, '.scmslinks')):
with open(os.path.join(PATHPREFIX, path, '.scmslinks'), encoding='utf-8') as scmslinks:
additional_links = json.load(scmslinks)
dirlist.extend(additional_links)
removeitems = []
for dir in [item[0] for item in dirlist]:
for directory in [item[0] for item in dirlist]:
for ignore in ignorelist:
if fnmatch.fnmatch(dir, ignore):
removeitems.append(dir)
if fnmatch.fnmatch(directory, ignore):
removeitems.append(directory)
dirlist = [item for item in dirlist if item[0] not in removeitems]
dirlist.sort()
return dirlist
Expand All @@ -118,12 +128,14 @@ def getparents(path):
Return a list of tupels with all parent elements.
Tupels have the format
(path, basename)
path: the full path relative to the "public" folder, leading to the parent, including the basename
path: the full path relative to the "public" folder, leading to the parent, including the
basename
basename: only the basename of the parent
"""
pathelements = path.split(os.path.sep)[:-1]
parents = []
i = 0
# pylint: disable=consider-using-enumerate
for i in range(0, len(pathelements)):
parents.append(('/' + '/'.join(pathelements[:i+1]), pathelements[i]))
return parents
Expand All @@ -134,18 +146,17 @@ def readfile(path, default=None):
"""
if not os.path.exists(path) and default:
return default
with open(path, 'r') as file:
return file.read()
with open(path, 'rb') as requested_file:
return requested_file.read()

def getfasicon(path):
"""
Check if a file named basename(path) + '.scmfasicon' exists, and return it's content.
If not, handover to getfastype(path)
"""
if os.path.isfile(os.path.join(pathprefix, path) + '.scmsfasicon'):
return readfile(os.path.join(pathprefix, path) + '.scmsfasicon')
else:
return getfastype(path)
if os.path.isfile(os.path.join(PATHPREFIX, path) + '.scmsfasicon'):
return readfile(os.path.join(PATHPREFIX, path) + '.scmsfasicon')
return getfastype(path)

def getfastype(path):
"""
Expand All @@ -155,30 +166,34 @@ def getfastype(path):
(the part of the mime-type before the slash).
If this fails as well, fallback to a default.
"""
if os.path.isdir(os.path.join(pathprefix, path)):
if os.path.isdir(os.path.join(PATHPREFIX, path)):
return "fa-folder"

mimetype = mimetypes.guess_type(path)[0]
if not mimetype == None:
if not mimetype is None:
if mimetype in mimetype_fas_mapping:
return mimetype_fas_mapping[mimetype]
if mimetype.split('/')[0] in mimetype_fas_mapping:
return mimetype_fas_mapping[mimetype.split('/')[0]]
return 'fa-file'

def getlastmodifiedfile(path):
"""
Recursively search for the newest file in the specified directory.
"""

path = os.path.join('..', path)
assert(os.path.isdir(path))
assert os.path.isdir(path)
newest = {"file": path, "timestamp": os.path.getmtime(path)}
for root, dirs, files in os.walk(path):
for path in dirs:
timestamp = os.path.getmtime(os.path.join(root, path))
for directory in dirs:
timestamp = os.path.getmtime(os.path.join(root, directory))
if timestamp > newest['timestamp']:
newest['file'] = os.path.join(root, path)
newest['file'] = os.path.join(root, directory)
newest['timestamp'] = timestamp
for path in files:
timestamp = os.path.getmtime(os.path.join(root, path))
for directory in files:
timestamp = os.path.getmtime(os.path.join(root, directory))
if timestamp > newest['timestamp']:
newest['file'] = os.path.join(root, path)
newest['file'] = os.path.join(root, directory)
newest['timestamp'] = timestamp
return newest
Loading
Loading