How to write your own Web File Browser

Πως να γράψετε τον δικό σας Διαδικτυακό Περιηγητή Αρχείων

· Coding Προγραμματισμός · python python flask flask web διαδίκτυο file αρχείο browser περιηγητής

IntroductionΕισαγωγή

In this article, we will show how to implement a Web File Browser using the Python programming language and the Flask micro web framework. This will be a basic skeleton design. You can use it to develop your own "cloud" software which you can install it on a server and thus, have access to your files through a web interface. You can add whatever features you want.

Σε αυτό το άρθρο, θα δείξουμε πώς να υλοποιούμε έναν Διαδικτυακό Περιηγητή Αρχείων χρησιμοποιώντας τη γλώσσα προγραμματισμόυ Python και τη μικρo-πλατφόρμα Flask. Αυτό που κάνουμε θα είναι ένα βασικό σχέδιο σκελετός. Μπορείτε να το χρησιμοποιήσετε για να αναπτύξετε το δικό σας λογισμικό απομακρυσμένης πρόσβασης, το οποίο μπορείτε να εγκαταστήσετε σε έναν διακομιστή και έτσι να έχετε πρόσβαση στα αρχεία σας μέσω μιας διεπαφής ιστού (web interface). Μπορείτε να προσθέσετε οποιεσδήποτε λειτουργίες θέλετε.

Backend codeΚώδικας πίσω μέρους

requirements.txt

First of all, let's see what python packages we are going to make use of:

Πρώτα από όλα, ας δούμε τι πακέτα Python πρόκειται να χρησιμοποιήσουμε:

flask
flask-talisman
redis>=3.0.0
rq>=1.2.0

Talisman is a small Flask extension that facilitates the protection against a few common web application security issues.

RQ (Redis Queue) is a simple Python library (backed by the Redis database) for queueing jobs and processing them in parallel in the background with workers.

Το Talisman είναι μια μικρή επέκταση για το Flask που μας διευκολύνει στην προστασία έναντι μερικών κοινών θεμάτων ασφαλείας στις διαδικτυακές εφαρμογές.

Το RQ (Redis Queue) είναι μια απλή βιβλιοθήκη της Python (υποστηριζόμενη από τη βάση δεδομένων Redis) για την αναμονή εργασιών σε σειρά και την παράλληλη επεξεργασία τους στο περιθώριο μέσω εργατών.

config.py

This is our configuration file:

Αυτό είναι το αρχείο ρυθμίσεών μας:

# Enable/Disable Debugging
DEBUG = False

# What paths to show in our Web File Browser
SHARES = ["/mnt/MEDIA/COMICS", "/mnt/MEDIA/IMAGES"]

# Show hidden (.dot) files?
SHOW_HIDDEN = False

# Absolute static path
ABS_STATIC_PATH = "/var/www/fmsuite/fmsuite/static/"

# Theme config
THEME = "red"
REL_THEME_PATH = "img/themes/" + THEME + "/"
ABS_THEME_PATH = ABS_STATIC_PATH + REL_THEME_PATH

# Thumbnails config
THUMBNAILS = True
THUMB_SIZE = (128, 128)
THUMB_CREATION_TIMEOUT = 30
REL_THUMB_PATH = "img/thumbs/"
ABS_THUMB_PATH = ABS_STATIC_PATH + REL_THUMB_PATH
THUMB_TYPES = ["image", "video", "pdf", "epub", "cbz"]

# Create a secret key (in order to use sessions)
import secrets
SECRET_KEY = secrets.token_hex()

fmsuite/__init__.py

Here is our main file, named __init__.py:

Εδώ είναι το κεντρικό μας αρχείο, που ονομάζεται __init__.py:

import rq
import uuid
import redis
import logging
import mimetypes
from flask import Flask, g, jsonify, render_template, request, send_file, session
from flask_talisman import Talisman
from fmsuite import filesystem
from fmsuite import thumbnail
from config import SHARES, THUMBNAILS, THUMB_SIZE


def generate_csrf_token():
    ''' Generate a unique token in order to prevent
        CSRF (Cross-Site Request Forgery) attacks. '''
    if "_csrf_token" not in session:
        session["_csrf_token"] = str(uuid.uuid4())
    return session["_csrf_token"]


def get_db():
    ''' Get a Redis db connection '''
    # g is a namespace object/proxy that store data during an application context/request.
    db = getattr(g, 'DB', None)
    if db is None:
        g.DB = redis.Redis(host="localhost", port=6379, db=0)
    # If thumbnails are enabled, setup a RQ queue for the jobs
    if THUMBNAILS:
        g.RQ_THUMBS = rq.Queue("thumbs", connection=g.DB)
    else:
        g.RQ_THUMBS = None
    return g.DB


