templates_wikidir = $(templatesdir)/wiki
+templates_wiki_files_DATA = \
+ src/templates/wiki/files/index.html
+
+templates_wiki_filesdir = $(templates_wikidir)/files
+
templates_wiki_modules_DATA = \
src/templates/wiki/modules/list.html \
src/templates/wiki/modules/navbar.html
"""
self.db.execute("REFRESH MATERIALIZED VIEW wiki_search_index")
+ # Files
+
+ def _get_files(self, query, *args):
+ res = self.db.query(query, *args)
+
+ for row in res:
+ yield File(self.backend, row.id, data=row)
+
+ def _get_file(self, query, *args):
+ res = self.db.get(query, *args)
+
+ if res:
+ return File(self.backend, res.id, data=res)
+
+ def get_files(self, path):
+ files = self._get_files("SELECT * FROM wiki_files \
+ WHERE path = %s AND deleted_at IS NULL ORDER BY filename", path)
+
+ return list(files)
+
+ def get_file_by_path(self, path):
+ path, filename = os.path.dirname(path), os.path.basename(path)
+
+ return self._get_file("SELECT * FROM wiki_files \
+ WHERE path = %s AND filename = %s AND deleted_at IS NULL", path, filename)
+
+ def upload(self, path, filename, data, mimetype, author, address):
+ # Upload the blob first
+ blob = self.db.get("INSERT INTO wiki_blobs(data) VALUES(%s) RETURNING id", data)
+
+ # Create entry for file
+ return self._get_file("INSERT INTO wiki_files(path, filename, author_uid, address, \
+ mimetype, blob_id, size) VALUES(%s, %s, %s, %s, %s, %s, %s) RETURNING *", path,
+ filename, author.uid, address, mimetype, blob.id, len(data))
+
class Page(misc.Object):
def init(self, id, data=None):
return sidebar
parts.pop()
+
+
+class File(misc.Object):
+ def init(self, id, data):
+ self.id = id
+ self.data = data
+
+ @property
+ def url(self):
+ return os.path.join(self.path, self.filename)
+
+ @property
+ def path(self):
+ return self.data.path
+
+ @property
+ def filename(self):
+ return self.data.filename
+
+ @property
+ def mimetype(self):
+ return self.data.mimetype
+
+ @property
+ def size(self):
+ return self.data.size
+
+ def is_image(self):
+ return self.mimetype.startswith("image/")
+
+ @lazy_property
+ def blob(self):
+ res = self.db.get("SELECT data FROM wiki_blobs \
+ WHERE id = %s", self.data.blob_id)
+
+ if res:
+ return bytes(res.data)
--- /dev/null
+{% extends "../base.html" %}
+
+{% block title %}{{ _("Files") }}{% end block %}
+
+{% block sidebar %}
+ {% set help = backend.wiki.get_page("/wiki/media") %}
+
+ {% if help %}
+ {% raw help.html %}
+ {% end %}
+{% end block %}
+
+{% block main %}
+ {% if files %}
+ <div class="card mb-4">
+ <div class="card-body">
+ <h4 class="card-title">{{ _("Files") }}</h4>
+
+ <div class="row">
+ {% for f in files %}
+ {% if f.is_image() %}
+ <div class="col-sm-6 col-md-4">
+ <figure class="figure">
+ <img class="figure-img img-fluid img-thumbnail" src="{{ f.url }}" alt="{{ f.filename }}">
+ <figcaption class="figure-caption">{{ f.filename }}</figcaption>
+ </figure>
+ </div>
+ {% end %}
+ {% end %}
+ </div>
+
+ <ul class="list-inline">
+ {% for f in files %}
+ {% if not f.is_image() %}
+ <li class="list-inline-item">
+ {% if "pdf" in f.mimetype %}
+ <span class="fas fa-file-pdf fa-fw"></span>
+ {% else %}
+ <span class="fas fa-file fa-fw"></span>
+ {% end %}
+
+ <a href="{{ f.url }}">{{ f.filename }}</a>
+ </li>
+ {% end %}
+ {% end %}
+ </ul>
+ </div>
+ </div>
+ {% end %}
+
+ <div class="card">
+ <div class="card-body">
+ <h6 class="card-title">{{ _("Upload File") }}</h6>
+
+ <form method="POST" action="/actions/upload" enctype="multipart/form-data">
+ {% raw xsrf_form_html() %}
+
+ <input type="hidden" name="path" value="{{ path }}">
+
+ <div class="form-group">
+ <input type="file" class="form-control-file" name="file" required>
+
+ <small class="form-text text-muted">
+ {{ _("Choose a file to upload") }}
+ </small>
+ </div>
+
+ <input class="btn btn-primary btn-block" type="submit" value="{{ _("Upload") }}">
+ </form>
+ </div>
+ </div>
+{% end block %}
self.add_handlers(r"wiki(\.dev)?\.ipfire\.org",
authentication_handlers + [
+ # Actions
+ (r"/actions/upload", wiki.ActionUploadHandler),
+
# Handlers
(r"/recent\-changes", wiki.RecentChangesHandler),
(r"/search", wiki.SearchHandler),
+ # Media
+ (r"([A-Za-z0-9\-_\/]+)?/files", wiki.FilesHandler),
+ (r"((?!/static)(?:[A-Za-z0-9\-_\/]+)?(?:.*)\.(?:\w+))$", wiki.FileHandler),
+
# Render pages
(r"([A-Za-z0-9\-_\/]+)?", wiki.PageHandler),
])
from . import base
from . import ui_modules
+class ActionUploadHandler(auth.CacheMixin, base.BaseHandler):
+ @tornado.web.authenticated
+ def post(self):
+ path = self.get_argument("path")
+
+ try:
+ filename, data, mimetype = self.get_file("file")
+
+ # XXX check valid mimetypes
+
+ with self.db.transaction():
+ file = self.backend.wiki.upload(path, filename, data,
+ mimetype=mimetype, author=self.current_user,
+ address=self.get_remote_ip())
+
+ except TypeError as e:
+ raise e
+
+ self.redirect("%s/files" % path)
+
+
+class FilesHandler(auth.CacheMixin, base.BaseHandler):
+ @tornado.web.authenticated
+ def get(self, path):
+ files = self.backend.wiki.get_files(path)
+
+ self.render("wiki/files/index.html", path=path, files=files)
+
+
+class FileHandler(auth.CacheMixin, base.BaseHandler):
+ def get(self, path):
+ file = self.backend.wiki.get_file_by_path(path)
+ if not file:
+ raise tornado.web.HTTPError(404, "Could not find %s" % path)
+
+ # Set headers
+ self.set_header("Content-Type", file.mimetype or "application/octet-stream")
+ self.set_header("Content-Length", file.size)
+
+ self.finish(file.blob)
+
+
class PageHandler(auth.CacheMixin, base.BaseHandler):
@property
def action(self):
class WikiNavbarModule(ui_modules.UIModule):
def render(self, suffix=None):
+ _ = self.locale.translate
+
breadcrumbs = self.backend.wiki.make_breadcrumbs(self.request.path)
- title = self.backend.wiki.get_page_title(self.request.path)
+ # Don't search for a title for the file manager
+ if self.request.path.endswith("/files"):
+ title = _("Files")
+ else:
+ title = self.backend.wiki.get_page_title(self.request.path)
return self.render_string("wiki/modules/navbar.html",
breadcrumbs=breadcrumbs, page_title=title, suffix=suffix)