diff --git a/zerobin.py b/zerobin.py
deleted file mode 100644
index 271fcac..0000000
--- a/zerobin.py
+++ /dev/null
@@ -1,6 +0,0 @@
-#!/usr/bin/env python
-# coding: utf-8
-
-from zerobin.cmd import main
-
-main()
diff --git a/zerobin/__main__.py b/zerobin/__main__.py
new file mode 100644
index 0000000..eadae7c
--- /dev/null
+++ b/zerobin/__main__.py
@@ -0,0 +1,5 @@
+#!/usr/bin/env python
+
+from zerobin.cli import main
+
+main()
diff --git a/zerobin/cmd.py b/zerobin/cli.py
similarity index 74%
rename from zerobin/cmd.py
rename to zerobin/cli.py
index 28f9827..c879752 100644
--- a/zerobin/cmd.py
+++ b/zerobin/cli.py
@@ -7,16 +7,20 @@
import sys
import re
+import os
+
+import zerobin
from zerobin.utils import (
settings,
SettingsValidationError,
- ensure_var_env,
+ ensure_app_context,
hash_password,
)
from zerobin.routes import get_app
from zerobin.paste import Paste
+
from bottle import run
import clize
@@ -26,11 +30,9 @@ def runserver(
*,
host="",
port="",
+ config_dir="",
+ data_dir="",
debug=None,
- user="",
- group="",
- settings_file="",
- compressed_static=None,
version=False,
paste_id_length=None,
server="paste",
@@ -39,23 +41,18 @@ def runserver(
print("0bin V%s" % settings.VERSION)
sys.exit(0)
- settings.HOST = host or settings.HOST
- settings.PORT = port or settings.PORT
- settings.USER = user or settings.USER
- settings.GROUP = group or settings.GROUP
- settings.PASTE_ID_LENGTH = paste_id_length or settings.PASTE_ID_LENGTH
- settings.DEBUG = bool(debug) if debug is not None else settings.DEBUG
-
- ensure_var_env()
-
try:
- _, app = get_app(debug, settings_file, compressed_static, settings=settings)
+ settings, app = get_app(debug=debug, config_dir=config_dir, data_dir=data_dir,)
except SettingsValidationError as err:
print("Configuration error: %s" % err.message, file=sys.stderr)
sys.exit(1)
+ settings.HOST = host or os.environ.get("ZEROBIN_HOST", settings.HOST)
+ settings.PORT = port or os.environ.get("ZEROBIN_PORT", settings.PORT)
+
if settings.DEBUG:
- print(f"Admin URL: http://{settings.HOST}:{settings.PORT}{settings.ADMIN_URL}")
+ print(f"Admin URL: {settings.ADMIN_URL}")
+ print()
run(
app, host=settings.HOST, port=settings.PORT, reloader=True, server=server,
)
@@ -100,7 +97,7 @@ def delete_paste(*pastes, quiet=False):
print("Paste {} doesn't exist".format(paste_uuid))
-def print_admin_url():
+def infos():
""" Print the route to the 0bin admin.
The admin route is generated by zerobin so that bots won't easily
@@ -123,8 +120,16 @@ def print_admin_url():
"""
- ensure_var_env()
- print(settings.ADMIN_URL)
+ ensure_app_context()
+ print(f"Zerobin version: {zerobin.__version__}")
+ print(f"Admin URL (to moderate pastes): {settings.ADMIN_URL}")
+ print(f"Data dir (pastes and counter): {settings.DATA_DIR}")
+ print(
+ f"Config dir (config file, secret key, admin password and custom views): {settings.CONFIG_DIR}"
+ )
+ print(
+ f"Static files dir (to configure apache, nging, etc.): {settings.STATIC_FILES_ROOT}"
+ )
def set_admin_password(password):
@@ -134,17 +139,17 @@ def set_admin_password(password):
"""
- ensure_var_env()
+ ensure_app_context()
settings.ADMIN_PASSWORD_FILE.write_bytes(hash_password(password))
def main():
- subcommands = [runserver, delete_paste, print_admin_url, set_admin_password]
+ subcommands = [runserver, delete_paste, infos, set_admin_password]
subcommand_names = [
clize.util.name_py2cli(name)
for name in clize.util.dict_from_names(subcommands).keys()
]
if len(sys.argv) < 2 or sys.argv[1] not in subcommand_names:
sys.argv.insert(1, subcommand_names[0])
- clize.run(runserver, delete_paste, print_admin_url, set_admin_password)
+ clize.run(runserver, delete_paste, infos, set_admin_password)
diff --git a/zerobin/default_settings.py b/zerobin/default_settings.py
index ce8c248..afefa84 100644
--- a/zerobin/default_settings.py
+++ b/zerobin/default_settings.py
@@ -1,57 +1,19 @@
-from zerobin import ROOT_DIR
-
-# Path to the directory that will contains all variable content, such
-# as pastes, the secret key, etc
-VAR_DIR = ROOT_DIR.parent / "var"
-
-# debug will get you error messages and auto reload
-# don't set this to True in production
+# Get error messages and auto reload.
+# Don't set this to True in production
DEBUG = False
-# Should the application serve static files on it's own ?
-# If yes, set the absolute path to the static files.
-# If no, set it to None
-# In dev this is handy, in prod you probably want the HTTP servers
-# to serve it, but it's OK for small traffic to set it to True in prod too.
-STATIC_FILES_ROOT = ROOT_DIR / "static"
-
-# If True, will link the compressed verion of the js and css files,
-# otherwise, will use the ordinary files
-COMPRESSED_STATIC_FILES = False
-
-# A tuple of absolute paths of directory where to look the template for
-# the first one will be the first to be looked into
-# if you want to override, it needs to be it a directory at the begining of
-# this tuple. By default, custom_views is meant for that purpose.
-TEMPLATE_DIRS = (
- VAR_DIR / "custom_views",
- ROOT_DIR / "views",
-)
-
-# Port and host the embeded python server should be using
-# You can also specify them using the --host and --port script options
-# which have priority on these settings
+# Port and host the embedded python server should be using
HOST = "127.0.0.1"
-PORT = "8000"
-
-# User and group the server should run as. Set to None if it should be the
-# current user. Some OS don't support it and if so, it will be ignored.
-USER = None
-GROUP = None
+PORT = "3255"
# Display a tiny counter for pastes created.
-# Be carreful if your site have to many pastes this can hurt your hard drive performances.
-# Refresh counter interval. Default to every minute after a paste.
DISPLAY_COUNTER = True
-REFRESH_COUNTER = 60 * 1 # Fill this if you want to
-ADMIN_CREDENTIALS = {
- "username": None,
- "password": None,
-}
+# Refresh counter interval.
+REFRESH_COUNTER = 60 # in seconds
-
-# Names/links to insert in the menu bar.
-# Any link with "mailto:" will be escaped to prevent spam
+# Names/links to insert in the footer.
+# Any link with "mailto:" will be escaped to limit spam, but displayed
+# correctly to the user using JS.
MENU = (
("Create paste", "/"), # internal link. First link will be highlited
("Github", "https://github.com/Tygs/0bin"), # external link
@@ -60,11 +22,11 @@ MENU = (
("Zerobin Pastebin", "https://www.0bin.net/"), # Thanks the authors :)
)
-# limit size of pasted text in bytes. Be careful allowing too much size can
-# slow down user's browser
+# Size limit of the paste content in bytes. Be careful, allowing a size too big can
+# slow down the user's browser
MAX_SIZE = 1024 * 500
-# length of base64-like paste-id string in the url, int from 4 to 27 (length of sha1 digest)
+# Length of the paste-id string in the url, int from 4 to 27 (length of sha1 digest)
# total number of unique pastes can be calculated as 2^(6*PASTE_ID_LENGTH)
# for PASTE_ID_LENGTH=8, for example, it's 2^(6*8) = 281 474 976 710 656
PASTE_ID_LENGTH = 8
diff --git a/zerobin/routes.py b/zerobin/routes.py
index 272cc07..df83018 100644
--- a/zerobin/routes.py
+++ b/zerobin/routes.py
@@ -27,15 +27,15 @@ from beaker.middleware import SessionMiddleware
from zerobin import __version__
from zerobin.utils import (
- settings,
SettingsValidationError,
- ensure_var_env,
+ ensure_app_context,
check_password,
+ settings,
)
from zerobin.paste import Paste
-ensure_var_env()
+ensure_app_context()
GLOBAL_CONTEXT = {
@@ -235,33 +235,33 @@ def server_static(filename):
return static_file(filename, root=settings.STATIC_FILES_ROOT)
-def get_app(debug=None, settings_file="", compressed_static=None, settings=settings):
+def get_app(debug=None, config_dir="", data_dir=""):
"""
Return a tuple (settings, app) configured using passed
parameters and/or a setting file.
"""
- settings_file = settings_file or os.environ.get("ZEROBIN_SETTINGS_FILE")
+ data_dir = data_dir or os.environ.get("ZEROBIN_DATA_DIR")
+ config_dir = config_dir or os.environ.get("ZEROBIN_CONFIG_DIR")
- if settings_file:
- settings.update_with_file(os.path.realpath(settings_file))
+ ensure_app_context(config_dir=config_dir, data_dir=data_dir)
+
+ settings.DEBUG = bool(debug or os.environ.get("ZEROBIN_DEBUG", settings.DEBUG))
+
+ settings.DISPLAY_COUNTER = bool(
+ os.environ.get("ZEROBIN_DISPLAY_COUNTER", settings.DISPLAY_COUNTER)
+ )
+ settings.REFRESH_COUNTER = int(
+ os.environ.get("ZEROBIN_REFRESH_COUNTER", settings.REFRESH_COUNTER)
+ )
+ settings.MAX_SIZE = int(os.environ.get("ZEROBIN_MAX_SIZE", settings.MAX_SIZE))
+ settings.PASTE_ID_LENGTH = int(
+ os.environ.get("ZEROBIN_PASTE_ID_LENGTH", settings.PASTE_ID_LENGTH)
+ )
if settings.PASTE_ID_LENGTH < 4:
raise SettingsValidationError("PASTE_ID_LENGTH cannot be lower than 4")
- if compressed_static is not None:
- settings.COMPRESSED_STATIC_FILES = compressed_static
-
- if debug is not None:
- settings.DEBUG = debug
-
- # make sure the templates can be loaded
- for d in reversed(settings.TEMPLATE_DIRS):
- bottle.TEMPLATE_PATH.insert(0, d)
-
- if settings.DEBUG:
- bottle.debug(True)
-
return settings, app
diff --git a/zerobin/static/css/style.css b/zerobin/static/css/style.css
index e09e839..3505102 100644
--- a/zerobin/static/css/style.css
+++ b/zerobin/static/css/style.css
@@ -46,9 +46,6 @@
/* body & other stuff */
-body {
- padding-bottom: 40px;
-}
.blk-space {
height: 20px;
@@ -101,16 +98,24 @@ blockquote {
/* Footer */
+#app {
+ display: flex;
+ min-height: 100vh;
+ flex-direction: column;
+}
+
+#wrap-content {
+ flex: 1;
+}
+
.footer {
text-align: center;
- bottom: 0px;
- position: fixed;
height: 60px;
width: 100%;
+ position: relative;
display: block;
margin: 0px 0px 0px 0px;
padding: 8px 0 0 0;
- left: 0;
background-color: #424141;
}
diff --git a/zerobin/static/js/behavior.js b/zerobin/static/js/behavior.js
index de18797..cf0499e 100644
--- a/zerobin/static/js/behavior.js
+++ b/zerobin/static/js/behavior.js
@@ -18,8 +18,6 @@ setTimeout(function () {
document.getElementById('content').focus()
}, 100)
-
-
const app = new Vue({
el: '#app',
diff --git a/zerobin/utils.py b/zerobin/utils.py
index 623e164..0f431d9 100644
--- a/zerobin/utils.py
+++ b/zerobin/utils.py
@@ -8,6 +8,13 @@ import hashlib
import secrets
from functools import partial
+from pathlib import Path
+
+import bottle
+
+from appdirs import AppDirs
+
+import zerobin
from zerobin import default_settings
@@ -92,7 +99,7 @@ def as_unicode(obj):
return str(obj)
-def ensure_var_env():
+def ensure_app_context(data_dir=None, config_dir=None):
""" Ensure all the variable things we generate are available.
This will make sure we have:
@@ -101,20 +108,39 @@ def ensure_var_env():
- a content dir
- a secret key
- an admin URL
+
+ This function is idempotent if nothing touch the files it created.
"""
- settings.VAR_DIR.mkdir(exist_ok=True, parents=True)
- settings.PASTE_FILES_ROOT = settings.VAR_DIR / "content"
+ app_dirs = AppDirs("0bin", "tygs")
+
+ settings.DATA_DIR = Path(data_dir or app_dirs.user_data_dir).expanduser()
+ settings.DATA_DIR.mkdir(exist_ok=True, parents=True)
+
+ settings.CONFIG_DIR = Path(config_dir or app_dirs.user_config_dir).expanduser()
+ settings.CONFIG_DIR.mkdir(exist_ok=True, parents=True)
+
+ settings.STATIC_FILES_ROOT = zerobin.ROOT_DIR / "static"
+
+ settings.PASTE_FILES_ROOT = settings.DATA_DIR / "pastes"
settings.PASTE_FILES_ROOT.mkdir(exist_ok=True)
- settings.SESSIONS_DIR = settings.VAR_DIR / "sessions"
+
+ settings.SESSIONS_DIR = settings.DATA_DIR / "sessions"
settings.SESSIONS_DIR.mkdir(exist_ok=True)
- secret_key_file = settings.VAR_DIR / "secret_key"
+ bottle.TEMPLATE_PATH.insert(0, zerobin.ROOT_DIR / "views")
+
+ CUSTOM_VIEWS_DIR = settings.CONFIG_DIR / "custom_views"
+ CUSTOM_VIEWS_DIR.mkdir(exist_ok=True)
+
+ bottle.TEMPLATE_PATH.insert(0, CUSTOM_VIEWS_DIR)
+
+ secret_key_file = settings.CONFIG_DIR / "secret_key"
if not secret_key_file.is_file():
secret_key_file.write_text(secrets.token_urlsafe(64))
settings.SECRET_KEY = secret_key_file.read_text()
- admin_password_file = settings.VAR_DIR / "admin_password"
+ admin_password_file = settings.CONFIG_DIR / "admin_password"
if not secret_key_file.is_file():
admin_password_file.write_text(
"No password set. Use the set_admin_passord command. Don't write this file by hand."
@@ -124,6 +150,13 @@ def ensure_var_env():
payload = ("admin" + settings.SECRET_KEY).encode("ascii")
settings.ADMIN_URL = "/admin/" + hashlib.sha256(payload).hexdigest() + "/"
+ settings_file = settings.CONFIG_DIR / "settings.py"
+ if not settings_file.is_file():
+ default_config = (zerobin.ROOT_DIR / "default_settings.py").read_text()
+ settings_file.write_text(default_config)
+
+ settings.update_with_file(settings_file)
+
def hash_password(password):
return hashlib.scrypt(
diff --git a/zerobin/views/base.tpl b/zerobin/views/base.tpl
index 65ef041..777d36a 100644
--- a/zerobin/views/base.tpl
+++ b/zerobin/views/base.tpl
@@ -12,7 +12,7 @@
- %if settings.COMPRESSED_STATIC_FILES:
+ %if not settings.DEBUG:
%else:
@@ -94,10 +94,11 @@
-
- %if settings.COMPRESSED_STATIC_FILES:
+
+ %if not settings.DEBUG:
%else:
+
%end
@@ -107,8 +108,8 @@
- %if settings.COMPRESSED_STATIC_FILES:
-
+ %if not settings.DEBUG:
+
%else:
diff --git a/zerobin/wsgi.py b/zerobin/wsgi.py
new file mode 100644
index 0000000..33fb133
--- /dev/null
+++ b/zerobin/wsgi.py
@@ -0,0 +1,3 @@
+from zerobin.wsgi import setup_app
+
+settings, app = get_app()