# Init Flask app
app = Flask(__name__, static_folder='static', static_url_path='')
app.config.from_pyfile('../config.py')
app.jinja_env.globals['csrf_token'] = generate_csrf_token
log = logging.getLogger('werkzeug')
log.setLevel(logging.WARNING)


# Content Security Policy for Flask-Talisman
csp = {"default-src": ["'self'", "blob:", "data:"],
       "script-src": ["'self'",
                      "'sha256-2pWe29RAjmUSi77PP/nCW8IcD9XnGs0VJaq+dLPBlzg='" # For Readium
                     ],
       "style-src": ["'self'", "blob:", "'unsafe-inline'"]}

talisman = Talisman(app,
    content_security_policy=csp,
    content_security_policy_nonce_in=["script-src"]
)


@app.before_first_request
def initialization():
    ''' Run only once, before the first request. '''
    # Get a Redis db connection
    get_db()
    # Prepare unique IDs for all the shared dirs and files. All access is done through
    # those unique IDs (thus, we avoid path traversal and many other types of attacks).
    log.warn("Preparing unique IDs for all the shared dirs and files. Please wait...")
    for share in SHARES:
        filesystem.prepare_uids_recursively(share)
    log.warn("READY")


@app.before_request
def _init():
    ''' Run before each request '''
    # Get "view" setting value from cookie (or use value "#gallery-view" as default).
    g.VIEW = request.cookies.get("view") or "#gallery-view"
    # Get a Redis db connection.
    get_db()


@app.route("/")
def index():
    ''' View function for / '''
    dirs, files = filesystem.get_path_contents("/", g.RQ_THUMBS)
    return render_template("folder.html",
                           paths=filesystem.get_paths("/", limits=SHARES),
                           dirs=dirs, files=files)


@app.route("/epub_reader/")
def epub_reader():
    ''' View function for /epub_reader '''
    return render_template("epub_reader.html")


@app.route("/explore/<uid>/")
def explore_path(uid):
    ''' View function for /explore/<uid>/ '''
    path = g.DB.get(uid).decode()
    dirs, files = filesystem.get_path_contents(path, g.RQ_THUMBS)
    return render_template("folder.html",
                           paths=filesystem.get_paths(path, limits=SHARES),
                           dirs=dirs, files=files)


@app.route("/serve/<uid>/")
def serve_file(uid):
    ''' Serve file. '''
    path = g.DB.get(uid).decode()
    mimetype = mimetypes.guess_type(path)[0]
    return send_file(path, mimetype=mimetype, conditional=True)


@app.route("/get/<uid>/")
def get_file(uid):
    ''' Get (i.e. download) file. '''
    path = g.DB.get(uid).decode()
    mimetype = mimetypes.guess_type(path)[0]
    return send_file(path, mimetype=mimetype, as_attachment=True)


@app.route("/job/finished/")
def get_finished_thumbs():
    ''' Get finished thumbnail jobs. '''
    if not g.RQ_THUMBS:
        return jsonify([])
    job_ids = g.RQ_THUMBS.finished_job_registry.get_job_ids()
    if not job_ids:
        if not g.RQ_THUMBS.scheduled_job_registry.get_job_ids():
            if not g.RQ_THUMBS.started_job_registry.get_job_ids():
                return jsonify("empty")
        return jsonify([])
    thumb_ids = [g.DB.get(job_id).decode() for job_id in job_ids]
    # Clean finished jobs
    for job_id in job_ids:
        g.RQ_THUMBS.finished_job_registry.remove(job_id, delete_job=True)
    return jsonify(thumb_ids)


@app.route("/refresh/<uid>/", methods=["POST"])
def refresh_path(uid):
    ''' Refresh thumbnail. '''
    if not g.RQ_THUMBS:
        return jsonify(-1)
    jobid = thumbnail.refresh_thumbnail(uid, THUMB_SIZE, g.RQ_THUMBS)
    return jsonify(jobid)


@app.template_filter("mime2type")
def jinja_mime2type(s):
    ''' Jinja2 filter to get the media type from a MIME type. '''
    return filesystem.mime2type(s)

fmsuite/filesystem.py

Here is our filesystem.py, which contain all the functionality related to files and directories:

Εδώ είναι το αρχείο μας filesystem.py, που εμπεριέχει όλη τη λειτουργικότητα σχετικά με τα αρχεία και τους φακέλους:

