diff options
-rw-r--r-- | .gitignore | 1 | ||||
-rw-r--r-- | README.md | 13 | ||||
-rw-r--r-- | TODO | 13 | ||||
-rw-r--r-- | assets/admin.css | 36 | ||||
-rw-r--r-- | assets/cargohold.css | 35 | ||||
-rw-r--r-- | assets/cargohold.js | 22 | ||||
-rw-r--r-- | backend/Admin.py | 19 | ||||
-rw-r--r-- | backend/config.py | 6 | ||||
-rw-r--r-- | backend/utils.py | 16 | ||||
-rw-r--r-- | interface/admin_aliases.tmpl | 40 | ||||
-rw-r--r-- | interface/admin_delete.tmpl | 21 | ||||
-rw-r--r-- | interface/admin_dirs.tmpl | 6 | ||||
-rw-r--r-- | interface/admin_files.tmpl | 21 | ||||
-rw-r--r-- | interface/admin_rename.tmpl | 40 | ||||
-rw-r--r-- | interface/admin_upload.tmpl | 36 | ||||
-rw-r--r-- | interface/listing.htm | 6 | ||||
-rw-r--r-- | interface/listing_default.tmpl | 58 |
17 files changed, 357 insertions, 32 deletions
@@ -1,2 +1,3 @@ *.swp __pycache__ +backend/cargohold.db3 @@ -13,6 +13,8 @@ Required dependencies * uwsgi-plugin-python3 * sqlite3 * python3 +* python3-jinja2 +* python3-passlib if using `LocalBasicAuth` Basic infrastructure @@ -59,4 +61,13 @@ Other httpds may provide a similar mechanism, which will need to be called out t ## Authentication -TBD +This projects provides multiple methods to identify and authenticate users, including delegating authentication to external software. +Implementing custom authentication methods is possible and patches contributing such are welcome. + +The following authentication methods are currently available: + +* *NoneAuth*: The default. Does not support any logins. Use this if you want to manage cargohold exclusively via the command line or direct database interfaces +* *ExternalAuth*: Delegate authentication to an external entity. Expects the authenticated user (if any) in the REMOTE_USER environment variable +* *LocalBasicAuth*: Internal implementation of HTTP Basic Auth using htpasswd-style user databases +* *RemoteCookieAuth*: Custom authentication scheme validating a domain-wide cookie against an external service +* @@ -2,8 +2,17 @@ Size limiting HTML-only frontend Automatic resizing for gallery mode Alias titles -'Download all' button - +Password protection per alias +'Scramble' file links (do not make the parent directory available via the individual links) Admin Size limit administration + Add aliases + Add specific aliases? + +JS Admin + Rename + Create Alias + Delete + +Storage size calculation diff --git a/assets/admin.css b/assets/admin.css index 4fdc5fb..bfa854c 100644 --- a/assets/admin.css +++ b/assets/admin.css @@ -26,20 +26,44 @@ div.entry { vertical-align: center; } -div.entry a.button { +.info, .permission { padding: 0.5em; background-color: #c8e; - text-decoration: none; - margin-left: 0.5em; - display: block; border-radius: 0.5em; + margin-left: 0.5em; + text-decoration: none; + color: #453; } -a.delete { +.button.delete { background-color: #f79 !important; } -a.directory { +.button.active { + background-color: #569; +} + +.permission { + background-color: #366; + color: #acb; +} + +.permission.disabled { + text-decoration: line-through; + background-color: #636; +} + +a.entry { text-decoration: none; color: #aade88; } + +span.dirname { + font-weight: bold; + font-style: italic; +} + +#topmenu, #action { + display: inline-flex; + margin: 1em; +} diff --git a/assets/cargohold.css b/assets/cargohold.css index 747b916..eddd1c7 100644 --- a/assets/cargohold.css +++ b/assets/cargohold.css @@ -56,7 +56,7 @@ html { } #dirlisting { - padding: 2em; + padding: 0 2em 2em 2em; } #storage { @@ -98,9 +98,14 @@ html { background-color: #728; padding: 0.5em; text-align: left; +} + +#header h1 { font-size: 120%; font-weight: bold; font-family: sans; + display: inline-block; + margin: 0; } #container { @@ -108,6 +113,24 @@ html { width: 85%; } +a.button { + padding: 0.5em; + background-color: #c8e; + text-decoration: none; + margin-left: 0.5em; + border-radius: 0.5em; +} + +.buttonrow { + display: block; + text-align: right; + padding: 0.5em; +} + +.buttonrow > a { + display: inline-block; +} + .tab-wrap > input { display: none; } @@ -167,3 +190,13 @@ html { border-radius: 1em; background-color: #8885; } + +.footer, .footer a { + text-align: right; + text-decoration: none; + color: #777; +} + +.footer { + padding: 1em; +} diff --git a/assets/cargohold.js b/assets/cargohold.js index 18dbeae..ea882fa 100644 --- a/assets/cargohold.js +++ b/assets/cargohold.js @@ -7,7 +7,6 @@ var running = false; var queue = []; - function queue_work(){ let i = 0; for(; i < queue.length; i++){ @@ -118,6 +117,7 @@ function interface_update(access){ function listing_add(file, style = "default"){ let link = node("a", "listing-entry", file.name); link.href = "file/" + encodeURIComponent(file.name); + link.setAttribute("data-filename", file.name) if(style == "gallery"){ let image = node("img", "preview"); @@ -139,14 +139,34 @@ function listing_update(){ for(let i = 0; i < data.files.length; i++){ listing_add(data.files[i], data.display); } + + element("download-all").innerText = "Download all (" + data.files.length + " Files, " + (data.total / 1024 /1024) + "MB)"; }; req.open("GET", "listing"); req.send(); } +function download_all(ev){ + let links = document.getElementsByClassName("listing-entry"); + var current = 0; + var interval = setInterval(function(){ + if(current < links.length){ + link = links[current]; + link.download = link.getAttribute("data-filename"); + link.click(); + link.removeAttribute("download"); + current++; + } + else{ + clearInterval(interval); + } + }, 1000); +} + function init(){ element("file-submit").style.display = "none"; element("files").onchange = upload_start; + element("download-all").onclick = download_all; listing_update(); } diff --git a/backend/Admin.py b/backend/Admin.py index a844aff..2070293 100644 --- a/backend/Admin.py +++ b/backend/Admin.py @@ -8,7 +8,10 @@ template_factory = jinja2.Environment(loader=jinja2.FileSystemLoader('../interfa admin_dirs = template_factory.get_template("admin_dirs.tmpl") admin_files = template_factory.get_template("admin_files.tmpl") +admin_upload = template_factory.get_template("admin_upload.tmpl") admin_delete = template_factory.get_template("admin_delete.tmpl") +admin_rename = template_factory.get_template("admin_rename.tmpl") +admin_aliases = template_factory.get_template("admin_aliases.tmpl") def route(path, env, post): auth = config.Auth.get(env) @@ -39,11 +42,19 @@ def route(path, env, post): files = utils.dirlisting(directory, True, False) return [admin_files.render({"user": auth["user"], "listing": files, "directory": utils.sanitize_filename(path[1])}), [("Content-Type", "text/html")], "200 OK"] + # Upload + if len(path) == 3 and path[2] == "upload": + return [admin_upload.render({"user": auth["user"], "directory": utils.sanitize_filename(path[1])}), [("Content-Type", "text/html")], "200 OK"] + # Alias management / Limits config - # TODO + if len(path) == 3 and path[2] == "aliases": + aliases = utils.aliases(auth["user"], utils.sanitize_filename(path[1])) + return [admin_aliases.render({"user": auth["user"], "directory": utils.sanitize_filename(path[1]), "aliases": aliases, "baseurl": config.baseurl}), [("Content-Type", "text/html")], "200 OK"] # Renaming - # TODO + if len(path) == 3 and path[2] == "rename": + # TODO + return [admin_rename.render({"user": auth["user"], "directory": utils.sanitize_filename(path[1])}), [("Content-Type", "text/html")], "200 OK"] # Deletion if len(path) == 3 and path[2] == "delete": @@ -52,9 +63,7 @@ def route(path, env, post): return utils.redirect("/admin") files = utils.dirlisting(directory, True, False) if len(files) == 0 or env.get("QUERY_STRING", "") == "confirm": - # TODO remove contents - # TODO remove aliases - os.rmdir(directory) + utils.cleanup(auth["user"], utils.sanitize_filename(path[1])) return utils.redirect("/admin") return [admin_delete.render({"user": auth["user"], "listing": files, "directory": utils.sanitize_filename(path[1])}), [("Content-Type", "text/html")], "200 OK"] diff --git a/backend/config.py b/backend/config.py index 2de63bc..a4a1209 100644 --- a/backend/config.py +++ b/backend/config.py @@ -8,5 +8,11 @@ global_limit = 0 user_subdirs = True # Path to the backing database database = "cargohold.db3" + +# Settings for the web admin panel + +# Base URL for rendering alias links +baseurl = "https://files.stumpf.es/" + # Select the authentication provider for the web admin interface import NoneAuth as Auth diff --git a/backend/utils.py b/backend/utils.py index bab7ebe..1e23e63 100644 --- a/backend/utils.py +++ b/backend/utils.py @@ -57,3 +57,19 @@ def dirlisting(path, files, dirs): listing.append({"name": entry, "size": size}) return listing + +def cleanup(user, subdir): + directory = userdir(user) + subdir + "/" + print("Cleaning up %s" % (directory, )) + # TODO + # Remove contents + # Remove aliases + os.rmdir(directory) + return + +def aliases(user, subdir): + aliases = [] + data = db.cursor().execute("SELECT alias, access, storage, display FROM aliases WHERE user = :user AND real = :dir", {"user": user, "dir": subdir}).fetchall() + for alias in data: + aliases.append({"alias": alias[0], "access": alias[1], "storage": alias[2], "display": alias[3]}) + return aliases diff --git a/interface/admin_aliases.tmpl b/interface/admin_aliases.tmpl new file mode 100644 index 0000000..97315f7 --- /dev/null +++ b/interface/admin_aliases.tmpl @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="robots" content="noindex,nofollow" /> + <title>cargohold</title> + <link rel="stylesheet" href="/assets/cargohold.css" /> + <link rel="stylesheet" href="/assets/admin.css" /> + <link rel="icon" href="/assets/cargohold.ico" /> + </head> + <body> + <div id="header"> + <h1>cargohold - <span class="dirname">{{ directory }}</span></h1> + <a class="button" href="/admin">Back to overview</a> + </div> + + <div id="container"> + <div id="topmenu"> + <a class="button" href="/admin/{{ directory }}">View</a> + <a class="button" href="/admin/{{ directory }}/upload">Upload</a> + <a class="button" href="/admin/{{ directory }}/rename">Rename</a> + <a class="button active" href="/admin/{{ directory }}/aliases">Links</a> + <a class="button delete" href="/admin/{{ directory }}/delete">Delete</a> + </div> + + <div id="listing"> + {% for alias in aliases %} + <div class="entry"> + <a class="entry" href="{{ baseurl }}{{ alias.alias }}">{{ baseurl }}{{ alias.alias }}</a> + <a class="permission {% if 'r' not in alias.access %}disabled{% endif %}" href="#">Download</a> + <a class="permission {% if 'c' not in alias.access %}disabled{% endif %}" href="#">Upload</a> + <a class="permission {% if 'd' not in alias.access %}disabled{% endif %}" href="#">Delete</a> + <a class="permission" href="#">Previews</a> + </div> + {% endfor %} + </div> + </div> + </body> +</html> diff --git a/interface/admin_delete.tmpl b/interface/admin_delete.tmpl index bf6d8a2..102d908 100644 --- a/interface/admin_delete.tmpl +++ b/interface/admin_delete.tmpl @@ -11,24 +11,35 @@ </head> <body> <div id="header"> - cargohold - Administration + <h1>cargohold - <span class="dirname">{{ directory }}</span></h1> + <a class="button" href="/admin">Back to overview</a> </div> <div id="container"> + <div id="topmenu"> + <a class="button" href="/admin/{{ directory }}">View</a> + <a class="button" href="/admin/{{ directory }}/upload">Upload</a> + <a class="button" href="/admin/{{ directory }}/rename">Rename</a> + <a class="button" href="/admin/{{ directory }}/aliases">Links</a> + <a class="button active" href="/admin/{{ directory }}/delete">Delete</a> + </div> + <div id="message"> - This will delete the directory <span class="highlight">{{ directory }}</span>. + This will delete the directory <span class="highlight dirname">{{ directory }}</span>. <br /> This directory still contains {{ listing | length }} files. </div> + <div id="dirlisting"> {% for file in listing %} <div class="entry"> - <a class="file" href="#">{{ file.name }}</a> + <a class="entry" href="/admin/{{ directory }}/file/{{ file.name }}">{{ file.name }}</a> </div> {% endfor %} </div> + <div id="action"> - <a class="button" href="/admin">Cancel</a> - <a class="button delete" href="?confirm">Confirm & delete</a> + <a class="button" href="/admin/{{ directory }}">Cancel</a> + <a class="button delete" href="?confirm">Confirm & delete all</a> </div> </div> </body> diff --git a/interface/admin_dirs.tmpl b/interface/admin_dirs.tmpl index cebaa62..d7c5317 100644 --- a/interface/admin_dirs.tmpl +++ b/interface/admin_dirs.tmpl @@ -11,7 +11,7 @@ </head> <body> <div id="header"> - cargohold - Administration + <h1>cargohold - All directories</h1> </div> <div id="container"> {% if error %} @@ -28,9 +28,9 @@ <div id="dirlisting"> {% for dir in listing %} <div class="entry"> - <a class="directory" href="/admin/{{dir.name}}">{{dir.name}}</a> + <a class="entry" href="/admin/{{dir.name}}">{{dir.name}}</a> <a class="button" href="/admin/{{dir.name}}/rename">Rename</a> - <a class="button" href="/admin/{{dir.name}}/aliases">Aliases</a> + <a class="button" href="/admin/{{dir.name}}/aliases">{{dir.nlinks}} Links</a> <a class="delete button" href="/admin/{{dir.name}}/delete">Delete</a> </div> {% endfor %} diff --git a/interface/admin_files.tmpl b/interface/admin_files.tmpl index 76b63d5..76e97aa 100644 --- a/interface/admin_files.tmpl +++ b/interface/admin_files.tmpl @@ -11,18 +11,25 @@ </head> <body> <div id="header"> - cargohold - Administration + <h1>cargohold - <span class="dirname">{{ directory }}</span></h1> + <a class="button" href="/admin">Back to overview</a> </div> + <div id="container"> - {% if message %} - <div id="message"> - {{ message }} - </div> - {% endif %} + <div id="topmenu"> + <a class="button active" href="/admin/{{ directory }}">View</a> + <a class="button" href="/admin/{{ directory }}/upload">Upload</a> + <a class="button" href="/admin/{{ directory }}/rename">Rename</a> + <a class="button" href="/admin/{{ directory }}/aliases">Links</a> + <a class="button delete" href="/admin/{{ directory }}/delete">Delete</a> + </div> + <div id="dirlisting"> {% for file in listing %} <div class="entry"> - <a class="file" href="#">{{ file.name }}</a> + <a class="entry" href="/admin/{{ directory }}/file/{{ file.name | urlencode }}">{{ file.name }}</a> + <div class="info">{{ (file.size / 1024 / 1024) | round(2, "ceil") }} MB</div> + <a class="button delete" href="/admin/{{ directory }}/delete/{{ file.name | urlencode }}">Delete</a> </div> {% endfor %} </div> diff --git a/interface/admin_rename.tmpl b/interface/admin_rename.tmpl new file mode 100644 index 0000000..a760cb5 --- /dev/null +++ b/interface/admin_rename.tmpl @@ -0,0 +1,40 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="robots" content="noindex,nofollow" /> + <title>cargohold</title> + <link rel="stylesheet" href="/assets/cargohold.css" /> + <link rel="stylesheet" href="/assets/admin.css" /> + <link rel="icon" href="/assets/cargohold.ico" /> + </head> + <body> + <div id="header"> + <h1>cargohold - <span class="dirname">{{ directory }}</span></h1> + <a class="button" href="/admin">Back to overview</a> + </div> + + <div id="container"> + <div id="topmenu"> + <a class="button" href="/admin/{{ directory }}">View</a> + <a class="button" href="/admin/{{ directory }}/upload">Upload</a> + <a class="button active" href="/admin/{{ directory }}/rename">Rename</a> + <a class="button" href="/admin/{{ directory }}/aliases">Links</a> + <a class="button delete" href="/admin/{{ directory }}/delete">Delete</a> + </div> + + <div> + This will rename the directory <span class="highlight dirname">{{ directory }}</span>. + <br /> + All aliases will be kept intact. + </div> + + <input type="text" placeholder="New name" /> + + <div id="action"> + <a class="button" href="?confirm">Rename</a> + </div> + </div> + </body> +</html> diff --git a/interface/admin_upload.tmpl b/interface/admin_upload.tmpl new file mode 100644 index 0000000..be5b298 --- /dev/null +++ b/interface/admin_upload.tmpl @@ -0,0 +1,36 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="robots" content="noindex,nofollow" /> + <title>cargohold</title> + <link rel="stylesheet" href="/assets/cargohold.css" /> + <link rel="stylesheet" href="/assets/admin.css" /> + <link rel="icon" href="/assets/cargohold.ico" /> + </head> + <body> + <div id="header"> + <h1>cargohold - <span class="dirname">{{ directory }}</span></h1> + <a class="button" href="/admin">Back to overview</a> + </div> + + <div id="container"> + <div id="topmenu"> + <a class="button" href="/admin/{{ directory }}">View</a> + <a class="button active" href="/admin/{{ directory }}/upload">Upload</a> + <a class="button" href="/admin/{{ directory }}/rename">Rename</a> + <a class="button" href="/admin/{{ directory }}/aliases">Links</a> + <a class="button delete" href="/admin/{{ directory }}/delete">Delete</a> + </div> + + <div> + TBD + </div> + + <div id="action"> + <a class="button" href="#">Upload</a> + </div> + </div> + </body> +</html> diff --git a/interface/listing.htm b/interface/listing.htm index 5866825..d7d08cd 100644 --- a/interface/listing.htm +++ b/interface/listing.htm @@ -11,7 +11,7 @@ </head> <body> <div id="header"> - cargohold + <h1>cargohold</h1> </div> <div id="container"> <div class="tab-wrap"> @@ -30,6 +30,9 @@ current link. </div> </noscript> + <div class="buttonrow"> + <a class="button" href="#" id="download-all">Download all</a> + </div> <div id="dirlisting"> </div> </div> @@ -55,5 +58,6 @@ </div> </div> </div> + <div class="footer"><a href="https://git.services.cbcdn.com/cargohold/">cargohold</a> is a free and open source project</div> </body> </html> diff --git a/interface/listing_default.tmpl b/interface/listing_default.tmpl new file mode 100644 index 0000000..4a6d8bc --- /dev/null +++ b/interface/listing_default.tmpl @@ -0,0 +1,58 @@ +<!DOCTYPE html> +<html> + <head> + <meta charset="utf-8" /> + <meta name="viewport" content="width=device-width, initial-scale=1" /> + <meta name="robots" content="noindex,nofollow" /> + <title>cargohold</title> + <link rel="stylesheet" href="/assets/cargohold.css" /> + <link rel="icon" href="/assets/cargohold.ico" /> + <script src="/assets/cargohold.js"></script> + </head> + <body> + <div id="header"> + <h1>cargohold</h1> + </div> + <div id="container"> + <div class="tab-wrap"> + <input type="radio" id="tab-view" name="tabs" class="tab" checked> + <label for="tab-view" id="tab-view-label">View</label> + + <input type="radio" id="tab-upload" name="tabs" class="tab"> + <label for="tab-upload" id="tab-upload-label">Upload</label> + + <div class="tab-content"> + <div class="buttonrow"> + <a class="button" href="#" id="download-all">Download all</a> + </div> + <div id="dirlisting"> + {% for file in listing %} + <a class="listing-entry" href="file/{{ file.name | urlencode }}" data-filename="{{ file.name }}">{{ file.name }}</a> + {% endfor %} + </div> + </div> + <div class="tab-content"> + <div id="upload"> + <form action="upload" method="post" enctype="multipart/form-data"> + <label for="files" id="selector-label"> + △ Select files to upload + <input type="file" id="files" name="files" multiple> + </label> + <input type="submit" value="Start upload" name="submit" id="file-submit"> + + </form> + </div> + <div id="queue"> + </div> + <!-- + <div id="storage"> + Storage space left: 2GB / 2GB <br/> + <progress value="70" max="100">70 %</progress> + </div> + --> + </div> + </div> + </div> + <div class="footer"><a href="https://git.services.cbcdn.com/cargohold/">cargohold</a> is a free and open source project</div> + </body> +</html> |