changeset 437:3985b3d72353

web: Add basic ability to upload files.
author Ludovic Chabant <ludovic@chabant.com>
date Fri, 14 Apr 2017 21:27:46 -0700
parents 8b777637da75
children b259b440a9bd
files wikked/templates/error-unauthorized-upload.html wikked/templates/upload-file.html wikked/views/__init__.py wikked/views/edit.py wikked/views/history.py wikked/views/read.py wikked/web.py wikked/webimpl/edit.py
diffstat 8 files changed, 160 insertions(+), 9 deletions(-) [+]
line wrap: on
line diff
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/error-unauthorized-upload.html	Fri Apr 14 21:27:46 2017 -0700
@@ -0,0 +1,13 @@
+{% extends 'index.html' %}
+{% block content %}
+<article>
+    <header>
+        <h1>You're not authorized to upload files</h1>
+    </header>
+    <section>
+        <p>It seems you can't upload files.
+                Please <a href="/login">log into an account</a> that has upload permissions.</p>
+    </section>
+</article>
+{% endblock %}
+
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/wikked/templates/upload-file.html	Fri Apr 14 21:27:46 2017 -0700
@@ -0,0 +1,54 @@
+{% extends 'index.html' %}
+{% block content %}
+<article>
+    <header>
+        <h1>Upload File</h1>
+    </header>
+    <section>
+        {% if success %}
+        <h2>File Uploaded!</h2>
+        <section>
+            <p>The file is accessible with the following syntax: <code>{%raw%}{{file: {%endraw%}{{success.example}}{%raw%}}}{%endraw%}</code></p>
+            {% if success.is_page_specific %}
+            <p><strong>Note</strong>: this syntax is valid only on <a href="{{get_read_url(for_page)}}">the current page</a>.</p>
+            {% endif %}
+        </section>
+        {% else %}
+        <form id="file-upload" class="pure-form" action="{{post_back}}" method="POST" enctype="multipart/form-data">
+            <fieldset>
+                {%if error%}
+                <section class="editing-error alert alert-danger">
+                    <p><strong>Error:</strong> <span class="upload-error-message">{{error}}</span></p>
+                </section>
+                {%endif%}
+                <section class="editing-input">
+                    <div class="pure-control-group">
+                        <label for="file">Choose a file:</label>
+                        <input type="file" name="file" />
+                    </div>
+                    <div class="pure-control-group pure-control-addon">
+                        <label for="title" class="">Rename File</label>
+                        <input type="text" name="path" placeholder="Name..." value="{{path}}"></input>
+                    </div>
+                    <div class="pure-form-help">
+                        <span for="title">You can specify a different name and/or path: <code>Folder/Filename</code>.</span>
+                    </div>
+                    {% if for_page %}
+                    <div class="pure-control-group">
+                        <label for="is_page_specific">Page-specific file</label>
+                        <input type="checkbox" name="is_page_specific" value="true"></input>
+                    </div>
+                    <div class="pure-form-help">
+                        <span for="title">Check this to upload this file under: <code>{{for_page}}</code></span>
+                    </div>
+                    {% endif %}
+                    <div class="pure-control-group">
+                        <button name="do-upload" type="submit" class="pure-button pure-button-primary"><span class="fa fa-upload"></span> Upload</button>
+                    </div>
+                </section>
+            </fieldset>
+        </form>
+        {% endif %}
+    </section>
+</article>
+{% endblock %}
--- a/wikked/views/__init__.py	Thu Mar 30 20:14:59 2017 -0700
+++ b/wikked/views/__init__.py	Fri Apr 14 21:27:46 2017 -0700
@@ -82,7 +82,7 @@
 def add_navigation_data(
         url, data, *,
         home=True, new_page=True,
-        read=False, edit=False, history=False, inlinks=False,
+        read=False, edit=False, history=False, inlinks=False, upload=False,
         raw_url=None, extras=None, footers=None):
     if url is not None:
         url = url.lstrip('/')
@@ -112,6 +112,13 @@
             'icon': 'link'
             })
 