import os
import hashlib
import mimetypes
from flask import g, url_for
from config import SHARES, SHOW_HIDDEN, REL_THEME_PATH, REL_THUMB_PATH, THUMB_SIZE, THUMB_TYPES
from fmsuite import mediatypes
from fmsuite import thumbnail


def get_path_uid(path):
    ''' Get a unique (but computable/deterministic) ID for a path. '''
    return hashlib.sha3_512(path.encode(), usedforsecurity=False).hexdigest()


def get_path_contents(path, thumbs_rq):
    ''' Returns the contents of a path (dirs, files). '''
    # If path == "/" return the SHARES instead.
    if path == "/":
        return [{"name": share, 
                 "path": share,
                 "stat": os.stat(share), 
                 "uid": get_path_uid(share), 
                 "thumb": url_for("static", filename=REL_THEME_PATH + "folder_" + 
                                  mediatypes.folder2type(share) + ".svg")}
                for share in SHARES], []
    # We use os.scandir() to get the contents of a path
    dirs = []
    files = []
    contents = os.scandir(path)
    for c in contents:
        # Hide hidden files?
        if c.name.startswith('.') and not SHOW_HIDDEN:
            continue
        # File
        if c.is_file():
            f = {"name": c.name,
                 "path": c.path,
                 "mime": mimetypes.guess_type(c.path)[0],
                 "stat": c.stat(),
                 "uid": get_path_uid(c.path)}
            # Setup thumbnail
            mtype = mediatypes.mime2type(f["mime"])
            if thumbs_rq and (mtype in THUMB_TYPES):
                # Make a thumbnail, if possible (we enqueue a RQ job)
                job = thumbs_rq.enqueue(thumbnail.make_thumbnail, 
                                        c.path, f["uid"], mtype, THUMB_SIZE, False,
                                        failure_ttl=300)
                # Store the job ID -> Unique file ID association in the db
                g.DB.set(job.id, f["uid"])
                # Stote the URL path for our thumbnail
                f["thumb"] = url_for("static", filename=REL_THUMB_PATH + f["uid"][:3] +
                                                        "/" +  f["uid"] + ".jpg")
            else:
                # Otherwise, use the generic icon for the medium type
                f["thumb"] = url_for("static", filename=REL_THEME_PATH + "file_" +
                                                        mtype + ".svg")
            # Add file to the list of files
            files.append(f)
            # Associate the unique ID of the file with its path in the db.
            g.DB.set(f["uid"], f["path"])
        # Directory
        elif c.is_dir():
            d = {"name": c.name,
                 "path": c.path,
                 "stat": c.stat(),
                 "uid": get_path_uid(c.path),
                 "thumb": url_for("static", filename=REL_THEME_PATH +
                                  "folder_" + mediatypes.folder2type(c.path) + ".svg")}
            # Add directory to the list of directories
            dirs.append(d)
            # Associate the unique ID of the directory with its path in the db.
            g.DB.set(d["uid"], d["path"])
    # Sort the lists of dirs and files
    dirs.sort(key=lambda e: (e["name"].casefold(), e["name"]))
    files.sort(key=lambda e: (e["name"].casefold(), e["name"]))
    return dirs, files


def get_paths(path, limits=["/"]):
    ''' Get a list of tupples [(last folder, full path), ...] of all the paths included in
        a path. e.g.: If path is "/home/bob/" -> [("home", "/home"), ("bob", "/home/bob")]. 
    '''
    suppaths = []
    p = path.rstrip('/')
    while p:
        p, folder = p.rsplit('/', 1)
        fullpath = p + "/" + folder
        d = {"name": folder, "path": fullpath, "uid": get_path_uid(fullpath)}
        suppaths.append(d)
        g.DB.set(d["uid"], d["path"])
        if fullpath in limits:
            break
    return suppaths[::-1]


def prepare_uids_recursively(dir_path):
    '''  Prepare unique IDs for all the shared dirs and files.
         All access is done through those unique IDs.
         (thus, we avoid path traversal and many other types of attacks). 
    '''
    # Set a unique ID for this path (it's a directory) in the db
    g.DB.set(get_path_uid(dir_path), dir_path)
    # We use os.scandir() to get the contents of a directory
    contents = os.scandir(dir_path)
    for c in contents:
        if not SHOW_HIDDEN and c.name.startswith('.'):
            # Hide hidden files
            continue
        if c.is_dir(): 
            # Directory
            prepare_uids_recursively(c.path)
        else:          
            # File: Get a unique ID for this
            uid = get_path_uid(c.path)
            # Set the unique ID for this file path in the db.
            g.DB.set(uid, c.path)

