Home > Software engineering >  How to serve static site with flask
How to serve static site with flask

Time:01-23

I use MKdocs to create a very sensitive documentation, and I wont it to be securely available online.
My project is:

  • [V] create encrypted volume on server (i used veracrypt, cli and python compatibility)
  • [V] making a simple page to ask key to decrypt
  • [V] decrypt the volume
  • [X] serve site on flask
  • [V] detect session closure and encrypt volume again

(this way there is not persistent clear data on server)

Documentation get edited only locally, encrypted and rsynced to server.

Now, I'm blocked on serving site on flask step, because the MKdocs site is structured like this:

├── 404.html
├── documentation
│   ├── Folder 1
│   │   ├── Sub folder 1
│   │   │   └── index.html
│   │   └── Sub Folder 2
│   │       └── index.html
│   ├── Folder 2
├── css
│   └── ...
├── fonts
│   └── ...
├── img
│   └── ...
├── index.html
├── js
│   └── ...
├── search
│   └── ...
└── ...

I tryed to put this content under ./static/site/ then replacing all the local href and src links with the flask

{{ url_for('static', filename='site/[original_content]) }}

But as far I can tell it can't work on a static call like

app.send_static_file('site/index.html')

Because there is no replacement of {{ .. }} content (and I expected this problem, is a static page).

The question: Is there a way to serve a static site with a "complex" structure like this, with flask?

MORE and not necessary information, spoiler is not part of question!:

As far as I could accept any suggestion on how to do the whole thing differently, now, I would like to achieve it.
Other solution I thought of:
- serving with nginx the static website, on an empty folder, where flask with previous mechanics would decrypt the volume. I don't know how to detect end nginx session to crypt again.
- Condense ALL the structure in one single big HTML file. This would be awesome but I lost really too much time searching and trying, but... not lucky research. (but would be my referred solution

CodePudding user response:

We can consider MkDocs's rendered HTML pages as simple Flask templates. So all we have to do is to create a Flask endpoint that first do the permissions-checking then based on the URL, serves the rendered MkDocs HTML file or a related static file.

Let's call our new endpoint bridge. First, put everything from MkDocs site directory to Flask's templates/bridge directory. My example MkDocs tree looks like this:

$ tree templates/bridge
templates/bridge
├── 404.html
├── css
│   ├── base.css
│   ├── bootstrap.min.css
│   └── font-awesome.min.css
├── dir1
│   ├── sub1
│   │   └── index.html
│   └── sub2
│       └── index.html
├── dir2
│   ├── sub1
│   │   └── index.html
│   └── sub2
│       └── index.html
├── fonts
│   ├── fontawesome-webfont.eot
│   ├── fontawesome-webfont.svg
│   ├── fontawesome-webfont.ttf
│   ├── fontawesome-webfont.woff
│   └── fontawesome-webfont.woff2
├── img
│   ├── favicon.ico
│   └── grid.png
├── index.html
├── js
│   ├── base.js
│   ├── bootstrap.min.js
│   └── jquery-1.10.2.min.js
├── search
│   ├── lunr.js
│   ├── main.js
│   ├── search_index.json
│   └── worker.js
├── sitemap.xml
└── sitemap.xml.gz

And our new Flask bridge endpoint:

from flask import Flask, render_template, send_from_directory

app = Flask(__name__)

@app.route('/bridge/')
@app.route('/bridge/<path:p1>/')
@app.route('/bridge/<path:p1>/<path:p2>/')
@app.route('/bridge/<path:p1>/<path:p2>/<path:p3>/')
def bridge(p1=None, p2=None, p3=None):
    # Permissions checking...

    # Serve MkDocs's static files requested from CSS files
    if p1 == 'css' and p2 in ('img', 'fonts'):
        # CSS fix, e.g. /bridge/css/img/example.png -> /bridge/img/example.png
        return send_from_directory(f'templates/bridge/{p2}/', p3)

    # Serve MkDocs's static files
    if p1 in ('css', 'js', 'fonts', 'search'):
        return send_from_directory(f'templates/bridge/{p1}/', p2)

    # Serve rendered MkDocs HTML files
    if p3 != None:
        template = f'bridge/{p1}/{p2}/{p3}/index.html'
    elif p2 != None:
        template = f'bridge/{p1}/{p2}/index.html'
    elif p1 != None:
        template = f'bridge/{p1}/index.html'
    else:
        template = 'bridge/index.html'

    return render_template(template)

As you see we created a couple of path definitions. Since MkDocs uses relative paths everywhere, the URL path dir1/sub1/ in MkDocs will become https://yoursite.com/bridge/dir1/sub1/ so we can catch them with this URL routing scheme and the URL parts will land into p1, p2, p3 path variables that we will use to serve the corresponding content.

There are two types of content: static files (e.g. CSS files or images) and HTML content files. The static files will be in the css, js, images, fonts, etc. directory, so when p1 equals one of these we use Flask's send_from_directory function to serve them. Static files referenced from CSS files need to have a specific treatment because MkDocs uses relative paths there as well.

And for the rendered index.html files we just need to determine the nesting level based on the path and return the selected index.html file as a normal Flask template. Since MkDocs uses relative URLs we don't have to modify anything in the rendered HTML files, every URL will receive the /bridge/ prefix, so we can serve them with our bridge endpoint.

You should do the permissions checking at the beginning of bridge (or as a decorator, depending your solution). You may need to add p4 and/or p5 path variables if you have more deeply nested content, but it's a straightforward extension of my example.

Note: the 404 error page will be also served by Flask.

CodePudding user response:

TOTALLY thanks to Dauros (i cannot upvote you, but would need at least 1000 votes!) but will check you as answer! Again, thanks for the effort and time you spent for me!

I had a lot of problems with your code, I redirected to /bridge/index.html after decrypt, and anything clicked from there on would begin with /bridge/index.html/resource/p1/p2/etc..

After that I had to reconsider what where p1, p2 and others, the solution I found BASED totally on Dauros solution, that I really like is:

@app.route('/<path:p1>/')
@app.route('/<path:p1>/<path:p2>/')
@app.route('/<path:p1>/<path:p2>/<path:p3>/')
@app.route('/<path:p1>/<path:p2>/<path:p3>/<path:p4>/')
@app.route('/<path:p1>/<path:p2>/<path:p3>/<path:p4>/<path:p5>/')
@app.route('/<path:p1>/<path:p2>/<path:p3>/<path:p4>/<path:p5>/<path:p6>/')
def bridge(p1=None, p2=None, p3=None, p4=None, p5=None, p6=None):
    # Permissions checking...
    # I'm planning on using basic auth from nginx to even try to show a page
    
    resource = '/'.join([p for p in (p1,p2,p3,p4,p5,p6) if p])
        
    the_file = resource.split('/')[-1]
    the_path = '/'.join(resource.split('/')[:-1])
    
    if p1 in ('css', 'fonts', 'img', 'js', 'search'):
        return send_from_directory(f'templates/bridge/{the_path}/', the_file)

    else:
        template = f'bridge/{resource}/index.html'
        return render_template(template)


@app.route('/', methods=HTTP_METHODS)
def home():
    method = request.method
    if method == "GET":
        # TO BE CHANGED, checking if volume is already decrypted.
        if os.path.exists(f"{BASE_FODLER}/locked"):
            # decrypt page
            resp = make_response(render_template('decrypt.html'))
            return resp
        else:
            template = 'bridge/index.html'
            return render_template(template)
    elif method == "POST":
        if 'password' in request.form:
            decrypt_key = request.form['password']
            # decryption omitted
        
            template = 'bridge/index.html'
            return render_template(template)
        else:
            return not_allowed_ans()
        
    else:
        return not_allowed_ans()
    
    return SendOk()

As you see, from bottom, template = 'bridge/index.html' and then return render_template(template) bring the bridge site on root mystime.com/ this means all the route have to be root.
Any "link" will be then: mysite.com/folder_1/sub_folder_1/index.html
After that I made it simple for me to add pX in future (would be nice a single route with dynamic PXs, BUT, this is far enough for a static documentation!)

I don't know why my question has a "-1" vote... If i click on "eidt (1)" I see: Approve, reject, or improve this suggested edit.... I can't find suggested edit, hence, I didn't learn anything from the "-1" vote, and any error that lead to that vote, will be done again if I need again help here.

  •  Tags:  
  • Related