+    if upload:
+        nav['extras'].append({
+            'title': "Upload File",
+            'url': url_for('upload_file', p=url),
+            'icon': 'upload'
+        })
+
     if raw_url:
         nav['footers'].append({
             'title': "RAW",
--- a/wikked/views/edit.py	Thu Mar 30 20:14:59 2017 -0700
+++ b/wikked/views/edit.py	Fri Apr 14 21:27:46 2017 -0700
@@ -6,7 +6,7 @@
 from wikked.web import app, get_wiki
 from wikked.webimpl import url_from_viewarg
 from wikked.webimpl.edit import (
-    get_edit_page, do_edit_page, preview_edited_page)
+    get_edit_page, do_edit_page, preview_edited_page, do_upload_file)
 
 
 @app.route('/create/')
@@ -63,7 +63,7 @@
         add_auth_data(data)
         add_navigation_data(
                 url, data,
-                read=True, history=True, inlinks=True,
+                read=True, history=True, inlinks=True, upload=True,
                 raw_url=url_for('api_read_page', url=url.lstrip('/')))
         return render_template('edit-page.html', **data)
 
@@ -89,3 +89,37 @@
 
         else:
             abort(400)
+
+
+@app.route('/upload', methods=['GET', 'POST'])
+@errorhandling_ui2('error-unauthorized-upload.html')
+def upload_file():
+    p = request.args.get('p')
+    data = {
+        'post_back': url_for('upload_file', p=p),
+        'for_page': p
+    }
+    add_auth_data(data)
+    add_navigation_data(p, data)
+
+    if request.method == 'GET':
+        return render_template('upload-file.html', **data)
+
+    if request.method == 'POST':
+        wiki = get_wiki()
+        user = current_user.get_id() or request.remote_addr
+
+        for_url = None
+        is_page_specific = (request.form.get('is_page_specific') == 'true')
+        if is_page_specific:
+            for_url = p
+
+        res = do_upload_file(
+            wiki, user, request.files.get('file'),
+            for_url=for_url)
+
+        data['success'] = {
+            'example': res['example'],
+            'is_page_specific': is_page_specific}
+
+        return render_template('upload-file.html', **data)
--- a/wikked/views/history.py	Thu Mar 30 20:14:59 2017 -0700
+++ b/wikked/views/history.py	Fri Apr 14 21:27:46 2017 -0700
@@ -38,7 +38,7 @@
     add_auth_data(data)
     add_navigation_data(
             url, data,
-            read=True, edit=True, inlinks=True,
+            read=True, edit=True, inlinks=True, upload=True,
             raw_url='/api/history/' + url.lstrip('/'))
     return render_template('history-page.html', **data)
 
@@ -104,4 +104,3 @@
     add_navigation_data(
             '', data)
     return render_template('diff-rev.html', **data)
-
--- a/wikked/views/read.py	Thu Mar 30 20:14:59 2017 -0700
+++ b/wikked/views/read.py	Fri Apr 14 21:27:46 2017 -0700
@@ -56,7 +56,7 @@
     add_auth_data(data)
     add_navigation_data(
             url, data,
-            edit=True, history=True, inlinks=True,
+            edit=True, history=True, inlinks=True, upload=True,
             raw_url='/api/raw/' + url.lstrip('/'))
     return render_template(tpl_name, **data)
 
@@ -94,7 +94,6 @@
     add_auth_data(data)
     add_navigation_data(
             url, data,
-            read=True, edit=True, history=True,
+            read=True, edit=True, history=True, upload=True,
             raw_url='/api/inlinks/' + url.lstrip('/'))
     return render_template('inlinks-page.html', **data)
-
--- a/wikked/web.py	Thu Mar 30 20:14:59 2017 -0700
+++ b/wikked/web.py	Fri Apr 14 21:27:46 2017 -0700
@@ -57,10 +57,12 @@
 
 
 # Make the app serve static content and wiki assets in DEBUG mode.
+app.config['WIKI_ROOT'] = wiki_root
+app.config['WIKI_FILES_DIR'] = os.path.join(wiki_root, '_files')
 if app.config['WIKI_DEV_ASSETS'] or app.config['WIKI_SERVE_FILES']:
     app.wsgi_app = SharedDataMiddleware(
             app.wsgi_app,
-            {'/files': os.path.join(wiki_root, '_files')})
+            {'/files': app.config['WIKI_FILES_DIR']})
 
 
 # In DEBUG mode, also serve raw assets instead of static ones.
--- a/wikked/webimpl/edit.py	Thu Mar 30 20:14:59 2017 -0700
+++ b/wikked/webimpl/edit.py	Fri Apr 14 21:27:46 2017 -0700
@@ -1,5 +1,7 @@
+import os.path
 import logging
 import urllib.parse
+from werkzeug.utils import secure_filename
 from wikked.page import Page, PageData
 from wikked.formatter import PageFormatter, FormattingContext
 from wikked.resolver import PageResolver
@@ -102,3 +104,44 @@
     resolver = PageResolver(dummy)
     dummy._setExtendedData(resolver.run())
     return dummy.text
+
+
+def do_upload_file(wiki, user, reqfile, for_url=None, submit=True):
+    if not reqfile:
+        raise Exception("No file was specified.")
+    if not reqfile.filename:
+        raise Exception("No file name was specified.")
+
+    # TODO: check permissions for the user.
+
+    filename = secure_filename(reqfile.filename)
+
+    files_dir = os.path.join(wiki.root, '_files')
+    upload_dir = files_dir
+    if for_url:
+        upload_dir = os.path.join(wiki.root, for_url)
+
+    path = os.path.join(upload_dir, filename)
+    path = os.path.normpath(path)
+    if not path.startswith(wiki.root):
+        raise Exception("Don't try anything weird, please.")
+
+    # Save to disk.
+    os.makedirs(os.path.dirname(path), exist_ok=True)
+    reqfile.save(path)
+
+    # Commit the file to the source-control.
+    if submit:
+        commit_meta = {
+            'author': user,
+            'message': "Uploaded '%s'." % filename}
+        wiki.scm.commit([path], commit_meta)
+
+    if for_url:
+        example = './%s' % filename
+    else:
+        example = os.path.relpath(path, files_dir)
+    result = {
+        'example': example
+    }
+    return result