fmsuite/thumbnail.py

Here is our thumbnail.py, where the creation of thumbnails take place. We use RQ (Redis Queue) to queue the thumbnailing jobs in order to process them in parallel in the background, mobilizing as many workers as we want.

Εδώ είναι το thumbnail.py, όπου λαμβάνει χώρα η δημιουργία των μικρογραφιών. Χρησιμοποιούμε το RQ (Redis Queue) για να βάλουμε σε σειρά αναμονής τις εργασίες μικρογράφησης ώστε να τις επεξεργαζόμαστε με παραλληλία στο περιθώριο, επιστρατεύοντας όσους εργάτες θέλουμε.

import os
import shutil
import zipfile
import subprocess
from lxml import etree
from flask import g, url_for
from PIL import Image, UnidentifiedImageError
from config import ABS_THEME_PATH, ABS_THUMB_PATH, THUMB_CREATION_TIMEOUT, THUMB_TYPES
from fmsuite import filesystem
from fmsuite import mediatypes


# XML namespaces for the EPUB format (these are used to avoid name conflicts in elements).
namespaces = {
        'u':"urn:oasis:names:tc:opendocument:xmlns:container",
        'xsi':"http://www.w3.org/2001/XMLSchema-instance",
        'opf':"http://www.idpf.org/2007/opf",
        'dcterms':"http://purl.org/dc/terms/",
        'calibre':"http://calibre.kovidgoyal.net/2009/metadata",
        'dc':"http://purl.org/dc/elements/1.1/",
        }


def create_image_thumbnail(img, target_size, out_thumb_path):
    ''' Make a thumbnail with target size by cropping out a maximal region from an image. '''
    if (target_size[0] / target_size[1]) > (img.size[0] / img.size[1]):
        # If image is too tall, crop some off from top and bottom
        scale_factor = target_size[0] / img.size[0] 
        height = int(target_size[1] / scale_factor)
        top = int((img.size[1] - height) / 2)
        img = img.crop((0, top, img.size[0], top + height))
    elif (target_size[0] / target_size[1]) < (img.size[0] / img.size[1]):
        # If image is too wide, crop some off from left and right
        scale_factor = target_size[1] / img.size[1] 
        width = int(target_size[0] / scale_factor)
        left = int((img.size[0] - width) / 2)
        img = img.crop((left, 0,  left + width, img.size[1]))
    # Resize cropped image
    thumb = img.resize(target_size, Image.ANTIALIAS)
    # Convert mode to RGB
    if thumb.mode in ("RGBA", "LA", "P"):
        new = Image.new("RGB", thumb.size, (255,255,255))
        new.paste(thumb)
        thumb = new
    # Save thumbnail
    thumb.save(out_thumb_path, "JPEG")


def make_thumbnail(in_file_path, uid, mtype, thumb_size, overwrite=False):
    ''' Make a thumbnail for a file. '''
    thumb_dir = ABS_THUMB_PATH + uid[:3] + "/"
    out_thumb_path = thumb_dir + uid + ".jpg"
    if not overwrite and os.path.exists(out_thumb_path):
        return
    # Create thumbnail directory if it doesn't exist
    if not os.path.isdir(thumb_dir):
         os.makedirs(thumb_dir)
    if mtype == "image":
        # Open image
        with Image.open(in_file_path) as im:
            # Create image thumbnail
            create_image_thumbnail(im, thumb_size, out_thumb_path)
    elif mtype == "video":
        # We use ffmpeg to create a thumbnail for the video files
        try:
            subprocess.run(["ffmpeg",
                            "-loglevel", "fatal",
                            "-i",  in_file_path,
                            "-ss", "120",
                            "-vframes", "1",
                            "-s", str(thumb_size[0]) + "x" + str(thumb_size[1]),
                            "-filter:v", "scale='min(" + str(thumb_size[0]) + "\,iw):-1'",
                            "-y", out_thumb_path],
                           timeout=THUMB_CREATION_TIMEOUT)
        except subprocess.TimeoutExpired:
            pass
    elif mtype == "pdf":
        # We use imagemagick to create a thumbnail for the pdf files
        try:
            subprocess.run(["convert", 
                            "-flatten",
                            "-density", "300",
                            "-resize", str(thumb_size[0]) + "x" + str(thumb_size[1]),
                            in_file_path + "[0]",  out_thumb_path], 
                           timeout=THUMB_CREATION_TIMEOUT)
        except subprocess.TimeoutExpired:
            pass
    elif mtype == "cbz":
        # Decompress first image from cbz archive
        with zipfile.ZipFile(in_file_path) as z:
            for member in sorted(z.namelist()):
                if member[-1] != "/":
                    with z.open(member) as zm:
                        with Image.open(zm) as im:
                            # Create image thumbnail
                            create_image_thumbnail(im, thumb_size, out_thumb_path)
                    break
    elif mtype == "epub":
        # Get EPUB cover image
        with zipfile.ZipFile(in_file_path) as z:
            t = etree.fromstring(z.read("META-INF/container.xml"))
            rootfile_path =  t.xpath("/u:container/u:rootfiles/u:rootfile",
                                     namespaces=namespaces)[0].get("full-path")
            t = etree.fromstring(z.read(rootfile_path))
            cover_id = t.xpath("//opf:metadata/opf:meta[@name='cover']",
                               namespaces=namespaces)[0].get("content")
            cover_href = t.xpath("//opf:manifest/opf:item[@id='" + cover_id + "']",
                                  namespaces=namespaces)[0].get("href")
            cover_path = os.path.join(os.path.dirname(rootfile_path), cover_href)
            with z.open(cover_path) as zm:
                with Image.open(zm) as im:
                    # Create image thumbnail
                    create_image_thumbnail(im, thumb_size, out_thumb_path)
    # If thumbnail creation failed, copy the image of a failed icon in its place.
    if not os.path.exists(out_thumb_path):
        shutil.copy(ABS_THEME_PATH + "thumb_failed.jpg", out_thumb_path)


def refresh_thumbnail(uid, thumb_size, thumbs_rq):
    ''' Refresh the thumbnail. '''
    path = g.DB.get(uid).decode()
    mtype = mediatypes.get_path_type(path)
    if mtype not in THUMB_TYPES:
        return -1
    else:
        # Enqueue a RQ job
        job = thumbs_rq.enqueue(make_thumbnail, path, uid, mtype,
                                thumb_size, True, failure_ttl=300)
        # Store the job ID -> Unique file ID association in the db
        g.DB.set(job.id, uid)
        return job.id

worker.py

Here is our worker.py:

Εδώ είναι το worker.py:

#!/usr/bin/env python
import rq
import logging

# Preload libraries for the worker
import os
import shutil
import zipfile
import subprocess
from lxml import etree
from flask import g, url_for
from PIL import Image, UnidentifiedImageError
from config import ABS_THEME_PATH, ABS_THUMB_PATH, THUMB_CREATION_TIMEOUT, THUMB_TYPES
from fmsuite import filesystem
from fmsuite import mediatypes

# Logging setup
logger = logging.getLogger("rq.worker")
file_handler = logging.FileHandler("workers.log")
formatter = logging.Formatter("%(asctime)s | %(levelname)s | PID %(process)d | %(message)s")
file_handler.setFormatter(formatter)
logger.addHandler(file_handler)

# Provide queue names to listen to as arguments to this script,
# similar to rq worker
with rq.Connection():
    w = rq.Worker(["thumbs"])
    w.work()

Frontend codeΚώδικας εμπρόσθιου μέρους

Τhird-party componentsΣτοιχεία τρίτων

We make use of several (free) third-party components:

Κάνουμε χρήση αρκετών (δωρεάν) στοιχείων από τρίτους:

fmsuite/templates/layout.html

Here is our jinja template for the basic layout. Notice the "nonce" attributes in the style and script elements. The nonce attribute is a way to indicate (to the browsers) that the inline contents of a particular style or script element were put in the document by the serve and weren’t injected by some malicious third party.

Εδώ είναι το jinja πρότυπο της βασική μας διάταξης (layout). Παρατηρείστε τις ιδιότητες "nonce" στα στοιχεία ύφους (style) και σεναρίου (script). Η ιδιότητα nonce είναι ένας τρόπος για να υποδείξουμε (στους περιηγητές) ότι τα περιέχομενα ενός συγκεκριμένου στοιχείου ύφους ή σεναρίου μπήκαν στο έγγραφο από τον διακομιστή και δεν είναι εμβόλιμα από κάποιον κακόβουλο τρίτο.

<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="utf-8">
  <meta name="viewport" content="width=device-width, initial-scale=1">
  <meta http-equiv="x-ua-compatible" content="ie=edge">

  <title>{% block title %}FM Suite{% endblock title %}</title>

  <link rel="shortcut icon" href="{{ url_for('static', filename='favicon.ico') }}">
  <!-- Font Awesome Icons -->
  <link rel="stylesheet" href="{{url_for('static',filename='css/font-awesome.min.css')}}"
        nonce="{{ csp_nonce() }}">
  <!-- simplelightbox -->
  <link rel="stylesheet" href="{{url_for('static',filename='css/simplelightbox.min.css')}}" 
        nonce="{{ csp_nonce() }}">
  <!-- Datatables -->
  <link rel="stylesheet" href="{{url_for('static',filename='css/datatables.min.css')}}" 
        nonce="{{ csp_nonce() }}">
  <!-- AdminLTE style -->
  <link rel="stylesheet" href="{{url_for('static',filename='css/adminlte.min.css')}}"
        nonce="{{ csp_nonce() }}">
  <!-- Our own CSS -->
  <link rel="stylesheet" href="{{url_for('static',filename='css/fmsuite.css')}}"
        nonce="{{ csp_nonce() }}">
</head>

<body class="hold-transition layout-navbar-fixed sidebar-collapse">

<div class="wrapper">
  <!-- Navbar -->
  <nav class="main-header navbar navbar-expand navbar-white navbar-light border-bottom">

    <!-- Left navbar links -->
    <ul class="nav nav-pills" role="tablist">
      <li><a class="nav-link {% if g.VIEW == "#gallery-view" %}active{% endif %}"
             aria-selected="true" data-toggle="pill" href="#gallery-view" 
             role="tab"><i class="fa fa-th"></i></a></li>
      <li><a class="nav-link {% if g.VIEW == "#tables-view" %}active{% endif %}" 
             aria-selected="false" data-toggle="pill" href="#tables-view" 
             role="tab"><i class="fa fa-list"></i></a></li>
    </ul>

    <!-- SEARCH FORM -->
    <form class="form-inline ml-auto">
      <div class="input-group input-group-sm">
        <input class="form-control form-control-navbar" type="search" 
               placeholder="Filter" aria-label="Search" id="filter-search">      
      </div>
    </form>
    <ul class="navbar-nav ml-auto">
      <li><a id="logout" class="nav-link" href="#"><i class="fa fa-sign-out"></i></a></li>
    </ul>

  </nav> <!-- /.navbar -->

  <!-- Content Wrapper. Contains page content -->
  <div class="content-wrapper">

    <!-- Content Header (Page header) -->
    <div class="content-header">
      <div class="container-fluid">
        {% block header %}
        {% endblock header %}
      </div><!-- /.container-fluid -->
    </div>
    <!-- /.content-header -->

    <!-- Main content -->
    <section class="content">
      <div class="container-fluid">
        {% block content %}
        <!--------------------------
          | Your Page Content Here |
          -------------------------->
        {% endblock content %}
      </div><!--/. container-fluid -->
    </section><!-- /.content -->
  </div><!-- /.content-wrapper -->

  <!-- Main Footer -->
  <footer class="main-footer">
    <!-- To the right -->
    <div class="float-right d-sm-none d-md-block">
      2022
    </div>
    <!-- Default to the left -->
    <strong>Alamot Software</strong>
  </footer>

  {% include "messages.html" %}
</div> <!-- ./wrapper -->


<!-- jQuery -->
<script type="text/javascript" src="{{ url_for('static',filename='js/jquery.js') }}"
        nonce="{{ csp_nonce() }}"></script>
<!-- Bootstrap -->
<script type="text/javascript" 
        src="{{ url_for('static',filename='js/bootstrap.bundle.min.js') }}" 
        nonce="{{ csp_nonce() }}"></script>
<!-- simple-lightbox -->
<script type="text/javascript" src="{{ url_for('static',filename='js/simple-lightbox.js') }}" 
        nonce="{{ csp_nonce() }}"></script>
<!-- DataTables -->
<script type="text/javascript" src="{{ url_for('static',filename='js/datatables.min.js') }}"
        nonce="{{ csp_nonce() }}"></script>
<!-- AdminLTE App -->
<script type="text/javascript" src="{{ url_for('static',filename='js/adminlte.min.js') }}" 
        nonce="{{ csp_nonce() }}"></script>
<!-- SlimScroll 1.3.0 -->
<script type="text/javascript" 
        src="{{ url_for('static',filename='js/jquery.slimscroll.min.js') }}" 
        nonce="{{ csp_nonce() }}"></script>


<script type="text/javascript" nonce="{{ csp_nonce() }}">

$('a[data-toggle="pill"]').on("click", function() {
    /* Saves the "#gallery-view" or "#table-view" setting to a cookie named "view" */
    var target = $(this).attr("href");
    document.cookie = 'view='+target+';expires=Fri, 31 Dec 9999 23:59:59 GMT;path=/';
});

$(document).ready(function() {
  $("#flashed-messages-modal").modal("show");
  $.extend(true, $.fn.dataTable.defaults, { mark: true });
});

{% block js %}
{% endblock js %}

</script>


</body>
</html>

fmsuite/templates/folder.html

Here is our jinja template for the presentation of the contents of a folder. We implement two different views in two tab panes: a gallery view and a table view (using Datatables).

Εδώ είναι το jinja πρότυπο μας για την παρουσίαση των περιεχομένων ενός φακέλου. Υλοποιούμε δύο διαφορετικές απεικονίσεις σε δύο παράθυρα καρτέλες (tab panes): μία εκθεσιακή απεικόνιση (gallery view) και μια απεικόνιση πίνακα (table view, χρησιμοποιώντας τα Datatables).

{% extends "layout.html" %}


{% block header %}
<div class="row mb-2">
  <div class="col-sm-12">
    <ol class="breadcrumb">
          <li class="breadcrumb-item">
            <a href="{{ url_for('index') }}"><i class="fa fa-home"></i></a>
          </li>
        {% for e in paths %}
          <li class="breadcrumb-item">
            <a href="{{ url_for('explore_path', uid=e['uid']) }}">{{ e['name'] }}</a>
          </li>
        {% endfor %}
    </ol>
  </div><!-- /.col -->
</div><!-- /.row -->
{% endblock header %}


{% block title %}FM Suite{% endblock %}


{% block content %}
<div class="tab-content">

<!-- tab-pane gallery-view  -->
<div class="tab-pane filter-container row p-0 align-items-end fade {% if g.VIEW == '#gallery-view' %}show active{% endif %}" 
     id="gallery-view" role="tabpanel">

{% for dir in dirs %}
  <div class="fm-item col-lg-2 col-md-3 col-sm-4 col-6">
    <div class="small-box bg-white">
      <div class="inner">
        <a href="{{ url_for('explore_path', uid=dir['uid']) }}"><img src="{{ dir['thumb'] }}"
           class="img-fluid mx-auto d-block align-middle" alt="folder"/></a>
      </div>
      <a class="small-box-footer text-truncate px-2" 
         href="{{ url_for('explore_path', uid=dir['uid']) }}"
         title="{{ dir['name'] }}"><small>{{ dir['name'] }}</small></a>
    </div>
  </div>
{% endfor %}

{% for file in files %}
  <div class="fm-item col-lg-2 col-md-3 col-sm-4 col-6"> 
    <div class="small-box bg-white">
      <div class="inner">
        <a class="slb-item" href="{{ url_for('serve_file', uid=file['uid']) }}" 
           title="{{ file['name'] }}" type="{{ file['mime'] }}">
             <img id="{{ file['uid'] }}" src="{{ file['thumb'] }}" 
                  class="img-fluid mx-auto d-block align-middle" alt="{{ file['mime'] }}"/>
        </a>
      </div>
      <a class="small-box-footer text-truncate px-2"
         href="{{ url_for('get_file', uid=file['uid']) }}"
         title="{{ file['name'] }}" download>
           <small><i class="fa fa-download"></i> {{ file['name'] }}</small>
      </a>
    </div>
  </div>
{% endfor %}

</div> <!-- tab-pane gallery-view end -->


<!-- tab-pane tables-view -->
<div class="tab-pane row p-0 fade {% if g.VIEW == '#tables-view' %}show active{% endif %}" 
     id="tables-view" role="tabpanel">

<div class="card col-12">
 <div class="card-body">

  <table id="folder-contents" class="table table-striped table-hover">
    <thead>
      <tr>
      <th class="text-center">Name</th>
      <th class="text-center">Mime type</th>
      <th data-order="0" class="text-center">Size</th>
      </tr>
    </thead>

    <tbody>

    {% for dir in dirs %}
      <tr>
        <td class="text-center">
           <a href="{{ url_for('explore_path', uid=dir['uid']) }}">{{ dir["name"] }}</a>
        </td>
        <td class="text-center">directory</td>
        <td class="text-center"></td>
      </tr>
    {% endfor %}

    {% for file in files %}
      <tr>
        <td class="text-center">{{ file["name"] }}</td>
        <td class="text-center">{{ file["mime"] }}</td>
        <td data-order="{{ file['stat'].st_size }}"
            class="text-center">{{ file["stat"].st_size|filesizeformat }}</td>
      </tr>
    {% endfor %}

    </tbody>
  </table>

 </div>
</div>
</div> <!-- tab-pane tables-view end -->

</div> <!-- tab-content -->
{% endblock content %}


{% block js %}
$(function () {

// On image error show a loading circle gif
$("img").on("error", function () {
  $(this).attr("src", "{{ url_for('static', filename=config.REL_THEME_PATH +
                                                    'thumb_loading.gif') }}");
});

// Datatables setup
var TABLES = ["folder-contents"];
{% include "tables.js" %}

// Filter folder contents
$("#filter-search").on("input", function() {
  var value = $(this).val().toLowerCase();
  $(".small-box-footer").filter(function() {
    var test = $(this).text().toLowerCase().indexOf(value) > -1;
    $(this).parents(".fm-item").toggle(test);
  });
  datatables["folder-contents"].search(value).draw();
});

// Initiate lightbox
var lightbox = $(".slb-item").simpleLightbox({captionSelector: "self",
                                             captionType:"title",
                                             captionPosition: "outside",
                                             alertError: false,
                                             docClose: false });

function check_thumb_jobs() {
  /* It loads the new thumbnails when thumbnail jobs are finished. */
  $.getJSON("{{ url_for('get_finished_thumbs') }}", function(ids) {
    //console.log(ids);
    if (!(ids === "empty")) {  setTimeout(check_thumb_jobs, 1000); }
    for (let i in ids) {
      $("#" + ids[i])
       .attr("src",
             "{{ url_for('static', filename=config.REL_THUMB_PATH) }}" + 
             ids[i].slice(0, 3) + "/" + ids[i] + ".jpg");
    }
  });
}

// Run check_thumb_jobs()
check_thumb_jobs();

// Shift + left mouse click on a file -> Refresh its thumbnail.
$(".slb-item > img").on("click", function(e) {
  if (e.shiftKey) {
     e.preventDefault();
     e.stopPropagation();
     $.post("/refresh/" + this.id + "/");
  }
});

});
{% endblock js %}

Running the appΕκτέλεση της εφαρμογής μας

Before starting the app, make sure you have a running Redis service, e.g. (depending on your system):

Πριν εκκινήσετε την εφαρμογή, βεβαιωθείτε ότι τρέχει η υπηρεσία Redis: π.χ. (εξαρτάται από το σύστημά σας):

$ sudo rc-service redis start
$ sudo systemctl start redis.service

./start.sh

# Start 10 workers for thumbnailing
for i in {1..10}; do 
  ./worker.py &
done
# Trap to kill all the child processes (i.e. the workers) when the script terminates.
trap "trap - SIGTERM && kill -- -$$" SIGINT SIGTERM EXIT
# Start our web app
FLASK_APP=fmsuite FLASK_ENV=development flask run

ScreenshotsΣτιγμιότυπα

Gallery view (files)
Gallery view (files)
Εκθεσιακή απεικόνιση (αρχεία)
Gallery view (folders)
Gallery view (folders)
Εκθεσιακή απεικόνιση (φάκελοι)
Table view
Table view
Απεικόνιση πίνακα

In web design, a lightbox is an overlay that appears on top of a webpage, dimming the background in order to draw the attention to the content of the lightbox. For this project, I have modified SimpleLightbox in order to support, besides viewing images, opening e-books and playing audio and video files in a lightbox.

Στον σχεδιασμό ιστοσελίδων, ονομάζουμε lightbox (διαφανοσκόπειο) μια επικάλυψη που εμφανίζεται πάνω από μια ιστοσελίδα, κάνωντας πιο αμυδρό το φόντο, ώστε να τραβήξει την προσοχή στα περιέχομενα του lightbox. Για αυτό το έργο, έχω τροποποιήσει το SimpleLightbox ώστε να υποστηρίζει, εκτός από τη θέαση εικόνων, το άνοιγμα ηλεκτρονικών βιβλίων και το παίξιμο ήχων και βίντεο μέσα σε ένα lightbox.

>Reading an e-book (in a lightbox)
Reading an e-book (in a lightbox)
Διαβάζοντας ένα ηλεκτρονικό βιβλίο (σε lightbox)
Watching a video (in a lightbox)
Watching a video (in a lightbox)
Βλέπωντας ένα βίντεο (σε lightbox)

Full codeΠλήρης κώδικας

You can download the full code from here: FM Suite

Μπορείτε να κατεβάσετε τον πλήρη κώδικα απο εδώ: FM Suite

See also...

Δείτε επίσης...